Fully ajax website with Yii – Part 1

This article will be suitable for who made fully ajax site like  Gmail with best PHP Framework – Yii.
Main requirements are:

  1. All pages should be loaded via ajax.
  2. All forms should be submitted via ajax.
  3. Pages should be reachable by search engines.
  4. History support.
  5. Window title support
  6. Minimal additional code.

Here  is my solution.
I get for example simple web application which you can made with help of yiic utility.

cd /home/www/mywebsite
wget http://yii.googlecode.com/files/yii-1.1.7.r3135.tar.gz
gunzip yii-1.1.7.r3135.tar.gz
tar -xvf yii-1.1.7.r3135.tar
mv yii-1.1.7.r3135/framework/ ./
php framework/yiic.php webapp ./

That is it your application has been created!

Add this code to the protected/views/layout/main.php file before  </head> tag.


getClientScript();
  		$cs->registerCoreScript('jquery');
  		$cs->registerCoreScript('bbq');
?>
  	
  	registerScript('applychanges', $script, CClientScript::POS_HEAD);
?>	

  	registerScript('loading-indicator', $script, CClientScript::POS_READY);
?>



registerScript('ajaxlinks-and-forms', $script, CClientScript::POS_READY);
?>

Next code should be added after <body> tag

We’ve jus added javascript code which will handle all client side job
Then we extend Base controller class “Controller”.

protected/componens/Controller.php; Add new methods below:

public function ajaxRender($file, $data=array()) {
		
		$data['title'] = CHtml::encode($this->pageTitle);			
		header('Content-type: text/x-json');
		echo CJSON::encode($data);
		Yii::app()->end();

	}
		
	public function render($file, $params = array(), $data=array()) {
		
		if(Yii::app()->request->isAjaxRequest){

			if(Yii::app()->user->hasFlash('updatedata')) {
				$flashdata = Yii::app()->user->getFlash('updatedata');		
				$data = $data + $flashdata;	
			}
			
			
			$data['#content'] = parent::renderPartial($file, $params, true);
			$data['title'] = CHtml::encode($this->pageTitle);
			
			header('Content-type: text/x-json');
			echo CJSON::encode($data);
			Yii::app()->end();
			
		} else {			
			echo parent::render($file, $params, true);	
		}	
	
	}
	
	public function redirect($url) {
		
		if(is_array($url) && isset($url[0]))
			$url = $url[0];
	
		if(Yii::app()->request->isAjaxRequest){
			
			$data = array();
			
			if(Yii::app()->user->hasFlash('updatedata')) {
				$flashdata = Yii::app()->user->getFlash('updatedata');		
				$data = $data + $flashdata;	
			}
			
			$data['#content']= '';
			
			
			
			header('Content-type: text/x-json');
			echo CJSON::encode($data);
			Yii::app()->end();
				
		} else {
			parent::redirect($url);
		}
	}
	
	public function refresh() {
		if(Yii::app()->request->isAjaxRequest){

			$data = array();
			
			if(Yii::app()->user->hasFlash('updatedata')) {
				$flashdata = Yii::app()->user->getFlash('updatedata');		
				$data = $data + $flashdata;	
			}
			
			
			$data['#content']= '';
			
			header('Content-type: text/x-json');
			echo CJSON::encode($data);
			Yii::app()->end();
			
		} else {
			parent::refresh();
		}
	}

Then you have to override some Request methods to support CWebUser redirects:

Put content below to new file with name protected/components/EHttpRequest.php

request->isAjaxRequest){

			$data = array();
			
			if(Yii::app()->user->hasFlash('updatedata')) {
				$flashdata = Yii::app()->user->getFlash('updatedata');		
				$data = $data + $flashdata;	
			}
			
			$data['#content']= '';
			
			header('Content-type: text/x-json');
			echo CJSON::encode($data);
			
			Yii::app()->end();	
		} else {
			parent::redirect($url);
		}
	}
	
	public function refresh() {
		if(Yii::app()->request->isAjaxRequest){

			$data = array();
			
			if(Yii::app()->user->hasFlash('updatedata')) {
				$flashdata = Yii::app()->user->getFlash('updatedata');		
				$data = $data + $flashdata;	
			}
			
			
			$data['#content']= '';
			
			header('Content-type: text/x-json');
			echo CJSON::encode($data);
			
			
			Yii::app()->end();	
		} else {
			parent::refresh();
		}
	}
}

Set new class name for request component in your main config protected/config/main.php

It should looks like this

  .... 
  'components'=>array(
		'user'=>array(
			// enable cookie-based authentication
			'allowAutoLogin'=>true,
		),
		
		'request'=>array(
                     'class'=>'EHttpRequest',
               ),

   ....

That’s all. Now you have full ajax simple web application.
Offcourse it has some limitations and issues, further I’ll extend this article.

P.S. I have forgotten to say about styles.
Just add these lines to your css file.

#loading {
	position:fixed;
	padding:3px;
	background:#80B646;
	color:#fff;
	display:none;
	z-index:999;
	right:0;
	top:0;
}
#loading.activity {
	display:block;
}

Gearman воркер для скачивания файлов по URl

Gearman, сервер организации и распределения задач, более подробно можно узнать на оффициальном сайте http://gearman.org/

Данную статью я пишу для тех кто уже знает для чего он нужен и как его устанавливать.  В одном из моих проектов gearman воркеры скачивают файлы по ссылке. Также они во время работы отсылают статусы о прогрессе. Вы можете запустить нужно количество воркеров в зависимости от доступного интернет канала и конфигурации серверов.

Собственно сам класс воркера

download_size = 0;
		$this->downloaded = 0;
		$this->username = '';
		$this->pswd = '';
		$this->referrer = '';
		
	}
	private function closefp() {
		@fclose($this->tmp_file_pointer);
	}
	
	private	function remote_filesize()
	{	
		ob_start();
		$ch = curl_init($this->url);
		curl_setopt($ch, CURLOPT_HEADER, 1);
		curl_setopt($ch, CURLOPT_NOBODY, 1);
	    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
	    curl_setopt($ch, CURLOPT_REFERER, $this->referrer);
	    
		if(!empty($this->username) && !empty($this->pswd)) {
			curl_setopt($ch, CURLOPT_USERPWD, $this->username.':'.$this->pswd);
		}
	
		$ok = curl_exec($ch);
		
		curl_close($ch);
		$head = ob_get_contents();
		ob_end_clean();
	
		$regex = '/Content-Length:\s([0-9].+?)\s/';
		$count = preg_match($regex, $head, $matches);
	
		return isset($matches[1]) ? $matches[1] : 0;
	}
	
	
	function fetchfile($job) {
		$this->resetvalues();
		
		$this->job = $job;
		$workload= $this->job->workload();
 	
		echo "Received job: " . $this->job->handle() . "\n";
		
		$args = json_decode($workload);
		
		if(!isset($args->url) || !isset($args->tmp_file)) {
			print "Url and tmp_file are required";
			return;
		}
		
		$this->url = $args->url;
		$this->pswd = (isset($args->password))? $args->password : '';
		$this->username = (isset($args->username))? $args->username : '';
		$this->referrer = (isset($args->referrer)) ? $args->referrer : $this->url;
		
		$this->download_size = $this->remote_filesize();
		
		if($this->download_size==0) {
			$this->resetvalues();
			print "Can't get size header of the remote file";
			return;
		}
		
		$this->tmp_file_name = $args->tmp_file;
		$this->tmp_file_pointer = fopen($this->tmp_file_name, "wb");
		
		if($this->tmp_file_pointer)
			$this->fetchurl();
		else {
				$this->resetvalues();
				print("Couldn't open tmp file");
				return;
			 }
	}
	
	function percent($num_amount, $num_total) {
		$count1 = $num_amount / $num_total;
		$count2 = $count1 * 100;
		$count = ceil($count2);
		return $count;
	}

	function fetchurl() {
	
		$ch = curl_init($this->url);	
		curl_setopt($ch, CURLOPT_NOPROGRESS, true);
		curl_setopt($ch, CURLOPT_WRITEFUNCTION, array($this, 'fetchcallback'));
		curl_setopt($ch, CURLOPT_REFERER, $this->referrer);
		
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
		
		if(!empty($this->username) && !empty($this->pswd)) {
			curl_setopt($ch, CURLOPT_USERPWD, $this->username.':'.$this->pswd);
		}
		
		curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($this, 'readHeader'));
		curl_exec($ch);
		curl_close($ch);
	}
	
	function readHeader($ch, $headers_text) {
		return strlen($headers_text);
	}
	
	function fetchcallback($ch, $str) {
			$length =  strlen($str);
			
			$this->downloaded +=$length;
			if(!fwrite($this->tmp_file_pointer,$str)) {
				$this->resetvalues();
				print "Can't write to tmp file\n";
				return;
			} 
			
			if($this->downloaded >= $this->download_size) {
				 $this->resetvalues();
				 $this->job->sendStatus(100,100);
				 $this->job->sendComplete();
				  
				 $this->closefp();			
				 print "100% Done\n";	
				 return;
			}		
			
			if($this->last_part_time > time()- 2) // 2 seconds
			return $length;
			
			$this->last_part_time = time();
			$this->job->sendStatus($this->percent($this->downloaded, $this->download_size) ,100);

			print $this->percent($this->downloaded, $this->download_size)."%...";
			return $length;	
	}
	
}

Как видите для скачивания используется curl.
Далее прилагаю код демона, который поднимает вышеуказанный класс. У меня это реализовано как консольная комманда Yii фреймворка, но вы можете реализовать демон как угодно.

Версия с использованием Yii

addServer(); 
		$worker->addFunction("fetchfile", array($worker, "fetchfile")); 
		
		
		while (1)
		{
		  print "Waiting for job...\n";
		 
		  $ret= $worker->work();
		  if ($worker->returnCode() != GEARMAN_SUCCESS)
		    break;
		}

	}

}

Standalone версия


addServer(); 
		$worker->addFunction("fetchfile", array($worker, "fetchfile")); 
		
		
		while (1)
		{
		  print "Waiting for job...\n";
		 
		  $ret= $worker->work();
		  if ($worker->returnCode() != GEARMAN_SUCCESS)
		    break;
		}

Запуск standalone версии


php worker.php

Запус версии с использованием Yii

php dispatch.php Fetch

Ну и наконец чтобы статья была исчерпывающей я приведу скрипт (точку входа) dispatch.php для Yii. В принципе он очень похож на обычный index.php для Yii application

require_once(dirname(__FILE__).'/framework/yii.php');
$config=dirname(__FILE__).'/protected/config/console.php';

// remove the following line when in production mode
defined('YII_DEBUG') or define('YII_DEBUG',true);

Yii::createConsoleApplication($config)->run();

Нужно только создать отдельный конфиг для консоли. Как минимум скопируйте содержимое main config
Все, будем считать что наши демоны 🙂 запущены и ждут камманды от gearman jоb сервера чтобы начать работу.

Даем комманду job серверу

addServer();

if(isset($_GET['handle'])){
	$handle = $_GET['handle'];
	$status = $client->jobStatus($handle);
	print_r($status);
	die;
}

$params = array(
		'referrer'=>'',
		'tmp_file'=>dirname(__FILE__).'/test.file',
		'url'=>'http://dl1.overload.in.ua/files/2050/SOD3z9jF3LiXdxO/file.zip',
		);

$params = json_encode($params);
$handle = $client->doBackground("fetchfile", $params);
header('Location: index.php?handle='.urlencode($handle));

В примере идет подключение к job серверу, дается фоновая команда с параметрами. Далее работа скрипта заканчивается и идет редирект с параметром именем задачи job-handle
Далее по этому параметру скрипт может следить за прогрессом выполнения именно этого таска.

Конец.

Columns for CListView

В составе лучшего PHP фреймворка в мире – Yii есть класс для отображения списка элементов – CListView. К сожалению он не отображает список в несколько колонок, для одного из моих проектов понадобилось именно это.

Представляю ColumnListView
Continue reading Columns for CListView