[Extension] Jii

Hi to all,

even though I’ve been using Yii for almost one year and a half, I am still pretty new to it’s forum.

Nevertheless I would like to give my contribution to its great community with an extension I would like to share.

It’s purpose is to help you keeping your code more maintainable and, in particular, to keep the clearest separation between PHP and Javascript code.

Jii extension allows you to share variables and models from your server side PHP code with your client side Javascript code.

Each variabile is encoded using CJavaScript::encode and each model - with it’s relations - (or models, or any instance inheriting from CModel) is converted to its JSON equivalent.

Moreover, for security reason, you can decide with attributes will be available on the client side.

Anyway below you will find copy and paste code, configuration instructions and a simple how-to guide.

Feel free to use it and to report any bug you find as well as feature requests or comments.

Thank you

How to use Jii:




    // adding params

    Yii::app()->jii->addParam('integer', 10);


    Yii::app()->jii->addParam('unsigned_integer', -10);


    Yii::app()->jii->addParam('unsigned_float', 451.239873);


    Yii::app()->jii->addParam('signed_float', -309.0092927);


    Yii::app()->jii->addParam('bool_false', false);


    Yii::app()->jii->addParam('bool_true', true);


    Yii::app()->jii->addParam('string', '<h1>Title</h1><a href="#">link</a>');


    Yii::app()->jii->addParam('associative_array', array('goofy' => 3409879, '+349287//' => '<a>link</a>'));


    Yii::app()->jii->addParam('numeric_array', array(0, 1, -39, -938.2223, '<a href="#">Test</a>', true));


    Yii::app()->jii->addParam('object', $object);


    // adding urls

    Yii::app()->jii->addUrl('view_test_url', $this->createUrl('test/view', array('id' => 1)));


    // adding functions

    Yii::app()->jii->addFunction('function', 'function(){ alert("This is an alert!"); }');



How to add Jii to your page:




    Yii::app()->clientScript->registerScript('jii', Yii::app()->jii->getScript(), CClientScript::POS_END);



How to add models to Jii:




    $jsonized_model = Yii::app()->jii->jsonize($model);


    // adding models to jii

    Yii::app()->jii->addModel('model', $jsonized_model);



You can define which attributes will be jsonized by adding the following method to your model classes:




    ...

    public function getJsonizeables()

    {

        return array(

            'attribute_name_1',

            'attribute_name_3',

            'attribute_name_4',

        );

    }

    ...




Then, from your Javascript code, Jii will be available as an object on the global scope with the following properties:




    var Jii = {

        params: {},

        models: {},

        urls: {},

        functions: {},

    }



How to configure Jii:




    'components' => array(

        ...

        'jii' => array(

            'class' => 'Jii',

        ),

        ...

    ),



Create a Jii.php file under protected/components and paste the following code:




<?php

class Jii extends CComponent

{

	private  $_jsonizer;

	

	private $_obj = 'var Jii = {params: {{params}}, models: {{models}}, urls: {{urls}}, functions: {{functions}}}';

	

	private $_models = array();

	

	private $_params = array();

	

	private $_urls = array();


	private $_functions = array();

	

	public function init()

	{

		$this->_jsonizer = new Jsonizer();

	}

	

	public function jsonize($models)

	{

		return $this->_jsonizer->jsonize($models);

	}


	public function addModel($name, $data)

	{

		$this->_models[$name] = $data;

	}


	public function addFunction($name, $code)

	{

		$this->_functions[$name] = $code;

	}

	

	/**

	* Converts a Php variable into a Javscript one

	*/

	public function addParam($name, $value)

	{

		if (is_object($value) || $this->_isAssoc($value)) {


			$this->_params[$name] = json_encode($value);


		} else {


			if (!is_array($value)) {


				$this->_params[$name] = $this->_toJsPrimitive($value);


			} else {


					$array = '[{items}]';


					$items_string = '';


					foreach ($value as $item) {

						$items_string .= $this->_toJsPrimitive($item) . ',';

					}


					$items_string = substr($items_string, 0, -1);


					$array = str_replace('{items}', $items_string, $array);


					$this->_params[$name] = $array;




			}

		}

		


	}

	

	private function _toJsPrimitive($value)

	{

		return CJavaScript::encode($value);

	}


	private function _isAssoc($arr)

	{

	    return array_keys($arr) !== range(0, count($arr) - 1);

	}

	public function addUrl($label, $url)

	{

		$this->_urls[$label] = '"' . htmlspecialchars($url) . '"';

	}

	

	public function getScript()

	{

		$models = $params = $urls = $functions = '';

		

		if (!empty($this->_params)) {

			foreach($this->_params as $name => $data) {

				$params .= "$name: " . $data . ',' . PHP_EOL;

			}

			$params = substr($params, 0, -2);

		}


		if (!empty($this->_models)) {

			foreach($this->_models as $name => $data) {

				$models .= "$name: " . $data . ',' . PHP_EOL;

			}

			$models = substr($models, 0, -2);

		}


		if (!empty($this->_urls)) {

			foreach($this->_urls as $name => $data) {

				$urls .= "$name: " . $data . ',' . PHP_EOL;

			}

			$urls = substr($urls, 0, -2);

		}


		if (!empty($this->_functions)) {

			foreach($this->_functions as $name => $code) {

				$functions .= "$name: " . $code . ',' . PHP_EOL;

			}

			$functions = substr($functions, 0, -2);

		}


		$this->_obj = str_replace(array('{models}', '{params}', '{urls}', '{functions}'), array($models, $params, $urls, $functions), $this->_obj);

		

		return $this->_obj;

	}

}


class Jsonizer

{

	/**

	* Converts a CActiveRecord instance into an array

	* @param CActiveRecord $model

	* @return array $model

	*/

	private function _jsonizeOne($model)

	{

		

		// for each model we store only jsonizeables attributes

		$attributes 	= array();

		$jsonizeables 	= array();


		// we select which attributes must be jsonized

		if (method_exists($model, 'getJsonizeables')) {

			$attributes = $model->getJsonizeables();

		

		// we get all model attributes if no jsonizeables attributes have been found

		} else {

			$attributes = array_keys($model->getAttributes());

		}


		// we encode each attribute into a javascript variable

		foreach ($attributes as $attribute_name) {

			$jsonizeables[$attribute_name] = $model->$attribute_name;

		}

		 

		// basic inheritance detection

		if ($this->_isParent('CModel', $model)) {

			$modelArray = $jsonizeables;			

			if (method_exists($model, 'relations')) {

				$relations = array_keys($model->relations());


				foreach ($relations as $relation) {

					if ($model->hasRelated($relation)) {


						$related_models = $model->getRelated($relation);

						

						if ($related_models !== null) {

							

							if (is_array($related_models)) {

								

									if (!empty($related_models)) {

										foreach($related_models as $related) {

	 

											$modelArray[$relation][] = $this->_jsonizeOne($related);

											// print_r($this->_jsonizeOne($related));

									 	}

									} else {


										$modelArray[$relation][] = array();

										

									}


							} else {

								 

								// print_r($related_models->getAttributes());

								$modelArray[$relation] = $this->_jsonizeOne($related_models);

							}


						} else {

							

							$modelArray[$relation] = null;


						}

						 

					}

				}

			}

		}

	

		// print_r($modelArray);

		return $modelArray;			

	}	

	

	/**

	* Converts an array of CActiveRecord instances into a php array

	* @param array $models

	* @return array 

	*/

	private function _jsonize($models)

	{	

		$modelArray = array();

		$i = 0;

		 

		foreach ($models as $model) {

			$model->getAttributes();

			$modelArray[$i++] = $this->_jsonizeOne($model);

		}

		

		return $modelArray;

	}

	

	private function _isParent($classname, $child)

	{

		while(($parent = get_parent_class($child)) !== false) {

			if ($parent === $classname) {

				return true;

			}

			$child = $parent;

		}

		return false;

	}

	

	/**

	* Converts CModel instances into JSON objects

	* @param CModel $data

	* @return string $json

	*/

	public function jsonize($data)

	{

		if (is_array($data)) {

			return json_encode($this->_jsonize($data));

		} else {

			return json_encode($this->_jsonizeOne($data));

		}

		

	}	

	

}



I just have fixed some bugs. Sorry. Hope someone will find this useful.

nice idea! thanks for sharing .

and … why not post it to extension repo or github . :lol:

Thank you so much!

Actually, Jii github repository is at: github.com/postypython/jii

I cannot post it to the extensions repo because I my user account is not allowed: I have too few posts, I’m sorry.

Jii is now available on Yii extensions repository at: http://www.yiiframework.com/extension/jii/

@pmaselkowski about Ko mapping

Thank you for your suggestion.

I think that jii jsonizer behaves as much as you describe your converter does: it accepts objects or array of objects inheriting from CModel class on one side and on the other site it can parse CActiveDataProvider(s).

Each object is parsed checking first if the user has specifed any jsonableAttributes through the method getJsonizeables and, if non is found, each attribute is returned.

Then it checks if any relation has been loaded and, if so, it performs the necessary operations to extract the data.

The result is a simple array that will be encoded to JSON and added to jii.

So I think that Jii offers two ways of supporting ko.mapping plugin (which by the way I haven’t still used or tested). I’d personally choose the second one.

The first is "server side" through Yii::app()->jii->jsonize that returns a JSON representation of your models:




<script type="text/javascript">

    ...

    // note that there are of course a few ways to achieve the following result

    ko.mapping.fromJS(<?php echo Yii:<img src='http://www.yiiframework.com/forum/public/style_emoticons/default/sad.gif' class='bbc_emoticon' alt=':(' />)->jii->jsonize($model); ?>);

    ...

</script>



The second is "client side" through jii.models.your_model.toJS() method.

In this case the model you need to assign to ko mapping is available as a Javascript object on the Jii Javascript object:




    ...

    ko.mapping.fromJS(jii.models.your_model.toJS());

    ...



That’s it.

Anyway Jii jsonizer is still missing for lazy loaded relations: I am wondering if that could be useful or not.

@Cherif

I tried to simplify Jii jsonizer to make it more similar at what It would be if it was attached to a Model as a behavior:




<?php

// $model is automatically converted to JSON

Yii::app()->jii->addModel('your_model', $model);

?>



Then you can get your_model observable in the following way:




var ko_observable = jii.utils.observable(jii.models.your_model.getObservable());

var ko_observable_array = jii.utils.observableArray(jii.models.your_model.getObservableArray());



I am planning to add attribute validation. Do you think that it would be useful?

Otherwise, what do you think it could be useful to add?

:D

Sorry, but why not :


<?php

class Jii extends CComponent

{

	public  $config;

	

	private $_jsonizer;

	

	private $_bindings	= array();


	private $_models 	= array();

	

	private $_params 	= array();

	

	private $_urls 		= array();


	private $_functions = array();

	

	private $_script 	= 'jii-min-0.0.4.js';


	public function init()

	{

		if (!isset($this->config['script'])) {

			$this->config['script'] = $this->_script;

		}


		// publish jii script

		$jii_js = Yii::app()->assetManager->publish(Yii::getPathOfAlias('common.extensions.jii.js') . DIRECTORY_SEPARATOR .  $this->config['script']);

		

		// registers jii

		Yii::app()->clientScript->registerScriptFile($jii_js, CClientScript::POS_END);

		

		$this->_jsonizer = new Jsonizer();

	}

	

	public function jsonize($models)

	{

		return $this->_jsonizer->jsonize($models);

	}


	/**

	* Adds a model to Jii

	* @param string $name the name of the jii object property

	* @param mixed $data the model to be added

	*/

	public function addModel($name, $data)

	{

		// if we cannot decode JSON $data, we try to jsonize

		if (json_decode($data) === null) {			 

			$data = $this->jsonize($data);

		}

		 

		$this->_models[$name] = $data;

	}


	public function addFunction($name, $code)

	{

		$this->_functions[$name] = $code;

	}


	/**

	* Allows to add custom function to be executed after document is ready

	* @params string $function javascript anonymous function

	*/

	public function addBindings($function)

	{		

		$this->_bindings[] = $function;

	}

	

	/**

	* Converts a Php variable into a Javscript one

	*/

	public function addParam($name, $value)

	{		 

		if (!is_array($value)) {

			

			$this->_params[$name] = $value;


		} else {

			

			if (is_object($value) || $this->_isAssoc($value)) {

	

				$this->_params[$name] = json_encode($value);

	

			} else {

				$this->_params[$name] = $value;

			}

		}

		


	}

	

	private function _toJsPrimitive($value)

	{

		return CJavaScript::encode($value);

	}

	

	private function _jsonEncode($value)

	{

		return CJavaScript::jsonEncode($value);

	}


	private function _isAssoc($arr)

	{

	    return array_keys($arr) !== range(0, count($arr) - 1);

	}

	public function addUrl($label, $url)

	{

		$this->_urls[$label] = htmlspecialchars($url);

	}

	

	/**

	 * The following to ensure that each script is rendered wherever you add it to your view

	 * @return string $javascript javascript code

	 */

	public function getScript()

	{

		$models = $params = $urls = $functions = $bindings = '';

		

		if(!empty($this->_params)) {

			$params = 'jii.params = ' . $this->_jsonEncode($data) .  ';';


		if(!empty($this->_urls)) {				

				$urls .= 'jii.urls = ' . $this->_jsonEncode($data) .  ';';

		}


		if (!empty($this->_functions)) {				

				$functions .= 'jii.functions = ' . $this->_jsonEncode($data) .  ';';

					

		}

		

		if (!empty($this->_models)) {

			foreach($this->_models as $name => $data) {				

				$models .= 'jii.models.' . $name .' = new jii.Model(' . $this->_jsonEncode($data) . ');';

			}

		}


		if (!empty($this->_bindings)) {

			foreach ($this->_bindings as $binding) {

				$bindings .= 'jii.bindings.bindings.push(' . $this->_jsonEncode($binding) .');' . PHP_EOL;

			}

		}

		

		// clears everything after each call

		$this->_params 		= array();

		$this->_urls 		= array();

		$this->_models 		= array();

		$this->_bindings 	= array();

		

		return $urls . PHP_EOL . $params . PHP_EOL . $functions . PHP_EOL . $models . PHP_EOL . $bindings;	

	}

}


class Jsonizer

{	

	

	/**

	* Converts a CActiveRecord instance into an array

	* @param CActiveRecord $model

	* @return array $model

	*/

	private function _jsonizeOne($model)

	{

		

		// for each model we store only jsonizeables attributes

		$attributes 	= array();

		$jsonizeables 	= array();


		// we select which attributes must be jsonized

		if (method_exists($model, 'getJsonizeables')) {

			$attributes = $model->getJsonizeables();

		

		// we get all model attributes if no jsonizeables attributes have been found

		} else {

			$attributes = array_keys($model->getAttributes());

		}


		// we encode each attribute into a javascript variable

		foreach ($attributes as $attribute_name) {

			$jsonizeables[$attribute_name] = $model->$attribute_name;

		}

		 

		// basic inheritance detection

		if ($this->_isParent('CModel', $model)) {

			$modelArray = $jsonizeables;			

			if (method_exists($model, 'relations')) {

				$relations = array_keys($model->relations());


				foreach ($relations as $relation) {

					if ($model->hasRelated($relation)) {


						$related_models = $model->getRelated($relation);

						

						if ($related_models !== null) {

							

							if (is_array($related_models)) {

								

									if (!empty($related_models)) {

										foreach($related_models as $related) {

	 

											$modelArray[$relation][] = $this->_jsonizeOne($related);

											// print_r($this->_jsonizeOne($related));

									 	}

									} else {


										$modelArray[$relation][] = array();

										

									}


							} else {

								 

								// print_r($related_models->getAttributes());

								$modelArray[$relation] = $this->_jsonizeOne($related_models);

							}


						} else {

							

							$modelArray[$relation] = null;


						}

						 

					}

				}

			}

		}

	

		// print_r($modelArray);

		return $modelArray;			

	}	

	

	/**

	* Converts an array of CActiveRecord instances into a php array

	* @param array $models

	* @return array 

	*/

	private function _jsonize($models)

	{	

		$modelArray = array();

		$i = 0;

		 

		foreach ($models as $model) {

			$model->getAttributes();

			$modelArray[$i++] = $this->_jsonizeOne($model);

		}

		

		return $modelArray;

	}

	

	private function _isParent($classname, $child)

	{

		while(($parent = get_parent_class($child)) !== false) {

			if ($parent === $classname) {

				return true;

			}

			$child = $parent;

		}

		return false;

	}

	

	/**

	* Converts CModel instances into JSON objects

	* @param CModel $data

	* @return string $json

	*/

	public function jsonize($data)

	{

		// support for CActiveDataProvider

		if ($data instanceof CActiveDataProvider) {

			$data = $data->getData();

		}

		

		if (is_array($data)) {

			return json_encode($this->_jsonize($data));

		} else {

			return json_encode($this->_jsonizeOne($data));

		}

		

	}	

		

}

Change code : public function addParam() and public function getScript()

please screen shoots demo aplications :)