JavaScript and Underscore.js type methods in PHP

PHP

I’ve been working with PHP since 2000 and with JavaScript even longer. Over last decade JavaScript evolved drastically. It had a promising design from the very beginning and gained its current incredible shape with the shift to ES2015 (Harmony). PHP though didn’t change that much. Apparently it’s doomed always to suffer from its own legacy. It arrived in middle 90s as a simple CGI app to maintain Rasmus Lerdorf’s home page and have been more like spontaneously developing ever since. That resulted in syntax inconsistency, mix of functional and OOP-based assets, and absence of a decent language specification. So every time I switch to PHP I’m missing rationality, predictability and flexibility of JavaScript. This made me consider a library, which would bring JavaScript-like development experience to PHP. Eventually I came up with Extras, which extends PHP types with JavaScript (besides with Underscore) methods, unlocks chaining and introduces PlainObject type representing object-literal.

JavaScript meets PHP

The library enriches with JavaScript and Underscore.js methods the following PHP types: array (both sequential and associative), string, number (integer, float, NaN), function (callable), collection (Iterable, ArrayObject, Iterator) and boolean. Every added method is a static method of the class representing the type. So we can access any directly without creation of a chain like Arrays::map

What is good about? Let’s examine the following snippet:

 
<?php
use \Dsheiko\Extras\Arrays;

function numToArray(int $value, int $index, array $array): array
{
  return [$value];
}
$res = Arrays::map(range(1,3), "numToArray"); // [[1],[2],[3]]

Unlike \array_map the library method has interface consistent to any other array extra. First argument is always the source array, next iteratee/predicate callaback then anything else if needed. What is more? With PHP array functions we need to remember which one accepts only references for source or for callback. Arrays::* methods always accept standalone values as well as references.

Have you noticed the callback parameter list? Yes, it’s extended according to EcmaScript specification. Besides the value, it always receives the element index (key in an associative array) and the initial array.

In addition all the added methods can be chained:

 
<?php
use \Dsheiko\Extras\Arrays;

$res = Arrays::chain([1, 2, 3])
    ->map(function($num){ return $num + 1; })
    ->filter(function($num){ return $num > 1; })
    ->reduce(function($carry, $num){ return $carry + $num; }, 0)
    ->value();

To me it looks cleaner than that:

 
$array = [1, 2, 3];
$array = \array_map(function($num){ return $num + 1; }, $array);
$array = \array_filter($array, function($num){ return $num > 1; });
$array = \array_reduce($array, function($carry, $num){ return $carry + $num; }, 0);

What about any other type? If we take a string it works the same way:

<?php
use \Dsheiko\Extras\Strings;
function sanitizeSearchTerm(string $value, int $maxLength): string
{
    return Strings::chain($value)
            ->trim()
            ->replace('`[^0-9\-_ \p{L}]`u', ' ')
            ->replace('` +`', ' ')
            ->substr(0, $maxLength)
            ->value();
}

Furthermore, we can also manipulate multiple types in the same chain:

<?php
use \Dsheiko\Extras\Arrays;
$res = Arrays::chain(["foo" => "FOO", "bar" => "BAR"])
    // input: array, output: string
    ->reduce(function($carry, $val, $key){ return $carry . " {$key}:{$val} "; }, "")
    // input: string, output: integer
    ->indexOf("bar:")    
    // input: integer, output: double
    ->toFixed(2)    
    ->value();

Actually you do not even need to guess the type on the entry point. Instead you can go with Any type:

<?php
use \Dsheiko\Extras\Any;
$res = Any::chain(new \ArrayObject([1,2,3]))
    ->toArray() // value is [1,2,3]
    ->map(function($num){ return [ "num" => $num ]; })
    // value is [[ "num" => 1, ..]]
    ->reduce(function($carry, $arr){
        $carry .= $arr["num"];
        return $carry;

    }, "") // value is "123"
    ->replace("/2/", "") // value is "13"
    ->then(function($value){
      if (empty($value)) {
        throw new \Exception("Empty value");
      }
      return $value;
    })
    ->value();

As you can see in this example I use also chaining method then that works similar to one of Promise API. It accepts the actual value of the chain and passes it further by the chain modified or not.

Underscore.js meets PHP

Not persuaded? Well, we do a lot of manipulations with types and especially with arrays. Array functions help to isolate iteration scope, improve performance, but they are not that handy as to show developer intend. In fact we often solve similar logical tasks, where we could simply reuse existing patterns. Extras library incorporates the collection of Underscore.js methods. And it makes the difference. Just imagine you have an array of key-value objects (actually associative arrays) carrying data about all the plays performed in a theater. According to user-applied filters you need to search for the matching plays. Instead re-creating every time a callback for \array_filter you can use Arrays:where:

$listOfPlays = [
    ["title" => "Cymbeline", "author" => "Shakespeare", "year" => 1611],
    ["title" => "The Tempest", "author" => "Shakespeare", "year" => 1611],
    ["title" => "Hamlet", "author" => "Shakespeare", "year" => 1603]
];
$res = Arrays::where($listOfPlays, ["author" => "Shakespeare", "year" => 1611]);
// [
//    ["title" => "Cymbeline", "author" => "Shakespeare", "year" => 1611],
//    ["title" => "The Tempest", "author" => "Shakespeare", "year" => 1611],
// ]

Or let’s say you have a key-value data structure describing a product. You need to extract a smaller object (here associative array) containing the only pairs you choose:

$res = Arrays::pick([
    'name' => 'moe',
    'age' => 50,
    'userid' => 'moe1',
     //… 100 more lines
  ], 'name', 'age');
// ['name' => 'moe', 'age' => 50, ]

As you probably noted Extras reflects both JavaScript types array and object on PHP array (sequential and associative respectively). Besides, one can get from it a type that is similar to JavaScript plain object:

<?php
use \Dsheiko\Extras\Arrays;

$po = Arrays::object(["foo" => "FOO", "bar" => ["baz" => "BAZ"]]);
echo $po->foo; // FOO
echo $po->bar->baz; // BAZ

The type inherits Underscore Object methods:

<?php
use \Dsheiko\Extras\Arrays;
$po = Arrays::object([
    "start" => 5,
    "end" => 12,
]);
$res = $po->mapObject(function($val){
        return $val + 5;
    }) // PlainObject{ "start": 10, "end": 17 }
    ->invert(); // PlainObject{ 10: "start", 17: "end" }

Recap

Extras is PHP package available via composer:

composer require "dsheiko/extras"

It is designed to improve developing experience in PHP. Namely it provides type manipulation utility-belt with consistent naming convention and consistent parameter order. Any of delivered methods are chainable. In addition to PHP generic types, the package brings PlainObject, similar to JavaScript plain object. Package methods adhere the syntax and logic of corresponding JavaScript and Underscore.js/Lodash methods.

The project accompanied with online documentation in the very style of Underscore:

https://dsheiko.github.io/extras

Also there is a Cheat Sheet PDF