środa, 14 stycznia 2009

jQuery 1.3

Dzisiaj ( 14.01.2009 ) ekipa jQuery ogłośiła zakończnie prac nad jQuery w wersji 1.3. Jako główne zmiany wymienione zostały:
  • Sizzle: A sizzlin’ hot CSS selector engine.
  • Live Events: Event delegation with a jQuery twist.
  • jQuery Event Overhaul: Completely rewired to simplify event handling.
  • HTML Injection Rewrite: Lightning-fast HTML appending.
  • Offset Rewrite: Super-quick position calculation.
  • No More Browser Sniffing: Using feature detection to help jQuery last for many more years to come.
Zmian naturalnie jest dużo więcej - o wszystkich można przeczytać na stronie informacyjnej tej wersji.

Moim zdaniem najważniejszymi zmianami jest wykorzystanie Sizzle który według różnych testów jest znacznie szybszy od innych silników.

Bardzo podobają mi się też tzw. "Live Events". Nie jest to może gigantyczna zmiana ale brak konieczności przypisywania elementów po każdym AJAX'owym zapytaniu itp. może znacznie przyspieszyć pracę.

Poza zmianami w samej bibliotece zmienia się również część strony jQuery.com na której możemy przeglądać API. Moim zdaniem zmiana na plus bo przegląda się teraz wszystko dużo szybciej. Ale to nie koniec. Aby było bardziej spektakularnie stworzona została również aplikacja w Adobe AIR pozwalająca na przeglądanie API bez odpalania przeglądarki internetowej.

Linki w CakePHP a'la Rails.

Piszą aplikacje w Ruby on Rails bardzo podobał mi się sposób tworzenia linków :
link_to "Dodaj", new_admin_client_reservation_path(client)
Może nie do końca chodzi mi o samo link_to ale już tworzenie url'a to fajna sprawa.

Pisząc aplikacje w CakePHP nie jestem szczególnie fanem korzystania z metody link znajdującego się w HtmlHelper. Osobiście wolałem wykorzystać samo Router::url() i wstawić to w element. Wszystko pięknie ale w momencie gdy trzeba zrobić link w którym konieczne jest podanie kontrolera, akcji, prefixu admin, id i jeszcze czegoś to już nie wygląda to tak pięknie gdy przekazujesz wszystko za pomocą tablicy:
Router::url(array('controller'=>'posts', 'action'=>'view', 'admin'=>true, 'id'=>$post['Post']['id']))
Dlatego też postanowiłem napisać mały helper który pozwala na tworzenie linków w trochę przyjemniejszy sposób. Oto mały przykład. Powiedzmy że mamy w aplikacji
Configure::read('Routing.admin') => secure
//w widoku
$post = array(['Post']=>array('id'=>3,'title'=>'Test post','slug'=>'test-post'));
Możemy teraz tworzyć linki tak :
$r->posts() # => /posts
$r->securePosts() # => /secure/posts
$r->formatedPosts('xml') # => /posts.xml
$r->secureFormatedPosts('xml') # => /secure/posts.xml

$r->addPost() # => /posts/add
$r->secureAddPost() # => /secure/posts/add
$r->formatedAddPost('xml') # => /posts/add.xml
$r->secureFormatedAddPost('xml') # => /secure/posts/add.xml

$r->editPost($post) # => /posts/edit/3
$r->secureEditPost($post) # => /secure/posts/edit/2
$r->formatedEditPost($post,'xml') # => /posts/edit/3.xml
$r->secureFormatedEditPost($post,'xml') # => /secure/posts/edit.xml
Naturalnie nie ma problemu żeby podać również standardowe argumenty jakie można przekazać do Router::url()
$r->SecureFormatedEditPost($post,'xml',array('full_base'=>true,'simple','#'=>'top')); # => http://example.com/secure/posts/edit/3/simple.xml#top
$r->SecureFormatedEditPost($post,'xml',array('full_base'=>true,'simple','foo'=>'bar','#'=>'top')) # => http://example.com/secure/posts/edit/3/simple/foo:bar.xml#top
Postanowiłem również ułatwić trochę proces przekazywania danych z wybranego rekordu do linku. Zawsze denerwowało mnie gdy musiałem wpisywać $post['Post']['id'] itp. Dlatego też w moim helperze można zrobić to tak:
$r->SecureFormatedEditPost($post,'xml',array('foo'=>'bar','title'=>':slug')); # => /secure/posts/edit/3/foo:bar/title:test-post.xml
$r->ViewPost($post,array(':slug')); # => /secure/posts/view/3/test-post
Uwaga! nie jest to jeszcze finalna wersja helpera! To co jeszcze trzeba zrobić
  • Poprawienie wydajności ( helper jest minimalnie wolniejszy od samego Router::url() ale wydaje mi się, że można jeszcze to poprawić )
  • Dla tych co jednak używają $html->link() możliwość tworzenia kompletnych linków
Helper był testowany w CakePHP 1.2  korzystając z PHP 5 ( sorry, PHP 4 jest blee ).
class rHelper extends AppHelper{
private $adminRouting = null;
private $idMethods = array('view','edit','delete');
private $methods = array('add','view','edit','delete');
public function __construct(){
parent::__construct();
$this->adminRouting = Configure::read('Routing.admin');
}
public function __call($method,$args){
$method = explode('_',Inflector::underscore($method));
$url = array();
if($this->_adminSection($method[0])){
$url[$this->adminRouting] = true;
array_shift($method);
}
if($method[0]=='formated'){
array_shift($method);
if($this->_idRequired($method[0])){
if(isset($args[1]) && is_string($args[1])){
$url['ext'] = $this->_clearArgs($args,1);
}else{
throw new Exception('Link format is not specified or it\'s not a string.', E_USER_ERROR);
}
}else{
if(isset($args[0]) && is_string($args[0])){
$url['ext'] = $this->_clearArgs($args,0);
}else{
throw new Exception('Link format is not specified or it\'s not a string.', E_USER_ERROR);
}
}
}
if($this->_hasMethod($method[0])){
$url['action'] = $method[0];
array_shift($method);
}else{
$url['action'] = 'index';
}
$url['controller'] = Inflector::pluralize($method[0]);
array_shift($method);
if($this->_idRequired($url['action'])){
if(!is_array($args[0])){
throw new Exception('Param $args must be an array.', E_USER_ERROR);
}
$model_name = Inflector::classify($url['controller']);
$data = $this->_clearArgs($args,0);
if(!isset($data[$model_name])){
throw new Exception('"'.$model_name.'" Model data not found.', E_USER_ERROR);
}else{
$data = $data[$model_name];
}
$model = ClassRegistry::getObject($model_name);
if(!$model){
App::import('model',$model_name);
$model = new $model_name;
}
if(!is_object($model)){
throw new Exception('Model "'.$model_name.'" not found.', E_USER_ERROR);
}
$model_id = $model->primaryKey;
if(!isset($data[$model_id])){
throw new Exception('Column "'.$model_id.'" is not specified.', E_USER_ERROR);
}
$url['id'] = $data[$model_id];
}
if(isset($args[0]) && is_array($args[0])){
foreach($args[0] as $n => $v){
if($v[0]==':' && isset($data[substr($v,1)])){
$v = $data[substr($v,1)];
}
if(is_numeric($n)){
$url[] = $v;
}else{
if($n[0]==':' && isset($data[substr($n,1)])){
$v = $data[substr($n,1)];
}
$url[$n] = $v;
}
}
}
return Router::url($url);
}
private function _idRequired($method){
return in_array($method,$this->idMethods);
}
private function _adminSection($method){
return $method == $this->adminRouting;
}
private function _hasMethod($method){
return in_array($method,$this->methods);
}
private function _clearArgs(&$args,$index){
$value = $args[$index];
unset($args[$index]);
$args = array_values($args);
return $value;
}
}

poniedziałek, 12 stycznia 2009

TranslateBehavior i formularze w CakePHP

W moim ostatnim projekcie tworzonym w oparciu o framework CakePHP ważnym elementem była możliwość dodawania treści przez użytkowników w wielu językach. Naturalnym krokiem było więc wykorzystanie TranslateBehavior ( link do API bo w manualu jeszcze nic nie ma ) który znajduje się w CakePHP.

Dodanie TranslateBehavior do modelu jest tak samo proste jak w przypadku każdego innego behaviora i ogranicza się do :
  1. stworzenia tabeli w bazie
  2. dodania konfiguracji do modelu
  3. ustawienia wersji językowej w aplikacji
Tak więc, nasza konfiguracja w modelu mogłaby wyglądać w taki sposób:

public $actsAs = array(
'Translate' => array('title', 'content')
);

Naturalnie podczas odczytywania lub w zapisywania nowego rekordu w momencie gdy wykorzysujemy jedynie jedną wersję językową nie występują żadne komplikacje. Nie spotkałem się jednak nigdy z aplikacją która oferując tworzenie treści w różnych wersjach językowych rozbija to na osobne formularze lub coś w tym stylu. Logicznym podejściem jest stowrzenie formularza który pozwala na dodawanie treści równocześnie np. w języku polskim, niemieckim i angielskim. Tutaj jednak pojawia się problem.

Zgodnie z tym co można wyczytać w testach dołączonych do CakePHP aby zapisać dany rekord w kilku wersjach językowych jednocześnie należy dostarczyć dane w następujący sposób:

$data = array(
'slug' => 'new_translated',
'title' => array('eng' => 'New title', 'spa' => 'Nuevo leyenda'),
'content' => array('eng' => 'New content', 'spa' => 'Nuevo contenido')
);

Nie ma tutaj nic nadzwyczajnego. Aby uzyskać taki efekt musimy w formularzu stworzyć pola z lekką modyfikacją w porównaniu ze standardową procedurą.

echo $form->input('Article.title.eng');
echo $form->input('Article.title.spa');

Schody zaczynają się w momencie gdy chcemy te same dane odczytać ( przykładowo podczas edytowania danych ). Tutaj również posłużę się fragmentem kodu z testów:

array(
'TranslatedItem' => array('id' => 4, 'slug' => 'new_translated', 'locale' => 'eng', 'title' => 'New title', 'content' => 'New content'),
'Title' => array(
array('id' => 21, 'locale' => 'eng', 'model' => 'TranslatedItem', 'foreign_key' => 4, 'field' => 'title', 'content' => 'New title'),
array('id' => 22, 'locale' => 'spa', 'model' => 'TranslatedItem', 'foreign_key' => 4, 'field' => 'title', 'content' => 'Nuevo leyenda')
),
'Content' => array(
array('id' => 19, 'locale' => 'eng', 'model' => 'TranslatedItem', 'foreign_key' => 4, 'field' => 'content', 'content' => 'New content'),
array('id' => 20, 'locale' => 'spa', 'model' => 'TranslatedItem', 'foreign_key' => 4, 'field' => 'content', 'content' => 'Nuevo contenido')
)
);

Jak widać dane te są zwracane w zupełnie inny sposób, co uniemożliwia szybkie wstawienie tego do formularza. Jak widać CakePHP zwraca takie dane tak jakby tworzył relacje pomiędzy modelami dla każdej kolumny - jest to prawda. Aby zwrócić dane w kilku wersjach językowych jednocześnie nasz kod konfiguracyjny w modelu należy zmienić na :

public $actsAs = array(
'Translate' => array('title' => "Title", 'content' => "Title")
);

Zapis ten można odczytać tak : 'kolumna_ktora_bedzie_tlumaczona' => 'NazwaRelacji'.

Ok, fajnie że da się to zrobić, fajnie że zapisuje, fajnie że odczytuje. Jednak perspektywa przetwarzania teraz po każdym odczytaniu tablicy z danymi albo ręczne podawanie wartości pola w formularzu może człowieka skutecznie zniechęcić.

Można jednak temu łatwo zaradzić. Wystarczy w AppModel dodać metodę afterFind która zawsze gdy będzie to potrzebne sama przerobi dane z modelu tak aby miały one taką samą strukturę jak dane które trzeba dostarczyć do modelu aby je zapisać. Jej kod wygląda tak :

public function afterFind($results){
if( isset($this->Behaviors->Translate) ){
foreach( $this->Behaviors->Translate->settings[$this->alias] as $key => $value){
foreach($results as $index => $row){
if( array_key_exists($value, $row) ){
foreach($row[$value] as $locale){
if( isset($results[$index][$this->alias][$locale['field']])){
if( !is_array($results[$index][$this->alias][$locale['field']]) ){
$results[$index][$this->alias][$locale['field']] = array();
}
$results[$index][$this->alias][$locale['field']][$locale['locale']] = $locale['content'];
}
}
}
}
}
}
return $results;
}

Metoda po wyciągnięciu danych z bazy sprawdza czy model zawiera jakieś pola które zostały przypisane do TranslateBehavior i jeżeli tak przetwarza odpowiednio tablicę. Dzięki temu możemy zapisywać, odczytywać dane w formularzach i całej aplikacji bez konieczności tworzenia modyfikacji w przypadku gdy dany model korzysta z TranslateBehavior.