
When PHPStan reports that your extension calls a deprecated method, the expected next step is quite clear: find the replacement and migrate your code.
But what if there is no replacement?
Consider Context::scope(). Previously, its planned change for Shopware 6.8 was announced like this:
Static analysis sees @deprecated and reports every call to the method. However, the method is not going away. A new optional parameter will be added, so existing calls will continue to work without any changes.
Static analysis sees @deprecated and reports every call to the method. However, the method is not going away. A new optional parameter will be added, so existing calls will continue to work without any changes.
There is no alternative API to migrate to and no warning to resolve. In this situation, @deprecated is effectively crying wolf.
With Shopware 6.7.14.0, we are changing how these planned API changes are communicated. Real deprecations remain deprecations. Other backward-compatibility changes are now described with dedicated, structured PHP attributes.
The immediate result is less noise for extension developers.
Additionally, the new attributes give us a foundation for preparing extensions for Shopware 6.8 - and future major releases - before those releases arrive.
TL;DR
Shopware now uses two different signals for two different purposes:
@deprecatedmeans that an API is obsolete and will be removed or replaced. Extension developers need to migrate away from it.BC-change attributes describe a future change to an API that remains available, such as a new parameter, a narrower return type, or a class becoming final.
The attributes also distinguish between changes that affect code calling an API and changes that affect classes extending it.
This means deprecation warnings become trustworthy and actionable again, while planned contract changes carry enough structured information for PHPStan, Rector, IDEs, and other tools to reason about them.
We were asking @deprecated to do two different jobs
The commonly understood meaning of '@deprecated' in the PHP ecosystem is quite narrow: this API should no longer be used. IDEs may strike it through, and static-analysis tools warn when application code still depends on it.
This is useful behaviour. If a method is going away, extension developers should know about it as early as possible.
Shopware, however, also used @deprecated as an internal planning mechanism for the next major version. A reason:* marker could announce that:
A return type will become narrower.
A parameter type will change.
A new optional parameter will be added.
A class will become final or internal.
A method’s visibility will change.
These are all relevant backward-compatibility changes, but they are not all deprecations.
In many cases, the annotated method remains the correct API to use. Whether an extension needs to change anything depends on how it uses that API. Calling a method and overriding it in a subclass are two very different forms of dependency.
The standard @deprecated signal cannot express that distinction. It also cannot express a future type, parameter name, default value, or affected audience in a machine-readable form. Static-analysis tools should not be expected to understand Shopware-specific prose inside a PHPDoc annotation.
We could have recommended that every extension add more ignore patterns. But a blanket ignore would also hide genuine deprecations (the warnings where an extension developer really does need to act).
The problem was therefore not PHPStan being too strict. We were giving it the wrong signal.
A more precise model for backward-compatibility changes
The new attributes live in the Shopware\Core\Framework\Deprecation\BCChange namespace. There are currently 15 concrete attributes covering changes to types, parameters, visibility, inheritance, and exception contracts.
Every attribute belongs to one or both of two audiences:
CallSiteCompatibilityChangedescribes a change that may affect code calling a method.ExtenderCompatibilityChangedescribes a change that may affect a subclass or method override.
For example, narrowing a parameter type affects callers that currently pass a value outside the future type. Widening that parameter type does not affect callers, but it may make an overriding method incompatible.
Return types work in the opposite direction because PHP return types are covariant. A narrower return type does not break callers, but subclasses overriding the method may need to adjust their declaration.
That is also why the attributes have directional names such as ReturnTypeNarrowing, ReturnTypeWidening, ParameterTypeNarrowing, and ParameterTypeWidening. The direction tells us who may be affected.
Some changes affect both sides. If a method becomes internal, for example, code calling it and code extending it may both need to stop relying on it.
Whether you need to act is therefore no longer answered with a generic “this method is deprecated.” It is answered by the concrete change and the way your extension uses the API.
The design started as a community-driven RFC and was refined through discussion and implementation feedback. It was introduced through the attribute family, PHPStan enforcement, and the migration of existing planning annotations; ParameterDefaultValueChange followed as a later refinement.
How extension developers can use the new information
The attributes themselves are marked @internal. They are not intended as a new extension API that plugins should instantiate or add to their own code.
What extension developers can rely on is the meaning of the announcement on a Shopware Core API: what changes, when it changes, and whether callers or extenders may be affected.
Let us return to Context::scope(). Its planned change is now expressed as structured metadata:
- #[NewOptionalParameter(
- version: 'v6.8.0',
- parameterName: 'states',
- parameterType: 'array',
- defaultValue: [],
- )]
- public function scope(string $scope, \Closure $callback): mixed
For callers, there is nothing to do. The new parameter is optional, so existing calls remain valid.
For a class overriding the method, the situation is different. The override can add the optional parameter today:
- public function scope(
- string $scope,
- \Closure $callback,
- array $states = [],
- ): mixed {
- // ...
- }
Additional optional parameters are compatible with the current parent declaration. Once Shopware 6.8 adds the parameter, the override remains compatible. The extension can therefore prepare without a Shopware version check or two separate implementations.
Return-type narrowing provides another example. Field::setFlags() currently returns self, but will return static in Shopware 6.8:
- #[ReturnTypeNarrowing(version: 'v6.8.0', newType: 'static')]
- public function setFlags(Flag ...$flags): self
Callers are not affected because every static value is also a valid self value. An overriding method can already declare static, though, because PHP permits covariant return types:
- public function setFlags(Flag ...$flags): static
- {
- // ...
- }
Again, one declaration works with both the current version and the future one.
Parameter renames mainly affect named arguments. For example:
- #[ParameterNameChange(
- version: 'v6.8.0',
- parameterName: 'filename',
- newName: 'fileName',
- )]
- public function __construct(string $content, string $filename)
A call using filename: will break after the rename. Switching to positional arguments works with both parameter names:
- new DomainVerificationRequestStruct($content, $fileName);
The backward-compatibility guide contains the full attribute inventory and a per-change guide explaining which adaptations can already be made without dropping compatibility with the current Shopware version.
Static metadata does not replace runtime deprecations
Not every compatibility problem should be left entirely to static analysis.
If incompatible legacy usage can be detected when the method runs, Shopware still emits a conditional runtime deprecation. For example, when a parameter will be narrowed from ?string to string, Core can detect that a caller still passes null and trigger Feature::triggerDeprecationOrThrow() for that call.
This is more precise than deprecating the entire method. Callers already passing a string receive no warning, while callers depending on the behaviour that will break in 6.8 get an actionable message.
Other changes cannot be detected reliably at runtime. A method cannot generally know that an extension overrides it with a future-incompatible declaration. A class becoming final cannot warn only the subclasses that matter without also creating noise for test doubles and other legitimate current usage.
For those changes, structured static information is the better tool.
Shopware’s PHPStan rules validate these announcements inside Core. They verify that parameter names and types resolve, that the announced state differs from the current state, and that runtime deprecations exist where incompatible use can be detected. Attributes also fail validation after their target version has arrived, so they cannot silently become stale documentation.
A separate rule prevents the old @deprecated reason:* planning markers from being reintroduced.
What becomes possible next
The first benefit for extension projects requires no additional tooling: after updating to Shopware 6.7.14.0, Core’s internal planning notes no longer appear as generic deprecation errors.
The structured metadata also allows us to go further.
A forward-looking proof of concept in phpstan-shopware explores an opt-in future-compatibility ruleset. Instead of analysing only the declarations available today, it can interpret the announced contracts and check whether extension code is already compatible with the next major version.
Such a ruleset can, for example, find:
Calls that pass values outside a future narrowed parameter type.
Named arguments using a parameter’s old name.
Overrides missing a future optional parameter.
Classes extending something that will become final or internal.
Code that does not handle a future widened return type.
This remains forward-looking work and is deliberately separate from the default PHPStan rules. Extension projects should be able to opt into stricter future-compatibility analysis when it fits their support strategy.
The same metadata could be used as input for custom Rector rules. Instead of parsing free-form PHPDoc text, a rule can read a specific change type and its payload, then apply a focused migration. IDE inspections, LSP diagnostics, compatibility reports, and upgrade-note generators are other possible consumers.
The important part is that we now have reliable input on which such tools can be built.
Stop ignoring deprecations
When @deprecated cries wolf often enough, developers stop listening. That was the real cost of using the same signal for both removals and internal BC planning: every false alarm made the next genuine deprecation easier to ignore.
With the two concerns now separated, that excuse is gone. Extension developers should take three concrete actions.
Remove broad deprecation suppressions
An @deprecated annotation on a public Shopware Core API is actionable. The API will be removed or replaced, and extensions using it need to follow the documented migration path.
Broad ignore patterns that hide Shopware deprecations should therefore be removed. They now risk hiding work that must be completed before the next major release.
Review the remaining deprecation reports in your extension, identify the documented replacement, and treat the migration as required maintenance rather than optional cleanup.
Check BC-change attributes on the APIs you use
Removing false warnings is only one half of the improvement. Extension developers also need to pick up the new BC-change signals.
When an API used by your extension carries a BC-change attribute, check whether it applies to call sites, extenders, or both. Then compare the announced change with how your extension uses that API.
The attributes do not make an extension forward-compatible by themselves. They provide the information needed to do so. If they are ignored until the major upgrade, the incompatibilities will still arrive all at once.
Adopt forward-compatible declarations early
Many announced changes can be handled before the next major release without dropping support for the current Shopware version.
The examples above show the concrete benefit:
An override can add an announced optional parameter today.
An override can adopt an announced narrower return type today.
A call site can stop relying on a parameter name that will change.
These are not merely preparations written down for later. They are changes extension developers can make now. One implementation can remain backward-compatible with the current Shopware version while already being compatible with the announced future contract.
Make these checks part of regular extension maintenance and static analysis instead of postponing them until the major upgrade. That turns one disruptive migration into a series of smaller, safer changes.
Shopware 6.8 is the first major upgrade to benefit from this model, but it will not be the last. Future major changes can use the same structured announcements, while PHPStan, Rector, and other tools can become increasingly effective at turning them into concrete migration work.
The goal is to stop @deprecated from crying wolf. When it appears now, treat it as a warning that demands action. When a BC-change attribute appears, use the head start it provides.




