Baza danych SQLite stanowi ciekawą alternatywę dla niezwykle popularnego serwera MySQL. Jest niezwykle prosta w utrzymaniu, bo cała baza mieści się w 1 pliku, który można łatwo skopiować lub przenieść ale mimo tej pozornej prostoty posiada obsługę transakcji, triggerów, widoków, możliwość tworzenia własnych funkcji oraz wykonywania zagnieżdżonych zapytań. Nie korzysta ona z typowego silnika pracującego w tle ale ze zwykłej biblioteki dostępnej dla większości współczesnych języków programowania. Powszechnie panująca opinia mówi, że o ile odczyt z bazy SQLite jest do zniesienia to już zapis zupełnie ją dyskredytuje. Przeprowadziłem kilka testów (dość uproszczonych ale oddających ideę) i na podstawie wyników doszedłem do ciekawych wniosków. Testy przeprowadziłem porównując bazy SQLite i MySQL. W obu przypadkach dla operacji zapisu testy podzieliłem na zapis każdego zapytania osobno oraz stosując grupowe wykonanie serii zapytań z użyciem transakcji (BEGIN i COMMIT). W obu bazach przeprowadziłem osobne testy dla tabel typu MEMORY czyli przechowywanych w pamięci a w przypadku bazy MySQL dodatkowo uwzględniłem tabele typu MyISAM i InnoDB.

Na początek kilka szczegółów technicznych:

- baza MySQL 5.0.51
- baza SQLite 2.8.17
- komputer z procesorem Intel Core2 Duo 2GHz, 2 GB pamięci DDR2 i systemem MS Windows XP SP2
- testy przeprowadzone w PHP 5.2.5 na serwerze Apache 2.2.6

Wyniki przeprowadzonych testów:

1000 zapytań INSERT:
- SQLite - 74.157 sek.
- SQLite (BEGIN-COMMIT) - 0.16181 sek.
- SQLite (Memory, BEGIN-COMMIT) - 0.13252 sek.
- MySQL (MyISAM) - 0.11942 sek.
- MySQL (InnoDB) - 25.019 sek.
- MySQL (InnoDB, BEGIN-COMMIT) - 0.16452 sek.
- MySQL (Memory) - 0.08914 sek.

Powyższe wyniki są bardzo różne co wynika z typu tabel oraz sposobu przeprowadzenia operacji. Baza SQLite domyślnie korzysta z transakcji przez co wykonanie 1000 zapytań bez rozpoczęcia transakcji przed ich wykonaniem i zakończenia jej po wykonaniu wszystkich zapytań skutkuje drastycznym spadkiem wydajności. Podobna sytuacja dotyczy bazy MySQL, która przy zastosowaniu transakcji (tabela typu InnoDB) jest równie wrażliwa na sterowanie transakcjami. W przypadku zastosowania BEGIN przed wykonaniem zapytań i COMMIT po ich wykonaniu czasy w obu bazach są bardzo podobne. Najszybciej wykonanie wszystkich zapytań przebiegło w bazie MySQL z wykorzystaniem tabeli typu MyISAM, co jest zupełnie naturalne ze względu na brak obsługi transakcji. Zastosowanie tabel typu MEMORY w obu przypadkach zgodnie z przypuszczeniami przyśpieszyło wykonanie zapytań.

1000 identycznych zapytań SELECT * FROM tabela:
- SQLite - 0.77956 sek.
- SQLite (Memory) - 0.76029 sek.
- MySQL (MyISAM) - 0.86335 sek.
- MySQL (InnoDB) - 1.31506 sek.
- MySQL (Memory) - 0.77291 sek.

W przypadku 1000 identycznych zapytań SQLite okazuje się być nieznacznie szybszy zarówno dla tabel typu MEMORY jak i zwykłych. Najwolniejszy okazała się tabela InnoDB w bazie MySQL.

1000 unikatowych zapytań SELECT * FROM tabela WHERE pole LIKE ‘zmienna_wartosc%’ :
- SQLite - 0.6806 sek.
- SQLite (Memory) - 0.71688 sek.
- MySQL (MyISAM) - 0.16667 sek.
- MySQL (InnoDB) - 0.13743 sek.
- MySQL (Memory) - 0.33858 sek.

Dla 1000 różnych zapytań z frazą LIKE baza SQLite okazała się wolniejsza niż konkurenci zarówno w przypadku tabeli typu MEMORY jak i standardowej). Co ciekawe zastosowanie w bazie MySQL tabeli typu InnoDB przyśpieszyło operację pobierania danych a użycie tabeli typu MEMORY ją spowolniło.

Powyższe wyniki wskazują, iż czasy wykonywania zapytań modyfikujących tabele w porównywalnych warunkach (czyli MySQL korzystające z tabel InnoDB) są zbliżone co jest niewątpliwym sukcesem bazy SQLite, tym bardziej, iż obalają powszechnie panującą opinię o znacznym spadku wydajności bazy SQLite w przypadku zapisu danych. Porównanie odczytu danych zależy natomiast od typu zapytań.

Na koniec kilka linków związanych z SQLite:
- http://www.sqlite.org - oficjalna strona projektu
- http://www.php.rk.edu.pl/w/p/optymalizacja-sqlite - optymalizacja SQLite
- http://www.sqlite.org/cvstrac/wiki?p=SpeedComparison - porównanie wydajności baz danych na opicjalnej stronie projektu

Jak wiadomo Google udostępnia coraz więcej darmowych narzędzi online umożliwiającymi współdzielenie danych między użytkownikami. Narzędzia te pozwalają na tworzenie kalendarzy, arkuszy i dokumentów, albumów zdjęć i wiele innych. Narzędzia te oferują dużą funkcjonalność a największa zaleta to możliwość udostępniania wszelkich wprowadzonych danych. Do pełni szczęścia brakuje jedynie możliwości dostępu do aplikacji Google z poziomu własnych aplikacji.

Taka możliwość oferuje niedawno wydana biblioteka GData firmy Zend wykorzystująca Google Data API. Biblioteka dostępna jest jako część frameworka Zend Framework oraz jako samodzielna biblioteka. Pozwala ona na dostęp do kalendarza, arkuszy Spreadsheet, dokumentów Docs, albumu zdjęć Picassa, serwisu YouTube oraz Base. W niedalekiej przyszłości planowane jest także dodanie obsługi aplikacji Blogger, CodeSearch i Notebook.

Biblioteka wymaga PHP w wersji 5.1.4 lub nowszej a jej opis wraz z przykładami można znaleźć w manualu Zend Framework.

DOMPDF jest jedną z lepszych bibliotek do konwersji dokumentów HTML do formatu PDF w PHP. Jest darmowa, wygodna w obsłudze i posiada stosunkowo dobre odwzorowanie kodu html. Niestety w tego typu konwersjach zazwyczaj pojawia się problem z polskimi znakami i klasa DOMPDF nie należy do wyjątków. Okazuje się, że istnieje rozwiązanie tego problemu.

Aby polskie znaki były wyświetlane poprawnie należy przede wszystkim skonwertować wymagane czcionki (wykorzystywane w pliku html) z formatu TTF (True Type Font) na PT1 (Postscript Type 1) od razu dokonując konwersji kodowania poprzez program ttf2pt1:

CODE:
  1. ttf2pt1.exe -b -L iso-8859-2.map arial.ttf arial

Do konwersji będzie potrzebny plik mapy kodowania iso-8859-2.map dostępny np. w bibliotece fpdf.

W wyniku tego polecenia powstaną 2 pliki: arial.afm (zawierający metrykę czcionki) i arial.pfb (zawierający opis kształtu czcionki), które należy skopiować do katalogu czcionek klasy DOMPDF czyli do katalogu dompdf/lib/fonts.

Następnie należy sprawdzić czy nowe czcionki są dodane do pliku domdpf/lib/fonts/dompdf_font_family_cache.dist a jeśli ich tam nie ma należy je dodać/zmienić nazwy (i usunąć plik cache). Należy także pamiętać, że w ten sposób należy stworzyć także czcionki pogrubione czy pochylone, jeśli są używane w źródłowym pliku html.

W bibliotece DOMPDF znajduje się plik class.pdf.php (w katalogu dompdf/lib/), który niestety trzeba będzie podmienić na inny zawierający drobne poprawki. Poprawiony plik można zobaczyć tutaj.

Na koniec należy usunąć definicję kodowania z meta tagów w konwertowanym pliku html i wczytać zawartość tegoż pliku dokonując przy tym konwersji kodowania na Windows-1250 np. poprzez funkcję iconv():

PHP:
  1. $pdf = new DOMPDF();
  2. $pdf->load_html(iconv('UTF-8','Windows-1250', file_get_contents('zrodlowy.html')));
  3. $pdf->render();
  4. $pdf->stream('wynik.pdf');

Po wykonaniu powyższego kodu nastąpi wczytanie pliku zrodlowy.html, zmiana kodowania z UTF-8 na Windows-1250 i konwersja do pliku wynik.pdf, który zostaje wysłany do przeglądarki. Oczywiście źródłowy plik html może zawierać inne kodowanie i należy to uwzględnić podczas konwersji.

Wbudowany w PHP mechanizm sesji ze względu na łatwość użycia jest zdecydowanie najbardziej popularnym sposobem przechowywania zmiennych między kolejnymi przeładowaniami strony. Niestety mechanizm ten ze względu na swoją prostotę nie jest do końca bezpieczny. Istnieją różne sposoby zabezpieczenia sesji i własne mechanizmy obsługi zmiennych sesyjnych jednak najczęściej są one dosyć skomplikowane. Ja chciałem przedstawić bardzo prosty bo wbudowany sposób uczynienia sesji bardziej bezpiecznymi.

Jak wiadomo najprostszym sposobem przechwycenia sesji np. zalogowanego użytkownika jest pobranie identyfikatora sesji SESSID i zwykłe doklejenie go do adresu. Takie działania spowoduje, że użytkownik, który dokonał takiego wklejenie staje się dla serwera tym samym użytkownikiem co ten prawdziwy. Oczywiście można nieco utrudnić taki atak poprzez włączenie opcji session.use_only_cookies jednak nadal istnieje możliwość skorzystania z cudzej sesji. Wydaje się, że dobrym rozwiązaniem byłaba zmiana identyfikatora sesji przy każdorazowym przeładowaniu strony bądź żądaniu do serwera poprzez obiekt XMLHttpRequest poprzez funkcję session_regenerate_id(). Wówczas jednak na serwerze zaczęłaby się pojawiać ogromna ilość bezużytecznych plików starych sesji. Rozwiązaniem okazuje się być wprowadzony w PHP od wersji 5.1 parametr powyższej funkcji typu bool określający czy usuwać stare pliki sesji. Dzięki temu wbudowany w PHP mechanizm sesji staje się bezpieczniejszy bo jeśli nawet atakującemu uda się przechwycić id sesji na niewiele mu się on przyda bo po ponownym przeładowaniu strony otrzyma on nowy i przy okazji inny niż atakowanego użytkownika identyfikator.

Podsumowując za pomocą funkcji session_regenerate_id(true) możemy zabezpieczyć sesję przed próbą wykorzystania jego identyfikatora przez innego użytkownika. A dzięki parametrowi true unikniemy zbędnego gromadzenia starych plików sesji.

Wzorzec factory należy do wzorców konstrukcyjnych. Pozwala na tworzenie obiektów bez konieczności specyfikacji klas na podstawie której będą tworzone. Umożliwia on stworzenie osobnej metody odpowiedzialnej za tworzenie nowych obiektów.

Aby pokazać działanie wzorca factory posłużę się prostym przykładem. Załóżmy, że mam kontroler, który w zależności od typu przekazanego pliku musi stworzyć nowy obiekt widoku i wywołać w nim metodę generującą widok. Zacznę od stworzenia interfejsu IView dla widoków:

PHP:
  1. interface IView
  2. {
  3.     function render();
  4. }

Następnie tworzę klasy dla widoków implementujące interfejs IView:

PHP:
  1. class HtmlView implements IView
  2. {
  3.     function render()
  4.     {
  5.         // generowanie widoku html
  6.         // [...]
  7.     }
  8. }
  9.  
  10. class XmlView implements IView
  11. {
  12.     function render()
  13.     {
  14.         // generowanie widoku xml
  15.         // [...]
  16.     }
  17. }

Pozostaje mi stworzenie klasy głównej:

PHP:
  1. class View
  2. {
  3.     static function factory($fileName)
  4.     {
  5.         switch(end(explode('.', $fileName)))
  6.         {
  7.             case 'html' :
  8.                 return new HtmlView();
  9.             case 'xml' :
  10.                 return new XmlView();
  11.             default :
  12.                 throw new Exception('Unknown file type');
  13.         }
  14.     }
  15. }

Metoda renderView() w zależności od rozszerzenia pliku przekazanego w parametrze zwraca obiekt odpowiedniej klasy. Na koniec pozostało jeszcze tylko uruchomienie metody renderView() klasy View:

PHP:
  1. $view = View::factory('index.html');
  2. $view->render();

Jak widać implementacja wzorca factory jest bardzo prosta i przydatna zarazem kiedy zachodzi potrzeba tworzenia obiektów bez specyfikacji klas.

Więcej o wzorcu factory:
http://pl.wikipedia.org/wiki/Wzorzec_metody_fabrykującej
http://www.fluffycat.com/PHP-Design-Patterns/Factory-Method
http://www.vincehuston.org/dp/factory_method.html
http://www.phppatterns.com/docs/design/the_factory_method

Wzorzec singleton należy do wzorców konstrukcyjnych. Służy on do ograniczania możliwości tworzenia obiektów danej klasy do jednej instancji oraz zapewniania globalnego punktu dostępu do niej. Wzorzec ten jest krytykowany ze względu na zbyt częste traktowanie go jako zamiennika dla zmiennych globalnych powszechnie uznawanych za niekorzystne.

Implementacja wzorca singleton:

PHP:
  1. class Singleton
  2. {
  3.     /**
  4.      * Obiekt instancji
  5.      *
  6.      * @var object
  7.      * @access private
  8.      * @static
  9.      */
  10.     static private $_instance;
  11.    
  12.     /**
  13.      * Konstruktor klasy
  14.      *
  15.      * @access private
  16.      */
  17.     private function __construct()  { }
  18.  
  19.     /**
  20.      * Metoda klonujaca
  21.      *
  22.      * @access private
  23.      */
  24.     private function __clone() { }
  25.    
  26.     /**
  27.      * Metoda zwraca instance obiektu
  28.      *
  29.      * @static
  30.      * @return object
  31.      */
  32.     static function getInstance()
  33.     {
  34.         if(!self::$_instance) {
  35.             self::$_instance = new self();
  36.         }
  37.         return self::$_instance;
  38.     }
  39.  
  40.     function foo()
  41.     {
  42.         // [...]
  43.     }
  44. }

Kluczowym elementem jest statyczna metoda getInstance(), która sprawdza istnienie prywatnej zmiennej statycznej $_instance. Jeśli zmienna ta nie istnieje wywoływany jest konstruktor klasy i do zmiennej jest przypisywany jej obiekt co zapobiega wielokrotnemu tworzeniu jej instancji. Jeśli obiekt klasy istnieje jest po prostu zwracany przez referencję. Dzięki temu, iż zarówno konstruktor jak i metoda __clone są prywatne nie ma możliwości ich wywołania spoza klasy co jest dodatkowym zabezpieczeniem przed próbą utworzenia obiektu bądź jego sklonowaniem.
Przykład wykorzystania:

PHP:
  1. Singleton::getInstance()->foo();

Najpierw pobieram instancję klasy Singleton a następnie wywołuję metodę foo().

Jak wspomniałem na początku wzorzec ten często uznawany jest za antywzorzec ponieważ bywa wykorzystywany jako zamienni dla zmiennych globalnych. Dlatego powinien być stosowanych tylko w określonych przypadkach głównie związanych z wydajnością np. pobieranie jednej instancji obiektu bazy danych ze względów na czas połączenia z bazą, oszczędność zasobów pamięci itp.

Więcej o wzorcu singleton:
http://pl.wikipedia.org/wiki/Wzorzec_singletonu
http://4programmers.net/PHP/Wzorce_Projektowe#id-Singleton
http://www.phppatterns.com/docs/design/singleton_pattern
http://www.fluffycat.com/PHP-Design-Patterns/Singleton
http://www.vincehuston.org/dp/singleton.html

Wzorzec decorator należy do wzorców strukturalnych. W praktyce umożliwia dynamiczne dodawanie nowych funkcji do istniejących klas bez ich fizycznej modyfikacji. Dzięki temu można rozszerzyć możliwości klasy bez ingerowania w jej kod pozostawiając jej oryginalną funkcjonalność.

Aby pokazać działanie wzorca decorator posłużę się prostym przykładem. Załóżmy, że mam klasę zapisującą dane do bazy. Nie chcę zmieniać jej kodu i funkcjonalności ale chcę ją rozszerzyć o walidację danych. Mógłbym zastosować dziedziczenie i wykorzystać dziedziczoną metodą do walidacji ale wówczas musiałbym zmodyfikować kod klasy głównej czego bardzo chcę uniknąć a dodatkowo każda zmiana klasy dziedziczącej wymuszałaby zmiany klas dziedziczonych.

Zacznę od stworzenia wspólnego interfejsu dla klasy dekorowanej i dekoratora:

PHP:
  1. interface IDecorator
  2. {
  3.     function save();
  4. }

Następnie stworzę klasę główną (dekorowaną) implementującą interfejs IDecorator, której kodu podczas rozszerzania nie mam zamiaru modyfikować:

PHP:
  1. class Decorable imlements IDecorator
  2. {
  3.     function save()
  4.     {
  5.         // zapis danych do bazy
  6.         // [...]
  7.     }
  8. }

Teraz przyszła kolej na stworzenie klasy dekorującej czyli wzbogacającej klasę główną Decorable o funkcję walidującą dane, która także implementuje interfejs IDecorator:

PHP:
  1. class Decorate implements IDecorator
  2. {
  3.     /**
  4.      * Obiekt klasy dekorowanej
  5.      *
  6.      * @var object
  7.      * @access private
  8.      */
  9.     private $_decorate;
  10.  
  11.     /**
  12.      * Konstruktor klasy
  13.      *
  14.      * @param object $decorable
  15.      */
  16.     function __construct(Decorable $decorable)
  17.     {
  18.         // przypisanie obiektu klasy dekorowanej do zmiennej prywatnej
  19.         $this->_decorable = $decorable
  20.     }
  21.  
  22.     /**
  23.      * Metoda uruchamia walidacje danych
  24.      */
  25.     function save()
  26.     {
  27.         // walidacja danych
  28.         // [...]
  29.  
  30.         // wywolanie oryginalnej metody z klasy dekorowanej
  31.         $this->_decorable->save();
  32.     }
  33. }

Do konstruktora klasy zostaje przekazany obiekt oryginalnej klasy dekorowanej Decorable. Na koniec pozostaje mi stworzenie obiektu klasy dekorowanej Decorable i przekazanie jej klasie dekorującej Decorate:

PHP:
  1. $action = new Decorate(new Decorable());
  2. $action->save();

Po stworzeniu obiektu klasy Decorable wywołuję metodę save(), która po przeprowadzeniu walidacji wywołuje metodę o tej samej nazwie klasy dekorowanej Decorable.

Jak więc widać rozszerzyłem funkcjonalność klasy oryginalnej nie dokonując w niej żadnych istotnych zmian. Podsumowując dekoratory są w pewnym sensie alternatywą dla dziedziczenia. Dziedziczenie rozszerza zachowanie klasy w trakcie kompilacji, w przeciwieństwie do dekoratorów, które rozszerzają klasy w czasie działania programu.

Więcej o wzorcu dekoratora:
http://pl.wikipedia.org/wiki/Wzorzec_dekoratora
http://4programmers.net/PHP/Wzorce_Projektowe#id-Dekorator
http://www.phppatterns.com/docs/design/decorator_pattern
http://www.fluffycat.com/PHP-Design-Patterns/Decorator
http://www.vincehuston.org/dp/decorator.html

Wzorzec observer należy do wzorców czynnościowych. W praktyce umożliwia on powiadamianie obiektów (obserwatorów) o zmianie danych obiektu czy klasy centralnej (obserwowanej) dając możliwość wielokrotnego wykorzystywania klas bez zbędnych modyfikacji ich kodu czyniąc go przy tym czytelniejszym i bardziej przejrzystym.

Aby pokazać działanie wzorca observer posłużę się prostym przykładem. Załóżmy, że mamy klasę modyfikującą dane w bazie i chcemy aby w momencie zapisu zmienionych danych został utworzony plik z cachem zawierającym nowe dane oraz aby informacja o tych zmianach trafiła do administratora.
Zacznę od stworzenia interfejsu dla obiektów obserwatorów, który zawierać będzie definicje metody update:

PHP:
  1. interface IObserver
  2. {
  3.     function update();
  4. }

Następnie stworzę 2 klasy obserwatorów implementujące powyższy interfejs, jedna odpowiedzialna będzie za utworzenie nowego cache'u a druga za powiadomienie admina o zmianach:

PHP:
  1. class CacheObserver implements IObserver
  2. {
  3.     /**
  4.      * Metoda zapisuje nowe dane do cache
  5.      */
  6.     function update()
  7.     {
  8.         // zapis danych do cache
  9.         // [...]
  10.     }
  11. }
  12.  
  13. class MailObserver implements IObserver
  14. {
  15.     /**
  16.      * Metoda wysyla maila do admina
  17.      */
  18.     function update()
  19.     {
  20.         // wyslanie maila do admina
  21.         // [...]
  22.     }
  23. }

Mając już obserwatorów utworzę klasę centralną (obserwowaną) czyli w naszym przykładzie modyfikującą dane osoby w bazie:

PHP:
  1. class ModifyObservable
  2. {
  3.     /**
  4.      * Tablica obserwatorow
  5.      *
  6.      * @var array
  7.      * @access private
  8.      */
  9.     private $_observers = array();
  10.  
  11.     /**
  12.      * Metoda dodaje obserwatora
  13.      *
  14.      * @param object Obiekt obserwatora
  15.      */
  16.     function addObserver($observer)
  17.     {
  18.         $this->_observers[] = $observer;
  19.     }
  20.    
  21.     /**
  22.      * Metoda powiadamia swoich obserwatorow o uaktualnieniu
  23.      */
  24.     function notify()
  25.     {
  26.         foreach($this->_observers as $observer) {
  27.             $observer->update();
  28.         }
  29.     }
  30.    
  31.     /**
  32.      * Metoda dokonuje modyfikacji danych
  33.      */
  34.     function update()
  35.     {
  36.         // modyfikacja danych
  37.         // [...]
  38.  
  39.         // powiadomienie obserwatorow
  40.         $this->notify();
  41.     }
  42. }

Metoda update() w klasie Modify aktualizuje dane oraz wywołuje metodę notify(), która to poprzez cykliczne wywołanie metod update() swoich obserwatorów (CacheObserver i MailObserver)

PHP:
  1. $modify = new ModifyObservable();
  2. $modify->addObserver(new CacheObserver());
  3. $modify->addObserver(new MailObserver());
  4. $modify->update();

Jak więc widać wzorzec ten jest dość prosty w implementacji i daje nam wymierne korzyści takie jak możliwość wielokrotnego wykorzystywania klas bez zbędnych modyfikacji kodu, unikanie trwałych i sztywnych zależności między klasami, czytelny i przejrzysty kod.

Więcej na temat wzorca observer:
http://pl.wikipedia.org/wiki/Wzorzec_obserwatora
http://4programmers.net/PHP/Wzorce_Projektowe#id-Obserwator
http://www.phppatterns.com/docs/design/observer_pattern
http://www.fluffycat.com/PHP-Design-Patterns/Observer/
http://www.vincehuston.org/dp/observer.html

Natchniony artykułem "Własne Google Video, czyli video streaming w PHP" w magazynie PHP Solutions 5/2006 (16) postanowiłem stworzyć prosty system na wzór google video. Użytkownik wrzuca plik (film bądź plik audio) w praktycznie dowolnym formacie a system konwertuje go do animacji Flash, generuje miniaturkę z klatki filmu i umieszcza odnośnik na stronie po kliknięciu na który uruchomiony zostaje odtwarzacz z wybranym filmem bądź muzyką pobierający kolejne części strumieniowo. Wydaje się być to proste i ku mojemu wielkiemu zdumieniu właśnie takie jest.

Aby dobrze zrozumieć zagadnienie zacznijmy od teorii czyli co będzie nam potrzebne. W skrócie będziemy potrzebowali formularza wysyłającego plik na serwer, enkodera, który wykona konwersję pliku oraz odtwarzacza multimedialnego stworzonego we Flashu.

Formularz

Jak wiadomo aby wysłać plik na serwer należy posłużyć się formularzem i polem typu file oraz opcjonalnym polecem ukrytym z nazwą MAX_FILE_SIZE umieszczonym przed polem typu file oznaczającym maksymalną wielkość pliku (w bajtach) jaka może być przesłana. Mimo, iż pole to jest opcjonalne warto je dodać do formularza, ponieważ unikneimy w ten sposób próby wysłania zbyt dużego pliku, który i tak zostanie odrzucony po stronie serwera, jeśli jego rozmiar będzie większy niż pozwala na to opcja upload_max_filesize i/lub post_max_size. Należy także pamiętać, że formularz musi być wysłany metodą POST a do jego definicji należy dodać atrybut enctype z wartością multipart/form-data informującego formularz, iż przesyłana będą zawartość wieloczęściowa. Formularz w najprostszej wersji może wyglądać tak:

HTML:
  1. <form name="upload" action="index.php" method="post" enctype="multipart/form-data">
  2.   <input type="hidden" name="MAX_FILE_SIZE" value="1000000" />
  3.   <input type="file" name="my_file" />
  4.   <input type="submit" value="wyślij" />
  5. </form>

Po poprawnym wysłaniu formularza plik trafia do katalogu tymczasowego i stamtąd powinien być przeniesiony do dowolnego naszego katalogu (posiadającego odpowiednie prawa do zapisu) np. upload co można wykonać prostym skryptem:

PHP:
  1. if(isset($_FILES['my_file'])) {
  2.     if(is_uploaded_file($_FILES['my_file']['tmp_name'])) {
  3.         move_uploaded_file($_FILES['my_file']['tmp_name'], 'upload/'.$_FILES['my_file']['name']));
  4.     }
  5. }

Glonalna tablica $_FILES zawieraja informacje o przesłanych plikach i posiada takie elementy jak:

  • $_FILES['my_file']['name'] - nazwa przesyłanego pliku
  • $_FILES['my_file']['type'] - typ mime przesyłanego pliku
  • $_FILES['my_file']['size'] - rozmiar przesyłanego pliku
  • $_FILES['my_file']['tmp_name'] - nazwa pliku w katalogu tymczasowym
  • $_FILES['my_file']['error'] - kod ewentualnego błędu

Dokładny opis obsługi uploadu w PHP znajduje się w manualu.

Jeśli powyższe operację się powiadą w naszym katalogu upload pojawi się przesłany plik. Dodatkowo, można zadbać o to, czy przesyłany format pliku jest właściwy co można wykonać poprzez sprawdzenie rozszerzenia (bywa zawodne) lub sprawdzenie typu mime (pewniejszy sposób).

Konwersja

Jak już wcześniej wspomniałem do wyświetlania uploadowanego pliku wykorzystamy odtwarzacz multimedialny wykonany we Flashu. Odtwarzacz ten standardowo obsługuje format pliku FLV (Flash Video) a więc aby poprawnie odtworzyć plik musimy dokonać konwersji przesłanego pliku. Do tego zadania wykorzystamy zewnętrzeny program ffmpeg dostępny zarówno dla systemów Uniksowych jak i Windowsowych. Program ten obsługuje pokaźną ilość formatów (audio, video oraz obrazki) i znakomicie nada się do naszych potrzeb. Posiada on sporą liczbę opcji i umożliwia dokonywanie wielu operacji na przetwarzanych plikach. Pełna lista opcji dostępna jest w dokumentacji. Aby bardziej zintegrować program ffmpeg z technologią PHP i ułatwić korzystanie z niego stworzono moduł do tego języka. Ze względu na problemy wynikające z różnych wersji PHP oraz małą dostępność na serwerach hostingowych nie będziemy używać modułu lecz zewnętrzenej aplikacji. Składnia polecenia dokonującego konwersji jest następująca:

CODE:
  1. ffmpeg [[opcje pliku wejściowego][-i plik wejściowy]]... {[opcje pliku wyjściowego] plik wyjściowy}...

W naszym przypadku musimy dokonać konwersji pliku do formatu FLV użyjemy więc poelcenia:

CODE:
  1. ffmpeg -i plik_wejsciowy -s 352x288 -r 25 -ar 22050 -ab 24 -f flv plik_wyjsciowy.flv

Powyższe polecenie dokona konwersji pliku wejściowego do formatu FLV. Dodane opcje oznaczają:

  • -s ustalenie rozmiaru (szerokość x wysokość) pliku wyjściowego (352x288)
  • -r ustawienie framerate'u pliku wyjściowego (25 klatek/sek)
  • -ar ustawienie częstotliwości próbkowania dźwięku w Hz (22050)
  • -ab ustawienie bitrate'u dźwięku pliku wyjściowego (24)
  • -f wymuszenie formatu pliku wyjściowego (FLV)

Po wykonaniu powyższego polecenia na ekranie zostaną pokazane informacje o typach plików, informacjach o nich oraz o wyniku operacji. Jeśli konwersja się powiedzie otrzymamy plik wynikowy w formacie Flash Video.

Teraz pozostaje wykonać to polecenie w skrypcie PHP podając jako plik wejściowy uploadowany poprzez formularz plik. Aby wykonać polecenie z poziomu skryptu PHP można skorzystać z kilku podobnych funkcji czyli exec, system, shell_exec oraz passthru. Różnią się one od siebie sposobem i typem zwracanego wyniku. My skorzystamy z funkcji exec.

PHP:
  1. exec('ffmpeg -i upload/'.$_FILES['my_file']['name'].' -s 352x288 -r 25 -ar 22050 -ab 24 -f flv upload/'.$_FILES['my_file']['name'].'.flv', $result);

Po wykonaniu powyższego skryptu przesłany na serwer plik zostanie skonwertowany do formatu FLV, do jego nazwy zostanie dodane nowe rozszerzenie a nowy plik zostanie utworzony w tym samym katalogu co plim przesłany czyli upload. Wynik operacji zostanie wstawiony do tablicy $result.

Program ffmpeg posiada także możliwość tworzenia zrzutów ekranu z wybranych klatek filmu co umożliwia nam np. wygenerowanie miniaturki zrzutu z klatek zaraz po konwersji pliku. Skrypt wykonujący taki zrzut może wyglądać następująco:

PHP:
  1. exec('ffmpeg -i upload/'.$_FILES['my_file']['name'].' -s 80x60 -vframes 1 -f mjpeg upload/'.$_FILES['my_file']['name'].'.jpg', $result)

Dokładny opis wszystkich opcji programu ffmpeg można znaleźć w dokumentacji.

Aby uniknąć trudnych do zlokalizowania błędów polecam najpierw przeprowadzić konwersję bezpośrednio z wiersza poleceń i śledzić komunikaty pojawiające się na ekranie i jeśli konwersja się powiedzie wtedy uruchomić polecenie ze skryptu PHP.

Po wykonaniu powyższych operacji powstanie nowy skonwertowany plik w formacie akceptowalnym przez odtwarzacz we Flashu.

Odtwarzanie

Na koniec przejdźmy do najbardziej efektownej operacji czyli odtworzenia przekonwertowanego filmu bądź dźwięku. Będziemy potrzebowali do tego odtwarzacz multimedialny, który dostępny jest jako komponent w programie Macromedia Flash. Ze względu na fakt, iż program ten jest komercyjny możemy skorzystać z innych rozwiązań. Jeśli mamy czas i jesteśmy zapalonymi programistami możemy stworzyć własny odtwarzacz bez posiadania programu firmy Macromedia i skorzystać z modułu PHP o nazwie MING. Moduł ten pozwala na generowanie animacji Flash bez żadnych zewnętrznych aplikacji. Instalacja tego rozszerzenia opisana jest w dokumentacji.

Możemy jednak pominąć ten krok i skorzystać z gotowych odtwarzaczy plików FLV. Godnym uwagi jest darmowy do zastosowań niekomercyjnych Flash Video Player umożliwiający odtwarzanie strumieniowe plików FLV. Na stronie autora dostępna jest zarówno wersja skompilowana (SWF) jak i źródła (FLA) dzięki czemu możemy sami dokonać stosowanych modyfikacji w odtwarzaczu dostosowując go do naszych potrzeb. Odtwarzacz ten jest bardzo prosty i przez to idealnie nadaje się do naszych celów.

Aby skorzystać z odtwarzacza należy osadzić go w kodzie strony a następnie dodać zmienną file, której wartość stanowi nazwa pliku FLV, który ma być odtworzony. Najprościej można zrobić to w następujący sposób:

HTML:
  1. <object type="application/x-shockwave-flash" width="352" height="288" wmode="transparent" data="flvplayer.swf?file=upload/plik.flv">
  2.     <param name="movie" value="flvplayer.swf?file=upload/plik.flv" />
  3.     <param name="wmode" value="transparent" />
  4. </object>

Odtwarzacz znajduje się w pliku flvplayer.swf, jego rozmiar to 400x200 a odtwarzany plik znajduje się w katalogu upload/plik.flv. Pozostałe zmienne, których użyć można aby zmodyfikować nieco sam odtwarzacz opisane są w pliku readme.html.

Podsumowanie

Na koniec chciałbym złożyć wszystko w jedną całość czyli 1 skrypt PHP, który umożliwi upload pliku, wyświetlenie listy plików wraz ze zrzutami ekranu a następnie ich odtworzenie. Do poprawnego działania będzie potrzebny opisany wcześniej program ffmpeg, możliwość użycia jednej z wcześniej opisanych funkcji uruchamiających polecenia systemowe oraz katalog do uploadu (z prawami do zapisu).

Źródło skryptu napisane w PHP5 znajduje się w pliku stream.php. Umieściłem w nim formularz do upload'u plików, kod odtwarzacza oraz centralną klasę stream, która zawiera metody umożliwiające obsługę uploadu, konwersje pliku i wygenerowanie zrzutu. Dodatkowo użyłem w nim stałych, które definiują ścieżki do plików oraz programu ffmpeg. Kod został napisany w PHP5 jednak ze względu na nadal dużą popularność PHP4 na serwerach hostingowych napisałem także wersję pod PHP4 - stream4.php.

Mam nadzieje, że opis wykonania streamingu video wydał się zrozumiały. Jeśli pojawią się jakieś problemy bądź uwagi proszę o komentarze bądź maile.