This is an old revision of the document!
PHP RFC: Immutable classes and properties
- Version: 0.1
- Date: 2016-08-12
- Author: Michal Brzuchalski michal@brzuchalski.com
- Author: Silvio Marijic marijic.silvio@gmail.com
- Status: In Draft
- First Published at: https://wiki.php.net/rfc/immutability
Introduction
This RFC proposes introduction of immutable classes and properties. Currently PHP lacks native support for immutability. Because of that user-land applications are using third party libraries or resort to custom implementations and still there is no easy enforcement of immutability. Introducing this feature would help bring one unified solution to this problem, and also it would remove unnecessary logic from user-land applications.
Pros
- Immutability guaranteed by language instead of user-land implementation.
- Safe for concurrency.
- Value objects, DTO's etc. can be easily created.
- Properties can be public which removes need for getters without allowing state modification.
- (Please point it out more advantages)
Cons
- Cloning has to be disabled for immutable objects
- (Please point it out more disadvantages)
Before
<?php class Email { private $_email; public function __construct ($email) { // validation $this->_email = $email; } public function getValue() { return $this->_email; } }
After
<?php immutable class Email { public $email; public function __construct ($email) { // validation $this->email = $email; } }
Proposal
Class defined as immutable will imply immutability across all of it's properties by default. After constructor returns, it will not be possible to modify state of immutable properties from any scope.
immutable class Email { public $email; public function __construct ($email) { // validation $this->email = $email; } } $email = new Email("foo@php.net"); $email->email = "bar@php.net" // Call will result in Fatal Error
Regular classes can define immutability per property.
class Email { public immutable $email; public function __construct ($email) { // validation $this->email = $email; } } $email = new Email("foo@php.net"); $email->email = "bar@php.net" // Call will result in Fatal Error
Any class that extends immutable class must be also declared as immutable.
immutable class Foo{} class Bar extends Foo{} // Will result in Fatal Error
It will not be possible to assign value by reference to immutable property.
immutable class Email { public $email; public function __construct ($email) { // validation $this->email = $email; } } $email = new Email("foo@php.net"); $emailRef = &$email->email; $emailRef = "bar@php.net" // Call will result in Fatal Error
If immutable property contains object, object must also be an instance of immutable class.
class Bar{} immutable class Foo { public $bar; public function __construct (Bar $bar) { // validation $this->bar = $bar; } } $foo = new Foo(new Bar()); // Will result in a error because Bar is not instance of immutable class
Examples
Money
Money Pattern, defined by Martin Fowler and published in Patterns of Enterprise Application Architecture, is a great way to represent value-unit pairs. It is called Money Pattern because it emerged in a financial context.
class Currency { private $centFactor; private $stringRepresentation; private function __construct(int $centFactor, string $stringRepresentation) { $this->centFactor = $centFactor; $this->stringRepresentation = $stringRepresentation; } public function getCentFactor() : int { return $this->centFactor; } public function getStringRepresentation() : string { return $this->stringRepresentation; } public static function USD() : Currency { return new self(100, 'USD'); } public static function EUR() : Currency { return new self(100, 'EUR'); } } class Money { private $amount; private $currency; public function __construct($amount, Currency $currency) { $this->amount = $amount; $this->currency = $currency; } public function getAmount() : float { return $this->amount; } public function getCurrency() : Currency { return $this->currency; } public function add(Money $other) : Money { $this->ensureSameCurrencyWith($other); return new Money($this->amount + $other->getAmount(), $this->currency); } public function subtract(Money $other) { $this->ensureSameCurrencyWith($other); return new Money($this->amount - $other->getAmount(), $this->currency); } public function multiplyBy($multiplier, $roundMethod = PHP_ROUND_HALF_UP) { $product = round($this->amount * $multiplier, 0, $roundMethod); return new Money($product, $this->currency); } private function ensureSameCurrencyWith(Money $other) { if ($this->currency != $other->getCurrency()) { throw new \Exception("Both Moneys must be of same currency"); } } } $oneThousand = new Money(1000, Currency::USD());
After refactoring classes to immutable this example will look like this:
immutable class Currency { /** @var int */ public $centFactor; /** @var string */ public $stringRepresentation; private function __construct(int $centFactor, string $stringRepresentation) { $this->centFactor = $centFactor; $this->stringRepresentation = $stringRepresentation; } public static function USD() : Currency { return new self(100, 'USD'); } public static function EUR() : Currency { return new self(100, 'EUR'); } } immutable class Money { /** @var float */ public $amount; /** @var Currency */ public $currency; public function __construct(float $amount, Currency $currency) { $this->amount = $amount; $this->currency = $currency; } public function add(Money $other) : Money { $this->ensureSameCurrencyWith($other); return new Money($this->amount + $other->amount, $this->currency); } public function subtract(Money $other) { $this->ensureSameCurrencyWith($other); return new Money($this->amount - $other->amount, $this->currency); } public function multiplyBy($multiplier, $roundMethod = PHP_ROUND_HALF_UP) { $product = round($this->amount * $multiplier, 0, $roundMethod); return new Money($product, $this->currency); } private function ensureSameCurrencyWith(Money $other) { if ($this->currency != $other->currency) { throw new \Exception("Both Moneys must be of same currency"); } } } $oneThousand = new Money(1000, Currency::USD());
There is no need for getters because internally immutable object if deeply frozen and none of his properties cannot be written anymore. All properties accepts scalar values or objects which implements immutable classes so there is high guarantee such Money object will keep his internal state untouched.
URI
Backward Incompatible Changes
No backward incompatible changes.
Proposed PHP Version(s)
- PHP 7.2
RFC Impact
To SAPIs
No SAPI impact.
To Existing Extensions
- Reflection.
To Opcache
New Constants
No new constants.
php.ini Defaults
No changes for INI values.
Open Issues
No open issues.
Unaffected PHP Functionality
Future Scope
Proposed Voting Choices
Proposals require 2/3 majority
Patches and Tests
Implementation
After the project is implemented, this section should contain
- the version(s) it was merged to
- a link to the git commit(s)
- a link to the PHP manual entry for the feature