0
votes

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

1 Answers

1
votes

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.