[{"data":1,"prerenderedAt":593},["ShallowReactive",2],{"switcher-blog-pareja":3,"art-php-8-5-six-months-production-en":6},{"en":4,"es":5},"\u002Fen\u002Fblog\u002Fphp-8-5-six-months-production\u002F","\u002Fes\u002Fblog\u002Fphp-8-5-seis-meses-produccion\u002F",{"id":7,"title":8,"author":9,"body":10,"date":578,"description":579,"extension":580,"image":581,"meta":582,"navigation":583,"pareja":584,"path":585,"seo":586,"stem":587,"tags":588,"__hash__":592},"blogEn\u002Fen\u002Fblog\u002Fphp-8-5-six-months-production.md","PHP 8.5 at six months: what we've learned shipping it to real clients","Paco Cubel",{"type":11,"value":12,"toc":559},"minimark",[13,18,22,30,33,37,46,53,56,72,75,103,110,116,122,131,149,155,158,173,188,192,203,223,245,249,253,278,281,285,296,300,303,396,399,403,406,453,457,464,512,519,523,530,533,537,555],[14,15,17],"h2",{"id":16},"what-it-promised-and-what-it-delivered","What it promised and what it delivered",[19,20,21],"p",{},"PHP 8.5 shipped on 20 November 2025. We've been running it on client servers for six months (some in the first week, some only in the March refresh cycle), so we've got data rather than demos.",[19,23,24,25,29],{},"Short version: ",[26,27,28],"strong",{},"it's the cleanest minor in the 8.x line for a low-drama upgrade",". More stable out of the gate than 8.3, less disruptive than 8.4 in breaking changes, and with a couple of features that are genuinely landing in real code rather than just tweets.",[19,31,32],{},"Let's go feature by feature: what we use, what we don't, and what to break before migrating.",[14,34,36],{"id":35},"whats-actually-landing-in-client-code","What's actually landing in client code",[38,39,41,42],"h3",{"id":40},"the-pipe-operator","The pipe operator ",[43,44,45],"code",{},"|>",[19,47,48,49,52],{},"The change that surprised us most. On paper it looked like RFC eye-candy; in real code it's replacing nested ",[43,50,51],{},"array_map"," chains that took effort to read.",[19,54,55],{},"Before:",[57,58,63],"pre",{"className":59,"code":60,"language":61,"meta":62,"style":62},"language-php shiki shiki-themes github-light github-dark","$name = strtoupper(trim(str_replace('-', ' ', $slug)));\n","php","",[43,64,65],{"__ignoreMap":62},[66,67,70],"span",{"class":68,"line":69},"line",1,[66,71,60],{},[19,73,74],{},"After:",[57,76,78],{"className":59,"code":77,"language":61,"meta":62,"style":62},"$name = $slug\n    |> fn($s) => str_replace('-', ' ', $s)\n    |> trim(...)\n    |> strtoupper(...);\n",[43,79,80,85,91,97],{"__ignoreMap":62},[66,81,82],{"class":68,"line":69},[66,83,84],{},"$name = $slug\n",[66,86,88],{"class":68,"line":87},2,[66,89,90],{},"    |> fn($s) => str_replace('-', ' ', $s)\n",[66,92,94],{"class":68,"line":93},3,[66,95,96],{},"    |> trim(...)\n",[66,98,100],{"class":68,"line":99},4,[66,101,102],{},"    |> strtoupper(...);\n",[19,104,105,106,109],{},"Not revolutionary. But in input normalisation pipelines, validation chains and API payload builders, ",[26,107,108],{},"it reads better",", and the next PR author doesn't have to count nested brackets. We're adopting it in new code; old code stays as is, because refactoring for sport is the most expensive disease in the trade.",[38,111,113],{"id":112},"clone-with",[43,114,115],{},"clone with",[19,117,118,119,121],{},"For readonly classes (which we've leaned on without remorse since 8.2), ",[43,120,115],{}," cleans up the \"build a new object with one field changed\" pattern:",[57,123,125],{"className":59,"code":124,"language":61,"meta":62,"style":62},"$order_v2 = clone $order with { status: 'confirmed' };\n",[43,126,127],{"__ignoreMap":62},[66,128,129],{"class":68,"line":69},[66,130,124],{},[19,132,133,134,137,138,141,142,145,146,148],{},"Without this, every class needed a ",[43,135,136],{},"withStatus()"," method or a constructor with N arguments. ",[26,139,140],{},"This one we are retrofitting into existing code",", especially DTOs and value objects. Where there was ",[43,143,144],{},"withX()",", now ",[43,147,115],{},".",[38,150,152],{"id":151},"nodiscard",[43,153,154],{},"#[\\NoDiscard]",[19,156,157],{},"Our favourite and the one with least publicity. An attribute that marks a function as \"the return value matters, don't drop it\":",[57,159,161],{"className":59,"code":160,"language":61,"meta":62,"style":62},"#[\\NoDiscard(\"The transaction may have failed\")]\npublic function execute(): Result { ... }\n",[43,162,163,168],{"__ignoreMap":62},[66,164,165],{"class":68,"line":69},[66,166,167],{},"#[\\NoDiscard(\"The transaction may have failed\")]\n",[66,169,170],{"class":68,"line":87},[66,171,172],{},"public function execute(): Result { ... }\n",[19,174,175,176,179,180,183,184,187],{},"If someone calls ",[43,177,178],{},"$tx->execute();"," ignoring the result, PHP warns via ",[43,181,182],{},"error_reporting",". This is ",[26,185,186],{},"gold for payment libraries, locks, queues and anything that can fail quietly",". We're adding it to the critical methods of the financial services in Atellum (our in-house POS) — the classic \"I called the method and didn't check the result\" bug almost vanishes on its own.",[38,189,191],{"id":190},"uri-extension","URI extension",[19,193,194,195,198,199,202],{},"Until now, parsing URLs in PHP was a mosaic of ",[43,196,197],{},"parse_url()",", ",[43,200,201],{},"urlencode()"," and a prayer. The new URI extension implements RFC 3986 and the WHATWG URL Standard:",[57,204,206],{"className":59,"code":205,"language":61,"meta":62,"style":62},"$url = new \\Uri\\WhatWg\\Url('https:\u002F\u002Fexample.com\u002Fblog\u002Fpost?utm=foo');\n$url = $url->withQuery('utm=bar');\necho $url;  \u002F\u002F https:\u002F\u002Fexample.com\u002Fblog\u002Fpost?utm=bar\n",[43,207,208,213,218],{"__ignoreMap":62},[66,209,210],{"class":68,"line":69},[66,211,212],{},"$url = new \\Uri\\WhatWg\\Url('https:\u002F\u002Fexample.com\u002Fblog\u002Fpost?utm=foo');\n",[66,214,215],{"class":68,"line":87},[66,216,217],{},"$url = $url->withQuery('utm=bar');\n",[66,219,220],{"class":68,"line":93},[66,221,222],{},"echo $url;  \u002F\u002F https:\u002F\u002Fexample.com\u002Fblog\u002Fpost?utm=bar\n",[19,224,225,226,229,230,233,234,237,238,148],{},"Two reasons it matters: ",[26,227,228],{},"subtle encoding bugs disappear"," (",[43,231,232],{},"%20"," vs ",[43,235,236],{},"+",", slashes inside parameters, IDN), and ",[26,239,240,241,244],{},"we stop reaching for ",[43,242,243],{},"league\u002Furi"," for the millionth time",[14,246,248],{"id":247},"what-were-not-using","What we're NOT using",[38,250,252],{"id":251},"first-class-callables-in-constant-expressions","First-class callables in constant expressions",[57,254,256],{"className":59,"code":255,"language":61,"meta":62,"style":62},"const PROCESSORS = [\n    'json' => json_decode(...),\n    'xml'  => simplexml_load_string(...),\n];\n",[43,257,258,263,268,273],{"__ignoreMap":62},[66,259,260],{"class":68,"line":69},[66,261,262],{},"const PROCESSORS = [\n",[66,264,265],{"class":68,"line":87},[66,266,267],{},"    'json' => json_decode(...),\n",[66,269,270],{"class":68,"line":93},[66,271,272],{},"    'xml'  => simplexml_load_string(...),\n",[66,274,275],{"class":68,"line":99},[66,276,277],{},"];\n",[19,279,280],{},"Pretty on paper. In practice our dispatch tables sit on classes or configs, not global constants, so nothing changed for us. I suspect this is a framework-targeted feature (Symfony will love it) rather than an application-code one.",[38,282,284],{"id":283},"extended-asymmetric-visibility","Extended asymmetric visibility",[19,286,287,288,291,292,295],{},"Lets you declare ",[43,289,290],{},"public(set: private)"," and other combinations. Fine, but ",[26,293,294],{},"a new joiner takes longer to read a class with asymmetric visibility than one with a private setter",". We're parking it until readability earns its place back.",[14,297,299],{"id":298},"what-to-break-before-migrating","What to break before migrating",[19,301,302],{},"These are the breaking changes we've seen explode in actual projects:",[304,305,306,322],"table",{},[307,308,309],"thead",{},[310,311,312,316,319],"tr",{},[313,314,315],"th",{},"Change",[313,317,318],{},"Typical symptom",[313,320,321],{},"How to fix",[323,324,325,340,357,379],"tbody",{},[310,326,327,334,337],{},[328,329,330,333],"td",{},[43,331,332],{},"MYSQLI_REFRESH_*"," constants deprecated",[328,335,336],{},"Warning on boot",[328,338,339],{},"Drop them, nobody uses them",[310,341,342,348,351],{},[328,343,344,347],{},[43,345,346],{},"mb_substr_count"," validates encoding more strictly",[328,349,350],{},"Exception where there used to be a warning",[328,352,353,354],{},"Always pass ",[43,355,356],{},"$encoding",[310,358,359,369,372],{},[328,360,361,364,365,368],{},[43,362,363],{},"date()"," with ",[43,366,367],{},"'P'"," and a timezone string deprecated",[328,370,371],{},"Warnings in logs",[328,373,374,375,378],{},"Use ",[43,376,377],{},"DateTime::format('P')"," with an object, not a string",[310,380,381,387,390],{},[328,382,383,386],{},[43,384,385],{},"var_dump"," float formatting changed",[328,388,389],{},"Snapshot tests fail",[328,391,392,393],{},"Regenerate snapshots or move to ",[43,394,395],{},"var_export",[19,397,398],{},"None of them is bad. All of them together in an old project (Laravel 8, Symfony 5) can add up to 50-100 warnings on the first boot. Usual plan: one day of cleanup before touching production.",[14,400,402],{"id":401},"framework-compatibility","Framework compatibility",[19,404,405],{},"As of today:",[407,408,409,416,422,428,434,444],"ul",{},[410,411,412,415],"li",{},[26,413,414],{},"Laravel 11 and 12",": officially supported from day one.",[410,417,418,421],{},[26,419,420],{},"Symfony 7.2 onwards",": officially supported.",[410,423,424,427],{},[26,425,426],{},"WordPress 6.7+",": tested and green. Key plugins (WooCommerce, Yoast, ACF) fine.",[410,429,430,433],{},[26,431,432],{},"Drupal 11",": supported.",[410,435,436,439,440,443],{},[26,437,438],{},"PrestaShop 8.x",": requires a patch on ",[43,441,442],{},"vendor\u002Fcomposer\u002Finstallers"," before migrating. We've seen it.",[410,445,446,449,450,148],{},[26,447,448],{},"Magento 2.4.7+",": announced, but with caveats around third-party extensions. ",[26,451,452],{},"Do not migrate to production without thorough staging",[14,454,456],{"id":455},"how-we-migrate-a-server","How we migrate a server",[19,458,459,460,463],{},"If the client is already on 8.3 or 8.4, ",[26,461,462],{},"the jump to 8.5 is barely a maintenance event",". Steps:",[465,466,467,473,479,485,495],"ol",{},[410,468,469,472],{},[26,470,471],{},"Staging first",", always. The staging VPS mirrors production exactly.",[410,474,475,478],{},[26,476,477],{},"Automated smoke test",": scripts that hit the critical pages (login, checkout, dashboard) and diff HTTP responses and timings.",[410,480,481,484],{},[26,482,483],{},"Migrate PHP-FPM in the quiet hours"," (4-6 AM for Spanish B2C projects).",[410,486,487,494],{},[26,488,489,490,493],{},"Keep ",[43,491,492],{},"php8.4-fpm"," installed alongside"," for 48 hours. If something blows up, swap in Nginx in 30 seconds.",[410,496,497,500,501,504,505,198,508,511],{},[26,498,499],{},"Watch the PHP logs"," for the first 24 hours with ",[43,502,503],{},"tail -F"," and prepared greps (",[43,506,507],{},"PHP Warning",[43,509,510],{},"PHP Deprecated",").",[19,513,514,515,518],{},"This isn't 8.5-specific. It's our standard PHP-jump procedure. Worth repeating because every time a client does it solo, ",[26,516,517],{},"there's always"," some warning that breaks that one old logging plugin nobody's touched in three years.",[14,520,522],{"id":521},"should-we-migrate-your-project","Should we migrate your project?",[19,524,525,526,529],{},"PHP 8.5 carries official active support until December 2027 and security support until December 2029. If your project is on 8.1 or earlier, you're running on a version that ",[26,527,528],{},"no longer receives security patches",". That's the main reason to move — not the pipe operator.",[19,531,532],{},"If you want to know what you'd have to retouch in your code before the jump, let's talk. Initial audit is half a day; the migration itself, with staging and rollback ready, usually closes in a day or two.",[14,534,536],{"id":535},"references","References",[407,538,539,548],{},[410,540,541],{},[542,543,547],"a",{"href":544,"rel":545},"https:\u002F\u002Fwww.php.net\u002Freleases\u002F8.5\u002Fen.php",[546],"nofollow","PHP 8.5 Release Announcement — php.net",[410,549,550],{},[542,551,554],{"href":552,"rel":553},"https:\u002F\u002Fwww.zend.com\u002Fblog\u002Fphp-8-5-features",[546],"PHP 8.5: New Features and Deprecations — Zend",[556,557,558],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":62,"searchDepth":87,"depth":87,"links":560},[561,562,569,573,574,575,576,577],{"id":16,"depth":87,"text":17},{"id":35,"depth":87,"text":36,"children":563},[564,566,567,568],{"id":40,"depth":93,"text":565},"The pipe operator |>",{"id":112,"depth":93,"text":115},{"id":151,"depth":93,"text":154},{"id":190,"depth":93,"text":191},{"id":247,"depth":87,"text":248,"children":570},[571,572],{"id":251,"depth":93,"text":252},{"id":283,"depth":93,"text":284},{"id":298,"depth":87,"text":299},{"id":401,"depth":87,"text":402},{"id":455,"depth":87,"text":456},{"id":521,"depth":87,"text":522},{"id":535,"depth":87,"text":536},"2026-06-02","PHP 8.5 has been in production since November. Here's our balance: what's actually making a difference, what's still hype, and what to break before hitting the upgrade button.","md","\u002Fog\u002Fog-default.png",{},true,"php-8-5-seis-meses-produccion","\u002Fen\u002Fblog\u002Fphp-8-5-six-months-production",{"title":8,"description":579},"en\u002Fblog\u002Fphp-8-5-six-months-production",[589,590,591],"PHP","Servers","Migrations","O5Seu7BdLmnbvSjzzd2dyHZNAo6pAnHSFo2_t3YSG7Y",1781154907975]