قالب وردپرس درنا توس
Home / Tips and Tricks / Approaches to creating typed arrays in PHP – CloudSavvy IT

Approaches to creating typed arrays in PHP – CloudSavvy IT



PHP logo

PHP does not let you define typed arrays. Any array can contain any value, making it difficult to enforce consistency in your codebase. Here are a few workarounds to help you create typed collections of objects using existing PHP functions.

Identifying the problem

PHP arrays are a very flexible data structure. You can add anything you want to an array, from scalars to complex objects:

$arr = [
    "foobar",
    123,
    new DateTimeImmutable()
];

In practice, it is rare that you want an array with such a varied range of values. It is more likely that your arrays contain multiple instances of the same kind of value.

$times = [
    new DateTimeImmutable(),
    new DateTimeImmutable(),
    new DateTimeImmutable()
];

You could then create a method that works on all values ​​in your array:

final class Stopwatch {
 
    protected array $laps = [];
 
    public function recordLaps(array $times) : void {
        foreach ($times as $time) {
            $this -> laps[] = $time -> getTimestamp();
        }
    }
 
}

This code repeats itself over the DateTimeInterface instances in $timesThe Unix timestamp representation of the time (seconds measured as an integer) is then stored in $laps

The problem with this code is that it has a assumption Which $times consists entirely of DateTimeInterface cases. There is nothing to guarantee that this is the case, so a caller can still pass a series of mixed values. If one of the values ​​is not implemented DateTimeInterface, the call to getTimestamp() would be illegal and a runtime error would occur.

$stopwatch = new Stopwatch();
 
// OK
$stopwatch -> recordLaps([
    new DateTimeImmutable(),
    new DateTimeImmutable()
]);
 
// Crash!
$stopwatch -> recordLaps([
    new DateTimeImmutable(),
    123     // can't call `getTimestamp()` on an integer!
]);

Add type consistency with variadic arguments

Ideally, specifying that the $times array can only DateTimeInterface cases. Since PHP has no support for typed arrays, we need to look at alternative language features instead.

The first option is to use variadic arguments and the $times array before passing to recordLaps()Variadic arguments allow a function to accept an unknown number of arguments that are then made available as a single array. Important for our use case is that you can type hint variadic arguments normally. Each argument passed must then be of the specified type.

Variadic arguments are often used for mathematical functions. Here’s a simple example that lists every given argument:

function sumAll(int ...$numbers) {
    return array_sum($numbers);
}
 
echo sumAll(1, 2, 3, 4, 5);     // emits 15

sumAll() is not passed to an array. Instead, it receives multiple arguments that PHP combines in the $numbers array. The int typehint means that each value must be an integer. This acts as a guarantee that $numbers will consist of integers only. We can now apply this to the stopwatch example:

final class Stopwatch {
 
    protected array $laps = [];
 
    public function recordLaps(DateTimeInterface ...$times) : void {
        foreach ($times as $time) {
            $this -> laps[] = $time -> getTimestamp();
        }
    }
 
}
 
$stopwatch = new Stopwatch();
 
$stopwatch -> recordLaps(
    new DateTimeImmutable(),
    new DateTimeImmutable()
);

It is no longer possible to pass unsupported types to recordLaps()Attempts to do this will surface much sooner, before the getTimestamp() call is attempted.

If you already have a slew of times to go to recordLaps(), you have to extract it with the splat operator (...) when you call the method. Trying to get it right away will fail – it would be treated as one of several times required to get one int and not one array

$times = [
    new DateTimeImmutable(),
    new DateTimeImmutable()
];
 
$stopwatch -> recordLaps(...$times);

Limitations of Variadic Arguments

Variadic arguments can be of great help when you need to pass a sequence of items to a function. However, there are some restrictions on its use.

The main limitation is that you can only use one set of variadic arguments per function. This means that each function can only accept one “typed” array. In addition, the variadic argument must be defined last, after any regular arguments.

function variadic(string $something, DateTimeInterface ...$times);

By nature, variadic arguments can only be used with functions. This means that they cannot help you when needed store an array as a property, or return it from a function. We can see this in the stopwatch code – the Stopwatch class has one laps array intended to store only integer timestamps. There is currently no way to enforce this.

Collection classes

In these circumstances, a different approach has to be taken. One way to create something close to a “typed array” in userland PHP is to write a special collection class:

final class User {
 
    protected string $Email;
 
    public function getEmail() : string {
        return $this -> Email;
    }
 
}
 
final class UserCollection implements IteratorAggregate {
 
    private array $Users;
 
 
    public function __construct(User ...$Users) {
        $this -> Users = $Users;
    }
 
    public function getIterator() : ArrayIterator {
        return new ArrayIterator($this -> Users);
    }
 
}

The UserCollection class can now be used anywhere you would normally run a range of User cases. UserCollection uses variadic arguments around a sequence User instances in the constructor. Although the $Users property must be typed as the generic array, it is guaranteed to consist entirely of user instances as it is only written to in the constructor.

It may seem tempting to get one get() : array method that exposes all items of the collection. This should be avoided as it takes us back to the vague array type hint problem. Instead, the collection has been made iterable so that consumers can use it in a foreach loop. In this way, we managed to create a type-hintable “array” that our code can safely assume contains only users.

function sendMailToUsers(UserCollection $Users) : void {
    foreach ($Users as $User) {
        mail($user -> getEmail(), "Test Email", "Hello World!");
    }
}
 
$users = new UserCollection(new User(), new User());
sendMailToUsers($users);

Make collections more array-like

Collection classes solve the problem with type hinting, but they mean that you have some of the useful functionality of arrays. Built-in PHP functions such as count() and isset() does not work with your custom collection class.

Support for these functions can be added by implementing additional built-in interfaces. If you implement Countable, your class will be usable with count()

final class UserCollection implements Countable, IteratorAggregate {
 
    private array $Users;
 
 
    public function __construct(User ...$Users) {
        $this -> Users = $Users;
    }
 
    public function count() : int {
        return count($this -> Users);
    }
 
    public function getIterator() : ArrayIterator {
        return new ArrayIterator($this -> Users);
    }
 
}
 
$users = new UserCollection(new User(), new User());
echo count($users);     // 2

Implement ArrayAccess allows you to access items in your collection using array syntax. It also makes the isset() and unset() functions. You must implement four methods for PHP to communicate with your items.

final class UserCollection implements ArrayAccess, IteratorAggregate {
 
    private array $Users;
 
 
    public function __construct(User ...$Users) {
        $this -> Users = $Users;
    }
 
    public function offsetExists(mixed $offset) : bool {
        return isset($this -> Users[$offset]);
    }
 
    public function offsetGet(mixed $offset) : User {
        return $this -> Users[$offset];
    }
 
    public function offsetSet(mixed $offset, mixed $value) : void {
        if ($value instanceof User) {
            $this -> Users[$offset] = $value;
        }
        else throw new TypeError("Not a user!");
    }
 
    public function offsetUnset(mixed $offset) : void {
        unset($this -> Users[$offset]);
    }
 
    public function getIterator() : ArrayIterator {
        return new ArrayIterator($this -> Users);
    }
 
}
 
$users = new UserCollection(
    new User("example@example.com"),
    new User("hello@world.com")
);
 
echo $users[1] -> getEmail();   // hello@world.com
var_dump(isset($users[2]));     // false

You now have a class that alone User instances and that also looks and feels like an array. One point to note ArrayAccess is the offsetSet implementation – as $value must be mixed, this may add incompatible values ​​to your collection. We explicitly check the success type $value to prevent this.

Conclusion

Recent PHP releases have evolved the language towards stronger typing and more consistency. However, this does not yet apply to array elements. Type a hint against it array is often too relaxed, but you can get around the limitations by building your own collection classes.

When combined with variadic arguments, the collection pattern is a viable way to enforce the types of aggregated values ​​in your code. You can type in your collections and iterate them, knowing that only one type of value will be present.


Source link