Blog

  • Cómo exponer un WebService SOAP con PHP

    Cómo exponer un WebService SOAP con PHP

    ¿Qué es un WebService?

    Ya en el artículo sobre cliente SOAP di una pequeña definición de un WebService (Una más exhaustiva está en el curso de WebServices con PHP), por si no lo leíste te lo cuento:

    Un WebService es una pequeña aplicación web diseñada para interactuar con otras aplicaciones (en lugar de hacerlo con personas).

    Las dos aplicaciones que se comunican toman el rol de:

    1. Servidor: quien expone el servicio
    2. Cliente: quien lo consume

    ¿Qué es SOAP?

    SOAP es un protocolo de intercambio de información basado en XML.

    ¿Cómo se implementa en PHP?

    Ahora que estamos claros con las definiciones veamos un ejemplo:

    server.php:

    <?php
    
    class MiClase
    {
     public function saludar()
     {
     return 'Hola ' . func_get_args()[0] . PHP_EOL;
     }
    }
    
    try {
     $server = new SoapServer(
     null,
     [
     'uri'=> 'http://localhost:8080/soap_server.php',
     ]
     );
    
     $server->setClass('MiClase');
     $server->handle();
    } catch (SOAPFault $f) {
     print $f->faultstring;
    }

    cliente.php

    <?php
    
    $client = new SoapClient(null, array(
          'location' => "http://localhost:8080/server.php",
          'uri'      => "http://localhost:8080/server.php",
          'trace'    => 1 ));
    
    try {
    	echo $return = $client->__soapCall("saludar", ["mundo!" ] );
    } catch ( SOAPFault $e ) {
    	echo $e->getMessage().PHP_EOL;
    }
    

    Para que todo esto tenga sentido, primero necesitamos tener un webserver levantado en localhost:8080. Para hacerlo simple, usemos el servidor incorporado al intérprete de PHP:

    php -S localhost:8080 &

    Y entonces, al ejecutar php soap_client.php veremos:

    Hola mundo!

    Si en lugar de publicar este script (server.php) en nuestro localhost lo subiéramos a un servidor accesible públicamente, cualquier aplicación conectarse a este servicio e invocar nuestro método saludar.

    Puedes utilizar un archivo WSDL para darle más robustez al servicio (y hacerlo descubrible también), pero por el momento tienes todo lo necesario para permitir a otras aplicaciones interactuar con la tuya a través de un WebService SOAP.

    ¡Feliz integración! 🙂

  • Cómo tratar con fechas en Excel usando PHP

    Cómo tratar con fechas en Excel usando PHP

    En el desarrollo de aplicaciones para empresas es bastante común tener que trabajar con Excel (Ya sea importando planillas a bases de datos o bien lo inverso).

    En general, la libería PHPSpreadSheet funciona muy bien para estos casos (Algo más de información aquí), sin embargo, el tratamiento de las fechas no es tan sencillo como esperamos.

    Cómo Excel maneja las fechas

    El problema radica en que el valor almacenado en la celda no es en realidad una fecha… si no la cantidad de días transcurridos desde el primero de Enero de 1900 (Si tenés curiosidad abrí el Excel y probá la fórmula «=DATEVALUE(‘1900-01-01’)»).

    Esto provoca que, al hacer algo como:

    $value = $worksheet->getCell('A1')->getValue();

    Obtengamos un número entero (Generalmente grande) en lugar de una fecha

    Cómo leer datos de tipo fecha con PhpSpreadsheet

    Para resolver este pequeño inconveniente PhpSpreadsheet dispone de un método especial:

    $date = \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($value);

    Este método retorna un objeto de tipo DateTime.

    A partir de este objeto podemos usar todas las operaciones que queramos.

    Cómo escribir datos de tipo fecha con PhpSpreadsheet

    Y ¿qué pasa si tenemos que generar valores fecha para exportar a Excel? (Básicamente lo inverso de lo anterior).

    Para eso también tenemos un método especial:

    \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel( $date )

    Este método tomará un objeto DateTime de php y lo convertirá a su correspondiente valor numérico para ser guardado sin problemas en una planilla de cálculo Excel.

    Cuidado: que lo guarde bien no quiere decir que se vea bien al abrir el Excel.

    Si lo dejas así verás algo como:

    Que si bien técnicamente es correcto, muy probablemente no sea lo que esperarías.

    No te preocupes, la solución es bien simple. Todo lo que tienes que hacer es establecer un formato para la celda que contiene la fecha.

    Sería algo así como:

    $worksheet->getStyle('A1')
    ->getNumberFormat()
    ->setFormatCode(
    \PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_DATE_YYYYMMDDSLASH
    );

    Con esto tendrás una salida bien formateada y no tendrás problemas de incompatibilidad con configuraciones regionales.

  • 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

  • Validaciones… ¿lado cliente o lado servidor?

    Validaciones… ¿lado cliente o lado servidor?

    Nadie duda de que los campos de los formularios que envían los usuarios deben ser validados.

    Seguro eso es lo que te dijeron cuando empezaste a estudiar, lo que no es muy claro cuando se trata de programación web es cuál es la manera correcta de hacerlo.

    En otras palabras, ¿cuáles son las mejores prácticas cuando se trata de validar?

    Más allá de las herramientas que vayas a usar para realizar la validación, lo primero que debes responder es dónde debe realizarse esa validación, ¿en el cliente o en el servidor?

    La respuesta corta primero: Idealmente las dos, y si sólo puedo elegir una, definitivamente del lado servidor.

    ¿Querés más detalles? Te invito a acompañarme a la respuesta larga 🙂

    Respuesta larga:

    ¿Por qué es más importante la validación del lado servidor que del lado cliente?

    Sencillamente porque tenés mucho más control sobre lo que sucede.

    En una aplicación de escritorio (o una que no sea de tipo cliente-servidor) no existe este problema: la aplicación es una sola con lo cual, lo que el front-end captura puede ser considerado confiable y volver a realizar la validación en el backend resultaría redundante.

    Cuando hablamos de una aplicación web, en realidad estamos hablando de dos aplicaciones independientes. Compartir en X

    Pensá un momento en cómo es el modelo de ejecución de una aplicación web:

    1. El cliente realiza un pedido
    2. El servidor responde con HTML, JavaScript, CSS, etc…
    3. El cliente dibuja la página
    4. El usuario completa los datos
    5. El cliente realiza un nuevo pedido enviando los datos ingresados
    6. El servidor los procesa y eventualmente almacena
    7. Vuelta al paso 2

    Desde el punto de vista del servidor, el pedido que se le realiza puede haber sido generado usando el HTML que él mismo ha enviado o no… imposible saberlo.

    De modo que, en definitiva, los datos recibidos no pueden ser considerados confiables.

    Podés ver cómo «fabricar» un pedido cualquiera usando una herramienta como cURL.

    Tomemos un sitio cualquiera como http://www.recruitersonline.com/members/ron4_add.php:

    Si vemos el código fuente:

    Podemos notar rápidamente que los datos que se introduzcan en este formulario serán enviados a ron4_add_thanks.php.

    Valiéndonos de cURL podemos armar un pedido como:

    Esto haría que el servidor reciba XXXX como valor del campo adm_first_name.

    Suponiendo que tal valor no sea aceptable y que la validación sólo se realizara a través de JavaScript… habríamos logrado saltearnos esta validación simplemente usando un cliente diferente.

    De hecho, en este tipo de problemas se basan los ataques tales como sql injection.

    Pero entonces…

    ¿Vale la pena hacer validaciones del lado cliente?

    ¡Claro que SI!.

    Las validaciones del lado del cliente son un favor que le hacemos a los usuarios bien intencionados que simplemente han cometido un error al ingresar información de buena fé.

    El favor consiste en ahorrarles el round-trip al servidor con información que sabemos de antemano que será rechazada.

    Es cierto que hoy en día el tiempo de ida y vuelta se ha reducido en forma significativa, sin embargo, aún hay escenarios en los que puede hacer una diferencia (Por ejemplo en el caso de aplicaciones móviles donde la red es inestable o peor, se consumen datos del plan).

    La validación del lado servidor es una necesidad mientras que la validación del lado cliente es una comodidad. Compartir en X
  • 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 :).