Blog

  • ¿Cómo cambiar el upload_max_file_size desde el Dockerfile?

    ¿Cómo cambiar el upload_max_file_size desde el Dockerfile?

    Que tiempos aquellos en que el tamaño los archivos que se subían vía HTTP se medía en KB, ¿no? Qué fácil era la vida por entonces.

    Pero bueno… el mundo avanza y con él la necesidad de compartir nuestras fotos en alta calidad se hace presente.

    En ese contexto, tener un upload máximo de 2 MB puede convertirse en un gran limitante.

    Y, casualmente, el tamaño máximo por defecto del upload_max_filesize de PHP es de… 2 MB.

    En un ambiente de desarrollo normal modificar esto es fácil, ¿no?

    Basta con:

    1. Ejecutar el comando php --ini
    2. Abrir el archivo referido en Configuration File (php.ini)
    3. Buscar la línea upload_max_filesize = 2M
    4. Modificarla por un valor acorde a nuestras necesidades (Por ejemplo upload_max_filesize => 10M)
    5. Reiniciar el servidor (o php-fpm)

    Pero si estamos usando un contenedor Docker las cosas no son tan simples.

    Es decir, hacer el cambio en sí no presenta una dificultad especial:

    docker exec -it 7b8da0fb9892 bash
    apt update
    apt install -y vim
    vim /usr/local/etc/php/php.ini

    Modificar el valor de la configuración, guardar y listo.

    El problema es que… apenas el contenedor se destruya, el cambio se fue con él y estamos otra vez donde empezamos.

    Es todo una cuestión de imagen

    La solución basada en el Dockerfile pasa por tener, dentro de la imagen que se construya, una definición del php.ini que contenga los cambios que necesitamos.

    ¿Cómo lograr eso? Pues se trata de tener, dentro de nuestro proyecto, nuestro propio php.ini y meterlo dentro de la imagen durante el build.

    En concreto, supongamos que partimos de un Dockerfile que se ve así:

    FROM php:7.4-apache
    
    LABEL authors="Mauro Chojrin <mauro.chojrin@leewayweb.com>"
    
    RUN apt-get update && \
        apt-get install -y \
            libpng-dev \
            libzip-dev
    
    RUN docker-php-ext-install gd && \
        docker-php-ext-install zip && \
        docker-php-ext-install mysqli
    
    ADD app /var/www/html

    Con esto podemos crear una imagen para luego levantar un contenedor con un Apache que podrá procesar php para responder a las peticiones.

    En este ejemplo, el código (El contenido del directorio /var/www/html dentro del contenedor) será el que se encuentre dentro del directorio app de nuestro proyecto.

    Lo que toca hacer es agregar un archivo php.ini dentro del proyecto e indicar a Docker que lo agregue a la imagen durante el build:

    FROM php:7.4-apache
    
    LABEL authors="Mauro Chojrin <mauro.chojrin@leewayweb.com>"
    
    RUN apt-get update && \
        apt-get install -y \
            libpng-dev \
            libzip-dev
    
    RUN docker-php-ext-install gd && \
        docker-php-ext-install zip && \
        docker-php-ext-install mysqli
    
    ADD app /var/www/html
    
    ADD php.ini /usr/local/etc/php/

    ¿Qué debería contener el archivo php.ini? Si queremos asegurarnos de que sólo se modifique el upload_max_filesize necesitamos empezar por un archivo que contenga la configuración presente dentro del contenedor.

    Cómo obtener el archivo original

    Lo primero que podemos probar es buscar el archivo dentro del contenedor1:

    docker exec -it 7b8da0fb9892 php --ini

    Esto dará un resultado similar a:

    Configuration File (php.ini) Path: /usr/local/etc/php
    Loaded Configuration File:         (none)
    Scan for additional .ini files in: /usr/local/etc/php/conf.d
    Additional .ini files parsed:      /usr/local/etc/php/conf.d/docker-php-ext-sodium.ini

    Si tuviera un archivo lo copiaría usando:

    docker cp 7b8da0fb9892:/usr/local/etc/php/php.ini .

    Pero, como en mi caso me indica que el archivo de configuración cargado es ninguno, tendré que probar otra cosa.

    Lo que haré será crear en el directorio del proyecto un pequeño script llamado dump_php_ini.php:

    <?php 
    $s = []
    foreach(ini_get_all() as $k=>$v) {
         if ($v['local_value'] || $v['global_value']) {
            $s[] = sprintf("%s = %s", $k, $v['local_value']?$v['local_value']:$v['global_value']);
        }
    }
    
    echo join("\n", $s);

    Y luego ejecutar este comando:

    docker run -v $(pwd):/var/www/html -it php:7.4-apache php dump_php_ini.php > php.ini

    Con eso tengo el archivo que contiene la configuración exacta que se está usando actualmente.

    Y ahora sí, puedo editarlo, hacer la modificación que se requiere y volver a construir la imagen usando:

    docker build . -t my_apache_php

    A partir de aquí, todos los contenedores que sean creados usando este Dockerfile tendrán la configuración deseada y los visitantes del sitio podrán compartir sus fotos sin inconvenientes.

    1. Cambia el ID del contenedor por el tuyo ↩︎
  • ¿Cómo direccionar un dominio a un contenedor Docker?

    ¿Cómo direccionar un dominio a un contenedor Docker?

    Tenés tu entorno local funcionando perfecto con Docker.

    Llevar la imagen al hosting y crear el contenedor no es un problema pero, cuando querés que los clientes vean el sitio… ¿qué les tenés que pasar? ¿No sería genial poder pasarles una URL con tu propio dominio?

    Veamos qué necesitarías para hacerlo.

    1. Un servidor donde alojar tus contenedores
    2. Un dominio que puedas direccionar a tu servidor
    3. La configuración de DNS
    4. La configuración del webserver dentro de Docker

    Asumiré que los puntos 1, 2 y 3 los tenés resueltos y me concentraré en el punto 4.

    Cómo configurar Docker para que responda a un dominio único

    Si se trata de un único contenedor corriendo en el servidor no es necesario hacer demasiado. Basta con asegurarse de que el contenedor esté conectado al puerto 80 del servidor.

    Por ejemplo, si se inicia en un servidor un contenedor usando algo como:

    docker run --restart=always -p  80:80 imagen -d

    Será suficiente para dejar el contenedor corriendo en background en forma constante.

    Con una configuración estándar de Apache como por ejemplo:

    <VirtualHost *:80>
    	# The ServerName directive sets the request scheme, hostname and port that
    	# the server uses to identify itself. This is used when creating
    	# redirection URLs. In the context of virtual hosts, the ServerName
    	# specifies what hostname must appear in the request's Host: header to
    	# match this virtual host. For the default virtual host (this file) this
    	# value is not decisive as it is used as a last resort host regardless.
    	# However, you must set it for any further virtual host explicitly.
    	ServerName www.example.com
    
    	ServerAdmin webmaster@localhost
    	DocumentRoot /var/www/html
    
    	# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
    	# error, crit, alert, emerg.
    	# It is also possible to configure the loglevel for particular
    	# modules, e.g.
    	#LogLevel info ssl:warn
    
    	ErrorLog ${APACHE_LOG_DIR}/error.log
    	CustomLog ${APACHE_LOG_DIR}/access.log combined
    
    	# For most configuration files from conf-available/, which are
    	# enabled or disabled at a global level, it is possible to
    	# include a line for only one particular virtual host. For example the
    	# following line enables the CGI configuration for this host only
    	# after it has been globally disabled with "a2disconf".
    	#Include conf-available/serve-cgi-bin.conf
    </VirtualHost>
    
    # vim: syntax=apache ts=4 sw=4 sts=4 sr noet

    Es suficiente.

    En definitiva, casi que es más difícil lograr que el contenedor no quede asociado al dominio que lo contrario.

    Cómo configurar Docker para que responda a diferentes dominios

    Distinto, y más interesante, es el caso de querer alojar en un mismo servidor varios sitios diferentes, cada uno con su propio contenedor.

    Para lograr eso se requiere algo más de maña a nivel de infraestructura.

    Un webserver como Apache permite, por ejemplo, usar múltiples hosts virtuales (VirtualHost) pero, obviamente, el puerto 80 del servidor es uno solo. Esto quiere decir que hay un único proceso escuchando en el puerto 80 y, de todo el tráfico que se recibe a través de él, es su responsabilidad decidir cómo responder.

    Para que a cada sitio le llegue el tráfico que le corresponde, y sólo ese tráfico, es necesario definir algún tipo de mapeo entre la petición que recibe el webserver y el host virtual que lo atenderá.

    Una forma de hacer esto es asociar diferentes hosts a diferentes puertos.

    <VirtualHost *:80>
    	...
    </VirtualHost>
    <VirtualHost *:81>
    	...
    </VirtualHost>
    <VirtualHost *:82>
    	...
    </VirtualHost>

    Claro que, si la idea es que a todos los sitios se acceda a través del puerto estándar, esta opción queda descartada.

    Otra opción disponible es desambiguar por IP:

    <VirtualHost 172.21.0.1:80>
    	...
    </VirtualHost>
    <VirtualHost 137.184.122.59:80>
    	...
    </VirtualHost>
    <VirtualHost 93.184.216.34:80>
    	...
    </VirtualHost>

    Esto puede funcionar si hay más de una IP pública asociada al servidor.

    Lo más común, sin embargo, es desambiguar a través del ServerName:

    <VirtualHost *:80>
    	ServerName dominio1.com
    </VirtualHost>
    <VirtualHost *:80>
    	ServerName dominio2.com
    </VirtualHost>
    <VirtualHost *:80>
    	ServerName dominio3.com
    </VirtualHost>

    En este esquema, Apache determinará qué VirtualHost es el requerido analizando la URL. Un request de tipo http://dominio1.com será atendido siguiendo la primera definción, http://dominio2.com la segunda y http://dominio3.com la tercera.

    El primer paso es claro: definir un VirtualHost por cada dominio que se quiera usar.

    Segundo paso: conectar el tráfico del host al contenedor que le corresponde.

    Para lograr esto es necesario contar con el módulo proxy de Apache. En el caso de Ubuntu basta con el comando sudo a2enmod proxy_http; sudo service apache2 restart para tenerlo todo listo.

    Luego, por cada sitio que se quiera servir, se debe crear la configuración del VirtualHost:

    <VirtualHost *:80>
        ServerName dominio1.com
        ProxyPass / http://127.0.0.1:5000/
        ProxyPassReverse / http://127.0.0.1:5000/
    </VirtualHost>

    De esta forma, todo el tráfico que se reciba a través de http://dominio1.com será redirigido automáticamente hacia el puerto 5000 dentro del mismo servidor (De ahí el 127.0.0.1).

    Por último, para cerrar el círculo se necesita contar con un contenedor Docker escuchando en el puerto seleccionado, en este caso, el 5000. Con esto será suficiente para la prueba.

    docker run -p 5000:80 -v $(pwd):/var/www/html php:7.4-apache

    Asumiendo que el directorio actual contiene un archivo como este:

    <?php
    
    echo phpversion();

    Al abrir el navegador en http://dominio1.com/ se verá algo como:

    Si aún no tenés los dominios apuntados al servidor o querés hacer pruebas en tu entorno local basta con que modifiques tu archivo /etc/hosts apuntando todos los dominios a 127.0.0.1.

    El caso del segundo dominio será prácticamente igual:

    <VirtualHost *:80>
        ServerName dominio2.com
        ProxyPass / http://127.0.0.1:5001/
        ProxyPassReverse / http://127.0.0.1:5001/
    </VirtualHost>

    La diferencia está en el puerto. El contenedor para el segundo dominio deberá estar escuchando a un puerto diferente en el host, pero éste deberá estar mapeado al puerto 80 dentro del contenedor. Por ejemplo:

    docker run -p 5001:80 -v $(pwd):/var/www/html php:8.2-apache

    Y, nuevamente, al abrir el navegador en http://dominio2.com verás:

    Y así continúa por cada uno de los dominios que necesites apuntar

  • ¿Puede Docker sustituir a XAMPP?

    ¿Puede Docker sustituir a XAMPP?

    Hace unos años, montar un entorno de desarrollo para una aplicación web PHP era una verdadera molestia:

    Primero era necesario instalar Apache, luego MySQL, después configurar todo para que el Apache pudiera procesar correctamente los archivos PHP y generar la salida esperada, pasando en el medio por la configuración de MySQL y rogar que todo funcionara bien.

    Y así pasábamos los días hasta que llegó XAMPP.

    Qué es XAMPP

    XAMPP fue, en su día, una verdadera revolución: un paquete único que contenía todo lo que podíamos necesitar para desarrollar cómodamente: click aquí, click allí y listo, a programar se ha dicho!

    Si bien XAMPP simplicaba, y mucho, el montaje de un entorno local, en el fondo no se trataba de algo esencialmente diferente de lo que veníamos haciendo hasta el momento… sólo que mucho más rápido y cómodo.

    El problema empezó cuando muchos programadores comenzaron a usarlo como si se tratara de una solución mágica y dejaron de lado comprender qué es lo que estaba ocurriendo tras bambalinas.

    Craso error.

    Error que se hace patente a la hora de subir el sitio al hosting. Es entonces cuando empiezan los problemas como:

    Tengo este código que en localhost funciona perfectamente, que recibe los datos enviados mediante un formulario y saca los resultados mediante una tabla que se rellena automáticamente según los datos enviados. Resulta que cuando lo subo al servidor (hosting) no funciona y no da ningún tipo de error, simplemente no dibuja la tabla.

    O:

    tengo una pagina en php, en el servidor local funciona bien, es un login que al ingresar los datos dirige a una parte especifica de la web dependiendo el perfil, ahora si subo esto a mi hosting funciona perfectamente, pero si subo la web a otro dominio al ingresar los datos del login se queda la página en blanco. Ya intente que mostrara errores pero no me sale nada.

    Hace algunos años habría recomendado usar máquinas virtuales para desarrollar. Hoy por hoy, existiendo Docker hay un nuevo ganador.

    Qué es Docker

    Docker es, ante todo, una plataforma de ejecución de aplicaciones basada en el concepto de contenedor.

    La idea, muy resumida, es tener una única unidad (un paquete) que contenga en sí mismo todo lo necesario para ejecutar una aplicación.

    De este modo, nos olvidamos de las pequeñas sorpresas que pueden encontrarse al llevar a un hosting un proyecto desarrollado en forma local.

    ¿Puede usarse Docker para desarrollos PHP?

    ¡Claro que sí!

    Existen varias formas diferentes de usar Docker en proyectos PHP. En general, lo que se necesitará será una imagen que incluya el intérprete de PHP y, si se trata de una aplicación web, también un servidor.

    XAMPP vs. Docker

    Recapitulando un poco, como para que quede claro: XAMPP es un paquete de software específicamente diseñado para PHP, mientras que Docker es un sistema de manejo de contenedores genérico, es decir, no sólo puede usarse para PHP si no para cualquier lenguaje.

    Por otro lado, con Docker es sumamente sencillo tener, en una misma máquina física, muchos procesos PHP corriendo diferentes versiones. Te reto a que intentes eso con XAMPP.

    La desventaja, por llamarle de algún modo, de usar Docker en lugar de XAMPP es su dificultad para montarlo. Claro que, una vez que lo domines ésta desaparecerá.

    ¿Cómo reemplazar XAMPP por Docker?

    Para reemplazar XAMPP por Docker se requiere contar con una configuración de Docker que incluya los servicios que XAMPP contiene (Además del propio Docker instalado localmente, claro).

    En principio se trata de:

    Esto supone crear 4 contenedores diferentes, los cuales tendrán que estar en sincronía.

    Suena complicado, ¿no? Bueno… no es para tanto.

    Usando docker-compose es posible lograrlo con poco esfuerzo.

    Por ejemplo, si tenemos un proyecto donde tenemos un archivo llamado index.php que contiene lo siguiente:

    <?php
    
    echo "Viva PHP!";

    Se puede:

    1. Crear un archivo docker-compose.yml en el mismo directorio con este contenido:
    version: '3.8'
    services:
        mariadb:
            image: 'mariadb:11.0'        
            volumes:
                - 'database-data:/var/lib/mysql'
            environment:
                - MYSQL_ROOT_PASSWORD=root
                - MYSQL_DATABASE=my_app
                - MYSQL_USER=my_user
                - MYSQL_PASSWORD=my_pwd
            ports:
                - '29003:3306'
            restart: always
        webserver:
            image: 'php:7.4-apache'
            working_dir: '/var/www/html'
            volumes:
                - '.:/var/www/html'
            ports:
                - '29000:80'
            restart: always   
        pma:
            image: 'phpmyadmin:latest'
            ports:
                - '29005:80'
            links:
                - 'mariadb:db'
            environment:
                - PMA_ARBITRARY=1
    volumes:
        database-data: {}
    1. Darle al docker-compose up -d
    1. Abrir el navegador en http://localhost:29000 y voilá:

    Luego, si se quiere entrar al PhpMyAdmin basta con abrir una nueva ventana en http://localhost:29005, ingresar las credenciales y se verá esto:

    Y poco más…

    A partir de aquí es posible hacer todo lo que se hacía con XAMPP, con la diferencia de que luego subir todo al hosting será mucho más sencillo.

  • XDebug con VSCode y Docker en Ubuntu

    Usar Docker en proyectos PHP es un viaje de ida.

    Olvidarse del «te juro que en mi casa andaba!» es una bendición.

    Claro que, para poder desplegar, primero hay que desarrollar. Y desarrollar implica, claro está, debuggear.

    En PHP no contamos con un debugger incorporado a nuestros IDEs… afortunadamente existe XDebug.

    El problema, sin embargo, suele ser la configuración que depende de dos factores que deben combinarse:

    1. La instalación y configuración del lado del servidor.
    2. La configuración del IDE para poder utilizarlo.

    Existen muchas combinaciones posibles para realizar esta tarea, cada una con sus pequeñas particularidades. En este artículo me enfocaré en una de las combinaciones más populares por estos días:

    VSCode sobre Ubuntu con un WebServer montado en Docker.

    La aplicación que usaré como ejemplo es el CRM de código abierto rukovoditel.

    El Dockerfile

    Lo primero será definir el archivo Dockerfile que permita contar con un servidor web que pueda ejecutar php y, a la vez, tenga instalado XDebug:

    FROM php:7.4-apache
    LABEL authors="mauro.chojrin@leewayweb.com"
    
    RUN apt-get update && \
        apt-get install -y \
            libpng-dev \
            libzip-dev && \
            pecl install xdebug-3.1.6
    
    RUN docker-php-ext-install gd && \
        docker-php-ext-install zip && \
        docker-php-ext-install mysqli && \
        docker-php-ext-enable xdebug
    
    COPY xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
    
    ADD app /var/www/html
    
    RUN chown -R www-data.www-data /var/www/html

    La configuración de XDebug

    A continuación ha de crearse el archivo de configuración de xdebug (xdebug.ini) en la raíz del proyecto:

    zend_extension=xdebug
    
    [xdebug]
    xdebug.mode=develop,debug
    xdebug.client_host=host.docker.internal
    xdebug.start_with_request=yes

    El archivo docker-compose.yml

    Posteriormente será el turno del archivo docker-compose.yml que contendrá los servicios necesarios para dar vida a la aplicación:

    version: '3.1'
    services:
      mysql:
        image: 'mysql:5.7.42-debian'
        environment:
          - MYSQL_ROOT_PASSWORD=root
          - MYSQL_DATABASE=ruko
          - MYSQL_USER=ruko
          - MYSQL_PASSWORD=ruko
        restart: always
      webserver:
        image: 'ruko_ws'
        build:
          context: '.'
        ports:
          - '8888:80'
        volumes:
          - './app:/var/www/html:rw'
          - './xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini'
        extra_hosts:
          - host.docker.internal:host-gateway

    Con esto está listo todo lo necesario del lado de la infraestructura.

    Los siguientes pasos se darán del lado del host.

    Configuración de VS Code

    La extensión de debugging de PHP

    Una vez abierto el proyecto, hay que asegurarse de tener instalada la extensión requerida para realizar debugging:

    La configuración de lanzamiento

    A continuación, debe crearse una configuración de lanzamiento para realizar el debugging:

    De las múltiples entradas que encuentro la que interesa es la llamada Listen for Xdebug.

    Por defecto, lo que se ve es:

    {
          "name": "Listen for Xdebug",
          "type": "php",
          "request": "launch",
          "port": 9003
    },

    Lo que se necesita es agregar la definición del mapeo de directorios, desde el local al del servidor.

    En este caso, el directorio en el servidor (en el contenedor Docker) es /var/www/html/, el cual debe ser conectado con el directorio app dentro de la raíz del proyecto.

    Para usar el directorio del host, sin hardcodearlo, es posible usar la variable de VisualStudio workspaceFolder la cual contiene el directorio raíz del proyecto.

    De modo que la configuración quedará de esta forma:

    {
          "name": "Listen for Xdebug",
          "type": "php",
          "request": "launch",
          "port": 9003,
          "pathMappings": {
                 "/var/www/html/": "${workspaceFolder}/app"
          }
    },

    Debuggeando

    Con esto listo es posible arrancar el debugger:

    Para ver su efecto es conveniente introducir un punto de interrupción (breakpoint) en algún lugar del código.

    Tomemos como ejemplo el archivo app/install/index.php ya que es el primero que se ejecuta al ingresar a la aplicación.

    Lo próximo será ir al navegador web, donde se dará comienzo a la sesión de debugging usando el asistente de XDebug.

    Luego se debe ingresar la dirección de la página (http://localhost:8888 en este caso) y, al dar enter se abrirá automáticamente la ventana de VS Code:

    Donde podrá ejecutarse el código paso a paso:

    Inspeccionar variables en tiempo real

    Y, en general, utilizar todos los servicios provistos por XDebug.

    Puntos clave

    En resúmen, las claves para hacer funcionar XDebug con VSCode en Docker sobre Ubuntu son:

    1. Contar con una imagen basada en un Dockerfile que incluya la instalación de XDebug
    2. Tener instalada la extensión de debugging con PHP en VS Code
    3. Configurar correctamente el mapeo de directorios en la configuración de lanzamiento (launch.json)
    4. Tener la definición de extra_hosts del contenedor que tiene php apuntando a host-gateway
    5. Tener el xdebug correctamente configurado dentro del contenedor de php
  • ¿Cualquier aplicación PHP se puede dockerizar?

    ¿Cualquier aplicación PHP se puede dockerizar?

    ¿Vale la pena Dockerizar tu php?

    Permitime contestarte con un ejercicio.

    Imaginate esta situación:

    Venís trabajando sin descanso en una aplicación muy importante para entregar a un cliente.

    Vas a buen ritmo, vas a llegar a la entrega sin mucho problema.

    De pronto, te aparece la notificación de actualizar el sistema operativo y pensás: «Y… la verdad que no estaría mal… no puedo seguir toda la vida con la 1.0…».

    Le das aceptar y, luego de unos minutos, cuando todo terminó te das cuenta de que la aplicación que andaba perfecta dejó de funcionar.

    ¿Eh?

    ¿Cómo puede ser?

    Así como instintivamente tirás un php -v y comprobás que de 7.4 saltaste sin escalas a 8.1… y las cosas ya no son como antes.

    ¡Qué problema! Menos mal que es una situación hipotética ¿no?

    Pues… no tanto. Esta historia es una adaptación libre de esta conversación que leí recientemente:

    «se me actualizo php a la version 8 en Manjaro, y por ende se me descuadró todo, qué tengo que cambiar en el httpd.conf para que me funcione php8«

    «Soy usuario de manjaro y me pasó lo mismo. Pero por suerte tenia dockerizado mis proyectos y no sufrí demasiado, deberías considerarlo«

    Yo diría que suerte habría sido no tener que enfrentarse al problema.

    Tener tus proyectos php dockerizados no es suerte, es una decisión.

    Una decisión que en algún momento este desarrollador tomó y que también deberías, al menos, evaluar.

    Es un caso muy similar al que analizo acá.

    Pensando un poco en esto empecé a preguntarme: ¿Cualquier aplicación php se puede dockerizar?

    Qué significa dockerizar una aplicación PHP

    Cuando se habla de dockerizar (me encanta este verbo) una aplicación, se hace referencia a hacer las adaptaciones necesarias para poder correrla sobre contenedores docker, tanto en un etorno de desarrollo como en uno de producción.

    Estos ajustes dependen en gran medida de la naturaleza de la aplicación pero, a rasgos generales, se trata de:

    • Crear un Dockerfile (O al menos seleccionar una imagen pre-existente que sea compatible con las necesidades de la aplicación)
    • Cambiar, si las hubiera, referencias a localhost por otras que apunten a servicios dentro del ecosistema de docker
    • Crear una configuración de inicio de los servicios (Ya sea a través de Makefiles o similar o, mejor aún, a través de un archivo docker-compose)

    En principio creo que no hay mucho más, pero si te parece que me olvidé de algo importante, por favor dejame un comentario abajo.

    Qué se necesita para dockerizar una aplicación PHP

    Para dockerizar una aplicación PHP se necesita, primero que nada, contar, tanto en local como en producción, con el motor y el cliente de docker.

    Si se pretende usar docker-compose, también deberá estar presente en ambos lados.

    Más allá de eso, es conveniente contar con un buen IDE, aunque no es imprescindible.

    Técnicamente no se necesita nada más. Claro que saber lo que se está haciendo ayuda mucho.

    Cómo dockerizar una aplicación PHP

    Teniendo todo en su lugar el proceso es simple, aunque, dependiendo de la aplicación, puede ser más laborioso que en otros casos.

    Relevamiento

    Lo primero es entender cuáles son las dependencias:

    • ¿Qué versión de PHP usás en producción?
    • ¿Qué extensiones de PHP necesitás instaladas y habilitadas?
    • ¿Qué otros servicios de infraestructura se usan con tu aplicación? ¿MySQL? ¿Redis?
      • ¿Qué versiones?
    • ¿Cómo está la configuración del webserver?
    • ¿Cómo está la configuración de PHP?
    • ¿Qué permisos sobre archivos necesita la aplicación para funcionar?

    Con las respuestasa a estas preguntas podrás darte una idea de lo que requerirás en tu imagen docker.

    Instalación de las herramientas

    Si no las tienes disponibles, el próximo paso será instalar las herramientas necesarias (docker engine, docker client y docker-compose al menos).

    Adaptación de la aplicación

    Luego deberás preparar la aplicación:

    • Reemplazar referencias a servicios externos (Bases de datos, colas de mensajes, etc…) de IPs o nombres conocidos en el host a nombres conocidos dentro de la red de Docker
    • Reemplazar rutas absolutas por relativas
    • Reemplazar enlaces simbólicos por archivos concretos

    Creación de los archivos de Docker

    Es muy probable que tu aplicación tenga algunos requisitos específicos que no encuentres en una de las imágenes disponibles en el dockerhub. No te preocupes, siempre podés tomar una imagen lo más parecida posible a tus necesidades para usar como base y agregar tus cambios particulares.

    Si el sistema es suficientemente complejo (si requiere de otros componentes de infraestructura), es conveniente definir también un archivo docker-compose.yml para mayor comodidad.

    Pruebas y ajustes

    Una vez hayas pasado por los pasos anteriores será cuestión de probar la aplicación y validar que todo funciona.

    Los comandos exactos dependerán de cómo hayas armado los archivos de configuración, pero será algo así como:

    docker build . -t mi_php
    
    docker run -it mi_php 

    O, si usas docker-compose:

    docker-compose up --build

    Una vez estén levantados los servicios podrás entrar a http://localhost:8000 (Asumo que el puerto 8000 está mapeado al 80 en el contenedor) y ya podrás verificar que toda la aplicación funciona.

    Por qué dockerizar tus aplicaciones php

    Si te parece mucho trabajo, tenés razón, dockerizar una aplicación PHP puede ser molesto, especialmente cuando lo tienes que hacer por primera vez pero, si queda bien, es muy probable que no tengas que volver a hacerlo por mucho tiempo y, a la vez, tu aplicación quedará blindada ante cambios en el ambiente, tanto local como de producción.

    De pronto no se ve tan mal, ¿no?

  • Por qué NO deberías usar XAMPP

    Por qué NO deberías usar XAMPP

    Me llegó esta pregunta que me pareció interesante compartir:

    Estoy usando PHPStorm, y como sólo me pide el intérprete cuando trato de ejecutar en el navegador, todavía no lo he instaldo, quisiera saber si ya es mejor instalar el Xampp, si es recomendable y en caso de que no lo sea ¿por que?

    Por si no sabés de qué se trata XAMPP, es un paquete que trae, todo lo que típicamente se requiere para desarrollar con PHP:

    La X del comienzo puede ser reemplazada por L (Linux), W (Windows) o M (Mac).

    A primera vista parece la panacea, ¿no?

    «Es lo más fácil!»

    «Un par de clics y listo!»

    «¿Para qué complicarme instalando todo por separado si puedo tenerlo en un solo paquete»?

    Seguramente habrás escuchado este tipo de argumentos a su favor.

    Y sí, todo eso es verdad: XAMPP es un paquete sumamente cómodo… al principio.

    Los problemas llegan cuando:

    • Es momento de ir a producción
    • Necesitás trabajar con diferentes proyectos a la vez.

    Son estos los momentos en te das cuenta que el salvavidas estaba hecho de plomo.

    Cuál es el problema con XAMPP

    La sencillez que aporta XAMPP lo hace la opción más difundida entre los desarrolladores menos experimentados. Pero esa sencillez tiene un costo.

    El primero de los problemas es que, al ocultar la complejidad real que implica montar un servidor, se propicia el efecto «¡Te juro que en mi casa andaba!». Como no sabés realmente qué tenés instalado es difícil verificar que el hosting tenga lo mismo (Más detalle de qué es exactamente lo que deberías mirar acá).

    El segundo de los problemas es que hace muy difícil trabajar en diferentes proyectos donde cada uno tiene requerimientos de infraestructura diferentes (Por ejemplo, versiones diferentes del intérprete de PHP).

    Por supuesto que, si sos conciente de estas limitaciones y sabés trabajar con ellas XAMPP puede ser una opción aceptable.

    Qué usar en lugar de XAMPP

    Las opciones son varias, hay algunas mejores que XAMPP y otras peores:

    En general, docker es la mejor opción cuando se trata de montar entornos locales ya que es muy simple luego llevarlos a producción y/o compartir con tu equipo.

    Pero bueno, es cierto también que dominarlo no es una tarea muy sencilla.

    Si recién estás empezando (Y quiero decir que apenas estás dando tus primeros pasos), está bien que uses XAMPP pero es importante que tengas la idea de migrar lo antes posible.

  • «Mi sitio funcionaba bien hasta que el hosting actualizó PHP»

    «Mi sitio funcionaba bien hasta que el hosting actualizó PHP»

    Hace poco me contactó un colega por una situación algo complicada que le tocó enfrentar.

    La historia comienza así:

    Nuestro sitio está programado con PHP 5.5 y usa memcached. Estamos queriendo migrarlo a una versión más alta de PHP y tenemos un problemita.

    Y sigue:

    …guardamos la sesión en memcache porque tenemos varios fronts y dependiendo de la versión el error es diferente

    Se va poniendo interesante, ¿no? Veamos un poco más:

    …con php 5.5 todo andaba perfecto, tanto en Windows con WAMP para desarrollar como con Linux/Centos

    El resto te lo podrás imaginar supongo.

    En general no soy muy partidario de desarrollar en Windows y desplegar en Linux (Una de las razones que llevan a la falsa idea de que todo va a funcionar bien).

    Vayamos a lo importante: ¿cómo tomaron la decisión de migrar a una versión más actualizada?

    …la gente que instala los servidores sugirió ir a la última versión de PHP y Apache y Centos y postgresql pero no va

    Bien, ahora está más claro.

    Llegados a este punto estamos realmente en un problema: por un lado, el sitio tiene que seguir funcionando.

    Por el otro, no podemos pedirle al hosting que habilite una versión de PHP que sabemos que es compatible con nuestro sistema.

    ¿Qué se puede hacer?

    Lo mejor sería buscar una imagen de docker que tenga la versión que buscamos, instalarla en el servidor y seguir la vida como si tal cosa.

    Si esa no es una posibilidad, habrá que hacerse a la idea de que la solución tomará algún tiempo… con todo lo que ello implica.

    Saltarse versiones de php puede ser bastante arriesgado.

    En este caso, ir de la 5.5 directo a la 8.1 implica dejar de lado una serie de cambios que se realizaron al intérprete a tavés de los años, algunos para hacerlo más eficiente y otros para quitarle funciones que ya pueden seguir soportándose.

    El camino más seguro llegados a este punto sería ir bajando de versión del lado del hosting (8.1., 8.0, 7.4 y así) hasta llegar a una que, o bien sea compatible con el sistema, o sea la más antigua soportada por el proveedor.

    Si se llega al primer escenario (Una versión superior a la que veníamos usando pero en la que el sistema funciona bien) perfecto, tema resuelto.

    Si, en cambio, la versión más antigua con la que podemos contar no es compatible con el sistema, tenemos trabajo por delante.

    Supongamos por tomar un ejemplo que la versión más baja que nos permite usar el proveedor es la 7.1.

    Entre la versión 5.5 y la 7.1 hubo tres versiones intermedias (5.6, 7.0 y la propia 7.1).

    La manera más segura de llegar de una versión de php a otra es pasar por todas las intermedias.

    Antes de que me lo digas, sí, es un trabajo arduo y molesto, lo sé… pero me temo que es la posibilidad menos riesgosa, así que… más vale empezar a trabajar.

    Lo que vas a necesitar es seguir este proceso:

    1. Instalar en un ambiente de pruebas la versión a la que vas a intentar migrar (5.6 sería la primera en este caso)
    2. Instalar y configurar el sistema en dicho ambiente de pruebas
    3. Ejecutar las pruebas
    4. Hacer los ajustes correspondientes al código
    5. Pasar a la siguiente versión y volver al paso 1

    Algunas herramientas que te van a ayudar

    Existen algunas herramientas en las que te podés apoyar para hacer este proceso algo menos laborioso:

    En general, migrar a una nueva versión de php no es precisamente una tarea sencilla pero si encima lo tenés que hacer a las apuradas… puede volverse una verdadera pesadilla.

    Esto es algo que difícilmente puedas prevenir si estás en ambientes de hosting que no controlás.

    Si este es tu caso tal vez te convenga evaluar migrarte hacia tu propio servidor.

  • 3 Herramientas para usar Docker con PHP

    3 Herramientas para usar Docker con PHP

    Escuchaste a más de un colega comentar que Docker es una gran herramienta y te estás empezando a preguntar si no te estás perdiendo de algo.

    Miraste un poco la documentación y, honestamente, el calificativo amigable le queda algo holgado… por decirlo amablemente.

    Todo ese tema de las imágenes, los contenedores, los volúmenes… es mucho.

    Y de entrada toparse con un archivo como:

    FROM php:7.1-apache
    
    LABEL vendor="Mautic"
    LABEL maintainer="Luiz Eduardo Oliveira Fonseca <luiz@powertic.com>"
    
    # Install PHP extensions
    RUN apt-get update && apt-get install --no-install-recommends -y \
        cron \
        git \
        wget \
        sudo \
        libc-client-dev \
        libicu-dev \
        libkrb5-dev \
        libmcrypt-dev \
        libssl-dev \
        libz-dev \
        unzip \
        zip \
        && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
        && rm -rf /var/lib/apt/lists/* \
        && rm /etc/cron.daily/*
    
    RUN docker-php-ext-configure imap --with-imap --with-imap-ssl --with-kerberos \
        && docker-php-ext-configure opcache --enable-opcache \
        && docker-php-ext-install imap intl mbstring mcrypt mysqli pdo_mysql zip opcache bcmath\
        && docker-php-ext-enable imap intl mbstring mcrypt mysqli pdo_mysql zip opcache bcmath
    
    # Install composer
    RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer
    
    # Define Mautic volume to persist data
    VOLUME /var/www/html
    
    # Define Mautic version and expected SHA1 signature
    ENV MAUTIC_VERSION 2.16.2
    ENV MAUTIC_SHA1 df6735df8d7d31cc6bc505c38ee8147b40b8311b
    
    # By default enable cron jobs
    ENV MAUTIC_RUN_CRON_JOBS true
    
    # Setting an root user for test
    ENV MAUTIC_DB_USER root
    ENV MAUTIC_DB_NAME mautic
    
    # Setting PHP properties
    ENV PHP_INI_DATE_TIMEZONE='UTC' \
        PHP_MEMORY_LIMIT=512M \
        PHP_MAX_UPLOAD=128M \
        PHP_MAX_EXECUTION_TIME=300
    
    # Download package and extract to web volume
    RUN curl -o mautic.zip -SL https://github.com/mautic/mautic/releases/download/${MAUTIC_VERSION}/${MAUTIC_VERSION}.zip \
        && echo "$MAUTIC_SHA1 *mautic.zip" | sha1sum -c - \
        && mkdir /usr/src/mautic \
        && unzip mautic.zip -d /usr/src/mautic \
        && rm mautic.zip \
        && chown -R www-data:www-data /usr/src/mautic
    
    # Copy init scripts and custom .htaccess
    COPY docker-entrypoint.sh /entrypoint.sh
    COPY makeconfig.php /makeconfig.php
    COPY makedb.php /makedb.php
    COPY mautic.crontab /etc/cron.d/mautic
    RUN chmod 644 /etc/cron.d/mautic
    
    # Enable Apache Rewrite Module
    RUN a2enmod rewrite
    
    # Apply necessary permissions
    RUN ["chmod", "+x", "/entrypoint.sh"]
    ENTRYPOINT ["/entrypoint.sh"]
    
    CMD ["apache2-foreground"]

    No da muchas ganas de incursionar, ¿cierto?

    Todo lo que querías era hacer una prueba sencilla. Como para comprobar por tus propios medios, si todo lo que te dijeron de Docker era realmente así y en cambio… tenés que ponerte a escribir archivos de texto inentendibles.

    Te tengo buenas noticias: hay varias herramientas muy simples que podés usar para generar los dichosos Dockerfile.

    Te presento algunas.

    Devilbox

    http://devilbox.org/ te permite crear un entorno moderno y altamente personalizado con soporte para LAMP sobre docker.

    Su instalación es sencilla:

    git clone https://github.com/cytopia/devilbox
    cd devilbox
    cp env-example .env
    docker-compose up

    Y listo.

    En tus manos un LAMP con Redis, Memcached, MongoDB, phpMyAdmin y un montón de herramientas de administración disponibles en http://localhost para hacer todo bien fácil.

    Ah, y claro, si querés ver qué hay detrás de la magia, el archivo docker-compose.yml está a tu disposición en el directorio donde clonaste el repo.

    PHPDocker

    https://phpdocker.io/ es un sitio donde podés, a través de un wizard, configurar la imagen Docker que querés generar:

    Una vez tenés todo donde debe estar descargás el archivo comprimido, lo descomprimís en el directorio que más te guste, docker-compose up y, voilá, tu entorno php Dockerizado está disponible con todos los condimentos que hayas seleccionado.

    Sail

    Sail es una herramienta perteneciente al framework Laravel. Si creas una nueva aplicación desde cero no tendrás más que ejecutar ./vendor/bin/sail up dentro del directorio raíz de tu proyecto para comenzar.

    Una vez descargado y configurado todo podrás entrar en http://localhost y disfrtuar de tu nuevo entorno de trabajo con Docker.

    Y, como siempre, el archivo docker-compose.yml estará allí para investigar/modificar.

    Y un par más…

    Un par de herramientas más que vale la pena conocer son:

    • Deck: una aplicación de escritorio basada en electron.js con la que puedes crear un ambiente entero para desarrollo. La iba a incluir en este listado pero al probarla falló la instalación… tal vez más adelante cuando esté más madura la agregue.
    • Las imágenes de ServerSideUp optimizadas para ir a producción.

    Ahora sí, todo listo para dockerizar tus aplicaciones!

  • 8 razones por las que falla una aplicación PHP

    Es una historia que se repite una y otra vez: el sistema funciona perfecto en tu XAMPP pero cuando lo subís al hosting misteriosamente nadie puede registrarse ni logearse.

    Es el mismo codigo en el webserver y en localhost, no entiendo por qué da error en uno sí y otro no

    Dentro de xampp no me presenta problema, sólo en el hosting

    Estoy trabajando con laravel y estoy tratando de generar un sitemap.xml automatico, pero cuando lo genero no me lo guarda en la carpeta publica, solo logre que me lo guarde en el raiz del cpanel y no puedo hacer que me lo genere dentro del public_html

    ¿Qué pasó? El código es exactamente el mismo, lo viste funcionar con tus propios ojos.

    La única diferencia es que la URL, en lugar de empezar por http://localhost empieza por http://dominio.com, ¿cierto?

    Me temo que las diferencias pueden ir bastante más allá de eso.

    Para evitarte unas cuantas vueltas y dolores de cabeza te voy a contar las razones más frecuentes que llevan a este comportamiento.

    Intentaré ir de lo más frecuente hacia lo menos.

    Aquí voy.

    La versión de PHP

    La primera gran dependencia que toda aplicación php tiene es el propio intérprete.

    Si desarrollás en forma local tenés instalado algún paquete que permite transformar esto:

    <html>
      <body><p><?php echo "Hola mundo!"; ?></p></body>
    </html>

    En

    <html>
      <body><p>Hola mundo!"</p></body>
    </html>

    Generalmente esta responsabilidad le corresponde al servidor web que tenés instalado en tu computadora (¿XAMPP tal vez?).

    En tu hosting hay otro servidor web con una configuración similar pero no necesariamente idéntica.

    PHP es un lenguaje que está en constante evolución, año a año salen nuevas versiones y, si bien se hace un esfuerzo muy grande por mantener la compatibilidad con versiones viejas, esto no siempre se logra.

    Es por eso que algunas cosas que funcionan en una versión dejan de funcionar en las siguientes.

    En conclusión: lo primero que deberías verificar es que tu versión de PHP local es igual a la que tiene tu hosting.

    El archivo php.ini

    Además del intérprete, php dispone de un archivo de configuración especial: php.ini.

    En este archivo se definen ciertos parámetros que determinan detalles de cómo el intérprete realiza esa transformación.

    Las variables que pueden configurarse a través de este archivo son ciertamente muchas. Algunas son más críticas que otras.

    Un ejemplo muy simple es el de la directiva short_open_tag. Si su valor es "1" php no tendrá problemas en interpretar algo como:

    <html>
      <body><p><? echo "Hola mundo!"; ?></p></body>
    </html>

    Sin embargo, si este no es el caso, php dejará el texto tal como está, con lo cual, en lugar de transformarlo al HTML que esperás, el visitante de tu sitio verá el código.

    En conclusión: si las cosas no funcionan vale la pena comparar los cotenidos de tu php.ini y los de tu hosting.

    Las extensiones php

    PHP como tal trae una cantidad de funcionalidad incorporada pero es muy común que ciertas funciones se dejen como opcionales por razones de eficiencia.

    Para esos casos en los que se necesitan funcionalidades extras, php cuenta con el mecanismo de extensiones.

    Si tu aplicación depende de alguna en particular, por ejemplo si necesita generar gráficos, es muy probable que requiera de la presencia de una extensión en particular en el ambiente de ejecución.

    Si tu hosting no tiene instalada dicha extensión las cosas no van a funcionar como lo esperás.

    En conclusión: verificá si tu aplicación requiere alguna extensión especial y, en tal caso, asegurate de que esté presente en tu hosting.

    Los permisos

    Para que una aplicación php pueda ejecutarse es preciso que el usuario que ejecuta el servidor web tenga ciertos permisos sobre el sistema de archivos.

    Como mínimo, debe ser capaz de leer los archivos .php que componen tu aplicación.

    En muchos casos también será necesario que pueda escribir en ciertos directorios.

    En conclusión: validá que el usuario que ejecuta el webserver tenga los permisos necesarios para realizar las tareas que debe.

    La topología de red

    Típicamente cuando desarrollas en forma local la red donde tu aplicación vive es bastante sencillita: sólo existe localhost.

    Esto quiere decir que todos los servicios de los que depende son accesibles a través de la dirección de red 127.0.0.1 (O el nombre localhost, lo que es lo mismo).

    Más en concreto, si tu aplicación usa un MySQL como base de datos, es casi seguro que en algún lugar de tu código habrá una línea que se parece a:

    $link = mysqli_connect("127.0.0.1", "mi_usuario", "mi_contraseña", "mi_bd");

    Y esto funcionará sin problemas.

    El tema es que, en un entorno de hosting compartido donde hay muchos sitios hosteados en una misma computadora, es muy común que los servicios estén distribuidos en diferentes nodos de la red del proveedor.

    Esto quiere decir que, al intentar conectar al servidor de MySQL a través de la dirección 127.0.0.1 se producirá un fallo.

    En conclusión: verifica que las conexiones a servicios satélites se estén realizando utilizando las direcciones que correspnden a la red de tu hosting.

    Las rutas absolutas

    Un problema similar al anterior se da cuando se utilizan rutas absolutas.

    Por ejemplo:

    <?php
    
    require_once '/var/www/html/libreria.php';

    Puede funcionar en tu computadora porque existe en tu disco un archivo llamado libreria.php en un directorio /var/www/html/

    Si bien es posible que dicho directorio exista también en tu hosting, es bastante probable que no sea así.

    Si ese archivo está dentro del árbol de directorios de tu aplicación, una mejor forma de lograr el mismo objetivo es utilizar rutas relativas:

    <?php
    
    require_once __DIR__.'/libreria.php';

    En conclusión: usá rutas relativas en lugar de absolutas.

    La configuración del Apache

    Otro elemento que puede afectar negativamente al funcionamiento de la aplicación es la configuración del servidor web. No es que necesariamente se trate a Apache pero lo más probable es que así sea, especialmente en un ambiente de hosting compartido.

    Estas directivas afectan la forma en que el servidor web atiende los pedidos de tus visitantes.

    Muchas aplicaciones php dependen de ciertas configuraciones existentes en el archivo .htaccess (Reglas de re-escritura por ejemplo).

    Para que esto funcione, el webserver debe tener habilitado el procesamiento de este tipo de archivos

    En conclusión: valida que la configuración de tu Apache coincida con la de tu hosting.

    El sistema operativo

    El último de los sospechosos, que muchas veces es pasado por alto, es el sistema operativo sobre el que está montado el servidor.

    Si estás desarrollando en Windows y tu hosting es un Linux hay buenas chances de que no todo funcione a la primera.

    La diferencia para referenciar archivos (Cosas como c:\progams\file.php vs. /var/www/html/file.php) o el hecho de que para Linux un archivo llamado file.php no es el mismo que File.php mientras que para Windows sí son sólo algunas de las sutiles diferencias que pueden darte la idea de que el sistema está listo para subirse cuando en realidad estás a punto de abrir la caja de Pandora.

    En conclusión: verificá que tu código no dependa de características particulares de un sistema operativo.

    Cómo usar esta información

    A lo largo de este post te conté las razones que más comúnmente hacen que un despliegue de una aplicación PHP sea más traumático de lo que debería.

    Ahora que las conocés te recomiendo que las revises nuevamente y las uses a modo de lista de verificación para planificar tus próximas entregas.

  • 6 simples pasos para limpiar tu código PHP

    6 simples pasos para limpiar tu código PHP

    El tema del código limpio está de moda, ¿no? Sí, pero también tiene sus fundamentos.

    El código limpio es más comprensible y, por ende, mantenible en el tiempo.

    Esto es especialmente importante cuando trabajas como parte de un equipo. Cuanto más claro sea el código, más fácil será para nuevos miembros hacerse productivos.

    Todo el mundo está de acuerdo en esto pero lo cierto es que limpiar un código que lleva años acumulando capas de mugre puede ser una tarea titánica.

    La respuesta más comúnmente usada ante estos escenarios es «¿para qué intentarlo cuando sé que no lo lograré?»

    Cuando en realidad el escenario debería ser más parecido a:

    Es decir: la opción de no limpiar no es realmente una opción… es cuestión de definir por dónde empezar.

    Pero ahora, volviendo al código, hay algunas técnicas de limpieza más sencillas (¡y menos riesgosas!) que otras.

    No es lo mismo cambiar el nombre de una variable que modificar la arquitectura.

    Así que… vamos al punto. Mi enfoque para pasar de algo como esto:

    <?php
    
    declare(strict_types=1);
    
    namespace GildedRose;
    
    final class GildedRose
    {
        /**
         * @param Item[] $items
         */
        public function __construct(
            private array $items
        ) {
        }
    
        public function updateQuality(): void
        {
            foreach ($this->items as $item) {
                if ($item->name != 'Aged Brie' and $item->name != 'Backstage passes to a TAFKAL80ETC concert') {
                    if ($item->quality > 0) {
                        if ($item->name != 'Sulfuras, Hand of Ragnaros') {
                            $item->quality = $item->quality - 1;
                        }
                    }
                } else {
                    if ($item->quality < 50) {
                        $item->quality = $item->quality + 1;
                        if ($item->name == 'Backstage passes to a TAFKAL80ETC concert') {
                            if ($item->sellIn < 11) {
                                if ($item->quality < 50) {
                                    $item->quality = $item->quality + 1;
                                }
                            }
                            if ($item->sellIn < 6) {
                                if ($item->quality < 50) {
                                    $item->quality = $item->quality + 1;
                                }
                            }
                        }
                    }
                }
    
                if ($item->name != 'Sulfuras, Hand of Ragnaros') {
                    $item->sellIn = $item->sellIn - 1;
                }
    
                if ($item->sellIn < 0) {
                    if ($item->name != 'Aged Brie') {
                        if ($item->name != 'Backstage passes to a TAFKAL80ETC concert') {
                            if ($item->quality > 0) {
                                if ($item->name != 'Sulfuras, Hand of Ragnaros') {
                                    $item->quality = $item->quality - 1;
                                }
                            }
                        } else {
                            $item->quality = $item->quality - $item->quality;
                        }
                    } else {
                        if ($item->quality < 50) {
                            $item->quality = $item->quality + 1;
                        }
                    }
                }
            }
        }
    }

    A algo como esto:

    <?php
    
    declare(strict_types=1);
    
    namespace GildedRose;
    
    final class GildedRose
    {
        const BACKSTAGE_PASSES_TO_A_TAFKAL_80_ETC_CONCERT = 'Backstage passes to a TAFKAL80ETC concert';
        const SULFURAS_HAND_OF_RAGNAROS = 'Sulfuras, Hand of Ragnaros';
        const MAX_ITEM_QUALITY = 50;
        const FIRST_SELLIN_THRESHOLD = 11;
        const SECOND_SELLIN_THRESHOLD = 6;
        const AGED_BRIE = 'Aged Brie';
    
        /**
         * @param Item[] $items
         */
        public function __construct(
            private array $items
        )
        {
        }
    
        public function updateQuality(): void
        {
            foreach ($this->items as $item) {
                $this->updateItem($item);
            }
        }
    
        /**
         * @param Item $item
         * @return void
         */
        protected function updateItem(Item $item): void
        {
            if ($this->isSulfuras($item)) {
    
                exit;
            }
    
            $this->updateItemQuality($item);
            $this->updateItemSellIn($item);
        }
    
        /**
         * @param Item $item
         * @return void
         */
        protected function updateItemQuality(Item $item): void
        {
            if ($this->isBackStagePass($item)) {
                if ($this->isQualityBelowMaximum($item)) {
                    $this->increaseQuality($item);
                    if ($item->sellIn < self::FIRST_SELLIN_THRESHOLD && $this->isQualityBelowMaximum($item)) {
                        $this->increaseQuality($item);
                    }
                    if ($item->sellIn < self::SECOND_SELLIN_THRESHOLD && $this->isQualityBelowMaximum($item)) {
                        $this->increaseQuality($item);
                    }
                }
            } elseif ($this->isAgedBrie($item)) {
                if ($this->isQualityBelowMaximum($item)) {
                    $this->increaseQuality($item);
                }
            } else {
                if ($this->isQualityAboveMinimum($item)) {
                    $this->reduceQuality($item);
                }
            }
        }
    
        /**
         * @param Item $item
         * @return void
         */
        protected function updateItemSellIn(Item $item): void
        {
            $this->decreaseSellIn($item);
    
            if ($this->isSellinExpired($item)) {
                if ($this->isAgedBrie($item)) {
                    if ($this->isQualityBelowMaximum($item)) {
                        $this->increaseQuality($item);
                    }
                } elseif ($this->isBackStagePass($item)) {
                    $item->quality = 0;
                }
            } else {
                if ($this->isQualityAboveMinimum($item)) {
                    $this->reduceQuality($item);
                }
            }
        }
    
        /**
         * @param Item $item
         * @return void
         */
        protected
        function reduceQuality(Item $item): void
        {
            $item->quality = $item->quality - 1;
        }
    
        /**
         * @param Item $item
         * @return bool
         */
        protected
        function isQualityAboveMinimum(Item $item): bool
        {
            return $item->quality > 0;
        }
    
        /**
         * @param Item $item
         * @return bool
         */
        protected
        function isQualityBelowMaximum(Item $item): bool
        {
            return $item->quality < self::MAX_ITEM_QUALITY;
        }
    
        /**
         * @param Item $item
         * @return void
         */
        protected
        function increaseQuality(Item $item): void
        {
            $item->quality = $item->quality + 1;
        }
    
        /**
         * @param Item $item
         * @return void
         */
        protected
        function decreaseSellIn(Item $item): void
        {
            $item->sellIn = $item->sellIn - 1;
        }
    
        /**
         * @param Item $item
         * @return bool
         */
        protected
        function isSellinExpired(Item $item): bool
        {
            return $item->sellIn < 0;
        }
    
        /**
         * @param Item $item
         * @return bool
         */
        protected
        function isSulfuras(Item $item): bool
        {
            return $item->name == self::SULFURAS_HAND_OF_RAGNAROS;
        }
    
        /**
         * @param Item $item
         * @return bool
         */
        protected
        function isAgedBrie(Item $item): bool
        {
            return $item->name == self::AGED_BRIE;
        }
    
        /**
         * @param Item $item
         * @return bool
         */
        protected
        function isBackStagePass(Item $item): bool
        {
            return $item->name == self::BACKSTAGE_PASSES_TO_A_TAFKAL_80_ETC_CONCERT;
        }
    }

    Consiste en ejecutar, casi mecánicamente, los siguientes pasos

    1. Eliminar el hard-coding

    Expresiones del tipo if ($item->quality < 50) { son bastante peligrosas. A priori surgen dos preguntas importantes:

    1. ¿Por qué 50 es un número especial?
    2. ¿Todos los 50 significan lo mismo?

    En general, este tipo de números mágicos, tienen un sentido claro dentro del contexto de una aplicación. Probablemente cuando se escribió este código dicho contexto era claro pero, al no hacelo explícito, se hace muy complicado comprenderlo para alguien que lo ve por primera vez.

    Una forma de evitar este problema es, simplemente, reemplazar este valor clavado por una constante de clase:

    const MAX_ITEM_QUALITY = 50;

    De esta forma se logra:

    1. Dar significado a un número que a simple vista parece arbitrario
    2. Permitir que si ese valor llega a cambiar en el futuro no sea necesario rastrear todos los lugares donde se usó el valor. Bastará con modificar la definición de la constante.

    Lo mismo vale para valores tipo string como en el caso de $item->name != 'Backstage passes to a TAFKAL80ETC concert'

    2. Extraer condicionales

    El siguiente paso que suelo dar en este proceso es extrear las condiciones a métodos propios. Por ejemplo:

    if ($item->name == 'Backstage passes to a TAFKAL80ETC concert') {

    Se transformará en:

    protected function isBackStagePass(Item $item): bool
    {
    return $item->name == self::BACKSTAGE_PASSES_TO_A_TAFKAL_80_ETC_CONCERT;
    }

    La idea es la misma. En lugar de, cada vez que se lea el código haya que concluir que el hecho de que name sea 'Backstage passes to a TAFKAL80ETC concert' significa que el ítem es un Backstage pass, dispongo de un método re-utilizable que me responde lo que realmente quiero saber.

    Este ejemplo puede parecer trivial al comienzo, pero no lo es tanto.

    ¿Qué pasaría si, más adelante, la determinación del tipo de item viniese dada por alguna otra propiedad?

    Este pequeño truco cobra todavía mayor importancia cuando las condiciones son complejas (cuando hay &&, || y/o muchos paréntesis)

    3. Extraer cuerpo de loops

    Del mismo modo, extraer los cuerpos de los bucles a métodos logra un resultado similar. Por un lado se reduce la complejidad del método y por otro se gana la posibilidad de re-utilizar la operación en diversos contextos.

    En el ejemplo se hace muy visible el ciclo principal:

    foreach ($this-&gt;items as $item) {
                if ($item-&gt;name != 'Aged Brie' and $item-&gt;name != 'Backstage passes to a TAFKAL80ETC concert') {
                    if ($item-&gt;quality &gt; 0) {
                        if ($item-&gt;name != 'Sulfuras, Hand of Ragnaros') {
                            $item-&gt;quality = $item-&gt;quality - 1;
                        }
                    }
                } else {
                    if ($item-&gt;quality &lt; 50) {
                        $item-&gt;quality = $item-&gt;quality + 1;
                        if ($item-&gt;name == 'Backstage passes to a TAFKAL80ETC concert') {
                            if ($item-&gt;sellIn &lt; 11) {
                                if ($item-&gt;quality &lt; 50) {
                                    $item-&gt;quality = $item-&gt;quality + 1;
                                }
                            }
                            if ($item-&gt;sellIn &lt; 6) {
                                if ($item-&gt;quality &lt; 50) {
                                    $item-&gt;quality = $item-&gt;quality + 1;
                                }
                            }
                        }
                    }
                }
    
                if ($item-&gt;name != 'Sulfuras, Hand of Ragnaros') {
                    $item-&gt;sellIn = $item-&gt;sellIn - 1;
                }
    
                if ($item-&gt;sellIn &lt; 0) {
                    if ($item-&gt;name != 'Aged Brie') {
                        if ($item-&gt;name != 'Backstage passes to a TAFKAL80ETC concert') {
                            if ($item-&gt;quality &gt; 0) {
                                if ($item-&gt;name != 'Sulfuras, Hand of Ragnaros') {
                                    $item-&gt;quality = $item-&gt;quality - 1;
                                }
                            }
                        } else {
                            $item-&gt;quality = $item-&gt;quality - $item-&gt;quality;
                        }
                    } else {
                        if ($item-&gt;quality &lt; 50) {
                            $item-&gt;quality = $item-&gt;quality + 1;
                        }
                    }
                }
            }

    Que se reemplaza por:

    public function updateQuality(): void
    {
    foreach ($this->items as $item) {
    $this->updateItem($item);
    }
    }

    4. Eliminar cláusulas else

    Siempre que sea posible, deberías preferir prescindir las cláusulas else en tu código.

    Una forma de lograrlo es utilizar early return.

    Por ejemplo:

    public function canSeeMovie(Person $person): bool
    {
       if ($person->getAge() >= 18) {
          ...
       } else {
          return false;
       }
    }

    Bien podría ser reemplazado por:

    public function canSeeMovie(Person $person): bool
    {
       if ($person->getAge() < 18) {
    
          return false;
       }
    
       ...
    }

    Esto facilita mucho la lectura. Primero se ponen todas las validaciones o condiciones que podrían hacer que el método no pueda ejecutarse por completo y luego se codifica para el camino feliz.

    Otro caso bastante parecido es el de usar valores por defecto:

    public function getMaxSpeed(Car $car): int
    {
       if (in_array($car->getName(), ["Ferrari", "Porsche"])) {
          $maxSpeed = 300;
       } else {
          $maxSpeed = 200;
       }
    
       return $maxSpeed;
    }

    Se transformaría en:

    public function getMaxSpeed(Car $car): int
    {
       $maxSpeed = 200;
    
       if (in_array($car->getName(), ["Ferrari", "Porsche"])) {
          $maxSpeed = 300;
       }
    
       return $maxSpeed;
    }

    5. Eliminar variables temporales

    Las variables temporales suelen crearse para aclarar cosas pero muchas veces su efecto es precisamente el contrario:

    public function showMovieTo(Movie $movie, Person $person): void 
    {
       $canSeeTheMovie = $this->canSeeMovie($person);
    
       if ($canSeeTheMovie) {
           $this->playMovie($movie);
       }
    }

    En este pequeño ejemplo vemos que al llegar a la línea if ($canSeeTheMovie) { se necesita volver hacia atrás buscando la última asignación realizada a la variable $canSeeTheMovie para determinar qué hará este método.

    Mucho más fácil de comprender es:

    public function showMovieTo(Movie $movie, Person $person): void 
    {
       if ($this->canSeeMovie($person)) {
           $this->playMovie($movie);
       }
    }

    Esta técnica no siempre es aplicable. En todo caso, lo segundo mejor por hacer es traer la definición y/o asignación de la variable lo más cerca de su uso que se pueda.

    6. Renombrar, renombrar y renombrar

    Este es un paso algo más sutil pero no menos importante.

    El código que escribimos debe ser legible por humanos… muy probablemente por nosotros mismos en un futuro no muy lejano, de modo que es casi un deber moral escribirlo de un modo legible.

    Encontrar buenos nombres para los elementos de nuestro código es una de las partes más complejas de programar, principalmente porque no hay recetas.

    Algunos lineamientos que te puedo dar:

    1. Escribir todo el código en inglés (No usar Spanglish)
    2. No usar abreviaturas (Preferir $unusedTableNames a $utNm)
    3. Poner nombres que revelen intención, no implementación (Prefereir $emptyAccounts sobre $emptyAccountsArray)
    4. En el caso de clases o funciones, poner nombres que coincidan con el código.

    Este último es, probablemente el punto más delicado. Durante la vida de un proyecto de software, es muy probable que un elemento cambie su definición conforme va pasando el tiempo.

    Cuando eso sucede, es decir, cuando te toca modificar el código del cuerpo de un método (O de una clase en su totalidad), vale la pena preguntarte si el nombre que tenía antes de tu intervención sigue reflejando lo que el método hace (o lo que aporta mejor dicho) y, en caso de no ser así, es una buena idea darle un nuevo nombre más ligado a la realidad.

    Y ahora… ¿qué?

    Casi todos los cambios que nombré en este post se pueden realizar de forma bastante simple y segura utilizando un IDE, más adelante tocará encarar el refactor propiamente dicho:

    1. Agregar tipado
    2. Re-distribuir responsabilidades
    3. Aplicar patrones de diseño

    Pero… antes de intentar avanzar por este camino hay que asegurarse de contar con buena covertura de tests.

    Si te interesa este tema te recomiendo leer el libro de Código Limpio de Robert C. Martin.

    Por último te animo a que practiques tus habilidades de refactoring con la kata Gilded Rose, de la que saqué el código que usé para el ejemplo principal.