Blog

  • Un sistema de caché distribuido en PHP

    Un sistema de caché distribuido en PHP

    Hace un tiempo, cuando trabajaba para una gran .com del rubro turismo, surgió un escenario muy interesante que teníamos que resolver junto con mi equipo:

    Teníamos un servidor de bases de datos que daba soporte a siete servidores que hacían de FrontEnd. Nuestra aplicación era php puro (con un «framework» desarrollado in-house… muy mala idea) y todos los frontends corrían el mismo código (Todo detrás de un balanceador de carga, obvio):

    El problema que teníamos era que el sitio tenía bastante tráfico y la base de datos se nos convertía en un cuello de botella bastante a menudo.

    La solución que implementamos consistía en tener ciertas partes de las respuestas pre-calculadas.

    Ahora, como te imaginarás, mantener una experiencia de navegación consistente teniendo varios servidores diferentes tiene sus complicaciones, en nuestro caso, el desafío más importante era cómo mantener copias sincronizadas de la información pre-calculada (Para evitar, por ejemplo, que un simple F5 mostrara algo diferente de una página supuestamente estática).

    Por otro lado, los servidores de FrontEnd que usábamos eran simples instancias de máquinas virtuales que (no estoy muy seguro de por qué), tenían una tendencia a romperse intempestivamente (Bueno… un poco por eso teníamos tanta redundancia :)), con lo cual, no era viable tener un único responsable de la generación del caché…

    Lo que necesitábamos básicamente era una arquitectura que permitiera:

    1. Que cualquier FrontEnd fuera capaz de generar la versión estática de la información
    2. Que no hubiese dos FrontEnds generando la versión estática a la vez
    3. Que todos los FrontEnds sirvieran el mismo contenido (Si estaba disponible)

    Sin entrar en detalles sobre cómo logramos el objetivo 2 (Lo dejo para otro post en todo caso, pero involucra un sistema de semáforos), lo que hicimos fue crear una función (Método de una clase en realidad) que recibiera dos funciones:

    • Una para verificar si la copia local de la información estaba vigente aún
    • Otra para generar la información en caso de ser necesario

    Lo interesante de este mecanismo es que, gracias al uso de los callbacks fue bastante sencillo separar (¡y reutilizar!) el mecanismo de exclusión mutua y todo lo que hacía al andamiaje de lo que tenía que ver con la generación de la información propiamente dicha.

    Una versión simplificada del código a modo de ilustración es esta:

    public function get( $sKey, $iTtl, Closure $fGeneration, array $aGenerationParams = [], Closure $fValidation = null ) {   
       $oRemoteStorage = $this->getRemoteStorageFactory()->build($sKey);
       if ( ( $oCache = $this->getFromStorage( $oRemoteStorage ) ) && !$oCache->isExpired() && $fValidation( $mContents = $oCache->getContents() ) ) {
           $this->putInStorage( $oCache, $oLocalStorage );
           return $mContents;
       }
       $mContents = call_user_func_array( $fGeneration, $aGenerationParams );   
       $oCache = new CacheObject( $mContents, time() + $iTtl );
       $this->putInStorage( $oCache, $oLocalStorage );
       $this->putInStorage( $oCache, $oRemoteStorage );
       return $mContents;
    }
    

    Lo interesante de esta función son los parámetros $fGeneration y  $fValidation, ambos Closures, esta es la clave para que el mecanismo de caché se mantenga agnóstico respecto de qué es exactamente lo que se está cacheando… cómo se genera ese caché y cómo se verifica su vigencia son problemas del usuario del mecanismo de caché.

    De esta forma queda un sistema altamente reutilizable :).

    Si te interesa ver el código completo (Está un poco viejo, pero la idea sirve), acá está el repo en GitHub.

     

  • Dónde almacenar la configuración de una aplicación PHP de forma segura

    Dónde almacenar la configuración de una aplicación PHP de forma segura

    Una pregunta que me llegó de un amigo que viene del mundo .Net (Que parece ser un poco más organizado o estandarizado que el nuestro :).

    Lo primero que deberíamos preguntarnos es de qué nos estamos queriendo proteger.

    Por lo general, la posibilidad de que alguien externo a nuestra organización tenga acceso a la configuración de nuestra aplicación no parece muy tentadora… ¿por qué? Básicamente porque puede haber información sensible (Como ser contraseñas, nombres de hosts donde están las bases de datos, etc…) que podrían dar a un atacante una ventaja importante si decidiera hacernos un daño.

    Una línea de defensa que tenemos es guardar estos archivos en un directorio que no sea públicamente accesible.

    Si usás el webserver Apache, conocerás seguramente la directiva DocumentRoot: lo que está «a la vista» de los visitantes es exclusivamente lo que está dentro de ese directorio (O subdirectorios, según cómo esté configurado el servidor), pero (y esta es la parte interesante), no es lo único que está a la vista para un script php.

    Ejemplo:

    <VirtualHost *:80>
     ServerName my.domain.com
     ServerAdmin webmaster@localhost
     DocumentRoot /usr/share/apache2/mysite
    
     <Directory /usr/share/apache2/mysite>
      Options FollowSymLinks
      AllowOverride All
     </Directory>
    </VirtualHost>

    Con esta configuración, cuando yo ingrese a la URL http://my.domain.com/index.php, el servidor me dará el HTML generado por la ejecución del archivo /usr/share/apache2/mysite/index.php.

    Imaginemos que index.php tiene algo como esto:

    <?php 
    
    require_once '../config.php';
    echo 'Hola Mundo!';

    Si yo intentara entrar (desde mi browser) al archivo config.php debería escribir algo como http://my.domain.com/../config.php (Eso obviamente, sabiendo que ahí se encuentra el archivo que busco).

    Si bien podría llegar a pasar que el webserver esté mal configurado y permita hacer eso, en la gran mayoría de los casos, eso simplemente será imposible (Los requests están confinados a leer archivos que están dentro del DocumentRoot).

    Si estás usando un framework estándar (Symfony, CakePHP, Laravel, etc…), esto ya estará previsto (Habrá un directorio especial para almacenar los archivos de configuración, pero siempre en última instancia se va a tratar de un tema de permisos configurados a nivel de webserver).

    ¿Conocés alguna otra forma de proteger tus archivos de configuración? ¡Compartila en los comentarios!

  • Cómo acceder a Google Drive usando PHP

    Cómo acceder a Google Drive usando PHP

    El escenario que voy a analizar es este: Una aplicación desarrollada en PHP requiere acceder a archivos que sus usuarios tienen almacenados en sus propios documentos en Google Drive.

    Configuración de la API De Google

    1. Crear un proyecto en Google (yendo a https://console.developers.google.com/apis/credentials):

    1. Habilitar el acceso a la API via OAuth (https://console.developers.google.com/apis/api/drive/overview?project=MI_PROYECTO):
    1. Crear las credenciales de acceso vía OAuth:

    Seleccionar OAuth:

    1. Descargar las credenciales a un lugar seguro:
    1. Autorizar la URI de redireccionamiento

    Uso de la API de Google desde PHP

    Con esto hecho ya tenés lo básico para empezar a programar.

    Si bien podés hacer todo en un solo archivo, no es muy recomendable, es mejor tener un archivo para cada paso del trabajo.

    Ante todo, vas a necesitar la librería google/apiclient (Te recomiendo incorporarla con composer), acá podés ver un ejemplo de composer.json:

    {
     "name": "leeway/google-drive",
     "require": {
     "google/apiclient": "^2.1"
     },
     "authors": [
     {
     "name": "Mauro Chojrin",
     "email": "mauro.chojrin@leewayweb.com"
     }
     ]
    }

    No olvides después correr el comando

    composer install

    Este codigo (config.php) va a ser común a todos los archivos .php que vendrán luego:

    <?php
    require_once 'vendor/autoload.php';
    session_start();
    $redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . '/paso2.php';
    $client = new Google_Client();
    $client->setAuthConfig('<PATH_AL_ARCHIVO_JSON_DE_CREDENCIALES>');
    $client->setRedirectUri($redirect_uri);
    $client->addScope("https://www.googleapis.com/auth/drive");

    El primer paso:

    Ofrecer al usuario un formulario para conectarse a su Drive (paso1.php):

    <?php require_once 'config.php' ?>
    <div class="request">
     <a class='login' href='<?php echo $client->createAuthUrl() ?>'>Conectame!</a>
    </div>

    Contar con una URL a la que Google informe que la autenticación fue exitosa (paso2.php):

    <?php 
    require_once 'config.php';
    
    if (isset($_GET['code'])) {
     $token = $client->fetchAccessTokenWithAuthCode($_GET['code']);
     $client->setAccessToken($token);
    
     $_SESSION['upload_token'] = $token;
    
     header('Location: paso3.php');
    }

    Muy importante: este archivo (paso2.php) debe ser accesible para el cliente (Para eso es importante que su URL completa esté ingresada en la configuración de URI de redireccionamiento, sin eso nada de esto funcionará.

    Paso 3 (Completar la autenticación):

    <?php
    
    require_once 'config.php';
    
    if (!empty($_SESSION['upload_token'])) {
     $client->setAccessToken($_SESSION['upload_token']);
     if ($client->isAccessTokenExpired()) {
      unset($_SESSION['upload_token']);
      echo 'La sesion expiro. <a href="paso1.php">Volver a comenzar</a>';
      die;
     }
    }
    echo 'Autenticacion completa <a href="paso4.php">Descargar archivo</a>';

    Paso 4:

    Hacer lo que uno quiera en el drive autorizado :).

    Ejemplo: descargar un archivo desde Google Drive usando PHP

    <?php
    require_once 'config.php';
    
    echo '<p>Descargando el archivo "Mi archivo"</p>';
    $service = new Google_Service_Drive($client);
    $files = $service->files->listFiles([
     'q' => "name='Mi archivo'",
     'fields' => 'files(id,size)'
     ]);
    
    if ( count($files) < 1 ) {
     echo 'No se encontro el archivo :(';
     die;
    }
    $fileId = $files[0]->id;
    $fileSize = intval($files[0]->size);
    
    $http = $client->authorize();
    
    $fp = fopen('Mi archivo', 'w');
    
    $chunkSizeBytes = 1 * 1024 * 1024;
    $chunkStart = 0;
    
    while ($chunkStart < $fileSize) {
     $chunkEnd = $chunkStart + $chunkSizeBytes;
    
     $response = $http->request(
      'GET',
      sprintf('/drive/v3/files/%s', $fileId),
      [
       'query' => ['alt' => 'media'],
       'headers' => [
           'Range' => sprintf('bytes=%s-%s', $chunkStart, $chunkEnd)
       ]
      ]
      );
      $chunkStart = $chunkEnd + 1;
      fwrite($fp, $response->getBody()->getContents());
     }
     fclose($fp);
    echo '<p>Listo! Archivo disponible en: "'.realpath('Mi archivo').'"</p>';

    Y listo, tenemos el archivo buscado descargado a nuestro disco.

    Para probar esta aplicación (suponiendo que fuiste guardando cada porción de código en su propio archivo), iniciá el servidor de php:

    php -S localhost:8080

    y abrí en tu navegador:

    http://localhost:8080/paso1.php

    Claramente, no se trata de magia, lo que hay detrás de esto es un WebService, al que se está accediendo usando la clase Google_Service_Drive. Podes ver más información entrando a https://github.com/google/google-api-php-client.

    Y ahora que sabés cómo manipular archivos que están en el drive… ¡cuidado con lo que hacés!

  • Los bundles mínimos para un proyecto empresarial Symfony

    Los bundles mínimos para un proyecto empresarial Symfony

    Ultimamente me estuve entusiasmando bastante con un par de proyectos en los que estamos trabajando en Leeway (obviamente codeados en mi framework favorito Symfony :)) y me pareció interesante compartir algo de la experiencia.

    Symfony por sí mismo es un gran Framework cuando se trata de armar un sólido Back-End, pero la parte de front… es un poco floja para mi gusto. Afortunadamente, el diseño modular que tiene ha permitido que mucha gente colabore con excelentes bundles, entre ellos:

    MopaBootstrapBundle

    Lograr un frontend responsive sin saber casi nada de css es, en mi opinión, un sueño hecho realidad. Este bundle permite apalancarse en Twitter Bootstrap para lograr una UI muy profesional (y estándar).

    Ejemplo de una pantalla lograda:

    Más información acá

    AsseticBundle

    La librería Assetic provee una forma muy buena de manejar los recursos estáticos (js, css, etc…). Mopa funciona muy bien con Assetic para el pre-procesamiento de css (vía less/sass).

    Más información acá

    FOSUserBundle

    Es raro que una aplicación (especialmente una empresarial) no requiera manejar diferentes perfiles de usuario.

    FOSUserBundle provee todo el andamiaje necesario para tener listo un sistema de autenticación, con registro de usuarios, edición de perfiles y demás casi sin escribir código.

    KnpMenuBundle

    No puedo dejar de mencionar este paquete que nos provee una excelente funcionalidad para crear menúes de forma extremadamente simple:

    Para instrucciones de instalación y uso ver acá

    cspooSwiftmailerMailgunBundle

    Otra tarea sumamente común en aplicaciones web es el envío de correos electrónicos.

    Cuando la cantidad y/o frecuencia de los envíos es alta, la carga de trabajo que implica puede ser pesada para el servidor web donde corre nuestra aplicación, es por eso que siempre es una buena idea delegar esto en servicios especializados.

    De las opciones que conozco, la que más me gusta es MailGun (sobre todo la parte de los 10k envíos mensuales gratuitos :).

    Su API es super accesible y, lo mejor de todo, existe cspooSwiftmailerMailgunBundle que permite interactuar con el servicio de forma muy sencilla (La instalación puede ser un poco tortuosa, pero vale la pena).

    Incorporando estos cuatro bundles a tu proyecto podrás lograr con muy poco esfuerzo una aplicación de alto potencial.

    Y si querés levantar la vara un poco más, te recomiendo probar PetkoparaCrudGeneratorBundle.

    Este bundle provee un generador de CRUDS muy superior al que trae Doctrine. Basta ver una pantalla generada para enamorarse:

    Así que bueno… ¡ahora sólo te queda armar tus aplicaciones profesionales!

    ¿Tenés algún otro bundle que considerás imprescindible? ¡Dejá la referencia en los comentarios!

  • Cómo enviar emails desde PHP

    Cómo enviar emails desde PHP

    Es muy común la necesidad de enviar emails desde PHP, tanto si se trata de un script de CLI (como puede ser un cronjob) o de una aplicación web.

    Existen varias opciones a la hora de conseguirlo:

    La más simple es el uso de la función mail

    Es una función de bastante bajo nivel (es decir, algo tosca), pero está disponible en casi cualquier instalación de php.

    Esta opción está bien para escenarios simples, pero para usos más avanzados (como enviar adjuntos, HTML o similares), es conveniente utilizar alguna otra opción algo más robusta.

    Algunas alternativas interesantes son:

    Cualquiera de estas resultará más adecuada que el uso de la simple función mail en contextos en los que la performance sea una preocupación (Por ejemplo, cuando hay alto tráfico en el sitio) o cuando se prefiere realizar el envío a través de un servidor SMTP externo.

    Si decides usar una librería (Es decir, pasar de la función mail), algo muy interesante por hacer es apoyarte en algún servicio externo (Por ejemplo MailGun).

    Si bien es posible utilizar un servicio como este (O algún otro SMTP externo) a través de la función mail, es bastante tortuoso (Ya no depende directamente de código PHP si no que requiere de meterle mano a la configuración del sendmail y otras cuestiones así…).

    Lo mejor es usar una librería que tenga esta posibilidad, como por ejemplo SwiftMailer (Aquí un poco más de detalle).

    Otro tema importante: enviar correos con HTML (Por ejemplo para que incluyan imágenes) no es nada divertido si usas la función mail a secas.

    Por último, ten en cuenta que la función mail no realiza ninguna clase de optimización… si usas SMTP, cada envío realizará su propia conexión… imagínate la penalización de rendimiento que tendrán tus usuarios… Para evitar este problema deberías, al menos, intentar usar una librería que maneje un pool de conexiones SMTP (Casi cualquiera que no use mail :p).

    Espero haberte convencido de que la función mail es sólo para casos de emergencia :).

    ¿Lo logré o tienes alguna experiencia que indique algo diferente? ¡Deja un comentario!

  • Cómo se usan las funciones anónimas en PHP

    Cómo se usan las funciones anónimas en PHP

    Las funciones anónimas son, como te habrás imaginado, funciones que no tienen un nombre.

    Esto puede sonar bastante raro, siendo que seguramente cuando aprendiste qué es y cómo se define una función en PHP te dijeron que era algo con la pinta:

    function f( $param1, $param2 )
    {
        // Algo de código
    }

    Pues te mintieron.

    Bueno, no, en realidad no te mintieron pero te dieron una versión parcial de la verdad.

    Lo que quiero decir es que generalmente las cosas son así como las conoces, pero existen otras posibilidades.

    Te presento la función anónima:

    $f = function( $param1, $param2 ) {
        return $param1 + $param2;
    }

    En este ejemplo estoy asignando a la variable $f la función que estoy definiendo (¡No el resultado de su evaluación! No te confundas).

    Y ¿por qué querrías hacer una cosa semejante? (Además de molestar a tus compañeros de trabajo o tratar de sacar un poco de chapa).

    Sucede que muchas veces la función que estás definiendo tiene un uso extremadamente puntual (Es decir, no va a ser reutilizada) y es más conveniente entonces definirla en el mismo lugar donde se usa (Además de dejar el código más claro).

    Es muy común usar las funciones anónimas en combinación con los callbacks, por ejemplo, si usás la función array_map, es más directo usar una función anónima para expresar la transformación que necesitás realizar. Ejemplo:

    $simples = [ 1, 2, 3, 4, 5 ];
    $dobles = array_map( function( $a ) { return $a * 2; }, $simples );

    También podés usar funciones anónimas en llamadas a funciones que vos mismo hayas definido (Siempre y cuando estén preparadas para recibir este tipo de parámetros).

    ¿Te parece más claro escribir de esta forma o usando funciones con nombre?

  • Cómo se usan los callbacks en PHP

    Cómo se usan los callbacks en PHP

    Este es uno de mis temas preferidos cuando a PHP se refiere.

    Debo confesar que lo descubrí bastante tarde, pero fue prácticamente amor a primera vista.

    Veamos un ejemplo:

    Si tengo un array con números ( [ 1, 2, 3, 4, 5 ] ) y quiero obtener sólo los números pares, la forma normal de hacerlo sería algo como:

    $pares = [];
    foreach ( $numeros as $numero ) {
       if ( $numero % 2 == 0 ) {
           $pares[] = $numero;
       }
    }

    Levantemos un poco el nivel de abstracción:

    function esPar( $numero )
    {
        return $numero % 2 == 0;
    }
    
    $pares = [];
    foreach ( $numeros as $numero ) {
       if ( esPar( $numero ) ) {
           $pares[] = $numero;
       }
    }

    Hasta acá la versión clásica.

    Ahora, si mirás nuevamente, podés ver que se está llamando a la función esPar() sobre cada uno de los elementos del array $numeros y nos estamos quedando sólo con aquellos que lo son, o, dicho de otro modo, estamos dejando afuera aquellos elementos que no son pares. A esta operación se la conoce como filtro.

    Ahora bien, es claro que esPar no es el único criterio por el que podríamos querer filtrar un array.

    Si tomamos un ejemplo donde sólo queremos quedarnos con los elementos mayores que tres:

    function esMayor3( $numero )
    {
        return $numero > 3;
    }
    
    $mayores = [];
    
    foreach ( $numeros as $numero ) {
       if ( esMayor3( $numero ) ) {
           $mayores[] = $numero;
       }
    }

    La estructura de estos dos scripts es muy similar:

    • Se parte de un array
    • Se aplica una función booleana a cada elemento
    • Se retornan sólo los elementos para los cuales la función evalúa en true

    Notá que la única diferencia entre los dos scripts es cuál es la condición que determina si el elemento queda adentro del array de resultado o no

    Y acá es donde aparece este genial concepto tomado de la programación funcional: la posibilidad de que las funciones puedan ser argumentos de otras funciones.

    Si nunca viste este concepto antes, te sugiero que te tomes un par de segundos para digerir lo que acabás de leer.

    ¿Ya está? Ok, sigamos.

    Lo que estoy diciendo es que podrías hacer una función genérica que tome las partes en común de ambas situaciones y reciba como parámetro el criterio de filtro que se debe usar.

    Se vería algo como:

    function filtrar( array $array, $f )
    {
       $res = [];
    
       foreach ( $array as $elemento ) {
          if ( $f( $elemento ) ) {
             $res[] = $elemento;
          }
       }
    
       return $res;
    }

    Notá como $f es a la vez una variable y una función.

    A mi esto me recuerda a la escena de Matrix donde el chico monje dobla la cuchara (Si no viste la peli no tiene mucho sentido el comentario, pero si la viste seguro entenderás de lo que hablo)

    Antes de seguir te cuento (por si no lo adivinaste aún) que esta función ya existe en PHP: array_filter.

    La tomé como ejemplo (existen muchas funciones nativas que usan callbacks) porque me parece de lo más claro para explicar el concepto para quien nunca lo vio, pero las posibilidades son realmente muchas.

    La pregunta que puede surgirte es: ¿por qué no puedo seguir escribiendo esto como lo hacía hasta ahora? ¿Qué me aporta conocer los callbacks?

    La respuesta es que sí, no hay nada que puedas lograr (desde el punto de vista de funcionalidad) que no pudieras lograr sin usar callbacks.

    La segunda pregunta es más interesante. Lo que te aporta usar este mecanismo es lo mismo que cualquier otro mecanismo de abstracción (como las funciones con parámetros por ejemplo) es la posibilidad de lograr el mismo resultado con menor esfuerzo (En este caso, escribiendo menos código, ya que tal vez te exija pensar un poquito más que de la otra forma).

    Un buen programador siempre está ávido de nuevas formas de conseguir esto.

    Menos código implica menos probabilidades de dejar errores molestos e indetectables, es decir, menos fines de semana y noches sin dormir arreglando el sistema.

    ¿Qué usos interesantes le ves a este mecanismo?

  • Cómo interactuar con Excel desde PHP

    Cómo interactuar con Excel desde PHP

    Un problema bastante común en el desarrollo de aplicaciones para empresas es la interacción con Excel (Levantar datos, generar planillas, etc…).

    Un modo sencillo de evadir el problema (y que muchas veces funciona), es exportar la información hacia archivos de texto separados por comas (Los famosos CSV) y luego tratarlos desde PHP con funciones como fgetcsv.

    Obviamente, esto servirá si se trata de una aplicación que sólo requiere leer datos… ¿Qué pasa si necesitamos generarlos? (O si la estructura de la planilla en cuestión es algo más compleja).

    Como en la mayoría de los casos, existen diversas opciones de solución. Una particularmente buena es la librería PHPExcel PHPSpreadSheet.

    Generar un archivo Excel desde PHP

    Un detalle importante de la librería PHPSpreadSheet es que permite interactuar con planillas de cálculo diversas (xls, xlsx, Gnumeric… ¡hasta CSV!), con lo cual, como podrás imaginar, al escribir (o leer) un archivo habrá que aclarar en qué formato se lo quiere.

    Veamos un ejemplo:

    <?php
    
    use PhpOffice\PhpSpreadsheet\IOFactory;
    use PhpOffice\PhpSpreadsheet\Spreadsheet;
    
    $spreadsheet = new Spreadsheet();
    
    $spreadsheet->setActiveSheetIndex(0)
        ->setCellValue('A1', 'Hola')
        ->setCellValue('B2', 'Mundo!')
        ;
    
    $writer = IOFactory::createWriter($spreadsheet, 'Xls');
    $writer->save('salida.xls');

    En este caso estamos usando dos clases (PhpOffice\PhpSpreadsheet\Spreadsheet y PhpOffice\PhpSpreadsheet\IOFactory).

    La primera es la planilla propiamente dicha, la segunda tiene el objetivo de crear un objeto capaz de escribir esa planilla al formato específico (En este caso, xls).

    Una vez obtenido el objeto PhpOffice\PhpSpreadsheet\Writer\Xls (Resultado de la llamada a IOFactory::createWriter) podemos usar su método save para efectivamente generar la planilla.

    Veamos otro ejemplo:

    <?php
    
    use PhpOffice\PhpSpreadsheet\IOFactory;
    use PhpOffice\PhpSpreadsheet\Spreadsheet;
    
    $spreadsheet = new Spreadsheet();
    
    $spreadsheet->setActiveSheetIndex(0)
        ->setCellValue('A1', 'Hola')
        ->setCellValue('B2', 'Mundo!')
        ;
    
    header('Content-Type: application/vnd.ms-excel');
    header('Content-Disposition: attachment;filename="salida.xls"');
    
    $writer = IOFactory::createWriter($spreadsheet, 'Xls');
    $writer->save('php://output');

    Aquí, en lugar de escribir el archivo salida.xlsen el servidor, lo estamos ofreciendo para que el cliente lo descargue (Algo muy útil si se trata de exportar un reporte desde nuestra aplicación).

    Un tercer ejemplo:

    <?php
    
    use PhpOffice\PhpSpreadsheet\IOFactory;
    use PhpOffice\PhpSpreadsheet\Spreadsheet;
    
    $spreadsheet = new Spreadsheet();
    
    $spreadsheet->setActiveSheetIndex(0)
        ->setCellValue('A1', 'Hola')
        ->setCellValue('B2', 'Mundo!')
        ;
    
    header('Content-Type: application/vnd.ms-excel');
    header('Content-Disposition: attachment;filename="salida.xlsx"');
    
    $writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
    $writer->save('php://output');

    Es muy sutil la diferencia, por si no lo notaste, el objeto writer que se creará será de tipo  PhpOffice\PhpSpreadsheet\Writer\Xlsx en lugar de  PhpOffice\PhpSpreadsheet\Writer\Xls.

    ¡Esa pequeña «x» hace una gran diferencia!

    Los formatos xls cayeron en desuso a partir de la versión 2003 de Excel, el xlsx es un formato diferente (basado en XML y comprimido), con lo cual, será muy diferente el resultado obtenido usando uno u otro writer… ¡Ojito! 😉

    Leer un archivo Excel usando PHP

    La lectura de un archivo Excel es bastante similar:

    <?php
    
    use PhpOffice\PhpSpreadsheet\IOFactory;
    
    $spreadsheet = IOFactory::load('entrada.xls');
    $sheetData = $spreadsheet->getActiveSheet()->toArray(null, true, true, true);
    var_dump($sheetData);

    En este caso, la clase PhpOffice\PhpSpreadsheet\IOFactoryintentará «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 PhpOffice\PhpSpreadsheet\Reader\Xls;
    
    $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).

  • Cuál es la diferencia entre isset() y empty()

    Cuál es la diferencia entre isset() y empty()

    Empiezo por las formalidades:

    Ambas son funciones del lenguaje.

    isset informa si un identificador (el nombre de una variable) está definido o no dentro del hilo de ejecución actual (Muy importante entender esto, no se trata de que haya sido definido dentro del archivo actual).

    empty indica si el valor asociado a un identificador es o no vacío (La definición de vacío de PHP tiene sus peculiaridades como te voy a mostrar más adelante).

    Veamos algunos ejemplos para aclarar el punto:

    $a = 0;
    
    if (empty($a)) {
      echo 'empty'.PHP_EOL;
    }
    
    if (isset($a)) {
      echo 'isset'.PHP_EOL;
    }

    Este código dará como salida:

    empty
    
    isset

    Si se lo ejecuta desde la línea de comandos.

    Este otro

    if (isset($a)) {
      echo 'isset'.PHP_EOL;
    }
    
    $a = 0;
    
    if (empty($a)) {
      echo 'empty'.PHP_EOL;
    }

    Mostrará en cambio:

    empty

    Ya que la variable $a recién se define después de la primera verificación.

    Es interesante ver el siguiente ejemplo:

    if (isset($a)) {
      echo 'isset'.PHP_EOL;
    }
    
    if (empty($a)) {
      echo 'empty'.PHP_EOL;
    }

    Que también dará como resultado

    empty

    En definitiva, podrías pensar que !isset() es un caso especial de empty().

    Respecto de los valores que PHP considera como vacío, son estos:

    • 0
      null
      false
      ""
      "0"
      []
      array()
      0.0

    A pesar de que la diferencia parezca sutil, existen casos de uso muy diferentes para cada una.

    Una técnica que he visto usada alguna vez para verificar si un archivo ha sido accedido en forma directa o no es verificar si un identificador ha sido definido.

    Algo como:

    primero.php

    <?php
    
    $a = "1";
    
    require_once "segundo.php";
    

    segundo.php

    <?php
    
    if ( isset($a) ) {
       ... // Hacer algo util
    } else {
       die ('Acceso no autorizado');
    }

    No sé si es la forma que más me convence, pero funciona. Si alguien quiere acceder ilegalmente a segundo.php no podrá hacerlo.

    Un caso de uso deempty sería cuando una variable puede tomar cualquiera de los valores considerados vacíos, pero a los efectos de nuestro script, cualquiera de ellos vale… usando empty la expresión if será mucho más clara que una que tenga todos los || posibles (o bien algo como !in_array()). La desventaja clara de este enfoque es que depende de saber muy bien cuáles son los valores que PHP considera como vacíos… Usualmente yo prefiero dejar la lógica lo más explícita que pueda para evitar sorpresas.

    ¿Me olvidé de algo importante?

  • ¿Qué es PHP Composer? Todo lo que Necesitas Saber

    ¿Qué es PHP Composer? Todo lo que Necesitas Saber

    Estás desarrollando una aplicación PHP y te enfrentás a un problema que seguro alguien ya resolvió antes.

    Para qué re-inventar la rueda, ¿cierto?

    Abrís una nueva pestaña, entrás a Google y escribís «Cómo hacer X con php».

    La lista es larga, pero hay un factor común en todos los ejemplos: composer.

    Ahí está otra vez.

    Composer por aquí, composer por allá.

    Por todos lados te muestran cosas como:

    php composer require base/demo-base-code

    Como quien dice «Buenos días».

    Y, a pesar de no ser precisamente un newbie, te frustra tener que copiar y pegar… y rezar.

    Necesitás entender qué es composer de una buena vez.

    Así que… ¿de qué se trata el famoso php composer?

    Qué es Composer

    Composer es un gestor de dependencias para PHP (Similar a lo que npm es para JavaScript o pip para Python).

    Es una aplicación PHP que ayuda a administrar las librerías desarrolladas por terceros que vas a incorporar a tu proyecto.

    De algún modo podrías considerarlo como el heredero de PEAR.

    Hoy día se considera el gestor de dependencias de-facto por unas cuantas buenas razones:

    • Es muy simple de operar
    • Cuenta con un repositorio super completo (Packagist)
    • Disminuye significativamente los problemas de cambio de ambiente de ejecución (Mediante su funcionalidad de congelar dependencias)

    Si nunca lo usaste, te recomiendo que lo pruebes.

    Cómo se instala Composer

    La instalación de composer es sumamente simple. Como dice el sitio getcomposer.org en su página de descargas:

    php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
    php -r "if (hash_file('SHA384', 'composer-setup.php') === '544e09ee996cdf60ece3804abc52599c22b1f40f4323403c44d44fdfdd586475ca9813a858088ffbc1f233e9b180f061') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
    php composer-setup.php
    php -r "unlink('composer-setup.php');"

    A partir de ahí tendrás disponible un comando llamado composer (Qué sorpresa ¿no?). Si lo ejecutas sin decir más nada te va a dar una lista de los sub-comandos disponibles:

      about           Shows the short information about Composer.
      archive         Creates an archive of this composer package.
      browse          Opens the package's repository URL or homepage in your browser.
      clear-cache     Clears composer's internal package cache.
      clearcache      Clears composer's internal package cache.
      config          Sets config options.
      create-project  Creates new project from a package into given directory.
      depends         Shows which packages cause the given package to be installed.
      diagnose        Diagnoses the system to identify common errors.
      dump-autoload   Dumps the autoloader.
      dumpautoload    Dumps the autoloader.
      exec            Executes a vendored binary/script.
      global          Allows running commands in the global composer dir ($COMPOSER_HOME).
      help            Displays help for a command
      home            Opens the package's repository URL or homepage in your browser.
      info            Shows information about packages.
      init            Creates a basic composer.json file in current directory.
      install         Installs the project dependencies from the composer.lock file if present, or falls back on the composer.json.
      licenses        Shows information about licenses of dependencies.
      list            Lists commands
      outdated        Shows a list of installed packages that have updates available, including their latest version.
      prohibits       Shows which packages prevent the given package from being installed.
      remove          Removes a package from the require or require-dev.
      require         Adds required packages to your composer.json and installs them.
      run-script      Runs the scripts defined in composer.json.
      search          Searches for packages.
      self-update     Updates composer.phar to the latest version.
      selfupdate      Updates composer.phar to the latest version.
      show            Shows information about packages.
      status          Shows a list of locally modified packages.
      suggests        Shows package suggestions.
      update          Updates your dependencies to the latest version according to composer.json, and updates the composer.lock file.
      validate        Validates a composer.json and composer.lock.
      why             Shows which packages cause the given package to be installed.
      why-not         Shows which packages prevent the given package from being installed.

    Te voy a mostrar algunas pocas cosas como para que veas el poder que tiene:

    Cómo funciona Composer

    Composer trabaja con dos archivos de configuración: composer.json y composer.lock.

    El segundo es más bien de uso interno así que no hace falta conocer muchos detalles sobre él.

    El primero en cambio es el que vas a usar bastante a menudo ya que en él se declaran todas las dependencias que tu sistema tendrá.

    Es muy simple de leer (al igual que cualquier json) y permite una gran flexibilidad para definir las dependencias (por ejemplo se puede decir a partir de qué versión se acepta) tanto de librerías escritas en php como del intérprete y sus extensiones.

    Típicamente un archivo composer.json se ve más o menos así:

    {
      "name": "leeway/scrapper",
      "description": "Un pequeño web scrapper",
      "minimum-stability": "stable",
      "license": "proprietary",
      "authors": [
        {
          "name": "Mauro Chojrin",
          "email": "mauro.chojrin@leewayweb.com"
        }
      ],
      "require": {
        "fabpot/goutte": "v3.2.0"
      }
    }

    Donde la parte más importante es lo que está dentro de require (Las dependencias específicas del proyecto).

    Se puede crear el archivo «manualmente» o, mejor, se puede usar el comando composer init que lo hace por nosotros (y aparte permite definir algunas propiedades en forma interactiva).

    Pero claramente, de poco sirve la declaración de las dependencias si no tenemos las librerías instaladas…

    Cómo instalar librerías usando Composer

    Para instalar librerías composer dispone del comando install, el cual se encarga de bajarlas, armar el autoloading y demás… ¡magia!.

    Lo único que te queda por hacer es require_once 'vendor/autoload.php'; donde vayas a usar las librerías instaladas y voilà.

    Hasta acá todo muy lindo, se pueden instalar y usar dependencias con muy poco esfuerzo pero… ¿Te pasó alguna vez que desarrollaste un sistema, en tu computadora andaba 10 puntos pero al subirlo a producción daba algún error raro?

    ¿Te pasó estar horas dando vueltas sin entender nada hasta que te diste cuenta de que la versión 3.4.1 de la librería de envío de mails no era compatible con la 3.4.0 (Instalada en producción)?

    Justamente para eso composer tiene el archivo composer.lock. Si bien es un archivo de texto, ¡no lo toques!.

    Te muestro el que corresponde al composer.json que veías:

    {
        "_readme": [
            "This file locks the dependencies of your project to a known state",
            "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
            "This file is @generated automatically"
        ],
        "content-hash": "cc70bd9c59b4fdbb7e8e632402e15bcc",
        "packages": [
            {
                "name": "fabpot/goutte",
                "version": "v3.2.0",
                "source": {
                    "type": "git",
                    "url": "https://github.com/FriendsOfPHP/Goutte.git",
                    "reference": "8cc89de5e71daf84051859616891d3320d88a9e8"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/8cc89de5e71daf84051859616891d3320d88a9e8",
                    "reference": "8cc89de5e71daf84051859616891d3320d88a9e8",
                    "shasum": ""
                },
                "require": {
                    "guzzlehttp/guzzle": "^6.0",
                    "php": ">=5.5.0",
                    "symfony/browser-kit": "~2.1|~3.0",
                    "symfony/css-selector": "~2.1|~3.0",
                    "symfony/dom-crawler": "~2.1|~3.0"
                },
                "type": "application",
                "extra": {
                    "branch-alias": {
                        "dev-master": "3.2-dev"
                    }
                },
                "autoload": {
                    "psr-4": {
                        "Goutte\\": "Goutte"
                    }
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Fabien Potencier",
                        "email": "fabien@symfony.com"
                    }
                ],
                "description": "A simple PHP Web Scraper",
                "homepage": "https://github.com/FriendsOfPHP/Goutte",
                "keywords": [
                    "scraper"
                ],
                "time": "2016-11-15T16:27:29+00:00"
            },
            {
                "name": "guzzlehttp/guzzle",
                "version": "6.3.0",
                "source": {
                    "type": "git",
                    "url": "https://github.com/guzzle/guzzle.git",
                    "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699",
                    "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699",
                    "shasum": ""
                },
                "require": {
                    "guzzlehttp/promises": "^1.0",
                    "guzzlehttp/psr7": "^1.4",
                    "php": ">=5.5"
                },
                "require-dev": {
                    "ext-curl": "*",
                    "phpunit/phpunit": "^4.0 || ^5.0",
                    "psr/log": "^1.0"
                },
                "suggest": {
                    "psr/log": "Required for using the Log middleware"
                },
                "type": "library",
                "extra": {
                    "branch-alias": {
                        "dev-master": "6.2-dev"
                    }
                },
                "autoload": {
                    "files": [
                        "src/functions_include.php"
                    ],
                    "psr-4": {
                        "GuzzleHttp\\": "src/"
                    }
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Michael Dowling",
                        "email": "mtdowling@gmail.com",
                        "homepage": "https://github.com/mtdowling"
                    }
                ],
                "description": "Guzzle is a PHP HTTP client library",
                "homepage": "http://guzzlephp.org/",
                "keywords": [
                    "client",
                    "curl",
                    "framework",
                    "http",
                    "http client",
                    "rest",
                    "web service"
                ],
                "time": "2017-06-22T18:50:49+00:00"
            },
            {
                "name": "guzzlehttp/promises",
                "version": "v1.3.1",
                "source": {
                    "type": "git",
                    "url": "https://github.com/guzzle/promises.git",
                    "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
                    "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
                    "shasum": ""
                },
                "require": {
                    "php": ">=5.5.0"
                },
                "require-dev": {
                    "phpunit/phpunit": "^4.0"
                },
                "type": "library",
                "extra": {
                    "branch-alias": {
                        "dev-master": "1.4-dev"
                    }
                },
                "autoload": {
                    "psr-4": {
                        "GuzzleHttp\\Promise\\": "src/"
                    },
                    "files": [
                        "src/functions_include.php"
                    ]
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Michael Dowling",
                        "email": "mtdowling@gmail.com",
                        "homepage": "https://github.com/mtdowling"
                    }
                ],
                "description": "Guzzle promises library",
                "keywords": [
                    "promise"
                ],
                "time": "2016-12-20T10:07:11+00:00"
            },
            {
                "name": "guzzlehttp/psr7",
                "version": "1.4.2",
                "source": {
                    "type": "git",
                    "url": "https://github.com/guzzle/psr7.git",
                    "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
                    "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
                    "shasum": ""
                },
                "require": {
                    "php": ">=5.4.0",
                    "psr/http-message": "~1.0"
                },
                "provide": {
                    "psr/http-message-implementation": "1.0"
                },
                "require-dev": {
                    "phpunit/phpunit": "~4.0"
                },
                "type": "library",
                "extra": {
                    "branch-alias": {
                        "dev-master": "1.4-dev"
                    }
                },
                "autoload": {
                    "psr-4": {
                        "GuzzleHttp\\Psr7\\": "src/"
                    },
                    "files": [
                        "src/functions_include.php"
                    ]
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Michael Dowling",
                        "email": "mtdowling@gmail.com",
                        "homepage": "https://github.com/mtdowling"
                    },
                    {
                        "name": "Tobias Schultze",
                        "homepage": "https://github.com/Tobion"
                    }
                ],
                "description": "PSR-7 message implementation that also provides common utility methods",
                "keywords": [
                    "http",
                    "message",
                    "request",
                    "response",
                    "stream",
                    "uri",
                    "url"
                ],
                "time": "2017-03-20T17:10:46+00:00"
            },
            {
                "name": "psr/http-message",
                "version": "1.0.1",
                "source": {
                    "type": "git",
                    "url": "https://github.com/php-fig/http-message.git",
                    "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
                    "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
                    "shasum": ""
                },
                "require": {
                    "php": ">=5.3.0"
                },
                "type": "library",
                "extra": {
                    "branch-alias": {
                        "dev-master": "1.0.x-dev"
                    }
                },
                "autoload": {
                    "psr-4": {
                        "Psr\\Http\\Message\\": "src/"
                    }
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "PHP-FIG",
                        "homepage": "http://www.php-fig.org/"
                    }
                ],
                "description": "Common interface for HTTP messages",
                "homepage": "https://github.com/php-fig/http-message",
                "keywords": [
                    "http",
                    "http-message",
                    "psr",
                    "psr-7",
                    "request",
                    "response"
                ],
                "time": "2016-08-06T14:39:51+00:00"
            },
            {
                "name": "symfony/browser-kit",
                "version": "v3.3.10",
                "source": {
                    "type": "git",
                    "url": "https://github.com/symfony/browser-kit.git",
                    "reference": "317d5bdf0127f06db7ea294186132b4f5b036839"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/symfony/browser-kit/zipball/317d5bdf0127f06db7ea294186132b4f5b036839",
                    "reference": "317d5bdf0127f06db7ea294186132b4f5b036839",
                    "shasum": ""
                },
                "require": {
                    "php": "^5.5.9|>=7.0.8",
                    "symfony/dom-crawler": "~2.8|~3.0"
                },
                "require-dev": {
                    "symfony/css-selector": "~2.8|~3.0",
                    "symfony/process": "~2.8|~3.0"
                },
                "suggest": {
                    "symfony/process": ""
                },
                "type": "library",
                "extra": {
                    "branch-alias": {
                        "dev-master": "3.3-dev"
                    }
                },
                "autoload": {
                    "psr-4": {
                        "Symfony\\Component\\BrowserKit\\": ""
                    },
                    "exclude-from-classmap": [
                        "/Tests/"
                    ]
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Fabien Potencier",
                        "email": "fabien@symfony.com"
                    },
                    {
                        "name": "Symfony Community",
                        "homepage": "https://symfony.com/contributors"
                    }
                ],
                "description": "Symfony BrowserKit Component",
                "homepage": "https://symfony.com",
                "time": "2017-10-02T06:42:24+00:00"
            },
            {
                "name": "symfony/css-selector",
                "version": "v3.3.10",
                "source": {
                    "type": "git",
                    "url": "https://github.com/symfony/css-selector.git",
                    "reference": "07447650225ca9223bd5c97180fe7c8267f7d332"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/symfony/css-selector/zipball/07447650225ca9223bd5c97180fe7c8267f7d332",
                    "reference": "07447650225ca9223bd5c97180fe7c8267f7d332",
                    "shasum": ""
                },
                "require": {
                    "php": "^5.5.9|>=7.0.8"
                },
                "type": "library",
                "extra": {
                    "branch-alias": {
                        "dev-master": "3.3-dev"
                    }
                },
                "autoload": {
                    "psr-4": {
                        "Symfony\\Component\\CssSelector\\": ""
                    },
                    "exclude-from-classmap": [
                        "/Tests/"
                    ]
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Jean-François Simon",
                        "email": "jeanfrancois.simon@sensiolabs.com"
                    },
                    {
                        "name": "Fabien Potencier",
                        "email": "fabien@symfony.com"
                    },
                    {
                        "name": "Symfony Community",
                        "homepage": "https://symfony.com/contributors"
                    }
                ],
                "description": "Symfony CssSelector Component",
                "homepage": "https://symfony.com",
                "time": "2017-10-02T06:42:24+00:00"
            },
            {
                "name": "symfony/dom-crawler",
                "version": "v3.3.10",
                "source": {
                    "type": "git",
                    "url": "https://github.com/symfony/dom-crawler.git",
                    "reference": "40dafd42d5dad7fe5ad4e958413d92a207522ac1"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/40dafd42d5dad7fe5ad4e958413d92a207522ac1",
                    "reference": "40dafd42d5dad7fe5ad4e958413d92a207522ac1",
                    "shasum": ""
                },
                "require": {
                    "php": "^5.5.9|>=7.0.8",
                    "symfony/polyfill-mbstring": "~1.0"
                },
                "require-dev": {
                    "symfony/css-selector": "~2.8|~3.0"
                },
                "suggest": {
                    "symfony/css-selector": ""
                },
                "type": "library",
                "extra": {
                    "branch-alias": {
                        "dev-master": "3.3-dev"
                    }
                },
                "autoload": {
                    "psr-4": {
                        "Symfony\\Component\\DomCrawler\\": ""
                    },
                    "exclude-from-classmap": [
                        "/Tests/"
                    ]
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Fabien Potencier",
                        "email": "fabien@symfony.com"
                    },
                    {
                        "name": "Symfony Community",
                        "homepage": "https://symfony.com/contributors"
                    }
                ],
                "description": "Symfony DomCrawler Component",
                "homepage": "https://symfony.com",
                "time": "2017-10-02T06:42:24+00:00"
            },
            {
                "name": "symfony/polyfill-mbstring",
                "version": "v1.6.0",
                "source": {
                    "type": "git",
                    "url": "https://github.com/symfony/polyfill-mbstring.git",
                    "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
                    "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
                    "shasum": ""
                },
                "require": {
                    "php": ">=5.3.3"
                },
                "suggest": {
                    "ext-mbstring": "For best performance"
                },
                "type": "library",
                "extra": {
                    "branch-alias": {
                        "dev-master": "1.6-dev"
                    }
                },
                "autoload": {
                    "psr-4": {
                        "Symfony\\Polyfill\\Mbstring\\": ""
                    },
                    "files": [
                        "bootstrap.php"
                    ]
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Nicolas Grekas",
                        "email": "p@tchwork.com"
                    },
                    {
                        "name": "Symfony Community",
                        "homepage": "https://symfony.com/contributors"
                    }
                ],
                "description": "Symfony polyfill for the Mbstring extension",
                "homepage": "https://symfony.com",
                "keywords": [
                    "compatibility",
                    "mbstring",
                    "polyfill",
                    "portable",
                    "shim"
                ],
                "time": "2017-10-11T12:05:26+00:00"
            }
        ],
        "packages-dev": [],
        "aliases": [],
        "minimum-stability": "stable",
        "stability-flags": [],
        "prefer-stable": false,
        "prefer-lowest": false,
        "platform": [],
        "platform-dev": []
    }

    Es un json por dentro (Bastante más extenso y complejo que el original) pero su objetivo es el de congelar las dependencias que efectivamente se están usando en tu código.

    Si mirás de nuevo el composer.json vas a notar que la versión de Goutte está expresada en forma explícita (v3.2.0) pero, si mirás un poco más profundamente (por ejemplo en el archivo vendor/fabpot/goutte/composer.json) verás que hay otras que no son tan estrictas:

    {
        "name": "fabpot/goutte",
        "type": "application",
        "description": "A simple PHP Web Scraper",
        "keywords": ["scraper"],
        "homepage": "https://github.com/FriendsOfPHP/Goutte",
        "license": "MIT",
        "authors": [
            {
                "name": "Fabien Potencier",
                "email": "fabien@symfony.com"
            }
        ],
        "require": {
            "php": ">=5.5.0",
            "symfony/browser-kit": "~2.1|~3.0",
            "symfony/css-selector": "~2.1|~3.0",
            "symfony/dom-crawler": "~2.1|~3.0",
            "guzzlehttp/guzzle": "^6.0"
        },
        "autoload": {
            "psr-4": { "Goutte\\": "Goutte" }
        },
        "extra": {
            "branch-alias": {
                "dev-master": "3.2-dev"
            }
        }
    }

    Aquí dice que se necesita una versión de Guzzle al menos 6.0.

    Si ves en composer.lock notarás que la instalación de Guzzle es clara:

    ...
            {
                "name": "guzzlehttp/guzzle",
                "version": "6.3.0",
    ...
            }

    Y es que 6.3.0 es al menos 6.0 pero, al momento de distribuir el código necesitamos saber exactamente qué versión estamos usando.

    De modo que el composer.lock se genera:

    1. Recorriendo el árbol de dependencias completo
    2. Especificando las versiones concretas que están instaladas actualmente

    De esta forma, si el código se lleva a un entorno nuevo (o hay que reinstalarlo por alguna razón), la información en composer.lock asegura que se esté usando exactamente el mismo set de dependencias que se usó en testing (Si el testing no fue suficiente es otro tema…).

    Actualización de dependencias

    Por otro lado, si querés probar lo último disponible podés (¡en desarrollo!) ejecutar el comando composer update y composer se encargará nuevamente de descargar las nuevas versiones de tus dependencias (siempre respetando tus especificaciones) para que puedas probarlas y generar un nuevo composer.lock (Que obviamente vas a tener que committear).

    Remoción de dependencias

    Esto puede ser algo más tricky (Siempre es más fácil meter cosas que no se usarán que sacarlas y arriesgarse pero… si sabés lo que hacés…). El comando que se usa para esto es composer remove.

    ¡A usarlo con juicio!

    Y si necesitas ayuda…

    Siempre tenés la posibilidad de ejecutar el comando composer help <comando>.

    Conclusión

    Ahora ya sabés qué es Composer y cómo te puede simplificar la vida.

    La próxima vez que vayas a leer un ejemplo de código comprenderás los pasos de la instalación y podrás leer el archivo composer.json para ver qué otras dependencias vienen incluidas.

    ¿Todavía no descargaste composer?

    ¡Hacelo ahora mismo!

    Acá te dejo el link otra vez, ya vas a tener tiempo de volver acá a agradecerme.