Categoría: Cómo hacer para…

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

  • Cómo acelerar un sitio desarrollado con PHP

    Cómo acelerar un sitio desarrollado con PHP

    Por qué invertir en acelerar un sitio web

    La primera pregunta que uno se haría ante esta situación es: ¿para qué molestarse? 🙂

    Es decir, cualquiera preferiría un sitio rápido antes que uno lento, ¿cierto?

    Pero… ¿para qué sirve realmente tener un sitio más rápido?

    Bueno pues hay dos respuestas inmediatas:

    1. Mejor experiencia de usuario
    2. Mejor posicionamiento orgánico

    Ambas redundan en beneficios muy palpables para los dueños de los sitios: clientes más satisfechos y mayor afluencia de potenciales clientes.

    Qué factores influyen

    Muy bien, ahora que estamos de acuerdo en que tener un sitio rápido es beneficioso tenemos que comprender qué factores influyen en la velocidad de carga.

    La realidad es que son muchos y, lamentablemente, unos cuantos están fuera de nuestro control, pero igualmente analicémoslos un momento.

    Toda la interacción con un servidor web se basa en:

    • Una traducción de un nombre de dominio en una dirección IP
    • El establecimiento de una conexión entre el cliente y el servidor
    • El envío de información desde el cliente hacia el servidor
    • La resolución del pedido por parte del servidor
    • El envío de información desde el servidor al cliente
    • El rendering del lado del cliente

    Como puede verse, un factor decisivo será la velocidad de la red subyacente en esta comunicación (Lo que se conoce como ancho de banda) pero definitivamente no es el único.

    De modo que existen medidas que pueden tomarse para mejorar el tiempo del lado del cliente (frontend) y otras tantas del lado del servidor (backend).

    Cómo acelerar el frontend de una aplicación Web

    Acelerar el frontend implica minimizar:

    • La cantidad de pedidos al servidor
    • La cantidad de información intercambiada en cada pedido
    • El tiempo que insume el rendering

    Un típico cuello de botella lo encontramos en el tamaño de las imágenes u otros archivos estáticos (JavaScript, CSS, etc…).

    Reduciendo estos es posible ganar mucho.

    Una herramienta muy útil para medir estos cuellos de botella es GTMetrix

    Cómo acelerar el backend de una aplicación Web

    Y luego está la otra cara de la moneda: el servidor.

    ¿Qué podemos hacer para que nuestro servidor responda más rápido?

    Nuevamente, tenemos que saber a dónde apuntar nuestros cañones.

    Usualmente una aplicación web consiste en:

    • Un webserver
    • Una base de datos
    • Algún script

    En nuestro caso asumiremos que se trata de PHP, aunque los principios aplican para cualquier lenguaje.

    Es muy probable que la parte más compleja de optimizar sea el código de la aplicación.

    Principalmente porque lograr el mismo resultado usando mejor código requiere mucho análisis y, sobre todo, mucho testing para asegurarnos de no romper nada en el camino.

    Hay unas cuantas mejoras que pueden hacerse desde la infraestructura que son muy simples y aportan mucho. Por ejemplo:

    • Distribuir la carga a través de un CDN
    • Enviar el contenido comprimido
    • Usar algún minificador de CSS/JS
    • Usar un caché agresivo en el webserver
    • Usar PHP vía PHP-FPM

    Y luego hay otras herramientas que complementan al servidor web como ser los cachés de memoria (APC, Memcached, Redis, etc…)

    También vale la pena investigar qué consultas pueden estar trabando la base de datos, por ejemplo usando el MySQL slow query log e intentar mejorarlas.

    Y por último, si sospechamos que el código está haciendo de las suyas podemos usar un profiler como XDebug para orientarnos sobre dónde poner la lupa.

    Conclusión

    En definitiva, acelerar un sitio web lento es perfectamente posible pero también bastante laborioso.

    Siempre debe primar un criterio de costo/beneficio para saber cuando se ha alcanzado una velocidad aceptable y ya no vale la pena el esfuerzo de seguir mejorándolo.

  • Cómo armar un carrito de compras con PHP

    Cómo armar un carrito de compras con PHP

    Tema popular si los hay, ¿cierto? 🙂

    Muchos clientes se acercan a cualquier desarrollador con la idea de agregar a su sitio un «carrito de compras» pero, cuando indagamos un poco más vemos que el tema no es tan simple.

    Por ejemplo: ¿sirve de algo el carrito de compras sin la posisbilidad de realizar el pago al final?

    Pero bueno, para no hacer un post enorme, comencemos por la parte del carrito propiamente dicho y dejemos el tema de los pagos para otro.

    Qué puede hacerse con un carrito de compras

    El carrito de compras es un espacio donde un visitante puede llevar registro de los productos que desea comprar.

    Desde el punto de vista técnico/funcional, debe ser posible:

    • Ingresar productos
    • Quitar productos
    • Ver los productos existentes
    • Confirmar la compra

    Para poder ingresar productos al carrito será necesario visualizar cuáles son los productos disponibles en la tienda.

    Qué se necesita para armar un carrito de compras

    La implementación de todo el mecanismo de administración del carrito requerirá de:

    • Algún tipo de base de datos que contenga el catálogo de productos
    • Una aplicación que permita al visitante:
      • Ver ese catálogo
      • Ver los detalles de cada producto
      • Agregar productos a su carrito
      • Ver los contenidos de su carrito
      • Eliminar productos del carrito
      • Confirmar su compra
    • Una pasarela de pagos para completar la transacción

    Por simplicidad imaginemos que tenemos una primera página tipo catalog.php:

    <?php
    
    $dsn = require_once 'db.php';
    
    $pdo = new PDO($dsn);
    ?>
    <table>
      <thead>
         <tr>
            <th>SKU</th>
            <th>Nombre</th>
            <th>Precio</th>
            <th> </th>    
        </tr>
      </thead>
      <tbody>
         <?php
    $sql = "SELECT * FROM products";
    
    foreach ($pdo->query($sql, PDO::FETCH_ASSOC) as $product ) {
    ?>
         <tr>
              <td><a href="product_details?product_id=<?php echo $product['id'];?>"><?php echo $product['sku']; ?></a></td>
              <td><?php echo $product['name']; ?></td>
              <td><?php echo $product['price']; ?></td>
              <td><button onclick="window.location.href='add_to_cart.php?product_id=<?php echo $product['id']; ?>'">Agregar al carrito</button></td>
         </tr>
    <?php
    }?>
      </tbody>
    </table>

    Estoy dejando de lado unos cuantos detalles para no complicar el ejemplo: como ser categorías de productos, cantidad a agregar en cada interacción, etc…

    Continuemos por el archivo add_to_cart.php.

    Aquí es donde reside la clave de la cuestión: el carrito debe acompañar al visitante durante toda su estadía en el sitio.

    Precisamente, la idea del carrito es permitir que la compra se vaya armando conforme el usuario va descubriendo los productos.

    De modo que necesitaremos un medio de almacenamiento que persista durante la navegación.

    A estos efectos php pone a nuestra disposición el arreglo $_SESSION.

    Es ahí donde vamos a almacenar los productos seleccionados por el visitante:

    <?php
    $product_id = $_GET['product_id'];
    session_start();
    
    if (!array_key_exists('products', $_SESSION) {
         $_SESSION['products'] = [];
    }
    
    $_SESSION['products'][$product_id] = array_key_exists($product_id, $_SESSION['products']) ? $_SESSION['products'][$product_id] + 1 : 1;
    
    header('Location: catalog.php');

    De este modo guardamos los ids de los productos seleccionados y su cantidad.

    No es necesario guardar más que los ids de producto ya que con eso será suficiente para recuperar toda la información a la hora de mostrar el carrito o confirmar la compra.

    Pensemos en un archivo tipo show_cart.php:

    <?php
    session_start();
    
    $ids = implode( ', ', array_keys($_SESSION['products']));
    $sql = "SELECT * FROM products WHERE id IN ($ids);"
    ?>
    <table>
      <thead>
         <tr>
            <th>SKU</th>
            <th>Nombre</th>
            <th>Precio</th>
            <th>Cantidad</th>
            <th>Subtotal</th>
            <th> </th>    
        </tr>
      </thead>
      <tbody>
    <?php
    $total = 0;
    foreach ($pdo->query($sql, PDO::FETCH_ASSOC) as $product ) {
          $quantity = $_SESSION['products'][$product['id']];
          $subtotal = $quantity * $product['price'];
          $total += $subtotal;
    ?>
         <tr>
              <td><a href="product_details?product_id=<?php echo $product['id'];?>"><?php echo $product['sku']; ?></a></td>
              <td><?php echo $product['name']; ?></td>
              <td><?php echo $product['price']; ?></td>
              <td><?php echo $quantity; ?></td>
              <td><?php echo $subtotal; ?></td>
              <td><button onclick="window.location.href='remove_from_cart.php?product_id=<?php echo $product['id']; ?>'">Quitar del carrito</button></td>
         </tr>
    <?php
    }?>
      </tbody>
    </table>
    <p>Total: <?php echo $total;?></p>

    Queda por completar los archivos de ver el detalle del producto y remover del carrito pero creo que con lo que viste hasta aquí no deberías tener problemas para armarlos.

    Un detalle que haría más amena la experiencia para el usuario sería utilizar AJAX para meter y sacar productos del carrito.

    Y, por supuesto, deberás tener un enlace para realizar el pago.

  • Cómo recolectar correos electrónicos de una página web usando PHP

    Cómo recolectar correos electrónicos de una página web usando PHP

    Es una necesidad bastante usual la de recoger información disponible en Internet.

    Algunos sitios permiten hacerlo en forma amigable exponiendo algún tipo de API, que puede ser consumida conectándose a un webservice.

    En otros casos, lo mejor que puede hacerse es algo de WebScrapping.

    Claro que es una técnica muy poco fiable y bastante costosa en términos computacionales, pero… si no queda otra…

    En este caso, de lo que se trata es de extraer las direcciones de correo electrónico presentes en una página cualquiera, por ejemplo:

    Se trata de un proceso de dos pasos

    Cómo obtener los contenidos de una página usando PHP

    El primer paso es obtener el contenido de la página. En realidad, lo que nos interesa es el HTML de la página, las imágenes, hojas de estilo y demás no es necesario en este momento.

    Una forma muy sencilla de conseguirlo es usar la función file_get_contents:

    <?php
    
    $html = file_get_contents('http://pagina-objetivo.com');

    Cómo identificar correos electrónicos dentro de un texto

    El segundo problema a resolver es, una vez que obtuvimos todo ese texto HTML… ¿cómo podemos determinar qué direcciones de correo electrónico hay dentro?

    Un modo muy simple es utilizar una expresión regular.

    Afortunadamente, las direcciones de correo electrónico siguen un patrón bastante estructurado: todas tienen una @ en el medio y, al menos, un . a la derecha.

    Podríamos usar una expresión mucho más compleja, pero comencemos por esta.

    Nuestro aliado en esta ocasión será la función preg_match_all:

    <?php
    
    $html = file_get_contents('http://pagina-objetivo.com');
    preg_match_all('/[\._a-zA-Z0-9-]+@[\._a-zA-Z0-9-]+/i', $html, $matches);

    Con esto nos quedamos con todos los candidatos a correos electrónicos.

    Luego, para mayor seguridad podemos eliminar aquellos que no pasen la verificación propia de PHP:

    <?php
    
    $html = file_get_contents('http://pagina-objetivo.com');
    preg_match_all('/[\._a-zA-Z0-9-]+@[\._a-zA-Z0-9-]+/i', $html, $matches);
    
    $emails = array_unique(array_filter($matches, function(string $str) { return filter_var($str, FILTER_VALIDATE_EMAIL);}));

    Con esto obtendremos un arreglo que contiene todas las direcciones de correo presentes en una página.

  • Cómo automatizar búsquedas en Google usando PHP

    Cómo automatizar búsquedas en Google usando PHP

    Un cliente con el que trabajé estaba buscando aumentar el tamaño de su base de datos para realizar mailings y me pidió que le diseñe un robotito para extraer las direcciones de correo que estén presentes en las páginas resultantes de ciertas búsquedas de Google.

    Si bien personalmente no lo considero algo muy productivo (Discusión aparte sobre la efectividad/ética de enviar correo no deseado o si realmente se trata de correo no deseado cuando se ofrece una solución que realmente va a ayudar a quien lo reciba), me pareció interesante el desafío técnico (y también, hay que reconocerlo, a veces simplemente hay que darle al cliente lo que quiere :)).

    Lo primero que se me ocurrió fue que, así como hay APIs para entrar a Gmail, a GoogleDocs y demás, debía haber alguna para usar el motor de búsqueda y obtener los datos en formato JSON, XML o algún otro formato digno de un webservice.

    Y ahí vino mi primera sorpresa (Eso es lo que me fascina de la programación: siempre hay cosas nuevas para aprender :)): a Google no le gusta que los robots le usen su motor (Irónico, ¿no? ellos viven de scrappear toda la web, pero…).

    De modo que, para lograr esta hazaña había que arremangarse y parsear HTML (Algo como lo que te comenté sobre cómo acceder a sitios que no te dan API) o bien, siendo que se trata de un sitio archi-conocido, buscar en Packagist.org que seguro iba a tener algo piola para arrancar al menos.

    Como de costumbre, Packagist no me defraudó y conocí el proyecto SERPS.

    Qué es el proyecto SERPS

    El proyecto SERPS es un esfuerzo por crear un conjunto de herramientas que permitan suplir esta falta, es decir, es un set de bibliotecas que permite (o, mejor dicho, permitirá) automatizar las búsquedas en diferentes motores (Actualmente sólo Google está implementado).

    Es bastante interesante desde su arquitectura, bien modular y de bajo acoplamiento (Se nota la aplicación del patrón strategy).

    A nivel práctico, se trata de una capa de abstracción muy útil para «olvidarnos» de la complejidad de interpretar los resultados de una búsqueda (¡y los caprichos de los pedidos!).

    Preparando el proyecto para usar SERPS

    SERP está diseñado para ser usado con composer. Al principio de su documentación hay un ejemplo de archivo composer.json que te recomiendo copiar y pegar (Yo intenté incluir las dependencias que iba necesitando y no funcionó nada :p):

    {
        "require": {
            "serps/core": "*",
            "serps/search-engine-google": "*",
            "serps/http-client-curl": "*"
        }
    }

    Además de estos paquetes básicos, para que todo funcione hace falta correr estos comandos:

    composer require 'serps/search-engine-google'
    composer require 'guzzlehttp/psr7'

    Con esta configuración (y, obviamente, un composer install) tenés todo lo necesario para hacer queries a Google usando PHP.

    Cómo hacer una búsqueda en Google usando SERPS

    Como decía al comienzo, a Google no le gustan los robots, así que… hay que hacer de cuenta que la búsqueda la hace una persona. Obviamente, una persona que está usando algún navegador. Todo esto se traduce en usar un User-Agent adecuado.

    Te muestro un ejemplo de lo que yo hice:

    #!/usr/bin/php
    <?php
    
    use Serps\HttpClient\CurlClient;
    use Serps\Core\Browser\Browser;
    use Serps\SearchEngine\Google\GoogleClient;
    use Serps\SearchEngine\Google\GoogleUrl;
    
    require_once 'vendor/autoload.php';
    
    $userAgent = "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36";
    $browserLanguage = "es-AR";
    
    $browser = new Browser(new CurlClient(), $userAgent, $browserLanguage);
    
    $googleClient = new GoogleClient($browser);
    
    $googleUrl = new GoogleUrl();
    foreach ( file($argv[1]) as $searchTerms ) {
        $googleUrl->setSearchTerm($searchTerms);
    
        $response = $googleClient->query($googleUrl);
    
        $results = $response->getNaturalResults();
    
        foreach ($results as $result) {
            if ( $result->url ) {
                echo $result->url . PHP_EOL;
            }
        }
    }

    En mi caso, el script toma como parámetro un archivo (que asumo tiene una línea por búsqueda que se quiere hacer) y emite los links encontrados.

    Fijate que sencillo:

    Se crea un objeto Serps\SearchEngine\Google\GoogleClient usando un objeto Serps\Core\Browser\Browsercomo interface para realizar la comunicación HTTP y otro objeto Serps\SearchEngine\Google\GoogleUrlpara armar la consulta como es debido.

    Luego de consultar se obtienen los resultados naturales (Es decir, aquellos resultados no pagos) y, por cada uno se emite el link. En mi caso todo esto va acompañado de otro script que busca direcciones de correo electrónico dentro de una página web y emite los resultados como para ser incorporados a un archivo .csv.

    Ejemplo de uso del script

    Ya teniendo las dos herramientas (la que busca links en Google y la que busca direcciones de mail dentro de una página), es muy fácil componerlas (Si usás algún sistema operativo POSIX como Linux al menos :p):

    ./get_links.php terms.csv | ./get_emails.php

    En el archivo terms.csv se escribe, línea por línea, las búsquedas que quiero realizar, el script get_links.php emite cada resultado que obtiene y get_emails.php toma sus datos de la entrada estándar… justo lo que se necesita para usar pipes :).

    Y claro, la frutilla del postre es guardar la salida de get_emails.php en un nuevo archivo .csv:

    ./get_links.php terms.csv | ./get_emails.php > mails.csv

    Conclusión

    La tarea de hacer webscrapping no es muy grata que digamos (Especialmente porque los grandes esfuerzos que se hacen pueden resultar en vano si el sitio a ser scrappeado cambia su layout…), pero… a veces resulta útil.

    Y, si bien algunos sitios hacen lo posible para «protegerse» de este tipo de prácticas… es poco lo que pueden hacer para evitarlas al 100%…

    ¿Qué experiencia tuviste vos con el scrapping?

  • Cómo pasar datos JSON a CSV usando PHP

    Cómo pasar datos JSON a CSV usando PHP

    La pregunta que dió origen a este post era un poco más amplia:

    Pero como ya hablé de cómo consumir webservices (Sean REST o SOAP) me voy a concentrar en la parte que me llamó la atención: cómo pasar de JSON a CSV.

    Aclaremos los tantos antes de ir a los detalles:

    Qué es JSON

    JSON significa JavaScript Object Notation, es decir: notación de objetos de JavaScript. Está más allá del alcance de este artículo (y de este blog en general) hablar de las bondades (o falta de ellas) de JavaScript… hay mucho material muy bueno al respecto.

    El punto es que, más allá de lo que a vos te guste o no, JavaScript tiene una sintaxis muy práctica para describir los objetos: todo lo que esté encerrado entre {} es un objeto (Con la excepción tal vez del cuerpo de las funciones…).

    Esto lo hace un formato sumamente conveniente para el intercambio de información de estructuras complejas, ya que constituye un modo muy simple de pasar de un objeto en memoria a una representación textual (que puede ser enviada a través de un protocolo de intercambio de textos… por ejemplo HTTP :)).

    En definitiva un texto que se vea así:

    {
       "nombre": "Mauro",
       "amigos": [
          {
             "nombre": "Luis"
          },
          {
             "nombre": "Juan"
          }
       ],
    }

    Estaría representando un objeto con dos propiedades: nombre y amigos. La primera es una cadena, la segunda es un arreglo de otros objetos.

    Más información acá.

    Se ve muy simple (¡y lo es!) pero tiene un pequeño inconveniente: su sintaxis es muy poco permisiva (Dejá una , suelta y agarrate :p), por lo tanto, es muy conveniente usar las funciones propias de PHP para manipularlo (json_encode y json_decode).

    Y acá me da un poco de nostalgia hablar del primer post que escribí para este blog, pero pienso que puede aportar a la respuesta saber cómo iterar sobre un JSON usando PHP.

    Qué es CSV

    CSV significa Comma Separated Values (Valores separados por comas). Se trata de un formato de texto que también es utilizado para el intercambio de información entre sistemas.

    Usualmente el contenido de este tipo de archivos es una sucesión de filas que corresponden con registros cuya cantidad de campos puede variar de uno a otro y la longitud de cada campo también varía.

    Ejemplo:

    Nombre,Apellido,Mail,Edad,Ciudad
    Mauro,Chojrin,mauro.chojrin@leewayweb.com,40,Buenos Aires,
    Steve,Jobs,sjobs@apple.com,,,
    Alfredo,,alopez@gmail.com,,,
    ,Gutierrez,dgutierrez@hotmail.com,46,,

    De JSON a CSV

    Una primera salvedad que hay que hacer es que JSON es un formato que permite estructuras complejas (de múltiples niveles por ejemplo), mientras que CSV es un formato mucho más simple (Se basa en estructuras «planas»), con lo cual, algo se perderá en el camino (o habrá que hacer alguna suposición o transformación de la información).

    A efectos prácticos, asumamos que el JSON de partida es de un único nivel, algo como:

    [
     {
      "nombre": "Mauro",
      "apellido": "Chojrin",
      "correo": "mauro.chojrin@leewayweb.com"
     },
     {
      "nombre": "Pedro",
      "apellido": "Alvarez",
      "correo": "palvarez@gmail.com"
     },
     {
      "nombre": "Laura",
      "apellido": "Perez",
      "correo": "lau.perez@hotmail.com"
     }
    ]

    El CSV correspondiente sería:

    nombre,apellido,correo,
    Mauro,Chojrin,mauro.chojrin@leewayweb.com,
    Pedro,Alvarez,palvarez@gmail.com,
    Laura,Perez,lau.perez@hotmail.com",

    Un código PHP que logra eso es:

    <?php
    
    $f = fopen( 'salida.csv', 'w+' );
    $array = json_decode( file_get_contents( 'entrada.json' ), true );
    
    array_unshift( $array, [ 'nombre', 'apellido', 'correo' ] );
    foreach ( $array as $object ) {
    fputcsv( $f, $object );
    }
    
    fclose( $f );

    Ahora, si la necesidad es más compleja… nada mejor que buscar un poco :).

    Buscando buscando encontré esta herramienta: https://github.com/danmandle/JSON2CSV (No la probé pero parece interesante… ¿te animás a probarla y comentar?)

  • Cómo acceder a Gmail desde PHP

    Cómo acceder a Gmail desde PHP

    Este post está inspirado en un caso muy interesante que me tocó resolver para un cliente.

    Un poco de contexto para que se entienda de dónde viene el tema:

    Una buena parte de los clientes de mi cliente llegan a través de correos que se reciben en info@...., claramente, te imaginarás que, dentro de los muchos que llegan, una parte es SPAM y la otra son contactos genuinos.

    Separar la paja del trigo sería un desafío realmente interesante (Que probablemente involucraría algo de procesamiento del lenguaje natural, IA y esas cosas tan divertidas), pero… por el momento el presupuesto dio sólo para mejorar un poco el proceso de tratamiento del trigo una vez haya sido debidamente identificado.

    Muy bien, entonces, el punto era que se estaba queriendo, además de re-enviar los correos útiles al equipo de ventas, subirlos de inmediato al sistema de newsletters de la empresa (Obviamente, se trataba de MailChimp).

    Decidimos entonces crear una cuenta de GMail a la cual la persona encargada de identificar los correos de potenciales clientes pudiera re-enviarlos (además de al equipo de ventas) y, a partir de ahí, tener un robot que los procesara e incorporara la lista de mailing.

    Técnicamente se trata de cuatro problemas:

    1. Cómo autorizar el acceso a GMail vía API
    2. Cómo ingresar a GMail y descargar los correos
    3. Cómo procesar los datos descargados para extraer las direcciones de correo
    4. Cómo incorporar esos datos a MailChimp

    Sobre el último de los problemas escribí acá, así que concentrémonos en los tres primeros:null

    Cómo configurar el acceso a GMail vía API

    Hay una primera parte que hace a la autorización de la aplicación (Muy parecido a lo que hicimos para entrar al Google Drive):

    1. Logeate a la cuenta a la que vayas a querer acceder en forma programática
    2. Entrá al panel de administración de API
    3. Creá un nuevo proyecto
    4. Entrá al panel de administración del proyecto
    5. Habilitá la API de Gmail
    6. Creá las credenciales de acceso
      1. Seleccioná la opción «Crear ID de cliente de OAuth»
      2. Configurá la pantalla de consentimiento (No te preocupes mucho por esto, casi no lo vas a usar)
    7. Tipo de aplicación: Otro (Ponele el nombre que quieras, tampoco es importante)
    8. Si todo salió bien deberías ver una pantalla como esta:
    Datos de acceso secretos
    1. Descargá el archivo json (Este es probablemente el paso más importante ya que sin él no vas a poder acceder a tus correos)

    Cómo ingresar a GMail usando PHP

    Bien, llegó el momento de codear 🙂

    Lo más sencillo y directo es usar el SDK provisto por Google.

    Para eso, nada mejor que composer:

    1. Creá un directorio para el proyecto
    2. Posicionate ahí
    3. composer init

    Y cuando llegue la pregunta, incorporá como dependencia google/apiclient.

    En nuestro caso, nos interesan las clases Google_Client (Necesaria para acceder a cualquier servicio de Google) y Google_Service_Gmail (Específica para el procesamiento de peticiones a Gmail).

    La creación de la instancia de Google_Client requiere de la existencia de un set de credenciales válidas (que se obtienen simplemente siguiendo un enlace que generará la propia aplicación).

    Una vez obtenido este objeto lo usaremos para crear nuestra instancia de la segunda clase:

    $service = new Google_Service_Gmail($client);

    Y a partir de ahí usaremos la propiedad users_messages para traer los correos.

    Cómo procesar correos descargados de GMail usando PHP

    Una vez obtenido cada uno de los correos necesitaremos procesarlos.

    Lo que sabemos es que los correos vienen codificados como texto MIME, con lo cual, necesitaremos algo de ayuda para procesarlos sin volvernos locos… por ejemplo, la ayuda de PHPMimeMailParser.

    Claro que no todo es tan fácil… en particular, los mails en Gmail vienen además codificados usando base64 (Pero no el base64 que viene con PHP… es un base64 codificado para URLs… Nada muy terrible, sólo hay que saberlo para actuar en consecuencia: se necesita cambiar los caracteres - y _ por + y /respectivamente. Más detalles en https://medium.com/@jrdnrc/decoding-gmail-messages-in-php-408194aeb767).

    Una vez hecho esto podremos extraer alegremente las diferentes partes del correo, por ejemplo:

    $from = $parser->getHeader('from');

    Para conocer el remitente. O:

    $subject = $parser->getHeader('Subject')

    Para el asunto.

    Resultado final

    Ahora sí, veamos el ejemplo completo.

    Como usé Composer para instalar las librerías, lo primero que voy a mostrarte es el archivo composer.json:

    {
        "name": "leeway/gmail2mailchimp",
        "require": {
            "google/apiclient": "^2.0",
            "php-mime-mail-parser/php-mime-mail-parser": "^2.9"
        },
        "authors": [
            {
                "name": "Mauro Chojrin",
                "email": "mauro.chojrin@leewayweb.com"
            }
        ]
    }

    Ahora sí, el script que da vida a todo esto import.php:

    #!/usr/bin/php -q
    
    <?php
    require_once __DIR__ . '/vendor/autoload.php';
    
    define('APPLICATION_NAME', 'Gmail2MailChimp');
    define('CREDENTIALS_PATH', __DIR__.'/gmail2mailchimp.json');
    define('CLIENT_SECRET_PATH', __DIR__ . '/client_secret.json');
    define('SCOPES', implode(' ', array(
            Google_Service_Gmail::MAIL_GOOGLE_COM)
    ));
    
    if (php_sapi_name() != 'cli') {
        throw new Exception('Esta aplicación sólo puede ejecutarse de la terminal.');
    }
    
    echo 'Usando credenciales de: '.CREDENTIALS_PATH.PHP_EOL;
    /**
     * @return Google_Client
     */
    function getClient()
    {
        $client = new Google_Client();
        $client->setApplicationName(APPLICATION_NAME);
        $client->setScopes(SCOPES);
        $client->setAuthConfig(CLIENT_SECRET_PATH);
        $client->setAccessType('offline');
    
        // Load previously authorized credentials from a file.
        $credentialsPath = expandHomeDirectory(CREDENTIALS_PATH);
        if (file_exists($credentialsPath)) {
            $accessToken = json_decode(file_get_contents($credentialsPath), true);
        } else {
            // Request authorization from the user.
            $authUrl = $client->createAuthUrl();
            printf("Open the following link in your browser:\n%s\n", $authUrl);
            print 'Enter verification code: ';
            $authCode = trim(fgets(STDIN));
    
            // Exchange authorization code for an access token.
            $accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
    
            // Store the credentials to disk.
            if (!file_exists(dirname($credentialsPath))) {
                mkdir(dirname($credentialsPath), 0700, true);
            }
            file_put_contents($credentialsPath, json_encode($accessToken));
            printf("Credentials saved to %s\n", $credentialsPath);
        }
        $client->setAccessToken($accessToken);
    
        // Refresh the token if it's expired.
        if ($client->isAccessTokenExpired()) {
            $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
            file_put_contents($credentialsPath, json_encode($client->getAccessToken()));
        }
        return $client;
    }
    
    /**
     * Expands the home directory alias '~' to the full path.
     * @param string $path the path to expand.
     * @return string the expanded path.
     */
    function expandHomeDirectory($path)
    {
        $homeDirectory = getenv('HOME');
        if (empty($homeDirectory)) {
            $homeDirectory = getenv('HOMEDRIVE') . getenv('HOMEPATH');
        }
        return str_replace('~', realpath($homeDirectory), $path);
    }
    
    
    // Get the API client and construct the service object.
    $client = getClient();
    $service = new Google_Service_Gmail($client);
    
    // Print the labels in the user's account.
    $user = 'me';
    
    $results = $service->users_messages->listUsersMessages($user, [ 'labelIds' => ['INBOX'] ]);
    
    echo "Message count: " . count($results->getMessages()) . PHP_EOL;
    
    $parser = new PhpMimeMailParser\Parser();
    
    foreach ($results->getMessages() as $message) {
        if ($real_message = $service->users_messages->get(
            $user,
            $message->getId(),
            [
                'format' => 'raw',
            ]
        )) {
            $real_message = base64_decode(str_replace(['-', '_'], ['+', '/'], $real_message->getRaw()));
            $parser->setText($real_message);
            $from = $parser->getHeader('from');
            $body = $parser->getMessageBody();
            $matches = [];
            preg_match('/mailto:(.+)\]/', $body, $matches);
            if (count($matches) > 1) {
                $sender = $matches[1];
                echo 'Subject: "' . $parser->getHeader('Subject') . '" vino originalmente de: "' . $sender . '"' . PHP_EOL;
                echo "Subiendo a MailChimp" . PHP_EOL;
                if (syncMailchimp([
                        'email' => $sender,
                        'status' => 'subscribed',
                    ])) {
                    echo 'Exito!' . PHP_EOL;
                    try {
                            $service->users_messages->delete(
                                'me',
                                $message->getId()
                            );
                            echo 'Eliminado del servidor!' . PHP_EOL;
                    } catch (Exception $e) {
                            echo 'Error eliminando del servidor: ' . $e->getMessage() . PHP_EOL;
                    }
                } else {
                    echo 'Error :(' . PHP_EOL;
                }            
            }
        } else {
            echo 'No real message here...';
        }
    }
    echo 'Fin!' . PHP_EOL;
    
    function syncMailchimp($data)
    {
        $apiKey = 'XXXXXX';
        $listId = 'YYYYYY';
    
        $memberId = md5(strtolower($data['email']));
        $dataCenter = substr($apiKey, strpos($apiKey, '-') + 1);
        $url = 'https://' . $dataCenter . '.api.mailchimp.com/3.0/lists/' . $listId . '/members/' . $memberId;
    
        $json = json_encode([
            'email_address' => $data['email'],
            'status' => $data['status'], // "subscribed","unsubscribed","cleaned","pending"
    	'source' => 'Cronjob',
        ]);
    
        $ch = curl_init($url);
    
        curl_setopt($ch, CURLOPT_USERPWD, 'user:' . $apiKey);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
    
        $result = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
    
        return $httpCode;
    }

    Y por último, la línea del crontab:

    0 17 * * 1-5 /usr/bin/php import.php

    Para que corra automáticamente a las 17:00 de Lunes a Viernes.

    Los archivos gmail2mailchimp.jsonclient_secret.json son los descargados de Google al configurar la aplicación.

    Es un poco enrevesado, pero funciona :), no está mal, ¿cierto?

  • Cómo enviar XML a un WebService con PHP

    Cómo enviar XML a un WebService con PHP

    Para empezar, debemos responder una pregunta escencial: ¿se trata de un WebService de tipo SOAP o uno de tipo REST?

    ¡La forma de enviarlo en uno u otro caso será muy diferente!

    Otra historia es cómo recibir XML a través de WebServices desarrollados usando PHP… tema para otro post 🙂

    Cómo enviar XML a un WebService SOAP con PHP

    Como siempre, para el caso de que se trate de un webservice de tipo SOAP, usaremos la clase SOAPClient

    Tomemos como ejemplo este Servicio Web que tiene un solo método disponible (CustomerSearch), el cual recibe XML y devuelve XML.

    Tenemos dos alternativas para generar el XML que queremos enviar:

    1. Escribirlo explícitamente (o «a mano»)
    2. Usar la clase SimpleXMLElement

    Si aún estás en dudas, no dudes más: usa la clase, te ahorrará una gran cantidad de dolores de cabeza.

    Con lo que la llamada se vería algo así como:

    <?php
    
    $url = 'https://secure.softwarekey.com/solo/webservices/XmlCustomerService.asmx?WSDL';
    $client = new SoapClient($url);
    
    $xmlr = new SimpleXMLElement("<CustomerSearch></CustomerSearch>");
    $xmlr->addChild('AuthorID', 1);
    $xmlr->addChild('UserID', 'mchojrin');
    $xmlr->addChild('UserPassword', '1234');
    $xmlr->addChild('Email', 'mauro.chojrin@leewayweb.com');
    
    $params = new stdClass();
    $params->xml = $xmlr->asXML(); // OJO: La propiedad xml es particular de este WebService, debes reemplazarla por el nombre del parámetro que espera recibir el servicio al que buscas conectarte
    
    $result = $client->CustomerSearchS($params);
    
    print_r($result);
    
    echo PHP_EOL;

    Si ejecutas este código te encontrarás con algo como:

    stdClass Object
    (
        [CustomerSearchSResult] => stdClass Object
            (
                [any] => <Customers xmlns=""><ResultCode>-1</ResultCode><ErrorMessage>Invalid Login</ErrorMessage></Customers>         
            ) 
    ) 

    Lo que seguramente te interese es lo que está dentro de la clave any, con lo cual, para obtenerlo podrías usar echo $result->CustomerSearchSResult->any; en lugar de print_r($result);. Aunque probablemente lo que quieras no sea mostrar el resultado explícitamente, si no procesarlo de alguna manera… ¿qué mejor que recurrir nuevamente a SimpleXMLElement?

    <?php
    
    $url = 'https://secure.softwarekey.com/solo/webservices/XmlCustomerService.asmx?WSDL';
    $client = new SoapClient($url);
    
    $xmlr = new SimpleXMLElement("<CustomerSearch></CustomerSearch>");
    $xmlr->addChild('AuthorID', 1);
    $xmlr->addChild('UserID', 'mchojrin');
    $xmlr->addChild('UserPassword', '1234');
    $xmlr->addChild('Email', 'mauro.chojrin@leewayweb.com');
    
    $params = new stdClass();
    $params->xml = $xmlr->asXML();
    
    $result = new SimpleXMLElement($client->CustomerSearchS($params)->CustomerSearchSResult->any);
    
    $r = current($result->xpath('/Customers/ResultCode'));
    
    if ( $r == '-1' ) {
            echo 'Fallo: '.$result->xpath('/Customers/ErrorMessage')[0];
    } else {
            echo 'Exito!';
    }
    
    echo PHP_EOL;

    Cómo enviar XML a un WebService REST con PHP

    En el caso de tratarse de un Servicio Web basado en REST todo es más fácil.

    Puedes usar cURL:

    <?php
    
    $server = 'http://www.leewayweb.com/miaplicacion';
    $headers = [
        "Content-type: text/xml",
        "Content-length: " . strlen($requestXML), "Connection: close",
    ];
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $server);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_TIMEOUT, 100);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $requestXML);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    $data = curl_exec($ch);
    
    if (curl_errno($ch)) {
        print curl_error($ch);
        echo "Algo fallo";
    } else {
        curl_close($ch);
    }

    O bien algo un poco más elaborado como Guzzle:

    <?php
    
    use GuzzleHttp\Client;
    use GuzzleHttp\Psr7\Request;
    
    $client = new Client();
    $request = new Request(
        'POST',
        $uri,
        [
            'Content-Type' => 'text/xml; charset=UTF8'
        ],
        $xml
    );
    
    echo $response->getBody();
    

    (Para que funcione este ejemplo hay que tener instalada la librería o incluirla, por ejemplo, usando composer.).

  • Cómo exponer un WebService REST con PHP

    Cómo exponer un WebService REST con PHP

    ¿Qué es un WebService?

    Ya en el artículo sobre cliente REST di una pequeña definición de un WebService, 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 REST?

    REST es un protocolo de intercambio de información basado en HTTP.

    ¿Cómo se implementa en PHP?

    Los servicios web basados en REST suelen ser mucho más fáciles de crear (y consumir) que los basados en SOAP.

    De hecho, cualquier aplicación PHP que hayas hecho podría ser un WebService REST! (Bueno… tal vez no uno muy útil, pero eso es otro tema :)).

    Te muestro un ejemplo super simple:

    <?php
    
    echo json_encode( [ 'Hola' ] );
    

    No está mal, ¿cierto?

    En este caso lo que vemos es una aplicación que, al ser invocada usando curl http://localhost:8080/rest_server.php (Asumiendo que está montada sobre el servidor local) nos dará esta salida:

    ["Hola"]

    El cliente que haya realizado dicha invocación deberá saber qué tipo de contenido le estamos enviando (¡y actuar en consecuencia!).

    Podés probarlo iniciando el servidor de esta forma:

    php -S localhost:8080 &

    (El & para que el proceso se ejecute en background y puedas seguir).

    En caso de que se hubiese producido un error (Por ejemplo, que el recurso buscado no se encontrara disponible), deberíamos usar la función http_response_code para enviar un aviso al cliente:

    http_response_code( 404 );

    Por último, una buena práctica es también hacer explícito el tipo de contenido que vamos a enviar al cliente:

    header('Content-type: application/json');

    De esta forma el cliente tiene algo más de información y puede tomar mejores decisiones.

    En definitiva, si podés elegir, te recomiendo usar siempre servicios web basados en REST. Otro consejo es que tengas a mano los códigos de error HTTP (No es necesario saberlos todos de memoria, pero ayuda :)).

    ¿Qué ejemplos se te ocurren ahora que sabés armar servicios REST?

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