Uwaga, blog przeniesiony

Posty na tym blogu już nie będą się pojawiać. Zapraszam gorąco pod nowy adres: blog.grzegorzpawlik.com



Subskrybuj ten blog...

Pokazywanie postów oznaczonych etykietą DRY. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą DRY. Pokaż wszystkie posty

środa, 25 marca 2009

Zarządzanie wersjami STRUKTURY bazy danych w cakePHP 1.2

W poprzednich postach (m.in. zarządzanie wersjami oprogramowania) udało mi się nakreślić problem przy zarządzaniu oprogramowaniem pojawiający się na styku kod-baza danych. Nawet mogę powiedzieć, że mały sukces na tym polu odnotowałem przy pomocy ImageBehavior, jednak jeśli chodzi o strukturę - ciągle zmagałem się do tej pory z przeciwnościami.

Jednak okazuje się, że cake w nowym wydaniu wychodzi nam na przeciw razem z klasą Schema, oraz z narzędziem konsolowym ./cake schema ... po krótce opowiem o co chodzi.

Zabawę z tym narzędziem najlepiej zacząć mając już jakiś zalążek aplikacji (tabele + modele). Jeśli sprawiamy ten podstawowy warunek możemy wpisać w konsoli ./cake schema generate ... ot tak, dla jaj.

Następnie możemy się w katalogu app/config/sql/ namierzyć plik schema.php. To właśnie artefakt wygenerowany przez nas przed sekundą. Można w celach samorozwojowych zajrzeć do środka...

Jednak ciekawe rzeczy dzieją się, kiedy ponownie wywołamy to samo polecenie: otóż cake rezolutnie zauważy, że plik schema.php już istnieje i zapyta nas co dalej. Polecam wybór opcji [S]napshot i ponowny rzut oka do wspomnianego wyżej katalogu. Co widzimy? Dokładnie! Nowy plik o nazwie schema_2.php :D Zachęcam do zapoznania się z helpem (./cake schema help).

Wystarczy, że teraz przekonam zespół, aby w sytuacji, gdy nastąpiły zmiany w bazie, przed commitem wywołali to polecenie. Jest jeden problem, którego ewentualnie można się spodziewać - sporadycznych konfliktów. To znaczy sytuacji, w której dwóch programistów:
  1. ściąga repozytorium, 
  2. dokonuje (nawet różnych) zmian w bazie, 
  3. zatwierdza dane: 
    1. schema generate, 
    2. svn add schema_X.php, 
    3. svn commit
Problem w tym, że w takiej sytuacji w punkcie 3.3 jeden z nich dostanie informację

Nie mogę dodać schema_X.php do repozytorium, gdyż takowy  już w repozytorium istnieje.
Z poważaniem Twój
SVN
Nie jest to jakaś wielka tragedia, jak przy każdym konflikcie trzeba będzie go rozwiązać (w tym wypadku przy spotkaniu tych dwóch programistów). Jednak myślę, że takie sytuacje można by zlikwidować wywołując tą sekwencję w jednym ciągu (nie np. commit po dwóch godzinach od schema generate), może nawet napisać prosty skrypt, który załatwi to za nas (taki svncommitwithcakeschemagenerate.sh ;))

wtorek, 3 marca 2009

ImageBehavior - uploaduj pliki prosto do bazy

Jakiś czas temu napisałem o pomyśle cake'owego Behavior (http://webbricks.blogspot.com/2009/02/pliki-w-formie-binarnej-w-bazie.html). Poniżej prezentuję pierwsze podejście do problemu:

<?php
/**
 * ImageBehavior - take best from database blobs adn file image storage

 * requires 'content' field that is a blob (mediumblob or longblob), and
 * 'ext' varchar(10) field  and

 * 'modified' datetime field
 * @author Grzegorz Pawlik
 * @version 1.0
 */
class ImageBehavior extends ModelBehavior {

  
   
/**
    * directory in which cached files will be stored
    *
    * @var string
    */
   
var $cacheSubdir 'filecache';

   
/**
    * if set to false - never check if cached file is present (nor actual)
    *

    * @var bool
    */
   
var $usecache true;
  
   function 
setup(&$Model) {
      
// no setup at this time

   
}
  
   
/**
    * Insert proper blob when standard data after upload is present
    *
    * @param object $Model

    * @return bool true
    */
   
function beforeSave(&$Model) {

      if(isset(
$Model->data[$Model->name]['file']['tmp_name']) && is_uploaded_file($Model->data[$Model->name]['file']['tmp_name'])) {

      
// podnieś wyżej parametry
      
$Model->data[$Model->name] = array_merge($Model->data[$Model->name],  $Model->data[$Model->name]['file']);

      
// przygotuj blob
      
$this->_prepareBlob($Model);
     
      
$this->_getExt($Model);
      }

     
      return 
true;
   }
  
   
/**
    * prepares blob contents
    *
    * @param object $Model

    */
   
function _prepareBlob(&$Model) {
      
App::import('Core''File');
      
$file = new File($Model->data['Medium']['tmp_name'], false);

      
$content $this->addSlashes$file->read() );
      
$Model->data[$Model->name]['content'] = $content;

   }
  
   
/**
    * Get uploaded file extension
    *
    * @param object $Model
    */
   
function _getExt(&$Model) {

      
$file explode('.'$Model->data['Medium']['name']);
      
$ext array_pop($file);

      
$Model->data[$Model->name]['ext'] = $ext;
   }
  
   
/**
    * replace blob contents with file path

    * After reading database checks if cached file is present. If not creates it (from blob contents) and

    * returns a 'file' field with path relative to /app/webroot/img
    *
    *
    * @param object $model

    * @param array $results
    * @param unknown_type $primary
    * @return unknown
    */
   
function afterFind(&$model$results$primary) {

      foreach(
$results as $key => $val) {
        
        
        
         
$relpath $this->cacheSubdir DS .

                 
$val[$model->name]['id'] . '_' $model->name '_' .

                 
$val[$model->name]['modified'] . '.' $val[$model->name]['ext'];

         
$relpath str_replace( array(' '':') , '_'$relpath);
        
         
$fullpath IMAGES $relpath;

        
         if(!
file_exists($fullpath) || !$this->usecache ) {
            
file_put_contents($fullpath$this->stripSlashes($results[$key][$model->name]['content']));

         }
        
         
$results[$key][$model->name]['file'] = $relpath;
         
// remove blob from results (its messy when You want to output results in debug)

         
unset($results[$key][$model->name]['content']);
      }
      return 
$results;
   }
  

   
/**
    * add slashes (just wrapper)
    *
    * @param string $string
    * @return string with slashes
    */

   
function addSlashes($string) {
      return 
addslashes($string);
   }
  
   
/**
    * strip slashes (just wrapper)

    *
    * @param string $string
    * @return string without slashes
    */
   
function stripSlashes($string) {

      return 
stripslashes($string);
   }
}
?>


Zasada działania jest dość prosta. Wyjaśnię ją na przykładzie.
Tabela media:


CREATE TABLE IF NOT EXISTS `media` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(50) NOT NULL,

  `ext` varchar(10) NOT NULL,
  `content` longblob NOT NULL,
  `size` int(11) NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,

  `type` varchar(20) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM;


Model:
<?php
class Medium extends AppModel {

    var 
$name 'Medium';
   var 
$actsAs = array('Image');

}

?>


Kontroler:
<?php

class MediaController extends AppController {

    var 
$name 'Media';
    var 
$helpers = array('Html''Form');

   
    function 
index() {
     
      
$this->set('media'$this->Medium->findAll());

     
    }
   
    function 
add() {
       if(!empty(
$this->data)) {
          
$this->Medium->save($this->data);

       }
    }
   
}

?>

Przy uploadzie ImageBehavior oczekuje, że plik będzie przekazany w poly ModelName.file (tutaj Media.file).

add.ctp:
span style="color: rgb(0, 0, 187);"><?php
   echo $form->create(
      array(
'url' => array(
                           
'controller' => 'media',

                           
'action'    => 'add'
                     
),
            
'enctype' => 'multipart/form-data'
            
)
   );
?>

<?php echo $form->file('Medium.file'); ?>


<?php echo $form->end('submit'); ?>



Przy odczycie dzieje się to co mnie najbardziej interesowało. Zamiast dostać zawartość (BLOB) pliku, dostajemy w polu file ścieżkę (relatywną do app/webroot/img). Domyślne ustawienia wymagają, żeby był tam katalog filecache (z możliwością zapisu). Przy operacji read behavior sprawdzi, czy istnieje aktualy plik w filecache, i jesli nie - utworzy go.

index.ctp:
<?php foreach($media as $medium): ?>
   <?php echo $html->image($medium['Medium']['file']); ?>

<?php 
endforeach; ?>


To rozwiązanie ma przynajmniej dwa zauważalne braki:
  1. Gdy dodamy taki plik do treści np. postu, po jego updacie - nie będą widoczne zmiany
  2. Dobrze byłoby, gdyby przy operacji read nie zwracał zawartości BLOB (ważne, gdy baza jest gdzieś dalej), ale odpytywał tylko wtedy, gdy jest potrzebne zaktualizowanie zawartości pliku w filecache.
To rzeczy do zrobienia w kolejnym podejściu ;)

poniedziałek, 6 października 2008

Nam obiektowo pisać (nie) kazano...

... czyli co się dzieje, kiedy ktoś kto nie wie dlaczego się używa kodu obiektowego, pisze takowy bo musi.
Jest sobie jedna, bardzo wazna metoda w bardzo ważnej klasie, w bardzo ważnym projekcie. Ciągnie się od linii 484 do majaczącej na horyzoncie (i zdecydowanie poza ekranem) linii 745.
Jest w niej o wiele za dużo. Sprawdza czy dane są poprwane, zlicza, podlicza, zapisuje do bazy - sprawdza czy dobrze zapisało, w razie czego robi rollback. Do tego na koniec wysyła maila.
No i bach! Okazało się, że w mailu trzeba wysłać jedną dodatkową informację i jak jej teraz szukać. Tak bardzo chciałbym mieć tą funkcję zrefaktorowaną. Nie mieć zagnieżdżeń, tylko ciąg wywołań prywatnych metod:
$this->zrobTo();
$this->zrobTamto();
$foo = $this->znajdzToITamto($bar);
...
$this->wyslijMaila($dane, $temat, $inneDane);

I tylko w trzech kropkach dodać $dane = array_merge($dane, $foo), albo coś równie banalnego. Ale nie. Najpierw 5 minut na znalezienie gdzie ten cholerny mail jest wysyłany. 10 na zlokalizowanie tablicy z której dane trzeba dołączyć do maila. Potem z 20 minut na poprawki + sprawdzanie czy wsztstko jest tak jak powinno.
No i to nieodparte poczucie. Ten szept na ramieniu mówiący "Możesz być pewien, że w kodzie od teraz masz jedną dziurę więcej (buahahahaha)".

Ale patrzę na motto tego bloga i myślę sobie - trzeba sobie radzić ze swoim kodem...

czwartek, 19 czerwca 2008

Jak korzystać z requestAction w CakePHP

Ta bardzo przydatna funkcja nie jest zbyt dobrze opisana w dokumentacji, dlatego pozwolę sobie ją tu opisać.

Object::requestAction(string $url, array $extra);
Służy do łatwego wywoływania funkcji z jednego kontrolera w innym kontrolerze. Sprawa jest prosta, jeżeli wywoływana funkcja potrzebuje jedynie parametrów, które możemy przekazać w url-u (/posts/show/1).
Jednak sprawa komplikuje się, kiedy potrzebna metoda korzysta z danych przesłanych w formularzu. Komplikuje się o tyle, że w wyniku ubogiej dokumentacji, trudno odgadnąć czy jest to możliwe.
Jednak m.in. po to jest parametr $extra. Jeśli wasz kontroler czeka na dane z formularza ['User']['username'] i ['User']['password'], to można je "wcisnąć" do kontrolera poprzez requestAction w następujący sposób:

$data['data'][['User'] = array('username'=> 'ala', 'password' => 'makota');
$this->requestAction('/users/login', $data);

Dzięki tej sztuczce uda Ci się ominąć kilka sytuacji, w których wcześniej pisalibyście osobną funkcję.

Ps. Cake jest pełen takich nieudokumentowanych niespodzianek, które czekają na odkrycie - zachęcam do eksperymentów.
Pps. Szczególnie przydatność requestAction odczujesz, kiedy zaczniesz intensywniej korzystać z pluginów w CakePhp.

piątek, 13 czerwca 2008

Czy każdy może być programistą?

Właśnie teraz, w przerwie w pracy nad komponentem, który miałby automatycznie generować menu w panelu administracyjnym w zależności od zdefiniowanych (w bazie) uprawnień przypomniały mi się słowa jednego z wykładowców UO.
Próbował on odróżnić programistów od normalnych ludzi(sic!). Powiedział, że normalny człowiek, mając za zadanie wykopać 100 metrowy rów weźmie łopatę, tydzień będzie kopał i wykopie. Programista z kolei tydzień będzie myślał, aż wymyśli metodę. Drugi tydzień będzie tą metodę implementował. I po dwóch tygodniach rów będzie wykopany.
W tym momencie na sali zapanowała cisza, może nawet konsternacja, a po chwili doktor dodał "Ha! Ale my teraz takich rowów możemy wykopać 50!".

Kim zatem jest programista? Przede wszystkim jest leniem. Śpieszę dodać: leniem w dobrym tego słowa znaczeniu. Leniem, który nie znosi powtarzać zadania. Leń ów wie do czego służą maszyny, a w szczególności komputery. Jeśli jest coś co poddaje się algorytmizacji - leń-programista nie zrobi tego sam. Leń programista widzi, że wykopanie rowu to:
1. wbić łopatę
2. podnieść łopatę z ziemią
3. odrzucić ziemię w bok
4. sprawdzić, czy już 100 metrów
4a. jeśli nie - powtórz,
4b. jeśli tak - idź po wypłatę

Dlatego śmiem twierdzić, że im bardziej boi się pracy (w sensie "roboty") programista, tym lepiej to o nim świadczy. No i może to nieskromnie zabrzmi, ale drugi dzień pracuję nad tym dynamicznym menu, choć mógłbym w godzinę zrobić to "na sztywno"... i dobrze mi z tym (również dlatego, że przyjdzie następny projekt i nie będzie trzeba tego robić).

poniedziałek, 5 maja 2008

Odpowiednie narzędzia

W poprzednich postach( Czego tak na prawdę ode mnie chcesz [kliencie] ? i Funkcjonalność czy bajery?) napisałem o podstawowych niebezpieczeństwach jakie czyhają na nas w procesie tworzenia dedykowanego systemu dla klienta. W obu przypadkach w pewnym sensie ochroną przed tymi zagrożeniami będzie odpowiedni framework. Poznałem (lepiej lub gorzej) do tej pory trzy frameworki: CakePHP, ZendFramework i Django. Intensywnie pracuję z CakePHP i na nim oprę ten artykuł, pozostałe znam na tyle, żeby mieć pewność, że w podobny sposób pomagają w produkcji oprogramowania i żeby nie musieć opisywać każdego z osobna.

Głównym celem frameworku jest usprawnienie pracy programisty. Ma on dostarczać mechanizmy, które odciążają nas od żmudnego powtarzania nieciekawych czynności (łączenie z bazą, tworzenie milionowego formularza, pisanie kolejnego foreach żeby wydłubać coś z tablicy...). Nieciekawych oczywiście z punktu widzenia doświadczonego programisty, który robi to już długo - zaczynając przygodę z tą dziedziną wszystko jest ciekawe, ale ten stan po jakimś czasie mija.

Jeśli jesteś kompletnie zielony - odradzam zaczynanie przygody z programowaniem od poznania frameworku. Wydaje mi się, że postępując w ten sposób trudniej będzie Ci zrozumieć mechanizmy w nim zachodzące i zamiast mieć możliwość pełnego wykorzystania narzędzia, stanie się on dla Ciebie magiczną różdżką, której potencjału nie wykorzystasz. (oczywiście są wyjątki i możesz robić co chcesz).

Ja jednak nie będę skupiał się na dostarczanych przez framework bibliotekach, bo o tym już wiele napisano. Spróbuję za to napisać jak można go wykorzystać do przeciwdziałania trudnym tendencjom pojawiającym się w procesie produkcji i nie mających wiele wspólnego z samym programowaniem.

Na początek problem specyfikacji wymagań. Maksymalne starania Twoje i klienta, żeby dokładnie określić wymagania i tak nie zabezpieczą Cię w stu procentach. Zawsze po drodze okazuje się, że przydało by się coś innego. Coś mogło by działać lepiej i że pojawił się nowy pomysł. Tak jak z budowaniem samochodu - wkładając wysiłek w obliczenia będziesz pewny, że wycieraczki obejmą maksymalnie przednią szybę, ale dopiero jak się nim przejedziesz to stwierdzisz, że drążek zmiany biegów jest za krótki, a słupek boczny ogranicza Twoje pole widzenia. Gdybyś produkował samochody doświadczenie mógłbyś wykorzystać dopiero w następnym modelu. Ty jednak jesteś farciarzem jakich mało i produkujesz ciągi zer i jedynek, które możesz w każdej chwili zmienić/wyciąć/dodać! Mechanizm zwany w CakePHP Scaffold jest tym, co owo wycinanie, zmienianie i wklejanie czyni bezbolesnym w początkowej fazie produkcji.

W CakePHP scaffolding objawia się w dwóch formach. W pierwszej jest w stanie wygenerować fizycznie modele, kontrolery i widoki na podstawie struktury bazy danych (i podpowiedzi użytkownika). Jest to wykonywane przy pomocy skryptu bake.php znajdującego się w cake/libs/scripts (w wersji 1.19). Efektem tego jest powstanie wszystkich standardowych metod i widoków do operacji takich jak list/view/edit/add/delete. Z uwagi na to, że są one generowane fizycznie - możesz je zacząć zmieniać dostosowując je do swoich potrzeb. Jednak minusem jest to, że w razie zmiany struktury bazy trzeba albo na nowo wygenerować widoki i metody, albo edytować je ręcznie.
Drugą emanacją jest scaffold właściwy. Włącza się go w prosty sposób dodając w kontrolerze linię

var $scaffold;

(Tutaj uwaga - można włączyć scaffold 'globalnie' dodając tą zmienną w AppController, ale wtedy nie należy dodawać już jej w kontrolerach).
Efektem tego będzie każdorazowe generowanie metody i widoku dla standardowych operacji na podstawie struktury bazy danych (tej zdefiniowanej w modelach). Dziać się będzie to tak długo, aż nie usuniemy deklaracji zmiennej, albo nie zdefiniujemy w kontrolerze którejś z metod standardowych. W pierwszym przypadku scaffolding wyłączymy całkowicie dla danego kontrolera, w drugim nie będzie on działał dla metody, którą już zdefiniowaliśmy. Minusem jest to, że nie możemy dostosować widoku do naszych widzimisię (widok nie istnieje fizycznie na dysku)*, ale za to przy każdej zmianie struktury bazy danych widok jest uaktualniany bez naszej ingerencji (dodanie pola varchar zaowocuje pojawieniem się w formularzu pola input, text - textarea, enum - listy wyboru).
To rozwiązanie jest genialne w najwcześniejszej fazie produkcji oprogramowania. Wyobraź sobie, że po zaprojektowaniu samochodu od razu możesz się nim przejechać (fakt będzie brzydki, bez maski i tapicerki) i zobaczyć jak się nim jeździ i czy nie przydałby się większy bagażnik. Tak właśnie jest w tym wypadku: projektujesz bazę danych i już możesz pokazać klientowi podstawowe funkcjonalności**. Możecie z klientem UŻYWAĆ tej aplikacji i stwierdzić, że czegoś brakuje.
Oczywiście nie jest tak super cały czas - w pewnym momencie trzeba po prostu napisać trochę kodu, czasem aplikacji nie wpisują się tak łatwo w szablon listuj/wyświetl/dodaj/edytuj/usuń.
Jednak w 8 przypadkach na 10 sprawdza się doskonale. Dorzuć do tego dobrze zaprojektowaną, znormalizowaną bazę i początkowa faza, która obfituje w zmiany jest tak bezbolesna, jak to tylko możliwe.

* te pliki istnieją fizycznie na dysku, są cache'owane, ale nie nadaje się to do edycji (zostanie nadpisane prędzej, czy później). Do tego wydaje mi się, że istnieje możliwość edycji szablonu, który definiuje jak mają być tworzone widoki, jednak nie zajmowałem się tym do tej pory, więc nie będę o tym pisał.
** pod warunkiem, że nauczysz go patrzeć na system pod tym kątem. Jak klient chce tylko ładny interfejs i nie współpracuje to scaffold nie spełni się w 100%.

sobota, 22 marca 2008

"Sprytny" redirect (cakePHP 1.2)

Załóżmy, że mamy mały system bloga (posty + komentarze).
Cake upiekł za nas widoki i metody. No i powiedzmy, że teraz w post/view/[id_posta] chcesz umieścić możliwość dodania komentarza. Oczywiście tworzysz formularz, którego action='/comments/add' i w CommentsController::add() wpisujesz na sztywno (hard-coding) $this->redirect('/posts/view/'.$comment['post_id']).

Wszystko pięknie i ładnie działa, ale powiedzmy teraz chciałbyś w widoku posts/list_all, w którym wyświetlasz skrócone wersje wszystkich postów dać możliwość szybkiego dodania komentarza. No i znów pakujesz do środka formularz z action='/comments/add' i fajnie działa, ale redirect jest do jakiegoś jednego konkretnego posta, a nie listy w której szanowny user kliknął 'submit'. Niby nic złego się nie dzieje, ale szanowny user odczuwa dyskomfort, jest wręcz zagubiony.

Ostatnio wymyśliłem i z powodzeniem zastosowałem mały trik, który ten problem rozwiązuje w sposób uniwersalny i (w miarę) elegancki.
Zazwyczaj, jeśli tworzę formularz do innego kontrolera, to najczęściej wtedy wiem, gdzie po przerobieniu tych danych powinien się odbyć redirect... najczęściej z powrotem. Zatem preparuję parametr action w następujący sposób:

action=<?php echo $html->url('/comments/add') ?>?r=/posts/list_all/"

A w AppController
    function beforeRender(){        if(isset($this->params['url']['r'])){             $this->redirect($this->params['url']['r']);          }     }

Prosta metoda, która zawsze sprawdzi mi czy przekazałem informację o przekierowaniu i w razie czego ustawi odpowiedni adres przekierowania. Nie muszę pamiętać o tym w każdej metodzie, w której jest prawdopodobne, że będę chciał zrobić redirect w różne miejsca. Po prostu implementuję to na początku projektu i cieszę się kolejnymi zaoszczędzonymi minutami :D

Smacznych jajec ;)

środa, 5 marca 2008

Preload obrazków w cakePHP

Dla tych, dla których DRY nie jest tylko angielskim słowem napisanym z niewiadomych przyczyn wielkimi literami.
Usprawnienie preloadu obrazków w cakePHP może przydać się, gdy chcesz wyświetlić typową galerię: miniaturki + duży obraz zmieniający się po kliknięciu w miniaturkę. Kiedy zaimplementujesz preload obrazków, to znaczy, że jesteś świadomym budowniczym stron internetowych. Wiesz, że możesz wykorzystać przeglądarkę do przyśpieszenia działania Twojej strony, a nie tylko jej biernego wyświetlania. Zatem do dzieła!

Moje podejście obiera się na tzw. elemencie. Element to (wg. manuala CakePHP) "częściowy layout". Czyli cegiełki, z których możesz stworzyć swoje strony.

app/views/elements/image_preload.thtml:
<?php $images_count = count($images); $i = 0;?>  <script type="text/javascript"> //<![CDATA[  var _images = new Array();  <?php $i=0; foreach($images as $image): ?>    _images[<?= $i ?>] = new Image();   _images[<?= $i++ ?>].src = '<?= 'http://'.$_SERVER['SERVER_NAME'].$this->webroot.'img/'.$subdir.$image['filename'] ?>';     <?php endforeach; ?>       //]]> </script>
O co chodzi? Wpisz w google "javascript image preload" to się dowiesz :)
Jak z tego skorzystać? Już śpieszę z wyjaśnieniami:
W pliku Twojego layoutu, na samym końcu (przed </body> ofcoz) dodaj:
 <?php if(isset($images_to_preload) && isset($images_to_preload_subdir)): ?>   <?= $this->renderElement('image_preload', array('images'=>$images_to_preload, 'subdir'=>$images_to_preload_subdir)); ?>  <?php endif; ?>
A w metodzie odpowiedniego kontrolera wygeneruj tablicę z nazwami plików, które chcesz załadować do cache przeglądarki i przekaż do layoutu:
   $this->set('images_to_preload', $content['Photo']);    $this->set('images_to_preload_subdir', 'photos/'); 
U mnie akurat model Photo ma pole filename, więc tak napisałem sobie element. Do tego dodałem możliwość przekazania z kontrolera podkatalogu w którym są obrazki w /app/webroot/img (teraz już nie wiem dlaczego, ale jak to pisałem, na pewno było to uzasadnione ;))

Uwaga! blog przeniesiony

Posty na tym blogu już nie będą się pojawiać. Zapraszam gorąco pod nowy adres: blog.grzegorzpawlik.com
Komentowanie artykułów możliwe jest pod nowym adresem.