Skip to content

Instantly share code, notes, and snippets.

@enl
Last active May 9, 2022 19:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save enl/0e109e8a9ea6962c496db3a040cba0d6 to your computer and use it in GitHub Desktop.
Save enl/0e109e8a9ea6962c496db3a040cba0d6 to your computer and use it in GitHub Desktop.
Immutable classes in PHP
/**
* This class is purely immutable.
* PHP protects the string from changing by its copy-on-change mechanics
* If you do `$str = $immutable->getField();` and then change `$str`
* PHP will copy the value and change only second one.
*/
class Immutable
{
/** @var string */
private $field;
public function __construct($field)
{
$this->field = $field;
}
public function getField(): string
{
return $this->field;
}
}
/**
* This class only looks immutable :)
* Because all objects in PHP are used by reference,
* `$immutable->getField()` returns not a \DateTime object, it returns _reference_ on it.
* So, this code will mutate object's state:
* ```
* $date = new \DateTime();
* $immutable = new NotThatImmutable($date);
* $date->setYear('2017');
* ```
* This example explains the biggest rule of immutable objects:
*
* Immutable object can contain only immutable objects as fields.
*
*/
class NotThatImmutable
{
/** @var \DateTime */
private $field;
public function __construct(\DateTime $field)
{
$this->field = $field;
}
public function getField($field): \DateTime
{
return $this->field;
}
}
/**
* What if I have lots of parameters for my Immutable class?
* The first idea is to implement all of them as constructor parameters.
* It is usually ok if the list of parameters is no longer than 5-6 parameters
*/
BigImmutableConstructor
{
public function __construct($field, $field2, $field3, $field4, $field5)
{
// The code is quite obvious
}
}
/**
* What if I have more?
* First of all, think before you code.
* Probably, such a big list of parameters means only one thing:
* It looks like you can group them up into another immutable object and pass it instead of some parameters.
* For example, if you have parameters like this: $amount, $currency you can group them into one class Money and pass it.
*/
/**
* Another way to do this is to use Builder pattern to build the object:
* @see https://github.com/enl/retry-loop/blob/master/src/LoopBuilder.php
*
* The idea behind is to split the whole list constructor parameters into chainable list of builder functions
* and then call `call()` or ` build()` function:
*/
class BigImmutableConstructor()
{
public function __construct($field, $field2, $field3, $field4, $field5)
{
// The code is quite obvious
}
public static function builder(): BigImmutableBuilder
{
return new BigImmutableBuilder()
}
}
class BigImmutableBuilder
{
private $field;
public function reset()
{
// reset all private field to null
}
public function field($field)
{
$this->field = $field;
return $this;
}
public function build()
{
$object = new BigImmutableConstructor($field);
$this->reset();
return $object;
}
}
/**
* This way of building immutable object is not purely immmutable in PHP
* It was borrowed from Java where you just cannot call undefined method on interface
* even you are 100% sure that it exists in implementation
*
* PHP does not give a shit, so that this is NOT PURELY immutable.
* But it is still useful if you have enough discipline.
*/
class ImmutableInterface
{
public function getField();
public function getAnotherField();
}
/**
* Basically, this way of building "immutable" object is quite widely use in Symfony community.
* First of all, because Symfony Form component requires both getters and setters for its DTOs.
*/
class IamNotImmutable implements ImmutableInterface
{
private $field;
private $anotherField;
public function getField()
{
return $this->field;
}
public function setField($field)
{
$this->field = $field;
}
public function getAnotherField()
{
return $this->field;
}
public function setAnotherField($anotherField)
{
$this->anotherField = $anotherField;
}
}
@WinterSilence
Copy link

__set(), __unset() must be define as final and throw exception
setters must check passed values and clone objects when it's possible

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment