Namespace’y javascript w widokach cakePHP

Czasem zdarza się tak, że w danym widoku potrzebuję bardzo specyficzną funkcję javascript. Na przykład w widoku invoices/add funkcja count_gros_value, która na podstawie wpisanej wartości w polu netto (net) i stawki var wyliczy wartość brutto (gros).

Załóżmy, że nie wiem jak napisać taką funkcję, żeby była elastyczna, po prostu we wnętrzu mam zaszyte id pól formularza:

function count_gros_value(){
   $("#Invoice[gros]").val( 
      parseFloat($("#Invoice[net]").val()) * 
      parseFloat($("#Invoice[vat]").val())
   );
}
// wywołanie
count_gros_value();

(nie sprawdzałem tego kodu, służy jako przykład i nie mam pojęcia czy dobrze działa).

Oprócz wielkiej nieelastyczności kodu (o czym teraz pisał nie będę) istnieje problem polegający na tym, że nie wiem czy jakiś inny programista nie zdefiniował funkcji count_gros_value gdzieś w layoucie. Oczywiście dostanę error jeśli tak jest, więc poprawię ją na my_count_gros_value() (sic!). Ok, wszystko działa. Jednak przychodzi inny programista i odczuwa potrzebę zdefiniowania funckji my_count_gros_value() w layoucie (no bo count_gros_value() już istnieje, a jego robi coś inaczej/lepiej więc trzeba ją zaimplementować). W ten sposób rozwala twój zaimplementowany fragment i nikt tego nie zauważa.

Jak tego uniknąć? Można zacząć nazywać takie “lokalne” funkcje invoices_index_count_gros_value() ale to zaczyna wyglądać nieestetycznie i aż się ciśnie na usta magiczne słowo “namespace”.

W Javascript nie ma przestrzeni nazw, ale możesz użyć obiektów:

var invoices = {
   index: {
      count_gros_value: function(){
         $("#Invoice[gros]").val( 
            parseFloat($("#Invoice[net]").val()) * 
            parseFloat($("#Invoice[vat]").val())
         );
      }
   }
}
// wywołanie
invoices.index.cunt_gros_value();

to jest już fajniejsze, ale ma dwa problemy:

  1. ciągłe definiowanie obiektów dla namespace’ów jest równie uciążliwe jak nazwa funkcji invoices_index_count…
  2. gdy ktoś przed nami zdefiniuje zmienną var invoices = “invoices”; to mu ją nadpiszemy

Dlatego proponuję taką funckję, która definiuje dowolnie głębokie obiekty udające namespace’y i do tego tworzy je wszytkie w zmiennej o pseudolosowej nazwie (w stylu: Namespace12883495048110.1425782083547854):

function Namespace(namespace_path){
	var w = window;

	window.namespaceFrameworkNamespaceName = 
		window.namespaceFrameworkNamespaceName || 
		"Namespace"+ new Date().getTime() + new Math.random();

	var arr  = (window.namespaceFrameworkNamespaceName + namespace_path).
						split(".");

	for(i in arr){
		w[ arr[i] ] = w[ arr[i] ] || {};
		w = w[ arr[i] ];
	}
	return w;
}

Ok, jakiś uparciuch mógłby się uprzeć, że ktoś może w aplikacji zmiennej ‘namespaceFrameworkNamespaceName’ – takiego problem nie rozwiązywałbym na poziomie kodu, ale personalnym (zwiałbym z projektu) ;)

tak wygląda definiowanie funkcji w namespace:

Namespace("app.users.index").check_form = function(){ 
	document.write("my_check_form()");
};

a tak jej wywołanie:

Namespace("app.users.index").check_form();
// lub gdy mamy zamiar wywołać wiele funkcji z danego namespace:
with(Namespace("app.users.index")){
 check_form();
}

Można by z tego zrobić tez fajne rozszerzenie jQuery,
$().nspc(“app.invoices.index”) (pisałem już o rozszerzaniu jQuery w tutorialu “longPolling”)

Share Button

Lazy Loading w modelach a’la CakePHP

CakePHP nie wspiera aktualnie leniwego ładowania obiektów. W prawdzie są jakieś skrypty, ale jeszcze żaden z nich nie chciał mi zadziałać. Zacząłem się zastanawiać: “Gdybym miał zaimplementować Lazy Loading w warstwie modelu, to jak bym to zrobił?” To co mi wyszło korzysta z rozwiązań PHP5, więc może wsparcie gałęzi 1.x Cake’a dla PHP4 implikuje brak tego wzorca?

Tak czy siak, podzielę się z Wami moimi pomysłami.

Zacznijmy od początków. Przeciwieństwem leniwego ładowania jest ładowanie gorliwe (lub zachłanne), to znaczy ładowanie na zapas wszystkiego co może okazać się potrzebne. Warstwa modelu w cake’u mogła by wyglądać tak (duże uproszeczenie, jedyna możliwa relacja to belongsTo).

Eager Loading


class Model {
	function find(){

		$r = new ReflectionClass($this);
		print "calling ".$r->getName()."::find($conditions)";

		$query = "Select * from `".strtolower($r->getName())."s` ".
					"as `".$r->getName()."`";

		var_dump($query);

		//return query result
	}

	function __construct(){
		$r = new ReflectionClass($this);
		print("creating ".$r->getName()."
"); if(isset($this->belongsTo) && is_array($this->belongsTo)){ foreach($this->belongsTo as $related){ $this->{$related} = new $related(); } } } } class User extends Model{ protected $belongsTo = array("Group"); } class Group extends Model{ } $user = new User(); $user->find(); echo "
"; $user->Group->find();

Widzimy, że obiekt Group jest inicjowany długo przed użyciem:

creating User
creating Group
calling User::find()

string 'Select * from `users` as `User`' (length=31)


calling Group::find()

string 'Select * from `groups` as `Group`' (length=33)

Mało tego, ładuje się nawet jeśli nigdy nie wywołamy metody w tym obiekcie (zakomentuj $user->Group->find(); żeby się o tym przekonać).

Lazy Loading

class Model {
	function find(){

		$r = new ReflectionClass($this);
		print "calling ".$r->getName()."::find($conditions)";

		$query = "Select * from `".strtolower($r->getName())."s` ".
					"as `".$r->getName()."`";

		var_dump($query);

		//return query result
	}

	function __construct(){
		$r = new ReflectionClass($this);
		print("creating ".$r->getName()."
"); } function __get($property){ if(!isset($this->{$property})) { $this->{$property} = new $property(); } if(in_array($property, $this->belongsTo)){ return $this->{$property}; }else{ $r = new ReflectionClass($this); throw new Exception( "$property not related with {$r->getName()}" ); } } } class User extends Model{ protected $belongsTo = array("Group"); } class Group extends Model{ } $user = new User(); $user->find(); echo "
"; $user->Group->find();

W tym rozwiązaniu obiekt Group nie zostanie stworzony, dopóki nie będziemy go potrzebować. Wykorzystana jest do tego magiczna metoda __get, która jest wywoływana wtedy, gdy próbujemy się odnieść do atrybutu obiektu, który nie istnieje.

ale pozostaje kwestia wyszukiwania w modelu tak, żeby zwrócił powiązane dane
np.: var_dump($user->find());

array
  0 => 
    array
      'User' => 
        array
          'id' => int 1
          'name' => string 'Franek' (length=6)
      'Group' => 
        array
          'id' => int 1
          'name' => string 'Admin' (length=5)
  1 => 
    array
      'User' => 
        array
          'id' => int 2
          'name' => string 'Janek' (length=5)
      'Group' => 
        array
          'id' => int 10
          'name' => string 'User' (length=4)

Wiemy, że belongsTo oznacza, że User powinien mieć pole (group_id), zatem query powinno wyglądać mniej więcej tak:

Select User.id, User.name, Group.id, Group.name from users as User
Inner Join groups as Group on (User.group_id = Group.id)

W tej chwili cakephp zawiera behavior containable, jednak on polega na załadowanych już modelach.

Zostańmy przy terminologii cake’a i przyjmijmy, że będziemy przekazywać informacje na temat powiązanych elementów w zmiennej $contain

function find($contain=null)

Teraz chciałbym powiedzieć: obiekcie $user zwróć mi wszystkich użytkowników z bazy wraz z informacją w jakich są grupach:

$user =  new User();

$user->find(
	array(
		"belongsTo" => array($user->Group)
	)
);

Zatem zaktualizuję metodę find:

	function find($contain=null){

		$r = new ReflectionClass($this);
		print "calling ".$r->getName()."::find($conditions)";

		$query = "Select * from `".strtolower($r->getName())."s` ".
					"as `".$r->getName()."` \n";


		if(!empty($contain) && is_array($contain["belongsTo"])){
			foreach($contain["belongsTo"] as $belongsToModel){
				$query .= $belongsToModel->getBelongsToJoin($this);
			}
		}


		var_dump($query);

		//return query result
	}

która od teraz zakłada, że model posiada metodę, która potrafi utworzyć odpowiedniego joina:

	function getBelongsToJoin($obj){
		$owner_reflection = new ReflectionObject($this);
		$obj_reflection = new ReflectionObject($obj);
		return "INNER JOIN `".strtolower($owner_reflection->getName())."s` 
					as `".$owner_reflection->getName()."`
						ON (`".$owner_reflection->getName()."`.`id` = `".$obj_reflection->getName()."`.`".strtolower($owner_reflection->getName())."_id`)
					";
	}

to jest oczywiście bardzo brzydkie ze względu na refleksje. Metodę getName() można jednak zaimplementować w klasie Model.

Tak czy siak, w tym wypadku nawet w find()’zie uwzględniającym relacje można stosować Lazy Loading

Na koniec całość kodu, z większą ilością relacji i przykładem działania (i małym refactoringiem – skoro już mowa o lazy loading to głupio było ładować piętnaście razy ReflectionClass):


class ModelException extends Exception{}

class Model {

	protected $reflection = null;

	function getName(){
		if(is_null($this->reflection)){
			$this->reflection= new ReflectionClass($this);
		}
		return $this->reflection->getName();
	}

	function getTableName(){
		return strtolower( $this->getName() . "s" );
	}

	function getFKName(){
		return strtolower( $this->getName() . "_id");
	}

	function getBelongsToJoin($obj){
		$owner_reflection = new ReflectionObject($this);
		$obj_reflection = new ReflectionObject($obj);
		return "INNER JOIN `".$this->getTableName()."` 
					as `".$this->getName()."`
						ON (`".$this->getName()."`.`id` = `".$obj->getName()."`.`".$this->getFKName()."`)
					";
	}

	function find($contain=null){

		$r = new ReflectionClass($this);
		print "calling ".$r->getName()."::find($conditions)";

		$query = "\nSelect * from `".strtolower($r->getName())."s` ".
					"as `".$r->getName()."` \n";


		if(!empty($contain) && is_array($contain["belongsTo"])){
			foreach($contain["belongsTo"] as $belongsToModel){
				$query .= $belongsToModel->getBelongsToJoin($this);
			}
		}


		var_dump($query);

		//return query result
	}

	function __construct(){
		$r = new ReflectionClass($this);
		print("creating ".$r->getName()."
"); } function __get($property){ if(!isset($this->{$property})) { $this->{$property} = new $property(); } if(in_array($property, $this->belongsTo)){ return $this->{$property}; }else{ $r = new ReflectionClass($this); throw new ModelException( "$property not related with {$r->getName()}" ); } } } class User extends Model{ protected $belongsTo = array("Group", "Unit", "Company"); } class Group extends Model{ } class Unit extends Model{} class Company extends Model{} $user = new User(); $user->find( array( "belongsTo" => array($user->Unit, $user->Company) ) );
Share Button

Long Polling – tutorial, part 2

how to mimic “server push” with “client pull”?

As You could already see at wikipedia, long pooling means just requests (in our case ajax requests) which can wait quite a long time for server response. Server doesn’t send a single bit of information until something interesting (for the request) appears.

There are two possible results:

  1. something appears (in our case – new post in database), so server sends this info, request hits a success and do something with received data
  2. nothing appears for some time, and request time-outs.

Both possible paths ends the same – request starts another one to wait for something to happen.

You probably already have some ideas how to solve it. If so – please try it (and if it differs from mine solution – I’d love to see it, please post it in comments.)

There are some pitfalls, though. I’ll show most of them.

assumptions

Thing to remember at this point is where request are always going. So it’s not a good way to send html code back and forth. XHttpRequests should expect JSON responses (cleaned of white-spaces if possible).
Another conclusion is: if there’s nothing to send from server it’s better for xhr to timeout than receive response “nothing new”. Even though this is not elegant solution (time-out is still some kind of error).

We should decide how much of information should be send to server in order to get new messages in the thread. The minimum is the thread’s root id and last message id (biggest one). With that information it is possible to get all messages that are in the subtree which is rooted in particular id and their id is bigger than the very last one. But in that case we at least need to make two queries: one to get root lft and rght values, second to get all his children (which ids are between roots lft and rght).

The second approach is to send all messages ids in thread and just get all messages that have parent_id in that set.

My approach is that one should use cleaner solution (1st one) because we don’t have enough data to decide which one is faster (when the application is in production mode You could do some research. Of course we could make some fancy simulations but it is not the point of this tutorial).

So create an action that takes two parameters (tree root id and last message id in the thread) and sending back new messages (if any) or do nothing (not even sending empty response).

//controllers/posts_controller.php
	function get_new($rootId, $lastMessageId){
		$root = $this->Post->read(null, $rootId);
		
		$posts = array();
		
		while(empty($posts)){
			$posts = $this->Post->find(
				"all",
				array(
					"conditions"=>array(
						"and"=> array(
							"Post.lft > " => $root["Post"]["lft"],
							"Post.lft < " => $root["Post"]["rght"],
							"Post.id >" => $lastMessageId
						)
					)
				)
			);
			if(empty($posts)){
				sleep(2); // sleep for two seconds
			}
		}
		$this->set("posts", $posts);
		$this->render("get_new", false);
	}

// views/posts/get_new.ctp

You can try it out in Your browser. But if You choose /posts/get_new/1/8 when 8 is the very last message in this thread it would choke your server up ;) For testing use parameters where at least one message will appear (in the example: /posts/get_new/1/7).

Server issue

This clash on server is connected to sessions. Apache waits to the end of every scripts until it saves session data, at the end it implicitly closes session. For some reason Apache cannot release thread for this request if session is not closed (even if You hit cancel in Your browser, what triggers kill for the processing request in the most of the cases). What you need is to add session_commit(); at the very beginning of actions which are long polled. (if You need to write to session – do it as fast as it’s possible before while loop).

//controllers/posts_controller.php
	function get_new($rootId, $lastMessageId){
		session_commit();
		$root = $this->Post->read(null, $rootId);
		//...

Now we have nice action which gives us JSON string with new messages. Lets load them to our thread.

First of all we’ll need root id and last post id stored in Thread object. Root id is in the first element’s parent_id, last post we’ll find while generating thread tree.
Let’s add those properties to Thread class:

//webroot/js/thread.js
(function(window, document, udefined){
	window.Thread = function(_posts) {
		this.posts = _posts;
		this.rootId = false; //<==
		this.lastId = false; //<==
		this.extendJQuery();
		return this;	
	};
	
})(window, document);

Store root's and last message ids in createThread function:

//webroot/js/thread.js
Thread.prototype.createThread = function(){
	if(this.posts.length <1){
		return this;
	}
	this.rootId = this.lastId = posts[0].Post.parent_id; //<==
	var _this = this; //need to remember this, because in each method this means actual object from collection we iterate through
	$(this.posts).each(function(k, v){
		if($("#"+v.Post.parent_id).find("ul").length == 0){
			if($("#"+v.Post.parent_id).find("div.reply").length == 0){
				_this.createReplyDiv().appendTo("#"+v.Post.parent_id);
			}
	        $("
    ", {class: "posts children"}).appendTo("#"+v.Post.parent_id); } $("
  • ", { id: v.Post.id, innerHTML: "
    "+v.Post.message+"
    " , class: "post child" }). append(_this.createReplyDiv()). appendTo("#"+v.Post.parent_id+">ul"); if(v.Post.id>_this.lastId){ //<== _this.lastId = v.Post.id; } }); return this; };

Now I want to get data from get_new action, but my thread object have no idea where to get it from. Let's allow to pass endpoint url to constructor.

//webroot/js/thread.js
(function(window, document, udefined){
	window.Thread = function(_posts, _updateEndpoint) {//<==
		this.posts = _posts;
		this.rootId = false;
		this.lastId = false;
		this.updateEndpoint = _updateEndpoint; //<==
		this.extendJQuery();
		return this;	
	};
	
})(window, document);

update Thread object creation:

//views/layouts/default.ctp
		$(document).ready(function(){
			if(!window.posts) return false;
			window.thread = new Thread(
				posts, 
				"<?php echo $html->url(
							array(
							'controller'=>'posts', 
							'action'=>'get_new')
				);?>"
			).createThread();	
			console.log(thread);
		});

and create update() method. It should get JSON with new posts, and in case of either success or time-out Thread.update() should be called again:

//webroot/js/thread.js
Thread.prototype.update = function(){
	if(!this.updateEndpoint || !this.rootId || !this.lastId){
		alert("Thread corrupted");
		return false;
	}
	_this = this;
	$.ajax({
		url: this.updateEndpoint +"/"+ this.rootId+ "/" + this.lastId,
		dataType: 'json',
		timeout: 5000,
		success: function(){
			_this.update();
		},
		error: function(){
			_this.update();
		}
	});
	return true;
}

bug #1

I've found a bug after adding post with id=10 to the thread. It appears that I was storing lastId - the biggest id but in... dictionary order (check in JS: "10">"9" => false). Please patch last if statement in Thread.addPost() like that:

//...
		if(parseInt(v.Post.id)>_this.lastId){
			_this.lastId = parseInt(v.Post.id);
		}
}

Call newly created method in Thread.createThread():

Thread.prototype.createThread = function(){
	var _this = this;
	if(this.posts.length <1){
		return this;
	}
	this.rootId = this.lastId = posts[0].Post.parent_id;
	$(this.posts).each(function(k, v){
		if($("#"+v.Post.parent_id).find("ul").length == 0){
			if($("#"+v.Post.parent_id).find("div.reply").length == 0){
				_this.createReplyDiv().appendTo("#"+v.Post.parent_id);
			}
        	$("
    ", {class: "posts children"}).appendTo("#"+v.Post.parent_id); } $("
  • ", { id: v.Post.id, innerHTML: "
    "+v.Post.message+"
    ", class: "post child" }). append(_this.createReplyDiv()). appendTo("#"+v.Post.parent_id+">ul"); if(parseInt(v.Post.id)>_this.lastId){ _this.lastId = parseInt(v.Post.id); } }); this.update(); //<== return this; }

You can check now if it's trying to get new data again and again. Try to add new post in other browser and see what happens with request...
As You can see - there's ok response but from now on every request is getting the same response, and the thread isn't updated. We'll fix that now. First extract the method that creates post elements from createThread method - we will reuse it...

Thread.createThread should look like that:

Thread.prototype.createThread = function(){
	if(this.posts.length <1){
		return this;
	}
	this.rootId = this.lastId = posts[0].Post.parent_id;
	var _this = this; //need to remember this, because in each method this means actual object from collection we iterate through
	$(this.posts).each(function(k, v){
		_this.addPost(v.Post.id, v.Post.parent_id, v.Post.message); //<==
	});
	this.update();
	return this;
};

and our new method Thread.addPost:

Thread.prototype.addPost = function(id, parentId, message){
	if($("#"+parentId).find("ul").length == 0){
		if($("#"+parentId).find("div.reply").length == 0){
			this.createReplyDiv().appendTo("#"+parentId);
		}
        $("
    ", {class: "posts children"}).appendTo("#"+parentId); } $("
  • ", { id: id, innerHTML: "
    "+message+"
    " , class: "post child" }). append(this.createReplyDiv()). appendTo("#"+parentId+">ul"); if(id>this.lastId){ this.lastId = id; } }

bug #2

I encountered even more important bug - we caused a memory leak in our server (just wait long enough with get_new requests running) - after xhr time-out the script is not terminating.
I added a simple counter to get_new method:

//controllers/posts.php
	function get_new($rootId, $lastMessageId){
		//...
		$counter = 2; //<==
		while(empty($posts)){
			$posts = $this->Post->find(
				"all",
				array(/*...*/)
			);
			if($counter>5){ //<==
				break; //<==
			}else{ //<==
				$counter += 2; //<==
			} //<==
			if(empty($posts)){
				sleep(2); // sleep for two seconds
			}
		}
		$this->set("posts", $posts);
		$this->render("get_new", '');
	}

Why is it incremented by two? It's because there's 2 seconds sleep, so I'm checking if the script slept for at least 5 seconds altogether (5 seconds is the timeout of ajax request).

To finish - call extracted method in success callback:

//webroot/js/thread.js
Thread.prototype.update = function(){
	if(!this.updateEndpoint || !this.rootId || !this.lastId){
		alert("Thread corrupted");
		return false;
	}
	_this = this;
	$.ajax({
		url: this.updateEndpoint +"/"+ this.rootId+ "/" + this.lastId,
		dataType: 'json',
		timeout: 5000,
		success: function(data){ //<==
			$(data).each(function(k,v){
				_this.addPost(v.Post.id, v.Post.parent_id, v.Post.message); //<==
			});
			_this.update();
		},
		error: function(){
			_this.update();
		}
	});
	return true;
}

That's all. Of course with longPooling (and soon with web sockets) you can implement some more cool stuff. You can display information about somebody typing a reply right now in some particular place.

There are some details to be polished, maybe newly added post should attract attention (blink?), but it's not the case of this tutorial.

I hope you enjoyed it, please let me know what you think about it.

Share Button