Categoría: Buenas prácticas

En estos artículos podrás leer sobre buenas prácticas de programación

  • ¿Es mala práctica commitear el composer.lock?

    ¿Es mala práctica commitear el composer.lock?

    Acabas de montar tu proyecto.

    Es hora de instalar alguna que otra librería. Sin dudarlo un segundo arranca la seguidilla de composer require.

    Escribes algo de código, realizas tus pruebas, todo listo para hacer un commit.

    git add .

    git commit -m "Initial commit"

    Y de pronto… algo llama tu atención.

    ¿Por qué se están agregando dos archivos de composer?

    Más específicamente, ¿por qué el composer.json y el composer.lock?

    ¿Acaso el .lock no se genera automáticamente al ejecutar composer install?

    Es más, dando una rápida mirada se ve que el .lock pesa mucho más (¡muchísimo más!) que el .json:

    ¿Es realmente necesario engordar el repo con este archivo?

    Respuesta corta: sí.

    ¿Querés saber por qué?

    Te lo explico a continuación.

    composer.json vs. composer.lock

    Ambos archivos tienen formato json, así que, por este lado no pasa el tema.

    ¿Qué tiene composer.lock que no tenga composer.json?

    Si abrís ambos archivos notarás que son parecidos, pero no iguales.

    En principio, en el composer.json se almacenan los patrones de dependencias, mientras que en el composer.lock se almacenan las dependencias con sus versiones exactas (Más las dependencias de esas dependencias y otro montón de información).

    Un ejemplo:

    Donde composer.json dice:

    "doctrine/common": "^3.4"

    En composer.lock encontrarás:

    {
                "name": "doctrine/common",
                "version": "3.4.3",
                "source": {
                    "type": "git",
                    "url": "https://github.com/doctrine/common.git",
                    "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/doctrine/common/zipball/8b5e5650391f851ed58910b3e3d48a71062eeced",
                    "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced",
                    "shasum": ""
                },
                "require": {
                    "doctrine/persistence": "^2.0 || ^3.0",
                    "php": "^7.1 || ^8.0"
                },
                "require-dev": {
                    "doctrine/coding-standard": "^9.0 || ^10.0",
                    "doctrine/collections": "^1",
                    "phpstan/phpstan": "^1.4.1",
                    "phpstan/phpstan-phpunit": "^1",
                    "phpunit/phpunit": "^7.5.20 || ^8.5 || ^9.0",
                    "squizlabs/php_codesniffer": "^3.0",
                    "symfony/phpunit-bridge": "^6.1",
                    "vimeo/psalm": "^4.4"
                },
                "type": "library",
                "autoload": {
                    "psr-4": {
                        "Doctrine\\Common\\": "src"
                    }
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Guilherme Blanco",
                        "email": "guilhermeblanco@gmail.com"
                    },
                    {
                        "name": "Roman Borschel",
                        "email": "roman@code-factory.org"
                    },
                    {
                        "name": "Benjamin Eberlei",
                        "email": "kontakt@beberlei.de"
                    },
                    {
                        "name": "Jonathan Wage",
                        "email": "jonwage@gmail.com"
                    },
                    {
                        "name": "Johannes Schmitt",
                        "email": "schmittjoh@gmail.com"
                    },
                    {
                        "name": "Marco Pivetta",
                        "email": "ocramius@gmail.com"
                    }
                ],
                "description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, proxies and much more.",
                "homepage": "https://www.doctrine-project.org/projects/common.html",
                "keywords": [
                    "common",
                    "doctrine",
                    "php"
                ],
                "support": {
                    "issues": "https://github.com/doctrine/common/issues",
                    "source": "https://github.com/doctrine/common/tree/3.4.3"
                },
                "funding": [
                    {
                        "url": "https://www.doctrine-project.org/sponsorship.html",
                        "type": "custom"
                    },
                    {
                        "url": "https://www.patreon.com/phpdoctrine",
                        "type": "patreon"
                    },
                    {
                        "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcommon",
                        "type": "tidelift"
                    }
                ],
                "time": "2022-10-09T11:47:59+00:00"
            },

    En otras palabras, en el archivo composer.json se almacena la mínima información necesaria para generar el composer.lock

    Ya sé, ya sé, todavía no respondí a la pregunta: si puedo generar el composer.lock a partir del composer.json… ¿para qué quiero comitear el primero?

    La respuesta radica en el hecho de que un patrón de dependencias puede resolverse con muchas opciones diferentes.

    Por ejemplo,

    "doctrine/common": "^3.4"

    Podría satisfacerse con doctrine/common en sus versiones :

    • 3.4.0
    • 3.4.1
    • 3.4.2
    • 3.4.3
    • 3.4.4
    • 3.4.5

    Aunque no sería compatible con 3.3.* ni con 3.5.*.

    El sistema de restricciones de versiones tiene sus vueltas pero si querés aprender más podés consultarlo directamente acá.

    Y todo esto es importante porque…?

    Porque, si hiciste tus pruebas con la versión x.y.z de una librería y luego, al momento de instalar la aplicación, sin que te des cuenta instalás la versión x.y.z+1 es posible que te encuentres con ese tipo de sorpresitas que a nadie le gustan.

    De hecho, una de las razones más importantes que dieron origen a Composer es precisamente esta, asegurarte de que las versiones de las dependencias estén sincronizadas entre entornos.

    Por eso el archivo se llama .lock, porque las dependencias están cerradas.

    Así que, si pensabas ahorrarte algunos bytes en el repo… me temo que no estás de suerte en esta ocasión.

  • Scripts de CLI: ¿dentro o fuera de Docker?

    Scripts de CLI: ¿dentro o fuera de Docker?

    Tenés una aplicación web montada sobre Docker.

    Cuando accedés usando el navegador todo funciona a las mil maravillas.

    Está todo listo para ir a producción.

    O casi.

    Existen algunas pequeñas tareas que hay que hacer por fuera de la web. Limpiar archivos viejos… borrar las cuentas de usuario inactivas… lo típico, bah.

    Qué mejor para esto que un script de CLI, ¿no?

    Así que, ahí fuiste a codearlo.

    Y ahora, hay que probarlo:

    php my_script.php

    No funciona.

    ¿Qué pasó?

    La conexión a la base de datos falla.

    ¿Cómo es posible?

    A ver qué dice el archivo .env:

    DB_HOST="db"
    DB_NAME="my_db"
    DB_USER="my_user"
    DB_PASS="my_pass"

    Nada extraño por aquí.

    El archivo de conexión:

    <?php
    
    $dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
    $dotenv->load();
    $conn = new PDO("mysql:dbname={$_ENV['DB_NAME']};host={$_ENV['DB_HOST'}", $_ENV['DB_USER'], $_ENV['DB_PASS'] );

    Por aquí tampoco hay nada raro…

    ¡Momento!

    Si la URL a la que ingresás es http://localhost:8080… el host de la db ¿no debería ser localhost?

    Probar no cuesta mucho… cambiás db por localhost y… ¡funciona!

    Listo, vamos a producción.

    mmm, mejor hacemos una última prueba del sitio, ¿no?

    Boom. Error 500.

    PHP Fatal error:  Uncaught PDOException: SQLSTATE[HY000] [2002] No such file or directory in...

    ¿Cómo es posible? Si recién funcionaba…

    ¿Es que acaso es imposible hacer funcionar la web y el script de CLI sobre Docker?

    No, claro que no.

    Veámoslo paso a paso.

    Qué pasa cuando se accede vía web

    Cuando accedés a tu aplicación a través de la url http://localhost:8080, a pesar de que diga localhost, no es realmente tu computadora la que atiende esa petición (Bueno técnicamente sí lo es, pero a través de Docker).

    El :8080 juega un papel muy importante.

    Para que esto funcione, el puerto 8080 debe estar mapeado al puerto 80 del webserver que está corriendo en tu contenedor Docker.

    Esto significa que, si bien tu computadora está escuchando a través del puerto 8080, lo que hace es re-enviar todo el tráfico recibido a través de él hacia el puerto 80 dentro del contenedor Docker configurado a tal efecto (Probablemente esto está definido en el archivo docker-compose.yml).

    Distinto sería el caso si estuvieses usando el servidor incorporado a php (Es decir, si iniciaras tu aplicación vía php -S localhost:8080). En tal caso, la web y el CLI estarían usando el mismo entorno y, por lo tanto, no tendrías problemas.

    Veamos ahora qué es lo que ocurre en el otro caso.

    Qué pasa cuando se accede vía CLI

    Lo primero que debés comprender es qué es exactamente lo que se ejecuta cuando hacés php my_script.php.

    Ante todo, estás invocando al intérprete de php pasándole como argumento la cadena my_script.php.

    Hasta aquí supongo que no hay nada muy novedoso, ¿cierto?

    El problema comienza cuando tu script depende de configuraciones de entorno, como en este ejemplo.

    Lo que ocurre es que, precisamente, el entorno de tu host es diferente del de los contenedores Docker. De eso se tratan los contenedores: de unidades de ejecución aisladas del host.

    De hecho, Docker tiene su propio manejo de redes interno.

    Esto quiere decir que el nombre db dentro del contenedor está asociado con una IP, mientras que fuera del entorno Docker no.

    Cómo solucionar el problema

    Ahora que tenés claro por dónde pasa el problema, la solución es ejectuar el script de PHP dentro del contenedor.

    Tenés varias formas de hacerlo. Te comento rápidamente dos de ellas:

    Con docker-compose

    Si estás usando docker-compose podés ejecutar el siguiente comando:

    docker-compose exec my_service php my_script.php

    Suponiendo que el servicio donde está tu script está activo y se llama my_service, lo que verás en pantalla será el resultado de la ejecución de tu script.

    Sin docker-compose

    Si no usás docker-compose podés usar un comando del estilo de:

    docker exec -v $(pwd):/var/www my_container php my_script.php

    Este ejemplo es muy similar al de arriba. La principal diferencia es que en lugar de referirte a un servicio en ejecución, tenés que referirte directamente a un contenedor (my_container en este ejemplo) y tenés que montar los volúmenes en forma explícita.

    Si el contenedor está corriendo el resultado que obtendrás será el mismo que el anterior.

    Un pequeño consejo

    Algo que puede ayudarte a evitar este tipo de situaciones es eliminar el php de tu host.

    De esta forma, no vas a tener más opción que ejecutarlo dentro de Docker cuando así lo requieras.

    Es cierto, es una opción algo extremista pero aún así puede resultarte útil ya que de esta forma siempre estarás seguro de que la versión de php que está utilizando tu script es exactamente la que esperás.

  • 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!

  • 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.

  • Cómo garantizar un estándar de codificación en PHP

    Cómo garantizar un estándar de codificación en PHP

    ¿Alguna vez te tocó trabajar sobre el código de otras personas? Apuesto a que sí.

    A que es molesto encontrar cosas como esta:

    <?php
    
    if ($nombre== 'Pedro')
       echo 'Hola Pedro!';
    else{
       echo 'Tú no eres Pedro!';
    }
    
    if ('Juan' ==$nombre)
       echo 'Hola Juan!';
    else
       echo 'Tú no eres Juan!';

    ¿O no?

    O tal vez te parezca lo mismo que:

    <?php
    
    if ('Pedro' == $nombre) {
       echo 'Hola Pedro!';
    } else {
       echo 'Tú no eres Pedro!';
    }
    
    if ('Juan' == $nombre) {
       echo 'Hola Juan!';
    } else {
       echo 'Tú no eres Juan!';
    }

    Si ese es el caso, puedes dejar de leer este artículo, dudo que haya algo valioso para tí en lo que queda por delante.

    En mi caso, estoy convencido de que la calidad del código es tan importante como la de la funcionalidad de la aplicación que ese código sustenta.

    El caso es que ambos códigos son funcionalmente equivalentes, de modo que… ¿qué es lo que está mal ahí?

    El problema es que no se está utilizando un estándar de codificación.

    Qué es un estándar de codificación

    Un estándar de codificación es una serie de reglas que determinan cómo debe escribirse el código.

    El objetivo es lograr un código fácil de leer por otros humanos (para la computadora mientras funcione todo lo demás da igual).

    Un ejemplo de una regla como esta podría ser «todos los if llevan {} independientemente de que haya una línea dentro del bloque o más de una«.

    Por qué es importante usar un estándar de codificación

    Los estándares de codificación ayudan a disminuir el esfuerzo necesario para escribir el código: no se pierde tiempo tomando micro-decisiones como si poner o no poner las {} en cada situación.

    A la vez, el uso de un estándar de codificación hace más fácil la lectura del código escrito por diferentes personas, lo que hace más sencillo el mantenimiento del código a largo plazo.

    En definitiva, seguir un estándar de codificación permite disminuir la carga cognitiva que soportan los desarrolladores.

    Esta es la misma razón por la que no deberías escribir tu código en spanglish.

    Qué estándares de codificación existen en PHP

    El estándar más ampliamente aceptado actualmente es PSR-12 pero es bastante común que cada organización cree su propio estándar, esperablemente basado en alguno pre-existente, lo cual suele derivar en una situación como la que bien ilustra este cómic de xkcd:

    Cómo adaptar el código existente al estándar

    Una vez se ha definido el estándar a utilizar, no suele ser complejo implementarlo en el código que se generará a partir del momento actual.

    El desafío consiste en realizar las modificaciones requeridas al código pre-existente para que sea compatible con el estándar.

    Aquí existen básicamente dos caminos posibles:

    Opción 1: Manualmente

    Opción 2: Usando php-cs-fixer.

    Qué es php-cs-fixer

    php-cs-fixer es una herramienta de línea de comandos escrita en php por Fabien Potencier y Dariusz Rumiński cuyo objetivo es, precisamente, el de modificar código de modo de garantizar que cumpla con las reglas definidas en un estándar de codificación.

    Su uso es bastante simple, sólo requiere indicarle la ruta a la base de código sobre la que se trabajará y las reglas que se desea hacer cumplir.

    Ejemplo de uso de php-cs-fixer

    Partiendo de un código que no respeta el estándar PSR-12, usando este comando:

    ./php-cs-fixer fix -v

    Se obtiene el siguiente resultado:

    PHP CS Fixer 3.7.0 #StandWithUkraine️ by Fabien Potencier and Dariusz Ruminski.
    PHP runtime: 8.1.3
    Loaded config default from "/home/mauro/Code/car-rental-php/.php-cs-fixer.dist.php".
    Using cache file ".php-cs-fixer.cache".
    FSSSSSSFFFSSSFSSSSFSFSFFSFFFSSFFFFFFFFFFFFFFFFFFFF                                                                                                        50 / 50 (100%)
    Legend: ?-unknown, I-invalid file syntax (file ignored), S-skipped (cached or empty file), .-no changes, F-fixed, E-error
       1) www/test_db_pdo.php (braces, single_blank_line_at_eof)
       2) www/tests/_support/Helper/Functional.php (braces, blank_line_after_opening_tag)
       3) www/tests/_support/Helper/Acceptance.php (braces, blank_line_after_opening_tag)
       4) www/tests/_support/Helper/Unit.php (braces, blank_line_after_opening_tag)
       5) www/tests/acceptance/CarDetailsCest.php (no_spaces_inside_parenthesis)
       6) www/index.php (full_opening_tag)
       7) www/templates/components/navbar.php (braces)
       8) www/templates/profile.php (braces)
       9) www/templates/register.php (indentation_type, braces)
      10) www/templates/rentals.php (braces)
      11) www/templates/car.php (elseif, braces)
      12) www/templates/signin.php (braces)
      13) www/templates/rent.php (elseif, braces)
      14) www/templates/home.php (elseif, braces)
      15) www/classes/Router.php (class_definition, braces, single_blank_line_at_eof)
      16) www/classes/Utils.php (class_definition, braces, single_blank_line_at_eof)
      17) www/classes/db/UserService.php (class_definition, braces, array_syntax, ternary_operator_spaces, single_blank_line_at_eof)
      18) www/classes/db/Database.php (class_definition, braces, method_argument_space, single_blank_line_at_eof)
      19) www/classes/db/RentalService.php (class_definition, braces, array_syntax, single_blank_line_at_eof)
      20) www/classes/Renderer.php (class_definition, braces, array_syntax, single_blank_line_at_eof)
      21) www/classes/pages/Signin.php (elseif, class_definition, braces, array_syntax, single_blank_line_at_eof)
      22) www/classes/pages/Profile.php (class_definition, braces, no_spaces_inside_parenthesis, single_blank_line_at_eof)
      23) www/classes/pages/Register.php (elseif, class_definition, braces, array_syntax, single_blank_line_at_eof)
      24) www/classes/pages/CarDetails.php (class_definition, braces, single_blank_line_at_eof)
      25) www/classes/pages/Homepage.php (class_definition, braces, no_spaces_inside_parenthesis, single_blank_line_at_eof)
      26) www/classes/pages/Rentals.php (class_definition, braces, no_spaces_inside_parenthesis, single_blank_line_at_eof)
      27) www/classes/pages/BasicPage.php (class_definition, braces, visibility_required, single_blank_line_at_eof)
      28) www/classes/pages/NotFound.php (class_definition, braces, single_blank_line_at_eof)
      29) www/classes/pages/Rent.php (class_definition, braces, no_spaces_inside_parenthesis, array_syntax, single_blank_line_at_eof)
      30) www/classes/pages/Logout.php (class_definition, braces, single_blank_line_at_eof)
      31) www/phpinfo.php (blank_line_after_opening_tag, single_blank_line_at_eof)
      32) www/test_db.php (blank_line_after_opening_tag)
    
    Checked all files in 0.178 seconds, 14.000 MB memory used

    Donde puede verse qué archivos han sido modificados y qué regla se ha aplicado a cada uno.

    Cómo especificar qué reglas aplicar en php-cs-fixer

    Existen dos formas de especificar qué reglas se aplicarán en una corrida en particular:

    1. Parámetros al momento de ejecutar el script
    2. Mediante un archivo de configuración

    Independientemente de cuál sea el método elegido, las reglas pueden ser especificadas en forma explícita (una por una) o bien usando conjuntos pre-definidos.

    Las reglas y los conjuntos se diferencian porque los últimos tienen un nombre que comienza con @.

    En mi caso estoy usando este archivo de configuración (.php-cs-fixer.dist.php):

    <?php
    
    $finder = PhpCsFixer\Finder::create()
        ->in(__DIR__.'/www')
    ;
    
    $config = new PhpCsFixer\Config();
    return $config->setRules([
            '@PSR12' => true,
            'array_syntax' => [
                    'syntax' =>
                    'short'
            ],
            'full_opening_tag' => true,
        ])
        ->setFinder($finder)
    ;

    Esto quiere decir que las reglas que se aplicarán serán las siguientes:

    El código antes y después de php-cs-fixer

    Aquí puedes ver las diferencias que se generaron en algunos archivos después de la corrida de php-cs-fixer:

    git diff www/classes/Renderer.php

    diff --git a/www/classes/Renderer.php b/www/classes/Renderer.php
    index c5e2e0f..ac90cec 100644
    --- a/www/classes/Renderer.php
    +++ b/www/classes/Renderer.php
    @@ -1,15 +1,16 @@
     <?php
     
    -class Renderer {
    +class Renderer
    +{
    +    private static $injection = [];
     
    -    private static $injection = array();
    -
    -    public static function inject($key, $value){
    +    public static function inject($key, $value)
    +    {
             self::$injection[$key] = $value;
         }
     
    -    public static function render($contentFile, $variables = array()) {
    -
    +    public static function render($contentFile, $variables = [])
    +    {
             $contentFileFullPath = "../templates/" . $contentFile;
     
             // making sure passed in variables are in scope of the template
    @@ -28,20 +29,19 @@ class Renderer {
                 }
             }
     
    -    require_once("../templates/components/header.php");
    +        require_once("../templates/components/header.php");
     
    -    echo "\n<div class=\"container\">\n";
    +        echo "\n<div class=\"container\">\n";
     
    -    if (file_exists($contentFileFullPath)) {
    -        require_once($contentFileFullPath);
    -    } else {
    -        require_once("../templates/error.php");
    -    }
    +        if (file_exists($contentFileFullPath)) {
    +            require_once($contentFileFullPath);
    +        } else {
    +            require_once("../templates/error.php");
    +        }
     
    -    // close container div
    -    echo "</div>\n";
    +        // close container div
    +        echo "</div>\n";
     
    -    require_once("../templates/components/footer.php");
    +        require_once("../templates/components/footer.php");
    +    }
     }
    -
    -}
    \ No newline at end of file

    git diff www/templates/register.php

    diff --git a/www/templates/register.php b/www/templates/register.php
    index 1aa92ce..aab01a6 100644
    --- a/www/templates/register.php
    +++ b/www/templates/register.php
    @@ -1,6 +1,6 @@
     <div class="panel panel-default">
         <div class="panel-body">
    -        <?php if($loginInfo == 0) { ?>
    +        <?php if ($loginInfo == 0) { ?>
     
             <form class="form-horizontal" method="post" action="">
                 <fieldset>
    @@ -96,20 +96,20 @@
             </form>
             <br>
             <?php
    -            if(isset($errors)) {
    +            if (isset($errors)) {
                     foreach ($errors as $error) {
                         echo "<div class=\"alert alert-dismissible alert-danger fade in\">\n" .
    -  					"<button type=\"button\" class=\"close\" data-dismiss=\"alert\">&times;</button>\n" .
    -  					"$error\n" .
    -				    "</div>\n";
    +                    "<button type=\"button\" class=\"close\" data-dismiss=\"alert\">&times;</button>\n" .
    +                    "$error\n" .
    +                    "</div>\n";
                     }
                 }
     
    -            if(isset($success) && strlen($success) > 0) {
    +            if (isset($success) && strlen($success) > 0) {
                     echo "<div class=\"alert alert-dismissible alert-success fade in\">\n" .
    -  					"<button type=\"button\" class=\"close\" data-dismiss=\"alert\">&times;</button>\n" .
    -  					"$success\n" .
    -				"</div>\n";
    +                    "<button type=\"button\" class=\"close\" data-dismiss=\"alert\">&times;</button>\n" .
    +                    "$success\n" .
    +                "</div>\n";
                 }
             ?>

    git diff www/classes/db/UserService.php

    diff --git a/www/classes/db/UserService.php b/www/classes/db/UserService.php
    index 148bc00..1f5dd82 100644
    --- a/www/classes/db/UserService.php
    +++ b/www/classes/db/UserService.php
    @@ -2,9 +2,10 @@
     
     require_once('../classes/db/Database.php');
     
    -class User {
    -
    -    public static function isUserAdmin($id) {
    +class User
    +{
    +    public static function isUserAdmin($id)
    +    {
             $query = "SELECT _id FROM admins WHERE user_id = :id";
     
             $stmt = Database::getInstance()
    @@ -14,13 +15,15 @@ class User {
             $stmt->bindParam(":id", $id);
             $stmt->execute();
     
    -        if ($stmt->rowCount() > 0)
    +        if ($stmt->rowCount() > 0) {
                 return $stmt->fetchColumn();
    -        else
    +        } else {
                 return 0;
    +        }
         }
     
    -    public static function getUserInfo($id) {
    +    public static function getUserInfo($id)
    +    {
             $query = "SELECT first_name, last_name FROM user WHERE _id = :id";
     
             $stmt = Database::getInstance()
    @@ -33,7 +36,8 @@ class User {
             return $stmt->fetch(PDO::FETCH_ASSOC);
         }
     
    -    public static function getUserDetails($id) {
    +    public static function getUserDetails($id)
    +    {
             $query = "SELECT *, date_format(join_time, '%D %b %Y, %I:%i %p') as join_date  FROM user, address WHERE user._id = :id AND user.address_id = address._id";
     
             $stmt = Database::getInstance()
    @@ -46,7 +50,8 @@ class User {
             return $stmt->fetch(PDO::FETCH_ASSOC);
         }
     
    -    private static function userExists($id, $method) {
    +    private static function userExists($id, $method)
    +    {
             $query = "SELECT _id FROM user WHERE $method = :$method";
     
             $stmt = Database::getInstance()
    @@ -56,18 +61,21 @@ class User {
             $stmt->bindParam(":$method", $id);
             $stmt->execute();
     
    -        if ($stmt->rowCount() > 0)
    +        if ($stmt->rowCount() > 0) {
                 return $stmt->fetchColumn();
    -        else
    +        } else {
                 return 0;
    +        }
         }
     
    -    public static function doesUserExist($id) {
    +    public static function doesUserExist($id)
    +    {
             $_id = self::userExists($id, "username");
    -        return $_id>0?$_id:self::userExists($id, "email");
    +        return $_id>0 ? $_id : self::userExists($id, "email");
         }
     
    -    public static function verifyUser($id, $password) {
    +    public static function verifyUser($id, $password)
    +    {
             $query = "SELECT first_name, password FROM user WHERE _id = :id";
     
             $stmt = Database::getInstance()
    @@ -80,13 +88,14 @@ class User {
             if ($stmt->rowCount() > 0) {
                 $user = $stmt->fetch(PDO::FETCH_ASSOC);
     
    -            return password_verify($password, $user['password'])?$user['first_name']:false;
    +            return password_verify($password, $user['password']) ? $user['first_name'] : false;
             }
     
             return false;
         }
     
    -    public static function insertAddress($addressArray) {
    +    public static function insertAddress($addressArray)
    +    {
             $fields = ['street', 'city', 'state', 'country', 'zip'];
     
             $query = 'INSERT INTO address(' . implode(',', $fields) . ') VALUES(:' . implode(',:', $fields) . ')';
    @@ -95,7 +104,7 @@ class User {
                 ->getDb()
                 ->prepare($query);
     
    -        $prepared_array = array();
    +        $prepared_array = [];
             foreach ($fields as $field) {
                 $prepared_array[':'.$field] = @$addressArray[$field];
             }
    @@ -104,7 +113,8 @@ class User {
             return Database::getInstance()->getDb()->lastInsertId();
         }
     
    -    public static function insertUser($userArray) {
    +    public static function insertUser($userArray)
    +    {
             $fields = ['first_name', 'last_name', 'email', 'username', 'password', 'ph_no', 'gender', 'address_id'];
     
             $query = 'INSERT INTO user(' . implode(',', $fields) . ') VALUES(:' . implode(',:', $fields) . ')';
    @@ -112,7 +122,7 @@ class User {
             $db = Database::getInstance()->getDb();
             $stmt = $db->prepare($query);
     
    -        $prepared_array = array();
    +        $prepared_array = [];
             foreach ($fields as $field) {
                 $prepared_array[':'.$field] = @$userArray[$field];
             }
    @@ -135,5 +145,4 @@ class User {
     
             return $id;
         }
    -
    -}
    \ No newline at end of file
    +}

    A que se ve mejor que ir archivo por archivo aplicando cada regla, ¿cierto?

    Cómo garantizar la adhesión al estándar

    Por último, para garantizar que algo sucede, nada mejor que una buena automatización.

    En este caso, será cuestión de agregar la ejecución de php-cs-fixer a algún hook de git o al sistema de integración continua que se esté usando.

    No quedan excusas para no implementar un estándar de codificación en tus proyectos php.

  • Por qué programar en Spanglish es una mala idea

    Por qué programar en Spanglish es una mala idea

    Es muy común, en los comienzos de la carrera profesional, la adopción de malas prácticas.

    En este sentido los desarrolladores de habla hispana tienen una desventaja respecto de sus pares angloparlantes.

    Para los segundos palabras como if, for o while tienen sentido por sí mismas.

    Los primeros en cambio requieren un esfuerzo extra para hacer la traducción, por pequeño que sea.

    El hecho de que no contar con el inglés como lengua materna lleva muchas veces a plasmar las ideas utilizando el vocabulario que resulta natural.

    Sin embargo, las palabras reservadas del lenguaje utilizado, en este caso PHP, están tomadas del inglés.

    Es por eso que es muy común encontrar construcciones como esta en el código producido por programadores latinos:

    if (!empty($usuariosRegistrados)) {
      ...
    }

    Este código no tendrá problemas en ser ejecutado, sin embargo, encierra varios inconvenientes no siempre detectables a simple vista.

    Aumento de la carga cognitiva

    Este tipo de estructuras fuerza al cerebro a hacer un esfuerzo de traducción (sea de Español a Inglés o viceversa) que, por pequeño que sea, afecta el rendimiento.

    De alguna forma es similar a la diferencia entre:

    $verbose = $input->getOption('verbose');
    
    if ( $verbose ) {
        $output->writeln('Importing from: '. $filename);
    }
    
    $f = fopen( $filename, 'r' );
    $doctrine = $this->getContainer()->get('doctrine');
    $em = $doctrine->getEntityManager();
    
    $processed = $newAccounts = $newAgents = $changedAgents = $skipped = 0;
    $limit = $input->getOption('limit');
    if ( $limit && $verbose ) {
        $output->writeln('Processing maximum '.$limit.' accounts');
    }

    Y:

    $verbose = $input->getOption('verbose');if ( $verbose ) {    $output->writeln('Importing from: '. $filename);}$f = fopen( $filename, 'r' );$doctrine = $this->getContainer()->get('doctrine');
    $em = $doctrine->getEntityManager();
    $processed = $newAccounts = $newAgents = $changedAgents = $skipped = 0;$limit = $input->getOption('limit');if ( $limit && $verbose ) {    $output->writeln('Processing maximum '.$limit.' accounts');}

    Dificultad para la colaboración internacional

    Este punto se hace evidente cuando se requiere que desarrolladores que no hablan Español tengan una participación activa en el proyecto.

    Comprender código escrito por otras personas (O por nosotros mismos habiendo pasado un tiempo considerable) resulta difícil. Si a esa complejidad se le suma la falta de significado de los identificadores, la tarea se volverá realmente ardua, cuando no directamente imposible.

    Basta pensar en un código como:

    if (!empty($a) && $bxbhg > 50 ) {
      ...
    }

    Independientemente de la experiencia y conocimientos que se tengan sobre PHP, trabajar sobre este código no será sencillo, mucho menos placentero.

    Imposibilidad de aprovechar el potencial de herramientas

    Los entornos de desarrollo (IDEs) modernos, los frameworks y demás herramientas a menudo incorporan caractetísticas de generación de código en forma automática.

    Por ejemplo, un framework de testing automatizado como phpUnit puede generar sus resultados de esta forma:

    PHPUnit 9.5.10 by Sebastian Bergmann and contributors.
    
    ..                                                                  2 / 2 (100%)
    
    Time: 00:00.009, Memory: 6.00 MB
    
    OK (2 tests, 2 assertions)    

    O de esta:

    PHPUnit 9.5.10 by Sebastian Bergmann and contributors.
    
    Calculator
     ✔ Add adds
    
    Time: 00:00.004, Memory: 6.00 MB
    
    OK (2 tests, 2 assertions)

    Según los parámetros pasados en la invocación del ejecutable.

    Claro que, para que esto tenga sentido, el nombre del método a ser probado debe tener algún significado como frase escrita en inglés.

    En este caso sería:

    <?php
    
    declare(strict_types=1);
    
    use PHPUnit\Framework\TestCase;
    
    final class CalculatorTest extends TestCase
    {
            public function testAddAdds()
            {
                    $sut = new Calculator();
                    $this->assertEquals(8, $sut->add(5, 3));
            }
    }

    Entonces… ¿es necesario aprender inglés?

    La respuesta depende en gran medida de los objetivos profesionales.

    Para quien aspire a integrar equipos de desarrollo compuestos por profesionales de diversas nacionalidades, no dominar el inglés puede convertirse en un limitante importante.

    Claro que un buen nivel de inglés no es suficiente para competir globalmente pero es un buen comienzo.

  • Cuál es la mejor forma de almacenar fechas en MySQL

    Cuál es la mejor forma de almacenar fechas en MySQL

    ¿Tenés que desarrollar una aplicación que maneja fechas?

    Tal vez un portal para reserva de turnos, o quizás algún sistema de membresías por tiempo limitado o por qué no un programa que le pregunte al visitante su fecha de nacimiento y le diga el signo del horóscopo chino al que pertenece.

    Más allá de cuál sea el objetivo de la aplicación en algún lado vas a necesitar almacenar fechas.

    Y ese lado será probablemente una base de datos relacional y, más aún, si estás usando PHP, seguramente sea MySQL.

    Existen varias opciones que podrías usar para definir el tipo de datos del campo en cuestión y la decisión puede no ser trivial.

    Usar un VARCHAR para almacenar un dato fecha

    Si bien técnicamente podrías guardar una fecha en un campo de tipo VARCHAR (O cualquier otro tipo string), esto te hará difícil resolver algunos problemas como por ejemplo ordenar un set de resultados en función de la línea de tiempo.

    Es decir, si una fecha es '2020-02-01' y la otra es '19000-02-01' el motor te dirá que la primera es posterior a la segunda.

    Esto sucede porque, en lugar de tomar el valor como una fecha, se lo está tratando como una simple cadena de caracteres y, por lo tanto, se está aplicando el orden lexicográfico.

    Esto significa que para comparar se están tomando los caracteres y se comparan uno a uno, de este modo:

    20200201
    190000201

    Por otra parte, como se trata de cadenas, el motor no será capaz de determinar que '2020-13-35' no es un valor aceptable.

    Así que… no te lo recomiendo

    Usar un TIMESTAMP para almacenar un dato fecha

    Un segundo tipo de datos que podrías usar para guardar fechas es el TIMESTAMP.

    Este seguramente va a funcionar mejor que el VARCHAR, pero tampoco es el ideal.

    El TIMESTAMP es, internamente, un número muy grande que mide cuántos segundos pasaron desde segundo uno de UNIX (1970-01-01 00:00:01).

    Usualmente este tipo de datos se utiliza para operaciones que requieren altísima precisión, como procesos de tiempo real… un poco exagerado cuando se quiere saber la fecha en que una persona recibió su último aumento de sueldo, ¿no?

    Usar un DATE para almacenar un dato fecha

    La mejor opción es usar un campo de tipo DATE.

    Este tipo de datos modela una fecha mediante una estructura que tiene separados los componentes del mes, día y año.

    Si bien al momento de visualizarlo no será distinguible de un VARCHAR que tenga el mismo contenido, a nivel funcional será muy diferente.

    Entre otras, el tener los datos almacenados usando el tipo correcto permitirá realizar consultas como:

    SELECT * FROM usuarios WHERE subscription_date BETWEEN '2020-01-10' AND '2020-01-20';

    Y obtener como resultado aquellos usuarios que se han suscrito a nuestro sitio entre el 10 y el 20 de Enero de 2020.

    Usar un DATETIME para almacenar un dato fecha

    Si necesitas almacenar, además de la fecha, la hora exacta en que sucedió algo, lo mejor es utilizar el tipo de datos DATETIME que se comporta igual que DATE pero agregando la información de horas, minutos y segundos.

  • Cuándo usar una clase abstracta y cuándo una interface

    Cuándo usar una clase abstracta y cuándo una interface

    Un lector de mi libro sobre Programación Orientada a Objetos con PHP me envía esta pregunta a través de LinkedIn:

    Empecé a responderle a su mensaje pero luego se me ocurrió que sería mejor aprovechar y contestarlo en público así que aquí voy.

    Empecemos por comprender de qué se trata cada uno.

    Qué es una clase abstracta

    En su definición más cruda una clase se dice abstracta si no es posible utilizarla para crear objetos (instancias).

    Suena un poco raro, ¿no? ¿Para qué quiero tener una clase si no es para crear instancias?

    La explicación viene asociada al concepto de Herencia (Tema para otro artículo en todo caso).

    Una clase abstracta puede usarse como base de una jerarquía.

    Se define de esta forma:

    <?php
    
    abstract class Abstracta
    {
        public function metodoConcreto()
        {
            return true;
        }
    
        abstract public function metodoAbstracto();
    }

    Notá cómo el método metodoConcreto() tiene definición (está el cuerpo completo) y como en cambio, de metodoAbstracto() sólo está su declaración (Aparte de llevar la palabra «abstract» como modificador).

    Para usarla en una clase concreta se necesita que la hija complete las definiciones que su padre (o alguno de sus ancestros) han dejado inconclusas:

    class Concreta extends Abstracta
    {
        public function metodoAbstracto()
        {
            return true;
        }
    }

    En la vida real, este tipo de estructura viene muy bien para implementar, por ejemplo, hooks.

    La idea será algo como esto: la clase padre (abstracta) define una serie de operaciones bastante complejas y repetitivas y deja una o dos funciones sin definir para que la clase hija escriba aquí sus particularidades.

    Un ejemplo que me viene a la mente es un ORM basado en ActiveRecord.

    En este caso, habría una clase Record que tendría un método save y podría tener algún método tipo preSave/postSave para que cada tipo de registro en particular pueda intercalar validaciones u operaciones encadenadas.

    Qué es una interface

    Una interface puede definirse como una declaración de métodos abstractos.

    En este sentido se parece a una clase abstracta… la diferencia (a simple vista al menos) es que una interface no puede definir métodos (Sólo puede declararlos).

    Se ve de esta forma:

    interface UnaInterface
    {
        public function f1();
        
        public function f2();
    }

    Según los teóricos más puristas de la Programación Orientada a Objetos toda clase debería implementar al menos una interface.

    Por lo general se utilizan interfaces cuando se quiere unificar nombres de métodos pero seguir manteniendo comportamientos que no tienen nada que ver uno con el otro.

    De hecho, las interfaces suelen utilizarse como factor común entre clases que no pertenencen a una misma jerarquía.

    Por ejemplo, si tomamos una clase Book y otra clase Invoice sería difícil establecer una relación jerárquica entre ellas (Ni Book es un tipo especial de Invoice ni viceversa).

    Sin embargo, es muy probable que ambas clases se beneficien de contar con un método print, aunque la forma específica de responder a esa llamada (Es decir, la forma de imprimir) será muy diferente para cada uno de ellos.

    Habiendo clases abstractas… ¿por qué se necesitan interfaces?

    La respuesta a esta pregunta tiene que ver con algunos problemas de implementación.

    En algunos lenguajes (C++ por ejemplo) existe lo que se conoce como Herencia Múltiple (La posibilidad de que una clase tenga más de un antecesor directo).

    Es decir, el esquema se vería algo como:

    Se ve que la clase TA hereda de Student Y de Faculty. Hasta ahí no hay problema… pero ¿qué pasa si Student define un método con el mismo nombre que Faculty?

    Cuando se invoque $ta->metodo() ¿cuál debería ejecutarse? (asumiendo por supuesto que TA no tiene una definición propia de ese método).

    Una forma elegante de resolver este problema es impedir la herencia múltiple (Eso es lo que hacen, entre otros, Java y PHP).

    Pero la necesidad de reutilizar nombres (y comportamientos) a lo largo de diferentes jerarquías de clases no desaparece… de ahí surge la idea de las interfaces (y los traits… tema para otro artículo).

    Un punto importante: una clase (en PHP al menos) puede implementar tantas interfaces como se desee (pero sólo puede extender una clase base).

    Cómo decidir si conviene una clase abstracta o una interface

    Pues bien, ahora sí estamos listos para responder a la pregunta original 🙂

    Debe usarse una clase abstracta cuando se está modelando una jerarquía de clases y una interface cuando se pretende homogeneizar nombres entre objetos que no están emparentados. Compartir en X

    Esto parece una obviedad, pero no es tan así… a veces nos encontramos con objetos que parecen estar relacionados mediante herencia pero en realidad no es así.

    Para determinar si este es el caso vale preguntarse: «¿Es este objeto un caso particular de sus predecesores?» (Un auto es un vehículo).

    El uso de interfaces nos permite «olvidar» momentáneamente con qué tipo de objetos estoy trabajando.

    En esto se basa el principio de segregación de la interface (La I de SOLID)

    Un ejemplo que me viene a la mente es algo que utilizamos hace unos años para una red social de viajes en la que trabajaba.

    Originalmente esta red social permitía a sus usuarios subir sus fotos y diarios de viaje (compartir sus experiencias en formato similar a un blog).

    Una foto y un diario de viaje tenían bastante pocas similitudes (De hecho las clases que las representaban no tenían ningún ancestro en común).

    Un día surgió la necesidad de dotar al sistema de la posibilidad de que otros usuarios dejaran sus opiniones sobre las fotos y/o los diarios de los demás.

    Entonces se nos ocurrió agregarle a la clase usuario un método opinar().

    Pero no podíamos crear un método opinarSobreFoto( Foto $foto ) y otro opinarSobreDiario( Diario $diario )… en rigor de verdad podríamos haberlo hecho pero no era para nada mantenible.

    Una forma mejor de resolverlo fue crear una interface Opinable que tanto la clase Foto como la clase Diario implementaran y luego el método Usuario::opinar pudiera usar, dando lugar a algo como:

    <?php
    
    class Usuario
    {
       public function opinar( Opinable $opinable, string $texto )
       {
           return new Opinion( $opinable, $this, $texto ); 
       }
    } 

    De esta forma, agregar otro opinable (Por ejemplo, un destino visitado) no supone ningún problema para la clase Usuario.

    Y así todos felices 🙂