JavaScript w projekcie internetowym

JavaScript jest technologią kojarzoną ze stronami WWW. Uruchamiany w przeglądarce internetowej język przez długi czas stanowił dodatek ożywiający jedynie statyczną treść. Sam przez wiele lat trzymałem się paradygmatu, że strona internetowa powinna być w pełni funkcjonalna i czytelna nawet jeśli użytkownik wyłączy w swojej przeglądarce obsługę JavaScript-u. Ta reguła jest nadal dobrze widzianą praktyką ale rzadziej przestrzeganą, gdyż pozbawiony javascript-owego dopingu interfejs użytkownika na tyle traci na wygodzie i atrakcyjności, że już mało kto decyduje się na skrajne ustawienia bezpieczeństwa.

W chwili obecnej ilość kodu pisanego w JavaScript-cie powoli zrównuje się z ilością kodu tworzonego w językach działających po stronie serwera, a podejrzewam, że są i serwisy gdzie ją przewyższa. Programiści frontendowi zyskują coraz bardziej na znaczeniu bo też wymagania stawiane im są coraz większe. Dzisiaj pracodawcy nie szukają „koderów” potrafiących dołączyć i uruchomić cudzy, znaleziony gdzieś w sieci skrypt, którego jedynym zadaniem jest rozwijanie i zwijanie menu. Dzisiaj tworzy się rozbudowane aplikacje oparte na profesjonalnych frameworkach stworzonych przez – nierzadko duże (np. Yahoo YUI Library) – firmy i mające szerokie wsparcie wśród społeczności (np. jQuery) internetowych.

Im bardziej złożone programy, im większa ilość skryptów – tym zarządzanie i utrzymywanie kodu JavaScript staje się trudniejsze. Fakt, że interpreterem języka JavaScript jest przeglądarka internetowa czyni sprawę jeszcze bardziej skomplikowaną gdyż musimy wziąć pod uwagę kilka dodatkowych aspektów związanych ze specyficznym środowiskiem w jakim pisane w tym języku programy są uruchamiane.

Osadzać JavaScript w kodzie strony HTML czy wydzielać do plików js?

Nie ma zastrzeżeń natury techninej ograniczających ilość kodu w źródle strony lub też określających co powinno być przeniesione do plików zewnętrznych. Zdrowy rozsądek oraz zasada, że należy rozdzielać logikę od warstwy wizualnej podpowiadają, że kod osadzony inline-owo powinien być jak najskromniejszy, a wszystko co nie jest specyficzne dla danej podstrony powinno zostać umieszczone w pliku.

Przemawiają za tym zarówno względy estetyczne jak i pragmatyczne. Kod umieszczony w pliku łatwiej się debuguje, można go skompresować, poddać obfuskacji i dołączyć do innych stron. Ponadto przeglądarki domyślnie buforują pliki js – chyba, że mają inne ustawienia – co oszczędza liczbę żądań (requestów) kierowanych do serwera.

Skrypt osadzony powinien być umieszczony w jednym miejscu. Przeplatanie kodu JavaScript między innymi elementami HTML-a dopuszczalne jest tylko w przypadku obsługi zdarzeń.

<script type="text/javascript">
    function someFunction() { alert('OK'); }
</script>
<input id="somebutton" type="button" value="Call function" onclick="someFunction();" />

Jest to już trochę archaizm ponieważ dzięki nasłuchiwaczom (ang. listener) można każde zdarzenie obsłużyć bez dodawania do tagów HTML atrybutów onclick, onmouseover itd. Nicholas C. Zakas w książce pt. „JavaScript dla webmasterów – zaawansowane programowanie” opisuje różnice w implementacji tego mechanizmu w różnych przeglądarkach. Wspominam o tym tylko jako o ciekawostce gdyż każdy z czołowych frameworków udostępnia ujednolicone api do obsługi zdarzeń.

Przykładowy kod z użyciem jQuery pokazuje jak podpiąć funkcję pod określone zdarzenie wybranego obiektu na stronie.

$(document).ready(function() {
    $('#somebutton').click(someFunction);
});

Kod ten umieszczony w nagłówku strony uruchomi się po załadowaniu całego drzewa DOM. Wynika z tego kilka niedogodności.

Wszelkie inicjowane JavaScript-em widżety pojawią się z widocznym opóźnieniem – zostaną uruchomione dopiero po wyrenderowaniu całej strony. Niecierpliwi użytkownicy mogą zacząć klikać w kalendarzyk zanim jeszcze zostanie uruchomiony, albo zatwierdzać formularza bez podpiętych javascript-owych walidatorów. Jest to cena za czystość i porządek w kodzie.

Czy można sterować cache-owaniem plików JavaScript w pamięci podręcznej przeglądarki?

Podczas pierwszej wizyty na stronie WWW wszystkie jej elementy zostają pobrane i w zależności od ustawień przeglądarki oraz przesłanych nagłówków HTTP zapisane w pamięci podręcznej przeglądarki. Buforowanie (ang. cache) oszczędza liczbę żądań wysyłanych do serwera, transfer i przyśpiesza ładowanie strony. (Użyj about:cache w Firefox jeśli chcesz zobaczyć co zawiera Twój cache)

Do sterowania zachowaniem cache-a służą nagłówki HTTP (ang. HTTP Headers). Dla treści dynamicznej najlepiej skorzystać z Cache-Control i tak aby zapobiec buforowaniu dokumentu należy wysłać następujące nagłówki.

header("Pragma: no-cache");
header("Cache-Control: max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0");
header("Expires: 0");

UWAGA! Można w tym celu użyć meta tagów ale nie wszystkie przeglądarki honorują meta tagi strujące cachem i nie są one brane pod uwagę przez serwery proxy

<head>
    <meta http-equiv="Expires" content="0" />
    <meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate" />
    <meta http-equiv="Cache-Control" content="post-check=0, pre-check=0" />
    <meta http-equiv="Pragma" content="no-cache" />
</head>

Przy rzadko zmieniającej się treści lepsze są inne ustawienia bufora.

header("Cache-Control: max-age=3600, must-revalidate, post-check=600, pre-check=1800");

przeglądarka powinna przez 600 sekund podawać dokument wyłącznie z bufora, następnie przez 1200 sekund brać dokument z bufora, ale też sprawdzać w tle czy na serwerze jest nowsza wersja (i jeżeli jest to ją zaciągnąć do kolejnego użycia). Po upłynięciu 1800 sekund od pobrania przeglądarka powinna najpierw sprawdzić czy jest nowa wersja i jeśli jest to ją zaktualizować, a następnie użyć.

Dla elementów statycznych takich jak np. pliki JavaScript mamy do dyspozycji dwa inne nagłówki: ETag i Expires. Nagłówek ETag informuje przeglądarkę czy konkretny plik został zmieniony. Wymaga każdorazowo nawiązania połączenia z serwerem w celu odpytania o aktualność danego zasobu. Z kolei nagłówek Expires określa okres (a właściwie datę końcową) przez jaki obiekty danego rodzaju powinny być traktowane jako aktualne. Ma to tę zaletę, że Expires wyznacza datę ważności i do czasu jej minięcia przeglądarka nie będzie próbowała podejmować nawet próby weryfikacji aktualności danego pliku na serwerze.

<FilesMatch "\.(ico|jpg|jpeg|png|gif|js|css|swf)$">
    <IfModule mod_expires.c>
        ExpiresActive on
        # 2592000 sekund = 30 dni
        ExpiresDefault A2592000
        Header append Cache-Control "public"
    </IfModule>
    # Wylaczenie naglowkow ETag
    Header unset ETag
    FileETag None
</FilesMatch>

Powyższy przykład prezentuje jak w htaccess zdefiniować 30 dniowy (2592000 sekund = 30 dni) okres ważności dla wybranych plików. Nagłówek ETag został wyłączony ponieważ w przypadku równoczesnego wysłania do klienta (czytaj przeglądarki) obu nagłówków, ETag ma priorytet.

UWAGA! Jedynym sposobem na wymuszenie odświeżenie pliku z ustawionym Expires w przeglądarkach wszystkich odwiedzających jest zmiana jego nazwy.

Zmiana nazwy pliku, który został podpięty w różnych częściach serwisu wiąże się z koniecznością dokonania poprawek w kilku miejscach w kodzie. Najłatwiej i najwygodniej zmienić nazwę foldera zawierającego elementy statyczne serwisu i jego nazwę trzymać w zmiennej. Wystarczy wtedy zmienić jedną wartość w konfigu i adresy wszystkich dynamicznie osadzonych w kodzie html obiektów zmienią się w jednej chwili. Jest to przepis, który można od biedy zastosować dla małopopularnych serwisów bowiem takie podejście ma przynajmniej dwie wady

  1. Trzeba zmieniać nazwę katalogu co jest kłopotem jeśli trzyma się statiki w repozytorium. Obejściem tego problemu jest użycie dowiązań symbolicznych.
  2. Zmiana pojedynczego obiektu dezaktualizuje wszystkie w danym folderze. Nawet jeśli zmienimy tylko favico-nkę to adresy wszystkich plików w danym katalogu się zmienią i zostaną pobrane na nowo przez przegladarki wszystkich użytkowników marnując transfer i zasoby serwera.

Sprytniejszym rozwiązaniem jest dynamiczna zmiana nazwy pojedynczego pliku przez dodanie do niego np. czasu ostaniej modyfikacji oraz zdefiniowanie odpowiedniej regułki mode_rewrite w htaccess.

RewriteEngine on
RewriteRule ^(.*)\.[\d]{10}\.(css|js|jpe?g|gif|png)$ $1.$2 [L]

Funkcja, która zajmie się modyfikowaniem nazwy pliku w kodzie dla PHP może wyglądać tak:

/**
 *  Given a file, i.e. /js/base.js, replaces it with a string containing the
 *  file's mtime, i.e. /js/base.1221534296.js.
 *
 *  @param $file  The file to be loaded.  Must be an absolute path (i.e.
 *                starting with slash).
 */
function auto_version($file)
{
  if(strpos($file, '/') !== 0 || !file_exists($_SERVER['DOCUMENT_ROOT'] . $file))
    return $file;
 
  $mtime = filemtime($_SERVER['DOCUMENT_ROOT'] . $file);
  return preg_replace('{\\.([^./]+)$}', ".$mtime.\$1", $file);
}

sposób użycia

<script src="<?php echo auto_version('/js/base.js'); ?>" type="text/javascript"></script>

Analogiczny przykład dla Django wykorzystujący tę samą regułę mode_rewrite-a.

Załóżmy, że kod jest zdefiniowany w /project/app/templatetags/helpers.py

from django import template
register = template.Library()
import os, re
 
STATIC_PATH="/path/to/templates/"
version_cache = {}
 
rx = re.compile(r"^(.*)\.(.*?)$")
def version(path_string):
    try:
        if path_string in version_cache:
            mtime = version_cache[path_string]
        else:
            mtime = os.path.getmtime('%s%s' % (STATIC_PATH, path_string,))
            version_cache[path_string] = mtime
 
        return rx.sub(r"\1.%d.\2" % mtime, path_string)
    except:
        return path_string
 
register.simple_tag(version)

i użycie w szablonie

{% load helpers %}
<link rel="stylesheet" type="text/css" href="{% version '/static/css/style.css' %}">

UWAGA! Dodanie parametru GET do nazwy pliku nie jest rekomendowanym sposobem odświeżania plików statycznych. Niektóre przeglądarki (Opera, Safari) i serwery pośredniczące nie cache-ują takich obiektów, inne z kolei (Firefox i IE) ignorują parametry.

Najmniej inwazyjne, niewymagające interwencji ze strony programisty jest użycie mod_pagespeed, który na poziomie Apache podmienia odsyłacze do wszelkich mediów a w przypadku zmiany w pliku sam ją wykrywa i generuje inny adres.

Czy łączyć pliki JavaScript w jeden duży plik?

Każdy serwer ma swoją przepustowość i może przyjąć ograniczoną liczbę żądań (ang request). Żądanie http – nawet jednopikselowego obrazka – wymaga nawiązania połączenia pomiędzy przeglądarką a serwerem, przesłania danych na serwer definiujących treść żądania, rozpoznanie czy dane żądanie może być spełnione czy też nie, a następnie zwrócenie do przeglądarki żądanego zasobu lub też nagłówków z odpowiednim kodem błędu. Wszystko to absorbuje czas i angażuje zasoby serwera. Wziąwszy pod uwagę, że pobraniu każdego dokumentu HTML towarzyszą dodatkowe zapytania o tzw. media czyli grafiki, video, pliki css, js, fonty, filmy flash itd., trzeba sobie uświadomić, że wpisanie jednego krótkiego url-a na pasku adresu przeglądarki i wciśnięcie entera uruchamia przy bardziej rozbudowanych stronach całą lawinę zapytań.

To zagadnienie zaczyna być istotne dopiero, kiedy liczba unikalnych wizyt na Twojej stronie składa się przynajmniej z pięciu cyfr. Zatem jeśli nie masz przynajmniej 10 tys. unikalnych odwiedzin lub też znacząca ich liczba jest wygenerowana przez narzędzie do fałszowania statystyk, to możesz odłożyć przeczytanie tego rozdziału na później.

Ograniczenie liczby requestów najłatwiej osiągnąć poprzez połącznie kilku plików w jeden. Jeden plik js to rozwiązanie teoretycznie idealne, ale niepraktyczne. Jeśli na stronie używamy, jQuery, tinymce z dużą liczbą pluginów, do tego chcielibyśmy mieć fancyboxa i parę widgetów z jquery.UI i jeszcze kilka innych ciekawych bibliotek to rozmiar pliku byłby niepokojąco duży nawet gdyby miał się ładować tylko raz a potem został zbuforowany.

Osobiście uważam, że łączenie plików w jeden większy sprawdza się dla małych skryptów podobnie jak to się ma w przypadku zestawu małych ikonek połączonych w jedną grafikę i wyświetlanych za pomocą techniki CSS Sprite polegającej na manipulacji parametrami position-background.

Warto też rozważyć połączenie skryptów, które są używane na większości, lub też często odwiedzanych podstronach. W dużych serwisach nigdy nie ma tak, aby 100% kodu JavaScript było wykorzystywane na każdej podstronie witryny. Poza tym część programów JavaScript urychamianych jest np. w części administracyjnej serwisu. Nie ma sensu łączyć kodu uniwersalnego z kodem uruchamianym dla wąskiego grona użytkowników.

Kodem logicznie podzielonym między pliki łatwiej się zarządza dlatego warto pomyśleć nad rozwiązaniami atomatycznie łączącymi i kompresującymi pliki JavaScript takimi jak np. jsmin

require 'jsmin.php';
// Output a minified version of example.js.
echo JSMin::minify(file_get_contents('example.js') . file_get_contents('example2.js'));

Dla Django mamy szeroki wybór asset-manager-ów, ale możemy też skorzystać z algorytmu Douglas Crockford-a, przepisanego w języku Python i zaprezentowanego w jednej z recept w serwisie stackoverflow.

#!/usr/bin/python
# -*- coding: utf-8 -*-
 
import os, os.path, shutil
 
# This code is original from jsmin by Douglas Crockford, it was translated to
# Python by Baruch Even. The original code had the following copyright and
# license.
#
# /* jsmin.c
#    2007-05-22
#
# Copyright (c) 2002 Douglas Crockford  (www.crockford.com)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# The Software shall be used for Good, not Evil.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# */
 
from StringIO import StringIO
 
def jsmin(js):
    ins = StringIO(js)
    outs = StringIO()
    JavascriptMinify().minify(ins, outs)
    str = outs.getvalue()
    if len(str) > 0 and str[0] == '\n':
        str = str[1:]
    return str
 
def isAlphanum(c):
    """return true if the character is a letter, digit, underscore,
           dollar sign, or non-ASCII character.
    """
    return ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or
            (c >= 'A' and c <= 'Z') or c == '_' or c == '$' or c == '\\' or (c is not None and ord(c) > 126));
 
class UnterminatedComment(Exception):
    pass
 
class UnterminatedStringLiteral(Exception):
    pass
 
class UnterminatedRegularExpression(Exception):
    pass
 
class JavascriptMinify(object):
 
    def _outA(self):
        self.outstream.write(self.theA)
    def _outB(self):
        self.outstream.write(self.theB)
 
    def _get(self):
        """return the next character from stdin. Watch out for lookahead. If
           the character is a control character, translate it to a space or
           linefeed.
        """
        c = self.theLookahead
        self.theLookahead = None
        if c == None:
            c = self.instream.read(1)
        if c >= ' ' or c == '\n':
            return c
        if c == '': # EOF
            return '\000'
        if c == '\r':
            return '\n'
        return ' '
 
    def _peek(self):
        self.theLookahead = self._get()
        return self.theLookahead
 
    def _next(self):
        """get the next character, excluding comments. peek() is used to see
           if an unescaped '/' is followed by a '/' or '*'.
        """
        c = self._get()
        if c == '/' and self.theA != '\\':
            p = self._peek()
            if p == '/':
                c = self._get()
                while c > '\n':
                    c = self._get()
                return c
            if p == '*':
                c = self._get()
                while 1:
                    c = self._get()
                    if c == '*':
                        if self._peek() == '/':
                            self._get()
                            return ' '
                    if c == '\000':
                        raise UnterminatedComment()
 
        return c
 
    def _action(self, action):
        """do something! What you do is determined by the argument:
           1   Output A. Copy B to A. Get the next B.
           2   Copy B to A. Get the next B. (Delete A).
           3   Get the next B. (Delete B).
           action treats a string as a single character. Wow!
           action recognizes a regular expression if it is preceded by ( or , or =.
        """
        if action <= 1:
            self._outA()
 
        if action <= 2:
            self.theA = self.theB
            if self.theA == "'" or self.theA == '"':
                while 1:
                    self._outA()
                    self.theA = self._get()
                    if self.theA == self.theB:
                        break
                    if self.theA <= '\n':
                        raise UnterminatedStringLiteral()
                    if self.theA == '\\':
                        self._outA()
                        self.theA = self._get()
 
 
        if action <= 3:
            self.theB = self._next()
            if self.theB == '/' and (self.theA == '(' or self.theA == ',' or
                                     self.theA == '=' or self.theA == ':' or
                                     self.theA == '[' or self.theA == '?' or
                                     self.theA == '!' or self.theA == '&' or
                                     self.theA == '|' or self.theA == ';' or
                                     self.theA == '{' or self.theA == '}' or
                                     self.theA == '\n'):
                self._outA()
                self._outB()
                while 1:
                    self.theA = self._get()
                    if self.theA == '/':
                        break
                    elif self.theA == '\\':
                        self._outA()
                        self.theA = self._get()
                    elif self.theA <= '\n':
                        raise UnterminatedRegularExpression()
                    self._outA()
                self.theB = self._next()
 
 
    def _jsmin(self):
        """Copy the input to the output, deleting the characters which are
           insignificant to JavaScript. Comments will be removed. Tabs will be
           replaced with spaces. Carriage returns will be replaced with linefeeds.
           Most spaces and linefeeds will be removed.
        """
        self.theA = '\n'
        self._action(3)
 
        while self.theA != '\000':
            if self.theA == ' ':
                if isAlphanum(self.theB):
                    self._action(1)
                else:
                    self._action(2)
            elif self.theA == '\n':
                if self.theB in ['{', '[', '(', '+', '-']:
                    self._action(1)
                elif self.theB == ' ':
                    self._action(3)
                else:
                    if isAlphanum(self.theB):
                        self._action(1)
                    else:
                        self._action(2)
            else:
                if self.theB == ' ':
                    if isAlphanum(self.theA):
                        self._action(1)
                    else:
                        self._action(3)
                elif self.theB == '\n':
                    if self.theA in ['}', ']', ')', '+', '-', '"', '\'']:
                        self._action(1)
                    else:
                        if isAlphanum(self.theA):
                            self._action(1)
                        else:
                            self._action(3)
                else:
                    self._action(1)
 
    def minify(self, instream, outstream):
        self.instream = instream
        self.outstream = outstream
        self.theA = '\n'
        self.theB = None
        self.theLookahead = None
 
        self._jsmin()
        self.instream.close()
 
def compress(in_files, out_file, in_type='js', verbose=False,
             temp_file='.temp'):
    temp = open(temp_file, 'w')
    for f in in_files:
        fh = open(f)
        data = fh.read() + '\n'
        fh.close()
 
        temp.write(data)
 
        print ' + %s' % f
    temp.close()
 
    out = open(out_file, 'w')
 
    jsm = JavascriptMinify()
    jsm.minify(open(temp_file,'r'), out)
 
    out.close()
 
    org_size = os.path.getsize(temp_file)
    new_size = os.path.getsize(out_file)
 
    print '=> %s' % out_file
    print 'Original: %.2f kB' % (org_size / 1024.0)
    print 'Compressed: %.2f kB' % (new_size / 1024.0)
    print 'Reduction: %.1f%%' % (float(org_size - new_size) / org_size * 100)
    print ''
 
    os.remove(temp_file)
 
if __name__ == '__main__':
    compress(['script1.src.js','script2.src.js'], 'script.min.js')

UWAGA! Dodatkową zaletą obfuskacji – czyli zaciemniania kodu – jest jego mniejsza objętość. Dlatego też zachęcam do tego „procederu” nie tyle w celu ochrony własności intelektualnej co minimalizacji plików js.

Czy zaciągać pliki JavaScript z zewnętrznych serwerów?

Istnieje wiele serwisów społecznościowych czy też wyspecjalizowanych w określonej tematyce, które oferują funkcjonalności mogące w uatrakcyjnić naszą własną stronę www. Poprzez integrację z usługą uwierzytelniania użytkowników za pośrednictwem Facebooka, wykorzystując api Soundcoluda do udostępniania muzyki lub choćby osadzając mapy Googla wzbogacamy nasz serwis o elementy, które samodzielnie byłoby o wiele trudniej zaimplementować. Większość zaawansowanych dodatków, jak również tych całkiem niepozornych jak Facebook Like wymaga dołączenia JavaScriptu. Okazuje się, że także duże javascriptowe frameworki możemy zaciągać z zewnętrznego serwera.

Google hostuje wybrane biblioteki JavaScript i udostępnia je w wielu wersjach zachęcając do osadzenia na swoich stronach.

Js frameworki hostowane przez google

Jeśli spojrzymy na oficjalną stronę jQuery zobaczymy w jej źródle.

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>

Można się zastanawiać nad zaletami i wadami takiej praktyki. Niewątpliwie warto mieć kopię podstawowej biblioteki – używanej w serwisie jako bazę i szkielet wszelkich skryptów – na swoim serwerze. Nie przeszkadza to równocześnie używać kopii tej biblioteki udostępnianej z innego serwera. Delegowanie części ruchu na zewnątrz z pewnością odciąży nieco naszego własnego Apacha. Poza tym trzeba wziąć pod uwagę, że serwer Googla jest zoptymalizowany dla treści statycznych, co gwarantuje szybkie i niezawodne działanie.

Temat dotyczący serwerów plików statycznych CDN-ów (Content Delivery Network) jest bardzo ciekawy, ale wykracza nieco poza ramy tego opracowania, które traktuje raczej o tym co może zrobić programista i porusza sprawy całkiem podstawowe.

Czy ładować pliki JavaScript head czy w body?

Osadzając odnośniki do plików JavaScript w HTML-u w sposób tradycyjny …

<script type="text/javascript" src="scripts_1.js"></script>
<script type="text/javascript" src="scripts_2.js"></script>

… skrypty ładowane są synchronicznie czyli po kolei. Co więcej – do czasu załadowania pliku js wstrzymywane jest renderowanie strony. Można to zasymulować wywołując w skrypcie funkcję stop, która przerywa całkowicie dalsze ładowanie HTML-a, a co za tym idzie pozostałych, osadzonych w kodzie obiektów.

stop();

Z tego powodu pliki JavaScript mogą stać się tzw. SPOF (Single Point Of Failure) czyli pojedynczym elementem zaburzającym działanie całości. W praktyce ma to miejsce wtedy kiedy plik JavaScript z powodu wielkości lub kłopotów na łączach długo się ładuje. Najczęściej spowalnia to po prostu załadowanie całej witryny. W najgorszym przypdaku serwer plików nie odpowiada, a przeglądarka czaka w nieskończoność na zasób lub też kod błędu.

Testowanie SPOF na stronie WWW spowodowanego ładowaniem JavaScript

Zob. Testing for Frontend SPOF

Programiści często obchodzą ten problem ładując skrypty nie w nagłówku strony (czyli w head) tylko w ciele strony (w body) i to najlepiej na samym dole. Wychodzą z założenia, że jeśli nawet sypnie się JavaScript to przynajmniej cała treść, style i obrazki się załadują.

UWAGA! Ładowanie skryptów w body jest absolutnie niepotrzebne. Wystarczy odroczyć pobieranie skryptów do czasu sparsowania całej strony. Służy do tego atrybut defer (od ang. deferred – odroczony).

<script type="text/javascript" src="script.js" defer="defer"></script>

Teraz funkcja strop nie spowoduje przerwania renderowania strony ani ładowania innych skryptów.

Czy ładować pliki JavaScript synchronicznie czy asynchronicznie?

HTML5 przyniósł wiele ciekawych innowacji także w sprawie ładowania skryptów pojawiło się coś nowego. Tag script wzbogacił się o atrybut async, który determinuje ładowanie asynchroniczne.

<script async="async" src="scripts_1.js"></script>
<script async="async" src="scripts_2.js"></script>

Oba skrypty będą pobierane niezależnie od siebie i od dokumentu HTML dlatego jeśli nawet w pierwszym skrypcie wywołamy funkcję „stop” nie spowoduje to przerwania renderowania strony ani ładowania drugiego skryptu. Przewagą trybu asynchronicznego nad odraczaniem ładowania do czasu sparsowania strony HTML (patrz. atrybut defer), że skrypt może być pobierany równocześnie z samym dokumentem.

UWAGA! Jeśli drugi skrypt będzie pobierany w trybie synchrnicznym to pierwszy także przełączy się w ten tryb. Mówiąc inaczej skrypty będą pobierane asynchronicznie jeśli mają ustawiony parametr async na true oraz wszystkie później rozpoczynane pobrania także odbywają sie w tym trybie.

Ładowanie asynchroniczne jest idealne do inicjowania na stronie dodatkowych usług takich jak np. statystyki:

  var _gaq = _gaq || [];
  _gaq.push(['_setAccount', 'UA-XXXXX-Y']);
  _gaq.push(['_trackPageview']);
 
  (function() {
    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  })();

lub facebook connect

$(document).ready(function() {
 
    window.fbAsyncInit = function() {
        // kod realizujący logowanie na stronie via facebook
    };
    (function(d){
        // nadanie id zapobiega wielokrotnemu ładowaniu skryptu
        var js, id = 'facebook-jssdk'; if (d.getElementById(id)) {return;}
        js = d.createElement('script'); js.id = id; js.async = true;
        // brak protokołu nie jest błędem
        js.src = "//connect.facebook.net/pl_PL/all.js";
        d.getElementsByTagName('head')[0].appendChild(js);
    }(document));
});

UWAGA W powyższym przykładzie warto zwrócić uwagę na brak protokołu w adresie skryptu. Zostanie użyty taki sam protokół jaki został wywołany dla strony www. Zapobiega to zgłaszaniu ostrzeżeń przez przeglądarki internetowe, które reagują, kiedy strona idzie po SSL-u (https) a media są dostarczane do przeglądarki bez szyfrowania (http).

W przypadku plików doładowywanych asynchronicznie trzeba czekać aż zostaną w całości pobrane i będą gotowe do pracy. Jeśli jesteś autorem oprogramowania możesz przyjąć, że w kodzie strony zostaną zdefiniowane jakieś zmienne (np. _gaq), albo funkcje (np. window.fbAsyncInit), które po załadowaniu pliku js zostaną w nim użyte. Tej strategi nie można użyć dla ładowanych asynchronicznie bibliotek takich jak jQuery. To nie jQuery wywoła zdefiniowany przez Ciebie kod tylko Ty chcesz użyć metod oferowanych przez ten rozbudowany framework. Dlatego musisz wykryć moment załadowania pliku i uruchomić swoje funkcje dopiero wtedy kiedy dostępny będzie obiekt jQuery.

JavaScript jest językiem zorientowanym na zdarzenia. Między innymi obsługuje też zdarzenia związane z ładowaniem obrazków czy plików js. Także w tej kwestii IE wyróżnia się spośród innych przeglądarek autorskimi rozwiązaniami i zamiast zdarzenia load ma swoje onreadystatechange. Tak czy owak można to wykorzystać do napisania funkcji umożliwiającej dynamiczne ładowanie skryptów.

(function(global){
    var cache = {}
    var call = function(url, callback) {
        if (typeof(callback) == "function") {
            cache[url].callbacks.push(callback);
        }
        // run only if loaded
        if (cache[url].status == 'loaded') {
            for (i in cache[url].callbacks) {
                cache[url].callbacks[i]();
            }
            // reset callbacks
            cache[url].callbacks = [];
        }
    }
    global.loadScript = function(url, callback){
        if (url in cache) {
            call(url, callback);
            return;
        } else {
            cache[url] = {'status':'init','callbacks':[]};
        }
        var script = document.createElement("script")
        script.type = "text/javascript";
        script.async = true;
        if (script.readyState){ //IE
            script.onreadystatechange = function(){
                if (script.readyState == "loaded" ||
                    script.readyState == "complete"){
                    script.onreadystatechange = null;
                    cache[url].status = 'loaded';
                    call(url, callback);
                }
            };
        } else { //Others
            script.onload = function(){
                cache[url].status = 'loaded';
                call(url, callback);
            };
        }
        script.src = url;
        document.getElementsByTagName("head")[0].appendChild(script);
    }
})(this);
 
 
loadScript('//ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js'
    , function(){
        $('p').attr('style', 'border: 1px solid red');
    });

Funkcja ta choć prosta ma dwie przewagi nad jquerową metodą $.getScript, a mianowicie nie wymaga jQuery (nie używa też do ładowania ajaxa) i dba o to aby każdy skrypt był ładowany tylko raz. Ma to duże znaczenie w dynamicznie bydowanych aplikacjach wykorzystujących wzorzec projektowy opóźnionego ładowania (ang. lazy loading pattern), którego idea zasadza się na myśli, że zasób pobieramy dopiero w chwili kiedy go potrzebujemy.

Koncepcję wzorca opóźnionego ładowania dla języka używanego w przeglądarce trzeba nieco zmodyfikować. W końcu nie wyobrażam sobie aby pobierać TinyMce wraz z jego wszystkimi pluginami dopiero w chwili kiedy użytkownik kliknie w pole tekstowe formularza. Choć jest to technicznie możliwe to z punktu widzenia funkcjonalności byłoby z uwagi na opóźnienia totalnym nieporozumieniem. Chodzi tu raczej o dołączanie do poszczególnych podstron HTML skryptów js tylko tam gdzie są faktycznie potrzebne.

Rozbudowane aplikację mają to do siebie, że składają się z wielu klocków, z których jeden nie musi wiedzieć nic o innym, a przynajmniej nie musi widzieć czy inny moduł załadował już wymagane biblioteki JavaScript. Co z tego, że główny kontent strony stanowi formularz, rejestracji nowego użytkownika a w nagłówku strony umieszczono formularza logowania, który prezentowany jest zresztą standardowo na wszystkich podstronach serwisu. Oba formularze nie mają ze sobą nic wspólnego. Dla każdego z nich można podjąć próbę załadowania jquery.validate. Jeśli skorzysta się z loadScript można mieć pewność, że plugin zostanie pobrany tylko raz.

loadScript('jquery.validate.js', function(){loginFormValidation();});
loadScript('jquery.validate.js', function(){registerFormValidation();});

Kwestia wielokrotnego ładowania tego samego pliku została rozpracowana, jednak mamy do rozwiązania jeszcze jedną zagwozdkę, a mianowicie zależności

Fancybox jest dodatkiem, który świetnie sprawdza się do prezentacji zdjęć na stronach internetowych. Opcjonalnie można go używać z pluginem jquery.mousewheel umożliwiającym przechodzenie pomiędzy zdjęciami w galerii poprzez ruch rolki w myszce. Zanim zainicjujemy fancyboksa byłoby dobrze aby oba pliki były już gotowe do użycia, z kolei oba pluginy wymagają wczesniejszego załadowania podstawowej biblioteki jQuery. Kolejność ładowania skryptów jest zatem istotna.

LABjs udostępnia przyjemny interfejs zoptymalizowany dla celu asynchronicznego, łańcuchowego ładowania skryptów.

<script type="text/javascript" src="script.js"></script>
<script type="text/javascript">
$LAB
    .script("jquery.js")
    .script("jquery.mousewheel.js")
    .script("jquery.fancybox.js")
    .wait(function(){
        $('a.img').fancybox();
    });
</script>

albo jeszcze prościej

$LAB
    .script("jquery.js", "jquery.mousewheel.js", "jquery.fancybox.js")
    .wait(function(){
        $('a.img').fancybox();
    });

Przy użyciu funkcji loadScript trzebaby to było zrobić w mniej zgrabny sposób.

loadScript('jquery.js', function(){
    loadScript('jquery.mousewheel.js', function(){
        loadScript('jquery.fancybox.js', function(){
            $('a.img').fancybox();
    });
    });
});

LABjs ma wszystkie zalety loadScript i dokłada do tego jeszcze trochę od siebie. Szczerze polecam ją do zastosowań profesjonalnych. We wpisie Sposoby wczytywania JavaScript przeczytałem o jeszcze innych podobnych narzędziach.

Podsumowanie

Opisałem zaledwie kilka i to tych podstawowych aspektów pracy z JavaScriptem w projekcie aplikacji internetowej. Jak widać jest on ściśle powiązany z technologiami z którymi koegzystuje i już sam proces jego dołączania może mieć wpływ na funkcjonowanie całości jaką jest strona www.

Wszystkich zachęcam jeszcze raz do przeczytania – przynajmniej najważniejszych moim zdaniem – twierdzeń czy wskazówek zawartych w tym artykule. Wytłuściłem je wszystkie i poprzedziłem słowem UWAGA!, tak więc nie powinno być problemu z ich odnalezieniem.

Lepszy var_dump czyli przyjemniejsze debugowanie PHP

bigWeb/Debug/Dumper to narzędzie funkcjonalnie odpowiadające funkcji var_dump
Jego przewagą jest sposób prezentacji danych, a także dodatkowe informacje
ułatwiające debugowanie aplikacji.

Najpoważniejszą wadą Dumpera jest to, że jest on dość obciążający dla aplikacji
gdyż uzyskanie informacji o pliku i linii, w której dump został wywołany
wymaga każdorazowo rzucenia wyjątku. Dlatego też w wersji produkcyjnej
Dumper powinien być wyłączony

UWAGA! Biblioteka zaprezentowana w przykładach wymaga min PHP 5.3
z uwagi na użycie przestrzeni nazw.
Ponieważ jednak nie na wszystkich serwerach jest już PHP w wersji obsługującej
przestrzenie nazw przygotowałem także wersję Dumpera nie wymagającą ich użycia.
w takim przypadku wywołanie bigWeb\Debug\Dumper::factory(); należy zastąpić
poprzez wywołanie bigWeb_Debug_Dumper::factory(); i analogicznie w przypadku
innych klas. Wersja Dumpera dla PHP < 5.3 zawarta jest w archiwum zip bigWeb\Debug\Dumper do ściągnięcia.

Instalacja

include_once('./Debug.php');

Można też użyć autoloadera. Wszystkie klasy potrzebne do działania Dumpera
zdefiniowane są w pliku „Debug.php”. Wyjątkiem jest FirePHP, który należy
dodać osobno jeśli chcemy wyświetlać dane w konsoli javascript

include_once('./FirePHP.php');

Podstawowe użycie

dump('some data');

w wyniku otrzymamy:

TIME: 20:46:44 FILE: /home/www/bigWeb/Debug/example.php LINE: 37 ET: 0 MU: 1.44 mb MPU: 1.67 mb

‚some data’
string(9) "some data"

Dumpa można wywołać z wieloma parametrami o różnych wartościach równocześnie.

$str = 'text';
$int = 7;
$arr = array('foo', 'bar');
$ob = new ArrayObject();
$bool = FALSE;
 
dump($str, $int, $arr, $ob, $bool);
TIME: 20:46:44 FILE: /home/www/bigWeb/Debug/example.php LINE: 47 ET: 0.000904 MU: 1.47 mb MPU: 1.67 mb

$str
string(4) "text"
$int
int(7)
$arr
array(2) {
  [0]=>
  string(3) "foo"
  [1]=>
  string(3) "bar"
}
$ob
object(ArrayObject)#3 (1) {
  ["storage":"ArrayObject":private]=>
  array(0) {
  }
}
$bool
bool(false)

Ukrywanie komunikatów

Aby zapobiec wyświetlaniu jakichkolwiek komunikatów przez dumpera należy go wyłączyć.

bigWeb\Debug\Dumper::setEnabled(FALSE);
dump('it should not show');

Aby ponownie włączyć:

bigWeb\Debug\Dumper::setEnabled(TRUE);

Definiowanie alternatywnych logerów

Domyślnie dumper do zrzutu danych używa wbudowanej funkcji var_dump jednak
klasa Dumpera jest zbudowana w oparciu o wzorzec projektowy Obserwator, gdzie
obserwatorami są wyspecjalizowane klasy do logowania i prezentacji zrzucanych danych.
Dzięki temu możemy wybierać sposób logowania informacji.

Możemy zapisywać dane w pliku – przydatne np. przy testowaniu przekierowań

$d = bigWeb\Debug\Dumper::factory();
$o = new bigWeb\Debug\Dumper\FileDump();

Koniecznym jest wskazanie katalogu w którym będą zapisywane logi.
Katalog ten musi mieć oczywiście ustawione prawa do zapisu

$o->setDir(dirname(__FILE__));
$d->attach($o);
$d->notify('foo');

wynik zostanie zapisany w pliku /home/www/bigWeb/Debug/2012-01-12.log.php

TIME: 20:46:18 FILE: /home/zh/www/Debug/example.php LINE: 71 ET: 0.001143 MU: 1.55 mb MPU: 1.67 mb
----------| 'foo' |----------
foo
.----------------------------

Można też skorzystać z dobrodziejstw FireBuga – dodatku do Firefoxa i
wyświetlać dane w konsoli javascript. W tym przypadku musimy najpierw
załadować bibliotekę FirePHP będącej „pomostem” pomiędzy PHP i FireBugiem.

include_once('./FirePHP.php');
 
$d = bigWeb\Debug\Dumper::factory();
$d->attach(new bigWeb\Debug\Dumper\FireDump());
$d->notify('bar');

wynik pokarze nam się w konsoli FireBug-a

Wynik Dumpera w konsoli FireBug

UWAGA! Ponieważ informacje do FireBuga są przesyłane za pomocą nagłówków HTTP
biblioteka FirePHP wymaga buforowania wyjścia (output bufering). Koniecznym
jest zatem użycie funkcji ob_start() w przeciwnym razie można się spotkać
z błędem „headers already sent error”

Łańcuch wywołań

Niekiedy chcemy wiedzieć jakie funkcje i metody zostały wywołane nim został
wykonany kod w danym miejscu. Aby zobaczyć cały łańcuch wywołań należy
ustawić flagę show_trace na TRUE.

function foo() {
    $d = bigWeb\Debug\Dumper::factory();
    $d->attach(new bigWeb\Debug\Dumper\VarDump());
    $d->setShowTrace(TRUE);
    $d->notify('Show chain requests');
}
 
function bar() {
    foo();
}
 
bar();
TIME: 20:46:44 FILE: /home/www/bigWeb/Debug/example.php LINE: 97 ET: 0.005149 MU: 1.57 mb MPU: 1.67 mb

$d->notify(‚Show chain requests’);
string(19) "Show chain requests"
Array
(
    [0] => Array
        (
            [file] => /home/www/bigWeb/Debug/example.php
            [line] => 97
            [function] => notify
            [class] => bigWeb\Debug\Dumper
            [type] => ->
            [args] => Array
                (
                    [0] => Show chain requests
                )

        )

    [1] => Array
        (
            [file] => /home/www/bigWeb/Debug/example.php
            [line] => 101
            [function] => foo
            [args] => Array
                (
                )

        )

    [2] => Array
        (
            [file] => /home/www/bigWeb/Debug/example.php
            [line] => 104
            [function] => bar
            [args] => Array
                (
                )

        )

)

Praca na serwerze produkcyjnym

W zasadzie Dumper powinien być wyłączony na serwerze produkcyjnym bo obciąża
aplikację, a ponadto może wyświetlać dane wrażliwe. Nie mniej w pewnych
wyjątkowych okolicznościach możemy chcieć go użyć. Musimy wcześniej przewidzieć
taką sytuację i dodatkowo skonfigurować przynajmniej jedną z instancji Dumpera
ustawiając tak zwany secret_key

$d = bigWeb\Debug\Dumper::factory();
$d->attach(new bigWeb\Debug\Dumper\FireDump());
$d->setSecretKey('verysicretkey');

Aby te dane były widoczne należy wywołać url metodą GET z parametrem
secret_key=1. W tym przypadku będzie to

example.php?verysicretkey=1
bigWeb\Debug\Dumper::setEnabled(FALSE);
$d->notify('Visible when set secret_key');

aby ponownie włączyć:

bigWeb\Debug\Dumper::setEnabled(TRUE);

Jeśli wywołamy url metodą GET z sekretnym kluczem, Dumper ustawia ciasteczko
debugcookie o wartości wywiedzionej z secret_key. Od tej pory nie trzeba już dodawać do
adresu żadnego specjalnego parametru. Jeśli jednak chcielibyśmy wyłączyć
debugowanie należy wywołać url z secret_key=0

example.php?verysicretkey=0

Tworzenie funkcji pomocniczych

Tworzenie obiektu i dodawanie obserwatorów nie jest zbyt wygodnym rozwiązaniem.
Narzędzie do debugowania powinno być extremalnie proste i szybkie w użyciu.
Dlatego warto sobie zdefiniować funkcję pomocniczą – podobną do „debug();”

function dump_all() {
    $_args = func_get_args();
    static $d = null;
    if ( $d === null )
    {
        // Proszę zwrócić uwagę na wywołanie metody factory z parametrem $level = 3
        $d = bigWeb\Debug\Dumper::factory(3);
        $o = new bigWeb\Debug\Dumper\FileDump();
        $o->setDir(dirname(__FILE__));
        $d->attach($o);
        $d->attach(new bigWeb\Debug\Dumper\FireDump());
        $d->attach(new bigWeb\Debug\Dumper\VarDump());
 
    }
    call_user_func_array(array($d, 'notify'), $_args);
}
 
dump_all('foo bar');

wynik zostanie zapisany w pliku

TIME: 20:46:44 FILE: /home/zh/Praca/bigWeb/Debug/example.php LINE: 155 ET: 0.006137 MU: 1.59 mb MPU: 1.67 mb
----------| 'foo bar' |----------
foo bar
.--------------------------------

w konsoli FireBug-a

Wynik Dumpera w konsoli FireBug

oraz wyświetlony w przeglądarce:

TIME: 20:46:44 FILE: /home/www/bigWeb/Debug/example.php LINE: 155 ET: 0.006137 MU: 1.59 mb MPU: 1.67 mb

‚foo bar’
string(7) "foo bar"

UWAGA! Od wartości level zależy prawidłowe wskazanie linii oraz pliku, w którym
wywołano dumpa. Domyślnie level = 1. Jeżeli metoda „notify” jest zagnieżdżona
w funkcji pomocniczej to wartość level powinna być inkrementowana (level = 2).
Jeśli dodatkowo metoda „notify” jest wywoływana za pośrednictwem funkcji
„call_user_func_array” to należy ustawić level = 3

$d = bigWeb\Debug\Dumper::factory(1);
 
function d1($param) {
    $d = bigWeb\Debug\Dumper::factory(2);
    // ...
    $d->notify($param);
}
 
function d2($param) {
    $_args = func_get_args();
    $d = bigWeb\Debug\Dumper::factory(3);
    // ...
    call_user_func_array(array($d, 'notify'), $_args);
}

UWAGA! Jeśli masz już zdefiniowaną funkcję „dump” w swojej aplikacji
to po dołączeniu kodu Dumpera otrzymasz wszystkomówiący wyjątek. Poinformuje
Cię on, że musisz zdefiniować sobie funkcję pomocniczą o innej nazwie.
Należy zakomentować kod wywołujący wyjątek lub też dołączyć kod Dumpera
w sposób umożliwiający przechwycenie wyjątka i utworzyć funkcję pomocniczą
np. o nazwie „d”.

try { include_once("Debug.php"); } catch (bigWeb\Debug\Exception $e) {
    function d() {
        $_args = func_get_args();
        static $d = null;
        if ( $d === null )
        {
            $d = bigWeb\Debug\Dumper::factory(3);
            $d->attach(new bigWeb\Debug\Dumper\VarDump());
 
        }
        call_user_func_array(array($d, "notify"), $_args);
    }
}

Napisana przeze mnie klasa nie zastąpi zaawansowanych i rozbudowanych narzędzi debugowania i profilowania aplikacji jednak jest prosta w użyciu nie związana stricte z żadnym frameworkiem, przez co łatwo ją zaadaptować zarówno do pracy z Zend Frameworkiem, Symfony, Kohaną czy jakimkolwiek innym – napisanym w PHP – skryptem. Biblioteka zwraca wyniki w postaci pokolorowanego kodu przy okazji pokazując czas wykonania oraz wielkość użytych zasobów. Zapomniany var_dump potrafi zmusić programistę do przeszukiwania plików projektu. W przypadku Dumpera nie ma takiego zagrożenia gdyż każdorazowo wskazuje on ścieżkę do pliku oraz nr linii, w której funkcja robiąca zrzut danych została wywołana.

Serdecznie zapraszam wszystkich do wypróbowania Dumpera oraz wszelkich uwag na temat wdrożeń i ewentualnych błędów.

PHP – operacje na bitach w praktyce

Na początku mojej nauki PHP kupiłem sobie książkę „PHP4 Aplikacje” (Tobiasa Ratschiller i Till Gerken – Wydawnictwo Robomatic). Zawarta w tej lekturze tematyka była wtedy dla mnie zbyt zaawansowana i potem wielokrotnie wracałem do tej pozycji stopniowo dojrzewając do poruszanych w niej tematów. Najdłużej wzbraniałem się przed zgłębieniem wiedzy dotyczącej operacji na bitach. Dzisiaj nie wiem właściwie dlaczego bo zagadnienie jest całkiem proste, a rozwiązania oparte o system binarny mają wiele zalet.

Najpowszechniej chyba spotykanym przypadkiem stosowania wartości bitowych są wszelkiego rodzaju systemy uprawnień. Każdy chyba programista PHP zaprzyjaźnił się z dyrektywami

ini_set('display_errors', 1);
ini_set('error_reporting', E_ALL);

Druga z wymienionych dyrektyw ma też odpowiadającą jej funkcję „error_reporting”, która jako argument przyjmuje pozom raportowania błędów. Poziom ten można przekazać w postaci maski bitowej złożonej ze stałych odzwierciedlających wartości przypisane poszczególnym rodzajom błędów PHP.

Wartość bitowe stałych nie są jak widać w formacie binarnym tylko dziesiętnym.

stała zapis w formacie dwójkowym (binarnym) zapis w formacie dziesiętnym
E_ERROR 00000001 1
E_WARNING 00000010 2
E_PARSE 00000100 4
E_NOTICE 00001000 8

Do przeliczania wartości binarnych na dziesiętne służy w PHP funkcja bindec, a z dziesiętnych na binarne decbin.

I tak wywołanie …

error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE);

spowoduje że wyświetlane będą wszystkie błędy czasu wykonania, ostrzeżenia, błędy parsowania oraz uwagi.

Z kolei użycie takiej maski …

error_reporting(E_ALL &~ E_NOTICE);

pozwoli na ukrycie wszystkich mało ważnych uwag. Natomiast wszystkie pozostałe błędy i bardziej ważne ostrzeżenia będą dalej raportowane.

W celu zrozumienia wyżej zaprezentowanych operacji niezbędnym będzie poznanie operatorów logicznych. Ambitnych z kolei odsyłam do Algebry Boole’a, choć nie zachęcam zbyt mocno.

Czasami zbyt gruntowny wykład teoretyczny utrudnia zrozumienie prostych spraw, które podawane w małych porcjach powoli poszerzają horyzonty. Czytałem kilka prac poświęconych operacjom na bitach, w których była cała masa operacji na 0 (zerach) i 1 (jedynkach), wiele tabel, wzorów, równań. Wiele z nich wspominało o prawach De Morgana czy postulatach Huntigtona, ale mało który materiał traktował o tym jak tego używać. Dopiero niedawno kolega podesłał mi link do artykułu, który podchodzi do tematu z praktycznej strony.

Nospor w jednym z wpisów na swoim blogu pt. opcje dwuwartościowe prezentuje studium przypadku użycia operacji bitowych. Proponuje zastąpienie kilku flag – czyli dwustanowych pól przyjmujących wartość logiczną TRUE lub FALSE – w tabeli w bazie danych, jednym polem przechowującym wartość bitową.

Polega to konkretnie na tym, że zamiast mieć w tabeli trzy pola i trzy indeksy:

CREATE TABLE `offer` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(32) NOT NULL,
  `is_active` tinyint UNSIGNED NOT NULL DEFAULT 0,
  `is_promotion` tinyint UNSIGNED NOT NULL DEFAULT 0,
  `is_sale` tinyint UNSIGNED NOT NULL DEFAULT 0,
  PRIMARY KEY  (`id`),
  KEY `is_active` (`is_active`),
  KEY `is_promotion` (`is_promotion`),
  KEY `is_sale` (`is_sale`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Można je zastąpić jednym polem i jednym indeksem.

CREATE TABLE `offer` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(32) NOT NULL,
  `options` tinyint UNSIGNED NOT NULL DEFAULT 0,
  PRIMARY KEY  (`id`),
  KEY `options` (`options`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Dla trzech, niezależnych od siebie opcji powyższe rozwiązanie nie jest zbyt wygodne, ale dla potrzeb edukacyjnych wystarczy. Niżej pokazuję bardziej adekwatny przypadek użycia, tymczasem wyjaśniam że aby móc zapisać w jednym polu wartości kilku opcji trzeba najpierw każdej opcji przypasować bit.

// Normalnie staram się nie nadużywać stałych 
// jednak tutaj zrobiłem to dla jasności przykładów
define('OPT_NONE', 0);_
define('OPT_IS_ACTIVE', 1);
define('OPT_IS_PROMOTION', 2);
define('OPT_IS_SALE', 4);

Przy okazji chciałbym wspomnieć że poniższe zapisy są tożsame.

1 1 << 0 bindec(‚00000001’)
2 1 << 1 bindec(‚00000010’)
4 1 << 2 bindec(‚00000100’)

Wiedząc jaki bit oznacza jaką opcję możemy zapisać tę informację w bazie danych. W tym celu trzeba zsumować poszczególne opcje przy pomocy logicznego operatora OR „|”.

// Wszystkie opcje
$options = (OPT_IS_ACTIVE | OPT_IS_PROMOTION | OPT_IS_SALE);
// otrzymana wartość to 7
 
// Oferta jest aktywna i w wyprzedaży, ale nie w promocji
$options = (OPT_IS_ACTIVE | OPT_IS_SALE);
// otrzymana wartość to 5
 
// Oferta jest w promocji, ale nie jest aktywna ani w wyprzedaży
$options = OPT_IS_PROMOTION;
// otrzymana wartość to 2

Kiedy już przypiszemy ofercie jakieś opcje chcielibyśmy sprawdzić czy takową posiada.

// Oferta jest aktywna i w wyprzedaży, ale nie w promocji
$options = (OPT_IS_ACTIVE | OPT_IS_SALE);
 
// Upewniamy się czy oferta jest aktywna
($options & OPT_IS_ACTIVE) > 0 ? 'yes' : 'no';
// otrzymany wynik yes
 
// Sprawdzamy czy jest w promocji
($options & OPT_IS_PROMOTION) > 0 ? 'yes' : 'no';
// otrzymany wynik no
 
// Sprawdzamy czy jest aktywna i w wyprzedaży
($options & (OPT_IS_ACTIVE | OPT_IS_SALE)) == ((OPT_IS_ACTIVE | OPT_IS_SALE)) ? 'yes' : 'no';
// otrzymany wynik yes
 
// Sprawdzamy czy jest w promocji i/lub w wyprzedaży
($options & (OPT_IS_PROMOTION | OPT_IS_SALE)) > 0 ? 'yes' : 'no';
// otrzymany wynik yes
 
// Sprawdzamy czy jest w promocji albo w wyprzedaży
($options & (OPT_IS_PROMOTION | OPT_IS_SALE)) != (OPT_IS_PROMOTION | OPT_IS_SALE) ? 'yes' : 'no';
// otrzymany wynik yes

Proszę zwrócić uwagę na zastosowane nawiasy. Operatory logiczne mają priorytet niższy od operatorów arytmetycznych, a także od operatorów porównania więc jeśli wykonamy test (2 | 4 == 6) to z pewnością otrzymamy inny wynik niż jeśli zastosujemy następujący zapis ((2 | 4) == 6)

Opcje możemy modyfikować

// Oferta jest aktywna i w wyprzedaży, ale nie w promocji
$options = (OPT_IS_ACTIVE | OPT_IS_SALE);
// 5
 
// Dodajemy opcję w promocji
$options |= OPT_IS_PROMOTION;
// 7
 
// Usuwamy opcję w wyprzedaży
$options &= ~OPT_IS_SALE;
// 3
 
// Usuwamy też opcję jest aktywna
$options ^= OPT_IS_ACTIVE;
// 2
 
// I dodajemy ją spowrotem
$options ^= OPT_IS_ACTIVE;
// 3

Jak widać w ostatnim przykładzie operator ^ jest przełącznikiem, który usuwa bit jeśli jest ustawiony i ustawia jeśli nie jest ustawiony.

Przykład opcji – zapożyczony zresztą od Nospora – nie prezentuje pełnego potencjału bitów, które nie bez powodu są często używane przy konstruowaniu wszelkiego rodzaju systemów uprawnień. Oprócz oszczędności miejsca za pomocą szablonów bitowych można w łatwy sposób zaimplementować dziedziczenie uprawnień.

Stwórzmy kilka stref dostępu, a następnie grupy użytkowników, o różnych poziomach uprawnień pozwalających na dostęp do poszczególnych stref.

strefy dostępu

  • Strefa dla wszystkich – dostęp do niej powinni mieć wszyscy użytkownicy.
  • Strefa dla użytkowników uwierzytelnionych. Zalogowani użytkownicy powinni mieć dostęp do tego co użytkownicy anonimowi oraz do kilku innych funkcjonalności
  • Strefa dla moderatorów – moderatorzy mogą z założenia wszystko to co użytkownicy zalogowani, ale mają też funkcje edycyjne.
  • Strefa dla sponsorów – ich pole obejmuje zakres aktywności użytkowników zalogowanych oraz częściowo pokrywa się ze strefą moderatora. Nie mogą jednak edytować treści, za to mają dostęp do raportów, które zwykły moderator nie ma prawa widzieć
  • Administrator jak to zwykle bywa może wszystko

Przypiszmy każdej ze stref bit.

$for_logged = 1;
$for_moderators = 2;
$for_sponsors = 4;
$for_administrators = 8;

Następnie poszczególnym grupom użytkowników ustawmy taki szablon bitowy, który pozwoli im na dostęp do określonych stref. Przy pomocy szablonów bitów stosunkowo łatwo jest zdefiniować hierarchię grup użytkowników pozwalającą zrealizować założenie dziedziczenia uprawnień.

$logged_user = 1;
$moderator = $logged_user | 2; // 3
$sponsor = $logged_user | 4; // 5
$admin = $moderator | $sponsor | 8; // 15

Przetestujmy!

$bob = $sponsor; // 5
 
// Czy Bob ma dostęp do strefy zalogowanych użytkowników?
($for_logged & $bob) > 0 ? 'yes' : 'no';
// wynikiem jest yes
 
// Czy Bob ma dostęp do strefy moderatora?
($for_moderators & $bob) > 0 ? 'yes' : 'no';
// wynikiem jest no
 
// Czy Bob ma dostęp do strefy sponsorów?
($for_sponsors & $bob) > 0 ? 'yes' : 'no';
// wynikiem jest yes
 
// Czy Bob ma dostęp do strefy administratorów?
($for_administrators & $bob) > 0 ? 'yes' : 'no';
// wynikiem jest no

Operatory bitowe wyglądają tak samo w PHP, Pythonie czy MySQL-u. Można zapisać strefy, grupy i użytkowników bazie danych i większość operacji wykonać za pomocą zapytań sql-owych.

CREATE TABLE `zones` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`name` VARCHAR( 255 ) NOT NULL ,
`level` TINYINT NOT NULL DEFAULT '0'
) ENGINE = MYISAM ;
 
INSERT INTO `zones` (`id`, `name`, `level`) VALUES 
(1, 'for_logged', '1'), (2, 'for_moderators', '2'), 
(3, 'for_sponsors', '4'), (4, 'for_administrators', '8');
 
CREATE TABLE `groups` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`name` VARCHAR( 255 ) NOT NULL ,
`perms` TINYINT NOT NULL DEFAULT '0'
) ENGINE = MYISAM ;
 
INSERT INTO `groups` (`id`, `name`, `value`) VALUES 
(1, 'logged_user', '1'), (2, 'moderator', '3'), 
(3, 'sponsor', '5'), (4, 'administrator', '15');
 
CREATE TABLE `users` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`username` VARCHAR( 255 ) NOT NULL ,
`groups_id` INT UNSIGNED NOT NULL
) ENGINE = MYISAM ;
 
INSERT INTO `users` (`id`, `username`, `groups_id`) VALUES 
('1', 'Frank', '1'), ('2', 'Charlie', '2'), ('3', 'Bob', '3'), 
('4', 'Jon', '4'), ('5', 'Mary', '1');

Poniżej użyłem podzapytań w celu pobrania listy użytkowników, którzy mają dostęp do strefy dla użytkowników zalogowanych, a następnie dla moderatorów.

SELECT username FROM `users` AS u INNER JOIN groups AS g ON u.groups_id = g.id 
WHERE g.perms & (SELECT z.level FROM zones AS z WHERE z.name = 'for_logged');
// wynikiem jest lista: Frank, Mary, Charlie, Bob, Jon
 
SELECT username FROM `users` AS u INNER JOIN groups AS g ON u.groups_id = g.id 
WHERE g.perms & (SELECT z.level FROM zones AS z WHERE z.name = 'for_moderators');
// wynikiem jest lista: Charlie, Jon

Na zakończenie chciałbym zaprezentować mały testowy skrypt. Jeśli nie chce Ci się analizować kodu, skopiuj go, zapisz w pliku i uruchom, a wszystko stanie się jasne.

/**
 * Rodzaje powiadomień
 */
$notification_methods = array(
    'email' => 1,
    'internal_message' => 2,
    'notification' => 4,
    'wall' => 8
);
 
 
$nm = $notification_methods;
 
/**
 * Rodzaje zdarzeń
 *
 * Do każdego rodzaju zdarzeń przypisane są dopuszczalne rodzaje powiadomień
 * np. o prywatnej wiadomości można powiadomić mailem lub za pośrednictwem
 * wewnętrznej wiadomości, ale nie wolno wyświetlić tej informacji na ścianie
 */ 
$event_types = array(
    'newsletter'        => $nm['email'],
    'invite_to_friends' => $nm['internal_message'] | $nm['notification'],
    'priv_message'      => $nm['email'] | $nm['internal_message'],
    'image_comment'     => $nm['internal_message'] | $nm['notification'] | $nm['wall']
);
 
 
print '<form action="" method="post">';
foreach ($event_types as $event_name => $avaliable_noti_methods) {
    echo '<p>';
    echo '<strong>'.$event_name.'</strong><br>';
    foreach ($notification_methods as $noti_name => $noti_val) {
        if ($avaliable_noti_methods & $noti_val) {
            $checked = (isset($_POST[$event_name.'-'.$noti_name]) ? 'checked="checked"' : '');
            echo '<input type="checkbox" name="'.$event_name.'-'.$noti_name.'[]" id="id_'.$event_name.'-'.$noti_name.'" '.$checked.'>';
            echo '<label for="id_'.$event_name.'-'.$noti_name.'">'.$noti_name.'</label><br>';
        }
    }
    echo '</p>';
}
echo '<p><input type="submit" name="ok" value="OK"></p>';
echo '</form>';
 
if (isset($_POST['ok'])) {
 
    $user_notification_settings = array_combine(array_keys($event_types), array_fill(0,4,0));
    foreach (array_keys($_POST) as $key) {
        $values = explode("-", $key);
        if (count($values) != 2) continue;
        list($event_name, $noti_name) = $values;
 
        if (!isset($user_notification_settings[$event_name])) {
            $user_notification_settings[$event_name] = $notification_methods[$noti_name];
        } else {
            $user_notification_settings[$event_name] |= $notification_methods[$noti_name];
        }
    }
 
    printf('<strong>Ustawienia wybrane przez użytkownika</strong>%s', print_r($user_notification_settings, true));
 
    print '<strong>Podsumowanie</strong><br>';
    foreach ($user_notification_settings as $event_name => $avaliable_noti_methods) {
        foreach ($notification_methods as $noti_name => $noti_val) {
            if ($avaliable_noti_methods & $noti_val) {
                print $event_name.' - tak - '.$noti_name.'<br>';
            } else {
                print $event_name.' - NIE - '.$noti_name.'<br>';
            }
        }
    }
}

Nice url – czyli przyjazne linki

Jakiś czas temu pisałem o routingu w Kohana 3.1, w którym to wpisie stwierdziłem, że nie działają mi nazwane podwzorce (named subpattern) pomimo tego że mam wersję PHP wyższą niż minimalna wymagana 5.2.2. Trafiłem jednak na wpis nospora „Ładne url’e (nice url)” i postanowiłem mimo wszystko powrócić do tematu routingu i powalczyć z tymi wyrażeniami regularnymi.

Może to kwestia konfiguracji komputera – gdyż eksperyment przeprowadzałem na innym kompie niż poprzednio ale tym razem okazało się, że jednak da się w PHP nadawać nazwy wycinkom wzorca.

<?php
// Przykładowe urle
$urls = array(
    '/pl/news/list/1/',
    '/news/list/1/',
    '/news/list/',
);
// Ścieżka która pasuje do powyższych urli - parametry lang i page są opcjonalne
$route = '%^/((?P<lang>\w{2})/)?(?P<controler>\w+)/(?P<method>\w+)/((?P<page>\d+)/)?$%';
 
// Definicja wartości domyślnych parametrów opcjonalnych
$defaults = array('lang' => 'pl', 'page' => 1);
 
// Odczytanie ze ścieżki nazw parametrów
preg_match_all('/P\<(\w+)\>/', $route, $matches, PREG_PATTERN_ORDER);
$params = $matches[1];
 
foreach ($urls as $url) {
    // Założyłem w przykładzie, że wszystkie urle pasują do ścieżki 
    // więc nie sprawdzam czy tak jest
    preg_match($route, $url, $matches);
    $vars = array();
    foreach ($params as $param) {
        // Oczytanie z urla wartości parametrów lub nadanie wartości domyślnych
        $vars[$param] = (isset($matches[$param]) && !empty($matches[$param])) 
            ? $matches[$param] : $defaults[$param];
    }
    print $url."<br />"; print_r($vars);
}
?>

Wynikiem wywołania powyższego kodu jest

/pl/news/list/1/

Array
(
    [lang] => pl
    [controler] => news
    [method] => list
    [page] => 1
)

/news/list/1/

Array
(
    [lang] => pl
    [controler] => news
    [method] => list
    [page] => 1
)

/news/list/

Array
(
    [lang] => pl
    [controler] => news
    [method] => list
    [page] => 1
)

Wyżej przytoczony przykład to tylko proof of concept, którego stworzenie nie zajęło mi więcej czasu niż wpisanie tych paru słów komentarza. Daje on jednak pojęcie jak łatwe i przyjemne jest stworzenie routingu w oparciu o nowe możliwości wyrażeń regularnych (nowe w PHP :)).

Kohana 3 mod_rewrite i błąd „No input file specified”

Stawiałem już projekty oparte na frameworku Kohana 3 na różnych serwerach. Jak dotąd zawsze działał mi plik .htaccess o treści:

# Turn on URL rewriting
RewriteEngine On
 
# Installation directory
RewriteBase /
 
# Protect application and system files from being viewed
RewriteRule ^(?:application|modules|system)\b - [F,L]
 
# Allow any files or directories that exist to be displayed directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
 
# Rewrite all other URLs to index.php/URL
RewriteRule .* index.php/$0 [PT]

(No ok mój plik htaccess jest bardziej robudowany, ale dla przejrzystości problemu podaję wersję minimalną – standardzik.)

Dzisiaj jednak szybkie i przyjemne przerzucenie projektu na serwer docelowy zostało brutalnie zburzone przez niemiły komunikat „No input file specified”, który pokazywał mi się przy próbie przejścia na dowolną podstronę.

Początkowo próbowałem użyć dyrektywy .htaccess-a

Options -MultiViews

Przy jakimś projekcie to mi kiedyś pomogło o ile dobrze kojarzę, ale tym razem nie.

Problematyczna okazała się linijka:

RewriteRule .* index.php/$0 [PT]

, którą na wszelkich forach, blogach itd. proponowano zastąpić na kilka różnych sposobów

RewriteRule .* index.php?$0 [PT,L,QSA]

albo

RewriteRule .* index.php [L]

jednemu podobno zadziałało coś takiego

RewriteRule .* index.php?kohana_uri=$0 [PT,L,QSA]

Mnie zadziałało dowolne z powyższych rozwiązań, ale tylko połowicznie. To znaczy – komunikat błędu zniknął, ale za to bez względu na wybraną podstronę zawsze pokazywała mi się strona główna. Nienawidzę takich zagadek.

Przeanalizowałem zawartość tablicy $_SERVER na moim serwerze testowym oraz produkcyjnym i wyszło mi, że na tym drugim brakuje PATH_INFO. Widać routing Kohany (przynajmniej w wersji 3.0) korzysta z tej wartości i bez niej zwyczajnie przestaje działać.

Ostatecznym remedium na mój kłopot okazało się prostackie obejście

$_SERVER['PATH_INFO'] = $_SERVER['REQUEST_URI'];

Ja osobiście dodałem to w bootstrapie.

Routing w Kohana 3.1

Z frameworkiem Kohana pracuję już kilka lata. O ile Kohana w wersji 2 była przyjazna i w miarę dobrze udokumentowana o tyle 3-cie wydanie tego frameworka jest już mniej przyjazne. Nie zrozumcie mnie źle – ogólnie zmiany w architekturze i implementacji oceniam pozytywnie, ale ogromne braki w dokumentacji i ciągłe zmiany w kodzie mają zdecydowanie negatywny wpływ na przyjemność pracy z tym oprogramowaniem.

Zabieram się właśnie za nowy projekt. Jest niewielki więc postanowiłem wypróbować Kohanę w najnowszej „stabilnej” wersji 3.1. Utworzyłem katalog dla projektu, przekopiowałem do niego pliki frameworka, utworzyłem plik hteaccess według wzoru i odpaliłem w przeglądarce adres.

http://localhost/project/

Uruchomił mi się instalator, który poinformował mnie, że muszę nadać prawa do zapisu odpowiednim katalogom. Pozostałe wymagane warunki miałem spełnione. Nie miałem PECL HTTP i cURL ale to opcjonalne zależności bez, których wszystko, a przynajmniej fundamenty Kohany powinny ruszyć więc je zignorowałem. Zgodnie z instrukcją zmieniłem nazwę plikowi install.php i…

Otrzymałem komunikat błędu

HTTP_Exception_404 [ 404 ]: The requested URL project/index was not found on this server.

Popularny błąd 404 – not found – wydawałoby się i wszystko jasne. Tylko, że to pierwsze uruchomienie frameworka, w który jest wstępnie skonfigurowany, posiada domyślny kontroler „welcome” a w nim domyślną akcję „index” tak więc powinienem zobaczyć przyjazny napis „hello, world!”.

Gdyby to było moje pierwsze zetknięcie z frameworkiem Kohana to pewnie po pierwszych 5 min. dałbym sobie spokój. Nie dość, że przy pierwszym uruchomieniu dostajemy exceptiona to jeszcze tak naprawdę nic nam on nie mówi o możliwościach jego rozwiązania. W końcu kontroler jest tam gdzie trzeba, klasa ma odpowiednią nazwę i jest metoda, która zgodnie z dokumentacją powinna zostać wywołana.

Tymczasem przyczyną jest zła konfiguracja. W pliku /application/bootstrap.php mamy taki fragment kodu

Kohana::init(array(
    'base_url'   => '/',
));

Parametr base url w moim przypadku powinien wyglądać tak

Kohana::init(array(
    'base_url'   => '/project/',
));

Proste prawda? Tylko ktoś kto dopiero poznaje tę platformę się tego nie domyśli. Przyznaję się, że sam się trochę tego naszukałem, a to dlatego, że kiedyś już rozwiązałem ten problem kawałkiem uniwersalnego kodu i zdążyłem o nim zapomnieć. Tymczasem teraz stawiając nowy projekt na świeżutkiej Kohanie prosty problemik wrócił do mnie i trafił w głowę.

$dirname = dirname($_SERVER['SCRIPT_NAME']);
$base_url = preg_replace('@/+$@', '', $dirname=="\\"?'':$dirname).'/'; 
 
Kohana::init(array(
    'base_url'   => $base_url,
));

Dla purystów, którzy nie lubią zbędnych operacji i nie ufają zbytnio uniwersalności tego rozwiązania (w końcu było testowane tylko na serwerach z Apache 2) proponuję nieco zmodyfikowaną wersję

Kohana::init(array(
	'base_url'   => '/',
));
 
if (Kohana::$environment != Kohana::PRODUCTION) {
    $dirname = dirname($_SERVER['SCRIPT_NAME']);
    $base_url = preg_replace('@/+$@', '', $dirname=="\\"?'':$dirname).'/';
    if (Kohana::$base_url != $base_url) {
        throw new Kohana_Exception(sprintf('Perhaps you have a bad parameter set 
            base_url in bootstrap.php. Most likely, the correct value should be 
            "%s"', $base_url));
    }
}

Routing w Kohana nie jest szczytem elegancji, wygody i elastyczności. W stosunku do routingu w Django, każdy tego typu system w PHP jest prymitywną próbą naśladownictwa. Wynika to w dużej mierze z ograniczeń wyrażeń regularnych w PHP, które aż do wersji 5.2.2 (PCRE 7.0) – przynajmniej według dokumentacji – nie obsługiwały nazwanych podwzorców (named subpattern).

$str = 'foobar: 2008';
preg_match('/(?P<name>\w+): (?P<digit>\d+)/', $str, $matches);
print_r($matches);
//Array
//(
//    [0] => foobar: 2008
//    [name] => foobar
//    [1] => foobar
//    [digit] => 2008
//    [2] => 2008
//)

Ja mam na swoim kompie PHP 5.3.5 (PCRE 8.12) i dalej mi to nie działa.

Nie mniej routing w Kohana 3.1 wzbogacił się o możliwość definiowania ścieżek z użyciem funkcji lambda lub callback – w zależności od wersji PHP oczywiście. Daje to spore możliwości, których namiastkę spróbuję teraz zaprezentować.

Standardowe ustawienia routingu w Kohana Framework wyglądają następująco.

Route::set('default', '(<controller>(/<action>(/<id>)))')
	->defaults(array(
		'controller' => 'welcome',
		'action'     => 'index',
	));

Powoduje to, że do strony głównej serwisu możemy się odwołać w trojaki sposób.

http://domena.com/
http://domena.com/welcome/
http://domena.com/welcome/index

Jest to problem SEO tylko jeśli w projekcie budujemy różne linki odwołujące się do strony głównej. Jeśli dojdzie do tego wielojęzyczność do adresu zostanie dodany dodatkowy parametr (no chyba, że strony w poszczególnych językach trzymane są na subdomenach). Wtedy strona główna będzie występować w różnych wersjach językowych, a dodatkowo strona główna w języku domyślnym będzie w wersji z oznaczeniem języka i bez.

http://domena.com/
http://domena.com/pl-pl/

Dobrze byłoby zrobić aby strona główna w domyślnej wersji językowej była dostępna tylko pod adresem „/”. Natomiast próba wejścia przez uri „/pl-pl/” kończyła się przekierowaniem na „/”. Po wpisaniu w adres przeglądarki adresów „/welcome/” lub „/welcome/index” powinien być zgłoszony błąd 404.

Aby to osiągnąć stworzyłem klasę ProcessRoute w pliku /application/classes/processroute.php

<?php defined('SYSPATH') or die('No direct script access.');
 
class ProcessRoute {
    public static function main_page($uri) {
        $base_url = Kohana::$base_url;
        if ($uri == I18n::$lang) {
            header("Location: {$base_url}", true, 302); die();
        } else if ($uri == '') {
            if ($_SERVER["REQUEST_URI"] != $base_url) {
                throw new HTTP_Exception_404('Unable to find a route to match the URI: :uri'
                , array(':uri' => str_replace($base_url, '', $_SERVER["REQUEST_URI"])));
            }
            return array(
		        'lang' => 'pl-pl',
		        'directory' => '',
		        'controller' => 'welcome',
		        'action' => 'index',
	        );
        } else if (preg_match('/^[a-z]{2,2}-[a-z]{2,2}$/', $uri)) {
            return array(
		        'lang' => $uri,
		        'directory' => '',
		        'controller' => 'welcome',
		        'action' => 'index',
	        );
        }
        return false;
    }
}

Myślę, że dodatkowego wyjaśnienia wymaga jedynie fragment

if ($_SERVER["REQUEST_URI"] != $base_url) {
    throw new HTTP_Exception_404('Unable to find a route to match the URI: :uri'
    , array(':uri' => str_replace($base_url, '', $_SERVER["REQUEST_URI"])));
}

Otóż w przypadku wpisania w pole adresu przeglądarki urla „http://localhost/project/index/” wartość uri przekazana do metody ProcessRoute::main_page będzie miała pustą wartość „”. Jest to ewidentny bug i dlatego musiałem zastosować to nieeleganckie obejście.

Aby podpiąć wyżej zaprezentowaną klasę do routingu należy w bootstrapie dodać

/**
 * Set the routes. Each route must have a minimum of a name, a URI and a set of
 * defaults for the URI.
 */
Route::set('main', array('ProcessRoute', 'main_page'));

Na stronie głównej budowa witryny internetowej się nie kończy. Podpięcie funkcji zwrotnej pod routing daje o wiele większe możliwości. Załóżmy, że chcę wyświetlać dane kontaktowe różne dla różnych wersji językowych.

class Controller_Contact extends Controller {
 
    public function action_index()
    {
        $lang = Request::current()->param('lang');
        if ($lang == 'en-en') {
            $this->response->body('Contact');
        } else {
            $this->response->body('Kontakt');
        }
    }
} // Contact

Chciałbym aby polską wersję podpiąć pod adres „/pl-pl/kontakt”, a w wersję angielską pod „/en-en/contact”. W tym celu stworzyłem sobie plik konfiguracyjny /application/config/routes.php.

return array
(
	'pl-pl' => array(
	    'kontakt' => array(
		    'controller' => 'contact',
		    'action' => 'index',
	    )
	),
	'en-en' => array(
	    'contact' => array(
		    'controller' => 'contact',
		    'action' => 'index',
	    )
	)
);

Aby routing korzystał z tego pliku konfiguracyjnego do klasy ProcessRoute (w pliku /application/classes/processroute.php) dodałem statyczną metodę „static_pages”.

class ProcessRoute {  
    public static function main_page($uri) {
        // ...
    }
    public static function static_pages($uri) {
        // $lang = I18n::$lang;
        if (preg_match('/^([a-z]{2,2}-[a-z]{2,2})\/(.*)/', $uri, $matches) && count($matches) == 3) {
            $lang = $matches[1];
            // if ($lang == I18n::$lang) return false;
            $uri = $matches[2];
        }
        $routes = Kohana::config("routes")->as_array();
        if (!isset($lang) || !isset($routes[$lang])) {
            return false;
        } else {
            $routes = $routes[$lang];
        }
        if (array_key_exists($uri, $routes)) {
            $route = $routes[$uri];
            $route['lang'] = $lang;
            return $route;
        } 
        return false;
    }
}

Proszę zwrócić uwagę na wykomentowaną linię.

$lang = I18n::$lang;

Chciałem mieć pewność, żeby za wyjątkiem strony głównej na wszystkich podstronach był używany parametr wersji językowej. Mogę odkomentować wspomnianą linię i wtedy dane kontaktowe pojawią się zarówno po wybraniu adresu „/pl-pl/kontakt”, jak i „/kontakt”. Jeśli do tego odkomentuję jeszcze linię

if ($lang == I18n::$lang) return false;

wtedy z kolei dane kontaktowe w domyślnej wersji językowej będą dostępne jedynie pod adresem „/kontakt”.

Zapomniałbym. W bootstrapie trzeba jeszcze wywołać metodę ProcessRoute::static_pages.

Route::set('static_pages', array('ProcessRoute', 'static_pages'));

Jak pokazują te proste przykłady routing w Kohana 3.1 zyskał na elastyczności. Odrobina pracy i można by z tego mechanizmu wycisnąć dużo więcej. Panel administracyjny można oprzeć o tradycyjny routing oparty na kontrolerach i akcjach z kolei część publiczną adresować z wykorzystaniem plików konfiguracyjnych lub też trzymać ścieżki w bazie danych.

Kilka reflaksji na temat frameworków

Ostatnio – po długiej przerwie – wszedłem na blog Zyxa wierząc, że znajdę tam coś ciekawego do przeczytania. Nie zawiodłem się. Znalazłem dwie recenzje najpopularniejszych w Polsce frameworków PHP tj. Symfony 1.4 okiem Zyxa i Zend Framework także okiem Zyxa.

Ostatnio przerzuciłem się na Pythona i Django, a wcześniej przez co najmniej dwa lata budowałem aplikacje w oparciu o Kohanę, jednak od czasu do czasu – w tak zwanym międzyczasie – próbowałem też coś sklecić na ZF i polubić Symfony dlatego też orientuję się przynajmniej pobieżnie w ich konstrukcji. Artykuły Zyxa i własne doświadczenie skłoniły mnie do kilku refleksji na temat ogólnej konstrukcji frameworków, sensu użycia ORM-ów a także systemów szablonów.

Architektura

Dobrze zaprojektowana struktura plików to podstawa porządnego frameworka. Każdy szanujący się projektant stara się utrzymać porządek w swoim projekcie i lubi bez zbędnego zastanawiania się wiedzieć gdzie ma czego szukać. Kohana umożliwia tworzenie modułów z których każdy może mieć swoje kontrolery, modele, widoki, pliki konfiguracyjne, a nawet biblioteki. Układ katalogów jest rozbudowany. Kaskadowość z jednej strony umożliwia elastyczność np. nadpisanie konfiguracji domyślnej, konfiguracją specyficzną dla danego modułu. (w Kohanej nie tylko konfigurację można nadpisać ale też „wymienić” klasy z zachowaniem ich dotychczasowej nazwy. Założenie to w PHP wyklucza proste użycie dziedziczenia dlatego też w Kohanie 2 było to zrobione po chamsku z użyciem evala. W wersji 3 tego frameworka mechanizm ten został bardziej elegancko zaimplementowany). Z drugiej strony przy bardziej rozbudowanych projektach człowiek zaczyna się gubić nie pamiętając niekiedy skąd się wzięła bieżąca wartość danej zmiennej konfiguracyjnej. Zaczyna się szukanie. Pół biedy, kiedy struktura katalogów jest płaska, ale jeśli musimy przeklikać się przez kilka poziomów zagłębień staje się to męczące.

Nie wiem kto na to wpadł, aby każdą klasę trzymać w osobnym pliku, nie wiem też kto wymyślił aby nazwa klasy odzwierciedlała położenie pliku w strukturze katalogów (patrz ZF, PEAR), ale doprowadziło to do powstania całej masy katalogów i podkatalogów i jeszcze większej liczby plików, z których niektóre np. zawierają jedynie jednolinijkową definicję wyjątku. W Django w poszczególnych app-sach znajdziemy z reguły pliki (__init__.py, views.py, models.py, urls.py, admin.py, tests.py) i to w 90% przypadków wystarcza. W Kohanie jeden moduł to kilka katalogów. Python jakoś obywa się bez autoloadera i kiedy czytam to co wyżej sam napisałem zaczynam rozumieć dlaczego. Na marginesie tylko wspomnę, że w związku z wprowadzeniem namespace-ów, sposób organizacji klas w plikach PHP się zmieni.

PHP w wersji piątej poszło wyraźnie w kierunku „magi”. Wszystkie poprzedzone podwójnym podkreśleniem metody są bardzo wygodnym rozwiązaniem i osobiście bardzo je lubię ale nadmiar czarów daje się we znaki w chwili kiedy zachodzi potrzeba prześledzenia procesów zachodzących w aplikacji. Debugowanie przesyconych „magią” klas jest znacznie utrudnione przede wszystkim przez niejednoznaczne komunikaty błędów. Dodatkową wadą użycia metod magicznych jest to, że dynamiczne settery i gettery nie będą podpowiadane przez żadne IDE typu NetBeans, czy Eclipse mimo w sumie dobrze zrealizowanej w nich funkcji podpowiadania składni.

Inną kwestią jest uniwersalność kodu. Klas Kohanej można używać tylko w ramach tego frameworka, z kolei biblioteki eZ Components lub Zend Frameworka można używać niezależnie lub w ramach zupełnie innej platformy. Sam wielokrotnie w projektach opartych na Kohanej sięgałem do wybranych komponentów Zend Frameworka. Klasy ZF są jak to zauważył Zyx dopracowane i przetestowane i jedyną ich wadą jest z reguły to, że są zbyt dobre. Zamiast w najprostszy sposób realizować banalną funkcjonalność, autorzy poszczególnych bibliotek prześcigają się w wymyślaniu wariantów zastosowań i sposobów użycia. Tak właśnie z noża powstał szwajcarski scyzoryk – fajny ale do smarowania chleba najlepszy jest zwykły nóż kuchenny. Ponoć w nowej wersji ZF ma to ulec zmianie.

ORM

Symfony zniechęciło mnie do siebie przede wszystkim ORM-em. Do Propela nawet nie startowałem, natomiast z Doctrine-m walczyłem jakiś czas. Niestety na etapie kiedy przeprowadzałem moje eksperymenty Doctrine było mocno niedopracowane w związku z czym wielokrotnie wzbudzało to moją irytacją. Z tego co pamiętam nie mogłem nawet dowolnie wskazać miejsca generowania modeli tylko było to z góry narzucone. Dlatego między innymi zarzuciłem pomysł „dokooptowania” Doctrine do Kohany.

Na tę chwilę o wiele większe doświadczenie mam z ORM-em Django uważanym za wzorcowy. Jest on o wiele bardziej dopracowany niż wyżej wspomniane PHP-owe odpowiedniki w związku z czym da się z nim w miarę sprawnie pracować. Mimo to pozostaję sceptyczny w kwestii użycia tego typu narzędzi. Opanowanie django-wego ORM-a kosztowało mnie sporo czasu, a do tego nie wyobrażam sobie aby można było efektywnie go używać nie znając wcześniej SQL-a. Przy prostych konstrukcjach jest miło i przyjemnie przy bardziej skomplikowanych użycie obiektów rzutuje przede wszystkim na wydajność, do czego przyczyniają się w dużej mierze bajery typu „lazy loading”. Zmiana ORM-owej konstrukcji w celu optymalizacji i tak poprzedzana jest napisaniem zapytania w SQL więc człowiek nie ma żadnej korzyści z użycia ORM-a, która by rekompensowała nakłady poniesione w celu jego poznania.

O wiele bardziej przemawiają do mnie składacze zapytań SQL takie jak Zend_Db – choć jak większość bibliotek Zenda jest ona przedobrzona. Mając prostą klasę do konstruowania zapytań SQL – taką która ułatwia, nie ogranicza i bynajmniej nie zmusza do uczenia się zamienników w stylu „annotate” zamiast „group by”, wspartą wzorcem projektowym DAO można stworzyć prosty w utrzymaniu, debugowaniu, elastyczny, wcale nie pracochłonny i przede wszystkim odseparowany od warstwy logiki i widoku mechanizm dostępu do bazy danych.

ORM-y mogą być w moim mniemaniu jedynie dodatkami używanymi np. do automatycznego generowania backedu (czytaj panelu administracyjnego), ale w żadnym wypadku nie powinny być jedynym ani nawet głównym sposobem pracy z danymi pobieranymi z bazy danych.

Szablony

Symfony a także Zend Framework, jak równierz Kohana używają natywnych szablonów PHP. Podobnie jak Zyx zgadzam się, że można znaleźć lepszą alternatywę dla warstwy widoku. W odróżnieniu jednak od niego nie stawiałbym na Open Power Template’a, które uważam za trudne i pracochłonne w użyciu (z samej swojej xml-owej natury), a do tego mniej elastyczne i intuicyjne niż kontestowane przez niego Smarty.

System szablonów Smarty w wersji 3 zostało wzbogacone o kilka ciekawych funkcjonalności jak np. dziedziczenie szablonów i możliwość nadpisywania bloków kodu znane z szablonów Django. W przeciwieństwie do ORM-ów uważam, że warto potrudzić się troszkę ze składnią Smarty i pogodzić się z narzutem na wydajność bo w zamian można zyskać szereg usprawnień jakich jesteśmy pozbawieni w przypadku użycia gołych szablonów PHP. Wspomnę tylko wygodniejszą składnię czy zaimplementowany cache, ale też zwiększone bezpieczeństwo wymuszone ograniczoną dostępnością funkcji PHP wewnątrz szablonu czy też encapsulacją zmiennych. Smarty 3 umożliwia też użycie natywnych szablonów PHP ale jest to alternatywa nie ograniczenie.

Do niedawna pracowałem jeszcze z Django w wersji 9.6, w której szablony w stosunku do wersji 1.2 były mocno ograniczone. Obecne szablony Django w wersji 1.x przypominają Smarty (albo na odwrót jak ktoś woli) – z tym, że Smarty 3 są po prostu lepsze. Jestem fanem składni i możliwości języka Python mimo to na polu szablonów PHP u mnie wygrywa.

Reasumując

We wpisie tym nie dążyłem bynajmniej do porównania frameworków między sobą, a już na pewno się do porównywania rozwiązań stosowanych w języku PHP czy Python. Poruszyłem trzy zagadnienia, które w mniejszym lub większym stopniu dotyczą wszystkich z wyżej wymienionych platform. Wspomniałem o tym, że jestem zwolennikiem rozwiązań uniwersalnych ale elastyczność nie może być realizowana kosztem przejrzystości i prostoty. Dałem też do zrozumienia, że mapowanie na siłę relacyjnej bazy danych do postaci obiektów jest raczej wyrazem fundamentalizmu ideologicznego niż pragmatycznym działaniem. Z kolei w użyci systemów szablonów dostrzegłem wiele zalet.

Domknięcia, funkcje anonimowe przestrzenie nazw i dekoratory

Pojęcie dekoratora w języku Python to coś więcej niż wzorzec projektowy. To elegancki sposób na zwiększenie możliwości danej funkcji czy metody doskonale wykorzystujący unikalne cechy języka Python. Mechanizm działania dekoratorów oraz sposób ich użycia doskonale opisał Kent S Johnson (opracowanie to można znaleźć także w języku polskim).

Prostym i użytecznym przykładem dekoratora może być np. funkcja mierząca czas wykonania funkcji udekorowanej.

import time
 
def timeit(method):
    """Mierzy czas wykonania funkcji"""
 
    def check_time(*args, **kw):
        ts = time.time()
        result = method(*args, **kw)
        te = time.time()
        log(method.__name__, args, kw, ts, te)
        return result
 
    def log(name, args, kw, time_start, time_end):
        msg = '%r (%r, %r) %2.10f sec' % (name, args, kw, time_end - time_start)
        print msg
 
    check_time.__name__ = method.__name__
    return check_time

Użycie dekoratora w Pythonie to syntaktyczna perełka.

@timeit
def say_hello(param):
    print param
    time.sleep(0.5)

W PHP osiągnięcie czegoś takiego jest po prostu niemożliwe. Od wersji PHP 5.3 język został wzbogacony o nowe cechy takie jak domknięcia, funkcje anonimowe i przestrzenie nazw. Czy owe mechanizmy umożliwiają implementację czegoś co choćby przypominało pythonowy dekorator?

W PHP nie da się zdefiniować funkcji, a potem przypisać jej inną nazwę.

def foo():
    print 'OK'
 
faa = foo
 
faa() # w wyniku otrzymamy: OK

no chyba że użyjemy funkcji anonimowej (ang. anonymous function)

$foo = function() {echo 'OK';};
$faa = $foo;
$faa(); // w wyniku otrzymamy: OK

Dekorowanie funkcji anonimowych przy użyciu domknięć

Możemy zapomnieć (na razie) o dekorowaniu tradycyjnych funkcji ale udekorowanie funkcji anonimowej staje się potencjalnie możliwe.

$foo = function() {echo 'OK';};
 
$foo = function() use ($foo) {
    echo 'Jest '; return $foo();
};
 
$foo(); // w wyniku otrzymamy: Jest OK

Domknięcia (ang. closures) stoją u podstaw programowania w Pythonie, są też od zawsze obecne i powszechnie używane np. w Javascript-cie.

var Example = function()
{ 
    this.public = function() 
    { 
        return "This is a public method"; 
    }; 
 
    var private = function() 
    { 
        return "This is a private method"; 
    };
};
 
Example.public()  // returns "This is a public method" 
Example.private() // error - doesn't work

Ich implementacja w PHP to kwestia dyskusyjna podobnie jak ich użyteczność. Z uwagi na określone cechy PHP domknięcia w tym języku wydają się nie być odpowiednikiem tego mechanizmu znanym z innych języków programowania. W przeprowadzanym przeze mnie teście przydały się jednak i osobiście dostrzegam jeszcze kilka innych ich praktycznych zastosowań. Wracając jednak do głównego wątku tego wpisu.

W miarę prosty sposób udało się udekorować funkcję „foo”. Domknięcie można zamknąć w funkcji dzięki czemu nadaje się do wielokrotnego użytku.

function timeit($func) {
    return function() use ($func) {
        // pobranie wszystkich argumentów funkcji
        $args = func_get_args();
 
        // utworzenie funkcji pomocniczej
        $microtime_float = function()
        {
            list($usec, $sec) = explode(" ", microtime());
            return ((float)$usec + (float)$sec);
        };
 
        // rozpoczęcie pomiaru czasu
        $time_start = $microtime_float();
 
        // wywolanie funkcji
        $result = call_user_func_array($func, $args);
 
        // zakończenie pomiaru czasu i wyświetlenie wyników
        $time_end = $microtime_float();
        $time = $time_end - $time_start;
        printf ("\nfunction was executed during %01.6f seconds\n", $time);
 
        // zwrócenie resultatu działania funkcji
        return $result;
    };
}

Zaprezentuje może użycie „dekoratora” timeit na funkcji wymagającej parametrów.

$func_say_hello = function(param) {
	return 'Hello '. $param;
};
 
$func_say_hello = timeit($func_say_hello);
echo $func_say_hello('World');

Funkcje anonimowe są, a raczej będą stosunkowo rzadko używane, podobnie zresztą jak funkcje lambda w Pythonie, których są odpowiednikiem. Pomiar czasu wykonania np. funkcji sortującej to z pewnością przydatna sprawa, ale wzbogacanie funkcji anonimowych o nowe możliwości przy pomocy „dekoratorów” wydaje się być mało przydatne. Czy zatem można udekorować zwykłą funkcję?

Czy przestrzenie nazw pomagają w dekorowaniu funkcji?

Po co nam przestrzenie nazw (ang. namespaces)? Załóżmy, że mamy w pliku functions.php zdefiniowaną przestrzeń nazw „lib” i funkcję „say_hello”

<?php
namespace lib {
    function say_hello($param)
    {
        return 'Hello '. $param;
    }
}

Gdybyśmy w pliku index.php dołączyli plik functions.php moglibyśmy używać zdefiniowanych w nim funkcji.

<?php
include './functions.php';
echo \lib\say_hello('World');

Wywołanie say_hello(‚World’); bez podania poprzedzającego go namespace-a zakończyłoby się błędem. Niestety PHP nie wspiera jak na razie możliwości aliasowania funkcji czy stałych zamkniętych w przestrzeni nazw dlatego też chęć odwoływania się do funkcji „say_hello” bezpośrednio musi zostać poprzedzona definicją nazwijmy to „atrapy”, która przy okazji pozwoli udekorować opakowywowaną funkcję.

include './functions.php';
 
function timeit($func) {...}
 
function say_hello($param)
{
    $func_say_hello = '\lib\say_hello';
    $func_say_hello = timeit($func_say_hello);
    $result = $func_say_hello($param);
}
echo say_hello('World');

Zwróćmy uwagę na to, że funkcje „say_hello” i „\lib\say_hello” to nie to samo i równie dobrze moglibyśmy nie używać przestrzeni nazw i osiągnęlibyśmy to samo (tylko nazwy funkcji byłyby inne)

function timeit($func) {...}
 
function say_hello($param) {...}
 
function decorated_say_hello($param)
{
    $func_say_hello = 'say_hello';
    $func_say_hello = timeit($func_say_hello);
    $result = $func_say_hello($param);
}

Z punktu widzenia wygody użycia to czy użyjemy przestrzeni nazw czy nie to mamy to samo. I tak chcąc zmierzyć czas wykonania funkcji „say_hello” musimy zmienić jej wywołanie w kodzie na „decorated_say_hello”. Nie da się udekorować zdefiniowanych funkcji w pythoniczny sposób.

Aliasy – nowe możliwości dekorowania klas

W przypadku funkcji moje rozważania to jedynie gimnastyka mózgu, jednak w przypadku klas teoretyczne dywagacje – jak się zaraz okaże – przybiorą praktyczną formę.

Wspominałem wyżej o aliasach. Tego czego nie da się zrobić z funkcjami i stałymi w PHP > 5.3 da się uczynić z klasami.

Załóżmy, że plik functions.php wygląda tak

namespace lib {
	class Say
	{
		public static function hello($param)
		{
			return 'Hello '. $param;
		}
 
		public function goodbye($param)
		{
			return 'Goodbye '.$param;
		}
	}
}

Do pliku index.php możemy dołączyć functions.php i przypisać klasie „/lib/Say” alias Say

include './functions.php';
 
function timeit($func) {...}
 
use \lib\Say as Say;
 
echo Say::hello('Adam');
$say = new Say();
echo $say->goodbye('Ewa');

Nadanie aliasu sprawia, że możemy używać klasy tak jakby była zdefiniowana bez użycia przestrzeni nazw. Jeśli w tym momencie przyjdzie nam ochota zmierzenia czasu wykonania metod klasy „Say” (a właściwie „\lib\Say”) bez zmiany wywołań musimy zrobić dwie rzeczy. Zakomentować linijkę nadającą alias oraz udekorować klasę.

include './functions.php';
 
function timeit($func) {...}
 
//use \lib\Say as Say;
 
class Say
{
	private $_obj;
 
	public function  __construct() {
		$this->_obj = new \lib\Say();
	}
 
	public static function hello($param)
	{
		$func = '\lib\Say::hello';
		$func = \deco\timeit($func);
		return $func($param);
	}
 
	public function goodbye($param)
	{
		$func = function($obj, $param) {
			return $obj->goodbye($param);
		};
 
		$func = \deco\timeit($func);
		return $func($this->_obj, $param);
	}
}
 
echo Say::hello('Adam');
$say = new Say();
echo $say->goodbye('Ewa');

Wywołanie metod się nie zmieniło, ale wzbogaciliśmy naszą klasę o funkcjonalność pozwalającą nam zmierzyć czas wykonania zarówno metody statycznej klasy, jak i metody instancji. Do wygody użycia dekoratora „@timeit” w Pythonie jest jeszcze bardzo daleko. Udekorowaliśmy zaledwie dwie metody, a trzeba było napisać tyle kodu. Z każdą kolejną metodą przybędzie dodatkowego kodu w dekoratorze. Czy da się ten przyrost ograniczyć?. Dzięki metodom magicznym da się zrobić bardziej uniwersalny dekorator.

include './functions.php';
 
function timeit($func) {...}
 
//use \lib\Say as Say;
 
class Say
{
	private $_obj;
 
	public function  __construct() {
		$this->_obj = new \lib\Say();
	}
 
	public static function  __callStatic($name, $arguments)
	{
		$func = '\lib\Say::'.$name;
		return self::_addDecorators($func, $arguments);
	}
 
	public function __call($name, $arguments)
	{
		$func = function() {
			$params = func_get_args();
			$obj = array_shift($params);
			$method = array_shift($params);
			return call_user_func_array(array($obj, $method), $params);
		};
 
		array_unshift($arguments, $name);
		array_unshift($arguments, $this->_obj);
		return self::_addDecorators($func, $arguments);
	}
 
	private static function _addDecorators($func, $arguments)
	{
		$func = \deco\timeit($func);
		return call_user_func_array($func, $arguments);
	}
}
 
echo Say::hello('Adam');
$say = new Say();
echo $say->goodbye('Ewa');

Konkluzje

1. To naprawdę działa – doznałem intelektualnego orgazmu.
2. Nie stosujcie takich rozwiązań w prawdziwym projekcie

Próba debugowania takiego kodu skazana jest z góry na porażkę. Przeprowadzony przeze mnie eksperyment dostarczył mi najlepszego dowodu na to, że nie ma sensu implementować na siłę mechanizmów znanych z innych języków. PHP ma swoją naturę, która czasami jest ograniczeniem, ale często stanowi też jego zaletę.

Pogodzenie Python-a z PHP na Apache2 (mod_wsgi + mod_php)

Wstęp

Poradników i tutoriali opisujących konfigurację Apache z PHP i MySQL pod Linuxem (LAMP) jest cała masa. W Ubuntu sprowadza się to do wywołania w konsoli prostego polecenia

sudo apt-get install php5 php5-mysql mysql-server apache2 libapache2-mod-auth-mysql

… i mamy wszystko co potrzeba. Więcej na ten temat znajdziemy choćby w artykule Instalacja Apache + PHP5 + MySQL.

Instalacja frameworka Django i preferowanego przez pythonowców serwera baz danych Postgres jest równie prosta – przynajmniej pod Ubuntu.

sudo apt-get install python-django postgresql python-psycopg2 postfix python-imaging python-docutils

Django ma jedną z najlepszych dokumentacji, a o Postgresie pisze niemal co drugi bloger, który miał coś z nim do czynienia tak więc zainteresowanych po prostu odeślę do wciąż aktualnego i w miarę kompletnego wpisu Instalacja PostgreSQL 8.3 w Ubuntu 8.04.

O ile hostowanie skryptów PHP na Apache-u jest rzeczą naturalną zarówno w środowisku deweloperskim jak i produkcyjnym o tyle w przypadku Pythona a konkretnie aplikacji Django już nie. Django posiada świetny wbudowany serwer stworzony na podstawie klasy BaseHttpServer ze standardowej biblioteki Pythona, który świetnie sprawdza się do szybkiego testowania aplikacji o niepełnej funkcjonalności. Restartuje się automatycznie przy każdej zmianie w kodzie a ponadto ponieważ uruchamiany jest w terminalu wyświetla na wyjściu wszystkie instrukcje print.

Użycie serwera wbudowanego w środowisku produkcyjnym jest zabronione licencją, a poza tym istnieje jeszcze kilka innych obiektywnych powodów aby tego nie robić, jak choćby bezpieczeństwo i stabilność, czy wydajność. Jak się okazuje także dla celów developerskich warto czasem podjąć trud konfiguracji serwera zewnętrznego.

W przypadku Apache-a w kontekście hostowania skryptów napisanych w języku Python najczęściej stosuje się dwa rozwiązania tj. mod_python lub mod_wsgi. Oba z modułów nie powodują konfliktów z mod_php, tak więc chcąc na swoim serwerze obsługiwać zarówno aplikacje PHP i Python można zastosować dowolne z wymienionych rozwiązań.

Ponoć konfiguracja Apache2 + mod_python jest częściej stosowana, lepiej przetestowana i udokumentowana, ja jednak zdecydowałem się na WSGI z uwagi na elastyczność jaką udało mi się uzyskać.

Założenia

W przypadku środowisk produkcyjnych kluczową kwestią jest bezpieczeństwo i stabilność serwera. W środowisku developerskim, w którym skrypty uruchamiane są jedynie na komputerze lokalnym osobiście stawiam na elastyczność kosztem nawet bezpieczeństwa i stabilności. Nie lubię grzebać w plikach konfiguracyjnych Apache-a i edytowanie pliku /etc/apach2/httpd.conf lub któregoś z plików w katalogu /etc/apache2/sites-available/ za każdym razem kiedy tworze nowy projekt lub zmieniam jego nazwę. W przypadku projektów realizowanych w języku PHP wystarczy, że w moim katalogu workspace stworzę nowy katalog np. „nowy_projekt_php”, wrzucę do niego plik index.php i wywołam w pasku adresu przeglądarki

http://localhost/nowy_projekt_php

Podobny efekt chciałem uzyskać w przypadku projektów realizowanych w Django.

Instalacja mod_wsgi

Moduł WSGI możesz zainstalować ze źródeł, co zostało przystępnie opisane w artykule Ubuntu Hardy – mod_wsgi Installation, ale w Ubuntu można skorzystać z repozytorium:

sudo apt-get install libapache2-mod-wsgi

Aby aktywować moduł (w Ubuntu 10.04 – można pominąć)

sudo a2enmod mod-wsgi

W przypadku instalacji z repozytorium wystarczy już tylko restart serwera.

sudo /etc/init.d/apache2 restart

Konfiguracja Apache

Integracja mod_wsgi z Django jest świetnie opisana zarówno na stronach dokumentacji Django, jak też (może nawet lepiej) na stronie rozszerzenia specjalnie poświęconej temu zagadnieniu w artykule pod pt. Integration With Django.

Ja jednak zaproponuję jeszcze inne rozwiązanie na które natknąłem się na forum w wątku Ubuntu + Apache2 + WSGI (mod_wsgi) with LAMP i lekko zmodyfikowałem na swoje potrzeby.

Aby osiągnąć wyżej opisane założenie w najprostszy sposób należy:

Stworzyć Virtualhosta w pliku httpd.conf

sudo vim /etc/apache2/httpd.conf

<VirtualHost *>
    ServerName testhost
    DocumentRoot /home/myuser/workspace/

    <Directory /home/myuser/workspace/>
        Options Indexes FollowSymLinks MultiViews ExecCGI

        AddType application/x-httpd-php .php .phtml
        AddType application/x-httpd-php-source .phps
        AddHandler application/x-httpd-php .php .phtml
        AddHandler cgi-script .cgi
        AddHandler wsgi-script .wsgi

        Order allow,deny
        allow from all
    </Directory>
</Virtualhost>

W przeciwieństwie do rozwiązania zaproponowanego na forum proponuję usunąć dyrektywę „AllowOverride None” dzięki czemu możliwe będzie skorzystanie z mod_rewrite.

Trzeba jeszcze poinformować serwer, że domyślnym plikiem w aplikacji napisanej w Pythonie będzie index.wsgi

sudo gedit /etc/apache2/mods-enabled/dir.conf

<IfModule mod_dir.c>
    DirectoryIndex index.html index.cgi index.pl index.php index.xhtml index.htm index.wsgi
</IfModule>

W katalogu /home/myuser/workspace/ utwórzmy nowy podkatalog nowy_projekt_python a w nim plik index.wsgi o treści:

def application(environ, start_response):
    status = '200 OK' 
    output = 'Hello World!'
 
    response_headers = [('Content-type', 'text/plain'),
                        ('Content-Length', str(len(output)))]
    start_response(status, response_headers)
 
    return [output]

Jeśli po wywołaniu

http://localhost/nowy_projekt_python

widać „Hello World!” to znaczy, że wszystko jest ok, jeśli nie to może wystarczy zrestartować serwer 😉

Twórcy Django dążą do tego aby ich framework był pythonowy jak to tylko możliwe dlatego np. konfigurację trzymają w plikach py a nie np. w xml albo yml-u. Dzięki takiemu podejściu możemy mieć pewność, że po wrzuceniu do katalogu nowy_projekt_python aplikacji Django i modyfikacji pliku index.wsgi, zobaczymy w przeglądarce to co byśmy widzieli po uruchomieniu serwera wbudowanego.

Zmodyfikujmy index.wsgi

import os, sys
sys.path.append('/home/myuser/workspace')
sys.path.append('/home/myuser/workspace/nowy_projekt_python')
 
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
 
import django.core.handlers.wsgi
 
_application = django.core.handlers.wsgi.WSGIHandler()
 
def application(environ, start_response):
	if environ['wsgi.url_scheme'] == 'https':
		environ['HTTPS'] = 'on'
	return _application(environ, start_response)

Przydałby się jeszcze plik .htaccess

<IfModule mod_rewrite.c>
    # Turn on URL rewriting
    RewriteEngine On

    # Installation directory
    RewriteBase /nowy_projekt_python/

    # Protect hidden files from being viewed
    <Files .*>
        Order Deny,Allow
        Deny From All
    </Files>

    # Allow any files or directories that exist to be displayed directly
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d

    # Rewrite all other URLs to index.wsgi/URL
    RewriteRule .* index.wsgi/$0 [PT]

</IfModule>

Zaproponowane tutaj rozwiązanie jest jedynie punktem wyjścia do stworzenia swojej własnej uniwersalnej konfiguracji środowiska developerskiego do pracy z projektami PHP i Python. Zapewne przydałoby się parę rzeczy inaczej skonfigurować lub też dodać kilka opcji jak np. dyrektywę zapobiegającą wyświetlaniu treści plików python w przeglądarce. Zachęcam do eksperymentowania i dzielenia się swoim doświadczeniem.

PHP 5.3 – wywołanie array_multisort za pośrednictwem call_user_func_array

Chcąc dostosować swój kod do wersji PHP 5.3, tak aby wykonywał się bezproblemowo z uwzględnieniem błędów STRICT natrafiłem na taki drobny szczegół. Swego czasu dodałem do manuala funkcji array_multisort na php.net taką drobną [notkę zawierającą implementację funkcji ułatwiającej sortowanie wyników zapytań sql-owych] (http://usphp.com/manual/en/function.array-multisort.php#87268).

Chcąc ją dzisiaj użyć dostałem na twarz warning-a „Parameter 2 to array_multisort() expected to be a reference, value given”.

Załóżmy, że miałem następujące dane.

$data = array(
	0 => array (
		'id' => '1',
		'name' => 'Rodzaje',
	),
	1 =>
	array (
		'id' => '2',
		'name' => 'Kategorie',
	),
	2 =>
	array (
		'id' => '3',
		'name' => 'Statusy',
	),
	3 =>
	array (
		'id' => '4',
		'name' => 'Kolory',
	),
);

Aby posortować tę tablicę według kolumny „name” wywołałem wspomnianą funkcję w następujący sposób

sortDbResult($data, 'name', SORT_ASC, SORT_STRING);

Przyjrzałem się problematycznej linii i zobaczyłem, że wywołuję funkcję array_multisort przy pomocy funkcji call_user_func_array, do której parametry przekazuję za pośrednictwem tablicy.

call_user_func_array('array_multisort', $_params);

W moim przypadku zmienna $_params miała następującą zawartość

array(
0 => &array (
	0 => 'Rodzaje',
	1 => 'Kategorie',
	2 => 'Statusy',
	3 => 'Kolory',
),
1 => 2,
2 => 4,
3 => array(
	0 => array (
		'id' => '1',
		'name' => 'Rodzaje',
	),
	1 =>
	array (
		'id' => '2',
		'name' => 'Kategorie',
	),
	2 =>
	array (
		'id' => '3',
		'name' => 'Statusy',
	),
	3 =>
	array (
		'id' => '4',
		'name' => 'Kolory',
	),
));

Pomyślałem sobie o co chodzi z tym błędem w końcu drugi parametr funkcji array_multisort to w moim przypadku wartość stałej SORT_ASC czyli 2. Przeprowadziłem mały eksperyment i wywołałem funkcję array_multisort bezpośrednio.

$data_1 = array (
	0 => 'Rodzaje',
	1 => 'Kategorie',
	2 => 'Statusy',
	3 => 'Kolory',
);
 
$data_2 = array(
	0 => array (
		'id' => '1',
		'name' => 'Rodzaje',
	),
	1 =>
	array (
		'id' => '2',
		'name' => 'Kategorie',
	),
	2 =>
	array (
		'id' => '3',
		'name' => 'Statusy',
	),
	3 =>
	array (
		'id' => '4',
		'name' => 'Kolory',
	),
);
array_multisort($data_1, SORT_ASC, SORT_STRING, $data_2);
 
var_dump($data_2);

Zadziałało. Tablica $_data2 została posortowana prawidłowo.

call_user_func_array('array_multisort',  array(&$data_1, SORT_ASC, SORT_STRING, &$data_2));

Powyższy kod również wykonał się nie zwracając żadnych komunikatów błędów.

$params = array(&$data_1, SORT_ASC, SORT_STRING, &$data_2);
call_user_func_array('array_multisort',  $params);

Wykonanie powyższego kodu kończy się już niestety warningiem. Aby się go pozbyć trzeba użyć małego obejścia i przypisać wartości stałych do zmiennych a następnie przekazać je do tablicy przy pomocy referencji.

$o = SORT_ASC;
$v = SORT_STRING;
$params = array(&$data_1, &$o, &$v, &$data_2);
call_user_func_array('array_multisort', $params);

W tym wypadku nie działa ukrywanie błędów. Funkcja array_multisort po prostu nie zadziała prawidłowo.

Poprawiona funkcja sortDbResult wygląda następująco:

/**
 * Sort DB result
 *
 * @param array $data Result of sql query as associative array
 *
 * Rest of parameters are optional
 * [, string $name  [, mixed $name or $order  [, mixed $name or $mode]]]
 * $name string - column name i database table
 * $order integer - sorting direction ascending (SORT_ASC) or descending (SORT_DESC)
 * $mode integer - sorting mode (SORT_REGULAR, SORT_STRING, SORT_NUMERIC)
 *
 * 
 *  $i,
 *                      'first_name' => sprintf('first_name_%s', rand(1, 9)),
 *                      'last_name' => sprintf('last_name_%s', rand(1, 9)),
 *                      'date' => date('Y-m-d', rand(0, time()))
 *                  );
 * }
 * $data = sortDbResult($data, 'date', SORT_DESC, SORT_NUMERIC, 'id');
 * var_dump($data);
 * $data = sortDbResult($data, 'last_name', SORT_ASC, SORT_STRING, 'first_name', SORT_ASC, SORT_STRING);    
 * var_dump($data);
 * ?>
 * 
 *
 * @return array $data - Sorted data
 */
function sortDbResult(array $data /*$name, $order, $mode*/) {
	$_argList = func_get_args();
	$_data = array_shift($_argList);
	if (empty($_data)) {
		return $_data;
	}
	$_max = count($_argList);
	$_params = array();
	$_cols = array();
	$_rules = array();
	for ($_i = 0; $_i < $_max; $_i += 3)
	{
		$_name = (string) $_argList[$_i];
		if (!in_array($_name, array_keys(current($_data)))) {
			continue;
		}
		if (!isset($_argList[($_i + 1)]) || is_string($_argList[($_i + 1)])) {
			$_order = SORT_ASC;
			$_mode = SORT_REGULAR;
			$_i -= 2;
		} else if (3 > $_argList[($_i + 1)]) {
			$_order = SORT_ASC;
			$_mode = $_argList[($_i + 1)];
			$_i--;
		} else {
			$_order = $_argList[($_i + 1)] == SORT_ASC ? SORT_ASC : SORT_DESC;
			if (!isset($_argList[($_i + 2)]) || is_string($_argList[($_i + 2)])) {
				$_mode = SORT_REGULAR;
				$_i--;
			} else {
				$_mode = $_argList[($_i + 2)];
			}
		}
		$_mode = $_mode != SORT_NUMERIC
					? $_argList[($_i + 2)] != SORT_STRING ? SORT_REGULAR : SORT_STRING
					: SORT_NUMERIC;
		// references below required from PHP 5.3
		$_rules[] = array('name' => $_name, 'order' => &$_order, 'mode' => &$_mode);
	}
	foreach ($_data as $_k => $_row) {
		foreach ($_rules as $_rule) {
			if (!isset($_cols[$_rule['name']])) {
				$_cols[$_rule['name']] = array();
				$_params[] = &$_cols[$_rule['name']];
				// references below required from PHP 5.3
				$_params[] = &$_rule['order'];
				$_params[] = &$_rule['mode'];
			}
			$_cols[$_rule['name']][$_k] = $_row[$_rule['name']];
		}
	}
	$_params[] = &$_data;
	call_user_func_array('array_multisort', $_params);
	return $_data;
}
Posted in PHP | Tagged byZbigniew Heintze