Artículos etiquetados con ‘symfony’

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.

Retweet
Publicado el : 18 febrero 2010
Categorías: Desarrollo
Etiquetas:
Comentarios: 5 comentarios

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

Retweet
Publicado el : 28 enero 2010
Categorías: Desarrollo
Etiquetas: , ,
Comentarios: Ningún comentario

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 [email protected]
 */
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);
        }
}
Retweet
Publicado el : 29 diciembre 2009
Categorías: General
Etiquetas: , , ,
Comentarios: Ningún comentario

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 [email protected]
 */
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 [email protected]
 */
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;
        }
 
}
Retweet
Publicado el : 28 diciembre 2009
Categorías: Desarrollo
Etiquetas: , ,
Comentarios: Ningún comentario

Problemas (casi)resueltos de Propel (Symfony) y Oracle

Acaban de dejarme un comentario al post: Propel 1.3 ya puede inspeccionar Oracle digno de ser respondido como dios manda. Os pongo el extracto de una serie de preguntas que plantea mppfiles:

1) Los nombres de tablas deben estar sí o sí en mayúsculas
2) Los nombres de los campos DEBEN definirse con minúsculas y/o sin tildar el “preserve case” en ApEx (o sin encerrar los nombres de los campos entre comillas)
3) En varias ocasiones me han salido errores extraños del tipo: “duplicate table found: propel” y otros mas extraños aun, tuve que borrar los generated-schema*.yml y volver a construir el esquema y el modelo para que camine.
4) Al generar los forms, los nombres de los widgets se ponen en minusculas, lo que genera problemas al guardarlos despues (porque definimos previamente que sean mayusculas)
5) Ahora estoy peleando con los campos de tipo fecha. Al parecer el Oracle que tengo instalado (XE bajo Windows) maneja las fechas en formato “d/m/Y” o dd/MM/YYYY (o equivalentes), en definitiva: fechas en español) y me salen errores del tipo “Unable to execute INSERT statement. [wrapped: SQLSTATE[HY000]: General error: 1861 OCIStmtExecute: ORA-01861: el literal no coincide con la cadena de formato”, pensaba que tenía que ver con el formato ISO de los validators (Y-m-d, o YYYY/MM/DD o como sea), y aunque cambié el valor de
$this->validatorSchema['FECHA_NAC']->setOption(’date_output’,’d/m/Y’)
al hacer $form->getValues() lo muestra con ese formato, pero Propel insiste en guardar el valor en la base con el formato ISO, lo cual me parece mas que bien.

Paso a contestar la mayoría de puntos ya que creo que los he resuelto en mayor o menor medida:

1. Nombres de tablas

En Oracle los nombres de tablas se normalizan siempre a un formato en mayúsculas. Me parece una opción igual de válida como cualquier otra, aunque a la hora de programar no suele ser recomendable reproducir esto ya que normalmente se reservan las palabras en mayúsculas para nombres de constantes o, en el caso de PHP, superarrays como _REQUEST, _SERVER, etc.

La manera tradicional de nombrar tablas en bases de datos como MySQL, PostreSQL y demás suele ser la de palabras en minúsculas separadas por guión bajo. Una tabla llamada “user_personal_data” debería mapearse a una clase del estilo UserPersonalData en Propel. El problema es que Propel utiliza un método de la clase PhpNameGenerator llamado phpnameMethod que no convierte el nombre a minúsuclas antes de capitalizar la primera letra de cada parte del nombre.

En la versión de Propel en la que pude contribuir recientemente he corregido este comportamiento y he creado un nuevo método de nombrado que abarca más a la hora de normalizar nombres extraños de tablas o columnas. De este modo, no solo obtendrás un esquema de base de datos con un formato tipo UserPersonalData, sino que además podrás tolerar nombres de tabla con carácteres extraños.

Sin embargo, me preocupa el tema de las tildes que no había considerado aun. Creo que tendré que hacer una modificación para que los carácteres “á é í ó ú” se traduzcan en “a e i o u” y no desaparezcan sin más.

Si has descargado Symfony a través de su repositorio de Subversion, debes realizar un “svn up” en la carpeta “symfony/lib/plugins/sfPropelPlugin/lib/vendor/propel-generator” ya que de lo contrario no tendrás la última versión del generador ya que Symfony “bloquea” las librerías de Propel en una versión anterior a mi contribución.

2. Nombres de campos

Esto está resuelto del mismo modo que con el nombre de las tablas del que hablo en el punto anterior.

Yo trabajo contra una base de datos de un ERP Baan IVc4 que utiliza como nombre de columnas cosas como “T$ORNO” o “S$NAMA$RE“. Con una versión de Propel actualizada no tendrás ningún problema en generar el schema.yml en semejante escenario. Estos nombres se traducirían como “TOrno” y “SNamaRe“.

Sin embargo, el modelo generado a partir del schema.yml anterior no funcionará a no ser que lo edites manualmente ya que en las clases generadas creará incorrectamente el código fuente de las constantes de clase y algún que otro método. En los Peer generará, por ejemplo:

const T$ORNO = "T$ORNO";

Y esto falla.

Yo lo he resuelto realizando modificaciones a otras clases de Propel. Estas modificaciones no las he subido al repositorio de Propel porque creo que exigen un pequeño debate y aprobación por parte del grupo de desarrolladores de Propel ya que son bastante intrusivas y pueden afectar a modelos generados a partir de otros motores de bases de datos.

Básicamente, lo que he hecho es decirle al generador de modelos que emplee para todo el “phpName” y no el “name” de las columnas. Esto soluciona el problema por completo. Puedo enviarle un diff de los cambios que he hecho en estas clases a quien me lo pida para que pueda aplicar los cambios en su instalación de Propel.

3. Duplicate table found: propel

Esto es un error frecuente en Symfony cuando trabajas con varias bases de datos al mismo tiempo. En el fichero databases.yml tendrás una configuración parecida a esta:

all:
db1:
configuración de db1
db2:
configuración de db2
...

(Disculpad la maldita indentación de Worpdress)

El problema es que Symfony no respeta el nombre de la base de datos a la hora de generar el fichero schema.yml y utiliza el nombre “propel” para los esquemas siempre. Lo único que hay que hacer es asegurarse de que la primera posición del fichero schema.yml coincide con db1, db2, etc. antes de generar el modelo y luego todo funcionará de maravilla.

Si os devuelve este error a pesar de haber hecho lo anterior, lo más seguro es que tengáis un problema de cache. En este caso, recomiendo borrar el fichero schema.yml y el directorio lib/model (haz copia de seguridad de esto antes de borrarlo por si las moscas) y empezar de nuevo.

4.Forms

Creo que esto se resuelve con las medidas que he tomado con los nombres de tablas y columnas. En cualquier caso, a mi no me ocurre en mi escenario.

5. Fechas

Lamentablemente, todavía no me he peleado suficiente con esto como para darte una resupesta. Seguramente tú sepas más de esto que yo en este momento. Sólo te puedo recomendar optar siempre que se pueda por Timestamps en la base de datos.

En cualquier caso, hice una corrección en este sentido en el motor de ingeniería inversa que ganantiza que Propel no la cagará (como nos tiene acostumbrados) con los valores por defecto en los campos fecha.

Retweet
Publicado el : 5 marzo 2009
Categorías: General
Etiquetas: , ,
Comentarios: 4 comentarios

Propel 1.3 ya puede inspeccionar Oracle

Gracias a unos cambios en Propel 1.3 en los que he estado currando por necesidad, he podido contribuir al magnífico proyecto Propel y me han hecho comitter en su SVN para temas de Oracle, a pesar de que soy un novato en el área.

A partir de ahora, si necesitáis trabajar contra Oracle con Propel, podréis realizar tareas de ingeniería inversa para generar el modelo a partir de una base de datos viva con los cambios del changeset 1107 de la versión 1.3 de Propel (http://propel.phpdb.org/trac/changeset/1107).

Huelga decir que supone un gran honor para mi el poder realizar esta aportación y que estoy muy contento por ello.

Si encontráis cualquier problema avisad!

Retweet
Publicado el : 25 febrero 2009
Categorías: General
Etiquetas: , , , ,
Comentarios: 3 comentarios

Symfony 1.2 + Propel 1.3 + Oracle (Parte 2)

Ahora que ya tenemos el módulo PDO_OCI funcionando, podemos intentar hacer una prueba en Symfony 1.2 con Propel 1.3. Después de dar muchas vueltas, he llegado a la conclusión de que el formato para databases.yml es el siguiente:

all:

propel:

class: sfPropelDatabase

param:

classname: PropelPDO

phptype: oracle

dsn: oci:dbname=//%IP_DEL_SERVIDOR%:%PUERTO_DEL_SERVIDOR%/%BASE_DE_DATOS%

username: %USUARIO%

password: %CONTRASEÑA%

encoding: utf8

persistent: true

pooling: true

(No consigo que el maldito WordPress indente correctamente el código anterior así que recuerda que la jerarquía es: all >> propel >> param >> resto)

El formato de propel.ini sería el siguiente:

propel.database            = oracle

propel.database.driver     = oracle

propel.database.url        = oci:dbname=//%IP_DEL_SERVIDOR%:%PUERTO_DEL_SERVIDOR%/%BASE_DE_DATOS%

propel.database.creole.url = ${propel.database.url}

propel.database.user       = %USUARIO%

propel.database.password   = %CONTRASEÑA%

propel.database.encoding   = utf8

Lo último que necesitas para que la ingeniería inversa funcione es un parseador de estructura de base de datos para bases de datos de Oracle. Propel 1.3 no trae uno, así que puedes usar el que un servidor ha preparado convenientemente: Oracle Schema Parser (PHP5 script) (Bórrale la extensión “doc” y déjalo en un directorio “lib/plugins/sfPropelPlugin/lib/vendor/propel-generator/classes/propel/engine/database/reverse/oracle” nuevo que tienes que crear en la instalación de Symfony).

Con esto y un bizcocho, ya deberías ser capaz de ejecutar la tarea “propel:build-schema” :)

Retweet
Publicado el : 17 febrero 2009
Categorías: General
Etiquetas: , , , , , ,
Comentarios: 3 comentarios

Symfony 1.2 + Propel 1.3 + Oracle

Después de intentar poner el marcha la extensión de PHP5 de PDO_OCI durante dos días, hoy al fin lo he conseguido.

No hay apenas documentación y para más inri, la poca que hay mezcla versiones incompatibles entre si. Ha sido una auténtica aventura ponerlo en marcha.

La configuración del servidor es la siguiente:

  • Ubuntu Server 8.10 x86 sobre VMWare
  • PHP5 (la versión del repositorio de Ubuntu)

Parte 1: Instalar el driver OCI (oracle) de PDO

Antes de nada debes saber que vas a tener que instalar la extensión PDO y PDO_OCI a mano. Por un lado, la de PDO_OCI no tiene candidatos y si la compilas a mano, resulta que es incompatible con el API de PDO que te instala el repositorio de ubuntu, por lo que al final, la mejor solución es compilar a mano ambas, a partir del repositorio de fuentes de PEAR.

Para instalar PDO solo hay que hacer un “sudo pecl install pdo”. Asegúrate de tener instalado el paquete php-pear y el php5-dev porque si no no podrás hacer esto.

Despues de hacerlo, habrás reemplazado la extensión PDO que te viene al hacer un “apt-get install php5″ por el de PEAR.

Ahora viene la parte divertida: instalar en el sistema la extensión PDO_OCI. Para ello debes instalar dos cositas de nuestro gran amigo Oracle (http://www.oracle.com/technology/software/tech/oci/instantclient/index.html):

  • Oracle Instant Client (library)
  • Oracle Instant Client (SDK)

Lamentablemente, solo podrás utilizar las versiones inferiores a la 11 (de la 10.2.0.4 para abajo).

Luego descomprime la librería en /usr/lib/oracle/10.2.0.4/client/lib y el sdk en /usr/include/oracle/10.2.0.4/client

Luego dile al sistema que añada estas rutas con el comando ldconfig y corriges un par de nombres de ficheros:

sudo ldconfig /usr/lib/oracle/10.2.0.4/client/lib
sudo ldconfig /usr/include/oracle/10.2.0.4/client
sudo ln -s /usr/lib/oracle/10.2.0.4/client/lib/libclntsh.so.10.1 /usr/lib/oracle/10.2.0.4/client/lib/libclntsh.so
sudo ln -s /usr/lib/oracle/10.2.0.4/client/lib/libocci.so.10.1 /usr/lib/oracle/10.2.0.4/client/lib/libocci.so

Ahora vamos a descargar las fuentes de PDO_OCI, compilarlas e instalarlas en el sistema:

sudo mkdir /usr/src/pdo_oci
cd /usr/src/pdo_oci
sudo pecl download pdo_oci
sudo tar -zxvf PDO_OCI-1.0.tgz
cd PDO_OCI-1.0
sudo phpize5
sudo ./configure –prefix=/usr –with-pdo-oci=/usr/lib/oracle/10.2.0.4/client
* editar Makefile *
sudo make
sudo make install

Al editar Makefile debéis añadir la ruta a /usr/include/oracle/10.2.0.4/client/include en la directiva “INCLUDES”

Después de esto ya tendréis la extensión instalada en vuestro sistema. Sólo hay que editar /etc/php5/conf.d/pdo.ini y añadir:

extension=pdo_oci.so

Ale, a disfrutar! En el siguiente post explicaré como hacer que todo esto funcione con Propel 1.3 y Symfony, pero antes tengo que averiguar cómo coño se hace T_T.

Retweet
Publicado el : 16 febrero 2009
Categorías: General
Etiquetas: , , , , ,
Comentarios: 4 comentarios