Niano Niano
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
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
29Dic/090

Colorea los logs de Symfony

Si eres uno de esos desarrolladores a los que nos gusta hacer un "tail -f" y ver los logs de tus aplicaciones vía shell, esta clase te puede ayudar con tus logs de Symfony.

Se trata de una clase con un par de métodos estáticos que colorearán los mensajes que vayas a añadir al log en función de su tipo o severidad. Está implementado de una manera muy guarrilla funcional y seguro que se puede modificar fácilmente para extender Symfony como dios manda usando un poco de herencia.

No la he probado aun con Symfony 1.3 ó 1.4, pero no creo que falle y si os causa algún problema solo hay que corregir una línea (teóricamente), por lo que lo publico tal cual. La única pega que tiene esta clase es que ensuciará un poco los logs cuando los veáis a través del debug web ya que veréis los códigos ANSI. Como he dicho antes, esta clase es sobre todo para los que inspeccionamos los logs en shell.

Logger.class.php:

/**
 * This class will use Symfony's logging system adding some ANSI color codes
 * depending on the severity/type of the message. The goal is to provide a quick
 * visual recognition of log lines on shell.
 *
 * TODO:
 * Actually do some inheritance here and extend Symonfy intself
 *
 * WARNING:
 * By using this class you will find some weird codes in the web debug interface.
 *
 * @author Guillermo Gutiérrez guille@nianoniano.com
 */
class Logger {
	// These are the log levels defined in Symfony
	const EMERG = 0; // System is unusable
	const ALERT = 1; // Immediate action required
	const CRIT = 2; // Critical conditions
	const ERR = 3; // Error conditions
	const WARNING = 4; // Warning conditions
	const NOTICE = 5; // Normal but significant
	const INFO = 6; // Informational
	const DEBUG = 7; // Debug-level messages
 
	const ANSI_END = "\033[0m";
 
	/**
	 * Adds a message to the log
	 *
	 * @param string $msg The message to be added
	 * @param string $module The module that is producing this message. Default: _DEBUG_
	 * @param string $severity The severity of the message. Must be one of the class constants
	 */
	public static function log($msg, $module = '_DEBUG_', $severity = self::DEBUG) {
		// I'm sure that there is a more elegant way to do this
		if (!in_array($severity, array(
			self::EMERG,
			self::ALERT,
			self::CRIT,
			self::ERR,
			self::WARNING,
			self::NOTICE,
			self::INFO,
			self::DEBUG
			))) {
			throw new Exception(__METHOD__ . " requires a valid severity code");
		}
		switch ($severity) {
			case self::EMERG:
			case self::ALERT:
				$style = "\033[0;49;31;1m";
				break;
			case self::CRIT:
			case self::ERR:
				$style = "\033[0;49;31m";
				break;
			case self::WARNING:
				$style = "\033[0;49;33;1m";
				break;
			case self::NOTICE:
			case self::INFO:
				$style = "\033[0;49;32m";
				break;
			case self::DEBUG:
				$style = "\033[0;49;36m";
				break;
		}
		$msg = $style."{".$module."} ".$msg.self::ANSI_END;
		try {
			sfContext::getInstance()->getLogger()->log($msg, $severity);
		} catch (Exception $e) {
			// Replace this with something else in production environments!
			echo $msg."\n";
		}
	}
 
	/**
	 * This method is a shortcut for self::log(print_r($someArray, true), $module, $severity);
	 * @param Array $array Array to be logged
	 * @param string $module The module that is producing this message. Default: _DEBUG_
	 * @param string $severity The severity of the message. Must be one of the class constants
	 */
	public static function printR($array, $module = '_DEBUG_', $severity = self::DEBUG) {
		self::log(print_r($array, true), $module, $severity);
	}
}
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
3Jun/092

Oracle y PHP5 (y van tres)

Recientemente me ha tocado reinstalar un servidor donde tenía que montar una aplicación que usa el cambalache que monté anteriormente con el módulo pdo_oci.

He podido descubrir que esta no es la manera más sencillo ni correcta de hacerlo. Resulta que desde Oracle nos dicen que nos olvidemos de pdo_oci y que usemos oci8. Recomiento la lectura de: Underground PHP and Oracle Manual

Pues bien, a ello me he lanzado y ha sido sorprendente la facilidad con la que se puede instalar el soporte de oracle si lo haces con el módulo oci8. Los pasos a seguir son los siguientes:

  1. Descargar las librerías "Basic" y "SDK" del Instant Client de Oracle. Esta vez podremos usar el último que hayan sacado. Yo lo he hecho con la versión 11.1 y funciona de maravilla. Podéis descargarlo todo aquí: http://www.oracle.com/technology/software/tech/oci/instantclient/index.html
  2. Crea el directorio /opt/oracle/instantclient, mueve los zips allí y descomprímelos. Creará un directorio /opt/oracle/instantclient/instantclient_11_1
  3. Instala, si no lo has hecho ya, los paquetes php-pear, php5-dev y build-essential
  4. Ejecuta "pecl install oci8" con privilegios de root (sudo o lo que más te apetezca)
  5. Te preguntará por la ruta a las librerías. Debes configurar la ruta para que apunte al directorio recién creado y pasarle algún parámetro más, por lo que deberías escribir "shared,instanclient,/opt/oracle/instantclient/instantclient_11_1" (sin las comillas, claro)
  6. Después de hacer sus cosillas, el instalador te habrá dejado en /usr/lib/php5/20060613+lfs un fichero llamado oci8.so. Ahora tienes que decirle a PHP5 que lo tenga en cuenta escribiendo al final de los ficheros "php.ini" que tengas la línea "extension=oci8.so". Los ficheros "php.ini" están en /etc/php5/cli y /etc/php5/apache

Solo tienes que tener una cosa más en cuenta: Asegúrate de que el usuario www-data puede acceder a la ruta /opt/oracle... porque si no, verás un mensaje de error como este:

PHP Warning: PHP Startup: Unable to load dynamic library '/usr/lib/php5/20060613+lfs/oci8.so' - libaio.so.1: cannot open shared object file: No such file or directory in Unknown on line 0

Además, si vas a ejecutar scripts con tu usuario de sistema, tu también tendrás que tener acceso al directorio.

Nada más :)

En el próximo post pondré como configurar Symfony para que utilice las funciones de oci8 en vez de las de pdo_oci.

Comparte este post:
  • Print
  • del.icio.us
  • Facebook
Etiquetado con: , , , 2 Comentarios
11Mar/090

Generar PDF dinámicos

En la empresa en la que trabajo tenemos un catálogo de más de 1000 productos vendibles que tienen su descripción, fotos, especificaciones, etc. Toda esta información está almacenada en bases de datos, hojas de cálculo y demás soportes y hasta ahora el proceso de generación de las fichas comerciales de los productos era realizada a mano por una persona del departamento de diseño.

Pues bien, ya que la información está disponible desde varias fuentes a las que se puede acceder por medio de un script PHP, me han encargado la tarea de automatizar el proceso.

Librerías de generación de PDFs en PHP

En la búsqueda de librerías de generación de PDFs dinámicos he encontrado las siguientes opciones:

PDFLib

Se trata de una librería de pago con bindeos para muchos lenguajes de programación que permite componer los PDFs objeto a objeto. Está perfectamente integrada en PHP y ofrece dos versiones, dependiendo de si trabajamos con PHP4 o PHP5 orientado a objetos.

También tiene una versión PDFLib Lite que es gratuita y tiene casi todo lo necesario.

FPDF

Esta es una versión libre, más limitada y lenta que la anterior. En este caso no podremos utilizar las funciones incorporadas en PHP sino que usaremos métodos de una clase desarrollada "a pelo" para generar nuestros PDFs. Sin embargo, la filosofía detrás de esta librería es la misma que en PDFLib.

R&OS CPDF

Igual que la anterior, pero parece aun más limitada.

DOMPDF

Esta librería es un wrapper que utilizará como backend de generación PDFLib, R&OS CPDF o GD para contruir los PDFs. Es un misterio para mi cómo hacen para generar PDFs con GD, pero sospecho que generan una "foto" de cada página y se lo enchufan a un fichero con formato PDF de alguna manera.

Conclusión

Si no me quedara más remedio que construir a mano los PDFs en PHP seguramente usaría DOMPDF o PDFLib Lite. Sin embargo, creo que la mejor opción es la que voy a describir a continuación:

SVG: La alternativa que gusta a pequeños y mayores

SVG: Scalable Vector Graphics. Se trata de un lenguage de marcado creado por la W3C para contener dibujos vectoriales. Se supone que será uno de los standards para ficheros de dibujo vectorial más usados en los próximos años.

La estructura de un SVG es la de un XML. Cada etiqueta define propiedades del documento y objetos como bloques de texto, párrafos, imágenes, formas, etc.

Este formato gustará a todos y todas porque las personas responsables del diseño podrán utilizar magnificas aplicaciones de diseño vectorial como Inkscape en las que podrán dar rienda suelta a su creatividad y nosotros, los encargados de rellenar de contenido sus diseños, podremos utilizar sencillas funciones de reemplazo de texto para tratar el contenido XML del fichero SVG y "producir" un documento final. El programador más habilidoso podrá incluso utilizar sus funciones preferidas de modificación del árbol DOM de XML para trabajar con el documento e incluso crear objetos on-the-fly.

Una vez creado un SVG final, podemos usar una infinidad de aplicaciones para convertir el SVG en PDF (o en el formato que nos dé literalmente la gana). El propio Inkscape permite ser ejecutado en línea de comandos y de la siguiente manera conseguiremos un radiante PDF:

$ inkscape -A documento.pdf documento.svg

Para más info hacer un 'man inkscape'. El programa permite definir parámetros de exportación de las imágenes del documento para definir sus DPIs, por ejemplo.

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