Posts Tagged DRY
Tutorial: dashboard web 2.0 dla leniwych…
W moim pierwszym tutorialu napiszę jak wydajnie korzystać z tego co daje Ci cakePHP i jQuery. Aktualnie cake opiera się na współpracy z innym frameworkiem javascript – prototype. Ale już w wersji 1.3 core cake’a ma współpracować z jquery. Już nie mogę się doczekać. Ten tutorial oparłem na wersji 1.3.0-beta dlatego, że przy okazji jego pisania chciałem rzucić okiem na kolejną wersję tego świetnego frameworka php. Jednak nie będę korzystał z żadnych helperów jquery, zatem większość z tych rad powinna pasować do wersji 1.2 (w razie problemów – chętnie służę pomocą).
Jest to mój pierwszy tutorial, więc proszę o wskazówki, komentarze i odrobinę wyrozumiałości.
Efekt jaki chcemy osiągnąć można obejrzeć jako demo oraz źródła na świetnym hostingu, który gorąco wszystkim webdeveloperom i ich klientom polecam: vipserv.org.
W tym tekście zakładam, ze znasz podstawy cakePHP i javascript. Czyli przynajmniej zrobiłeś sławny blog tutorial , wiesz jak mniej więcej używać narzędzia bake, oraz potrafisz przynajmniej zniknąć element na stronie i zmienić jego kolor przy pomocy czystego javascript’u.
Zacznijmy od przygotowania przykładowej aplikacji. Wypiekamy (cake bake) aplikację złożoną z dwóch modeli Thing i Item (każda tabela składa się z id i pola tekstowego name).
Następnie tworzymy dla niej zbiorczy widok (tzw. dashboard, metoda all())
// /app/controllers/items_controller.php
class ItemsController extends AppController {
//...
function all() {}
}
// /app/views/items/all.ctp
<?php echo $this->requestAction(
array(
"controller"=> "items",
"action"=> "index"),
array("return")
); ?>
<?php echo $this->requestAction(
array(
"controller"=> "things",
"action"=> "index"),
array("return")
); ?>
Na razie nie przejmujcie się kwestią requestAction i wydajności. Zazwyczaj efekty requestAction należy umieszczać w cache’owanych elementach, ale w naszym przypadku już niedługo zrezygnujemy z samego requestAction.
Zróbmy z tego widoku zakładki modyfikując widok all.ctp w następujący sposób:
<script type="text/javascript">
$(document).ready(function() {
$("#tabs").tabs();
});
</script>
<div id="tabs">
<ul>
<li><a href="#tabs-1">Items</a></li>
<li><a href="#tabs-2">Things</a></li>
</ul>
<div id="tabs-1">
<?php echo $this->requestAction(
array(
"controller"=> "items",
"action"=> "index"),
array("return")
); ?>
</div>
<div id="tabs-2">
<?php echo $this->requestAction(
array(
"controller"=> "things",
"action"=> "index"),
array("return")
); ?>
</div>
</div>
Żeby wypieczone widoki były “otoczone” ramką z zakładki – trzeba w nich dodać taki fragment na samym końcu plików index.ctp:
<div style="float: none; clear: both;"> </div>
Dodajmy więcej elementów, żeby zobaczyć jak zachowuje się pagination…
insert into items(`name`) values ('Item 4'), ('Item 5'), ('Item 6'), ('Item 7')
i zmieńmy parametry stronicowania, żeby efekty stronicowania zobaczyć szybko
class ItemsController extends AppController {
//...
var $paginate = array(
'limit'=> 5
);
//...
Widzimy, że w naszym zbiorczym widoku link ciągle prowadzi do items/index. Wolelibyśmy, żeby stronicowanie odbyło się w zakładkach.
Można oczywiście próbować rozwiązania via php. Oprogramować obsługę parametrów w metodzie all(), zmienić cel linków stronicowania i sortowania w widokach. I ogólnie dużo napracować się nad tym, żeby nasz kod wyglądał źle
. Już za chwilę moc javascript i w szczególności jQuery przyjdzie nam z pomocą. Zanim to nastąpi – zamieńmy nasze zakładki na Ajaxowe zmieniając widok all.ctp:
// views/items/all.ctp
<script type="text/javascript">
$(document).ready(function() {
$("#tabs").tabs();
});
</script>
<div id="tabs">
<ul>
<li>
<?php echo $html->link(
"Items",
array(
"controller"=> "items",
"action"=> "index"
)
);
?>
</li>
<li>
<?php echo $html->link(
"Things",
array(
"controller"=> "things",
"action"=> "index"
)
);
?>
</li>
</ul>
</div>
…prościzna, prawda? Kwestię requestAction też już mamy z głowy. Jednak pojawił się problem – wnętrze zakładek pojawiło się w powtórzonym layoucie.
Szybko naprawimy to w app_controller.php przy pomocy RequestHanlder:
// /app/app_controller.php
var $components = array("RequestHandler");
function beforeFilter(){
if($this->RequestHandler->isAjax()){
$this->layout = "ajax";
}
}
Wrócmy do kwestii stronicowania (i sortowania przy okazji). Chcemy, aby kolejne strony ładowały się w danej zakładce. Wykorzystajmy do tego potęgę jQuery.
Dodajemy taki prosty skrypt
// views/items/all.ctp
$('#tabs a[href*=/sort:],#tabs a[href*=/page:]').live('click', function(){
$('#tabs>div:visible').load($(this).attr('href'));
return false;
});
Jeśli interesuje Cię co się w tym fragmencie dzieje, zapraszam na do innego wpisu. W dużym skrócie: łapiemy linki, które mają w adresie “sort:” i “page:” i zmuszamy je aby przeładowały zawartość zakładki, zamiast całej strony.
Kolejną kwestią jest usuwanie elementów. Jeśli nie używasz na co dzień selektorów jQuery – może Cię kusić, aby linkom odpowiedzialnym za usuwanie elementów dodawać atrybut onclick, a w nim wywołanie funkcji, która sprawi, że całość odbędzie się ajaxem. Nie jest to złe podejście, ale jeśli trochę poznasz selektory jQuery – będziesz wiedział, że to rozwiązanie nie jest sexy
Zróbmy to od razu porządnie, przy pomocy tylko kilku linii kodu.
W widokach index.ctp (dla items i things) znajdź fragment odpowiadający za linki. Dodajmy dla linku “delete” klasę “tab-update”,
dzięku temu łatwo je złapiemy w przeglądarce:
<td class="actions">
<?php echo $this->Html->link(__('View', true), array('action' => 'view', $item['Item']['id'])); ?>
<?php echo $this->Html->link(__('Edit', true), array('action' => 'edit', $item['Item']['id'])); ?>
<?php echo $this->Html->link(
__('Delete', true),
array('action' => 'delete', $item['Item']['id']),
array("class"=> "tab-update"),
sprintf(__('Are you sure you want to delete # %s?', true),
$item['Item']['id'])
); ?>
</td>
Metoda tutaj zastosowana nie będzie współgrała z confirm message cake’a (czwarty parametr metody link()), więc proponuję to na razie usunąć, bo chwilowo nie mam pomysłu jak to naprawić (spodziewam się w finalnej wersji 1.3 rozwiązania tej kwestii, więc pozwalam sobie teraz nie zaprzątać nią głowy).
Używamy podobnego triku jak przy pagination, a właściwie dopisujemy jedynie nowy selektor “.tab-update” do funkjcji, którą już mamy:
jQuery('#tabs a[href*=/sort:],#tabs a[href*=/page:],.tab-update').live('click', function(){
jQuery('#tabs>div:visible').load(jQuery(this).attr('href'));
return false;
});
Jednak pojawia się problem z redirectami. To dlatego, że dispatcher nie wie jaki to controller, żeby to naprawić wystarczy w metodzie delete zamienić
// /app/controllers/items_controller.php
$this->redirect(array('action'=>'index'));
na
// /app/controllers/items_controller.php
$this->redirect(array("controller"=> "items", 'action' => 'index'));
jeśli chcesz mieć bardziej estetyczny kod, to zamiast explicite podawać “items” możesz zmienić na $this->name,
Uważny obserwator (czyli Twój klient) zauważy, że już nie widać informacji, że element został usunięty. Zostawmy sobie tą drobnostkę na koniec.
Teraz warto by poprawić działanie przycisku “Add Item”. Teraz przenosi nas do nowego widoku, a potem do /items/index. W przypadku pierwszej zakładki nie ma problemu – wystarczyłoby zmienić przekierowanie na adres /items/all. Ale w kwestii kolejnych zakładek – natkniemy się na problem włączenia odpowiedniej zakładki. Dlatego tu również pomocny będzie Ajax. Do tego wykorzystamy fajny plugin do jQuery – Boxy.
Myślę, że teraz jest najwyższa pora aby pozbyć się powtórzeń w widokach add i edit. Szkoda życia na edycję dwóch niemal identycznych widoków. Aby to zrobić – rzuć okiem tutaj. W dalszej części kod będzie po przeprowadzeniu takiego refaktoringu.
Od razu zrobimy to tak aby wszystkie linki, które chcemy, wywoływały się ajaxem w oknie dialogowym. Dodaj klasę “display-in-box” dla linków edit i add (w widokach index.ctp items i things), a następnie w widoku /items/all dodaj taki kod:
// /app/views/items/all.ctp
$("a.display-in-box").live(
"click",
function(){
Boxy.load(
this.href,
{
title: (this.title)? this.title: this.href,
modal: true,
}
);
return false;
}
);
Przypisze on wszystkim linkom klast “display-in-box” zachowanie polegające na tym, że po kliknięciu otworzy okno dialogowe Boxy, załaduje do niego ajaxem zawartość z pod adresu na który ten link wskazywał (href), ustawi tytuł boxu taki jak title linku (lub w wypadku braku – wywołany adres) oraz zapobiegnie przeładowaniu strony po kliknięciu linku.
Wygląda to teraz ładnie, działa poprawnie, ale po dodaniu, lub edycji elementu lądujemy nie tam gdzie się spodziewamy.
Pierwszym problemem jest to, że kliknięcie submit skutkuje przeładowaniem strony (i w następstwie redirectem nie tam gdzie chcemy). Przekonajmy formularz, żeby wysłał się również ajaxem
jQuery("body").ajaxComplete(
function( event, xhr, options) {
if(Boxy.isModalVisible()){
$(".boxy-wrapper form").live(
"submit",
function() {
var form = this;
jQuery.ajax({
type: "post",
url: this.action,
data: jQuery(this).serialize(),
success: function(data, status, xhr) {
if(data != ":ajaxRedirect:") {
Boxy.get(form).setContent(data);
}else{
Boxy.get(form).hideAndUnload();
}
},
});
return false;
}
);
}
}
);
Ten fragment wymaga konkretnego wyjasnienia. W linii 1 podpinamy naszą funkcję (linia 2) pod zdarzenie ajaxComplete, a to znaczy, że przy każdym odpaleniu jakiegokolwiek ajaxa na naszej stronie przez jQuery zostanie ona uruchomiona (z odpowiednimi parametrami jak w linni 2).
W linni 3 sprawdzamy czy istnieje jakiekolwiek okno dialogowe otwarte na tej stronie (modal to typ okna, które pojawia się z takim szarym tłem przykrywającym resztę strony). Jeśli tak, to w lini 4 łapiemy wszystkie (w naszym wypadku zawsze jeden) formularze, które znajdują się w oknie dialogowym i podpinamy się w nich pod zdarzenie “submit” (4i5). Teraz przy każdej próbie wysłania naszego formularza odpali się nasza funkcja (linia 6).
Przy pomocy jQuery odpalamy wywołanie ajaxowe, przekazując odpowiednie parametry:
sposób przesłania danych (8), endpoint wywołania (9), dane do wysłania (10) (zostawiamy brudna robotę jQuery), a następnie definiujemy zachowanie w przypadku, gdy wywołanie ajaxa się powiedzie (11).
Zanim opiszę działanie następnego bloku – trochę teorii. Wywołanie ajaxa się powiedzie zawsze, kiedy uda sie wysłać zapytanie pod wskazany adres, a w odpowiedzi dostaniemy jakiś ciąg tekstowy. Metody zapisywania elementów w cakePHP korzystają z mechanizmów przekierowań, więc niezaleźnie od tego, czy nasz zapis zakończy się poprawnie, czy wyskoczą błędy walidacji – zapytanie z punktu widzenia Javascript zakończy się sukcesem. Dlatego należy zastosować mały hack, żeby przesłać klientowi informacje “Zapis się udał, zamknij okno”. Proponuję podejście nadpisania działania funkcji redirect w AppController:
// /app/app_controller.php
function redirect($url, $status = null, $exit = true) {
if(!$this->RequestHandler->isAjax()){
parent::redirect($url, $status, $exit);
}else{
exit(":ajaxRedirect:");
}
}
która w przypadku zwykłych wywołań działa normalnie, a przypadku wywołań ajaxowych – zwraca ustalony ciąg znaków, będący umówionym hasłem
Gdy już mamy ten fragment łatwiej zrozumieć co się dzieje w liniach 12-16. Gdy zwrócone dane nie są umówionym hasłem – ładujemy je do naszego otwartego okna dialogowego (13) (zobacz to w działaniu dodając walidację do modelu i wysyłając dane nie spełniające wymagań walidacji), a gdy hasło się zgadza – wiemy, że zapis się powiódł i zamykamy okno (15).
Linia 19 zabezpiecza nas przed wysłaniem formularza ponownie przez przeglądarkę.
Niestety, z powodu nowej metody AppController::redirect – usuwanie elementów się popsuło:
zajmiemy się tym później. Teraz mamy na tapecie inną kwestię: po prawidłowym zapisaniu elementu nie widzimy żadnego efektu na stronie.
Odnajdźmy fragment, w którym podpięliśmy okno dialogowe pod linki klasy “display-in-box”. Na początku naszej funkcji “złapmy” element klasy .tab-container (element, w kŧórym są zakładki) w którym został kliknięty link, który otworzył okno dialogowe, a następnie sprawmy, żeby po zamknięciu tegoż okna została odświeżona aktualnie aktywna (wybrana) zakładka w złapanym tab-containerze. Cała funkcja będzie wyglądać tak:
$("a.display-in-box").live(
"click",
function(){
var tabContainer = $(this).parents(".ui-tabs");
Boxy.load(
this.href,
{
title: (this.title)? this.title: this.href,
modal: true,
fixed: false,
afterHide: function() {
tabContainer.tabs(
"load",
tabContainer.tabs("option", "selected")
);
}
}
);
return false;
}
);
Naprawmy szybko popsute usuwanie. Znajdźmy teraz fragment, w którym podpięliśmy ajaxa pod linki “delete” (czyli te klasy “tab-update”). I rozbudujmy wywołanie ajaxowe, na identycznej zasadzie jak przy okazji ładowania do boxy formularza z błędami walidacji.
Żeby coś z tym zrobić, musimy przy obsłudze linków sortowania i stronicowania w zakładkach trochę pogrzebać
jQuery('#tabs a[href*=/sort:],#tabs a[href*=/page:],.tab-update').live('click', function(){
jQuery.ajax({
url: jQuery(this).attr('href'),
success: function(response, status, xhr){
if(response != ":ajaxRedirect"){
jQuery('#tabs>div:visible').html(response);
}
}
});
return false;
});
A teraz po prostu w wypadku otrzymania “:ajaxRedirect:” – odświeżmy zawartość zakładki, zamiast ładować response, dopisując blok else:
jQuery('#tabs a[href*=/sort:],#tabs a[href*=/page:],.tab-update').live('click', function(){
jQuery.ajax({
url: jQuery(this).attr('href'),
success: function(response, status, xhr){
if(response != ":ajaxRedirect:"){
jQuery('#tabs>div:visible').html(response);
}else {
$("#tabs").tabs(
"load",
$("#tabs").tabs("option", "selected")
);
}
}
});
return false;
});
Kolejnymi elementami są flashMessage. Nie działają teraz dobrze – po pierwsze nie dostajemy odpowiedniego feedbacku. Po drugie, po usunięcie elementu i kliknięciu “view” przy innym – w widoku szczegółów widzimy “Thing deleted” – spóźniony feedback jest gorszy niż żaden, bo wprowadza zamieszanie. Najprosztszym sposobem, aby to poprawić jest skopiowanie layoutu ajax:
/cake/libs/views/layouts/ajax.ctp do /app/views/layouts/ajax.ctp i dodanie kodu odpowiadającego za wyświetlenie wiadomości:
<?php echo $this->Session->flash(); ?> <?php echo $content_for_layout; ?>
Jako wisienkę na torcie można dodać ficzer, żeby flash zniknął po chwili – zachęcam do własnych ekpserymentów, dzielenia się tym na swoich blogach linkując do tego tutoriala, oraz do komentarzy.
Jeśli uważasz, że autor tego wpisu odwalił niezłą robotę i czegoś Cię nauczył, albo jeśli uważasz zawartość tego bloga za interesującą nie wahaj się subskrybowac kanał rss, dodać jego adres w sygnaturkach swoich profili, dodać link do niego na własnym blogu i napisać co nieco od siebie w komentarzach pod postem.
Zcalenie edit() i add() w kontrolerze
Gdy przyjrzysz się bliżej kontrolerom stworzonym przy pomocy narzędzia ‘bake’ (lub takim, jakie powstają po wykonaniu tutoriala) możesz stwierdzić, że łamią one koncepcję DRY. Jesteś w stanie powiedzieć, które z dwóch metod są niemal identyczne? Jeśli nie – sprawdź, które dwa widoki są niemal identyczne. Ok, może spaliłem swoją zagadkę, bo w tytule tego postu jest wyraźna sugestia o czym będę mówił. Zgadza się.
Najpierw rzućmy okiem na widoki (tu w przykładzie dla Modelu Thing, który ma jedno pole – name):
// app/views/things/add.ctp
<div class="things form">
<?php echo $this->Form->create('Thing');?>
<fieldset>
<legend>
<?php printf(__('Add %s', true), __('Thing', true)); ?>
</legend>
<?php
echo $this->Form->input('name');
?>
</fieldset>
<?php echo $this->Form->end(__('Submit', true));?>
</div>
<div class="actions">
<h3><?php __('Actions'); ?></h3>
<ul>
<li>
<?php echo $this->Html->link(
sprintf(
__('List %s', true),
__('Things', true)
),
array('action' => 'index')
);?>
</li>
</ul>
</div>
// app/views/things/edit.ctp
<div class="things form">
<?php echo $this->Form->create('Thing');?>
<fieldset>
<legend>
<?php printf(__('Edit %s', true), __('Thing', true)); ?>
</legend>
<?php
echo $this->Form->input('id');
echo $this->Form->input('name');
?>
</fieldset>
<?php echo $this->Form->end(__('Submit', true));?>
</div>
<div class="actions">
<h3><?php __('Actions'); ?></h3>
<ul>
<li>
<?php echo $this->Html->link(
__('Delete', true),
array('action' => 'delete', $this->Form->value('Thing.id')),
null,
sprintf(
__('Are you sure you want to delete # %s?', true),
$this->Form->value('Thing.id')
)
); ?>
</li>
<li>
<?php echo $this->Html->link(
sprintf(__('List %s', true), __('Things', true)),
array('action' => 'index')
);?>
</li>
</ul>
</div>
Różnią się dokładnie w trzech linijkach (na 16 łącznie). Jeśli zmienisz coś w jednym (na przykład dodasz pole) to będziesz musiał poprawić formularz w dwóch plikach. Czysta głupota.
Przyjrzyjmy się jeszcze metodom:
// app/controllers/things_controller.ctp
function add() {
if (!empty($this->data)) {
$this->Thing->create();
if ($this->Thing->save($this->data)) {
$this->Session->setFlash(__('The thing has been saved', true));
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash(
__('The thing could not be saved. Please, try again.', true)
);
}
}
}
function edit($id = null) {
if (!$id && empty($this->data)) {
$this->Session->setFlash(__('Invalid thing', true));
$this->redirect(array('action' => 'index'));
}
if (!empty($this->data)) {
if ($this->Thing->save($this->data)) {
$this->Session->setFlash(__('The thing has been saved', true));
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash(
__('The thing could not be saved. Please, try again.', true)
);
}
}
if (empty($this->data)) {
$this->data = $this->Thing->read(null, $id);
}
}
Praktycznie całe ciało metody add() można by wkleić w miejsce drugiego if’a w metodzie edit() i niewiele by się zmieniło, prawda?
Kilka tricków pozwoli nam naprawić to marnotrawstwo. W zasadzie najważniejsza kwestia jest następująca – jak działa metoda $this->Thing->create()? Wymusi dodanie nowego elementu (dlatego, że metoda save() działa jako create lub update w zależności od stanu modelu). Ale nie przekazując żadnych parametrów do tej metody tak naprawdę sprowadzi się do wyczyszczenia $this->data i $this->id (this oznacza w tym przypadku model). W przypadku metody add() nie ma możliwości, żeby jakieś śmieci były w $this->Thing->data, więc możemy spokojnie usunąć tą linijkę – dalej będzie działać tak samo (czy na pewno? – spoiler).
Teraz całkowicie ciało metody add() mogłoby zawrzeć się w metodzie edit(). Dlatego trzeba ją usunąć (razem z jej widokiem). A wszystkie linki, które prowadziły do /things/add niech teraz prowadzą do /things/edit.
Warto sprawdzić, co się teraz dzieje?
W zależności od wersji – 1.2 się wywali, 1.3 pokaże informacje “Invalid thing”. Teraz skupię się na wersji 1.3 – bo na niej sprawdzam na bieżąco efekty pisząc ten wpis. (dla 1.2 jeśli dobrze pamiętam wystarczy wstępnie zmienić sygnaturę metody edit($id) na edit($id=null), dalsze kroki będą analogiczne, choć kod w szczegółach może się różnić)
Od teraz możemy założyć, że jeśli w url’u nie ma podanego id – to znaczy, że chcemy dodać nowy element, zatem tego if’a możemy usunąć:
// app/controllers/things_controller.php
if (!$id && empty($this->data)) {
$this->Session->setFlash(__('Invalid thing', true));
$this->redirect(array('action' => 'index'));
}
Możemy spokojnie teraz dodawać nowe elementy przez formularz- wszystko działa.
Warto jeszcze dopieścić widok – bez sensu jest nagłówek “edit” przy dodawaniu elementu, oraz przycisk “delete”,
fragment z legend poprawiamy na taki (dodałem wcięcia dla przejrzystości):
// app/views/things/edit.ctp
<legend>
<?php printf(
(!is_null($this->data["Thing"]["id"])?__('Edit %s', true): __('Add %s', true)),
__('Thing', true));
?>
</legend>
a pierwszy element li otaczamy if’ami tak:
// app/views/things/edit.ctp
<?php if(!is_null($this->data["Thing"]["id"])): ?>
<li>
<?php echo $this->Html->link(
__('Delete', true),
array('action' => 'delete', $this->Form->value('Thing.id')),
null,
sprintf(
__('Are you sure you want to delete # %s?', true),
$this->Form->value('Thing.id')
)
); ?>
</li>
<?php endif; ?>
I tyle.
Usunięcie $this->Thing->create() nie takie bezpieczne?
Napisałem, że po usunięciu tego elementu nic się złego nie stanie. Oczywiście jest to prawdą pod warunkiem, że wykonasz wszystkie czynności opisane w tym poscie. Jeśli tylko usuniesz $this->Thing->create() narażasz się na niebezpieczeństwo, że ktoś wyśle spreparowanego post’a pod ten adres. Jeśli poda w nim pole o nazwie data[Thing][id] – będzie mógł zmienić dane dla elementu o danym id. Jeśli masz sytuację taką, że jedna grupa może dodawać elementy, a inna je edytować i polegasz tylko na acl’ach (dana grupa ma dostęp do edit(), a kolejna do add()) – to w ten sposób możesz stworzyć lukę. Jednak ta sytuacja jest dość rzadka i to jest raczej temat na inny wpis, więc jeśli interesuje Cię rowiązanie tego problemu – zachęcam do komentowania.
Skróć swoje listingi
Pewnie w niemal każdym swoim widoku index.ctp masz fragment kodu podobny do tego:
<?php
$i = 0;
foreach ($tracks as $track):
$class = null;
if ($i++ % 2 == 0) {
$class = ' class="altrow"';
}
?>
<tr<?php echo $class;?>>
<td>
<?php echo $track['Track']['id']; ?>
</td>
<td>
<?php echo $track['Track']['name']; ?>
</td>
<td>
<?php echo $track['Car']['name']; ?>
</td>
<td>
<?php echo $track['Track']['begin']; ?>
</td>
<td>
<?php echo $track['Track']['end']; ?>
</td>
<td>
<?php echo $track['Track']['length']; ?>
</td>
<td class="actions">
<?php echo $html->link(
__('Edycja', true),
array(
'action' => 'edit',
$track['Track']['id'] ,
$track['Car']['id']
)
);
?>
</td>
</tr>
<?php endforeach; ?>
</table>
Chce Ci się pisać ten kod kilkadziesiąt razy w jednym projekcie? Mnie też nie. Dlatego napisałem prosty element, który to trochę automatyzuje (nazwałem go “list”):
// app/views/elements/list.ctp
<?php
$i = 0;
foreach($elements as $key => $element):
$class = null;
if ($i++ % 2 == 0) {
$class = ' class="altrow"';
}
?>
<tr <?php echo $class?>>
<?php foreach($fields as $path): ?>
<td>
<?php echo Set::classicExtract($element, $path) ?>
</td>
<?php endforeach; ?>
<?php if(isset($links)): ?>
<td>
<?php foreach($links as $name => $url): ?>
<?php
if(!is_array($url["params"])) {
$url["params"] = array($url["params"]);
}
foreach($url["params"] as $key => $path){
$url[$key] = Set::classicExtract(
$element,
$path
);
}
unset($url["params"]);
?>
<?php echo $html->link($name, $url)?>
<?php endforeach; ?>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
Jego użycie jest dość proste. Dla przykładu wygenerujemy ten sam “output” dla pierwszego listingu z tego postu:
echo $this->renderElement(
"list",
array(
"elements" => $tracks,
"fields" => array(
"Track.name", "Car.name", "Track.begin",
"Track.end", "Track.length"
),
"links"=> array(
"edycja" => array(
"controller"=> "tracks", "action"=> "edit",
"params" => array(
"Track.id", "Car.id"
)
)
)
)
)
Opiszę poszczególne parametry przekazywane do tego elementu:
- “elements” – kolekcja (w tym wypadku tablica) elementów zwrócona na przykład przez $this->Track->find(“all”)
- “fields” – pola, które mają się pojawić w komórkach tabeli
- “links” – tu definiujemy jakie linki mają się pojawić w ostatniej kolumnie (zazwyczaj tam znajdują się “opcje”). Indeksy kolejnych elementów to nazwy linków (tutaj “edycja”).
- “controller” i “action” są identycznie używane jak w metodzie HtmlHelper::link()
- “params” jest dość ciekawy. Jeśli chcemy w wygenerowanym linku mieć jeden parametr (najczęściej id), robimy to tak:
"edycja" => array( "controller"=> "cars", "action"=> "edit", "params"=>"Car.id" ),gdy parametrów ma być więcej – ścieżki do nich przekazujemy w tablicy (tak jak w przykładzie):
"edycja" => array( "controller"=> "tracks", "action"=> "edit", "params" => array( "Track.id", "Car.id" ) )Można także używać parametrów typu “named”, wystarczy podać dodatkowo indeksy:
"edycja" => array( "controller"=> "tracks", "action"=> "edit", "params" => array( "parametr_1"=> "Track.id", "parametr_2"=> "Car.id" ) )
Zachęcam do korzystania z tego elementu i zgłaszania wszelkich niedociągnięć






