Skip to content

[DI] Add "inherit-tags" with configurable defaults + same for "public", "tags" & "autowire" #21071

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

Merged
merged 3 commits into from
Jan 7, 2017

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Dec 27, 2016

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

Instead of making services private by default (#20048), which is going to create a big migration burden that might not be worth it, I'd like to propose the idea of changing the default for the current file.

Having a place to redefine some defaults, this can also be used to enable autowiring for a file, making it much lighter in the end.

This PR handles defaults for "public", "autowired" and "tags". Not sure the other options need a configurable default. Please comment if you think otherwise.

Short example (the feat is more interesting with a longer list of services):

services:
    _defaults:
        public: false
        autowire: ['_construct', 'set*']

    foo:
        class: Foo
<?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>
        <defaults public="false">
            <autowire>__construct</autowire>
            <tag name="foo"/>
        </defaults>
    </services>
</container>

ping @dunglas @weaverryan

@ogizanagi
Copy link
Contributor

ogizanagi commented Dec 27, 2016

Looks like a good idea to me. What about aliases, though?
Should we provide a way to let them public by default while services will be private?

service_defaults:
    public: ['aliases'] # aliases are public whereas services are private

service_defaults:
    public: true # default: aliases and services are public <=> ['aliases', 'services']

service_defaults:
    public: false # aliases and services are private <=> []

(It'll need a special treatment for the service_defaults.public key, but as we won't generalize the service_defaults for every definition keys, I'd rather see this as a loader_config key than a "parent definition" one)

@linaori
Copy link
Contributor

linaori commented Dec 27, 2016

What about adding tags like this? I often find myself with a specific service definition file that contains a specific type of services. Think of a file for form types, they all have the same tag. It would be really nice if I could use this feature to automatically tag all definitions in that file by this implicit parent definition.

@GuilhemN
Copy link
Contributor

👍, you should add this new section here or the ContainerBuilder will throw an exception.

@nicolas-grekas nicolas-grekas force-pushed the di-pragma branch 2 times, most recently from ac66912 to cd74550 Compare December 27, 2016 18:40
@javiereguiluz
Copy link
Member

Minor comment: could we please take some time to think about this proposal before merging it? This kind of changes can have a deep impact in how developers use Symfony. Personally I don't have an opinion about the idea yet ... but I don't like the implementation. It doesn't feel "natural" for Symfony.

@ogizanagi
Copy link
Contributor

ogizanagi commented Dec 27, 2016

As soon as it doesn't behaves blindly as a pseudo parent definition, but rather as a context/configuration exploitable by the loaders, it looks good to me. Hence, I don't really like the service_defaults naming, but rather see something like loader_config/context or whatever.

@@ -146,9 +146,10 @@ private function parseDefinitions($content, $file)
if (!is_array($content['services'])) {
throw new InvalidArgumentException(sprintf('The "services" key should contain an array in %s. Check your YAML syntax.', $file));
}
$defaults = isset($content['service_defaults']) ? $content['service_defaults'] : array();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think unsupported keys should throw an exception to be consistent with the rest of the loader.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Dec 27, 2016

@javiereguiluz don't worry we have a process for that, this PR is no exception.

@ogizanagi I'm 👎 on loader_config because this is just leaking internal vocabulary that users don't necessarily know about.

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Dec 27, 2016

PR completed for YamlLoader. If someone would like to work on XmlLoader, I'd happily accept a PR against my branch!

Copy link
Contributor

@ro0NL ro0NL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -176,7 +188,7 @@ private function parseDefinition($id, $service, $file)
static::checkDefinition($id, $service, $file);

if (isset($service['alias'])) {
$public = !array_key_exists('public', $service) || (bool) $service['public'];
$public = array_key_exists('public', $service) ? (bool) $service['public'] : !empty($defaults['public']);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to define this variable twice? One doing array_key_exists other isset?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are not on the same path, and don't have the same semantic (don't forget about ChildDefinition on the other path.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had services defined: (Symfony 3.1.8):

foo:
    class: My\FooService

bar:
    alias: foo

I could access bar service in my controller because it was public by default. After this change here my aliased service is private by default, thus the service is not accessible in controller.

Shouldn't this be mentioned somewhere? I couldn't find any info about it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. That's a bug. See #21219

throw new InvalidArgumentException(sprintf('Service defaults must be an array, "%s" given in "%s".', gettype($defaults), $file));
}
foreach ($defaults as $key => $default) {
if (!in_array($key, $defaultKeys)) {
Copy link
Contributor

@ro0NL ro0NL Dec 27, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps go strict here?

Copy link
Member Author

@nicolas-grekas nicolas-grekas Dec 27, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done this way for consistency with the many other in_array in this file, good enough anyway

@dunglas
Copy link
Member

dunglas commented Dec 28, 2016

I like the idea!

What about using just defaults as key? service_defaults is a bit redundant with service:

defaults:
    public: false
    autowire: ['_construct', 'set*']

services:
    foo:
        class: Foo

The best looking config is:

services:
   defaults:
        public: false
        autowire: ['_construct', 'set*']

    'Foo\Bar': {}

But I'm not sure that it's ok to deprecate some services names like default for this example.

@ro0NL
Copy link
Contributor

ro0NL commented Dec 28, 2016

The best looking config is:

Really? The one above looks more intuitive to me...

defaults: #...
services: #...

that is.

Looking at it like this looks good... then again; if parameters are involved (parameters: #...) it may work counter-intuitive.

Meaning either service_defaults (:+1:), or indeed service.defaults.

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Dec 28, 2016

The root namespace is used for container extensions configuration, that means there is a potential for collision with an existing bundle that uses this "defaults" name.
This leads to another reason pro-service_defaults: the defaults we're talking about are related to services only.
If you have eg:

framework: #...
services: #...
defaults: #...

it's not intuitive at all that defaults applies only to services.
That's why I agree with @dunglas that defaults inside services is the best of the two proposals.
But is it worth the potential collision?

@nicolas-grekas
Copy link
Member Author

What about adding an underscore in this way?

services:
    _defaults:
        #...

@ro0NL
Copy link
Contributor

ro0NL commented Dec 28, 2016

Perhaps * works out as well? It looks nicer then _defaults at least :) and seems to fit the case perfectly?

@nicolas-grekas
Copy link
Member Author

I doubt * would be friendly to anyone not familiar with what it does.

@ro0NL
Copy link
Contributor

ro0NL commented Dec 28, 2016

Think you're right.. just typed it out to visualize in fact. It looks good (imo.) but indeed.. you need to know it.

service._defaults 👍

@GuilhemN
Copy link
Contributor

What about using something that sounds more specific to yaml such as !defaults (as a key of services)?
It would also decrease the risk of conflicts.

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Dec 28, 2016

!defaults must be quoted in yaml, not very friendly. And we already use _ prefixes for special keys in eg routing yaml.

@GuilhemN
Copy link
Contributor

@nicolas-grekas I was thinking about a tag returning something similar to the JavaScript Symbol but yes custom tags aren't supported yet.
Currently, unknown tags are considered as part of the scalar (e.g. !defaults remains !defaults as it is not known) so it's possible to use them and we could later migrate to a better tags system, wdyt?

@dunglas
Copy link
Member

dunglas commented Dec 28, 2016

+1 for services._defaults. Btw we should take this occasion to deprecate any service name starting by a _.

@TomasVotruba
Copy link
Contributor

TomasVotruba commented Dec 30, 2016

Just for inspiration from @nette, there is similar tool, that decorates specific classes/types of classes:

See this article - jump to "Decorator Extension to the Rescue" headline.

And this Tweet: https://twitter.com/VotrubaT/status/812802092356276224

decorator

I'm amazed, how similar these tools are :)

@grachevko
Copy link
Contributor

What about add factory to list of _default options?
It can be useful to define doctrine repositories as service for example.

@TomasVotruba
Copy link
Contributor

@grachevko How would that look like? From example I understand it's global setup for all services.

@grachevko
Copy link
Contributor

grachevko commented Dec 31, 2016

@TomasVotruba not a global. This PR add _default options which have affect only on file where it.
We can create subscribers.yml and set _default.tags: [{ name: kernel.event_subscriber }] for all subscribers in this file without typing same tag to each.
Just the same we could create repositories.yml and set _default.factory: ['@doctrine', 'getRepository'] without typing same factory.

@TomasVotruba
Copy link
Contributor

@grachevko Ah. That's a pity. I was looking forward to get rid of Console, EventSubscriber, FormType etc. tags.

I'll work on that Decorator feature then :)

@fabpot
Copy link
Member

fabpot commented Jan 7, 2017

👍

1 similar comment
@dunglas
Copy link
Member

dunglas commented Jan 7, 2017

👍

@nicolas-grekas nicolas-grekas force-pushed the di-pragma branch 2 times, most recently from 9545899 to 88b99fb Compare January 7, 2017 16:39
@fabpot
Copy link
Member

fabpot commented Jan 7, 2017

Thank you @nicolas-grekas.

@fabpot fabpot merged commit beec1cf into symfony:master Jan 7, 2017
fabpot added a commit that referenced this pull request Jan 7, 2017
…ame for "public", "tags" & "autowire" (nicolas-grekas, ogizanagi)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DI] Add "inherit-tags" with configurable defaults + same for "public", "tags" & "autowire"

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

Instead of making services private by default (#20048), which is going to create a big migration burden that might not be worth it, I'd like to propose the idea of changing the default for the current file.

Having a place to redefine some defaults, this can also be used to enable autowiring for a file, making it much lighter in the end.

This PR handles defaults for "public", "autowired" and "tags". Not sure the other options need a configurable default. Please comment if you think otherwise.

Short example (the feat is more interesting with a longer list of services):
```yaml
services:
    _defaults:
        public: false
        autowire: ['_construct', 'set*']

    foo:
        class: Foo
```

```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">
    <services>
        <defaults public="false">
            <autowire>__construct</autowire>
            <tag name="foo"/>
        </defaults>
    </services>
</container>
```

ping @dunglas @weaverryan

Commits
-------

beec1cf [DI] Allow definitions to inherit tags from parent context
05f24d5 [DI] Add "defaults" tag to XML services configuration
7b4a18b [DI] Add "_defaults" key to Yaml services configuration
@chalasr
Copy link
Member

chalasr commented Jan 8, 2017

The new inherit-tags option would have deserved its own line in the changelog imho, it's just great! Thanks

@nicolas-grekas nicolas-grekas deleted the di-pragma branch January 9, 2017 14:01
fabpot added a commit that referenced this pull request Jan 9, 2017
…gizanagi)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DI] Fixes aliases visibility with and without defaults

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #21071 (comment)
| License       | MIT
| Doc PR        | N/A

Commits
-------

f1cc090 [DI] Fixes aliases visibility with and without defaults
fabpot added a commit that referenced this pull request Jan 23, 2017
…izanagi)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DI] Add Yaml syntax for short services definition

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | N/A
| License       | MIT
| Doc PR        | todo

In my experience, most (at least, a lot) of the service definitions in an end-application only require the class and constructor arguments.

#21133 allows to get rid of the `class` attribute by using the service id.
Which means only arguments remain for most use-cases. Hence, we could support this syntax:

#### Before:

```yml
services:
    App\Foo\Bar:
        arguments: ['@baz', 'foo', '%qux%']
```

#### After:

```yml
services:
    App\Foo\Bar: ['@baz', 'foo', '%qux%']
```

It works especially well along with services `_defaults` introduced in #21071 :

```yml
services:
    _defaults:
        public: false
        autowire: ['set*']
        tags: ['serializer.normalizer']

    App\Serializer\FooNormalizer: ['@baz', 'foo', '%qux%']
```

Commits
-------

83b599c [DI] Add Yaml syntax for short services definition
@fabpot fabpot mentioned this pull request May 1, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.