Niano Niano
16Abr/101

Malditas direcciones – Los países, sus regiones y la madre que los parió a todos.

Mi relación amor-odio con las direcciones seguro que es muy parecida a la de cualquier desarrollador detallista que haya tenido que lidiar con ellas en alguna aplicación.

Las personas humanas, entre las cuales me incluyo depende del día, estamos repartidos en 203 estados soberanos distintos a lo largo y ancho del planeta. Os desafío a que encontréis dos con exactamente el mismo sistema postal.

El fenómeno de etiquetar y catalogar los lugares donde las personas viven o ejercen su profesión está justificado exclusivamente por la necesidad de comunicarse de manera distribuida. Si no, para rato le gustaría a la gente que los demás supieran dónde vive uno.

"Tras cinco leguas, jinete y animal, sufren por igual" - Haiku ad hoc

Tras cinco leguas/ jinete y animal/ sufren por igual

Ante la imposibilidad que suponía que los correos (predecesores de los actuales carteros) supieran dónde vive cada persona en un contexto de ebullición humana a cascoporrillo por todo el globo, más de una mente pensante tuvo la misma idea de registrar los nombres de las calles y caminos y ponerlos en una lista.

Y los que no tuvieran nombre, pues se les ponía uno. Luego vinieron los números de calle, pisos, letras, manos y demás datos que ayudaron a especificar la ubicación del destinatario al que había que enviarle el edicto de embargo, la sentencia de muerte o la factura del móvil. Finalmente, con la llegada de mejoras tecnológicas y la mayor descentralización de los sistemas postales de cada país, se introdujeron los códigos postales para facilitar la gestión de cartas y paquetes pintiparados.

Este proceso genérico se ha ido produciendo en cada país, cada uno a su manera. A nadie le gustaba que llegara el Imperio Británico y les dijera cómo tenían que mandar las cartas. Es por ello por lo que, por ejemplo, no tenemos códigos postales en Hong Kong o por lo que en muchos países anglófonos usan dos líneas para la dirección y si les hablas de calle, número, piso, letra... se ponen ojipláticos, caen en barrena, entran en bucle y la palomita les hace *pop* en la cabeza. No sabrán de qué narices les estás hablando.

El territorio de las direcciones es el lejano oeste informático en pleno peregrinaje: un terreno basto, desigual e inhóspito, minado de dudas, peligros e inseguridades.

Desde el punto de vista del análisis de software y de la arquitectura de bases de datos, no hay estructura o modelo sencillo, elegante, completo, coherente y mantenible que represente toda la casuística posible. Si tenemos una estructura para cada país es completo pero no es coherente ni mantenible. Si optamos por una estructura con el mínimo común denominador será sencilla y elegante, pero estará muy lejos de ser completa (y seguramente solo se componga de una columna con un identificador entero autoincremental). Así ad-eternum.

Wait a moment! Seguro que alguien más listo que yo (y que tú) ya ha pensado cómo solucionar esto. Repite mil veces Design Patterns y luego usa un poco de Google antes de seguir. Resulta que hay esperanza, al menos para parte del problema.

Como el logo de la ISO está protegido con copyright, pongo uno de Dharma, que mola más

ISO: International Standards Organization

Lo que más les gusta hacer a los señores de la ISO es catalogar y codificar cosas. Si le echáis un ojo a las ISO-3166-1 e ISO-3166-2, veréis que en los últimos (casi) 40 años se han dedicado a mantener y codificar un listado de países y sus subdivisiones. Existen 3 tipos de codificaciones para los países: la ISO-3166-1 alpha 2, la ISO-3166-1 alpha3 y la ISO-3166-1 numeric. España, por ejemplo es "ES", "ESP" y 724 a la vez, depende del sabor de la ISO elijas.

Actualmente la ISO-3166-1 alpha 2 se distribuye gratuitamente en la web de la ISO. Sin embargo, la ISO-3166-2 solo se puede conseguir pasando por caja y os aseguro que es la que queréis conseguir para un sistema de direcciones. Al fin y al cabo, no queremos que nuestros usuarios escriban en los campos "localidad" o "provincia" lo que les dé la gana, porque terminaremos con una preciosa base de datos llena de cosas como: "sebiya", "cebilla", "seviya" y "sevilla".

Los de la ISO son unos suizos cachondos donde los haya porque, a parte de cobrarte por una información que debería ser pública y gratuita, los jodíos no se encargan de mandarte actualizaciones de la base de datos cuando introducen cambios, sino que te envía un boletín de cambios para que tú, si eso, los apliques en tu base de datos. Por no hablar del formato de la base de datos, que es Microsoft Access (eso es otro cantar).

Así que si decides pasar por caja (las dos ISOs cuestan 366 francos suizos), no solo tendrás que importar una base de datos Access, sino que además tendrás que estar al loro de los boletines de cambios para mantener tus datos al día. Ouch!

Por lo menos te dan los nombres traducidos al inglés y al francés...

Alternativa: Geonames.org

No se puede describir con palabras el esfuerzo monumental que supone este proyecto. Para empezar todo lo publicado en geonames.org tiene licencia Creative Commons Attribution 3.0 License. Se trata de una base de datos mucho más completa que las ISO mencionadas ya que incluye *todo tipo* de topónimos relevantes como accidentes geográficos, monumentos, etc. y, por si no fuera poco, ponen un API de consulta a disposición del que la quiera utilizar. Os animo a echar un buen vistazo.

La pega (y la ventaja) de geonames.org es que depende del esfuerzo colectivo de su red de colaboradores y la base de datos no avanza al mismo ritmo en todas sus áreas. Por ejemplo, aún no se han corregido los datos a nivel de subdivisiones territoriales en España del último boletín de la ISO. El proyecto necesita todos los colaboradores que pueda conseguir, entre los cuales, tenemos algunos muy relevantes como Luistxo de Tagzania.

Otra pega que viene con Geonames es que nos podemos encontrar códigos ISO junto con otros estándares como el FIPS o símplemente creados ad-hoc. Sencillamente, es más importante el geoposicionamiento que la aplicación de estos estándares, muchas veces incompletos para determinadas regiones.

Además, si decidimos importar la base de datos de geonames, tendremos que filtrar mucha paja para quedarnos solo con los lugares poblados y las divisiones administrativas. Supone curro, pero no es descabellado. De lo que no te libras es de estar pendiente de los cambios publicados en los boletines de las ISO (y, de paso, les mandas los cambios a geonames para que actualicen la info).

Lo que haría Chuck Norris

Realmente Chuck Norris no necesita buscar direcciones porque se las sabe todas de memoria, pero si no fuera así, lo que haría es recorrer la Wikipedia e importar la información en su base de datos y completarla con los boletines de la ISO u otras fuentes. Es frustrante ver que la información está ahí, al alcance de las manos, pero no tenerla en un formato exportable/explotable.

Lo que yo haría

Esta es la parte menos importante del artículo, ya que eres tú quien debe decidir cómo implementar direcciones en tu aplicación. En mi caso, me haría las siguientes preguntas:

  • ¿Es importante para mi aplicación normalizar estas cosas?
  • ¿Supone una ventaja normalizar estas cosas en mi aplicación?
  • En caso de responder Sí en las dos, ¿Mi aplicación debe (por cuestiones legales o por interactuar con otras) usar códigos standard?
  • En caso de responder Sí buscaría una manera de conseguir datos de la ISO
  • En caso de responder No en cualquiera de las preguntas, seguramente usaría la base de datos de geonames o sus APIs para aligerar mi aplicación

Cabos sueltos

Hasta ahora solo hemos hablado de países, regiones y localidades. Aun hay que resolver cómo manipular las demás partes que pueden conformar una dirección. Vista la envergadura de este artículo, dejo el apasionante mucho de las calles, números, códigos postales, bloques, y demás salsas para el siguiente de esta serie.

¡Nos vemos en el siguiente artículo de esta serie!

Comparte este post:
  • Print
  • del.icio.us
  • Facebook
Etiquetado con: , , 1 Comentario
31Mar/103

Desarrollando aplicaciones sobre Twitter

Quién me lo iba a decir a mi cuando me di de alta en 2007. Twitter en aquella época no tenía ningún sentido para mi. Apenas tenía amigos que lo usaran y no le vi ninguna utilidad.

De hecho, hace bien poco publiqué un artículo poniéndolo a parir, pero como dijo aquel: rectificar es de sabios. Hoy en día me comunico más por Twitter que por cualquier otro medio online. Lo que más me gusta es la capacidad que tiene de descubrirte gente cercana con intereses afines. Gracias a Twitter he podido descubrir la gran red de profesionales de mi sector en el País Vasco que hace 1 año ni me imaginaba que existiera.

Hasta la fecha no he necesitado hacer ningún desarrollo relacionado con Twitter pero, en este momento, tanto en mi trabajo como en Trastos de Guerra, el fansite oficial de World of Warcraft que mantengo junto con Aitor y Juantxo, se están planteando desarrollos vinculados de algún modo con Twitter.

Bajo mi punto de vista, al margen de la potencia que tiene como red social, Twitter aporta una gran ventaja para cualquier desarrollo que implique gestión de usuarios. Sin ninguna duda, esto es cada vez un mayor problema en Internet (para los usuarios, no para nosotros, los desarrolladores). Yo ya no sé cuántas veces me he dado de alta en páginas web y llevar un control minucioso de las distintas credenciales de acceso es prácticamente imposible.

Twitter implementa un sistema de autenticación de usuarios mediante OAuth, cosa que nos puede solucionar la papeleta del registro y login de usuarios en nuestras aplicaciones. Por otro lado, el hecho de autenticar los usuarios de nuestras aplicaciones mediante sus cuentas de Twitter las convierte automáticamente en potencialmente "socializables". Existen otras buenas alternativas como OpenId pero, llegado al caso de obligar a alguien a que se registre en un sistema externo, en este momento prefiero mandarle a Twitter que a OpenId porque ofrece más funcionalidades.

El problema que me he encontrado de cara a implementar algo de Twitter en mis desarrollos es la heterogeneidad de las librerías que hay disponibles para PHP. Como norma derivada de los Design Patterns, siempre conviene reutilizar el trabajo de otros pero en este caso no hay una librería que reuna todas las condiciones mínimas como para resolver cualquier problema.

Las que implementan OAuth no implementan toda la API de Twitter, las que implementan la API de Twitter no están orientadas a objetos, las que están orientadas a objetos están obsoletas o no reciben mantenimiento y ninguna plantea soluciones a nivel de cacheo o modularidad.

Así que, remangado, me he puesto a trabajar en una librería modular orientada a objetos según los últimos estándares de PHP5.2 con los siguientes objetivos (copio y traduzco de la Wiki de GitHub donde la he albergado):

  • (Casi) Soporte completo de la API de Twitter
  • Estructura de 2 capas:
    • Capa inferior: Implementa el DAO para interaccionar con Twitter
    • Capa superior: Implementa Operaciones Abstractas e Interacciones entre Objetos
  • Cache de llamadas al API de Twitter (casi lista)
  • Sistema de Autenticación Modular (sistema standard básico ya implementado, obviamente)
  • No reinventar la rueda
  • Buena documentación
  • Test unitarios con PHPUnit

Hay temas que estoy procrastinando deliberadamente:

  • Optimización de código y estructura de herencia, clases, etc.
  • Definición funcional de la capa superior.

Quiero esperar que alguien empiece a usar la librería para que me eche una mano con estos temas.

La librería se llama TwiPHPr (tuífer), está publicada bajo licencia MIT y podéis probar ya la versión 0.1 que está subida a GitHub junto con las páginas del Wiki de detallan cuestiones de diseño, funcionalidades y demás historias.

Necesito ayuda en muchos campos así que estaré encantado de charlar con todo aquel que se sienta motivado y quiera aportar.

Comparte este post:
  • Print
  • del.icio.us
  • Facebook
Etiquetado con: , , 3 Comentarios
18Feb/103

Primer vistazo a Symfony2

El resto del año hasta la liberación de la primera versión de Symfony2 promete ser muy entretenida. Hoy hemos podido saborear la primera entrega de una versión de previsualización y es hora de ver diferencias y opinar.

Git vs Subversion

Symfony quiere dar carpetazo a Subversion para pasarse a Git, un sistema de control de versiones distribuido mucho más adecuado para proyectos en los que interviene una infinitud de desarrolladores.

Siendo un subversion-boy, este tema me escuece especialmente, porque he adaptado mi manera de trabajar al excelente duo Subversion-Trac pero he de reconocer que el cambio es completamente coherente y que va a facilitar que más y más gente intervenga y aporte código a Symfony. Solo espero que surjan herramientas como Trac que permitan a las organizaciones a montar su mini-reino de control de desarrollos Git lo antes posible.

Retrocompatibilidad

Adios con la manita. Piénsate muy bien cómo vas a desarrollar todos los proyectos a lo largo de este año, porque cuando quieras migrar a Symfony2 vas a tener que picar mucho. Aplicar buenas costumbres de programación orientada a objetos es más importante que nunca. Un buen punto de partida sería desarrollar los módulos de tus aplicaciones Symfony 1.x como plugins directamente. De este modo, la transición te dolerá menos (aunque me parece una locura).

Doctrine vs Propel vs Doctrine2

Aunque me considero más que solvente en Propel, soy un novatillo en Doctrine, por lo que no puedo entrar a valorar en profundidad sus bondades. Sin entrar en lo técnico, Propel va más con mi estilo de programación y me siento un poco "sucio" cuando hago algo en Doctrine. Supongo que se pasará con el tiempo...

Lo que no se puede discutir es que Doctrine es el camino a seguir, te guste o no. La historia de Propel y Doctrine es una historia muy triste: un ejemplo de las dificultades que un projecto OpenSource se puede encontrar en su vida. Si Propel hubiese tenido una evolución más sostenida seguramente hoy estaría en igualdad de oportunidades frente a Doctrine pero, lamentablemente, el frenazo de la versión 1.2 le ha costado el nacimiento de un serio competidor que además tiene el apoyo motriz de los propios creadores de Symfony.

Lo que no entiendo es que se quiera potenciar Doctrine cuando Propel gana en los benchmarks de rendimiento. Uno de los principales objetivos de Symfony2 es mejorar la eficiencia y el rendimiento del framework, por lo que apoyar Propel parecería lo adecuado. Mucho tiene que mejorar Doctrine2, creo yo.

Algo que me gustaría es dejar de depender de los drivers PDO y empezar a usar drivers nativos para aprovechar un poquito más las características particulares de cada motor de bases de datos. Según he leido, parece que se podrá con esta nueva versión :)

Symfony vs Symfony2

En cuanto al framework en sí, la nueva versión parece mucho más estructurada y de nivel más bajo que la actual. Es muy recomendable leerse la QuickTour para entrar en detalles, pero grosso-modo:

  • La organización del código se hace a través de los namespaces de PHP 5.3.
  • Las configuraciones se pueden hacer en PHP, YAML, XML o INI. Se recomienda XML.
  • Se implementa un paradigma MVC más convencional. El Controlador, la Vista y el Modelo son muchos más presentes.
  • Los plugins y los módulos se fusionan en los bundles. Cada bundle tiene la estructura de una app actual: librerías, plantillas, javascript, css, etc.
  • Los bundles están diseñados para encapsular una determinada funcionalidad y para ser intercambiado entre desarolladores y proyectos.
  • La distinción entre acciones y componentes se diluye. Es como si las acciones se vistieran de componentes y su integración en las vistas es mucho más flexible.
  • El sistema de enrutado es muy parecido al actual, pero parte se delega en los bundles para dar mayor flexibilidad.
  • Ahora las variables entran en las acciones desde el sistema de enrutado.
  • Los helpers vuelven para quedarse. El concepto ha sido rediseñado y ahora son objetos que amplían la funcionalidad de la vista.
  • Mayor coherencia en torno a las clases responsables de las peticiones, respuestas, cabeceras, sesiones... Pocos cambios al respecto.

A partir de aquí el territorio es completamente nuevo para los que venimos de Symfony. El sistema de directorios es completamente distinto y se basa principalmente en los bundles, el proceso de bootstraping del controlador es nuevo también... La QuickTour es de obligada lectura definitivamente. También es recomendable ver la presentación de Fabien Potencier sobre Symfony2.

Resumen

Como digo al principio de este artículo, va a ser un año movidito. Hay que ponerse las pilas. Personalmente, tengo que suplir el handicap de no tener soltura con Doctrine, pero todo se andará. Me gusta mucho la dirección en la que se está moviendo el proyecto Symfony, aunque lo de Doctrine me resulta difícil de aceptar.

A Symfony2 le falta mucho para estar listo. Por ejemplo, no tiene un framework de formularios, generador de CRUDs, framework de testeo y Doctrine solo está integrado parcialmente. Espero que tengamos pronto una versión alfa para hincarle el diente y volver a repetir la historia que viví cuando le metí mano por primera vez al alfa 0.6 de Symfony allá por el 2006.

Comparte este post:
  • Print
  • del.icio.us
  • Facebook
Etiquetado con: 3 Comentarios
28Ene/100

Parche para que Symfony i18n:find encuentre alts y titles

Estoy poniendome al día con Symfony 1.4 y una de las cosas que tenía pendientes de ver era todo el asunto de la internacionalización de contenidos.

El panorama del i18n en Symfony ha mejorado muchísimo desde que me tocó sufrirlo usarlo por última vez. Ahora él solito te dirá qué textos están internacionalizados en tus plantillas y cuales no lo están con las tareas i18n:extract e i18n:find.

Es normal que se nos olviden algunos textos en el proceso de implementación de plantillas en nuestro motor de presentación y los más proclives a quedarse fuera suelen ser los atributos de las etiquetas html. Estoy hablando de los alt y los title, que llevan un contenido preciado por los motores de indexación y esencial a nivel de usabilidad.

Pues bien, resulta que i18n:find no tiene en cuenta estos atributos, por lo que he procedido a modificar la tarea para que los tenga en cuenta. Aquí va el parche:

          if ($node->hasAttributes())  
          {
            foreach($node->attributes as $attr) 
            {
              if (in_array($attr->name, array('alt', 'title')) && trim($attr->value) != "") 
              {
                $strings[$template][] = $attr->value;
              }
            }
          }

Hay que enchufar esto en la línea 92 del fichero lib/task/i18n/sfI18nFindTask.class.php.

Voy a ver si les mando el parche a la gente de Symfony para que lo incluyan si consideran que el cambio vale la pena.

Otro tema que tengo pendiente es el de montar el subsistema de i18n para que ataque a una base de datos en MySQL. Ya lo he hecho en el pasado (para Fon) y tengo que ver cómo ha cambiado y si vale la pena para la aplicación que estoy montando. En cualquier caso, sigue sin haber una guía clara y definitiva que explique cómo hacer esto, por lo que en cuanto lo consiga lo publicaré aquí.

Bola Extra

Parece que algunos editores gettext necesitan que se declare el charset explícitamente en los ficheros .po, porque si no se hacen la picha un lío y destrozan los ficheros de traducciones. Aquí va el parche para que i18n:extract incluya meta-información en los .po con el charset:

      $result['meta']['Content-Type'] = 'text/plain; charset=utf-8';

Enchufa esto en la línea 166 del fichero lib/i18n/sfMessageSource_gettext.class.php

Comparte este post:
  • Print
  • del.icio.us
  • Facebook
Etiquetado con: , , No hay comentarios
28Dic/090

Inyección de dependencia en PHP5

La gracia de tener en marcha proyectos personales es que puedes permitirte perder un poco de tiempo para hacer cosas nuevas o darle a tu código esa "vuelta de tuerca" que en tu trabajo te ahorras por no tener tiempo para virguerías.

Una de las últimas con las que me he entretenido mucho ha sido con la inyección de dependencia, a partir de un magnífico artículo sobre ello del creador de Symfony, Fabien Potencier.

En este caso, necesitaba dotar a una clase de la posibilidad de atacar a un servicio externo por curl. Además necesitaba poder activar y desactivar la utilización de un proxy para la transmisión y el uso de caché en función de la situación, por lo que la inyección de dependencia parecía un método adecuado para la ocasión.

Inicialmente, planteé un escenario con tres clases: IO, IOProxied e IOCached, pero por un lado no se puede desligar de una manera elegante el manejo de proxies de la clase que realiza finalmente la llamada curl y, por otro lado, es matar moscas a cañonazos.

Finalmente, el escenario que decidí desarrollar contiene la clase IO, con el core de la funcionalidad: realiza llamadas curl vía proxy si es necesario. Después está la clase IOCached, que implementa la capa de caché extendiendo IO. Por último, tengo la clase Armory que, vía inyección de dependencia, realiza sus llamadas con o sin cache, en función de cómo instanciemos sus objetos.

Al final, el código queda así:

...
$ioParams = array(
	'proxy' => array(
		'ip' => sfConfig::get('curl_proxy_ip'),
		'port' => sfConfig::get('curl_proxy_port')
	),
	'userAgent' => true,
	'language' => $request->getParameter('language'),
	'due' => 30
);
$armory = new Armory(new IOCached($ioParams));
...

o, sencillamente, así:

...
$ioParams = array(
	'proxy' => array(
		'ip' => sfConfig::get('curl_proxy_ip'),
		'port' => sfConfig::get('curl_proxy_port')
	),
	'userAgent' => true,
	'language' => $request->getParameter('language')
);
$armory = new Armory(new IO($ioParams));
...

La diferencia está en la instancia que le pasamos a Armory en su constructor.

Como bola extra, aquí va el código de estas clases. No las he revisado demasiado, así que disculpad cualquier fallito que podáis encontrar. Si intentáis hacer un copy pasteo de este código seguramente no funcione directamente en vuestras aplicaciones ya que hace llamadas a métodos de otras clases que no incluyo.

Clase IO:

/**
 * Input Output class
 *
 * This class implements an abstraction layer for curl HTTP calls
 *
 * TODO:
 *  - Change behavior of user agent configuration option to let the string itself to be set
 *  - Implement dependency injection for log and debug
 *
 * @author     Guillermo Gutiérrez guille@nianoniano.com
 */
class IO {
	/**
	 * Stores the description for connection errors using curl. Connection errors come from connection timeouts and DNS resolution errores from curl interaction with the remote host or the proxy
	 * @var string
	 */
	const IO_CONNECTION_ERROR = "Host/proxy resolutionor connection error";
	/**
	 * Stores the description for remote host error response codes
	 * @var string
	 */
	const IO_HOST_ERROR = "Received an error from the host";
	/**
	 * Stores the description for timeouts after connection is established
	 * @var string
	 */
	const IO_TIMEOUT = "Timeout error";
	/**
	 * Stores the description for a generic error
	 * @var string
	 */
	const IO_ERROR = "Generic error";
 
	/**
	 * Stores the User Agent string to be used in the HTTP headers if needed
	 * @var unknown_type
	 */
	const USER_AGENT = "Mozilla/5.0 (X11; U; Linux x86_64; es-ES; rv:1.9.0.8) Gecko/2009032712 Ubuntu/8.10 (intrepid) Firefox/3.0.8";
 
	/**
	 * Stores the IP for the proxy
	 * @var string with format: www.xxx.yyy.zzz
	 */
	protected $proxyIp;
	/**
	 * Stores the port number for the proxy
	 * @var integer
	 */
	protected $proxyPort;
	/**
	 * Stores a boolean flag to tell this instance if it has to include User Agent HTTP header or not
	 * @var boolean
	 */
	protected $userAgent;
	/**
	 * Stores the language for the communication
	 * @var string with 2 or 5 character code
	 */
	protected $language;
	/**
	 * Stores the maximum number of tries to be perfomed before giving up
	 * @var integer
	 */
	protected $maxTries;
	/**
	 * Stores the maximum number of seconds to wait for connection to the remote host or proxy before giving up
	 * @var integer
	 */
	protected $connectionTimeout;
	/**
	 * Stores the maximum number of seconds to wait for data before giving up
	 * @var integer
	 */
	protected $timeout;
 
	/**
	 * Class constructor. It may receive an array of optional configuration options:
	 *
	 *  - proxy: An array with IP and port indexes specifying the information to use a proxy for calls. Optional
	 *  - userAgent: Boolean that will tell the instance to use User Agent header in the calls or not. Default: true
	 *  - language: 2 or 5 character culture code. Default: es
	 *  - maxTries: Integer with the number of tries before throwing an IO_CONNECTION_ERROR exception. Default: 3
	 *  - connectionTimeout: Integer with the number of seconds to wait establishing the connection to the remote host before throwing an IO_CONNECTION_ERROR. Default: 5
	 *  - timeout: Integer with the number of seconds to wait for data from the remote host before throwing an IO_TIMEOUT. Default: 15
	 *
	 * @param $options array with options to be set
	 */
	public function __construct($options = array()) {
		if (array_key_exists('proxy', $options)) {
			$this->proxyIp = $options['proxy']['ip'];
			$this->proxyPort = $options['proxy']['port'];
		}
		if (array_key_exists('userAgent', $options)) {
			$this->userAgent = $options['userAgent'];
		} else {
			$this->userAgent = true;
		}
		if (array_key_exists('language', $options)) {
			$this->language = $options['language'];
		} else {
			$this->language = 'es';
		}
		if (array_key_exists('maxTries', $options)) {
			$this->maxTries = max(1, $options['maxTries']);
		} else {
			$this->maxTries = 3;
		}
		if (array_key_exists('connectionTimeout', $options)) {
			$this->connectionTimeout = max(1, $options['connectionTimeout']);
		} else {
			$this->connectionTimeout = 5;
		}
		if (array_key_exists('timeout', $options)) {
			$this->timeout = max(1, $options['timeout']);
		} else {
			$this->timeout = 15;
		}
	}
 
	/**
	 * Returns the language for this instance
	 */
	public function getLanguage() {
		return $this->language;
	}
 
	/**
	 * Returns the proxy's IP for this instance
	 */
	public function getProxyIp() {
		return $this->proxyIp;
	}
 
	/**
	 * Returns the proxy's port for this instance
	 */
	public function getProxyPort() {
		return $this->proxyPort;
	}
 
	/**
	 * Returns the user agent for this instance
	 */
	public function getUserAgent() {
		return $this->userAgent;
	}
 
	/**
	 * Sets the proxy's IP for this instance
	 *
	 * @param string $proxyIp with the IP to be used (xxx.xxx.xxx.xxx format)
	 */
	public function setProxyIp($proxyIp) {
		$this->proxyIp = $proxyIp;
	}
 
	/**
	 * Sets the proxy's port for this instance
	 *
	 * @param integer $proxyPort with the port number to be used
	 */
	public function setProxyPort($proxyPort) {
		$this->proxyPort = $proxyPort;
	}
 
	/**
	 * Sets the user agent for this instance
	 *
	 * @param bool $userAgent with a boolean that will decide if we use user agent header or not
	 */
	public function setUserAgent($userAgent) {
		$this->userAgent = $userAgent;
	}
 
	/**
	 * Sets the language for this instance
	 *
	 * @param string $language with the language (culture) to be used (2 or 5 character code)
	 */
	public function setLanguage($language) {
		$this->language = $language;
	}
 
	/**
	 * Performs a curl call to a given url, using the given method and post payload if it is present
	 *
	 * @param string $url with the url to be called
	 * @param string $method with the method to be used (get or post). Default: get
	 * @param string $postPayload (optional) with the post payload to be sent (for form simulation)
	 */
	public function call($url, $method = 'get', $postPayload = null) {
		$stop = false;
		$tries = 0;
		while(!$stop) {
			$tries++;
			Logger::log(vsprintf("Performing try #%s to %s", array($tries, $url)));
			$ch = curl_init($url);
			curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
			curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->connectionTimeout);
			curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
			curl_setopt($ch, CURLOPT_HTTPHEADER, array(sprintf('Accept-Language: %s', $this->language)));
			if ($method == 'post') {
				curl_setopt($ch, CURLOPT_POST, true);
				curl_setopt($ch, CURLOPT_POSTFIELDS, $postPayload);
			}
			if ($this->userAgent) {
				curl_setopt($ch, CURLOPT_USERAGENT, self::USER_AGENT);
			}
			if ($this->proxyIp != null && $this->proxyPort != null) {
				curl_setopt($ch, CURLOPT_PROXY, vsprintf("%s:%s", array($this->proxyIp, $this->proxyPort)));
			}
			$result = curl_exec($ch);
			switch(curl_errno($ch)) {
				case CURLE_OK:
					$stop = true;
					break;
				case CURLE_COULDNT_RESOLVE_PROXY:
				case CURLE_COULDNT_RESOLVE_HOST:
				case CURLE_COULDNT_CONNECT:
					Logger::log($url." => ".curl_error($ch), __METHOD__, Logger::CRIT);
					if ($tries == $this->maxTries) {
						throw new Exception(self::IO_CONNECTION_ERROR);
					}
					break;
				case CURLE_HTTP_RETURNED_ERROR:
					Logger::log($url." => ".curl_error($ch), __METHOD__, Logger::WARNING);
					if ($tries == $this->maxTries) {
						throw new Exception(self::IO_HOST_ERROR);
					}
					break;
				case CURLE_OPERATION_TIMEDOUT:
					Logger::log($url." => ".curl_error($ch), __METHOD__, Logger::WARNING);
					if ($tries == $this->maxTries) {
						throw new Exception(self::IO_TIMEOUT);
					}
					break;
				default:
					Logger::log($url." => ".curl_error($ch), __METHOD__, Logger::WARNING);
					throw new Exception(self::IO_ERROR);
					break;
			}
			$stop = ($stop || $tries == $this->maxTries);
		}
		curl_close($ch);
		return $result;
	}
 
	/**
	 * Prepares params to be added to a url for a get call
	 *
	 * @param mixed $params Array or string with params
	 */
	public static function prepareParams($params) {
		$newParams = "";
		if (is_array($params) && count($params) > 0) {
			foreach ($params as $name => $value) {
				$newParams .= "$name=$value&";
			}
			$newParams = substr($newParams, 0, -1);
		} else {
			$newParams .= trim($params);
		}
		return $newParams;
	}
}

Clase IOCached:

/**
 * Input Output class, cached flavour
 *
 * This class is used to abstract curl calls and implements a cache layer to economize network usage
 *
 * TODO:
 *  - Adapt log and debug when it's implemented in IO
 *
 * @author     Guillermo Gutiérrez guille@nianoniano.com
 */
class IOCached extends IO {
	protected $due;
 
	/**
	 * Class constructor. It may receive an array of optional configuration options:
	 *
	 *  - due: Integer with the timestamp for the date and time that the cache will due for the calls this instance will perform
	 *  - and everyone defined in IO class
	 *
	 * @param $options array with options to be set
	 * @see IO
	 */
	public function __construct($options = array()) {
		parent::__construct($options);
		if (array_key_exists('due', $options)) {
			$this->due = $options['due'];
		} else {
			throw new Exception(__CLASS__." constructor needs a 'due' option to be set");
		}
	}
 
	/**
	 * Performs a curl call to a given url, using the given method and post payload if it is present
	 *
	 * @param string $url with the url to be called
	 * @param string $method with the method to be used (get or post). Default: get
	 * @param string $postPayload (optional) with the post payload to be sent (for form simulation)
	 */
	public function call($url, $method = 'get', $postPayload = null) {
		$postPayload = IO::prepareParams($postPayload);
		$hash = $this->getHash($url, $postPayload);
		$ioCache = IoCachePeer::retrieveByPK($hash);
		/* @var ioCache IoCache */
		if (!($ioCache instanceof IoCache)) {
			Logger::log("Creating cache for {".$method."} $url", __METHOD__, Logger::INFO);
			$ioCache = new IoCache();
			$ioCache->setHash($hash);
			$ioCache->setInfo(vsprintf(
				"{%s} %s (%s) - User Agent: %s",
				array(
					$method,
					$url,
					($postPayload != "") ? $postPayload : "no payload",
					($this->getUserAgent()) ? "ON" : "OFF"
				)
			));
		} else {
			Logger::log("Found cache for {".$method."} $url", __METHOD__, Logger::INFO);
		}
		if ($ioCache->getTs('U') < (time() - $this->due)) {
			$ioCache->setContent(parent::call($url, $method, $postPayload));
			$ioCache->setTs(time());
			$ioCache->save();
			Logger::log("Cache updated with fresh content", __METHOD__, Logger::INFO);
 
		}
		return $ioCache->getContent();
	}
 
	/**
	 * Returns the calculated hash that identifies the given url and param string pair
	 *
	 * @param $url string
	 * @param $params string
	 */
	private function getHash($url, $params) {
		return sha1($this->getUserAgent().$url.$params);
	}
}

La estructura de datos para la versión cacheada es super sencilla:

CREATE TABLE  `tdg_delta`.`io_cache` (
  `hash` varchar(255) NOT NULL,
  `info` varchar(255) NOT NULL,
  `ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `content` longtext,
  PRIMARY KEY  (`hash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Y como bola extra, la clase Armory, que hace un par de operaciones sencillas contra la Armería (ojo, sin comentarios: you're on your own):

class Armory {
	protected $io;
 
	public function __construct($io) {
		$this->io = $io;
	}
 
	public function searchItems($name) {
		$url = sfConfig::get('armory_eu_url') . "/" . sprintf(sfConfig::get('armory_query_search_item'), $name);
		$data = new SimpleXMLElement($this->io->call($url));
		$itemXMLList = $data->xpath('//item');
		$itemList = array();
		foreach ($itemXMLList as $itemXML) {
			$item = array();
			$item['id'] = (int)$itemXML['id'];
			$item['name'] = (string)$itemXML['name'];
			$item['rarity'] = (int)$itemXML['rarity'];
			$item['icon'] = (string)$itemXML['icon'];
			$itemList[] = $item;
			self::getArmoryIcon($item['icon']);
		}
		return array_slice($itemList, 0, 25);
	}
 
	public static function getArmoryIcon($image) {
		$localPath = sfConfig::get('sf_root_dir') . "/web" . sfConfig::get('armory_icon_local_path') . "/$image.png";
		if (!file_exists($localPath)) {
			$remotePath = sfConfig::get('armory_eu_url') . sfConfig::get('armory_icon_remote_path') . "/$image.png";
			$io = new IO(array('proxy' => array('ip' => sfConfig::get('curl_proxy_ip'), 'port' => sfConfig::get('curl_proxy_port'))));
			file_put_contents($localPath, $io->call($remotePath));
		}
	}
 
	public function searchChars($name, $wowServerGroupId = 1) {
		if (trim($name) == "") {
			$name = "Donald";
		}
 
		$wowServerGroup = WowServerGroupPeer::retrieveByPK($wowServerGroupId);
		if (!($wowServerGroup instanceof WowServerGroup)) {
			Logger::log("Received server group doesn't exist", __METHOD__, Logger::CRIT);
			throw new Exception("Wrong server group selected");
		}
 
		// Get content from armory
		$url = $wowServerGroup->getArmoryUrl()."/".sprintf(sfConfig::get('armory_query_search_char'), $name);
		$data = new SimpleXMLElement($this->io->call($url));
		$characterXMLList = $data->xpath('//character');
 
		// Realms and guilds
		$criteria = new Criteria();
		$wowRealms = array();
		foreach($wowServerGroup->getWowRealms($criteria) as $wowRealm) {
			$wowRealms[$wowRealm->getName()] = $wowRealm;
		}
		$wowGuilds = WowGuildPeer::doSelectIndexedByPK($criteria);
 
		// Parse chars
		$wowChars = array();
		foreach($characterXMLList as $char) {
			$wowChar = new WowChar();
			$wowChar->setWowClassId((int)$char['classId']);
			$wowChar->setWowGenderId((int)$char['genderId']);
			if (!array_key_exists((string)$char['realm'], $wowRealms)) {
				$wowRealm = new WowRealm();
				$wowRealm->setName((string)$char['realm']);
				$wowRealm->setWowServerGroupId($wowServerGroup->getWowServerGroupId());
				$wowRealm->save();
				$wowRealms[$wowRealm->getName()] = $wowRealm;
			}
			if ((int)$char['guildId'] != 0) {
				if (!array_key_exists((int)$char['guildId'], $wowGuilds)) {
					$wowGuild = new WowGuild();
					$wowGuild->setWowGuildId((int)$char['guildId']);
					$wowGuild->setName((string)$char['guild']);
					$wowGuild->setWowRealmId($wowRealms[(string)$char['realm']]);
					$wowGuild->save();
					$wowGuilds[$wowGuild->getWowGuildId()] = $wowGuild;
				}
				$wowChar->setWowGuildId((int)$char['guildId']);
			}
			$wowChar->setWowRealmId($wowRealms[(string)$char['realm']]->getWowRealmId());
			$wowChar->setLevel((int)$char['level']);
			$wowChar->setName((string)$char['name']);
			$wowChar->setWowRaceId((int)$char['raceId']);
			$wowChars[] = $wowChar;
		}
		return $wowChars;
	}
 
}
Comparte este post:
  • Print
  • del.icio.us
  • Facebook
Etiquetado con: , , No hay comentarios
28Sep/090

Uff que pereza…

Definitivamente, parece que tener un blog me viene bien cuando estoy aburrido... Últimamente estoy bastante liadete tanto en lo profesional como en lo ocioso.

... en lo profesional

Parece que el proyecto en el que estoy metido está dando un curioso giro que no esperaba. Vamos a ver, si a parte de modernizar la plataforma de venta online, damos un poquito de coherencia a las distintas fuentes de datos que disponemos por aquí: ERP, PLM, CMS y toda una plétora de más TLAs divertidos y curiosetes.

El destino ha querido que yo me meta en semejante berenjenal y que retome mis estudios de Java, por lo que tengo dedos, zarpas, muñecas, codos y hombros metidos en ver por dónde tiro: ¿Google Web Toolkit? ¿Struts? ¿Struts 2? ¿Spring? Sobre todo, doy las gracias a los amigos que me están echando una mano: Carol, Jordi, Virgilio, espero que no me añadáis a la lista de ignorados en Facebook y otras redes sociales :P Estoy teniendo muy en cuenta todos vuestros consejos y de verdad que me vienen de perlas.

La decisión es muy difícil, porque las herramientas que tengo que diseñar tienen una carga muy grande en el diseño de la UI, que debe ser muy usable por lo complejo de las operaciones que han de realizarse en ellas, por lo que me gustaría mucho poder usar Google Web Toolkit (cada vez me gusta menos "perder el tiempo" haciendo HTMLs, CSSs y Javascripts). Por otro lado, tengo las letras MVC grabadas a fuego en el cerebro, por lo que GWT no me convence, ya que tendría que montarme la película de la M y la C (y una v, pero minúscula) por mi cuenta. Los demás frameworks, por otro lado, me dan todo lo que necesito, pero tendría que codear toda la maquetación y UIs yo solito y a manubrio, así que no sé qué prefiero más.

Además, trato de decidir si una aproximación MVC "purista" es la más apropiada, dado que con GWT lo que estaría implementando sería mucho más parecido a una aplicación de escritorio que a una web (Google Gears is your friend). Igual puedo dar con algo que me permita separar de una manera racional y mantenible el contenido de la presentación y de la lógica. Son las cosas de no haber realizado antes un proyecto serio en Java. Me falta el punto de partida y si la cagamos en esto, nos podemos dar por jodidos, como diría aquel.

... en lo ocioso

Han empezado las series: Heroes (cacota del quince), Fringe, Flash Forward y The Big Bang Theory, oiga! Warehouse 13 parece que ha terminado su primera temporada y lo ha hecho de una manera muy digna. Estaremos ahí para la segunda.

He retomado el World of Warcraft también. Como más de uno sabrá, llevo jugando más de 4 años a este MMORPG, pero hace unos meses lo dejé por diversos motivos. El caso es que con la próxima expansión se van a pasar por la piedra Azeroth y dejará de existir tal y como lo hemos conocido desde hace casi 5 años ya. Esto ha despertado en mi una ansiedad que se ha materializado en alguna cosa de la que pronto os hablaré y en la urgencia de completar ciertos logros relacionados con el viejo Azeroth. En concreto, me he sacado el título de Maestro Cultural, que consiste en completar 650 misiones en Kalimdor y 550 en los Reinos del Este, los dos continentes que conforman el viejo Azeroth.

Cuando empecé con este logro me faltaban unas 500 misiones en total y os puedo garantizar que no es moco de pavo, ni mucho menos. Hay que dedicarle mucho tiempo tanto jugando como investigando dónde te dan misiones. Llega un momento en el que te faltan muy pocas misiones para terminar pero no encuentras nadie que te las dé. En cualquier caso, ya lo tengo hecho, y ahora me puedo concentrar en completar las mazmorras y bandas del viejo Azeroth que me faltan con la ayuda de la gente de mi querida hermandad, Wipe Express: Anh Qiraj (20 y 40 personas), Molten Core y Zul'Gurub (igual me estoy dejando 1, no recuerdo bien).

También diré que mi novia y yo hemos estado subiendo nuestra pareja de personajes Naiura y Cerdotesan, maga y sacerdote respectivamente, que ya cumplen los 13 niveles :P Está siendo muy divertido hacerlo.

Antes de que me tachéis injustamente de no tener vida, os daré algún argumento para borrar ese pensamiento de vuestras cabezas: También he estado en el Festival de Cine Internacional de San Sebastián viendo películas, me he ido de cañas y he estado con mi familia :P

Resumiendo

Entre lo "movido" de mi vida en este momento y que he retomado Twitter, este blog sufrirá probablemente un decremento en la frecuencia de publicación de artículos. Os sugiero leerlo por RSS o seguirme en Twitter (ggalmazor), donde avisaré de nuevos artículos. Mientras tanto, que el SEO y San Google hagan su trabajo.

Comparte este post:
  • Print
  • del.icio.us
  • Facebook
15Sep/090

Scripts en Android! Yeah!

Los mozos y mozas de Google Labs están que no paran. Al margen del nuevo servicio de visionado de noticias que se llama Google Fast Flip, que me ha chivado Iñigo que existe, he encontrado un proyecto que dota a las bichas con Android de un entorno de ejecución de scripts. Por ahora los lenguajes que se soportan son Python, Perl, JRuby, Lua, BeanShell y shell, pero añadirán más en el futuro (a ver si cuela y añaden PHP, aunque no es muy robusto para scripting).

Se pueden hacer chuladas como esta en LUA, que silencia el móvil si lo pones boca abajo:

--Placing the phone face down will disable the ringer. Turning it face up again will enable
--the ringer.
require "android"
android.startSensing()
silent = false
while true do
  e = android.receiveEvent()
  facedown = e.result and e.result.zforce and e.result.zforce < -5
  if facedown and not silent then
    android.vibrate(100)  --A short vibration to indicate we're in silent mode.
    android.setRingerSilent(true)
    silent = true
  elseif not facedown and silent then
    android.setRingerSilent(false)
    silent = false
  end
  android.sleep(1)
end

Así de sencillo :) ¡Qué ganas de conseguir de una vez el HTC Hero!

Comparte este post:
  • Print
  • del.icio.us
  • Facebook
Etiquetado con: No hay comentarios
1Sep/091

Reloj-juguete en JavaScript

Me he inspirado He copiado de Microsiervos el concepto de un reloj que da la hora aproximada como si te la dijera alguien por la calle después de decirle la famosa frase:

¿Tiene usted hora, caballero?

(Siempre y cuando se trate de un hombre, claro está)

Ellos, a su vez lo vieron en no sé dónde blablabla, así que os pongo el link original a la web de Simon Heys, quien parió la idea el primero, y así hacemos justicia. Se trata de un screensaver para MacOSX muy chulo.

La demo la tenéis aquí (http://www.nianoniano.com/test/reloj.html), completamente fusilable para el que quiera y pueda.

Personalmente, he de decir, que sirve bien de ejemplo del uso de Prototype para recorrer el árbol DOM y hacer cosicas con los elementos en función de su clase CSS. También he de decir que lo suyo es hacerse una clase JavaScript como dios manda y que no lo veréis implementado en clases porque el ejecutor de procesos periódicos me daba problemas con la clase y no tenía ganas de perder más tiempo con la chorrada. Se admiten sugerencias y las correcciones serán toleradas con aplomo y hombría por un servidor.

Pinche en la imagen para acceder al reloj, oiga.

Pinche en la imagen para acceder al reloj, oiga.

Comparte este post:
  • Print
  • del.icio.us
  • Facebook