mlg / shovel
A PHP library for manipulating Arrays, Strings and Objects - inspired by ramda.js
Requires
- php: >=7.2
README
A PHP library for manipulating Arrays, Strings and Objects - inspired by ramda.js
Requirements
PHP 7.2
Install
composer require mlg/shovel
Example
<?php use Shovel\A; use Shovel\S; $fruits = ['banana', 'raspberry', 'orange', 'strawberry', 'apple', 'blueberry']; $berries = A::filter(function($fruit) { return S::endsWith('berry', $fruit); }, $fruits); print_r($berries); // ['raspberry', 'strawberry', 'blueberry']
API
Array
A::of
Concatenates every argument into an array as is
See also: A::concat()
A::of(1, 2, [3]); // [1, 2, [3]]
A::isArray
checks whether the given parameter is an array (returns true for both numeric and associative)
A::isArray([1, 2, 3]); // true
A::isArray(["a" => 10]); // true
A::isArray("asdf"); // false
A::isArray(50); // false
A::isArray(new stdClass()); // false
A::isAssoc
checks whether the given parameter is an associative array. empty arrays are treated as normal arrays and the function will return false for them
The method is based on this solution: https://stackoverflow.com/a/173479/1806628
A::isAssoc([]); // false
A::isAssoc([1, 2, 3]); // false;
A::isAssoc(["x" => 10, "y" => 20]); // true
A::reduce
Calls $fn with each element from left to right in the array and passes the returned value to the subsequent $fn calls
See also: A::reduceRight()
A::reduce($fn, $init, [1, 2, 3]); // same as $fn(3, $fn(2, $fn(1, $init)))
A::reverse
Flips the order of values around in an array
A::reverse([1, 2, 3]); // [3, 2, 1]
A::reduceRight
Calls $fn with each element from right to left in the array and passes the returned value to the subsequent $fn calls
See also: A::reduce()
A::reduceRight($fn, $init, [1, 2, 3]); // same as $fn(1, $fn(2, $fn(3, $init)))
A::sum
adds up the numbers in the given array and returns the sum
A::sum([1, 2, 3, 4, 5]); // 15
A::map
Calls $fn with all the elements and return the results in an array
$numbers = [1, 2, 3, 4, 5]; function double(int $n){ return $n * 2; } $doubles = A::map(double, $numbers); // [2, 4, 6, 8, 10]
A::keys
returns the indices of an array
See also: O::keys()
A::keys([3, 6, 9]); // [0, 1, 2]
A::values
returns the values of an array
See also: O::values()
A::values([3, 6, 9]); // [3, 6, 9]
A::equals
Compares two numeric arrays and returns true when their content is the same
A::equals([1, 2, 3], [1, 2, 3]); // true
A::equals([1, 2, 3], [1, 2, 3, 4, 5]); // false
A::length
Returns the size of an array
A::length([1, 2, 3]); // 3
A::isEmpty
Returns true when the given array has no elements inside
See also: A::isNotEmpty()
A::isEmpty([]); // true
A::isEmpty([1, 2, 3]); // false
A::isNotEmpty
Returns true when the given array has elements inside
See also: A::isEmpty()
A::isNotEmpty([1, 2, 3]); // true
A::isNotEmpty([]); // false
A::ensureArray
Wraps the given value into an array (even associative arrays) unless it already is
A::ensureArray([10]) // [10]
A::ensureArray(['a' => 10]) // [['a' => 10]]
A::ensureArray(123); // [123]
A::ensureArray([4, 5, 6]); // [4, 5, 6]
A::append
A::prepend
A::pluck
A::uniq
A::uniqByKey
A::sortBy
A::sortByKey
A::unnest
A::forEach
A::head
returns the first element of an array, or null, if empty
A::head([1, 2, 3]) // 1
A::head([]) // null
A::first
alias for A::head()
See also: A::head()
A::last
returns the last element of an array, or null, if empty
A::last([1, 2, 3, 4, 5]) // 5
A::last([]) // null
A::init
returns a copy of a given array without the last element
A::init([1, 2, 3, 4, 5]) // [1, 2, 3, 4]
A::tail
returns a copy of a given array without the first element
A::tail([1, 2, 3, 4, 5]) // [2, 3, 4, 5]
A::filter
calls the given function on the elements of an array and returns every value where the function gave truthy value
$numbers = [1, 2, 3, 4, 5, 6]; function isOdd($n){ return $n % 2 === 0; } A::filter('isOdd', $numbers); // [2, 4, 6]
A::reject
calls the given function on the elements of an array and removes every value where the function gave truthy value
$numbers = [1, 2, 3, 4, 5, 6]; function isOdd($n){ return $n % 2 === 0; } A::reject('isOdd', $numbers); // [1, 3, 5]
A::find
calls the given function on the elements of an array and returns the value for the first match. if there's no match, it will return null
$data = [ ["a" => 8], ["a" => 10], ["a" => 12] ]; $result = A::find(fn($x) => $x["a"] > 3, $data); // $result = ["a" => 8]
$data = [ ["a" => 8], ["a" => 10], ["a" => 12] ]; $result = A::find(fn($x) => $x["a"] === -4, $data); // $result = null
A::findLast
calls the given function on the elements of an array and returns the value for the last match. if there's no match, it will return null
$data = [ ["a" => 8], ["a" => 10], ["a" => 12] ]; $result = A::findLast(fn($x) => $x["a"] > 3, $data); // $result = ["a" => 12]
$data = [ ["a" => 8], ["a" => 10], ["a" => 12] ]; $result = A::findLast(fn($x) => $x["a"] === -4, $data); // $result = null
A::findIndex
calls the given function on the elements of an array and returns the key for the first match. if there's no match it will return null
$data = [1, 1, 1, 0, 0, 0, 0, 0]; $result = A::findIndex(fn($x) => $x === 0, $data); // $result = 3
$data = [1, 1, 1, 0, 0, 0, 0, 0]; $result = A::findIndex(fn($x) => $x === 2, $data); // $result = null
A::findLastIndex
calls the given function on the elements of an array and returns the key for the last match. if there's no match it will return null
$data = [1, 1, 1, 0, 0, 0, 0, 0]; $result = A::findLastIndex(fn($x) => $x === 1, $data); // $result = 2
$data = [1, 1, 1, 0, 0, 0, 0, 0]; $result = A::findLastIndex(fn($x) => $x === 2, $data); // $result = null
A::any
calls the given predicate function on the elements in the given array and returns true if for at least one of them the predicate returns true
$data = [2, 3, 5, 6, 7, 9, 10]; $result = A::any(fn($x) => $x % 5 === 0, $data); // $result = true
A::none
A::all
A::includes
A::contains
A::slice
Returns the elements of the given list from fromIndex (inclusive) to toIndex (exclusive)
A::join
A::pickRandom
selects a random item from the given array
A::concat
concatenates every argument into an array. if any of the arguments are numeric arrays, then those will get unnested
See also: A::of()
A::concat([1, 2], 3, [4, 5]); // [1, 2, 3, 4, 5]
A::zipObj
A::without
removes items from the second array by values in the first array. if first value is not an array, then it is transformed into one
A::without([1, 3], [1, 2, 3, 4, 5]); // [2, 4, 5]
A::without(['a' => 12], [1, 2, 3, 4, ['a' => 12]]); // [1, 2, 3, 4]
A::without('t', ['t', 'f', 'f', 't', 'f']); // ['f', 'f', 'f']
String
Most string operations come with an optional 3rd parameter called $caseSensitivity, which can be either
S::CASE_SENSITIVE
(default) orS::CASE_INSENSITIVE
.
All string operations are multibyte safe!
S::isString
checks whether given argument is a string
S::isString('hello'); // true
S::isString(['hello']); // false
S::isString(304.2); // false
S::length
counts the number of characters in the given parameter
S::length('őz'); // 2 -- strlen('őz') returns 3
S::isEmpty
checks whether the given string has no characters
S::isEmpty(''); // true
S::isEmpty('caterpillar'); // false
S::isNotEmpty
checks whether the given string contains any characters
S::isNotEmpty(''); // false
S::isNotEmpty('caterpillar'); // true
S::toLower
converts every character in a string to lowercase
S::toLower('AsDf JkLÉ'); // "asdf jklé"
S::toUpper
converts every character in a string to uppercase
S::toUpper('AsDf JkLÉ'); // "ASDF JKLÉ"
S::includes
checks, if the string given as the 1st parameter is a substring of the 2nd parameter string
S::includes('erf', 'butterfly'); // true
S::includes('ERF', 'butterfly', S::CASE_INSENSITIVE); // true
S::includes('ERF', 'butterfly', S::CASE_SENSITIVE); // false
S::includes('', 'butterfly'); // false -- edge case
S::contains
alias for S::includes()
See also: S::includes
S::split
splits a string into multiple parts at points matching another string
S::split("/", "foo/bar/baz") // ["foo", "bar", "baz"]
S::splitAt
splits a string into 2 at a given position
S::splitAt(3, "abcdef") // ["abc", "def"]
S::equals
compares two strings together to see if they match
S::equals('asdf', 'asdf'); // true
S::equals('asdf', 'ASDF', S::CASE_INSENSITIVE); // true
S::equals('asdf', 'ASDF', S::CASE_SENSITIVE); // false
S::slice
copies a substring between starting(inclusive) and ending(exclusive) positions
S::slice(2, 5, "abcdefgh"); // "cde"
S::slice(-3, PHP_INT_MAX, "abcdefgh") // "fgh"
S::startsWith
checks if the second parameter starts with the first
S::startsWith("inf", "infinity"); // true
S::startsWith("inf", "iNFinItY", S::CASE_INSENSITIVE); // true
S::startsWith("inf", "iNFinItY", S::CASE_SENSITIVE); // false
S::endsWith
checks if the second parameter ends with the first
S::endsWith("ed", "throwed"); // true
S::endsWith("ed", "tHRoWeD", S::CASE_SENSITIVE); // false
S::endsWith("ed", "tHRoWeD", S::CASE_INSENSITIVE); // true
S::trim
removes leading and trailing whitespaces from a string
S::trim(" asd f "); // "asd f"
S::replace
replaces substring with another
S::replace("a", "e", "alabama"); // "elebeme"
Object
O::isObject
check whether the passed in argument is an object
$point = new stdClass(); $point->x = 10; $point->y = 20; O::isObject($point); // true
O::isObject("asdf"); // false
O::toPairs
gets all keys and values of an array or object and returns it as array of key-value pairs
$point = new stdClass(); $point->x = 10; $point->y = 20; O::toPairs($point); // [["x", 10], ["y", 20]]
$user = [ "firstName" => "John", "lastName" => "Doe" ]; O::toPairs($user); // [["firstName", "John"], ["lastName", "Doe"]]
$temperatures = [75, 44, 36]; O::toPairs($temperatures); // [[0, 75], [1, 44], [2, 36]]
O::pick
O::assoc
assigns value to an object via a given key. already existing keys will get overwritten
$point2d = new stdClass(); $point2d->x = 10; $point2d->y = 20; $point3d = O::assoc("z", 30, $point2d); // {"x": 10, "y": 20, "z": 30}
$point2d = [ "x" => 10, "y" => 20 ]; $point3d = O::assoc("z", 30, $point2d); // ["x" => 10, "y" => 20, "z" => 30]
Does not work on arrays with numeric keys!
O::dissoc
removes a key from an object
$point3d = new stdClass(); $point3d->x = 10; $point3d->y = 20; $point3d->z = 30; $point2d = O::dissoc("z", 30, $point3d); // {"x": 10, "y": 20}
$point3d = [ "x" => 10, "y" => 20, "z" => 30 ]; $point2d = O::dissoc("z", 30, $point3d); // ["x" => 10, "y" => 30]
Does not work on arrays with numeric keys!
O::has
checks presence of a key inside an object and an associative array
uses array_key_exists()
internally
$data = new stdClass(); $data->x = 10; O::has('x', $data); // true O::has('y', $data); // false
$data = ['x' => 10]; O::has('x', $data); // true O::has('y', $data); // false
O::keys
O::values
O::prop
Reads the given value for the given key from objects and associative arrays. If not found, then returns null.
$data = new stdClass(); $data->x = 10; O::prop('x', $data); // 10 O::prop('y', $data); // null
$data = ['x' => 10]; O::prop('x', $data); // 10 O::prop('y', $data); // null
Functions
F::complement
Wraps the passed in function in a way that when called it inverts it's returning value
function isOdd(int $n):boolean { return $n % 2 === 1; } $isEven = F::complement(isOdd);
Concepts
Every method is abide to the following rules ( or at least they should. if they don't, then 1) PRs are welcome, 2) Issues are welcome ):
stateless
each method should get all the necessary info from the parameters and should not rely on any external parameters or state
static
since every method is stateless, there is no need to create class instances
pure
not using anything apart from the passed in parameters
immutable
not going to change any of the parameters, no & references or stuff like that
the last parameter should be the input data you are working on...
like in Lodash FP or Ramda
except if the argument list has optional parameters!
suggestions are welcome on where to place optional parameters
not doing any validation on the parameters
if you are using a method from A
, then you better be sending it an array. PHP is a loosely typed language and you could spend all day validating input parameters.
not casting any of the input parameters
it's the same as for the validation, you should check the data you pass to the function beforehand
does only a single, well defined thing
small is beautiful, and maintainable - and probably easier to test later on when I'll get the willpower to write tests for this lib
null return values on error
when an error happens and the underlying php function returns false (eg. end or strpos), then it's being normalized to null
camelCase naming
Plain numeric arrays are handled best via the methods in A, while associative arrays and objects are handled via the methods in O.
Future plans
I keep adding methods as I come across the need for them, so if you're missing a method you'd use, then 1) PRs are welcome, 2) Issues are welcome.
If the methods are all static and stateless, then why not just write simple functions?
There are multiple functions, which have the same parameter signature, but operate with different parameter types. To avoid having to type check the parameters at every function call I've chose to namespace the functions based on the types they work on into static methods.
For example take a look at includes for both Arrays and Strings. Their implementation is relative simple, because their types are expected to be arrays and strings respectively (type hinting will come soon).
class A { public static function includes($value, $data) { return in_array($value, $data, true); } } class S { public static function includes($value, $data) { return mb_strpos($data, $str) !== false; } }
If I were to have a single, combined includes
function, then I would have to do type checking every time and it would make the code unnecessarily noisy.
Plus, sometimes the function name I would like to use is already taken by PHP, like in the case of S::split
Credits
https://www.geeksforgeeks.org/php-startswith-and-endswith-functions/ - used for S::startsWith() and S::endsWith()
https://stackoverflow.com/a/173479/1806628 - used for A::isAssoc()
https://www.php.net/manual/en/function.array-unique.php#116302 - used for A::uniqByKey()