Sign In or Create Account

Huge image

Zend Framework 2 and Doctrine 2

Posted on Dec 04, 2011

Still trying to figure out the best way but that's what I have so far. Decided to keep it separate for now in it's own module Doctrine. The only file in the module is Module.php. Of course don't forget to register it in appConfig.

Here's the code:

 

namespace Doctrine2;

use Zend\EventManager\StaticEventManager,
    Zend\Module\Consumer\AutoloaderProvider,
    Zend\Registry;

class Module implements AutoloaderProvider
{
    public function init()
    {
        $events = StaticEventManager::getInstance();
		$events->attach('bootstrap', 'bootstrap', array($this, 'initializeDoctrine'), 100);
    }

    public function getAutoloaderConfig()
    {
        return array(
            'Zend\Loader\StandardAutoloader' => array(
            	'namespaces' => array(
	            	'Doctrine\Common' => VENDOR_PATH . '/doctrine2-orm/lib/vendor/doctrine-common/lib/Doctrine/Common',
	            	'Doctrine\DBAL'	  => VENDOR_PATH . '/doctrine2-orm/lib/vendor/doctrine-dbal/lib/Doctrine/DBAL',
	            	'Doctrine' 		  => VENDOR_PATH . '/doctrine2-orm/lib/Doctrine',
	            )
	        )
        );
    }

    public function initializeDoctrine()
    {
		$config = new \Doctrine\ORM\Configuration();

		$modelsPath = APPLICATION_PATH . '/modules/Application/src/Application/Model';

		// Proxy Configuration
		$config->setProxyDir($modelsPath . '/Proxies');
		$config->setProxyNamespace('Application\Models');
		$config->setAutoGenerateProxyClasses((APPLICATION_ENV == "development"));

		// Mapping Configuration
		$driverImpl = $config->newDefaultAnnotationDriver($modelsPath);
		$config->setMetadataDriverImpl($driverImpl);

		// database configuration parameters
		$conn = array(
		    'driver'	  => 'pdo_mysql',
		    'dbname'	  => 'db-dev',
		    'charset'	  => 'UTF-8'
		);

		// obtaining the entity manager
		$evm = new \Doctrine\Common\EventManager();
		$entityManager = \Doctrine\ORM\EntityManager::create($conn, $config, $evm);		
	
		Registry::set('doctrine-em', $entityManager);
    }
}






If there's a *try* why there's no *retry*

Posted on Aug 28, 2010

Just wanted to share something I'm using on a daily basis...

We all know that external APIs sometimes can be slow and unreliable. It's quite annoying to guard any call to an external API with try...catch block so here's my PHP 5.3 only "solution" to this problem... Obviously if external service is down it's not going to magically fix it but it's going to retry a few times before returning false.

Here's an example:

$response = null;
$success = retry(3, function() use ($api, &$response) {
    $response = $api->getSomething();
});

I think it's pretty obvious what this function does. Usually your external service handling class will throw an exception in case HTTP connection has timed out or reponse it's received is not valid. retry will catch that exception (or it can only catch exceptions of specified types) and will retry again after a certain delay (you can specify how many times you want it to retry before givinig up).

Here's the retry function itself:

/**
* Calls the callback function and in the case if exception is thrown retries
* a given number of times. Returns true on success.
*
* @param numeric $retries
* @param callback $callback
* @param array $exceptions
* @return bool
*/
function retry($retries, $callback, array $exceptions = null) {
    if (!is_callable($callback)) {
        throw new Bi_Exception('Argument 2 must be a valid callback');
    }
    $sleep = 1;
    while ($retries--) {
        try {
            $callback();
            return true;
        }
        catch (Exception $e) {
            sleep($sleep);
            $sleep *= 2;
        }
    }
    if (is_array($exceptions)) {
        $should_throw = true;
        foreach ($exceptions as $exception_type) {
            if (is_a($e, $exception_type) || is_subclass_of($e, $exception_type)) {
                $should_throw = false;
                break;
            }
        }
        if ($should_throw) {
            throw $e;
        }
    }
    return false;
}






Handling Errors in PHP 5

Posted on Feb 08, 2010

This class could be helful in case you have no error/exception handling code and believe you have absolute test coverage for every single component of your site ;) Just add Ns_ErrorHandler::setHandlers() to your bootrap file and who knows, maybe there's actually something you've missed.

class Ns_ErrorHandler
{
    /**
     * @var array
     */
    private static $_ignoredErrors = array(8, 2048, 8192, 16384); // E_NOTICE, E_STRICT, E_DEPRECATED, E_USER_DEPRECATED
    
    /**
     * @var array
     */
    private static $_fatalErrors = array(1, 256); // E_ERROR, E_USER_ERROR

    /**
     * Sets error handlers
     */
    public static function setHandlers()
    {
        set_error_handler(array(__CLASS__, 'handleError'));
        set_exception_handler(array(__CLASS__, 'handleException'));
        register_shutdown_function(array(__CLASS__, 'shutdown'));        
    }
    
    /**
     * Handles PHP errors
     *
     * @param int    $errno
     * @param string $error
     * @param string $errfile
     * @param string $errline
     */
    public static function handleError($errno, $errstr, $errfile, $errline)
    {
        // Ignore error?
        if (in_array($errno, self::$_ignoredErrors)) {
            return;
        }

        // Send email
        $errorString = "{$errno}: {$errstr} in {$errfile} on line {$errline}";
		mail('notify@this.address', 'Error', $errorString);
		
		// Display eror page
		if (in_array($errno, self::$_fatalErrors)) {
			@ob_end_clean();
			self::_showErrorPage();
		}
    }
    
    /**
     * Handles Exception
     *
     * @param Exception $e
     */
    public static function handleException(Exception $e)
    {
		// Send email
		mail('notify@this.address', 'Error', print_r($e, true));
		
		// Display erro page
		@ob_end_clean();
		self::_showErrorPage();
    }
    
    /**
     * Using shutdown function we can catch fatal errors
     * which don't go through error_handler
     */
    public static function shutdown()
    {
        if ($err = error_get_last()) {
            call_user_func_array(array(__CLASS__, 'handleError'), array_values($values));
        }
    }
	
	private static function _showErrorPage()
	{
		// show some html here
	}
}

// Set handlers
Ns_ErrorHandler::setHandlers();

Questions?







PHP + PCNTL

Posted on Jan 28, 2010

На днях на работе мне было поручено написать очень простенький crawler который будет находить ссылки в разных источниках (к примеру Twitter) и будет эти ссылки нам сохранять. Само собой всякие укоротители ссылок вроде bit.ly нас не интересуют, нам нужны конечные ссылки и желательно titles страниц на которые ссылки указывают.

Сначала казалось что никаких проблем возникнуть не должно. У меня был тестовый list на твиттере где появлялось в среднем до 10 ссылок в минуту. Логично было написать скрипт, запускаемый cron-ом каждые 10-15 минут, который будует получать новые tweets в этом листе и будет их сканировать на наличие ссылок. Сказано - сделано, скрипт написан, cron подправлен.

class Crawler
{
	public function crawl(array $lists)
	{
		foreach ($lists as $list) {
			$tweets = $twitterClient->getLatestTweets( $list );
			
			// Collect urls first
			$urls = array();
			foreach ($tweets as $tweet) {
				$urls += $this->_extractUrls($tweet['text']);
			}
			
			// Now analyze them
			foreach ($urls as $url) {
				list($final_url, $title) = $this->_analyzeUrl($url);
			}
		}
	}
	
	private function _analyzeUrl($url)
	{
		list($url, $body) = $http_client->_analyze($url);
		$title = $this->_extractPageTitle($body);
		return array($url, $title);
	}
}

$crawler = new Crawler;
$crawler->crawl($list);

Однако когда я получил настоящий список twitter аккаунтов (а их оказалось 50) подобный метод оказался слишком медленным. В среднем анализ одной ссылки занимал 1-1.5 секунды. Итого 50 аккунтов * в среднем 30 ссылок в каждом (если запускать скрипт каждые 10 минут) - получаем 1500 ссылок которые надо проверить. Итого ~30-35 минут. Абсолютно неприемлемо.

Используя pcntl extension мы получаем возможность fork наш процесс столько раз сколько мы хотим параллельно работающих процессов. Однако есть одна проблема, между старшим и дочерним процессом нет никакой связи. Первая версия этого класса использовала временные файлы.  Поскольку старший процесс знает PID дочерних процессов мы можем периодически проверять появился ли файл допустим с названием /tmp/crawler_result.[PID]. Как только файл появился и его содержимое валидно (я использовал набор символов в конце файла) мы знаем что дочерний процесс закончил работу и мы можем его освободить и запустить следующий.

Однако следующий вариант мне нравится еще больше поскольку операции чтения-записи файлов будут помедленнее чем прямой доступ к памяти. Итак здесь мы спользуем еще два extension - sysvshm и sysvsem. Немного теории. Нам нужен кусок памяти доступный из старшего и дочерних процессов. Однако чтобы избежать повреждения информации нам нужно сделать так чтобы только один процесс мог записывать в определённое время. Для этого мы будем использовать семафор. Смотрим код и постигаем! :)

class Crawler
{
	private $_runningThreads = array();
	
	private $_concurrentThreadsNum = 10;

	public function __construct()
	{
        // Setup semaphore and shared memory
        $token = ftok(__FILE__, 'c');
        $this->_semaphore = sem_get($token);
        if (! $this->_shm = @shm_attach($token, 1 * 1024 * 1024, 0644)) {
			echo 'Unable to allocate memory (1 megabyte)';
            exit;
        }
	}
	
	public function __destruct()
	{
		if ($this->_parentProcess) {
            shm_remove($this->_shm);
            shm_detach($this->_shm);
		}
	}

	public function crawl(array $lists)
	{
		foreach ($lists as $list) {
			$tweets = $twitterClient->getLatestTweets( $list );
			
			// Collect urls first
			$urls = array();
			foreach ($tweets as $tweet) {
				$urls += $this->_extractUrls($tweet['text']);
			}

			// Analyze forked
			$this->_analyzeForked($urls);
		}
	}
	
	private function _analyzeForked(array $urls)
	{
		$this->_urls = $urls;
		$this->_parentProcess = false;
		
		$current_index = 0;
		$urls_count = count($urls);
	
		while (true) {
			// Harvest zombies
            foreach ($this->_runningThreads as $i => $threadId) {
                if (pcntl_waitpid($threadId, $status, WNOHANG) > 0) {
                    unset($this->_runningThreads[$i]);
                }
            }
			
			// Run some more threads
            if ($current_index < $urls_count) {
                // Run more threads if there are empty slots
                if (count($this->_runningThreads) < $this->_concurrentThreads) {
                    // Spawn thread
                    if ($pid = $this->_fork($current_index)) {
                        $this->_runningThreads[] = $pid;
                    }
                    // Fallback in case spawning thread has failed
                    else {
                        $analysed_data = $this->_analyzeUrl($urls[$current_index]);
                        $this->_updateData($current_index, $analysed_data);
                    }
 
                    $current_index++;
                }
            }		
		
			// Are all threads done?
            if (!$this->_runningThreads) {
                $this->_parentProcess = true;
                break;
            }
		}

        // Final data
        return $this->_getData();
	}
	
	/**
	 * Fork this process
	 *
	 * @param int $index
	 */
	private function _fork($index)
    {
        $thread_id = pcntl_fork();
        
        // Couldn't spawn child process
        if ($thread_id == -1) {
            return false;
        }
        // Child spawned, save the process id
        elseif ($thread_id) {
            return $thread_id;
        }
        
        // Update data
        $analysed_data = $this->_analyzeUrl($this->_urls[$index]);
        $this->_updateData($index, $analysed_data);

        exit;
	}
	
	/**
	 * Analyze url
	 *
	 * @param string $url
	 * @return array
	 */
	private function _analyzeUrl($url)
	{
		list($url, $body) = $http_client->_analyze($url);
		$title = $this->_extractPageTitle($body);
		return array($url, $title);
	}
	
	/**
	 * Safely updated data in the shared memory block
	 *
	 * @param int $index
	 * @param mixed $_data
	 */
    private function _updateData($index, $_data)
    {
       sem_acquire($this->_semaphore);
       $data = shm_has_var($this->_shm, $this->_shmId) ?
           shm_get_var($this->_shm, $this->_shmId) :
           array();
       $data[$index] = $_data;
       shm_put_var($this->_shm, $this->_shmId, $data);
       sem_release($this->_semaphore);
    }
    
	/**
	 * Return data currently stored in shared memory block
	 *
	 * @return array
	 */
    private function _getData()
    {
        if (shm_has_var($this->_shm, $this->_shmId)) {
            sem_acquire($this->_semaphore);
            $data = shm_get_var($this->_shm, $this->_shmId);
            sem_release($this->_semaphore);
            return $data;
        }
        return array();
    }
}

$crawler = new Crawler;
$crawler->crawl($list);

Не стесняемся, задаём вопросы.

Материалы по теме:

http://pleac.sourceforge.net/pleac_php/processmanagementetc.html







ZF Routes + Smarty

Posted on Nov 03, 2009

Че-то надоело мне мучаться с ссылками в Smarty и я, как говорится, пошел другим путём :) Напишем маленький скриптик который будет для каждого зарегистрированного пути создавать функцию для смарти, которую мы потом сможем вызывать для генерирования полноценного URL:

$routesFunctions = '/some_path_where_you_want_this/smarty.url_plugins.php';
$fileContents = '<?php ' . PHP_EOL;

foreach ($router->getRoutes() as $routeName => $routeParams) {
    $methodName = "{$routeName}_url";    
    $fileContents .= <<<HEREDOC
function smarty_function_{$methodName}(\$params, \$smarty) {
    \$params = array_merge(\$params, array('name' => '{$routeName}'));
    return smarty_function_url_for(\$params, \$smarty);
}
HEREDOC;
}

file_put_contents( $routesFunctions, $fileContents );

И теперь при условии что у нас вот такой вот простенький список routes...

$routes = array(
    'login' => array(
        'route' => '/login',
        'defaults' => array(
            'controller' => 'auth',
            'action' => 'login'
        )
    ),
    'show_blog_post' => array(
        'route' => '/blog/posts/:id/show',
        'defaults' => array(
            'module' => 'blog',
            'controller' => 'posts',
            'action' => 'show'
        )
    )
);

... в шаблоне мы можем сделать следующее:

<a href="{login_url}">Login</a>

либо

<a href={show_blog_post id=$post->id}">Show this blog post</a>

Само собой, при каждом изменении файла с путями прийдётся заново генерировать файлик с функциями, но думается мне, что это не большая проблема. Кстати вот пример того как может выглядеть функция url_for, которая вызывается внутри сгенерированых функций.

function smarty_function_url_for($params, $smarty)
{
	// Надо где-то взять  urlHelper :)
	global $urlHelper;

	if (!$routeName = $params['name']) {
		throw new Exception('Route name must be given');
	}
	unset($params['name']);
	
	if (array_key_exists('params', $params)) {
		$params = $params['params'];
		if (!is_array($params)) {
			$params = array();
		}
	}

	$routeDetails = Zend_Controller_Front::getInstance()->getRouter()->getRoute( $routeName );
	$routeVars = $routeDetails->getVariables();

	// Required vars to generate URL
	$urlVars = array();
	foreach ($params as $name => $val) {
		if (in_array($name, $routeVars)) {
			$urlVars[ $name ] = $val;
			unset( $params[ $name ] );
		}
	}
	$url = $urlHelper->url($urlVars, $routeName);
	if (!count($params)) {
		return $url;
	}

	// Query vars
	$queryVars = array();
	foreach ($params as $key => $var) {
		$queryVars[] = $key. '=' . urlencode($var);
	}
	$url .= '?' . implode('&', $queryVars);
	
	return $url;
}

И да, главное не забыть где-нибудь в bootstrap.php:

	require '/some_path_where_you_want_this/smarty.url_plugins.php';






Categories


Tags

Oops. No tags.



tr