# symfony/contracts: the interface layer behind Symfony components

> A review of the abstractions Symfony extracted from its own components, who benefits from depending on them, and where the package stops short.

**symfony/contracts** — A set of abstractions extracted out of the Symfony components

- Repository: https://github.com/symfony/contracts
- Website: https://symfony.com/contracts
- Stars: 3,929 · Forks: 19
- Language: PHP
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/symfony-contracts

## What symfony/contracts actually is, and who it is for

The repository is not a framework and not a component in the usual sense. It is a set of PHP interfaces, traits and normative docblocks pulled out of the Symfony components, split by domain into their own sub-namespaces. The top-level directories listed in the repository are Cache/, Deprecation/, EventDispatcher/, HttpClient/, Service/ and Translation/, with Tests/ and a phpunit.xml.dist alongside them.

The intended user is a library author. If you write a package that needs to accept a cache backend, an HTTP client or a translator, type-hinting against Symfony's component classes forces your users onto those classes. Type-hinting against the contract interface instead lets any implementation that satisfies the same interface pass through. The README frames this as the point of the package: "By using the provided interfaces as type hints, you are able to reuse any implementations that match their contracts."

That framing also sets the boundary. This is not a package an application developer installs to get features. There is no runtime behaviour to configure, no service to boot. It is a dependency you take on when you are the one defining the seams.

## Design principles that constrain what enters the repository

Four rules in the README govern the contents, and they are stricter than they first look. Contracts are split by domain into sub-namespaces. They are small, consistent sets of interfaces, traits, docblocks and reference test suites where applicable. Every contract must have a proven implementation before it can enter the repository. And contracts must remain backward compatible with the existing Symfony components.

The third rule is the interesting one. It means the repository is not a place for speculative interface design. Someone has to have shipped a working implementation first, which is why the set tracks Symfony's own components rather than anticipating them. The fourth rule cuts the other way: because the contracts must stay compatible with the components, an interface cannot drift into a shape the components could not satisfy. Both rules push in the same direction, toward interfaces that describe what already exists rather than what might be nice.

The consequence for adopters is that contract coverage is uneven by design. Domains where Symfony has a battle-tested implementation are represented; domains where it does not are absent.

## How implementations declare themselves with the provide key

The mechanism that ties an implementation to a contract lives in Composer metadata, not in PHP. The README states that packages implementing specific contracts should list them in the "provide" section of their composer.json, following the symfony/*-implementation convention, and gives this example:

## Installing it and type-hinting a first interface

There are no install steps in the README beyond the package name, so the entry point is Composer. The repository's composer.json is the authoritative source for the package name and the PHP version constraint; neither is stated in the README, so check that file before pinning anything.

The general shape of a first use is to require the package and then type-hint against an interface from one of the sub-namespaces. Which interface you pick depends on the domain: Cache/ for caching, HttpClient/ for outbound HTTP, EventDispatcher/ for events, Translation/ for message translation, Service/ for service-container related abstractions, Deprecation/ for deprecation handling. The README does not enumerate the interfaces or their method signatures, so read the docblocks in the sub-namespace you need.

The second half of the mechanism is autowiring. The README notes that depending on their semantics, some interfaces can be combined with autowiring to inject a service into your classes. It also describes the other role interfaces can play: labeling. A labeling interface carries no methods and exists to signal that a behaviour should be enabled, through autoconfiguration or manual service tagging or whatever other means the framework provides. Two different jobs, one package.

## Where the abstraction breaks down

The most concrete limitation is stated in the package's own design rules: all contracts must be backward compatible with existing Symfony components. An interface that has to remain satisfiable by the components cannot be reshaped freely. If you were hoping to define a cleaner signature than the one Symfony ships, the contract is not the place to do it, and a pull request that breaks the rule is out of scope by definition.

The second limitation is coverage. Because every contract needs a proven implementation to enter the repository, anything Symfony has not built is not represented. A library that needs an abstraction for a domain outside Cache, Deprecation, EventDispatcher, HttpClient, Service and Translation will not find it here, and the repository layout gives no indication that the set is meant to be exhaustive.

The third is a mismatch of purpose. If your application is built on the full framework and every collaborator is a Symfony component, the interfaces add a layer of indirection with no payoff. You are paying the cost of an abstraction whose entire value is that other implementations can pass through it, and in that setup no other implementation does.

## How this differs from PHP-FIG's PSRs

The README addresses the comparison directly, and the answer is about goals and process rather than syntax. Where a PSR applies, the contracts are built on top of it. But the two groups are not trying to produce the same artifact.

The PSR process is cross-vendor and consensus-driven, which makes it slow and makes each specification a negotiated settlement. The contracts process is narrower: the stated focus is providing abstractions that are useful on their own while remaining compatible with implementations provided by Symfony. The README adds that contributing to PHP-FIG is a hope rather than the main target.

For a library author the practical difference is who guarantees the interface. A PSR is backed by the FIG membership; a Symfony contract is backed by the Symfony project and by the requirement that a component already implements it. If your consumers are Symfony users, the contract is the closer fit. If your consumers are spread across frameworks, a PSR is the safer bet, and the contracts will usually sit underneath it anyway.

## Maintenance, versioning and licence

The repository is not archived, and the last push was on 2026-09-21. The most recent releases listed are v3.7.3 on 2026-08-30, v3.7.2 on 2026-06-27 and v3.7.1 on 2026-06-27. The patch-level versioning across those three releases suggests the interfaces are treated as stable, which matches the backward-compatibility rule the README sets out.

Upgrade cost is mostly a function of that rule. Interfaces that must remain compatible with the components are unlikely to change shape between patch releases, so the practical work of upgrading is reading the CHANGELOG.md at the repository root rather than rewriting type hints. The file is present at the top level, which is where to look.

The licence is MIT. That is permissive and places few conditions on redistribution, but the LICENSE file at the root is the text that governs, and reading it is not the same as taking legal advice. If you are embedding the interfaces in a distributed product, read that file rather than this paragraph.

One structural detail worth knowing: the repository carries a splitsh.json at the top level, and issues and pull requests are directed to the main Symfony repository rather than to this one. Contributing therefore happens upstream, not here.

## Conclusion

Adopt symfony/contracts if you maintain a library or a framework layer that must accept Symfony's cache, HTTP client, event dispatcher, translation or service implementations without depending on the components themselves. Do not adopt it if you are building an application on the full framework and never type-hint against third-party code, because the interfaces buy you nothing there. Before committing, check the composer.json of each component you plan to use to confirm it declares the matching symfony/*-implementation provide entry, and read the docblocks in the specific sub-namespace you intend to type-hint against, since the README does not enumerate the interfaces or their method signatures.

## FAQ

### How do I install symfony/contracts?

The README does not list install steps; the package is distributed through Composer, and the repository's composer.json gives the package name and PHP constraint. Install it the way you install any Composer dependency and then type-hint against the interfaces in the sub-namespace you need.

### How is symfony/contracts different from PHP-FIG's PSRs?

The README states that where applicable the contracts are built on top of the PSRs, but the two groups have different goals and processes. The contracts focus on abstractions that are useful on their own and compatible with Symfony's own implementations, and contributing to PHP-FIG is described as a hope rather than the main target.

### How should a package declare that it implements a symfony/contracts interface?

The README says implementing packages should list the contract in the "provide" section of their composer.json using the symfony/*-implementation convention, and gives "symfony/cache-implementation" as the example. That metadata is what signals which contract the package satisfies.

### Which domains does symfony/contracts cover?

The repository layout shows Cache/, Deprecation/, EventDispatcher/, HttpClient/, Service/ and Translation/ at the top level, each holding its own contracts. Because every contract must have a proven implementation to enter the repository, the set is not exhaustive and other domains are not represented.

## Sources

- [License: MIT](https://github.com/symfony/contracts/blob/main/LICENSE)
- [Project website](https://symfony.com/contracts)
- [README](https://github.com/symfony/contracts/blob/main/README.md)
- [Releases](https://github.com/symfony/contracts/releases)
- [symfony/contracts on GitHub](https://github.com/symfony/contracts)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/symfony-contracts
