PHP Tuples

Let's talk about PHP Tuples and why exploring new languages is good for you.


How do we call a gopher and an elephant dating? A tuple. 

Wow, this makes no sense whatsoever...ok, we should move on now...

I've been working in golang for quite some time and I started to pick up some patterns subconsciously. Since golang has multiple return values, which I find quite useful and am a big fan of, I started doing this more and more in php:

<?php

list($items, $count) = $contentRepository->getSlice($page, $perPage, $filters, $sorts);

// or the even sweeter shorthand syntax

[$items, $count] = $contentRepository->getSlice($page, $perPage, $filters, $sorts);

What we are doing here is just destructuring the plain ole' integer indexed array, commonly know in more CS uppity circles as a list. But there is a downside to this approach, arrays are kinda like blackboxes, we don't have a clue what's in them, it could be anything. Let's take a look at getSlice method signature to get a better understanding:

<?php

public function getSlice(int $page, int $perPage, array $filters = [], array $sorts = []): array;
{
    // something, something
    return [$items, 15];
}

You see, we don't know what type the array's members will be and there is really no way to enforce it, so we could easily introduce some nasty bugs if we were to accidentally put some unexpected values.

We can tweak this approach a bit by adding doc blocks to the method signature which will help our IDE and static analysis tools like PHPStan to know what values are expected:

<?php

/**
 * @return array<int,string>
 **/
public function getTitleAndName(): array;
{
    return ["Supreme Overlord", "Sasa Blagojevic"];
}

Wait, what? Why are we using a different example now? Well, this has its limitations as well, doc blocks only add value if all the array's members are of the same type. In our first example that's not the case. So what do we do now?

Say welcome to PHP Tuples

class Tuple implements ArrayAccess, Countable
{
    public function __construct(private array $values = []) {}

    // Array Access
    public final function offsetExists(mixed $offset): bool
    {
        return array_key_exists($offset, $this->values);
    }

    public final function offsetGet(mixed $offset): mixed
    {
        return $this->values[$offset] ?? null;
    }

    public final function offsetSet(mixed $offset, mixed $value): void {}

    public final function offsetUnset(mixed $offset): void {}

    // Countable
    public function count(): int
    {
        return count($this->values);
    }
}

Let's dissect this code snippet a bit:

  • We've implemented the ArrayAccess interface to make the objects of the class Tuple accessible as arrays
  • We've also implemented the Countable interface so we can use the count function on them and know how many elements the touple has
  • We've left the offsetSet and offsetUnset methods with empty bodies so that our tuples are immutable, we don't want to introduce bugs by accidentally mutating their state
  • We've made all the implemented methods final so other developers can't override them and break the underlying behaviour of our tuple, except the constructor

 

This is going to be our generic tuple. To make things more strict and explicit we will make custom tuple classes on per case basis by extending our Generic Tuple class and adding types to the constructor:

<?php

class ContentSlice extends Tuple
{
    /**
     * ContentSlice constructor.
     * @param array<int, Content> $items
     * @param int $count
     */
    public function __construct(array $items, int $count)
    {
        parent::__construct([$items, $count]);
    }
}

Let's apply this new found knowledge to our first example:

<?php

class ContentRepository 
{
    public function getSlice(int $page, int $perPage, array $filters = [], array $sorts = []): Tuple;
    {
        // something, something
        $items = [Content(), Content(), Content()];
        return new ContentSlice($items, 15);
    }
}

Now if we were to put wrong values in the ContentSlice constructor the code would explode. So what we have achieved now is that we have both multiple return values and explicit and strict code like in golang.

via GIPHY

This is why learning new languages is great, you will stretch out your brain and force it to solve problems in a different way that is idiomatic to the new language. This new knowledge will then pay dividens in your main language when you start to apply all the cool concepts that you've subconsciously or conscioulsy picked up. This is a great way to hone your skills.

I had this random thought tonight and wanted to share this with you guys (my imaginary audience). I find this pretty cool and I'm probably going to try it out in a project to see how it fares in real world applications, what are your thoughts about PHP Tuples?

Last updated: 1 year ago 7580