Bundled PHP classes you might have not known about
Bài đăng này đã không được cập nhật trong 7 năm
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 toarray_filter
orfilter
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 inaccept
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
— providesattachIterator
anddetachIterator
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?
- Native PHP validation which will throw an exception if there is an attempt to set invalid status.
- The Enum type might have a default value if constructed without an argument passed.
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