[{"data":1,"prerenderedAt":593},["ShallowReactive",2],{"switcher-blog-pareja":3,"art-php-8-5-seis-meses-produccion-es":6},{"es":4,"en":5},"\u002Fes\u002Fblog\u002Fphp-8-5-seis-meses-produccion\u002F","\u002Fen\u002Fblog\u002Fphp-8-5-six-months-production\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},"blogEs\u002Fes\u002Fblog\u002Fphp-8-5-seis-meses-produccion.md","PHP 8.5 a los seis meses: lo que hemos aprendido aplicándolo en clientes reales","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},"lo-que-prometía-y-lo-que-ha-dado","Lo que prometía y lo que ha dado",[19,20,21],"p",{},"PHP 8.5 salió el 20 de noviembre de 2025. Llevamos seis meses con ella desplegada en clientes (algunos a la primera semana, otros en los últimos refrescos de marzo), así que ya hay datos en lugar de demos.",[19,23,24,25,29],{},"El resumen rápido: ",[26,27,28],"strong",{},"es la mejor versión menor de la serie 8.x para actualizar sin sustos",". Más estable de salida que 8.3, menos disruptiva que 8.4 a nivel de breaking changes, y con un par de features que sí están entrando en código real, no solo en tweets.",[19,31,32],{},"Vamos a ver una a una qué hemos usado, qué no y qué romper antes de migrar.",[14,34,36],{"id":35},"lo-que-sí-está-entrando-en-código-de-cliente","Lo que sí está entrando en código de cliente",[38,39,41,42],"h3",{"id":40},"el-operador-pipe","El operador pipe ",[43,44,45],"code",{},"|>",[19,47,48,49,52],{},"El cambio que más nos ha sorprendido. Sobre el papel parecía azúcar sintáctico para pajearse en RFC; en código real está sustituyendo cadenas de ",[43,50,51],{},"array_map"," anidadas que costaba leer.",[19,54,55],{},"Antes:",[57,58,63],"pre",{"className":59,"code":60,"language":61,"meta":62,"style":62},"language-php shiki shiki-themes github-light github-dark","$nombre = 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],{},"Después:",[57,76,78],{"className":59,"code":77,"language":61,"meta":62,"style":62},"$nombre = $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],{},"$nombre = $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],{},"No es revolucionario. Pero en pipelines de validación, normalización de inputs y construcción de payloads para APIs ",[26,107,108],{},"se lee mejor",", y los autores de los próximos PRs que entren al proyecto no tienen que descifrar paréntesis anidados. Lo estamos adoptando en código nuevo; el viejo lo dejamos en paz porque refactorizar por placer es la enfermedad más cara de la profesión.",[38,111,113],{"id":112},"clone-with",[43,114,115],{},"clone with",[19,117,118,119,121],{},"Para clases readonly (que llevamos abusando desde 8.2 sin remordimiento), ",[43,120,115],{}," resuelve el patrón \"construye un nuevo objeto con un solo campo distinto\":",[57,123,125],{"className":59,"code":124,"language":61,"meta":62,"style":62},"$pedido_v2 = clone $pedido with { estado: 'confirmado' };\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],{},"Sin esto, había que escribir un método ",[43,135,136],{},"withEstado()"," en cada clase o pasar por un constructor con N argumentos. ",[26,139,140],{},"Esto sí lo estamos metiendo en código existente",", sobre todo en DTOs y value objects. Donde había ",[43,143,144],{},"withX()",", ahora ",[43,147,115],{},".",[38,150,152],{"id":151},"nodiscard",[43,153,154],{},"#[\\NoDiscard]",[19,156,157],{},"El que más nos gusta y menos publicidad ha tenido. Es un atributo que marca una función como \"el valor de retorno importa, no lo tires\":",[57,159,161],{"className":59,"code":160,"language":61,"meta":62,"style":62},"#[\\NoDiscard(\"La transacción puede haber fallado\")]\npublic function ejecutar(): Resultado { ... }\n",[43,162,163,168],{"__ignoreMap":62},[66,164,165],{"class":68,"line":69},[66,166,167],{},"#[\\NoDiscard(\"La transacción puede haber fallado\")]\n",[66,169,170],{"class":68,"line":87},[66,171,172],{},"public function ejecutar(): Resultado { ... }\n",[19,174,175,176,179,180,183,184,187],{},"Si alguien llama a ",[43,177,178],{},"$tx->ejecutar();"," ignorando el retorno, PHP avisa en ",[43,181,182],{},"error_reporting",". Esto es ",[26,185,186],{},"oro para librerías de pagos, locks, queues y todo lo que pueda fallar silenciosamente",". Lo estamos metiendo en los métodos críticos de los servicios financieros de Atellum (TPV interno) — los bugs típicos de \"llamé al método y no comprobé el resultado\" desaparecen casi solos.",[38,189,191],{"id":190},"extensión-uri","Extensión URI",[19,193,194,195,198,199,202],{},"Hasta ahora, parsear URLs en PHP era un mosaico de ",[43,196,197],{},"parse_url()",", ",[43,200,201],{},"urlencode()"," y rezar. La nueva extensión URI implementa RFC 3986 y el WHATWG URL Standard:",[57,204,206],{"className":59,"code":205,"language":61,"meta":62,"style":62},"$url = new \\Uri\\WhatWg\\Url('https:\u002F\u002Fmiweb.es\u002Fblog\u002Fpost?utm=foo');\n$url = $url->withQuery('utm=bar');\necho $url;  \u002F\u002F https:\u002F\u002Fmiweb.es\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\u002Fmiweb.es\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\u002Fmiweb.es\u002Fblog\u002Fpost?utm=bar\n",[19,224,225,226,229,230,233,234,237,238,148],{},"Importante por dos razones: ",[26,227,228],{},"deja de haber bugs sutiles de codificación"," (los ",[43,231,232],{},"%20"," vs ",[43,235,236],{},"+",", las barras en parámetros, los IDN), y ",[26,239,240,241,244],{},"se acaba el ciclo eterno de \"vale, instalamos ",[43,242,243],{},"league\u002Furi"," y otra dependencia más\"",[14,246,248],{"id":247},"lo-que-no-estamos-usando","Lo que NO estamos usando",[38,250,252],{"id":251},"callables-de-primera-clase-en-expresiones-constantes","Callables de primera clase en expresiones constantes",[57,254,256],{"className":59,"code":255,"language":61,"meta":62,"style":62},"const PROCESADORES = [\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 PROCESADORES = [\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],{},"Bonito sobre el papel. En la práctica nuestras tablas de despacho están en clases o configs, no en constantes globales, así que no nos ha cambiado nada. Sospechamos que es una feature pensada para frameworks (Symfony lo aprovechará), no para código de aplicación.",[38,282,284],{"id":283},"asymmetric-visibility-ampliado","Asymmetric visibility ampliado",[19,286,287,288,291,292,295],{},"Permite declarar ",[43,289,290],{},"public(set: private)"," y demás combinaciones. Está bien, pero hemos visto que ",[26,293,294],{},"el equipo nuevo tarda más en leer una clase con visibility asimétrica que una con un setter privado",". Lo estamos dejando en cuarentena hasta que la legibilidad lo justifique.",[14,297,299],{"id":298},"qué-romper-antes-de-migrar","Qué romper antes de migrar",[19,301,302],{},"Estos son los breaking changes que hemos visto petar en proyectos reales:",[304,305,306,322],"table",{},[307,308,309],"thead",{},[310,311,312,316,319],"tr",{},[313,314,315],"th",{},"Cambio",[313,317,318],{},"Síntoma típico",[313,320,321],{},"Cómo arreglarlo",[323,324,325,340,357,379],"tbody",{},[310,326,327,334,337],{},[328,329,330,333],"td",{},[43,331,332],{},"MYSQLI_REFRESH_*"," constantes deprecadas",[328,335,336],{},"Warning al cargar",[328,338,339],{},"Quitarlas, ya no se usan",[310,341,342,348,351],{},[328,343,344,347],{},[43,345,346],{},"mb_substr_count"," ahora valida encoding más estricto",[328,349,350],{},"Excepción donde antes había warning",[328,352,353,354],{},"Pasar siempre el argumento ",[43,355,356],{},"$encoding",[310,358,359,369,372],{},[328,360,361,364,365,368],{},[43,362,363],{},"date()"," con formato ",[43,366,367],{},"'P'"," deprecada usando timezone string",[328,370,371],{},"Warning en logs",[328,373,374,375,378],{},"Usar ",[43,376,377],{},"DateTime::format('P')"," con objeto, no string",[310,380,381,387,390],{},[328,382,383,386],{},[43,384,385],{},"var_dump"," cambia formato de floats",[328,388,389],{},"Tests con snapshot rotos",[328,391,392,393],{},"Regenerar snapshots o usar ",[43,394,395],{},"var_export",[19,397,398],{},"Ninguno es grave, pero todos juntos en un proyecto antiguo (Laravel 8, Symfony 5) pueden sumar 50-100 warnings en el primer arranque. Plan típico: un día de purga antes de tocar producción.",[14,400,402],{"id":401},"compatibilidad-con-frameworks","Compatibilidad con frameworks",[19,404,405],{},"A día de hoy:",[407,408,409,416,422,428,434,444],"ul",{},[410,411,412,415],"li",{},[26,413,414],{},"Laravel 11 y 12",": oficialmente soportado desde el día 1.",[410,417,418,421],{},[26,419,420],{},"Symfony 7.2 en adelante",": oficialmente soportado.",[410,423,424,427],{},[26,425,426],{},"WordPress 6.7+",": testeado y verde. Plugins clave (WooCommerce, Yoast, ACF) sin problema.",[410,429,430,433],{},[26,431,432],{},"Drupal 11",": soportado.",[410,435,436,439,440,443],{},[26,437,438],{},"PrestaShop 8.x",": requiere parche en ",[43,441,442],{},"vendor\u002Fcomposer\u002Finstallers"," antes de migrar. Lo hemos visto.",[410,445,446,449,450,148],{},[26,447,448],{},"Magento 2.4.7+",": anunciado, pero con caveat de extensiones de terceros. ",[26,451,452],{},"No migrar en producción sin staging exhaustivo",[14,454,456],{"id":455},"cómo-migramos-nosotros-un-servidor","Cómo migramos nosotros un servidor",[19,458,459,460,463],{},"Si el cliente ya está en 8.3 u 8.4, ",[26,461,462],{},"el salto a 8.5 es casi un evento de mantenimiento",". Pasos:",[465,466,467,473,479,485,495],"ol",{},[410,468,469,472],{},[26,470,471],{},"Staging primero",", siempre. La VPS de staging tiene exactamente la misma config que producción.",[410,474,475,478],{},[26,476,477],{},"Smoke test automatizado",": scripts que recorren las páginas críticas (login, checkout, dashboard) y comparan respuestas HTTP y tiempos.",[410,480,481,484],{},[26,482,483],{},"Migrar PHP-FPM en horario valle"," (4-6 AM en proyectos B2C españoles).",[410,486,487,494],{},[26,488,489,490,493],{},"Mantener ",[43,491,492],{},"php8.4-fpm"," instalado en paralelo"," durante 48 horas. Si algo peta, switch en Nginx en 30 segundos.",[410,496,497,500,501,504,505,198,508,511],{},[26,498,499],{},"Revisar logs de PHP"," las primeras 24 horas con ",[43,502,503],{},"tail -F"," y un par de greps preparados (",[43,506,507],{},"PHP Warning",[43,509,510],{},"PHP Deprecated",").",[19,513,514,515,518],{},"Esto no es exclusivo de 8.5. Es el procedimiento que usamos para cualquier salto de versión de PHP. Pero merece la pena recordarlo porque cada vez que un cliente lo hace solo, ",[26,516,517],{},"siempre"," aparece algún warning que rompe ese plugin de logs antiguo que nadie había tocado en tres años.",[14,520,522],{"id":521},"migramos-tu-proyecto","¿Migramos tu proyecto?",[19,524,525,526,529],{},"PHP 8.5 lleva soporte oficial activo hasta diciembre de 2027 y soporte de seguridad hasta diciembre de 2029. Si tu proyecto está en 8.1 o anterior, estás corriendo sobre una versión que ",[26,527,528],{},"ya no recibe parches de seguridad",". Esa es la razón principal para actualizar — no el pipe operator.",[19,531,532],{},"Si quieres saber qué tendrías que retocar en tu código antes de la migración, hablamos. La auditoría inicial es media jornada; la migración en sí, con staging y rollback preparado, suele cerrarse en uno o dos días.",[14,534,536],{"id":535},"referencias","Referencias",[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},"El operador pipe |>",{"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 lleva en producción desde noviembre. Hacemos balance de lo que sí está marcando diferencia, lo que sigue siendo postureo y qué romper antes de pulsar el botón de actualización.","md","\u002Fog\u002Fog-default.png",{},true,"php-8-5-six-months-production","\u002Fes\u002Fblog\u002Fphp-8-5-seis-meses-produccion",{"title":8,"description":579},"es\u002Fblog\u002Fphp-8-5-seis-meses-produccion",[589,590,591],"PHP","Servidores","Migraciones","vG5I6iLmzB5MmK6oL-ckXe_etj6R-lc31zy7ka0aa-U",1781154907654]