Skip to content

mnapoli/fluent-symfony

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

92 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fluent configuration for Symfony

Build Status

This package offers an alternative configuration syntax for Symfony's container, inspired by PHP-DI's configuration.

Why?

The main goal is to benefit from stricter analysis from the PHP engine and IDEs. If you are interested you can also read why YAML was replaced by a similar syntax in PHP-DI 5.

  • auto-completion on classes or constants:

  • auto-completion when writing configuration:

  • real time validation in IDEs:

  • constant support:

  • better refactoring support

Comparison with existing formats

Currently, in Symfony, you can configure the container using:

  • YAML

    parameters:
        mailer.transport: sendmail
    
    services:
        mailer:
            class:     Mailer
            arguments: ['%mailer.transport%']
  • XML

    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <parameters>
            <parameter key="mailer.transport">sendmail</parameter>
        </parameters>
    
        <services>
            <service id="mailer" class="Mailer">
                <argument>%mailer.transport%</argument>
            </service>
        </services>
    </container>
  • PHP code

    $container->setParameter('mailer.transport', 'sendmail');
    $container
        ->register('mailer', 'Mailer')
        ->addArgument('%mailer.transport%');

With this package, you can now use a 4th alternative:

return [
    'mailer.transport' => 'sendmail',

    'mailer' => create(Mailer::class)
        ->arguments('%mailer.transport%'),
];

Installation

composer require mnapoli/fluent-symfony

To enable the new format in a Symfony fullstack application, simply import the EnableFluentConfig trait in app/AppKernel.php, for example:

<?php

use Fluent\EnableFluentConfig;
use Symfony\Component\HttpKernel\Kernel;
// ...

class AppKernel extends Kernel
{
    use EnableFluentConfig;

    // ...
}

You can now either:

  • write all your config in "fluent" syntax, to do that change your AppKernel to load .php files instead of .yml:

    class AppKernel extends Kernel
    {
        use EnableFluentConfig;
    
        // ...
    
        public function registerContainerConfiguration(LoaderInterface $loader)
        {
            $loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.php');
        }
    }
  • or import PHP config files from YAML config files:

    imports:
        - services.php
        
    # ...

Be advised that PHP config files in the "traditional" form (see the documentation) are still supported and will continue to work.

Syntax

A configuration file must return a PHP array. In that array, parameters, services and imports are defined altogether:

<?php
# app/config/config.php

return [
    // ...
];

Parameters

Parameters are declared as simple values:

return [
    'foo' => 'bar',
];

This is the same as:

parameters:
    foo: 'bar'

Parameters and services can be mixed in the same array.

Services

Services can be declared simply using the create() function helper:

use function Fluent\create;

return [
    'mailer' => create(Mailer::class),
];

When calling $container->get('mailer') an instance of the Mailer class will be created and returned.

This is the same as:

services:
    mailer:
        class: Mailer

Using the class name as the entry ID

If the container entry ID is a class name, you can skip it when calling create().

return [
    Mailer::class => create(),
];

Autowiring

Services can also be automatically wired using the autowire() function helper in place of create():

use function Fluent\autowire;
 
return [
    Mailer::class => autowire(),
];

This is the same as:

services:
    Mailer:
        class: Mailer
        autowire: true

Constructor arguments

return [
    'mailer' => create(Mailer::class)
        ->arguments('smtp.google.com', 2525),
];

This is the same as:

services:
    mailer:
        class: Mailer
        arguments: ['smtp.google.com', 2525]

Dependencies

Parameters can be injected using the '%foo%' syntax:

return [
    'mailer' => create(Mailer::class)
        ->arguments('%mailer.transport%'),
];

This is the same as:

services:
    mailer:
        class:     Mailer
        arguments: ['%mailer.transport%']

Services can be injected using the get() function helper:

use function Fluent\get;

return [
    'newsletter_manager' => create(NewsletterManager::class)
        ->arguments(get('mailer')),
];

This is the same as:

services:
    newsletter_manager:
        class: NewsletterManager
        arguments: ['@mailer']

Setter injection

return [
    'mailer' => create(Mailer::class)
        ->method('setHostAndPort', 'smtp.google.com', 2525),
];

This is the same as:

services:
    mailer:
        class: Mailer
        calls:
            - [setHostAndPort, ['smtp.google.com', 2525]]

Property injection

return [
    'mailer' => create(Mailer::class)
        ->property('host', 'smtp.google.com'),
];

This is the same as:

services:
    mailer:
        class: Mailer
        properties:
            host: smtp.google.com

Optional service references

Services can have optional dependencies, so that the dependency is not required for it to work.

Setting missing dependencies to null
use function Fluent\create;
use function Fluent\get;

return [
    'newsletter_manager' => create(NewsletterManager::class)
        ->arguments(get('mailer')->nullIfMissing()),
];

This is the same as :

<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="newsletter_manager" class="NewsletterManager">
            <argument type="service" id="mailer" on-invalid="null" />
        </service>
    </services>
</container>
Ignore missing dependencies

When used with setter injection, it's possible to remove the method call using ignoreIfMissing() :

use function Fluent\create;
use function Fluent\get;

return [
    'newsletter_manager' => create(NewsletterManager::class)
        ->method('setMailer', get('mailer')->ignoreIfMissing()),
];

This is the same as :

services:
    app.newsletter_manager:
        class:     AppBundle\Newsletter\NewsletterManager
        calls:
            - [setMailer, ['@?app.mailer']]

Private Services

return [
    Mailer::class => create()
        ->private(),
];

This is the same as:

services:
    mailer:
        class: Mailer
        public: false

Decorated services

Services can be decorated with the decorate() method

return [
    'decorating_mailer' => create(MailerDecorator::class)
        ->decorate('mailer')
        ->argument(get('decorating_mailer.inner')),
];

This is the same as:

services:
    decorating_mailer:
        class: 'MailerDecorator'
        decorates: 'mailer'
        arguments: ['@decorating_mailer.inner']

If you want to apply more than one decorator to a service, you can change the inner service name (IE the decorated service) and configure the priority of decoration :

return [
    'foo' => create(Foo::class),
    'bar' => create(Bar::class)
        ->decorate('foo', 'bar.foo', 5)
        ->arguments(get('bar.foo'))
    ,
    'baz': create(Baz::class)
        ->decorate('foo', 'baz.foo', 1),
        ->arguments(get('baz.foo'))
];

This is the same as:

foo:
    class: Foo

bar:
    class: Bar
    decorates: foo
    decoration_inner_name: 'bar.foo'
    decoration_priority: 5
    arguments: ['@bar.foo']

baz:
    class: Baz
    decorates: foo
    decoration_inner_name: 'baz.foo'
    decoration_priority: 1
    arguments: ['@baz.inner']

Non shared services

All services are shared by default. You can force the container to always create a new instance using the unshared() function helper:

return [
    'app.phpmailer' => create(PhpMailer::class)
        ->unshared(),
];

This is the same as:

services:
    app.phpmailer:
        class: AppBundle\Mail\PhpMailer
        shared: false

Synthetic services

Services can be injected at runtime. You can inject a class instance as service, instead of configuring the container to create a new instance using the synthetic() function helper:

return [
    'app.phpmailer' => create(PhpMailer::class)
        ->synthetic(),
];

This is the same as:

services:
    app.phpmailer:
        class: AppBundle\Mail\PhpMailer
        synthetic: true

Factories

Services can be created by factories using the factory() function helper:

use function Fluent\factory;

return [
    'newsletter_manager' => factory([NewsletterManager::class, 'create'], NewsletterManager::class)
        ->arguments('foo', 'bar'),
];

When calling $container->get('newsletter_manager') the result of NewsletterManager::create('foo', 'bar') will be returned.

This is the same as:

services:
    newsletter_manager:
        factory: ['AppBundle\Email\NewsletterManager', 'create']
        class: 'AppBundle\Email\NewsletterManager'
        arguments: ['foo', 'bar']

When using the class name as service ID, you don't have to explicitly state the class name of the service:

return [
    // you can write:
    NewsletterManager::class => factory([NewsletterManager::class, 'create']),
    // instead of:
    NewsletterManager::class => factory([NewsletterManager::class, 'create'], NewsletterManager::class),
];

Aliases

Services can be aliased using the alias() function helper:

use function Fluent\create;
use function Fluent\alias;

return [
    'app.phpmailer' => create(PhpMailer::class),
    'app.mailer' => alias('app.phpmailer'),
];

When calling $container->get('app.mailer') the app.phpmailer entry will be returned.

This is the same as:

services:
    app.phpmailer:
        class: AppBundle\Mail\PhpMailer
    app.mailer:
        alias: app.phpmailer

Private Aliases

return [
    'app.phpmailer' => create(PhpMailer::class),
    'app.mailer' => alias('app.phpmailer')
        ->private(),
];

This is the same as:

services:
    app.phpmailer:
        class: AppBundle\Mail\PhpMailer
    app.mailer:
        alias: app.phpmailer
        public: false

Tags

Services can be tagged :

return [
    'mailer' => create(Mailer::class)
        ->tag('foo', ['fizz' => 'buzz', 'bar' => 'baz'])
        ->tag('bar'),
];

This is the same as:

services:
    mailer:
        class: Mailer
        tags:
            - {name: foo, fizz: buzz, bar: baz}
            - {name: bar}

Imports

Other configuration files can be imported using the import() function helper:

use function Fluent\import;

return [
    import('services/mailer.php'),
];

You will notice that the array item is not indexed by an entry ID.

This is the same as:

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

Extensions

Extensions (like the framework configuration for example) can be configured using the extension() function helper:

use function Fluent\extension;

return [
    extension('framework', [
        'http_method_override' => true,
        'trusted_proxies' => ['192.0.0.1', '10.0.0.0/8'],
    ]),
];

This is the same as:

framework:
    http_method_override: true
    trusted_proxies: [192.0.0.1, 10.0.0.0/8]