Dziś jest piątek, 22 sierpnia 2008 roku (z kalendarza...)

Hackujemy OPT

Icon

22.09.2007, 10:10

PHP

Komentarze (8)

Powrót

Domyślne rozwiązania zaimplementowane w systemie szablonów OPT powinny w zupełności wystarczyć większości jego użytkowników. Mogą oni po prostu skopiować kilka plików do swojej struktury katalogowej i zacząć kodować. Są jednak sytuacje, kiedy trzeba pokusić się o lekkie zmodyfikowanie działania OPT tak, aby dostosować je do naszych potrzeb. Niedawno sam znalazłem się w takowej, gdyż zaszła konieczność takiego zmodyfikowania mechanizmu wybierającego szablony, aby przeszukiwał kilka katalogów, a nie jeden zdefiniowany domyślnie w dyrektywie root.

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 :))

<ul>
{section=page_nums}
 <li>{$page_nums[$opt.section.page_nums.id]}</li>
{/section}
</ul>

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.

Powrót

Komentarze

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ę:

{section=Sekcja}Kod HTML{/section}
I wiele podobnych instrukcji. Spotykam się z opiniami, że to bardziej przypomina język programowania. Ciekawe rozwiązanie prezentuje XT, bo operuje na ID, klasach, CSS... Zaletą OPT w stosunku do Smarty jest to, że posiada XML-owe znaczniki sekcji.

* Z bazy danych pobieram nowości, które wyświetlam pętlą FOREACH. Dane nie są buforowane - wzrasta szybkość aplikacji.
foreach($res as $val) { ... }
Nie wiem, jak OPT czy XT implementują pętle, jednak dobrze by było, gdyby kod HTML wysyłał się do przeglądarki przy każdej iteracji FOREACH.

Czy istnieje wersja "lite" OPT?

Napisał Zyx w poniedziałek, 24 września 2007 o 18:29

Wiele z Twoich pytań jest wyjaśnionych na stronie opt.openpb.net po polsku np. w FAQ, ale opiszę tu kilka spraw.

XT przede wszystkim skupia się na eliminacji tzw. "szablonowego pseudojęzyka", jak to nazywa jego autor, pisząc elegancką nakładkę do manipulowania XHTML-em z poziomu PHP znacznie łatwiejszą w użyciu, niż DOM i trzeba przyznać, że mu to wyszło. Jednak szablonów zacząłem używać, by mieć wreszcie porządek: kod PHP i algorytmy w miejscu A, kod HTML z opisem sposobu wyświetlania w miejscu B - dlatego z dwóch powodów nie użyłbym tego w swoim projekcie niezależnie od tego, czy miałbym OPT, czy nie:
- Szablony wybrałem też po to, by skrypt PHP miał święty spokój na zasadzie "wyślij dane i zapomnij". Zauważ, że w XT uzależniasz kod PHP od budowy kodu HTML. Jeśli zmieniasz szatę graficzną i nie ma ona dokładnie identycznej struktury, jak stara, to masz problem, bo musisz cały skrypt ponownie przepisać. Pojawia się przez to problem z ponownym wykorzystaniem. Możesz zapytać, co to za różnica - przecież jak się zmienia szablony, też trzeba te wszystkie znaczniki na nowo powstawiać; racja, tylko że jest to znacznie prostsze do wykonania, szczególnie gdy same znaczniki są proste.
- PHP jako język takiego opisu jest zbyt niskopoziomowy i rozwlekły, a "szablonowy pseudojęzyk" w teorii ukrywa to.

Sam fakt, że się używa znaczników nie znaczy, że mamy do czynienia z językiem programowania, bo inaczej za taki musielibyśmy uważać wszystko, co jest bardziej skomplikowane od formatu TXT. Fakt, OPT udostępnia język programowania, ale wyszedłem z zasady, żeby nie używać go, gdy nie ma takiej potrzeby. Efekt - w 95% moich szablonów najbardziej skomplikowaną "strukturą programistyczną" jest IF sprawdzający, czy blok jest ustawiony:

{if $blok}...{/if}


Ponadto OPT posiada wysokopoziomowe pętle - właśnie wspomniane sekcje, które w 95% przypadków zastępują foreach. Jest instrukcja TREE, która rendering drzew czyni prostą zabawą, instrukcja systemu stronicowania oraz komponenty znakomicie ułatwiające pracę z formularzami. Oto przykład OPT+OPF z mojego własnego kodu:

<tr opf:classfor="name">
  <td class="desc"><label for="f_local">{$address@name}</label></td>
  <td><opt:opfInput name="name" id="f_local"><opt:load event="aMessage"/></opt:opfInput></td>
</tr>


Mam tu wszystko: wielojęzyczność, przeładowywanie, domyślne wartości, wyświetlanie komunikatu błędu w przypadku błędu wraz z przekolorowywaniem całego wiersza tabeli...

Ad. wersji LITE -> jest tzw. OPT API, na bazie którego można zbudować własny parser korzystający z kompilatora OPT, można też użyć dołączonego narzędzia OPT Toolset, które potrafi usunąć z kodu źródłowego OPT niepotrzebną funkcjonalność (główny plik zmniejsza się o więcej niż połowę po wyłączeniu wszystkich opcji).

Napisał Bob w wtorek, 25 września 2007 o 20:40

A co jeżeli ten kod który opisujesz jako wadę XT jest właśnie tą pętlą? <ns:section> i oprogramowanie w php. Zresztą już kiedyś o tym z Tobą rozmawiałem. Patrzysz na XT jako na system szablonów do operacji przekształcania XMLa (i pochodnych) na wynikowy (x)html tylko w warstwie wyglądu. A tymczasem zastosowanie jest dowolne - może być nawet identyczne z OPT.
Co nie zmienia faktu że kilka jego błędów np. nie do końca poprawnie obsługiwane przestrzenie nazw skłaniają mnie do korzystania z OPY w którym to tylko obsługa błędów mi się nie podoba :)

Napisał Zyx w środę, 26 września 2007 o 20:13

Eee... mógłbyś rozwinąć pierwsze zdanie? Nie bardzo rozumiem, co masz na myśli. Natomiast dalej - raczej nie podoba mi się przenoszenie opisów tychże operacji do warstwy logiki, która w teorii powinna jedynie przetwarzać dane, a nie zajmować się ich osadzaniem w szablonie. Jeśli natomiast dodamy trzecią warstwę pośredniczącą, to jesteśmy w punkcie wyjścia. Mamy to samo, co w OPT jedynie z tą różnicą, że zamiast korzystać z prostych, zaprojektowanych do tego celu instrukcji, musimy programować.

Algorytmika daje nam jasny wybór: programowanie lub ograniczenia funkcjonalne do pozbioru czynności przewidzianych przez autora. W którymś miejscu ono się zawsze pojawi - jak nie w szablonie, to w skrypcie. W OPT wyszedłem z założenia, że skoro już zdecydowałem się na szablon, to mogę tak zaprojektować składnię i instrukcje, aby jak najwięcej typowych czynności realizować prościej i czytelniej, niż w PHP. Tyle filozofii.

Ad. obsługi błędów - w OPT 2.0.0 ją poprawiłem dzięki zaprojektowaniu nowego, logicznego systemu wyjścia. Powinna Ci się spodobać; niedługo postaram się dać więcej szczegółów :).

Napisał Komentator newsa w środę, 26 września 2007 o 23:22

OPT chwali się swoją niezależnością. Potęgą XT jest możliwość wpływu na szablon bez śmietnika w kodzie.

Znaczniki mają swoje atrybuty, np. <a> - href
(czego nie ma <input>). Muszę przekazać adres pewnej strony szablonowi. W xHTML 2.0 parametr HREF będą posiadać prawie wszystkie znaczniki, więc z tym nie powinno być problemu. Zazwyczaj do odnośników stosuje się <a>, bo tylko te wykrywają roboty wyszukiwarek, choć w niektórych miejscach można wstawić <input>.

Myślę, że najlepsze jest rozwiązanie pośrednie - DOM + wstawki {zmienna} bez $. Co o tym sądzicie?

Napisał Zyx w czwartek, 27 września 2007 o 10:54

Samo wrzucenie DOM-u i wstawek do jednego kotła nie sprawi, że automatycznie będzie ono najlepsze. Widzę tu co najmniej trzy istotne czynniki:
1. Sposób połączenia DOM z parserem wstawek.
2. Możliwości parsera wstawek. Jeśli ma on służyć tylko to umieszczania zmiennych, to prawdę mówiąc niewiele powiększa to funkcjonalność.
3. Wydajność.

Ad. znaku dolara - W przypadku parserów jedynie wstawiających dane faktycznie nie ma on sensu, ale w OPT i Smarty zapis "foo" może oznaczać mnóstwo rzeczy. Dzięki dolarowi kompilator od razu wie: "o, to jest blok, więc trzeba go skompilować tak a tak", nawet gdy programista nie zdefiniował takowego w skrypcie. Ponadto łatwo jest pisać przetwarzatory kodu czy choćby kolorowarki składni. Nawiasem mówiąc znak dolara przy zmiennych to moim zdaniem jeden z lepszych wynalazków Perla i PHP.

PS. Nie martw się, jak ktoś ma odpowiedni styl pisania, to sobie w kodzie PHP zrobi śmietnik, zamiast w szablonie i na jedno wyjdzie :).

Napisał Bob w niedzielę, 30 września 2007 o 11:13

"Eee... mógłbyś rozwinąć pierwsze zdanie?"

Miałem na myśli że nic nie stoi na przeszkodzie aby z OPT zrobić kopiuj-wklej do szablonu XT (przy składni XML, z poprawkami na zmienne) i oprogramować to w PHP tak samo jak w OPT. Ty patrzysz na XT jak na zwykły parser tylko z podstawianiem danych. A tymczasem jest on w zasadzie tylko nakładką na XML ze wszystkimi tego zaletami.

Możesz już coś zdradzić z OPT 2.0? Na razie niewiele tego było.

Napisał Komentator newsa w niedzielę, 7 października 2007 o 14:10

Znalazłem kompromisowe rozwiązanie - to była nawet pierwsza decyzja w tej sprawie podjęta ok. 3 miesiące temu. Zadowoleni zostaną zarówno zwolennicy OPT/Smarty/XT i użycia PHP jako języka szablonów. :)
Temat na forum SitePoint (czytajcie do końca)
Co Wy o tym myślicie? Postaram się, aby można było kompilować skórki w innych formatach niż natywnym dla projektu, m.in. OPT, XT... - ale to na końcu, jak będzie czas.

Strona 1 z 1 :: 1

Skomentuj

NickInformacja
E-mailTylko do użytku wewnętrznego.
WWWNie zapomnij o http://
LayoutNapisz tu, czy widzisz dzienny czy nocny layout.
WpisFormatowanie wiki
Internauto, pamiętaj! Wolność to nie samowola - dbaj o kulturę wypowiedzi oraz dyskusji w sieci.

Na Zyxist.com panuje swoboda wyrażania opinii oraz krytyki pod dowolnym adresem. Jedyny warunek: musi być ona kulturalna i rzeczowa. Na chamstwo, prostactwo lub jawne obrażanie kogokolwiek nie ma tu miejsca i takie komentarze są bardzo szybko usuwane. Jeśli zamierzasz polemizować z treścią wpisu, wpierw uważnie ją przeczytaj.

© Tomasz "Zyx" Jędrzejewski 2005 - 2008 | Wykonanych zapytań: 2 | Serwer wirtualny zapewnia