Problem został rozwiązany dzięki prostemu dziedziczeniu. W OPT istnieje metoda needCompile(), której zadaniem jest zwrócenie nazwy skompilowanego szablonu do wykonania. Po drodze sprawdza ona dodatkowo, czy wymaga on rekompilacji i jeśli tak, odpala kompilator. Wystarczyło ją nadpisać własnym kodem z własną wyszukiwarką plików. W poprzednich wersjach w oryginale była ona co prawda oznaczona jako prywatna, lecz w wersji 1.1.3 zostało to zmienione na protected.
Przyjrzyjmy się zatem, jak wygląda zmodyfikowana klasa.
<?php class sOptClass extends optClass { protected function needCompile($filename, $noException = false) { global $files; $filename = $files -> getPath('templates/'.$filename); $rootTime = @filemtime($filename); if($rootTime === false) { if($noException) { return NULL; } $this -> error(E_USER_ERROR, '"'.$filename.'" not found in '.$this->root.' directory.', OPT_E_FILE_NOT_FOUND); } $cname = optCompileFilename($filename); $compiledTime = @filemtime(DIR_DATA.'templates_c/'.$cname); if($compiledTime === false || $compiledTime < $rootTime || $this -> alwaysRebuild) { if(!is_object($this -> compiler)) { require_once(OPT_DIR.'opt.compiler.php'); $this -> compiler = new optCompiler($this); if(sizeof($this -> compileMasterPages) > 0) { // Load master pages now foreach($this -> compileMasterPages as $page) { $this -> getTemplate($page, $this -> compiler, true); } } } $this -> compiler -> parse($this -> compile.$cname, file_get_contents($filename)); } return $cname; } // end needCompile(); } // end sOptClass; ?>
Obiekt $files jest tutaj moim własnym mechanizmem wyszukiwania plików po kilku katalogach (konkretniej: katalog aplikacji oraz foldery wszystkich używanych modułów) z wbudowanym buforowaniem raz znalezionych wyników, aby nie zarżnąć wydajności operacjami dyskowymi. Tu jest właściwie jedyna poważniejsza zmiana. Zamiast doklejać do zmiennej $filename zawartość dyrektyry $this->root, przepuszczam nazwę pliku przez jedną z metod obiektu $files i otrzymuję na wyjściu pełną ścieżkę lub NULL, gdy nie została ona znaleziona.
Jednak to nie wszystko - niestety z powodu dziedziczenia musimy sami ponownie wklepać cały kod sprawdzania, czy szablon wymaga rekompilacji i uruchamiania kompilatora. W zasadzie skopiowałem to bezpośrednio z oryginalnej klasy, wycinając jedynie obsługę źródeł danych, ponieważ nie były mi one do niczego potrzebne w tym projekcie. Na początku pobieramy daty modyfikacji źródłowego szablonu oraz wersji skompilowanej i je porównujemy. Gdy zamiast pierwszej mamy false, szablon nie istnieje. Zamiast drugiej - nie został on jeszcze skompilowany. Gdy data modyfikacji drugiej jest większa od pierwszej, można użyć istniejącej wersji bez rekompilacji. Dodatkowy kod sprawdza, czy przypadkiem nie mamy włączonej dyrektywy alwaysRebuild nakazującej każdorazową kompilację szablonu (przydatne przy debugowaniu).
Jeśli stwierdzimy, że faktycznie trzeba coś przekompilować, na początku musimy upewnić się, że istnieje obiekt kompilatora. Gdy trzeba go utworzyć, musimy pamiętać o załadowaniu wszystkich zdefiniowanych szablonów kompilacyjnych (master templates). Na końcu wywołujemy metodę parse(), do której przekazujemy ścieżkę, pod którą należy zapisać wynik oraz treść pliku źródłowego.
Był to jeden z prostszych hacków. Pokażę teraz nieco inny, związany z samym kompilatorem, zacznę jednak od omówienia pewnej sytuacji. Do tej pory odnośnie sekcji pisałem cały czas, że pojedynczy element musi zawierać tablicę bloków lub ewentualnie obiekt implementujący interfejsy dostępu tablicowego. Druga ewentualność odpadła po premierze PHP 5.2, gdzie zabronili przekazywania obiektów przez referencje, niwecząc moje misterne plany i przy okazji szablony części użytkowników :). Okazało się, że kilku spryciarzy znalazło sposób na używanie w sekcjach wartości skalarnych - innymi słowy, tablica źródłowa miała następującą postać:
<?php $dane = array(0 => 'abc', 'def', 'ghi'); ?>
Kod po stronie szablonu omijający to był ciekawy (już pal sześć, że powinni tu raczej użyć foreach :))
Bazował on na moim przeoczeniu w kompilatorze. Mianowicie jest tam sobie metoda compileBlock(), która, jak wskazuje nazwa, odpowiada za prawidłowe przetworzenie nazw bloków na kod PHP. Był tam pewien fragment sprawdzający, czy mamy do czynienia z odwołaniem do danych sekcji i w instrukcji warunkowej brakowało tam alternatywy else, przez co w sytuacjach takich, jak powyższa, kompilował się on, jak zwykły blok nawet, gdy był nazwą sekcji.
// section match if(isset($this -> processors['section'])) { $cnt = sizeof($ns); if($cnt >= 2) { $ns[$cnt-2] = $this -> getConverterItem($ns[$cnt-2]); if(in_array($ns[0], $this -> processors['section'] -> sectionList)) { return '$__'.$ns[$cnt-2].'_val[\''.$ns[$cnt-1].'\']'; } } } $result = '$this->data';
Tymczasem w OPT 1.1.3, zupełnie niezależnie, postanowiłem dać możliwość stosowania następującej składni:
{* wariant 1 *} {section=aaa} {$aaa} {/section} {* wariant 2 *} {section=aaa} {$aaa->method()} {/section}
Oczywiście aby takie coś zaszło, $aaa powinno być kompilowane jako odwołanie do aktualnego elementu sekcji, a nie do całej tablicy. Wobec tego zwyczajnie dodałem wspomnianą brakującą alternatywę do ifa i nowa składnia zaczęła śmigać... jednocześnie uniemożliwiając działanie trickowemu kodowi :).
// section match if(isset($this -> processors['section'])) { $cnt = sizeof($ns); if($cnt >= 2) { $ns[$cnt-2] = $this -> getConverterItem($ns[$cnt-2]); if(in_array($ns[0], $this -> processors['section'] -> sectionList)) { return '$__'.$ns[$cnt-2].'_val[\''.$ns[$cnt-1].'\']'; } } else { if(in_array($ns[0], $this -> processors['section'] -> sectionList)) { return '$__'.$ns[0].'_val'; } } } $result = '$this->data';
W OPT jest wiele takich miejsc, gdzie rozszerzenie istniejącej funkcjonalności to kwestia dodania w pewnym miejscu algorytmu paru nowego warunku, alternatywy, pętli itd. W taki sposób dodałem separatory oraz instrukcję tree. Jako ciekawostkę podam, że kod odpowiedzialny za kompilację sekcji zajmuje 1/3 pliku opt.instructions.php :). Dwie podstawowe metody w klasie optSection to showAction() oraz getLink(). Pierwsza z nich służy do przetworzenia listy parametrów oraz wygenerowania wstępnego kodu sprawdzającego, czy przekazane w bloku dane są prawidłowe. Tam też znajduje się kod odpowiedzialny za sekcje dynamiczne. Wszystkie informacje o aktualnej sekcji rejestrowane są w tablicy $this->sections. Dzięki takiemu wyodrębnieniu, zarówno znacznik show, jak i section, mogą korzystać z identycznego zestawu parametrów bez powodowania jakichkolwiek nieścisłości. Druga z metod tworzy odwołanie w PHP do tablicy w sekcji w zależności od poziomu zagłębienia, wartości parametru datasource oraz ustawień dyrektywy sectionStructure.
Nie wykluczam kolejnych wpisów omawiających wewnętrzną budowę Open Power Template'a. Zdaję sobie doskonale sprawę, że w wielu miejscach kod można było lepiej zorganizować, ale póki działa, jak trzeba, nie ma sensu go już zmieniać, za to obserwacje i doświadczenia przydadzą się w pracach nad OPT 2 - nawiasem mówiąc uruchomił on niedawno swój pierwszy szablon.















Napisał Komentator newsa w niedzielę, 23 września 2007 o 22:33
Rozglądam się za szybkim i lekkim systemem szablonów z cache. Rozważam też napisanie własnego na potrzeby CMS-a. To, co wykorzystam, to: wstawianie zmiennych, np. {title}, instrukcje warunkowe*, pętle*.
* OPT i Smarty stosują zasadę:
* Z bazy danych pobieram nowości, które wyświetlam pętlą FOREACH. Dane nie są buforowane - wzrasta szybkość aplikacji.
Czy istnieje wersja "lite" OPT?