1

I am trying to understand the necessary steps for creating reusable bundles. I'm using Symfony 5.2 and PHP 8.0.

I have two sibling directories containing the projects MainProject and FirstModule.

composer.json for project MainProject:

{
    "type": "project",
    "name": "modulartest/main_project",
    "license": "unlicense",
    "minimum-stability": "dev",
    "prefer-stable": true,
    "repositories": [{
        "type": "path",
        "url": "../FirstModule/"
    }],
    "require": {
        "php": ">=8.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "doctrine/annotations": "^1.12",
        "modulartest/first_module": "dev-master",
        "sensio/framework-extra-bundle": "^6.1",
        "symfony/console": "5.2.*",
        "symfony/dotenv": "5.2.*",
        "symfony/flex": "^1.3.1",
        "symfony/framework-bundle": "5.2.*",
        "symfony/maker-bundle": "^1.29",
        "symfony/yaml": "5.2.*"
    },
    "config": {
        "optimize-autoloader": true,
        "preferred-install": {
            "*": "dist"
        },
        "sort-packages": true
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "App\\Tests\\": "tests/"
        }
    },
    "replace": {
        "symfony/polyfill-ctype": "*",
        "symfony/polyfill-iconv": "*",
        "symfony/polyfill-php72": "*"
    },
    "scripts": {
        "auto-scripts": {
            "cache:clear": "symfony-cmd",
            "assets:install %PUBLIC_DIR%": "symfony-cmd"
        },
        "post-install-cmd": [
            "@auto-scripts"
        ],
        "post-update-cmd": [
            "@auto-scripts"
        ]
    },
    "conflict": {
        "symfony/symfony": "*"
    },
    "extra": {
        "symfony": {
            "allow-contrib": false,
            "require": "5.2.*"
        }
    }
}

composer.json for project FirstModule:

{
    "type": "symfony-bundle",
    "name": "modulartest/first_module",
    "license": "unlicense",
    "minimum-stability": "dev",
    "prefer-stable": true,
    "require": {
        "php": ">=8.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "sensio/framework-extra-bundle": "^6.1",
        "symfony/console": "5.2.*",
        "symfony/dotenv": "5.2.*",
        "symfony/flex": "^1.3.1",
        "symfony/framework-bundle": "5.2.*",
        "symfony/maker-bundle": "^1.29",
        "symfony/yaml": "5.2.*"
    },
    "config": {
        "optimize-autoloader": true,
        "preferred-install": {
            "*": "dist"
        },
        "sort-packages": true
    },
    "autoload": {
        "psr-4": {
            "ModularTest\\FirstModuleBundle\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "ModularTest\\FirstModuleBundle\\Tests\\": "tests/"
        }
    },
    "conflict": {
        "symfony/symfony": "*"
    },
    "extra": {
        "symfony": {
            "allow-contrib": false,
            "require": "5.2.*"
        }
    }
}

In projeto FirstModule, I implemented a service, and a controller that uses that serve, all too simple, just for testing.

Service code in FirstModule\src\Controller\FirstController.php:

<?php


namespace ModularTest\FirstModuleBundle\Service;


/**
 * Class FirstService
 * @package ModularTest\FirstModuleBundle\Service
 */
class FirstService
{
    /**
     * @return string
     */
    public function now(): string
    {
        return 'First Service time: '.date('c');
    }
}

Code for controller in FirstModule\src\Controller\FirstController.php:

<?php

namespace ModularTest\FirstModuleBundle\Controller;

use ModularTest\FirstModuleBundle\Service\FirstService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

/**
 * Class FirstController
 * @package ModularTest\FirstModuleBundle\Controller
 */
class FirstController extends AbstractController
{


    /**
     * FirstController constructor.
     * @param FirstService $firstService
     */
    public function __construct(private FirstService $firstService)
    {
    }

    /**
     * @return Response
     */
    #[Route('/first', name: 'modular_test_first_module_first')]
    public function index(): Response
    {
        return $this->json([
            'message' => 'Welcome to your new controller!',
            'path' => 'src/Controller/FirstController.php',
            'date' => $this->firstService->now()
        ]);
    }
}

I tried to follow rigorously the instructions given by Symfony documentation in those three pages:

Thus, I implemented bundle's class this way:

<?php


namespace ModularTest\FirstModuleBundle;


use Symfony\Component\HttpKernel\Bundle\Bundle;

/**
 * Class ModularTestFirstModuleBundle
 * @package ModularTest\FirstModuleBundle
 */
class ModularTestFirstModuleBundle extends Bundle
{
    /**
     * @return string
     */
    public function getPath(): string
    {
        return \dirname(__DIR__);
    }
}

I implemented extension's class this way:

<?php


namespace ModularTest\FirstModuleBundle\DependencyInjection;


use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

/**
 * Class ModularTestFirstModuleExtension
 * @package ModularTest\FirstModuleBundle\DependencyInjection
 */
class ModularTestFirstModuleExtension extends Extension
{

    /**
     * @param array $configs
     * @param ContainerBuilder $container
     * @throws \Exception
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new YamlFileLoader(
            $container,
            new FileLocator(__DIR__.'/../Resources/config')
        );
        $loader->load('services.yaml');
    }
}

And the content inside file FirstModule\src\Resources\config\services.yaml is as follows:

# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.

# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:

services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    ModularTest\FirstModuleBundle\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'
            - '../src/Tests/'

    # controllers are imported separately to make sure services can be injected
    # as action arguments even if you don't extend any base controller class
    ModularTest\FirstModuleBundle\Controller\:
        resource: '../src/Controller/'
        tags: ['controller.service_arguments']

    # add more service definitions when explicit configuration is needed
    # please note that last definitions always *replace* previous ones

Ok, all that done, I ran the server in MainProject with symfony server:start, e there were no immediate errors. Then I tried to browser the route that points to the controller that was defined by the bundle, that is, https://127.0.0.1:8000/first, and I received this:

FileLocatorFileNotFoundException

The file "../src/" does not exist (in: "C:\Users\eu\dev\php\symfony\modulartest\FirstModule\src\DependencyInjection/../Resources/config").

This is the Stack Trace:

Symfony\Component\Config\Exception\FileLocatorFileNotFoundException:
The file "../src/" does not exist (in: "C:\Users\eu\dev\php\symfony\modulartest\FirstModule\src\DependencyInjection/../Resources/config").

  at C:\Users\eu\dev\php\symfony\modulartest\MainProject\vendor\symfony\config\FileLocator.php:71
  at Symfony\Component\Config\FileLocator->locate('../src/', 'C:\\Users\\eu\\dev\\php\\symfony\\modulartest\\FirstModule\\src\\DependencyInjection/../Resources/config', true)
     (C:\Users\eu\dev\php\symfony\modulartest\MainProject\vendor\symfony\config\Loader\FileLoader.php:117)
  at Symfony\Component\Config\Loader\FileLoader->glob('', true, array(object(FileExistenceResource)), false, false, array())
     (C:\Users\eu\dev\php\symfony\modulartest\MainProject\vendor\symfony\dependency-injection\Loader\FileLoader.php:176)
  at Symfony\Component\DependencyInjection\Loader\FileLoader->findClasses('ModularTest\\FirstModuleBundle\\', '../src/', array('../src/DependencyInjection/', '../src/Entity/', '../src/Kernel.php', '../src/Tests/'))
     (C:\Users\eu\dev\php\symfony\modulartest\MainProject\vendor\symfony\dependency-injection\Loader\FileLoader.php:99)
  at Symfony\Component\DependencyInjection\Loader\FileLoader->registerClasses(object(Definition), 'ModularTest\\FirstModuleBundle\\', '../src/', array('../src/DependencyInjection/', '../src/Entity/', '../src/Kernel.php', '../src/Tests/'))
     (C:\Users\eu\dev\php\symfony\modulartest\MainProject\vendor\symfony\dependency-injection\Loader\YamlFileLoader.php:671)
  at Symfony\Component\DependencyInjection\Loader\YamlFileLoader->parseDefinition('ModularTest\\FirstModuleBundle\\', array('resource' => '../src/', 'exclude' => array('../src/DependencyInjection/', '../src/Entity/', '../src/Kernel.php', '../src/Tests/')), 'C:\\Users\\eu\\dev\\php\\symfony\\modulartest\\FirstModule\\src\\DependencyInjection/../Resources/config\\services.yaml', array('autowire' => true, 'autoconfigure' => true))
     (C:\Users\eu\dev\php\symfony\modulartest\MainProject\vendor\symfony\dependency-injection\Loader\YamlFileLoader.php:234)
  at Symfony\Component\DependencyInjection\Loader\YamlFileLoader->parseDefinitions(array('parameters' => null, 'services' => array('ModularTest\FirstModuleBundle\' => array('resource' => '../src/', 'exclude' => array('../src/DependencyInjection/', '../src/Entity/', '../src/Kernel.php', '../src/Tests/')), 'ModularTest\FirstModuleBundle\Controller\' => array('resource' => '../src/Controller/', 'tags' => array('controller.service_arguments')))), 'C:\\Users\\eu\\dev\\php\\symfony\\modulartest\\FirstModule\\src\\DependencyInjection/../Resources/config\\services.yaml')
     (C:\Users\eu\dev\php\symfony\modulartest\MainProject\vendor\symfony\dependency-injection\Loader\YamlFileLoader.php:154)
  at Symfony\Component\DependencyInjection\Loader\YamlFileLoader->load('services.yaml')
     (C:\Users\eu\dev\php\symfony\modulartest\FirstModule\src\DependencyInjection\ModularTestFirstModuleExtension.php:30)
  at ModularTest\FirstModuleBundle\DependencyInjection\ModularTestFirstModuleExtension->load(array(array()), object(MergeExtensionConfigurationContainerBuilder))
     (C:\Users\eu\dev\php\symfony\modulartest\MainProject\vendor\symfony\dependency-injection\Compiler\MergeExtensionConfigurationPass.php:76)
  at Symfony\Component\DependencyInjection\Compiler\MergeExtensionConfigurationPass->process(object(ContainerBuilder))
     (C:\Users\eu\dev\php\symfony\modulartest\MainProject\vendor\symfony\http-kernel\DependencyInjection\MergeExtensionConfigurationPass.php:39)
  at Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass->process(object(ContainerBuilder))
     (C:\Users\eu\dev\php\symfony\modulartest\MainProject\vendor\symfony\dependency-injection\Compiler\Compiler.php:91)
  at Symfony\Component\DependencyInjection\Compiler\Compiler->compile(object(ContainerBuilder))
     (C:\Users\eu\dev\php\symfony\modulartest\MainProject\vendor\symfony\dependency-injection\ContainerBuilder.php:736)
  at Symfony\Component\DependencyInjection\ContainerBuilder->compile()
     (C:\Users\eu\dev\php\symfony\modulartest\MainProject\vendor\symfony\http-kernel\Kernel.php:541)
  at Symfony\Component\HttpKernel\Kernel->initializeContainer()
     (C:\Users\eu\dev\php\symfony\modulartest\MainProject\vendor\symfony\http-kernel\Kernel.php:780)
  at Symfony\Component\HttpKernel\Kernel->preBoot()
     (C:\Users\eu\dev\php\symfony\modulartest\MainProject\vendor\symfony\http-kernel\Kernel.php:183)
  at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
     (C:\Users\eu\dev\php\symfony\modulartest\MainProject\public\index.php:20) 

What can I try next?

1 Answer 1

2

You were almost there. The error message was misleading since it indicated src was a missing file and of course it is a directory. Plus it is obviously right there. The error is actually coming from the autowire section in services.yaml. You copied the lines from config/services.yaml but failed to account for the fact that in a bundle the relative path to the src directory is different.

# first_module/src/Resources/config/services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true

  ModularTest\FirstModuleBundle\:
    resource: '../../../src/' # Tweak the relative path
    exclude:
      - '../DependencyInjection/'
      - '../Entity/'
      - '../Kernel.php'
      - '../Tests/'

  ModularTest\FirstModuleBundle\Controller\:
    resource: '../../../src/Controller/'
    tags: ['controller.service_arguments']

I find it easier to trouble shoot these sorts of wiring problems from the command line. I just make a console command in main project and injected FirstService into it.

You don't need Bundle::getPath(). Probably had that in there when debugging.

Be aware that if these bundles are truly meant to be shared across multiple applications then autowiring is discouraged. It works but you might want to consider just manually defining services like the other bundles do.

By the way, I had forgotten about using the path attribute for repositories in composer.json. So thank you for that. Seems to work better than a symbolic link.

Sign up to request clarification or add additional context in comments.

12 Comments

Thank you! After making those changes, I got a new error: ===> Expected to find class "ModularTest\FirstModuleBundle\Resources\config\bundles" in file "C:\Users\ACJP\dev\php\symfony\modulartest\FirstModule\src\Resources\config\bundles.php" while importing services from resource "../../../src/", but it was not found! Check the namespace prefix used with the resource.
Hopefully you have figured out that you need to add Resources to the excluded directories in order to prevent autowire from trying to create a service from any .php file it happens to find.
Guess I woke up in a better mood today. No lecture about reading the docs or searching for answers. Just take a look at config/routes/dev/web_profiler.yaml for an example of how to load routes from a bundle. Use bin/console debug:router to test your work. And I would strongly suggest checking out the symfonycast tutorials.
Keep in mind that you are starting out by trying to do a fairly advanced configuration. Something that relatively few Symfony developers do on a routine basis. Back when Symfony 2 was first released, using bundles was quite common. But it turned out to be a big stumbling block for new developers so the need for bundles was reduced. Instead of starting with bundles it would have been better to just build an app, become familiar with the framework and then refactor if you felt the need.
Look at it this way. Imagine if someone with a very basic knowledge of python and no experience at all with django came to you and asked how to setup an enterprise application based on some rather sketchy specifications. There is a Symfony Reddit board where these sort of vague questions might have a better chance.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.