slava-basko / functional-php
Collection of php functions that allows you to write code in a declarative way.
Requires
- php: ^5.5|^7|^8
Requires (Dev)
- ext-json: *
- phpunit/phpunit: ^4|^5|^7|^8|^9
- squizlabs/php_codesniffer: 3.*
This package is auto-updated.
Last update: 2024-10-27 02:22:14 UTC
README
Collection of PHP functions that allows you to write code in a declarative way.
General ⚙️
Name convention
The snake_case
is used to be closer to a PHP native functions.
Zero dependencies
This library has no dependencies on any external libs.
PHP 5.5+
Supporting PHP versions since 5.5. Why? Because legacy projects also deserve a bit of functional approach.
"Data last" principe
The data to be operated on is generally supplied last (last functions argument). Functions is more convenient for currying in this way.
Functions are curried by default
This allows us to be more efficient in building new functions from old ones simply by not supplying the final parameter.
The last two points together make it easy to build functions as sequences of simpler functions, each of which transforms the data and passes it along to the next.
Docs 📚
Here you can find available function.
Other useful things.
- Identity / aka simple container
- Constant / container that holds value unchanged
- Maybe / container that holds
value
orNothing
- Either / container that holds two values,
Left
aka error andRight
aka success - Optional / container that holds
value
including NULL orNothing
- IO / container that holds impure function that returns value
OOP 🤝 FP
The purpose of this library is not to replace imperative and OOP. They can be combined, and I believe they should be combined because any of these approaches is not a silver bullet.
I will omit the theory about functional programming because you can find a lot of information about it yourself. But I want to show you examples.
Collection example
Let's imagine that you are using collection lib, and you want to upper all elements. You need to write things like this:
$collection = new Collection(['one']); $collection->map(function ($value) { return strtoupper($value); });
You can get an error like ArgumentCountError : strtoupper() expects exactly 1 argument, X given
when you will write $collection->map('strtoupper');
.
Only user defined functions does not throw an exception when called with more arguments. But you can do this:
$collection = new Collection(['one']); $collection->map(unary('strtoupper'));
Bam! You get less bloated code without function
, {
, return
, }
, ;
. Function unary
is a higher-order function,
it takes function with any arity and return new function that accept only one argument.
That's what I mean when I talk about combining imperative/OOP and functional code.
One more example with the collection. We need to filter users by isActive
method for example.
$collection = new Collection([$user1, $user2, $user3]); $collection->filter(function ($user) { return $user->isActive(); }); // VS $collection->filter(invoker('isActive'));
Point-free example
Now let's consider the second example when we need to calculate qty of items in order.
$products = [ [ 'description' => 't-shirt', 'qty' => 2, 'value' => 20 ], [ 'description' => 'jeans ', 'qty' => 1, 'value' => 30 ], [ 'description' => ' boots', 'qty' => 1, 'value' => 40 ], ]; $imperativeTotalQty = 0; foreach ($products as $product) { $imperativeTotalQty += $product['qty']; } // OR $totalQty = compose(sum, pluck('qty'))($products);
You can read code compose(sum, pluck('qty'))
like sum of 'quantity' properties
.
Ok, I understand that this could be a bit odd for you. You get used to writing code differently.
Pipe and partial application
We have a $products[]
and we need to create a common description from the description
property of each one.
So, here are the basic steps:
- Fetch property 'description' from products.
- Strip whitespace from the beginning and end of each value.
- Remove empty elements.
- Join elements with commas.
- Cut generated description up to 34 characters.
- Trim the comma at the end if present.
The imperative way could be:
$commonDescription = trim(substr(implode(', ', array_filter(array_map('trim', array_column($products, 'description')), 'strlen')), 0, 34), ', '); // OR $commonDescription = trim( substr( implode( ', ', array_filter( array_map( 'trim', array_column($products, 'description') ), 'strlen' ) ) , 0, 34 ), ', ' );
Quite big cognitive load 🤯. Let's try to reorder it and make it more readable.
$descriptions = array_column($products, 'description'); $trimmedDescriptions = array_map('trim', $descriptions); $nonEmptyDescriptions = array_filter($trimmedDescriptions, 'strlen'); $descriptionString = implode(', ', $nonEmptyDescriptions); $shortDescription = substr($descriptionString, 0, 34); $commonDescription = trim($shortDescription, ', ');
Now it's more readable, but we need to mess with states.
The functional code could be like this:
$commonDescription = pipe( pluck('description'), map(unary('trim')), select(unary('strlen')), join(', '), take(34), partial_r('trim', ', ') )($products);
This is precisely what we need. It's in a natural order. No intermediate states.
One more example where we need to get user initials.
$initials = pipe( partial('explode', ' '), map(pipe(take(1), partial_r(concat, '.'))), join(' ') )('Slava Basko'); // $initials = 'S. B.'
I know, maybe that line looks weired to you, but the idea of composing functions without having to stop at every step to consider the control flow structures and what the parameters are going to be named is pretty powerful.
What about some real-life example? 🤔
No problem, this project has a doc auto-generation script. Written in an entirely point-free manner.
Show me your doc_generator.php
No variable were harmed during script development.
License ⚖️
Use as you want. No liability or warranty from me. Can be considered as MIT.