PHP 8.5 adds a tiny new syntax feature that fits Magento 2 code really well: the pipe operator |>.
It doesn’t give you new superpowers, but it makes a ton of existing Magento patterns cleaner and easier to read—especially anything that looks like “take this value, transform it, transform it again, transform it again…”.
Let’s walk through how Magento 2 code can look once you’re on PHP 8.5 and start leaning into |>.
Quick recap: what |> actually does
The pipe operator lets you write this:
$result = " Hello World "
|> trim(...)
|> strtoupper(...)
|> strrev(...);
instead of:
$result = strrev(strtoupper(trim(" Hello World ")));
or the classic temp-variable ladder:
$str = " Hello World ";
$str = trim($str);
$str = strtoupper($str);
$result = strrev($str);
Rules of the game:
- The value on the left is passed as the first (and required) argument to whatever’s on the right.
- The right side must be a callable that takes (at least) one argument:
trim(...),$this->doSomething(...),SomeClass::staticMethod(...)fn ($x) => ...
- It evaluates left → right, line by line.
Once you see it as “a nice way to chain transformations”, it clicks.
1. Collections & view models
Magento loves collections and data massaging. That’s a perfect fit for |>.
Before:
$collection = $this->productCollectionFactory->create();
$collection->addAttributeToSelect('*');
$collection->addMinimalPrice()
->addFinalPrice();
$items = $collection->getItems();
$items = array_filter($items, fn(Product $p) => $p->isSaleable());
$items = array_map([$this, 'mapProductToDto'], $items);
$items = $this->sortProductsByPrice($items);
After, with pipe-friendly helpers:
$products = $this->productCollectionFactory->create()
|> $this->loadProductCollection(...)
|> $this->filterSaleableProducts(...)
|> $this->mapProductsToDto(...)
|> $this->sortProductsByPrice(...);
And the helpers:
private function loadProductCollection(
\Magento\Catalog\Model\ResourceModel\Product\Collection $collection
): array {
$collection->addAttributeToSelect('*')
->addMinimalPrice()
->addFinalPrice();
return $collection->getItems();
}
private function filterSaleableProducts(array $products): array
{
return array_filter(
$products,
static fn(\Magento\Catalog\Model\Product $p) => $p->isSaleable()
);
}
Each step does one thing, takes one argument, returns something. The pipe line at the top is basically the “story” of how your data flows.
2. Request parameter cleanup
Controllers and ViewModels always normalize request params. The pipe operator makes those little “sanitize” sequences easier to scan.
Before:
$page = (int) $this->getRequest()->getParam('p', 1);
$page = max($page, 1);
$page = min($page, self::MAX_PAGE);
After:
$page = $this->getRequest()->getParam('p', 1)
|> 'intval'
|> fn (int $p) => max($p, 1)
|> fn (int $p) => min($p, self::MAX_PAGE);
Wrap the pattern once and reuse it:
private function sanitizeIntParam(string $name, int $default, int $min, int $max): int
{
return $this->getRequest()->getParam($name, $default)
|> 'intval'
|> fn (int $v) => max($min, $v)
|> fn (int $v) => min($max, $v);
}
// Usage
$page = $this->sanitizeIntParam('p', 1, 1, self::MAX_PAGE);
3. Config pipelines
Config values almost always need a bunch of cleanup: cast, trim, explode, filter.
Before:
$value = $this->scopeConfig->getValue(
'vendor_module/feature/allowed_ids',
\Magento\Store\Model\ScopeInterface::SCOPE_STORE
);
$value = (string) $value;
$value = trim($value);
$value = strtolower($value);
$ids = array_filter(explode(',', $value));
After:
$ids = $this->scopeConfig
->getValue('vendor_module/feature/allowed_ids', \Magento\Store\Model\ScopeInterface::SCOPE_STORE)
|> 'strval'
|> 'trim'
|> 'strtolower'
|> fn (string $v) => explode(',', $v)
|> 'array_filter';
You read that top-to-bottom and instantly see what’s going on.
4. Service workflows
Application services (e.g. cart updates, order processing) are basically “do X, then Y, then Z” pipelines already.
Before:
$cart = $this->cartRepository->get($cartId);
$cart = $this->discountApplier->apply($cart, $discountCode);
$cart = $this->shippingEstimator->estimate($cart);
$cart = $this->taxCalculator->calculate($cart);
$cart = $this->totalsCollector->collect($cart);
$this->cartRepository->save($cart);
After:
$cart = $this->cartRepository->get($cartId)
|> fn ($cart) => $this->discountApplier->apply($cart, $discountCode)
|> $this->shippingEstimator->estimate(...)
|> $this->taxCalculator->calculate(...)
|> $this->totalsCollector->collect(...);
$this->cartRepository->save($cart);
Or, if you’re in the mood, including the save as the final step:
$this->cartRepository->get($cartId)
|> fn ($cart) => $this->discountApplier->apply($cart, $discountCode)
|> $this->shippingEstimator->estimate(...)
|> $this->taxCalculator->calculate(...)
|> $this->totalsCollector->collect(...)
|> fn ($cart) => $this->cartRepository->save($cart);
That’s a nice, linear description of the workflow.
5. Import / export & ETL-style code
Importers and exporters scream “pipeline”: normalize → validate → enrich → persist.
$row
|> $this->normalizeRow(...)
|> $this->mapExternalToInternalSkus(...)
|> $this->validateRow(...)
|> $this->saveRow(...);
Signatures stay simple:
private function normalizeRow(array $row): array;
private function mapExternalToInternalSkus(array $row): array;
private function validateRow(array $row): array; // throws on errors
private function saveRow(array $row): void;
The last step returning void is fine, since it’s the end of the chain.
6. Plugins and observers
After-plugins
After-plugins usually adjust a result in a couple of steps. Perfect pipe territory.
public function afterGetPrice($subject, float $result): float
{
return $result
|> fn (float $price) => $this->applyTierPrice($subject, $price)
|> fn (float $price) => $this->roundPrice($price)
|> fn (float $price) => $this->applyCustomerGroupAdjustment($subject, $price);
}
You instantly see the sequence of tweaks applied.
Observers
Observers are often “grab event data, build a payload, send it somewhere”.
public function execute(\Magento\Framework\Event\Observer $observer): void
{
$order = $observer->getEvent()->getOrder();
$payload = $order
|> $this->orderToExportPayload(...)
|> $this->maskSensitiveData(...)
|> $this->serializePayload(...);
$this->queuePublisher->publish('order.export', $payload);
}
Again: straight line from source to sink.
7. Writing “pipe-friendly” Magento code
The new operator pays off more if you nudge your APIs in its direction.
7.1 One main argument, return something
Instead of designing methods like:
public function process(array &$data): void;
prefer:
public function process(array $data): array;
Same for domain classes: take one main value, return a new value (or the same object). That naturally fits into value |> method(...).
7.2 Tiny callable helpers
A neat trick is to expose pipe-ready callables:
class DiscountApplier
{
public function apply(string $code): callable
{
return fn (CartInterface $cart): CartInterface =>
$this->applyDiscount($cart, $code);
}
private function applyDiscount(CartInterface $cart, string $code): CartInterface
{
// ...
}
}
Now your workflow reads:
$cart = $cart
|> $this->discountApplier->apply($discountCode)
|> $this->taxCalculator->calculate(...)
|> $this->totalsCollector->collect(...);
7.3 Invokable utilities
Instead of static utility classes, small invokable services slot nicely into pipelines:
class FilterSaleableProducts
{
public function __invoke(array $products): array
{
return array_filter(
$products,
static fn(\Magento\Catalog\Model\Product $p) => $p->isSaleable()
);
}
}
Used like:
$products = $collection->getItems()
|> $this->filterSaleableProducts(...)
|> $this->mapProductsToDto(...);
8. When not to use the pipe operator
Don’t force it everywhere. A few cases where plain old code is simpler:
- Heavy branching logic with lots of
if/elseand early returns. - Side-effect-heavy methods (sending emails, writing files, hitting multiple repositories) that don’t really feel like “transform X into Y”.
- Very long, twisty flows – if your pipe chain is 10+ steps and full of complex closures, breaking it into named methods is usually clearer.
As a rule of thumb: if you’re transforming data and each step is small and focused, the pipe operator almost always improves readability. If you’re orchestrating a multi-headed hydra of side effects, maybe keep that in a classic service method and use pipes only around the data shaping.
Wrap-up
Once you’re on PHP 8.5, the pipe operator gives Magento 2 code a nice little boost:
- Cleaner chains for collections, config values, and DTOs.
- More readable plugins, observers, and service workflows.
- A gentle push towards pure-ish, one-argument, testable functions.
You don’t have to rewrite everything. Just start by:
- Extracting small transformation helpers,
- Making them take a single main argument and return a value,
- And then wiring them together with
|>where it actually makes the code easier to follow.
If you want, you can drop in a real chunk of your Magento code and we can refactor it into a “pipe-first” version side by side.
