ElegantElephant - small library of PHP primitives in EO style. Inspired by Cactoos from @yegor256.
PHP was designed and created like a procedural language. Then PHP started support OOP paradigm, but it's not really pure OOP. We got classes but we still use them in a procedural way. This library enforces you to write real objects in a real OOP.
Principle | Yes/No |
---|---|
No null | âž– |
No code in constructors | ✔️ |
No getters and setters | ✔️ |
No mutable objects | ✔️ |
No static methods, not even private ones | âž– |
No instanceof, type casting, or reflection | âž– |
No public methods without a contract | ✔️ |
No statements in test methods except assertThat | ✔️ |
- PHP >= 8.0
composer require maxonfjvipon/elegant-elephant
To get flatten array:
// The result will be [0, 1, 2, 2, 3, 3, 4, 5]
(new ArrFlatten(
[0, [1], [[2, 2], [3, 3]], [4, 5]],
deep: 2
))->asArray();
To merge arrays:
/*
* If user is admin, result array will contain permissions field
*
* [
* 'name' => ...,
* 'age' => ...,
* 'permissions' => [...],
* 'projects' => [
* ['name' => ..., 'created_at' => ...],
* [...],
* ...
* ]
* ]
*/
(new ArrMerged(
[
'name' => $user->name,
'age' => $user->age,
],
new ArrIf(
$user->isAdmin(),
ArrOf::func(fn () => [
'permissions' => [...]
])
),
new ArrObject(
'projects',
new ArrMapped(
$projects,
fn (Project $project) => [
'name' => $project->name,
'created_at' => $project->created_at
]
)
)
))-asArray();
To manipulate with a text:
// To lower case
(new TxtLowered(
TxtOf::str("Hello")
))->asString();
// To upper case
(new TxtUpper("Hello"))->asString();
// Join texts, if $isAdmin === TRUE the result will be "Hello Admin" else "Hello username, what'up"
(new TxtJoined([
"Hello ",
// Conditional text, behaves like first text if condition is TRUE, like second otherwise
new TxtCond(
$isAdmin,
"Admin"
TxtOf::func(fn () => $user->name())
),
// Conditional text, behaves like first text if condition is TRUE, like empty string otherwise
new TxtIf(
!$isAdmin,
", what's up"
)
]))->asString();
To get first x
from array of x y
points:
$points = [['x' => 1, 'y' => 1], ['x' => 2, 'y' => 2], [...], ...];
NumOf::any(
new AtKey(
'x',
ArrOf::any(
new FirstOf(
$points
)
)
)
)->asNumber(); // 1
To get length of filtered array:
(new LengthOf(
new ArrFiltered(
[1, 2, 3, 4, 5, 6],
fn (int $num) => $num > 3
)
))->asNum(); // 3
Complicated example from commercial project with no visible algorithm:
(new AtKey( // 12. Get element by key "pump" from given array
'pump',
ArrOf::any( // 11. Try to cast given element to array
new FirstOf( // 10. Get first element of given array
new ArrCond( // 9.1. If given sorted jockey pumps are not empty - take them
new Not(
new IsEmpty(
$jockeyPumps = new ArrSticky( // 8. Cache given sorted jockey pumps
new ArrSorted( // 7. Sort given mapped jockey pumps by "cost" key
new ArrMapped( // 6. Map given jockey pumps
new ArrCond( // 5.1. If optimized jockey pumps are not empty - take them
new Not(
new IsEmpty(
$optimized = new ArrSticky( // 4. Cached optimized jockey pumps
new ArrFiltered( // 3. Filter cached jockey pumps with optimization
$dbPumps = new ArrSticky( // 2. Cache jockey pumps
new ArrPumpsForJockeySelecting( // 1. Take jockey pumps from somewhere
$request->jockey_series_ids,
$request->accounting_rests
)
),
$filterPump(threeFifths: true)
)
)
)
),
$optimized,
new ArrFiltered( // 5.2. If opmtimized jockey pumps are empty - filter jockey pumps without optimization
$dbPumps,
$filterPump(threeFifths: false)
)
),
fn (Pump $pump) => [
'pump' => $pump,
'cost' => $pump->priceByRates($rates),
]
),
'cost'
)
)
)
),
$jockeyPumps,
[['pump' => null]] // 9.2. or take [['pump' => null]] otherwise
),
)
),
))->value();
Since PHP does not allow you to have more than one constructor in the class, some classes in the library have public static methods that return an instance of the class and replace secondary constructors. Some classes have private primary constructor to prevent user to use them in a wrong way.
For example take a look at the class ArrOf
that has private constructor but let you create an instance of Arr
object in a several ways via static methods:
use Maxonfjvipon\ElegantElephant\Arr\ArrOf;
use Maxonfjvipon\ElegantElephant\Any\AnyOf;
// From items
ArrOf::items($item1, $item2, $item3)->asArray(); // [$item1, $item2, $item3]
// From array
ArrOf::array([1, 2, 3])->asArray(); // [1, 2, 3]
// From Maxonfjvipon\ElegantElephant\Any
ArrOf::any(AnyOf::arr(["Hello", "Jeff"]))->asArray(); // ["Hello", "Jeff"]
// From callback
ArrOf::func(fn () => ["Hello", "World"])->asArray(); // ["Hello", "World"]
If you write code and see that you can't create an object via new
keyword - definitely it's the class with private constructor but public static methods that replace constructors, so try them.
Almost every classes allows you to pass union-typed arguments to the constructor.
For example, class TxtJoined
behaves like a joined string and accepts an array
or an instance of Arr
that both may contains strings, instances of Txt
or both of them.
use Maxonfjvipon\ElegantElephant\Txt\TxtJoined;
use Maxonfjvipon\ElegantElephant\Txt\TxtOf;
use Maxonfjvipon\ElegantElephant\Arr\ArrOf;
use Maxonfjvipon\ElegantElephant\Any\AnyOf;
$joined1 = new TxtJoined([
TxtOf::str("Hello"),
" ",
TxtOf::any(AnyOf::str("Jeff"))
]);
$joined2 = new TxtJoined(
ArrOf::func(fn () => [
new TxtJoined(["Hello", " "]),
"Jeff"
])
);
$joined1->asString() === $joined2->asString(); // "Hello Jeff" === "Hello Jeff" => TRUE
So when you write code you may not care about conversion an argument to the desired type, for example conversion string
to Maxonfjvipon\ElegantElephant\Txt
. Just pass what you have to the object, it knows how to deal with it.
Something that has value of any (mixed) type. Any
interface has only one public method value
that must return something of mixed
type.
Class | Description |
---|---|
AnyFork | Behaves like first value if condition is TRUE, like second otherwise |
AnyCond | Alias of AnyFork |
AnyOf | Allow you to create Any from array, Arr, string, Txt, number, Num or function |
AnySticky | Any with caching mechanism |
AnyWrap | Envelope for Any classes |
AtKey | Get element from array or Arr by key |
AtValue | Get key from array or Arr by value |
EnsureAny | Helper trait to cast mixed |
FirstOf | Get the first element from array, Arr, string or Txt |
LastOf | Get the last element from array, Arr, string or Txt |
See Any unit tests for better undestanding.
Elegant arrays. Arr
interface has only one public method asArray()
that must return an array
.
There is one more interface that you can use in your own classes - IterableArr
that extends Arr
and \IteratorAggregate
.
\IteratorAggregate
allows you to apply spread operator ...
to your class. If you want to use spread operator with your class without actual calling asArray()
method you should make your class implement IterableArr
and use HasArrIterator
trait. The trait is the default implementation of spreading Arr
classes. And now when you use ...
with your class trait calls asArray()
behind the scene.
And here is the example for better understanding:
use Maxonfjvipon\ElegantElephant\Arr;
use Maxonfjvipon\ElegantElephant\Arr\IterableArr;
use Maxonfjvipon\ElegantElephant\Arr\HasArrIterator;
class MyArr implements Arr { /** code */ }
class MyIterableArr implements IterableArr {
use HasArrIterator;
/** code */
}
$arr = [...(new MyArr())->asArray()]; // good
$arr = [...new MyIterableArr()]; // good, no actual calling asArray()
$arr = [...(new MyIterableArr())->asArray()]; // good, but verbose
$arr = [...new MyArr()]; // wrong
All Arr
classes in the library can be spead.
Class | Description | PHP |
---|---|---|
ArrCast | Cast all elements in given array | - |
ArrCombined | Combine two arrays into signe one | array_combine |
ArrFork | Behaves like first array if condition is TRUE, like second otherwise | - |
ArrCond | Alias Of ArrFork | - |
ArrEmpty | Empty array | [] |
ArrExploded | Elements of exploded by separator string | explode |
ArrFiltered | Filter elements of given array by given callback | array_filter |
ArrFlatten | Array flatten with given deep | - |
ArrIf | Behaves like given array if condition is TRUE, like empty otherwise | - |
ArrKeys | Get keys of given array | array_keys |
ArrMapped | Map elements of given array by given callback | array_map |
ArrMerged | Merge given arrays | array_merge |
ArrObject | Alias of ArrSingle | [key => value] |
ArrOf | Allows to create Arr from array, Any or function | - |
ArrPlucked | Pluck an array of values from an array | - |
ArrRange | Array of elements in given range | range |
ArrReversed | Reverse given array | array_reverse |
ArrSingle | Array with single key => value element | [key => value] |
ArrSorted | Sort array by given key or callback | sort, usort |
ArrSplit | Alias or ArrExploded | explode, split |
ArrSticky | Array with cache mechanism | - |
ArrUnique | Array with unique elements | array_unique |
ArrValues | Array of just values of given array (ignore keys) | array_values |
ArrWith | Given array + new given element | [...] + [$item] |
ArrWithout | Given array - element by key | unset($arr[$key]) |
ArrWrap | Envelope for Arr classes | - |
CountArr | Default implementation of counting Arr if your Arr impelement Countable | count($array) |
EnsureArr | Helper trait for casting array or Arr to array | - |
HasArrIterator | Default implementation of spreading IterableArr | ...$array |
See Arr unit test for better undestanding.
Elegant boolean. Logic
interface has only one method asBool()
that must return bool
.
Class | Description | PHP |
---|---|---|
Conjunction | Conjunction, logical AND | and, && |
Conj | Alias of Conjunction | and, && |
ContainsIn | Check if something contains in string, Txt, array or Arr | - |
Disjunction | Disjunction, logical OR | or, || |
Disj | Alias of Disjunction | or, || |
EnsureLogic | Helper trait to cast bool or Logic to bool | - |
GreaterOrEqual | Check if first given number is greater than or equal to the second one | >= |
GreaterThan | Check if first given number is greater than the second one | > |
InArray | Check if something contains in array or Arr | in_array |
InText | Check if string or Txt contains in other string or Arr | strcontains |
IsEmpty | Check if string, Txt, array or Arr is empty | empty() |
IsEqual | Check if one mixed element is equal to another | === |
IsNotEmpty | Chekc if string, Txt, array or Arr is not empty | !empty() |
IsNotEqual | Check if one mixed element is not equal to another | !== |
IsNull | Check if given element is null | === null |
IsNotNull | Check if given element is not null | !== null |
KeyExists | Check if key exists in array or Arr | array_key_exists |
LessOrEqual | Check if first given number is less than or equal to the second one | <= |
LessThan | Check if first given number is less than the second one | < |
LogicCond | Alias of LogicFork | - |
LogicFork | Behaves like first given logic if given condition is TRUE, like second otherwise | - |
LogicOf | Allows you to create Logic from bool or function | - |
LogicSticky | Logic with caching mechanism | - |
LogicWrap | Envelope for Logic classes | - |
Not | Logical Not | ! |
PregMatch | Check if string or Txt is match to regular expression | preg_match |
See Logic unit tests for better undestanding.
Elegant numbers. Num
interface has only one method asNumber()
that must return float
or int
.
Class | Description | PHP |
---|---|---|
ArraySum | Alias of SumOf | array_sum |
Divided | Division | a / b |
EnsureNum | Helper trait for casting number or Num to float or int | - |
LengthOf | Length of string, Txt, array or Arr | strlen, count |
MaxOf | Max number | max |
MinOf | Min number | min |
Multiplied | Multiplication | a * b |
NumFork | Behaves like first number if condition is TRUE, like second otherwise | - |
NumCond | Alias of NumFork | - |
NumIf | Behaves like given number if condition is TRUE, like 0 otherwise | - |
NumOf | Allows to create Num from float, int, Any or function | - |
NumSticky | Num with caching mechanism | - |
NumWrap | Envelope for Num classes | - |
Rounded | Rounded number | round |
SumOf | Sum of given numbers or Nums | array_sum |
See Num unit tests for better undestanding.
Elegant strings. Txt
interface has only one method asString()
that must return string
.
There is one more interface that you can use in your own classes - StringableTxt
that extends Txt
and \Stringable
.
\Stringable
allows you to cast your class to string via calling __toString()
method. So instead of Txt
you can use StringableTxt
interface and TxtToString
helper trait that calls asString()
behind the scene.
Here's an example for better undestanding:
use Maxonfjvipon\ElegantElephant\Txt\StringableTxt;
use Maxonfjvipon\ElegantElephant\Txt\TxtToString;
use Maxonfjvipon\ElegantElephant\Txt;
class MyTxt implements Txt { /** code */ }
class MyStringableTxt implements StringableTxt {
use TxtToString;
/** code */
}
$txt = new MyTxt();
$stringableTxt = new MyStringableTxt();
echo $txt->asString(); // good
echo $stringableTxt; // good, no actual calling asString()
echo $stringableTxt->asString(); // good, but verbose
echo $txt; // wrong
All Txt
classes in the library implements StringableTxt
.
Class | Description | PHP |
---|---|---|
EnsureTxt | Helper trait for casting string or Txt to string | - |
TxtBlank | Empty text. | "" |
TxtFork | Behaves like first text if condition is TRUE, like second otherwise | - |
TxtCond | Alias of TxtFork | - |
TxtIf | Behaves like given text if condition is TRUE, likey empty otherwise | - |
TxtImploded | Imploded text by separator | implode |
TxtJoined | Joined text | join("", [...]) |
TxtJsonEncoded | Object encoded to JSON as | json_encoded |
TxtLowered | Text in a lower case | strtolower |
TxtLtrimmed | Text trimmed from left | ltrim |
TxtOf | Allows to create Txt from string, number, float, int, Any of function. | - |
TxtPregReplaced | Text with replacements by regex | preg_replace |
TxtReplaced | Text with replacements | str_replace |
TxtRtimmed | Text trimmed from right | rtrim |
TxtSticky | Text with caching mechanism | - |
TxtSubstr | Sub-text | substr |
TxtToString | Default implementation converting Txt to string via __toString() | - |
TxtTrimmed | Text trimmed | trim |
TxtUpper | Text in a upper case | strtoupper |
TxtWrap | Envelope for Txt classes | - |
See Txt unit test for better undestanding.
Fork repository, make changes, send a pull request. To avoid frustration, before sending your pull request please run:
$ ./pre-push.sh
And make sure you have no errors.