Categoría: Cómo hacer para…

Estos artículos te explicarán cómo resolver problemas específicos usando PHP

  • Cómo manejar las excepciones en API Rest con Symfony

    Cómo manejar las excepciones en API Rest con Symfony

    Desarrollaste tu primer servicio web usando Symfony.

    Lo probaste por acá y por allá y funciona todo… salvo cuando no es así.

    Justo hubo un caso de esos muy extraños en que el registro que buscabas no tiene exactamente todos los campos solicitados… cosas que pasan en el desarrollo de software.

    Nada es grave, ¿no? Al final sólo se trata de enviar al cliente una respuesta adecuada para que sea él quien se encargue de resolver el problema y acompañarla del código de error HTTP correspondiente.

    A lo sumo, agregar una capa de Logging para poder analizar el tema cuando sea posible.

    La idea es bastante simple pero la pregunta es cómo hacerlo correctamente dentro aprovechando al máximo las capacidades del framework Symfony.

    La forma simple

    Para hacer algo rápido no se requiere mucho, basta con atrapar la excepción dentro del controlador en cuestión, generar una respuesta JSON, asignarle el código HTTP que corresponda y enviarla.

    Algo así:

    #[Route('/dangerous', name: 'dangerous_action', methods: ['GET'])]
        public function dangerousAction(Request $request): JsonResponse
        {
            try {
                $this->exceptionThrowerMethod();
            } catch (\Exception $exception) {
                
                $response = new JsonResponse([
                    'message' => $exception->getMessage(),
                    'data' => [],
                    'errors' => []
                ]);
                
                $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
                
                return $response;
            }
        }

    El problema es que esto habrá que replicarlo en cada acción (o en cada controlador al menos).

    Usando algunos elementos de Symfony es posible lograr una solución mucho más elegante y escalable.

    La forma correcta

    Si la API en cuestión cuenta con múltiples end-points es conveniente estandarizar el formato de las respuestas que se enviarán, sean estas exitosas o fallidas.

    El mejor modo de lograr esta homogeneidad es, como siempre que se trata de software, centralizar la funcionalidad común en un único componente.

    En este caso vale la pena contar con una clase como esta:

    <?php
    
    namespace App\Responses;
    
    class APIResponse extends \Symfony\Component\HttpFoundation\JsonResponse
    {
        /**
         * ApiResponse constructor.
         *
         * @param string $message
         * @param mixed  $data
         * @param array  $errors
         * @param int    $status
         * @param array  $headers
         * @param bool   $json
         */
        public function __construct(string $message, $data = null, array $errors = [], int $status = 200, array $headers = [], bool $json = false)
        {
            parent::__construct([
                'message' => $message,
                'data' => $data,
                'errors' => $errors,
            ], $status, $headers, $json);
        }
    }

    Con esto cuentas con lo necesario para estandarizar las respuestas a las peticiones de la API… pero el problema que se menciona en la sección anterior persiste: ¿cómo utilizar esta clase para responder a las excepciones?

    La respuesta: usando el sistema de eventos de Symfony.

    Cada excepción que no es atrapada explícitamente termina generando un evento de tipo kernel.exception, el cual puede ser atrapado por un EventListener como este:

    <?php
    
    namespace App\EventListener;
    
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
    use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
    
    class ExceptionListener
    {
        public function onKernelException(GetResponseForExceptionEvent $event)
        {
            $exception = $event->getException();
            $request   = $event->getRequest();
            
            $response = $this->createApiResponse($exception);
            $event->setResponse($response);
        }
        
        private function createApiResponse(\Exception $exception)
        {
            $statusCode = $exception instanceof HttpExceptionInterface ? $exception->getStatusCode() : Response::HTTP_INTERNAL_SERVER_ERROR;
            $errors     = [];
    
            return new APIResponse($exception->getMessage(), null, $errors, $statusCode);
        }
    }

    Con esta clase ya tienes definida la acción que debe tomarse ante una excepción y, combinándola con la clase definida en el paso anterior te aseguras de que la respuesta siempre tenga el mismo formato (Algo que los consumidores de tu API te agradecerán seguramente).

    Sólo falta un último paso: conectar esta clase con el EventDispatcher.

    Registrar un EventListener en Symfony

    Para lograr esto necesitas hacer un par de ajustes a la configuración de tu aplicación.

    Al final del archivo config/services.yaml debes agregar:

    App\EventListener\ExceptionListener:
            tags:
                - { name: kernel.event_listener, event: kernel.exception }

    Y ya está.

    Con esta configuración Symfony sabrá conectar el método onKernelException de tu clase App\EventListener\ExceptionListener.

    Si te quedan dudas de cómo es hace Symfony para establecer esta relación puedes leer este artículo.

    Para seguir investigando

    Si estás desarrollando o, mejor aún, pensando en desarrollar una API REST usando Symfony te sugiero que le des una mirada a API-Platform, seguro que te puede ahorrar muchos dolores de cabeza.

  • Cómo leer un archivo de Excel desde PHP

    Cómo leer un archivo de Excel desde PHP

    Referencias:

    • https://www.it-swarm-es.com/es/php/como-puedo-leer-archivos-.xls-excel-con-php/941255940/
    • https://forobeta.com/temas/ayuda-como-leer-archivo-de-excell-desde-php.675902/#post-5491417
    • https://foro.elhacker.net/buscador-t324525.0.html
    <?php
    
    use PhpOfficePhpSpreadsheetIOFactory;
    
    $spreadsheet = IOFactory::load('entrada.xls');
    $sheetData = $spreadsheet->getActiveSheet()->toArray(null, true, true, true);
    var_dump($sheetData);

    En este caso, la clase PhpOfficePhpSpreadsheetIOFactoryintentará «adivinar» el tipo de planilla de la que se trata (Algo bastante útil cuando tienes que tratar con diferentes tipos de planilla).

    ¿Cómo funciona esta adivinación? es bastante complejo… el punto es que puede fallar, con lo cual, si sabés exactamente el tipo de planilla que vas a usar, más vale usar un Reader específico:

    <?php
    
    use PhpOfficePhpSpreadsheetReaderXls;
    
    $reader = new Xls();
    $spreadsheet = $reader->load('entrada.xls');
    
    $sheetData = $spreadsheet->getActiveSheet()->toArray(null, true, true, true);
    var_dump($sheetData);
    

    Conclusión

    Como podrás ver en los ejemplos, es bastante sencillo realizar operaciones sobre una planilla Excel casi como si estuvieses escribiendo una macro.

    Además de lo dicho hasta aquí, la documentación de PHPSpreadSheet es un lujo (Sólo que está en Inglés).

  • Cómo enviar archivos grandes a través de WebServices

    Cómo enviar archivos grandes a través de WebServices

    Horacio, un ex-alumno, me escribió esta consulta:

    Hola profesor, tanto tiempo, sabe que tengo una pregunta , sabe que tengo que hacer un web service en php y queria saber que me conviene si soap o rest , son datos de gran tamaño , la idea es hacer una aplicación que permite cargar archivos xlsx(importar archivo) a una base de datos mysql (destino) y de ahí que lo consuma otro sistema web . Son archivos de gran tamaño.según su conocimiento que me recomienda?Aguardo respuesta.Saludos y gracias.

    El problema en que se encuentra Horacio radica en que los webservices, tanto REST como SOAP, están basados en el protocolo HTTP.

    Este protocolo se presta muy bien para el intercambio de texto, pero cuando se trata de datos, ya no resulta tan conveniente. Mucho menos si se trata del intercambio de muchos datos.

    ¿Cuál es el problema? Pues que para que los datos se envíen a través de HTTP, primero deben ser transformados a texto, lo cual agrega una sobrecarga de procesamiento y, a la vez, hace que la cantidad total de información a enviar sea mucho mayor.

    ¿Por qué sucede esto?

    Transferencia binaria versus transferencia textual

    La raíz del problema es la representación interna de los datos en la computadora.

    Un ejemplo muy simple: el número 1234567890 puede ser almacenado entero o como la secuencia de caracteres 1 2 3 4 5 6 7 8 9 0.

    En el primero de los casos, nos alcanzarían 31 bits para almacenarlo (230 = 1073741824 y 231=2147483648), mientras que en el segundo necesitaríamos 80 (Asumiendo 8 bits por cada caracter).

    Esto como para poner un poco en contexto por qué la transmisión binaria es más eficiente que la textual.

    Si multiplicamos esta diferencia de 49 bits por un gran conjunto de información a enviar pronto nos chocamos contra el límite establecido por el servidor web… seguramente lo que le ocurrió a Horacio.

    ¿Cuál podría ser la solución?

    Cambiar la configuración del servidor web

    En algunos escenarios, especialmente si se tiene la capacidad y posibilidad de alterar la configuración del servidor web, se podría relajar un poco el criterio para permitir operaciones HTTP más pesadas.

    Esta solución no es la óptima principalmente por dos motivos:

    1. No soluciona el problema de la eficiencia
    2. Siempre podrá aparecer un archivo lo suficientemente grande como para superar los nuevos límites y se estaría nuevamente en la situación inicial

    ¿REST o SOAP?

    Respecto de la pregunta de si conviene más usar REST o SOAP diría que da lo mismo.

    El problema es usar HTTP para realizar el envío y, tanto REST como SOAP depende de él.

    La solución pasa por desligar el envío del archivo respecto del aviso a la contraparte.

    Más en concreto, se trata de dividir el problema en tres partes:

    1. Dejar el archivo disponible para que la contraparte lo acceda
    2. Informar a la contraparte dónde está alojado tal archivo para que pueda accederlo
    3. Desde el receptor del mensaje realizar la descarga

    Sólo el paso 2 debería depender de un servicio web.

    Los pasos 1 y 3 pueden realizarse usando diversas opciones de almacenamiento de archivos binarios. Lo importante es que luego pueda realizarse la descarga sin usar HTTP.

    Un ejemplo usando FTP

    En esta sección se puede ver un ejemplo de solución usando un servidor FTP.

    client.php

    <?php
    
    if (!($remoteServer = ftp_connect(getenv('FTP_HOST')))) {
        die('Not connected :(' . PHP_EOL);
    }
    echo 'Connected!' . PHP_EOL;
    if (!ftp_login($remoteServer, getenv('FTP_USER'), getenv('FTP_PWD'))) {
        die('Wrong credentials' . PHP_EOL);
    }
    echo 'Login succesful!' . PHP_EOL;
    $remoteFullPath = getenv('REMOTE_BASE_PATH') . '/' . basename($argv[1]);
    if (!ftp_put($remoteServer, $remoteFullPath, $argv[1], FTP_BINARY)) {
        die('Upload failed');
    }
    
    echo 'File ' . $argv[1] . ' uploaded!'.PHP_EOL;
    
    ftp_close($remoteServer);
    
    $ch = curl_init(getenv('SERVER_URL') . '/upload');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => [
            'path' => $remoteFullPath
        ],
        CURLOPT_RETURNTRANSFER => true,
    ]);
    
    $response = curl_exec($ch);
    if (200 !== curl_getinfo($ch, CURLINFO_RESPONSE_CODE)) {
        die('Webservice call failed: ' . curl_error($ch));
    }
    curl_close($ch);
    echo 'Message sent!' . PHP_EOL;

    server.php:

    <?php
    
    error_log('Request received!' . PHP_EOL);
    if ('post' === strtolower($_SERVER['REQUEST_METHOD'])) {
        $remotePath = $_POST['path'];
        error_log('Downloading ' . $remotePath . PHP_EOL);
        if (!($remoteServer = ftp_connect(getenv('FTP_HOST')))) {
            http_response_code(500);
            trigger_error('Not connected :(' . PHP_EOL);
            die;
        }
        trigger_error('Connected!' . PHP_EOL);
        if (!ftp_login($remoteServer, getenv('FTP_USER'), getenv('FTP_PWD'))) {
            http_response_code(500);
            trigger_error('Wrong credentials' . PHP_EOL);
            die;
        }
        trigger_error('Login succesful!' . PHP_EOL);
        if (!ftp_get($remoteServer, __DIR__ . '/' . basename($remotePath), $remotePath)) {
            trigger_error('Download of file '.$remotePath.' failed' . PHP_EOL);
            http_response_code(500);
            die;
        }
        trigger_error('Sucessuflly downloaded '.$remotePath.PHP_EOL);
    }

    Notas:

    1. Para hacer más genérica la solución todos los datos específicos están guardardos en variables de entorno.
    2. En este caso el cliente se bloquea hasta que el servidor realice la descarga. En un caso más realista esto no debería ser así. El servidor debería guardar los datos de la descarga y emitir un acuse de recibo para que el cliente pueda continuar y eventualmente realizar la descarga.
  • Cómo ejecutar varias versiones de PHP en mismo NginX

    Cómo ejecutar varias versiones de PHP en mismo NginX

    Trabajando con un cliente me encontré ante este escenario:

    En un mismo servidor (Una instancia de AWS) había una aplicación desarrollada usando CodeIgniter y otra basada en Symfony.

    Al momento de comenzar mi intervención ambas aplicaciones compartían un mismo servidor web (NginX) y el binario de php para ejecutar algunos scripts de línea de comandos.

    El problema era que la versión de CodeIgniter en la que se había realizado el desarrollo (2.2.6) no podía actualizarse a la más reciente (4.1.5) sin re-escribir casi por completo la aplicación.

    Mientras tanto, la aplicación Symfony fue siguiendo la evolución del framework bastante de cerca.

    Para continuar el mantenimiento de la aplicación Symfony, y aprovechar nuevas caracterísitcas de rendimiento y seguridad, se hacía conveniente actualizar la versión de PHP y, difícilmente este cambio sería soportado por la aplicación vieja.

    La mejor solución sería darle a cada aplicación su propio entorno de ejecución de modo de que ninguna se vuelva un cuello de botella para la otra.

    La decisión fue mantener ambas versiones del lenguaje disponibles y que cada sitio utilizara la que le correspondiera (Al menos hasta definir qué hacer con la aplicación CodeIgniter).

    Pensé en pasar ambas a Docker y no descarto hacerlo más adelante pero, por el momento, me pareció demasiado esfuerzo para poco beneficio.

    Para tener dos versiones diferentes de PHP ejecutando en un mismo servidor es necesario:

    1. Instalar la nueva versión
    2. Modificar la configuración del WebServer
    3. Modificar la configuración de los cronjobs

    Cómo instalar una nueva versión de PHP

    El servidor en cuestión es un Ubuntu, de modo que instalar una nueva versión de PHP (8.0 en mi caso) requiere estos comandos:

    sudo add-apt-repository ppa:ondrej/php
    sudo apt install php8.0

    Con esto queda instalado el binario de php en su versión 8, luego hay que tener en cuenta instalar las extensiones que sean necesarias.

    Una vez finalizado el proceso, con este comando:

    update-alternatives --display php

    Vemos que se ha instalado la nueva versión:

    php - manual mode
      link best version is /usr/bin/php8.0
      link currently points to /usr/bin/php7.3
      link php is /usr/bin/php
      slave php.1.gz is /usr/share/man/man1/php.1.gz
    /usr/bin/php7.3 - priority 73
      slave php.1.gz: /usr/share/man/man1/php7.3.1.gz
    /usr/bin/php8.0 - priority 80
      slave php.1.gz: /usr/share/man/man1/php8.0.1.gz

    Notá como el comando php es un link simbólico que está alojado en /usr/bin/php y apunta a /usr/bin/php7.3. Es por eso que php -v muestra:

    PHP 7.3.19-1+ubuntu18.04.1+deb.sury.org+1 (cli) (built: Jun 12 2020 07:48:30) ( NTS )
    Copyright (c) 1997-2018 The PHP Group
    Zend Engine v3.3.19, Copyright (c) 1998-2018 Zend Technologies
        with Zend OPcache v7.3.19-1+ubuntu18.04.1+deb.sury.org+1, Copyright (c) 1999-2018, by Zend Technologies

    Es decir, si queremos asegurarnos de estar ejecutando la versión 8.0 el comando que nos da más certeza es /usr/bin/php8.0 -v:

    PHP 8.0.13 (cli) (built: Nov 22 2021 09:50:24) ( NTS )
    Copyright (c) The PHP Group
    Zend Engine v4.0.13, Copyright (c) Zend Technologies
        with Zend OPcache v8.0.13, Copyright (c), by Zend Technologies 

    Perfecto. Tengamos esto en cuenta para dentro de un rato cuando veamos lo que pasa con los cronjobs.

    Cómo especificar la versión de PHP para cada sitio

    Para este punto viene muy bien el hecho de estar usando NginX como servidor web. A diferencia de Apache que puede tener PHP incorporado, NginX se limita a servir contenido estático y delega la ejecución de PHP en otro proceso (php-fpm).

    Si miramos el archivo de configuración de cada sitio veremos algo como:

    server {
        ...
        location ~ ^/index\.php(/|$) {
            fastcgi_split_path_info ^(.+\.php)(/.*)$;
            fastcgi_pass unix:/var/run/php/php7.3-fpm.sock;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    
            fastcgi_intercept_errors off;
            fastcgi_buffer_size 16k;
            fastcgi_buffers 4 16k;
        }
    
        ...
    }

    En particular la línea que nos interesa en este caso es:

    fastcgi_pass unix:/var/run/php/php7.3-fpm.sock;

    Aquí se está definiendo el canal de comunicación entre el servidor web y el proceso que atenderá las peticiones php.

    De modo que lo que debemos cambiar es el 7.3 por 8.0 en el archivo que corresponde a la aplicación más nueva.

    Claro que, para que esto funcione debemos contar con php8.0-fpm.

    Para instalarlo puede usarse el comando:

    sudo apt install php8.0-fpm

    Y con eso ya contaremos con el proceso FastCGI que necesitamos.

    Por supuesto que, antes de aplicar estos cambios en producción es prudente realizar pruebas en un ambiente controlado. Asumiendo que ya se realizaron dichas pruebas y se corrigieron eventuales inconvenientes, con los cambios realizados la aplicación web está lista.

    Resta ver qué hacer con las tareas programadas y/o scripts de línea de comandos que se utilicen.

    En este caso lo que hay son algunas tareas programadas vía cron.

    Cómo especificar la versión de PHP para cada cronjob

    Usando el comando crontab -l obtenemos algo como:

    # Edit this file to introduce tasks to be run by cron.
    # 
    # Each task to run has to be defined through a single line
    # indicating with different fields when the task will be run
    # and what command to run for the task
    # 
    # To define the time you can provide concrete values for
    # minute (m), hour (h), day of month (dom), month (mon),
    # and day of week (dow) or use '*' in these fields (for 'any').# 
    # Notice that tasks will be started based on the cron's system
    # daemon's notion of time and timezones.
    # 
    # Output of the crontab jobs (including errors) is sent through
    # email to the user the crontab file belongs to (unless redirected).
    # 
    # For example, you can run a backup of all your user accounts
    # at 5 a.m every week with:
    # 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
    # 
    # For more information see the manual pages of crontab(5) and cron(8)
    # 
    # m h  dom mon dow   command
    
    MAILTO=mauro.chojrin@leewayweb.com
    0 6 1 2,5,8,11 * /usr/bin/php /home/ubuntu/followapp/bin/console app:agents:send-transaction-summary

    Esto significa que todos los 1 de los meses de Febrero, Mayo, Agosto y Noviembre a las 6:00 AM se ejecuta el script PHP /home/ubuntu/followapp/bin/console pasándole como argumentos app:agents:send-transaction-summary.

    El script es el que Symfony dispone para la ejecución de comandos diseñados utilizando el framework.

    El argumento especifica cuál de todos los comandos disponibles es el que se desea ejecutar (En este caso se trata de uno diseñado a efectos de enviar a los vendedores un resumen de las transacciones realizadas durante el trimestre anterior).

    Y la parte de la línea que dice /usr/bin/php es la que especifica cuál será el binario que deberá invocarse cuando sea el momento indicado. En este caso se trata del binario de PHP.

    Claro que, en este servidor donde hay más de una versión de PHP, esta definición puede resultar ambigua (A los ojos humanos claro, para la computadora está muy claro que /usr/bin/php es un alias de /usr/bin/php7.3).

    Lo conveniente para evitar confusiones es hacer explícita la versión del intérprete que se requiere, dejando la línea del crontab de esta forma:

    0 6 1 2,5,8,11 * /usr/bin/php /home/ubuntu/followapp/bin/console app:agents:send-transaction-summary

    Y ahora sí, con este último cambio está todo listo para tener ambas versiones conviviendo en armonía.

    Unas últimas notas

    Como habrás podido apreciar, no es realmente complejo lograr esta configuración, sin embargo, siendo que el servidor ya se encuentra virtualizado, lo más conveniente sería tener una instancia separada para cada aplicación.

    La desventaja de este enfoque es que, además de los costos propios de la infraestructura, la administración puede hacerse un poco más compleja.

    Lo que debe evaluarse para decidir es el riesgo de que una aplicación falle y, al competir por los recursos con la otra, deje fuera de línea un sitio que, en principio, podría continuar operativo.

  • Cómo hacer un join en Doctrine

    Cómo hacer un join en Doctrine

    Parece algo bien simple, ¿no? Después de todo, hacerlo en SQL lo es. Pero si estás usando Doctrine, lo correcto es usar las capacidades del ORM.

    Recientemente tenía que resolver un problema de un cliente: se le habían duplicado registros en la base y era necesario eliminarlos.

    El punto es que no se tenían que eliminar todos los registros, si no sólo los problemáticos.

    El modelo de datos es algo así:

    Lo que yo necesitaba hacer era eliminar aquellas transacciones que, entre otros criterios de filtro, pertenecieran a un proveedor en particular.

    La forma en que lo hice fue mediante una consulta que busque todos los objetos necesarios y luego los elimine:

    $qb = $this->em
                ->createQueryBuilder()
                ->select('t')
                ->from(Transaction::class, 't')
                ->where('t.date BETWEEN :f AND :t')
                ->setParameter('f', $fromDate)
                ->setParameter('t', $toDate)
                ->orderBy('t.date')
            ;

    Hasta aquí se arma la consulta genérica, sólo incluyendo los filtros obligatorios (Las fechas básicamente).

    Luego, si se desea usar el filtro por proveedor tenemos:

    $qb->innerJoin(Account::class, 'a')
       ->andWhere('a.provider = :p')
       ->setParameter('p', $provider);

    Todo normal, ¿cierto?

    Pues aquí arrancaron los problemas.

    Cuando fuí a verificar en la pantalla de la aplicación cuántos registros correspondían al rango de fecha y al proveedor encontré unos 3000, sin embargo, el script me estaba diciendo que iba a eliminar unas 100k transacciones.

    De modo que decidí agregarle al script una opción para ver el SQL que estaba a punto de ejecutar y esto es lo que encontré:

    SELECT t0_.id AS id_1, t0_.amount AS amount_5, t0_.account_id AS account_id_10 FROM transaction t0_ INNER JOIN account a1_ WHERE (t0_.date BETWEEN ? AND ?) AND a1_.provider_id = ? ORDER BY t0_.date ASC

    Nuevamente me quedé un rato mirando el SQL. Todo se veía bien.

    ¿Qué podía estar pasando?

    Bueno… la verdad es que no estaba tan bien el SQL. Si le prestás un poco de atención notarás que al INNER JOIN le falta un detalle: la cláusula ON.

    El SQL que debería haberse generado debía ser más parecido a:

    SELECT t0_.id AS id_1, t0_.amount AS amount_5, t0_.account_id AS account_id_10 FROM transaction t0_ INNER JOIN account a1_ ON a1_.id = t0.account_id WHERE (t0_.date BETWEEN ? AND ?) AND a1_.provider_id = ? ORDER BY t0_.date ASC

    Sí. Ese simple detalle estaba haciendo una diferencia fundamental.

    Sin la cláusula ON el INNER JOIN se convierte en el producto cartesiano de las dos tablas, con lo cual se pierde totalmente el sentido de usar un JOIN y el resultado tiene muy poco sentido

    Perfecto, el problema está identificado. ¿Cómo lo solucionamos?

    Como de costumbre, se trata de volver a las fuentes. La documentación de Doctrine es bastante clara al respecto.

    Se puede hacer algo como:

    $qb->innerJoin(Account::class, 'a', Join::ON, 't.account_id = a.id')
       ->andWhere('a.provider = :p')
       ->setParameter('p', $provider);

    Y el resultado será correcto, pero si lo dejamos así estamos haciendo trabajo extra… y ya que tenemos un ORM, ¿por qué hacerlo?

    Siendo t el alias de la clase Transaction y estando la relación definida como parte del mapeo objeto-relacional una mejor versión es esta:

    $qb->innerJoin('t.a', 'a')
       ->andWhere('a.provider = :p')
       ->setParameter('p', $provider);

    En conclusión SQL y DQL son parecidos pero no tanto. Vale la pena conocer las diferencias y usar lo mejor de cada uno en cada ocasión.

  • Cómo validar que un correo existe usando PHP

    Cómo validar que un correo existe usando PHP

    Seguramente alguna vez te habrás topado con la necesidad de registrar correos electrónicos de los visitantes de tu sitio, ¿cierto?

    Usualmente esto se hace para mantenerlos al tanto de las novedades.

    Claro que, aunque vos tengas las mejores intenciones, es entendible que la gente sea un poco escéptica con tanto spammer dando vueltas, por lo tanto, no es raro que muchos llenen el campo correo electrónico con basura, lo cual no sirve más que para hacer crecer tu base de datos inútilmente.

    ¿Qué podés hacer para evitar esto?

    Hay varias medidas que se pueden tomar, te voy a comentar algunas:

    Cómo validar que el texto sea una dirección de correo electrónico

    Lo primero de lo que deberías asegurarte es de que el texto ingresado por el usuario es (o al menos podría ser) una dirección de correo electrónico.

    Para hacer esto tenés dos opciones: hacerlo desde el frontend o desde el backend. Si tenés dudas de cuál te conviene te recomiendo este artículo.

    Me voy a concentrar en la validación del lado del servidor que es la que efectivamente se hará usando PHP.

    Asumamos que se recibe un POST que incluye un campo email y necesitamos verificar su contenido.

    Como de costumbre, existen muchas formas de hacerlo pero ciertamente hay una que supera a las demás: usar las herramientas incorporadas.

    Concretamente me refiero a la función filter_var, se puede usar de esta forma:

    <?php
    
    echo (filter_var($argv[1], FILTER_VALIDATE_EMAIL) ? 'Es email' : 'No es email').PHP_EOL;
    

    Corriendo este script de esta forma:

    php test.php mauro.chojrin@leewayweb.com

    Obtendrás esta salida:

    Es email

    Mientras que si lo ejecutas así:

    php test.php mauro.chojrin.leewayweb.com

    La salida será:

    No es email

    Hasta aquí pinta bien, ¿cierto? Pero… ¿es realmente una dirección de correo? Es decir, ¿existe una casilla de correo que responda a esa dirección?

    Cómo validar que la dirección ingresada exista

    Como decía, que el texto ingresado cumpla con el formato de una dirección de correo es condición necesaria pero no suficiente para nuestros propósitos.

    Necesitamos algo más sólido.

    Algo que podemos usar es una validación a nivel de SMTP, es decir, intentar enviar un correo y ver qué responde el servidor que, supuestamente, debería recibirlo.

    Implementar esto desde 0 es, claramente, una tarea compleja.

    Mejor apoyarnos en alguna librería existente. Por ejemplo esta.

    La instalamos usando composer así:

    composer require zytzagoo/smtp-validate-email --update-no-dev

    Y luego podemos usar un código como este:

    <?php
    
    require 'vendor/autoload.php';
    
    use SMTPValidateEmail\Validator as SmtpEmailValidator;
    
    $validator = new SmtpEmailValidator($argv[1], 'sender@example.org');
    
    $results   = $validator->validate();
    
    echo $results[$argv[1]] ? 'El email existe' : 'El email no existe';
    
    echo PHP_EOL;

    Y al ejecutar esto:

    php test.php mauro.chojrin@leewayweb.com

    Nos confirmará que la dirección existe.

    Nos queda sólo una pequeña duda… ¿hay alguien leyendo esos emails?

    Cómo validar que alguien chequea esa casilla

    Esta parte es un poco más compleja, aunque definitvamente es lo mejor que se puede hacer.

    Un método muy conocido es el doble opt-in.

    Se trata de enviar un correo pidiendo la confirmación y sólo activar el registro cuando alguien haga click en el link que contiene dicho correo.

    Una forma de implementarlo es crear un campo adicional dentro de la tabla de usuarios que se llene con un token, el cual será enviado a la dirección introducida por el usuario dentro de un link similar a:

    http://tusitio.com/users/activate.php?token=$token

    Del lado de activate.php tenés que buscar un usuario cuyo token coincida con el parámetro recibido y, en tal caso, marcar el registro como activo.

    En este video podés ver un ejemplo de solución de un problema parecido (El recupero de contraseña).

    Recapitulando

    En este post hablamos de tres modos de validar que un correo recibido sea, efectivamente, un correo.

    Cada una de las formas es un poco más compleja (y certera) que la anterior.

    ¿Cuál deberías usar? Idealmente las tres.

    ¿Por qué las tres? Simplemente porque el aumento de complejidad también implica una menor eficiencia entonces si un dato debería ser descartado, lo mejor es hacerlo lo antes posible de modo de ahorrar recursos de tu servidor y tiempo de tus usuarios.

  • Cómo mostrar tu proyecto PHP sin usar un hosting

    Cómo mostrar tu proyecto PHP sin usar un hosting

    Listo. Terminado. Finito.

    Ah… qué placer, ¿no?

    Después de horas frente a la pantalla, incontables tazas de café y miles de bugs resueltos, por fin llegará el merecido descanso… sólo falta hacer la demo para el cliente.

    Es que si no se hace el cliente no podrá dar su visto bueno y sin él… difícil que haga el último pago :p

    Podrías hacer un despliegue completo en su servidor pero… ¿y si algo sale mal?

    O peor, ¿qué tal si sale todo bien y el código ya está fuera de tu control?

    ¿Cómo hacer para que otra persona vea tu trabajo sin entregarle el código?


    Existen varias opciones según cuál sea tu modo de trabajar.

    Para este artículo asumiré que desarrollas en un servidor local (XAMPP o similar).

    Usar un servidor de pruebas

    Una opción perfectamente válida es utilizar un servidor intermedio en el cual puedas desplegar tu código.

    Idealmente este servidor será tuyo, con lo cual no tendrás que entregar tu código en forma prematura.

    La principal desventaja de este método es que puede tener un costo asociado: el del hosting.

    Por otra parte, preparar este espacio puede no ser una tarea trivial y hacerlo cada vez que tengas que realizar una demostración puede ser una pérdida de tiempo significativa.

    Abrir temporalmente el acceso a localhost

    Otra opción que puede resultar mucho más económica y sencilla es abrir temporalmente el acceso a tu computadora a través de internet.

    Existen diversas herramientas que puedes utilizar para asociar un nombre de dominio a tu dirección IP, de modo de evitarle a tu cliente tener que tipearla en su navegador.

    Un ejemplo de ese tipo de solución es No-IP.

    No es una mala opción, aunque puede ser un poco compleja de administrar (amén de requerir la instalación y ejecución constante de un cliente).

    Otra posiblidad bastante más atractiva es utilizar ngrok.

    Se trata de una utilidad muy sencilla que te permite levantar un túnel hacia tu computadora y pasarle a tu cliente una URL a la cual conectarse.

    Basta con ejecutar un comando como ngrok http 80 para obtener una pantalla como esta:

    Información de ngrok ejecutándose

    Aquí puedes ver claramente cómo la dirección http://7da0fd4f278a.ngrok.io apunta al puerto 80 en mi computadora local.

    Sólo se necesita ingresar a http://7da0fd4f278a.ngrok.io para ver lo mismo que yo veo usando http://localhost.

    No está mal, ¿cierto?

    Nada de FTP, nada de crear servidores temporales ni cosas parecidas.

    ¿Lo más interesante? Al terminar la prueba sólo se necesita dar Ctrl+C y asunto finalizado.

    Terminó la demo y terminó el acceso remoto.

    Simple, rápido y económico (o BBB si lo prefieres :))

    Adelante, la próxima demo que vayas a hacer no te compliques, crea ahora mismo una cuenta en ngrok, configura el cliente y olvídate del problema de los despliegues temporales

  • Guía para solucionar problemas de DataTables con Ajax y PHP

    Guía para solucionar problemas de DataTables con Ajax y PHP

    Usá DataTables te dijeron.

    «Es el mejor plugin que vas a encontrar para mostrar datos en forma resumida»

    «Es súper fácil de usar»

    Si… claro.

    Armar la tabla no es muy complicado, es cierto, pero cuando le empezabas a encontrar el gustito… las cosas se complicaron.

    ¿Y ahora?

    ¿¿Qué pasa que no se cargan los datos??

    Estarás pensando «¿Por qué no me quedé con las viejas tablas HTML que andaban sin problemas?»

    Pero en el fondo sabés por qué.

    Querés un sitio moderno.

    Querés que los visitantes tengan una buena experiencia.

    Querés que el cliente te vuelva a contratar y le cuente a sus amigos.

    Así que más vale hacer un esfuercito más.


    Lo primero que hay que tener claro es que no se puede resolver un problema sin primero diagnosticarlo.

    Es por eso que en este artículo voy a hablar sobre los problemas más frecuentes que te podés encontrar al usar DataTables.

    Aclaración: Aquí hablaré exclusivamente de problemas que impiden que el DataTable funcione. Si tenés un problema de performance mirá esto.

    Andá, buscate un café, yo te espero.

    ¿Listo?

    Bien, arranquemos.

    Partes que hacen al DataTable

    El DataTable se compone de tres elementos:

    • HTML
    • JavaScript
    • PHP

    El HTML del DataTable

    La primera pregunta que cabe es ¿El HTML está correctamente escrito?

    Doy por descontado que los tags <table> y </table> están en su lugar.

    Luego, ¿está puesto el <thead>? ¿Y el </thead>?

    Ok, y dentro del <thead>, ¿hay algún <tr>? y dentro del <tr>, ¿hay elementos <th>?

    En resumen, ¿tu table se ve parecido a este?

    <table id="theTable" class="display" style="width: 100%">
        <thead>
            <tr>
                <th>Id</th>
                <th>Name</th>
                <th>Price</th>
            </tr>
        </thead>
    </table>

    ¿Notaste que no hay <tbody>?

    No, no es un error.

    El <tbody> lo va a armar el JavaScript, ya vamos a llegar.

    El JavaScript del DataTable

    El segundo componente es el código JavaScript que le dará vida al DataTable.

    Lo principal (Donde yo mismo siempre me equivoco):

    ¿Está puesta la referencia a jQuery?

    Es decir, ¿hay en tu código algo como <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>?

    ¿Por qué se necesita esto? Porque jQuery es una dependencia de DataTables (Recordá que DataTables es simplemente un plugin de jQuery).

    A renglón seguido viene: ¿Está puesta la referencia al script de DataTables?

    O, en criollo, ¿hay en tu código algo como <script src="https://cdn.datatables.net/1.10.24/js/jquery.dataTables.min.js"></script>?

    ¿Sí? ¡Perfecto!

    Entonces, lo que queda es saber si la configuración del DataTable es correcta.

    En principio lo único imprescindible en ese escenario es la llamada Ajax, algo como:

    <script type="application/javascript">
        $(document).ready( function () {
            $('#theTable').DataTable({
                ajax: '/get_data.php',
            });
        } );
    </script>

    ¿Qué puede andar mal aquí? Bueno… aparte de errores de sintaxis que siempre puede haber, el id de la tabla podría no coincidir en la función y en el HTML.

    En este caso theTable está definido en la llamada a la función $ (En $('#theTable')) y en la definición de la tabla (<table id="theTable"...>).

    Otro tema conflictivo puede ser la URL a la que debe realizarse la llamada Ajax.

    Si todo esto está bien podrías sospechar de que la llamada Ajax no se está comportando como es debido (Tal vez no está mandando todos los parámetros o encabezados que espera tu PHP).

    Es un caso bastante extremo y, honestamente, no creo que se te presente pero… si desconfiás te recomiendo recurrir a la consola del desarrollador de tu navegador (En la pestaña Network):

    Consola del desarrollador del navegador

    Si todo esto está ok sólo queda un sospechoso posible…

    El PHP del DataTable

    Y al fin llegamos a la parte más jugosa del artículo 🙂

    Aquí realmente no hay muchas reglas que seguir ya que todo depende de cómo obtengas la información que buscas mostrar en tu DataTable.

    Lo que sí es importante es que tengas en cuenta qué es lo que tu FrontEnd espera recibir y cómo espera recibirlo.

    Así que vamos a las preguntas:

    ¿Estás retornando json?

    Si tienes dudas puedes hacer un request directamente a la URL a donde apunta tu Ajax.

    En mi caso sería http://localhost:8000/get_data.php y la respuesta que obtendré será:

    {
      "data": [
        [
          "1",
          "Chair",
          "20.0"
        ],
        [
          "2",
          "Shoe",
          "1.0"
        ],
        [
          "3",
          "Candle",
          "0.75"
        ]
      ]
    } 

    La forma más fácil de generar algo como esto es guardar los datos en un arreglo y luego usar la función json_encode.

    Si estás recibiendo una página en blanco o algún error, probablemente te convenga debuggear tu php antes de continuar.

    Claro que esto es sólo el primer paso.

    Luego debes verificar que ese json tenga la estructura correcta.

    ¿Tiene una propiedad llamada data?

    Y dentro de dicha propiedad, ¿está el arreglo de registros que buscas mostrar?

    Si todavía tienes problemas, otras dos preguntas que pueden orientarte:

    1. ¿La cantidad de datos que contienen los registros coincide con las columnas definidas en el HTML?
    2. ¿El orden de los elementos dentro del arreglo coincide con el de las columnas definidas en el HTML?

    Recapitulando

    DataTables es ciertamente un aliado poderoso, pero también es un cúmulo de partes móviles que deben estar coordinadas cual reloj suizo para que todo salga bien.

    Guárdate este artículo en tus favoritos para tenerlo siempre a mano si te vuelves a encontrar desorientado sobre cómo solucionar tus problemas con un DataTable.

    Simplemente recorre la lista de preguntas y rápidamente tendrás la clave que te ayudará a seguir adelante.

  • Cómo exportar una tabla de MySQL a Excel usando PDO

    Cómo exportar una tabla de MySQL a Excel usando PDO

    Seguro que te ha pasado algo como esto: creaste una aplicación con su base de datos, con unas funcionalidades espectaculares, fantásticos reportes y al momento de la demo… la pregunta tan temida:

    «¿Cómo puedo hacer para llevar toda esta información a Excel?»

    Y en tu cabeza suena:

    «¿A Excel? ¿En serio? Pero si se puede hacer todo mucho mejor con esta aplicación… ¿para qué querrías usar una planilla de cálculo?»

    Creeme, no estás solo, a todos nos ha pasado.

    La triste realidad es que difícilmente vayas a ganar la batalla… más vale amigarte con el Excel (y ganar algo de dinero mientras tanto, ¿no?).

    Una de las razones más comunes que se escuchan por ahí es que a través de Excel es fácil compartir la información con otros sistemas (Personalmente elegiría una integración basada en webservices, pero… el cliente manda).

    Otra razón que, aunque a veces duela, la veo más entendible es la facilidad para analizar datos que tiene Excel (La posibilidad de aplicar filtros, sumatorias y demás).

    Pero como este no es un blog de Excel si no de Programación, pasemos a lo nuestro.

    En este artículo te voy a mostrar los pasos que tendrás que dar para generar archivos que puedan ser abiertos con Excel a partir de tu base de datos MySQL.

    Encontrarás ejemplos de código que, con pocas modificaciones, podrás adaptar a tu escenario particular.

    ¡Acompañame!

    Conectarse a MySQL usando PDO

    Lo primero que necesitarás para realizar esta exportación es conectarte a tu base de datos.

    Siendo que se trata de MySQL puedes usar la librería específica (mysqli) o la genérica (PDO).

    Personalmente, prefiero ir por PDO porque si eventualmente necesito cambiar de motor de BD es muy fácil hacerlo.

    Todo comienza por la creación de un objeto:

    <?php
    
    $db = new PDO('mysql:host=localhost;dbname=mydb', 'root', '');

    Con este código estoy intentando hacer una conexión a una base de datos MySQL instalada en la misma computadora donde se está corriendo el script.

    Digo intentando porque, como siempre, algo podría salir mal… para este ejemplo voy a dejarlo así, pero lo correcto sería incluir este código dentro de un bloque try…catch.

    (Si no tenés muy claros los conceptos de objetos, excepciones y/o PDO este libro puede ayudarte 😉

    Una vez realizada la conexión debemos hacer alguna consulta que nos permita obtener aquellos registros que queremos exportar.

    Realizar una consulta usando PDO

    Existen varios modos de realizar la consulta mediante PDO, pero hay algo de lo que no podrás escapar: escribir el SQL correspondiente.

    Usualmente lo que yo hago es algo como:

    $sql = "SELECT * FROM table WHERE field > 10";
    
    $results = $pdo->query($sql, PDO::FETCH_NUM);

    Nuevamente, en un caso real habría que verificar si la consulta se ejecutó o hubo algún error antes de avanzar… para no hacer un post kilométrico lo dejo aquí.

    Para darle algo de formato al resultado que vamos a generar vamos a necesitar los nombres de los campos (que, si usamos un SELECT * no siempre serán los mismos!)

    Obtener los nombres de los campos usando PDO

    $columns = [];
    
    for ($i = 0; $i < $results->columnCount(); $i++) {
        $columns[] = $results->getColumnMeta($i)['name'];
    }

    Una vez obtenidos los registros y los nombres de las columnas a exportar podemos comenzar a generar la respuesta.

    Y aquí tenemos varias opciones.

    Independientemente de cuál sea la que elijas para generar el resultado, seguramente quieras forzar la descarga del mismo, veamos cómo lograrlo.

    Forzar la descarga de un archivo

    Para obligar al navegador a guardar el resultado devuelto por el servidor en un archivo en el disco local del visitante debe usarse la función header:

    header("Content-Type:$contentType"); 
    header("Content-Disposition:attachment;filename=output.$outputFileExtension"); 

    A continuación te mostraré tres opciones diferentes para generar la respuesta.

    Las variables $contentType y $outputFileExtension dependerán de cuál de las opciones disponibles quieras utilizar.

    Generar un archivo CSV y abrirlo en Excel

    Esta es probablemente la manera más sencilla de exportar los resultados, usando un archivo de valores separados por comas (CSV).

    header("Content-Type:application/csv"); 
    header("Content-Disposition:attachment;filename=output.csv"); 
    
    $outputFile = fopen('php://output', 'w+'));
    
    fputcsv($outputFile, $columns);
    foreach ($results as $result) {
            fputcsv($outputFile, $result);
    }

    Este código permitirá que el usuario reciba un archivo separado por comas que podrá ser abierto usando Excel y eventualmente guardado en formato nativo (xls, xlsx, etc…).

    Generar un archivo HTML y abrirlo en Excel

    Una característica interesante del Excel (Y algunas otras aplicaciones tipo planilla de cálculo modernas) es que pueden interpretar archivos HTML que tengan estructuras de tablas y mostrarlos como planillas.

    En este caso el código de la generación de la salida sería algo como:

    header("Content-Type:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); 
    header("Content-Disposition:attachment;filename=output.xls"); 
    
    $outputFile = fopen('php://output', 'w+'));
    
    ?>
    <table>
            <thead>
                    <tr>
                    <?php foreach($columns as $column): ?>
                            <th><?php echo $column; ?></th>
                    <?php endforeach; ?>
                    </tr>
            </thead>
            <tbody>
                    <?php foreach( $results as $result ): ?>
                    <tr>
                            <?php foreach ($result as $value): ?>
                            <td><?php echo $value; ?></td>
                            <?php endforeach; ?>
                    </tr>
                    <?php endforeach; ?>
            </tbody>
    </table>

    Generar un archivo Excel usando PHP

    Si bien estas dos opciones pueden resultar aceptables en una gran cantidad de casos, existen otros en los que realmente se necesita exportar a un formato específico, como XLSX.

    Estos archivos se basan en XML (es decir, en texto) con lo cual podrías generarlos usando simples strings (O a lo sumo valiéndote de SimpleXMLElement) pero… ¿para qué reinventar la rueda no?

    Hay una librería realmente buena que podés usar: PhpSpreadsheet.

    Esta librería te permite tanto escribir como leer archivos con formato específico para muchas planillas de cálculo (Excel, LibreOffice, etc…).

    Una característica que la hace sumamente atractiva es que presenta una API orientada a objetos realmente simple de usar:

    $spreadsheet = new Spreadsheet();
    
    $activeSheet = $spreadsheet->getActiveSheet();
    
    foreach($columns as $i => $column) {
            $activeSheet->setCellValueByColumnAndRow($i + 1, 1, $column);
    }
    
    foreach($results as $result) {
            $activeSheet->insertNewRowBefore($activeSheet->getHighestRow() + 1);
            foreach ($result as $k => $field) {
                    $activeSheet->setCellValueByColumnAndRow($k + 1, $activeSheet->getHighestRow(), $field);
            }
    }
    
    $writer = IOFactory::createWriter($spreadsheet, 'Xls');
    $writer->save('php://output');

    Un detalle importante: para usar esta librería hay que instalarla primero (Preferentemente usando composer).

    Qué se puede hacer con Excel usando PHP

    Así como es posible exportar información hacia un archivo Excel es perfectamente posible importar información contenida en un archivo Excel hacia una base de datos MySQL o manipular un archivo existente.

    La librería PhpSpreadsheet permite realizar prácticamente cualquier operación que Excel soporte, todo lo que se requiere es leer un poco de documentación 😉

  • Cómo saber cuáles son los procedimientos que tiene un WebService SOAP usando PHP

    Cómo saber cuáles son los procedimientos que tiene un WebService SOAP usando PHP

    Consumir Servicios Web basados en SOAP es una tarea muy común estos días, especialmente cuando se trata de integrar con entidades gubernamentales (Típico caso es la facturación electrónica).

    Una de las características interesantes que tiene este protocolo (SOAP) es que están definidas en forma explícita las operaciones disponibles a través de un archivo de descripción de WebService (WSDL).

    La desventaja es que, salvo que conozcas bien la especificación, leer un archivo como este puede ser algo complicado:

    <?xml version = "1.0" encoding = "utf-8"?>
    <definitions name="WS_EmissionFactura" targetNamespace="Gx" xmlns:wsdlns="Gx" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="Gx">
    	<types>
    		<schema targetNamespace="Gx" xmlns="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" elementFormDefault="qualified">
    			<element name="WS_EmissionFactura.Execute">
    				<complexType>
    					<sequence>
    						<element minOccurs="1" maxOccurs="1" name="Xmlrecepcao" type="xsd:string" />
    					</sequence>
    				</complexType>
    			</element>
    			<element name="WS_EmissionFactura.ExecuteResponse">
    				<complexType>
    					<sequence>
    						<element minOccurs="1" maxOccurs="1" name="Xmlretorno" type="xsd:string" />
    					</sequence>
    				</complexType>
    			</element>
    		</schema>
    	</types>
    	<message name="WS_EmissionFactura.ExecuteSoapIn">
    		<part name="parameters" element="tns:WS_EmissionFactura.Execute" />
    	</message>
    	<message name="WS_EmissionFactura.ExecuteSoapOut">
    		<part name="parameters" element="tns:WS_EmissionFactura.ExecuteResponse" />
    	</message>
    	<portType name="WS_EmissionFacturaSoapPort">
    		<operation name="Execute">
    			<input message="wsdlns:WS_EmissionFactura.ExecuteSoapIn" />
    			<output message="wsdlns:WS_EmissionFactura.ExecuteSoapOut" />
    		</operation>
    	</portType>
    	<binding name="WS_EmissionFacturaSoapBinding" type="wsdlns:WS_EmissionFacturaSoapPort">
    		<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" />
    		<operation name="Execute">
    			<soap:operation soapAction="Gxaction/AWS_EMISSIONFACTURA.Execute" />
    			<input>
    				<soap:body use="literal" />
    			</input>
    			<output>
    				<soap:body use="literal" />
    			</output>
    		</operation>
    	</binding>
    	<service name="WS_EmissionFactura">
    		<port name="WS_EmissionFacturaSoapPort" binding="wsdlns:WS_EmissionFacturaSoapBinding">
    			<soap:address location="https://appuypruebas.migrate.info/InvoiCy/aws_emissionfactura.aspx" />
    		</port>
    	</service>
    </definitions>

    Un modo mucho más simple y conveniente es utilizar un método propio de la clase SoapClient (La que PHP pone a tu disposición para interactuar con esta clase de servicios web): __getFunctions.

    Un ejemplo de obtención de listado de funciones provistas por un WebService SOAP

    Para este ejemplo se necesita tener instalado el soporte de SOAP para PHP.

    Los comandos que te voy a mostrar a continuación pueden ejecutarse sin problemas en Ubuntu (Si usás otro sistema operativo puede que tengas que adaptar un poco los ejemplos).

    Asumiré que tenés acceso a una terminal (Si es tu máquina local no debería haber problema, si es un servidor remoto necesitarás algún tipo de acceso como ssh y si tu proveedor no te lo da te sugiero evaluar usar un VPS).

    Lo primero será verificar que tengas instalado el soporte para SOAP en la computadora donde vas a correr el script.

    Para ello podés usar el comando:

    php -i | grep soap

    Y la salida debería ser similar a :

    /etc/php/7.4/cli/conf.d/20-soap.ini,
    soap
    soap.wsdl_cache => 1 => 1
    soap.wsdl_cache_dir => /tmp => /tmp
    soap.wsdl_cache_enabled => 1 => 1
    soap.wsdl_cache_limit => 5 => 5
    soap.wsdl_cache_ttl => 86400 => 86400

    En caso contrario, deberás instalarlo. Este comando te ayudará:

    sudo apt install php-soap

    Ahora entonces sí, pasemos a ver un poco de PHP 🙂

    <?php
    
    $ws = new SoapClient($argv[1]);
    print_r($ws->__getFunctions());

    Guardá este código en un archivo llamado get_ws_functions.php y luego tenés que ejecutarlo de esta forma:

    php get_ws_functions.php URL_DEL_WSDL

    Por ejemplo, si ejecutás

    php get_ws_functions.php "https://appuypruebas.migrate.info/InvoiCy/aws_emissionfactura.aspx?wsdl"

    Obtendrás:

    Array
    (
        [0] => WS_EmissionFactura.ExecuteResponse Execute(WS_EmissionFactura.Execute $parameters)
    )

    Ahora te toca cambiar la URL por el WebService al que te querés conectar y listo, tenés ahí el listado de métodos que podés invocar.