Skip to content

Add symfony/contracts: a set of abstractions extracted out of the Symfony components #27093

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 1 commit into from
Jul 13, 2018

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Apr 30, 2018

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

A set of abstractions extracted out of the Symfony components.

This is a topic I've been thinking about for a long time. I feel like the time has come for Symfony to publish some abstractions so that people could build on them in a decoupled way.
I've identified interfaces in some components that would greatly benefit from being moved out from the components where they are for now. E.g. #26929 is something that has a broader scope than the Cache component itself.

By putting them in a new symfony/abstractions package, we would allow more innovation in the Symfony community, at the abstraction level.

In order to start small, I propose only one interface that gathers a concept that is shared amongst many components already: ResetInterface. It would provide a standard reset() method, whose purpose is to set an object back to its initial state, allowing it to be reused many times with no side effects/leaks related to its history. By this definition, it could also be autoconfigured (as done here, see update in FrameworkExtension). See wording in the docblock in the attached source code.

Ideally, I'd like this package to provide not only interfaces, by also generic traits, and reference test suites when possible. We could work on adding more abstractions during the 4.2 cycle. WDYT?

Here is the attached README:

A set of abstractions extracted out of the Symfony components.

Can be used to build on semantics that the Symfony components proved useful - and
that already have battle tested implementations.

Design Principles

  • contracts are split by domain, each into their own sub-namespaces;
  • contracts are small and consistent sets of PHP interfaces, traits, normative
    docblocks and reference test suites when applicable, etc.;
  • all contracts must have a proven implementation to enter this repository;
  • they must be backward compatible with existing Symfony components.

FAQ

How to use this package?

The abstractions in this package are useful to achieve loose coupling and
interoperability. By using the provided interfaces as type hints, you are able
to reuse any implementations that match their contracts. It could be a Symfony
component, or another one provided by the PHP community at large.

Depending on their semantics, some interfaces can be combined with autowiring to
seamlessly inject a service in your classes.

Others might be useful as labeling interfaces, to hint about a specific behavior
that could be enabled when using autoconfiguration or manual service tagging (or
any other means provided by your framework.)

How is this different from PHP-FIG's PSRs?

When applicable, the provided contracts are built on top of PHP-FIG's PSRs. But
the group has different goals and different processes. Here, we're focusing on
providing abstractions that are useful on their own while still compatible with
implementations provided by Symfony. Although not the main target, we hope that
the declared contracts will directly or indirectly contribute to the PHP-FIG.

Why isn't this package split into several packages?

Putting all interfaces in one package eases discoverability and dependency
management. Instead of dealing with a myriad of small packages and the
corresponding matrix of versions, you just need to deal with one package and one
version. Also when using IDE autocompletion or just reading the source code, it
makes it easier to figure out which contracts are provided.

There are two downsides to this approach: you may have unused files in your
vendor/ directory, and in the future, it will be impossible to use two
different sub-namespaces in different major versions of the package. For the
"unused files" downside, it has no practical consequences: their file sizes are
very small, and there is no performance overhead at all since they are never
loaded. For major versions, this package follows the Symfony BC + deprecation
policies, with an additional restriction to never remove deprecated interfaces.

Resources

@nicolas-grekas nicolas-grekas added this to the next milestone Apr 30, 2018
@sstok
Copy link
Contributor

sstok commented Apr 30, 2018

This would only work if the used terms have an equal purpose and meaning, Resettable has a different purpose in Cache than OutputFormatterStyleStack or Compiler in the Expression-Language.

Basically this would make the interfaces extreme generic in purpose and less about their specific situation. A Trait can be easily re-used as it's simple code-level copy-paste but doesn't enforce an interface contract.

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Apr 30, 2018

This would only work if the used terms have an equal purpose and meaning

Correct, and that's actually the exact purpose of the proposed interface. I don't know why you state that they have a different purpose in your list, because AFAIK, they have the exact same one: resetting an object to its initial state. This is even more apparent when considering that all (most) these existing "reset" methods are bound to "kernel.reset" tags.

Copy link
Member

@lyrixx lyrixx left a comment

Choose a reason for hiding this comment

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

I like it 👍
(minor comments / questions)

@nicolas-grekas nicolas-grekas force-pushed the abstraction branch 6 times, most recently from 69525e5 to 1446d14 Compare April 30, 2018 18:42
@derrabus
Copy link
Member

Maybe we should use the discussion of PR to assemble a list of classes, interfaces and traits that could possibly be moved into the abstractions package, to get an idea of where we would be heading to with this idea.

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Apr 30, 2018

@derrabus why not. Here is my short list (I would really try hard to not add any classes here, even abstract ones):

There might be more abstractions that we could extract from other components (Console, EventDispatcher, Lock, Messenger, Routing, Translation, HttpKernel, etc.?)

@derrabus
Copy link
Member

derrabus commented May 1, 2018

I really like the idea of separating abstractions from implementations.

I would really try hard to not add any classes here

Regarding EventDispatcher, a (dead-simple) class would be among the candidates for me:

  • EventDispatcherInterface
  • EventSubscriberInterface
  • Event

Event could be split into an interface + a trait, but I don't see much benefit here.

Moving those to interfaces into an abstraction package would allow components to define optional event subscribers without depending on the actual event dispatcher. We could also simplify code like this.

@ostrolucky
Copy link
Contributor

This PR is weird, seeing #6129 was voted down on. What has changed?

@nicolas-grekas
Copy link
Member Author

Thanks for the link @ostrolucky. About your question, re-reading my own downvote, I then stated:

Symfony didn't embrace the mission of providing generic interfaces on their own

that's exactly what I'm proposing here: have the Symfony project broaden its missions a bit, and allow itself to publish interfaces on their own for genericity. The benefits are well discussed and presented in the issue. Why did I change my mind? Because I feel like not having this package is preventing us from some kind of innovation we'd like to do. E.g. taggable cache items, this linked cache interface, the attached reset method, etc. It comes also as a realization that the FIG is not the right place for all of these: I don't expect everyone in the FIG to agree on all things we'd still like to do. And this is fine, we don't have the exact same goals obviously. We need our own freedom on the "abstractions" topic, so that we're able to contribute to this space also. Some topics could be of interest to the FIG (e.g. taggable cache items), still that shouldn't prevent us from being able to move forward at our own pace.

@jakzal
Copy link
Contributor

jakzal commented May 2, 2018

I like the idea of creating standards by extracting or defining interfaces out of established and working solutions. This is what PSRs are missing in many cases.

However, how do you see the symfony/abstractions package to be consumed by others?

My main issue with this idea is that the package wouldn't be cohesive, as a lot of unrelated interfaces would be packaged together. If I only need the ResettableInterface I shouldn't be forced to bring in the ServiceSubscriberInterface.

@javiereguiluz
Copy link
Member

@nicolas-grekas to better understand the purpose (and limits) of this new package, which of the following packages fits the idea of symfony/abstractions better?

Thanks!

@nicolas-grekas
Copy link
Member Author

a lot of unrelated interfaces would be packaged together

That's a desired property of the package IMHO: it will ease discoverability and dependency maintenance (there are some comments in #6129 on the topic). We might reconsider later if the number of interfaces grows out of control, but I don't expect this to happen anytime soon. Meanwhile, we could/should group domains in subnamespaces, when cohesiveness is desired (not the case for ResettableInterface, but could be for the mentioned Cache interfaces.) The fact that you get potentially unused interfaces poses no issues IMHO.

@javiereguiluz certainly not like doctrine/common, which is not an abstraction at all. What I presented above: this would contain interfaces (=abstractions), related documentation defining their semantics (in docblocks), generic traits (=type-less behaviors) and reference test suites when applicable (of primary importance, see eg https://github.com/php-cache/integration-tests/ for what I mean).

@nicolas-grekas
Copy link
Member Author

ping @symfony/deciders

@weaverryan
Copy link
Member

This seems like a simple optimization: by moving some interfaces to this component, the only practical difference is that you're allowing a library that wants to use that interface (but not use our implementation) to depend on the smaller abstractions component instead of needed to require the entire component (but then only use the interface). A real-world example would is http-kernel, which is used in several libraries that don't use its implementation (e.g. Laravel who only references HttpKernelInterface & HttpExceptionInterface ).

Are there any big downsides?

* process loop (note that we advise making your services stateless instead of
* implementing this interface when possible.)
*/
interface ResettableInterface
Copy link
Member

Choose a reason for hiding this comment

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

In the Symfony world, we would say ResetInterface (I don't think we have any -able interface right now in Symfony). In the ZF world, it would be Resettable. I propose to rename to ResetInterface here for consistency with our practices. Any better ideas?

Copy link
Contributor

Choose a reason for hiding this comment

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

@fabpot Symfony\Component\HttpKernel\RebootableInterface, Symfony\Component\HttpKernel\TerminableInterface, Symfony\Component\Cache\PruneableInterface, Symfony\Component\Cache\ResettableInterface...

A set of abstractions extracted out of the Symfony components.

Can be used to build on semantics that the Symfony components proved useful - and
that already have neat implementations if you don't mind.
Copy link
Member

Choose a reason for hiding this comment

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

I would remove neat.
if you don't mind -> can be removed.

Design Principles
-----------------

* contracts are split by domain, each into their own sub-namespace;
Copy link
Member

Choose a reason for hiding this comment

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

namespaces

### How to use this package?

The abstractions in this package are useful to achieve loose coupling and
interoperability. By using the provided interfaces as type hints, you will
Copy link
Member

Choose a reason for hiding this comment

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

you are able

interoperability. By using the provided interfaces as type hints, you will
be able to reuse any implementations that match their contracts. It could be a
Symfony component, or another one provided by the PHP community at large
(or by you.)
Copy link
Member

Choose a reason for hiding this comment

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

I would remove this last part.

encourage relying on them and won't duplicate the effort. Still, the FIG has
different goals and different processes. Here, we don't need to seek universal
standards. Instead, we're providing abstractions that are compatible with the
implementations provided by Symfony components. This should actually also
Copy link
Member

Choose a reason for hiding this comment

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

by Symfony (components can be removed as we are talking about the project here)

different goals and different processes. Here, we don't need to seek universal
standards. Instead, we're providing abstractions that are compatible with the
implementations provided by Symfony components. This should actually also
contribute positively to the FIG (from which Symfony is a member), by hinting the
Copy link
Member

Choose a reason for hiding this comment

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

PHP-FIG

@fabpot
Copy link
Member

fabpot commented Jul 9, 2018

The contracts should have a different versioning than Symfony. As we won't break BC for instance, we just have to increase the minor version. Having a 5.0 version next years does not really make sense. So, here is the plan: we keep the code under symfony/symfony to make it easier to move interfaces from the actual code to the contracts ones, but the release cycle is going to be different and done directly via the subtree-split.

@nicolas-grekas nicolas-grekas force-pushed the abstraction branch 4 times, most recently from 02f8c05 to b90951a Compare July 9, 2018 16:21
@nicolas-grekas
Copy link
Member Author

Here we are, the interface is now ResetInterface. PR green.

we keep the code under symfony/symfony to make it easier to move interfaces from the actual code to the contracts ones, but the release cycle is going to be different and done directly via the subtree-split.

Makes perfect sense to me, everything ready to put it into practice :)
I've updated the branch-alias and the composer.json to reference v1.0.

@derrabus
Copy link
Member

derrabus commented Jul 9, 2018

The contracts should have a different versioning than Symfony.

I absolutely agree.

the release cycle is going to be different and done directly via the subtree-split

I see how keeping the contracts inside the monorepo would ease the extraction of more interfaces, but I must admit that the detached versioning confuses me already, thinking about branching tagging of releases. A separate repository would be easier to maintain and understand in the long run, I guess.

How about this: We could start with the contracts in the monorepo to ease the extraction for now and right before the feature freeze of Symfony 4.2 (which would be 1.0 of the contracts), the subtree split becomes an independant repository like the polyfills or the monolog bundle.

@nicolas-grekas
Copy link
Member Author

@derrabus we'll have some time to figure out what works best. For now, we anticipate that the easiest to maintain could be a subtree-splitted repository.

Status: needs review

@fabpot
Copy link
Member

fabpot commented Jul 13, 2018

Thank you @nicolas-grekas.

@fabpot fabpot merged commit 8982036 into symfony:master Jul 13, 2018
fabpot added a commit that referenced this pull request Jul 13, 2018
… out of the Symfony components (nicolas-grekas)

This PR was merged into the 4.2-dev branch.

Discussion
----------

Add symfony/contracts: a set of abstractions extracted out of the Symfony components

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

A set of abstractions extracted out of the Symfony components.

This is a topic I've been thinking about for a long time. I feel like the time has come for Symfony to publish some abstractions so that people could build on them in a decoupled way.
I've identified interfaces in some components that would greatly benefit from being moved out from the components where they are for now. E.g. #26929 is something that has a broader scope than the Cache component itself.

By putting them in a new `symfony/abstractions` package, we would allow more innovation in the Symfony community, at the abstraction level.

In order to start small, I propose only one interface that gathers a concept that is shared amongst many components already: `ResetInterface`. It would provide a standard `reset()` method, whose purpose is to set an object back to its initial state, allowing it to be reused many times with no side effects/leaks related to its history. By this definition, it could also be autoconfigured (as done here, see update in FrameworkExtension). See wording in the docblock in the attached source code.

Ideally, I'd like this package to provide not only interfaces, by also generic traits, and reference test suites when possible. We could work on adding more abstractions during the 4.2 cycle. WDYT?

## Here is the attached README:

A set of abstractions extracted out of the Symfony components.

Can be used to build on semantics that the Symfony components proved useful - and
that already have battle tested implementations.

Design Principles
-----------------

 * contracts are split by domain, each into their own sub-namespaces;
 * contracts are small and consistent sets of PHP interfaces, traits, normative
   docblocks and reference test suites when applicable, etc.;
 * all contracts must have a proven implementation to enter this repository;
 * they must be backward compatible with existing Symfony components.

FAQ
---

### How to use this package?

The abstractions in this package are useful to achieve loose coupling and
interoperability. By using the provided interfaces as type hints, you are able
to reuse any implementations that match their contracts. It could be a Symfony
component, or another one provided by the PHP community at large.

Depending on their semantics, some interfaces can be combined with autowiring to
seamlessly inject a service in your classes.

Others might be useful as labeling interfaces, to hint about a specific behavior
that could be enabled when using autoconfiguration or manual service tagging (or
any other means provided by your framework.)

### How is this different from PHP-FIG's PSRs?

When applicable, the provided contracts are built on top of PHP-FIG's PSR. We
encourage relying on them and won't duplicate the effort. Still, the FIG has
different goals and different processes. Here, we don't need to seek universal
standards. Instead, we're providing abstractions that are compatible with the
implementations provided by Symfony. This should actually also contribute
positively to the PHP-FIG (from which Symfony is a member), by hinting the group
at some abstractions the PHP world might like to take inspiration from.

### Why isn't this package split into several packages?

Putting all interfaces in one package eases discoverability and dependency
management. Instead of dealing with a myriad of small packages and the
corresponding matrix of versions, you just need to deal with one package and one
version. Also when using IDE autocompletion or just reading the source code, it
makes it easier to figure out which contracts are provided.

There are two downsides to this approach: you may have unused files in your
`vendor/` directory, and in the future, it will be impossible to use two
different sub-namespaces in different major versions of the package. For the
"unused files" downside, it has no practical consequences: their file sizes are
very small, and there is no performance overhead at all since they are never
loaded. For major versions, this package follows the Symfony BC + deprecation
policies, with an additional restriction to never remove deprecated interfaces.

Resources
---------

  * [Documentation](https://symfony.com/doc/current/components/contracts.html)
  * [Contributing](https://symfony.com/doc/current/contributing/index.html)
  * [Report issues](https://github.com/symfony/symfony/issues) and
    [send Pull Requests](https://github.com/symfony/symfony/pulls)
    in the [main Symfony repository](https://github.com/symfony/symfony)

Commits
-------

8982036 Added symfony/contracts: a set of abstractions extracted out of the components
@nicolas-grekas nicolas-grekas deleted the abstraction branch July 13, 2018 13:27
@nicolas-grekas
Copy link
Member Author

\o/ thank you all for the discussion, to the core team for their support and to fabpot for merging.
There are more interfaces to move in contracts before releasing 1.0 in November.
PRs welcome!

@derrabus
Copy link
Member

There are more interfaces to move in contracts before releasing 1.0 in November.
PRs welcome!

I'd love to help. I would start with the event interfaces as I suggested in my comment: #27093 (comment)

Do you track somewhere what needs to be done and who's already working on which part?

@nicolas-grekas
Copy link
Member Author

I just created https://github.com/symfony/symfony/projects/7 so we can track ideas and progress.
Please open issues/PRs and add them to the project (if permissions don't allow it, @symfony/deciders can do it.)

@nicolas-grekas nicolas-grekas modified the milestones: next, 4.2 Nov 1, 2018
This was referenced Nov 3, 2018
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.