Stefano Alletti

Home » Code » PHP » Symfony

Category Archives: Symfony

Test double with webservices, Symfony and PHPUnit

Often, applications that i have developed was interfaced with external web services. Sometimes the services that my application would have to consume were themselves under development. Other times did not respond as they should have. What to do in these cases? Wait for the resolution of problems or the conclusion of the development of the API’s could stop the development of our application. But in a scenario of continuous development that is not acceptable.

So, the solution are mocking web services. But how to mock web services?

Case 1) Linking to internal routes instead of real webservice

Suppose we have to replace a web-service defined on config_dev.yml as well:

web_services:
  service1:
    api_info:
      endpoint: 'http://api-services/service1'
      api_user: 'service1'
      api_pass: 'service1'

1 – Replace the real config with:

web_services:
  service1:
    api_info:
      endpoint: 'http://salletti.myapplication.dev/api-services-mocked/service1'
      api_user: ''
      api_pass: ''

2 – Create new route in routing.yml:

service1-mocked:
    path:     /api-services-mocked/service1
    defaults: { _controller: 'MyApplicationMockBundle:Mock:service1' }
    methods: [POST]

3 – Create Action in MockController of MoclBundle:

<?php

namespace MyApplication\MockBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * @codeCoverageIgnore
 * No need to be cover
 */
class MockController extends Controller
{
    public function servive1Action(Request $request)
    {
        //Your logic here
       $response = new Response($this->container->get('mock_service')->getGenericResultWithoutError());
       $response->headers->set('Content-Type', 'application/json');

       return $response;
    }
}

It’s all, with this strategy you can mock every external web service in a easy way. You can as well use the mock for yours unit tests, simply changing in config_test.yml urls pointing to the external serviceswith your internal urls.

 

Case 2) Injecting mocked class instead real classes

Imagine having clients interfacing with external web services. Clients are injected inside others classes. For example:

#MyApplication/MyBundle/Resources/services.yml
services:
  service1_client:
    class: MyApplication\MyBundle\Client\Service1Client
    calls:
      - [ setLogger, ['@my_logger']]
  service1_consumer:
    class: MyApplication\MyBundle\Client\Service1Consumer
    calls:
      - [ setClient, ['@service1_client']]

And the classes:

<?php

namespace MyApplication\MyBundle\Client
class Service1Client {

   public function getFoo(array $params)
   {
      $result = $this->callService('getFoo', $params);
      return isset($result->foo) ? $result->foo : false;
   } 
}
<?php

namespace MyApplication\MyBundle\Service1Consumer
class Service1Consumer {

   public function getFoo()
   {
      $params = ...;
      return $this->client->getFoo($params);
   }

   public function setClient($client)
   {
      $this->client = $client; 
   } 
}

 

The idea is to change service to inject:

1 – Create a Mock class

<?php

namespace MyApplication\MockBundle\Client
class Service1Client {

   public function getFoo(array $params)
   {
      //return what you want. Your logic here
      return array('foo' => 'bar');
   } 
}

2 – Create a services_test.yml

services:
  service1__mock_client:
    class: MyApplication\MockBundle\Client\Service1Client
    calls:
      - [ setLogger, ['@my_logger']]
 service1_consumer:
    class: MyApplication\MyBundle\Client\Service1Consumer
    calls:
      - [ setClient, ['@service1_mock_client']]

3 – Load services_test.yml file if you are in a test environment

namespace MyApplication\MyBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
class MyApplicationMyBundleExtension extends Extension
{
 /**
 * {@inheritDoc}
 */
 public function load(array $configs, ContainerBuilder $container)
 {
     $configuration = new Configuration();
     $config = $this->processConfiguration($configuration, $configs);
     $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
     $loader->load('services.yml');
    
    //If we are on test env we load classes mocked defined on service_test.yml
    if ($container->getParameter('kernel.environment') == 'test') {
      $loader->load('services_test.yml');
    }
 }

 

Case 3) Useing Test Doubles of PHPUnit

1 – Using getMockBuilder

<?php
use PHPUnit\Framework\TestCase;

class ConsumerTest extends TestCase
{
    public function testConsumer()
    {
        // Create a stub for the SomeClass class.
        $stubClient = $this->getMockBuilder(Service1Client:class) 
       ->disableOriginalConstructor() 
       ->getMock(); 

       // Configure the stub. 
      $stubClient->method('getFoo')->willReturn(array('foo' => 'bar'));
      $consumer = new Service1Consumer();
      $consumer->setClient($stubClient)
      $this->assertArrayHasKey('foo', $consumer->getFoo()); 
   } 
} 
?>

2 – Using Prophecy

<?php
use PHPUnit\Framework\TestCase;

class ConsumerTest extends TestCase
{
    public function testConsumer()
    {
        $consumer = new Service1Consumer();

        // Create a prophecy for the Observer class.
        $client = $this->prophesize(Service1Client::class);

        // Set up the expectation for the update() method
        // to be called only once
        // as its parameter.
        $client->getFoo(Argument::type('array'))->shouldBeCalled();
        $client->willReturn(array('foo' => 'bar'));        
        // Reveal the prophecy and attach the mock object
        // to the Consumer.
        $consumer->attach($client->reveal());
        $this->assertArrayHasKey('foo', $consumer->getFoo());
    }
}
?>

Conclusion

Sometimes it is just plain hard to test the system under test (SUT) because it depends on other components that cannot be used in the test environment. This could be because they aren’t available, they will not return the results needed for the test or because executing them would have undesirable side effects. In other cases, our test strategy requires us to have more control or visibility of the internal behavior of the SUT. I have listed three strategies that i used, hoping that can be help to someone.

References:

Symfony and Monolog, how use Processor in your project: a practical example

We often have to use different micro-services who write in many log files. Use utilities like Kibana is a good thing, but in order to take full advantage of its features we have to try to standardize and normalize the logs.
The company where I work having introduced Kibana recently, he asked me to implement a proper strategy to log all the micro-services.

First we defined the basic rules of the logs.

General logging rules of team

 

Mandatory Fields

  • Date + Hours : 2017-02-09 10:24:00
  • Log Level : TRACE DEBUG INFO WARN ERROR FATAL
  • Customer Id
  • Site or ProductLine
  • Message

If log is ERROR mandatory fields are also:

  • File
  • Class
  • Method
  • Line


Describing the exact point of the code that has launched the exception.

When logging

  • Start and End of service call
  • External call
  • Exceptions
  • Key points of the application

 

Where logging

  • 1 file for service
  • 1 error file for service

Technology

Monolog


Monolog sends your logs to files, sockets, inboxes, databases and various web services. See the complete list of handlers below. Special handlers allow you to build advanced logging strategies.
This library implements the PSR-3 interface that you can type-hint against in your own libraries to keep a maximum of interoperability. You can also use it in your applications to make sure you can always use another compatible logger at a later time. As of 1.11.0 Monolog public APIs will also accept PSR-3 log levels. Internally Monolog still uses its own level scheme since it predates PSR-3. Reference:https://github.com/Seldaek/monolog

Working with Symfony and Monolog

Symfony comes with native Monlog library that allows you to create logs that can be stored in a variety of different places.
The logger service has different methods for different logging levels/priorities. You can configure the logger to do different things based on the level of a message (e.g. send an email when an error occurs).
The logger has a stack of handlers, and each can be used to write the log entries to different locations (e.g. files, database, Slack, etc).

For a proper and complete vision of Monolog in Symfony, refer to the official documentation: http://symfony.com/doc/current/logging.htmlhttp://symfony.com/doc/current/logging/processors.html http://symfony.com/doc/current/reference/configuration/monolog.html

 

Usage example

We use as example one of many micro-services that my company use: “serviceactivator” web service.
What it does the micro-service does not matter. What interests us is how it write logs.

1) Configuration of monolog

#app/config.yml

monolog:
    handlers:
        main:
            type:   stream
            path:   "%kernel.logs_dir%/%kernel.environment%.log"
            channels: ['!serviceactivator']
        serviceactivator:
            type: stream
            path: '%kernel.logs_dir%/%kernel.environment%.serviceactivator.log'
            channels: [serviceactivator]
            formatter: monolog.formatter.service_activator


2) Base Logger: the parent logger class

<?php

namespace Meetic\LogBundle\Logging;

use Meetic\LogBundle\Logging\Processor\IntrospectionProcessor;
use Monolog\Logger;

/**
 * BaseLogger provide methods in order to write log using monolog.
 *
 * @package Meetic\LogBundle\Logging
 */
class BaseLogger
{
    protected $logger;
    protected $disabled;
    protected $processor;
 
    public function __construct(Logger $logger, $disabled = false)
    {
        $this->logger = $logger;
        $this->disabled = $disabled;
        $this->processor = new IntrospectionProcessor();
    }

    /**
     * Generic log method to transform message in string if
     * it's an object or an array
     *
     * @param string $level Log level
     * @param mixed $message Message (can be string, array, or object)
     * @param array $context Context
     */
    protected function log($level, $message, $context)
    {
        if (is_array($message) || is_object($message)) {
            $message = json_encode($message);
        }

        if (is_object($context)) {
            $context = json_decode(json_encode($context), true);
        }

        $this->logger->addRecord($level, (string)$message, (array)$context);
    }

    /**
     * Write error log
     *
     * @param string $message the message to write in log file
     *
     * @return void
     */
    public function logError($message, $context = [])
    {
        $this->logger->pushProcessor($this->processor);
        $this->log(Logger::ERROR, $message, $context);
    }

    /**
     * Write warning log
     *
     * @param string $message the message to write in log file
     *
     * @return void
     */
    public function logWarning($message, $context = [])
    {
        $this->log(Logger::WARNING, $message, $context);
    }

    /**
     * Write notice log
     *
     * @param string $message the message to write in log file
     *
     * @return void
     */
    public function logNotice($message, $context = [])
    {
        $this->log(Logger::NOTICE, $message, $context);
    }

    /**
     * Write info log
     *
     * @param string $message the message to write in log file
     *
     * @return void
     */
    public function logInfo($message, $context = [])
    {
        $this->log(Logger::INFO, $message, $context);
    }
}


3) The specific logger class

<?php

namespace Meetic\LogBundle\Logging;

class ServiceActivatorLogger extends BaseLogger
{

}

 

4) Configuration of services

services:
    serviceactivator_logger:
        class: Meetic\LogBundle\Logging\ServiceActivatorLogger
        arguments: ["@logger"]
        tags:
            - { name: monolog.logger, channel: serviceactivator }

    monolog.formatter.service_activator:
        class: Meetic\LogBundle\Logging\Formatter\MeeticLineFormatter
        arguments:
            - "[%%datetime%%] [%%extra.customerId%%] [%%extra.productLine%%] %%channel%%.
               %%level_name%%: %%message%% %%context%%\n"


5) Creation of Monolog Processor

Monolog allows you to process the record before logging it to add some extra data. A processor can be applied for the whole handler stack or only for a specific handler.
A processor is simply a callable receiving the record as its first argument. Processors are configured using the monolog.processor DIC tag. See the reference about it.

<?php

namespace Meetic\LogBundle\Logging\Processor;

class ServiceActivatorProcessor
{
    /**
     * @param  array $record
     * @return array
     */
    public function __invoke(array $record)
    {
        foreach ($record['context'] as $key => $val) {
            $record['extra'][$key] = $val;
        }
       return $record;
    }
}


This processor, for example, is responsible to place in extra all key/value pairs passed in the context.

6) Declare service Processor

monolog.processor.serviceactivator:
    class: Meetic\LogBundle\Logging\Processor\ServiceActivatorProcessor
    tags:
        - { name: monolog.processor, method: __invoke, handler: serviceactivator }

The method __invoke of Processor will be called every times that ServiceActivatorLogger write log.

7) Call log Example

$this->logger->logInfo(
    'Result of curl call',
    array(
        'codeResponse' => $this->httpReponseCode,
        'result' => $result,
        'customerId' => $this->customerId,
        'productLine' => $this->productLine
    )
);


Generated log:

[2017-03-13 11:50:58] [29341843] [DATING] serviceactivator.INFO: Result of curl call 
{"codeResponse":204,"result":"blablabla","customerId":"29341843","productLine":"DATING"} 

 
8) Specific Processor for log error

We will use IntrospectionProcessor that will be called only if application want log a error:

namespace Meetic\LogBundle\Logging;
use Meetic\LogBundle\Logging\Processor\IntrospectionProcessor;
class BaseLogger {
    
protected $processor;

    public function __construct(Logger $logger, $disabled = false)
    {
        
        $this->processor = new IntrospectionProcessor();
    }
   
  
    ...
    ...

    /**
    * Write error log
    *
    * @param string $message the message to write in log file
    *
    * @return void
    */
    public function logError($message, $context = [])
    {
        $this->logger->pushProcessor($this->processor);
        $this->log(Logger::ERROR, $message, $context);
    }
    ...
}

 

Generated log:

[2017-03-10 10:40:38] [61762462] [DATING]serviceactivator.ERROR: : user not active
{"file":"<path>/ConstraintsAboidActifValidator.php",
"line":19,
"class":"ServiceActivatorWSBundle\\Validator\\ConstraintsConstraintsAboidActifValidator",
"function":"validate"}

9) Separate file for error log

Nothing could be simpler with monolog:

serviceactivator_error:
    type: stream
    path: '%kernel.logs_dir%/%kernel.environment%.serviceactivator_error.log'
    channels: [serviceactivator_error]
    formatter: monolog.formatter.service_activator

 

10) Custom Formatter

So far everything is fine, but what happens if we have not ‘customerId’ or productLine in context array? We will have a log like this:

[2017-03-13 11:09:14] [] [] serviceactivator_error.ERROR: Error parameters validator
{"error":"6176243423423423462: user not active","customerId":"6176243423423423462","productLine":"DATING"} {"file":"<path>/ConstraintsAboidActifValidator.php","line":19,
"class":"Meetic\\Payment\\ServiceActivatorWSBundle\\Validator\\Constraints
ConstraintsAboidActifValidator","function":"validate"}

You can see that brackets that should contain customerId and productLine are empty.
So, the last step, act to printing well logs are customize monolog formatter class. Specifically we will customize LineFormatter class.

<?php

namespace Meetic\LogBundle\Logging\Formatter;

use Monolog\Formatter\NormalizerFormatter;

class MeeticLineFormatter extends NormalizerFormatter
{
    //DEFAULT FORMAT
    const SIMPLE_FORMAT = "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n";
   
    //MEETIC FORMAT
    const MEETIC_FORMAT = "[%datetime%] [%extra.customerId%] [%extra.productLine%]".
                          "%channel%.%level_name%: %message% %context% %extra%\n";

    protected $format;
    protected $allowInlineLineBreaks;
    protected $ignoreEmptyContextAndExtra;
    protected $includeStacktraces;

    /**
     * @param string $format The format of the message
     * @param string $dateFormat The format of the timestamp: one supported by DateTime::format
     * @param bool $allowInlineLineBreaks Whether to allow inline line breaks in log entries
     * @param bool $ignoreEmptyContextAndExtra
     */

    public function __construct(

        $format = null,
        $dateFormat = null,
        $allowInlineLineBreaks = false,
        $ignoreEmptyContextAndExtra = false
    ) {
        $this->allowInlineLineBreaks = $allowInlineLineBreaks;
        $this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra;
 
        parent::__construct($dateFormat);
    }

    ...
    ...
    ...

    /**
     * {@inheritdoc}
     */
    public function format(array $record)
    {
        $vars = parent::format($record);
        
        //If customerId and productLine are not defined we use default format.
        if (empty($record['extra']['customerId']) || empty($record['extra']['productLine'])) {
            $output = str_repeat('-', 120).PHP_EOL.static::SIMPLE_FORMAT;
        } else {
            $output = str_repeat('-', 120).PHP_EOL.static::MEETIC_FORMAT;
        }
        ...
        ...
        ...

        return $output;
    }
    ...
    ...
    ...
}

 

If customerId and productLine are empty the application use default format (SIMPLE_FORMAT) and we will have a log like this:

[2017-03-13 11:43:30] serviceactivator_error.ERROR: Error parameters validator
{"error":"DATINGa is not a valid ProductLine","customerId":"29341843","productLine":"DATINGa"} 
{"file":"<path>/ConstraintsProductLineAcceptedValidator.php","line":26,
"class":"ServiceActivatorWSBundle\\Validator\\Constraints\\ConstraintsProductLineAcceptedValidator","function":"validate"}

So now, the formatter service can change in this way:

monolog.formatter.service_activator:
  class: Meetic\LogBundle\Logging\Formatter\MeeticLineFormatter
  arguments: [null, null, true, true]

We can do this because we put the custom logic, act to handle format of log, inside custom format method of formatter class.

 

Evolutions

The next development could be use configuration files to tell to processor which fields include in extra field of record structure:

 

mandatory_logger_fields:
    serviceactivator:
        customerId: ~
        productLine: ~    

 

And pass it to service:

monolog.processor.serviceactivator:
    class: Meetic\LogBundle\Logging\Processor\ServiceActivatorProcessor
    arguments: ["@=container.getParameter('mandatory_logger_fields')['serviceactivator']]
    tags:
      - { name: monolog.processor, method: __invoke, handler: serviceactivator  }

 

 

References

 

Symfony 3: some new features that i like

New in Symfony 3.3: Getter injection

As part of our experimental features program, in Symfony 3.3 we’ve added a new feature called getter injection. This adds up to the usual mechanisms used for dependency injection and doesn’t replace any of them. Instead, it provides an additional way that fits some specific use cases.

Getter injection allows the dependency injection container to leverage classes that provide inheritance-based extension points that matches the following requirements: public or protected methods with zero arguments and free of side-effects.

Some examples found while grepping Symfony and its vendors:

  • Kernel::getRootDir/CacheDir/LogDir() in HttpKernel
  • SessionListener::getSession() in HttpKernel also
  • AbstractBaseFactory::getGenerator() in ProxyManager

This is only a small subset of all the classes that apply this flavor of the open/closed principle in Symfony core and elsewhere. As shown in the examples, this applies both to objects injection (services) and to values injection (parameters).

Getter injection is a way to turn these classes into DI candidates via simple DI configuration. In Yaml, taking the SessionListener::getSession() example, this could look like:

1
2
3
4
services:
  SessionListener:
    getters:
      getSession: '@session'

In practice, this tells the Symfony Dependency Injection Container to create an anonymous inheritance-proxy class like this one:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$sessionListener = new class ($container) extends SessionListener {
    private $container;

    function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function getSession()
    {
        return $this->container->get('session');
    }
};

See More

New in Symfony 3.3: Simple Cache

In Symfony 3.1, we added a new Cache component that implemented the PSR-6: Caching Interface standard. In Symfony 3.2 we improved the component with tagged caches and other improvements.

Although the Cache component provides everything that enterprise applications need, for smaller applications it’s a bit cumbersome to use. For example, to use a file system based cache to store, fetch and delete a simple variable, you must do the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
use Symfony\Component\Cache\Adapter\FilesystemAdapter;

$cache = new FilesystemAdapter();

// save an item in the cache
$numProducts = $cache->getItem('stats.num_products');
$numProducts->set(4711);
$cache->save($numProducts);

// fetch the item from the cache
$numProducts = $cache->getItem('stats.num_products');
if (!$numProducts->isHit()) {
    // ... item does not exist in the cache
} else {
    $total = $numProducts->get();
}

// remove the item from the cache
$cache->deleteItem('stats.num_products');

In Symfony 3.3 we decided to improve the Cache component by implementing a related standard called PSR-16: Common Interface for Caching Libraries. In short, it’s a simplified cache mechanism to store, fetch and remove items from a cache. This is how the previous example would look with the new cache:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use Symfony\Component\Cache\Simple\FilesystemCache;

$cache = new FilesystemCache();

// save an item in the cache
$cache->set('stats.num_products', 4711);

// fetch the item from the cache
if (!$cache->has('stats.num_products')) {
    // ... item does not exist in the cache
} else {
    $total = $cache->get('stats.num_products');
}

// remove the item from the cache
$cache->delete('stats.num_products');

The simple cache also allows to define default values when items don’t exist in the cache and it defines the setMultiple(), getMultiple() and deleteMultiple() methods to work with several items simultaneously.

Both the regular cache and the simple cache support the same cache adapters (file system, Redis, Memcache, etc.) and both provide similar performance, so the decision to choose one or another should be based on the features that you’ll need for the cache.

 

New in Symfony 3.3: Asset preloading with HTTP/2 Push

One of the most relevant new features proposed by HTTP/2 to improve the loading of web pages is called Server Push. HTTP/2 Push allows a web server to send resources to a web browser before the browser gets to request them.

In Symfony 3.3 we added HTTP/2 Push support for web assets (CSS, JS, images) to allow preloading them as explained in the Preload W3C Working Draft. In practice, and following the traditional Symfony philosophy of using composition, the new feature is enabled by wrapping your assets with the new preload() function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<head>
    {# by default, assets are pushed #}
    <link href="{{ preload(asset('/css/app.css')) }}">
    {# ... but you can disable pushing... #}
    <link href="{{ preload(asset('/css/app.css'), { nopush: true }) }}">
    {# ... and you can also explicit the asset type #}
    <link href="{{ preload(asset('/css/app.css'), { as: 'style' }) }}">
    ...
</head>
<body>
    ...
    /spanspan%20class=
</body>

Behind the scenes, the preload() function adds a Link HTTP header that is processed by intermediate proxies compatible with HTTP/2:

1
2
3
4
HTTP/1.1 200 OK
Content-Type: text/html
...
Link: </css/app.css>; rel=preload,</cjs/app.js>; rel=preload

An added bonus of using this technique is that all those assets are downloaded using just one connection, improving the page speed dramatically.

Sources:

Circuit breaker pattern – Comment fiabiliser nos microservices

Aujourd’hui les architectures micro-services sont de plus en plus répandues. Mais quels sont les moyens de contrôler votre nouveau système d’information ?

Mettons fin au mystère dès maintenant, le circuit-breaker, c’est le disjoncteur de votre architecture micro-services. Mais comment cela fonctionne et pourquoi en aurions-nous besoin ?

Voila une suite d’articles intéressants en français concernant le Circuit Breaker.

 

http://blog.octo.com/circuit-breaker-un-pattern-pour-fiabiliser-vos-systemes-distribues-ou-microservices-partie-1/

http://blog.octo.com/circuit-breaker-un-pattern-pour-fiabiliser-vos-systemes-distribues-ou-microservices-partie-2/

http://blog.octo.com/circuit-breaker-un-pattern-pour-fiabiliser-vos-systemes-distribues-ou-microservices-partie-3/

http://blog.octo.com/circuit-breaker-un-pattern-pour-fiabiliser-vos-systemes-distribues-ou-microservices-partie-4/

 

Ici l’implémentation du design pattern en Syfmony :

http://blog.eleven-labs.com/fr/le-circuit-breaker-kesako/

Playing with RabbitMQ, PHP and node

I need to use RabbitMQ in one project. I’m a big fan of Gearman, but I must admit Rabbit is much more powerful. In this project I need to handle with PHP code and node, so I want to build a wrapper for those two languages. I don’t want to re-invent the wheel so I will use existing libraries (php-amqplib and amqplib for node).

Basically I need to use three things: First I need to create exchange channels to log different actions. I need to decouple those actions from the main code. I also need to create work queues to ensure those works are executed. It doesn’t matter if work is executed later but it must be executed. And finally RPC commands.

Let’s start with the queues. I want to push events to a queue in PHP

Reed more from original source

Symfony : Mieux structurer ces déclarations de services

Par défaut lorsque l’on crée un bundle, celui-ci ne charge qu’un seul fichier de service via le code suivant:

$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');

Lorsque l’on développe une grosse application, mais qu’on ne souhaite qu’un seul bundle, on se retrouve avec des dizaines de fichiers de services et le fichier src/AppBundle/DependencyInjection/AppExtension.php commence à ressembler à cela:

$loader->load('services.yml');
$loader->load('customer.yml');
$loader->load('group.yml');
$loader->load('product.yml');
$loader->load('admin/customer.yml');
$loader->load('admin/group.yml');
$loader->load('admin/product.yml');
...

Si l’on charge les fichiers de services depuis le fichier app/config/config.yml, on peut importer un dossier ce qui va charger tout les fichiers qu’il contient, mais impossible de modifier les services lorsqu’ils sont chargé de cette façon.

Pour reproduire ce comportement dans un bundle, il suffit de modifier la manière dont sont chargé les fichiers de déclaration de services :

use Symfony\Component\Config\Loader\DelegatingLoader;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\DependencyInjection\Loader\DirectoryLoader;
$resolver = new LoaderResolver([
    new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')),
    new DirectoryLoader($container, new FileLocator(__DIR__ . '/../Resources/config')),
]);

$loader = new DelegatingLoader($resolver);
$loader->load('services.yml');

Avec ce code, vous pouvez maintenant importer des dossiers en plus des fichiers yml.

# services.yml
imports:
    - { resource: services/ }

Source: Benjamin Leveque