|
| 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