Skip to content

Commit 1d29564

Browse files
[TASK] About readonly (#5319)
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. Co-authored-by: Christian Kuhn <lolli@schwarzbu.ch>
1 parent 0f7fb8c commit 1d29564

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)