Blog

  • Introducir objetos en un código viejo

    Introducir objetos en un código viejo

    Seguramente a vos nunca te haya pasado lo que te voy a contar, pero tenés algún amigo al que sí.

    Uno de esos pobres programadores que se enfrenta a una codebase que combina cosas como:

    function shipTo(string $street, int $number, string $postalCode, string $city): void;
    
    function getDeliveryCost(string $city, string $street, int $number, string $postalCode): float;
    
    function canPurchase(string $city, string $postalCode, int $number, string $street): bool;

    Claramente, este tipo de estructuras son una invitación al error.

    A partir de PHP 8.0 existen los argumentos nombrados, con lo cual el problema está de algún modo mitigado aunque, en mi opinión, es más un workaround que una solución como tal.

    Un mejor enfoque sería tomar una mirada algo menos pegada al código y más cercana al dominio y darse cuenta de que aquí hay un concepto implícito: la dirección postal.

    Lo correcto en esta situación sería implementar una clase que modele este concepto de dominio:

    <?php
    
    declare(strict_types=1);
    
    readonly class Address {
        public function __construct(
            private string $street,
            private int $number,
            private string $postalCode, 
            private string $city
        ) {
        }
    
        public function __toString(): string {
            return "$this->street $this->number, $this->postalCode, $this->city";
        }
    }

    Y luego hacer que las funciones usen instancias de este objeto:

    function shipTo(Address $address): void;
    
    function getDeliveryCost(Address $address): float;
    
    function canPurchase(Address $address): bool;

    Ahora sí el código de tu amigo se ve mucho más bonito, limpio, mantenible y profesional, ¿cierto?

    Pero… ¿cómo puede tu amigo implementar este cambio sin romper todo el código ya está escrito usando las firmas viejas?

    Paso intermedio: wrappers al rescate

    Aunque parezca una tarea titánica (y ciertamente lo es si se intenta hacer en un solo paso), es muy sencillo realizar el cambio en forma segura. Sólo requiere un poquito de planificación.

    La clave está en crear un intermediario que haga de adaptador entre el código viejo y el nuevo.

    Tomemos por ejemplo la función shipTo. Queremos pasar de esta firma function shipTo(string $street, int $number, string $postalCode, string $city): void a esta otra: function shipTo(Address $address): void pero, si simplemente hacemos el cambio, todas las llamadas a la vieja comenzarán a fallar.

    Lo que podemos hacer es:

    1. Crear la nueva función con la firma objetivo (function shipTo(Address $address): void)
    2. Copiar el cuerpo de la función vieja a la nueva
    3. Adaptar el código de la nueva función para que use objetos Address en lugar de parámetros individuales
    4. Cambiar el código en la función original por una llamada a la nueva implementación, pasándole como parámetro una nueva instancia de Address creada a partir de los parámetros recibidos:
    function shipTo(string $street, int $number, string $postalCode, string $city): void
    {
        shipTo(new Address($street, $number, $postalCode, $city));
    }
    1. Marcar el método (o función) viejo como #[Deprecated] (O @Deprecated si tu versión de PHP no soporta atributos), de modo que, en lo sucesivo, todo el equipo sepa que debe usar el método nuevo.

    Una vez hecho todo esto será cuestión de tiempo hasta que todo el código haya sido migrado hacia la implementación que usa objetos.

    Un detalle importante:

    Si prestaste atención abrás notado que hay dos funciones con el mismo nombre: shipTo.

    Desafortunadamente, PHP no soporta la sobrecarga de funciones como otros lenguajes (Java por ejemplo) lo cual hará que, para que todo esto funcione, sea necesario ensuciar un poquito el código usando un nombre diferente para la nueva función, por ejemplo shipToAddress.

    No es lo que más me gusta pero lo considero un precio aceptable.

    Así que ahora sí podés ir a contarle a tu amigo cómo hacer para dejar de sufrir ese código que vaya uno a saber quién escribió.

  • Ejemplo de inyección de depencias en PHP

    Ejemplo de inyección de depencias en PHP

    Un concepto muy simple y, a la vez, muy potente de la Programación Orientada a Objetos es la inyección de dependencias.

    Diría que se trata de la piedra angular de cualquier sistema desacoplado y, por lo tanto, fácil de evolucionar y testear.

    Y lo mejor de todo es que su implementación es realmente sencilla. En otras palabras: puras ventajas.

    Acompañame a darle una mirada más de cerca.

    Qué es una dependencia en POO

    Como en muchas ocasiones en este mundo de la informática, un mismo término puede referirse a diferentes cosas y, para saber realmente de qué estamos hablando es necesario poner en claro el contexto.

    Por ejemplo, si hablamos de las dependencias de una aplicación, nos estaremos refiriendo a aquellas librerías necesarias para que dicha aplicación funcione. Tema que está mejor tratado en este artículo.

    En este contexto, cuando hablo de dependencia me refiero a aquellos objetos de los que otros dependen para realizar sus tareas.

    Te pongo un ejemplo que me compartió un lector de mi newsletter y me viene muy bien para ilustrar este concepto:

    <?php
    namespace Controllers;
    
    use ApplicationService\BillFinderService;
    use ApplicationService\TotalDeductiblesByYearService;
    use Dao\DeductibleDao;
    use Infraestructure\Connection\ConnectionMySql;
    
    class FrontController extends Controller{
    
        private $connection;
    
        public function __construct()
        {
            parent::__construct();
            $this->connection = new ConnectionMySql();
        }
        
        public function home($year = null){
            if (!$year) {
                $year = new \DateTime();
                $year = $year->format('Y');
            }
            $billFinderService = new BillFinderService($this->connection);
            $totalDeductibleByYear = new TotalDeductiblesByYearService($this->connection);
            $lastBillsRegistered = $billFinderService->findByYear($year);
            $totalByDeductible = $totalDeductibleByYear->getTotalByYear($year);
            echo $this->templates->render('index', [
                'title' => 'Main menu',
                'lastBillsRegistered' => $lastBillsRegistered,
                'totalByDeductible' => $totalByDeductible,
                'year' => $year,
                'years' => [2023, 2024, 2025]
            ]);
    
        }
        
        public function error404(){
            
            echo $this->templates->render('404');
        
        }
    
        public function getBillsByDeductibleIdAndYear($deductibleId, $year){
    
            $totalDeductibleByYear = new TotalDeductiblesByYearService($this->connection);
            $bills = $totalDeductibleByYear->getBillsByDeductibleIdAndYear($deductibleId, $year);
            $deductibleDao = new DeductibleDao($this->connection);
            $deductible = $deductibleDao->findById($deductibleId);
            echo $this->templates->render('bills-by-deductible-and-year', [
                'title' => "Bills by deductible by year",
                'deductibleName' => $deductible->getName(),
                'bills' => $bills,
                'year' => $year
            ]);
        }
    }

    Aquí tenemos una clase llamada FrontController que depende, entre otras, de:

    • ConnectionMySql
    • BillFinderService
    • TotalDeductiblesByYearService
    • DeductibleDao

    ¿Por qué digo que FrontController depende de estas clases? Porque, para lograr sus objetivos se sirve de métodos de ellas, ejemplo:

    public function home($year = null){
            ...
            $lastBillsRegistered = $billFinderService->findByYear($year);
            $totalByDeductible = $totalDeductibleByYear->getTotalByYear($year);
            ...
        }

    Para que este código pueda ejecutarse, será necesario que las variables que contienen referencias a dichos objetos ($billFinderService y $totalDeductibleByYear) hayan sido previamente asignadas a instancias de sus respectivas clases.

    Algo que se está haciendo precisamente al comienzo del método:

    public function home($year = null){
    ...
            $billFinderService = new BillFinderService($this->connection);
            $totalDeductibleByYear = new TotalDeductiblesByYearService($this->connection);
    ...
    }

    Entonces… ¿cuál es el problema?

    Bueno, más que un problema es una oportunidad perdida.

    Implementando inyección de dependencias

    Empiezo por una pregunta que seguramente se pasó por alto al escribir este código:

    ¿Es necesario crear una nueva instancia de cada colaborador cada vez que se llama al método?

    Ya dije que crear la instancia es inevitable pero la pregunta apunta a determinar si el mejor momento (O lugar, como quieras verlo) para hacerlo es aquí.

    ¿Qué pasaría si modificáramos ligeramente el código para que crear las dependencias fuera responsabilidad de alguien más?

    Algo como:

    class FrontController extends Controller{
    
        private $connection;
        private $billFinderService;
        private $totalDeductiblesByYearService;
    
        public function __construct(ConnectionMySql $connection, BillFinderService $billFinderService, TotalDeductiblesByYearService $totalDeductiblesByYearService)
        {
            parent::__construct();
            $this->connection = $connection;
            $this->billFinderService = $billFinderService;
            $this->totalDeductiblesByYearService = $totalDeductiblesByYearService;
        }
        
        public function home($year = null){
            if (!$year) {
                $year = new \DateTime();
                $year = $year->format('Y');
            }
            echo $this->templates->render('index', [
                'title' => 'Main menu',
                'lastBillsRegistered' => $this->billFinderService->findByYear($year),
                'totalByDeductible' => $this->totalDeductiblesByYearService->getTotalByYear($year),
                'year' => $year,
                'years' => [2023, 2024, 2025]
            ]);
        }
    ...
    }

    Con este pequeño cambio pateamos la responsabilidad hacia arriba. Ahora es problema del cliente de la clase FrontController conseguir instancias válidas para pasarle (Ya sea creándolas él mismo o pidiéndoselas a algún otro componente).

    Esta forma de vincular una clase con sus colaboradores es lo que se conoce como inyección de dependencias.

    Ventajas de la Inyección de Dependencias

    Ahora que el tecnicismo está claro, veamos por qué vale la pena diseñar de esta manera.

    Para empezar, porque el costo de hacerlo es muy bajo. Si mirás el código inicial y lo comparás con el actual diríamos que mucha diferencia no hay, ¿cierto?

    Pero vamos a ver los aspectos más jugosos del tema:

    Performance

    Este punto depende mucho del contexto particular de tu aplicación. Si el método que crea los objetos no se ejecuta con mucha frecuencia la diferencia no va a ser mucha, pero si es al revés, te vas a estar ahorrando la creación de objetos idénticos, lo que implica menos gasto de memoria y de tiempo de procesamiento.

    Es cierto que en PHP esta ventaja se diluye por el hecho de que cada request se procesa en su propio hilo pero aún así, algo se puede ganar.

    Versatilidad

    Este punto me parece el más interesante ya que es el que da lugar a los dos siguientes.

    Pensalo así: al dejar de lado la decisión de qué instancia específica del colaborador va a usar tu clase dejás abierta la puerta a que un mismo método pueda usarse en diferentes contextos.

    Imaginá un escenario en el que existe no una si no dos instancias de la clase BillFinderService y que, dependiendo de alguna configuración se necesita usar una o la otra.

    Si el código está escrito usando Inyección de Dependencias, implementar ese cambio no supone ningún inconveniente, basta con cambiar el parámetro que se usa para construir FrontController y listo.

    Es más, dado que estás trabajando con Programación Orientada a Objetos, bien podrías pasarle una instancia de una clase derivada de BillFinderService y todo seguiría funcionando como si tal cosa.

    De hecho, este mecanismo es el que posibilita mucho de los principios SOLID.

    Testabilidad

    Un caso muy claro en el que viene bien poder cambiar la instancia de un colaborador por otra es a la hora de hacer tests unitarios.

    Si no podés aislar de sus la clase que querés testear, las cosas se te van a complicar.

    Una vez que lo viste…

    … no podés des-verlo.

    No te digo que vayas corriendo a desarmar todos tus proyectos que no usan Inyección de Dependencias pero sí te propongo que le des una segunda mirada teniendo en cuenta lo que podrías ganar si lo implementás y, de a poco, lo vayas incorporando a tu día a día.

  • Un ejemplo de Laravel y React sobre Docker que funciona

    Un ejemplo de Laravel y React sobre Docker que funciona

    Cuando me llegó este mensaje

    Necesito aprender a preparar entorno de desarrollo php, laravel y react con Docker, ¿tendrás algún ejemplo para recomendarme?

    Mi primer instinto fue buscar algún tutorial para compartir. ¿Qué otra cosa podría ser mejor, no?

    Y ahí me metí a hurgar en GitHub y probar unas cuantas opciones pero ninguna parecía funcionar.

    Al menos no sin hacer unos cuantos ajustes y, dado que mi framework de referencia no es Laravel si no Symfony, me pareció que lo más lógico era seguir buscando.

    Pues bien, hurgando por aquí y por allí llegué a este tutorial que me dio (casi) justo lo que estaba buscando.

    Se trata de una sencilla app de manejo de usuarios:

    Lo interesante: está desarrollada usando Laravel y React:

    Y montada sobre Docker.

    Si querés ver los detalles de cómo logré levantar la aplicación te recomiendo que leas el tutorial original aunque debo hacer una advertencia: tuve que hacer pequeños ajustes al código para dejarla funcionando pero, ahora que está ahí, podés descargar el código del repositorio y ejecutando estos comandos desde la raíz del proyecto:

    1. docker compose up -d --wait (O su primo hermano vendor/bin/sail up -d --wait)
    2. npm run dev

    Deberías ser capaz de entrar a http://localhost y ser recibido por la aplicación.

    El objetivo de este post no es analizar el propio código (Ni PHP ni React), si no centrarme en cómo se montó todo esto sobre Docker.

    Para comprender lo que te voy a mostrar a continuación necesitás conocer los conceptos básicos de Docker. Si no los tenés frescos te recomiendo que, antes de seguir, busques aprender cómo funciona Docker en general (Tal vez este post sea un mejor lugar para arrancar).

    ¿Avanzamos?

    Los archivos de docker

    Primero que nada, una aclaración: Estos archivos los generó sail.

    No es que eso tenga nada de malo, simplemente comentar que, si usás Laravel, no parece una mala idea apoyarte en este wrapper.

    Ahora sí, el primer archivo a analizar no es otro que el docker-compose.yml

    services:
        laravel.test:
            build:
                context: './vendor/laravel/sail/runtimes/8.4'
                dockerfile: Dockerfile
                args:
                    WWWGROUP: '${WWWGROUP}'
            image: 'sail-8.4/app'
            extra_hosts:
                - 'host.docker.internal:host-gateway'
            ports:
                - '${APP_PORT:-80}:80'
                - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
            environment:
                WWWUSER: '${WWWUSER}'
                LARAVEL_SAIL: 1
                XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
                XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
                IGNITION_LOCAL_SITES_PATH: '${PWD}'
            volumes:
                - '.:/var/www/html'
            networks:
                - sail
            depends_on:
                - mysql
        mysql:
            image: 'mysql/mysql-server:8.0'
            ports:
                - '${FORWARD_DB_PORT:-3306}:3306'
            environment:
                MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
                MYSQL_ROOT_HOST: '%'
                MYSQL_DATABASE: '${DB_DATABASE}'
                MYSQL_USER: '${DB_USERNAME}'
                MYSQL_PASSWORD: '${DB_PASSWORD}'
                MYSQL_ALLOW_EMPTY_PASSWORD: 1
            volumes:
                - 'sail-mysql:/var/lib/mysql'
                - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
            networks:
                - sail
            healthcheck:
                test:
                    - CMD
                    - mysqladmin
                    - ping
                    - '-p${DB_PASSWORD}'
                retries: 3
                timeout: 5s
    networks:
        sail:
            driver: bridge
    volumes:
        sail-mysql:
            driver: local
    

    No te preocupes si te resulta algo intimidante, suele ser así cuando se ven por primera vez. Intentaré desmenuzarlo para que sea más sencillo el consumo.

    Lo primero que encontramos es que hay dos servicios: laravel.test y mysql.

    Analizaré cada uno por separado

    El servicio laravel.test

    Lo primero que se ve en la definición de este servicio es la clave build, lo que significa que se utilizará una imagen custom para crear el respectivo contenedor:

            build:
                context: './vendor/laravel/sail/runtimes/8.4'
                dockerfile: Dockerfile
                args:
                    WWWGROUP: '${WWWGROUP}'

    A partir de esta definición se pueden extraer las siguientes conclusiones:

    1. Se utilizará el archivo Dockerfile ubicado en vendor/laravel/sail/runtimes/8.4
    2. El proceso de construcción de la imagen requiere un argumento llamado WWWGROUP
    3. El valor del argumento de build se extrae de una variable de contexto llamada WWWGROUP

    Veamos el contenido del Dockerfile para hacernos una idea más clara de qué tendrá la imagen y cómo se hará la construcción:

    El archivo vendor/laravel/sail/runtimes/8.4/Dockerfile

    FROM ubuntu:24.04
    
    LABEL maintainer="Taylor Otwell"
    
    ARG WWWGROUP
    ARG NODE_VERSION=22
    ARG MYSQL_CLIENT="mysql-client"
    ARG POSTGRES_VERSION=17
    
    WORKDIR /var/www/html
    
    ENV DEBIAN_FRONTEND=noninteractive
    ENV TZ=UTC
    ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
    ENV SUPERVISOR_PHP_USER="sail"
    
    RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
    
    RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
        echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
        echo "Acquire::BrokenProxy    true;" >> /etc/apt/apt.conf.d/99custom
    
    RUN apt-get update && apt-get upgrade -y \
        && mkdir -p /etc/apt/keyrings \
        && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano  \
        && curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
        && echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
        && apt-get update \
        && apt-get install -y php8.4-cli php8.4-dev \
           php8.4-pgsql php8.4-sqlite3 php8.4-gd \
           php8.4-curl php8.4-mongodb \
           php8.4-imap php8.4-mysql php8.4-mbstring \
           php8.4-xml php8.4-zip php8.4-bcmath php8.4-soap \
           php8.4-intl php8.4-readline \
           php8.4-ldap \
           php8.4-msgpack php8.4-igbinary php8.4-redis \
    #       php8.4-swoole \
           php8.4-memcached php8.4-pcov php8.4-imagick php8.4-xdebug \
        && curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
        && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
        && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
        && apt-get update \
        && apt-get install -y nodejs \
        && npm install -g npm \
        && npm install -g pnpm \
        && npm install -g bun \
        && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
        && echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
        && curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
        && echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
        && apt-get update \
        && apt-get install -y yarn \
        && apt-get install -y $MYSQL_CLIENT \
        && apt-get install -y postgresql-client-$POSTGRES_VERSION \
        && apt-get -y autoremove \
        && apt-get clean \
        && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
    
    RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.4
    
    RUN userdel -r ubuntu
    RUN groupadd --force -g $WWWGROUP sail
    RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
    
    COPY start-container /usr/local/bin/start-container
    COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
    COPY php.ini /etc/php/8.4/cli/conf.d/99-sail.ini
    RUN chmod +x /usr/local/bin/start-container
    
    EXPOSE 80/tcp
    
    ENTRYPOINT ["start-container"]

    Creo que comentar línea por línea este archivo sería un poco demasiado así que me voy a centrar en las partes más relevantes:

    1. La imagen base que se usa es ubuntu:24.04 (Link acá)
    2. Es posible especificar, además de WWWGROUP, la versión de NodeJS, el cliente de MySQL y la versión de PostgreSQL
    3. La versión de PHP que se utiliza es la 8.4
    4. Hay una cantidad de extensiones php instaladas (sqlite3, gd, mongodb, readline, etc…)
    5. XDebug viene instalado

    Respecto del argumento obligatorio WWWGROUP, se usa para establecer el id del grupo al que pertenecerá el usuario sail (el encargado de levantar el proceso php dentro del contenedor).

    El objetivo de esto es evitar los problemas de permisos que podría haber cuando Laravel requiera escribir sobre archivos que deben ser accedidos por el servidor web.

    El valor de este argumento lo provee sail.

    Este Dockerfile es, por definición, bastante genérico, para ir a producción te convendría revisarlo bien.

    En mi caso, por ejemplo, sólo utilizo MySQL, con lo cual, todo lo relativo a PostgreSQL y MongoDB no lo necesito.

    A continuación vemos

            image: 'sail-8.4/app'

    Lo que indica que la imagen que se generará llevará el nombre de sail-8.4/app

            extra_hosts:
                - 'host.docker.internal:host-gateway'

    Esta definición agrega la capacidad de conectarse desde dentro del contenedor hacia el host, fundamental para que xdebug funcione.

            ports:
                - '${APP_PORT:-80}:80'
                - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'

    En esta sección se establecen los mapeos entre los puertos del host y los del contenedor, de modo de que al acceder a localhost:{$APP_PORT} y localhost:{$VITE_PORT} se pueda realizar peticiones al servidor web y al vite respectivamente.

    Los -80 y -5173 son los valores por defecto, es decir, los que se usarán a menos que haya otros especificados en variables de entorno (o en archivos .env).

            environment:
                WWWUSER: '${WWWUSER}'
                LARAVEL_SAIL: 1
                XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
                XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
                IGNITION_LOCAL_SITES_PATH: '${PWD}'

    Esta parte define algunas variables de entorno que estarán presentes cuando el contenedor se levante.

    El uso específico de cada una de ellas depende de lo que hagan los procesos instalados en el contenedor (el webserver principalmente).

            volumes:
                - '.:/var/www/html'

    Esta sección define el punto de montaje del código de la aplicación hacia el directorio /var/www/html dentro del contenedor.

            networks:
                - sail

    Aquí se define la red a la que estará conectado el contenedor (sail).

            depends_on:
                - mysql

    Y por último, se define una dependencia entre el servicio laravel.test y mysql, lo que en la práctica quiere decir que el orden de inicio de los servicios será primero mysql y luego laravel.test.

    El servicio mysql

    El segundo servicio es para la base de datos, en mi caso MySQL:

            image: 'mysql/mysql-server:8.0'
            ports:
                - '${FORWARD_DB_PORT:-3306}:3306'
            environment:
                MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
                MYSQL_ROOT_HOST: '%'
                MYSQL_DATABASE: '${DB_DATABASE}'
                MYSQL_USER: '${DB_USERNAME}'
                MYSQL_PASSWORD: '${DB_PASSWORD}'
                MYSQL_ALLOW_EMPTY_PASSWORD: 1
            volumes:
                - 'sail-mysql:/var/lib/mysql'
                - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
            networks:
                - sail
            healthcheck:
                test:
                    - CMD
                    - mysqladmin
                    - ping
                    - '-p${DB_PASSWORD}'
                retries: 3
                timeout: 5s

    A simple vista podés notar que no hay una clave build, lo que significa que se usará una imagen estándar, en este caso mysql/mysql-server:8.0 (Link acá)

    A través de la siguiente definición:

            environment:
                MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
                MYSQL_ROOT_HOST: '%'
                MYSQL_DATABASE: '${DB_DATABASE}'
                MYSQL_USER: '${DB_USERNAME}'
                MYSQL_PASSWORD: '${DB_PASSWORD}'
                MYSQL_ALLOW_EMPTY_PASSWORD: 1

    Se establecen algunos valores necesarios para la incialización del servidor de base de datos.

            volumes:
                - 'sail-mysql:/var/lib/mysql'
                - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'

    Aquí se monta el volumen nombrado sail-mysql al directorio /var/lib/mysql del contenedor, con el objetivo de lograr persistencia de los datos.

    Mediante el segundo mapeo se consigue crear la base de datos como puedes ver en vendor/laravel/sail/database/mysql/create-testing-database.sh:

    #!/usr/bin/env bash
    
    mysql --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL
        CREATE DATABASE IF NOT EXISTS testing;
        GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%';
    EOSQL

    Aquí se define la pertenencia de este contenedor a la misma red (sail) que se utiliza en el servicio laravel.test para garantizar la conectividad entre ambos

            networks:
                - sail

    Y mediante esta configuración:

            healthcheck:
                test:
                    - CMD
                    - mysqladmin
                    - ping
                    - '-p${DB_PASSWORD}'
                retries: 3
                timeout: 5s

    Se establece un comando capaz de detectar si el servicio está funcional o no para tomar alguna acción al respecto o, al menos, poder informar de esta situación.

    Finalmente nos encontramos con la definición de las redes:

    networks:
        sail:
            driver: bridge

    Y los volúmenes nombrados:

    volumes:
        sail-mysql:
            driver: local

    Qué hacer con todo esto

    Este post, y el código que lo acompaña, están armados como un recurso didáctico, con lo cual, el mejor uso que les podés dar es estudiarlos a fondo.

    Tu tarea pues es:

    1. Descargar el código
    2. Levantar la aplicación localmente
    3. Estudiar los archivos de Docker
    4. Hacer algún pequeño cambio, por ejemplo agregar phpMyAdmin
    5. Si te animás, agregar alguna funcionalidad, ya sea al frontend o al backend
  • ¿Puede tener éxito una aplicación en PHP estructurado?

    ¿Puede tener éxito una aplicación en PHP estructurado?

    Para hacer un proyecto grande, ¿tenés que utilizar PHP Orientado a Objetos?

    ¿Podés lograr lo mismo programando estructurado mientras que lo hagas de forma limpia y organizada?

    ¿Tendrá futuro un proyecto desarrollado con PHP estructurado?

    Estas son algunas de las preguntas que surgen cuando tenés algo de experiencia programando y empezás a pensar en grande.

    A continuación te comentaré las ventajas de incorporar estas técnicas.

    También intentaré despejar la confusión sobre ciertos mitos para que puedas tomar la mejor decisión para tu carrera.

    Pero, antes de meterme en los detalles, contestaré a la pregunta original.

    Qué determina el éxito de una aplicación

    Para responder a ¿Puede tener éxito una aplicación en PHP estructurado? lo primero que necesitas es tener una definición clara de éxito.

    Parece una obviedad, sí, pero no creas que lo es tanto.

    Si por éxito te refieres a éxito comercial la respuesta es un sí rotundo. Proyectos muy exitosos desde el punto de vista comercial como remoteok se basan en un código extremadamente sencillo.

    En otras palabras: el éxito comercial y la estructura del código no guardan una relación estrecha en general.

    Por otro lado, si por éxito te refieres a la posibilidad de generar aplicaciones a las que puedas agregar funcionalidad en forma sencilla y eficiente la Programación Orientada a Objetos te ofrece mejores chances.

    Diferencias entre Programación Estructurada y Orientada a Objetos

    Las principales características de la Programación Estructurada son:

    • Una separación muy fuerte entre datos y procesos
    • Una forma de escribir los programas indicando con mucho detalle cómo deben ser resueltos los problemas

    La POO (Programación Orientada a Objetos) no es esencialmente diferente de la estructurada.

    La forma de resolver los problemas también se basa en una serie de instrucciones que la computadora ejecutará en forma secuencial.

    Pero las similitudes llegan prácticamente hasta aquí.

    En POO los programas se parecen más a descripciones de la colaboración entre diversos actores.

    Cada actor ofrecerá a los demás algunos servicios (Llamados métodos) y, a su vez, contará con una serie de propiedades o atributos, los cuales le servirán para llevar a cabo sus tareas.

    Tomemos por ejemplo un cruce de calles con semáforo. 

    En este escenario existen:

    • 2 semáforos (Cada uno de los cuales puede tener encendida una luz color rojo, amarillo o verde)
    • Autos que se acercan hasta la bocacalle

    Lo único que hacen los semáforos es cambiar el color de la luz encendida y esperar.

    Mientras tanto, los autos pueden acelerar, frenar, abrir sus puertas, etc…

    A su vez, las luces de colores pertenecen al semáforo, junto con la acción de cambiar la luz activa.

    En un programa diseñado bajo el paradigma estructurado escribirías una función de cambiar la luz activa de un semáforo.

    Probablemente esta función recibiría el número de semáforo como parámetro y el color de la luz de cada uno se guardaría en un arreglo.

    En POO sería el propio semáforo el reponsable de cambiar su luz activa, informar al sistema cuál es su estado actual y velar por que las transiciones sean correctas (Por ejemplo, que no se pase de Rojo directo a Verde).

    Como para darle algo más de concretud a estos conceptos, y para acercarlo un poco más a algo que ya conoces, los métodos se implementan mediante funciones (Sí, con la palabra clave function) y las propiedades mediante variables (De esas que empiezan con $).

    ¿Es malo Programar en PHP Estructurado?

    Existen algunos mitos como que los programas desarrollados usando POO son más seguros o más eficientes que sus pares desarrollados usando Programación Estructurada.

    Lamentablemente, el mundo no es blanco o negro… se pueden hacer muy buenos programas estructuras y muy malos programas Orientados a Objetos.

    La calidad de los desarrollos depende de factores más sutiles que el paradigma en que estén escritos los programas.

    Lo que sí diría es que, si se escribe correctamente, el código Orientado a Objetos es más sencillo de matener y testear.

    La Programación Orientada a Objetos y el mercado laboral

    Más allá de tus preferencias personales, hay una realidad que no te conviene ignorar: la gran mayoría de las empresas de programación utilizan Programación Orientada a Objetos.

    De hecho, un número cada vez más grande de desarrollos se realizan utilizando frameworks basados en POO (Laravel, Symfony, CodeIgniter y Yii son los ejemplos más prominentes).

    Personalmente, siempre que puedo, elijo Symfony, pero los otros también tienen sus méritos.

    ¿Vale la pena aprender Programación Orientada a Objetos con PHP?

    El soporte que tiene PHP para implementar los conceptos principales de Programación Orientada a Objetos es muy bueno (especialmente en las versiones superiores a la 7.0).

    En todo caso, no tenés por qué tomar una decisión a ciegas.

    Hacé alguna prueba con una aplicación simple (Una calculadora por ejemplo). Intentá hacerla usando POO y después contame cómo te fue.


    Si se está complicando incorporar los conceptos de Programación Orientada a Objetos esta guía puede serte útil.

  • Cómo enviar  encabezados SOAP desde PHP

    Cómo enviar encabezados SOAP desde PHP

    El protocolo SOAP, a pesar de lo que indica su nombre, es de todo menos sencillo.

    Principalmente, su complejidad deriva del hecho de estar basado en XML, aunque no es lo único que tiene.

    En teoría, es un protocolo super flexible. En la realidad… un dolor de cabeza importante.

    En PHP existen varias implementaciones que intentan simplificar un poco el problema.

    Las veces que me ha tocado enfrentarme a SOAP las clases SoapClient y SoapServer han estado a la altura, aunque cuando aparecieron los encabezados no fue tan sencillo.

    Tomemos como ejemplo este servicio web que define el siguiente WSDL:

    <?xml version="1.0" encoding="utf-8"?>
    <wsdl:definitions xmlns:s="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:tns="https://sccnlp.com/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" targetNamespace="https://sccnlp.com/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
      <wsdl:types>
        <s:schema elementFormDefault="qualified" targetNamespace="https://sccnlp.com/">
          <s:element name="registrarNombradas">
            <s:complexType>
              <s:sequence>
                <s:element minOccurs="1" maxOccurs="1" name="rutEmpresa" type="s:int" />
                <s:element minOccurs="0" maxOccurs="1" name="nombradas" type="tns:ArrayOfNombrada" />
              </s:sequence>
            </s:complexType>
          </s:element>
          <s:complexType name="ArrayOfNombrada">
            <s:sequence>
              <s:element minOccurs="0" maxOccurs="unbounded" name="Nombrada" nillable="true" type="tns:Nombrada" />
            </s:sequence>
          </s:complexType>
          <s:complexType name="Nombrada">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="fechaInicioNombrada" type="s:dateTime" />
              <s:element minOccurs="1" maxOccurs="1" name="idPuerto" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="idTurno" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="idNave" nillable="true" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="idLocacion" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="trabajadores" type="tns:ArrayOfTrabajadorNombrada" />
            </s:sequence>
          </s:complexType>
          <s:complexType name="ArrayOfTrabajadorNombrada">
            <s:sequence>
              <s:element minOccurs="0" maxOccurs="unbounded" name="TrabajadorNombrada" nillable="true" type="tns:TrabajadorNombrada" />
            </s:sequence>
          </s:complexType>
          <s:complexType name="TrabajadorNombrada">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="idContrato" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="idLabor" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="idFuncion" type="s:int" />
            </s:sequence>
          </s:complexType>
          <s:element name="registrarNombradasResponse">
            <s:complexType>
              <s:sequence>
                <s:element minOccurs="0" maxOccurs="1" name="registrarNombradasResult" type="tns:NombradaCreada" />
              </s:sequence>
            </s:complexType>
          </s:element>
          <s:complexType name="NombradaCreada">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="Estado" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="Mensaje" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="nombradaCreadaDetalle" type="tns:ArrayOfNombradaCreadaDetalle" />
            </s:sequence>
          </s:complexType>
          <s:complexType name="ArrayOfNombradaCreadaDetalle">
            <s:sequence>
              <s:element minOccurs="0" maxOccurs="unbounded" name="NombradaCreadaDetalle" nillable="true" type="tns:NombradaCreadaDetalle" />
            </s:sequence>
          </s:complexType>
          <s:complexType name="NombradaCreadaDetalle">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="id" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="error" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="idEstado" type="s:int" />
            </s:sequence>
          </s:complexType>
          <s:element name="UserCredentials" type="tns:UserCredentials" />
          <s:complexType name="UserCredentials">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="userName" nillable="true" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="password" nillable="true" type="s:string" />
            </s:sequence>
            <s:anyAttribute />
          </s:complexType>
          <s:element name="modificarNombradas">
            <s:complexType>
              <s:sequence>
                <s:element minOccurs="1" maxOccurs="1" name="rutEmpresa" type="s:int" />
                <s:element minOccurs="0" maxOccurs="1" name="lista" type="tns:ArrayOfTrabajadorNombradaEdicion" />
              </s:sequence>
            </s:complexType>
          </s:element>
          <s:complexType name="ArrayOfTrabajadorNombradaEdicion">
            <s:sequence>
              <s:element minOccurs="0" maxOccurs="unbounded" name="TrabajadorNombradaEdicion" nillable="true" type="tns:TrabajadorNombradaEdicion" />
            </s:sequence>
          </s:complexType>
          <s:complexType name="TrabajadorNombradaEdicion">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="idNombrada" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="idContrato" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="idContratoNuevo" nillable="true" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="idLabor" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="idFuncion" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="activo" type="s:boolean" />
              <s:element minOccurs="1" maxOccurs="1" name="rutTrabajador" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="dvTrabajador" nillable="true" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="pasaporte" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="extensionJornada" type="s:boolean" />
            </s:sequence>
          </s:complexType>
          <s:element name="modificarNombradasResponse">
            <s:complexType>
              <s:sequence>
                <s:element minOccurs="0" maxOccurs="1" name="modificarNombradasResult" type="tns:NombradaCreada" />
              </s:sequence>
            </s:complexType>
          </s:element>
          <s:element name="getResolucionNombrada">
            <s:complexType>
              <s:sequence>
                <s:element minOccurs="1" maxOccurs="1" name="idNombrada" type="s:int" />
                <s:element minOccurs="1" maxOccurs="1" name="rutEmpresa" type="s:int" />
              </s:sequence>
            </s:complexType>
          </s:element>
          <s:element name="getResolucionNombradaResponse">
            <s:complexType>
              <s:sequence>
                <s:element minOccurs="0" maxOccurs="1" name="getResolucionNombradaResult" type="tns:NombradaResolucion" />
              </s:sequence>
            </s:complexType>
          </s:element>
          <s:complexType name="NombradaResolucion">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="Estado" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="Mensaje" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="nombradaResolucionDetalle" type="tns:NombradaResolucionDetalle" />
            </s:sequence>
          </s:complexType>
          <s:complexType name="NombradaResolucionDetalle">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="id" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="idEmpresa" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="fechaInicioNombrada" nillable="true" type="s:dateTime" />
              <s:element minOccurs="1" maxOccurs="1" name="fechaFinNombrada" nillable="true" type="s:dateTime" />
              <s:element minOccurs="1" maxOccurs="1" name="idTurno" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="Turno" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="idNave" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="Nave" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="idLocacion" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="lugar" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="posicion" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="fechaCreacion" type="s:dateTime" />
              <s:element minOccurs="1" maxOccurs="1" name="activo" nillable="true" type="s:boolean" />
              <s:element minOccurs="1" maxOccurs="1" name="idEstadoNombrada" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="glosaEstadoNombrada" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="trabajadores" type="tns:ArrayOfTrabajadorResolucion" />
            </s:sequence>
          </s:complexType>
          <s:complexType name="ArrayOfTrabajadorResolucion">
            <s:sequence>
              <s:element minOccurs="0" maxOccurs="unbounded" name="TrabajadorResolucion" nillable="true" type="tns:TrabajadorResolucion" />
            </s:sequence>
          </s:complexType>
          <s:complexType name="TrabajadorResolucion">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="idNombrada" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="idContrato" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="idContratoNuevo" nillable="true" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="idTrabajador" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="rutTrabajador" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="dvTrabajador" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="pasaporte" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="nombres" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="apellidos" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="idLabor" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="idFuncion" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="estadoTrabajador" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="activo" type="s:boolean" />
              <s:element minOccurs="1" maxOccurs="1" name="idEstadoTrabajador" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="extensionJornada" type="s:boolean" />
              <s:element minOccurs="1" maxOccurs="1" name="horasExtras" nillable="true" type="s:double" />
              <s:element minOccurs="1" maxOccurs="1" name="fechaCreacion" type="s:dateTime" />
              <s:element minOccurs="1" maxOccurs="1" name="fechaFinNombradaTrabajador" type="s:dateTime" />
              <s:element minOccurs="1" maxOccurs="1" name="fechaModificacion" nillable="true" type="s:dateTime" />
            </s:sequence>
          </s:complexType>
          <s:element name="consultarNombrada">
            <s:complexType>
              <s:sequence>
                <s:element minOccurs="1" maxOccurs="1" name="rutEmpresa" type="s:int" />
                <s:element minOccurs="1" maxOccurs="1" name="filtro" nillable="true" type="tns:FiltroNombrada" />
              </s:sequence>
            </s:complexType>
          </s:element>
          <s:complexType name="FiltroNombrada">
            <s:sequence>
              <s:element minOccurs="0" maxOccurs="1" name="idNombrada" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="fechaInicio" nillable="true" type="s:dateTime" />
              <s:element minOccurs="0" maxOccurs="1" name="idLabor" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="idNave" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="idLocacion" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="rutTrabajador" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="dvTrabajador" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="pasaporte" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="idEstado" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="idFuncion" nillable="true" type="s:int" />
            </s:sequence>
          </s:complexType>
          <s:element name="consultarNombradaResponse">
            <s:complexType>
              <s:sequence>
                <s:element minOccurs="0" maxOccurs="1" name="consultarNombradaResult" type="tns:NombradaConsultaMuellaje" />
              </s:sequence>
            </s:complexType>
          </s:element>
          <s:complexType name="NombradaConsultaMuellaje">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="Estado" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="Mensaje" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="consultaDetalle" type="tns:ArrayOfNombradaOutMuellaje" />
            </s:sequence>
          </s:complexType>
          <s:complexType name="ArrayOfNombradaOutMuellaje">
            <s:sequence>
              <s:element minOccurs="0" maxOccurs="unbounded" name="NombradaOutMuellaje" nillable="true" type="tns:NombradaOutMuellaje" />
            </s:sequence>
          </s:complexType>
          <s:complexType name="NombradaOutMuellaje">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="idNombrada" nillable="true" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="fechaInicioNombrada" type="s:dateTime" />
              <s:element minOccurs="1" maxOccurs="1" name="fechaFinNombrada" type="s:dateTime" />
              <s:element minOccurs="1" maxOccurs="1" name="idNave" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="nombreNave" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="idPuerto" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="nombrePuerto" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="idLocacion" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="lugar" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="posicion" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="idTurno" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="glosaTurno" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="idEstadoNombrada" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="glosaEstadoNombrada" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="rutMuellaje" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="nombreMuellaje" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="rutConcesionaria" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="nombreConcesionario" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="NOMBRE_INSTALACION" type="s:string" />
            </s:sequence>
          </s:complexType>
          <s:element name="eliminarNombrada">
            <s:complexType>
              <s:sequence>
                <s:element minOccurs="1" maxOccurs="1" name="rutEmpresa" type="s:int" />
                <s:element minOccurs="1" maxOccurs="1" name="idNombrada" type="s:int" />
              </s:sequence>
            </s:complexType>
          </s:element>
          <s:element name="eliminarNombradaResponse">
            <s:complexType>
              <s:sequence>
                <s:element minOccurs="0" maxOccurs="1" name="eliminarNombradaResult" type="tns:Respuesta" />
              </s:sequence>
            </s:complexType>
          </s:element>
          <s:complexType name="Respuesta">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="Estado" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="Mensaje" type="s:string" />
            </s:sequence>
          </s:complexType>
          <s:element name="consultarNombradaByConcesionaria">
            <s:complexType>
              <s:sequence>
                <s:element minOccurs="1" maxOccurs="1" name="rutEmpresa" type="s:int" />
                <s:element minOccurs="1" maxOccurs="1" name="filtro" nillable="true" type="tns:FiltroNombrada" />
              </s:sequence>
            </s:complexType>
          </s:element>
          <s:element name="consultarNombradaByConcesionariaResponse">
            <s:complexType>
              <s:sequence>
                <s:element minOccurs="0" maxOccurs="1" name="consultarNombradaByConcesionariaResult" type="tns:NombradaConsultaConcesionario" />
              </s:sequence>
            </s:complexType>
          </s:element>
          <s:complexType name="NombradaConsultaConcesionario">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="Estado" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="Mensaje" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="consultaDetalle" type="tns:ArrayOfNombradaOutConcesionario" />
            </s:sequence>
          </s:complexType>
          <s:complexType name="ArrayOfNombradaOutConcesionario">
            <s:sequence>
              <s:element minOccurs="0" maxOccurs="unbounded" name="NombradaOutConcesionario" nillable="true" type="tns:NombradaOutConcesionario" />
            </s:sequence>
          </s:complexType>
          <s:complexType name="NombradaOutConcesionario">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="idNombrada" nillable="true" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="fechaInicioNombrada" type="s:dateTime" />
              <s:element minOccurs="1" maxOccurs="1" name="fechaFinNombrada" type="s:dateTime" />
              <s:element minOccurs="1" maxOccurs="1" name="idNave" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="nombreNave" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="idLocacion" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="lugar" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="idPuerto" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="nombrePuerto" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="idTurno" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="glosaTurno" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="posicion" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="idEstadoNombrada" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="glosaEstadoNombrada" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="rutMuellaje" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="nombreMuellaje" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="idLabor" nillable="true" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="idFuncion" nillable="true" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="rutConcesionaria" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="nombreConcesionaria" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="trabajadores" type="tns:ArrayOfTrabajador" />
            </s:sequence>
          </s:complexType>
          <s:complexType name="ArrayOfTrabajador">
            <s:sequence>
              <s:element minOccurs="0" maxOccurs="unbounded" name="Trabajador" nillable="true" type="tns:Trabajador" />
            </s:sequence>
          </s:complexType>
          <s:complexType name="Trabajador">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="idContrato" nillable="true" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="rutTrabajador" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="dvTrabajador" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="pasaporteTrabajador" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="nombresTrabajador" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="apellidoPaternoTrabajador" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="idLabor" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="glosaLabor" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="idFuncion" nillable="true" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="glosaFuncion" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="glosaEstadoTrabajador" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="activoTrabajador" nillable="true" type="s:boolean" />
            </s:sequence>
          </s:complexType>
          <s:element name="verificarTrabajadorPortuario">
            <s:complexType>
              <s:sequence>
                <s:element minOccurs="1" maxOccurs="1" name="rutEmpresa" type="s:int" />
                <s:element minOccurs="1" maxOccurs="1" name="rutTrabajador" type="s:int" />
                <s:element minOccurs="0" maxOccurs="1" name="dvTrabajador" type="s:string" />
                <s:element minOccurs="1" maxOccurs="1" name="fecha" type="s:dateTime" />
              </s:sequence>
            </s:complexType>
          </s:element>
          <s:element name="verificarTrabajadorPortuarioResponse">
            <s:complexType>
              <s:sequence>
                <s:element minOccurs="0" maxOccurs="1" name="verificarTrabajadorPortuarioResult" type="tns:Respuesta" />
              </s:sequence>
            </s:complexType>
          </s:element>
        </s:schema>
      </wsdl:types>
      <wsdl:message name="registrarNombradasSoapIn">
        <wsdl:part name="parameters" element="tns:registrarNombradas" />
      </wsdl:message>
      <wsdl:message name="registrarNombradasSoapOut">
        <wsdl:part name="parameters" element="tns:registrarNombradasResponse" />
      </wsdl:message>
      <wsdl:message name="registrarNombradasUserCredentials">
        <wsdl:part name="UserCredentials" element="tns:UserCredentials" />
      </wsdl:message>
      <wsdl:message name="modificarNombradasSoapIn">
        <wsdl:part name="parameters" element="tns:modificarNombradas" />
      </wsdl:message>
      <wsdl:message name="modificarNombradasSoapOut">
        <wsdl:part name="parameters" element="tns:modificarNombradasResponse" />
      </wsdl:message>
      <wsdl:message name="modificarNombradasUserCredentials">
        <wsdl:part name="UserCredentials" element="tns:UserCredentials" />
      </wsdl:message>
      <wsdl:message name="getResolucionNombradaSoapIn">
        <wsdl:part name="parameters" element="tns:getResolucionNombrada" />
      </wsdl:message>
      <wsdl:message name="getResolucionNombradaSoapOut">
        <wsdl:part name="parameters" element="tns:getResolucionNombradaResponse" />
      </wsdl:message>
      <wsdl:message name="getResolucionNombradaUserCredentials">
        <wsdl:part name="UserCredentials" element="tns:UserCredentials" />
      </wsdl:message>
      <wsdl:message name="consultarNombradaSoapIn">
        <wsdl:part name="parameters" element="tns:consultarNombrada" />
      </wsdl:message>
      <wsdl:message name="consultarNombradaSoapOut">
        <wsdl:part name="parameters" element="tns:consultarNombradaResponse" />
      </wsdl:message>
      <wsdl:message name="consultarNombradaUserCredentials">
        <wsdl:part name="UserCredentials" element="tns:UserCredentials" />
      </wsdl:message>
      <wsdl:message name="eliminarNombradaSoapIn">
        <wsdl:part name="parameters" element="tns:eliminarNombrada" />
      </wsdl:message>
      <wsdl:message name="eliminarNombradaSoapOut">
        <wsdl:part name="parameters" element="tns:eliminarNombradaResponse" />
      </wsdl:message>
      <wsdl:message name="eliminarNombradaUserCredentials">
        <wsdl:part name="UserCredentials" element="tns:UserCredentials" />
      </wsdl:message>
      <wsdl:message name="consultarNombradaByConcesionariaSoapIn">
        <wsdl:part name="parameters" element="tns:consultarNombradaByConcesionaria" />
      </wsdl:message>
      <wsdl:message name="consultarNombradaByConcesionariaSoapOut">
        <wsdl:part name="parameters" element="tns:consultarNombradaByConcesionariaResponse" />
      </wsdl:message>
      <wsdl:message name="consultarNombradaByConcesionariaUserCredentials">
        <wsdl:part name="UserCredentials" element="tns:UserCredentials" />
      </wsdl:message>
      <wsdl:message name="verificarTrabajadorPortuarioSoapIn">
        <wsdl:part name="parameters" element="tns:verificarTrabajadorPortuario" />
      </wsdl:message>
      <wsdl:message name="verificarTrabajadorPortuarioSoapOut">
        <wsdl:part name="parameters" element="tns:verificarTrabajadorPortuarioResponse" />
      </wsdl:message>
      <wsdl:message name="verificarTrabajadorPortuarioUserCredentials">
        <wsdl:part name="UserCredentials" element="tns:UserCredentials" />
      </wsdl:message>
      <wsdl:portType name="NombradasSoap">
        <wsdl:operation name="registrarNombradas">
          <wsdl:input message="tns:registrarNombradasSoapIn" />
          <wsdl:output message="tns:registrarNombradasSoapOut" />
        </wsdl:operation>
        <wsdl:operation name="modificarNombradas">
          <wsdl:input message="tns:modificarNombradasSoapIn" />
          <wsdl:output message="tns:modificarNombradasSoapOut" />
        </wsdl:operation>
        <wsdl:operation name="getResolucionNombrada">
          <wsdl:input message="tns:getResolucionNombradaSoapIn" />
          <wsdl:output message="tns:getResolucionNombradaSoapOut" />
        </wsdl:operation>
        <wsdl:operation name="consultarNombrada">
          <wsdl:input message="tns:consultarNombradaSoapIn" />
          <wsdl:output message="tns:consultarNombradaSoapOut" />
        </wsdl:operation>
        <wsdl:operation name="eliminarNombrada">
          <wsdl:input message="tns:eliminarNombradaSoapIn" />
          <wsdl:output message="tns:eliminarNombradaSoapOut" />
        </wsdl:operation>
        <wsdl:operation name="consultarNombradaByConcesionaria">
          <wsdl:input message="tns:consultarNombradaByConcesionariaSoapIn" />
          <wsdl:output message="tns:consultarNombradaByConcesionariaSoapOut" />
        </wsdl:operation>
        <wsdl:operation name="verificarTrabajadorPortuario">
          <wsdl:input message="tns:verificarTrabajadorPortuarioSoapIn" />
          <wsdl:output message="tns:verificarTrabajadorPortuarioSoapOut" />
        </wsdl:operation>
      </wsdl:portType>
      <wsdl:binding name="NombradasSoap" type="tns:NombradasSoap">
        <soap:binding transport="http://schemas.xmlsoap.org/soap/http" />
        <wsdl:operation name="registrarNombradas">
          <soap:operation soapAction="https://sccnlp.com/registrarNombradas" style="document" />
          <wsdl:input>
            <soap:body use="literal" />
            <soap:header message="tns:registrarNombradasUserCredentials" part="UserCredentials" use="literal" />
          </wsdl:input>
          <wsdl:output>
            <soap:body use="literal" />
          </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="modificarNombradas">
          <soap:operation soapAction="https://sccnlp.com/modificarNombradas" style="document" />
          <wsdl:input>
            <soap:body use="literal" />
            <soap:header message="tns:modificarNombradasUserCredentials" part="UserCredentials" use="literal" />
          </wsdl:input>
          <wsdl:output>
            <soap:body use="literal" />
          </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="getResolucionNombrada">
          <soap:operation soapAction="https://sccnlp.com/getResolucionNombrada" style="document" />
          <wsdl:input>
            <soap:body use="literal" />
            <soap:header message="tns:getResolucionNombradaUserCredentials" part="UserCredentials" use="literal" />
          </wsdl:input>
          <wsdl:output>
            <soap:body use="literal" />
          </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="consultarNombrada">
          <soap:operation soapAction="https://sccnlp.com/consultarNombrada" style="document" />
          <wsdl:input>
            <soap:body use="literal" />
            <soap:header message="tns:consultarNombradaUserCredentials" part="UserCredentials" use="literal" />
          </wsdl:input>
          <wsdl:output>
            <soap:body use="literal" />
          </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="eliminarNombrada">
          <soap:operation soapAction="https://sccnlp.com/eliminarNombrada" style="document" />
          <wsdl:input>
            <soap:body use="literal" />
            <soap:header message="tns:eliminarNombradaUserCredentials" part="UserCredentials" use="literal" />
          </wsdl:input>
          <wsdl:output>
            <soap:body use="literal" />
          </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="consultarNombradaByConcesionaria">
          <soap:operation soapAction="https://sccnlp.com/consultarNombradaByConcesionaria" style="document" />
          <wsdl:input>
            <soap:body use="literal" />
            <soap:header message="tns:consultarNombradaByConcesionariaUserCredentials" part="UserCredentials" use="literal" />
          </wsdl:input>
          <wsdl:output>
            <soap:body use="literal" />
          </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="verificarTrabajadorPortuario">
          <soap:operation soapAction="https://sccnlp.com/verificarTrabajadorPortuario" style="document" />
          <wsdl:input>
            <soap:body use="literal" />
            <soap:header message="tns:verificarTrabajadorPortuarioUserCredentials" part="UserCredentials" use="literal" />
          </wsdl:input>
          <wsdl:output>
            <soap:body use="literal" />
          </wsdl:output>
        </wsdl:operation>
      </wsdl:binding>
      <wsdl:binding name="NombradasSoap12" type="tns:NombradasSoap">
        <soap12:binding transport="http://schemas.xmlsoap.org/soap/http" />
        <wsdl:operation name="registrarNombradas">
          <soap12:operation soapAction="https://sccnlp.com/registrarNombradas" style="document" />
          <wsdl:input>
            <soap12:body use="literal" />
            <soap12:header message="tns:registrarNombradasUserCredentials" part="UserCredentials" use="literal" />
          </wsdl:input>
          <wsdl:output>
            <soap12:body use="literal" />
          </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="modificarNombradas">
          <soap12:operation soapAction="https://sccnlp.com/modificarNombradas" style="document" />
          <wsdl:input>
            <soap12:body use="literal" />
            <soap12:header message="tns:modificarNombradasUserCredentials" part="UserCredentials" use="literal" />
          </wsdl:input>
          <wsdl:output>
            <soap12:body use="literal" />
          </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="getResolucionNombrada">
          <soap12:operation soapAction="https://sccnlp.com/getResolucionNombrada" style="document" />
          <wsdl:input>
            <soap12:body use="literal" />
            <soap12:header message="tns:getResolucionNombradaUserCredentials" part="UserCredentials" use="literal" />
          </wsdl:input>
          <wsdl:output>
            <soap12:body use="literal" />
          </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="consultarNombrada">
          <soap12:operation soapAction="https://sccnlp.com/consultarNombrada" style="document" />
          <wsdl:input>
            <soap12:body use="literal" />
            <soap12:header message="tns:consultarNombradaUserCredentials" part="UserCredentials" use="literal" />
          </wsdl:input>
          <wsdl:output>
            <soap12:body use="literal" />
          </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="eliminarNombrada">
          <soap12:operation soapAction="https://sccnlp.com/eliminarNombrada" style="document" />
          <wsdl:input>
            <soap12:body use="literal" />
            <soap12:header message="tns:eliminarNombradaUserCredentials" part="UserCredentials" use="literal" />
          </wsdl:input>
          <wsdl:output>
            <soap12:body use="literal" />
          </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="consultarNombradaByConcesionaria">
          <soap12:operation soapAction="https://sccnlp.com/consultarNombradaByConcesionaria" style="document" />
          <wsdl:input>
            <soap12:body use="literal" />
            <soap12:header message="tns:consultarNombradaByConcesionariaUserCredentials" part="UserCredentials" use="literal" />
          </wsdl:input>
          <wsdl:output>
            <soap12:body use="literal" />
          </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="verificarTrabajadorPortuario">
          <soap12:operation soapAction="https://sccnlp.com/verificarTrabajadorPortuario" style="document" />
          <wsdl:input>
            <soap12:body use="literal" />
            <soap12:header message="tns:verificarTrabajadorPortuarioUserCredentials" part="UserCredentials" use="literal" />
          </wsdl:input>
          <wsdl:output>
            <soap12:body use="literal" />
          </wsdl:output>
        </wsdl:operation>
      </wsdl:binding>
      <wsdl:service name="Nombradas">
        <wsdl:port name="NombradasSoap" binding="tns:NombradasSoap">
          <soap:address location="https://sccnlpservices-piloto.dirtrab.cl/Servicios/Nombradas.asmx" />
        </wsdl:port>
        <wsdl:port name="NombradasSoap12" binding="tns:NombradasSoap12">
          <soap12:address location="https://sccnlpservices-piloto.dirtrab.cl/Servicios/Nombradas.asmx" />
        </wsdl:port>
      </wsdl:service>
    </wsdl:definitions>

    Con esta información puedes armar un script como este:

    <?php
    
    $url = "https://sccnlpservices-piloto.dirtrab.cl/Servicios/Nombradas.asmx?WSDL";
    $client = new SoapClient($url, ["trace" => 1, "exception" => 0]);
    print_r($client->__getFunctions());

    Y, al correrlo obtendrás algo como:

    Array
    (
        [0] => registrarNombradasResponse registrarNombradas(registrarNombradas $parameters)
        [1] => modificarNombradasResponse modificarNombradas(modificarNombradas $parameters)
        [2] => getResolucionNombradaResponse getResolucionNombrada(getResolucionNombrada $parameters)
        [3] => consultarNombradaResponse consultarNombrada(consultarNombrada $parameters)
        [4] => eliminarNombradaResponse eliminarNombrada(eliminarNombrada $parameters)
        [5] => consultarNombradaByConcesionariaResponse consultarNombradaByConcesionaria(consultarNombradaByConcesionaria $parameters)
        [6] => verificarTrabajadorPortuarioResponse verificarTrabajadorPortuario(verificarTrabajadorPortuario $parameters)
        [7] => registrarNombradasResponse registrarNombradas(registrarNombradas $parameters)
        [8] => modificarNombradasResponse modificarNombradas(modificarNombradas $parameters)
        [9] => getResolucionNombradaResponse getResolucionNombrada(getResolucionNombrada $parameters)
        [10] => consultarNombradaResponse consultarNombrada(consultarNombrada $parameters)
        [11] => eliminarNombradaResponse eliminarNombrada(eliminarNombrada $parameters)
        [12] => consultarNombradaByConcesionariaResponse consultarNombradaByConcesionaria(consultarNombradaByConcesionaria $parameters)
        [13] => verificarTrabajadorPortuarioResponse verificarTrabajadorPortuario(verificarTrabajadorPortuario $parameters)
    )

    Ahora bien, digamos que quieres ejecutar una función como consultarNombrada.

    Podrías hacerlo de esta forma:

    $response = $client->getResolucionNombrada([ 'idNombrada' => 1, 'rutEmpresa' => 2 ]);

    Pero, al correrlo obtendrás un error similar a:

    SOAP Fault:
    Fault Code: soap:Server
    Fault String: System.Web.Services.Protocols.SoapException: Server was unable to process request. ---> System.NullReferenceException: Object reference not set to an instance of an object.
       at Services.Servicios.Nombradas.getResolucionNombrada(Int32 idNombrada, Int32 rutEmpresa) in C:\VSTS-Apl-Agent\_work\48\s\SCCNLP_Services\Services\Servicios\Nombradas.asmx.cs:line 144
       --- End of inner exception stack trace ---

    ¿Qué ha ocurrido? ¿Es un problema del lado del webservice?

    No. Simplemente, no configuraste la autenticación, por lo tanto, la llamada falla.

    Si miras nuevamente el WSDL verás esta definición:

    <soap:header message="tns:registrarNombradasUserCredentials" part="UserCredentials" use="literal"/>

    Que indica que, como parte de la llamada, es necesario especificar un encabezado llamado tns:registrarNombradasUserCredentials

    Este elemento usa la definición que aparece al comienzo (En la parte de los tipos):

    <s:complexType name="UserCredentials">
     <s:sequence>
      <s:element minOccurs="1" maxOccurs="1" name="userName" nillable="true" type="s:string"/>
      <s:element minOccurs="1" maxOccurs="1" name="password" nillable="true" type="s:string"/>
     </s:sequence>
     <s:anyAttribute/>
    </s:complexType>

    Es decir que, para que la llamada tenga éxito, debes incluir la información de autenticación (userName y password) como un encabezado SOAP.

    Puedes lograr esto agregando lo siguiente a tu código:

    $client->__setSoapHeaders([
                new SoapHeader(
                        "https://sccnlp.com/",
                        "UserCredentials",
                        [
                                "userName" => "XXXX",
                                "password" => "YYYY",
                        ]
                )
        ]);

    Y, asumiendo que tengas datos correctos para realizar la autenticación y que tu usuario tenga los permisos requeridos, obtendrás la información que buscas.

    Algo interesante de hacer este ajuste de esta forma es que los datos de autenticación estarán disponibles para todas las subsiguientes llamadas al servicio.

  • Por qué PHP 8 no satisface el requisito ^7.3 de composer

    Por qué PHP 8 no satisface el requisito ^7.3 de composer

    He ejecutado el comando:

    composer install

    En un ordenador de desarrollo recién instalado. Me ha arrojado un error:

    Root composer.json requires php ^7.3 but your php version (8.1.0) does not satisfy that requirement

    ¿No se supone que si tengo la versión 8 de PHP ese requisito debería no dar un error?

    Me crucé con esta consulta recientemente y me pareció interesante responderla ya que, probablemente, quien la realizó no sea el único con este problema u otro similar.

    Claramente la respuesta a la pregunta ¿No se supone que si tengo la versión 8 de PHP ese requisito debería no dar un error? es un No rotundo. Vamos que si no fuese así la pregunta ni existiría pero bueno… tomémosla como una pregunta retórica y un buen disparador para este post.

    Es más, cambiaré la pregunta por la que puse como título: ¿Por qué PHP 8 no satisface el requisito ^7.3 de composer?

    Para responder a esta pregunta es necesario comprender qué significa el ^7.3 que está en el archivo composer.jon.

    Antes de meterme en los detalles es importante saber que, en este contexto, esa expresión corresponde a un requisito de versión (Version constraint).

    Cómo funcionan los version constraints de composer

    Una de las características más potentes (¡y más confusas!) de composer es esta: la especificación de las versiones aceptables de cada dependencia.

    Precisamente, el motivo de la creación de composer fue el poder evitar tener la versión X.Y.Z de una librería en desarrollo y luego, en producción encontrarte con la versión X+1.Y.Z y que todo explote misteriosamente.

    La premisa en la que se basa composer es que las dependencias definen sus versiones utilizando versionado semántico. Si este supuesto no se cumple… difícilmente composer pueda hacer bien su trabajo.

    Ahora, el modo de expresar estos requerimientos requiere prestarle bastante atención a los detalles.

    Para comenzar, existen diferentes tipos de restricción.

    La más simple de ellas es la exacta. Este tipo de restricción debe usarse cuando sabés exactamente qué versión de una dependencia querés usar. En este ejemplo sería algo como 7.3.33.

    En un caso como este no hay dudas, el trabajo de composer es bien sencillo: descargar los archivos correspondientes al tag 7.3.33 del repo de php.

    Un caso de uso más realista es aquel en que no sabés el número exacto de la versión si no un aproximado.

    Sería algo como decir quiero usar una versión de php 7.3, el patch no me interesa demasiado.

    Esto sería similar a decir quiero una versión que se encuentre entre la 7.3.0 y la última antes de la 7.4.0.

    Para expresar dicha restricción podés usar la combinación de dos restricciones:

    • >=7.3
    • <7.4

    En el caso de composer, estas se combinan usando un simple espacio en blanco, que se interpreta como un &:

    >=7.3 <7.4

    Un detalle importante: composer siempre responderá con la versión más reciente que pueda, es decir, aquella que satisfaga todas las restricciones solicitadas.

    Qué significan los símbolos de las versiones en composer

    Ahora sí, vamos a ir un poco más abajo y veamos cada uno de los símbolos que se pueden usar para especificar versiones.

    >= y < se explican por si mismos creo.

    Pero también existen ~, ^ y *… la cosa se complica, ¿no?

    Intentaré ir del más simple al más complejo.

    El * es un viejo conocido de quienes estamos en software. ¿Tal vez te suene de las experesiones regulares? Pues sí, es el comodín.

    Esto significa que una restricción de tipo 7.3.* se interpreta como cualquier patch de la rama 7.3. Es decir, composer mapeará este valor al tag más reciente que encuentre en dicha rama.

    El ~ es una forma abreviada de escribir una restricción tipo >= <.

    El ^ se comporta casi como ~ pero es algo más estricto.

    La idea tanto de ~ como de ^ es evitar problemas de incompatibilidad hacia atrás.

    Esta es la clave para comprender el problema que está experimentando el protagonista de esta historia:

    La restricción ^7.3 marca, ante todo, que el major de la versión de php a utilizar debe ser 7, el minor podría cambiar (y, por supuesto, también el patch).

    Esto se debe a que, en cualquier salto de versión major es posible encontrar cambios que rompan compatibilidad hacia atrás.

    Es por eso que composer no permitirá que inadevertidamente introduzcamos un cambio de infraestructura que ponga nuestro sistema en riesgo.

    El sistema de restricciones de versiones es bastante complejo, si quieres conocer más sobre él te recomiendo ir directo a la fuente.

    De hecho, si te encuentras en una situación como la que dio origen a este post, te recomiendo abrir ese documento en una ventana y tu composer.json en otra.

  • Cómo usar PHPUnit

    Cómo usar PHPUnit

    Te decidiste. Llegó la hora de incorporar el testing automatizado a tus proyectos.

    Permitime que te felicite, es un gran paso hacia la generación de software de calidad superior.

    Después de hacer la debida investigación hay pocas dudas: PHPUnit es la herramienta que debes conocer.

    Para hacerte un poco más sencillo el camino, te dejo los lineamientos para dar tus primeros pasos.

    Cómo instalar PHPUnit

    Lo primero, como de costumbre, será instalar la herramienta.

    Si vas al sitio de phpunit.de encontrarás algo como:

    Nada mal para tener a mano pero, para empezar de cero… puede ser algo intimidante.

    Antes de continuar, una advertencia: las versiones de phpUnit están bastante ligadas a las de php. Esto significa que, para asegurarte de elegir una versión que puedas utilizar, debes saber qué versión de php tienes instalada (O piensas instalar, por ejemplo, si utilizarás docker).

    Seleccionar la versión de phpunit

    Asumamos que, para arrancar, usarás el php instalado en tu propio host.

    Un simple php -v alcanzará:

    Pues bien, en este caso, la versión que más conviene utilizar es la 11, como puede verse aquí:

    El siguiente paso: instalar la librería.

    Instalar phpunit mediante el phar

    La primera opción disponible es descargar el paquete cerrado (el archivo .phar) de aquí y hacerlo ejecutable.

    Esta opción puede resultar útil si quieres dejar una única versión de phpunit disponible para todos tus proyectos.

    Instalar phpunit usando composer

    Por lejos, la forma que recomiendo (y utilizo) es hacerlo a través de composer.

    De esta forma, la dependencia quedará circunscripta al proyecto en el que estás trabajando en este momento, a la vez que puedes compartir esta configuración con el resto de tu equipo.

    Usa composer require --dev phpunit/phpunit ^11 y composer se encargará de todo.

    Para verificar tu instalación usa el comando ./vendor/bin/phpunit --version

    Si ves algo como

    PHPUnit 11.4.4 by Sebastian Bergmann and contributors.

    PHPUnit 11.4.4 by Sebastian Bergmann and contributors.

    Estás listo para avanzar.

    Cómo escribir un test con PHPUnit

    Escribir un test de PHPUnit supone crear una nueva clase que extienda de TestCase:

    <?php declare(strict_types=1);
    use PHPUnit\Framework\TestCase;
    
    final class MyTest extends TestCase
    {
    }

    Para ejecutar tus tests puedes utilizar el comando ./vendor/bin/phpunit MyTest.php y obtendrás una salida como:

    PHPUnit 11.4.4 by Sebastian Bergmann and contributors.
    
    Runtime:       PHP 8.4.1
    
    There was 1 PHPUnit test runner warning:
    
    1) No tests found in class "MyTest".
    
    No tests executed!

    Bastante razonable ¿no? Al fin y al cabo, se ha definido un TestCase pero ningún test dentro. Corrijamos eso.

    Por convención, PHPUnit entenderá que cualquier método de una clase TestCase cuyo nombre comience por test debe ser ejecutado por él, es decir, lo próximo que deberías hacer es agregar un método como este:

    <?php declare(strict_types=1);
    use PHPUnit\Framework\TestCase;
    
    final class MyTest extends TestCase
    {
       public function testSomething(): void
       {
       }
    }

    Al ejecutar este test verás algo como:

    PHPUnit 11.4.4 by Sebastian Bergmann and contributors.
    
    Runtime:       PHP 8.4.1
    
    R                                                                   1 / 1 (100%)
    
    Time: 00:00.003, Memory: 8.00 MB
    
    There was 1 risky test:
    
    1) MyTest::testSomething
    This test did not perform any assertions
    
    /home/mauro/phpunit-poc/MyTest.php:6
    
    OK, but there were issues!
    Tests: 1, Assertions: 0, Risky: 1.

    Es decir, se ha ejecutado un test, sólo que este test no ha verificado nada, es decir, no hay realizado ningún assertion.

    Para que efectivamente aporte algo de información, dentro del cuerpo del test debe utilizarse alguno de los métodos assert* que provee phpUnit, por ejemplo:

    <?php declare(strict_types=1);
    use PHPUnit\Framework\TestCase;
    
    final class MyTest extends TestCase
    {
       public function testSomething(): void
       {
          $this->assertTrue(true);
       }
    }

    Y ahora sí, llegarás a una salida algo más parecida a lo deseable:

    PHPUnit 11.4.4 by Sebastian Bergmann and contributors.
    
    Runtime:       PHP 8.4.1
    
    .                                                                   1 / 1 (100%)
    
    Time: 00:00.003, Memory: 8.00 MB
    
    OK (1 test, 1 assertion)

    Por supuesto que este test en la realidad no aporta mucho. Depende de vos hacer tests que sean relevantes para tu aplicación.

    Ejemplo de un test con PHPUnit

    Veamos un ejemplo algo más realista de lo que se puede hacer con phpUnit.

    Imaginemos que tienes una función que, recibe una lista de números de teléfono y una lista de prefijos y retorna aquellos que comienzan por alguno de los prefijos indicados por el segundo argumento.

    Un test para dicha función podría ser este:

    <?php declare(strict_types=1);
    use PHPUnit\Framework\TestCase;
    
    final class MyTest extends TestCase
    {
            public function testPhoneFilter(): void
            {
                    $filteredPhoneNumbers = filter_phone_numbers(
                            [ '(34) 665-55-22-112', '34 992 11 22 33', '+54 9 11 5494 2211', '054 121 123123' ],
                            [ '34', '054' ],
                    );
    
                    $this->assertEquals( [ '34 992 11 22 33', '054 121 123123'] , $filteredPhoneNumbers );
            }
    }

    Por dónde continuar aprendiendo

    PHPUnit ofrece una cantidad de posibilidades realmente interesante pero, para no marearte con todo al comienzo te recomiendo continuar aprendiendo sobre dobles de test y sobre la configuración de phpUnit.

    Ya habrá tiempo para lo demás.

  • Cómo instalar extensiones PHP en Docker

    Cómo instalar extensiones PHP en Docker

    Estás arrancando la dockerización de tu aplicación PHP.

    Como para probar un poco, levantaste un contenedor usando un comando:

    docker run -v $(pwd):/app -it php:latest index.php

    Y ahí nomás te encontraste con el primero de los problemas:

    PHP Fatal error: Uncaught Error: Class "ZipArchive" not found

    ¡Claro!

    Tu aplicación necesita de la extensión Zip para funcionar… ¿Y ahora?

    Bueno, a no desesperar.

    Tenés varias opciones disponibles:

    1. Buscar una imagen que ya tenga la extensión zip instalada
    2. Agregar las instrucciones para que se compile e instale la extensión durante el build de la imagen
    3. Usar un script de instalación

    Para el resto de los ejemplos tomaré una versión hiper simplificada de lo que puede ser tu aplicación:

    <?php
    
    $zip = new ZipArchive();
    if ($zip->open($argv[1]) == TRUE) {
     for ($i = 0; $i < $zip->numFiles; $i++) {
         echo $zip->getNameIndex($i).PHP_EOL;
     }
    }

    Y asumiré que tienes un archivo comprimido llamado compressed.zip.

    Una imagen Docker PHP con la extensión zip

    Seguramente habrá muchas opciones, una de ellas, como para salir del paso, es thecodingmachine/php:7.3-v4-slim-cli (Podés encontrar otras similares acá).

    Para usarla este comando puede ser útil: docker run -v $(pwd):/usr/src/app --rm -it thecodingmachine/php:7.3-v4-slim-cli php index.php compressed.zip

    Como decía, esta solución no es ni de lejos la mejor.

    ¿Por qué? Básicamente porque tenés muy poco control sobre lo que tiene la imágen. En otras palabras, no sabés muy bien qué otras cosas te estás trayendo además de la extensión zip.

    En el mejor de los casos vas a terminar con una imagen más grande de lo necesario.

    En el peor vas a incluir extensiones que no necesitás y exponerte a riesgos de seguridad.

    Mejor seguir leyendo.

    Compilar la extensión zip dentro del build de la imagen

    Una forma de contar con la extensión zip (o cualquier otra en realidad) en tu contenedor es compilar php de modo que lo tenga incorporado.

    Por ejemplo, usando un Dockerfile como este:

    FROM ubuntu:latest
    
    ADD https://www.php.net/distributions/php-8.4.1.tar.gz /php/php.tar.gz
    RUN apt-get update && \
            apt-get install -y libzip-dev build-essential pkg-config libxml2-dev libsqlite3-dev && \
            tar xzf /php/php.tar.gz -C /php/ && \
            cd /php/php-8.4.1/ && \
            ./configure --prefix=/usr/local/php-8.4.1 --with-zip && \
            make && make install && \
            ln -s /usr/local/php-8.4.1/bin/php /usr/local/bin
    WORKDIR /app

    Y construtendo la imagen usando docker build . -t php-zip

    Podrías ejecutar tu script usando: docker run -v $(pwd):/app --rm -it php-zip php index.php compressed.zip

    Y no verás el error que estaba al comienzo.

    Esta opción, si bien es más correcta que la primera, puede resultar un poco overkill. Particularmente, si tenés que instalar varias extensiones, llegar al Dockerfile correcto puede ser bastante laborioso.

    En la siguiente sección te doy la que personalmente recomiendo

    Un script de instalación de extensiones PHP para Docker

    Bueno, en realidad no es un script de instalación si no dos.

    Arranco por el más conocido: php-ext-install.

    Lo bueno de este script es que, si partís de una imagen oficial de php, no tenés que hacer nada para tenerlo disponible.

    Lo malo de este script es que requiere bastante ayuda para hacer lo suyo.

    Por ejemplo, instalar la extensión zip implica agregar a tu Dockerfile:

    RUN docker-php-ext-install zip

    Pero… con eso sólo no alcanza. Si lo intentas, al hacer el build verás este error:

    Package 'libzip', required by 'virtual:world', not found
    Package 'libzip', required by 'virtual:world', not found
    Package 'libzip', required by 'virtual:world', not found

    Lo que quiere decir que necesitas, antes de esto, instalar la librería zip.

    En definitiva, el Dockerfile que te dará lo que buscas es uno que se parezca a:

    FROM php:8.4.1-cli
    
    RUN apt update && apt-get install -y libzip-dev && docker-php-ext-install zip

    En este caso no ha sido tan terrible, cierto, pero hay otras extensiones que también requieren ciertos cambios en configuraciones. Nada del otro mundo pero es algo más que hay que recordar.

    El script que me gusta más, y el que recomiendo usar, es docker-php-extension-installer.

    A diferencia del primero, docker-php-extension-installer debe ser instalado explícitamente pero, una vez instalado, es mucho más cómodo.

    Aquí un ejemplo de Dockerfile para la extensión zip:

    FROM php:8.4.1-cli
    
    ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
    
    RUN install-php-extensions zip
    
    WORKDIR /app

    Nuevamente, ejecutando docker run -v $(pwd):/app --rm -it php-zip php index.php compressed.zip verás el listado de los archivos contenidos en el archivo comprimido.

    Así que ya lo sabés: la próxima vez que te toque agregar extensiones a un php sobre docker, agregá esta línea a tu Dockerfile:

    ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/

    Y problema resuelto.

  • Cómo agregar una página de error 500 en un proyecto PHP

    Cómo agregar una página de error 500 en un proyecto PHP

    Es muy feo encontrarse con una página tipo

    Cuando estás intentando encontrar lo que buscás, ¿cierto?

    Es esa sensación de impotencia sobre todo.

    Error 500.

    Error interno del servidor.

    Es la versión digital del «No sos vos, soy yo»…

    O peor, algo como:

    Ahí directamente te sentirías insultado…

    Imaginate que lo mismo sienten los visitantes de tus sitios. O, más probablement, los visitantes de los sitios de tus clientes.

    Claro que, por más que intentes, tener un sistema que no falle nunca jamás es bastante difícil. Especialmente cuando éste se haya en las versiones preliminares.

    Pero, si bien no es práctico pasar días o meses retrasando la salida a producción hasta estar 120% seguro de que no hay ningún caso de borde escondido acechando detrás de la puerta, algo que va muy bien es hacer de la experiencia de encontrarse ante un error algo menos traumático.

    Dependiendo del contexto podés ofrecer algo que cause gracia como:

    Pero, más allá de la elegancia de este tratamiento de errores, hay algo muy importante: no dejar al visitante encerrado en un callejón sin salida.

    Notá como, en el último ejemplo se preserva el menú desplegable, el link a volver a la página principal y la señal de notificaciones:

    Definitivamente, una mejor experiencia, ¿cierto?

    La pregunta infaltable: ¿Cómo podrías tener esto en tus aplicaciones?

    Veamos algunas alternativas.

    Configurar una página de error a través del webserver

    La primera forma de lograr esto es usando la configuración del webserver.

    La idea es que sea él quien, dependiendo del código HTTP que retorne tu aplicación, redirija el tráfico a un script definido a tal efecto.

    Versión Apache

    Si tu servidor es Apache podés usar un archivo .htaccess o, si tenés acceso directo a la configuración, hacerlo en la definición de tu VirtualHost.

    Se trata de usar la directiva ErrorDocument, por ejemplo:

    ErrorDocument 500 http://tudominio.com/errors/500.php

    De este modo, cuando tu script principal retorne un código 500 el visitante será redirigido automáticamente al script errors/500.php y, una vez ahí, podés ponerle toda la estética de tu sitio, como si fuera una página más.

    Versión NginX

    En el caso de NginX la herramienta a usar será error_page:

    error_page 500 /errors/500.php

    Esta puede ser una solución aceptable, aunque muchas veces es mejor dejar la infraestructura lo más simple posible y manejar toda la funcionalidad desde la aplicación.

    Configurar un error handler en php

    PHP ofrece una herramienta muy interesante para estos casos: la función set_error_handler

    Mediante esta función es posible establecer tu propio callback, el cual será invocado cuando se produzca un error.

    Por ejemplo si tuvieras un código como:

    <?php
    
    trigger_error("Este es un error personalizado");

    Al ejecutarlo verías esta salida:

    PHP Notice:  Este es un error personalizado in /home/mauro/error_handler.php on line 3

    Al agregarle el error_handler:

    <?php
    
    set_error_handler(function( int $errno, string $errstr, ?string $errfile, ?int $errline, array $errcontext = []) : bool {
            echo "Se produjo el error $errno: $errstr";
    
            return true;
    });
    
    trigger_error("Este es un error personalizado");

    Y ejecutarlo verías:

    Se produjo el error 1024: Este es un error personalizado

    Es cierto, la diferencia no es grandiosa… pero eso depende de lo que pongas en el error_handler. Podrías enviar correos, enviar eventos a una cola o incluso, y esta es la parte más interesante, a diferencia de la opción anterior, generar páginas de error específicas para cada situación.

    Otro detalle importante: la definición del error_handler deberías tomarla como algo similar al autoload. Es decir, si tu aplicación abarca muchos scripts, esta definición debería realizarse una única vez, en el que actúa como punto de entrada, probablemente index.php.