Artículos etiquetados con ‘wow’

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