{"id":123,"date":"2025-11-21T11:36:24","date_gmt":"2025-11-21T11:36:24","guid":{"rendered":"https:\/\/magendoo.ro\/insights\/?p=123"},"modified":"2025-11-21T11:36:24","modified_gmt":"2025-11-21T11:36:24","slug":"magento-2-on-php-8-5-having-fun-with-the-pipe-operator","status":"publish","type":"post","link":"https:\/\/magendoo.ro\/insights\/magento-2-on-php-8-5-having-fun-with-the-pipe-operator\/","title":{"rendered":"Magento 2 on PHP 8.5: Having Fun with the Pipe Operator"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">PHP 8.5 adds a tiny new syntax feature that fits Magento 2 code <em>really<\/em> well: the pipe operator <code>|&gt;<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It doesn\u2019t give you new superpowers, but it makes a ton of existing Magento patterns cleaner and easier to read\u2014especially anything that looks like \u201ctake this value, transform it, transform it again, transform it again\u2026\u201d.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Let\u2019s walk through how Magento 2 code can look once you\u2019re on PHP 8.5 and start leaning into <code>|&gt;<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Quick recap: what <code>|&gt;<\/code> actually does<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The pipe operator lets you write this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$result = \" Hello World \"\n    |&gt; trim(...)\n    |&gt; strtoupper(...)\n    |&gt; strrev(...);\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">instead of:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$result = strrev(strtoupper(trim(\" Hello World \")));\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">or the classic temp-variable ladder:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$str = \" Hello World \";\n$str = trim($str);\n$str = strtoupper($str);\n$result = strrev($str);\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Rules of the game:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The value on the <strong>left<\/strong> is passed as the <strong>first (and required) argument<\/strong> to whatever\u2019s on the <strong>right<\/strong>.<\/li>\n\n\n\n<li>The right side must be <strong>a callable<\/strong> that takes (at least) one argument:\n<ul class=\"wp-block-list\">\n<li><code>trim(...)<\/code>, <code>$this->doSomething(...)<\/code>, <code>SomeClass::staticMethod(...)<\/code><\/li>\n\n\n\n<li><code>fn ($x) => ...<\/code><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>It evaluates left \u2192 right, line by line.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Once you see it as \u201ca nice way to chain transformations\u201d, it clicks.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">1. Collections &amp; view models<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Magento loves collections and data massaging. That\u2019s a perfect fit for <code>|&gt;<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Before:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$collection = $this-&gt;productCollectionFactory-&gt;create();\n$collection-&gt;addAttributeToSelect('*');\n$collection-&gt;addMinimalPrice()\n           -&gt;addFinalPrice();\n\n$items = $collection-&gt;getItems();\n\n$items = array_filter($items, fn(Product $p) =&gt; $p-&gt;isSaleable());\n$items = array_map(&#91;$this, 'mapProductToDto'], $items);\n$items = $this-&gt;sortProductsByPrice($items);\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>After, with pipe-friendly helpers:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$products = $this-&gt;productCollectionFactory-&gt;create()\n    |&gt; $this-&gt;loadProductCollection(...)\n    |&gt; $this-&gt;filterSaleableProducts(...)\n    |&gt; $this-&gt;mapProductsToDto(...)\n    |&gt; $this-&gt;sortProductsByPrice(...);\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">And the helpers:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>private function loadProductCollection(\n    \\Magento\\Catalog\\Model\\ResourceModel\\Product\\Collection $collection\n): array {\n    $collection-&gt;addAttributeToSelect('*')\n               -&gt;addMinimalPrice()\n               -&gt;addFinalPrice();\n\n    return $collection-&gt;getItems();\n}\n\nprivate function filterSaleableProducts(array $products): array\n{\n    return array_filter(\n        $products,\n        static fn(\\Magento\\Catalog\\Model\\Product $p) =&gt; $p-&gt;isSaleable()\n    );\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each step does one thing, takes one argument, returns something. The pipe line at the top is basically the \u201cstory\u201d of how your data flows.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">2. Request parameter cleanup<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Controllers and ViewModels always normalize request params. The pipe operator makes those little \u201csanitize\u201d sequences easier to scan.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Before:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$page = (int) $this-&gt;getRequest()-&gt;getParam('p', 1);\n$page = max($page, 1);\n$page = min($page, self::MAX_PAGE);\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>After:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$page = $this-&gt;getRequest()-&gt;getParam('p', 1)\n    |&gt; 'intval'\n    |&gt; fn (int $p) =&gt; max($p, 1)\n    |&gt; fn (int $p) =&gt; min($p, self::MAX_PAGE);\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Wrap the pattern once and reuse it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>private function sanitizeIntParam(string $name, int $default, int $min, int $max): int\n{\n    return $this-&gt;getRequest()-&gt;getParam($name, $default)\n        |&gt; 'intval'\n        |&gt; fn (int $v) =&gt; max($min, $v)\n        |&gt; fn (int $v) =&gt; min($max, $v);\n}\n\n\/\/ Usage\n$page = $this-&gt;sanitizeIntParam('p', 1, 1, self::MAX_PAGE);\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">3. Config pipelines<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Config values almost always need a bunch of cleanup: cast, trim, explode, filter.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Before:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$value = $this-&gt;scopeConfig-&gt;getValue(\n    'vendor_module\/feature\/allowed_ids',\n    \\Magento\\Store\\Model\\ScopeInterface::SCOPE_STORE\n);\n\n$value = (string) $value;\n$value = trim($value);\n$value = strtolower($value);\n$ids   = array_filter(explode(',', $value));\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>After:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ids = $this-&gt;scopeConfig\n    -&gt;getValue('vendor_module\/feature\/allowed_ids', \\Magento\\Store\\Model\\ScopeInterface::SCOPE_STORE)\n    |&gt; 'strval'\n    |&gt; 'trim'\n    |&gt; 'strtolower'\n    |&gt; fn (string $v) =&gt; explode(',', $v)\n    |&gt; 'array_filter';\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You read that top-to-bottom and instantly see what\u2019s going on.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">4. Service workflows<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Application services (e.g. cart updates, order processing) are basically \u201cdo X, then Y, then Z\u201d pipelines already.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Before:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$cart = $this-&gt;cartRepository-&gt;get($cartId);\n\n$cart = $this-&gt;discountApplier-&gt;apply($cart, $discountCode);\n$cart = $this-&gt;shippingEstimator-&gt;estimate($cart);\n$cart = $this-&gt;taxCalculator-&gt;calculate($cart);\n$cart = $this-&gt;totalsCollector-&gt;collect($cart);\n\n$this-&gt;cartRepository-&gt;save($cart);\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>After:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$cart = $this-&gt;cartRepository-&gt;get($cartId)\n    |&gt; fn ($cart) =&gt; $this-&gt;discountApplier-&gt;apply($cart, $discountCode)\n    |&gt; $this-&gt;shippingEstimator-&gt;estimate(...)\n    |&gt; $this-&gt;taxCalculator-&gt;calculate(...)\n    |&gt; $this-&gt;totalsCollector-&gt;collect(...);\n\n$this-&gt;cartRepository-&gt;save($cart);\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Or, if you\u2019re in the mood, including the save as the final step:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$this-&gt;cartRepository-&gt;get($cartId)\n    |&gt; fn ($cart) =&gt; $this-&gt;discountApplier-&gt;apply($cart, $discountCode)\n    |&gt; $this-&gt;shippingEstimator-&gt;estimate(...)\n    |&gt; $this-&gt;taxCalculator-&gt;calculate(...)\n    |&gt; $this-&gt;totalsCollector-&gt;collect(...)\n    |&gt; fn ($cart) =&gt; $this-&gt;cartRepository-&gt;save($cart);\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That\u2019s a nice, linear description of the workflow.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">5. Import \/ export &amp; ETL-style code<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Importers and exporters scream \u201cpipeline\u201d: normalize \u2192 validate \u2192 enrich \u2192 persist.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$row\n    |&gt; $this-&gt;normalizeRow(...)\n    |&gt; $this-&gt;mapExternalToInternalSkus(...)\n    |&gt; $this-&gt;validateRow(...)\n    |&gt; $this-&gt;saveRow(...);\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Signatures stay simple:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>private function normalizeRow(array $row): array;\nprivate function mapExternalToInternalSkus(array $row): array;\nprivate function validateRow(array $row): array;   \/\/ throws on errors\nprivate function saveRow(array $row): void;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The last step returning <code>void<\/code> is fine, since it\u2019s the end of the chain.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">6. Plugins and observers<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">After-plugins<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">After-plugins usually adjust a result in a couple of steps. Perfect pipe territory.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public function afterGetPrice($subject, float $result): float\n{\n    return $result\n        |&gt; fn (float $price) =&gt; $this-&gt;applyTierPrice($subject, $price)\n        |&gt; fn (float $price) =&gt; $this-&gt;roundPrice($price)\n        |&gt; fn (float $price) =&gt; $this-&gt;applyCustomerGroupAdjustment($subject, $price);\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You instantly see the sequence of tweaks applied.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Observers<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Observers are often \u201cgrab event data, build a payload, send it somewhere\u201d.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public function execute(\\Magento\\Framework\\Event\\Observer $observer): void\n{\n    $order = $observer-&gt;getEvent()-&gt;getOrder();\n\n    $payload = $order\n        |&gt; $this-&gt;orderToExportPayload(...)\n        |&gt; $this-&gt;maskSensitiveData(...)\n        |&gt; $this-&gt;serializePayload(...);\n\n    $this-&gt;queuePublisher-&gt;publish('order.export', $payload);\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Again: straight line from source to sink.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">7. Writing \u201cpipe-friendly\u201d Magento code<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The new operator pays off more if you nudge your APIs in its direction.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">7.1 One main argument, return something<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of designing methods like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public function process(array &amp;$data): void;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">prefer:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public function process(array $data): array;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Same for domain classes: take one main value, return a new value (or the same object). That naturally fits into <code>value |&gt; method(...)<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">7.2 Tiny callable helpers<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A neat trick is to expose pipe-ready callables:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class DiscountApplier\n{\n    public function apply(string $code): callable\n    {\n        return fn (CartInterface $cart): CartInterface =&gt;\n            $this-&gt;applyDiscount($cart, $code);\n    }\n\n    private function applyDiscount(CartInterface $cart, string $code): CartInterface\n    {\n        \/\/ ...\n    }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now your workflow reads:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$cart = $cart\n    |&gt; $this-&gt;discountApplier-&gt;apply($discountCode)\n    |&gt; $this-&gt;taxCalculator-&gt;calculate(...)\n    |&gt; $this-&gt;totalsCollector-&gt;collect(...);\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">7.3 Invokable utilities<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of static utility classes, small invokable services slot nicely into pipelines:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class FilterSaleableProducts\n{\n    public function __invoke(array $products): array\n    {\n        return array_filter(\n            $products,\n            static fn(\\Magento\\Catalog\\Model\\Product $p) =&gt; $p-&gt;isSaleable()\n        );\n    }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Used like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$products = $collection-&gt;getItems()\n    |&gt; $this-&gt;filterSaleableProducts(...)\n    |&gt; $this-&gt;mapProductsToDto(...);\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">8. When <em>not<\/em> to use the pipe operator<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Don\u2019t force it everywhere. A few cases where plain old code is simpler:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Heavy branching logic<\/strong> with lots of <code>if<\/code>\/<code>else<\/code> and early returns.<\/li>\n\n\n\n<li><strong>Side-effect-heavy methods<\/strong> (sending emails, writing files, hitting multiple repositories) that don\u2019t really feel like \u201ctransform X into Y\u201d.<\/li>\n\n\n\n<li><strong>Very long, twisty flows<\/strong> \u2013 if your pipe chain is 10+ steps and full of complex closures, breaking it into named methods is usually clearer.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">As a rule of thumb: if you\u2019re transforming data and each step is small and focused, the pipe operator almost always improves readability. If you\u2019re orchestrating a multi-headed hydra of side effects, maybe keep that in a classic service method and use pipes only around the data shaping.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Wrap-up<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Once you\u2019re on PHP 8.5, the pipe operator gives Magento 2 code a nice little boost:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Cleaner chains for collections, config values, and DTOs.<\/li>\n\n\n\n<li>More readable plugins, observers, and service workflows.<\/li>\n\n\n\n<li>A gentle push towards pure-ish, one-argument, testable functions.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">You don\u2019t have to rewrite everything. Just start by:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Extracting small transformation helpers,<\/li>\n\n\n\n<li>Making them take a single main argument and return a value,<\/li>\n\n\n\n<li>And then wiring them together with <code>|><\/code> where it actually makes the code easier to follow.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If you want, you can drop in a real chunk of your Magento code and we can refactor it into a \u201cpipe-first\u201d version side by side.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>PHP 8.5 adds a tiny new syntax feature that fits Magento 2 code really well: the pipe operator |&gt;. It doesn\u2019t give you new superpowers, but it makes a ton of existing Magento patterns cleaner and easier to read\u2014especially anything that looks like \u201ctake this value, transform it, transform it again, transform it again\u2026\u201d. Let\u2019s [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":124,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"site-container-style":"default","site-container-layout":"default","site-sidebar-layout":"default","disable-article-header":"default","disable-site-header":"default","disable-site-footer":"default","disable-content-area-spacing":"default","footnotes":""},"categories":[9],"tags":[],"class_list":["post-123","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-magento-2"],"_links":{"self":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts\/123","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/comments?post=123"}],"version-history":[{"count":1,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts\/123\/revisions"}],"predecessor-version":[{"id":125,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts\/123\/revisions\/125"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/media\/124"}],"wp:attachment":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/media?parent=123"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/categories?post=123"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/tags?post=123"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}