Categoría: Cómo hacer para…

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

  • Cómo eliminar registros antiguos usando PHP y MySQL

    Me llega este correo de un ex alumno:

     

    Mauro como estás? Espero que muy bien!

    Sigo con el proyecto del centro cultural y quisiera saber si hay alguna forma de generar un script en sql que me borre los eventos que cargue a los cuales ya les haya pasado la fecha.
    Saludos,

     

    Si quisiéramos adherir estrictamente a la pregunta, la respuesta sería «Sí»… pero sería un poco mala persona si lo dejara ahí, ¿cierto? 🙂

    Voy a profundizar un poco para no dejar a nadie con la intriga.

    Una forma de resolver el problema desde sql (Asumo que se trata de MySQL) sería generar un procedimiento almacenado o directamente tener a mano una consulta que hiciera lo que se busca: borrar los eventos a los que les haya pasado la fecha.

    Pasando un poco en limpio, se cuenta con una tabla de eventos que tiene, al menos, un campo fecha. Para simplificar un poco el escenario, asumiré que el campo fecha es efectivamente de tipo date o datetime (Se puede resolver aún si no es este el caso, pero sería bastante más complejo).

    Lo que necesitamos es saber cuáles de estos eventos tienen una fecha que sea anterior a la actual, para lo cuál la pregunta es ¿cómo podemos saber qué día es hoy usando SQL? MySQL tiene para esto una función muy útil: CURDATE().

    De modo que todo el problema (o bueno, una gran parte) se limita a ejectuar una consulta de tipo:

    DELETE FROM eventos WHERE fecha < CURDATE()

    Con esto se logra eliminar todos los registros de la tabla eventos cuya fecha sea anterior a la actual… claro que esta consulta debe ser ejecutada periódicamente si queremos que el sistema se autodepure.

    Una buena forma de lograr este segundo propósito es crear un script que corra mediante un cronjob.

    Un modo muy simple de crear este script es utilizar la interface de línea de comandos de MySQL. Ejemplo:

    #!/bin/sh
    
    mysql -u centro -pcentro -e 'DELETE FROM eventos WHERE fecha < CURDATE()'

    Todo esto se puede guardar en un archivo limpiar_eventos.sh darle permisos de ejecución y agregarlo al directorio /etc/cron.daily/ y listo, todos los días se ejecutará este script y la tabla de eventos se encontrará limpia constantemente.

    Algunas consideraciones

    1. Me tomé la libertad de asumir que el script (y la aplicación entera) estarán alojadas en algún servidor tipo POSIX (Linux, OpenBSD, etc…), en caso de usar Windows no es que cambie demasiado (Será un .bat en lugar de un .sh) pero como hace mucho que dejé de trabajar con Windows no estoy 100% familiarizado con cómo crear tareas programadas (En alguna época el Panel de Control tenía algo que ver…).
    2. Si bien este script va a funcionar OK no es muy elegante y, sobre todo, no me gusta mucho el tema de dejar lógica de la aplicación fuera del propio código. Se podría hacer un script PHP que ejecute esta misma consulta (Puntos extra si se hace a través de las mismas clases que maneja la aplicación, por ejemplo usando un comando de consola de Symfony)

    Más información

    La combinación PHP+MySQL es una de las más comunes que se encuentran en la web. Si ya conocés algo de PHP y te interesa avanzar hacia el desarrollo de aplicaciones comerciales (Carritos de compra, clasificados o ese tipo de sitios) este curso te puede resultar interesante.

  • Cómo iniciar sesión en una web externa utilizando PHP

    Cómo iniciar sesión en una web externa utilizando PHP

    Me llegó esta pregunta a través de un grupo de Facebook en el que participo:

    Quiero iniciar sesión en una web mediante curl con php. ¿Cómo puedo modificar este php?

    El tema me recuerda un poco lo que escribí sobre obtener información de un sitio que no ofrece una API pero, si bien el problema es similar no es exactamente el mismo.

    La idea aquí es simple, se trata de obtener programáticamente la información que podemos obtener usando un navegador para entrar a algún sitio.

    Recientemente un amigo tiene en una farmacia me pedía ayuda para lograr algo bastante parecido: lo que él buscaba era ingresar al sitio de la droguería (su proveedor) y consultar el stock de un determinado medicamento (Algo que puede hacer muy sencillamente ingresando manualmente al sitio… imaginate que hacer una consulta por una buena cantidad de productos no es para nada práctico).

    Pues bien, veamos cómo resolver este problema:

    Para empezar, debemos entender que el sitio target está diseñado pensando en que será accedido mediante un navegador web, con lo cual, está esperando recibir peticiones HTTP (y sus respuestas serán HTML).

    Si conocés un poco cómo funciona HTTP y sus sesiones sabrás que en realidad no existe tal cosa… todo se simula utilizando cookies.

    cURL es una librería que permite realizar peticiones HTTP a bastante bajo nivel. Si bien no es la única opción (y probablemente no sea la mejor tampoco), voy a usarla para este ejemplo para dar respuesta a la pregunta original:

    Lo primero que necesitaremos hacer es realizar una petición al sitio objetivo.

    Seguramente en alguna parte de este sitio exista un formulario de login… algo que se vea más o menos así:

    En realidad, lo que nosotros queremos no es la URL de este formulario. Lo que buscamos es la dirección del receptor del formulario, ya que lo que haremos será simular un envío.

    Para ver eso simplemente necesitamos ver el código fuente de la página (CTRL+U) donde encontraremos algo como:

    <form action="http://dominio.com/login/logear" method="post">
       <input name="user" type="text"/>
       <input name="pass" type="password"/>
    </form>

    Con lo cual, ya sabemos que nuestros datos serán enviados a http://dominio.com/login/logear.

    Ahora sí, veamos algo de PHP 🙂

    <?php
    
    $processLoginURL = 'http://dominio.com/login/logear';
    
    $userName = 'xxxxx';
    $password = 'yyyyy';
    
    $ch = curl_init($processLoginURL);
    curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
    curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS,
        "user=$userName&pass=$password");
    
    $res = curl_exec( $ch );
    echo curl_getinfo( $ch, CURLINFO_HTTP_CODE);
    if ( $error = curl_error( $ch ) ) {
        die ($error);
    }
    echo $res;
    curl_close( $ch );

    Este código dará como resultado la página que se obtiene luego de hacer login (asumiendo que los datos son correctos).

    Pero lo realmente importante aquí es tener la cookie de sesión que nos permita hacer nuevas peticiones como un usuario autenticado.

    Veamos el código mejorado:

    <?php
    
    $processLoginURL = 'http://dominio.com/login/logear';
    
    $userName = 'xxxxx';
    $password = 'yyyyy';
    
    $ch = curl_init($processLoginURL);
    curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
    curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS,
        "user=$userName&pass=$password");
    curl_setopt( $ch, CURLOPT_COOKIEJAR, __DIR__.'/cookies.txt' );
    
    $res = curl_exec( $ch );
    echo curl_getinfo( $ch, CURLINFO_HTTP_CODE);
    if ( $error = curl_error( $ch ) ) {
        die ($error);
    }
    echo $res;
    curl_close( $ch );

    Al agregar la línea curl_setopt( $ch, CURLOPT_COOKIEJAR, __DIR__.'/cookies.txt' ); estamos indicando a cURL que almacene las cookies en el archivo cookies.txt.

    Una vez ejecutado este código, el archivo se verá similar a:

    # Netscape HTTP Cookie File
    # http://curl.haxx.se/docs/http-cookies.html
    # This file was generated by libcurl! Edit at your own risk.
    
    dominio.com   FALSE   /       FALSE   0       PHPSESSID       a9e5bdbdf0e28755c3f558222858f39a
    

    A partir de aquí, el desafío es usar esta cookie en pedidos subsiguientes.

    Como decía, nos interesaba entrar a ver información de stock, si vamos a la URI /stock nos encontramos con algo como:

    Así que nuestro próximo pedido será a http://dominio.com/stock (Información obtenida inspeccionando el HTML del form):

    curl_setopt( $ch, CURLOPT_URL, 'http://dominio.com/stock' );
    curl_setopt( $ch, CURLOPT_POSTFIELDS,
        "buscar[empieza_con]=A" );
    curl_setopt( $ch, CURLOPT_COOKIEFILE, __DIR__.'/cookies.txt' );
    $res = curl_exec( $ch );
    if ( $error = curl_error( $ch ) ) {
        die ($error);
    }

    Aquí la línea importante es curl_setopt( $ch, CURLOPT_COOKIEFILE, __DIR__.'/cookies.txt' ); con ella estamos indicando a cURL que tome el valor de las cookies de este archivo y lo envíe como parte del request.

    A la vuelta pues recibimos un HTML, por ejemplo:

    <table class=" zebra stockConsulta min3 tablesorter sortcompletecallback-callback-calczebra" id="fd-table-1" width="80%" cellspacing="0" cellpadding="2">
     <thead>
     <tr>
     <th><b>Cant</b></th><th> </th>
     <th class="sortable-text fd-column-2" style="width: 350px; -moz-user-select: none;"><a href="#" class="fdTableSortTrigger" title="Ordenar por “      Nombre”">      Nombre</a><span></span></th>
     <th> </th>
     <th class="d"><b>% Dto <br>S/Púb.</b></th>
     <th class="m sortable-text fd-column-5" style="-moz-user-select: none;"><a href="#" class="fdTableSortTrigger" title="Ordenar por “Oferta”">Oferta</a><span></span></th>
     <th class="d sortable-text fd-column-6" style="-moz-user-select: none;"><a href="#" class="fdTableSortTrigger" title="Ordenar por “Stock”">Stock</a><span></span></th>
     <th class="c"></th>
     
     <th class="d sortable-numeric fd-column-8" style="-moz-user-select: none;"><a href="#" class="fdTableSortTrigger" title="Ordenar por “Su Precio”">Su Precio</a><span></span></th>
     <th class="d sortable-numeric fd-column-9" style="-moz-user-select: none;"><a href="#" class="fdTableSortTrigger" title="Ordenar por “Su Preciocon Dto”">Su Precio<br>con Dto</a><span></span></th>
     <th class="d sortable-numeric fd-column-10" style="-moz-user-select: none;"><a href="#" class="fdTableSortTrigger" title="Ordenar por “Preciocon Bon”">Precio<br>con Bon</a><span></span></th>
     <th class="d " width="85"><b>$ Púb<br>Sugerido</b></th>
     </tr>
     </thead>
     <tbody>
     <tr class="even"> 
     <td width="25px"> <input maxlength="3" tabindex="1" autocomplete="off" class="cant" value="" name="cant[2168891]" type="text"></td>
     <td width="16px"><a href="#2168891" title="Buscar productos con las mismas monodrogas" class="siafCons"><img src="http://dominio.com/img/ico_siaf.png" alt="A ACIDO 005% CREMA X 10 GRS"></a></td>
     <td class="t1 filtro-b" onmouseover="ddrivetip('<center class=\'red\'></center><center><b>A ACIDO 005% CREMA X 10 GRS</b></center><b>Cod.Barras:</b> 7795327060006<br/><b>IVA:</b> No<br/><b>Proveedor:</b> DOMINGUEZ<br/><b>Rubro:</b> ESPECIALIDADES<br/><b>Troquel:</b> 2168891<br/>', 300,'#FFF','2168891',false); " onmouseout="hideddrivetip()"><a class="fbox" href="http://dominio.com/stock/verArticuloDetalle&alfabeta=2586&codigo=2168891">A ACIDO 005% CREMA X 10 GRS</a></td>
     <td class="t4"></td><td class="d">40,90</td>
     <td class="m"><b>PSL-14.29% Min.:7 Final</b></td>
     <td class="d">Si</td>
     <td class="c"></td>
     <td class="d ri">136,28</td>
     <td class="d ri">100,69</td>
     <td class="d"></td>
     <td class="d t2 ri" onmouseover="ddrivetip('<center><b>A ACIDO 005% CREMA X 10 GRS</b></center><b>Precio vigente desde: </b>08-02-2018 16:35:28<br/><b>Pre. Pub. anterior:</b> 163.33<br/><b>Variación:</b> 4.30%<br/>', 300,'#FFF');" onmouseout="hideddrivetip()"> 170,35</td>
     </tr>
     <tr class="odd">
     <td width="25px"> <input maxlength="3" tabindex="2" autocomplete="off" class="cant" value="" name="cant[2168971]" type="text"></td>
     <td width="16px"><a href="#2168971" title="Buscar productos con las mismas monodrogas" class="siafCons"><img src="http://dominio.com/img/ico_siaf.png" alt="A ACIDO 01% CREMA X 10 GRS"></a></td>
     <td class="t1 filtro-b" onmouseover="ddrivetip('<center class=\'red\'></center><center><b>A ACIDO 01% CREMA X 10 GRS</b></center><b>Cod.Barras:</b> 7795327060020<br/><b>IVA:</b> No<br/><b>Proveedor:</b> DOMINGUEZ<br/><b>Rubro:</b> ESPECIALIDADES<br/><b>Troquel:</b> 2168971<br/>', 300,'#FFF','2168971',false); " onmouseout="hideddrivetip()"><a class="fbox" href="http://dominio.com/stock/verArticuloDetalle&alfabeta=2587&codigo=2168971">A ACIDO 01% CREMA X 10 GRS</a></td>
     <td class="t4"></td><td class="d">40,90</td>
     <td class="m"><b>PSL-14.29% Min.:7 Final</b></td>
     <td class="d">Pocos</td>
     <td class="c"></td>
     <td class="d ri">161,30</td>
     <td class="d ri">119,18</td>
     <td class="d"></td>
     <td class="d t2 ri" onmouseover="ddrivetip('<center><b>A ACIDO 01% CREMA X 10 GRS</b></center><b>Precio vigente desde: </b>08-02-2018 16:35:28<br/><b>Pre. Pub. anterior:</b> 193.32<br/><b>Variación:</b> 4.30%<br/>', 300,'#FFF');" onmouseout="hideddrivetip()"> 201,63</td>
     </tr>
     </tbody>
    </table>

    Y ahora, ¿qué hacemos con esto? Bueno, una posibilidad es usar un parser de DOM (¿Qué tal SimpleDOM?) para buscar el elemento que quiera, almacenar el resultado en un archivo, enviarlo en un email… en fin, las posibilidades son variadas.

    Otra posibilidad para hacer lo mismo es usar alguna librería de más alto nivel que cURL, por ejemplo Guzzle o mejor el genial DomCrawler de Symfony.

    Es importante entender que, si bien esto va a funcionar, no es para nada recomendable ya que es un mecanismo muy endeble.

    Basta con que se modifique ligeramente el HTML para que la respuesta obtenida no sea la buscada (Ni hablar si llegan a cambiar las URLs)… siempre debemos preferir usar una API para este tipo de tareas (Asumiendo que esté disponible por supuesto :)).

    ¿Te quedó alguna duda? ¡Dejala en un comentario!

  • Cómo filtrar un arreglo multidimensional por clave en PHP

    Cómo filtrar un arreglo multidimensional por clave en PHP

    Me llegó esta pregunta a través de twitter:

    Veamos el arreglo en mayor detalle:

    Por lo que se ve, se trata de un array de arrays. El primer índice es por número y la segunda dimensión tiene índices string y, viendo el contenido del mismo entiendo que se trata del resultado de alguna consulta a una base de datos (Siendo que los elementos parecen tener la misma estructura y especialmente al haber un elemento llamado «id»).

    Muy bien, entonces el objetivo sería obtener sólo aquellos elementos en los cuales la columna «condial_39» vale 1, en este caso sería el array completo, pero obviamente este podría no ser el caso.

    Se me ocurren diferentes formas de lograr este objetivo:

    A la vieja usanza

    Simplemente se trata de hacer una recorrida del arreglo e ir recolectando los elementos buscados:

    <?php
    
    $r = [];
    foreach ( $array as $k => $v ) {        
            if ( $v['condial_39'] == 1 ) {
                    $r[$k] = $v;
            }
    }
    

    Al final de esta recorrida tendremos el resultado buscado en el arreglo $r.

    Nada espectacular pero da el resultado buscado.

    Usando un poco de programación funcional

    Ahora, viendo la pregunta original (que pide qué función de php se puede usar), se me ocurre un modo algo más elaborado, pero ciertamente más claro (y tal vez más eficiente):

    <?php
    
    $r = array_filter( $a, function( $e ) {
    
            return $e['condial_39'] == 1;
    });

    Aquí estamos usando la función array_filter en combinación con una función anónima que encapsula el criterio de filtro.

    Al fin de cuentas diría que es cuestión de gustos (Yo personalmente elijo la segunda opción).

    Ejemplo completo

    El código:

    <?php
    
    $a = [
            1 => [ 'id' => '23432aasasd', 'condial_39' => 1, 'nombre' => 'Juan' ],
            2 => [ 'id' => '26345awq2sd', 'condial_39' => 0, 'nombre' => 'Pedro'],
    ];
    
    $r = array_filter( $a, function( $e ) {
    
            return $e['condial_39'] == 1;
    });
    
    print_r($r);
    echo PHP_EOL;

    El resultado:

    ¿Tenés alguna pregunta para hacerme? Dejá un comentario, mandame un twit o un correo

  • Cómo restringir el acceso a una web según el país del visitante

    Cómo restringir el acceso a una web según el país del visitante

    Una persona hizo esta pregunta en un grupo de desarrolladores de Facebook y me atacó la curiosidad.

    Para empezar, se me ocurren dos formas de atacar el problema, dependiendo de tus conocimientos, hosting que estés usando, etc… En cualquiera de los casos, lo mejor que podremos hacer será filtrar el tráfico en base a la dirección IP desde la que nos están visitando (Cada país tiene un rango de direcciones IP asignadas, con lo cual, suele ser una medida suficientemente buena).

    De lo que se trata en definitiva es de verificar si la IP del visitante está dentro de las IPs permitidas y, en caso contrario redireccionarlo a algún otro lado o mostrarle algún mensaje especial.

    Probablemente sea más fácil tener una lista de IPs prohibidas que permitidas (Suelen ser menos las primeras que las segundas).

    Comencemos por la que va a funcionar en todos lados:

    Filtrar por IP usando PHP

    PHP tiene algunas herramientas útiles para saber cuál es la IP del visitante:

    La primera es ver el contenido de la variable $_SERVER["REMOTE_ADDR"].

    Salvo que la petición se haya realizado mediante un servidor proxy, este dato alcanzará para saber desde dónde nos están visitando.

    Una vez obtenida esta información, podemos usar algún listado de vinculación de IPs a países (uno que ya tengamos grabado o alguno tipo WebService como http://www.geoplugin.net/json.gp) para obtener la información que buscamos.

    Por ejemplo si hacemos una llamada tipo:

    echo file_get_contents('http://www.geoplugin.net/json.gp?ip=186.109.132.47');

    Obtendremos algo como:

    {
      "geoplugin_request":"186.109.132.47",
      "geoplugin_status":200,
      "geoplugin_credit":"Some of the returned data includes GeoLite data created by MaxMind, available from <a href='http:\/\/www.maxmind.com'>http:\/\/www.maxmind.com<\/a>.",
      "geoplugin_city":"Buenos Aires",
      "geoplugin_region":"Distrito Federal",
      "geoplugin_areaCode":"0",
      "geoplugin_dmaCode":"0",
      "geoplugin_countryCode":"AR",
      "geoplugin_countryName":"Argentina",
      "geoplugin_continentCode":"SA",
      "geoplugin_latitude":"-34.6033",
      "geoplugin_longitude":"-58.3816",
      "geoplugin_regionCode":"07",
      "geoplugin_regionName":"Distrito Federal",
      "geoplugin_currencyCode":"ARS",
      "geoplugin_currencySymbol":"&#36;",
      "geoplugin_currencySymbol_UTF8":"$",
      "geoplugin_currencyConverter":17.277
    }

    Y, si pasamos el resultado a través de json_decode obtendremos un arreglo del cual podremos simplemente tomar el índice geoplugin_countryCode y validar si está entre los prohibidos, por ejemplo:

    $data = json_decode( file_get_contents('http://www.geoplugin.net/json.gp?ip='.$argv[1]), true );
    
    if ( in_array( $data['geoplugin_countryCode'], [ 'AR', 'UY' ]  )  ) {
            echo 'No se admite gente de '.$data['geoplugin_countryName'];
    } else {
            echo 'Los visitantes de '.$data['geoplugin_countryName'].' siempre son bienvenidos :)';
    }

    Cómo filtrar un visitante que está detrás de un proxy

    En ocasiones la IP puede ser escondida detrás de un proxy. Para verificar si estamos ante esa situación debemos ver si existe en el arreglo $_SERVER una clave llamada HTTP_X_FORWARDED_FOR

    No cambia demasiado, sólo hay que tomar el valor de $_SERVER['HTTP_X_FORWARDED_FOR'] como dirección IP a validar, el resto de la validación sigue siendo igual.

    Filtrar por IP a través del WebServer

    Otra posibilidad es realizar el filtro a más bajo nivel, es decir, evitar que una IP no deseada siquiera llegue a ejecutar un archivo php en nuestro servidor.

    Para poder hacer esto se requiere tener acceso a la configuración del servidor web (o al menos poder usar archivos de configuración extra).

    No es lo más común en entornos de hosting compartido, pero si tenés la posibilidad de hacerlo, puede resultar más eficiente.

    El procedimiento exacto dependerá del servidor web con el que estés trabajando.

    En el caso de Apache, podés usar el archivo .htaccess o, mejor, hacerlo directo desde la configuración del servidor.

    Para ello puede usarse una configuración como la siguiente:

    <Directory /var/www/html/>
       Order allow,deny
       Deny from 185
    </Directory>

    De esta forma, cualquier visitante cuya IP comience con 185 tendrá el acceso negado al directorio /var/www/html/ y sus subdirectorios.

    ¿Te quedó alguna pregunta? ¡Espero tus comentarios!

  • Cómo tratar archivos comprimidos con PHP

    Cómo tratar archivos comprimidos con PHP

    Recientemente me tocó realizar una modificación a un sistema que había desarrollado para recibir un único archivo comprimido, en lugar de un conjunto de archivos en forma individual.

    Dejando de lado los ajustes hechos en el front-end (No fue gran cosa realmente, se trató de cambiar un formulario con 5 inputs por uno solo y, como lo había hecho usando el framework Symfony esa parte fue simple, ni tuve que tocar HTML), la parte interesante fue cómo procesar el archivo comprimido.

    Para empezar, algo que tuve que acordar con el usuario era el formato de compresión que íbamos a utilizar. Obviamente no es lo mismo descomprimir un archivo .rar que un .zip (Diferentes formatos, diferentes algoritmos de compresión, etc…).

    En mi caso no tuve problema porque tenía la posibilidad de definir cuál era el que más me convenía (a veces no tenemos esa suerte y debemos ajustarnos al contexto que nos toca), con lo cual, elegí el formato zip.

    Tomé esta determinación, principalmente, porque sabía que en PHP tenía buen soporte para usarlo (En general, no soy muy fanático de re-inventar la rueda… especialmente cuando hay deadlines involucrados :)).

    La pieza clave de todo este asunto es la clase ZipArchive.

    Se trata de una clase que muy probablemente tengas instalada (Si usás una distribución estándar de php) y, si no, tampoco es tan difícil instalarla.

    Varias cosas se pueden hacer con ella, pero las principales son: comprimir y descomprimir archivos.

    Descomprimir archivos zip

    Empiezo por la segunda porque es la que más probablemente te encuentres.

    Veamos un poco de código:

    $zip = new \ZipArchive();
    if ( $zip->open( $file ) ) {
        $tmp_dir = sys_get_temp_dir();
        $zip->extractTo($tmp_dir);
        for ($i = 0; $i < $zip->numFiles; $i++) {
            $originalName = $zip->getNameIndex($i);
            $movedFileName = $reports_dir.basename($originalName);
            rename($tmp_dir . DIRECTORY_SEPARATOR . $originalName, $movedFileName);
            $files[$originalName] = $movedFileName;
        }
        $zip->close();
    }

    Esta es la parte de mi sistema que trata con el archivo subido por el usuario:

    Primero creo una instancia de ZipArchive ($zip = new \ZipArchive();).

    Después abro el archivo (que se supone está comprimido debidamente):

    $zip->open( $file )

    Es importante hacer el chequeo con el if ya que, como cualquier otra operación de E/S, puede fallar (por ejemplo por falta de permisos, por formato incorrecto, etc…).

    Luego descomprimo (¡Lo que efectivamente quería hacer!):

    $zip->extractTo($tmp_dir);

    En mi caso, estoy extrayendo todo a un directorio temporal (Usando la función sys_get_temp_dir) para luego procesar los archivos contenidos en el zip uno por uno.

    Uso la propiedad numFiles ($zip->numFiles) para saber cuántos archivos estaban dentro del comprimido y voy tomándolos de a uno.

    Con $zip->getNameIndex($i) puedo obtener el nombre que tenía un determinado archivo antes de incorporarlo al comprimido.

    Por último estoy moviendo el archivo a otro directorio (donde almaceno todos los archivos que luego procesaré, pero eso ya es particular de mi aplicación):

    rename($tmp_dir . DIRECTORY_SEPARATOR . $originalName, $movedFileName);

    Por último, como con cualquier otro archivo, libero los recursos:

    $zip->close();

    En el caso de un archivo zip, esto es muy importante ya que los recursos ocupados pueden ser significativos.

    Comprimir archivos hacia un zip

    También me ha tocado en alguna ocasión realizar la operación inversa (Generar un archivo comprimido para enviar a un cliente a través de un WebService SOAP).

    Veamos algo de código:

    $zip = new ZipArchive();
    
    if ( $zip->open('codigo.zip',  ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE ) ) {
            $zip->addFile( 'table2.php' );
            $zip->addFile( 'table3.php' );
            $zip->addFile( 'table4.php' );
            $zip->close();
    }

    En este ejemplo, la clave está en el segundo parámetro que se pasa al método open: ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE aquí lo que se está haciendo es generar un número entero mediante una combinación binaria de otros dos (ZIPARCHIVE::CREATEZIPARCHIVE::OVERWRITE son dos constantes). Si bien esta no es una operación muy común en PHP, es perfectamente válida.

    En definitiva, se está indicando al método open que cree el archivo si no existe y que lo sobre-escriba en caso contrario.

    Y con eso está todo listo para agregar archivos al zip (¡que todavía no se generó!).

    Una vez ingresados todos los archivos requeridos, al ejecutar $zip->close() se escribe el resultado al disco.

    Agregar contraseña a un archivo zip

    Otra funcionalidad interesante que provee la clase ZipArchive es la de establecer una contraseña de descompresión, de modo que sólo quien la conozca sea capaz de obtener los contenidos del archivo.

    Esto se logra usando una combinación de métodos:

    1. setPassword para establecer la contraseña
    2. setEncryptionName (o setEncryptionIndex) para codificar los archivos una vez agregados.

    Ejemplo:

    $zip = new ZipArchive();
    if ($zip->open('test.zip', ZipArchive::CREATE) === TRUE) {
        $zip->setPassword('secret');
        $zip->addFile('text.txt');
        $zip->setEncryptionName('text.txt', ZipArchive::EM_AES_256);
        $zip->close();
        echo "Ok\n";
    } else {
        echo "KO\n";
    }
    

    De esta forma, para descomprimir el archivo se requiere conocer la contraseña (Y, si usamos un algoritmo como AES256, alguna utilidad moderna como 7z o WinZip):

    Este es un ejemplo de una clase que viene incorporada dentro de PHP (Existen unas cuantas), pero la riqueza de la Programación Orientada a Objetos con PHP no se agota ahí (¡Es sólo el comienzo de hecho!). En el curso Desarrollo de Grandes Aplicaciones Web con PHP aprenderás cómo sacarle provecho a toda la potencia que esta característica del lenguaje pone a tu disposición.

  • Cómo interactuar con MailChimp usando PHP

    Cómo interactuar con MailChimp usando PHP

    Uno de mis SaaS favoritos es MailChimp (El que uso para el envío de mis campañas de e-mail marketing y para algunos clientes también). Si bien su interface es algo rústica (A veces cuesta encontrar el modo de realizar ciertas tareas, sobre todo al comienzo cuando no se conoce bien), es sumamente funcional (Hasta tiene workflows automatizados).

    Pero, como siempre, las posibilidades que brinda de fábrica son más limitadas de lo que a un desarrollador le gustaría… afortunadamente, cuenta con una API muy buena que permite manipular todos los objetos del sistema y lograr algunas cosas que desde la interfaz web son complejas (o directamente imposibles).

    Algo que me sucedió recientemente (y que dio origen a este post) fue la necesidad de realizar un mismo envío (el cual incluyera el disparo de nuevos envíos dependiendo del comportamiento del usuario ante la lectura del envío original) en forma periódica.

    La parte de los envíos condicionales los resolví en forma bastante simple usando las automatizaciones propias de MailChimp. El problema que tenía era cómo realizar los envíos en forma periódica.

    Hurgando un poco en la documentación de la API llegué a la solución :).

    Lo primero fue buscar alguna librería php que me ayudara en la interacción con la API. Para ello dirigí mi browser hacia packagist.org y busqué «MailChimp» y encontré:

    Como no tenía ninguna otra referencia (ni estaba usando ningún framework en particular), decidí probar con la primera opción.

    Armé mi proyecto usando composer:

    mkdir mc
    cd mc
    composer init

    Y aproveché para declarar la dependencia en forma interactiva:

    Y una vez terminado el consabido composer install para que todo tenga sentido…

    Y ahora sí… ¡a codear!

    require_once 'vendor/autoload.php';
    
    use \DrewM\MailChimp\MailChimp;
    
    $MailChimp = new MailChimp('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-us15');
    

    Lo primero que se necesita para usar la API es autenticarse, para eso necesitás conocer tu API-Key. Ese dato lo sacás de la sección Extras de la configuración de tu cuenta:

    Si no tenés una API Key creada usá el botón de abajo, en mi caso ya la tenía, así que fue sólo copiar el contenido de la columna API key.

    Y ahí es empezar a jugar un poco.

    Por ejemplo, para saber qué listas tengo definidas:

    $result = $MailChimp->get('lists');
    
    print_r($result);

    Da algo como:

    Array
    (
        [lists] => Array
            (
                [1] => Array
                    (
                        [id] => 897874fd1b
                        [web_id] => 133557
                        [name] => Testing list
    ...
                    )
            )
    )

    Lo importante (entre todos los datos que devuelve la llamada) es el id de la lista (897874fd1b en este caso). Con este dato vamos a poder operar, por ejemplo, para obtener el listado de miembros:

    $result = $MailChimp->get('lists/897874fd1b/members');

    Una vez obtenida la lista de miembros, lo que quería hacer era sumarlos a la cola del envío automatizado. Para eso necesitaba hacer una llamada tipo POST (Estaba por agregar información en lugar de obtenerla), pero requería un par de ids (el del circuito de automatización y el de su primer correo):

    $result = $MailChimp->get('automations');

    Me dió el número 29f30d8f7a.

    Y:

    $result = $MailChimp->get('automations/29f30d8f7a/emails');

    Me dió el da174ffbca.

    Un punto importante, para que todo esto funcione, es la configuración de la automatización en MailChimp. El primer correo debe dispararse a partir de una llamada a la API:

    Una vez obtenidos todos los datos, sólo quedaba ingresar los mails en la cola:

    foreach ( $result['members'] as $member) {
        $email_address = $member['email_address'];
        echo "Enviando primer mail a ". $email_address;
        $MailChimp->post(
            'automations/29f30d8f7a/emails/da174ffbca/queue',
            [
                'email_address' => $email_address,
            ]
            );
    }

    Y dejar que MailChimp hiciera el resto.

    Conclusión

    La API de MailChimp es bastante simple de usar, adhiere de cerca al estándar RESTFul, logrando una interface limpia y muy funcional.

    El soporte para PHP está muy bien logrado, con lo cual, es muy sencillo integrar una aplicación desarrollada en este lenguaje con este potente motor de email marketing, así que… ¿para qué reinventar la rueda?

    Aclaración

    Claramente, para sacarle el máximo jugo hace falta conocer en profundidad MailChimp (y luego ver cómo integrarlo mediante la API), pero si ya conoces un poco la herramienta, esta posibilidad seguramente te abrira nuevas oportunidades.

  • Cómo hacer un PDF con PHP

    Cómo hacer un PDF con PHP

    Una necesidad bastante común en el desarrollo de aplicaciones basadas en web (Especialmente las que se usan dentro de ambientes corporativos) es la de emitir reportes.

    Una forma simple de resolver este problema es la generación de páginas html cuyo único objeto es ser impresas y dejar al usuario la decisión de guardarlas en lugar de imprimirlas.

    Si bien esta solución puede ser suficiente cuando se trata de emitir un reporte a un usuario, ciertamente no es lo ideal cuando se trata de intercambiarlo con algún otro sistema.

    Particularmente en este artículo voy a mostrarte algunas formas de generar archivos pdf usando PHP.

    Librería PDF de PHP

    Existe una extensión de PHP llamada PDF, la cual está basada en la librería PDFlib (Desafortunadamente, una librería propietaria).

    Esta librería debe ser instalada como una extensión PECL y su uso no es particularmente cómodo.

    Veamos un ejemplo:

    <?php 
    
    $pdf = pdf_new(); 
    pdf_open_file($pdf); 
    pdf_begin_page($pdf, 300, 300); 
    pdf_save($pdf); 
    pdf_translate($pdf, 100, 100); 
    pdf_rotate($pdf, 45); 
    pdf_rect($pdf, 0, 0, 20, 20); 
    pdf_stroke($pdf);
    pdf_end_page($pdf); 
    pdf_close($pdf);

    La librería provee un conjunto de funciones (no agrupadas en clases) y define un nuevo tipo de recurso (Similar a un archivo, sólo que específico para PDFs).

    A pesar de su aspecto rústico, PDF es probablemente la librería más versátil en cuanto a manejo de archivos de este tipo (y, a su vez, la de más bajo nivel).

    FPDF

    FPDF es un paquete sencillo. Se trata de una clase y una serie de archivos de fuentes (fonts) que pueden usarse para generar archivos pdf.

    Un ejemplo de su utilización es este:

    <?php
    require('fpdf.php');
    
    $pdf = new FPDF();
    $pdf->AddPage();
    $pdf->SetFont('Arial','B',16);
    $pdf->Cell(40,10,'¡Hola, Mundo!');
    $pdf->Output();
    

    El constructor de la clase recibe los parámetros para crear el marco de trabajo:

    • Orientación de la hoja (Portrait o Landscape)
    • Unidad de medida (mm para milímetros)
    • Tamaño de la página (A3, A4, A5)

    El método Output es el que efectivamente produce el pdf. Si no se aclara nada, la salida se envía al navegador, aunque puede ser redirigida a un archivo local para ser enviado a otro servidor (entre otras posibilidades):

    $pdf->Output('F', 'salida.pdf');

    El método Cell tiene que ver con el modo en que FPDF genera la salida: las páginas están divididas en celdas rectangulares contiguas.

    En todo momento existe un puntero a la posición actual dentro de la hoja y es esa posición la que se tomará como inicio de la celda sobre la que se quiera trabajar.

    En este ejemplo se está creando una nueva celda de 40 mm de ancho por 10 de alto y en ella se está escribiendo el texto «¡Hola, Mundo!» (Usando la tipografía Arial de 16 puntos y en negrita).

    Puedes consultar más información en FPDF

    mPDF

    mPDF es una librería algo más moderna. Su objetivo es presentarse como una opción de más alto nivel que FPDF.

    La principal diferencia es que mPDF toma textos HTML y los convierte en PDF, haciendo la tarea mucho más sencilla para el desarrollador (Aunque puede resultar bastante menos eficiente).

    Su instalación se realiza usando composer:

    composer require mpdf/mpdf

    Su uso puede ser tan sencillo como:

    <?php
    
    require_once __DIR__ . '/vendor/autoload.php';
    
    $mpdf = new \Mpdf\Mpdf();
    $mpdf->WriteHTML('<h1>¡Hola, Mundo!</h1>');
    $mpdf->Output();

    También es posible realizar ciertas configuraciones (como en FPDF) para, por ejemplo, definir que la hoja estará apaizada:

    $mpdf = new \Mpdf\Mpdf(['orientation' => 'L']);

    Puedes consultar la información completa en el manual de uso de mPDF.

    ¿Cuál es la mejor opción para generar PDFs con PHP?

    Las opciones son varias y, como de costumbre, dependerá del caso particular al que te enfrentes (y los recursos que tengas disponibles) cuál sea la más conveniente (¡Hasta podrías hacer tu propia implementación!).

    Lo importante es que conozcas estas posibilidades para no tener que re-inventar la rueda :).

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