Elfy
Elfy

Reputation: 1863

Immutable objects in PHP?

Is it a good idea to create objects that cannot be changed in PHP?

For example a date object which has setter methods, but they will always return a new instance of the object (with the modified date).

Would these objects be confusing to other people that use the class, because in PHP you usually expect the object to change?

Example

$obj = new Object(2);

$x = $obj->add(5); // 7
$y = $obj->add(2); // 4

Upvotes: 19

Views: 31549

Answers (7)

Valentine Shi
Valentine Shi

Reputation: 7810

Making object immutable in PHP is pretty easy. Here is an elegant and convenient approach.

All you need to do is to create the base abstract class with the specific __get() and __set() magic methods and extend this base class in the child object.

This is quite applicable if you use value objects (e.g. for DDD).

Here is the base class:

abstract class BaseValueObject
{
    public function __get(string $propertyName)
    {
        return $this->$propertyName;
    }

    public function __set(string $propertyName, $value): void
    {
        throw new \Exception("Cannot set property {$propertyName}. The object is immutable.");
    }
}

Now a child object (well, its class).

class CategoryVO extends BaseValueObject
{

    public $id;
    public $name;

    public function __construct(array $data)
    {
        $this->id = $data['id'];
        $this->name = $data['name'];
    }
}

It would throw an exception at attempt to set some value. Basically it is immutable.

This is it.

Make as many immutable objects as you need. Create the new objects via constructor. Dispose them and re-create the new ones when needed (add a specific creator method if required, a static or an instance one, to the base class or to the extended one).

Yet such an object would conveniently expose all its properties as read-only (for some kind of serialization or the like), unlike if we would have made them private (but even though we could use JsonSerializable interface to make the serialization as flexible as we need with private properties or even more drastic transformations).

Finally one cannot mistakenly instantiate BaseValueObject as it is an abstract class. From all standpoints nice elegant solution.

Upvotes: 1

pipe-devnull
pipe-devnull

Reputation: 342

An immutable object cannot be changed after its initial creation so having setter methods makes no sense as it goes against that base principle.

You could implement some workarounds to simulate immutability in PHP by manipulating class member visibility and overriding the magic __set() method but its not guaranteed immutable as immutability is not a feature of the PHP language.

I believe someone once wrote an extension to provide an immutable value type in PHP though so you could google for that.

Upvotes: 6

James
James

Reputation: 4783

If you want setters on a class and object this is perfectly fine, we do this all of the time as we need to set object data. Just simply don't call it immutable.

Many things in the dev world are subjective - our approaches, methodology etc - but "immutable" is a pretty solid definition:

"Immutable":
- Unchanging over time or unable to be changed.

If you want an immutable object it means it cannot be changed after instantiation. This is good for things such as data from a DB that needs to remain set in stone for the duration of the cycle.

If you need to call the object and set or change data on it after instantiation, this is not an immutable object.

Would you take 2 wheels off a car and calling it a motorbike?

There is some talk about methods on an "immutable" class being named without the word "set", but this doesn't stop the functionality of them being a method that sets data. You could call it thisDoesNotSetAnything(int $id) and allow data to be passed in which changes the object. It'll be a setter, and thus the object is mutable.

Upvotes: 0

Chemaclass
Chemaclass

Reputation: 2011

From an immutable object, you can get its values but there is no way to modify them. Here you can see an example of an immutable class:

<?php 

declare(strict_types=1);

final class Immutable 
{
    /** @var string */
    private $value;

    public static function withValue(string $value): self
    {
        return new self($value);
    }

    public function __construct(string $value) 
    {
        $this->value = $value;
    }

    public function value(): string 
    { 
        return $this->value;
    }
}

// Example of usage:
$immutable = Immutable::withValue("my value");
$immutable->value(); 

Upvotes: 0

Jean Claveau
Jean Claveau

Reputation: 1461

I made a little trait avoiding using Reflection to ease the implementation of immutability: https://github.com/jclaveau/php-immutable-trait

Obviously, as it's not a language feature, it won't impeach mutation by magic but lighten the code of the mutators that must clone the current instance before being applied. Applied to Massimiliano's example it would produce

class ImmutableValueObject
{
    use JClaveau\Traits\Immutable;

    private $val1;
    private $val2;

    public function __construct($val1, $val2)
    {
        $this->val1 = $val1;
        $this->val2 = $val2;
    }

    public function getVal1()
    {
        return $this->val1;
    }

    public function withVal1($val1)
    {
        // Just add these lines at the really beginning of methods supporting
        // immutability ("setters" mostly)
        if ($this->callOnCloneIfImmutable($result))
            return $result;

        // Write your method's body as if you weren't in an Immutable class
        $this->val1 = $val1;
        return $this;
    }

    public function getVal2()
    {
        return $this->val2;
    }

    public function withVal2($val2)
    {
        if ($this->callOnCloneIfImmutable($result))
            return $result;

        $this->val2 = $val2;

        return $this;
    }
}
  • You can see that you don't return $copy here but $this as Kanstantsin K noticed.
  • In native PHP https://secure.php.net/manual/en/class.datetimeimmutable.php has mutators that will return new instances with modification applied. So copy pasting sentences saying that immutable objects shouldn't have mutators doesn't seem super interesting.
  • The practice of using "withXXX" instead of "setXXX" is super interesting, thanks for the suggestion! I personnaly used "becomesXXX" for the api chainging the mutability of the instance (optionnal API in the trait SwitchableMutability).

Hoping it can help some people here!

PS: Suggestions on this little feature are really welcome :) : https://github.com/jclaveau/php-immutable-trait/issues

Upvotes: 0

Th&#233;o
Th&#233;o

Reputation: 656

In my opinion objects should be immutable for value objects. Other than that it does not have much benefits unless you're sharing your object across your whole application.

There is some wrong answers here, an immutable object can have setters. Here's some implementation of immutable objects in PHP.

Example #1.

class ImmutableValueObject
{
    private $val1;
    private $val2;

    public function __construct($val1, $val2)
    {
        $this->val1 = $val1;
        $this->val2 = $val2;
    }

    public function getVal1()
    {
        return $this->val1;
    }

    public function getVal2()
    {
        return $this->val2;
    }
}

As you can see once instantiated you cannot changed any value.

Example 2: with setters:

class ImmutableValueObject
{
    private $val1;
    private $val2;

    public function __construct($val1, $val2)
    {
        $this->val1 = $val1;
        $this->val2 = $val2;
    }

    public function getVal1()
    {
        return $this->val1;
    }

    public function withVal1($val1)
    {
        $copy = clone $this;
        $copy->val1 = $val1;

        return $copy;    // here's the trick: you return a new instance!
    }

    public function getVal2()
    {
        return $this->val2;
    }

    public function withVal2($val2)
    {
        $copy = clone $this;
        $copy->val2 = $val2;

        return $copy;
    }
}

There is several implementation possible and this is by no means an exclusive list. And remember that with Reflection there is always a way to get around that in PHP, so immutability is all in your head in the end!

It is also often good practice to put immutable objects as final.

EDIT:

  • changed setX for withX
  • added comment about final

Upvotes: 16

ThiefMaster
ThiefMaster

Reputation: 318518

Immutable objects don't have setter methods. Period.

Everyone will expect a setXyz() method to have a void return type (or return nothing in loosely typed languages). If you do add setter methods to your immutable object it will confuse the hell out of people and lead to ugly bugs.

Upvotes: 32

Related Questions