Blog

  • Cómo determinar la versión de PHP de un sistema

    Cómo determinar la versión de PHP de un sistema

    Una de las primeras tareas que debemos encarar cuando realizamos una auditoría de código de un sistema es detectar la versión de PHP que se está utilizando.

    Es importante saberlo para darnos una idea de qué tanto mantenimiento se ha realizado sobre el código y hasta cuánto se puede mejorar apalcándonos en las últimas características disponibles (O qué tan costoso será incorporarlas debido a refactors y demás).

    Hay diversas formas de encarar esta tarea. Nombraré algunas:

    Qué nos dice el webserver

    Una forma muy sencilla es realizar una petición al servidor web y analizar los encabezados de la respuesta.

    Ejemplo:

    No hace falta ser muy perspicaz… si el webserver tiene instalada la versión 5.3.29… el código a lo sumo está desarrollado usando esa versión.

    Si bien no es una información 100% certera, nos da una buena primera aproximación a lo que nos estamos enfrentando.

    Esta técnica, sin embargo, tiene un par de inconvenientes:

    1. Depende de que el código esté publicado en algún webserver (Suposición bastante lógica si lo que estamos viendo es una aplicación real)
    2. Depende de que la configuración del webserver no sea muy segura (En general, es preferible no mostrar este tipo de información a entidades externas de propósitos poco claros…).

    Tal vez composer pueda ayudarnos

    Si tenemos la suerte de encontrarnos con un proyecto hecho usando composer tal vez encontremos la solución dentro del archivo composer.json:

    O composer.lock:

    A hurgar se ha dicho

    Pero si realmente queremos saber para qué versión se escribió el código… nada mejor que analizar el código mismo.

    ¿Qué es lo que estamos buscando? Evidencias de uso de construcciones que sólo se hicieron disponibles a partir de cierta versión.

    En el caso de PHP, la evolución que ha tenido el lenguaje en los últimos años hace bastante simple esta tarea.

    Por ejemplo, a partir de la versión 5.4 apareció una característica muy popular: la definición de arreglos en forma literal ( $a = [1, 2, 3] ).

    Basta con encontrar un punto del código donde se utilice una definición como esta para decir que el código está escrito para un intérprete versión mayor o igual que 5.4.

    De la misma forma en que se agregan características al lenguaje en forma constante, también se quitan aquellas que ya no se consideran seguras.

    Esto significa que, si encontramos usos de características que han quedado obsoletas en una determinada versión, el código debe estar escrito pensando en una anterior.

    Para saber qué es lo que debemos buscar basta con leer las guías de migración de versión que se encuentran en php.net (Por ejemplo, acá están las indicaciones para migrar de 4 a 5 y acá de 5.6 a 7.0).

    Por qué es importante

    Saber en qué versión se encuentra un sistema nos permitirá, por ejemplo, evaluar un hosting antes de contratarlo.

    Por otro lado, siempre es preferible usar la última versión disponible del lenguaje a la hora de programar y saber que tenemos entre manos un sistema viejo nos dará una idea del esfuerzo que conllevará ponerlo al día (Y tal vez lo mejor sea re-escribirlo por completo).

    ¿Conocés alguna otra forma de determinar la versión de PHP utilizada en un sistema?

  • Backups de tu MySQL almacenados en Google Drive

    Backups de tu MySQL almacenados en Google Drive

    Cuando una aplicación entra en producción (si no antes), resulta clara la necesidad de realizar backups.

    Aunque tu código sea una obra de arte digna del Louvre, lo realmente importante son los datos que generan los usuarios.

    Por más que estés usando hostings virtualmente irrompibles (Como Digital Ocean), nunca podés ser demasiado precavido.

    Hay varias formas de resolver este problema, voy a nombrar algunas (De más fácil a más compleja).

    Cómo hacer backups usando MySQL WorkBench

    Desde la pantalla principal (Una vez conectado a un servidor)

    Arriba a la izquierda encontrás el menú «Management»:

    De ahí tenés la opción «Data Export»:

    Eso te lleva a la pantalla de selección de los objetos que vas a querer exportar:

    Seleccioná la base de datos y las tablas:

    Importante: seleccioná la opción «Export to self contained file»:

    De otro modo, la exportación se hará a un archivo por tabla (Lo que simplemente complicará el proceso de restauración).

    Por último, debés iniciar el proceso de exportación:

    Y listo! Lo que obtendrás será un archivo con las instrucciones SQL para crear tu base de datos tal y como está en tu servidor:

    Este proceso es simple pero tiene dos inconvenientes principales:

    1. Es un proceso manual (Es decir, alguien debe acordarse de hacerlo)
    2. Sólo funcionará si tenés disponible una terminal gráfica (O acceso remoto usando una, lo cual es algo poco usual).

    Cómo hacer backups usando MySQLDump

    Una herramienta que viene dentro del paquete MySQL es MysqlDump.

    Se trata de una sencilla utilidad de línea de comandos cuyo objetivo es simplemente realizar volcados (dumps) de bases de datos.

    Si alguna vez usaste el cliente de línea de comandos de MySQL (el comando mysql), la forma de utilización de mysqldump no debería resultarte ajena: se especifica el servidor, la base de datos y algunas opciones más:

    Y se obtienen las sentencias sql requeridas para crear una base de datos igual que la que tenemos ahora:

    Claro que… no es muy útil tenerlo todo en la pantalla, ¿cierto?

    ¡A no preocuparse! Basta con redireccionar la salida standar y estás listo:

    Y si querés hacer algo realmente bueno podés usar un poco más de la magia de POSIX y hacer algo como:

    Y terminar con un archivo comprimido con la fecha de hoy como parte del nombre como para identificarlo rápidamente.

    Este enfoque sigue siendo manual pero:

    1. No requiere de consola gráfica (Es más probable que no tengas problemas para usarlo en tu servidor)
    2. Es fácilmente scripteable

    El último punto es lo que permite justamente sacar de la ecuación a los humanos:

    Cómo automatizar los backups de la base de datos

    Si conocés la utilidad cron te habrás imaginado ya que la verdadera potencia de este esquema está en la realización automática (y periódica) de los backups.

    Así que, si te tomás el trabajito de crear un script con el comando como el que estaba antes:

    Le das permisos de ejecución:

    chmod +x backup.sh

    Y lo ponés dentro de un cronjob:

    Ya te podés olvidar de entrar a hacer los backups al servidor.

    No está mal, ¿cierto? ¿Qué falta? ¡Ah! Sí… ¿dónde deberías guardar los backups?

    Cómo almacenar los backups en Google Drive

    Si llegaste hasta acá debe ser que te interesa mucho tener bien resguardada la información de tus clientes.

    La frutilla del postre es, aparte de realizar los backups en forma automatizada, almacenarlos en algún lugar seguro (Distinto de tu hosting).

    Existen varias alternativas, pero una bastante al alcance de cualquiera es usar una cuenta de Google o Office 365.

    Con este script guardamos los backups de las últimas 5 semanas en nuestra cuenta de Google (Todo gracias a la ayudita de un cliente de GoogleDrive para línea de comandos).

    #!/bin/bash
    backup_dirs='/var/lib/mysql'
    today=`date -I`
    backup_gdrive_path='/opt/backups/'
    
    tar -zcf ${backup_gdrive_path}/prod-${today}.tgz $backup_dirs
    old_backups=`ls -1r $backup_gdrive_path | awk 'NR <= 5 { next} { print }'`
    
    if ! [ "$old_backups}x" == "x" ]; then
     for f in $old_backups; do
     rm -f ${backup_gdrive_path}/$f
     done
    fi
    
    cd $backup_gdrive_path
    drive push -no-prompt -quiet
    drive emptytrash -no-prompt -quiet

    Y por supuesto… ¡no olvides ponerlo como cronjob!

    Y ahora sí, tenés todo lo necesario para montar un sistema de backups remotos automatizados para tu sitio.

  • Cómo pasar una variable PHP a JavaScript

    Cómo pasar una variable PHP a JavaScript

    Un problema muy común es cómo hacer para utilizar información que se tiene en PHP en la ejecución de código JavaScript.

    Detrás de esta inocente pregunta sin embargo, se esconde un error conceptual similar al que tratamos en este artículo: no se domina por completo el esquema de ejecución de una aplicación web.

    Es entendible: la confusión se produce porque en muchas ocasiones se escribe un único texto que se ejecutará en diferentes lugares y momentos. Es un poco como jugar a este juego:

    Para entenderlo bien veamos un ejemplo simple:

    <table>
    <?php
    for ( $i = 0; $i < 5; $i++ ) {
       ?>
       <tr>
          <td><?php echo $i; ?></td>
       </tr>
       <?php
    }
    ?>
    </table>

    Este código está mezclando puro código HTML con PHP, pero el PHP se ejecuta en el servidor, mientras que el HTML es interpretado del lado del cliente

    Recordemos cómo es que funciona un pedido de este estilo:

    El cliente solicita un archivo llamado «pagina.html», pero lo que recibirá no será su contenido si no el resultado de la interpretación de ese archivo por PHP.

    Y ¿qué hace PHP con ese archivo? Todo lo que no esté encerrado entre <?php y ?> será emitido literalmente como se encuentra en el archivo original, mientras que el texto que está entre <?php y ?> será analizado según las reglas del lenguaje y, si genera alguna salida, será adicionado a la salida que se estaba generando.

    En definitiva, después de pasar por el intérprete de PHP, la salida generada será:

    <table>
      <tr>
        <td>0</td>
      </tr>
      <tr>
        <td>1</td>
      </tr>
      <tr>
        <td>2</td>
      </tr>
      <tr>
        <td>3</td>
      </tr>
      <tr>
        <td>4</td>
      </tr>
    </table>
    

    Y será eso lo que el cliente (el navegador probablemente) interprete y genere algo como:

    0
    1
    2
    3
    4

    Bien… y ¿qué hay del JavaScript?

    JavaScript es un lenguaje de programación que se ejecuta del lado del cliente (Salvo en casos como NodeJs, pero para nuestro ejemplo diremos que lo usaremos en su versión clásica).

    Esto quiere decir que, desde el punto de vista del servidor, HTML y JavaScript son indistinguibles (O mejor dicho, la diferencia entre ellos es irrelevante): para el servidor, todo lo que no es PHP es texto que debe ser enviado en forma literal al cliente (Y será el cliente el responsable de decidir qué quiere hacer con ese texto).

    De modo que hacer algo como:

    <?php 
    
    $a = "Hola Mundo!";
    ?>
    <script type="text/javascript">
    alert( $a );
    </script>

    No va a dar el efecto que buscamos (Mostrar en la pantalla del cliente el mensaje «Hola Mundo!» ya que la variable $a es conocida al momento de ejecutar PHP (Es decir, del lado del servidor), mientras que el alert es JavaScript (se ejecutará del lado del cliente).

    Para hacerlo más claro, lo único que el cliente recibirá es:

    <script type="text/javascript">
    alert( $a );
    </script>

    Ya que PHP se ejecutó antes (Cuando se estaba generando la respuesta) y, como todo este código está fuera de los tags de apertura y cierre de PHP, es enviado así como está.

    ¿Cómo se puede resolver este problema? La clave está en comprender bien los tiempos en los que cada evento sucede.

    Si pensamos en que JavaScript es parte del texto que se envía al cliente y que PHP es una herramienta para generar ese código… ¿por qué no generar texto JavaScript usando PHP?

    Algo como:

    <?php 
    
    $a = "Hola Mundo!";
    ?>
    <script type="text/javascript">
    alert( "<?php echo $a; ?>" );
    </script>

    De esta forma lo que recibe el cliente es:

    <script type="text/javascript">
    alert( "Hola Mundo!" );
    </script>

    En definitiva, si lo mirás con detenimiento notarás que no estamos realmente pasando la variable $a a JavaScript, si no que estamos usando su valor (El que tenía al momento de ejecutar PHP) hacia JavaScript para poder usarla (que no es lo mismo pero se acerca bastante 🙂 )

    No es del todo infrecuente usar este tipo de trucos para generar código JavaScript en forma dinámica… sólo hay que ser muy consciente de qué parte de nuestro código se ejecutará en cada momento (Para esto ayuda mucho tener un buen IDE a mano).

    ¿Te quedó alguna pregunta? Me encantará leerla en los comentarios 😉

  • Un redimensionador de imágenes eficiente hecho con PHP

    Un redimensionador de imágenes eficiente hecho con PHP

    Un proyecto interesante que tuve la oportunidad de realizar hace unos años fue un sistema de procesamiento de imágenes.

    El desafío era lograr un servicio simple que permitiera escalar y rotar imágenes velozmente.

    Lo diseñé como un componente separado de la aplicación principal (Una red social de viajes) para poder instalarlo sin inconvenientes en un servidor diferente (y eventualmente poder vincularlo a otros proyectos).

    Por entonces me pareció una buena idea montarlo sobre una arquitectura RESTFul y, como era un proyecto muy simple y acotado (y tenía ganas de aprender algo nuevo de paso) decidí usar un framework especialmente diseñado para estos efectos: Tonic.

    Lo más importante como siempre: ¿qué nombre ponerle a una aplicación como esta? Mucho de fotografía no sé, pero buscando un poco me pareció que Bresson podía servir 🙂

    Muy bien, teniendo despejado el terreno había que empezar a trabajar.

    Para la parte dura del desarrollo no había mucho que pensar: ImageMagick iba a ser el motor de procesamiento.

    Las URIs tenían que ser simples y a la vez semánticas. Un caso de uso muy común era requerir una cierta imagen en un tamaño diferente al original, ¿qué mejor que usar el clasico WWxHH (Ancho por alto) como sufijo?

    Para lograr la eficiencia buscada (además de tener un buen soporte de hardware) había que reducir al máximo las transformaciones de las imágenes… basta con almacenar cada imagen transformada para próximos usos… claro que si se trata de un sistema que maneja miles y miles de imágenes el almacenamiento puede volverse algo problemático (Menos mal que contábamos con Amazon S3).

    Y por último, como para darle un poco más de robustez, me pareció interesante hacerlo agnóstico respecto del medio de almacenamiento y del motor de manipulación de imágenes… al fin y al cabo, uno nunca sabe lo que depara el destino y la verdad… no costaba mucho hacerlo un poco más elegante.

    Y bueno… así nació este pequeño proyecto.

    ¿Querés ver código? Acá vamos 🙂

    En este caso sólo hay disponible un tipo de recurso (Resource): la imagen. Para eso tenemos la clase ImageResource.

    namespace Bresson;
    
    use Tonic\Resource;
    use Tonic\Exception;
    use Tonic\Response;
    
    /**
     * @uri /(.+)-(\d+)x(\d+)
     * @uri /(.+)-(\d+)x(\d+)-r(\d+)
     * @uri /(.+)-(th)
     * @uri /(.+)-(bg)
     * @uri /(.+)
     */
    
    class ImageResource extends Resource
    {
        const MAX_RETRIES = 20;
    
        /**
         * @method get
         * @provides image/jpeg
         */
        public function get($prefix, $width = null, $height = null, $degrees = null)
        {
            $dataSource = $this->app->container['DS'];
            $sOriginalRequest = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
            $this->app->container['logger']->addDebug('Buscando '.$sOriginalRequest);
            if(empty($height) && empty($width)){
                $original = true;
            } else {
                $original = false;
            }
            if ( $dataSource->elementExists($sOriginalRequest) && $contents = $dataSource->fetchElement($sOriginalRequest,$original) ) {
                $this->app->container['logger']->addInfo( $sOriginalRequest. ' encontrado en el storage');
                $this->app->container['logger']->addDebug('Contenido obtenido');
    
                return new Response(
                    Response::OK,
                    $contents,
                    array(
                        'Content-Length' => strlen($contents),
                    ));
            } else {
                $this->app->container['logger']->addInfo($sOriginalRequest.' NO encontrado en el storage');
                if ( !empty($width) && !$original) {
                    $sOriginalName = '/'.$prefix.substr( $sOriginalRequest, strrpos( $sOriginalRequest, '.' ) );
                    
                    $this->app->container['logger']->addDebug('Request contiene sufijo, buscando imagen original', array( 'imagenOriginal' => $sOriginalName ) );
                    if ( !$dataSource->elementExists( $sOriginalName ) ) {
                        $this->app->container['logger']->addDebug('Archivo original no encontrado', array( 'Archivo' => $sOriginalName ) );
                        $sOriginalName = '/'.$prefix.'-bg'.substr( $sOriginalRequest, strrpos( $sOriginalRequest, '.' ) );
                        if ( !$dataSource->elementExists( $sOriginalName ) ) {
                            $this->app->container['logger']->addDebug('Archivo bg no encontrado', array( 'Archivo' => $sOriginalName ) );
                            $sOriginalName = '/'.$prefix.'-th'.substr( $sOriginalRequest, strrpos( $sOriginalRequest, '.' ) );
                            if ( !$dataSource->elementExists( $sOriginalName ) ) {
                                $this->app->container['logger']->addDebug('Archivo th no encontrado', array( 'Archivo' => $sOriginalName ) );
    
                                throw new \Tonic\NotFoundException;                 
                            }
                        }
                    }
                    $this->app->container['logger']->addDebug('Imagen original encontrada en el storage', array( 'imagenOriginal' => $sOriginalName ) );
                    try {
                        $sLockFileName = '/tmp/'.preg_replace( '|/|', '.', $sOriginalRequest ) .'.lck';
                        if ( @$fp = fopen( $sLockFileName, 'x' ) ) {
                            $this->app->container['logger']->addDebug('Lock obtenido', array( 'lockFile' => $sLockFileName ) );
                            $processor = $this->app->container['imageProcessor'];
                            $tmpFile = tempnam(__DIR__, 'img');
                            $this->app->container['logger']->addDebug('Descargando archivo original a temporal', array( 'tmpFile' => $tmpFile ) );
                            file_put_contents( $tmpFile, $dataSource->fetchElement($sOriginalName,true) );
                            $processor->readImageFile( $tmpFile );
    
                            $this->app->container['logger']->addInfo('Archivo original obtenido, procesando imagen');
    
                            if ($degrees) {
                                $processor->rotate($degrees);
                            }
    
                            if ( !is_numeric($width) ) {
                                $this->app->container['logger']->addDebug('El ancho no es numérico, se busca una regla de redimensión', array( 'width' => $width ) );
                                $aParts = explode("/", $sOriginalName);
                                $taxonomy = $aParts[1];
                                if ( !array_key_exists($taxonomy, $this->app->container['config']['process_rules'] ) ) {
                                    $this->app->container['logger']->addError('Se pidio una taxonomía desconocida', array( 'taxonomy' => $taxonomy ) );
    
                                    throw new \Tonic\Exception( 'Unknown taxonomy "'.$taxonomy.'"', 400 );
                                }
    
                                $rules = $this->app->container['config']['process_rules'][$taxonomy];
    
                                if ( !array_key_exists( $width, $rules ) ) {
                                    $this->app->container['logger']->addError('Se pidio un tamaño desconocido para la taxonomia', array( 'taxonomy' => $taxonomy, 'width' => $width ) );
    
                                    throw new \Tonic\Exception( 'Unknown size "'.$width.'", available sizes for "'.$taxonomy.'": ['.implode(', ', array_keys($rules)).']', 400 );
                                }
    
                                $size = $rules[$width];
                                list( $width, $height ) = getimagesize($tmpFile);
    
                                if($width > $height){
                                    $width = $size;
                                    $height = 0;
                                } else {
                                    $width = 0;
                                    $height = $size;
                                }
    
                                if ( !$processor->scale( $width, $height ) ) {
                                    $this->app->container['logger']->addCritical('No se pudo hacer el resizing' );
    
                                    throw new \Tonic\Exception( 'Image couldn\'t be scaled', 500 );
                                }
                            } else {
                                $result = $processor->scale( $width, $height );
                                if ( !$result ) {
    
                                    $this->app->container['logger']->addCritical('No se pudo hacer el resizing' );
    
                                    throw new \Tonic\Exception( 'Image couldn\'t be resized', 500 );
                                }
    
                                $processor->centerCrop( $width, $height);
                            }
    
                            $processor->writeImageFile( $tmpFile );
                            $contents = file_get_contents($tmpFile);
                            unlink($tmpFile);
    
                            $this->app->container['logger']->addDebug('Almacenando en el storage' );
                            /**
                             * @todo Agregar manejo de exception y logging (En todo caso, si no se puede guardar que devuvelva y vuelva a calcular en el próximo request
                             */
                            $dataSource->storeElement( $sOriginalRequest, $contents );
    
                            $this->app->container['logger']->addDebug('Liberando el lock' );
                            fclose($fp);
                            unlink($sLockFileName);
                        } else {
                            $this->app->container['logger']->addDebug('No se pudo obtener el lock, otro proceso debe estar generando' );
                            $tries = 0;
                            do {
                                usleep( 10 );
                                $tries++;
                                $elementExists = $dataSource->elementExists($sOriginalRequest);
                            } while ( !$elementExists && $tries < ImageResource::MAX_RETRIES );
    
                            if ( $elementExists ) {
                                $this->app->container['logger']->addDebug('Otro proceso generó el archivo buscado' );
                                $contents = $dataSource->fetchElement($sOriginalRequest);
                            } else {
                                $this->app->container['logger']->addCritical('Se agotaron los reintentos' );
    
                                throw new \Tonic\NotFoundException;
                            }
                        }
    
                        return new Response(
                            Response::OK,
                            $contents,
                            array(
                                'Content-Length' => strlen($contents),
                            ));
                    } catch ( Exception $e ) {
                        $this->app->container['logger']->addCritical('Exception inesperada: ', array( 'exception' => $e->getMessage() ) );
    
                        throw new Exception( 500, $e->getMessage() );
                    }
                } else {
                    $this->app->container['logger']->addDebug('Archivo no encontrado',array('Archivo' => $sOriginalRequest) );
    
                    throw new \Tonic\NotFoundException;
                }
            }
        }
    }

    Las partes interesantes:

    /**
     * @uri /(.+)-(\d+)x(\d+)
     * @uri /(.+)-(\d+)x(\d+)-r(\d+)
     * @uri /(.+)-(th)
     * @uri /(.+)-(bg)
     * @uri /(.+)
     */
    

    Esta es la forma de mapear una URI a un controlador en Tonic… no está mal, ¿cierto?

    La primera regla es la más cómun: imagen-ANCHOxALTO, simplemente redimensiona.

    La segunda hace lo mismo más una rotación: imagen-ANCHOxALTO-rGRADOS.

    La tercera y la cuarta (th y bg) simplemente son tamaños predeterminados (Algo que tenía mucho sentido en la aplicación cliente).

    Por último, si se quiere una imagen sin alteración alguna también hay que proveerla…

    /**
     * @method get
     * @provides image/jpeg
     */

    Una forma muy simple de definir el método HTTP aceptado y el Content-Type retornado.

    $dataSource = $this->app->container['DS'];

    La parte del agnosticismo respecto del medio de almacenamiento se logra a través de esta dependencia (El DataSource), más sobre esto más abajo.

    $sLockFileName = '/tmp/'.preg_replace( '|/|', '.', $sOriginalRequest ) .'.lck';
    if ( @$fp = fopen( $sLockFileName, 'x' ) ) {

    Todas las partes del código que hacen referencia al LockFile son simplemente un mecanismo de exclusión mutua. Tené en cuenta que en este contexto era muy frecuente tener una gran cantidad de usuarios solicitando la misma imagen a la vez, y una de las premisas del proyecto era ser eficiente… no podíamos permitirnos procesar una misma imagen más de una vez, con lo cual, tuvimos que implementar este mecanismo para asegurarnos de no saturar los recursos del servidor en tareas redundantes (Ok, supongo que podríamos haber usado algo de más bajo nivel como los semáforos… en aquel momento no los conocía).

    $processor = $this->app->container['imageProcessor'];

    A través de esta dependencia logramos agnosticismo respecto del motor de procesamiento de imágenes.

    Y lo demás se explica por sí mismo 🙂

    Lo importante de notar en este código es que se trata de un thin controller, no hace mucho más que preparar el terreno para que el procesador de imágenes y el data source hagan su trabajo cómodamente.

    La inyección de las dependencias se realiza desde el entry point principal de la aplicación: web/dispatch.php

    $app = new Tonic\Application($config);
    $app->container = new Pimple();
    

    Apenas creada la aplicación se le agrega un simple contenedor de dependencias (Pimple) y luego se van generando las dependencias en base a una configuración:

    $bressonConfig = array(
       'process_rules' => array(),
       'data_source' => array(
          'class' => 'Bresson\LocalStorage',
          'init_params' => array(
             'base_dir' => __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'files',
          ),
       ),
       'image_processor' => array(
          'class' => 'Bresson\IMagickProcessor',
       ),
       'log' => array(
          'filename' => __DIR__.'/../log/bresson.log',
          'level' => 'DEBUG',
       ),
    );
    $app->container['config'] = $bressonConfig;
    $app->container['DS'] = new $app->container['config']['data_source']['class']( $app->container['config']['data_source']['init_params'] );
    $app->container['imageProcessor'] = new $app->container['config']['image_processor']['class']();

    Por último tenemos dos interfaces que nos permiten abstraer las clases específicas que realizan las tareas complejas:

    • ImageProcessor
    • Storage

    En este caso sólo tenemos un ImageProcessor: IMagickProcessor el cual en los hechos no es más que un wrapper para la clase Imagick.

    En cambio, del lado de los Storage tenemos dos posibilidades:

    • LocalStorage
    • S3Storage

    El primero es muy simple: utiliza como medio de almacenamiento el sistema de archivos local, el segundo es algo más interesante: usa S3.

    Pero lo importante es que, dada la arquitectura de la solución, implementar nuevos soportes de almacenamiento o procesadores de imágenes se vuelve algo super sencillo:

    Basta con crear una clase que implemente la interfase adecuada (Sea ImageProcessor o Storage) y modificar una pequeña configuración (Que, dicho sea de paso, debería estar en un archivo separado…).

    Y listo, tenemos un pequeño pero potente procesador de imágenes muy extendible y escalable.

    ¿Qué lecciones te llevás de este pequeño ejemplo?

     

  • Cómo ser el primero en enterarse de los errores de tu aplicación web

    Cómo ser el primero en enterarse de los errores de tu aplicación web

    Cuántas veces te pasó que te mande un mail un cliente (o peor, te llame por teléfono) para decirte que la aplicación que pusiste en producción hace más de una semana acaba de dar uno de esos errores inentendibles:

    Y vos, estando en cualquier otro tema tenés que buscar en lo más recóndito de la memoria para recordar de qué se trataba y contestar algo medianamente coherente…

    ¿No sería genial poder decirle a tu cliente apenas atender: «Sí, ya sé, hubo un problema con la app, ya lo estoy viendo y enseguida lo tenés solucionado»?

    Hay varios puntos para señalar en este escenario:

    1. Más allá de que «queda feo» que un cliente vea un mensaje en inglés cuando el resto de la aplicación está en castellano (o que vea una pantalla con fondo blanco y despojada de todo branding), esto da a un potencial atacante información que no tiene por qué tener: que tu aplicación está desarrollada usando Symfony1 (y por lo tanto PHP).
    2. Se pierde un tiempo valioso buceando en los logs de errores hasta entender qué pudo haber pasado. Asumiendo que tales logs existan, claro.

    Algo que mejora tanto la seguridad de la aplicación como la experiencia del usuario es simplemente contar una página de error personalizado:

    Fácilmente puede especificarse qué hacer en caso de que la respuesta a enviar no sea un código 200 usando configuración de Apache, NginX o el webserver que sea.

    En este post te voy a mostrar cómo lograrlo sin tocar nada desde la infraestructura, si no todo dentro de tu propio proyecto.

    Lo voy a hacer usando el framework Symfony. Si estás usando otro no va a ser tan literal el tema, pero las ideas te servirán igual:

    En Symfony los errores HTTP se manejan mediante Excepciones, para cada tipo de error existe un template que se utiliza para generar la respuesta a enviar.

    Todo esto en realidad no es de Symfony si no de Twig (el motor de templating que viene junto con el framework).

    Dentro del código de una aplicación Symfony típica vas a encontrar un directorio vendor/symfony/symfony/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/ donde se encuentran los templates usados en caso de error.

    Como en cualquier otro caso en que necesites modificar comportamientos estándar de los bundles, lo más conveniente es crear tu propia copia de los archivos originales dentro del directorio app/Resources de tu aplicación. En este caso sería el directorio app/resources/TwigBundle y dentro de él views/Exception/

    Hasta aquí resolvimos el problema de lo que el usuario ve pero… ¡todavía sos el último en enterarse de lo que pasó!

    Bien… ¿cómo podrías enterarte de algo que está sucediendo en el servidor sin estar pegado al log de errores 24×7?

    ¿Qué tal una alerta por email?

    Lo primero que podrías pensar es en codear tu propio mecanismo de envío de correos ante la aparición de un error… y por ahí hasta lo lograrías. Pero, como nos gusta decir por aquí, ¿para qué reinventar la rueda?

    ¿Te acordás de lo que te conté sobre Monolog? Vamos a ver cómo se puede usar una de sus características más interesantes: la composición de loggers (o handlers en terminología Monolog).

    De lo que se trata acá no es ni de reemplazar el logging tradicional por el mail ni de llenarte la casilla de mensajes crípticos, si no de tener lo mejor de ambos mundos (Una alerta a tiempo con la información necesaria para reaccionar y los logs completos donde deben estar).

    Una forma de lograr esto es usando inteligentemente la configuración Monolog y de SwiftMailer. Estas configuraciones las encontrás en el archivo app/config/config.yml(y por supuesto en sus hijos config_dev.yml y config_prod.yml).

    Del lado de Monolog debería quedar algo como:

    monolog:
        handlers:
            main:
                type:         fingers_crossed
                action_level: error
                handler:      grouped
            grouped:
                type:       group
                members:    [streamed, deduplicated]
            streamed:
                type:  stream
                path:  %kernel.logs_dir%/%kernel.environment%.log
                level: debug
            deduplicated:
                type: deduplication
                handler: swift
            swift:
                type:           swift_mailer
                from_email:     app@dominio.com
                to_email:       mauro.chojrin@leewayweb.com
                subject:        "App::Error_Exception"
                level:          error
                formatter:  monolog.formatter.html
                content_type: text/html
            console:
                type:  console

    Lo que estamos haciendo acá es decirle a Monolog (a través del sistema de configuración de Symfony) que cada mensaje debe ser procesado por el handler grouped que, como podrás ver, es de tipo group (Una forma muy elegante de hablar de un conjunto de loggers).

    El primero de esos loggers es el clásico: archivo de texto local (Se podría hacer algo más complejo si tuvieses que sincronizar logs de diferentes servidores, pero lo dejamos para otro momento).

    El segundo logger es más interesante: deduplicated (Una suerte de filtro para no llenarte la casilla con el mismo mensaje enviado 1000 veces). Este a su vez usa un handler propio: swift.

    El handler swift es el que efectivamente se encarga de realizar el envío del error a la casilla que le indiques… en realidad, de lo que se encarga es de pedirle a SwiftMailer que haga todo eso.

    Y ahí es donde entra en juego la configuración propia de SwiftMailer… después de todo, esto tampoco es mágico.

    Lo más importante acá es a través de qué medio se va a realizar el envío, es decir qué tipo de transport vas a querer usar.

    En mi ejemplo usé una cuenta de Gmail creada especialmente para esta aplicación (Total son gratis y andan muy bien :)).

    La configuración al final es esta:

    swiftmailer:
        host: smtp.gmail.com
        username:  '%mailer_user%'
        password:  '%mailer_password%'
        encryption: ssl
        port: 465

    Al usar el username '%mailer_user%' y el password '%mailer_password%' lo que estoy diciendo es que los valores de esta configuración se obtengan del archivo app/config/parameters.yml. Como este archivo es específico para cada entorno donde la aplicación esté desplegada (léase: ¡no lo versiones!), esta configuración permite usar diferentes cuentas para desarrollo y para producción.

    Lo único que debés tener en cuenta si vas a usar Gmail (En lugar de algún otro SMTP público) es que vas a tener que permitir el acceso a la cuenta a aplicaciones no seguras.

    Por último, para recibir un mensaje que se vea así:

    En lugar de así:

    Se usan las líneas:

    formatter: monolog.formatter.html

    content_type: text/html

    De la configuración de Monolog.

    Y ahora sí, todo listo, si llega a producirse un error inesperado, el mismo sistema te va a avisar con un email bien formateado.

    Y si todavía querés llevar el tema a un nivel superior, una herramienta sumamente interesante que te recomiendo usar es Honeybadger.

    Con Honeybadger podés tener un panel de administración donde se centralicen todos los problemas u otros mensajes que quieras que tu aplicación envíe.

    De esa forma, la gestión de los problemas puede hacerse todavía más eficiente.

    Te recomiendo que le des una mirada.

  • Cómo consumir un WebService REST con PHP

    Cómo consumir un WebService REST con PHP

    La conexión de tus aplicaciones php con WebServices REST te permitirá ampliar tus capacidades apoyándote en servicios de grandes compañías.

    Los detalles específicos dependen de las definiciones que haya realizado el productor del servicio pero, para no encontrarte con sorpresas, es buena idea tener claras las bases.

    Repasemos los conceptos comunes a todo Servicio Web REST.

    Qué es un webservice

    Un webservice (o servicio web) es una forma de integrar aplicaciones web.

    Básicamente se trata de un servidor que expone parte de su funcionalidad para que sus clientes (que generalmente son otros servidores de diferentes aplicaciones) puedan utilizarlas.

    Las principales ventajas de usar web services son:

    1. La posibilidad de valernos de la capacidad instalada (¡y mantenida!) por terceros
    2. La facilidad para extender la funcionalidad de nuestra aplicación (Basta con realizar las llamadas al servicio web que deseamos)

    La principal desventaja de usar este enfoque es que dependemos de servicios de terceros que, salvo en casos muy puntuales, no podemos controlar.

    Ejemplos de webservices:

    1. Autenticación de usuarios vía Facebook, Google, Twitter, etc…
    2. Generación de mapas en tiempo real
    3. Consulta de cotizaciones de acciones

    Qué es RESTful

    RESTful es un modelo arquitectónico de software que plantea una suerte de volver a las fuentes.

    Su filosofía se basa en el poder del protocolo HTTP (Subyacente en toda aplicación web), el cual incluye los conceptos de verbo (GET, POST, DELETE, PUT), recurso, código de errores (404, 500, etc…) y demás elementos que, en teoría al menos, deberían alcanzar para modelar cualquier tipo de interacción cliente-servidor.

    Es muy común que en una aplicación RESTful los objetos o entidades del modelo de datos sean expuestos casi directamente, teniendo una URL específica para cada uno de ellos.

    Por ejemplo, en una aplicación diseñada para una institución educativa sería esperable encontrarse con una URL de tipo:

    http://miescuela.com/alumnos

    Al realizar una petición de tipo GET a esta URL se esperará recibir información de los alumnos de esa institución.

    A su vez, una URL del tipo:

    http://miescuela.com/alumnos/14

    Debería darme información del alumno cuyo id es 14.

    Existe mucha controversia respecto de qué es una API RESTful y qué no lo es…

    Sin entrar en mayor detalle, la idea es que, para consumir un servicio web basado en REST sólo se requiere conocer su URL (De nuevo, esto es en teoría… en la práctica se utilizan varias formas de modificación del pedido, como ser el envío de headers y demás).

    Siendo que se trata de un servicio RESTful, la comunicación entre cliente y servidor es sencilla (A diferencia de lo que es un WebService SOAP), se trata simplemente de enviar un pedido HTTP y procesar su respuesta.

    Cómo realizar peticiones REST con PHP

    Existen varias opciones disponibles, veamos las más comúnmente utilizadas:

    cURL

    La librería cURL permite realizar peticiones a servidores remotos.

    Si bien funciona, es un método de bastante bajo nivel, con sus pros y sus contras.

    Volviendo al ejemplo que te comentaba antes, una forma de hacer la llamada sería esta:

    <?php
    $ch = curl_init();
    
    curl_setopt($ch, CURLOPT_URL, "http://miescuela.com/alumnos");
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $res = curl_exec($ch);
    
    curl_close($ch);

    En la variable $res quedará la respuesta del webservice (Asumiendo por supuesto que no haya habido ningún problema de conexión, transferencia, etc…).

    file_get_contents

    Una opción alternativa (y generalmente más conveniente) es el uso de la función file_get_contents.

    Esta función recibe como parámetro el nombre del archivo que se quiere leer y devuelve todo su contenido como un string.

    Si bien el uso normal de la función es la lectura de archivos locales, php en realidad entiende a los archivos como flujos. Por lo tanto, si está todo bien configurado, una URL puede usarse como nombre de un flujo.

    De este modo nos ahorramos muchas de las complicaciones propias de tratar con una interface de más bajo nivel (y, obviamente, perdemos cierto control, pero generalmente no es necesario hilar tan fino).

    El ejemplo sería:

    <?php
    $res = file_get_contents("http://miescuela.com/alumnos");

    Cómo se procesan los resultados

    Los resultados se procesan de acuerdo al formato de la respuesta.

    Usualmente los Servicios Web devuelven XML o JSON (Existen otras posibilidades, claro, pero estas son las más comunes).

    En el caso de tratarse de un XML, lo mejor es usar la biblioteca SimpleXML, si se trata de json, con json_decode será suficiente.

    Si se trata de otro formato habrá que estudiar el caso puntual, pero en última instancia, siempre se trata de procesar un string…

    Ejemplo de consumor de un WebService REST con PHP

    MercadoLibre tiene una API RESTful que permite interactuar con el sitio de forma simple.

    Veamos un ejemplo de una llamada de acceso público: la que nos da información básica de un usuario.

    La URL que se utiliza es https://api.mercadolibre.com/users/USERID/

    Por ejemplo https://api.mercadolibre.com/users/226384143/ retorna:

    {
      "id": 226384143,
      "nickname": "TETE9928972",
      "registration_date": "2016-08-25T11:36:00.000-04:00",
      "country_id": "AR",
      "address": {
        "city": "Palermo",
        "state": "AR-C"
      },
      "user_type": "normal",
      "tags": [
        "normal",
        "test_user",
        "user_info_verified"
      ],
      "logo": null,
      "points": 100,
      "site_id": "MLA",
      "permalink": "http://perfil.mercadolibre.com.ar/TETE9928972",
      "seller_reputation": {
        "level_id": null,
        "power_seller_status": null,
        "transactions": {
          "canceled": 0,
          "completed": 0,
          "period": "historic",
          "ratings": {
            "negative": 0,
            "neutral": 0,
            "positive": 0
          },
          "total": 0
        }
      },
      "buyer_reputation": {
        "tags": []
      },
      "status": {
        "site_status": "active"
      }
    }

    Un string JSON.

    El código completo para obtener esta información con php es:

    <?php
    
    echo file_get_contents('https://api.mercadolibre.com/users/226384143/');

    Claro que si quisiéramos hacer algo con esta información sería más conveniente hacer algo como:

    <?php
    
    $data = json_decode( file_get_contents('https://api.mercadolibre.com/users/226384143/'), true );
    
    echo $data['nickname'];

    ¡Listo! Ya estás en condiciones de integrar cualquier WebService RESTful en tu aplicación.

    Más información sobre WebServices con PHP

    Como te decía al comienzo, otro protocolo muy utilizado a la hora de conectar aplicaciones a través de Servicios Web es SOAP. Este protocolo es bastante más complejo que RESTful, principalmente porque se basa en intercambios de XML con un formato bastante particular.

    Puedes intentar leer e interpretar estos XML por tus propios medios aunque lo más recomendable es utilizar las herramientas que PHP ofrece para ello.

    De hecho, si te enfrentas a WebServices de basados en SOAP php tiene un soporte nativo muy completo.

  • Qué es un CDN y por qué deberías usarlo

    Qué es un CDN y por qué deberías usarlo

    CDN significa Content Delivery Network o Red de Distribución de Contenidos.

    Se trata de conjuntos (por lo general bastante grandes) de servidores sincronizados entre sí y preparados para servir contenido estático desde diversos puntos del planeta.

    Su objetivo principal es el de disminuir el tiempo de carga de una página web (Algo que siempre viene bien).

    Este objetivo se logra combinando varios factores. Entre ellos:

    1. Aprovechando el paralelismo de los pedidos HTTP: Al tener el contenido distribuido en diversos servidores (en lugar de tener el código php y los archivos estáticos en el mismo) el cliente puede lanzar varias peticiones en paralelo por un lado y, por el otro, el pobre servidor al que llegan todos los visitantes de tu sitio puede delegar parte de la carga, lo cual a su vez hace todo el proceso más ligero.
    2. Sirviendo el contenido estático desde la locación física más cercana a cada cliente. Parte de la magia del CDN es tener la capacidad de identificar velozmente cuál es el nodo que responderá con menos latencia (Generalmente el que se encuentre más próximo al cliente).

    Qué se puede almacenar en un CDN

    Cualquier recurso estático (html, javascript, imágenes, etc…) es susceptible de ser almacenado en un CDN. De hecho, la mayoría de las librerías populares de javascript (Como jQuery por ejemplo) tienen sus propios CDNs que podés usar para cualquier proyecto, basta con hacer un include como este:

    <head>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    </head>

    De esta forma, en lugar de usar un archivo de jQuery alojado en tu propio servidor, usás la capacidad instalada de Google.

    Otra ventaja de usar un CDN (En realidad esto no es particular del CDN si no de cualquier fuente conocida) es que muy probablemente el cliente ya haya descargado el archivo que tu sitio necesita para funcionar, aún cuando se trata de la primera visita (Otra optimización de performance que realizan los navegadores).

    Alternativas

    CloudFlareExiste una gran cantidad de empresas que ofrecen servicios de CDN, te dejo acá algunas que yo he usado en proyectos propios:

    En cuanto a su funcionalidad están todas bastante cerca, el factor que suele inclinar la balanza es el precio… ahí CloudFlare es un claro ganador ya que ofrece un muy buen paquete gratuito.

    ¿Qué opinás? ¿Vas a implementar un CDN en tu próximo proyecto?

  • Cuál es el modo más seguro de tratar con passwords en PHP

    Es bastante común últimamente recibir noticias de que algún sitio de gran popularidad ha sido hackeado (O, como suele comunicarse, «su seguridad se ha visto comprometida»).

    Dependiendo del tipo de sitio del que se trate el problema puede preocuparnos más o menos.

    Claro que eso es cuando somos meramente usuarios del sitio… ¿qué pasa cuándo se trata de un sitio que está bajo nuestra responsabilidad?

    Aclaremos algo antes de seguir: es imposible hacer un sitio 100% libre de vulnerabilidades.

    Si hay gente decidida a romper nuestra seguridad lo van a lograr (Si tienen la capacidad técnica y/o el teléfono de alguien que la tenga).

    Es por eso que es muy importante, no sólo poner trabas a los atacantes si no también dejar un botín poco atractivo para el caso de que lo consigan.

    Algo bastante común es que la gente reutilice sus contraseñas en diferentes sitios, con lo cual, el acceso a las contraseñas de un sitio puede permitir a los atacantes hacerse «gratuitamente» con el acceso a otros…

    Nuevamente, no vamos a poder evitar que se ponga en juego la integridad de los datos que nuestros usuarios dejan en otros sitios, pero al menos podremos decir que no lo han logrado por nuestra bajada de guardia.

    Cómo proteger la información sensible

    La única medida real con la que contamos hoy en día es la encripción de los datos sensibles (Como ser las contraseñas).

    Hasta hace un tiempo se utilizaba mucho la función MD5 para transformar una clave legible humanamente en un texto indescifrable. Por ejemplo el MD5 de ‘Hola mundo!’ es ‘d501194c987486789bb01b50dc1a0adb’.

    Lo que se hacía entonces era calcular el md5 del texto ingresado como contraseña y eso era lo que se almacenaba en la base de datos (y cuando se intentaba un login se volvía a calcular el md5 con lo que se tenía guardado, de modo que la verdadera contraseña no estaba en la base).

    Desafortunadamente, todo avanza rápido en la informática y estas funciones (md5 y sha1) ya no son suficientemente seguras (Las computadoras de hoy son tan rápidas que se ha vuelto un problema menor romper este tipo de encripción).

    ¡Pero no todo está perdido! 🙂

    Más allá de md5 y sha1

    Desde la versión 5.5 de PHP nos encontramos con buenas librerías de criptografía internas. Dos funciones interesantes son password_hash y password_verify.

    Con la primera se obtiene un hash a partir de un texto plano (lo ingresado por el usuario).

    Con la segunda se verifica que un texto plano corresponda con un hash.

    A muy alto nivel lo que logra es lo mismo que antes, sólo que mucho más seguro (Los algoritmos usados por password_hash son más fuertes).

    Pero en realidad es bastante más lo que se logra… Al levantar el nivel de abstracción (usamos una función de hashing que internamente puede usar diversas estrategias para lograr su objetivo) logramos también independizarnos de la implementación particular.

    ¿Qué significa esto? Simplemente que si en un futuro el algoritmo de encripción usado cambia (Por ejemplo porque se encuentra uno mejor), nuestro código no se verá afectado.

    Y ahora, ¿qué pensás hacer con las passwords de tus sitios?

  • ¿SQL vs. NoSQL?

    ¿SQL vs. NoSQL?

    Se escucha mucho últimamente que SQL pasó de moda, que hoy lo cool es usar NoSQL (MongoDB, CouchDB, etc…) pero… ¿es siempre así?

    Empecemos por entender qué es una base de datos NoSQL (formalmente no estoy muy convencido de que un almacenamiento que no respete las reglas ACID pueda llamarse base de datos, pero bueno… como para no entrar en más de una discusión a la vez, digamos que sí).

    De lo que estamos hablando es de un medio de almacenamiento no estructurado, comúnmente conocido como base de datos documental.

    Más allá de las formalidades (los registros de la base de datos relacional se llaman documentos en una base NoSQL, las tablas pasan a ser colecciones, etc..), existen diferencias muy concretas:

    1. Los documentos no tienen estructura (Pueden guardar literalmente cualquier cosa)
    2. No existen operaciones tipo JOIN

    Su razón de ser se encuentra en la premisa de que, en un ambiente como es la web, la cantidad de escrituras vs. lecturas es prácticamente despreciable, con lo cual, si se busca darle a un sitio la mejor performance posible, no tiene sentido optimizar el almacenamiento en función de la operación menos frecuente (como sería el caso de usar una base de datos relacional).

    Si bien esto es cierto, es muchas veces un arma de doble filo. Muchos desarrolladores poco experimentados (o seducidos por el último shinny object) olvidan hacerse la pregunta clave: ¿la premisa es válida en mi contexto?.

    En aplicaciones web que reciben una cantidad enorme de visitas de usuarios no registrados (Un diario, Wikipedia, etc…) es claro que la respuesta es un sí rotundo. Sin embargo, cuando se trata de aplicaciones transaccionales (Un home banking, un sistema de adminstración, etc…), lo contrario es lo que sucede.

    En estos casos la consistencia eventual que prometen las bases NoSQL no es un aliado si no más bien una carga pesada (Sí, me tocó trabajar con más de un sistema transaccional soportado por una NoSQL… no se lo deseo a nadie).

    Y si bien las operaciones JOIN son costosas, el no usar una base relacional no las vuelve innecesarias, es el objetivo de la aplicación el que determina si vamos a requerir cruzar tablas o no.

    Otra ventaja con la que se vende NoSQL es que, al no tener estructura, se puede desarrollar más rápido (No hay que perder tiempo en complejos diseños de estructuras de datos, migraciones y demás)… Mi opinión es que esto constituye un boomerang (Pero bueno, puede que ya esté poniéndome viejo…), ya que, por más que la base de datos no tenga siempre los mismos campos, la aplicación seguramente se beneficie de tener una estructura conocida (No habrá que andar llenando de if para ver qué tiene el objeto que estás queriendo manipular).

    En mi experiencia, lo mejor es, en los escenarios que lo ameriten, usar un mix de ambas, para tener lo mejor de los dos mundos: una base de datos relacional para el almacenamiento duro y alguna caché tipo clave-valor (Memcached por ejemplo) para acceso rápido a los objetos más comúnmente consultados.

    Los frameworks modernos ya vienen preparados para manejar la complejidad de invalidar los cachés en los momentos de escritura y aprovecharlos en los momentos de lectura, con lo cual… ¿para qué pelear? Todo en su justa medida y armoniosamente ayuda.

  • Cómo usar CC y BCC con PHPMailer

    Cómo usar CC y BCC con PHPMailer

    ¿Qué es PHPMailer?

    Comencemos por el principio: PHPMailer es una librería que permite enviar emails desde PHP (Podés consultar algunas opciones acá).

    Si bien no es la única (De hecho, mi preferida es SwiftMailer), su principal ventaja es que suele estar disponible en entornos de hosting compartido, con lo cual, su uso es bastante popular.

    ¿Cómo se usa?

    Su uso es bastante simple: basta con crear una instancia de PHPMailer para tener acceso a una gran cantidad de funcionalidad:

    <?php
    use PHPMailer\PHPMailer\PHPMailer;
    use PHPMailer\PHPMailer\Exception;
    
    require_once 'vendor/autoload.php';
    
    $mail = new PHPMailer();
    
    $mail->isSMTP();                                      
    $mail->Host = 'mail.google.com';
    $mail->SMTPAuth = true;                               
    $mail->Username = 'usuario@gmail.com';                
    $mail->Password = 'miSuperPassword';                           
    $mail->SMTPSecure = 'tls';                            
    $mail->Port = 587;                                   
    
    $mail->setFrom('acedmy@leewayweb.com', 'Leeway Academy');
    $mail->addAddress('juan.perez@yahoo.com', 'Juan Perez'); 
    
    $mail->Subject = 'Este es el asunto';
    $mail->Body    = 'Este el cuerpo del mensaje';
    
    if(!$mail->send()) {
        echo 'No se pudo enviar el mensaje...'.$mail->ErrorInfo;
    } else {
        echo 'El mensaje se envió!';
    }

    Claro que, antes de poder usarla, la librería debe estar instalada… En este ejemplo vemos cómo se utiliza asumiendo que se ha usado composer para incorporarla al proyecto.

    Para agregar otros destinatarios en copia (CC) o copia oculta (BCC), simplemente debemos agregar estas líneas:

    $mail->addCC('copiado@hotmail.com');
    $mail->addBCC('copia_oculta@outlook.com');

    Antes de realizar el envío.

    Por útlimo, como siempre, no hay que olvidar el $mail->send().

    ¿Alguna pregunta? ¡Deja un comentario!