PHP 8.5 RC1 wyszło w sierpniu 2025. Przez ostatnie tygodnie testowałem na kilku projektach. Pipe operator działa dokładnie tak jak RFC obiecywało – i zmienia sposób pisania przetwarzania danych bardziej niż jakikolwiek feature od czasu match() w PHP 8.0. Ale RC1 przyniosło też kilka niespodzianek których nie było w RFC drafcie. Raport z pierwszych tygodni.
Pipe Operator – pierwsze dwa tygodnie w kodzie
Zacząłem od refaktoringu starszego kodu przetwarzania danych importu. Wyniki mówią same za siebie:
<?php
declare(strict_types=1);
// Stary kod - trudny do czytania, wymaga śledzenia zagnieżdżeń
function processImportRow(array $rawRow): ?array
{
return array_filter(
array_combine(
array_map('strtolower', array_keys($rawRow)),
array_map('trim', array_values($rawRow))
),
fn($v) => $v !== ''
) ?: null;
}
// PHP 8.5 - ten sam algorytm, czytelny kierunek przepływu
function processImportRow(array $rawRow): ?array
{
return $rawRow
|> array_combine(
array_map('strtolower', array_keys($$)),
array_map('trim', array_values($$))
)
|> array_filter($$, fn($v) => $v !== '')
|> (fn($a) => $a ?: null)($$)
;
}
<?php
declare(strict_types=1);
// Szerszy przykład – pipeline przetwarzania zamówień
class OrderExportPipeline
{
public function export(array $orders): string
{
return $orders
|> array_filter($$, fn($o) => $o->getStatus() !== 'cancelled')
|> array_map($$, fn($o) => $this->toExportDto($o))
|> array_filter($$, fn($dto) => $dto->isValid())
|> $this->sortByDate($$)
|> $this->formatAsCsv($$)
;
}
private function sortByDate(array $dtos): array
{
usort($dtos, fn($a, $b) => $a->createdAt <=> $b->createdAt);
return $dtos;
}
private function formatAsCsv(array $dtos): string
{
$lines = array_map(fn($dto) => implode(',', [
$dto->orderId,
$dto->total,
$dto->createdAt->format('Y-m-d'),
]), $dtos);
return implode("\n", array_merge(['id,total,date'], $lines));
}
private function toExportDto(object $order): object
{
// ... mapowanie
return new \stdClass();
}
}
Niespodzianki i pułapki z RC1
<?php
declare(strict_types=1);
// Pułapka 1: $$ to placeholder dla wartości z lewej strony pipe
// Nie wszędzie można go pominąć – zależy od oczekiwanej sygnatury funkcji
// OK - funkcja przyjmuje array jako pierwszy argument
$result = [1, 2, 3]
|> array_reverse($$) // array_reverse([1, 2, 3])
|> array_sum($$) // array_sum([3, 2, 1]) = 6
;
// OK - closure
$result = "hello"
|> fn($s) => strtoupper($s)($$) // Uwaga: closure z natychmiastowym wywołaniem
|> strlen($$)
;
// Pułapka 2: metody na obiekcie wymagają closure lub ->method()
$order = new Order(42);
// To NIE działa bezpośrednio:
// $result = $order |> ->getStatus() // SyntaxError
// Obejście przez closure:
$result = $order
|> fn($o) => $o->getStatus()($$) // verbose ale działa
;
// Pułapka 3: void funkcje w pipe
// array_walk modyfikuje in-place i zwraca bool, nie tablicę
// Nie używaj void funkcji w środku pipeline
// Pułapka 4: null safety
// Pipe nie ma wbudowanego short-circuit dla null
// Jeśli któryś krok zwróci null i następny tego nie obsługuje – TypeError
$result = null
|> trim($$) // TypeError: trim(): Argument #1 must be of type string, null given
;
// Ochrona:
$result = $maybeNull
|> ($$ !== null ? trim($$) : null)
|> ($$ !== null ? strtoupper($$) : null)
;
Readonly dziedziczenie – działa zgodnie z RFC
<?php
declare(strict_types=1);
// Potwierdzenie z RC1 - readonly class może dziedziczyć po readonly class
readonly class Money
{
public function __construct(
public int $amount, // w groszach
public string $currency
) {}
public function add(Money $other): static
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException('Currency mismatch');
}
return new static($this->amount + $other->amount, $this->currency);
}
}
// PHP 8.5 - OK!
readonly class TaxedMoney extends Money
{
public function __construct(
int $amount,
string $currency,
public float $taxRate
) {
parent::__construct($amount, $currency);
}
public function netAmount(): int
{
return (int) round($this->amount / (1 + $this->taxRate));
}
public function taxAmount(): int
{
return $this->amount - $this->netAmount();
}
}
readonly class DiscountedMoney extends Money
{
public function __construct(
int $amount,
string $currency,
public float $discountPercent
) {
parent::__construct($amount, $currency);
}
public function originalAmount(): int
{
return (int) round($this->amount / (1 - $this->discountPercent / 100));
}
}
$price = new Money(12300, 'PLN'); // 123.00 PLN
$taxed = new TaxedMoney(12300, 'PLN', 0.23); // 123.00 PLN z 23% VAT
$discount = new DiscountedMoney(12300, 'PLN', 10); // 123.00 PLN po 10% rabacie
echo $taxed->netAmount(); // 10000 (100.00 PLN netto)
echo $discount->originalAmount(); // 13667 (136.67 PLN przed rabatem)
Nowe deprecacje w RC1
<?php
// Deprecacje które pojawiły się w RC1 a nie było ich w early RFC
// 1. zero-argument implode() deprecated
implode($array); // Deprecated - zawsze podawaj separator
implode(',', $array); // Poprawnie
// 2. Niektóre warianty preg_ z null w pattern
preg_match(null, $string); // TypeError (wcześniej E_WARNING)
// 3. Implicit float->int conversion w tablicach
$arr = [];
$arr[1.5] = 'value'; // Deprecated: float 1.5 truncated to int key 1
// Sprawdzenie projektu przez PHPStan
// vendor/bin/phpstan analyse --php-version=8.5 --level=8 src/
Benchmark – czy pipe operator wpływa na wydajność?
<?php
// Krótka odpowiedź: minimalny wpływ – pipe operator to syntactic sugar
// kompilowany do identycznych opcodes co zagnieżdżone wywołania
$data = range(1, 10000);
// Wersja zagnieżdżona
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
$result1 = array_sum(array_filter(array_map(fn($x) => $x * 2, $data), fn($x) => $x % 3 === 0));
}
$nested = (microtime(true) - $start) * 1000;
// Wersja z pipe
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
$result2 = $data
|> array_map(fn($x) => $x * 2, $$)
|> array_filter($$, fn($x) => $x % 3 === 0)
|> array_sum($$)
;
}
$piped = (microtime(true) - $start) * 1000;
echo "Zagnieżdżone: {$nested}ms\n"; // np. 142ms
echo "Pipe: {$piped}ms\n"; // np. 143ms - identyczne
// Opcodes są niemal identyczne - pipe to wygoda, nie optymalizacja
Kiedy spodziewać się final release
# Harmonogram PHP 8.5 (szacowany)
# RC1: sierpień 2025
# RC2: wrzesień 2025
# RC3: październik 2025
# Final: 6 lub 20 listopada 2025
# Testuj już teraz na RC
docker pull php:8.5-rc-cli
docker run --rm -v $(pwd):/app php:8.5-rc-cli php /app/test_85.php
# Composer z PHP 8.5 RC (ignoruj platform requirements)
docker run --rm -v $(pwd):/app php:8.5-rc-cli \
composer install --ignore-platform-reqs
Podsumowanie
PHP 8.5 RC1 potwierdza że pipe operator będzie jedną z największych ergonomicznych zmian PHP od lat. Kod przetwarzania danych który pisałem z wieloma zmiennymi tymczasowymi albo głębokim zagnieżdżeniem staje się linearny i czytelny. Readonly dziedziczenie dopełnia Value Object toolkit. RC1 jest stabilne – warto już testować projekty pod kątem nowych deprecacji, żeby final release w listopadzie nie zaskoczył.
