Thinking with Immutable Objects

by Woody Gilk
@shadowhand

Lone Star PHP 2016

Who is this guy?

Long time contributor to open source

Equip, League, Kohana, DOMPDF, etc …

4587 contributions to 270 repositories and counting

Tea drinker

It's not just for the British!

What is immutability?

An immutable object is an object whose state cannot be modified after it is created.

https://en.wikipedia.org/wiki/Immutable_object

An immutable object cannot be modified by accident.

  • Clearly states intent to modify.
  • Easier reason to reason about.
  • Reduces cognitive load.
<?php

final class LoginRequest
{
    private $username;
    private $password;

    public function __construct($username, $password)
    {
        $this->username = $username;
        $this->password = $password;
    }

    public function __get($key)
    {
        return $this->$key;
    }
}
But isn't using magic methods slow?!

Relatively speaking, yes.

In the real world, don't worry about it.

When should I use immutability?

Best fit for value objects and entities.

<?php

final class LoginRequest
{
    private $username;
    private $password;

    public function __construct($username, $password)
    {
        $this->username = $username;
        $this->password = $password;
    }

    public function __get($key)
    {
        return $this->$key;
    }
}
<?php

$login = new LoginRequest(
    $input['username'],
    $input['password']
);

if (!$this->authorization->isValid($login)) {
    throw AuthorizationException::invalidLogin($login);
}

// Carry on, lovely person!

Easy to create

<?php

function isValid(LoginRequest $login)
{
    if (!$login->username || !$login->password) {
        return false;
    }

    $user = $this->users->findBy([
        'username' => $login->username,
    ]);

    if (!$user) {
        return false;
    }

    return password_verify($login->password, $user->password);
}

Easy to use

Still need to be convinced?

Given this code…

$widget = new Widget([
    'price' => 5.00,
    'color' => 'yellow',
    'quantity' => 3,
]);

$cart->add($widget);

What is the state of $widget?

Unless the $widget is immutable we have to make assumptions.

What if the cart did this?

function add($item) {
    $this->items[$item->id] = $item;
    $item->in_cart = true;
}

A silent modification just happened.

This is relatively safe but what if there was a bug?

A junior developer might come along and do something silly.

A bug could be something trivial…

function add($item) {
    $this->items[$item->id] = $item;
    $item->quantity = 1;
}

The quantity just became corrupted!

These problems can be eliminated with immutability.

If we want to allow the cart to modify items…

function add($item) {
    $item = $item->withInCart();

    $this->items[$item->id] = $item;

    return $item;
}

When should I not use it?

Classes that require heavy configuration.

Classes that hold shared state.

What packages use immutability?

  • PSR-7 implementations
  • Immutable primitives
  • Data structures

PSR-7 with Zend Diactoros

use Zend\Diactoros\ServerRequestFactory;

$request = ServerRequestFactory::fromGlobals();

PSR-7 and REST

if ($request->getMethod() === 'GET') {
    return $this->fetchResource($request);
}

if ($request->getMethod() === 'POST') {
    return $this->createResource($request);
}

if ($request->getMethod() === 'PUT') {
    return $this->updateResource($request);
}

PSR-7 and Query Strings

$query = $request->getUri()->getQuery();

// the query is a string, NOT an array, so we need to parse it

parse_str($query, $params);

PSR-7 with League URI

use League\Uri\Components\Query;

$query = new Query($request->getUri()->getQuery());

// easier to access things

if ($query->hasKey('q')) {
    return $this->searchFor($query->getValue('q'));
}

PSR-7 and Request Bodies

$body = (string) $request->getBody();

// or read it as a stream

$body = $request->getBody();
while ($chunk = $body->read(1024)) {
    $this->writeUpload($chunk);
}

PSR-7 and Responses

use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;

$body = new Stream(json_encode($output));

$response = new Response;
$response = $response->withHeader('Content-Type', 'application/json');
$response = $response->withBody($body);

Primitives

A number of different packages in this space.

nicolopignatelli/valueobjects

use ValueObjects\Web\EmailAddress;

$email = new EmailAddress('me@example.com');

echo $email->toNative(); // me@example.com

// throws InvalidNativeArgumentException
$email = new EmailAddress('blah');
use ValueObjects\Person\Age;

$age = new Age(29);

echo $age->toNative(); // 29

// throws InvalidNativeArgumentException
$age = new Age(-1);

sebastian/money

use SebastianBergmann\Money\Currency;
use SebastianBergmann\Money\Money;

$currency = new Currency('USD');

$price = new Money(100, $currency);
$discount = new Money(5, $currency);

$discounted = $price->subtract($discount);

var_dump($price === $discounted); // false

echo $discounted->getAmount(); // 90

DataValues/DataValues

use DataValues\QuantityValue;
use DataValues\Geo\Values\LatLongValue;

$quantity = QuantityValue::newFromNumber(5);

$coords = new LatLongValue(32.962747, -96.827570); // you are here!

Quality package covering a number of interesting types.

Data Structures

A number of different packages in this space, too.

equip/structure

use Equip\Structure\Dictionary;

$dict = new Dictionary([
    'username' => 'shadowhand',
    'password' => 'too-secure-for-you',
]);

echo $dict['username']; // shadowhand

$dict = $dict->withValue('username', 'tron');

echo $dict['username']; // tron

// If unsure if something exists?

var_dump($dict->getValue('famous')); // null
use Equip\Structure\Set;
use Equip\Structure\OrderedList;

$set = new Set([1, 5, 100, 20, 1]);

var_dump($set->toArray()); // [1, 5, 100, 20]

$set = $set->withValues([5, 100]);

var_dump($set->hasValue(5)); // true
var_dump($set->hasValue(20)); // false

$list = new OrderedList($set->toArray());

Other notable packages?

  • Guzzle 6
  • league/period
  • ramsey/uuid
  • estvoyage/value
  • DateTimeImmutable

Are immutable data structures useful?

Definitely!

And I hope you think so too!

Fin.

https://joind.in/talk/4b12f

@shadowhand on Twitter, Github, etc.

Made with Slides.com