+3

Bundled PHP classes you might have not known about

in this post I will describe some native PHP features which are rarely used in common code implementations, but might significantly improve code quality if used in right situations.

In short, this is a review of datasets, iterators, type handling, advanced dates handling and advanced using of closures and generators.

Standard PHP Library (SPL)

Most of developers use SPL autoloading features and some common classes like SplFileInfo, but this library also provides much more useful things which are being used quite rarely.

Datasets

One of the most common operations required during development is using datasets. And while most modern applications use databases to deal with ordering and sorting, sometimes processing large amounts of data with PHP might be very handy.

Doubly linked lists, queues and stacks

SplStack and SplQueue are two default extensions of SplDoublyLinkedList class, which allows to operate with subsequent data sets faster and more comfortable than using arrays.

As it comes from the names, SplStack implements LIFO (Last-In-First-Out) iteration and SplQueue implements FIFO (First-In-First-Out) iteration.

$list = new SplQueue(); // OR: new SplStack();

$list->push('a');
//OR: $list->enqueue('a'); // alias of `push` in queue
$list->push('b');
$list->push('c');

for ($list->rewind(); $list->valid(); $list->next()) {
    echo $list->current()."\n";
}

Looks very simple and similar to something like Laravel Collections, but works much faster and available out of the box.

Heaps

If the order of data is dependent on it’s contents, a heap with sorting algorithm might help a lot. On the opposite to using array sorting or collection sorting, the heap will always have comparison algorithm defined inside of it.

With introducing the spaceship operator in PHP7 heaps became even more handy to work with.

class UserRankChart extends SplHeap
{
    protected function compare($user1, $user2)
    {
        // Move inactive users to bottom
        if (!$user1->active || !$user2->active) {
            return $user1->active <=> $user2->active
        }

        return $user1->likes <=> $user2->likes;
    }
}

$list = new UserRankChart;

$list->insert(new User([
    'id' => 1,
    'active' => false,
    'likes' => 0,
]);

$list->insert(new User([
    'id' => 2,
    'active' => true,
    'likes' => 5,
]);

$list->insert(new User([
    'id' => 3,
    'active' => true,
    'likes' => 2,
]);

for ($list->rewind(); $list->valid(); $list->next()) {
    echo $list->current()->id."\n";
}

In this case order of users will be: 2, 3, 1

SplMaxHeap and SplMinHeap are two extensions of heap with the opposite sorting orderings: highest first or lowest first.

Priority queues

SplPriorityQueue is a combination of a queue and heap, which uses 2 arguments in push method: the element itself and its priority separately, while priority can be any value.

For example, we can implement such queue when processing mixed instances and their ranks separately

class SubscriptionsRankChart extends SplPriorityQueue
{
    protected function compare($rank1, $rank2)
    {
        // Move inactive users to bottom
        if (!$rank1->active || !$rank2->active) {
            return $rank1->active <=> $rank2->active
        }

        return $rank1->likes <=> $rank2->likes;
    }
}

$list = new SubscriptionsRankChart();

$list->insert($post, $post->rank);
$list->insert($user, $user->rank);
$list->insert($tag, $tag->rank);

//...

In this example the element pushed to the queue might be any instance, while its rank should be compatible with queue compare method.

Fixed-length arrays

This class operates exactly as any array-like object. But it has fixed length and allows only integers as index values.

In most cases, small arrays will always be faster and less memory-consuming than objects. But with huge amount of elements, SplFixedArray gives great advantages.

Iterators

The SPL library offers a huge amount of different iterable classes. Most of them are rarely used within frameworks. But they give a lot of advantages if used properly.

Let’s take a look at several most interesting of them:

  • CallbackFilterIterator — implements an iterator which filters elements before passing them to output. The behavior is mostly similar to array_filter or filter method of most collection pattern implementations like in Laravel. The difference is that filtering callback is passed to constructor: $i = new \CallbackFilterIterator($anotherIterator, $filterCallback);
  • FilterInterator — similar to callback filter, but the filtering procedure must be defined in accept method, which is abstract by default.
  • InfiniteIterator — very interesting implementation of iterator, which automatically rewinds when it reaches the end. Might be very useful when needed to iterate through set of elements repeatedly (e. g. weekdays)
  • LimitIterator — accepts any another iterator and outputs a range defined by $offset and $count passed to constructor.
  • MultipleIterator — provides attachIterator and detachIterator methods to inject multiple different iterators inside and then use them as a single iterable element.

See full list at PHP: Iterators - Manual

Type handling

PHP does not have strict type handling by default. This gives both advantages and disadvantages. But any time you need to validate data type you can use native classes to work with it.

Abstract SplType class and its extensions: SplString, SplInt, SplFloat and SplBool provide a very handy way to deal with it.

<?php
$string = new SplString("Testing");
$int = new SplInt(94);

try {
    $string = array();
} catch (UnexpectedValueException $uve) {
    echo $uve->getMessage() . PHP_EOL;
}

try {
    $int = 'Try to cast a string value for fun';
} catch (UnexpectedValueException $uve) {
    echo $uve->getMessage() . PHP_EOL;
}

As you can see, an instance of this class detects reassignment and throws an exception if the value if the type does not match.

But the most interesting is the SplEnum class.

How many times did you have some code like this:

class Post extends Model
{
    //...

    const MODERATION_STATUS_BLOCKED = 'blocked';
    const MODERATION_STATUS_DISCARDED = 'discarded';
    const MODERATION_STATUS_PENDING = 'pending';
    const MODERATION_STATUS_APPROVED = 'approved';

    const STATUS_DRAFT = 'draft';
    const STATUS_PUBLIC = 'public';
    const STATUS_DRAFT_PUBLIC = 'draft_public';

    //...

    protected $filleble = [
        //...
        'status',
        'moderation',
        //...
    ];
}

Well, we have plenty of that in Viblo sources 😃

And here SplEnum might help a lot!

class PostModerationStatus extends SplEnum
{
    const __default = self::PENDING;

    const BLOCKED = 'blocked';
    const DISCARDED = 'discarded';
    const PENDING = 'pending';
    const APPROVED = 'approved';
}

class PostStatus extents SplEnum
{
    const __default = self::DRAFT;

    const DRAFT = 'draft';
    const PUBLIC = 'public';
    const DRAFT_PUBLIC = 'draft_public';
}

class Post extends Model
{
    // ...

    public function getModerationAttribute($value)
    {
        return new PostModeration($value);
    }

    public function setModerationAttribute($value)
    {
        $this->attributes['moderation'] = new PostModeration($value);
    }

    public function getStatusAttribute($value)
    {
        return new PostStatus($value);
    }

    public function setStatusAttribute($value)
    {
        $this->attributes['status'] = new PostStatus($value);
    }

    // ...
}

// Usage:

$avalilableStatusList = PostStatus::getConstList();
$post->status = PostStatus::PUBLIC_DRAFT;

What is the difference?

  1. Native PHP validation which will throw an exception if there is an attempt to set invalid status.
  2. The Enum type might have a default value if constructed without an argument passed.
  3. getConstList method returns the list of all available values.

Working with dates

Not only Carbon gives handy ways to work with date values, but PHP itself has 2 additional classes from Date\Time set, which are used rarely and unknown to many developers.

Immutable DateTime object

One of the main problems of Carbon and DateTime itself is that it modifies itself every time some method is being called.

$begin = new DateTime; // now
$end = $begin->modify('+1 day');

$something->between($begin, $end); // BAD

This example is wrong, because $begin itself is also changed.

DateTimeImmutable solves this problem.

$begin = new DateTimeImmutable; // now
$end = $begin->modify('+1 day');

$something->between($begin, $end); // GOOD

In this example $begin sill remain unchanged and that’s exactly what we need.

Date periods

In situation when you need to iterate through a period of dates, a default cycle suites well. But much better way is to have an itterable object.

// Every day until next year
$current = new DateTime;
$end = new DateTime('+1 year');

while ($current < $end) {
    $current->modify('+1 day');
    $someting->withDate($current);
}

This algorithm can be simply reimplemented with DatePeriod object

// Every day until next year
$period = new DatePeriod(new DateTime, new DateInterval('P1D'), new DateTime('+1 year');

foreach($period as $current) {
    $someting->withDate($current);
}

And the good thing is that it works well with Carbon and CarbonInterval

// Every day until next year
$period = new DatePeriod(Carbon::now(), CarbonInterval::days(1), Carbon::now()->addYear());

Another overloaded constructor supports integer as third argument and represents amount of requirements

// Every second day 5 times.
$period = new DatePeriod(Carbon::now(), CarbonInterval::days(2), 5);

// End date can be calculated right away
$end = $period->getEndDate();

Closure as a class

With introducing closures in PHP 5.3.0 approach to implementing callbacks changed significantly.

Now closures are a great part of the language and the main thing you need to know about them is that every closure is an object instance. It means that it have its methods, which might be very handy to use.

bind, bindTo and call

Imagine that you have a closure returned by another class method. It is very useful that closure includes $this scope to access object’s properties and methods.

But sometimes the closure might be related to factory or any other design pattern which requires its usage with different scopes.

Here these 2 methods can help a lot.

class Transformer
{
    protected static function getTransformer()
    {
        return function ($object) {
            return [
                 'id' => $object->id,
                 'name' => $object->name,
                 'rank' => $object->getSomeVeryComplexData(),
            ];
        }
    }

    public static function transform($object)
    {
        return static::getTransformer()($object);
    }
}

$data = Transformer::transform($post);

This is a very simple example.

The transformable object might have a very complex method. And we don’t need to run it right away. But we need to do it later in the code (for example inside a view). And if we use generators, it might save a lot of resources.

Bad thing here is that we cannot get a closure to be called later. And even if we do, we will need to pass a transformable instance as an argument anyway.

What if we need something like this:

// Get the closure here, but don't make transformation yet.
$transformers = [
    //...
    Transformer::transform($post);
    //...
];

// ...

// Somewhere later in the code we run transformations in some iteration

public function data() {
    foreach ($transformers as $transformer)
    {
        yield $transformer();
    }
}

// ...

// Inside your view

foreach ($collection->data() as $result) {
    echo $result['rank'];
} 

In previous

class Transformer
{
    protected static function getTransformer()
    {
        return function () {
            return [
                 'id' => $this->id,
                 'name' => $this->name,
                 'rank' => $this->getSomeVeryComplexData(),
            ];
        }
    }

    public static function transform($object)
    {
        $closure = static::getTransformer();
        return $closure->bindTo($object);
    }
}

Now it is possible and very handy. The returned closure has $this replaced by our transformable object, whatever it is. And when we iterate through our transformers, we call them one by one and destroy right after. Which means complex data will not be kept in memory.

Closure::bind is a static implementation of bindTo

$closure = function () {
    return $this->id;
}

$new = $closure->bindTo($post); // One way
$new = Closure::bind($closure, $post); // Alternative

What about call?

This method is similar to previous ones, but instead it reassigns the object right before calling it. Its usage is quite limited as the approach is almost the same as passing the object as an argument. But as we know, $this will grant you access to protected methods.

class Transformer
{
    protected static function getTransformer()
    {
        return function () {
            return [
                 'id' => $this->id,
                 'name' => $this->name,
                 'rank' => $this->someProtectedMethod(),
            ];
        }
    }

    public static function transform($object)
    {
        $transformer = static::getTransformer();
        // Same to $transformer->bindTo($object)();
        return $transformer->call($object);
    }
}

$data = Transformer::transform($post);

Note that bind and bindTo are available since PHP 5.4.0 and call since PHP 7.0

Generators as classes

Generators introduced in PHP 5.5 are also class instances and it might give a lot of advantages in their usage.

Sending values inside generator

The default pattern is using foreach to iterate over yielded values.

function makeModels($collection) {
    foreach ($collection as $data) {
        yield makeSingleModel($data);
    }
}

foreach ($makeModels($collection) as $model) //...

But not everyone knows that a value can be not only received from the generator, but passed inside it.

function printModels() {
    echo 'Begin printing';
    while (true) {
        $model = makeSingleModel(yield);
        print_r($model);
    }
}

$printer = printModels(); // Get the generator as class
$printer->send($post1);
$printer->send($user1);

Another usage of send is interrupting internal generator execution.

function printFiveModelIds($collection) {
    for ($i = 0; $i < 5; ++$i) {
        // get a value from the caller
        $cmd = (yield $i);

        if($cmd == 'stop')
            return; // exit the function
        }
}

$gen = printFiveModelIds($posts);

foreach($gen as $v)
{
    if ($v == 3) {
        // we are satisfied
        $gen->send('stop');
    }

    echo "{$v}\n";
}

Generators as iterable objects

And don’t forget that every generator implements Iterator interface. Which means we can use SPL iterators mentioned above with them

class FilteredModels extends FilterInterator {
    // Only iterate through models with high rank
    public function accept() {
        return parent::current()->rank > 10;
    }
}

function fetchModels($table, $class) {
    $results = getModelsFromDatabase($table);

    foreach ($results as $row) {
        yield new $class($row);
    }

    return count($results);
}

$posts = fetchModels('posts', Post::class);
$users = fetchModels('users', User::class);

// Create a set of models filtered by rank
$all = new MultipleIterator(
    new Filtered($posts),
    new Filtered($users)
);

$filteredCount = 0;

// Iterate through both posts and users
foreach ($all as $model) {
    $filteredCount++;
    print_r($model);
}

$initialCount = $posts->getReturn() + $users->getReturn();

This example displays quite advanced usage of generators.

First of all we use the return keyword to get the initial count of data in the result set. Our Filtered iterator allows to make filtering during iterations and save much memory on the opposite to filtering entire collection of models (see Generators in Laravel queries for more details) And MultipleIterator makes possible to iterate through different data sets within one loop.

After all execution we will have all models printed and two variables with count of filtered and initial models.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí