Skip to content

Commit 320dc5f

Browse files
authored
[TASK] About readonly (#5317)
Add a chapter about PHP readonly and how TYPO3 core development deals with it. This has been approved by core mergers team and substantial changes must be coordinated with TYPO3 core mergers.
1 parent c5e081d commit 320dc5f

File tree

3 files changed

+138
-0
lines changed

3 files changed

+138
-0
lines changed

Documentation/ApiOverview/DependencyInjection/Index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,7 @@ as dependency injection cannot handle consumer state. These services *must* be
783783
instantiated using :php:`makeInstance()` until their constructors are updated to be
784784
compatible with dependency injection.
785785

786+
.. _dependency-injection-new:
786787

787788
When to use :php:`new`?
788789
-----------------------

Documentation/PhpArchitecture/Index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ specifically for extension developers.
2727
Traits
2828
WorkingWithExceptions
2929
Singletons
30+
Readonly
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
.. include:: /Includes.rst.txt
2+
.. index:: pair: Coding guidelines; About readonly
3+
.. _php-architecture-readonly:
4+
5+
=====================
6+
About :php:`readonly`
7+
=====================
8+
9+
PHP v8.1 introduced
10+
`readonly properties <https://www.php.net/manual/en/language.oop5.properties.php#language.oop5.properties.readonly-properties>`_
11+
while PHP v8.2 added
12+
`readonly classes <https://www.php.net/manual/en/language.oop5.basic.php#language.oop5.basic.class.readonly>`_.
13+
:php:`readonly` properties can only be written once - usually in the constructor.
14+
15+
Declaring :ref:`services <cgl-services>` and
16+
:ref:`value objects <cgl-named-arguments-pcpp-value-objects>` as readonly is
17+
beneficial for TYPO3 Core and extensions, offering immutability and clarity
18+
regarding the statelessness of services.
19+
20+
This document discusses the use of readonly within the TYPO3 Core ecosystem,
21+
outlining best practices for TYPO3 extension and Core developers regarding the
22+
adoption and avoidance of this language feature.
23+
24+
.. _php-architecture-readonly-services:
25+
26+
Readonly services
27+
=================
28+
29+
Readonly properties align seamlessly with services using
30+
:ref:`constructor injection <Constructor-injection>`, e.g.:
31+
32+
.. code-block:: php
33+
34+
final class UserController
35+
{
36+
private string $someProperty = 'foo';
37+
38+
public function __construct(
39+
private readonly SomeDependency $someDependency,
40+
) {}
41+
42+
// ...
43+
}
44+
45+
Well designed stateless services with no properties apart from those declared using
46+
`constructor property promotion <https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion>`_
47+
can be declared :php:`readonly` on class level:
48+
49+
.. code-block:: php
50+
51+
final readonly class UserController
52+
{
53+
public function __construct(
54+
private SomeDependency $someDependency,
55+
) {}
56+
57+
// ...
58+
}
59+
60+
Declaring properties or - even better - entire service classes readonly is a great
61+
way to clarify possible impact of state within services: If used correctly, readonly
62+
tells developers this service is stateless, shared, and can be used in any
63+
context and as often as needed without side effects from previous usages, and without
64+
influencing possible further usages within the same request. Statelessness
65+
is an important asset of services and readonly helps to sort out this question.
66+
67+
Even when a service class is declared as readonly, ensuring immutability at its level,
68+
it can still become stateful if any of its injected dependencies are stateful. This
69+
undermines the benefits of readonly design, as statefulness in dependencies can
70+
introduce unintended side effects and compromise the stateless nature of the service.
71+
TYPO3 Core strives to avoid such scenarios, particularly for services that are widely
72+
used by extensions. This ensures predictable behavior, minimizes side effects, and
73+
maintains consistency in the broader ecosystem. Developers should carefully analyze
74+
dependencies for statefulness when designing readonly services.
75+
76+
The TYPO3 Core development adopted the readonly feature early, recognizing its
77+
advantages for improving immutability, reducing side effects, and clarifying
78+
service design. However, its use requires careful consideration. The Core merger
79+
team established guidelines to determine when readonly can and should be added,
80+
which also serve as best practices for extension developers:
81+
82+
* **General Recommendation:** Declaring services or their properties as readonly
83+
is highly encouraged. Once added, the readonly declaration is rarely removed since
84+
it aligns with the effort to make services stateless.
85+
86+
* **Leaf Classes:** Existing services that are "leaf" classes (i.e., not intended
87+
to be extended by other classes) can have readonly applied to single properties
88+
or the entire class. This is typically not considered a breaking change, even in
89+
stable code branches, as it only affects XCLASS extensions, which are not covered
90+
by TYPO3's backward compatibility promise.
91+
92+
* **Method Injection:** Services retrieved via :ref:`inject*() methods <method-injection>`
93+
are not currently declared readonly, as tools like PHPStan expect readonly properties
94+
to be initialized in the constructor only. This might change in the future, but it is
95+
not a high priority.
96+
97+
* **Abstract Classes:** Existing abstract classes that are intended for extension by
98+
developers should not be declared readonly. Declaring an abstract class readonly
99+
would force all inheriting classes to also be readonly, which can create compatibility
100+
issues for extensions that need to support multiple TYPO3 versions. For example,
101+
Extbase's abstract ActionController will not be declared readonly.
102+
103+
.. _php-architecture-value-objects:
104+
105+
Readonly value objects
106+
======================
107+
108+
Readonly value objects are immutable by design. They align seamlessly with
109+
public constructor property promotion for simplicity:
110+
111+
.. code-block:: php
112+
:caption: Read only value object using public constructor property promotion
113+
114+
final readonly class Label
115+
{
116+
public function __construct(
117+
public string $label,
118+
public string $color = '#ff8700',
119+
public int $priority = 0,
120+
) {}
121+
}
122+
123+
Immutable objects improve reliability and reduce side effects. TYPO3 Core gradually
124+
adopts immutability for newly created constructs and selectively for existing data
125+
objects. Such :php:`final readonly` data objects must be instantiated using
126+
:ref:`new() <dependency-injection-new>` and :ref:`named arguments <cgl-named-arguments-pcpp-value-objects>`.
127+
128+
.. _php-architecture-summary:
129+
130+
Summary
131+
=======
132+
133+
Readonly properties and classes provide a robust framework for stateless, immutable design
134+
in TYPO3 services and simplifies value objects. While Core development continues adopting
135+
these features, extension developers are encouraged to follow these best practices to
136+
enhance code clarity and maintainability.

0 commit comments

Comments
 (0)