Blog

  • Cómo alterar la configuración de PHP sin acceder al php.ini

    Cómo alterar la configuración de PHP sin acceder al php.ini

    En el servidor no tengo acceso a php.ini (alojamiento gratuito) por lo que debo configurar los cambios a través de “.htaccess”.

    La configuración estándar de PHP no siempre es la adecuada para nuestras aplicaciones (Por ejemplo, la cantidad de memoria permitida para un script o el tiempo máximo de ejecución).

    Desafortunadamente, no siempre podemos hacerlo del modo normal, es decir, modificando el archivo php.ini.

    Es más, existen casos en los que no querremos que estos cambios tengan efecto en scripts diferentes del que estamos desarrollando.

    El típico caso en que esto sucede es un entorno de hosting compartido.

    Algunos proveedores dan acceso a una versión particular del archivo por cada sitio que alojan, aunque la mayoría no lo permiten en absoluto (Es lógico si se piensa un poco, un servidor en el que corren muchas aplicaciones debe hacer un gran esfuerzo por evitar que un vecino poco solidario acapare los recursos del sistema o lo vuevla inestable).

    En estas situaciones, algo que podemos hacer es utilizar la función ini_set.

    Esta función permite alterar algún parámetro de la configuración por espacio del hilo de ejecución actual (Es decir, el cambio no tendrá efecto en otros scripts que se estén ejecutando simultáneamente ni una vez finalizada la ejecución en curso).

    Algunos ejemplos interesantes:

    Parámetros de sesión

    La directiva session.name especifica el nombre de la cookie de sesión que usará nuestra aplicación. Es muy importante ponerle un nombre único para evitar dar a un potencial atacante información que facilitaría su tarea (Por ejemplo, al dejar el nombre por defecto, PHPSESSION, el agresor ya sabe que nuestra aplicación está desarrollada en PHP)

    Zona horaria

    Algo muy útil cuando se trabaja con usuarios de diferentes lugares del mundo (O cuando el hosting está en una zona horaria diferente de la del usuario) y se requiere hacer cálculos de fecha/hora. Más detalles acá.

    Remitente de correos por defecto

    La directiva sendmail_from permite definir la dirección de email desde la que se enviarán los correos de nuestro script.

    Una aclaración importante: no todas las directivas de PHP pueden modificarse usando esta función.

    Acá está la lista completa.

  • Cómo consumir un WebService SOAP con PHP

    Cómo consumir un WebService SOAP con PHP

    Qué son los WebServices

    Los WebServices son un mecanismo muy útil para integrar aplicaciones a través del protocolo HTTP, y de ese modo, aprovechar las capacidades de terceros dentro de nuestras propias aplicaciones.

    Un ejemplo muy común es de las pasarelas de pago, como ser PayPal o MercadoPago.

    Se basan siempre en la existencia de dos procesos:

    • El cliente (Consumidor)
    • El servidor (Productor)

    A nivel técnico existen dos operaciones que pueden realizarse a través de WebServices:

    1. Consumirlos
    2. Exponerlos

    Uno de los protocolos que pueden utilizar los WebServices es SOAP (Otro muy común es REST).

    Consumirlos usando PHP es bastante simple, para ello se utiliza la clase SoapClient.

    Cómo obtener la localización del visitante usando su IP

    Para este ejemplo usaremos el WebService de cdyne.com para obtener información geográfica en base a la IP buscada.

    <?php
    
    $url = "http://ws.cdyne.com/ip2geo/ip2geo.asmx?wsdl";
    
    try {
     $client = new SoapClient($url, [ "trace" => 1 ] );
     $result = $client->ResolveIP( [ "ipAddress" => $argv[1], "licenseKey" => "0" ] );
    
     print_r($result);
    } catch ( SoapFault $e ) {
     echo $e->getMessage();
    }
    
    
    echo PHP_EOL;

    En este caso, este script debería ser corrido desde CLI.

    Por ejemplo, si lo guardás como «ws.php», al ejecutar php ws.php 210.45.151.101 obtendrás la salida:

    stdClass Object
    (
     [ResolveIPResult] => stdClass Object
     (
     [City] => Huainan
     [StateProvince] => 01
     [Country] => China
     [Organization] => 
     [Latitude] => 32.6264
     [Longitude] => 116.9969
     [AreaCode] => 0
     [TimeZone] => 
     [HasDaylightSavings] => 
     [Certainty] => 90
     [RegionName] => 
     [CountryCode] => CN
     )
    )

    Como podrás observar, la respuesta del método ResolveIP es un objeto de tipo StdClass.

    StdClass es una clase genérica de PHP (Algo medio raro y, casi diría que un abuso de la naturaleza interpretada del lenguaje). Esta clase no tiene métodos ni propiedades definidas, pero sirve como una especie de contenedor al que se le puede asignar arbitrariamente todo lo que uno quiera (En rigor de verdad, esto puede hacerse con cualquier clase de PHP, sólo que es preferible no hacerlo).

    Básicamente, al construir el cliente a partir de la definición de un WSDL están disponibles todos los servicios expuestos como métodos propios (como si estuviesen accesibles en forma local, a pesar de que la verdadera llamada es remota).

    Es interesante notar esto, si ves la línea $result = $client->ResolveIP( [ "ipAddress" => $argv[1], "licenseKey" => "0" ] ); podrás notar que se está invocando al método ResolveIP sobre un objeto de clase SoapClient.

    La clase SoapClient es una clase estándar de PHP, mientras que el método ResolveIP sólo tiene sentido dentro de este WebService. Si te estás preguntando cómo puede una clase estándar reconocer métodos desconocidos te diría que deberías darle una mirada al tema de los métodos mágicos de PHP (O tomar el curso de PHP Orientado a Objetos).

    Como te imaginarás, si existe la clase SoapClient… debe existir la clase SoapServer (Tema de otro post).

    En el curso de PHP WebServices estudiamos este tema en mayor profundidad, mientras tanto, si te quedó alguna duda podés dejarla en un comentario.

  • Cómo pasar un sistema monolingüe a multilingüe

    Cómo pasar un sistema monolingüe a multilingüe

    Hace un tiempo, trabajando para una red social de viajes, me topé con un desafío sumamente interesante: adaptar un sistema hecho para trabajar exclusivamente en Español para dar soporte a varios idiomas (En particular, en nuestro caso se trataba de la versión en Portugués, pero se preveía que podríamos necesitar más idiomas en el futuro).

    Si el sistema se arrancara desde cero no sería un gran problema. Se podría usar Symfony o algún otro framework similar que ya tienen incorporada la funcionalidad multi-idioma y listo.

    Desafortunadamente, en aquel momento el sistema estaba desarrollado sobre un framework propietario y la re-escritura estaba fuera de discusión.

    Así que, junto a mi equipo de desarrollo, comenzamos a pensar cómo resolver este problema.

    Lo primero que entendimos fue que teníamos que resolver dos desafíos complementarios:

    1. La internacionalización del contenido estático (Contenido generado por nosotros mismos, botones, menúes, etc…)
    2. La internacionalización del contenido dinámico (Contenido subido por los propios usuarios)

    Cómo internacionalizar el contenido estático de un sitio

    En nuestro caso, dado que la aplicación era grande, había mucho contenido con textos hardcodeados.

    Decidimos utilizar gettext como motor de traducción (Tenía la ventaja de usar archivos .po y .mo, lo cual hacía muy fácil el proceso de traducción).

    El siguiente paso era el más pesado: generar los archivos con los textos a traducir.

    En aquel momento usábamos el motor de templates Smarty (Hoy no dudaría un segundo en usar Twig), así que el camino por delante (Si bien sumamente tedioso) era claro: había que crear una pequeña extensión para Smarty que permitiera tomar un texto y devolverlo traducido.

    Y así lo hicimos (bueno, contratamos a alguien para que lo haga :p).

    Cómo internacionalizar el dinámico estático de un sitio

    Claro que la traducción del contenido estático era sólo el comienzo… el verdadero desafío era traducir el contenido dinámico (o lo más parecido que se pudiera).

    Decidimos utilizar un sistema de tags donde cada pieza de contenido (artículo, opinión, comentario, etc…) estuviera taggeado con el idioma en el que había sido escrito.

    Cada usuario tenía un idioma asociado (lo poníamos por default según el idioma de su país y el usuario después podía modificarlo) y, cada vez que generaba un contenido se asumía que lo hacía en su idioma (Después los administradores del sitio podían re-taggearlo para corregirlo si lo consideraban necesario).

    En definitiva era simple: una tabla idioma con un id y un nombre y en cada tabla de contenido una nueva columna para la clave foránea.

    Y funcionó bien… hasta que dejó de hacerlo :p

    ¡Ojo! ¡No fue un problema técnico! De hecho, casi diría que el sistema funcionó demasiado bien.

    ¿Qué pasó?

    La definición original desde el negocio era que cada visitante viera sólo contenido en su idioma, pero, cuando se iba a lanzar el sitio en portugués rápidamente nos dimos cuenta de que iba a arrancar vacío (Obvio, ¡no había ningún contenido taggeado en portugués!) y eso no iba a ser muy atractivo para los nuevos visitantes (ni para el SEO).

    Así que cambió la directiva por algo mucho más sensato: no filtrar el contenido según el idioma del usuario si no priorizarlo (Es decir: para un usuario de habla portuguesa, primero se debía ver contenido en Portugués pero, una vez finalizado, se debía comenzar a ver contenido en Español).

    Claro que eso estaba muy bien desde el punto de vista de los usuarios, pero… ¿quién piensa en los pobres programadores que tienen que implementar esa locura?

    Pues bien, la solución que encontramos fue incorporar una noción de distancia entre idiomas.

    La idea era que cada idioma (cultura en realidad porque también manejábamos variaciones regionales pero eso no cambia mucho así que no voy a complicar más de lo necesario por ahora).

    De modo que la base de datos se veía más o menos así:

    Esta estructura permite generar efectivamente una matriz donde cada cultura se relaciona con todas las demás:

    eses_ARpt_BR
    es012
    es_AR102
    pt_BR220

    Los números que están en el cruce de la matriz constituyen la distancia entre un idioma y el otro.

    Esto sirve como criterio de ordenamiento, de modo que, cuando queremos mostrar los comentarios haríamos algo así como:

    SELECT * FROM comments co INNER JOIN cultures_distances cd ON co.culture_id = cd.target_culture_id
    WHERE cd.source_culture_id = ??
    ORDER BY cd.distance

    El placeholder se reemplaza por el id de la cultura del visitante actual, de modo que los contenidos están ordenados en función de la distancia existente entre el idioma del visitante y aquel en el que están escritos… no está mal, ¿cierto? 🙂

    Este sistema a su vez, permite siempre meter más variaciones regionales entre medio (Basta con que la distancia sea un número real, o sea un float).

    Otro punto que vale la pena explicar es que la distancia no tiene por qué ser simétrica (No necesariamente debe valer lo mismo la distancia entre «es» y «es_AR» que viceversa).

    Siendo que no estamos hablando de temas de geometría si no de idiomas, podemos hacer lo que queramos (al fin de cuentas, ¡es nuestro sistema!).

    Y bueno, así fue como funcionó nuestro sitio multilingüe.

    Insisto en lo que decía al comienzo, si tenés la posibilidad, partí de un framework que soporte i18n… si no es el caso, espero este artículo te haya sido inspirador 🙂

  • Un motor de sugerencias en PHP

    Un motor de sugerencias en PHP

    Otro desafío interesante que me tocó encarar junto a mi equipo en el desarrollo de una red social de viajes fue el Sugeridor de Opinables.

    Una de las características que tenía el sitio en que estaba trabajando era la posibilidad de que los usuarios dejaran opiniones (o reseñas mejor dicho) sobre lugares que habían visitado (en general hoteles, restaurantes, atractivos, etc…).

    En un determinado momento, para el negocio se volvió sumamente importante incrementar la cantidad de reseñas (obviamente debían ser auténticas, no inventadas por robots ni plagiadas de otros sitios).

    Una idea que tuvimos junto con el equipo de producto fue la de intentar «adivinar» sobre qué otros lugares podría un usuario dejar su reseña, sabiendo qué lugares había visitado.

    Lo que debíamos lograr era algo similar a esto:

    Pero había una serie de restricciones que debíamos cumplir:

    • Siempre debía haber una próxima sugerencia para el usuario que acababa de opinar
    • Sólo debían sugerirse opinables de ciertos rubros (Por ejemplo, sólo hoteles y restaurantes y esta regla debía poder alterarse para acompañar las necesidades del negocio)
    • La cantidad de opinables por destino y rubro (en el conjunto inicial) debía poder ser modificable en forma sencilla (es decir configurable)
    • En el caso de que no se tuviera nada de historia (el sitio permitía las opiniones de usuarios no registrados), debía existir un conjunto de opinables general
    • No debían sugerirse empresas sobre las que el usuario ya hubiese opinado (Se buscaba ampliar la cantidad de empresas con al menos 1 opinión)

    Todo esto dio lugar a una arquitectura bastante compleja pero sumamente eficaz, basada en estas clases:

    SugeridorOpinables
    • CalculadorDestinosSugeridos
      CalculadorOpinablesParticulares
      CalculadorOpinablesGenerales

       

    • SaneadorOpinables
    • Con algunas subclases también para implementar diferentes estrategias de selección de destinos y empresas (Por ejemplo, si el usuario era anónimo o ya tenía algo de historia en el sitio).
    • Una vez que se habían instanciado las clases «auxiliares» según el caso particular, todas se inyectaban como dependencias de la clase principal:
    • $oSugeridor->setCalculadorDestinos( $oCalculadorDestinos );
      $oSugeridor->setCalculadorOpinablesParticulares( $oCalculadorOpinables );
      $oSugeridor->setRubrosParticipantes( $aRubrosSugeribles );
      $oSugeridor->setCalculadorOpinablesGenerales( $oCalculadorOpinablesGenerales );
      $oSugeridor->setSaneador( new SaneadorOpinablesDuplicados() );

      Y la parte más interesante era cómo se encadenaban las funciones de cada una de las clases en algo como esto:

    • /**
       * Obtiene el conjunto inicial del flow y genera los opinables para ser enviados posteriormente por @see self::getProximosNElementos
       * 
       * @return array<empresas> Conjunto inicial de sugerencias generado en base a las estrategias elegidas
       */
      public function getConjuntoInicial()
      {
         $aDestinosSugeridos = $this->getCalculadorDestinos()->getDestinos();
         $oCalculadorOpinablesParticulares = $this->getCalculadorOpinablesParticulares();
         $aRubrosParticipantes = $this->getRubrosParticipantes();
               
         /** En el caso de partir de una review la primera condición debería ser siempre true, la segunda depende de la configuración */
         if ( !empty($aDestinosSugeridos) && !empty($aRubrosParticipantes) && array_sum( $aDestinosSugeridos ) != array_sum( $aRubrosParticipantes ) ) {
            throw new ExcepcionDatoInvalido( 'Las expectativas de proporción de rubros no son compatibles con las de proporción de destinos' );
         }
         
         $oCalculadorOpinablesParticulares->setDestinosSugeridos( $aDestinosSugeridos );
         $oCalculadorOpinablesParticulares->setRubrosParticipantes( $aRubrosParticipantes );
         
         $preOpinablesParticulares = $oCalculadorOpinablesParticulares->getOpinables();
         $opinablesParticularesCalificacionAlta = array();
         $opinablesParticularesCalificacionBaja = array();
         
         foreach($preOpinablesParticulares as $opinablesParticulares)
         {
            $oCalificacionEditorial = $opinablesParticulares->getCalificacionEditorial();
            if( $oCalificacionEditorial && $oCalificacionEditorial['valor'] >= empresas::CALIFICACION_MEDIA ){
               $opinablesParticularesCalificacionAlta[$opinablesParticulares->getKeyValue()] = $opinablesParticulares;
            } else {
               $opinablesParticularesCalificacionBaja[$opinablesParticulares->getKeyValue()] = $opinablesParticulares;
            }
         }
         
         $aOpinablesParticulares = $opinablesParticularesCalificacionAlta + $this->shuffleArray($opinablesParticularesCalificacionBaja);
         
         // Se sanean los opinables según la estrategia enviada desde el llamador
         $oSaneador = $this->getSaneador();
         $oSaneador->setOpinablesGenerales($this->getCalculadorOpinablesGenerales()->getOpinables());
         $oSaneador->setOpinablesParticulares($aOpinablesParticulares);
         $oSaneador->sanear();
         
         $aOpinablesGenerales = $this->shuffleArray( $oSaneador->getOpinablesGenerales() );
         
         $aConjuntoInicial = $this->buildSetInicial(
               $this->getLongitudInicial(), 
               $aDestinosSugeridos, 
               $aRubrosParticipantes, 
               $aOpinablesParticulares, 
               $aOpinablesGenerales
         );
         
         // Se genera un conjunto con la union de los opinables particulares y generales
         $aConjuntoTotal = $aOpinablesParticulares + $aOpinablesGenerales;
      
         // Se eliminan del conjunto total los opinables que entraron en el conjunto inicial
         foreach ( $aConjuntoInicial as $oOpinableInicial ) {
            unset( $aConjuntoTotal[ $oOpinableInicial->getKeyValue() ] );
         }
      
         // Se guarda en el storage el conjunto total para ser accedido posteriormente
         // Solo se guardan los IDs de los opinables
         $this->getStorage()->store( array_map(function($a) { return $a->getKeyValue(); }, $aConjuntoTotal) );
      
         return $aConjuntoInicial;
      }

      (Es difícil entender todo el código sin tener una idea del framework que estábamos usando… desafortunadamente hecho en casa, pero lo importante son las líneas resaltadas).

    • Lo que se ve acá es cómo al tener todo tan desacoplado es muy simple alterar la lógica de una parte del motor sin afectar al resto (Por ejemplo, si cambiara la forma de seleccionar los destinos, la de los rubros o los opinables).

    Este es otro ejemplo de aplicación del patrón strategy, en este caso, para una red social de viajes.

  • Cómo evitar la inyección SQL  en PHP

    Cómo evitar la inyección SQL en PHP

    Uno de los fantasmas más temidos por quienes contratan servicios de desarrollo (especialmente cuando se trata de su primera experiencia) es el de los ataques de hackers.

    Si bien es imposible asegurar al 100% un sistema (de software o de cualquier otro tipo), existe una serie de buenas prácticas que disminuyen sensiblemente la probabilidad de ocurrencia de tales ataques (o al menos, su probabilidad de éxito).

    Por lo general, los ataques se basan en la explotación de código vulnerable como ser algún caso raro que el desarrollador no tuvo en cuenta.

    Uno de los ataques más usuales es el conocido como sql injection.

    De lo que se trata es de ejecutar código sql sin autorización.

    Los scripts de PHP que no están bien escritos pueden ser atacados de esta forma.

    Veamos un ejemplo:

    <?php
    
    $sql = "SELECT * FROM users WHERE nombre = '".$_POST['nombre']."'";
    
    $db->query($sql);

    Si el $_POST se llena normalmente no habría problema (Si lo hace un usuario legítimo de nuestra aplicación), pero… ¿qué pasa si un usuario malicioso lo hace?

    Por ejemplo, qué tal si alguien pusiera algo como esto en el campo nombre:

    ';DROP TABLE users;--

    El sql total quedaría así:

    SELECT * FROM users WHERE nombre = '';DROP TABLE users;--'

    Por si lo querés ver más gráfico, te dejo este excelente comic.

    Bueno… formas de protegerse de esto hay muchas, una muy práctica es utilizar los prepared statements de PDO.

    El ejemplo quedaría de esta forma:

    <?php
    
    $sql = "SELECT * FROM users WHERE nombre = :nombre";
    $st = $db->prepare( $sql );
    $st->execute( [ ':nombre' => $_POST['nombre'] ] );

    De este modo, dejamos en manos de PDO la realización de las validaciones y el agregado de comillas donde corresponda, de modo que el sql a ejecutar quede de esta forma:

    SELECT * FROM users WHERE nombre = '\';DROP TABLE users;--\''

    Con lo cual se vuelve completamente inofensivo 🙂

  • Un ejemplo de uso del patrón strategy en PHP

    Un ejemplo de uso del patrón strategy en PHP

    Hace poco, trabajando en una mejora para un sistema que desarrollé para un cliente me pasó lo siguiente:

    Una parte del trabajo de la aplicación era obtener información financiera de diferentes fuentes, básicamente se trataba de obtener precios históricos de bonos.

    Existían diferentes fuentes de consulta debido a que la información no siempre estaba disponible en todos los sitios (más allá de no disponer de APIs, pero esa es otra historia).

    El punto es que, en la primera versión de la aplicación, que obviamente estaba desarrollada sobre el framework Symfony, simplemente creamos un método dentro del Controlador:

    private function fetchBondPrice($symbol, \DateTime $date)
    {
        try {
            if ($price = $this->fetchBondPriceFromQuoteNet($symbol, $date)) {
    
                return $price;
            }
    
            if ($price = $this->fetchBondPriceFromMorningStar($symbol, $date)) {
    
                return $price;
    
            }
        } catch (Exception $e) {
    
        }
    
        return null;
    }

    Y las cosas funcionaron… pero desde el comienzo quedó pendiente este comentario en el código:

    * @todo Refactor into a Strategy Pattern

    El momento oportuno llegó cuando debimos alterar algunos aspectos de las búsquedas específicas y, para asegurarnos de que todo saliera bien, decidimos usar PHPUnit y ahí se hizo casi obvia la necesidad de refactorizar el código.

    La primera vuelta del refactor nos trajo consigo una clase sencilla, un Service de Symfony llamado BondPriceFetcherManager.

    Que, a priori no era mucho más que una forma de deslindar responsabilidad desde el controlador hacia una clase más específica… pero seguíamos teniendo un método que había que modificar si queríamos incorporar una nueva fuente de datos (Algo que muy probablemente se venía).

    Por otro lado, fue evidente que algo andaba mal cuando tuvimos que cambiar los métodos fetchBondPriceFromQuoteNet y fetchBondPriceFromMorningStar de privados a públicos para poder hacer los tests…Había llegado el momento de implementar el patrón Strategy.

    Muy brevemente, lo que hicimos fue crear una clase para cada fuente de datos: QuoteNetBondPriceFetcher y MorningStarBondPriceFetcher.

    Ambas derivadas del nuevo BondPriceFetcher que simplemente se ve así:

    <?php
    abstract class BondPriceFetcher
    {
        /**
         * @param $symbol
         * @param \DateTime $date
         * @return mixed
         */
        abstract public function fetchPrice($symbol, \DateTime $date);
    }

    Ahora sí las cosas se ponen interesantes 🙂

    Después queda una nueva clase genérica BondPriceFetcherManager que tiene un método:

    public function fetchBondPrice($symbol, \DateTime $date)
    {
        $price = null;
    
        foreach ($this->getFetchers() as $fetcher ) {
            try {
                if ( $price = $fetcher->fetchPrice( $symbol, $date) ) {
    
                    break;
                }
            } catch ( Exception $e ) {
                $this->logger->addDebug( 'Error buscando precio del bono: '.$symbol.'-'.$date->format('Y-m-d').' usando '.get_class($fetcher).': '.$e->getMessage() );
            }
        }
    
        return $price;
    }

    Obviamente, también tiene su método

    public function addFetcher( BondPriceFetcher $fetcher )
    {
        $this->fetchers[] = $fetcher;
    
        return $this;
    }

    Y

    /**
     * @param Logger $logger
     * @return BondPriceFetcherManager
     */
    public function setLogger(Logger $logger)
    {
        $this->logger = $logger;
        
        return $this;
    }

    Y listo! Ya nos quedó una muy linda implementación que permite:

    1. Extender la cantidad de fuentes de datos (Basta con crear un nuevo BondPriceFetcher y agregar una nueva llamada a addFetcher)
    2. Alterar la prioridad que se le da a cada fuente (Basta con cambiar el orden de las llamadas a addFetcher) y sin alterar en absoluto el código propio del FetcherManager
    3. Testear automáticamente toda esta funcionalidad
    4. Reutilizar esta funcionalidad (Si se hiciera en un Bundle por ejemplo)

    Debo reconocer que, si bien no soy un gran fanático de los patrones de diseño, Strategy es definitivamente mi favorito 🙂

    El mayor aporte del patrón Strategy es el desacoplamiento (además de generar un código más elegante a fuerza de un poco más de texto)

    ¿Cómo fue tu experiencia con la aplicación de patrones de diseño?

  • Cómo evitar la expiración de las sesiones en PHP

    Cómo evitar la expiración de las sesiones en PHP

    En un proyecto que hice para un cliente me sucedió algo que no había previsto: un formulario dinámico resultó muy largo para la persona que tenía que realizar la carga y, cuando terminó el sistema la deslogueó automáticamente y perdió su trabajo 🙁

    Analizando un poco el problema me di cuenta de que la sesión había expirado a pesar de que el usuario estaba interactuando con el sistema… sólo que no se estaba produciendo ninguna comunicación cliente-servidor, ya que toda la acción estaba pasando del lado cliente.

    A juzgar por algunos comentarios que he leído por ahí, como:

    Tengo un archivo sesion.php que se incluye en cada pagina del proyecto, para validar tanto si la sesión esta activa, como si la sesión ha expirado, estoy haciendo pruebas con 30 segundos para no tardar tanto.

    El problema surge que el código se ejecuta cada vez que visito una pagina, por lo tanto si estoy navegando sobre la misma pagina, como por ejemplo haciendo click o registrando datos, la sesión aún así expira.

    O también:

    Tengo mi web en un servidor de GoDaddy y manejo sesiones pero estas caducan solas por inactividad..

    Parece que no estoy solo en este sufrimiento 🙂

    La solución que encontré fue diseñar un mecanismo de tipo keepAlive de modo de avisar al servidor que todavía había actividad del lado del cliente (¡y pedir por favor que no me dejen afuera!).

    Lo primero que hice entonces fue agregar este pequeño código javascript:

    <script type="text/javascript">
        var keep_alive = false;
    
        $(document).bind("click keydown keyup mousemove", function() {
            keep_alive = true;
        });
    
        setInterval(function() {
            if ( keep_alive ) {
                pingServer();
                keep_alive = false;
            }
        }, 1200000 );
    
        function pingServer() {
            $.ajax('/keepAlive');
        }
    </script>

    La idea es llevar un control sobre cualquier evento que se suceda del lado cliente (Click, presionar una tecla o mover el mouse) y, cada tanto (20 minutos en mi caso), si hubo actividad, avisarle al server para prolongar la vida de la sesión.

    Cómo extender dinámicamente la vida de una sesión

    Del lado del servidor no hay mucho que hacer en realidad, basta con tener algún script que responda a la URI /keepAlive (Preferentemente con un 200 OK) y listo.

    Hay algunas otras cosas a tener en cuenta si, como en mi caso, la aplicación está corriendo en un ambiente de hosting compartido (Algo que trato de evitar por todos los medios, pero bueno… en este caso no pude):

    1. Dónde se almacena la información de las sesiones propias de la aplicación
    2. Cuál es el tiempo de vida de la sesión
    3. Cada cuánto tiempo se libera la basura de las sesiones

    La respuesta a estas tres preguntas (y otras más) está en el viejo y querido php.ini. Específicamente en las claves:

    Como cualquier otro setting de php, puede modificarse desde el propio código de nuestra aplicación (¡Pero hay que acordarse de hacerlo!).

    Un detalle interesante es esta notita sobre session.gc_maxlifetime:

    Nota: Si diferentes scripts tienen diferentes valores de session.gc_maxlifetime pero comparten la misma ubicación para almacenar la información de sesión, la información del script con el mínimo valor será limpiada. En este caso use esta directiva junto con session.save_path.

    No puedo estar seguro al 100%, pero mi apuesta más fuerte es que este fue el problema: mi aplicación estaba guardando sus archivos de sesión en el mismo lugar que otra y, cuando pasó el garbage collector… ¡adiós!

    ¿Tuviste alguna vez problemas de este tipo con tus aplicaciones web?

  • Cómo conectarse a bases de datos distintas de MySQL desde PHP

    Cómo conectarse a bases de datos distintas de MySQL desde PHP

    Si bien es casi una redundancia hablar de PHP+MySQL (Algo así como GNU y Linux), la realidad es que esta santa asociación es casi casual.

    En PHP no existe un motor de base de datos preferido y otros de segunda. No voy a decir que PHP puede conectarse a cualquier motor de bases de datos (Habiendo pasado por la FCEyN aprendí bien a no usar los absolutos con ligereza :)), la realidad es que puede conectarse, de forma muy simple, a una amplia cantidad.


    A su vez, las opciones son varias, dependiendo principalmente del nivel técnico de quien deba implementar la solución. Paso a explicar:

    En el escalón más bajo están las funciones propias de php para realizar las conexiones:

    Y un largo etcétera (Si tenés que conectarte a una base de datos diferente de estas, podés encontrar información útil acá).

    Este es el modo de realizar la conexión si estás buscando programar en modo procedural o estructurado.

    Si estás un poco más ducho y buscás un modo de realizar tus conexiones usando objetos, podés usar PDO. Al usar esta interfase, las cosas se hacen mucho más simples (Particularmente cuando se trata de migrar a otro motor, ya que lo único que cambia es el string con el que establecés la conexión).

    Un ejemplo de conexión a un SqlServer que está en un servidor identificado como DB_SERVER:

    <?php
    
    try {
      $conexion = new PDO("sqlsrv:Server=DB_SERVER;Database=midb", "Usuario", "Contraseña");
    } catch ( PDOException $e ) {
      echo $e->getMessage();
    }
    • Y una vez que tenés las conexión lista, las operaciones se realizan del mismo modo si se trata de un MySQL, SqlServer, Oracle, etc…
    •  
    • Por último, si estás realmente decidido a llevar el tema a su máximo nivel, te recomiendo usar algún ORM (Si me preguntás a mí, con Doctrine deberías andar muy bien).

    ¿Te quedó alguna duda? ¡Posteala en los comentarios!

  • Cómo mostrar resultados de un proceso largo en tiempo real en una aplicación web

    Cómo mostrar resultados de un proceso largo en tiempo real en una aplicación web

    Un caso interesante en el que me tocó trabajar fue la implementación de un sistema de gamification para una red social de viajeros en la que trabajaba.

    Los responsables del producto estaban muy interesados en fomentar la generación de contenido por parte de los usuarios del sitio y se les ocurrió que ofrecer «galardones» a quienes más contenido subían a la plataforma era una buena forma de lograrlo.

    Dejando de lado la discusión sobre la viabilidad de la estrategia, hay unas cuantas lecciones interesantes desde el punto de vista de la implementación técnica.

    Algo de información de contexto:

    1. Las reglas de obtención de los galardones no eran triviales (Tampoco eran super complejas, básicamente, según el tipo de contenido de que se trataba había un número mínimo de aportes que permitían alcanzar el siguiente nivel y ese número iba aumentando, haciendo que alcanzar los niveles más elevados fuera mucho más difícil que los primeros).
    2. Era muy importante dar satisfacción inmediata al usuario que hiciera aportes.

    Dimos muchas vueltas respecto de cómo lograr esto, que implicaba la actualización de contadores en una base de datos relativamente grande, la evaluación de las reglas y el rendering del mensaje adecuado para la situación que el usuario acababa de generar.

    La solución que encontramos fue hacer un pequeño truco: hicimos el cálculo en el FrontEnd y en el BackEnd:

    Lo primero que hacíamos era enviar la información que el cliente necesitaría para galardonar al usuario en caso de ser necesario (las reglas de cálculo de los galardones y el estado actual de la cuenta del usuario) y luego asentábamos ese cambio en la base de datos.

    De esta forma logramos que el usuario tuviera su cucarda al momento exacto en que cruzaba el umbral y, a la vez, que el resultado no se perdiera.

    Esta es una forma más de tratar con procesos largos que, en un entorno de usuarios impacientes (como es una red social), funciona muy bien :).

    Un tiempo más tarde leí que Apple había usado un truco similar para lograr que el IPhone mostrara la lista de aplicaciones disponibles al momento en que el teléfono se encendía: tenían pre-calculada una imagen estática que se mostraba mientras la aplicación terminaba de cargar.

    Siempre me gustaron estos ejemplos de combinación de ingeniería con cierta picardía (como el de los espejos en el hotel de ascensores lentos).

    ¿Tenés algún otro ejemplo de cómo tratar con este tipo de situación?

  • Cómo mostrar progreso de procesamiento en un entorno Web

    Cómo mostrar progreso de procesamiento en un entorno Web

    Cuando se requiere tratar con un proceso largo se presenta un problema. Existen varias alternativas (algunas las discutíamos acá).

    Idependientemente de cuál sea la estrategia elegida, el objetivo es siempre el mismo: evitar que el visitante se aburra (o piense que la aplicación se colgó o algo parecido).

     

    Algo que hasta hace un tiempo era impensable (o al menos muy poco práctico) era la posibilidad de ir mostrando progreso a medida que el procesamiento avanza.

    El truco se compone de tres partes:

    1. El front end
    2. El script del lado del servidor que ejecuta el proceso
    3. El script que reporta el progreso obtenido hasta el momento

    Del lado del frontend se usa Ajax (y probablemente jQuery o algún otro framework JavaScript) para disparar la acción, algo como:

    <input type="button" id="trigger" />
    <div id="progreso" style="visibility: hidden">
      <p id="mensaje"></p>
    </div>
    <script type="text/javascript">
    $('#trigger').click(function(){
     $.post(
      'iniciarProceso.php',
      {
        param: value
      },
      function( data ) {
       $('#mensaje').text('Proceso iniciado...');
       $('#progreso').show();
      }
     );
    })
    </script>

    Con esto hemos logrado que el proceso se inicie (y que al visitante se le muestre un mensaje indicador).

    Para completar nuestro cometido necesitamos contar con algún mecanismo que consulte periódicamente en qué estado está el proceso (lo que se conoce como polling) y un servicio (del lado del servidor) que pueda brindar la información requerida:

    Del lado del cliente será algo como:

    <script type="text/javascript">
    function pollServer()
    {
     $.get(
      'processStatus.php',
      {
        param: value
      },
      function( data ) {
       $('#mensaje').text( 'Porcentaje de avance: ' + data.percent );
      } 
     );
    }
    
    setTimeOut( pollServer, 2000 );
    </script>

    Con este código se está solicitando una actualización de progreso al servidor cada 2 segundos (y mostrando el resultado al usuario).

    Veamos ahora cómo debería ser el código de processStatus.php:

    <?php
    header('Content-Type: text/javascript');
    $percent = calculateProgress(); // Probablemente esto consultara a la BD o a algun otro registro de progreso
    
    echo json_encode($percent);
    ?>

    La clave de este sencillo proceso es la definición del header (De modo que el cliente entienda que recibe datos en un formato compatible con el request realizado vía Ajax).

    Obviamente, se pueden hacer implementaciones mucho más vistosas (Por ejemplo usando Bootstrap o mostrando imágenes en lugar de mensajes) pero la base de este mecanismo es esta.

    ¿En qué casos creés que te serviría usar esta técnica?