Magento 2 on PHP 8.5: Having Fun with the Pipe Operator

    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/else and 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.

    Leave a Reply

    Your email address will not be published. Required fields are marked *