INFO
This document represents core guidelines and has been mirrored from the core in our Shopware 6 repository. You can find the original version here
2023-05-16 - PHP 8.1 & Symfony 6.1 new features
Context
As of Shopware 6.5 the minimum version of PHP is 8.1 and the minimum version of Symfony is 6.1. We would like to promote the usage of the newly available features.
Many of the new features allow us to reduce boilerplate, make it easier to prevent common mistakes, improve refactoring support, increase legibility, perform faster and so on.
By using the latest features we allow the reader and writer of code to focus on the domain rather than the language.
PHP 8.0 & 8.1 new features
Promoted Properties
We have automatically refactored all existing code to use Promoted Properties using Rector.
Promoted properties allow us to reduce the boilerplate when defining classes, by removing the need to type the property name four times and the type twice.
Class properties, with their visibility and flags can now be specified entirely in the constructor.
From:
class Point {
private int $x;
private int $y;
public function __construct(int $x, int $y)
{
$this->x = $x;
$this->y = $y;
}
}
To:
class Point {
public function __construct(private int $x, private int $y)
{
}
}
Note: It is still possible to use normal property definitions/assignments with promoted properties. For example, if you need to manipulate some dependencies.
Advantages:
- Less code, less duplication.
- Better refactoring.
Backwards Compatibility / Migration Strategy
Migrating to promoted properties does not represent a breaking change.
New in initializers
It is now possible to specify an object as a default parameter value in a function/method. Previously it was only possible to specify scalar values.
From:
class PasswordHasher
{
private Hasher $hasher;
public function __construct(private Hasher $hasher = null)
{
$this->hasher = $hasher ?? new Bcrypt();
}
}
To:
class PasswordHasher
{
public function __construct(private Hasher $hasher = new Bcrypt())
{}
}
Advantages:
- Less code
- More consistent
Backwards Compatibility / Migration Strategy
Migrating to inline object default parameters does not represent a breaking change.
Match
We have automatically refactored all existing code to use match instead of switch using Rector.
In most cases, switch
statements can be replaced with match
statements:
- Match uses strict equality unlike switch which uses weak comparison and can lead to subtle bugs.
- Each match arm does not fall through without a break statement, unlike switch.
- Match expressions must be exhaustive, if there is no default arm specified, and no arm matches the given value, an
UnhandledMatchError
is thrown. - Match is an expression and thus returns a value, reducing unnecessary variables and reducing the risk of accessing undefined variables.
From:
switch ($statusCode) {
case 200:
case 300:
$message = null;
break;
case 400:
$message = 'not found';
break;
case 500:
$message = 'server error';
break;
default:
$message = 'unknown status code';
break;
}
To:
$message = match ($statusCode) {
200, 300 => null,
400 => 'not found',
500 => 'server error',
default => 'unknown status code',
};
Note: Conditions can be combined in a much simpler fashion.
Backwards Compatibility / Migration Strategy
There are cases where migrating from a switch to a match could case a BC break. For example switch performs lose type checks and throws an exception for unhandled values.
When migrating code, be sure to check that values are the correct types and that all cases are handled.
New string functions
str_contains
str_starts_with
str_ends_with
Advantages:
- Simpler and more concise.
- Saner return types.
- It is harder to get their usage wrong, for example checking for 0 vs false with
strpos
. - The functions are faster, being implemented in C.
- The operations require less function calls, for example no usages of strlen are required.
Named arguments
Named arguments are useful when calling code with bad and/or large API's. For example, many of PHP's global functions.
In terms of calling bad PHP API's, the following advantages apply:
- It is possible to skip defaults in between the arguments you want to change.
- The code is better documented since the argument label is specified with the value, very useful for boolean flags.
From:
htmlspecialchars($string, ENT_COMPAT | ENT_HTML, 'UTF-8', false);
To:
htmlspecialchars($string, double_encode: false);
Note: The second argument is not changed, but in the first example we must provide the default value, in order to change the double encode flag.
Backwards Compatibility / Migration Strategy
We do not want to use named parameters when calling Shopware API's as parameter names are not a part of the Backwards compatability promise.
Named parameters should only be used when calling PHP API's.
Type improvements
It will now only be necessary to reach for @var & @param annotations when defining array shapes, generics and more specific types such as class-string
, positive-int
etc. Everything else should be natively typed.
When a type can really be any value, this can now be expressed as mixed
.
When a type can be multiple, but not all, this can now be expressed as a union type, eg: int|string
.
When a type must be an intersection of multiple types, this can now be expressed as an intersection type, eg: MyService&MockObject
.
These improvements come with various advantages:
- The types are enforced by PHP, so TypeError's will be thrown when attempting to pass non-valid types.
- It allows us to move more type information from phpdoc into function signatures.
- It prevents incorrect function information. phpdocs can often become stale when they are not updated with the function itself.
Enums
PHP finally has native support for enumerations, with various advantages over common userland packages and using const's.
Enums are useful where we have a predefined list of constant values. It's now not necessary to provide values as constants, and it's not necessary to create arrays of the constants to check validity.
From:
class Indexer
{
public const PARTIAL = 'partial';
public const FULL = 'full';
public function product(int $id, string $method): void
{
if (!in_array($method, [self::PARTIAL, self::FULL], true)) {
throw new \InvalidArgumentException();
}
match ($method) {
self::PARTIAL => $this->partial($id),
self::FULL => $this->full($id)
};
}
}
To:
enum IndexMethod
{
case PARTIAL;
case FULL;
}
class Indexer
{
public function product(int $id, IndexMethod $method): void
{
match ($method) {
IndexMethod::PARTIAL => $this->partial($id),
IndexMethod::FULL => $this->full($id)
};
}
}
Advantages:
- Works great with
match
- anUnhandledMatchError
exception will be thrown if there is no match arm for a given enum case. - Can type hint on an enum.
- No need to validate a case.
- Can provide backed values and serialize/unserialize with
MyEnum::from()
&&MyEnum::tryFrom()
. - Enums can provide methods and implement interfaces.
- Better comparison features, e.g. Enums are singletons.
Backwards Compatibility / Migration Strategy
See the Use PHP 8.1 Enums ADR for the decision and migration strategy.
Readonly properties
Readonly properties are very useful when building DTOs. When you want to communicate a payload to a system or service, readonly
properties allow us to create immutable data structures with a lot less code.
In conjunction with promoted properties, we can reduce the boilerplate of a class significantly. Consider a product reindex command:
From:
class ProductReindexCommand
{
private int $productId;
private bool $includeStock:
public function __construct(int $productId, bool $includeStock)
{
$this->productId = $productId;
$this->includeStock = $includeStock;
}
public function getProductId(): int
{
return $this->productId;
}
public function includeStock(): bool
{
return $this->includeStock;
}
}
To:
class ProductReindexCommand
{
public function __construct(public readonly int $productId, public readonly bool $includeStock)
{
}
}
In the first example, we use private properties to prohibit updates and public getters to allow access to the data. In the second we change the properties to public
to allow access to the data without getters, but use readonly
to prohibit updates. We also use promoted properties to make it even more succinct.
Advantages:
- Reduced boilerplate.
- Make the intent of code clearer.
Backwards Compatibility / Migration Strategy
All private properties, which are not written to after instantiation can successfully be migrated to readonly
without BC breaks.
New code can use readonly
on public and protected properties, but for existing code, that would be a BC break.
First-class callable syntax
This is a new method of referencing callables with strings and arrays. It allows for improved refactoring support, better static analysis and fixes some subtle bugs with scope.
Consider an operation to find the longest string in an array, you might use strlen
within an array_map
:
From:
$longest = max(array_map('strlen', $strings));
To:
$longest = max(array_map(strlen(...), $strings));
Instead of using an arbitrary string as a reference to a function, we can now use the (...)
syntax to create a callable.
It can also be used with object methods, instance or static, e.g.:
$callable = $object->doCoolStuff(...);
$callable = \My\Object::doCoolStuff(...);
Advantages:
- Refactoring support.
- Better static analyses.
- Fixes scope issues.
Attributes
We have automatically refactored all existing code to use attributes instead of annotations using Rector.
It is now possible to use native PHP attributes to store structured metadata, rather than using the error-prone PHP docblock.
For us Shopware developers, this will be most useful in conjunction with Symfony bundled attributes, which allow us to configure services and routes directly in controllers and services.
From:
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class BlogController extends AbstractController
{
/**
* @Route("/blog", name="blog_list")
*/
public function list(): Response
{
// ...
}
}
To:
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class BlogController extends AbstractController
{
#[Route('/blog', name: 'blog_list')]
public function list(): Response
{
// ...
}
}
Advantages:
- Add Metadata to classes, methods, properties, arguments and so on.
- They can replace PHP doc blocks, each with custom parsers and rules to a unified standard supported by PHP.
- Type safety & autocompletion.
- The data can be introspected using PHP's Reflection API's.
Nullsafe operator
The nullsafe operator works the same as property or method accesses, except that if the object being dereferenced is null then null will be returned rather than an exception thrown. If the dereference is part of a chain, the rest of the chain is skipped.
Put another way; it allows chaining multiple property or method accesses on an object, without first checking if each returned value is null before proceeding.
Consider the following code:
class User
{
public string $firstName;
public string $lastName;
public ?int $age = null;
public ?Address $address = null;
}
class Address
{
public int $number;
public string $addressLine1;
public ?string $addressLine2 = null;
}
Pre PHP 8.0, in order to access addressLine2
for an address of a user, it would be necessary to write the following code:
$user = new User(/** */);
$address = $user->address;
if ($address !== null) {
$addressLine2 = $address->addressLine2;
if ($addressLine2 !== null) {
//do something
}
}
Instead, we can now write:
$user = new User(/** */);
$addressLine2 = $user?->address?->addressLine2;
if ($addressLine2 !== null) {
//do something
}
Advantages:
- Much less code for simple operations where null is a valid value.
- If the operator is part of a chain, anything to the right of the null will not be executed, the statements will be short-circuited.
- Can be used on methods where null coalescing cannot
$user->getCreatedAt()->format('d-m-Y') ?? null
wheregetCreatedAt()
could returnnull
or a\DateTime
instance.
Other
never
return type: https://www.php.net/manual/en/language.types.never.phparray_is_list
function: https://www.php.net/manual/en/function.array-is-list.phpfinal const X
final for class constants: https://www.php.net/manual/en/language.oop5.final.php$object::class
instead ofget_class($object)
: https://wiki.php.net/rfc/class_name_literal_on_object- Array unpacking with string keys is now supported: https://www.php.net/manual/en/language.types.array.php#language.types.array.unpacking
There are many more changes, including deprecations and backwards compatibility breaks. Please read the official announcement pages for both PHP 8.0 & PHP 8.1 for a deeper understanding:
Symfony 6.1 new features
Enums in route definitions
We can specify route parameters which will be validated against a given enums cases:
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Requirement\EnumRequirement;
#[Route('/foo/{bar}', requirements: ['bar' => new EnumRequirement(SomeEnum::class)])]
Service autowiring attributes
We can now wire up dependencies without touching XML. It is possible to define the required services directly in the class:
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class Mailer
{
public function __construct(
#[Autowire(service: 'email_adapter')]
private Adapter $adapter,
#[Autowire('%kernel.debug_mode%')]
private bool $debugMode,
) {}
}
Further to that, we can decorate services with attributes: https://symfony.com/blog/new-in-symfony-6-1-service-decoration-attributes
Backwards Compatibility / Migration Strategy
See the Symfony Dependency Management ADR for the decision and migration strategy.
Improved console autocompletion
Autocompletion values can now be defined directly in the command input definition, as the 5th parameter, for both arguments and inputs:
public function configure(): void
{
$this->addArgument(
'features',
InputArgument::REQUIRED | InputArgument::IS_ARRAY,
'The features to enable',
null,
fn () => self::availableFeatures()
);
}