DEV Community

Cover image for PHP Tuples
Sasha Blagojevic
Sasha Blagojevic

Posted on • Edited on • Originally published at sasablagojevic.com

PHP Tuples

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);
Enter fullscreen mode Exit fullscreen mode

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];
}
Enter fullscreen mode Exit fullscreen mode

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"];
}
Enter fullscreen mode Exit fullscreen mode

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
{
    private $values;

    public function __construct(...$values)
    {
        $this->values = $values;
    }

    public final function offsetExists($offset)
    {
        return isset($this->value[$offset]);
    }

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

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

    public final function offsetUnset($offset) {}
}
Enter fullscreen mode Exit fullscreen mode

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 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Mind blown

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?

Top comments (2)

Collapse
 
aleksikauppila profile image
Aleksi Kauppila • Edited

Hi Sasa, thanks for posting!

What do you think of implementing an ItemCollection implements Countable, IteratorAggretate for the use case in your example?

To be honest, i'm not crazy about this Tuple idea. When you return two different things from an interface, you're saying that these values returned together have a special meaning. When using a tuple that meaning is implicit and requires your clients to "just know" why they are returned together. If these values/objects have meaning together, could they be in a single object? This would make the meaning explicit.

Collapse
 
blackcat_dev profile image
Sasha Blagojevic • Edited

For the example I used here your suggestion makes perfect sense. You make some valid points, this might not be the best approach if it is going to be publicly exposed, it could be confusing.

Maybe I should have come up with better examples :D

Thank you for reading!

EDIT:

Ok just remember where I used this approach as well:

[$failed, $messages] = $validator->failedWithMessages();

Maybe this example would be better :))