Оператор pipe став важливим доповненням у PHP 8.5. Він поєднує ергономіку об’єктів-обгорток із гнучкістю окремих функцій. На жаль, він поки не набув масового вжитку. Стандартні функції PHP для маніпуляцій з array та string відомі своєю непослідовністю, що робить використання оператора pipe доволі незручним.
Piper — це спроба адаптувати стандартну бібліотеку PHP так, щоб вона стала сумісною з оператором pipe.
Пакет містить хелпери для array:
use function Spatie\Piper\Arr\{filter, map};
$popular = $posts
|> filter(fn (Post $post) => $post->views > 1000)
|> map(fn (Post $post) => $post->title);
// ["Claude Talk Small. Code Still Big.", …]
Та хелпери для string:
use function Spatie\Piper\Str\{lower, replace};
'Hello, world!'
|> lower()
|> replace('world', 'Piper')
// "hello, Piper!"
Оскільки всі функції працюють із примітивами, їх можна вільно комбінувати:
use function Spatie\Piper\Arr\{filter, join, map, values};
use function Spatie\Piper\Str\{prefix, suffix};
[1, 2, 3, 4, 5, 6]
|> filter(fn (int $i) => $i % 2 === 0)
|> map(fn (int $i) => pow($i, 2))
|> values()
|> join(', ', ', and ')
|> prefix('The winning numbers are ')
|> suffix('.');
// "The winning numbers are 4, 16, and 36."
Piper доступний на GitHub, його можна встановити через Composer:
composer require spatie/piper
Чому оператор pipe — це золота середина
Оператор pipe створений для того, щоб зробити послідовний код чистим і зрозумілим.
Без нього нам доводиться створювати купу проміжних змінних, коли ми хочемо виконати кілька операцій над одним значенням.
$orders = [
['id' => 1, 'status' => 'paid', 'total' => 120],
['id' => 2, 'status' => 'pending', 'total' => 80],
['id' => 3, 'status' => 'paid', 'total' => 45],
['id' => 4, 'status' => 'paid', 'total' => 200],
];
$largePaidOrders = array_filter(
$orders,
fn (array $order) =>
$order['status'] === 'paid' && $order['total'] > 100
);
$report = array_map(fn (array $order) => [
'id' => $order['id'],
'amount' => '$' . number_format($order['total'], 2),
], $largePaidOrders);
Якщо ми хочемо позбутися проміжних змінних, можна передавати результат попередньої функції безпосередньо в наступну. Але тоді код доведеться писати у порядку, зворотному до його виконання.
$report = array_map(
fn (array $order) => [
'id' => $order['id'],
'amount' => '$' . number_format($order['total'], 2),
],
array_filter($orders, fn (array $order) =>
$order['status'] === 'paid' && $order['total'] > 100
)
);
На перший погляд важко навіть визначити, де починається цей вираз (підказка: зі змінної $orders всередині виклику array_filter).
У чистому PHP (vanilla PHP) доводиться обирати між випадковими проміжними змінними або кодом-«матріошкою», де все вкладено навиворіт.
Інший варіант — обгорнути масив в об'єкт із fluent API, наприклад, у чудовий Collection від Laravel.
$report = collect($orders)
->filter(fn (array $order) =>
$order['status'] === 'paid' && $order['total'] > 100
)
->map(fn (array $order) => [
'id' => $order['id'],
'amount' => '$' . number_format($order['total'], 2),
])
->all();
Проте об’єкти-обгортки мають власні недоліки:
- Їх потрібно «розпаковувати» для подальшого використання (як-от виклик
->all()вище). - Їх важко розширювати. Якщо потрібен метод для специфічної доменної логіки, доведеться створювати кастомний об'єкт колекції. Це шкодить композиції, оскільки код розсіюється між різними класами.
Тут на сцену виходить оператор |>. Результат попереднього виразу передається у наступну функцію в ланцюжку. Це дозволяє використовувати звичайні функції, не обгортаючи дані в об’єкти, і при цьому зберігати логічний порядок читання коду.
$report = $orders
|> (fn (array $orders) => array_filter($orders, fn (array $order) =>
$order['status'] === 'paid' && $order['total'] > 100
))
|> (fn (array $orders) => array_map(fn (array $order) => [
'id' => $order['id'],
'amount' => '$' . number_format($order['total'], 2),
], $orders));
У поточному вигляді оператор pipe має обмеження: функція, в яку ми передаємо дані, повинна приймати рівно один аргумент. Якщо це не так, доводиться використовувати closure.
Приклад, що повертає всі парні числа з масиву:
[1, 2, 3, 4]
|> (fn (array $numbers) => array_filter($numbers, fn (int $i) => $i % 2 === 0));
Крім того, стандартна бібліотека PHP тягне за собою вантаж минулого з хаотичним порядком аргументів. Тому, попри наявність оператора pipe у мові, він не завжди може розкрити свій потенціал.
Piper: бібліотека утиліт, орієнтована на pipe
Piper — це адаптація методів Laravel для роботи з колекціями та рядками, оптимізована саме для оператора pipe.
use function Spatie\Piper\Arr\filter;
[1, 2, 3, 4]
|> filter(fn (int $i) => $i % 2 === 0);
Повернемося до прикладу зі звітом $report:
use function Spatie\Piper\Arr\{filter, map};
$report = $orders
|> filter(fn (array $order) =>
$order['status'] === 'paid' && $order['total'] > 100
)
|> map(fn (array $order) => [
'id' => $order['id'],
'amount' => '$' . number_format($order['total'], 2),
]);
Це вирішує всі проблеми: жодних зайвих змінних, а порядок коду відповідає порядку виконання. Ідеально!
Що означає «оптимізовано для оператора pipe»? Кожна функція є функцією вищого порядку (higher-order function), яка повертає функцію, що приймає рівно один аргумент.
Ось як виглядає функція filter у Piper:
function filter(?callable $callback = null): Closure
{
return function (array $items) use ($callback): array {
return $callback === null
? array_filter($items)
: array_filter($items, $callback, ARRAY_FILTER_USE_BOTH);
};
}
Без оператора pipe виклик виглядав би так:
filter(fn (int $i) => $i % 2 === 0)([1, 2, 3, 4]);
// [1 => 2, 3 => 4]
Аргументи, передані у зовнішню функцію, створюють нову функцію, яка вже обробляє вхідні дані. Це називається частковим застосуванням (partial application), яке стане ще зручнішим у PHP 8.6.
Piper підтримує більшість функцій для роботи з рядками та колекціями з Laravel. Повний список доступний у README.
Погляд у майбутнє
Можливо, я ніколи не буду використовувати цю бібліотеку. Можливо, я взагалі не буду використовувати оператор pipe. Ера «агентного програмування» та ШІ поступово нівелює значення синтаксичної естетики. Програмування майбутнього буде зосереджене на результативності виконання. Боротьба за те, щоб код «виглядав красиво», здається, вщухає — ШІ зміщує наш фокус на результат, а не на чистоту кожного рядка. Це не обов’язково погано, але частина нашого ремесла може бути втрачена.
Іронічно, що я наважився написати Piper саме тому, що маю під рукою ШІ. Ручне портування 234 функцій і написання тестів — це не те, чим я хотів би займатися. Натомість час, витрачений на розробку цього пакета, вимірюється годинами.
У будь-якому разі, це цікавий експеримент. Я планую попрацювати з ним у реальних проєктах. Якщо ідея приживеться, наступним кроком стане spatie/laravel-piper з глибшою інтеграцією в Laravel (наприклад, підтримка Arrayable, LazyCollection тощо без попереднього приведення до масиву).