8000 [DI] Add a new "PHP fluent format" for configuring the container · Issue #22407 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[DI] Add a new "PHP fluent format" for configuring the container #22407

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mnapoli opened this issue Apr 12, 2017 · 25 comments
Closed

[DI] Add a new "PHP fluent format" for configuring the container #22407

mnapoli opened this issue Apr 12, 2017 · 25 comments

Comments

@mnapoli
Copy link
Contributor
mnapoli commented Apr 12, 2017
Q A
Bug report? no
Feature request? yes
BC Break report? no
RFC? yes
Symfony version 3.3 or 4.0?

How about adding a new configuration format for the dependency injection container?

TL/DR

Short version: YAML and XML suck in their own ways. We are PHP developers, we know how to write PHP code and we have all the tools we need around PHP.

Let's write configuration in PHP. That doesn't have to be verbose or boring, on the contrary it's possible to improve the developer experience.

A possible solution is detailed at the end. The main idea is to take the best from YAML, XML and PHP and imagine a new and better configuration format.

Intro

The current formats:

  • YAML
  • XML
  • PHP API of the container

To be clear, I'm suggesting adding a 4th format.

3 options is already a lot, adding a 4th is maybe not the best. But I think we can write a new format that is better than these 3 formats altogether. I believe that new format could replace all other 3 eventually.

At first the new format could be tagged as experimental.

Why?

The downsides of the existing formats.

YAML

YAML is easy to read, but:

  • error prone to write (indentation issues, arrays in arrays in objects in array, …)
  • YAML is just a data structure, adding behavior requires hacking up the format and inventing a custom language on top of it:
    • @service_name for service references, %param_name% for parameters
    • %env(...)% for environment variables
    • @?app.mailer for optional injection
    • requires using the expression language for doing basic logic that could be done in PHP: "@=service('mailer_configuration').getMailerMethod()"
    • etc. YAML config starts to look like Lisp or regexps with all the special characters
  • no autocompletion of the YAML API available, e.g. should I type decorate or decorates?
  • no documentation of the YAML API in IDEs (e.g. wondering what does autowiring_types do? -> there is no phpdoc for that, you have to go find it in the online documentation)
  • no static analysis of the PHP items (e.g. recognize if you type a class name that doesn't exist, refactoring support, autocompletion, …)
  • both last points are partially addressed by IDE plugins like the amazing Symfony plugin for PhpStorm
    • the Symfony plugin still comes with issues:
      • a huge part of the IDE tooling around Symfony relies on one developer, which is very fragile
      • it's written in Java (obviously) which can explain why there are so few contributors yet so many users
    • in general all those plugins come with the root issue that they are extremely complicated since they have to support a whole configuration language with a lot of options
  • there is also no native YAML reader in PHP, forcing to require symfony/yaml. This is also why the XML format is encouraged for open source bundles, leading to 2 different formats being recommended.

XML

XML has the benefit of being more strict and benefit from validation/IDE helpers, but:

  • verbose and harder to read (which is probably why YAML is preferred in the community)
  • hard to type without an IDE
  • while there can be autocompletion of XML items (tags, attributes, etc.) thanks to the XSD, there is still no static analysis of PHP items here too (e.g. recognize if you type a class name that doesn't exist, refactoring support, autocompletion, …). The point about IDE plugins made in the YAML section still stands.

PHP

By "PHP" I mean the PHP API of the Container class, for example as a reminder:

$container = new ContainerBuilder();
$container
    ->register('mailer', 'Mailer')
    ->addArgument('sendmail');

This format brings stricter validation and IDE support (autocompletion of the $container methods for example, but also static analysis of PHP classes mentioned in the config), but:

  • the YAML and the XML format are declarative formats (you declare definitions and let the container read them), the PHP API is an imperative format (because the code is executed and manually registers services one after the other)
    • this is inconsistent with YAML and XML
    • this is not configuration, this is code: configuration is "data" and should declared in a declarative way - the reason is mostly related to DX and is detailed below
  • the container's API is optimized for writing code: method names are explicit and follow classic standards (getters, setters, hassers, …) => it's not optimized for declaring configuration, it's too verbose

Take for example this piece of config:

$container
    ->register('mailer', Mailer::class)
    ->addArgument('%mailer.transport%')
    ->addMethodCall('setLogger', [new Reference('logger')]);

These are the tokens we see:

  • $container: this word is noise, we are in the config so we know we are configuring the container
  • register: noise too, the goal of the config is to register stuff, no need to state the obvious
  • 'mailer', the service name, is not very visible
  • addArgument, addMethodCall: when configuring a service we don't really want to "add" arguments or method calls, we want to define them, the word add is noise.
  • new Reference(...): this is actually a good thing: an explicit PHP object for representing a reference, instead of using a string convention like @logger in YAML. It's a bit verbose though

Here is the equivalent config in YAML:

services:
    mailer:
        class: Mailer
        arguments: ['%mailer.transport%']
        calls:
            - [setLogger, ['@logger']]

YAML has a lot of problems (no need to list them again), but it gets some things right:

  • less "noise" words
  • the service name is more visible (as array key)
  • addArgument and addMethodCall are now declarative sections: arguments, calls -> just as explicit but also much shorter and clearer to read

The format

Learning from the pros and cons of all those formats, here is how I would sum it up:

  • needs to be in PHP
    • we are PHP developers, we know how to write PHP code and we have all the tooling we need that surrounds it
    • can embbed its own documentation (through phpdoc)
    • more powerful than YAML since you can write an infinite number of helpers (functions, methods, classes, …) whereas YAML is textual data and can only be expanded through weird conventions and use of special characters
    • less verbose than it used to be since PHP 5.4 (short arrays), PHP 5.5 (::class allowing to use imported class names)
  • needs to exploit as much as possible strict typing and static validation to reduce ambiguity or mistakes:
    • rely as few as possible on strings (or magic methods): use explicit methods as much as possible (for validation, autocompletion, …)
    • encourage using ::class to reference class names (means refactoring in most IDEs)
  • needs to have a declarative syntax as simple and explicit as possible

What I mean by that is that this kind of format, for example, is NOT a good solution:

return [
    'mailer' => [
        'class' => Mailer::class,
        'arguments' => ['@logger'],
    ],
];

This is a clone of YAML in PHP and it suffers from the same downsides. arguments could be mistyped, @logger is still a magical string, etc.

Proof of concept

With the help of others, I've played with a possible solution in https://github.com/mnapoli/fluent-symfony

The main idea behind that is to build definitions through a fluent API, and declare them at the same time in an array.

return [
    'mailer' => create(Mailer::class)
        ->arguments('Hello !'),
];

You will also notice the create() function, which is kind of unusual. This is a helper function that returns an object. Its role is to avoid this complexity:

return [
    'mailer' => (new ServiceDefinition(Mailer::class))
        ->arguments('Hello !'),
];

create() is simpler, shorter, and much more explicit. It says an instance of Mailer will be created for the mailer service: simple.

Here is another example with get(), which replaces the magic @ to inject another service:

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

Functions are usually viewed as "bad" because they are global: those are helper functions, they do not need to conform to OOP best practices. The only problem with those functions is to namespace them correctly to avoid conflicts with other libraries. Thanks to PHP 5.6 it's not an issue as they can be imported like classes:

use function Symfony\DependencyInjection\create;

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

Illustrations

A picture is worth a thousand words:

  • auto-completion on classes or constants:

  • auto-completion when writing configuration:

  • real time validation in IDEs:

  • constant support:

More examples

I will not list all syntax options, you can read all of them here: https://github.com/mnapoli/fluent-symfony#syntax

Below is a compilation of most common use cases, hopefully they illustrate how nice such a format could be in terms of DX:

return [
  
    // parameters can be defined as raw values in the same array, at the root
    'db_host' => 'localhost',
    'db_port' => 3306,
  
    // service ID == class name
    Mailer::class => create(),
  
    // autowiring
    Mailer::class => autowire(),
  
 
8000
   Mailer::class => create()
        ->arguments(get('logger')) // inject another service with the get() helper
        ->method('setHost', 'smtp.google.com'),
  
    // factory
    Mailer::class => factory([MailerFactory::class, 'create'])
        ->arguments('foo', 'bar'),
  
    // aliases
    'app.mailer' => alias('app.phpmailer'),
  
    // environment values
    'db_password' => env('DB_PASSWORD'),
  
    // tags
    Mailer::class => create()
        ->tag('foo', ['fizz' => 'buzz'])
        ->tag('bar'),
  
    // import another config file
    import('services/mailer.php'),
  
    // define extensions
    extension('framework', [
        'http_method_override' => true,
        'trusted_proxies' => ['192.0.0.1', '10.0.0.0/8'],
    ]),
  
];

We could even go further for extensions and define helpers that provide an object-oriented API to configure them. For example with the framework extension:

return [
    framework()
        ->enableHttpMethodOverride()
        ->trustedProxies('192.0.0.1', '10.0.0.0/8'),
];

But then comes nested entries which may require more thinking :)

Everything at the root

You may notice that services, parameters and extensions are all mixed up at the root of the array. I did it just because it was possible. The function helpers make it possible to mix everything, and I found it made everything simpler.

It's of course just one of the many possibilities.

How to add a 4th format without really adding a 4th format?

A possibility is to build on top of the existing PHP format. That would avoid having to write new loaders (and this kind of hack to support existing PHP config files).

In the existing format, a configuration file expects a $container variable to be available, for example:

<?php
$container->register('mailer', 'Mailer');

We could keep that and write a helper that would "apply" an array config to the container:

<?php
use Symfony\DependencyInjection\applyConfig;

applyConfig($container, [
    'mailer' => create(Mailer::class),
]);

// Another possibility
$container->load([
    'mailer' => create(Mailer::class),
]);

Just an idea for the implementation.

Disclaimer

I would not be honest if I did not mention that most of these suggestions are based on PHP-DI :) In version 3 it supported configuration formats very similar to Symfony (YAML, XML and PHP). Starting from version 4 I dropped all these formats and instead switched to a PHP-oriented format. Since then I'm entirely convinced that that kind of format is much better in terms of developer experience. I would love to see that kind of format land in Symfony.

Conclusion

What do you think?

I want to make clear that all of these are suggestions and that all aspects are up for discussion. In other words, if you disagree with very specific syntax choices, that's fine. I'm mostly interested for now in whether or not such a big change is doable.

@linaori
Copy link
Contributor
linaori commented Apr 13, 2017

it's written in Java (obviously) which can explain why there are so few contributors yet so many users

This is why I was unable to contribute. I was also unable to find out how to test the changes in the plugin after making adjustments. There's hardly any documentation and there was some old documentation to be found in German only.

At first I was skeptical, but from a DX perspective, this will be really nice to have and find typos before compiling the container. I think this would be a very nice addition to Symfony, regardless if it would be in the core or not.

@sstok
Copy link
Contributor
sstok commented Apr 13, 2017

requires using the expression language for doing basic logic that could be done in PHP: "@=service('mailer_configuration').getMailerMethod()"

Note that the container is compiled and dumped to PHP, which is why expressions were created. The main problem all other projects struggle with is performance, which Symfony solved by compiling down to a single optimized container class.

The usage of namespaced functions looks really nice and clean, I always use XML because of the mentioned problems of YAML but XML is no silver bullet either. I only use the PHP registering version when creating a registration factory.

One concern I have (which is just a thought), is XML/YAML used be anyone because they can't use PHP because of security reasons (analyzing rather then executing)?

8000

@linaori
Copy link
Contributor
linaori commented Apr 13, 2017

The reasons I use yaml:

  • Consistency with the rest (not sure if this remains in 4.0)
  • Easier to write than php
  • Because it was recommended when we started Symfony (and it stayed like that)

@Pierstoval
Copy link
Contributor

@iltar

  • Consistency with the rest (not sure if this remains in 4.0)

It should, else this will be a HUGE bc break preventing LOTS of projects to migrate 😆

@ro0NL
Copy link
Contributor
ro0NL commented Apr 13, 2017

👍 it may even replace all current loaders. (not YAML for extension configs though).

Agree with @iltar, YAML has its charm.. but it's also becoming more and more complex compared to how it started. Should we really continue with that approach?

About the format.. what about avoiding using array keys at all;

return [
  param('key', 'value'),

  service(Mailer::class)->autowire(),

  extension('framework', ['key' => 'value']),
];

I think using arrays for extension config is just fine, this is what Component\Config is all about.

@stof
Copy link
Member
stof commented Apr 13, 2017

For the bundle semantic config, keeping the array is fine, as we just need to build an array anyway (this is really a datastructure in this case).

@linaori
Copy link
Contributor
linaori commented Apr 13, 2017

Consistency with the rest (not sure if this remains in 4.0)

It should, else this will be a HUGE bc break preventing LOTS of projects to migrate 😆

When Fabien presented Symfony Flex in Berlin at SymfonyCon, it used php configuration, but he also specified that he wasn't sure if he wanted to keep this. Hence I'm not sure whether or not yaml will stay consistent for other configurations.

@Pierstoval
Copy link
Contributor

Just to add my 2cents:

Everything at the root

At first sight, I tended to think it was a pretty hard thing to keep in terms of readability, because one could be totally messing up the whole config just because everything is at the root (and then, no indentation, only a list of stuff to register).

But in the end, I find this very useful in technical terms: just analyze each entry of the array. Definition=>service, resource=>imported file, scalar/array => parameter, etc., so it's easier for the container to get everything just by adding instanceof or is_string or any "isser" to know what to do for each element in the config file.

So 👍 for this :)


We could even go further for extensions and define helpers that provide an object-oriented API to configure them. For example with the framework extension:

return [
    framework()
        ->enableHttpMethodOverride()
        ->trustedProxies('192.0.0.1', '10.0.0.0/8'),
];

Not sure it's a good option, because it complexifies a lot the implementation of this feature. "framework" would need to be an available function returning an object that would check ALL methods to exist in the configuration definition, etc.
Too much stuff on this, I don't think we should complexify the codebase just for a "prettier" API like this

I'm 👎 on this part


How to add a 4th format without really adding a 4th format?

THIS is the place where your proposal makes sense to me: keep current format AND add helpers, I would be totally on it!

Simply import functions and do the stuff.

Best part of this RFC to me 👍

For the use function part, if you think it's a bit cumbersome to add a lot of use function (because you can't "use" multiples functions like we do with relative namespaces), you could add use Symfony\DependencyInjection\ConfigHelpers, and it would have this:

namespace Symfony\DependencyInjection;
{
    final class ConfigHelpers {}
}

function parameter();

function service();

function extension();

Maybe it sounds like a dirty hack, but actually this could work and make things a bit easier.

We could even imagine to register these functions in the ConfigHelpers class itself as static calls, and register global functions afterwards, so we could even use ConfigHelpers::service() functions (but still, it's just an idea that would double the logic if we already have global functions, but I really like this approach because it contains more logic and is more likely to be aliased if people don't like the ConfigHelpers name for example).

@stof
Copy link
Member
stof commented Apr 13, 2017

or just put namespace Symfony\DependencyInjection at the top of your file, and you don't need to use functions anymore, as they are in the current namespace

@stof
Copy link
Member
stof commented Apr 13, 2017

When Fabien presented Symfony Flex in Berlin at SymfonyCon, it used php configuration, but he also specified that he wasn't sure if he wanted to keep this. Hence I'm not sure whether or not yaml will stay consistent for other configurations.

Flex allows to use any format supported by Symfony. And actually, the default format for bundle semantic config is still YAML (and will probably stay this way), and the file containing service definitions is also in YAML currently.

@Pierstoval
Copy link
Contributor

@stof:

or just put namespace Symfony\DependencyInjection at the top of your file, and you don't need to use functions anymore, as they are in the current namespace

If we do this, the functions be declared only in the Symfony\DependencyInjection namespace then, forcing us to use them with

use function Symfony\DependencyInjection\parameter;
use function Symfony\DependencyInjection\service;
use function Symfony\DependencyInjection\extension;

because the configuration file is using the global namespace?

@stof
Copy link
Member
stof commented Apr 13, 2017

@Pierstoval My comment is precisely telling you that your configuration file does not need to be in the global namespace.

Symfony functions MUST be namespaced if we add them. This is not an option to put them in the global namespace (otherwise, we must name them symfony_di_service and so on instead).

@Pierstoval
Copy link
Contributor

Oh yeah I didn't see things like that, I didn't notice the fact that you were talking about the namespace keyword and not use one, that seems pretty straightforward to include everything, as long as the autoloader allows it :)

@stof
Copy link
Member
stof commented Apr 13, 2017

autoloader has nothing to do with it.

@sstok
Copy link
Contributor
sstok commented Apr 13, 2017

About the functions importing: http://php.net/manual/en/language.namespaces.importing.php#language.namespaces.importing.group (PHP 7 🤘)

use function some\namespace\{fn_a, fn_b, fn_c};

@linaori
Copy link
Contributor
linaori commented Apr 13, 2017

I rather void using that notation ever. I don't mind the imports, my IDE will manage them for me.

@stof
Copy link
Member
stof commented Apr 13, 2017

@iltar you are free to use any of the alternatives:

  • config file in the same namespace than functions
  • a single grouped use statement
  • separate use statements

These have no impact on the feature, as the feature would not parse the source of the code anyway, but ask PHP to execute the code (and then, PHP supports all 3 alternatives)

@ScreamingDev
Copy link

Does this idea have the same downsides as listed for XML? I think so.

  • Clutter around what is really of interest.
  • Hard to type without IDE
  • Same autocompletion issues

Anyways this could be a nice alternative. I won't use it but I am sure people that are new to PHP, have an IDE and dislike yml will like it. As a rookie I always thought this is already possible using the DependencyInjection\ContainerBuilder.

@robfrawley
Copy link
Contributor

Very interesting idea. Do you have a basic proof of concept or any initial implementation or any code, really, you can share?

@Pierstoval
Copy link
Contributor

@robfrawley:

Very interesting idea. Do you have a basic proof of concept or any initial implementation or any code, really, you can share?

@mnapoli posted a link: https://github.com/mnapoli/fluent-symfony

@Taluu
Copy link
Contributor
Taluu commented Apr 14, 2017

It looks good, but how could we support the defaults and instanceof configs (specially the defaults, like setting private by default, ... etc) ? A call before returning the array ?

<?php
defaults()
 ->private();

return [
    //...
];

And in the loader, before loading set files, it sets the real defaults (public by default, etc)

@stof
Copy link
Member
stof commented Apr 14, 2017

@Taluu IMO, they should be entries in the array as well, like others (but the internal representation would use different value objects, allowing to distinguish them).

The current implementation of these functions are just builders for value objects, which are then processed by the loader. So anything which is not part of the returned array is not available for the loader.
Your proposal would require to make defaults() alter a global variable, which would be quite bad.

@Spomky
Copy link
Contributor
Spomky commented Apr 16, 2017

Hi there,

I am so happy to see that you've opened that issue.
I use it (intensively) in one of my projects and I am really happy with it.

Hoping it will be widely accepted by the Symfony community.

@jfdion
Copy link
jfdion commented Jul 24, 2017

Isn't it what Pimple does in the Silex framework, an all code DI for all your services? The framework has been developped primarly by Fabien and so does Pimple.

From my experience it worked quite well in the few projects I tried it.

@nicolas-grekas
Copy link
Member

see #23834

nicolas-grekas added a commit that referenced this issue Sep 20, 2017
…iner (nicolas-grekas)

This PR was merged into the 3.4 branch.

Discussion
----------

[DI] Add "PHP fluent format" for configuring the container

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #22407
| License       | MIT
| Doc PR        | -

This PR allows one to write DI configuration using just PHP, with full IDE auto-completion.
Example:
```php

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Foo;

return function (ContainerConfigurator $c) {

    $c->import('basic.php');

    $s = $c->services()->defaults()
        ->public()
        ->private()
        ->autoconfigure()
        ->autowire()
        ->tag('t', array('a' => 'b'))
        ->bind(Foo::class, ref('bar'))
        ->private();

    $s->set(Foo::class)->args([ref('bar')])->public();
    $s->set('bar', Foo::class)->call('setFoo')->autoconfigure(false);

};
```

Commits
-------

814cc31 [DI] Add "PHP fluent format" for configuring the container
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

0