How to create #Symfony app independent #SymfonyBundle

If you wish to create independent Symfony Bundle then you are on the Highway to Hell. At netpromotion we take the ball and run with it. Now we are ready to release our tool which solves the most common problems on the highway. Please, take a seat and read more.

Last things first - the tool

At the begin, please, let me to introduce you Symfony Up! - our tool which solves common problems with independent bundles. You can simply require it by composer require netpromotion/symfony-up command.

You can build minimal application by vendor/bin/symfony-up command if you wish.

Run tests without the target application

When you starts with bundles, you probably met the Best Practices for Reusable Bundles.

The test suite must be executable with a simple phpunit command run from a sample application;

Best practise is to create whole application to test your application independent bundle.

Symfony Up! provides you UpTestCase which requires only kernel. So your tests should be executed which only one additional class.

<?php

class YourBundle {/* ... */}

class YourKernel extends Netpromotion\SymfonyUp\UpKernel
{
    public function registerBundles()
    {
        return [
            /* ... */
            new YourBundle(),
        ];
    }
}

class YourBundleServiceTest extends Netpromotion\SymfonyUp\UpTestCase
{
    public static function getKernelClass()
    {
        return YourKernel::class;
    }
    
    public function testYourServiceWorks() {/* ... */}
}

Create easily modifiable configuration

When you are ready to use test-driven development you need to create configuration.

For simple configuration settings, rely on the default parameters entry of the Symfony configuration.

You can’t get control over parameters - so you must create configuration tree over DI.

<?php

class YourBundleConfiguration /* ... */ implements Symfony\Component\Config\Definition\ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new Symfony\Component\Config\Definition\Builder\TreeBuilder();
        $rootNode = $treeBuilder->root('your_bundle');

        $rootNode->children()
            ->scalarNode('scalar')
                ->defaultValue('value')
                ->end()
            ->arrayNode('strict_array')
                ->addDefaultsIfNotSet() // otherwise you don't get this key
                ->children()
                ->booleanNode('boolean')
                    ->defaultValue(false)
                    ->end()
                ->end()
            ->end()
            ->variableNode('dynamic_array')
                ->defaultValue(['key' => 'value'])
                ->end()
            ->end();

        return $treeBuilder;
    }
}

class YourBundleExtension extends Symfony\Component\DependencyInjection\Extension\Extension
{
    public function load(array $configs, Symfony\Component\DependencyInjection\ContainerBuilder $container)
    {
        $configuration = new YourBundleConfiguration();
        $config = $this->processConfiguration($configuration, $configs);

        /* ... */
    }
}

class YourBundle extends Symfony\Component\HttpKernel\Bundle\Bundle
{
    public function getContainerExtension()
    {
        return new YourBundleExtension();
    }
}

Now you have equivalent for:

your_bundle:
  scalar: value
  strict_array:
    boolean: true
  dynamic_array:
    key: value

Register services dependent on configuration

When you have configuration, you need to use the values in your services.

Services should not use autowiring or autoconfiguration. Instead, all services should be defined explicitly.

It looks easy and it’s easy - but you can’t use static definitions. Now you need to create definitions dynamically during load process.

<?php

class YourBundleService
{
    public function __construct(array $strictArray) {/* ... */}

    /* ... */
}

class YourBundleExtension extends Symfony\Component\DependencyInjection\Extension\Extension
{
    public function load(array $configs, Symfony\Component\DependencyInjection\ContainerBuilder $container)
    {
        /* ... */

        $container->setDefinition('your_bundle.service', new Symfony\Component\DependencyInjection\Definition(YourBundleService::class))
            ->setArguments([
                $config['strict_array'],
            ]);

        /* ... */
    }
}

Register your configuration as service for controllers

If you need to access your configuration in controllers, you can use controller as service and register it the same way as regular service. But it isn’t practical, much better solution is to register your configuration as service.

<?php

class YourBundleConfiguration extends ArrayObject implements Symfony\Component\Config\Definition\ConfigurationInterface
{
    public function getConfigTreeBuilder() {/* ... */}
}

class YourBundleExtension extends Symfony\Component\DependencyInjection\Extension\Extension
{
    public function load(array $configs, Symfony\Component\DependencyInjection\ContainerBuilder $container)
    {
        /* ... */

        $container->setDefinition('your_bundle.configuration', new Symfony\Component\DependencyInjection\Definition(YourBundleConfiguration::class))
            ->setArguments([
                $config,
            ]);
    }
}

Create application independent reusable symfony bundle

Congratulation, now you are ready to create your own application independent reusable Symfony bundle. You can follow netpromotion and my GitHub account if you wish to read real usages of this tips and hints.