PHP/Wersja do druku

< PHP


PHP



Witamy w kursie PHP na Wikibooks. Podręcznik nie jest jeszcze ukończony, jeśli czujesz się na siłach - chętnie przyjmiemy twoją pomoc.


 
 


Spis treści

edytuj

Wprowadzenie

edytuj
  1. O podręczniku Etap rozwoju: 100%
  2. Czym jest PHP Etap rozwoju: 100%
  3. Możliwości Etap rozwoju: 100%
  4. Jak się uczyć? Etap rozwoju: 100%

Instalacja

edytuj
  1. Opis instalacji Etap rozwoju: 100%
  2. Apache HTTP Server Etap rozwoju: 50%
  3. MySQL 5 Etap rozwoju: 50%
  4. PHP Etap rozwoju: 100%

Podstawy języka

edytuj
  1. Pierwszy skrypt Etap rozwoju: 100%
  2. Zmienne i tablice Etap rozwoju: 100%
  3. Formularze Etap rozwoju: 100%
    1. Instrukcja if Etap rozwoju: 100%
    2. Instrukcja switch Etap rozwoju: 100%
    3. Instrukcja for Etap rozwoju: 100%
    4. Instrukcja while Etap rozwoju: 100%
    5. Instrukcja do while Etap rozwoju: 100%
    6. Instrukcja foreach Etap rozwoju: 100%
  4. Funkcje Etap rozwoju: 100%
  5. Inne elementy składni Etap rozwoju: 100%
  6. Każdy popełnia błędy Etap rozwoju: 100%
  7. Korzystanie z dokumentacji Etap rozwoju: 100%
  8. Studium przypadku: Księga gości Etap rozwoju: 100%
  9. Ćwiczenia Etap rozwoju: 75%

Rozmaitości

edytuj
  1. Przetwarzanie tekstu Etap rozwoju: 100%
  2. Podstawy wyrażeń regularnych Etap rozwoju: 50%
  3. Obsługa ciastek Etap rozwoju: 100%
  4. Sesje Etap rozwoju: 100%
  5. Wysyłanie e-maili Etap rozwoju: 100%
  6. Internacjonalizacja Etap rozwoju: 100%
  7. System plików Etap rozwoju: 75%
  8. Data i czas Etap rozwoju: 100%
  9. Ćwiczenia

Programowanie obiektowe

edytuj
  1. Czym jest programowanie obiektowe? Etap rozwoju: 100%
  2. Klasy i obiekty Etap rozwoju: 100%
  3. Konstruktory i destruktory Etap rozwoju: 100%
  4. Dziedziczenie Etap rozwoju: 100%
  5. Interfejsy Etap rozwoju: 100%
  6. Wyjątki Etap rozwoju: 100%
  7. Elementy statyczne Etap rozwoju: 100%
  8. Metody magiczne Etap rozwoju: 100%
  9. Iteratory Etap rozwoju: 100%
  10. Automatyczne ładowanie Etap rozwoju: 100%
  11. Ćwiczenia Etap rozwoju: 25%

Zaawansowane programowanie

edytuj
  1. Domknięcia
  2. Przestrzenie nazw
  3. Archiwa PHAR
  4. Podstawy przetwarzania XML-a
  5. Wzorce projektowe
  6. XDebug
  7. Ćwiczenia

Bazy danych

edytuj
  1. Wstęp do baz danych Etap rozwoju: 100%
    1. Projekt bazy danych Etap rozwoju: 100%
    2. Zarządzanie rekordami Etap rozwoju: 100%
    3. Pobieranie rekordów Etap rozwoju: 100%
    4. Relacje i indeksy Etap rozwoju: 100%
  2. Biblioteka PDO Etap rozwoju: 100%
  3. ORM i biblioteka Doctrine
  4. Jak to się robiło kiedyś? Etap rozwoju: 100%
  5. phpMyAdmin Etap rozwoju: 100%
  6. Studium przypadku: System newsów Etap rozwoju: 25%
  7. Bazy danych - co dalej? Etap rozwoju: 100%
  8. Ćwiczenia

Systemy szablonów

edytuj
  1. Czym jest system szablonów? Etap rozwoju: 100%
  2. Prosty edukacyjny system szablonów Etap rozwoju: 100%
  3. Wybrane systemy szablonów: Etap rozwoju: 100%
    1. Savant
    2. Open Power Template Etap rozwoju: 75%
    3. PHPTAL
    4. Smarty
  4. Ćwiczenia

Frameworki

edytuj
  1. Czym jest framework? Etap rozwoju: 100%
  2. Wzorce złożone: MVC, MVP i pochodne
  3. Prosty framework edukacyjny
  4. Wybrane frameworki:
    1. Codeigniter Framework
    2. CakePhP Framework
    3. Prado Framework
    4. Yii Framework
    5. Symfony Framework
    6. Zend Framework
  5. Studium przypadku: Prosty blog
  6. Ćwiczenia

Bezpieczeństwo

edytuj
  1. Wstęp do zagadnień bezpieczeństwa
  2. Techniki ataków
  3. Zabezpieczanie sesji
  4. Bezpieczne zarządzanie danymi
  5. Kontrola formularzy
  6. Obrona przed botami
  7. Podstawy kryptografii
  8. Mechanizmy uwierzytelniania
  9. Mechanizmy kontroli uprawnień
  10. Połączenia szyfrowane
  11. Ćwiczenia

Dobre praktyki

edytuj
  1. Standardy kodowania
  2. Dokumentowanie kodu
  3. Testowanie aplikacji
  1. Edytory PHP Etap rozwoju: 75%
  2. Autorzy Etap rozwoju: 100%
  3. Dla twórców podręcznika Etap rozwoju: 100% (w dniu 13.04.2005)


Archiwum zawiera fragmenty rozdziałów, które były kiedyś zaczęte, lecz zostały odłożone na później lub są już niepotrzebne z różnych przyczyn (np. zawarta w nich treść jest przekazywana w zupełnie inny sposób).

  1. SQL Injection Etap rozwoju: 25%
  2. Hashowanie Etap rozwoju: 25%
  3. PHP Injection Etap rozwoju: 00%
  4. Bazy danych i sesje Etap rozwoju: 100%

Wprowadzenie

edytuj

O podręczniku

edytuj

Podręcznik ten nie jest "jeszcze jednym kursem PHP". To zbiorowe opracowanie, które nie tylko uczy, jak programować w języku PHP, lecz również pokazuje, jak robić to dobrze. Staramy się demonstrować najnowsze możliwości projektu, stawiając na aktualność. Podręcznik może być czytany przez wszystkich: zarówno przez osoby, które jeszcze nigdy nie miały do czynienia z programowaniem, jak i tych znających już jakieś języki.

PHP, choć na pierwszy rzut oka prosty do nauczenia, jest w rzeczywistości bardzo rozbudowanym projektem i zwyczajne wypisanie listy konstrukcji oraz funkcji nie wystarczy, aby powiedzieć o sobie "jestem doświadczonym programistą". Doświadczenie przychodzi z czasem, jednak w tym podręczniku staramy się to maksymalnie ułatwić, koncentrując się także na praktycznej stronie programowania. Pokazujemy, czego należy unikać, na co uważać, a w "Studiach przypadku" wyjaśniamy, jak zdobytą wiedzę wykorzystać do napisania skryptu realizującego konkretne zadanie.

Od czytelników tego podręcznika wymagamy pewnej znajomości języka HTML i orientowania się w tworzeniu stron WWW. Związane jest to z omawianym tu materiałem ukierunkowanym w głównej mierze na pisanie dynamicznych witryn internetowych. Jeżeli czujesz, że brakuje Ci wiedzy, zacznij swoją przygodę od poznania języka HTML. Znajomość programowania i algorytmów nie jest konieczna, aczkolwiek pozwoli na szybsze przyswajanie materiału.

Po przeczytaniu podręcznika będziesz wiedział:

  • Co to jest PHP i jak działa?
  • Jak postawić na własnym komputerze prosty serwer WWW.
  • Jak pisać aplikacje w języku PHP
  • Jak zarządzać bazami danych
  • W jaki sposób napisać profesjonalny i łatwy w rozbudowie dynamiczny serwis WWW
  • Jak poprawić bezpieczeństwo swych aplikacji i ustrzec się przed włamywaczami

Podręcznik poprawiać i uzupełniać może każdy. Jeśli tylko masz czas i nie widzisz żadnych przeszkód w liberalnej licencji GNU FDL, pomóż w jego rozwoju: dopisz rozdział, zaktualizuj zawartość, popraw błędy (dalsze informacje). Każda pomoc jest ważna, gdyż tworzą to ludzie dla ludzi. Pamiętaj jednak, aby nie używać żadnych materiałów objętych prawem autorskim, a wszelkie większe edycje konsultować w dyskusji z innymi.


Czym jest PHP?

edytuj

PHP to skryptowy język programowania - programy w nim napisane nie są kompilowane do postaci kodu maszynowego zrozumiałego dla procesora, lecz wykonywane przez specjalną aplikację zwaną interpreterem PHP. PHP został pierwotnie stworzony do wspomagania tworzenia dynamicznych stron WWW, lecz obecnie można w nim pisać także zwyczajne aplikacje dla systemu operacyjnego.

PHP jest projektem open-source. Każdy może pobrać za darmo jego kopię, zainstalować i używać bez żadnych ograniczeń. Do kodu źródłowego zapewniony jest pełny dostęp - jeżeli programista ma odpowiednie zdolności, może go modyfikować do woli oraz nadsyłać własne propozycje zmian do osób nadzorujących projekt. Dzięki takiej wolności PHP rozwija się bardzo dynamicznie, a w Internecie można znaleźć setki modyfikacji oraz dodatkowych modułów.

Mimo otwartości kodu, nad rozwojem oficjalnej wersji projektu czuwa firma Zend Company z Izraela założona przez twórców PHP. Zapewnia ona dodatkowe narzędzia i opiekę prawną, a także wyznacza kierunki rozwoju projektu.

Bardzo istotną cechą PHP jest skalowalność. Może go używać z powodzeniem zarówno początkujący programista, dzięki uproszczeniom składni, jak i ekspert, który znajdzie tutaj wszystkie zaawansowane narzędzia niezbędne do napisania rozbudowanej aplikacji. PHP znaleźć można wszędzie: od prywatnych stron domowych przez gry internetowe aż do potężnych witryn korporacji lub portali. Opanowanie języka jest proste także dlatego, iż Internet pełen jest przewodników oraz artykułów pokazujących, jak w praktyce wykorzystywać wiele z jego możliwości.

Kiedy zaczynaliśmy pisać podręcznik, pod sam koniec 2005 roku, najnowszą dostępną wersją PHP była 5.1.1. Obecnie (czerwiec 2009 roku) dostępna jest wersja 5.3.0. Jej możliwości są zbliżone do tych oferowanych przez konkurencyjne języki skryptowe dla stron WWW: ASP, JSP. Staramy się, aby podręcznik opisywał możliwie najnowszą wersję, jednak z powodu dużej ilości materiału niekiedy mogą pojawiać się fragmenty odnoszące się do starszych wydań. W podręczniku nie opisujemy PHP 4.x, praktycznie nierozwijanego, ale wciąż popularnego na serwerach oraz wśród starszych programistów.

PHP jest wykonywany po stronie serwera. Oznacza to, że PHP nie jest interpretowany (przetwarzany) przez przeglądarkę, lecz przez specjalny program na serwerze. Co innego HTML, JavaScript w wersji wykonywanej po stronie klienta. Jak działa interpretator PHP? To proste - przerabia instrukcje PHP na HTML i JavaScript w wersji do interpretacji po stronie klienta, które trafiają na Twój komputer i wiesz co się dzieje. A przy okazji - Wikibooks i jej siostrzany projekt - Wikipedia - są napisane w PHP.

Jak PHP współpracuje ze stroną WWW?

edytuj

PHP jest językiem server-side, tj. pracuje po stronie serwera WWW. Przeciwieństwem są języki client-side pracujące po stronie przeglądarki użytkownika (np. JavaScript w wersji wykonywanej po stronie klienta). Aby wykorzystywać go na własnej stronie, musisz upewnić się, że twój serwer WWW ma zainstalowaną jego obsługę. Zanim przejdziemy dalej, należy zrozumieć zasadę, na jakiej PHP generuje dynamiczne strony WWW.

Kiedy wpisujemy adres w przeglądarce internetowej, żądanie wyświetlenia strony kierowane jest do serwera HTTP zwanego także serwerem WWW. Jeśli stwierdzi on, na podstawie rozszerzenia pliku, że dany dokument zawiera kod PHP, kieruje do interpretera żądanie przetworzenia podanego pliku. Interpreter wyszukuje w jego treści tzw. wstawki PHP wplecione w statyczny kod HTML i zastępuje je wynikiem ich przetworzenia. Utworzony kod HTML jest zwracany serwerowi, a ten wysyła go z powrotem do internauty.

W tym procesie kod PHP nigdy nie opuszcza serwera. Internauta zawsze otrzyma wyłącznie utworzony przez PHP kod HTML. Oto przykład. Jeśli mamy plik PHP o następującej treści:

<html>
<body>
<?php
  echo 'Podaj login';
?>
</body>
</html>

To internauta zobaczy jedynie dokument o takiej treści:

<html>
<body>
Podaj login
</body>
</html>

Cały PHP zniknie, a na jego miejscu pojawi się utworzony przez niego kod HTML.

Dzięki pracy po stronie serwera, PHP idealnie nadaje się do tworzenia złożonych aplikacji zarządzających dużymi ilościami danych: forami dyskusyjnymi, systemami zarządzania treścią, sklepami internetowymi. Generują one odpowiedni kod HTML dla przeglądarki, a w momencie kiedy internauta przegląda stronę, PHP już zakończył nad nią swą pracę. Jest to bardzo istotne, ponieważ wszelkie dalsze reakcje na poczynania użytkownika należy albo pozostawić przeglądarce, albo obsłużyć je za pomocą języka JavaScript.

W PHP stworzono m.in. aplikację MediaWiki, za pomocą której podręcznik ten jest oficjalnie dostępny w ramach projektu Wikibooks. PHP zarządza tutaj pobieraniem treści żądanej strony z bazy, sformatowaniem jej, a w przypadku kliknięcia na opcję "Edytuj" - dodaniem nowej wersji tekstu. JavaScript pracujący po stronie przeglądarki użytkownika ułatwia edycję tekstu, obsługując przyciski automatycznie wstawiające niektóre rodzaje formatowania oraz znaki narodowe. Jest to dobry przykład współpracowania tych dwóch języków w naprawdę dynamicznej aplikacji internetowej.

Historia projektu

edytuj

Pierwsza wersja PHP, rozpowszechniana pod nazwą PHP/FI ("Personal Home Page/Forms Interpreter"), została stworzona przez Rasmusa Lerdorfa w roku 1994 jako zestaw skryptów Perla służący do monitorowania internautów odwiedzających jego witrynę. Gdy ruch stał się zbyt duży, przepisał je w języku C, dodając przy tym nowe opcje. Niedługo później ludzie zaczęli prosić go o możliwość użycia tych narzędzi na swoich stronach, zatem 8 czerwca 1995 roku autor udostępnił publicznie kod źródłowy (PHP Tools 1.0). Już kilka miesięcy później projekt przekształcił się w zalążek znanego obecnie języka programowania, gdy został połączony z innym narzędziem Rasmusa Lerdorfa - "Form Interpreter", które dało drugi człon nazwy. W 1997 roku pojawiło się PHP/FI 2.0, posiadające wtedy kilka tysięcy aktywnych użytkowników na całym świecie oraz obsługujące 50 tys. domen. Co ciekawe, wersja ta spędziła większość "życia" na beta testach. Oficjalne wydanie było tylko jedno i ukazało się w listopadzie 1997 roku.

W 1997 roku projektem zainteresowali się dwaj izraelscy programiści: Zeev Suraski i Andi Gutmans. Odkryli oni, że PHP/FI ma zbyt małe możliwości na potrzeby aplikacji eCommerce, którą tworzyli na uniwersytecie. Zdecydowali wtedy, że przepiszą kod PHP całkowicie od nowa, korzystając z pomocy już istniejącej społeczności PHP. W czerwcu 1998 roku ogłosili PHP 3.0 jako następcę PHP/FI, którego dalszy rozwój został wtedy zatrzymany. Był to wielki krok naprzód. PHP 3.0 posiadało całkowicie nową architekturę, która znacznie zwiększała wydajność. Pojawiły się w niej zalążki programowania obiektowego, ale najważniejszą cechą aplikacji była jej modularność. Użytkownicy mogli rozszerzać teraz funkcjonalność języka poprzez dodawanie nowych modułów.

Krótko po wydaniu PHP 3, w zimie 1998 Zeev Suraski oraz Andi Gutmans jeszcze raz zabrali się za przepisywanie kodu źródłowego PHP, korzystając z doświadczeń nabytych przy pracach nad poprzednią wersją. Za główne cele obrali poprawienie modułowości oraz wydajności złożonych aplikacji. Choć dotychczasowa wersja potrafiła sobie z nimi poradzić, nie była jednak stworzona do tego celu i przegrywała przez to z innymi rozwiązaniami.

W połowie roku 1999 ukazał się oficjalnie Zend Engine, nowy silnik języka skryptowego, wokół którego niedługo później zaczęto budować PHP 4. Jego nazwa to kompromisowe połączenie imion twórców projektu. Nowa, oparta o niego wersja PHP, ukazała się w maju 2000 roku. Tak jak poprzednio, był to potężny krok naprzód. Programiści mieli do dyspozycji teraz wiele nowych narzędzi, konstrukcji językowych oraz bezpieczniejszy system wejścia/wyjścia. Od strony administracyjnej pojawiło się oficjalne wsparcie dla wielu nowych serwerów. Przez cztery lata od chwili wydania ukazały się trzy kolejne edycje tej wersji oznaczone numerami: 4.1, 4.2 oraz 4.3. W każdej z nich odczuwalne było zwiększenie bezpieczeństwa, szybkości działania oraz możliwości. W 2004 roku obsługiwały one łącznie 20% wszystkich domen sieciowych. Również daleko po premierze PHP 5, "czwórka" była bardzo chętnie wykorzystywana przez administratorów, ze względu na dużą stabilność.

W 2002 roku Zeev Suraski oraz Andi Gutmans ponownie rozpoczęli znaczącą modernizację silnika PHP mającą na celu dodanie do tego języka modelu obiektowego z prawdziwego zdarzenia. W lutym 2003 ukazała się pierwsza wersja alpha nowej wersji PHP oznaczonej numerem 5.0.0. Stabilna wersja ukazała się prawie półtora roku później, w lipcu 2004 roku. Nowości sprawiły, że PHP zaczął konkurować z innymi rozwiązaniami server-side jak równy z równym. Pojawił się całkowicie nowy model programowania obiektowego, przez co niestety została utracona część kompatybilności z poprzednimi wersjami PHP w niektórych skryptach. Jest to spowodowane zmianą sposobu reprezentacji obiektów. Przebudowano także wiele modułów, w tym do obsługi XML'a i komunikacji z bazą danych, czyniąc je bardziej przyjaznymi dla programistów.

W połowie roku 2005 zaczęły pojawiać się oficjalne sygnały, że rozpoczęto wstępne prace nad PHP 6. Obecnie publicznie dostępne są codzienne snapshoty rozwojowego repozytorium kodu źródłowego, które można ściągnąć i przetestować. Głównym celem jest dalsze dążenie do ujednolicenia projektu, wprowadzenia kolejnych możliwości wymaganych przez złożone projekty (m.in. pełne wsparcie Unicode czy system cache'owania kodu). Usuwane są też kolejne archaiczne rozwiązania pochodzące jeszcze z czasów PHP/FI oraz PHP3, co w przypadku najstarszych skryptów ponownie spowoduje problemy z kompatybilnością.

Na podstawie Wikipedii


Możliwości

edytuj

Budowa PHP

edytuj

PHP ma budowę modułową. Jądro projektu zapewnia obsługę wszystkich elementów języka oraz dostęp do podstawowego zestawu funkcji. Aby dodać więcej możliwości, instaluje się dodatkowe moduły. Część z nich ma charakter oficjalny i jest dołączona do każdej dystrybucji, pozostałe zgrupowane są w tzw. repozytorium PECL i należy je samodzielnie pobrać.

PHP, dzięki otwartości kodu źródłowego, pracuje bez problemu na niemal każdym istniejącym obecnie 32-bitowym systemie operacyjnym (z Uniksem oraz Windowsem na czele) oraz pozwala na łatwą integrację z większością serwerów WWW, np. Apache, IIS, OmniHTTPD.

Co PHP może zrobić?

edytuj

PHP 5.1 oferuje swoim programistom wiele możliwości:

  1. Komunikacja z wieloma popularnymi bazami danych poprzez jednolity interfejs.
  2. Obsługa wielu popularnych protokołów sieciowych, m.in. SSL, IMAP, SMTP, IRC.
  3. Profesjonalne wsparcie standardu XML.
  4. Tworzenie obrazków w wielu popularnych formatach graficznych.
  5. Wiele funkcji obróbki tekstu.
  6. Wyrażenia regularne.
  7. Bardzo elastyczne tablice o mieszanych kluczach.
  8. Wsparcie dla usług sieciowych (SOAP, XML-RPC).
  9. Zaawansowany model programowania obiektowego.
  10. Możliwość zintegrowania z platformą .NET.
  11. Obsługa obiektów COM w Windows.
  12. Funkcje kryptograficzne.
  13. Funkcje konwersji kalendarza.
  14. Funkcje konwersji kodowań.
  15. Algorytmy kompresji: GZip oraz BZip2.

PHP jest nie tylko językiem internetowym. Można go także używać do pisania aplikacji na konsolę systemu operacyjnego, a nawet programów okienkowych z graficznym interfejsem użytkownika. Dlatego stanowi atrakcyjną alternatywę dla języków takich jak Perl. Instalator napisany w PHP, uruchamiany z konsoli systemowej, posiada m.in. repozytorium PEAR.

Wady PHP

edytuj

Początkowe lata rozwoju miały jednak w sobie coś z pospolitego ruszenia. Z tamtego okresu do dziś przetrwało trochę niekonsekwencji oraz nieścisłości, które sprawiają pewne kłopoty programistom. Twórcy powoli je usuwają, ale potrwa to jeszcze wiele lat.

  1. Ujednolicenie nazewnictwa - większość starszych modułów korzysta z różnych standardów nazywania poszczególnych funkcji, co utrudnia ich zapamiętywanie.
  2. Kilka niepotrzebnych rozwiązań utrudniających życie programistom: magic quotes oraz register globals

Konkurencja ma jeszcze jedną przewagę nad PHP. Interpretery języków, takich jak ASP, kompilują skrypty do bardziej czytelnej dla nich postaci, oszczędzając wiele czasu. PHP za każdym uruchomieniem wykonuje całą pracę od zera i choć jego interpreter jest niezwykle wydajny, z powodu nadkładania sobie pracy szybko traci zapas mocy. Możliwość kompilacji skryptów dostępna jest wyłącznie jako rozszerzenie.

Twórcy PHP świadomi są tych ograniczeń i starają się nie ignorować programistów. Problemem jest zachowanie kompatybilności wstecznej z olbrzymią bazą już stworzonego kodu PHP, który musi działać na nowych wersjach, najlepiej bez przeróbek. Zmiany są wprowadzane stopniowo i dlatego każdy, kto chciałby związać swoją karierę programisty WWW z językiem PHP, powinien śledzić aktualności pojawiające się na stronach www.zend.com oraz www.php.net.

Popularność PHP

edytuj

PHP jest, mimo swoich wad, niezwykle popularny. Wykonać w nim można wszystkie typy witryn internetowych: od stron domowych przez gry internetowe, aż do komercyjnych portali oraz witryn wielkich korporacji. Polska społeczność PHP jest bardzo aktywna i napisała do tego języka wiele poradników co w połączeniu z łatwym dostępem do książek, daje naprawdę atrakcyjny produkt, jeśli chodzi o wsparcie. Nietrudno jest także zlokalizować dobry hosting, który za niewielką opłatą udostępni na serwerze konto z PHP oraz bazą danych.


Jak się uczyć?

edytuj

Nauka PHP to długi i ciągły proces. Nie należy się spodziewać, że podręcznik ten nauczy kogokolwiek wszystkiego, ponieważ nie jest to jego założenie. Podręcznik ma na celu wprowadzić w temat, zainteresować nim i pokazać ogół zagadnień, a cała reszta zależy już od konkretnego człowieka. Pamiętaj, że bez praktyki dużo nie osiągniesz. Dyskutuj z innymi programistami, analizuj cudze skrypty, podpatruj rozwiązania lub też wymyślaj własne. Eksperymenty nie niosą ze sobą żadnych poważnych skutków ubocznych i powinny być praktykowane przez każdego.

Nie przejmuj się, że twoje pierwsze projekty będą miały w sobie nieco z chaotyczności. To normalny proces, niemniej nie można na tym poprzestać. Za drugim razem zadaj sobie pytanie: co poprzednio mogłem zrobić lepiej? Czego mnie ten projekt nauczył? W ten sposób stopniowo dojdziesz do wprawy.

Duże znaczenie ma również rozumienie samych komputerów i algorytmów. PHP jest normalnym językiem programowania i pewnych spraw nie da się ominąć. Teoria bardzo dobrze uzupełnia praktykę, dlatego dokształcaj się także pod tym względem. Pozwoli Ci to zrozumieć, dlaczego dany problem rozwiązywany jest tak, a nie inaczej lub dlaczego w ogóle się nim zajmujemy.

Cała wiedza o PHP zgromadzona jest w obszernej dokumentacji (www.php.net/docs.php), zawierającej m.in. opis wszystkich modułów. Należy nauczyć się sprawnego poruszania po niej i orientowania się, gdzie co leży. Temu zagadnieniu poświęcony został jeden z rozdziałów podręcznika. Do dokumentacji warto zaglądać regularnie, gdyż jest ona zawsze zgodna z najnowszą dostępną wersją PHP. Stamtąd najszybciej się dowiesz, czy nie pojawiły się nowe funkcje, parametry, możliwości wykorzystania istniejących elementów. Jeżeli zamierzasz wiązać się na dłużej ze środowiskiem PHP, adres [www.php.net] powinien nawet znaleźć się wśród najczęściej przeglądanych przez Ciebie witryn. Stanie w miejscu, kiedy projekt ten posuwa się naprzód, jest kiepską alternatywą i bez orientowania się w zachodzących zmianach szybko odkryjesz, że twoje skrypty z tajemniczych powodów nie chcą pracować na nowych wersjach, co przyprawia o złość klienta.

Teoria czy praktyka?

edytuj

Wielu początkujących programistów zadaje sobie pytanie - czy skupić się na teorii, czy też raczej na praktyce? Odpowiedź jest prosta: nie można się skupiać wyłącznie na jednym zagadnieniu. Ważna jest bowiem wiedza jak działa ten język programowania, ale nie można nie wiedzieć jak go wykorzystać.

Przed przystąpieniem do pisania skryptu powinniśmy rozrysować sobie jego projekt na papierze oraz ustalić pewne konwencje, jakich będziemy się trzymali w kodzie. Istotne jest, aby planowane przedsięwzięcie umieć zrealizować w praktyce. Choć projekty często tworzymy, aby nauczyć się czegoś nowego, poprzeczka nie powinna być windowana zbyt wysoko, gdyż wtedy nigdy go nie ukończymy. O wiele lepsze jest stopniowe poznawanie coraz bardziej złożonych elementów.

W książce tej duży nacisk położony jest na ponowne wykorzystywanie raz napisanego kodu. Uważamy, że nie ma sensu raz po raz na nowo odkrywać koła i tworzyć wszystkiego od nowa. Dobry programista po pewnym czasie utworzy sobie zbiór bibliotek z najczęściej używanymi funkcjami, które będzie przenosić między kolejnymi projektami, oszczędzając sobie zbędnej pracy. Praktyki te także powinny być w naszym planie uwzględnione. Inne sprawy, na które powinniśmy zwrócić uwagę, to:

  • nazewnictwo - czy stosujemy nazwy polskie, czy angielskie; w jakim stylu je zapisujemy: nazwa_nazwa czy nazwaNazwa,
  • jednolity styl kodowania,
  • jakie biblioteki zewnętrzne wykorzystamy oraz w jakim stopniu,
  • ogólna budowa całej aplikacji:
    • jaką drogę pokonują dane podczas tworzenia strony,
    • w jaki sposób dane są przetwarzane,
    • jak dane są wyświetlane,
    • jak połączone są ze sobą poszczególne elementy aplikacji,
    • czy przewidujemy w przyszłości dalszą rozbudowę projektu; jeśli tak: jakie kroki podejmiemy, aby ją maksymalnie ułatwić.

Projekty aplikacji można sporządzać również na komputerze. Pomocne będą tu wszelkie edytory tekstu, generatory diagramów itd. Po głębszym poznaniu języka PHP można zaznajomić się również z diagramami UML wykorzystywanymi w zawodowych zespołach programistycznych.

Fora dyskusyjne

edytuj

Niestety, nie każdy problem będziemy w stanie rozwiązać samodzielnie. Wtedy z pomocą przyjdą nam fora dyskusyjne, których dużo jest w polskim Internecie. Zanim zadasz jakieś pytanie, użyj wyszukiwarki, często bowiem odpowiedź została już wcześniej udzielona. Pamiętaj, że często aby uzyskać odpowiedź, musisz zastosować się do reguł forum. Poświęć przynajmniej kilka minut na zapoznanie się z regulaminem oraz zaleceniami. Ich przeczytanie to chwila, a może zaoszczędzić ci przykrości przy kontakcie z moderatorem. Zamieszczamy ten krótki poradnik, gdyż dużo początkujących programistów nie zwraca na te niby oczywiste rzeczy uwagi, co skutkuje długim oczekiwaniem na pomoc lub konfliktem z moderatorem na samym wstępie.

Zanim napiszesz post, upewnij się, że:

  • Umieszczasz go na odpowiednim forum i w odpowiednim dziale. Niektóre serwisy posiadają podział na forum dla spraw podstawowych i zaawansowanych. Zdecydowanie zalecamy pisanie na tym pierwszym. Wciskanie się na siłę na forum dla zaawansowanych programistów z tematami "Jak połączyć się z bazą XXX" niemal na pewno zostaną źle odebrane przez internautów.
  • Użyłeś wyszukiwarki i nie znalazłeś odpowiedzi na swoje pytanie. Z pomocą wyszukiwarki często znajdziesz odpowiedź znacznie szybciej, niż gdybyś miał oczekiwać na odpowiedź na forum.
  • Problem nie jest wyjaśniony w jakimś artykule w serwisie. Wiele serwisów posiada bazę artykułów i porad, a także zbiór FAQ (Najczęściej Zadawane Pytania), które wyjaśniają niektóre sprawy. Warto się z nimi zapoznać przed przystąpieniem do pisania.

Aby szybko uzyskać odpowiedź, upewnij się, że:

  • podałeś wersje oprogramowania,
  • podałeś fragment kodu źródłowego powodujący błąd,
  • w załączonym kodzie nie ma jakichś ważnych danych osobowych, haseł itd. które powinieneś usunąć,
  • wyjaśniłeś dokładnie, co jest z nim nie tak - nie używaj nigdy pojedynczego zwrotu nie działa, gdyż jest on wieloznaczny i prawie zawsze zostaniesz zapytany o więcej szczegółów,
  • twój post jest poprawnie napisany pod względem ortograficznym i interpunkcyjnym - błędy nie tylko utrudniają zrozumienie twojej wypowiedzi, ale także są oznaką ignorancji. Jeżeli nie jesteś pewny, pisz posty w edytorze tekstu ze sprawdzaniem pisowni.

Oczywiście istotna jest także sprawa tytułu tematu. Najlepiej kiedy streszcza on istotę problemu, np. "Problem z połączeniem z bazą danych". Nazwa "Mam problem, pomocy!" nie jest już dobra, gdyż nie mówi, co jest w środku. W efekcie temat może zostać niezauważony przez osobę znającą rozwiązanie. Zwróć też uwagę, czy forum nie wymaga dodania jakiegoś krótkiego prefiksu, np. "[php]" w celu łatwiejszego skatalogowania tematu.

Na koniec pamiętaj - nawet jeżeli zostaniesz przez moderatora publicznie upomniany, nie jest to jeszcze powód do rozpaczy. Zadaniem moderatora jest utrzymanie porządku i nie oznacza to, iż żywi on do ciebie przez to jakąś osobistą urazę. Po prostu przeproś i w przyszłości staraj się unikać podobnego błędu.


Instalacja

edytuj

Opis instalacji

edytuj

PHP jest dostępny na niemal każdym popularnym systemie operacyjnym, lecz do efektywnego korzystania z niego potrzebne jest dodatkowe oprogramowanie.

Serwer WWW

edytuj

PHP pracuje po stronie serwera, dlatego aby móc testować nasze skrypty w przeglądarce internetowej, musimy "postawić" taki na własnym komputerze. Nasz wybór padł na program o otwartych źródłach Apache HTTP Server rozwijany przez Apache Foundation (httpd.apache.org). Jest to najchętniej wykorzystywany serwer WWW na świecie, obsługujący prawie 60% wszystkich witryn, a przy tym bezpieczny i łatwy w konfiguracji. Dostępne jest wiele wersji na różne systemy operacyjne.

PHP komunikuje się z serwerem WWW na trzy sposoby:

  1. CGI - (Common Gateway Interface). PHP jest tu uruchamiane przez serwer jako samodzielna aplikacja na czas wykonywania skryptu. Rozwiązanie to jest bardzo mało wydajne przy większym natężeniu ruchu, ponieważ dla każdego żądania musi zostać uruchomiona nowa, niezależna kopia interpretera, jednak oferuje administratorowi zmiany wielu ustawień dotyczących bezpieczeństwa wykonywanych skryptów.
  2. Moduł serwera - do najpopularniejszych serwerów można podłączyć PHP jako moduł. Znacząco zwiększa wydajność, lecz utrudnia zachowanie odpowiedniego poziomu bezpieczeństwa na serwerze.
  3. FastCGI - rozwiązanie łączące w sobie zalety CGI oraz wydajność modułów.

W tym podręczniku opisane zostaną pierwsze dwa sposoby. Wsparcie dla FastCGI jest w PHP bardzo słabo udokumentowane oraz wymaga skorzystania z nieoficjalnych dodatków, przez co stanowi wyzwanie nawet dla doświadczonych administratorów.

Baza danych

edytuj

PHP sprawdza się najlepiej przy przetwarzaniu danych, lecz do ich przechowywania używana jest najczęściej niezależna aplikacja, np. relacyjna baza danych. Dane w bazie przechowywane są jako rekordy w tabelach o określonej strukturze, a słowo "relacyjny" oznacza, iż mogą istnieć wszelkiego rodzaju powiązania między tabelami. Do komunikacji z bazami wykorzystywany jest specjalny język SQL, którego podstawy także niebawem poznamy.

Wśród programistów PHP największą popularnością cieszy się baza danych darmowa do użytku domowego MySQL produkowana przez szwedzką firmę MySQL AB (www.mysql.com). Także i ona współpracuje z każdym popularnym systemem operacyjnym, a przy tym nietrudno znaleźć pomoc w przypadku ewentualnych kłopotów.

Biblioteki dodatkowe

edytuj

Jeżeli pracujesz na systemie Unix/Linux i chciałbyś zainstalować PHP ze źródeł, musisz upewnić się, że posiadasz zainstalowane wszystkie dodatkowe biblioteki. Zazwyczaj powinny być one już dostępne np. jako pakiety, ale na wszelki wypadek podamy adresy, pod którymi można je zdobyć:

Instalując PHP w systemach Windows, wszystkie biblioteki dostarczane są razem z pakietem instalacyjnym.

Proces instalacji

edytuj

Instalacja PHP zostanie przeprowadzona w kilku etapach:

  1. instalacja Apache,
  2. instalacja MySQL,
  3. instalacja PHP,
  4. integracja PHP z serwerem WWW,
  5. testowanie.

Skupiliśmy się na dwóch najpopularniejszych platformach:

  • Unix/Linux (instalacja ze źródeł)
  • Windows (przy pomocy instalatorów)

W przypadku instalacji na systemie Windows wszystkie aplikacje kopiowane będą do jednego katalogu: D:\Serwer\. Systemy uniksowe same wybiorą odpowiednie katalogi zgodnie ze swą własną strukturą.

Wszystkie liczące się dystrybucje systemu Linux posiadają wymienione tu aplikacje dostępne jako pakiety. W podręczniku nie będziemy opisywać, jak je z nich zainstalować, ponieważ opis rozrósłby się do niewyobrażalnych rozmiarów. Jeżeli jesteś zainteresowany tym sposobem instalacji, poszukaj odpowiedniego poradnika w dokumentacji twojej dystrybucji.

Alternatywny sposób instalacji

edytuj

Możemy również posłużyć się gotowym pakietem WAMP, który zawiera wszystkie wymienione wyżej składniki. Przykładowym zestawem tego typu jest program VertrigoServ, lub KrasnalServ, który zawiera polskie tłumaczenie i znacznie ułatwia instalacje i zarządzanie poszczególnymi komponentami serwera.

W zeszycie ćwiczeń podręcznika Informatyki dla gimnazjum znajduje się dział Serwer WWW wraz z bazą danych i PHP opisujący w skrócie instalację i uruchomienie pakietu XAMPP.


Instalacja Apache HTTP Server

edytuj

Aby wygodnie testować skrypty PHP i generowane przez nie witryny internetowe, należy postawić na własnym komputerze prywatny, testowy serwer WWW.

Najpopularniejszym wykorzystywanym w Internecie serwerem WWW jest program o otwartych źródłach Apache HTTP Server rozwijany przez Apache Foundation. Zarówno dokumentację, jak i pakiety instalacyjne można znaleźć pod adresem httpd.apache.org.

Moduł ten opisuje, jak zainstalować Apache na różnych systemach operacyjnych. Możemy również skorzystać z pakietu XAMPP, który zainstaluje Apache wraz z potrzebnymi nam serwerem MySQL oraz interpreterem PHP.

Instalacja w systemach Unix/Linux

edytuj

Kompilacja ze źródeł

edytuj

Wydajemy polecenia w konsoli: su [enter], hasło administratora

Przechodzimy do katalogu z kodem źródłowym

./configure
make
make install

Pakiety

edytuj

Serwer Apache oraz inne niezbędne programy można także zainstalować z pakietów dołączonych do Twojej dystrybucji Linuksa, jednak z powodu ich różnorodności nie będziemy w tym podręczniku omawiać szczegółowo instalacji na każdym z nich. Podajemy jedynie ogólne wskazówki, jak przystosować pakiet do uruchamiania przykładów z podręcznika. Bardzo prawdopodobne, że na stronie internetowej twojej dystrybucji znajduje się artykuł opisujący dokładnie instalację zestawu Apache + PHP, dlatego tam też odsyłamy po szczegóły.

Aby zainstalować serwer Apache z pakietu, upewnij się, że twoja dystrybucja go zawiera (pamiętaj, aby wybrać wersję 2.x), a następnie postępuj zgodnie z instrukcją instalacji dla twojego menedżera pakietów. Przeważnie pakiet automatycznie konfiguruje też serwer do pracy. Pliki konfiguracyjne odnajdziesz najczęściej w katalogu /etc/apache, natomiast strony WWW można otwierać z /var/www. Zmienimy sobie ten ostatni katalog tak, aby każdy użytkownik systemu miał na swoim koncie katalog www.

  1. Otwórz plik httpd.conf w katalogu /etc/apache lub, jeśli nie ma w nim za dużo treści, plik apache2.conf w tym samym katalogu
  2. Znajdź dyrektywę UserDir i ustaw ją na /home/*/www
  3. Poniżej znajduje się znacznik <Directory> - ustaw w nim identyczną ścieżkę (konfiguracja katalogu kont)

W ten sposób, jeżeli w swoim katalogu domowym utworzysz katalog WWW, a w nim katalogi dla użytkowników, będziesz mieć do nich dostęp poprzez adres http://localhost/~nazwa_uzytkownika/ (np. folder /home/test/www oraz dostęp przez localhost/~test)

Możesz teraz uruchomić serwer Apache poprzez skrypt startowy demona znajdujący się wraz z innymi (np. w katalogu /etc/rc.d/ albo /etc/init.d/ w zależności od dystrybucji). Pamiętaj, że jest to na razie tylko serwer, do którego wciąż musimy podpiąć PHP. Zajmiemy się tym w następnym rozdziale.

Instalacja w systemach Windows

edytuj

Pobieramy ze strony httpd.apache.org najnowszą wersję instalacyjną (MSI Installer) dla systemu Windows. Uruchamiamy instalator i po trzech kliknięciach "Next" pojawia się formularz, w którym wpisujemy następujące dane:

Network Domain localhost
Server Name localhost
Administator's Email Address admin@localhost

Zaznaczanie opcji for all users jest niekonieczne, ponieważ stawiamy serwer testowy na prywatnym komputerze. Jeżeli jest on podłączony do sieci lokalnej, jest to nawet niewskazane, ponieważ później zainstalujemy obsługiwany z przeglądarki menedżer baz danych, który ktoś mógłby wykorzystać do zniszczenia nam projektów.

Na następnym ekranie zaznaczamy opcję "Typical" i jako miejsce, do którego ma być zainstalowany serwer, wybieramy D:\Serwer. Instalator automatycznie utworzy tam katalog Apache2 i skopiuje wszystkie pliki.

Teraz serwer musimy przystosować do pracy i skonfigurować. Choć instalator utworzył nam już katalog D:\Serwer\Apache2\htdocs, z którego domyślnie odczytywane są strony, nie jest najlepszym rozwiązaniem umieszczać tam wszystkich naszych dokumentów. Pozostawimy go tylko w celu późniejszego zainstalowania w nim różnych menedżerów serwera (np. do zarządzania bazą danych) obsługiwanych z poziomu przeglądarki. Dla naszych własnych plików utworzymy katalog D:\Serwer\www.

Otwórz teraz plik konfiguracyjny serwera: D:\Serwer\Apache2\conf\httpd.conf lub też "D:\Serwer\Apache2\conf\default\httpd.conf". Odnajdź w nim następujące dyrektywy i przypisz im wartości:

Listen 127.0.0.1:80
ServerName localhost
DirectoryIndex index.html index.php index.php5
UserDir "D:/Serwer/www"
DocumentRoot "D:/Serwer/www"

Należy tu również pamiętać, że zmieniając DocumentRoot, trzeba dokonać zmian również około 20 linijek poniżej. Nakazuje to następujący komentarz : "# This should be changed to whatever you set DocumentRoot to".

UWAGA: W niektórych wersjach apache dyrektywa UserDir ma oddzielny plik. Znajduje się on w "D:\Serwer\conf\extra\httpd-userdir.conf" . Należy wtedy odkomentować linijkę # Include conf/extra/httpd-userdir.conf (tzn. usunąć znak #), jak również linijkę "# LoadModule userdir_module modules/mod_userdir.so"

Pod dyrektywą UserDir dodaj jeszcze poniższy blok:

 <Directory "D:/Serwer/www">
   AllowOverride All
   Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec ExecCGI
   Order allow,deny
   Allow from all
 </Directory>

Jego zadaniem jest powiadomienie serwera, jakie akcje są dozwolone wewnątrz tego katalogu.

Po instalacji i uruchomieniu aplikacji powinno pokazać się dosowe okno. Świadczy ono o tym, że serwer pracuje. Należy pamiętać o tym, aby nie zamykać tego okna ponieważ zakończy to działanie serwera.

Teraz testujemy, czy wszystko przebiegło poprawnie. Otwórz przeglądarkę i wpisz w niej http://localhost/. Powinna pojawić ci się domyślna strona startowa serwera. Aby przetestować katalog "www", utwórz w nim katalog np. test, zapisz w nim plik index.html, edytując go np. notatnikiem wpisz w jego treści "Hello World" , następnie uruchom w przeglądarce: http://localhost/~test/. Jeżeli wyświetlił się napis, wszystko jest OK. W systemach 2000/XP serwer Apache może pracować jako usługa systemu operacyjnego. Zamiast okienka dosowego, dostępna będzie w zasobniku systemowym ikonka programu "Apache Monitor", pozwalająca na włączanie, wyłączanie oraz restartowanie zainstalowanych usług Apache. Jeśli taka ikonka nie wyświetla się w zasobniku systemowym, należy uruchomić "D:\Serwer\bin\ApacheMonitor.exe". Aby mieć do niej dostęp na stałe, można dodać skrót do tego programu do Autostartu w Menu Start.

Należy pamiętać, że domyślna konfiguracja serwera Apache może różnić się znacząco od tych, które są ustawione w całych zestawach instalacyjnych (Krasnal, WebServ itd.) - np. często wykorzystywany mod_rewrite należy samemu włączyć po instalacji Apache'a, poprzez edycję pliku httpd.conf (znajdujemy linijkę #LoadModule rewrite_module modules/mod_rewrite.so i usuwamy # na początku.)

Instalacja w systemie Mac OS X

edytuj

Standardowo Mac OS X ma zainstalowany serwer Apache - możesz go włączyć w panelu preferencji Sharing jako Personal Web Sharing.

Jeśli chcesz nowszą wersję Apache, możesz zainstalować pakiet Apache2 za pomocą programów Fink oraz FinkCommander.

Apache można również skompilować samemu. Potrzebna będzie instalacja dodatku Developer Tools z płyty systemu Mac OS X, dalej należy postępować jak w przypadku systemów Unix/Linux.


Instalacja MySQL 5.0

edytuj

Aby testować strony na własnym komputerze, oprócz programu Apache będzie nam jeszcze potrzebna instalacja serwera MySQL (bazy danych).

Instalacja w systemach Unix/Linux

edytuj

Kompilacja ze źródeł

edytuj
$ ./configure --prefix=/usr/local/mysql
$ make
# make install

Pakiety

edytuj

Także serwer baz danych MySQL można zainstalować z pakietów. Przeważnie jest on już wtedy w pełni gotowy do pracy i wymaga jedynie uruchomienia. Po szczegóły instalacji odsyłamy do instrukcji twojego menedżera pakietów. Jeśli pakiet MySQL nie był zainstalowany domyślnie w Twoim komputerze, być może będziesz musiał sam skonfigurować i uruchomić serwer.

Konfiguracja i uruchomienie MySQL pod Linuksem

edytuj

Poniższy opis został sprawdzony w dystrybucji Slackware Linux. Pod innymi dystrybucjami może pójść tylko łatwiej. Żeby przekonać się, czy serwer MySQL działa, należy spróbować się na niego zalogować. Jako root:

$ mysql -u root -p

Jeśli MySQL zapyta o hasło to znaczy, że działa. Gdy zaś otrzymamy komunikat w stylu "cannot connect", musimy sprawdzić i poprawić/uzupełnić/dokończyć konfigurację:

Czy w katalogu /etc jest plik my.cnf? Jeśli go nie ma, należy skopiować w któryś z gotowych plików my-[small|medium|large|huge].cnf. Przykładowo dla prostej bazy danych na zwykłym domowym PC do ćwiczeń w zupełności wystarczy model small - nie zużyje wielkich połaci RAM-u.

# cp /etc/my-small.cnf /etc/my.cnf

Teraz należy utworzyć grupę mysql i użytkownika mysql (niewykluczone, że już istnieje), nadać mu hasło i prawa do katalogów i programów związanych z MySQL.

# useradd mysql
# passwd mysql
# groupadd mysql
# chown mysql:mysql /usr/bin/mysql*

Nieskonfigurowany MySQL daje nam możliwość łatwego wyboru miejsca, w którym będą przechowywane bazy. Można wybrać standardowe miejsce w /var/lib/mysql, ale byłoby dobrze nie przechowywać ważnych danych na partycji systemowej. Wielu użytkowników ma katalog /home domyślnie przez dystrybucję lub z własnego wyboru umieszczony na innej partycji lub nawet innym dysku. Warto wtedy utworzyć w /home katalog na bazy, który przeżyje każdą katastrofę łącznie z ponowną instalacją systemu. Obojętnie, które rozwiązanie wybierzesz, dalej będę się posługiwał nazwą /ścieżka/do/bazy.

# mkdir /ścieżka/do/bazy            (np. /home/mysql_bazy)
# chown -R mysql:mysql /sciezka/do/bazy

Następnie należy przygotować katalog roboczy MySQL-a. Należy to zrobić jako użytkownik mysql - inaczej będziemy mieli trudności z uruchomieniem i działaniem serwera, prawami dostępu etc.

# su - mysql
$ mysql_install_db  --datadir="/ścieżka/do/bazy"
$ exit  (wyjście do roota)

Jeśli instalowaliśmy bazę w innym niż domyślny katalogu, należałoby uwzględnić tę zmianę w skrypcie startowym serwera. W tym celu otwieramy w dowolnym edytorze plik /etc/rc.d/rc.mysqld, odnajdujemy zapis mysqld_safe --datadir=/var/lib coś_tam_dalej i zmieniamy go na mysqld_safe --datadir=/ścieżka/do/bazy coś_tam_dalej. Serwer musi wiedzieć, z jakim katalogiem będzie pracował. W innych dystrybucjach (np. Debian) skrypty startowe umieszczane są w katalogu /etc/init.d/. Przy okazji zajmiemy się automatycznym uruchamianiem serwera na przyszłość.

# chmod 755 /etc/rc.d/rc.mysqld

Teraz już możemy uruchomić serwer: Edytujemy plik /var/lib/mysql/mysqld.conf (w innych dystrybucjach np. /var/lib/mysql/mysqld.conf)i, jeśli baza będzie służyć tylko nam na naszym komputerze (a tak zakładamy w podręczniku), należy odkomentować (usunąć znak # z początku) linijkę, w której znajduje się zapis "skip-networking". W ten sposób odetniemy użytkownikom innych komputerów możliwość zdalnego logowania się do naszej bazy.

# su -mysql
$ /etc/rc.d/./rc.mysqld start &

Serwer powinien się uruchomić.

Jeśli chcemy zwiększyć bezpieczeństwo, możemy użyć skryptu mysql_secure_installation. Jest on dobrze opisany podczas wykonywania - prawie, że wizard. Pozostawi jedynie konto roota dla bazy, wyrzuci konta anonimowe i bazę test wraz z zawartymi w niej tabelami.

Instalacja w systemach Windows

edytuj

Poniższy sposób instalacji dotyczy systemów Windows 2000 oraz Windows XP. W następnym akapicie są opisane różnice dla przypadku Windows 98.

  1. Wejdź na stronę [1].
  2. Pobierz plik oznaczony jako "Windows (x86)" (lub x64, jeśli Twój komputer ma architekturę 64-bitową).
  3. Uruchom plik ściągnięty plik MSI lub, jeśli ściągnąłeś wersję ZIP, rozpakuj archiwum i uruchom plik Setup.exe.
  4. Wybierz rodzaj instalacji (Custom).
  5. Wybierz katalog, do którego chcesz zainstalować serwer.
  6. Zaakceptuj proponowane przez instalator komponenty pakietu.
  7. Poczekaj, aż wszystkie pliki zostaną skopiowane.
  8. Instalator zaproponuje Ci rejestrację w witrynie mysql.com. Możesz pominąć ten krok wybierając Skip Sign-up, chyba że posiadasz już konto lub chcesz takie utworzyć.
  9. Zaznacz opcję "Configure the MySQL Server now" i kliknij "Finish". Uruchomione zostanie narzędzie konfiguracji serwera.
  10. Wybierz "Reconfigure Instance" i kliknij "Next".
  11. Wybierz "Standard configuration" i kliknij "Next".
  12. W kolejnym kliknij na "Next" bez zaznaczania czegokolwiek.
  13. Zaznacz opcję "Modify Security Settings" i wpisz we wszystkie pola hasło "root".
  14. Kliknij "Execute", aby zapisać zmiany.

MySQL został zainstalowany i jest dostępny jako usługa systemu Windows. Możesz go zatrzymywać lub restartować poprzez Panel Sterowania > Narzędzia administracyjne > Usługi.

Poniższy sposób instalacji dotyczy systemu Windows 98.

  1. Wykonaj kolejno kroki od 1. do 9. z opisu instalacji w Windows 2000/XP (powyżej).
  2. Kliknij "Next", wybierz "Standard configuration" i ponownie kliknij "Next".
  3. W kolejnym kliknij na "Next" bez zaznaczania czegokolwiek.
  4. Kliknij "Execute", aby zapisać plik konfiguracyjny.

MySQL został zainstalowany. Jego jedynym użytkownikiem jest "root" z pustym hasłem. Aby uruchomić bazę, należy uruchomić plik "mysqld.exe". Możesz zatrzymywać bazę poprzez komendę "mysqladmin.exe -uroot -p shutdown".

W razie problemów z "Call to undefined function mysqli_connect()" skopiuj libmysql.dll z folderu "php5" do "Apache2\bin"

Aby dokonać w przyszłości aktualizacji bazy danych MySQL do nowszej wersji, zatrzymaj serwer MySQL, zrób na wszelki wypadek kopię zapasową katalogu data, a następnie powtórz wszystkie powyższe kroki, instalując nową wersję na starą.

Instalacja w systemie Mac OS X

edytuj

Wystarczy pobrać gotowy pakiet (.dmg) ze strony domowej MySQL. Zamieszczony jest na nim standardowy instalator. Dodatkowo warto kliknąć na plik .prefPane, który doda do systemowych preferencji możliwość startowania i zatrzymywania serwera.

Po instalacji Developer Tools z płyty systemu Mac OS X można skompilować bazę ze źródeł, tak samo jak w innych systemach uniksowych.


Instalacja PHP 5.2

edytuj

Na samym końcu instalujemy główny program, czyli interpreter PHP oraz podłączamy go do już zainstalowanego serwera Apache.

Instalacja w systemach Unix/Linux

edytuj

Pakiety

edytuj

Niemal wszystkie dystrybucje systemu Linux mają w swoich zasobach pakiety PHP. Jeżeli nie czujesz się na siłach instalować projektu ze źródeł, możesz skorzystać z tego rozwiązania i postępować odpowiednio według instrukcji systemu pakietów używanego w twojej dystrybucji. Pakiety mają jednak kilka wad, a najważniejszą z nich jest konieczność polegania na intuicji autora pakietów. Interpreter nie zawsze jest skonfigurowany tak, jak byśmy chcieli, a ponadto należy ręcznie uaktywnić sobie dodatkowe moduły. Choć nowe wersje PHP ukazują się przeważnie w kilkumiesięcznych odstępach, niekiedy dystrybucje wyposażone są w naprawdę stare wersje interpretera. Przypominamy, że podręcznik ten pisany jest pod wersje PHP 5.1.x/5.2.x, więc jeśli twój pakiet zainstaluje Ci starszą, niektóre przykłady mogą nie działać prawidłowo.

Po zainstalowaniu PHP z pakietu odnajdź w swoim systemie plik php.ini i upewnij się, że następujące dyrektywy są ustawione na:

  • error_reporting = E_ALL | E_STRICT - poziom raportowania błędów
  • display_errors = On - wyświetlanie błędów
  • doc_root = "/home/*/www" - katalogi kont użytkowników
  • register_globals = Off
  • magic_quotes_gpc = Off
  • magic_quotes_runtime = Off

Trzy ostatnie dyrektywy służą do zachowania kompatybilności ze skryptami pisanymi pod PHP 3 i pierwsze wersje PHP 4, aktualnie zdecydowanie odradza się korzystanie z nich nie tylko ze względów bezpieczeństwa, ale też z powodu planów ich wycofania w PHP 6. Dlatego pozostawiamy je wyłączone i tak też będziemy pisać nasze skrypty.

Upewnij się ponadto, że PHP ładuje moduły php_pdo.so, php_pdo_mysql.so, php_mysql.so, php_zlib.so oraz php_gd2.so.

Kompilacja ze źródeł

edytuj

Jeśli zdecydowałeś się na kompilację ze źródeł, zacznij od ich pobrania - źródła projektu dostępne są na stronie www.php.net. Rozpakowujemy je w katalogu /usr/src:

tar -xvf php-5.2.0.tar.gz

Jeżeli ściągnąłeś inną wersję projektu albo źródła są skompresowane innym algorytmem, odpowiednio zmodyfikuj nazwę swojego pliku. Po rozpakowaniu powinien pojawić się katalog php-5.2.0. Przechodzimy do niego:

cd php-5.2.0

Teraz rozpoczynamy konfigurowanie naszej kompilacji poprzez podanie odpowiednich dyrektyw do skryptu configure.

./configure --with-apxs2=/usr/local/apache2/bin/apxs --with-config-file-path=/etc/apache
--with-zlib --with-mysql=/usr/local/mysql
--with-mysqli=/usr/local/mysql/bin/mysql_config 
--with-pdo-mysql=/usr/local/mysql/bin/mysql_config --with-gd --enable-gd-native-ttf
--with-libxml-dir=/usr/local/libxml

Oto opis poszczególnych opcji:

  • --with-apxs2=/usr/local/apache2/bin/apxs - informujemy, że chcemy skompilować PHP jako moduł serwera Apache i podajemy ścieżkę do programu apxs dostarczanego wraz z nim.
  • --with-config-file-path=/etc/apache - informujemy, że plik konfiguracyjny PHP znajdować się będzie w katalogu /etc/apache.
  • --with-zlib - dodajemy bibliotekę Zlib potrzebną niektórym modułom.
  • --with-mysql=/usr/local/mysql - aktywujemy najstarsze rozszerzenie do obsługi baz danych MySQL, podając ścieżkę do katalogu, w którym znajduje się folder z nagłówkami C serwera.
  • --with-mysqli=/usr/local/mysql/bin/mysql_config - aktywujemy nowe rozszerzenie do obsługi baz danych MySQL, podając ścieżkę do programu mysql_config generującego odpowiednie pliki.
  • --with-pdo-mysql=/usr/local/mysql/bin/mysql_config - aktywujemy sterownik dla bazy MySQL dla biblioteki PHP Data Objects omawianej w tym podręczniku. Ścieżka także prowadzi do programu mysql_config.
  • --with-gd - aktywujemy bibliotekę GD do generowania obrazków.
  • --enable-gd-native-ttf - aktywujemy wbudowaną obsługę czcionek TTF w bibliotece GD.
  • --with-libxml-dir=/usr/local/libxml - podajemy ścieżkę do katalogu biblioteki libxml, dzięki czemu aktywne będą moduły do obsługi XML-a w PHP.

Kiedy skrypt konfiguracyjny zakończy działanie, wydajemy dwa kolejne polecenia kompilacji oraz instalacji PHP:

make
make install

Kopiujemy plik konfiguracyjny PHP do odpowiedniego katalogu:

cp php.ini-recommended /etc/apache/php.ini

Otwieramy ten plik i modyfikujemy w nim następujące dyrektywy:

  • error_reporting - ustawić na E_ALL | E_STRICT
  • doc_root - ustawić ścieżkę do katalogu kont, w których zamierzamy docelowo używać PHP, czyli /home/*/www (ta sama, co w Apache)

Otwieramy plik konfiguracji serwera Apache: /etc/apache/httpd.conf. Sprawdzamy, czy program "apxs" dodał linijkę

LoadModule php5_module modules/libphp5.so

Jeżeli nie, dopisujemy ją. Ponadto musimy dodać

AddType application/x-httpd-php .php .php5

Aby serwer wiedział, jakie pliki należy przepuszczać przez interpreter PHP. Zapisujemy zmiany i na koniec restartujemy/uruchamiamy serwer Apache:

/usr/local/apache2/bin/apachectl start

Pora na przetestowanie działania PHP. Do katalogu /usr/local/apache2/htdocs kopiujemy plik phpinfo.php o treści:

<?php phpinfo(); ?>

W przeglądarce wpisujemy http://localhost/phpinfo.php - powinien ukazać się bardzo długi raport nt zainstalowanej wersji PHP (wersja, konfiguracja, moduły itd.). Jeżeli zamiast tego pokazany zostanie wpisany wyżej kod, oznacza to, że instalacja nie przebiegła poprawnie i na którymś etapie został popełniony błąd.

Instalacja w systemach Windows

edytuj

Instalacja PHP jako jedyna nie polega na klikaniu dalej. Taka możliwość oczywiście istnieje, lecz wtedy nie można skonfigurować interpretera do pracy jako moduł serwera.

By zacząć działać, musimy ściagnąć najnowszą wersję PHP ze strony www.php.net w wersji dla systemu Windows. W chwili powstawania tego tekstu najnowszą wersją było PHP 5.1.1. Ściągnięty plik (tzw. binarki, przykładowo wyglądające: 'php-5.1.1-Win32.zip' ) rozpakowujemy do przykładowego katalogu D:/Serwer/php5/. Następnie kopiujemy plik php.ini-recommended do php.ini i zabieramy się za edytowanie php.ini:

  1. Edycja poziomu błędów: znajdź linię error_reporting = E_ALL i zmień ją na error_reporting = E_ALL | E_STRICT. Ponadto odszukaj display_errors i ustaw wartość na On.
  2. W dyrektywie doc_root wprowadź ścieżkę do katalogu "D:/Serwer/www" utworzonego przy okazji instalowania serwera Apache. Tu będziemy trzymać nasze projekty.
  3. W dyrektywie extension_dir wprowadzamy ścieżkę do katalogu D:/Serwer/php5/ext, aby PHP mógł zlokalizować dodatkowe moduły.
  4. Przechodzimy do sekcji ; Dynamic Extensions ; gdzie ustawimy, jakie dodatkowe moduły mają być ładowane przy starcie PHP. Wskazane jest usunąć średnik (co odblokowuje moduł) sprzed następujących linii:
 extension=php_gd2.dll
 extension=php_mysql.dll
 extension=php_mysqli.dll
 extension=php_pdo_mysql.dll

Pierwszy moduł to biblioteka obsługi obrazków. Dwa następne zapewniają możliwość komunikowania się z bazą MySQL starszym skryptom PHP. Ostatni moduł to nowa biblioteka PHP Data Objects służąca komunikacji z bazami danych, skonfigurowana do działania z bazą danych MySQL. Nie należy dopisywać linii z plikami bibliotek w php.ini, lecz jedynie odblokowywać istniejące! W pliku .ini są uwzględnione wszystkie pliki bibliotek istniejące fizycznie w zastosowanym przez nas pakiecie instalacyjnym PHP. Dopisanie nieistniejącego pliku spowoduje błąd.

Ostatnim krokiem jest podłączenie PHP do serwera Apache. Jeżeli zamierzamy zrobić to jako CGI, na koniec pliku konfiguracyjnego Apache dopisujemy:

 ScriptAlias /php5/ "D:/Serwer/php5/"
 AddType application/x-httpd-php .php
 Action application/x-httpd-php "/php5/php-cgi.exe"

W przypadku modułu linijek jest nieco mniej:

 LoadModule php5_module "D:/Serwer/php5/php5apache2_2.dll"
 AddType application/x-httpd-php .php

Należy dodać jeszcze jedną linijkę (pod poprzednio dodanym kodem):

 PHPIniDir "D:/Serwer/php5"

albo, jeśli serwer Apache zgłosi błąd podczas startu, wypróbować składnię:

 PHPIniDir="D:/Serwer/php5"

aby wskazać lokalizację pliku php.ini, ponieważ domyślna jego lokalizacja w serwerze Apache to C:\Windows. Jeśli więc tego nie zrobimy, nasze moduły nie zostaną załadowane. Teraz restartujemy serwer i zabieramy się za sprawdzenie, czy wszystko przebiegło poprawnie. Umieść w katalogu (wskazanym w pliku konfiguracyjnym serwera - opis znajdziesz w rozdziale Instalacja Apache) D:/Serwer/Apache2/htdocs/ plik phpinfo.php z poniższą linijką:

 <?php phpinfo(); ?>

W przeglądarce wpisz http://localhost/phpinfo.php - powinien pokazać Ci się bardzo długi raport na temat zainstalowanej wersji PHP (wersja, konfiguracja, moduły itd.). Jeżeli zamiast tego ujrzysz wpisany wyżej kod, oznacza to, że coś zostało zrobione źle na którymś z etapów podpinania PHP do serwera.

Instalacja w systemie Mac OS X

edytuj

Można zainstalować gotowy pakiet PHP5 za pomocą Fink i FinkCommander.

Kompilacja ze źródeł będzie wymagała instalacji dodatkowych bibliotek (dla ułatwienia można je zainstalować FinkCommanderem) oraz Developer Tools z płyty systemowej Mac OS X. Dalej kompilacja przebiega tak samo jak dla Unix/Linux.


Podstawy języka

edytuj

Pierwszy skrypt

edytuj

W tym rozdziale napiszemy pierwszy skrypt PHP.

Skrypt PHP

edytuj

Język PHP umożliwia zagnieżdżanie skryptów wykonywanych po stronie serwera.

Przeglądarka otrzymuje tylko już przetworzony kod, w tym przypadku <? echo 2*2; ?> zostało zamienione na 4. O szczegółach tej instrukcji - w dalszej części podręcznika.

Interpreter PHP rozpoznaje kod do przetworzenia po znakach <?php i ?>. Każdy kod między nimi jest programem PHP.

Na początku będziesz musiał poznać instrukcję echo, która wysyła tekst do przeglądarki:

 <?php
 echo 42;
 ?>

Funkcja ta została omówiona tutaj, ponieważ jej znajomość przydaje się do nauki zmiennych i wyrażeń; bardziej szczegółowo zostanie to omówione w kolejnych rozdziałach.

Konsola

edytuj

Możemy uruchamiać skrypty z linii poleceń (CLI)

Polecenia :

 php 1.php

uruchamia skrypt 1.php

Polecenie :

php -a

uruchamia tryb interaktywny:

Interactive mode enabled

wprowadzamy proste polecenie :


<?php echo "hi!"; ?>

i kończymy CTRL-D.

Wynik :

 Parse error:  syntax error, unexpected '<' in php shell code on line 1

Próbujemy :

 echo "hi!";

Wynik :

hi!


Sprawdzanie wersji php:

php -v

przykładowy wynik :

PHP 5.5.9-1ubuntu4.14 (cli) (built: Oct 28 2015 01:34:46) 
Copyright (c) 1997-2014 The PHP Group
Zend Engine v2.5.0, Copyright (c) 1998-2014 Zend Technologies
with Zend OPcache v7.0.3, Copyright (c) 1999-2014, by Zend Technologies

Skrypt PHP wewnątrz dokumentu HTML

edytuj

Jak wspomnieliśmy wcześniej, skrypty PHP możemy mieszać ze zwykłym kodem HTML. Kod naszych algorytmów zamykany jest wewnątrz specjalnych wstawek wyłapywanych przez interpreter oraz zmienianych później na wygenerowany kod. Tak też zrobimy w naszym pierwszym skrypcie, który tradycyjnie wyświetli na ekranie przeglądarki napis "Hello world!".

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
 <head>
  <title>Pierwszy skrypt PHP</title>
 </head>
 <body>
 <?php
   echo 'Hello world!';
 ?>
 </body>
</html>

W powyższym przykładzie widzimy skrypt PHP osadzony za pomocą znaczników <?php oraz ?> w zwyczajnym kodzie HTML, który w następnych przykładach będziemy już pomijać, aby nie marnować miejsca. Wewnątrz mamy jedną linijkę:

echo 'Hello world!';

Nakazuje ona wyświetlenie tekstu "Hello world!" w przeglądarce. Tekst do wyświetlenia ograniczyliśmy apostrofami. Średnik na końcu informuje o zakończeniu komendy. Możemy rozbić to na kilka linijek, ale dla PHP nie będzie to miało większego znaczenia - końcem komendy jest właśnie średnik.

<?php
echo 
     'Hello world!';
?>

Przejście do nowej linii poza apostrofami jest jednym z tzw. białych znaków ignorowanych przez interpreter. Innymi są spacja oraz tabulacja. Między tekstem, a komendą echo możemy wstawić niezliczoną liczbę tabulatorów i zejść do nowej linii, ale nie zmieni to w żaden sposób tego, jak PHP wykona nasz skrypt, gdyż znaki te zostaną zignorowane.

W skrypcie możemy umieścić więcej wyrażeń, oczywiście odseparowanych średnikami:

<?php
echo 'To jest tekst 1';
echo 'To jest tekst 2';
echo 'A to jest tekst 3';
?>

Zauważ, że choć w skrypcie mamy trzy komendy wyświetlenia trzech tekstów, przeglądarka wyświetli je nam w jednej linijce. Jest tak dlatego, że nowa linia oznaczana jest specjalnym znacznikiem HTML, którego tam nie umieściliśmy. Oto poprawiona wersja skryptu:

<?php
echo 'To jest tekst 1<br>';
echo 'To jest tekst 2<br>';
echo 'A to jest tekst 3<br>';
?>

Komentarze

edytuj

Kiedy twoje skrypty staną się bardziej rozbudowane, w kodzie przyda się pewna organizacja. Pomogą tu z pewnością komentarze służące do opisywania, co robi dana część algorytmu, jak działa, jakie ma wymagania itd. Mogą także pomóc w usystematyzowaniu całości lub też zamieszczeniu informacji o autorze oraz prawach autorskich. Komentarz jest całkowicie ignorowany przez interpreter PHP i nie wpływa na wynik jego działania.

Istnieją trzy rodzaje komentarzy:

<?php
/*
  komentarz wieloliniowy
  może być rozbijany na wiele linijek.
  Cały ten tekst jest ignorowany przez interpreter
*/
 
// to jest komentarz jednoliniowy - obowiązuje do końca danej linijki
# to jest jeszcze jeden komentarz jednoliniowy

?>

Oto przykładowe zastosowanie komentarzy:

<?php
/*
 * Generator miniaturek obrazków
 *  Wersja: 1.0
 *  Autor: Adam Kowalski (adres@example.com)
 *  Licencja: GNU GPL
 *
 * Użycie: tu opis użycia...
 */

// początek generowania obrazka
echo 'Tu są jakieś komendy';
echo 'Dużo komend...';

// zmniejszanie
echo 'Tu obrazek się zmniejsza';

// wysyłanie wyniku
echo 'Tu wysyłamy obrazek do przeglądarki';
  
?>

Cały kod został opisany i dzięki temu nawet po wielu miesiącach autor będzie wiedział, co do czego służy. Komentarze przydają się przy zbiorowych pracach nad projektem. Można w nich umieszczać informacje, co należy w danym fragmencie poprawić albo jak on funkcjonuje. W procesie usuwania błędów (debugowania) komentarzy można używać do chwilowego wyłączania kawałków kodu, aby zobaczyć, czy to przypadkiem one nie powodują problemu oraz jak aplikacja radzi sobie bez nich. Stosowanie komentarzy należy do dobrych praktyk programistycznych i nigdy nie należy o nich zapominać.

Znaczniki wstawek PHP

edytuj

Oficjalnymi znacznikami rozpoczynającymi i kończącymi kod PHP są <?php ... ?>, jednak wciąż można spotkać starszą formę <? ... ?>. Nie zalecamy jej stosowania, ponieważ wiele serwerów ma ją wyłączoną i Twoje skrypty bez przeróbek nie będą na nich działać.

Wartą odnotowania rzeczą jest możliwość pominięcia końcowego ?> jeśli chcemy, aby kod PHP ciągnął się do końca pliku. Taka też konwencja zostanie przyjęta w dalszych rozdziałach. Jest ona stosowana przez wiele skryptów, gdyż pozwala uniknąć przypadkowego pozostawienia np. spacji czy pustej linii na końcu pliku, co niekiedy może być źródłem poważnych problemów, o czym przekonamy się z dalszych rozdziałów. Nasz skrypt będzie zatem prezentować się następująco:

<?php
echo 'To jest tekst 1<br>';
echo 'To jest tekst 2<br>';
echo 'A to jest tekst 3<br>';

Uruchom go i przekonaj się, że to rzeczywiście działa!


Zmienne i tablice

edytuj

Samo wyświetlanie tekstu jest niezbyt ciekawe. Wszystkie programy komputerowe operują na danych, zatem w językach programowania muszą istnieć mechanizmy do przechowywania informacji w pamięci. Do tego służą zmienne i tablice, którymi zajmiemy się właśnie teraz.

PHP, jak każdy inny język programowania, operuje na danych. Niektóre z nich są zapisane na sztywno w skrypcie. Każda rzecz, która reprezentuje jakąkolwiek informację, zwana jest wyrażeniem. Oto prosty przykład:

 8

To jest wyrażenie reprezentujące liczbę całkowitą.

 6.454

To jest wyrażenie reprezentujące liczbę zmiennoprzecinkową będącą komputerowym, skończonym przybliżeniem (nie wartością dokładną) liczby rzeczywistej.

 0x6F44

To jest wyrażenie reprezentujące liczbę zapisaną w systemie szesnastkowym.

 07647

To jest wyrażenie reprezentujące liczbę zapisaną w systemie ósemkowym.

 'To jest tekst bez znaków specjalnych'
 "To też jest tekst, ale \t-\t ze znakami specjalnymi (tabulatorami)"

Powyżej mamy dwa wyrażenia reprezentujące tekst. Pomiędzy nimi istnieje istotna różnica. Apostrofy korzystają z jednego tylko znaku specjalnego: \' - pozwala on oznaczyć wewnątrz tekstu znak apostrofu. Gdybyśmy zapomnieli o poprzedzającym backslashu, PHP uznałby, że w tym momencie kończy się wyrażenie tekstowe i dalej jest już normalny skrypt.

Cudzysłów posiada więcej takich kodów formatujących:

 "\n - tak robimy zejście do nowej linijki w systemach uniksowych (np. Linux, Mac OS X)"
 "\r - tak kiedyś się robiło w Mac OS Classic"
 "\r\n - a tak wciąż się robi w systemach Windows"
 "Wyświetlimy cudzysłów: \" ..."
 "Wstawimy tabulator: \t"
 "Wstawiamy zmienną $zmienna"

W obu przypadkach, aby wyświetlić backslash, należy napisać go podwójnie:

 'Oto backslash: \\ '

Otrzymamy dzięki temu tekst:

 Oto backslash: \

Oto złożone wyrażenie (reprezentuje ono sumę dwóch mniejszych wyrażeń):

 5 + 7

Oto wyrażenie będące połączeniem dwóch mniejszych wyrażeń tekstowych.

 'Tekst A '.'Tekst B'

Znak kropki oraz plusa to tzw. operatory. Wykonują one pewne operacje na dwóch innych wyrażeniach i zwracają jakąś wartość, zatem same także stają się wyrażeniem. Podobnie jak w matematyce, obowiązuje ściśle określona kolejność ich wykonywania, a zmieniać ją możemy za pomocą nawiasów:

 5 * (6 + 8)

Pamiętaj, że PHP potrafi automatycznie rozpoznawać typ wyrażenia i w razie potrzeby odpowiednio go konwertować. Dla przykładu instrukcja echo wymaga danych tekstowych. W skrypcie:

<?php
echo 7;

Interpreter dokona konwersji liczby na tekst, a dopiero potem spowoduje jego wyświetlenie.

Funkcje

edytuj

Informatyka wiele zawdzięcza matematyce. W programowaniu występuje wiele pojęć zaczerpniętych bezpośrednio od królowej nauk. Jednym z nich jest funkcja, do której możemy wprowadzać parametry, a w zamian otrzymujemy jakiś wynik. Poniżej będzie pierwszy "naprawdę" dynamiczny skrypt, jaki stworzymy. Skorzystamy w nim z dwóch funkcji, aby wyświetlić aktualny czas:

<?php

echo 'Dzisiaj mamy: '.date('d.m.Y').'<br/>';
echo 'Od 1.1.1970 minęło: '.time().' sekund';

Jak widać, składnia funkcji jest następująca: nazwaFunkcji(parametry). Jeśli funkcja nie posiada parametrów, nawiasy są puste. Jeżeli jest ich więcej, niż jeden, oddzielamy je od siebie przecinkiem. Zaś sam parametr jest niczym innym, jak... pewnym wyrażeniem. Wynik działania funkcji również jest wyrażeniem, dlatego możemy ją wpleść w nasz tekst za pomocą operatora kropki.

Powyższy kod zachowa się następująco:

  1. Wykonana zostanie funkcja date(), przyjmując za argument tekst 'd.m.Y', a jej wynikiem będzie aktualna data.
  2. Następnie powyżej opisany wynik zostanie połączony (operator .) z sąsiednimi tekstami, przez co powstanie wyrażenie, np. "Dzisiaj mamy: 01.01.2010". Echo spowoduje wstawienie tego wyrażenia do kodu HTML strony wynikowej.
  3. Następna linijka wywoła się analogicznie, wywołanie funkcji zakończy się zwróceniem wyniku, który zostanie połączony z sąsiednimi wyrażeniami w jeden tekst.

Zmienne

edytuj

Innym pojęciem matematycznym jest zmienna, zawierająca pewną informację, przeważnie uzyskaną w trakcie wykonywania skryptu. Można traktować ją jako pojemnik, do którego będziemy mogli w trakcie wykonywania skryptu wstawić dowolną informację, zapamiętując ją w ten sposób. Umożliwia to przechowywanie i przetwarzanie danych w potrzebnym nam celu.

Każdej zmiennej przypisujemy własną, unikalną nazwę, która jednoznacznie ją identyfikuje. Język PHP wymaga, aby zaczynała się ona od znaku dolara, a następnie od litery (ew. podkreślenia). Dalsza część nazwy może już zawierać cyfry. Stosując wielkie litery trzeba uważać, ponieważ dla interpretera są one rozróżnialne od małych, co ma istotne znaczenie. $zmienna i $Zmienna to dwie różne zmienne. Przykłady poprawnych nazw zmiennych:

$a, $b, $foo, $_50, $_Foo, $moja_zmienna, $mojaZmienna3

Przykłady nieprawidłowych nazw:

$5a, $'a', $

Aby przypisać wartość do zmiennej, należy skorzystać z operatora =. Po lewej stronie umieszczamy naszą zmienną, a po prawej dowolne wyrażenie określające wartość, która zostanie zapisana w zmiennej. Oto, jak wygląda to w praktyce:

<?php
// inicjujemy zmienna $czas aktualnym czasem w sekundach od 1.1.1970
$czas = time();
$czas2 = $czas / 60;

echo 'Od 1.1.1970 minęło '.$czas.' sekund<br/>';
echo 'Od 1.1.1970 minęło '.$czas2.' minut<br/>';
echo 'Od 1.1.1970 minęło '.($czas / 3600).' godzin';

W powyższym przykładzie stworzyliśmy zmienne $czas i $czas2, zapisując w pierwszej z nich liczbę sekund zwróconą przez wywołanie funkcji time(). Następnie wykorzystaliśmy ją w obliczeniach w kolejnej linijce. Poznaliśmy w ten sposób jedno z zastosowań zmiennych. Zachowaliśmy w nich wynik działania jednej sekcji programu, aby potem używać go wielokrotnie gdzie indziej, bez konieczności każdorazowego odwoływania się do funkcji i zbędnego liczenia tego samego.

PHP, w przeciwieństwie do innych języków programowania, ma bardzo liberalne reguły stosowania zmiennych. Nie trzeba ich nigdzie uprzednio deklarować, a interpreter sam nam dopasuje rodzaj informacji do naszych potrzeb (ustali tzw. typ zmiennej). Dana zmienna jest tworzona podczas pierwszego jej wykorzystania w skrypcie. Jest sporo sytuacji, w których zachowanie to jest pożądane, lecz może też utrudnić pracę. Aby mieć świadomość zagrożenia, wyobraź sobie taką sytuację: programista pisząc szybko, może popełnić literówkę. Jeżeli zostanie ona popełniona podczas wpisywania nazwy, PHP utworzy zmienną zawierającą tę literówkę. W teorii nie jest to żadnym błędem, jednak możemy się domyślać, że zapis obliczeń do innej zmiennej spowoduje błędne działanie programu, a programista będzie musiał spędzić dużo czasu na odnalezienie przyczyny. Słowo "teoria" nie znalazło się tu przypadkowo. Podczas instalowania PHP wspominaliśmy o poziomach raportowania błędów. Im wyższy poziom, tym większej ilości rzeczy czepia się PHP. Na poziomie E_ALL zdefiniowanym w rekomendowanym pliku php.ini takie beztroskie podejście do zmiennych nie jest tolerowane. Tutaj PHP wymaga już, aby podczas pierwszego użycia zmiennej została jej przypisana jakaś wartość, ponieważ inaczej otrzymamy powiadomienie (ang. notice) o próbie odwołania się do nieistniejącej zmiennej. Popatrzmy sobie na ten przykład:

<?php

echo $a * 16 + 5;

Zmienna $a nie została w tym kodzie nigdzie początkowo zadeklarowana. Otwórz swój plik php.ini, odnajdź dyrektywę error_reporting i zmień jej wartość na E_ALL | ~E_NOTICE. Wyłączysz w ten sposób wyświetlanie powiadomień. Zrestartuj serwer i uruchom powyższy skrypt. Wynikiem powinno być "5". PHP bez pytania podstawił do $a wartość neutralną 0. Przywróć teraz poprzedni poziom (E_ALL | E_STRICT) i ponownie uruchom ten skrypt. Oprócz wyniku, ujrzysz też komunikat:

Notice: Undefined variable: a in D:\Serwer\www\katalog\twojskrypt.php on line 3

Sytuację można rozwiązać, ręcznie inicjując zmienną $a wartością 0:

<?php
$a = 0;
echo $a * 16 + 5;

Zalecane jest, aby wszystkie skrypty pracowały bezproblemowo przy włączonych powiadomieniach. Jeżeli zajdzie sytuacja, że odwołanie się do zmiennej, która może jeszcze nie istnieć, jest potrzebne, istnieje kilka sposobów "oszukania" PHP, lecz poznamy je w dalszej części podręcznika.

Początkujący programiści mają tendencję do tworzenia dużej liczby tzw. zmiennych tymczasowych, które nie wnoszą absolutnie niczego do programu poza wydłużeniem kodu i zmniejszeniem wydajności. Po każdym etapie przetworzenia jakiejś informacji, umieszczana jest ona w nowej zmiennej. Takie podejście jest nieprawidłowe. Oto przykłady "złych" skryptów:

<?php

$tekst = 'To jest jakiś tekst';
$tekstMaly = strtolower($tekst);
$tekstBezpieczny = addslashes($tekstMaly);
echo $tekstBezpieczny;

Przykład 2:

<?php

$format = 'd.m.Y';
echo date($format);

W pierwszym skrypcie niepotrzebnie po każdym etapie przetwarzania tekstu tworzymy dla niego nową zmienną. Możemy to poprawić na dwa sposoby. Pierwszy:

<?php

$tekst = 'To jest jakiś tekst';
$tekst = strtolower($tekst);
$tekst = addslashes($tekst);
echo $tekst;

Drugi sposób rozwiązania tego problemu - bez użycia zmiennych (już trochę mniej czytelny):

<?php
echo addslashes(strtolower('To jest jakiś tekst'));

W drugim "złym" skrypcie w ogóle niepotrzebnie tworzymy zmienną - format daty możemy wpisać bezpośrednio do funkcji.

<?php
echo date('d.m.Y');

Jednak nie zawsze jest to lepsza wersja. Jeżeli nasz skrypt bardzo często będzie formatować różne daty, a my będziemy chcieli mieć możliwość zmieniania tych formatów w przyszłości, użycie zmiennych bardzo ułatwiłoby sprawę - zmieniamy format daty w jednej zmiennej, zamiast w kilku czy kilkudziesięciu wywołaniach date().

Nauczenie się, kiedy warto użyć zmiennych, a kiedy nie, to kwestia praktyki. W niniejszym podręczniku będziemy zwracali na tę kwestię baczną uwagę. Jeśli zajdzie potrzeba użycia zmiennych tymczasowych - wyjaśnimy, dlaczego, bowiem całkowite rezygnowanie z ich użycia także może rodzić wiele problemów.

Do tej pory miałeś okazję zauważyć, że istnieje w PHP pewne rozróżnienie na tekst i liczby. Skoncentrujemy się teraz na poznaniu większej ilości typów oraz pokazaniu, jak PHP dokonuje konwersji między nimi.

Istnieją trzy kategorie typów: wielkości skalarne, typy złożone oraz typy specjalne. Dokumentacja wymienia jeszcze jedną, lecz stworzoną na jej własne potrzeby do zaznaczania niektórych rzeczy (powiemy o niej później).

Wielkości skalarne

edytuj

Pierwszym typem skalarnym jest liczba całkowita. Jej angielskim określeniem jest integer, używany bywa skrót int. Może być ona zapisana w trzech systemach liczbowych: dziesiętnym, szesnastkowym albo ósemkowym:

<?php
$a = 1234; // liczba całkowita
$a = -123; // liczba całkowita ujemna
$a = 0123; // zapis ósemkowy (odpowiednik dziesiętnego 83)
$a = 0x1A; // zapis szesnastkowy (odpowiednik dziesiętnego 26)

Możemy także korzystać z wartości ułamkowych zwanych liczbami zmiennoprzecinkowymi (ang. floating point numbers albo skrótowo float), które są przybliżeniem liczb rzeczywistych (ważne - nigdy nie zawierają dokładnej wartości, prawie zawsze jest to odrobinę różniąca się liczba, dlatego mówi się o "przybliżeniu"). Przy ich zapisywaniu obowiązują reguły języka angielskiego, więc części całkowite od ułamkowych oddzielamy za pomocą kropki. Także i tu mamy do wyboru kilka sposobów zapisu:

<?php
$a = 1.234; 
$a = 1.2e3; 
$a = 7E-10;

Kolejnym typem jest typ logiczny (boolean), przyjmujący jedynie wartości FALSE i TRUE. Jest on używany przez wiele funkcji do zwracania rezultatu, czy operacja się powiodła. Wyrażenia porównawcze (czy równy, czy większy itd.) także generują wartości logiczne.

Ostatnim z typów skalarnych jest ciąg tekstowy (ang. string). Zdążyliśmy już wspomnieć nieco o nim, m.in. o istnieniu dwóch składni zapisywania ciągów. Ta oparta na apostrofach dopuszcza mniejszy zestaw kodów formatujących (pozwalających na wstawienie do tekstu innych apostrofów oraz ukośników wstecznych):

<?php
echo 'To jest tekst zapisany w apostrofach. Kody formatujące pozwalają
   umieścić w tekście wyłącznie inne apostrofy: \' albo backslashe: \\. Wszystko inne,
   np. \n zostanie wyświetlone jako zwyczajny tekst, zamiast znaku nowej linii';

Uruchom powyższy skrypt, aby zobaczyć jaki tekst zostanie wyświetlony. Spróbuj usunąć backslash sprzed apostrofu (w tekście: \') i zobacz, co się stanie. Skrypt się nie uruchomi, ponieważ wystąpił błąd składni. PHP napotka w sumie trzy apostrofy, a więc między drugim i trzecim będzie nierozpoznany dla parsera tekst, natomiast trzeci będzie niedomknięty.

Więcej możliwości formatowania posiada tekst ograniczony cudzysłowami:

<?php
echo "To jest tekst zapisany w cudzysłowach. Za pomocą kodów formatujących możemy
  umieszczać wiele rzeczy: znak cudzysłowu \" backslash \\ znak nowej linii \n i inne: \t \r \$";

Cudzysłowy zezwalają na "proste" umieszczanie wewnątrz tekstu wartości zmiennych, co zilustrujemy w prymitywnym przykładzie:

<?php
$czas = time();
echo "Aktualny czas w sekundach: $czas sek.";

Wartym zapamiętania jest fakt, że wstawianie zmiennych w ten sposób jest kilka razy wolniejsze, niż łączenie ich z ciągiem operatorem kropki.

<?php
// tutaj można użyć cudzysłowu jak i apostrofu
echo "Aktualny czas w sekundach: ".time()." sek.";

Niektórzy początkujący programiści niezbyt rozumieją ideę tej możliwości - próbują wykorzystywać ciągi do wprowadzania wartości zmiennych jako parametrów do funkcji:

<?php
$formatDaty = 'd.m.Y';
echo date("$formatDaty");

Powinno się unikać takiej konstrukcji, i choć PHP ją akceptuje, nie jest to prawidłowe użycie tej struktury języka. Co więcej, przy złożonych typach powoduje zniekształcenie danych. Jeżeli spotkasz kogoś piszącego w ten sposób, poinformuj go o tym. Do tego wyświetlanie danych zawartych w cudzysłowach przebiega wolniej niż w apostrofach.

Inne typy

edytuj

Typami złożonymi w PHP są tablice oraz obiekty. Tablice poznamy jeszcze w tym rozdziale, natomiast obiektami oraz samym programowaniem obiektowym zajmiemy się w dalszej części podręcznika.

Istnieją jeszcze dwa typy specjalne: resource oraz NULL. Pierwszy z nich reprezentuje wszelkiego rodzaju połączenia z bazami danych, otwarte przez PHP pliki itd. Drugi to wartość pusta. Za jego pomocą możemy "zasymulować", że zmienna nie istnieje lub nie zawiera wartości. Pojawia się w trzech sytuacjach:

  • Do zmiennej przypisana została stała NULL.
  • Do zmiennej nie została przypisana jeszcze żadna wartość (zgłaszane jest wtedy powiadomienie)
  • Zmienna została zniszczona poleceniem unset().

Przyjrzyjmy się wspomnianemu w trzecim punkcie poleceniu. Czasem jakąś zmienną trzeba zniszczyć. Najlepiej nadaje się do tego polecenie unset():

<?php
$zmienna = 4856;

unset($zmienna);

echo $zmienna;

Zmiennej już nie ma, dlatego polecenie echo pokaże nam powiadomienie o nieistniejącej zmiennej.

Konwersja typów

edytuj

PHP potrafi sam rozpoznać typ informacji przypisanej do zmiennej oraz automatycznie konwertować go w zależności od potrzeb. Przykładowo liczby ułamkowe użyte tam, gdzie potrzeba całkowitych, są zaokrąglane w górę do zera. Wartości logiczne mogą być reprezentowane cyframi 0 (FALSE) oraz 1 (TRUE). Ciągi tekstowe mogą być konwertowane do liczb, jeżeli pierwszy z nich (pomijając wiodące białe znaki) znaków jest cyfrą. W przeciwnym przypadku PHP dobiera wartość 0.

Możemy sami wymusić konwersję typów:

<?php
// wyświetl liczbę całkowitą jako ułamek
echo (float) 10;

W nawiasie przed wartością piszemy angielską nazwę typu: integer, int, string, boolean, float, double (liczba zmiennoprzecinkowa mogąca przybierać znacznie większe wartości). Unikaj jakiejkolwiek konwersji typów złożonych: tablic, obiektów, zasobów, gdyż w każdym z tych przypadków informacje zostają całkowicie utracone; zamiast nich zwracana jest po prostu nazwa typu złożonego - to dlatego ostrzegaliśmy przed wprowadzaniem wartości zmiennych do funkcji przez cudzysłowy.

PHP posiada funkcję gettype() zwracającą nazwę aktualnego typu danych zawartych w zmiennej:

<?php
$a = 547;
echo 'Typ zmiennej $a to: '.gettype($a);

Więcej o operatorach

edytuj

Dotychczas poznałeś już kilka operatorów, np. + czy =. Jak zdążyłeś już zauważyć, po obu stronach takiego operatora stoją wyrażenia, na których wykonuje on operacje i zwraca wynik. Sam jest zatem wyrażeniem. Operatory mają określoną kolejność wykonywania wzorowaną na matematyce. Możemy ją naginać do własnych potrzeb, stosując nawiasy.

Oto wykaz najciekawszych operatorów na początek.

Operator Nazwa Składnia Opis
/ Dzielenie wyrażenie / wyrażenie Reprezentuje wynik dzielenia. Drugie wyrażenie nie może być zerem.
% Dzielenie modulo wyrażenie % wyrażenie Reprezentuje resztę z dzielenia. Drugie wyrażenie nie może być zerem.
* Mnożenie wyrażenie * wyrażenie Reprezentuje iloczyn dwóch wyrażeń.
+ Dodawanie wyrażenie + wyrażenie Reprezentuje sumę dwóch wyrażeń.
- Odejmowanie wyrażenie - wyrażenie Reprezentuje różnicę dwóch wyrażeń.
. Łączenie (Konkatenacja) wyrażenie . wyrażenie Reprezentuje połączenie dwóch wyrażeń w ciąg tekstowy.
++ Postinkrementacja (zwiększenie) $zmienna++ Reprezentuje wartość zmiennej, a następnie zwiększa ją o 1.
++ Preinkrementacja (zwiększenie) ++$zmienna Zwiększa wartość zmiennej o 1, a następnie reprezentuje ją.
-- Postdekrementacja (zmniejszenie) $zmienna-- Reprezentuje wartość zmiennej, a następnie zmniejsza ją o 1.
-- Predekrementacja (zmniejszenie) --$zmienna Zmniejsza wartość zmiennej o 1, a następnie reprezentuje ją.

Zwróć uwagę na cztery ostatnie pozycje. Wyszczególnione zostały tam tzw. operatory jednoargumentowe, czyli takie, które operują wyłącznie na jednym wyrażeniu. W dodatku koniecznie musi być ono zmienną, ponieważ modyfikują one jej wartość, powiększając lub zmniejszając o jeden. Każdy z tych operatorów został podany podwójnie, ponieważ w zależności od tego, czy postawimy go przed, czy po zmiennej, otrzymamy nieco inne rezultaty. Porównaj:

<?php
// najpierw składnia $zmienna++

$zmienna = 5;

echo 'Stan 1: '.($zmienna++).'<br/>';
echo 'Stan 2: '.$zmienna.'<br/><br/>';

// teraz składnia ++$zmienna
echo 'Restart zmiennej...<br/>';
$zmienna = 5;

echo 'Stan 1: '.(++$zmienna).'<br/>';
echo 'Stan 2: '.$zmienna.'<br/>';

Skrypt ten wygeneruje nam kilka linijek:

Stan 1: 5
Stan 2: 6

Restart zmiennej...
Stan 1: 6
Stan 2: 6

Przeanalizuj wyniki działania. Okazuje się, że w składni $zmienna++ najpierw dostajemy wartość zmiennej, a dopiero potem zwiększamy ją o jeden (dlatego zmiana widoczna jest dopiero w drugim stanie). ++$zmienna najpierw powiększa, potem zwraca, w efekcie czego otrzymujemy w obu stanach liczbę "6". Identyczna zasada obowiązuje operator --.

Zajmijmy się teraz przypisywaniem danych do zmiennej. Wiemy już, że operator przypisania po lewej stronie wymaga zmiennej, po prawej wyrażenia, którego wartość trzeba w niej umieścić. Skoro operator to też jest wyrażenie, to jaką wartość ono reprezentuje? Okazuje się, że tę, która jest przypisywana. Możemy wobec tego zastosować sprytną sztuczkę, o której wbrew pozorom wie niezbyt wielu programistów. Zainicjujmy pięć zmiennych naraz tą samą wartością:

<?php
$a = $b = $c = $d = $e = 5;

PHP najpierw przypisze "5" do zmiennej $e, zwracając jednocześnie "5" tak, by mogło być ono przypisane do $d, potem do $c, $b i na końcu $a. W ten sposób jednym wielkim wyrażeniem zainicjowaliśmy pięć zmiennych naraz.

Poznany już operator przypisania nie jest jedynym, jaki istnieje w PHP. Aby ułatwić modyfikację wartości zmiennych o liczby inne niż jeden, stworzono całą gamę operatorów łączących w sobie przypisywanie oraz jakąś operację matematyczną. Oto i one.

Operator Składnia Równoważna postać Opis
/= $zmienna /= wyrazenie $zmienna = $zmienna / wyrażenie Dzieli zmienną przez wyrażenie i umieszcza w niej wynik. Wyrażenie nie może być zerem.
%= $zmienna %= wyrazenie $zmienna = $zmienna % wyrażenie Umieszcza w zmiennej resztę z dzielenia tej zmiennej przez wyrażenie, które oczywiście nie może być zerem.
*= $zmienna *= wyrazenie $zmienna = $zmienna * wyrażenie Mnoży zmienną przez wyrażenie i zapisuje w niej wynik.
+= $zmienna += wyrazenie $zmienna = $zmienna + wyrażenie Dodaje do zmiennej wyrażenie i zapisuje w niej wynik.
-= $zmienna -= wyrazenie $zmienna = $zmienna - wyrażenie Odejmuje od zmiennej wyrażenie i zapisuje w niej wynik.
.= $zmienna .= wyrazenie $zmienna = $zmienna . wyrażenie Dołącza do zmiennej tekstowej nowy fragment.

Te operatory po prostu skracają zapis i czynią go czytelniejszym. Warto o nich pamiętać szczególnie przy operacjach na ciągach tekstu, kiedy nasz algorytm składa jakąś treść, doczepiając kolejne jej partie do wybranej zmiennej:

<?php

$tekst = 'Litwo, ojczyzno moja! Ty jesteś jak zdrowie<br/>';
$tekst .= 'Ile Cię trzeba cenić, ten tylko się dowie<br/>';
$tekst .= 'Kto Cię stracił, dziś piękność twą w całej ozdobie<br/>';
$tekst .= 'Widzę i opisuję, bo tęsknię po tobie.<br/>';

echo $tekst;

Rezultat:

Litwo, ojczyzno moja! Ty jesteś jak zdrowie
Ile Cię trzeba cenić, ten tylko się dowie
Kto Cię stracił, dziś piękność twą w całej ozdobie
Widzę i opisuję, bo tęsknię po tobie.

Kolejne partie tekstu były doklejane do właściwej zmiennej tak, że na końcu otrzymaliśmy spójny i kompletny tekst. Identycznie jest z pozostałymi operatorami. += dodaje wartość do zmiennej, -= odejmuje itd.

Na tym zakończymy na razie temat operatorów, lecz to jeszcze nie wszystko. Już niedługo zapoznamy się z operatorami służącymi do porównywania danych oraz podstawami operatorów logicznych. Będą nam one potrzebne przy omawianiu instrukcji warunkowych i pętli.

Tablice

edytuj

Zaznajomimy się teraz z tablicami, pierwszym złożonym typem danych. Cofnijmy się do czasów szkoły podstawowej/gimnazjum na lekcję matematyki i przypomnijmy nasz pierwszy kontakt z pojęciem funkcji. Była tam mowa, że funkcję można przedstawiać w postaci tabelki:

x 0 1 2 3 4 5 6 7 8
y 5 3 8 7 9 24 15 2 19

Spróbujmy przenieść taką tabelkę w świat programowania. Widzimy, że wszystkie "igreki" są ze sobą powiązane, ponieważ wszystkie są możliwymi wynikami funkcji. Gdyby to zapisać jako zupełnie osobne zmienne, mielibyśmy problem z późniejszym dostaniem się do nich. Popatrz na to w ten sposób: użytkownik wprowadza argument, a my mamy dla niego odnaleźć wartość spośród tych podanych. Skąd PHP może wiedzieć, że akurat ta grupa zmiennych jest ze sobą w jakiś sposób powiązania, a tym bardziej znać regułę wybierania "tej właściwej"? Na razie nie jest to w ogóle możliwe. Co nam pozostaje? Zapisać wszystkie wartości w strukturze zwanej tablicą.

Wiemy już, że tablica jest zmienną, która grupuje sobie mniejsze zmienne opisywane przez ich indeksy (wartości x w naszej tabelce funkcji). Najważniejsze jest jednak to, że przy odwoływaniu się do nich, potrzebny nam indeks możemy określić zwyczajnym wyrażeniem! To oznacza, że zadanie wymienione akapit wyżej staje się realne. Zanim je zaprogramujemy, nieco informacji o składni tablic. Na początek utwórzmy pustą tablicę:

<?php
$tablica = array();

Spróbujmy wprowadzić do niej wartości naszej funkcji:

<?php
$tablica = array(0 => 5, 3, 8, 7, 9, 24, 15, 2, 19);

Początkowe "0" określa, od jakiej liczby zacząć numerowanie kolejnych elementów. Po strzałce wymieniamy ich wartości oddzielone przecinkiem. Należy pamiętać, że PHP (tak jak wiele popularnych języków programowania) zaczyna numerację zmiennych (indeksów) w tablicy od 0. Dlatego też możemy śmiało pominąć zapis 0 =>. Zapis ten przyda się nam jeżeli nie chcemy zacząć numeracji od 0, ale np. od 1, piszemy wtedy array( 1=> 5.... Teraz spróbujmy dostać się do jakiejś z nich.

<?php
$tablica = array(0 => 5, 3, 8, 7, 9, 24, 15, 2, 19);
echo 'Pod numerem 5 kryje się wartość '.$tablica[5];

Pomiędzy nawiasami kwadratowymi wprowadzić musimy wyrażenie określające indeks tablicy, który chcielibyśmy odczytać. Możemy teraz pokusić się o napisanie skryptu losującego elementy:

<?php
$tablica = array(0 => 5, 3, 8, 7, 9, 24, 15, 2, 19);
$liczba_losowa = rand(0, 7);
echo 'Pod numerem '.$liczba_losowa.' kryje się wartość '.$tablica[$liczba_losowa];

W tym skrypcie za wybranie indeksu tablicy odpowiada funkcja rand() zwracająca losową[1] wartość z tablicy.

Indeksy tablicy wcale nie muszą być numeryczne. PHP dopuszcza także tekstowe wersje. Mamy wtedy do czynienia z tablicami asocjacyjnymi.

<?php
$artykul = array(
  'tytul' => 'Tytuł artykułu',
  'data' => date('d.m.Y'),
  'tresc' => 'To jest treść artykułu'
);

echo '<h1>'.$artykul['tytul'].'</h1>';
echo '<p>Napisany dnia '.$artykul['data'].'</p>';
echo '<p>'.$artykul['tresc'].'</p>';

Mogą istnieć także tablice mieszane, w których występują zarówno indeksy tekstowe, jak i numeryczne. Pamiętaj, że każdy element tablicy zachowuje się, jak zwykła zmienna, dlatego także możesz przypisywać do niego dowolne wartości już po utworzeniu tablicy.

<?php

$tablica = array(0 => 5, 3, 8, 7, 9, 24, 15, 2, 19);

// modyfikuj losowy element tablicy
$tablica[rand(0, 7)] = 6;

var_dump($tablica);

Najpierw przypisaliśmy do losowego elementu tablicy nową wartość: 6. Całość wyświetlamy funkcją var_dump(). Przydaje się ona przy poszukiwaniu błędów w skrypcie. Potrafi zaprezentować w czytelnej formie każdy typ danych, więc możemy za jej pomocą kontrolować, czy na danym etapie wykonywania rezultaty są takie, jakie być powinny. Podobne działanie ma funkcja print_r(), przy czym var_dump() generuje gotowy kod HTML, otoczony znacznikami <pre> </pre> i sformatowany, podczas gdy print_r() zwraca tekst sformatowany, ale bez znaczników HTML.

Zobacz teraz, jak zachowuje się $tablica, kiedy próbujemy ją wywołać samodzielnie:

<?php
$tablica = array(0 => 5, 3, 8, 7, 9, 24, 15, 2, 19);

echo $tablica;

Skrypt ten pokaże nam tylko jeden napis: Array, czyli... nazwę typu. Właśnie z tego powodu ostrzegaliśmy przed wprowadzaniem danych do funkcji jako funkcja("$zmienna");. Gdyby w zmiennej była tablica, do funkcji zamiast niej dotarłby napis Array i całość przestałaby działać. Osobną kwestią jest wydajność takiego zmuszania do konwersji do tekstu, a potem z powrotem na tekst właściwy.

Upewnij się, że rozumiesz już istotę działania tablic, gdyż bardzo przydadzą się nam one w następnym rozdziale.

Przypisy

  1. Tak naprawdę komputer nie potrafi losować liczb. Za całą tą zasłonką kryją się różne skomplikowane wzory matematyczne inicjowane najczęściej aktualnym czasem, dające wrażenie losowości wyników.


Poprzedni rozdział: Zmienne i tablice
Następny rozdział: Struktury kontrolne

Formularze

edytuj

W tym rozdziale zajmiemy się podstawami komunikacji skryptu PHP z przeglądarką.

Protokół HTTP

edytuj
 
Działanie protokołu HTTP.

Podstawą funkcjonowania stron WWW jest protokół HTTP, którego używa przeglądarka i serwer. Zasada jego działania jest bardzo prosta. Gdy chcemy obejrzeć dokument pod podanym adresem URL, wysyłamy do serwera tzw. żądanie HTTP zawierające lokalizację zasobu oraz garść informacji o nas samych. Serwer odnajduje lub generuje (np. przy pomocy PHP) odpowiedni dokument i odsyła wszystko jako odpowiedź HTTP. Odpowiedź zawsze zawiera pewną ilość nagłówków informacyjnych oraz opcjonalną treść, w której przesyłany jest dokument. Przeglądarka odbiera wszystko i rozpoczyna działanie. Protokół jest stosowany zarówno do pobierania kodu HTML strony, jak i znajdujących się na niej obrazków, ściągania plików i innych danych multimedialnych. Rodzaj pobieranej zawartości jest określany przez nagłówki.

Skrypty PHP zawsze pracują po stronie serwera, generując odpowiedzi HTTP na przychodzące do niego żądania. Przeważnie koncentrujemy się jedynie na budowaniu treści, ponieważ interpreter potrafi samodzielnie skonstruować podstawowy zestaw odpowiednich nagłówków, który co najwyżej uzupełniamy. Bardzo często do wygenerowania strony potrzebne są dodatkowe informacje, które najczęściej przechowywane są w bazie danych, a rzadziej - w plikach. Skrypty pobierają z nich dane, poddają je obróbce i osadzają w kodzie HTML, który jest odsyłany do klienta.

Istotną częścią protokołu HTTP są rodzaje żądań (zwane "metodami") informujące o tym, co próbujemy zrobić. Dwa podstawowe to:

  1. Żądania GET - zwyczajne pobieranie dokumentu z serwera.
  2. Żądania POST - wysłanie pewnych danych na serwer.

Istnieją jeszcze inne metody, które są coraz powszechniej stosowane w większych aplikacjach WWW, jednak na niektórych serwerach są one z nieznanych powodów poblokowane. Protokołem HTTP zajmiemy się dokładniej w dalszej części podręcznika, tymczasem na razie będą nas interesować te dwie metody. Pierwsza z nich jest wykorzystywana podczas zwykłego pobierania z serwera dokumentu, natomiast druga - przy formularzach.

Ogólnie o danych wejściowych

edytuj

W żądaniu HTTP przenoszonych jest wiele informacji o tym, co użytkownik chce obejrzeć oraz o nim samym. Wielu danych dostarcza także sam serwer HTTP. Wszystkie one są podstawą dla aplikacji PHP do wygenerowania odpowiedzi. Mechanizm ich odbierania ewoluował stopniowo. Pierwsze wersje języka rejestrowały wszystkie nadesłane parametry, dane formularzy itd. jako zmienne, lecz było to wyjątkowo niebezpieczne. Dodając nowe parametry można było rejestrować nowe zmienne, które mogły nadpisać zmienne używane w kodzie aplikacji powodując jego błędne działanie (np. zyskanie praw administratora strony przez atakującego). Programista nie mógł także policzyć, ile danych właściwie trafiało do skryptu i gdzie są one zawarte.

Wszystko zmieniło się wraz z pojawieniem się PHP4. Obecnie wszystkie pola są rejestrowane w specjalnych, tworzonych przez skrypt tablicach asocjacyjnych posegregowane według miejsca, z którego nadeszły. PHP potrafi odbierać informacje:

  • Charakterystyczne dla metody GET (adresy URL)
  • Charakterystyczne dla metody POST (zawartość formularzy)
  • z serwera
  • z ciasteczek
  • z sesji (emulowanych przez PHP)

Tutaj zajmiemy się trzema pierwszymi pozycjami. Ciastka oraz sesje zostaną omówione w dalszych rozdziałach podręcznika.

Adresy URL

edytuj

Do adresu URL można dołączać po znaku zapytania dodatkowe zmienne, np. www.example.com/strona.php?zmienna=wartosc&inna_zmienna=wartosc. Są one przechowywane w tablicy $_GET. Ten sposób przekazywania danych wykorzystujemy tylko, gdy skrypt nie wykonuje operacji mających efekty uboczne (np. może być wyszukiwanie, ale już nie dodawanie lub usuwanie rekordów) lub do przesyłania danych kontrolnych. W przeciwnym wypadku roboty indeksujące stronę i proxy ładujące strony z wyprzedzeniem mogą niechcący wykonywać operacje na serwerze. Ponadto cache przeglądarki i dostawców internetowych może spowodować zignorowanie zapytań.

Przyjrzymy się zawartości tablicy:

<?php
var_dump($_GET);

Wywołując skrypt normalnie: http://localhost/~programowanie_php/nazwaskryptu.php otrzymamy następujący rezultat:

array(0) { }

Oznacza to, że nie otrzymaliśmy tą drogą żadnych danych. Dodajmy teraz do adresu ciąg ?parametr=wartosc. Po odświeżeniu zobaczymy:

array(1) { ["parametr"]=> string(7) "wartosc" }

Tablica zawiera jeden element o nazwie parametr przechowujący wpisaną w adresie wartość.

Napiszemy teraz prosty skrypt wyświetlający informacje powitalne na podstawie danych z adresu:

<?php
if(sizeof($_GET) == 2)
{
   echo 'Witaj, '.$_GET['imie'].' '.$_GET['nazwisko'].'!';
}
else
{
   echo 'Nieprawidłowa liczba parametrów!';
}

Nie przejmuj się istnieniem konstrukcji, której jeszcze nie poznaliśmy. Niektórzy pewnie domyślili się, co ona robi, ale szczegóły będą podane już w następnym rozdziale. Na razie wpiszmy ją tak, jak jest. Funkcja sizeof() pojawiająca się w kodzie zwraca ilość elementów w tablicy. Sprawdzamy w ten sposób, czy użytkownik podał to, co trzeba. Kontrola nadchodzących danych jest niezwykle istotna i nigdy nie wolno jej zlekceważyć. Pominięcie tego aspektu zazwyczaj kończy się dla skryptu tragicznie, bo jeżeli coś jest do zepsucia, na pewno znajdzie się ktoś, kto tego dokona.

Wywołując skrypt z parametrami "imie" oraz "nazwisko" możemy wpływać na wyświetlane informacje: http://localhost/~programowanie_php/nazwaskryptu.php?imie=Adam&nazwisko=Kowalski. Dla lepszego efektu stwórzmy prosty formularz XHTML wysyłający dane metodą GET:

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
 <head>
  <title>Formularz XHTML</title>
 </head>
 <body>
  <form method="get" action="nazwaskryptu.php">
   <p>
    <label>Podaj imię: <input type="text" name="imie"/></label>
   </p>
   <p>
    <label>Podaj nazwisko: <input type="text" name="nazwisko"/></label>
   </p>
   <p>
    <input type="submit" value="OK"/>   
   </p>
  </form>
 </body>
</html>

Choć tworzenie formularzy teoretycznie pasuje do następnej sekcji, tak naprawdę w tym momencie zwyczajnie oszukujemy. Wypełnij ten formularz i wyślij go, a zobaczysz, że przeglądarka dokleiła do pliku podanego w znaczniku FORM parametry utworzone na podstawie pól! Tak więc nasz skrypt nawet nie ma możliwości stwierdzenia, skąd dane do niego przyszły! Poznajmy zatem metodę POST...

Formularze

edytuj

Obsługa formularzy z prawdziwego zdarzenia, którymi można przesyłać setki informacji, odbywa się dosyć podobnie, jak w przypadku użycia w formularzu metody "get" do przesyłania adresów z parametrami. Różnica jest taka, że wszystko wysyła się wyłącznie z formularza, który posiada parametr "method" ustawiony na "post" oraz że korzysta się z tablicy $_POST wewnątrz samego skryptu. Przeróbmy nasze ostatnie dzieło tak, aby pracowało w ten sposób.

<?php
if(count($_POST) == 2)
{
   echo 'Witaj, '.$_POST['imie'].' '.$_POST['nazwisko'].'!';
}
else
{
   echo 'Nieprawidłowa liczba parametrów!';
}

W skrypcie podmieniamy jedynie nazwy tablic na $_POST. W formularzu musimy jeszcze zmienić metodę:

<html>
<head>
 <title>Formularz HTML</title>
</head>
<body>
  <form method="post" action="nazwaskryptu.php">
  Podaj imię: <input type="text" name="imie"/><br/>
  Podaj nazwisko: <input type="text" name="nazwisko"/><br/>
  <input type="submit" value="OK"/>   
  </form>
</body>
</html>

I gotowe. Wyślij teraz formularz. Zauważ, że żadna informacja nie jest doklejana do adresu, ponieważ transmisja odbywa się niejako innym kanałem.

Z kursów języka HTML wiadomo, że istnieją różne typy pól formularzy. Oto, jakie wartości otrzymuje od nich PHP:

  • <input type="text" name="nazwa"/>
    - skrypt otrzymuje $_POST['nazwa'] z wartością wpisaną w pole formularza.
  • <input type="hidden" name="nazwa" value="wartosc"/>
    - skrypt otrzymuje $_POST['nazwa'] z wartością wpisaną w danym znaczniku. Użyteczne do przesyłania formularzem ukrytych informacji, o których typowy użytkownik wiedzieć nie musi.
  • <input type="radio" name="nazwa" value="wartosc"/>
    - pozycje należące do tej samej grupy muszą mieć identyczną nazwę. $_POST['nazwa'] będzie zawierać wartość tej pozycji, która jest aktualnie zaznaczona.
  • <input type="checkbox" name="nazwa"/>
    - jeśli pole jest zaznaczone, $_POST['nazwa'] zawierać będzie słowo "on".
  • <select name="nazwa">...</select>
    - $_POST['nazwa'] zawierać będzie wartość wybranego z listy elementu.
  • <input type="submit" name="nazwa"/>
    - zmienna $_POST['nazwa'] zostanie utworzona, jeżeli akurat ten przycisk zostanie wciśnięty.

Dzięki temu można do formularzy wstawiać kilka przycisków "submit" i reagować inaczej w zależności od tego, który z nich został naciśnięty.

Serwer

edytuj

Na sam koniec zostawiliśmy sobie kilka informacji przekazywanych interpreterowi przez serwer WWW (np. Apache). Zacznijmy od określenia adresu IP gościa:

<?php
echo 'Witaj, twój adres IP to '.$_SERVER['REMOTE_ADDR'].'!';

Ten skrypt pokaże nam adres IP komputera lub sieci, w której znajduje się internauta. W tym drugim przypadku dalsze informacje są niepewne. Serwerowi potrzebny jest wyłącznie ten adres, aby móc gdzieś wysłać rezultat, a pola $_SERVER['HTTP_X_FORWARDED_FOR'] lub $_SERVER['HTTP_CLIENT_IP'] są bardzo niepewne - generuje je właśnie serwer proxy, ale nic nie stoi na przeszkodzie, by podszył się pod nie hacker. Uznawanie ważności informacji tu zawartych było przyczyną wielu błędów bezpieczeństwa w popularnym skrypcie forów dyskusyjnych phpBB i jeżeli naprawdę zależy Ci na nim, pozostań przy REMOTE_ADDR, a resztę traktuj wyłącznie jako ciekawostkę.

Korzystając z funkcji gethostbyaddr() możemy uzyskać nazwę hosta, którego dotyczy adres IP:

<?php
echo 'Witaj, twój host to '.gethostbyaddr($_SERVER['REMOTE_ADDR']).'!';

Spróbujmy zidentyfikować przeglądarkę użytkownika:

<?php
echo 'Twoja przeglądarka została zidentyfikowana jako: '.$_SERVER['HTTP_USER_AGENT'].'!';

Pole to zawiera wyłącznie surowe informacje. Każda przeglądarka wpisuje tu co innego i dlatego wyciągnięcie ładnych i pogrupowanych w odpowiednie kategorie danych to zadanie na więcej, niż jeden artykuł. Pozostańmy zatem przy takiej postaci, a przetwarzaniem nagłówków przeglądarek zajmiemy się kiedy indziej.

Adres strony, z której przybyliśmy do naszego skryptu, znajduje się w zmiennej $_SERVER['HTTP_REFERER']. Można wykorzystać go do utworzenia linków "Cofnij" albo do detekcji, jakich słów kluczowych używają ludzie trafiający do nas z wyszukiwarek. Zauważ - szukane frazy zazwyczaj dołączane są właśnie do adresów URL i w ten sposób można je zdobyć.

Uwaga: nie można polegać na obecności i prawidłowości tej informacji. Istnieją przeglądarki i zapory ogniowe, które ją usuwają lub wstawiają tam dowolny adres podany przez użytkownika.

<?php
echo 'Przybyłeś do nas ze strony: '.$_SERVER['HTTP_REFERER'].'!';

W chwili obecnej to tyle, jeżeli chodzi o pobieranie danych. Dalszych informacji dowiemy się w trakcie następnych rozdziałów w miarę potrzeby.

Poprzedni rozdział: Formularze
Następny rozdział: Instrukcja if

Struktury kontrolne

edytuj

Programy wykonujące szereg zawsze tych samych instrukcji nie pozwalają rozwinąć skrzydeł. Jeżeli aplikacja ma być w pełni interaktywna, musi umieć reagować na poczynania użytkownika lub sytuacje kryzysowe na podstawie zadanych warunków. Tu do akcji wkraczają instrukcje kontrolne. Są one podstawowym budulcem wielu algorytmów i ich poznanie jest niezbędne. Za to my zyskamy pełną kontrolę nad wszystkim, mogąc efektywnie reagować na wszystkie zdarzenia.

Jedną z instrukcji kontrolnych - if - widziałeś już w poprzednim rozdziale. Zauważyłeś też pewnie, iż nie była ona zakończona średnikiem, a zamiast tego pojawiały się przy niej nawiasy klamrowe tworzące tzw. blok kodu. Jest to ciąg komend złożony z wyrażeń i innych struktur kontrolnych, czytelnie oznakowany. W połączeniu z odpowiednią instrukcją, PHP może decydować, czy wykonać go, czy nie, czy też powtórzyć raz jeszcze. Klamry pełnią tutaj rolę drogowskazu pokazującego, co się danej instrukcji tyczy.

PHP posiada siedem struktur kontrolnych, lecz my poznamy sześć z nich. Siódma jest tak rzadko stosowana, że większość zaawansowanych programistów nie umie z niej korzystać, a ponadto wymaga ona od nas pewnej dodatkowej wiedzy. Oto lista wszystkich instrukcji:

  1. Instrukcja if
  2. Instrukcja switch
  3. Instrukcja for
  4. Instrukcja while
  5. Instrukcja do while
  6. Instrukcja foreach
Poprzedni rozdział: Struktury kontrolne
Następny rozdział: Instrukcja switch

Instrukcja if

edytuj

Z tą instrukcją zetknęliśmy się już przy okazji omawiania formularzy. Przypomnijmy jeszcze raz ten przykład:

<?php
if(count($_GET) == 2)
{
   echo 'Witaj, '.$_GET['imie'].' '.$_GET['nazwisko'].'!';
}
else
{
   echo 'Nieprawidłowa liczba parametrów!';
}

Instrukcja if pozwala na wykonanie części kodu tylko wtedy, kiedy spełniony jest określony warunek, oraz opcjonalne dodawanie alternatywnych instrukcji w razie jego fałszywości. Z if-a korzysta się niezwykle często, ponieważ niemal zawsze musimy sprawdzać, czy dane informacje są prawidłowe, czy funkcja poprawnie połączyła się z plikiem itd. Poniżej napiszemy skrypt, który będzie potrafił rozwiązywać równanie kwadratowe  . Przypomnijmy, że ilość jego rozwiązań rzeczywistych zależy od wartości tzw. współczynnika Δ ( ). Jeżeli obliczony wynik (Δ) jest dodatni, istnieją dwa rozwiązania. Jeżeli ujemny - nie ma żadnych. Dla zera równanie daje jedno rozwiązanie (podwójne).

<?php
// 1
if(!isset($_GET['a']))
{
   $_GET['a'] = 0;
}
if(!isset($_GET['b']))
{
   $_GET['b'] = 0;
}
if(!isset($_GET['c']))
{
   $_GET['c'] = 0;
}
 
// 2
if($_GET['a'] == 0)
{
   die('Nieprawidłowy parametr A!');
}
 	
// 3
$delta = pow($_GET['b'], 2) - 4 * $_GET['a'] * $_GET['c'];

// 4
if($delta > 0)
{
   // 5
   echo 'Delta dodatnia. Dwa rozwiązania:<ul>';
   echo '<li>'.round((-$_GET['b']-sqrt($delta))/(2*$_GET['a']), 5).'</li>';
   echo '<li>'.round((-$_GET['b']+sqrt($delta))/(2*$_GET['a']), 5).'</li>';
   echo '</ul>';
}
elseif($delta < 0)
{
   // 6
   echo 'Delta ujemna. Brak rozwiązań w zbiorze liczb rzeczywistych!';
}
else
{
   // 7
   echo 'Delta = 0. Jedno rozwiązanie: '.round((-$_GET['b'])/(2*$_GET['a']), 5);
}

Jest to pierwszy tak długi kod zawarty w tym podręczniku, dlatego omówimy go sobie w punktach. Numery odpowiednich fragmentów zaznaczone są w kodzie komentarzami jednolinijkowymi.

  1. Na początek sprawdzamy, czy użytkownik podał wszystkie parametry. Funkcja isset() zwraca wartość TRUE, jeżeli zmienna istnieje, a operator negacji (!) sugeruje, że kod w nawiasie chcemy wykonać wtedy, gdy tej zmiennej nie ma. Musimy wtedy podstawić za nią neutralną wartość 0, ponieważ inaczej skrypt będzie nam zgłaszać powiadomienia.
  2. Tutaj zaczynamy właściwy algorytm. Najpierw sprawdzamy, czy rzeczywiście mamy do czynienia z równaniem kwadratowym. Parametr a musi być różny od zera. W razie problemów instrukcją die() zatrzymujemy skrypt w tym miejscu.
  3. Liczymy współczynnik Δ. Funkcja pow(liczba, potega) podnosi podaną liczbę do odpowiedniej potęgi i działa szybciej, niż ręczne mnożenie wartości.
  4. Pierwszy wariant - kiedy Δ jest dodatnia...
  5. Oblicz każde z dwóch rozwiązań równania odpowiednim wzorem. Funkcja round(liczba, miejsca) zaokrągla nam wynik do określonej ilości miejsc po przecinku, natomiast sqrt(liczba) zwraca pierwiastek z podanej liczby. Zwróć uwagę na użycie nawiasów do zasugerowania właściwej kolejności działań.
  6. Gdy Δ jest ujemna, równanie nie ma rozwiązania.
  7. Ostatni z wariantów jest oczywisty, dlatego nie piszemy już warunku. To przecież ostatnia z możliwości. Równanie ma tylko jedno rozwiązanie i także je wyliczamy.

Jest to nasz pierwszy prawdziwie dynamiczny skrypt, który potrafi reagować inaczej w zależności od sytuacji. Poznaliśmy tutaj nie tylko kilka nowych funkcji, ale także sporo operatorów i zasadę działania instrukcji if. Jej formalna składnia jest następująca:

<?php
if(wyrazenie)
{
   // blok kodu
}
elseif(wyrazenie)
{
   // blok kodu
}
else
{
   // blok kodu
}

Obowiązkowe jest podawanie pierwszego z członów zaczynającego się od if. Dwa pozostałe są opcjonalne, przy czym ilość elseif może być dowolna. Kolejność podawania kolejnych typów członów ukazana jest na przykładzie.

  • if - wykonuje się, gdy spełniony został podany warunek
  • elseif - jeżeli nie został spełniony poprzedni warunek, PHP testuje aktualny i jeżeli jest prawdziwy, wykonuje ten kawałek kodu.
  • else - wykonywane, jeżeli żaden z powyższych warunków nie został spełniony.

Jeżeli blok kodu zawiera tylko jedną instrukcję, PHP dopuszcza możliwość opuszczenia nawiasów klamrowych, np.

<?php
if($zmienna == 6)
   echo 'Wartość zmiennej wynosi 6';

W tym podręczniku jednak nie będziemy jej stosować z powodu pogorszenia czytelności kodu, niemniej warto wiedzieć, iż składnia ta jest w pełni poprawna, ponieważ część programistów stosuje ją w praktyce.

Wyrażenie warunkowe powinno przyjmować wartości logiczne TRUE lub FALSE. Oto kilka przydatnych operatorów:

Operator Nazwa Składnia Opis
== Równość wyrażenie == wyrażenie Zwraca prawdę, jeżeli oba wyrażenia mają identyczną wartość.
=== Równość wyrażenie === wyrażenie Zwraca prawdę, jeżeli oba wyrażenia mają identyczną wartość oraz typ.
!= Nierówność wyrażenie != wyrażenie Zwraca prawdę, jeżeli oba wyrażenia mają różne wartości.
!== Nierówność wyrażenie !== wyrażenie Zwraca prawdę, jeżeli oba wyrażenia mają różne wartości i/lub typ.
< Mniejsze niż wyrażenie < wyrażenie Zwraca prawdę, jeżeli lewe wyrażenie ma mniejszą wartość od prawego.
> Większe niż wyrażenie > wyrażenie Zwraca prawdę, jeżeli lewe wyrażenie ma większą wartość od prawego.
<= Mniejsze lub równe wyrażenie <= wyrażenie Zwraca prawdę, jeżeli lewe wyrażenie ma mniejszą lub równą wartość prawemu.
>= Większe lub równe wyrażenie >= wyrażenie Zwraca prawdę, jeżeli lewe wyrażenie ma większą lub równą wartość prawemu.
! Negacja (nie) !wyrażenie Zwraca prawdę, jeżeli wyrażenie jest fałszywe i fałsz, jeśli prawdziwe.
&& Koniunkcja logiczna (i) wyrażenie && wyrażenie Zwraca prawdę, jeżeli oba wyrażenia są prawdziwe.
|| Alternatywa logiczna (lub) wyrażenie || wyrażenie Zwraca prawdę, jeżeli przynajmniej jedno z wyrażeń jest prawdziwe.

A teraz kilka przykładów...

<?php
//Zakładamy, że do skryptu wysłano formularz z polami liczba1 i liczba2
if($_POST['liczba1'] == 1 && $_POST['liczba2'] == 2) 
{
   die('Liczba 1 wynosi 1, a liczba 2 wynosi 2');
}

Powyższy skrypt sprawdza, czy zmienna "liczba1" wynosi 1 i zmienna "liczba 2" wynosi 2.

<?php
//Zakładamy, że do skryptu wysłano formularz z polami liczba1 i liczba2
if($_POST['liczba1'] == 1 || $_POST['liczba2'] == 1) 
{
   die('Liczba 1, lub liczba 2 wynosi 1');
}

Natomiast ten skrypt sprawdza, czy zmienna "liczba1", lub zmienna "liczba2" wynosi 1.

Wszystkie operatory podane wcześniej w tabelce przydają się przy konstruowaniu warunków. Pewnego wyjaśnienia domagają się == oraz ===. Popatrz sobie na taki przykład:

<?php
if(FALSE == 0)
{
   echo 'Prawda!';
}

PHP automatycznie sprowadzi tu sobie obie wartości do identycznego typu i wtedy dopiero je porówna. Dlatego skrypt wyświetli napis "Prawda!". Zamień teraz ten operator na ===. Po odświeżeniu zobaczymy, że teraz nic się nie pokazało. To dlatego, że zażądaliśmy, aby i typy obu wyrażeń były identyczne, podczas gdy nie są. Operator ten przydaje się przy niektórych funkcjach zwracających różne typy wartości w zależności od powodzenia operacji.

Oprócz tego w warunkach przyda się nam kilka funkcji:

  • isset($zmienna) - zwraca prawdę, jeżeli zmienna istnieje.
  • empty($zmienna) - zwraca prawdę, jeżeli zmienna ma wartość pustą (np. NULL, 0 albo pusty ciąg tekstowy).
  • is_null($zmienna) - zwraca prawdę, jeżeli zmienna ma wartość NULL.
  • is_string($zmienna) - zwraca prawdę, jeżeli zmienna jest ciągiem tekstowym.
  • is_integer($zmienna) - zwraca prawdę, jeżeli zmienna jest liczbą całkowitą.
  • is_float($zmienna) - zwraca prawdę, jeżeli zmienna jest liczbą zmiennoprzecinkową.
  • is_numeric($zmienna) - zwraca prawdę, jeżeli zmienna jest liczbą.
  • is_bool($zmienna) - zwraca prawdę, jeżeli zmienna ma wartość logiczną.
  • is_array($zmienna) - zwraca prawdę, jeżeli zmienna jest tablicą.
Poprzedni rozdział: Instrukcja if
Następny rozdział: Instrukcja for

Instrukcja switch

edytuj

Instrukcja switch zwana jest także instrukcją wyboru. Jej działanie jest podobne do szczególnego przypadku poznanej ostatnio instrukcji warunkowej. Pokażemy to na przykładzie.

Nietrudno znaleźć witryny internetowe, które w jednym pliku grupują kilka różnych zadań wykonywanych w zależności od parametru, np. index.php?act=dodaj, index.php?act=usun itd. Możemy to zaprogramować za pomocą dużego ifa:

<?php
if($_GET['act'] == 'dodaj')
{
   echo 'Dodawanie danych';
}
elseif($_GET['act'] == 'edytuj')
{
   echo 'Edycja danych';
}
elseif($_GET['act'] == 'usun')
{
   echo 'Usuwanie danych';
}
else
{
   echo 'Wyświetlanie danych';
}

Rozwiązanie to nie jest wygodne nie tylko ze względu na objętość takiego kodu, ale również czytelność. Przypuśćmy, że z jakiegoś powodu musimy zmienić miejsce, z którego pobieramy informację o akcji. Trzeba to zrobić w czterech miejscach, a tych może być w teorii jeszcze więcej. Znacznie łatwiejszą w użyciu, a przy tym wydajniejszą alternatywą jest instrukcja switch. Działa ona w ten sposób, że wybiera spośród dostępnego zbioru określoną wartość na podstawie wartości pewnego wyrażenia i wykonuje zdefiniowany dla niej kod. Przepiszmy raz jeszcze powyższy przykład:

<?php
if(!isset($_GET['act']))
{
   $_GET['act'] = 'index';
}
switch($_GET['act'])
{
   case 'dodaj':
      echo 'Dodawanie danych';			
      break;
   case 'edytuj':
      echo 'Edycja danych';
      break;
   case 'usun':
      echo 'Usuwanie danych';
      break;
   default:
      echo 'Wyświetlenie danych';	
}

W nawiasie polecenia switch definiujemy, jakiemu wyrażeniu pragniemy sprawdzić wartość. Wewnątrz nawiasów klamrowych używamy struktury case wartość:, aby zdefiniować dopuszczalne wartości, czyli stany wyrażenia. Po dwukropku piszemy odpowiedni kod. Jeżeli żaden ze stanów nie spełnia naszych oczekiwań, istnieje także klauzula default: pisana na samym końcu opisująca domyślne zachowanie. Jej już nie musimy dodawać, jeżeli tego nie potrzebujemy, niemniej często się ona przydaje. Zwróć uwagę na komendę break; stojącą na końcu każdego kodu przypisanego do case. Mówi ona, że wykonywanie przerywane jest w tym miejscu i PHP ma skoczyć do końca switcha, a nie przypadkiem wykonać kolejny stan. Taka oryginalna budowa wynika z jednego prostego powodu: jeżeli chcemy dla trzech różnych stanów wykonać to samo zadanie, po prostu piszemy pod rząd trzy case'y, potem kod i na końcu przerwanie. W powyższym przykładzie dodaliśmy alternatywną nazwę pierwszej akcji: "dod". Kod po przeróbkach wygląda tak:

<?php
if(!isset($_GET['act']))
{
   $_GET['act'] = 'index';
}
switch($_GET['act'])
{
   case 'dod':
      echo 'Jak nie damy komendy "break", to pokaże nam się też...<br/>';
   case 'dodaj':
      echo 'Dodawanie danych';			
      break;
   case 'edytuj':
      echo 'Edycja danych';
      break;
   case 'usun':
      echo 'Usuwanie danych';
      break;
   default:
      echo 'Wyświetlenie danych';	
}

Wywołując skrypt jako nazwapliku.php?act=dodaj, zobaczymy:

Dodawanie danych

Wywołując skrypt jako nazwapliku.php?act=dod, zobaczymy:

Jak nie damy komendy "break", to pokaże nam się też...
Dodawanie danych

PHP wykonał zarówno stan "dod", jak i następujący po nim "dodaj", gdyż w tym pierwszym brakowało komendy break.

Kod stanu może zawierać inne struktury kontrolne:

<?php
if(!isset($_GET['act']))
{
   $_GET['act'] = 'index';
}
switch($_GET['act'])
{
   case 'dodaj':
      if($_SERVER['REQUEST_METHOD'] == 'POST')
      {
         echo 'Dodawanie danych';
      }
      else
      {
          echo 'Formularz dodawania';
      }
      break;
   case 'edytuj':
      echo 'Edycja danych';
      break;
   case 'usun':
      echo 'Usuwanie danych';
      break;
   default:
      echo 'Wyświetlenie danych';	
 }

Tutaj dodaliśmy instrukcję if, która sprawdza, czy żądanie nadeszło do nas z formularza HTTP, który należy przetworzyć, czy normalnie (wtedy wyświetlamy formularz). $_SERVER['REQUEST_METHOD'] zawiera nazwę metody, za pomocą której odbyło się żądanie HTTP.

Instrukcję switch warto stosować, kiedy wybieramy konkretną możliwość z określonego w kodzie zbioru. Dla PHP zaletą jest, że "wie", jakie są wszystkie stany. Instrukcja warunkowa if jest bardziej ogólna i tam interpreter sprawdza po prostu po kolei wszystkie warunki, aż natrafi na pasujący, nie zagłębiając się w jakieś większe zależności między danymi. Switcha nie należy używać, kiedy mamy tylko dwa stany, gdyż takie zagranie przypominałoby wytoczenie haubicy do zabicia komara. Instrukcja ta nie sprawdza się także przy wszystkich bardziej ogólnych warunkach rodzaju "mniejszy, większy".

Poprzedni rozdział: Instrukcja switch
Następny rozdział: Instrukcja while

Instrukcja for

edytuj

Pętle

edytuj

Wszystkie kolejne struktury kontrolne, jakie poznamy, określa się jednym wspólnym terminem: pętle.

Wiemy już, że pętla powtarza w kółko pewien fragment kodu. Różnice między poszczególnymi rodzajami dotyczą tego, jak i kiedy jest ona przerywana. Na początek zajmiemy się pętlą for. Pokazuje ona pazurki, kiedy zliczamy ilość wywołań pętli i na podstawie tego określamy, czy trzeba ją przerwać, czy nie. W for definiujemy trzy wyrażenia:

  • Startu - najczęściej inicjuje licznik wywołań
  • Końca - warunek zakończenia
  • Iteracji - najczęściej zwiększa licznik wywołań

Oddzielone są one średnikami. Pokażemy to na przykładzie skryptu wyświetlającego liczby od 0 do 9.

<?php
for($i = 0; $i < 10; $i++)
{
	echo $i.'<br/>';	
}

Warunek startu tworzy nową zmienną $i z wartością zero. Następnie określamy, że dopóki $i jest mniejsze od 10, pętla ma się powtarzać. Przy każdym cyklu należy zwiększyć wartość $i o 1.

Proste wyświetlanie tablic

edytuj

Pętla for jest użyteczna przy wyświetlaniu tablic z indeksami numerycznymi. Mamy plik tekstowy z zawartością:

Litwo, ojczyzno moja! Ty jesteś jak zdrowie,
Ile cię trzeba cenić, ten tylko się dowie,
Kto cię stracił, dziś piękność twą w całej ozdobie
Widzę i opisuję, bo tęsknię po tobie.

Zastosujemy funkcję file(), aby wczytać go do pamięci z jednoczesnym rozbiciem na poszczególne wiersze zapisane w tablicy. W ten sposób będziemy je mogli wyświetlić jako elementy listy wypunktowanej:

<?php
$plik = file('plik.txt');
 	
echo '<ul>';
for($i = 0, $x = count($plik); $i < $x; $i++)
{
	echo '<li>'.trim($plik[$i]).'</li>';	
}
echo '</ul>';

Do określenia ilości wierszy użyliśmy poznanej już wcześniej funkcji count(). Przy wyświetlaniu stosujemy jeszcze jedną: trim(). Usuwa ona z początku i końca każdego wiersza białe znaki, tj. spacje, zejścia do nowej linii, tabulatory. Wynikiem działania skryptu jest zawartość pliku wyświetlona w liście wypunktowanej.

Zwróć uwagę na specyficzną budowę wyrażenia inicjacji pętli. Pragniemy utworzyć dwie zmienne, dlatego oddzielamy je przecinkami. Podobną sztuczkę możemy zastosować również w wyrażeniu iteracyjnym. Można się zapytać, dlaczego zastosowaliśmy tak rozbudowaną konstrukcję. Przecież dopuszczalne jest także napisanie:

for($i = 0; $i < count($plik); $i++)

W typowych sytuacjach obie konstrukcje zachowają się podobnie, lecz warto pamiętać o pewnym niuansie technicznym. Pierwsza z konstrukcji pobiera ilość elementów tablicy na samym początku. Jeżeli któryś cykl pętli doda jakiś element, nie zostanie on uwzględniony. W drugim przypadku ilość ta jest pobierana po każdym cyklu, zatem pętla dysponuje bieżącymi informacjami o wielkości tablicy i wszelka jej zmiana zostanie uwzględniona w ilości wykonanych cykli. Sposób ten jest jednak mniej wydajny od pierwszego.

Break i Continue

edytuj

Przy okazji omawiania instrukcji switch poznaliśmy komendę break. Ma ona bardzo duże zastosowanie przy pętlach, które potrafi przerywać. Istnieje także kolejne polecenie: continue. Przerywa ono jedynie aktualny cykl pętli i powoduje rozpoczęcie następnego.

Mamy prosty ciąg tekstowy:

Komenda; Komenda; Komenda; Komenda. To już pomijamy.

Wiemy o nim trzy rzeczy:

  1. Spacje ignorujemy
  2. Kropka oznacza koniec wprowadzania komend
  3. Średnik separuje komendy

Naszym zadaniem jest wprowadzenie komend do tablicy, aby można je było łatwiej przetwarzać. Skrypt ten będziemy pisać kawałek po kawałku. Na początek stwórzmy sobie parę zmiennych:

<?php 
$tekst = 'Komenda; Komenda; Komenda; Komenda. To już pomijamy.';
$tablica = array(0 => '');
$t = 0;

$tekst to tekst do przetworzenia. $tablica jest miejscem docelowym komend z "firmowo" utworzonym pierwszym pustym elementem. $t to licznik mówiący, do którego elementu tablicy wprowadzamy znaki.

Rozpoczynamy pętlę. Do pobrania długości ciągu użyjemy funkcji strlen(). $i to licznik położenia w ciągu tekstowym. Wskazuje na aktualnie przetwarzany znak:

for($i = 0; $i < strlen($tekst); $i++)
{

Implementujemy możliwość pierwszą. Spacje ignorujemy, dlatego przy ich napotkaniu przerywamy aktualny cykl pętli komendą continue i przechodzimy do następnego:

	if($tekst[$i] == ' ')
	{
 		continue;
 	}

Zauważ, jak odwołujemy się do określonego znaku wewnątrz ciągu: $tekst[$i]. Numer znaku (począwszy od zera) podajemy jako indeks w nawiasach kwadratowych, identycznie jak w tablicach.

Druga możliwość - po napotkaniu kropki przerwać pętlę wcześniej:

	if($tekst[$i] == '.')
 	{
 		break;
 	}

Przechodzimy do ewentualności trzeciej. Przy średniku należy przesunąć się na nowy element tablicy wynikowej i zainicjować go pustym ciągiem. Każdy inny znak wprowadzamy do aktualnego elementu tablicy:

	if($tekst[$i] == ';')
 	{
 		$t++;
 		$tablica[$t] = '';
 	}
 	else
 	{
 		$tablica[$t] .= $tekst[$i];
 	}

Teraz dopełnienie formalności, tj. zamknięcie pętli i wyświetlenie zawartości tablicy funkcją var_dump():

}
 
echo '<pre>';
var_dump($tablica);
echo '</pre>';

Zapytajmy się, jak przerwać pętlę, jeżeli jesteśmy aktualnie w instrukcji switch? Wywołanie break i continue będzie się przecież odnosiło do niej, a tego nie chcemy. Rozwiązaniem jest podanie po nich numeru określającego, której instrukcji wzwyż dotyczy wywołanie. Przepiszmy jeszcze raz powyższy kod z wykorzystaniem instrukcji wyboru (notabene nawet bardziej pasującej w tym przypadku):

<?php
$tekst = 'Komenda; Komenda; Komenda; Komenda. To już pomijamy.';
$tablica = array(0 => '');
$t = 0;
 
for($i = 0; $i < strlen($tekst); $i++)
{
	switch($tekst[$i])
	{
		case ' ':
			continue 2;
		case '.':
			break 2;
		case ';':
			$t++;
			$tablica[$t] = '';
			break;
		default:
			$tablica[$t] .= $tekst[$i];		
	}
}
 	
echo '<pre>';
var_dump($tablica);
echo '</pre>';

Przy stanach spacji oraz kropki wywołujemy komendy continue oraz break z parametrem 2, aby podkreślić, że dotyczą one pętli for, a nie instrukcji switch. break w kodzie obsługi średnika nie ma parametru, więc odnosi się do instrukcji switch.

Poprzedni rozdział: Instrukcja for
Następny rozdział: Instrukcja do while

Instrukcja while

edytuj

Kolejną pętlą jest while, będąca znacznie prostszą odmianą poznanej ostatnio pętli for. Wymagany jest tu jedynie warunek zakończenia, a pętla wykonuje się, dopóki jest on prawdziwy. Oto prosty przykład:

<?php
while(rand(0,10) != 8)
{
	echo 'Jeszcze nie wylosowałem!<br/>';
}

Pętla ta będzie wykonywała się, dopóki funkcja rand() nie wylosuje liczby 8. Jeżeli dostaniemy ją już w pierwszym sprawdzeniu, napis nie pojawi się w ogóle.

Ze względu na taką ogólną konstrukcję while przydaje się tam, gdzie musimy coś powtarzać do czasu osiągnięcia pewnego stanu. Sztandarowym przykładem jest czytanie pliku, gdzie takim specyficznym stanem, w którym musimy przerwać naszą pracę, jest jego koniec. Zastosowanie pętli while będzie tu o wiele lepsze, niż obliczanie na podstawie wielkości pliku, ile "segmentów" musimy pobrać i zabawa z licznikami.

<?php
$f = fopen('plik.txt', 'r'); // 1
 	
while(!feof($f)) // 2
{
	echo fgets($f, 16); // 3
}
 	
fclose($f); // 4

Opis działania:

  1. Otwieramy plik do odczytu. Uchwyt do niego zapisujemy w zmiennej $f. W ten sposób poznaliśmy nowy typ danych: Resource, czyli zasób.
  2. Dopóki nie osiągniemy końca pliku...
  3. Pobieraj z niego kolejne 16-znakowe bloki.
  4. Na koniec zamykamy połączenie z plikiem.

Pętlę while można przerobić na pętlę for bez większych trudności. Oto nowa wersja pierwszego przykładu z rozdziału poprzedniego:

<?php
$i = 0;
while($i < 10)
{
	echo $i.'<br/>';
	$i++;
}

Tu także można stosować komendy break oraz continue poznane w poprzednim rozdziale.

Pętla while przyda się nam, gdy zaczniemy omawiać komunikację z bazami danych. Zostanie tam wykorzystana do pobierania rekordów.

Poprzedni rozdział: Instrukcja while
Następny rozdział: Instrukcja foreach

Instrukcja do while

edytuj

W przeciwieństwie od normalnego while, tutaj warunek sprawdzany jest na końcu, tak więc pętla zostanie wykonana przynajmniej raz. Nie jest to często wykorzystywana właściwość, ale warto o niej pamiętać.

Składnia pętli do while jest dość specyficzna. Przed nawiasem klamrowym pojawia się jedynie słowo kluczowe do, a while z warunkiem znajduje się na samym końcu. Przedstawimy to na przykładzie takiego oto skryptu:

<?php
do
{
	echo "Podaj i: \n";
	fscanf(STDIN, "%d\n", $i); // 1
}
while($i < 10);

Nie będziemy uruchamiali go w przeglądarce, ale w linii komend. Powyższy skrypt pracuje w konsoli systemowej i może pobierać stamtąd dane poprzez wiersz zaznaczony jako 1 (nie przejmuj się, że nie rozumiesz jego budowy. Poznamy ją dalej). Aby uruchomić skrypt, uruchom konsolę i ustaw się poleceniem cd na katalogu, w którym zainstalowałeś PHP. Następnie wydaj następujące polecenie:

php -f /scieżka/do/skrypt.php

Jako wartość parametru -f musisz podać pełną ścieżkę do skryptu, który chcesz uruchomić.

Zauważ, że dzięki użyciu pętli do while, nie musimy umieszczać w skrypcie dwa razy kodu do pytania się o zmienną $i. Oto analogiczny kod z wykorzystaniem normalnego while:

<?php
echo "Podaj i: \n";
fscanf(STDIN, "%d\n", $i);

while($i < 10)
{
	echo "Podaj i: \n";
	fscanf(STDIN, "%d\n", $i);
}

Tutaj musimy powielić kod dwa razy, bo przecież przed sprawdzeniem warunku wypada się choć raz zapytać użytkownika, co należy sprawdzić. W poprzednim przykładzie mogliśmy do tego celu użyć kodu wewnątrz pętli, ponieważ mieliśmy zagwarantowane wykonanie jej kodu przynajmniej raz.

Poprzedni rozdział: Instrukcja do while
Następny rozdział: Funkcje

Instrukcja foreach

edytuj

Ostatnią pętlą jest foreach. Ma ona specyficzne zastosowanie, ponieważ służy wyłącznie do przeglądania zawartości typów złożonych: tablic oraz obiektów. Kod wewnątrz niej jest powtarzany dla każdego z elementów tablicy, a on sam jest na ten czas przenoszony do tworzonej przez pętlę zmiennej. Wróćmy do naszego przykładu z pętlą for odczytującego zawartość pliku. Przepiszemy go z wykorzystaniem foreach:

<?php
$plik = file("plik.txt");
 	
echo '<ul>';
foreach($plik as $linia)
{
	echo '<li>'.trim($linia).'</li>';	
}
echo '</ul>';

Teraz skrypt ma o wiele bardziej przejrzystą budowę. Przyjrzyjmy się deklaracji pętli:

foreach($plik as $linia)

Mówi nam ona, że pętla ma analizować tablicę $plik, a aktualnie przetwarzany element ma być zapisany w zmiennej $linia.

Foreach umożliwia nam także zwracanie nazw indeksów elementów:

<?php
$plik = file('plik.txt');
 	
echo '<ul>';
foreach($plik as $numer => $linia)
{
	echo '<li>Linia #'.$numer.': '.trim($linia).'</li>';	
}
echo '</ul>';

Foreach ma tę przewagę nad innymi pętlami, że wie, jakie elementy należą do tablicy i zawsze przetworzy tylko je. Gdybyśmy przed wyświetleniem pliku usunęli z niego np. linijkę 1, pętla for nie dałaby rady, próbując przetworzyć nieistniejący element. Nie robi oczywiście tego dla złośliwości, lecz dlatego, że operuje na liczniku i nie wie, do czego jest on przez nas dalej wykorzystywany.

<?php
$plik = file('plik.txt');
unset($plik[1]); // usuwamy linijkę o indeksie 1
echo '<ul>';
foreach($plik as $numer => $linia)
{
	echo '<li>Linia #'.$numer.': '.trim($linia).'</li>';	
}
echo '</ul>';

Tworzone przez foreach zmienne są jedynie kopiami oryginalnych wartości, dlatego próba ich modyfikacji wewnątrz pętli w żaden sposób nie wpłynie na zawartość tablicy:

<?php
$plik = file('plik.txt');
 
foreach($plik as $linia)
{
	$linia = 'Próba skasowania';
}
  	
echo '<pre>';
var_dump($plik);
echo '</pre>';

Wewnątrz pętli próbujemy przypisać wartość do zmiennej $linia. Owszem, udaje nam się to, ale nowa treść nie trafia w ogóle do tablicy i systemowe wyświetlenie jej zawartości ukazuje brak jakiejkolwiek reakcji. Czy zatem możliwe jest dokonywanie przypisań wewnątrz foreach? Oczywiście. Są dwie sztuczki. Pierwsza polega na wykorzystaniu zwracanego przez pętlę indeksu. Pousuwajmy z tablicy zbędne białe znaki:

<?php
$plik = file('plik.txt');
 
foreach($plik as $i => $linia)
{
	// jeszcze jakiś napis sobie doklejmy
	$plik[$i] = trim($linia).' [OK]';
}
  	
echo '<pre>';
var_dump($plik);
echo '</pre>';

Rozwiązanie to jest nieco trikowe, ale działa. Możemy jednak zastosować coś innego. PHP posiada pewien element zwany referencją. Ogólnie rzecz biorąc jest to odnośnik do zmiennej, który zachowuje się tak, jak ona. Modyfikacja referencji powoduje także modyfikację oryginalnego elementu. Począwszy od PHP 5, referencje można używać w pętli foreach. Wystarczy poprzedzić w jej deklaracji zmienną $linia znakiem &:

<?php
$plik = file('plik.txt');
 
foreach($plik as &$linia)
{
	$linia = trim($linia).' [OK]';
}
  	
echo '<pre>';
var_dump($plik);
echo '</pre>';

Teraz modyfikacja zmiennej $linia jest równoznaczna z modyfikacją aktualnego elementu w tablicy $plik, ponieważ zmienna jest takim właśnie odnośnikiem. O referencjach szerzej powiemy w rozdziale Inne elementy składni.

Poprzedni rozdział: Instrukcja foreach
Następny rozdział: Inne elementy składni

Funkcje

edytuj

Funkcje są pojęciem znanym z matematyki. Przeniesione na grunt informatyki, zachowują się podobnie i mają podobne zastosowanie: reprezentują jakieś przekształcenie, jakie można wykonać na danych. Dane wejściowe, czyli patrząc na definicję - element zbioru X - wymieniamy jako argumenty funkcji, a po jej wykonaniu otrzymujemy wynik, czyli element zbioru Y. W matematyce jedną z najbardziej znanych funkcji jest sinus, który dla każdej wartości kąta "produkuje" stosunek długości odpowiednich boków w trójkącie prostokątnym zawierających ten kąt. Sinus może być jak najbardziej poprawną funkcją w PHP (i rzeczywiście, język ten udostępnia programiście funkcję sin()), ale ponieważ programy wykonują dużo więcej różnorodnych operacji, możemy spotkać również bardziej praktyczne funkcje z punktu widzenia generowania stron WWW, np. strip_tags(), która z podanego tekstu usuwa znaczniki HTML. Oprócz tego, programista może samodzielnie tworzyć własne funkcje i omówienie tego zagadnienia jest głównym celem rozdziału, który właśnie czytasz.

W programowaniu będziemy przede wszystkim chcieli, aby zdefiniować sobie zestaw funkcji, w którym zamkniemy często wykonywane operacje. Może to być np. obsługa błędów - zamiast kopiować i wklejać odpowiedzialny za nią kod w różne miejsca skryptu, opakowujemy go w funkcję, do której niezbędne dane podajemy jako argumenty i po prostu wywołujemy ją. Dobry podział skryptu na funkcje ma jeszcze jedną zaletę - jeśli w jakimś kawałku kodu znajdziemy błąd, wystarczy poprawić go tylko w jednej funkcji, a zmiana będzie widoczna wszędzie.

Znajomość funkcji to jeden z fundamentów programowania, dlatego w tym rozdziale niezbędna jest szczególna uwaga.

Tworzenie własnych funkcji

edytuj

Każda funkcja musi posiadać pewną unikalną nazwę, która pozwoli odróżnić ją od innych. Musimy także określić, jakie argumenty przyjmuje i co właściwie robi. Odpowiada za to następująca konstrukcja:

function nazwaFunkcji(argumenty)
{
   // kod funkcji
}

Od tego miejsca możemy wywoływać naszą funkcję w identyczny sposób, jak te dostępne w PHP.

<?php
function formatujTekst($tekst)
{
   echo '<font color="red">'.strtoupper($tekst).'</font>';	
}
 	
formatujTekst('to jest tekst 1');
formatujTekst('to jest tekst 2');

Stworzyliśmy tutaj funkcję formatujTekst(), dzięki której ustalimy jednolite formatowanie dla tekstów prezentowanych na stronie. Pobiera ona jeden argument: $tekst. Zauważmy, że nazwę tę piszemy ze znakiem dolara. Gdybyśmy chcieli podać więcej argumentów, oddzielamy je od siebie przecinkami. Jeżeli funkcja nie będzie używać żadnego argumentu, za nazwą pozostawiamy puste nawiasy. Abyśmy mogli z argumentów skorzystać, muszą one mieć swoje nazwy, gdyż wewnątrz funkcji stają się zwykłymi zmiennymi.

Kod funkcji jest dowolnym poprawnym kodem PHP i można w nim umieścić dowolną rzecz, z tworzeniem kolejnej funkcji włącznie. Jednak zauważmy, że nasza pierwsza funkcja nie zwraca wartości. Zamiast tego wynik wysyła od razu na ekran i próba wykonania

$zmienna = formatujTekst('to jest tekst 1');

nic by nam nie dała. Aby zwrócić cokolwiek jako wynik, musimy skorzystać z komendy return:

<?php
function formatujTekst($text)
{
   return '<font color="red">'.strtoupper($text).'</font>';	
}
 	
echo formatujTekst('to jest tekst 1').'<br/>';
echo formatujTekst('to jest tekst 2').'<br/>';

Po return podajemy wyrażenie generujące wartość do zwrócenia.

Zwróćmy uwagę na fakt, iż przy deklarowaniu argumentów nie podajemy żądanego typu danych. O to musimy zadbać sami, umieszczając na początku funkcji odpowiednie instrukcje warunkowe i w razie kłopotów zgłosić błąd. Napiszmy sobie skrypt wyświetlający zawartość katalogu. Stworzymy w nim jedną funkcję zwracającą zawartość podanego katalogu jako tablicę. Druga funkcja będzie uniwersalnym wyświetlaczem tablic. Dlaczego tak - zaraz wyjaśnimy.

<?php
function wyswietlKatalog($sciezka, $tylkoPliki = 0) // 1
{
	$dir = opendir($sciezka); // 2
	$wynik = array();
	while($f = readdir($dir)) // 3
	{
		if(is_file($sciezka.$f)) // 4
		{
			$wynik[] = $f; // 5
		}
		elseif(is_dir($sciezka.$f) && $f != '.' && $f != '..' && !$tylkoPliki) // 6
		{
			$wynik[] = $f;
		}	
	}
	closedir($dir); // 7
	
	return $wynik; // 8
} // end wyswietlKatalog();
 
function pokazListe(array $lista) // 9
{
	echo '<ul>';
	foreach($lista as $element)
	{
		echo '<li>'.$element.'</li>';		
	}
	echo '</ul>';	
} // end pokazListe();
 
pokazListe(wyswietlKatalog('./katalog1/')); // 10
echo '<br/>';
pokazListe(wyswietlKatalog('./katalog2/', true)); // 11

Opis skryptu:

  1. Oto deklaracja funkcji wyświetlania katalogów. Znak równości oraz wartość po drugim argumencie oznacza, że jest on opcjonalny. Jeżeli go nie podamy przy wywołaniu, przyjmie on wartość domyślną. Opcjonalnych argumentów może być więcej, z tym że podajemy je zawsze na końcu. W naszej funkcji opcjonalny argument określa, czy funkcja ma zwracać wszystko (domyślny stan: 0), czy jedynie pliki (stan 1).
  2. Otwieramy katalog o podanej ścieżce
  3. Pętla pobierająca kolejne elementy katalogu, dopóki istnieją.
  4. Sprawdzamy, czy zwrócony element jest plikiem. Zauważ, że do zwróconej nazwy elementu musimy dokleić ścieżkę, ponieważ funkcja is_file() jest niezależna od opendir() i nie obchodzi jej, że w takim kontekście ją wywołujemy. Jeżeli rzeczywiście mamy plik, dodajemy go do tablicy wynikowej jako kolejny element.
  5. Niepodanie indeksu oznacza: "utwórz nowy element o indeksie MAX+1".
  6. Warunek sprawdzający, czy mamy do czynienia z katalogiem, jest dość skomplikowany. Użyliśmy tu operatorów && (logiczne "oraz"), aby zagwarantować, że wszystkie muszą być spełnione, aby dodać element do listy. Mamy tu kolejno: czy element jest katalogiem, czy nie ma on nazwy "." i ".." oraz czy funkcja ma od programisty zezwolenie na pobieranie katalogów.
  7. Zamykamy katalog
  8. Zwracamy tablicę jako wynik
  9. A oto mała niespodzianka. Począwszy od PHP 5 można definiować typy argumentów obiektowych, a od PHP 5.1 - tablic, które również są typem złożonym. Robimy to właśnie w taki sposób. Jeśli próbowalibyśmy wysłać tutaj np. liczbę, PHP zgłosiłby błąd.
  10. Wywołanie funkcji z jednym argumentem i przekierowanie wyniku do funkcji wyświetlającej listę.
  11. Ponowne wywołanie, lecz tym razem żądamy wyłącznie plików.

W ten sposób poznaliśmy już niemal wszystko, co dotyczy definiowania argumentów. Pozostała jeszcze jedna rzecz, a mianowicie pobieranie ich zupełnie nieokreślonej liczby. Uruchom taki oto skrypt:

<?php 
function funkcja($a)
{
	echo $a;
}
 
funkcja(1, 2, 3, 4, 5);

Nasza funkcja pobiera tylko jeden argument, lecz my podajemy mu pięć. Mogłoby się wydawać, że spowodujemy tym samym błąd, jednak tak się nie stanie. PHP nadmiarowych argumentów nie ignoruje. Choć nie zadeklarowaliśmy żadnego z nich podczas tworzenia funkcji, istnieje pewien sposób, aby je wydostać. Jest to funkcja func_get_args() zwracająca tablicę z wartościami wszystkich argumentów, które przekazaliśmy do funkcji.

function funkcja()
{
	$argumenty = func_get_args();
	echo '<ul>';
	foreach($argumenty as $id => $wartosc)
	{
		echo '<li>'.$id.' - '.$wartosc.'</li>';
	}
	echo '</ul>';
}
 
funkcja(1, 2, 3, 4, 5);

Istnieje także func_get_arg(numer) pobierająca wartość konkretnego argumentu. Obie te funkcje operują bezpośrednio na funkcji, dlatego PHP nakłada kilka ograniczeń na ich stosowanie. Najlepiej jest wywołać je na samym początku tworzonej funkcji, aby uniknąć kłopotów.

Widzialność zmiennych

edytuj

Napiszmy taki skrypt:

<?php
$zmienna = 'To jest zmienna';
 
function funkcja()
{
	echo $zmienna.'<br/>';
}
 
funkcja();
echo $zmienna.'<br/>';

Próbuje on wyświetlić dwa razy wartość tej samej zmiennej: z wnętrza funkcji oraz bezpośrednio w skrypcie. Po uruchomieniu okazuje się, że tylko bezpośrednie wyświetlenie podało nam prawidłowy wynik. echo wewnątrz funkcji nie pokazało żadnej wartości. Dlaczego? Zmienna przecież istnieje. I owszem, lecz tylko w tej części skryptu, w której została utworzona. PHP ma zaimplementowaną tzw. widzialność zmiennych - dla każdej funkcji tworzony jest osobny stos, niezależny od drugiego. Jeżeli więc utworzymy zmienną bezpośrednio w skrypcie, nie będzie ona istnieć w żadnej z naszych funkcji, gdyż te mają własne stosy. Ma to wyeliminować konflikty nazewnictwa.

Istnieje jednak sposób na powiedzenie PHP, że używana w funkcji zmienna jest już stworzona w stosie głównym skryptu. Po lekkiej modyfikacji skryptu otrzymujemy:

<?php
$zmienna = 'To jest zmienna';
 
function funkcja()
{
	global $zmienna;
	echo $zmienna.'<br/>';
}

funkcja();
echo $zmienna.'<br/>';

Słowo kluczowe global informuje PHP, że wymienione po nim zmienne mają zostać zaimportowane ze stosu głównego. Działa ono nawet wtedy, jeśli zmienna o danej nazwie nie istnieje, dlatego korzystanie z funkcji używających global musi być bardzo uważne.

Static

edytuj

Inną przydatną rzeczą jest przenoszenie niektórych zmiennych między wywołaniami tej samej funkcji. Dzięki temu nie musimy zapamiętywać ich wartości w globalnych tablicach, narażając się na konflikty nazewnictwa. Aby tego dokonać, wystarczy zadeklarować wybrane zmienne jako static, a ich wartość zostanie zapamiętana do następnego wywołania.

<?php
function koloruj()
{
   static $i = 0;
 		
   $i++;

   if($i % 2 == 0)
   {
      return '#ffffff';
   }
   return '#cccccc';	
} // end koloruj();
 	
echo '<table width="30%">';
for($x = 0; $x < 10; $x++)
{
   echo '<tr><td bgcolor="'.koloruj().'">'.$x.'</td></tr>';	
}
echo '</table>';

Powyższy przykład koloruje naprzemiennie wiersze w tablicy. Sztuczka ta nie wymaga finezji: po prostu zwiększamy licznik i sprawdzamy, czy dzieli się bez reszty przez dwa. Jeśli tak, wstawiamy jeden kolor, jeśli nie - drugi. Z pomocą instrukcji switch można rozszerzyć algorytm na więcej kolorów.

Tutaj kolor jest zwracany przez odpowiednią funkcję. Zapamiętuje ona sobie stan wewnętrznego iteratora $i między kolejnymi wywołaniami przy pomocy słowa kluczowego static. Gdybyś usunął tę linijkę, funkcja cały czas zwracałaby ten sam kolor, gdyż zmienna tworzona byłaby w kółko od nowa z domyślną wartością 0.

Rekurencja

edytuj

Rekurencja w programowaniu oznacza odwoływanie się funkcji do samej siebie. Jest użyteczna, w niektórych sytuacjach wręcz niezbędna, lecz pochłania znacznie więcej zasobów, dlatego należy korzystać z niej ostrożnie.

Za pomocą rekurencji możemy wyświetlić w PHP drzewo katalogów:

<?php
function wyswietlKatalog($sciezka)
{
	$dir = opendir($sciezka);
	echo '<ul>';
	while($f = readdir($dir))
	{
		if(is_dir($sciezka.$f) && $f != '.' && $f != '..')
		{
			echo '<li>'.$f;
			wyswietlKatalog($sciezka.$f.'/'); // 1
			echo '</li>';
		}
	}
	echo '</ul>';
	closedir($dir);
} // end wyswietlKatalog();
 
wyswietlKatalog('../../');

Funkcja wyswietlKatalog() w przypadku napotkania katalogu w aktualnie sprawdzanej ścieżce, wywołuje samą siebie (1), z doklejoną do dotychczasowej ścieżki nazwą tego katalogu. W ten sposób możemy pobrać całe drzewo, niemniej w przypadku rozbudowanych struktur może trwać to nawet kilkanaście sekund!

Sprawdźmy następujący skrypt:

<?php
function wypisz($tekst, $ile)
{
   echo $ile.': '.$tekst.'<br/>';
   if($ile > 0)
   {
      wypisz($tekst, $ile - 1);
   }
} // end wypisz();

wypisz('Witaj', 30);

Demonstruje on pewną właściwość rekurencji - możemy nią zastąpić pętle, odpalając naszą funkcję rekurencyjnie określoną liczbę razy. Jako licznik służy nam wartość argumentu $ile. Gdy jest ona większa od zera, funkcja wywołuje samą siebie, zmniejszając go o 1, aż dojdziemy do zera. Nie ma w tym nic dziwnego. Takie rozumienie funkcji jest podstawą tzw. programowania funkcyjnego charakterystycznego dla takich języków programowania, jak Ocaml czy Erlang. Zamiast pętli, tworzymy funkcje wywoływane rekurencyjnie.

Brzmi to interesująco, lecz w PHP natrafia na bardzo ważną przeszkodę. Zmieńmy powyższy kod tak, aby wypisał nasz tekst 200 razy i wykonajmy go. Niespodzianka! Po dojściu do mniej więcej połowy otrzymaliśmy błąd:

Fatal error: Maximum function nesting level of '100' reached, aborting!

Gdy parser wywołuje nową funkcję, musi zapamiętać gdzieś wartości wszystkich zmiennych oraz ogólnie cały stan dotychczasowej. Odkłada go na tzw. stos i po zakończeniu wewnętrznej funkcji, pobiera go stamtąd z powrotem. Stos ten ma jednak ograniczoną głębokość (w PHP wynoszącą 100), dlatego nie możemy w sposób zagnieżdżony wywoływać funkcji w nieskończoność. Doprowadziłoby to bowiem do szybkiego wyczerpania się pamięci.

Wiemy, że każdą pętlę da się zapisać w postaci rekurencyjnej, ale zależność ta działa też w drugą stronę. Każdą rekurencję da się zapisać przy pomocy zwykłych pętli oraz instrukcji warunkowych, choć w pewnych przypadkach może to być zadanie bardzo trudne. Oto prosta implementacja rekurencyjna funkcji silnia, która w matematyce zdefiniowana jest następująco:  

<?php
function silnia($n)
{
  if($n > 0)
  {
     return $n * silnia($n - 1);
  }
  return 1;
} // end silnia();

echo silnia(6);

Jej wersja iteracyjna, czyli zapisana przy pomocy pętli, jest w PHP dużo wydajniejsza:

<?php
function silnia($n)
{
   $wynik = 1;
   while($n > 0)
   {
      $wynik *= $n--;
   }
   return $wynik;
} // end silnia();

echo silnia(6);

Użyteczne funkcje

edytuj

PHP dysponuje kilkoma funkcjami do zarządzania funkcjami. Brzmi to może dość śmiesznie, lecz w praktyce bywa bardzo przydatne.

Na początek zastanówmy się, kiedy PHP sprawdza, że funkcja nie istnieje. Okazuje się, że nie dzieje się to w momencie kompilacji, lecz wykonywania skryptu. Ma to swoje uzasadnienie przy konstruowaniu modułowych skryptów (zajmiemy się nimi w następnym rozdziale). Pierwszy plik PHP odwołuje się do funkcji zdefiniowanych w drugim, lecz ten z kolei ładowany jest później. Gdyby z powodu nieistnienia jednej z nich skrypt byłby przerywany w momencie kompilacji, skrypt nie miałby żadnych szans na działanie. Ponadto nie dałoby się pracować ze skryptami korzystającymi z rozszerzeń, których na serwerze nie ma. Ma to sens, przecież instrukcją warunkową możemy zdefiniować alternatywny kod dla tych uboższych serwerów.

PHP ułatwia nam zadanie jeszcze bardziej. Za pomocą function_exists() możemy sprawdzić, czy podana przez nas funkcja istnieje. Narzędziem tym można sondować zarówno nasze własne, jak i definiowane przez rozszerzenia funkcje. W poniższym przykładzie wykorzystamy to do sprawdzenia, czy serwer posiada obsługę protokołu IMAP:

<?php
if(function_exists('imap_open'))
{
   echo 'IMAP dostępny';
}
else
{
   echo 'IMAP niedostępny';
}

Innym sposobem sprawdzenia, czy rozszerzenie jest załadowane, jest skorzystanie z funkcji extension_loaded(), która ma tę przewagę, że działa także z rozszerzeniami obiektowymi, w których zwykłych funkcji nie ma:

<?php
if(extension_loaded('imap'))
{
   echo 'IMAP dostępny';
}
else
{
   echo 'IMAP niedostępny';
}

Jak dobrze korzystać z funkcji?

edytuj

Funkcje to jedno z podstawowych narzędzi programisty. Omówiliśmy techniczne aspekty ich działania w języku PHP, lecz nie poruszaliśmy dotąd tematu, jak je tworzyć, aby faktycznie były dla nas użyteczne. Istnieje kilka zasad, których przestrzeganie daje nam pewność, że nie natkniemy się gdzieś na problemy z utrzymaniem projektu.

  1. Funkcje powinny realizować jedno, konkretne zadanie. Unikamy tworzenia funkcji w stylu mydło i powidło. Jeśli nie wiemy bądź nie rozumiemy, co dana funkcja tak naprawdę pozwoli nam osiągnąć, przerywamy pisanie kodu i wracamy nad kartkę papieru. Oczywiście nic nie stoi na przeszkodzie, by zadanie było bardzo złożone (np. obsługa formatowania BBCode); dopóki jest to jedno zadanie, jesteśmy w domu.
  2. Jeśli kod danej funkcji staje się bardzo długi, możemy rozważyć jej rozbicie na podproblemy, które zapiszemy w mniejszych funkcjach. Powinniśmy to zrobić zwłaszcza wtedy, gdy dany problem pojawia się wielokrotnie w różnych postaciach. Próbujemy wtedy wyciągnąć wspólny mianownik, a różnice obsłużyć poprzez konfigurację argumentami.
  3. Funkcje nie powinny być zbyt długie.

Oczywiście zalecenia te stanowią punkt odniesienia, a nie prawo, za którego nieprzestrzeganie czeka nas lincz. Wielu początkujących programistów zadaje pytania, kiedy pisać tak, a kiedy inaczej. Odpowiedź jest bardzo prosta: nie ma jednej, uniwersalnej reguły, która mówi, że w przypadku danej funkcji mamy ją rozbić, a w przypadku innej - nie (zauważmy, że reguła taka musiałaby być albo bardzo błyskotliwa, albo obejmować nieskończoną liczbę przypadków). Kluczem jest zwyczajne myślenie. Dobry programista myśli podczas pisania kodu i każdy jego krok ma uzasadnienie. Punkt odniesienia jest pomocą, wokół którego się obraca, ale jeśli widać, że rozbijanie jakiegoś skomplikowanego i długiego algorytmu, który stanowi jedną całość, będzie niepotrzebną komplikacją, nikt poważny nie będzie tego robił. Oczywiście zdarzają się pomyłki; nie wszystkie uzasadnienia okazują się poprawne, ale duże projekty ulepszane są cały czas. Jeśli coś się nie sprawdziło, jest po prostu przerabiane. Sztuka polega na tym, że jeśli już się pomylimy, pomyłka nie powinna być poważna.

Aby zrozumieć inny aspekt tworzenia dobrej funkcji, porozmawiajmy nieco o tym, co one robią. Być może niektórzy uważni czytelnicy już zauważyli, że w dotychczasowych przykładach pojawiały się zarówno funkcje, które np. wypisywały coś na ekran, oraz takie, które wyłącznie obrabiały zewnętrzne argumenty i zwracały wynik. Okazuje się, że taki podział ma bardzo duże znaczenie praktyczne i teoretyczne, zwłaszcza przy dowodzeniu poprawności programu. Każda operacja języka programowania może mieć tzw. skutki uboczne. Jest to dowolny efekt jej działania, który zmienia stan programu. Przykładowo operacja $a + $b nie ma skutków ubocznych. Możemy ją wywołać 1000 razy, ale dopóki nie zaczniemy czegoś robić z wynikiem, nie zmieni to ani o jotę działania programu. Z drugiej strony operacja $a = 5 ma już skutek uboczny - od tego momentu zmienna $a ma już inną wartość, co potencjalnie może wpłynąć na działanie dalszej części kodu. Pomimo swojej nazwy, skutki uboczne często są właśnie spodziewanymi rezultatami jakiejś operacji. Nie należy ich rozumieć dosłownie, lecz właśnie jako taką zmianę stanu programu, która może wpłynąć na dalszy kod.

Także i funkcje możemy sklasyfikować względem tego czy mają one jakieś skutki uboczne czy nie. Spoglądając na napisany wyżej przykład funkcji silnia() łatwo stwierdzimy, że nie ma ona żadnych skutków ubocznych, ponieważ jedyne, co robi, to przetwarza podany jej argument i zwraca wynik, nie zajmując się tym, jak będzie on wykorzystany. Poza tym nie wypisujemy w niej nic na ekran, ani nie korzystamy z żadnych innych zewnętrznych źródeł danych. Przykład pobierający zawartość katalogu ma już skutek uboczny; w wyniku jej wykonania do przeglądarki wysyłany jest tekst z listą katalogów. Wywołując ją dwukrotnie, użytkownik dostanie taką listę dwa razy.

Jeśli chcemy napisać dobrą funkcję, po prostu musimy znać wszystkie jej skutki uboczne i wiedzieć, czy faktycznie są one dla nas pożądane czy nie. W tym drugim przypadku powinniśmy funkcję przepisać tak, aby ich nie zawierała. Rozpatrzmy funkcję dodającą jakiś kod HTML do podanego tekstu:

<?php
function kolorujTekst($tekst)
{
   echo '<font color="red">'.$tekst.'</font>';	
}

Jej skutkiem ubocznym jest wypisanie tekstu bezpośrednio na ekran. Oprócz tego mamy też drugą funkcję:

<?php
function pogrubTekst($tekst)
{
   echo '<strong>'.$tekst.'</strong>';	
}

Jeśli będziemy chcieli tworzyć złożenie w stylu kolorujTekst(pogrubTekst('tekst')), które wyświetli pogrubiony i pokolorowany tekst, taki skutek uboczny jest nie do przyjęcia. Zamiast echo powinniśmy użyć return tak, aby funkcja zwracała wynik, dzięki czemu jej użytkownik może zdecydować w miejscu wywołania, co tak naprawdę chce z nim zrobić. Przecież takiego kodu HTML nie musimy wcale wysyłać do przeglądarki, lecz np. zapisać do pliku. Nietrudno zauważyć, że dopiero wyeliminowanie skutku ubocznego zwiększyło nasze możliwości wykorzystania naszych funkcji do tego celu.

Poprzedni rozdział: Funkcje
Następny rozdział: Każdy popełnia błędy

Inne elementy składni

edytuj

Include i require

edytuj

Tworzenie dynamicznych stron byłoby bardzo kłopotliwe, gdybyśmy musieli pracowicie kopiować wszystkie utworzone przez nas funkcje do każdego pliku PHP z osobna. Na szczęście PHP udostępnia mechanizmy na dołączanie jednego skryptu do drugiego. Służą do tego instrukcje include, require, include_once oraz require_once.

Rozpatrzmy taką sytuację: mamy dwa pliki z funkcjami definiującymi wygląd treści: normalny.php oraz opcjonalny.php. Stworzone w nich są identyczne funkcje różniące się jedynie zawartością. We właściwym skrypcie dołączamy jeden z tych plików decydując o tym, w jakim stylu zaprezentowane zostaną dane na stronie. Oto kod dwóch dołączanych plików.

normalny.php:

<?php
function pokazTytul($tytul)
{
	echo '<h1>'.$tytul.'</h1>';	
} // end pokazTytul();

function pokazParagraf($tekst)
{
	echo '<p>'.$tekst.'</p>';
} // end pokazParagraf();

function pokazListe(array $tablica)
{
	echo '<ul>';
	foreach($tablica as $element)
	{
		echo '<li>'.$element.'</li>';
	}
	echo '</ul>';
} // end pokazListe();

opcjonalny.php:

<?php
function pokazTytul($tytul)
{
	echo '<h1>'.$tytul.'</h1>';	
} // end pokazTytul();

function pokazParagraf($tekst)
{
	echo '<p style="font-weight:bold;">'.$tekst.'</p>';
} // end pokazParagraf();
	
function pokazListe(array $tablica)
{
	echo '<ol>';
	foreach($tablica as $element)
	{
		echo '<li>'.$element.'</li>';
	}
	echo '</ol>';
} // end pokazListe();

Jeszcze raz zwracamy uwagę, że oba pliki tworzą funkcje o takich samych nazwach, dlatego naraz może być załadowany tylko jeden z nich. Oto plik index.php, który na podstawie tego czy ustawiony jest parametr "styl", decyduje, który z powyższych skryptów zostanie załadowany:

<?php
if(!isset($_GET['styl']))
{
	require('./normalny.php');
}
else
{
	require('./opcjonalny.php');
}
 	
pokazTytul('Tytuł');
pokazParagraf('To jest paragraf');

pokazListe(array(0 =>
	'To',
	'Jest',
	'Lista'	
));

Choć require wywołuje się identycznie, jak funkcję, funkcją nie jest, dlatego zapis np. $zmienna = require('skrypt.php'); jest nieprawidłowy. Różnica między nim, a include polega na sposobie obsługi błędów. Instrukcja require generuje komunikat Fatal error zatrzymujący skrypt, druga tylko ostrzeżenie. Istnieją także include_once oraz require_once, które są ignorowane, jeśli próbujemy po raz drugi dołączyć ten sam plik. Pozwala to uniknąć omyłkowego, kilkukrotnego ładowania tych samych funkcji, co oczywiście prowadziłoby do błędu. Powinniśmy używać ich tylko wtedy, kiedy liczymy się z taką możliwością.

Budowanie kompletnej strony z mniejszych plików jest bardzo pożyteczne. Generalnie nie zaleca się pisania wszystkiego ciurkiem bez podziału na funkcje, mniejsze moduły itd., gdyż zmniejsza to odporność skryptu na błędy, wprowadza chaos i utrudnia dodawanie/modyfikowanie nowych opcji. Przyjrzyjmy się, jak zatem zorganizować naszą witrynę. Przede wszystkim zróbmy sobie jeden katalog na wszystkie pliki z funkcjami. Może on się nazywać np. includes. Umieszczamy w nim funkcje ułatwiające komunikację z bazą danych, przetwarzanie tekstu, autoryzację i wykonujące wszystkie inne rutynowe operacje. Dodatkowo tworzymy katalog actions, w którym będzie zawarty kod różnych podstron takich, jak rejestracja czy lista artykułów. Oba katalogi powinny być umieszczone poza katalogiem publicznym dostępnym z przeglądarki. Tam umieścimy tylko jeden plik, index.php, który będzie zarządzać uruchamianiem poszczególnych akcji. Oto i on:

<?php
require('../includes/config.php');
require('../includes/dispatch.php');
require('../includes/session.php');
require('../includes/authorize.php');
require('../includes/templates.php');
require('../includes/functions.php');
require('../includes/layout.php');

initSystem();
dispatchAction(isset($_GET['act']) ? $_GET['act'] : 'index');

Funkcję dispatchAction() umieszczamy w jednym z plików w katalogu /includes jako funkcję silnika naszej strony (np. w dispatch.php). Jej zadaniem jest odpalenie wybranej akcji, a może ona wyglądać następująco:

<?php
function dispatchAction($action)
{
   if(!ctype_alpha($action))
   {
      displayError('Nazwa akcji zawiera nieprawidłowe znaki.');
   }
   if(!file_exists('../actions/'.$action.'.php'))
   {
      displayError('Podana akcja nie istnieje.');
   }
   require_once('../actions/'.$action.'.php');
   $action .= 'Action';

   $action();
} // end dispatchAction();

Nazwa akcji jest tłumaczona na ścieżkę do pliku w katalogu /actions. Wklejając jakiekolwiek dane zewnętrzne do ścieżek używanych przez system musimy zwracać szczególną uwagę na bezpieczeństwo. Gdyby jakiś wścibski użytkownik wywołał naszą stronę jako index.php?act=../includes/dispatch, albo jeszcze jakiś inny plik, moglibyśmy mieć poważne problemy. Dlatego najpierw sprawdzamy funkcją ctype_alpha() czy podana nazwa akcji składa się wyłącznie z liter. Teraz mamy pewność, że działalność użytkownika będzie ograniczona wyłącznie do plików PHP w katalogu /actions. Oczywiście musimy jeszcze funkcją file_exists() upewnić się, że odpowiedni plik istnieje i dopiero wtedy możemy go bezpiecznie załadować.

Zwróćmy uwagę, co dzieje się później. Zakładamy, że plik z akcją będzie zawierać funkcję o nazwie nazwaakcjiAction(). Dlatego do nazwy akcji doklejamy słowo Action i wywołujemy funkcję, wczytując jej nazwę ze zmiennej (podświetlona linia 14). Jest to jedna z ciekawych właściwości PHP i warto o niej pamiętać, aczkolwiek w przypadku przyjmowania danych z zewnątrz także zwracamy uwagę na bezpieczeństwo. W naszym przypadku załatwiają je już mechanizmy przeznaczone dla require_once.

W ramach ćwiczenia spróbuj uzupełnić czymś pozostałe pliki i napisać akcję index, która wyświetli jakiś tekst powitalny.

Stałe

edytuj

Spójrzmy raz jeszcze na przykład z plikiem index.php. Jak nietrudno się domyślić, większe projekty składają się z pewnej liczby katalogów. Pojawia się tu problem, skąd skrypt ma wiedzieć, gdzie leżą potrzebne mu pliki? Teoretycznie możemy ścieżki wpisywać ręcznie przy każdej konieczności, lecz jest to bardzo niewygodne zwłaszcza, gdy trzeba je będzie z jakiegoś powodu zmienić.

Zdefiniujmy zatem wszystkie używane ścieżki w pliku index.php za pomocą zmiennych. Już na pierwszy rzut oka widać, iż rozwiązanie to nie jest najlepsze, bowiem każdą zmienną ze ścieżką musimy przenosić do funkcji z użyciem global, a ponadto istnieje ryzyko, że w którymś miejscu omyłkowo ją nadpiszemy. Remedium na te kłopoty są stałe. Są to aliasy na pewne wartości, których po utworzeniu nie można już modyfikować. Najogólniej wykorzystuje się je dla często powtarzających się w skrypcie wartości. Przyjrzyjmy się poniższemu skryptowi:

<?php
define('DIR_GLOWNY', './');
define('DIR_SILNIK', './includes/');
define('DIR_ZDJECIA', './zdjecia/');
 	
require(DIR_SILNIK.'autoryzacja.php');
require(DIR_SILNIK.'funkcje.php');

Do zdefiniowania stałych używamy funkcji define(), w której definiujemy nazwę stałej oraz jej wartość. Zwyczajowo stałe mają nazwy złożone z samych dużych liter, a wartościami mogą być wyłącznie typy skalarne (czyli nie tablice, nie obiekty oraz nie zasoby). Podczas wywoływania stałych nie poprzedzamy znakiem dolara.

Oto wszystkie cechy stałych:

  1. Stałe nie mają znaku dolara ($) przed nazwą
  2. Stałe mogą być definiowane oraz używane wszędzie bez zważania na zasady dotyczące zakresu ich dostępności
  3. Stałe nie mogą być ponownie definiowane lub "oddefiniowane" po tym jak raz zostały zdefiniowane
  4. Stałe mogą zawierać tylko wartości skalarne

Oprócz przechowywania ścieżek do katalogów, stałe mają zastosowanie podczas pisania bibliotek programistycznych. Często zdarza się, że do funkcji musimy przekazać jakąś wartość liczbową identyfikującą konkretny stan. Ponieważ spamiętywanie cyferek jest uciążliwe, tworzy się dla nich stałe o bardziej czytelnych nazwach. Możemy to pokazać na podstawie funkcji error_reporting() pozwalającej ustawić poziom raportowania błędów przez PHP. Da się ją wywoływać w ten sposób:

error_reporting(1 | 2 | 4 | 8);

Jednak w takiej postaci chyba żaden programista nie potrafiłby odczytać, jaki właściwie poziom raportowania błędów został ustawiony. Zamiast cyfr, można użyć odpowiadające im stałe zdefiniowane przez PHP:

error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE);

Kolejnym ciekawym zagadnieniem, którego realizację ułatwiają stałe, jest przekazywanie parametrów do funkcji w sposób pokazany powyżej. Rozpatrzmy przypadek systemu raportowania stanu pojazdu. Stworzyliśmy sobie funkcję raportującą pobierającą pięć wartości logicznych (prawda-fałsz) opisujących, które elementy są sprawne, a które nie. Nasz skrypt wygląda tak:

<?php
// Raportowanie stanu pojazdu
function stan($silnikOK, $kolaOK, $swiatlaOK, $skrzyniaOK, $paliwoOK)
{
	if($silnikOK)
	{
		echo 'Silnik jest sprawny<br/>';		
	}
	if($kolaOK)
	{
		echo 'Koła są sprawne<br/>';		
	}
	if($swiatlaOK)
	{
		echo 'Światła są sprawne<br/>';		
	}
	if($skrzyniaOK)
	{
		echo 'Skrzynia jest sprawna<br/>';		
	}
	if($paliwoOK)
	{
		echo 'Paliwo jest w baku<br/>';		
	}
} // end stan();
 	
stan(true, false, false, true, true);

Znów mamy identyczny problem: wywołując funkcję gdzieś w skrypcie musimy pamiętać, jaka jest kolejność parametrów, a postronna osoba już w ogóle nie zrozumie, co oznacza która wartość logiczna. Wykorzystajmy więc fakt, iż komputer zapisuje wszystko w postaci zerojedynkowej i prześlijmy wszystkie parametry jako jedną liczbę o długości pięciu bitów (od 0 do 32). W stałych zdefiniujemy nazwy poszczególnych elementów przypisując im kolejne potęgi dwójki, czyli kolejne bity liczby. Następnie za pomocą operatora alternatywy bitowej zbudujemy z nich odpowiednią kombinację:

<?php
define('SILNIK', 1);
define('KOLA', 2);
define('SWIATLA', 4);
define('SKRZYNIA', 8);
define('PALIWO', 16);
 
// Raportowanie stanu pojazdu
function stan($stan)
{
	if($stan & SILNIK)
	{
		echo 'Silnik jest sprawny<br/>';		
	}
	if($stan & KOLA)
	{
		echo 'Koła są sprawne<br/>';		
	}
	if($stan & SWIATLA)
	{
		echo 'Światła są sprawne<br/>';		
	}
	if($stan & SKRZYNIA)
	{
		echo 'Skrzynia jest sprawna<br/>';		
	}
	if($stan & PALIWO)
	{
		echo 'Paliwo jest w baku<br/>';		
	}
} // end stan();

stan(SILNIK | SKRZYNIA | PALIWO);

Operatorem koniunkcji bitowej możemy sprawdzić, czy dany element jest sprawny. Cały ten sposób jest określany mianem ustawiania flag (każda stała to jedna flaga) i jest powszechnie wykorzystywany w programowaniu ze względu na wydajność. Efektywniej jest przesłać pięć wartości jednym parametrem, niż tyle samo pięcioma. Przyjrzyjmy się zatem tajemnicy tego sposobu. Najpierw - jak reprezentowana jest każda flaga w postaci binarnej:

SILNIK (1):   00001
KOLA (2):     00010
SWIATLA (4):  00100
SKRZYNIA (8): 01000
PALIWO (16):  10000

W każdej liczbie będącej potęgą dwójki "zapalony" jest tylko jeden bit - z tej własności korzystamy. Kiedy składamy kilka potęg dwójki operatorem alternatywy bitowej, zapalamy tym samym poszczególne bity:

SILNIK | SKRZYNIA | PALIWO:

SILNIK (1):   00001
SKRZYNIA (8): 01000
PALIWO (16):  10000
-------------------
REZULTAT:     11001

Jeżeli w danej kolumnie którakolwiek z wartości "zapala" bit, będzie on zapalony także w rezultacie. W operatorze koniunkcji bitowej używanym do testowania, dany bit rezultatu jest zapalany jedynie w wypadku jego obecności naraz w obu podanych liczbach:

$rezultat & SILNIK 
REZULTAT:     11001
SILNIK (1):   00001
-------------------
              00001

Otrzymujemy liczbę większą od zera, czyli prawdę logiczną. Pytając się o flagę SWIATLA, zostanie wykonane takie działanie:

$rezultat & SWIATLA 
REZULTAT:     11001
SWIATLA (4):  00100
-------------------
              00000

Teraz żadna z jedynek nie powtarza się w obu wierszach naraz, więc działanie da nam wynik 0, czyli fałsz. To jest cała tajemnica tego sposobu.

Ścieżki oraz include_path

edytuj

Zastanówmy się, skąd PHP wie gdzie szukać naszych plików. Podczas uruchamiania skryptu PHP ustawia mu katalog roboczy na folder, w którym znajduje się wykonywany plik i według niego domyślnie rozpoznaje wszystkie ścieżki względne, czyli takie, które zaczynają się od pojedynczej kropki (katalog bieżący) lub podwójnej (katalog nadrzędny). Załadowanie kolejnego skryptu poprzez require nie ma wpływu na tę ścieżkę! Wracając do naszego przykładu z silnikiem strony oznacza to, że gdybyśmy chcieli w pliku ../includes/dispatch.php załadować jakiś inny plik z tego samego katalogu, powinniśmy podać ścieżkę ../includes/innyPlik.php (względem index.php) zamiast ./innyPlik.php. Jest to jeden z powodów, dla którego nie jest zalecane tworzenie akcji bezpośrednio w katalogu publicznym, np. rejestracja.php, logowanie.php, lecz stworzenie pojedynczego punktu wejścia i wybór akcji poprzez odpowiedni algorytm napisany w PHP. Oprócz tego, ma to również pewien wpływ na bezpieczeństwo i niezawodność, a także umożliwia łatwe zastosowanie mechanizmu mod_rewrite oferowanego np. przez serwer Apache do stworzenia przyjaznych adresów URL.

Pozostaje wciąż pytanie, co się dzieje, jeśli ścieżka nie zaczyna się od pojedynczej lub podwójnej kropki. Gdy jest to ścieżka bezwzględna, czyli zaczynająca się od ukośnika w systemach uniksowych, a od litery dysku w systemach Windows, PHP od razu ładuje plik z podanej lokalizacji i nie wykonuje żadnych innych przeszukań. Ostatnia możliwość to podana od razu nazwa pliku lub katalogu:

require('plik.php');

PHP wykorzystuje wtedy mechanizm include_path. Jest to zwyczajna lista ścieżek oddzielonych średnikami, pod którymi należy szukać danego pliku. PHP sprawdza je po kolei, dopóki nie trafi. Jej domyślna wartość jest zdefiniowana w pliku php.ini i domyślnie będzie mieć wartość w stylu:

.;/sciezka/do/katalogu/publicznego;/sciezka/do/PEAR

W ten sposób PHP na początku sprawdzi zawsze katalog bieżący, później katalog publiczny, a później repozytorium klas PEAR, którym nie będziemy się teraz zajmować. Listę ścieżek można modyfikować z poziomu skryptu PHP przy pomocy funkcji set_include_path() oraz get_include_path(). Utwórz dwa pliki o nazwie wczytany.php. Jeden z nich umieść w katalogu publicznym, drugi w katalogu nadrzędnym. Ich kod niech będzie następujący:

<?php
echo __FILE__;

__FILE__ jest specjalną stałą, która zawsze zawiera ścieżkę bezwzględną pliku, w którym się ona znajduje. Dzięki niej będziemy wiedzieć, który z plików się faktycznie załadował. Dodajmy także plik index.php, który będzie ładować jeden z nich:

<?php
require('wczytany.php');

Uruchommy ten skrypt i zobaczmy, jaka ścieżka została wypisana na ekranie. Powinien zostać załadowany plik leżący w katalogu publicznym, ponieważ include_path nakazuje w pierwszej kolejności przeszukać właśnie jego. Teraz zmodyfikujmy include_path:

<?php
set_include_path('..'.PATH_SEPARATOR.'.');
require('wczytany.php');

Tym razem załadowany został plik z katalogu nadrzędnego, ponieważ po modyfikacji PHP w pierwszej kolejności szuka właśnie tam. Usuńmy teraz nadrzędny wczytany.php. PHP załadował plik z katalogu publicznego, gdyż w nadrzędnym nie znalazł tego, czego szukał. Ten eksperyment powinien pokazać istotę działania include_path. Użyty w kodzie identyfikator PATH_SEPARATOR to kolejna stała zawierająca aktualny separator ścieżek.

Oczywiście pojawia się pytanie, który sposób jest bardziej wydajny. Include_path generuje dodatkowy narzut wraz ze wzrostem liczby ścieżek, które musi sprawdzać, dlatego gdy znamy strukturę naszej strony, lepiej posługiwać się ścieżkami bezwzględnymi bądź względnymi:

<?php
// wczytuj zawsze z bieżącego katalogu
require('./wczytany.php');

Include_path przydaje się w sytuacjach, gdy potrzebna nam większa elastyczność lub chcemy skorzystać z bibliotek PHP udostępnianych na serwerze dla ogółu (np. wspomniane już repozytorium PEAR, które zazwyczaj umieszczane jest w katalogu instalacyjnym PHP). Dzięki temu nasz skrypt nie musi wiedzieć, gdzie fizycznie znajdują się odpowiednie pliki na serwerze, lecz po prostu je wczytuje, a interpreter zajmuje się resztą.

Będąc przy temacie ścieżek warto zwrócić uwagę na różnice między systemami operacyjnymi. PHP jest językiem niezależnym od platformy, dlatego skrypty napisane pod Windowsem powinny działać także na serwerze z zainstalowanym Linuksem, ale gdy zaczynamy bawić się plikami, mogą wystąpić problemy. Poniżej zostały zebrane wszystkie najważniejsze różnice, o których trzeba pamiętać, pisząc własne skrypty. Powinny zapamiętać je w szczególności osoby pracujące pod systemem Windows, który bardzo rzadko instaluje się na serwerach WWW.

Własność Windows Linux i inne
Wielkość liter w plikach Nie ma znaczenia Ma znaczenie
Separator katalogów \ /
Korzeń systemu plików Litera dysku /
Podział na dyski Widoczny dla użytkownika Niewidoczny dla użytkownika
Mechanizm uprawnień Tylko po skonfigurowaniu Zawsze

Najwięcej problemów generuje sprawa wielkości liter. Jeśli widzisz, że skrypt działający pod Windowsem nie może znaleźć plików na serwerze Linuksowym, w pierwszej kolejności sprawdź czy jeśli np. zaczynasz w wywołaniach nazwy plików dużą literą, faktycznie tak się zaczynają na dysku. Warto korzystać także z separatora katalogów w stylu uniksowym, czyli ukośnika w prawo. Jest on rozpoznawany także przez Windows, natomiast ukośnik w lewo przez systemy uniksowe - nie. PHP przechowuje odpowiedni ukośnik charakterystyczny dla systemu, na którym pracuje, w stałej DIRECTORY_SEPARATOR.

Referencje

edytuj

Referencja to swego rodzaju alias na zmienną, dzięki czemu może ona występować pod dwoma nazwami. Popatrzmy na taki przykład:

<?php 
$a = 5;
$b = &$a; // tworzymy referencje
 
echo 'B: '.$b.'; A: '.$a.'<br/>';
$b = 6;
echo 'B: '.$b.'; A: '.$a.'<br/>';
$a = 7;
echo 'B: '.$b.'; A: '.$a.'<br/>';

Referencję można utworzyć, stawiając przed zmienną źródłową znak &. Po uruchomieniu przykładu zobaczymy, że modyfikując jedną z tych zmiennych, zmieniała się także druga. Jest tak dlatego, że obie te zmienne reprezentują w rzeczywistości tę samą wartość.

Referencje są bardzo użyteczne przy pracy z dużymi zbiorami danych. Przeprowadzając operację:

$b = &$a;

nie wykonujemy żadnego żmudnego kopiowania tych danych, lecz tworzymy do nich kolejny odnośnik. Modyfikując jedną zmienną, zmiana automatycznie będzie widoczna w drugiej.

Referencje w połączeniu z innymi strukturami nadają im pewne dodatkowe właściwości. Przypomnij sobie pętlę foreach. Wspominaliśmy tam, iż dodatkowe zmienne reprezentujące indeks oraz wartość elementu tablicy są kopiami, dlatego nie można ich jawnie stosować do modyfikowania przetwarzanej tablicy. W PHP 5 ten problem zniknął, ponieważ możemy poinformować PHP, że zmienna wartości ma być referencją. Dzięki temu możliwe będzie modyfikowanie przez nią zawartości tablicy:

<?php
$tablica = array(0 => 1, 2, 3, 4, 5, 6);
   
foreach($tablica as $id => &$element)
{
   $element = rand(1, 6);  
}
  
var_dump($tablica);

W tym przykładzie wypełniamy tablicę losowymi numerami poprzez zmienną $element tworzoną przez pętlę. Jest to możliwe, gdyż zadeklarowaliśmy znakiem &, iż ma to być referencja do właściwego elementu. Gdyby usunąć ten znak, funkcja var_dump() pokazałaby nam dalej pierwotną zawartość tablicy. Tak się jednak nie dzieje, zatem sposób okazuje się skuteczny.

Dzięki referencjom funkcje mogą pozornie zwracać więcej wartości - poprzez referencyjne parametry. Napiszemy sobie teraz funkcję do kolorowania tekstów dłuższych, niż 5 znaków. W zależności od długości funkcja zwraca wartość 1 lub 0. Zmodyfikowany tekst jest oddawany tą samą drogą, którą się dostał, tj. referencyjnym parametrem. W celu przetestowania przetworzymy sobie dwa ciągi. Jeden liczy sobie trzy znaki, drugi jest wyraźnie dłuższy.

<?php
function przetworz(&$tekst)
{
   if(strlen($tekst) > 5)
   {
      $tekst = '<font color="red">'.$tekst.'</font>';
      return 1;    
   }
   return 0; 
} // end przetworz();
   
// Probujemy przerobic krotki tekst
$tekst = 'Jan';
   
if(przetworz($tekst))
{
   echo 'Przetworzony tekst: '.$tekst.'<br/>';
}
else
{
   echo 'Błąd: za krótki tekst! '.$tekst.'<br/>';
}
   
// A teraz dlugi
$tekst = 'Ala ma kota';
 
if(przetworz($tekst))
{
   echo 'Przetworzony tekst: '.$tekst.'<br/>';
}
else
{
   echo 'Błąd: za krótki tekst! '.$tekst.'<br/>';
}

Sposób ten także działa, ponieważ w drugim (dobrym) przypadku po wywołaniu funkcji przetworz($tekst) w zmiennej znalazła się zmodyfikowana postać.

Przekazywanie parametrów poprzez referencje ma pewne ograniczenia. Nie można do takiego parametru podać wartości stałej, ponieważ referencja musi operować na zmiennych.

<?php
function funkcja(&$a)
{
   // treść
}

// proba 1
funkcja('test');
   
// proba 2
$zmienna = 'test';
funkcja($zmienna);

Pierwszy zapis spowoduje błąd, gdyż nie przekazaliśmy wartości w postaci zmiennej.

W naszych skryptach powinniśmy unikać stosowania referencji. Prawidłowe ich zastosowanie to sytuacja, w której potrzebny jest nam alias na zmienną, czyli de facto kilka zmiennych prowadzących do tej samej wartości. Bardzo złą praktyką jest wykorzystywanie referencji do optymalizacji skryptu. Nie wykonujemy wtedy kopiowania, ale PHP implementuje specjalny algorytm, który opóźnia wykonanie kopii do momentu, gdy jest ona naprawdę potrzebna. Jeżeli zrobimy przypisanie $a = $b i nie wykonamy żadnej operacji, która uzasadniałaby utworzenie kopii, PHP nie będzie tracić czasu na jej robienie!

Należy wystrzegać się także wywoływania funkcji ze zmuszaniem do przekazywania referencji, gdy funkcja sobie tego wyraźnie nie życzy: funkcja(&zmienna) - zapis taki był poprawny jeszcze w PHP 4, lecz w PHP 5 został wycofany i generuje ostrzeżenie.

Niektóre algorytmy pisane przez programistów PHP generują kod w tym języku, który następnie jest wykonywany. Można go zapisać do pliku i załadować instrukcją include, lecz nie zawsze jest to optymalne rozwiązanie. PHP oferuje swoim programistom instrukcję eval, która wykonuje podany w ciągu tekstowym kod jako skrypt PHP.

<?php
$zmienna = 5;
eval('echo $zmienna;');

Eval najpierw spowoduje kompilację ciągu echo $zmienna;, a następnie jego wykonanie, czego efektem będzie wyświetlenie się w przeglądarce wartości zmiennej. Nie nadużywaj tej własności zbyt często. Wykonywanie tworzonego przez nas kodu PHP jest ciekawą opcją, która jednak prowadzi do dość dużego spadku wydajności, trudnych do wykrycia błędów, a nawet problemów z bezpieczeństwem. PHP jest tak zaprojektowany, że praktycznie nigdy nie potrzeba w nim używać eval. Oto dwa przykłady:

1. Chcemy wykonać funkcję, której nazwę mamy zapisaną w zmiennej $funkcja. Choć możemy napisać

eval($funkcja."($a, $b, $c)");

do dyspozycji mamy o wiele lepszy sposób:

$funkcja($a, $b, $c);

Pokazane to zostało w przykładach powyżej.

2. Chcemy wstawić w wyrażeniu wartość zmiennej, której nazwa zapisana jest w innej zmiennej (np. $zmienna). Zamiast pisać

eval('echo $'.$zmienna.';');

możemy zrobić:

echo $$zmienna;

Oba powyższe przykłady wykonają się szybciej, ponieważ są kompilowane wraz z właściwym skryptem. W przypadku eval PHP musi natomiast po raz drugi uruchamiać kompilator.

Poprzedni rozdział: Inne elementy składni
Następny rozdział: Korzystanie z dokumentacji

Każdy popełnia błędy

edytuj

Błąd w programowaniu jest wynikiem pomyłki/literówki w trakcie pisania aplikacji albo niedostrzeżeniem jakiejś cechy projektowanego algorytmu. Wielu początkujących programistów jest przerażonych, gdy na ekranie pojawią im się tajemnicze komunikaty, a w parze z tym idzie niezaradność. Dlatego w tym rozdziale pragniemy skupić się na zagadnieniu błędu. Omówimy komunikaty zgłaszane przez PHP, techniki pomagające zlokalizować i usunąć błędy, a także powiemy nieco o pakietach testowych od strony teoretycznej, ponieważ do praktyki pozostało nam wciąż parę zagadnień związanych ze składnią.

Komunikaty błędów PHP

edytuj

Przetworzenie skryptu PHP składa się w istocie z dwóch faz:

  • Interpretacji - kod skryptu tłumaczony jest na wewnętrzny zestaw instrukcji interpretera. Faza ta często jest także zwana parsowaniem.
  • Wykonywania - właściwe wykonanie skryptu.

Każda z nich generuje nieco inne komunikaty błędów, dzięki czemu wiemy, gdzie szukać przyczyny problemów. Napiszmy sobie taki skrypt:

<?php
if(isset($zmienna)
{
   echo $zmienna;
}

Po jego uruchomieniu powinniśmy ujrzeć następujący komunikat:

Parse error: syntax error, unexpected '{' in /home/uzytkownik/www/kurs/aplikacja.php on line 3

Jest to błąd składni powiązany z fazą kompilacji. PHP nie może skompilować skryptu, ponieważ w podanym pliku w linii 3 natrafił na nieprawidłową konstrukcję składniową. Nie oznacza to jednak, że błąd jest akurat w tej linii. Zazwyczaj powoduje go jakaś pomyłka nieco wyżej. Przyjrzyjmy się komunikatowi. Mówi on, że PHP natknął się na niespodziewany nawias klamrowy w linii 3. Rzeczywiście, znajduje się on na swym miejscu tak, jak być powinien, ale skoro według interpretera nie powinno go tu być, a nic więcej w tej linii nie mamy, zerknijmy wyżej: if(isset($zmienna) - okazuje się, że nie zamknęliśmy jednego z nawiasów. PHP oczekiwał znaku ), lecz zamiast tego przeszedł do kolejnej linijki, dostał { i zgłosił błąd. Po dodaniu brakującego nawiasu skrypt zaczyna poprawnie działać.

Po usunięciu pierwszego błędu postanowiliśmy rozbudować skrypt i wywołaliśmy funkcję inicjującą naszą aplikację WWW.

<?php 
if(isset($zmienna))
{
   echo $zmienna
   inicjujAplikacje();
}

Po uruchomieniu znów pojawił nam się komunikat błędu!

Parse error: syntax error, unexpected T_STRING, expecting ',' or ';' in /home/uzytkownik/www/kurs/aplikacja.php on line 5

Owo tajemnicze T_STRING jest tzw. tokenem. Kompilatory mają tendencję do ułatwiania pracy zarówno sobie, jak i programiście je tworzącemu. Wszystkie elementy pełniące identyczną funkcję, np. zmienne, grupowane są pod jedną wspólną nazwą zwaną tokenem. Tworzenie składni jest teraz bardzo proste. Jeżeli chcemy, aby w jakimś miejscu można było podać dowolną zmienną, używamy tokenu i kompilator już wie, co można tam umieszczać. Informacje o błędnym użyciu tokenu PHP wyświetla w sposób jawny, jak widać na powyższym przykładzie. Komunikat informuje nas teraz, że PHP natknął się na ciąg tekstowy, oczekując przecinka albo średnika. Spójrzmy linijkę wyżej - rzeczywiście, brakuje nam średnika. Mógłbyś zapytać - czemu ciąg tekstowy, skoro to jest funkcja? Kompilator po prostu nie dotarł jeszcze do występujących dalej nawiasów, więc wstępnie zaklasyfikował nazwę funkcji jako tekst. Gdyby udało mu się tam dotrzeć, połączyłby z nią nawiasy i powstał nowy token: T_FUNCTION.

<?php 
$zmienna = 'wartosc';
if(isset($zmienna))
{
   echo $zmienna;
   inicjujAplikacje();
}

Poprawiliśmy usterkę i zainicjowaliśmy zmienną $zmienna jakąś wartością, jednak nasz skrypt nadal nie działa. Tym razem mamy do czynienia z problemem innego kalibru:

Fatal error: Call to undefined function inicjujAplikacje() in /home/uzytkownik/www/kurs/aplikacja.php on line 5

Jest to błąd wykonywania skryptu. PHP poinformował nas w ten sposób, że dotarł do wywołania funkcji inicjujAplikacje(), lecz taka nie istnieje. Pozostało nam jedynie odkryć przyczynę tego stanu - może nie dołączyliśmy jakiejś ważnej biblioteki? Zwróć uwagę, że PHP sprawdza istnienie funkcji w momencie wykonywania skryptu, a nie kompilacji. Gdybyśmy nie zainicjowali zmiennej $zmienna, kod wewnątrz instrukcji if nie zostałby wykonany i problem pozornie by nie wystąpił. To oczywiście tylko złudzenie. On tam jest, lecz PHP nie wykonał tego kawałka kodu i nie wykrył problemu. Spróbuj wykonać taki eksperyment. Praktyka ta ma swoje uzasadnienie - przypomnijmy sobie instrukcję eval z końca poprzedniego rozdziału. Tam nazwa funkcji była ustalana w momencie wykonywania, gdyż tylko wtedy mają prawo istnieć jakiekolwiek zmienne.

Komunikaty Fatal error pojawiają się zawsze wtedy, gdy w trakcie wykonywania wystąpi problem uniemożliwiający dalszą pracę interpreterowi. Przykładem jest podanie do instrukcji require nazwy nieistniejącego pliku. PHP go nie odnajdzie i zatrzyma wykonywanie. Warto nadmienić, że użycie include spowoduje "tylko" pokazanie się ostrzeżenia, a skrypt będzie dalej wykonywany.

Innym rodzajem komunikatu o błędzie jest Warning, czyli ostrzeżenie. W odróżnieniu od Fatal error lub Parse error, Warning nie przerywa działania naszego skryptu. Przykładem pojawiania się tego komunikatu może być podanie nieprawidłowych parametrów w jakiejś funkcji:

<?php
$tekst = 'to jest jakiś tekst';
sort($tekst);

Widzimy, że zmienna $tekst nie jest tablicą, więc użycie tej zmiennej w funkcji sort(), która służy do sortowania tablic, wywołuje pojawienie się komunikatu o błędzie typu Warning:

Warning: sort() expects parameter 1 to be array, string given in /home/uzytkownik/www/kurs/aplikacja.php on line 3

Mówi on, że funkcja sort() jako parametr pierwszy może przyjąć tylko tablice, a dostała co innego.

Ostatnim z omówionych komunikatów błędów będą tzw. notices, czyli powiadomienia. Mają one niski priorytet i służą do informowania o miejscach, które potencjalnie mogą stanowić źródło problemu. Oto jedna z takich sytuacji:

<?php
echo $zmienna;

Skrypt próbuje tutaj wyświetlić zawartość zmiennej $zmienna, lecz nie jest ona zdefiniowana. Może to być zarówno świadome działanie (wiemy, że zmienna może w tym miejscu nie istnieć, ale dopuszczamy to) lub też wynik literówki w jej nazwie, co oczywiście niosłoby dla nas poważne konsekwencje. Dlatego interpreter na wszelki wypadek wyświetli w tym miejscu komunikat:

Notice: Undefined variable: zmienna in /home/uzytkownik/www/kurs/aplikacja.php on line 2

Podobny komunikat pojawi się, gdy spróbujemy odczytać wartość nieistniejącego elementu tablicy. Do dobrych zwyczajów należy takie pisanie skryptów, aby nie generowały one tego typu komunikatów z kilku powodów:

  1. Jeśli nasz skrypt będą też rozwijać inni programiści, mogą mieć oni ustawiony wysoki poziom raportowania błędów, przez co właściwa treść strony będzie im ginąć w setkach powiadomień.
  2. Wiele serwerów, pomimo ryzyka związanego z bezpieczeństwem, pozostawia włączony wysoki poziom raportowania błędów. Z identycznego powodu nie będzie można używać tam naszego skryptu.
  3. Brak komunikatów jest wyrazem dbałości o sytuacje wyjątkowe oraz możliwość ich obsługi.

Używaj komendy isset() do sprawdzania, czy wymagane w danym fragmencie zmienne istnieją, a także inicjuj je przed pierwszym użyciem domyślną wartością.

Operator @

edytuj

Jeżeli dowolne wyrażenie PHP poprzedzimy znakiem @, interpreter nie wyświetli żadnego komunikatu, jeżeli wykryje w nim błąd. Nie oznacza to naturalnie, że błąd ten nagle znika. On tam jest, lecz my nie jesteśmy o tym powiadamiani. Dlatego należy bardzo rozważnie postępować z jego użyciem. @ przydaje się głównie przy funkcjach do komunikacji z zewnętrznymi źródłami danych, np. plikami. Jeżeli PHP wykryje np. brak żądanego pliku, nie tylko generuje określony wynik, ale także pokazuje komunikat Warning, co nie zawsze jest zjawiskiem pożądanym. Operatorem tym możemy także "zakazać" wyświetlania komunikatów Notice o niezdefiniowaniu zmiennej, jeżeli tego naprawdę potrzebujemy. Poniżej pokazany przykład ma za zadanie pokazać datę modyfikacji pliku lub nasz własny komunikat, jeżeli on nie istnieje. Na początek wariant najprostszy:

<?php
echo filemtime('plik.txt');

Kiedy plik.txt nie będzie istnieć, interpreter wygeneruje stosowny komunikat, którego my w takiej formie oglądać nie chcemy. Dlatego wcześniej sprawdzimy drugą funkcją istnienie pliku.

<?php
if(file_exists('plik.txt'))
{
	echo filemtime('plik.txt');
}
else
{
	echo 'Podany plik nie istnieje.';
}

Co dwie funkcje to nie jedna, lecz pojawia się nam tu problem wydajności. Odczyt czegokolwiek z twardego dysku stanowi duży narzut czasowy dla oprogramowania, dlatego powinniśmy się starać wykonywać jak najmniej takich operacji. Wiemy z dokumentacji, że samo filemtime() zwraca nam false, jeśli plik nie istnieje, przeszkadza nam tu jedynie "firmowe" ostrzeżenie. Dlatego właśnie skorzystamy z @, aby się go pozbyć:

<?php
$wynik = @filemtime('plik.txt');
if($wynik !== FALSE)
{
	echo $wynik;
}
else
{
	echo 'Podany plik nie istnieje.';
}

Wynik funkcji zapisujemy w zmiennej, ponieważ jest on nam potrzebny w paru miejscach. Aby nie dostawać komunikatu Warning, poprzedziliśmy wywołanie funkcji operatorem @.

Wszystko można zapisać inaczej.

<?php
@echo filemtime('plik.txt') or die('Podany plik nie istnieje.');

Poziom raportowania błędów

edytuj

Poziom raportowania błędów określa, na które zdarzenia PHP ma reagować wyświetleniem stosownego komunikatu. Docelowo ustawia się go w pliku konfiguracyjnym php.ini, w dyrektywie error_reporting. Podczas instalacji według tego podręcznika, zaleciliśmy użycie najwyższego możliwego poziomu E_ALL | E_STRICT, który zgłasza dosłownie wszystko. Dzięki temu możemy w porę reagować na każdy problem i mieć pewność, że po zainstalowaniu aplikacji na innym serwerze nie zostaniemy zasypani toną komunikatów. Jednak takie ustawienie należy stosować tylko w fazie tworzenia/testowania naszych skryptów. Gdy już umieścimy je na stronie internetowej, należy wyłączyć wyświetlanie wszystkich błędów, ponieważ takie komunikaty często ujawniają wiele informacji o skrypcie i używanym oprogramowaniu, które mogą ułatwić atak na nasz serwis.

Poziom raportowania można też tymczasowo zmieniać z poziomu skryptu funkcją error_reporting():

<?php
error_reporting(E_ALL ^ E_NOTICE);
echo 'Teraz komunikaty Notice nie działają: '.$nieistniejacaZmienna;

Funkcja ta ma jeszcze jedną użyteczną właściwość, mianowicie zwraca jako rezultat dotychczasowy poziom raportowania, dzięki czemu możemy go potem przywrócić:

<?php
$poprzedni = error_reporting(E_ALL ^ E_NOTICE);
echo 'Teraz komunikaty Notice nie działają: '.$nieistniejacaZmienna;

error_reporting($poprzedni);
echo 'A teraz już tak: '.$nieistniejacaZmienna;

Techniki debugowania

edytuj

Inny rodzaj błędów związany jest z algorytmami produkującymi niewłaściwe rezultaty. Odnaleźć przyczynę problemu jest tu znacznie trudniej, ponieważ nie jesteśmy raczeni żadnymi komunikatami i musimy sami dochodzić, co się właściwie stało. Jedynym sposobem jest poumieszczanie w kodzie na chwilę własnych informacji, które dokładnie powiadomią nas, które fragmenty są wykonywane w jakiej kolejności i z jakimi danymi. Metoda wywodzi się w prostej linii z języka C/C++, gdzie w kodzie umieszcza się tzw. asercje. Jeżeli kompilujemy kod programu w trybie debug (wykrywanie błędów), w oznaczanych przez nas miejscach dodawane są odpowiednie instrukcje sprawdzające poprawność danych i raportujące ewentualne problemy. Kiedy kompilujemy program w wersji "oficjalnej", preprocesor nie umieszcza już tych instrukcji w kodzie. Oprócz tego do wielu języków istnieją specjalne debugery, które pozwalają śledzić wykonywanie krok po kroku lub zatrzymać na wybranej linii, co przydaje się przy analizie złożonych algorytmów.

PHP żadnego preprocesora nie ma (można nawet powiedzieć, że sam nim jest dla języka HTML), dlatego sami musimy poumieszczać w kodzie odpowiednie wstawki. Najprostszą z nich jest echo 'Test';. Komendę taką umieszczamy, chcąc sprawdzić, czy algorytm dociera do danego miejsca. Po uruchomieniu, jeśli na ekranie pokaże nam się rzeczony napis, oznacza to, iż akurat to miejsce działa dobrze. Instrukcją echo możemy także wyświetlać dane lub sprawdzać kolejność wykonywania operacji, wstawiając różne komunikaty. Komenda die() nie tylko wyświetla komunikat, ale także zatrzymuje skrypt. Do wyświetlania zawartości tablic lub obiektów możemy zastosować funkcje var_dump() albo print_r(). Po usunięciu usterki wszystkie poczynione przez nas wstawki usuwamy z kodu.

PHP posiada specjalną funkcję obsługi asercji: assert(). Generuje ona komunikat błędu, jeżeli podane jej wyrażenie ma wartość false:

<?php
$zmienna = 3; 
assert($zmienna == 6);

Jednak to jeszcze nie wszystko. Musimy funkcją assert_options() włączyć to sprawdzanie:

<?php
assert_options(ASSERT_ACTIVE, 1);
assert_options(ASSERT_WARNING, 1);
 
$zmienna = 3;
 
assert($zmienna == 6);

Po skończeniu debugowania wcale nie musimy usuwać wywołań funkcji assert(). Wystarczy ustawić opcję ASSERT_ACTIVE na 0 i przestaje być ona aktywna.

Do debugowania skryptów PHP mogą być także przydatne zewnętrzne moduły w stylu Xdebug (dostępny w repozytorium PECL tj. http://pecl.php.net/package/Xdebug a nawet jako paczka w niektórych dystrybucjach Linuksa). Rozwiązania takie są dosyć ciekawe, ponieważ pokazują znacznie więcej użytecznych dla programisty informacji. Przykładowo, Xdebug w momencie wystąpienia błędu pokazuje całą listę wywołań funkcji nadrzędnych. Rozważmy sytuację, gdy pewnej funkcji używamy w kilku modułach, lecz co jakiś czas generuje ona błąd. Dzięki obecności listy możemy natychmiast określić, który moduł wywołuje ją nieprawidłowo. Xdebug można również skonfigurować do pracy z zewnętrznymi środowiskami IDE do tworzenia skryptów (np. PDT wchodzący w skład pakietu Eclipse).

Pakiety testowe

edytuj

W praktyce okazuje się, że ręczne sprawdzanie aplikacji po każdej wprowadzonej zmianie bywa uciążliwe i jest narażone na ryzyko przeoczenia jakiegoś istotnego elementu. Dlatego w większych projektach używa się na co dzień systemów automatycznego testowania. Zagadnieniom testowania poświęcono wiele książek. Sprowadzają się one najczęściej do napisania dodatkowego zestawu testów, który wywołuje różne funkcje w określony sposób i sprawdza czy wynik ich działania zgadza się ze spodziewanym. Po wprowadzeniu większych zmian wystarczy jedynie uruchomić system testujący, którego wynikiem będzie raport, ile testów zostało pomyślnie zaliczonych i gdzie pojawiły się błędy.

Możemy wyróżnić dwa rodzaje testów:

  1. Testy funkcjonalne - sprawdzają funkcjonalność gotowej aplikacji, np. wypełniając formularz określonymi danymi i weryfikując odpowiedź programu.
  2. Testy jednostkowe - koncentrują się na pojedynczych elementach kodu aplikacji (np. funkcjach w kodzie) sprawdzając, jak reagują one na określone argumenty. Założenie polega na tym, że jeśli funkcje działają poprawnie i gdzieś wystąpił błąd, najprawdopodobniej leży on w sposobie poskładania programu z funkcji, a nie w samych funkcjach.

Do każdego z nich istnieją odpowiednie systemy testowania. Testy funkcjonalne mogą być realizowane przy pomocy Selenium, zaś do testów jednostkowych w PHP powszechnie stosowany jest skrypt PHPUnit. Nie będziemy ich teraz poznawać, ponieważ wciąż brakuje nam sporo wiedzy o samym PHP, aby zrozumieć, jak są one napisane i jak takie testy układać. Wrócimy do nich w dalszej części podręcznika.

Korzystanie z dokumentacji

edytuj

Najważniejszym miejscem dla każdego programisty PHP jest dokumentacja projektu. Jest ona o tyle ważna, że informacja o nowościach w składni w pierwszej kolejności pojawia się właśnie tutaj. Źródła w rodzaju artykułów niekoniecznie są aktualizowane wraz z nowymi wersjami i nie można się na nich w pełni opierać. Dokumentacja przydaje się też przy poszukiwaniu nowych funkcji, modułów lub mało znanych możliwości.

Angielska wersja dokumentacji dostępna jest pod adresem www.php.net/manual/en. Istnieje także polska, lecz jest ona bardzo rzadko uaktualniana, a ilość i jakość przetłumaczonych rozdziałów pozostawiają nieco do życzenia. Przyjrzyjmy się zatem jej strukturze.

Dokumentacja PHP powstaje w oparciu o XML-ową aplikację DocBook, dlatego jej struktura jest bardzo uporządkowana. W całym tekście przyjęte są jednolite konwencje stylistyczne, a nawigacja jest intuicyjna. Kody źródłowe drukowane są na szarym tle, z kolorowaną składnią, jeżeli dotyczy ona źródeł PHP. W wielu miejscach umieszczone są uwagi, ostrzeżenia oraz porady. Na dole każdej strony znajduje się sekcja z komentarzami programistów. Naprawdę warto od niej zaczynać studiowanie dokumentacji, ponieważ często znajdują się tam o wiele ciekawsze przykłady użycia lub naprawdę przydatne sztuczki i algorytmy. Ilość komentarzy do najpopularniejszych funkcji jest ogromna i obejmuje niemal wszystko: własne implementacje, przykłady i komentarze odnośnie użycia, linki do artykułów oraz inne.

Instalacja PHP

edytuj

Informacje dotyczące instalowania PHP znajdują się w rozdziale II. Opisane zostały tam przykłady oraz dodatkowe wiadomości dla wszystkich platform i serwerów. Nie wystarczą one jednak do skonfigurowania PHP do własnych potrzeb, ponieważ spis dyrektyw znajduje się gdzie indziej.

Spis dyrektyw konfiguracyjnych dla systemów Unix znajduje się w rozdziale IX ("Dodatki") i oznaczony jest literą "F": "Configure options". Nie zawiera on jednak spisu komend aktywujących poszczególne moduły. Te należy sobie odszukać na stronie poświęconej każdemu z nich.

Pozycję niżej znajduje się wykaz dyrektyw pliku php.ini, także bez listy modułów. Ogólnie wszystko, co związane z instalowaniem jakiegokolwiek rozszerzenia, znajduje się na poświęconej mu stronie dokumentacji w sekcji "Installation".

Składnia języka

edytuj

Aby dowiedzieć się, jak przedstawia się składnia języka, należy zajrzeć do rozdziału "Language reference". Jest to typowy spis dostępnych komend, instrukcji oraz elementów wyrażeń ułożony tematycznie. Najbardziej przydatny będzie dla programistów mających już pewną wprawę w tworzeniu aplikacji oraz skryptów, gdyż opisy nie są tak przystępne, jak w kursach i książkach. Ich zadaniem jest dostarczenie wyczerpującej wiedzy o danym elemencie i wypisanie wszystkich możliwych zagadnień z nim związanych.

Rozdział poświęcony programowaniu obiektowemu jest zdublowany. W PHP 5 ten element został napisany całkowicie od nowa, tracąc część kompatybilności ze starymi wersjami i stąd konieczność podania dwóch osobnych opisów w zależności od posiadanej wersji. Do części dla PHP 4 polecamy już sięgać jedynie z powodów historycznych.

"Security" oraz "Features"

edytuj

W tych dwóch rozdziałach znajdują się informacje praktyczne pokazujące niektóre aspekty charakterystyczne dla PHP. Tutaj możesz dowiedzieć się o:

  1. Ładowaniu plików na serwer
  2. Bezpieczeństwie skryptów
  3. Kilku niezbyt lubianych przez programistów opcjach: Register globals oraz Magic quotes.
  4. Metodach raportowania błędów
  5. Obsłudze ciastek i sesji
  6. Pisaniu skryptów dla tzw. trybu bezpiecznego wykorzystywanego na produkcyjnych serwerach.

Spis funkcji

edytuj

Jest to jeden z najważniejszych rozdziałów dla programisty. Zawiera spis wszystkich funkcji pogrupowanych według modułów. Na początku każdego podrozdziału znajduje się skrótowy opis rozszerzenia, wymagania oraz sposób jego zainstalowania. Należy tu zwrócić uwagę na następujące frazy:

No external libraries are needed to build this extension.

To rozszerzenie nie wymaga obecności żadnych dodatkowych bibliotek w systemie.

There is no installation needed to use these functions; they are part of the PHP core.

Funkcje te są częścią jądra PHP i nie da się ich wywalić. Są zatem zawsze dostępne.

The SimpleXML extension is enabled by default. To disable it, use the --disable-simplexml configure option.

Komunikat w tym stylu oznacza, że domyślnie rozszerzenie to jest zawsze aktywne, ale istnieje możliwość jego wyłączenia.

Następnie na stronie znajduje się kilka prostych przykładów i lista stałych definiowanych przez dany moduł. Na końcu wyszczególniony jest alfabetyczny spis wszystkich funkcji modułu wraz z lakonicznie zaznaczonym działaniem. Aby dowiedzieć się więcej, należy po prostu kliknąć na wybraną funkcję.

Opis funkcji zaczyna się od składni. Wygląda on mniej więcej tak:

bool setcookie ( string name [, string value [, int expire [, string path [, string domain [, bool secure]]]]] )

Zaczyna się on od nazwy typu danych zwracanego przez funkcję. W dokumentacji stosowana jest następująca konwencja:

  • int, string, float, bool - skalarne typy danych: liczby, ciągi tekstowe, wartości logiczne.
  • array - tablica
  • mixed - funkcja może zwrócić jeden z kilku rodzajów typów. W tym wypadku trzeba samodzielnie doczytać w opisie, co jest zwracane w jakich sytuacjach.

Jeżeli funkcja zwraca obiekt, podawana jest jego nazwa.

W nawiasach okrągłych podany jest spis parametrów, również z zaznaczonymi typami. Nawiasy kwadratowe oznaczają parametry opcjonalne. Z podanego powyżej przykładu wynika, że możemy podać:

  1. nazwę
  2. nazwę i wartość
  3. nazwę, wartość i czas wygaśnięcia
  4. nazwę, wartość, czas wygaśnięcia i ścieżkę
  5. nazwę, wartość, czas wygaśnięcia, ścieżkę i domenę
  6. nazwę, wartość, czas wygaśnięcia, ścieżkę, domenę oraz nakaz wysłania ciastka połączeniem szyfrowanym

Dokładniejsze informacje, co robi jaki parametr, znajdują się w opisie funkcji. Kończy się on sekcją "See also" odsyłającą do funkcji podobnych lub uzupełniających zawarte tu informacje.

W przypadku nowych, obiektowych rozszerzeń, nazwa funkcji jest dwuczęściowa: obiekt::funkcja.

Do PHP stworzono już bardzo dużo modułów i ogrom ich spisu może przerazić. Oto krótki przewodnik, gdzie co można znaleźć:

  1. Arrays - wszystko, co związane z tablicami
  2. ctype - sprawdzanie zawartości ciągów tekstowych
  3. Exif - wyciąganie dodatkowych informacji z plików JPG i TIFF
  4. Filesystem - operacje na plikach
  5. Function handling - zarządzanie funkcjami
  6. HTTP - kilka funkcji do protokołu HTTP (np. wysyłanie ciastek)
  7. Image functions - biblioteka GD do generowania obrazków
  8. Math - funkcje matematyczne
  9. Misc. - funkcje niepasujące nigdzie indziej (np. kolorowanie składni)
  10. MySQL - najstarszy zbiór funkcji do komunikacji z bazą MySQL
  11. mysqli - "Improved MySQL", komunikacja z nowymi wersjami bazy MySQL.
  12. Network - funkcje sieciowe
  13. Output Control - buforowanie wyjścia skryptu
  14. PDO - biblioteka PDO do komunikacji z różnymi bazami danych, którą niebawem poznamy.
  15. spl - "Standard PHP Library", obiektowe nakładki na wiele podstawowych operacji.
  16. Strings - operacje na ciągach tekstowych
  17. Variables handling - rozpoznawanie typów zmiennych itd.

Zachęcamy do przejrzenia tych rozdziałów i zorientowania się w ich strukturze. Dobra orientacja w dokumentacji PHP sprawia, że nie trzeba nawet przykładać dużej wagi do "wkuwania" funkcji na pamięć.

Poprzedni rozdział: Korzystanie z dokumentacji
Następny rozdział: Ćwiczenia

Studium przypadku: Księga gości

edytuj

Dotychczasowe rozdziały miały w sobie więcej teorii i nie ukazywały prawdziwej istoty tworzenia skryptów dla stron WWW. Najlepszym rodzajem nauki jest praktyczne stworzenie skryptu z prawdziwego zdarzenia i pokazanie w ten sposób, jak poszczególne elementy języka ze sobą się łączą. W tym rozdziale stworzymy samodzielnie funkcjonalną księgę w PHP. Wykorzystamy w niej pętle, tablice, instrukcje warunkowe, formularze oraz dołączanie plików zewnętrznych. Ponadto poznamy kilka interesujących funkcji.

Księgę gości oprzemy o pliki tekstowe. Każdy wpis będzie charakteryzowany przez:

  • Tytuł
  • Autora
  • Datę
  • Adres WWW (opcjonalny)
  • Treść

Wszystkie te dane zostaną zapisane w pliku w pojedynczym wierszu, a oddzielone będą znakiem "|". Aby wyeliminować sytuację, gdy ktoś wpisze np. w treść taki znak, albo (co gorsza) naciśnie enter, każda kolumna zostanie zakodowana w formacie Base64. Oczywiście nie będziesz musiał sam pisać odpowiedniego algorytmu - PHP posiada odpowiednie funkcje. Base64 został pierwotnie zaprojektowany do przesyłania 8-bitowych wiadomości w 7-bitowych protokołach. Do zapisu są tu używane wyłącznie litery, cyfry, ukośnik oraz znak równości, zatem nie ma obawy o uszkodzenie formatu pliku księgi. Zakodowana wiadomość jest o ok. 33% dłuższa od oryginału.

Księga nie będzie zapisywać i odczytywać z pliku bezpośrednio. W zewnętrznym pliku napiszemy sobie dwie funkcje zajmujące się wyłącznie tym. Księga będzie miała dostęp do danych jedynie za ich pomocą. Rozwiązanie to jest bardzo użyteczne. Kiedy poznasz już bazy danych, z pewnością zapragniesz przesiąść się właśnie na nie. Nie będziesz musiał przepisywać całej księgi. Po prostu napiszesz do tych dwóch funkcji nowy kod. Dzięki takiemu rozwiązaniu źródła skryptu będą uporządkowane i czytelne, a ponadto bardzo łatwe do modyfikacji.

dane.php

edytuj

Plik dane.php zawierać będzie dwie funkcje: dodajWpis() oraz pobierzWpisy() zajmujące się wyłącznie operowaniem na danych. Będziemy go pisać po kawałku.

<?php
 	define('WPISY', './wpisy.txt');
 
 	function dodajWpis($tytul, $autor, $www, $tresc)
 	{
 		// Ucinanie bialych znakow
 		$tytul = trim($tytul);
 		$autor = trim($autor);
 		$www = trim($www);
 		$tresc = trim($tresc);

Nazwę pliku zapisaliśmy za pomocą stałej. To na wypadek, gdyby zachciało nam się go kiedyś przenieść albo zmienić mu nazwę. Pierwszy etap obrabiania danych to ucięcie białych znaków (spacji, tabulatorów itp.) z początku i końca każdego ciągu. Nie są nam one do niczego potrzebne, a jedynie utrudniają nam sprawę.

		// Kontrola danych
 		
 		if(strlen($tytul) < 3)
 		{
 			return false;
 		}
 		
 		if(strlen($autor) < 3)
 		{
 			return false;
 		}
 		
 		if(strlen($tresc) < 10)
 		{
 			return false;
 		}
 
 		if(strlen($www) > 0)
 		{
 			// Jesli adres nie zaczyna sie od http:// to dodaj to
 			if(strpos($www, 'http://') !== 0)
 			{
 				$www = 'http://'.$www;
 			}		
 		}

Tutaj zajmujemy się kontrolą danych. Tytuł i autor muszą mieć przynajmniej trzy znaki, a treść 10. Długość pobieramy funkcją strlen(). Jeżeli podaliśmy adres WWW, za pomocą funkcji strpos() sprawdzamy, czy na jego początku jest na pewno dodany identyfikator protokołu. Zwróć uwagę na użyty operator: !==. Wspomniana funkcja zwraca pozycję pierwszego znaku szukanego ciągu (liczoną od zera), a jeżeli go nie znajdzie, zwraca false. Normalnie 0 i false są równoważne, dlatego musimy wymusić sprawdzenie także typu, bowiem nam chodzi o 0 jako pozycję identyfikatora w adresie, a nie informację, że go nie ma.

Jeżeli któraś z informacji nie będzie prawidłowo podana, funkcja zwróci do skryptu wartość false.

		// Dodawanie
 	
 		$f = fopen(WPISY, 'a');
 		
 		$dane = array(0 =>
 			base64_encode(htmlspecialchars($tytul)),
 			base64_encode(htmlspecialchars($autor)),
 			time(),
 			base64_encode(htmlspecialchars($www)),
 			base64_encode(nl2br(htmlspecialchars($tresc)))		
 		);
 		
 		fwrite($f, implode('|', $dane)."\r\n");
 		fclose($f);
 		return true;
 	} // end dodajWpis();

Ostatni akord to zapisanie wpisu w pliku. Na początek otwieramy go w trybie dopisywania (parametr a), następnie budujemy tablicę z danymi wpisu. Parametry przybyłe z formularza kodujemy w Base64, środkowa kolumna to czas dodania wpisu w sekundach od 1.1.1970. Dzięki takiemu jego zapisowi, będziemy go mogli później dowolnie formatować funkcją date(). Przy przetwarzaniu tekstu użyliśmy jeszcze kilku funkcji:

  • htmlspecialchars() - wszystkie wprowadzone tagi HTML są zamieniane na zwykły tekst (ograniczniki są zastępowane odpowiadającymi im encjami, np. < zmienia się w &lt;). W ten sposób nikt nie rozwali nam księgi złośliwym kodem.
  • nl2br() - zamienia znaki nowej linii na znaczniki <br/>.
  • implode() - łączy tablicę w ciąg tekstowy, wstawiając pomiędzy poszczególne elementy podany w pierwszym parametrze znak. Na końcu utworzonego przez nią rekordu wpisu musimy dodać samodzielnie znaki zejścia do nowej linii: \r\n, koniecznie w cudzysłowach, a nie w apostrofach.

Zwróć uwagę na rozbudowane wywołania funkcji:

base64_encode(nl2br(htmlspecialchars($tresc)))

Oznacza ona, że najpierw zawartość zmiennej $tresc trafia do funkcji najbardziej w prawo, tj. htmlspecialchars(). Z niej przechodzi do nl2br(), a z niej do base64_encode(). Alternatywne kursy preferują w tym miejscu czytelną, ale mniej wydajną formę zapisu:

$tresc = htmlspecialchars($tresc);
$tresc = nl2br($tresc);
$tresc = base64_encode($tresc);

Nasz kod ma tę przewagę, że wynik jednej funkcji od razu trafia do drugiego, tymczasem powyżej po drodze trafia do zmiennej, co ma szczególnie negatywny wpływ na wydajność przy dużych danych. Ponadto zapis ten często nie jest opatrzony stosownym komentarzem i wyrabia złe nawyki tworzenia mnóstwa niepotrzebnych zmiennych tymczasowych, o czym już wspominaliśmy. Trzeba zapamiętać, że nawet podawany przez nas wariant konstrukcji księgi nie jest "tym jedynym słusznym". Istnieje jeszcze wiele innych sposobów jej zaprogramowania.

Przyszła kolej na funkcję pobierającą wpisy.

	function pobierzWpisy()
 	{
 		$wpisy = array_reverse(file(WPISY));
 		
 		$i = 1;
 		$rezultat = array();

Wpisy pobieramy funkcją file(), która automatycznie rozbija nam plik na tablicę względem wierszy. Tam jednak najnowsze wpisy są na końcu, a my chcemy je wyświetlić w kolejności odwrotnej: najnowsze na górze. Dlatego od razu przepuszczamy wynik przez funkcję array_reverse() odwracającą tablicę. Następnie inicjujemy licznik wpisów $i oraz tablicę $rezultat, do której będziemy pakować sformatowane odpowiednio wpisy. Ją później przekażemy księdze do wyświetlenia.

		foreach($wpisy as $wpis)
 		{
 			$wpis = explode('|', trim($wpis));
 			
 			$rezultat[] = array(
 				'id' => $i,
 				'tytul' => base64_decode($wpis[0]),
 				'autor' => base64_decode($wpis[1]),
 				'data' => date('d.m.Y, H:i', $wpis[2]),
 				'www' => base64_decode($wpis[3]),
 				'tresc' => base64_decode($wpis[4])		
 			);
 			$i++;		
 		}
 		return $rezultat;	
 	} // end pobierzWpisy();
 
 ?>

Po kolei formatujemy każdy wpis. Do ich pobierania wykorzystaliśmy pętlę foreach(). Na wejściu przepuszczamy je przez funkcję trim(), bowiem file() pozostawia w tablicach znaki zejścia do nowej linii. Dopiero po ich usunięciu możemy funkcją explode() rozbić ciąg z powrotem na tablicę (czyli wykonać operację odwrotną do implode()). Wpis przepuszczamy przez base64_decode(), aby zdekodować informacje, formatujemy datę i pakujemy to do tablicy $rezultat. Później zwracamy ją.

ksiega.php

edytuj

W pliku tym znajdzie się główna część funkcjonalna księgi. To jego powinniśmy uruchamiać z poziomu przeglądarki. Zaczynamy jego pisanie od nagłówka HTML oraz dołączenia uprzednio napisanego pliku:

<!DOCTYPE html 
	PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="pl" lang="pl">
   <head>
   <meta http-equiv="content-type" content="text/html; charset=utf-8">
   <title>Księga gości Wikibooks</title>
   </head>
   <body>
   	<h1>Księga gości Wikibooks</h1>
 <?php
 
 	require('./dane.php');

Właściwy kod rozpocznie się instrukcją warunkową, która zadecyduje, czy wysłany został formularz dodawania, czy też wystarczy wyświetlić aktualną zawartość księgi. PHP zapisuje informację o użytej metodzie wysyłania żądania w specjalnej zmiennej $_SERVER['REQUEST_METHOD'], która może przyjąć wartości POST (formularz) albo GET (zwyczajne żądanie). Z niej właśnie skorzystamy:

	if($_SERVER['REQUEST_METHOD'] == 'POST')
 	{
 		// Dodawanie wpisu
 		if(dodajWpis($_POST['tytul'], $_POST['autor'], $_POST['www'], $_POST['tresc']))
 		{
 			echo '<p>Dziękujemy, wpis został dodany prawidłowo.</p>';
 		}
 		else
 		{
 			echo '<p>Proszę wypełnić prawidłowo formularz.</p>';
 		}
 		echo '<p><a href="ksiega.php">Powrót</a></p>';	
 	}

Kontrola danych jest realizowana w pliku dane.php i nie ma potrzeby jej tu powtarzać. Wprowadzamy do funkcji dodajWpis() poszczególne pola formularza i ifem sprawdzamy, jaki jest rezultat, aby móc wygenerować stosowny komunikat.

Jeżeli nie dodajemy aktualnie żadnego nowego wpisu, wyświetlamy te, które już mamy:

	else
 	{
 		// Wyświetlanie wpisów
 		$wpisy = pobierzWpisy();
 		
 		foreach($wpisy as $wpis)
 		{
 			echo '<hr /><p><b>Tytuł: <i>'.$wpis['tytul'].'</i>; 
 				Autor: '.$wpis['autor'].'; Data: '.$wpis['data'];
 			if(strlen($wpis['www']) > 0)
 			{
 				echo '; <a href="'.$wpis['www'].'" target="_blank">Strona WWW</a>';
 			}
 			echo '</b></p>';
 			echo '<p>'.$wpis['tresc'].'</p>';		
 		}
 		
 ?><hr />

Tablicę z wpisami dostajemy z funkcji pobierzWpisy(). Skanujemy ją pętlą foreach i wyświetlamy każdy z elementów. Zauważ, że skrypt potrafi ominąć pole z adresem WWW, jeśli ten nie został podany. Po prostu funkcją strlen() sprawdzamy, czy jego długość jest większa od zera.

Ostatnim akordem będzie dodanie formularza HTML:

 <form method="post" action="ksiega.php">
 <table border="0" width="50%">
 	<tr>
 		<td>Tytuł</td>
 		<td><input type="text" name="tytul"/></td>
 	</tr>
 	<tr>
 		<td>Autor</td>
 		<td><input type="text" name="autor"/></td>
 	</tr>
 	<tr>
 		<td>WWW</td>
 		<td><input type="text" name="www"/></td>
 	</tr>
 	<tr>
 		<td>Treść</td>
 		<td><textarea name="tresc" rows="4" cols="50"></textarea></td>
 	</tr>
 	<tr>
 		<td></td>
 		<td><input type="submit" value="Dodaj"/></td>
 	</tr>
 </table>
 </form>
 <?php
 	
 	}
 
 ?>
   </body>
 </html>

Formularz wysyłamy metodą POST do pliku ksiega.php. Jego wysłanie spowoduje uaktywnienie się dodawania wpisów. Przed wysłaniem ostatnich znaczników musimy dodać jeszcze klamrę kończącą instrukcję warunkową, która decyduje, co należy wykonać. Umieszczenie jej w tym miejscu gwarantuje, że formularz pokaże się tylko podczas wyświetlania wpisów. Jeśli skrypt będzie wysyłać komunikat o dodaniu wpisu, nie zostanie on już dołączony.

To już koniec naszej księgi gości. Utwórz teraz pusty plik wpisy.txt i zacznij korzystanie.

Co dalej?

edytuj

Napisana tutaj księga nie jest wyrafinowanym szczytem techniki. Jej głównym zadaniem było pokazanie podstawowych mechanizmów tworzenia dynamicznych stron WWW w praktyce. Wraz z powiększaniem się twojej wiedzy o języku PHP możesz rozszerzać ją o nowe możliwości:

  • Blokadę przed floodem.
  • Kasowanie wpisów.
  • Dzielenie wyników na strony.
  • Łatwy w modyfikacji wygląd.

Niektóre z tych rzeczy będą znacznie łatwiejsze do wykonania, kiedy poznasz zasady pracy z bazami danych dającymi nieporównywalnie większe możliwości, niż pliki tekstowe. Na bazach danych oparta jest obecnie zdecydowana większość istniejących aplikacji internetowych. Dzięki temu, że odseparowaliśmy pobieranie danych od ich wyświetlania, przepisanie księgi na MySQL będzie jedynie zabiegiem kosmetycznym. Zdecydowanie polecamy korzystanie z takiego rozwiązania, gdyż daje programiście sporą elastyczność. Nie zawsze bowiem zdarza się, że organizacja naszych danych będzie dokładnie taka, jakiej życzyłby sobie klient. Programując wszystko "na sztywno" wraz z wyglądem i kodem funkcjonalnym aplikacji narażamy się na większe ryzyko popełnienia błędu, zwiększamy rozmiar kodu i czynimy go mniej czytelnym.

Na tym kończymy poznawanie podstaw języka PHP. Przyszła pora na dokładniejsze poznanie kilku użytecznych funkcji przydatnych podczas tworzenia własnych skryptów.

Poprzedni rozdział: [[../../Studium przypadku - Księga gości/]]
Następny rozdział: [[../../Przetwarzanie tekstu/]]

Ćwiczenia

edytuj

Ćwiczenia utrwalające materiał z pierwszego rozdziału.

[[../../Odpowiedzi/Podstawy języka|Odpowiedzi]]

Zasada działania PHP

edytuj
  1. PHP jest językiem skryptowym. Co to oznacza?
  2. Wskaż czynności, które można wykonać przy pomocy PHP. Jeżeli czegoś nie da się zrobić w PHP, jakiej technologii użyjesz?
    • Licznik odwiedzin
    • Otwarcie pliku na dysku internauty
    • Rozwijane menu
    • Otwarcie pliku na serwerze
    • Rejestrowanie informacji o osobach przychodzących
    • System newsów
    • Aktualizacja fragmentu załadowanej strony WWW
    • Zareagowanie na kliknięcie myszką
    • Pobranie adresu MAC karty sieciowej internauty
    • Wyszukiwarka treści
  3. Co powinien generować skrypt PHP uruchamiany na serwerze WWW?

Podstawy języka

edytuj

Szybkie pytania:

  1. Do czego służy komenda echo?
  2. Dlaczego tekst, który chcemy wyświetlić przy pomocy echo nie musi być otoczony nawiasami, a przy printf musi?
  3. Objaśnij różnicę w działaniu cudzysłowów i apostrof.
  4. Do czego służą zmienne? Jakimi atrybutami można je opisać?
  5. Wskaż niepoprawne nazwy zmiennych:
    • $_
    • $zmienna
    • $Napis383
    • $dZi33cI_N30
    • $15newsow
    • $gżegżółka
    • $_i
  6. Opisz na przykładzie, dlaczego powinno się inicjować zmienne przed pierwszym użyciem.

Poniżej pokazane są dwie sytuacje, w których stosujemy zmienne tymczasowe, lecz w jednej z nich są one niepotrzebne. Wskaż "błędną" sytuację oraz objaśnij, dlaczego w jednym przypadku zmienna tymczasowa jest pożądana, a w drugim nie?

Przykład 1:
<?php
$tresc = $_POST['tresc'];
$tresc = htmlspecialchars($tresc);
$tresc = nl2br($tresc);
zapisz($tresc);
Przykład 2:
<?php
$wynik = skomplikowaneObliczenia();
if($wynik > 4)
{
   przetwarzajDalej($wynik * 536 - 832 / ($wynik / 2));
}
else
{
  buforuj($wynik);
}

Pętle i instrukcje warunkowe

edytuj

Szybkie pytania:

  1. Jaka musi być wartość wyrażenia w pętli while, aby jej treść wykonała się ponownie?
  2. Jaka musi być wartość wyrażenia w pętli do... while, aby jej treść wykonała się ponownie?
  3. Co najmniej ile razy wykona się kod w pętli do... while, a ile w while?
  4. W jaki sposób wykonać określony kod, gdy żądany przez nas warunek nie jest spełniony?
  5. Zamień podane wyrażenia logiczne na ich odwrotności (tj. odwrotność ma dawać true, gdy wyrażenie jest fałszywe i na odwrót) bez użycia operatora negacji !
    • $a == $b
    • ($a > 8) && ($a < 20)
    • ($a == 7) || ($b < 20)
  6. Opisz słownie, co opisują wyrażenia oraz ich odwrotności z poprzedniego ćwiczenia.

Funkcje

edytuj

Poniżej pokazany jest fragment pewnego skryptu. Podana funkcja ma dodawać formatowanie do podanego w argumencie ciągu tekstowego, lecz nie działa zgodnie z zamierzeniami autora. Znajdź błąd i zaproponuj jego rozwiązanie. Jeżeli nie znasz niektórych z funkcji, znajdź je w dokumentacji.

<?php

function formatuj($tekst)
{
   echo '<strong>'.$tekst.'</strong>';
} // end formatuj();

// ...

$dane = file('dane.txt');
echo '<ul>';
foreach($dane as $linia)
{
   $wiersz = explode('|', trim($linia));
   echo '<li>'.formatuj($wiersz[0]).': '.$wiersz[1].'</li>';
}
echo '</ul>';

Ćwiczenia praktyczne

edytuj

Ćwiczenie 1

Matematyczną operację x! (czyt. x silnia) definiujemy następująco:  ,  ,   - innymi słowy jest to mnożenie wyniku przez kolejne liczby naturalne aż do n. Zaimplementuj silnię w PHP w dwóch wariantach: rekurencyjnym oraz przy użyciu pętli for. Spróbuj przy pomocy każdej z nich policzyć silnię z 50 oraz 105. Co zaobserwowałeś?

Ćwiczenie 2

Na wielu stronach internetowych wykorzystuje się podział listy wyników na strony, aby ograniczyć ilość jednocześnie wyświetlanych informacji i przyspieszyć ładowanie strony. Wbrew pozorom, Twoja wiedza jest już w pełni wystarczająca, aby stworzyć taki system. Twoim zadaniem jest napisanie funkcji o następujących właściwościach:

  • Funkcja przyjmuje za argumenty:
    • łączną liczbę elementów,
    • liczba elementów na jedną stronę,
    • numer aktualnej strony.
  • Zwraca tablicę zawierającą:
    • kod HTML z listą dostępnych stron,
    • numer pierwszego elementu wyświetlonego na aktualnej stronie,
    • ilość elementów wyświetlanych na aktualnej stronie (pamiętaj, że na ostatniej stronie nie musi znajdować się pełna liczba elementów).
  • Funkcja musi wygenerować kod HTML z odnośnikami do wszystkich dostępnych stron. Aktualna strona musi być wyróżniona, np. pogrubieniem.
  • Funkcja musi obliczyć, który element powinien wyświetlać się jako pierwszy na aktualnej stronie.

Napisz skrypt testujący napisaną funkcję, który pobiera numer strony z adresu URL. Uwzględnij sytuację, gdy w adresie URL nie ma podanego numeru. Sprawdź, czy funkcja poprawnie zachowuje się, gdy próbujesz podać np. ujemną albo nieistniejącą stronę. Powinieneś zostać wtedy przerzucony do strony pierwszej. Dodaj do skryptu pętlę for, którą zasymulujesz wyświetlane elementy poprzez zwykłe wyświetlanie liczb naturalnych.

Ćwiczenie 3

Dodaj napisany system stronicowania do księgi gości z poprzedniego rozdziału.

Rozmaitości

edytuj
Poprzedni rozdział: Ćwiczenia
Następny rozdział: Podstawy wyrażeń regularnych

Przetwarzanie tekstu

edytuj

Największa ilość wykonywanych w PHP operacji dotyczy danych tekstowych oraz ich obróbki. Poznamy tutaj kilkanaście funkcji, które będą nam pomocne. W następnym rozdziale zaznajomimy się natomiast z podstawami wyrażeń regularnych pozwalających na znacznie bardziej zaawansowane manipulacje oraz dokładniejszą kontrolę poprawności.

Wyszukiwanie ciągów

edytuj

Podstawową funkcją wyszukiwania jednego ciągu w drugim jest strpos() posiadająca także wariant stripos(), w którym nie gra roli wielkość liter. Użyliśmy jej już raz podczas pisania księgi gości do sprawdzenia, czy wprowadzony adres WWW zawiera identyfikator protokołu. Przypomnijmy jeszcze raz ten skrypt:

<?php
 
 	$adresy = array(0 =>
 		'http://www.wikibooks.pl',
 		'www.wikibooks.pl'	
 	);
 	
 	foreach($adresy as $adres)
 	{
 		if(strpos($adres, 'http://') === 0)
 		{
 			// jest http://
 			echo '<p>'.$adres.'</p>';
 		}
 		else
 		{
 			// nie ma http://
 			echo '<p>http://'.$adres.'</p>';
 		}	
 	}
 
?>

Funkcje działają następująco: za pierwszy parametr podajemy ciąg do przeszukania, za drugi - szukany, a następnie patrzymy na rezultat. strpos() powinien zwrócić nam pozycję, od której zaczyna się tekst, którego szukamy, przy czym znaki liczone są od zera. Jeżeli nic nie zostanie znalezione, dostaniemy wartość false. Z tego powodu powinniśmy zawsze wykorzystywać te funkcje z operatorami === oraz !== sprawdzającymi nie tylko wartość, ale i typ zwracanych danych. Oto interesująca sztuczka, która w jednej linijce pozwoli nam na sprawdzenie, czy ciąg został znaleziony i EWENTUALNE pobranie pozycji jego wystąpienia:

<?php
 
 	$zrodlo = 'Litwo, ojczyzno moja!';
 	$szukany = 'moja';
 	
 	if(($id = strpos($zrodlo, $szukany)) !== false) // wlasnie tu
 	{
 		echo 'Szukany ciąg zaczyna się w znaku '.$id;
 	}
 	else
 	{
 		echo 'Nie znaleziono szukanego ciągu';
 	}
 
?>

Jeśli nie rozumiesz zastosowanej tu sztuczki, przypomnij sobie zabawy z wyrażeniami na początku podręcznika. PHP najpierw wykona przypisanie do zmiennej $id: $id = strpos($zrodlo, $szukany). Operator zwróci przypisywaną wartość, która zostanie wykorzystana z kolei do sprawdzenia, czy coś zostało znalezione: [wyrażenie] !== false. W ten sposób mamy jednocześnie wykonane i sprawdzenie, i pozycję pierwszego wystąpienia w zmiennej $id.

Powyższe dwie funkcje znajdują pierwsze wystąpienie szukanego ciągu. Jeżeli chcemy znaleźć ostatnie, musimy posłużyć się funkcjami strrpos() i strripos().

Modyfikacja ciągów

edytuj

Zajmiemy się teraz dosyć rozległą kwestią modyfikacji oraz obróbki tekstu. Jest ona niezwykle ważna, jeżeli zamierzasz pisać aplikacje typu forum dyskusyjnego czy księgi gości. Z wiadomych przyczyn nigdy nie powinniśmy zezwolić internaucie na wypisywanie wszystkiego, co mu się podoba i musimy poddać jego tekst elektronicznej obróbce. Za jej pomocą można również ułatwiać formatowanie tekstu. Przykładowo, treść źródłowa tego podręcznika zapisana jest w specjalnym zestawie znaczników dostosowanym do specyfiki Wikibooks. Odpowiednia biblioteka zajmuje się jego przetwarzaniem na język HTML. Tak złożonymi kwestiami nie będziemy się tu jednak zajmować. Wspomniane zostaną przede wszystkim te funkcje dostępne w PHP. Bardziej zaawansowane algorytmy musisz już zaprogramować samodzielnie.

Nagłówki na stronach internetowych przybierają różne style, w zależności od pełnionych funkcji. Część z nich pisana jest kapitalikami, w innych z dużych liter zaczyna się każdy wyraz. Wcale nie musimy zmuszać redaktorów, by to oni zajmowali się wprowadzaniem tytułów w wymaganej postaci. Istnieją odpowiednie algorytmy, które wykonają to za nich. Choć obecnie zadanie sformatowania tytułów można z powodzeniem zrealizować już z wykorzystaniem CSS-a, PHP posiada szereg funkcji do konwersji dużych liter na małe. Pamiętajmy wszak, że PHP to nie tylko strony internetowe.

<?php
 
 	$tekst = 'php jest językiem programowania skryptowego zaprojektowanego dla stron internetowych';
 	
 	echo '<p>Kapitaliki: '.strtoupper($tekst).'</p>';
 	echo '<p>Dużą literą: '.ucfirst($tekst).'</p>';
 	echo '<p>Każdy wyraz dużymi literami: '.ucwords($tekst).'</p>';
?>

Mamy tutaj tekst zapisany w zmiennej (symulującej jakieś źródło danych, ponadto wykorzystamy go kilkakrotnie, stąd ta zmienna) złożony z samych małych liter. Za pomocą funkcji strtoupper() zamieniamy je wszystkie na duże. Odwrotną operację wykonuje strtolower(). ucfirst() zamienia na dużą literę początek pierwszego wyrazu ciągu, a ucwords() - początek każdego wyrazu. Doświadczenie podpowiada nam, że otrzymywane dane nie zawsze są tak klarowne. Jeżeli potrzebne nam automatyczne kapitalizowanie początków zdań, musimy sami napisać odpowiedni algorytm.

Jeżeli chcemy wyciąć fragment jednego ciągu, aby np. poddać go bardziej szczegółowej obróbce, z pomocą przychodzi nam substr(). Podajemy w niej pozycję, od której zamierzamy ciąć oraz (opcjonalnie) ilość znaków do pobrania. Domyślnie funkcja kończy wycinanie na końcu ciągu. W przykładzie połączymy ją z funkcją strpos() do wycięcia nazwiska z personaliów pewnego człowieka.

<?php
 
 	$nazwa = 'Janusz Kowalski';
 	
 	if(($id = strpos($nazwa, ' ')) !== false)
 	{
 		$nazwisko = substr($nazwa, $id + 1); // +1, by zacząć wycinanie od jednego miejsca za znalezioną spacją
 	}
 	else
 	{
 		$nazwisko = '';
 	}
 	
 	echo $nazwisko;
 
?>

W personaliach odnajdujemy spację, która posłuży nam jako marker. strpos() zwróci nam jej pozycję. Przekazujemy ją do substr() jako punkt startowy wycinania i pobieramy ilość znaków równą długości personaliów minus pozycji spacji. W ten sposób w nasze ręce wpadnie nazwisko i będziemy mogli zrobić z nim, co tylko chcemy. Alternatywny sposób rozwiązania tego problemu polega na rozbiciu tego ciągu na tablicę poznaną już funkcją explode(). Przydaje się on, kiedy oprócz nazwiska pragniemy otrzymać także drugi ciąg z imieniem - tu pasuje ona, jak znalazł.

Teraz coś bardziej praktycznego. Na wielu witrynach internetowych można spotkać emotikonki zamieniane na obrazki. Jeżeli nie interesują nas żadne dodatkowe fajerwerki, napisanie konwertera jest bajecznie proste. PHP posiada funkcję str_replace() zamieniającą jeden fragment ciągu na drugi (jej wariant, str_ireplace() nie rozróżnia wielkości liter).

<?php
 
 	$post = 'Tak, zaiste jesteś bardzo zdolny :). Jeszcze nad praktyką popracuj :].';
 
 	// Pierwszy sposób zamiany
 	
 	echo '<p>'.str_replace(':)', '<img src="smile.gif"/>', $post).'</p>';
 	
 	// Drugi, kilka naraz
 	
 	echo '<p>'.str_replace(array(
 		':)',
 		':]',
 		':('	
 	), array(
 		'<img src="smile.gif"/>',
 		'<img src="eye.gif"/>',
 		'<img src="sad.gif"/>'	
 	), $post).'</p>';
 
?>

Pierwszym parametrem jest szukany ciąg, drugim - ciąg docelowy, a trzecim - ciąg, na którym operujemy. Możemy zdefiniować tylko jeden wzorzec do podmiany albo całą grupę, którą podajemy w postaci tablic, jak na przykładzie. Zwróć uwagę, że w przeciwieństwie do omawianego wcześniej substr(), tutaj ciąg, na którym operujemy, wskazywany jest dopiero na końcu.

Zajmując się księgą gości, dbaliśmy, aby do naszych wpisów nie przedostał się HTML. Funkcja htmlspecialchars() zamieniała wtedy znaki specjalne HTML-a na encje, przez co nie mogły być one przetworzone i pojawiały się we wpisie jako statyczny tekst. Okazuje się, że nie jest to jedyne dostępne nam rozwiązanie. Możemy znaczniki wyrzucić całkowicie. Różne warianty prezentuje poniższy przykład:

<?php
 
 	$post = '   To jest post
 	kilkulinijkowy. Który należy przerobić tak,
 	aby go <b>fajnie wyświetlać</b>';
 	
 	// Wariant 1
 	
 	echo '<p>'.nl2br(htmlspecialchars(trim($post))).'</p>';
 	
 	// Wariant 2
 	
 	echo '<p>'.nl2br(strip_tags(trim($post))).'</p>';
 
?>

Zarówno strip_tags(), jak i trim() mają pewne ciekawe dodatkowe parametry, o których nie wszyscy wiedzą. Przy wycinaniu znaczników możemy określić, które z nich mają być zostawiane, np. strip_tags($tekst, '<b><i><u><a>') pozostawi nam pogrubianie, kursywę, podkreślanie oraz linki. trim() natomiast niekoniecznie musi wycinać białe znaki - wystarczy, że podamy nasz własny zestaw jako drugi parametr, a możemy oszczędzić sobie dużo pracy.

Kiedy powstawały komputery, powstała konieczność stworzenia uniwersalnego standardu kodowania znaków alfabetu za pomocą kodów liczbowych rozumianych przez maszyny. Umożliwiłoby to pisanie dokumentów, które da się przenosić między maszynami bez konieczności ich konwersji. Tak narodził się siedmiobitowy standard ASCII zawierający 128 kodów. Jego cechą szczególną było istnienie tzw. kodów sterujących (od 0 do 32), dzięki którym można było nawet sterować pracą niektórych urządzeń. W latach późniejszych wykorzystano także ósmy bit (kody od 128 do 255). Jego wykorzystanie różni się w zależności od wariantu systemu kodowania.

PHP udostępnia kilka funkcji ułatwiających pracę z ASCII. Na początku zapoznamy się z funkcjami chr() oraz ord() konwertujących kod na odpowiadający mu znak i vice versa. Oto prosty skrypt , wykonujący to zadanie i pobierający dane z formularza HTML.

<?php
 
 	if($_SERVER['REQUEST_METHOD'] == 'POST')
 	{ // 1
 		if($_POST['kierunek'] == 0) // 2
 		{
 			if(strlen($_POST['dane']) != 1)
 			{
 				die('<p>Nieprawidłowe dane. Proszę podać JEDEN znak!</p>');			
 			}
 			echo '<p>Kod znaku <b>'.$_POST['dane'].'</b> to '.ord($_POST['dane']).'</p>';
 		}
 		else
 		{ // 3
 			if(!ctype_digit($_POST['dane']))
 			{
 				die('<p>Nieprawidłowe dane. Proszę podać kod znaku!');			
 			}
 			echo '<p>Kodowi <b>'.$_POST['dane'].'</b> odpowiada znak '.chr($_POST['dane']).'</p>';		
  		}	
 	}
 	else
 	{ // 4
 		echo '<form method="post" action="ascii.php">
 		<h2>Informator ASCII</h2>
 		<select name="kierunek">
 		<option value="0">Sprawdź kod znaku</option>
 		<option value="1">Sprawdź znak pod kodem</option>
 		</select>
 		<input type="text" name="dane"/>
 		<input type="submit" value="OK"/>		
 		</form>';	
 	}
?>

Formularz przesyła nam dwie informacje:

  • Kierunek - określa, czy konwertujemy znak na kod, czy też kod na znak.
  • Dane - znak albo kod do zamiany.

Oto omówienie działania:

  1. Dotarły do nas dane z formularza.
  2. Konwersja ze znaku na kod. Sprawdzamy, czy pole dane zawiera dokładnie jeden znak. Jeśli tak, wyświetlamy wynik.
  3. Konwersja z kodu na znak. Funkcją ctype_digit() sprawdzamy, czy wprowadzony kod składa się wyłącznie z cyfr. Jeśli tak, wyświetlamy wynik.
  4. Formularz do komunikacji ze skryptem.

W powyższym przykładzie wykorzystaliśmy funkcję ctype_digit(), która szybko bada, czy podany ciąg składa się wyłącznie z liczb. Istnieje kilkanaście funkcji z serii ctype. Oto kilka z nich:

  • ctype_alpha($tekst) - sprawdza, czy tekst składa się wyłącznie z liter.
  • ctype_lower($tekst) - sprawdza, czy tekst składa się wyłącznie z małych liter.
  • ctype_upper($tekst) - sprawdza, czy tekst składa się wyłącznie z dużych liter.
  • ctype_xdigit($tekst) - sprawdza, czy tekst zawiera wyłącznie znaki do zapisu liczb w systemie szesnastkowym (heksadecymalnym).

Jeśli żądany przez Ciebie zestaw znaków nie jest udostępniany przez żadną z funkcji, możesz skorzystać z odpowiedniego algorytmu:

<?php
 
 	function ctype($ciag, $zestaw)
 	{
 		for($i = 0; $i < strlen($ciag); $i++)
 		{
 			if(strpos($zestaw, $ciag{$i}) === FALSE)
 			{
 				return false;
 			}
 		}
 		return true;		
 	} // end ctype();
 
?>

Algorytm jest bardzo prosty. Przeszukujemy wprowadzony ciąg znaków $ciag i sprawdzamy funkcją strpos(), czy każdy jego znak zawiera się w zestawie $zestaw. Jeśli nie, przerywamy pracę i zwracamy false. Oto przykładowe użycie:

echo ctype('555-1234', '0123456789-');

Jeżeli poszukujesz jeszcze bardziej złożonych związków między znakami, musisz posłużyć się wyrażeniami regularnymi, z których podstawami zaznajomi Cię następny rozdział.

Formatowanie tekstu

edytuj

Korzenie PHP znajdują się w języku C, więc nic dziwnego, że dziedziczy on po nim funkcję printf() służącą do prezentacji sformatowanych danych.

<?php
 
 	printf('Liczba szesnastkowa: %x', 3342);
 
?>

Funkcja odnajduje w podanym ciągu specjalne kody formatujące rozpoczynające się od znaku procentu i umieszcza na ich miejscu dane z kolejnych parametrów. W powyższym przykładzie zastosowaliśmy kod %x, który spowoduje wyświetlenie się liczby 3342 w systemie szesnastkowym. Przeglądarka wyświetli zatem:

Liczba szesnastkowa: d0e

Za pomocą kodów formatujących możemy też np. określić precyzję wyświetlania ułamków. Aby wyświetlić liczbę π do 4 miejsc po przecinku, napiszemy:

<?php
 
 	printf('Liczba PI: %0.4f', M_PI);
 
?>

To są jedynie podstawy kodów formatujących. Szczegółowy ich opis znajduje się w dokumentacji PHP. Warto nadmienić także istnienie odmian funkcji printf(), np.

  • sprintf() - zwraca wynik jako ciąg tekstowy.
  • vprintf() - pobiera dane do kodów z tablicy przekazanej jako drugi parametr.
  • vsprintf() - pobiera dane do kodów z tablicy przekazanej drugim parametrem i zwraca wynik jako ciąg tekstowy.

Kodowanie

edytuj

Teraz nieco o przechowywaniu haseł użytkowników przez PHP. Generalnie nigdy nie składuje się ich w postaci jawnej oraz nie koduje się algorytmem dwukierunkowym (czyli dającym teoretyczną możliwość ich rozszyfrowania). Powszechna praktyka poleca stosowanie tzw. funkcji haszujących generujących unikalne, równej długości sygnatury niszczące oryginalny przekaz, przez co nie da się ich już rozszyfrować metodą inną, niż sprawdzenie wszystkiego na wszystkim. Wbrew początkowemu wrażeniu sens takiego działania jest bardzo oczywisty. Kiedy użytkownik rejestruje się w serwisie, haszujemy jego hasło i zapisujemy w profilu. Próbując się zalogować, przysyła nam swoje hasło jeszcze raz. Haszujemy je i wynik porównujemy z tym, co mamy w bazie. Jeżeli uzyskamy identyczne hasze, znaczy to, że użytkownik podał hasło poprawnie i może zostać zalogowany.

PHP posiada wbudowanych kilka algorytmów haszujących:

  • crypt() - najstarszy, generuje 8-znakowe hasze. Nie polecamy do celów autoryzacji.
  • md5() - do niedawna najpopularniejszy algorytm. Generował 32-znakowe ciągi, lecz niedawno ujawniono w nim poważne dziury.
  • sha1() - aktualnie najbezpieczniejszy algorytm haszujący dostępny domyślnie w PHP. Generuje 40-znakowe ciągi.

Zwróć uwagę na jedną rzecz: hasze mają stałą długość, dlatego ilość możliwych kombinacji jest ograniczona. Tymczasem możliwych ciągów jest nieskończenie wiele. Dlatego może się zdarzyć, że dwa różne ciągi generują ten sam hasz i zwiemy to kolizją. W dobrym algorytmie, aby doszło do takiej sytuacji, muszą to być naprawdę różne ciągi. Ponadto zmiana już pojedynczego znaku wewnątrz haszowanego ciągu musi powodować utworzenie zupełnie innego wyniku. Siła algorytmu zależy od tego, jak dużo czasu potrzeba na wykrycie kolizji metodą brute-force, czyli sprawdzenia wszystkiego na wszystkim. Algorytm MD5 został już złamany na tyle, że zwyczajny komputer PC jest w stanie znaleźć kolizję już w ciągu zaledwie ośmiu godzin. Dla SHA1 ilość niezbędnych do sprawdzenia kombinacji jest rzędu 2^64, z czym nie jest w stanie poradzić sobie żaden istniejący obecnie komputer.

Przypuśćmy, że $uzytkownicy jest tablicą asocjacyjną taką, że indeks jest nazwą użytkownika, a wartość hasłem zaszyfrowanym w SHA1. Aby sprawdzić, czy użytkownik wpisał poprawne hasło, musimy wykonać następującą czynność:

<?php

    if(isset($uzytkownicy[$_POST['login']]) && $uzytkownicy[$_POST['login']] == sha1($_POST['haslo']))
    {
        echo 'Dziękujemy, podałeś dobre hasło.';
    }
    else
    {
       echo 'Nieprawidłowy login i/lub hasło.';
    }

?>

Skonstruowanie systemu autoryzacji wymaga także zaimplementowania mechanizmu sesji, aby przekazać fakt bycia zalogowanym między poszczególnymi żądaniami. Zajmiemy się tym w dalszych rozdziałach.

Poprzedni rozdział: Przetwarzanie tekstu
Następny rozdział: Obsługa ciastek

Podstawy wyrażeń regularnych

edytuj

W poprzednim rozdziale poznaliśmy proste techniki wyszukiwania i manipulacji danymi tekstowymi. Ponadto dowiedzieliśmy się o funkcjach z serii ctype pozwalających na prostą kontrolę zawartości ciągu pod kątem występujących w nim znaków. Jak sam zapewne zdążyłeś zauważyć, narzędzia te nie posiadają jednak żadnych zaawansowanych funkcji. Czy wobec tego możliwe jest manipulowanie ciągami o złożonej strukturze? Okazuje się, że tak - dzięki wyrażeniom regularnym, których omówieniem zajmuje się niniejszy rozdział.

Istota wyrażeń regularnych

edytuj

Mechanizm wyrażeń regularnych (ang. regular expressions, czasem skracane do regexp) jest tak naprawdę parserem pewnego języka służącego do precyzyjnego definiowania dozwolonego formatu ciągu. Korzystanie z wyrażeń regularnych polega na stworzeniu za jego pomocą tzw. wzorca, a następnie jego porównania odpowiednimi funkcjami z interesującym nas ciągiem. Na wyjściu otrzymujemy informację, czy ciąg pasuje do wzorca, czy też nie.

Wyrażenia regularne mają jeszcze większe możliwości. Dzięki nim wyciągnięcie dowolnych interesujących nas informacji z ciągu nie stanowi kłopotu. Wystarczy, że znamy wzorzec go opisujący, a system wyrażeń zwróci nam dodatkowo tablicę uzyskanych z jego wnętrza danych, których potrzebujemy. Wyrażenia regularne dają nam także dostęp do znacznie bogatszego w możliwości kuzyna funkcji str_replace() z poprzedniego rozdziału. O ile tamta funkcja bezmyślnie zamieniała wszystkie napotkane wystąpienia jakiegoś fragmentu na inny, dzięki wyrażeniom regularnym możemy zdefiniować naprawdę wymyślne mechanizmy zamiany uwzględniające wiele dodatkowych czynników.

Jak widać, wyrażenia regularne to potężne narzędzie, jednak przez to też skomplikowane. Niemniej każdy szanujący się programista powinien znać przynajmniej jego podstawy, ponieważ praktyka zawodowa pokazuje, iż wykorzystywane są one bardzo często.

Pierwszy przykład

edytuj

Nasze pierwsze spotkanie praktyczne z wyrażeniami regularnymi rozpoczniemy od prostego sprawdzenia, czy wypełnione pole formularza zawiera dokładnie jedną cyfrę. Do porównywania wzorca z ciągiem służy funkcja preg_match(), która zwraca ilość wystąpień ciągu według podanego wzorca.

<?php

	if($_SERVER['REQUEST_METHOD'] == 'POST')
	{
		if(preg_match('/^[0-9]$/D', $_POST['cyfra']))
		{
			echo '<p>Wpisałeś cyfrę '.$_POST['cyfra'].'</p>';
		}
		else
		{
			echo '<p>Nieprawidłowe dane! Skrypt wymaga podania cyfry!</p>';
		}
	}
	else
	{
		echo '<form method="post" action="preg1.php">
			Podaj cyfrę: <input type="text" name="cyfra"/><input type="submit" value="OK"/>
			</form>';	
	}
?>

Wykorzystaliśmy tutaj wzorzec /^[0-9]$/. Zawarty jest on wewnątrz ograniczników /. Poza nimi mogą znajdować się jedynie dodatkowe flagi kontrolne i nic więcej. Znak ^ oznacza początek ciągu, a znak $ koniec lub "prawie" koniec zezwalając na zakończenie wyrażenia przejściem do nowej linii \n. Zastosowane dodatkowo D wymusza interpretację $ jako bezwzględnego końca wyrażenia (możliwość dodania \n w niektórych przypadkach może być luką w bezpieczeństwie skryptu). [0-9] definiuje klasę dozwolonych znaków, jakie mogą pojawić się w danym miejscu. Ostatecznie wzorzec ten opisuje wszystkie ciągi składające się z DOKŁADNIE jednego znaku będącego cyfrą z przedziału 0 do 9.

Istnieje jeszcze jeden sposób powiadomienia parsera, ile znaków chcemy tam widzieć. Jest nim użycie kwantyfikatorów zasięgu. Ich składnia jest następująca:

  • {długość} - dozwolona długość określona jest dokładnie.
  • {długość_min,długość_max} - podany jest przedział dozwolonych długości
  • {długość_min,} - określona jest minimalna długość
  • {,długość_max} - określona jest maksymalna długość

Kwantyfikator umieszczamy po znaku lub klasie dozwolonych znaków, zatem nasze wyrażenie będzie miało postać /^[0-9]{1}$/. W wyrażeniach regularnych można stosować kilka predefiniowanych kwantyfikatorów:

  • * - 0 lub więcej
  • + - 1 lub więcej
  • ? - 0 lub 1 (uwaga: znak ten jest także wykorzystywany w innym kontekście)

Klasy znaków

edytuj

Nauczymy się teraz bardziej dokładnego definiowania klas znaków, jakich można używać w danym miejscu ciągu. Zasada podstawowa jest bardzo prosta: jeśli w jakimś miejscu napiszemy "a", to parser będzie się tam spodziewać litery "a" występującej dokładnie jeden raz. Jeżeli zastosujemy klasę znaków, definiujemy w ten sposób listę dozwolonych na danej pozycji znaków dokładnie jeden raz. W obu przypadkach "dokładnie jeden raz" można zmienić na dowolną inną długość za pomocą omówionych wyżej kwantyfikatorów. Zatrzymajmy się jednak dokładniej przy tym zwrocie. Skoro dokładnie jeden raz, czemu w takim razie podany wyżej przykład dla wyrażenia /[0-9]/ akceptuje ciągi liczb o dowolnej długości? Aby lepiej pokazać, co naprawdę wtedy ma miejsce, wpisz w formularzu tekst "9a" - o dziwo także i on zostanie przyjęty, mimo że na drugiej pozycji mamy literę! Co jest nie tak? Nic - wyrażenie działa prawidłowo. Parser po prostu osiągnął jego koniec przy sprawdzeniu pierwszego znaku ciągu i resztę przepuścił bez żadnej kontroli. Dlatego istotne jest powiadomienie o tym, gdzie ma znajdować się koniec ciągu.

Tworząc klasę znaków, możemy stosować się do następujących reguł:

  • Wypisujemy w nawiasach kwadratowych wszystkie dopuszczalne znaki, np. [abcdefgh]
  • Wprowadzamy zakres: [a-h] (dopuszczalne małe litery od "a" do "h")
  • Wprowadzamy kilka zakresów: [a-hA-H] (dopuszczalne duże i małe litery od "a" do "h" i od "A" do "H").

Aby wprowadzić jakiś znak specjalny do klasy, poprzedzamy go backslashem: [a-hA-H\-] - znaki duże i małe od "a" do "h" wraz z pauzą. Dysponując tymi wiadomościami, jesteśmy już w stanie napisać pierwszą funkcję kontrolującą (w ograniczonym stopniu) poprawność adresu e-mail:

<?php

	if($_SERVER['REQUEST_METHOD'] == 'POST')
	{
		if(preg_match('/^[a-zA-Z0-9\.\-_]+\@[a-zA-Z0-9\.\-_]+\.[a-z]{2,4}$/D', $_POST['email']))
		{
			echo '<p>Wpisałeś e-mail '.$_POST['email'].'</p>';
		}
		else
		{
			echo '<p>Nieprawidłowe dane! Skrypt wymaga podania adresu e-mail!</p>';
		}
	}
	else
	{
		echo '<form method="post" action="preg2.php">
			Podaj adres e-mail: <input type="text" name="email"/><input type="submit" value="OK"/>
			</form>';	
	}
?>

Omówmy sobie poszczególne partie tego wyrażenia:

  • /^[a-zA-Z0-9\.\-_]+ - początek adresu składa się z dowolnych znaków alfanumerycznych, kropki, pauzy oraz podkreślenia i jego długość musi wynosić minimum 1 znak.
  • \@ - później ma być małpa
  • [a-zA-Z0-9\.\-_]+ - analogicznej klasy używamy do zdefiniowania domeny.
  • \.[a-z]{2,4}$/ - domena musi kończyć się kropką, po której spodziewamy się domeny nadrzędnej (np. .pl, .com).

W pełni poprawne wyrażenie sprawdzające poprawność adresu jest znacznie bardziej skomplikowane. Zainteresowanych odsyłamy do odpowiedniego dokumentu RFC definiującego je.

PCRE posiada kilka klas predefiniowanych:

  • . - kropka symbolizuje dowolny znak (za wyjątkiem przełamania linii).
  • \d - dowolna cyfra dziesiętna
  • \D - dowolny znak niebędący cyfrą
  • \s - biały znak (np. spacja, tabulator)

Predefiniowane klasy można ze sobą łączyć wewnątrz nawiasów kwadratowych: [\d\s] - dozwolone cyfry dziesiętne oraz białe znaki. Jeżeli po otwierającym nawiasie kwadratowym pojawi się symbol ^, będzie to oznaczać negację klasy: "wszystkie znaki, które NIE należą do wymienionych". Jak zdefiniowałbyś "dowolny znak niebędący cyfrą", czyli klasę \D w tradycyjny sposób?

Poszczególne fragmenty ciągu mogą być ze sobą łączone w większe grupy, ujmowane w okrągłych nawiasach. Są one wykorzystywane w dwóch celach. Po pierwsze, można do nich zbiorczo zastosować kwantyfikator, żądając, aby np. jakiś fragment powtarzał się od 3 do 5 razy. Za pomocą grup eksportujemy także do PHP interesujące nas dane. Przykładowo, do wyrażenia /^(abc)+$/ pasują ciągi "abc", "abcabc", "abcabcabc" itd.

Przedstawimy teraz, jak wykorzystać wyrażenia regularne w innych dziedzinach, niż tylko kontrola formularzy. Załóżmy, że zlecono nam zadanie przeprojektowania bazy danych, ponieważ stara nie spełnia stawianych jej wymagań. Oczywiście musimy napisać jakiś konwerter, który przeniesie automatycznie dane do nowej bazy. Natknęliśmy się jednak na problem: daty utworzenia rekordów zapisywane są w postaci tekstowej, np. "12 Dec 2006, 16:34", zamiast w łatwych do przetwarzania sekundach od 1.1970. Do rozbicia ciągu na poszczególne fragmenty wykorzystamy wyrażenia regularne:

<?php

	$date = '12 Dec 2006, 16:34';

	if(preg_match('/^(\d{1,2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4})\, (\d{1,2})\:(\d{1,2})/', $date, $found))
	{
		// Co nam zwrocilo...
		echo '<h3>Dane: "'.$date.'"</h3>';
		echo '<p>Dzien: '.$found[1].'</p>';
		echo '<p>Miesiac: '.$found[2].'</p>';
		echo '<p>Rok: '.$found[3].'</p>';
		echo '<p>Godzina: '.$found[4].'</p>';
		echo '<p>Minuta: '.$found[5].'</p>';

		$monthConverter = array('Jan' => 1, 'Feb' => 2, 'Mar' => 3, 'Apr' => 4, 'May' => 5,
			'Jun' => 6, 'Jul' => 7, 'Aug' => 8, 'Sep' => 9, 'Oct' => 10, 'Nov' => 11, 'Dec' => 12);

		echo '<p>Unix timestamp: '.mktime($found[4], $found[5], 0, $monthConverter[$found[2]], $found[1], $found[3]).'</p>';
	}
	else
	{
		echo '<p>Nieprawidłowy format daty!</p>';
	}

?>

W zastosowanym wyrażeniu regularnym pojawia się symbol | - jest to operator alternatywy. Ciąg Jan|Feb|Mar oznacza, że w tym miejscu możemy mieć "Jan" ALBO "Feb" ALBO "Mar". Zauważ, że wszystkie istotne elementy daty zawarliśmy w grupach, a do samej funkcji preg_match() podaliśmy trzeci parametr. Do podanej tam zmiennej zostanie przypisana tablica z treścią pasującego ciągu na indeksie 0 oraz wartościami wszystkich użytych grup na kolejnych indeksach. Teraz możemy już łatwo przekonwertować funkcją mktime() naszą datę na format uniksowy.


Dodatkowe rozwiązania

edytuj

Z biegiem czasu korzystanie z wbudowanych funkcji w PHP staje się coraz mniej opłacalne ze względu na ich niewiarygodną obsługę błędów (poleganie na warningach i errorach, zamiast na wyjątkach) oraz małe możliwości. W nowych projektach powinniśmy korzystać z bibliotek przeznaczonych specjalnie do tego celu, np. biblioteki T-Regx.

  Ta sekcja jest zalążkiem. Jeśli możesz, rozbuduj ją.
Poprzedni rozdział: Podstawy wyrażeń regularnych
Następny rozdział: Sesje

Obsługa ciastek

edytuj

Czym są nagłówki HTTP?

edytuj

Serwer w odpowiedzi na żądanie HTTP wysyła nie tylko kod HTML, lecz także zestaw nagłówków pomagających przeglądarce na zidentyfikowanie dostarczanych treści. Nagłówki precyzują typ MIME danych, np. text/html dla dokumentu HTML, kodowanie znaków, zachowanie się serwerów proxy, ustawienia cache'owania danych itd. PHP posiada funkcję header() pozwalającą skryptowi wysłać własne nagłówki. Muszą być one jednak zdefiniowane przed wysłaniem jakiegokolwiek kodu HTML (są to przecież NAGŁÓWKI). W przeciwnym wypadku dostaniemy niemiły komunikat Cannot add header information. Programiści często wykorzystują nagłówki do zdefiniowania typu oraz kodowania w nadsyłanym dokumencie:

<?php

    // Bedziemy wysylac dokument HTML z kodowaniem UTF-8
    header('Content-type: text/html;charset=utf-8');

?>

Ale nie tylko. Wiele stron udostępnia swe archiwa nie poprzez bezpośredni dostęp do katalogu, w którym są trzymane, ale poprzez specjalny skrypt, który wysyła ich zawartość do ściągnięcia. Nagłówki umożliwiają powiadomienie przeglądarki, że teraz będzie szedł plik o takiej i takiej nazwie, który internauta chce pobrać na swój dysk twardy. Nie będzie on jednak ujawniać katalogu na serwerze, gdzie on się znajduje. Cała komunikacja prowadzona jest za pośrednictwem PHP.

<?php
 
 	if(!isset($_GET['plik'])) // 1
 	{
 		die('Podaj nazwę pliku!');
 	}
 
 	$_GET['plik'] = basename($_GET['plik']); // 2
 	
 	if(@is_file('./pdf/'.$_GET['plik']))
 	{
 		// 3
 		header('Content-type: application/pdf');
 		header('Content-Disposition: attachment; filename="'.$_GET['plik'].'"');
 		readfile('./pdf/'.$_GET['plik']);
 	}
 	else
 	{
 		// 4
 		header('HTTP/1.1 404 Not Found');
 		exit('Nie znaleziono pliku '.htmlspecialchars($_GET['plik'])); // możesz wypisać całą treść strony z komunikatem o błędzie.
 	}
 
?>

Powyższy kod może być częścią jakiegoś serwisu, który swoje artykuły udostępnia także w formacie PDF do ściągnięcia, gdyż aktualnie tylko takie pliki można nim wysyłać. Przeanalizujmy go krok po kroku.

  1. Na początku dokonujemy sprawdzenia, czy ktoś w ogóle zainteresował się podaniem nazwy dokumentu do pobrania.
  2. Bezpieczeństwo na miejscu pierwszym - wszystko, co jest nazwą pliku i pochodzi od internauty, powinno być przepuszczone przez funkcję basename(), która wyciągnie z niego wyłącznie nazwę i odrzuci jakieś przejścia między katalogowe, co mogłoby zagrozić bezpieczeństwu. Wyobraź sobie, że ktoś wpisze sobie np. ../strona_hasla.php. Bez tego zabezpieczenia dostałby hasła dostępu do naszej strony, lecz basename() odrzuci niebezpieczny fragment ../. Mamy więc pewność, że internauta będzie ściągać TYLKO i WYŁĄCZNIE to, co chcemy, aby ściągał.
  3. Jeśli stwierdzimy, że plik istnieje, powiadamiamy nagłówkami, że oto nadejdzie dokument PDF jako załącznik o odpowiedniej nazwie. Funkcją readfile() wysyłamy jego zawartość.
  4. Gdyby ktoś podał niewłaściwą nazwę pliku, możemy wysłać mu komunikat błędu 404.

Powyższy przykład możemy nieco przerobić tak, aby z powodu podania błędnej nazwy internauta odsyłany był do naszego własnego komunikatu. Nagłówki umożliwiają robienie przekierowań HTTP i właśnie pragniemy pokazać, jak to się robi.

<?php
 
 	if(!isset($_GET['plik']))
 	{
 		die('Podaj nazwę pliku!');
 	}
 
 	$_GET['plik'] = basename($_GET['plik']);
 	
 	if(@is_file('./pdf/'.$_GET['plik']))
 	{
 		header('Content-type: application/pdf');
 		header('Content-Disposition: attachment; filename="'.$_GET['plik'].'"');
 		readfile('./pdf/'.$_GET['plik']);
 	}
 	else
 	{
 		header('Location: http://localhost/~kurs/notfound.php');
 		exit;
 	}
 
?>

Źródło jest zasadniczo podobne do poprzedniego przykładu. Zmiany widać jedynie w bloku else, gdzie wysyłamy tym razem nagłówek Location. Informuje on przeglądarkę, że treści nie będzie i powinna raczej skontaktować się z podanym plikiem. Innymi słowy, robimy przekierowanie internauty pod inny adres. Protokół HTTP 1.1 wymaga, aby w nagłówku był podany pełen adres do żądanego zasobu. HTTP 1.0 nie miał takich ograniczeń.

Kiedy omówiliśmy sobie już właściwości oraz niektóre możliwości nagłówków, możemy przejść do ciastek (ang. cookies) ustawianych właśnie za ich pomocą.

Ciastka w PHP

edytuj

Chyba każdy internauta słyszał o ciastkach i wyolbrzymianych "zagrożeniach" z nimi związanych. W rzeczywistości są to zwyczajne informacje umieszczane przez witryny WWW w przeglądarkach po to, aby był do nich dostęp między wywołaniami kolejnych podstron w obrębie witryny. Jedyny problem może pojawić się z tzw. ciastkami publicznymi, które może odczytać każda strona w Internecie. Jednak poza tym jest to bardzo pożyteczne narzędzie wykorzystywane m.in. w autoryzacji użytkowników.

Ciastka są ustawiane za pomocą nagłówków HTTP i mają pewien określony termin ważności. Po jego upływie przestają istnieć. Do wysyłania ciastek służy w PHP funkcja setcookie(), a do pobierania wartości tych ustawionych przez wcześniejsze żądania HTTP - specjalna tablica $_COOKIE. Napiszemy teraz prosty skrypt, który robił furorę kilka lat temu, w okresie popularyzacji dynamicznych witryn WWW. Chodzi o umieszczenie prostej informacji dot. ostatniej wizyty internauty u nas. Aby to wykonać, wystarczy przy pierwszym wejściu umieścić na np. miesiąc ciastko z datą ostatniej wizyty, po czym ją sukcesywnie odczytywać.

<?php
 	if(!isset($_COOKIE['wizyta']))
 	{
 		setcookie('wizyta', time(), time() + 30 * 86400);
 		echo 'Witaj, gościu.';
 	}
 	else
 	{
 		setcookie('wizyta', time(), time() + 30 * 86400);
 		echo 'Witaj, ostatni raz odwiedziłeś nas '.date('d.m.Y, H:i', $_COOKIE['wizyta']);	
 	}
 
?>

Trzy pierwsze parametry setcookie() są najważniejsze (ma ona ich trochę więcej). Jest to kolejno: nazwa ciastka, jego wartość oraz data ważności w sekundach od 1.1.1970. Data, a nie okres ważności, stąd przy jego ustawianiu przydaje się funkcja time(). W powyższym skrypcie sprawdzamy, czy ustawialiśmy już ciastko dla danego internauty. Jeśli nie, tworzymy je i wyświetlamy komunikat powitania. W przeciwnym wypadku także aktualizujemy wartość, ale też wyświetlamy datę ostatniej wizyty odczytaną właśnie z ciastka.

Zauważ, że wywołanie setcookie() nie nadpisuje wartości w tablicy $_COOKIE. Dlatego bez problemu można uprościć powyższy skrypt:

<?php
 	setcookie('wizyta', time(), time() + 30 * 86400);
 	if(!isset($_COOKIE['wizyta']))
 	{
 		echo 'Witaj, gościu.';
 	}
 	else
 	{
 		echo 'Witaj, ostatni raz odwiedziłeś nas '.date('d.m.Y, H:i', $_COOKIE['wizyta']);	
 	}
 
?>

Aby istniejące ciastko skasować, wywołujemy funkcję setcookie() z jakąś przeszłą datą ważności:

<?php
 
 	setcookie('wizyta', '', 0);
 
?>

Funkcje buforowania wyjścia

edytuj

Istnieją sytuacje, kiedy musimy wysłać jakiś kod HTML przed wysłaniem nagłówków HTTP, jednak tradycyjne metody nie pozwalają na to. W PHP można ten problem omijać, używając funkcji buforowania wyjścia. Ogólnie rzecz biorąc, przechwytują one treść wysyłaną przez echo albo print, zapisując ją do specjalnego bufora. Opróżniamy go samodzielnie na samym końcu skryptu, symulując efekt równoczesnego wysyłania nagłówków i kodu HTML. Pokażemy to na przykładzie:

<?php
 
 	ob_start();
 
 	echo '<h1>Tytuł witryny</h1><p>I inne komendy HTML.</p>';
 	
 	setcookie('wizyta', time(), time() + 30 * 86400);
 	if(!isset($_COOKIE['wizyta']))
 	{		
 		echo '<p>Witaj, gościu.</p>';
 	}
 	else
 	{
 		echo '<p>Witaj, ostatni raz odwiedziłeś nas '.date('d.m.Y, H:i', $_COOKIE['wizyta']).'</p>';	
 	}
 
 	ob_end_flush();
 
?>

Zwróć uwagę, że przed stworzeniem ciastka skrypt wysyła już kod HTML. Dlatego wszystkie instrukcje zawarliśmy między funkcjami ob_start() i ob_end_flush(). Pierwsza inicjuje buforowanie wyjścia, a druga kończy je, wysyłając jego zawartość do przeglądarki.

Buforowanie wyjścia może też posłużyć do celów algorytmicznych, kiedy musimy przechwycić wysyłany kod, aby go jeszcze dodatkowo obrobić. Naraz można mieć otwartych kilka buforów działających zgodnie z zasadą stosu, tj. ostatni otwarty bufor będzie pierwszym, z którego pobierzemy zawartość. Poniżej prezentujemy mały skrypt cenzorski. Przechwytuje on tekst i cenzuruje go, chyba że internauta zna sposób aktywujący prawdziwą treść. Jest to coś w sam raz dla walczących z dyktaturami opozycjonistów.

<?php
 	ob_start();	
 	
  	echo '<p>Pan Jan Nowak jest bardzo nieprzyzwoitym człowiekiem. Powiada, że dzień bez łapówki to
 		dzień stracony. Pracuje w urzędzie miejskim Obiektowa i nie wstydzi się swych podejrzanych interesów.</p>';
 	
 	// CENZURA
 	// Pobieramy zbuforowany tekst
 	// I **dla czytelności** przykładu zapisujemy go w zmiennej	
 	$kod = ob_get_clean();
 	
 	if(isset($_GET['real']))
 	{
 		// Wtajemniczeni znają całą prawdę
 		echo $kod;
 	}
 	else
 	{
 		// Reszta może się tylko domyślać
 		echo str_replace(array(
 			'Jan Nowak',
 			'Obiektowo',
 			'Obiektowa'	
 		), array(
 			'Alojzy Kromka',
 			'Hyzia Wólka',
 			'Hyziej Wólki'	
 		), $kod);
 	}
 
?>

Buforowanie wyjścia jest też podstawą tzw. kompresji GZip. Jest to kompresowanie treści strony przed wysłaniem tak, aby zajmowała mniejszą objętość, przez co użytkownik szybciej ją pobierze. Kompresję wspierają wszystkie nowoczesne przeglądarki (np. Opera, Firefox).

Aby uruchomić kompresję GZip, twoja wersja PHP musi mieć doinstalowaną bibliotekę zlib. Wtedy możesz rozpocząć buforowanie poniższym kodem:

 ob_start('ob_gzhandler');
 ob_implicit_flush(0);

Parametr przekazywany do ob_start() to nazwa tzw. uchwytu (handlera) służącego do modyfikacji zbuforowanej treści. ob_gzhandler jest jednym z predefiniowanych uchwytów, zajmującym się właśnie kompresją GZip. Druga z funkcji nakazuje wywołanie uchwytu dopiero, gdy będziemy mieli już cały kod HTML. Musimy o niej pamiętać dlatego, że nie można skompresować danych wyjściowych partiami - musi to być przeprowadzone w sposób ciągły. PHP samodzielnie wykrywa, czy przeglądarka użytkownika posiada obsługę tej możliwości, dlatego nie musisz o tym pamiętać.

Ciastka a bezpieczeństwo

edytuj

Ciastka są podstawą wielu systemów autoryzacji użytkowników dzięki możliwości przesyłania za ich pomocą danych między stronami. Jednak wielu początkujących programistów PHP nie rozumie lub nie wie, jak robić to bezpiecznie. Ciastko jest zwyczajnym nagłówkiem HTTP i naprawdę nie stanowi dużego problemu przechwycenie jego treści. Dlatego pamiętaj, aby nigdy nie przesyłać nim loginów, haseł zalogowanego użytkownika, ani żadnych innych danych potencjalnie pomocnych przy autoryzacji. Jest to całkowicie zła droga, gdyż systemy autoryzacji pisze się z wykorzystaniem sesji. Sesja jest pewnym rekordem z informacjami identyfikacji danego internauty przechowywanymi w bazie albo w pliku tekstowym na serwerze. Posiada długi, alfanumeryczny identyfikator i to on przesyłany jest ciastkiem. Porównując ID z ciastka, a także takie parametry, jak adres IP czy używana przeglądarka, można wyeliminować przypadki kradzieży ID sesji. Mechanizmu sesji nie musisz pisać samemu, gdyż PHP posiada własny. Omówimy go w następnym rozdziale.


Protokół HTTP jest protokołem bezstanowym. Oznacza to, że serwer WWW rozpatruje każde żądanie niezależnie od innych, nie szukając żadnych powiązań w stylu wysyłania ich przez tego samego internautę. Utrudnia to teoretycznie tworzenie wszelkich systemów autoryzacji, które wymagają śledzenia poczynań użytkownika na naszej stronie i przenoszenia jego danych autoryzacyjnych między kolejnymi żądaniami, czyli krótko mówiąc - wymagają obecności systemu sesji. Używając PHP lub innego dynamicznego języka server-side można je jednak zasymulować. "Nasz" język jest o tyle prosty, iż posiada już zaimplementowane stosowne funkcje. My tylko musimy zacząć ich używać.

Działanie sesji w PHP jest bardzo proste. W momencie pierwszego trafienia na stronę interpreter tworzy specjalny, losowy oraz unikalny identyfikator przesyłany między żądaniami za pomocą ciastek lub parametru PHPSESSID doklejanego automatycznie do adresów URL. Na jego podstawie odczytywany jest później odpowiedni plik z danymi sesji zapisany gdzieś na serwerze. Pod koniec przetwarzania żądania wszystkie wprowadzone przez skrypt zmiany są z powrotem zapisywane do wspomnianego pliku tak, aby były widoczne przy wejściu na kolejną podstronę. I tak to się toczy.

Wprowadzenie do sesji

edytuj

Czas na trochę praktyki. Aby zainicjować mechanizm sesji, wystarczy wywołać funkcję session_start(), najlepiej na początku naszej aplikacji. Od tego momentu do naszej dyspozycji zostaje oddana superglobalna tablica $_SESSION - wszystkie zapisane do niej dane są przesyłane między kolejnymi żądaniami. Popatrzmy na pierwszy, bardzo prosty przykład licznika odwiedzonych już podstron:

<?php
 
	session_start(); // 1
	
	if(!isset($_SESSION['licznik'])) // 2
	{
		$_SESSION['licznik'] = 0;
	}
	
	$_SESSION['licznik']++; // 3
	
	echo 'Odwiedziłeś już '.$_SESSION['licznik'].' podstron!'; // 4
?>

Oto analiza:

  1. Inicjalizujemy sesje
  2. Jeżeli jest to pierwsza wizyta, tablica z sesjami nie zawiera żadnych danych. Dobrym zwyczajem jest ich inicjowanie, aby nie zostać zaatakowanym tysiącami komunikatów Notice.
  3. Zmieniamy dane sesji
  4. Odczytujemy dane sesji

Po odświeżeniu strony zauważymy, że licznik wskazuje już "2", po kolejnym - "3". Oznacza to, że PHP zapisuje zmienną $licznikprzesyła ją między naszymi żądaniami. Różnica pomiędzy sesjami, a ciastkami jest taka, że dane te w ogóle nie opuszczają serwera WWW, są przez to (w teorii) bezpieczniejsze.

Prosta autoryzacja użytkowników

edytuj

Napiszemy teraz prosty skrypt do autoryzacji użytkowników bazujący na sesjach. Jak wspomnieliśmy w poprzednim rozdziale, nie powinno się przesyłać pomiędzy żądaniami haseł oraz loginów użytkowników, nieważne czy w formie ciastek, czy sesji. Alternatywą jest ich ID. Jak przekonasz się w następnych rozdziałach, bazy danych, gdzie większość aplikacji WWW trzyma swoje informacje, wcale nie identyfikują rekordów jakimiś abstrakcyjnymi rzeczami typu login lub tytuł. Operują na zwyczajnych, automatycznie nadawanych i unikalnych liczbach zwanych identyfikatorami (w skrócie pisze się "id"). Dzięki temu można szybko je do siebie porównać, co ma szczególne znaczenie w przypadku ogromnej liczby rekordów. Takie też ID przypisane użytkownikom przesyłane są w sesjach. Oczywiście, zgodnie z praktyką stosowaną w bazach danych, numerację rekordów rozpoczynamy od 1. Rekord o ID równym 0 nie istnieje.

Zatem, jak taki system logowania działa? Kiedy sesja jest już załadowana, skrypt sprawdza zapisany w niej ID użytkownika. Jeżeli jest on większy od zera, ktoś jest już zalogowany i wystarczy tylko pobrać skądś dane jego profilu. W przypadku ID równego 0 mamy do czynienia z kimś anonimowym. Tu, w zależności od sytuacji możemy mu wyświetlać ogólnodostępne treści albo formularz logowania. Pisanie skryptu zaczniemy od stworzenia sobie namiastki bazy danych. Będzie nią tablica $uzytkownicy przechowująca loginy oraz hasła użytkowników. Ponadto napiszemy funkcję czyIstnieje() znajdującą ID użytkownika o podanym loginie i haśle lub false, kiedy takowy nie istnieje. Hasła naturalnie haszujemy poznanym już algorytmem sha1:

<?php
 
	$uzytkownicy = array(1 =>
		array('login' => 'user1', 'haslo' => sha1('ppp')),
		array('login' => 'user2', 'haslo' => sha1('ddd')),
		array('login' => 'user3', 'haslo' => sha1('fff'))
	);
	
	function czyIstnieje($login, $haslo)
	{
		global $uzytkownicy;
		
		$haslo = sha1($haslo);
		
		foreach($uzytkownicy as $id => $dane)
		{
			if($dane['login'] == $login && $dane['haslo'] == $haslo)
			{
				// O, jest ktos taki - zwroc jego ID
				return $id;
			}
		}
		// Jeżeli doszedłeś aż tutaj, to takiego użytkownika nie ma
		return false;
	} // end czyIstnieje();

Teraz zaczniemy właściwy skrypt. Zainicjujemy sesję i obsłużymy sytuację pierwszej wizyty - w takim wypadku gościa oznaczymy jako osobę anonimową (niezalogowaną):

	// Wlasciwy skrypt
 
	session_start();
	
	if(!isset($_SESSION['uzytkownik']))
	{
		// Sesja się zaczyna, wiec inicjujemy użytkownika anonimowego
		$_SESSION['uzytkownik'] = 0;
	}

Krótka piłka - ID większy od 0? Ktoś jest zalogowany:

	if($_SESSION['uzytkownik'] > 0)
	{
		// Ktos jest zalogowany
		echo 'Witaj, '.$uzytkownicy[$_SESSION['uzytkownik']]['login'].' na naszej stronie!';	
	}
	else
	{

Jednak jeśli nie jest, to musimy zaprogramować zarówno pokazywanie formularza logowania, jak i autoryzację użytkownika na podstawie danych z niego. Zaczynamy od tej drugiej opcji. Korzystając z funkcji czyIstnieje() pobieramy ID użytkownika. Jeżeli jest on różny od false, wpisujemy go do sesji, tym samym logując go.

		// Niezalogowany
		if($_SERVER['REQUEST_METHOD'] == 'POST')
		{
			if(($id = czyIstnieje($_POST['login'], $_POST['haslo'])) !== false)
			{
				// Logujemy uzytkownika, wpisal poprawne dane
				$_SESSION['uzytkownik'] = $id;
				echo 'Dziekujemy, zostales zalogowany! <a href="sesje_2.php">Dalej</a>';
			}
			else
			{
				echo 'Podales nieprawidlowe dane, zegnaj! <a href="sesje_2.php">Dalej</a>';
			}		
		}
		else
		{

Kiedy dane nie nadeszły z formularza, znaczy to, że trzeba go wyświetlić:

			echo '<form method="post" action="sesje_2.php">
				Zaloguj sie: <input type="text" name="login"/>
                                <input type="password" name="haslo"/>
				<input type="submit" value="OK"/></form>';		
		}	
	}

I gotowe. Wszystko fajnie, wszystko pięknie, nasza witryna sobie działa, użytkownicy się logują, dodając coraz to nowe treści, lecz pewnego dnia dostajesz e-maila z ostrzeżeniem, że ktoś włamał się do serwisu i wszystko rozwalił. Co nawaliło? Czyżby zawiódł mechanizm sesji?

Bezpieczeństwo sesji

edytuj

Sytuacja z mechanizmem sesji standardowo dostępnym w PHP jest o tyle śmieszna, że jego autorzy właśnie nam zostawili swobodę działania odnośnie tego, jak go zabezpieczyć przed kradzieżą. Normalnie użyty jest on bowiem dziurawy jak sito. Kradzież sesji nie nastręcza wielkich trudności, lecz na nieszczęście, wiele osób o tym nie pamięta (także autorów artykułów pokazujących, jak z niego korzystać!).

Włamania wykorzystujące dziury w mechanizmach sesji mają swoje fachowe nazwy. Pierwszą z nich jest Session Fixation. Jej działanie jest bardzo proste. Wykorzystujemy tutaj właściwość, że kiedy podamy skryptowi jakiś nieistniejący ID sesji, PHP automatycznie dorobi dane i nie przejmie się tym, że tak naprawdę to my go wygenerowaliśmy, a nie jakiś algorytm losująco-mieszający. Teraz popatrz: podszywasz się pod pracownika jakiegoś serwisu internetowego i każesz nieświadomemu użytkownikowi odwiedzić jakiśtam adres, najlepiej wymagający zalogowania. Do adresu URL doczepiasz ciąg ?PHPSESSID=abcdef. "abcdef" jest wymyślonym przez nas identyfikatorem. Kiedy użytkownik posłusznie się zaloguje, jest zdany na naszą łaskę. Mamy ID jego sesji i możemy działać tak, jakbyśmy byli nim.Jeszcze więcej cwaniactwa i możemy uzyskać nawet sesję administratora, co dla serwisu oznacza oczywiście katastrofę. Aby się przed tym zabezpieczyć, wystarczy po zainicjowaniu mechanizmu sesji dokleić bardzo prosty kod:

<?php
 
	session_start();
	
	if (!isset($_SESSION['inicjuj']))
	{
		session_regenerate_id();
		$_SESSION['inicjuj'] = true;
	}

Przy tworzeniu nowej sesji, dzięki funkcji session_regenerate_id() mamy pewność, że sesja dostanie losowy ID. Teraz nawet, jeżeli podamy zmyślnie ?PHPSESSID=abcdef, nic nam to nie da, bo PHP i tak sobie wszystko wygeneruje po swojemu i zostaniemy w tym samym miejscu, co byliśmy. Jednak nie spoczywaj jeszcze na laurach. ID nadal może zostać wykradziony. Wystarczy, że jakaś ciamajda skopiuje znajomemu link do jakiegoś zasobu, mając przy tym wyłączone ciastka. Oczywiście w linku znajdzie się wtedy jego LOSOWY identyfikator sesji, który w ten sposób zostanie ujawniony przed światem. Znajomy może teraz wędrować sobie po serwisie wykorzystując sesję ciamajdy. session_regenerate_id() tutaj nie zadziałała, bo przecież ta sesja już istnieje. Ataki tego rodzaju określa się mianem Session Hijacking. Zabezpieczenie się przed nimi także jest proste. Wystarczy w sesji przesyłać np. adres IP komputera wraz z nazwą przeglądarki, spod których została ona utworzona, a następnie porównywać je przy kolejnych wizytach z danymi dostarczonymi przez serwer. Jeżeli wystąpi niezgodność, ktoś próbuje użyć cudzej sesji.

<?php
 
	session_start();
	
	if (!isset($_SESSION['inicjuj']))
	{
		session_regenerate_id();
		$_SESSION['inicjuj'] = true;
		$_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
	}
	
	
	if($_SESSION['ip'] != $_SERVER['REMOTE_ADDR'])
	{
		die('Proba przejecia sesji udaremniona!');	
	}

Powyższy przykład zawiera łatki przeciwko obu włamaniom (co prawda w przypadku tej drugiej o wiele lepszym rozwiązaniem byłoby utworzenie sesji na nowo w przypadku niezgodności, niż obwieszczanie całemu światu naszego "odkrycia"). Pamiętaj jednak, że najsłabszym ogniwem jest zawsze człowiek. Jeżeli hacker zdobędzie twoje hasło, zabezpieczenia na nic się nie zdadzą, chyba że akurat masz stały adres IP i tak sobie wszystko napisałeś, że na twoje konto można się tylko z niego logować. Tylko kto stosuje tak rygorystyczne i nieporęczne środki bezpieczeństwa?

Podsumowanie

edytuj

Sesje PHP są bardzo dobrym rozwiązaniem, jednak paradoksalnie wiele profesjonalnych aplikacji pisze własne systemy sesji całkowicie od zera. Powodów jest kilka:

  1. Integracja ze strukturą kodu reszty aplikacji
  2. Niezależność od PHP. Kiedy mechanizm sesji pojawił się w PHP po raz pierwszy, używało się go zupełnie inaczej, niż obecnie. Przyszłość może przynieść różne rzeczy, a nasz własny mechanizm zawsze pozostanie mimo tego taki sam.
  3. Bezpieczeństwo - kiedy piszesz wszystko od zera, możesz wstawić dodatkowe zabezpieczenia tam, gdzie normalnie nie sięgniesz.
  4. Elastyczność - dlaczego dane sesji muszą być trzymane akurat w pliku? Baza danych jest przecież równie dobra, jeśli nie lepsza.
  5. Specyfika zastosowania - wiele systemów sesji służy tylko celom autoryzacji. Stąd też pisze się je od zera pod to jedno zastosowanie, co umożliwia wprowadzenie pewnych uproszczeń oraz optymalizacji.

Jednak własny system sesji jest odpowiedzialnym kawałkiem kodu, gdyż tam to ty musisz wszystko od A do Z zaprogramować. Na razie jedynie sygnalizujemy taką możliwość jako alternatywę dla systemu sesji wbudowanego w PHP. Który z nich wybierzesz, to zależy tylko od Ciebie.

Poprzedni rozdział: Sesje
Następny rozdział: Internacjonalizacja

Wysyłanie e-maili

edytuj

Aby wysyłać e-maile z PHP, interpreter musi być skonfigurowany do pracy z demonem pocztowym. Odpowiednie dyrektywy znajdują się w pliku php.ini w sekcji [mail function]. Należy tam podać odpowiednie parametry w zależności od systemu operacyjnego:

  • Dla systemów Win32 - host oraz port, pod jakim pracuje demon pocztowy protokołu SMTP. Możesz próbować łączyć się z twoją własną skrzynką e-mail u jednego z providerów, lecz jeżeli znajdujesz się za firewallem, prawdopodobnie będziesz musiał zainstalować serwer poczty na własnym komputerze (np. Mercury for Win32).
  • Dla systemów Unix należy podać wywołanie oraz ewentualne parametry programu sendmail.

Funkcja mail()

edytuj

Aby wysłać e-maila za pomocą PHP, musimy użyć do tego funkcji mail(). Jej składnia jest następująca:

mail(adresat, temat, wiadomość[, nagłówki[, parametry]])

Funkcja zwraca wartość TRUE, jeżeli wiadomość została poprawnie przekazana serwerowi poczty.

Biblioteka pear::mail

edytuj

Dla osób które lubią używać gotowych bibliotek można polecić moduł mail z pear. Szczegóły znajdziesz pod adresem: http://pear.php.net/package/Mail . Biblioteka jest dosyć prosta i posiada w swojej dokumentacji jasne przykłady.

Wysyłanie załączników jest równie proste - zainteresuj się klasą pear::Mail_Mime.

Przykłady

edytuj

Następujący przykład spowoduje wysłanie wiadomości e-mail na adres jan_testowy@serwer.pl o temacie "Witaj" i treści "Oto test funkcji mail":

<?php
   if(mail('jan_testowy@serwer.pl', 'Witaj', 'Oto test funkcji mail'))
   {
      echo 'Wiadomość została wysłana';
   }

Nagłówki pozwalają na ustawienie dodatkowych informacji o wiadomości, np. jej nadawcy lub kodowaniu znaków: Możemy również użyć zmiennych:

<?php
   $naglowki = "From: moj@mail.pl".PHP_EOL."Reply-To: moj@mail.pl".PHP_EOL."Content-type: text/plain; charset=iso-8859-2";

   if(mail('jan_testowy@serwer.pl', 'Witaj', 'Oto test funkcji mail', $naglowki))
   {
      echo 'Wiadomość została wysłana';
   }

Nagłówki można podać zarówno wewnątrz cudzysłowów, jak i apostrofów. Użyto tutaj stałej PHP_EOL, zmiennej środowiskowej. Przejść do nowej linii można na dwa sposoby. Pierwszy, uniwersalny i mniej popularny to ta stała, drugi - zależny od systemu serwera - \r\n dla Windows, \n dla Linuksa i \r dla Mac'a. Wewnątrz edytora najlepiej podawać wszystkie nagłówki w jednym ciągu i zejścia zaznaczać przy użyciu właśnie tych kodów. Inaczej może to spowodować wysłanie niepoprawnej wiadomości, gdyż niektóre edytory mają tendencję do zniekształcania tych zejść.

Z poziomu PHP można także wysyłać e-maile w formacie HTML. W tym celu należy dodać do wiadomości odpowiednie nagłówki:

<?php
   // Naglowki mozna sformatowac tez w ten sposob.
   $naglowki = "Reply-to: moj@mail.pl <moj@mail.pl>".PHP_EOL;
   $naglowki .= "From: moj@mail.pl <moj@mail.pl>".PHP_EOL;
   $naglowki .= "MIME-Version: 1.0".PHP_EOL;
   $naglowki .= "Content-type: text/html; charset=iso-8859-2".PHP_EOL; 

   //Wiadomość najczęściej jest generowana przed wywołaniem funkcji
   $wiadomosc = '<html> 
   <head> 
      <title>Wiadomość e-mail</title> 
   </head>
   <body>
      <p><b>Treść wiadomości</b>: To jest treść wiadomości z formatowaniem HTML.</p>
   </body>
   </html>';


   if(mail('jan_testowy@serwer.pl', 'Witaj', $wiadomosc, $naglowki))
   {
      echo 'Wiadomość została wysłana';
   }

Dokumentacja

edytuj
Poprzedni rozdział: Wysyłanie e-maili
Następny rozdział: System plików

Internacjonalizacja

edytuj

W tym rozdziale przyjrzymy się zagadnieniom związanym z dostosowywaniem naszych witryn do konkretnych języków i ustawień regionalnych. PHP posiada odpowiedni zestaw narzędzi, który czyni to zadanie nietrudnym w wykonaniu. Lecz uważaj! Nieuważne skonfigurowanie mechanizmu może być przyczyną błędów!

Dlaczego "Ż" jest literą?

edytuj

Uruchom pewien prosty skrypt:

<?php

	echo strtolower('ŻÓŁTA WODA');

?>

W zależności od komputera, pokaże on napis żółta woda albo ŻóŁta woda. Zastanówmy się, skąd jeden interpreter wiedział, że Ż jest literą polskiego alfabetu, która również podlega zamianie, a drugi nie? Odpowiedź kryje się w ustawieniach regionalnych tych maszyn, znanych z terminologii linuksowej pod nazwą locale. PHP domyślnie wykonuje wszystkie operacje na tekstach, ustawia formatowanie walut oraz liczb na podstawie ustawień systemowych. Aby zmienić opcje regionalne dla aktualnie wykonywanego skryptu, należy skorzystać z funkcji setlocale(). Za pierwszy parametr podajemy flagi określające, które aspekty regionalizacji chcemy zmodyfikować, a wszystkie kolejne to identyfikatory możliwych ustawień. PHP będzie próbował każdego z nich po kolei, aż trafi na taki, który jest zainstalowany w systemie. Dzięki temu jedną funkcją można przystosować skrypt do pracy zarówno z serwerami opartymi o Linux, jak i Windows. Poniżej przedstawiamy jeszcze raz skrypt strtolower() skonfigurowany do pracy z polskimi ustawieniami:

<?php

	setlocale(LC_ALL, 'pl_PL', 'pl', 'Polish_Poland.28592');

	echo strtolower('ŻÓŁTA WODA');

?>

Pierwsze dwa identyfikatory (pl_PL oraz pl) dotyczą systemu Linux. Trzeci przeznaczony jest dla rodziny Windows. Listę aktualnie zainstalowanych ustawień można znaleźć w Panel sterowania → Opcje regionalne → Zaawansowane.

Polska data

edytuj

Pisząc aplikację przeznaczoną na rynek międzynarodowy, powinniśmy pomyśleć także o formatowaniu dat. Poznana już przez nas funkcja date() nawet przy pracy z polskimi ustawieniami wyświetli nam angielskie nazwy miesięcy. Sprawę rozwiązuje strftime(), przy której musimy pamiętać o innej składni formatowania daty. Przyjrzyjmy się przykładowi:

<?php

	setlocale(LC_ALL, 'pl_PL', 'pl', 'Polish_Poland.28592');
	
	// Dzień - nazwa miesiąca - rok
	echo strftime('%d %B %Y');

?>

Po uruchomieniu okazuje się, że nawet ten skrypt nie jest doskonały, ponieważ wprawdzie generuje spolszczoną datę, ale niepoprawną gramatycznie! Otrzymujemy komunikat np. 17 kwiecień 2006, a tymczasem poprawną formą jest 17 kwietnia 2006. W niuanse gramatyczne wgłębiać się nie będziemy - po prostu jest tak, a nie inaczej i musimy samodzielnie napisać sobie funkcję, która nam to przeformatuje. Umożliwiając dodawanie obsługi nowych języków do aplikacji, powinniśmy pomyśleć o możliwości jej podmiany tak, aby twórca nakładki językowej mógł zaprogramować datę zgodnie z wymogami swego języka. Oto przykład takiej funkcji dla języka polskiego. Jest to modyfikacja strftime() dodająca nowy znacznik - %F będący właśnie odmienioną nazwą miesiąca:

<?php
	
	function localStrftime($format, $timestamp = 0)
	{
		if($timestamp == 0)
		{
			// Sytuacja, gdy czas nie jest podany - używamy aktualnego.
			$timestamp = time();
		}

		// Nowy kod - %F dla odmienionej nazwy miesiąca
		if(strpos($format, '%F') !== false)
		{
			$mies = date('m', $timestamp);
			
			// odmienianie
			switch($mies)
			{
				case 1:
					$mies = 'stycznia';
					break;
				case 2:
					$mies = 'lutego';
					break;
				case 3:
					$mies = 'marca';
					break;
				case 4:
					$mies = 'kwietnia';
					break;
				case 5:
					$mies = 'maja';
					break;
				case 6:
					$mies = 'czerwca';
					break;
				case 7:
					$mies = 'lipca';
					break;
				case 8:
					$mies = 'sierpnia';
					break;
				case 9:
					$mies = 'września';
					break;
				case 10:
					$mies = 'października';
					break;
				case 11:
					$mies = 'listopada';
					break;
				case 12:
					$mies = 'grudnia';
					break;			
			}
			// dodawanie formatowania
			return strftime(str_replace('%F', $mies, $format), $timestamp);		
		}
		return strftime($format, $timestamp);	
	} // end localStrftime();
	
	echo localStrftime('%d %F %Y');

?>

Teraz otrzymujemy prawidłowy tekst: 17 kwietnia 2006.

Oto kilka przydatnych kodów dla funkcji strftime():

  • %A - pełna nazwa dnia tygodnia.
  • %B - pełna nazwa dnia miesiąca.
  • %d - numer dnia miesiąca (od 1 do 31)
  • %H - godzina w formacie 24-godzinnym
  • %m - numer miesiąca (od 1 do 12)
  • %M - minuta
  • %S - sekunda
  • %T - aktualny czas (równoważnik %H:%M:%S)
  • %Y - rok jako liczba czterocyfrowa
  • %% - znak %

Więcej kodów można znaleźć na stronie dokumentacji PHP.

Wielojęzyczny interfejs

edytuj

Kolejnym krokiem na drodze umożliwienia obcokrajowcom przeglądania naszych witryn jest stworzenie kilku wersji językowych. Realizuje się to, tworząc nakładki językowe, w których poszczególnym komunikatom interfejsu przypisane są odpowiednie identyfikatory. Następnie w kodzie aplikacji, zamiast pisać odpowiednie teksty, wywołujemy specjalną funkcję i podajemy ID tekstu do wstawienia. Ta wprowadza w tym miejscu odpowiedni tekst w zależności od wybranego języka. Ponieważ nie znamy jeszcze programowania obiektowego, będziemy musieli napisać odpowiednią bibliotekę strukturalnie:

<?php

	$preferencje = array();
	$tekstyI18n = array();
	
	function zainstalowanyJezyk($jezyk)
	{
		// Tutaj mozemy umiescic kod sprawdzajacy, czy nasz serwis posiada
		// wersje w podanym jezyku
		switch($jezyk)
		{
			case 'pl':
			case 'de':
			case 'fr':
			case 'en':
				return true;		
		}
		return false;	
	} // end zainstalowanyJezyk();

	function uzyjJezyk($jezyk)
	{
		global $preferencje;
		
		if(zainstalowanyJezyk($jezyk))
		{		
			$preferencje['jezyk'] = $jezyk;
		}
	} // end uzyjJezyk();
	
	function wybierzGrupe($grupa)
	{
		global $preferencje, $tekstyI18n;
		
		$tekstyI18n[$grupa] = @parse_ini_file('jezyki/'.$preferencje['jezyk'].'/'.$grupa.'.php');	
	} // end wybierzGrupe();
	
	function wstaw($grupa, $id)
	{
		global $tekstyI18n;
		if(isset($tekstyI18n[$grupa][$id]))
		{
			return $tekstyI18n[$grupa][$id];
		}
		return strtoupper($grupa.':'.$id);	
	} // end wstaw();

?>

W powyższym kodzie tekstom przypisane są nie tylko identyfikatory - podzielono je także na ładowane oddzielnie grupy. W ten sposób strona z newsami może załadować sobie tylko treści komunikatów dla newsów, z pominięciem np. tekstów przeznaczonych dla forum dyskusyjnego. Oto opis funkcji:

  • zainstalowanyJezyk() - zwraca true, jeżeli podany język jest obsługiwany przez nasz skrypt.
  • uzyjJezyk() - ustawia podany język.
  • wybierzGrupe() - ładuje podaną grupę na podstawie aktualnych ustawień języka. Do wczytania grupy używamy funkcji parse_ini_file() przetwarzającej podany plik identycznym parserem, jak ten, który przetwarza php.ini. Dzięki temu zyskujemy duże możliwości małym kosztem.
  • wstaw() - wstawia tekst o podanym ID należący do odpowiedniej grupy na podstawie aktualnych ustawień językowych.

Pliki językowe należy umieszczać w katalogu jezyki/kodjezyka/nazwagrupy.php. Oto przykładowy plik jezyki/pl/global.php:

komunikat = "Komunikat"
hello_world = "Witaj swiecie!"

Korzysta z niego poniższy przykład:

<?php

	require('./i18n.php');
	
	uzyjJezyk('pl');
	wybierzGrupe('global');
	
	
	echo '<h2>'.wstaw('global', 'komunikat').'</h2>';
	echo '<p>'.wstaw('global', 'hello_world').'</p>';

?>

Na początku ładujemy naszą bibliotekę i wybieramy język. Później wczytujemy grupę i wyświetlamy komunikaty. To wszystko - jesteśmy posiadaczami wielojęzycznego interfejsu. Pamiętaj, że jest to tylko przykładowy kod. Aby był on w pełni sprawny, powinieneś umożliwić użytkownikowi zmianę języka, a także zadbać o prawidłową obsługę błędów. Zwróć też uwagę, że dane obecnego systemu przechowywane są w tablicach globalnych. Nie jest to rozwiązanie najlepsze, ale na tym etapie wiedzy powinno nam wystarczyć. Kiedy poznamy programowanie obiektowe, będziesz mógł przepisać ten kod z jego wykorzystaniem, dzięki czemu zyska on na elegancji i bezpieczeństwie.

Do PHP można doinstalować także uniksowy moduł do obsługi wielojęzycznych interfejsów o nazwie gettext. Korzystanie z niego jest jednak dość ryzykowne, ponieważ wiele serwerów nie obsługuje go i aplikacje oparte o ten moduł nie będą działać.

Automatyczna detekcja języka

edytuj

Przeglądarka internetowa to cenne źródło informacji o internaucie. Przesyła ona m.in. informację o języku, w jakim ona pracuje. Możemy przyjąć, że jest to rodzimy język użytkownika i na podstawie tego automatycznie ustawić mu odpowiednią wersję językową strony. Całość zawarta jest w $_SERVER['HTTP_ACCEPT_LANGUAGE'].

<?php

	$zainstalowane = array( // 1
		'pl' => 'polski',
		'en' => 'angielski',
		'fr' => 'francuski'	
	);
	

	$jezyki = explode(';', $_SERVER['HTTP_ACCEPT_LANGUAGE']); // 2
	$jezyki = explode(',', $jezyki[0]);
	
	$uzyty = null;
	foreach($jezyki as $jezyk)
	{
		if(isset($zainstalowane[$jezyk])) // 3
		{
			$uzyty = $jezyk;
			break;		
		}
	}
	
	if(is_null($uzyty)) // 4
	{
		$uzyty = 'en';
	}
	
	echo 'Wybrany język to '.$zainstalowane[$uzyty];

?>

Analiza powyższego skryptu:

  1. Tablica z nazwami zainstalowanych języków - tylko do celów kontrolnych.
  2. Najpierw musimy usunąć niepotrzebne nam informacje. HTTP_ACCEPT_LANGUAGE posiada odpowiedni format informacji: języki;ważności, gdzie kody języków oddzielone są przecinkami. Druga część nie jest nam w ogóle potrzebna i możemy ją usunąć. Stąd w drugim explode pojawia się odwołanie $jezyki[0] wskazujące po prostu na pierwszą, istotną część przekazu.
  3. Pętlą przelatujemy całą tablicę. Jeśli zauważymy, że dany język jest zainstalowany, ustawiamy go i przerywamy pętlę.
  4. Jeżeli zmienna $uzyty ma w tym miejscu dalej domyślną wartość null, oznacza to, że żaden z języków użytkownika nie jest obsługiwany. W tym wypadku zmuszamy go do czytania po angielsku.

Naturalnie wypadałoby zadbać, aby powyższy skrypt był bardziej przyjazna użytkownikowi i na pierwszym miejscu sprawdzał informacje zapisane w ciastkach. Dopiero kiedy takowych nie będzie, można pobawić się w detekcję. Inaczej użytkownik nie będzie w stanie zmienić "narzuconego" mu języka nawet, jeśli jego własny będzie obsługiwany przez naszą witrynę.

Internacjonalizacja plikami .mo

edytuj

Takie wykonanie skryptu jest niewygodne. Najbardziej popularnym sposobem jest budowanie tego na plikach .mo. Utwórz plik lang.php:

<?php
 if ($_SERVER['HTTP_ACCEPT_LANGUAGE'] == "pl_PL") // 1
 {
 setlocale(LC_ALL, 'pl_PL');
 }
 else
 { 
 setlocale(LC_ALL, 'en_US');
 }
 bindtextdomain("domena", "./locale"); //2
 bind_textdomain_codeset("domena", 'UTF-8'); //3
 textdomain("domena"); //4
 ?>
  1. Przeglądarki wysyłają informacje o języku. Jeśli użytkownik ma ustawiony w przeglądarce język polski, zobaczy polską wersję strony, jeśli nie - angielską.
  2. Przygotowuje domenę tekstową. Zmień "domena" na coś innego.
  3. Ustawia domenę na kodowanie UTF-8. Zmień "domena" na coś innego.
  4. Wybiera domenę. Zmień "domena" na coś innego.

Ten plik dołączamy do każdych plików, które wymagają internacjonalizacji funkcją <pre>include("./lang.php");</pre> Teraz należy zamienić wszystkie teksty tak jak tu:

<?php
 echo "Polska wiadomość";
 // =
 echo _("Polska wiadomość");

Pobieramy edytor plików .po, np. Poedit i tworzymy pliki .mo (obsługa poniżej). Ważna jest także budowa struktury katalogów.

 |
 |-lang.php   |-en_US    -|LC_MESSAGES -|domena.mo
 |-locale    -|-pl_PL    -|LC_MESSAGES -|domena.mo
 |
 (katalog zawiera plik lang.php i folder locale. W nim znajdują się podfoldery z kodami języków (en_US i pl_PL). W każdym podfolderze języków znajduje się folder LC_MESSAGES, którego zawartością w obu podfolderach języków jest plik domena.mo z przetłumaczonymi tekstami.)
 

Obsługa PoEdit

edytuj

Obsługa jest bardzo prosta - wybieramy Plik > Nowy katalog, uzupełniamy dane. Zakładka ścieżki jest bardzo ważna. Pierwsze pole NIE może być ścieżką do katalogu z naszymi plikami, lecz do wcześniejszego katalogu (np. dla D:\Serwer\www\gettext\gettext podajemy D:\Serwer\www\gettext) a w polu niżej za pomocą przycisku nr. 2 dodajemy ścieżkę D:\Serwer\www\gettext\gettext oraz jeśli potrzebne jej podkatalogi.

Testowanie

edytuj

Aby przetestować skrypty, należy zainstalować GNU gettext i:

  • W przypadku systemu Windows - dodać php_gettext.dll do podkatalogu extensions katalogu PHP i aktywować ją w php.ini,
  • W przypadku linuxów należy ją dokompilować do php, XAMPP dla Linuksa zawiera ją wkompilowaną.

Zakończenie

edytuj

Internacjonalizacja jest bardzo ważnym zagadnieniem. Jeśli naprawdę myślimy o ekspansji na rynki zagraniczne, nie lekceważmy jej. Czas zainwestowany w uczynienie serwisu bardziej przyjaznym obcokrajowcom w przyszłości zaprocentuje ich dobrymi wrażeniami z pobytu u nas.

Poprzedni rozdział: Internacjonalizacja
Następny rozdział: Data i czas

System plików

edytuj

Przez wcześniejsze rozdziały często przewijały się nam różne funkcje odczytu danych z dysku twardego. Przyszedł czas na zebranie informacji o nich oraz ich usystematyzowanie. Pierwszą rzeczą, o której należy pamiętać, jest wydajność. Wszelkie odwołania do systemu plików są dosyć powolne i często stanowią nawet wąskie gardło w szybkości naszego kodu. Dlatego powinieneś starać się wykonywać ich tak mało, jak tylko się da i buforować wyniki działania niektórych z nich, aby późniejszy kod mógł odwoływać się do nich.

Odczyt danych

edytuj

Zanim zaczniemy, utwórz sobie plik plik.txt z jakąś długą zawartością (najlepiej w kilku linijkach).

Zawartość pliku można odczytać w PHP na kilka sposobów. Oto pierwszy z nich, wywodzący się jeszcze z języka C:

<?php
	// 1.1 - Tworzymy odwołanie do pliku
	$_Uchwyt = fopen('plik.txt', 'r');
	
	// 1.2 - Wykonujemy kod dopóki skrypt nie napotka końca pliku
	while(!feof($_Uchwyt))
	{
		// 1.2.1 - Czytamy z pliku jeden kilobajt (1024 B)
		echo fread($_Uchwyt, 1024);	
	}
	
	// 1.3 - Zamykamy plik
	fclose($_Uchwyt);
?>

Każdy dostęp do pliku musi rozpocząć się od jego otwarcia. Zadaniem tym zajmuje się funkcja fopen(). Parametr r nakazuje otwarcie pliku do odczytu. Następnie w pętli pobieramy plik po kawałkach o wielkości jednego kilobajta. W ten sposób dane mogą być przetwarzane "równolegle" z odczytem. Funkcja feof() służy do sprawdzenia, czy osiągnęliśmy koniec pliku. Po zakończonej pracy nasze połączenie z plikiem trzeba zamknąć. Odpowiada za to fclose().

Z powyższego kodu możemy wyrzucić pętlę i pobrać wszystko za jednym zamachem. Wystarczy tylko użyć funkcji filesize(), aby podała nam rozmiar pliku:

<?php
	// 1.1 - Otwieramy
	$_Uchwyt = fopen('plik.txt', 'r');
	
	// 1.2 - Czytamy całą zawartość pliku
	echo fread($_Uchwyt, filesize('plik.txt'));
	
	// 1.3 - Zamykamy
	fclose($_Uchwyt);

?>

Zwróćmy uwagę na jakość podanych przykładów. Zmień nazwę plików, do których się odwołujemy, na jakiś nieistniejący. Oba skrypty wtedy zgłupieją. Pierwszy zaleje nas falą ostrzeżeń przez 30 sekund (potem przestaną się pojawiać), drugi zrobi ich "tylko" kilka (przyczyną jest brak pętli). Dlatego powinniśmy tak przygotować wszystko, abyśmy sami panowali nad komunikatami. Czas stworzyć prymitywną obsługę błędów. Wykorzystamy tutaj operator @, aby zagłuszyć funkcję fopen() i sprawdzić zwracany wynik. Powinna ona zwrócić nam połączenie z plikiem, tj. wartość typu Resource. Zobaczmy:

<?php
	// 1.1 - Otwieramy plik, alarmujemy w wypadku błędu
	$_Uchwyt = @fopen('inny_plik.txt', 'r') or die('Wystąpił błąd.');
	// 1.2 - Czytamy plik
	echo fread($_Uchwyt, filesize('inny_plik.txt'));
	// 1.3 - Zamykamy
	fclose($_Uchwyt);
?>

Od PHP 4.3.0 nie trzeba już rozpisywać się, aby wczytać zawartość pojedynczego pliku. Cała czynność jest zautomatyzowana w funkcji file_get_contents(). Aby tu sprawdzić poprawność otwarcia, wystarczy porównać zwrócony wynik z wartością false, która jest zwracana w przypadku błędu:

<?php
	// 1.1 - Otwieramy, czytamy i zamykamy
	$_TrescPliku = @file_get_contents('plik.txt') or die('Wystąpił błąd.');
	
	// 1.2 - Zwracamy treść pliku
	echo $_TrescPliku;
?>

Pisząc księgę gości, poznaliśmy funkcję file(), która zwracaną zawartość rozbijała od razu na tablicę poszczególnych linijek. Dzięki tej właściwości wyświetlimy plik jako listę wypunktowaną HTML bez większych trudności:

<?php
	// 1.1 - Otwieramy plik i czytamy go do tablicy
	$_TrescPliku = @file('plik.txt') or die('Wystąpił błąd.');
	
	// 1.2 - Otwieramy znacznik listy
	echo '<ul>';
	// 1.3 - Dla każdej linii pliku...
	foreach($_TrescPliku as $_Linia)
	{
		// 1.3.1 - ...tworzymy nowy element listy...
		echo '<li>'.$_Linia.'</li>';	
	}
	// 1.4 - ...i zamykamy listę.
	echo '</ul>';
?>

Pamiętaj, że file() nie gubi znaków końca linii - te są nadal zapisane w poszczególnych linijkach. Dlatego gdy będziesz chciał z powrotem połączyć wszystko w całość, powinieneś napisać

implode("", $_TrescPliku);

zamiast np.

implode("\n", $_TrescPliku);

Pod żadnym pozorem nie odczytuj plików w ten sposób:

implode('', file('plik.txt'));

Sens takiego kodu można streścić w prostym porównaniu: pakować się tylko po to, by się natychmiast rozpakować. Nie służy to niczemu, a konsumuje niezbędny czas. Aby przekonać się, jak mało wydajne jest takie rozwiązanie, spróbuj załadować tak plik tekstowy o wielkości megabajta, następnie powtórz to samo z wykorzystaniem file_get_contents() i porównaj wrażenia.

Powinniśmy jeszcze wspomnieć o rozwiązaniu tzw. "wg pana od informatyki" (aczkolwiek bardzo go szanujemy). Rozwiązanie najlepiej pokazać na przykładzie:

<?php
	// 1.1 - Inicjacja zmiennej
	$_Plik='plik.txt';
	// 1.2 - Kontynuujemy jeśli plik istnieje
	if (file_exists($_Plik))
	{
		// 1.2.1 - Czytamy zawartość
		$zawartosc=file($_Plik);
		// 1.2.2 - Sprawdzamy czy plik jest pusty
		if (count($zawartosc)==0)
		{
			die("Plik: $_Plik jest pusty!");
		}
	}
	else
	{
		// 1.2.3 - Jeśli plik nie istnieje wyświetlamy komunikat
		die("Plik: $_Plik nie istnieje!"); 
	}
// Tutaj dalsze operacje na pliku
?>

Metoda na "pana nauczyciela" polega na pełnym obsłużeniu wszystkich możliwości jakie mogą wystąpić podczas czytania pliku oraz posłużenia się funkcjami typu file_exists(). Wadą takiego rozwiązania jest jednak to, że pisząc kod możemy zakopać się w blokach if gubiąc główny wątek programu, a także nie jesteśmy w stanie wymyślić wszystkich możliwych sytuacji, które mogą się zdarzyć.

Zapis danych

edytuj

Zapis danych wygląda analogicznie do odczytu. Różne jest tylko miejsce docelowe danych. Sposób pierwszy polega na otwarciu pliku funkcją fopen() i skorzystaniu z fwrite() do dodania nowej zawartości. Plik otwieramy z parametrem w (nadpisujemy starą zawartość) lub a (dopisujemy coś do pliku). W przypadku operowania danymi binarnymi, dodajemy jeszcze literę b.

<?php
	// 1.1 - Otwieranie
	$_Plik = fopen('./plik.txt', 'w');
	// 1.2 - Zapisywanie łańcucha
	fwrite($_Plik, 'To jest nowa zawartość pliku');
	// 1.3 - Zamykanie
	fclose($_Plik);

?>

Po uruchomieniu tego skryptu w plik.txt powinna pojawić nam się nowa zawartość. W przypadku pracy na systemie Linux/Unix sprawdź, czy PHP ma uprawnienia do edycji plików w twoim katalogu roboczym.

W PHP 5.0.0 pojawiła się funkcja file_put_contents(), która upraszcza całą sprawę. Zwraca ona liczbę zapisanych do pliku bajtów i możemy wykorzystać to do kontroli, czy operacja dopisywania faktycznie się udała. Funkcja pobiera dwa parametry: nazwę pliku oraz tekst do wpisania i "firmowo" nie zniekształca danych binarnych.

<?php
	// 1.1 - Jeśli zapis się powiódł wyświetl komunikat o powodzeniu
	if(file_put_contents('./plik.txt', 'To jest nowa zawartość pliku') != 0)
	{
		echo 'Udało się zapisać nową zawartość do pliku.';	
	}

?>

Zadajmy sobie pytanie, co jeśli musimy dopisać dodatkową treść. Naturalnie file_put_contents() także to potrafi. Trzeba tylko skorzystać z trzeciego parametru, w którym możemy ustawiać flagi. FILE_APPEND jest tym, czego potrzebujemy.

<?php
	// 1.1 - Jeśli zapis się powiódł wyświetl komunikat o powodzeniu
	if(file_put_contents('./plik.txt', 'Dopisana treść', FILE_APPEND) != 0)
	{
		echo 'Udało się dodać zawartość do pliku.';	
	}

?>

Ten skrypt będzie już dopisywać dane do pliku, zamiast je nadpisywać.

Informacje o plikach

edytuj

W wielu przypadkach przydaje się wiedza o tym, co w zasadzie w katalogach mamy. Możemy ją uzyskać, korzystając z rodziny funkcji udostępniających nam różne informacje o plikach. Wszystkie przyjmują za parametr nazwę pliku:

  • is_file() - zwraca true, jeśli obiekt jest plikiem.
  • is_dir() - zwraca true, jeśli obiekt jest katalogiem.
  • is_readable() - zwraca true, jeśli posiadamy prawa do odczytu zawartości obiektu.
  • is_writeable() - zwraca true, jeśli posiadamy prawa do zapisu do obiektu.
  • file_exists() - zwraca true, jeśli plik/katalog istnieje.
  • fowner() - zwraca ID właściciela pliku.
  • fgroup() - zwraca ID grupy, do której plik należy.
  • fperms() - zwraca uprawnienia pliku.
  • filesize() - zwraca wielkość pliku.
  • filemtime() - zwraca czas ostatniej modyfikacji pliku lub false, jeśli nie istnieje.

Przy korzystaniu z nich musimy pamiętać o wydajności. Odczyt wszelkich danych z dysku jest dość powolny, dlatego starajmy się jak najwięcej wycisnąć z pojedynczego wywołania funkcji. Oto przykład: załóżmy, że mamy plik A.txt i na jego podstawie generujemy B.txt zawsze, kiedy ulegnie on zmianie (taki kompilator). Musimy zatem napisać mechanizm sprawdzający, czy można uruchomić kompilację, czy też jest ona zbędna.

<?php

	if(!file_exists('A.txt'))
	{
		die('Plik A.txt nie istnieje!');
	}

	if(file_exists('B.txt'))
	{
		if(filemtime('B.txt') != filemtime('A.txt'))
		{
			echo 'Plik A.txt wymaga kompilacji.';
		}
		else
		{
			echo 'Można czytać z pliku B.txt';
		}	
	}
	else
	{
		echo 'Plik A.txt wymaga kompilacji';
	}

?>

Pozornie wszystko wygląda na poprawne - skrypt prawidłowo raportuje wszystkie sprawy. Jednak robi to zbyt wolno, gdyż przeciążyliśmy go dużą ilością odwołań do dysku twardego. Jeżeli uruchomimy go na witrynie z dużym ruchem, osiągnąłby gorsze wyniki wydajności, niż inne skrypty. Spróbujmy go nieco zmodyfikować. Czy naprawdę potrzebujemy funkcji file_exists()? Okazuje się, że nie. Przecież filemtime() zwróci nam false, jeżeli plik nie będzie istniał i możemy to wykorzystać. Oto poprawiony kod skryptu:

<?php

	$czasA = @filemtime('A.txt');
	
	if($czasA === false)
	{	
		die('Plik A.txt nie istnieje!');
	}
	else
	{
		$czasB = @filemtime('B.txt');
	}

	if($czasB !== false)
	{
		if($czasB != $czasA)
		{
			echo 'Plik A.txt wymaga kompilacji.';
		}
		else
		{
			echo 'Można czytać z pliku B.txt';
		}	
	}
	else
	{
		echo 'Plik A.txt wymaga kompilacji';
	}

?>

Zauważmy, w tym przypadku mamy tylko dwa odwołania do dysku, a jeśli plik A.txt nie będzie istnieć, to nawet jedno! Zamiast wykonywania za każdym razem setek nowych funkcji, wykorzystujemy maksymalnie te dane, które już mamy. To jest właściwa filozofia przy pracy z plikami.

Ścieżki dostępu

edytuj

Wydajność i bezpieczeństwo

edytuj

Zakończenie

edytuj

Plikom poświęciliśmy naprawdę bardzo duży rozdział. Jednak mało która aplikacja PHP wykorzystuje je jako główne źródło danych dla internauty. Znacznie poważniejszym i mającym większe możliwości narzędziem są bazy danych. Zagadnienie to jest omówione w następnej części podręcznika. Czy jednak pliki należy w takim razie wyrzucić? Nie, ze względu na wydajność. Wbrew pozorom, odczyt rekordów z bazy zazwyczaj jest wolniejszy, niż z pliku i w przypadku elementarnych ustawień aplikacji, które nie wymagają złożonego sortowania oraz stosowania rozbudowanych relacji (np. konfiguracja, dane systemowe), można pokusić się o zastąpienie ich plikami.

Poprzedni rozdział: System plików
Następny rozdział: Ćwiczenia

Data i czas

edytuj

Przeglądając wiele dynamicznych stron internetowych, można napotkać multum różnych przykładów użycia daty i czasu. W księgach gości i forach dyskusyjnych zapisywana jest godzina wysłania wiadomości. Większość liczników dziennych opiera się na tych funkcjach. W niektórych serwisach można zmieniać strefy czasowe, a wszystkie najskuteczniejsze zabezpieczenia przed spamem są bazowane na limitach czasowych. Ten rozdział będzie poświęcony właśnie funkcjom służącym do zadań związanych z operowaniem obecnym czasem.

Formatowanie daty

edytuj

Podstawową funkcją do zwracania obecnej daty jest date(), która przyjmuje jako parametr wartość tekstową. I tutaj pojawia się pierwsza zagadka, wielu na początku zastanawiałoby się czemu akurat wartość tekstowa - tekst nie ma wiele wspólnego z datą.

I tutaj kłania się PHP, pomagając wielu programistom. Łańcuch ten, mówi jak należy przeformatować datę - nie trzeba pisać własnych funkcji. W tym parametrze funkcji można umieszczać własne znaczniki, mówiące jakie informacje należy wyświetlić:

<?php
echo date('d-m-Y');
?>

Powyższy kod zwróci wartość podobną do 17-04-2004. Spróbuj usunąć myślniki pomiędzy literami. Co zauważyłeś?

Pamiętaj też, iż nie należy sugerować się nazwą funkcji. Funkcja date() umożliwia również wyświetlenie godziny, strefy czasowej, a nawet informacji, czy czas jest letni:

<?php
echo date('H:i:s'); // Godzina, np. 09:39:21
echo date('e / Z'); // Różnica dla strefy czasowej i identyfikator dla niej (np. Europe/Berlin / 3600)
if (date('I')=="1"){ echo "Czas letni"; }else{ echo "Czas zimowy"; }
// date('I') zwraca 1 gdy czas jest letni - można to wykorzystać
?>

Problemy z formatowaniem

edytuj

Przyjmijmy, że na końcu daty chcesz dodać wyraz "rok". Wiele osób korzystając z możliwości bezpośredniego formatowania daty zrobiłoby tak:

echo date('d-m-Y rok');

Wynik tego kodu byłby podobny do przykładu poniżej:

 06-03-2010 Sat, 06 Mar 2010 21:17:44 +01002010k

To co widzimy, jest zupełnie inny wynik, niż był oczekiwany. Dlaczego? Duża część liter w pierwszym parametrze funkcji date() jest używana do wypisywania danych. PHP widząc fragment:

 rok

Zamienił litery na dane i uzyskano:

 Sat, 06 Mar 2010 21:17:44 +01002010k

Jak to ominąć? Umieszczając tekst poza funkcją, np.:

echo date('d-m-Y ')."rok";

Jest jeszcze jedna metoda nie zapisana w dokumentacji tego języka, ale poprawnie działająca. Polega ona na poprzedzeniu liter backslashem. Trzeba pamiętać, że jeżeli format daty zostałby podany pomiędzy podwójnymi cudzysłowami, to przykładową literę "r" należałoby poprzedzić podwójnym ukośnikiem ( zrobiłby się wówczas z nich tylko jeden ). W przeciwnym wypadku zrobiłaby się z niej tabulacja, a nie litera.

echo date('d-m-Y \r\o\k');

Ten sposób jest bardziej przenośny oraz można go umieścić w jednej stałej i odwoływać się do niego w innych skryptach. W poprzednim sposobie nie ma takiego komfortu.

Lista znaczników

edytuj

d - Dzień miesiąca, 2 cyfry z wiodącymi zerami

D - Tekstowy opis angielskiej nazwy dnia, trzy litery

j - Dzień miesiąca bez zer wiodących

l (mała litera 'L') - Pełen angielski opis dnia tygodnia

N - Liczbowa forma dnia tygodnia, zgodna z normą ISO-8601 (dodana w PHP 5.1.0)

S - Angielski przyrostek porządkowy dla dnia miesiąca, 2 litery

w - Liczbowa forma dnia tygodnia

z - Dzień roku (Zaczynając od 0)

W - Numer tygodnia w roku, zgodny z normą ISO-8601, Tygodnie rozpoczynają Poniedziałki (dostępne od PHP 4.1.0)

F - Pełen angielski opis, dnia miesiąca, taki jak January czy March

m - Liczbowa forma miesiąca, z zerami wiodącymi

M - Krótki, angielski opis miesiąca, trzy litery

n - Liczbowa forma miesiąca, bez zer wiodących

t - Ilość dni w danym miesiącu

L - Informacja o tym, czy rok jest przestępnym

o - Numer roku, zgodny z normą ISO-8601. Zwraca to taką samą wartość jak Y, z takim wyjątkiem, że numer tygodnia ISO (W) należy do poprzedniego lub następnego roku, niż rok użyty w tym miejscu. (dodane w PHP 5.1.0)

Y - Pełna liczbowa forma roku, 4 cyfry

y - Dwie cyfry reprezentujące rok

a - Pora dnia - dwie małe litery (przed/po południu) (ang. Ante/Post meridiem)

A - Pora dnia - dwie duże litery (przed/po południu) (ang. Ante/Post meridiem)

g - Godzina, w formacie 12-godzinnym, bez zer wiodących

G - Godzina, w formacie 24-godzinnym, bez zer wiodących

h - Godzina, w formacie 12-godzinnym, z zerami wiodącymi

H - Godzina, w formacie 24-godzinnym, z zerami wiodącymi

i - Minuty z zerami wiodącymi

s - Sekundy, z zerami wiodącymi

e - Identyfikator strefy czasowej (dodano w PHP 5.1.0)

I (duże i) - Informacja o tym, czy czas jest letni

O - Różnica z czasem Greenwich (GMT) w godzinach

P - Różnica z czasem Greenwich (GMT) z dwukropkiem pomiędzy godzinami i minutami (dodano w PHP 5.1.3)

T - Skrót dla strefy czasowej

Z - Różnica dla strefy czasowej w sekundach. Wyrównanie to jest zawsze ujemne dla stref położonych na zachód od południka 0, oraz dodatnie dla tych leżących na wschód od niego.

c - Data w standardzie ISO 8601 (dodana w PHP 5)

r - Data sformatowana zgodnie z RFC 2822

U - Sekundy liczone od ery UNIX-a (1 stycznia 1970 00:00:00 czasu Greenwich - GMT)

Zmiana daty

edytuj

Wyświetlanie daty i czasu nie ogranicza się do wyświetlania informacji obecnych. Drugi parametr funkcji date() przyjmuje wartość czasu w sekundach od 1 stycznia 1970. Jeśli zostanie on podany, funkcja wyświetli czas, który był zapisany w tym parametrze. Jest on opcjonalny, tak więc:

date('r');

jest tym samym co:

date('r',time());

Przy następnych etapach będzie konieczne poznanie funkcji time, zwracającej czas liczony od tzw. "Ery UNIX-a" (01.01.1970 r.)

Teraz zastanówmy się - jak wyświetlić datę, która będzie za 1 dzień, tydzień, 21 dni, rok? Tutaj należy sobie przypomnieć jednostki i wykorzystać matematykę:

 1 minuta to 60 sekund
 60
 60 minut to 1 godzina
 60*60
 24 godziny to 1 doba
 60*60*24
 7 dni to 1 tydzień
 60*60*24*7
 365 dni to 1 rok (bez uwzględnienia przestępności)
 60*60*24*365

Teraz ładnie trzeba to przełożyć na PHP. Sprawa załatwiona:

<?php
// Aby nie liczyć kilka razy tego samego, użyjemy już wcześniej poznanych stałych
define('MINUTA',60);
define('GODZINA',60*60);
define('DOBA',60*60*24);
define('TYDZIEN',60*60*24*7);
define('ROK',60*60*24*365);

// Zwracamy dane
echo date('d-m-Y H:i:s',time()+MINUTA); // Za minutę
echo "<br>";
echo date('d-m-Y H:i:s',time()+GODZINA); // Za godzinę
echo "<br>";
echo date('d-m-Y H:i:s',time()+3*ROK+17*DOBA+6*GODZINA+39*MINUTA); // Za 3 lata, 17 dni, 6 godzin i 39 minut
?>

Jednak przed nami staje jeszcze problem optymalizacji, oraz dobra naszej klawiatury - pisanie np. 17*ROK+4*TYDZIEN+17*DOBA+4*GODZINA+7*MINUTA+42 jest bardzo męczące. Tutaj znów przychodzi z pomocą PHP, dzięki funkcji strtotime(). Funkcja ta zamienia większość angielskich tekstowych opisów daty i czasu na znacznik czasu:

<?php
echo date('d-m-Y H:i:s',strtotime("now"));
echo date('d-m-Y H:i:s',strtotime("+1 day"));
echo date('d-m-Y H:i:s',strtotime("+1 week 3 days 11 hours"));
echo date('d-m-Y H:i:s',strtotime("next Monday"));
echo date('d-m-Y H:i:s',strtotime("24 January 2003"));
?>

Dzięki temu nie jest wymagane użycie żadnych skomplikowanych operacji matematycznych - PHP zrobi wszystko za ciebie.

Sprawdzanie daty

edytuj

Czasem w formularzu wymagane jest sprawdzenie, czy podana data jest prawdziwa. Tutaj bardzo dobrze sprawdzi się funkcja checkdate(). Przyjmuje ona za argumenty miesiąc, dzień i na końcu rok daty do sprawdzenia.

var_dump(checkdate(7, 16, 2006));
var_dump(checkdate(2, 30, 2003));

Różne formaty daty

edytuj

Zauważ, że checkdate() nie przyjmuje tekstu jako parametrów. Czy to oznacza, że gdy mamy datę np. "9.21.2004", nie można jej sprawdzić? Tutaj pasuje idealnie wcześniej poznana funkcja explode().

$_Dane = explode(".","9.21.2004");
var_dump(checkdate($_Dane[0], $_Dane[1], $_Dane[2]));

Zakończenie

edytuj

Poznałeś już większość funkcji związanych z czasem. Naprawdę jest ich więcej, ale aby ich używać należy wejść w głębia programowania obiektowego, którego podstaw będziesz się uczył już w następnym rozdziale.

 

Sekcja „PHP/Ćwiczenia/Rozmaitości” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

Programowanie obiektowe

edytuj
Poprzedni rozdział: Ćwiczenia
Następny rozdział: Klasy i obiekty

Czym jest programowanie obiektowe?

edytuj

Dotychczas pisane przez nas skrypty miały charakter strukturalny. Programowanie strukturalne jest proste do opanowania i wystarczające dla podstawowych skryptów, jednak wraz z ich rozrastaniem się panowanie nad coraz większą liczbą funkcji staje się coraz trudniejsze. W tym rozdziale nauczysz się nowej techniki programowania zwanej programowaniem obiektowym, która jest wykorzystywana w niemal wszystkich bardziej zaawansowanych aplikacjach. W założeniu jest ona dość intuicyjna i opiera się na naturalnym postrzeganiu świata przez człowieka, lecz wielu programistów miewa z nią na początku kłopoty. Zalecamy dobre przyłożenie się do tego rozdziału, ponieważ znajomość prezentowanego materiału jest kluczowa do zrozumienia dalszej części podręcznika.

Filozofia programowania obiektowego

edytuj

Programowanie zorientowane obiektowo oparte jest na zupełnie innej filozofii, niż dotychczasowe skrypty, jakie pisaliśmy. Podstawowymi bytami, którymi operujemy, są obiekty oraz klasy, które całkiem nieźle odzwierciedlają rzeczywistość wokół nas. Próba wyrażenia wycieczki do sklepu po zakupy przy pomocy funkcji będzie wyglądać nienaturalnie i zupełnie nieintuicyjnie, tymczasem bardzo łatwo możemy ją opisać obiektami. Idziemy do sklepu i bierzemy jeden z dostępnych obiektów klasy koszyk. Wiemy, że we wszystkich koszykach możemy umieszczać towary. Wędrując wśród półek, do koszyka wrzucamy obiekt klasy chleb, która jest szczególnym przypadkiem towaru. Nasz chlebek znajduje się w koszyku, ale na półce są jeszcze inne takie obiekty, które mogą zostać wzięte przez innych klientów. Bierzemy też masło i jakiś napój (wszystko to obiekty odpowiednich klas) i z tym wszystkim wędrujemy do obiektu kasa, któremu przekazujemy nasz koszyk. W kasie dowiadujemy się, ile pieniędzy musimy przekazać, aby pobrane obiekty stały się nasze. Po zapłaceniu odbieramy nasze obiekty i bierzemy je do domu, czyli obiektu klasy dom.

W tej krótkiej historyjce zawarliśmy wszystkie podstawowe cechy programowania obiektowego. W kodzie naszego programu tworzymy klasy, które opisują pewien rodzaj przedmiotu, definiując jego właściwości oraz zachowania. W sklepie mieliśmy do czynienia z klasą koszyk, o której wiedzieliśmy że:

  • Każdy koszyk ma określoną pojemność (właściwość)
  • W koszykach mogą znajdować się towary (właściwość - lista towarów, które są w koszyku).
  • Do koszyków możemy wrzucać nowe towary (zachowanie).

Wszystkie koszyki w sklepie to obiekty tej klasy. Mają one wszystkie identyczny zestaw właściwości oraz identycznie się zachowują, lecz każdy koszyk jest na swój sposób wyjątkowy. Wzięliśmy obiekt koszyk o pojemności 10 litrów i aktualnie zawiera on chleb, masło i napój. Klient, który przeszedł obok nas, niesie swój własny koszyk o pojemności 15 litrów, w którym niesie warzywa, mąkę i mięso. Są to dwa różne koszyki, jednak zachowujące się tak samo.

 
Dla kasjera każda wędlina jest towarem. Dla klienta nie każdy towar jest wędliną.

W historyjce o wizycie w sklepie mówimy też o towarach, czyli obiektach klasy towar. Powiedzieliśmy także, że "chleb jest towarem". Taki związek nosi nazwę dziedziczenia i również występuje w programowaniu obiektowym. Stwierdzenie to oznacza, że chleb posiada wszystkie właściwości i zachowania towaru (cena, możliwość zmiany ceny przez pracownika), a jednocześnie dodaje nowe, specyficzne dla siebie, np. wagę czy możliwość pokrojenia bochenka na pół, co oczywiście wpłynie też na cenę. Każda klasa może dziedziczyć po dowolnej innej klasie, przejmując jej właściwości i zachowania. Dzięki temu w kasie można wszystko podliczyć. Kasa przetwarza towary, zatem interesuje ją jedynie cena bez względu na to, czy dany towar jest chlebem czy napojem. Gdyby w sklepie była krajalnica, obsługiwałaby ona wyłącznie różne rodzaje chleba - nie możemy położyć na niej napoju, ponieważ tu samo bycie towarem już nie wystarcza; obiekt, który kładziemy, musi być co najmniej chlebem.

Jest też jeszcze jeden aspekt dziedziczenia - klasa dziedzicząca może modyfikować zachowania klasy bazowej. Przypuśćmy, że nasze towary są wyposażone w zachowanie oblicz cenę. W podstawowej wersji podaje ono po prostu wartość właściwości cena, jednak niektóre rodzaje towarów mogą wymagać bardziej złożonych przeliczników. Tak będzie w przypadku warzyw, gdzie cena jest uzależniona od wagi kupowanego towaru. W klasie warzywa możemy zaznaczyć: "w zwykłym towarze po prostu braliśmy podaną cenę, jednak tutaj będziemy uwzględniać wagę". Gdy warzywa trafią do kasy, kasjer oczywiście wykona obliczanie ceny. W tym miejscu możliwe są dwa wyniki:

  • Ponieważ kasjer przetwarza towary, wybierze zawsze zachowanie charakterystyczne dla towaru nawet, gdy będzie kasować warzywa, które powinny być traktowane inaczej.
  • Kasjer przetwarza towary, ale wie, że niektóre towary mogą mieć specyficzne zasady wyceniania i stosuje się do nich.

W tym drugim przypadku mamy do czynienia z tzw. polimorfizmem, czyli z sytuacją, gdy wciąż pamiętamy o prawdziwej naturze obiektu, nawet gdy traktujemy go bardziej ogólnie. W PHP wszystkie zachowania są polimorficzne, dlatego w tym języku obowiązywać Cię będzie wyłącznie druga możliwość. Brak polimorfizmu jest ze względów wydajnościowych stosowany w niektórych językach kompilowanych, jak C++ i wspominamy o nim jedynie przy okazji.

Podsumujmy to, czego zdążyliśmy się dowiedzieć. Wbrew pozorom nowych pojęć nie ma wcale aż tak dużo:

  • Klasy - definiują pewien rodzaj obiektów o określonych właściwościach i zachowaniach
  • Obiekty - rzeczywiste byty, na których pracujemy.
  • Właściwości - pewne informacje charakteryzujące obiekt. W dalszej części będziemy je nazywać "polami" klasy bądź obiektu.
  • Zachowania - definiują, co obiekty danej klasy mogą robić. W dalszej części będziemy je nazywać "metodami".
  • Dziedziczenie - pozwala wyrażać zależności "X jest Y-kiem".
  • Polimorfizm - pamiętanie o prawdziwej naturze obiektów nawet, gdy rozpatrujemy je z punktu widzenia ogólniejszej klasy.

Wiemy już, że programowanie obiektowe świetnie opisuje otaczającą nas rzeczywistość, dlatego teraz zastanowimy się, jak za jego pomocą opisać środowisko programu komputerowego, a w szczególności skryptu strony internetowej.

Programowanie obiektowe w aplikacjach

edytuj

Przejście z poziomu analizy świata rzeczywistego do abstrakcyjnego środowiska programu komputerowego sprawia początkującym programistom wiele trudności. Na pierwszy rzut oka ciężko powiedzieć, co w programie powinno być klasą, ile i jakich obiektów tworzyć oraz jak rozdzielić funkcje programu między klasy, by wszystko miało ręce i nogi? Najlepiej przyglądać się rzeczywistym skryptom. Techniki programowania obiektowego są rozbudowaną dziedziną wiedzy, o której można napisać kilka podręczników takich, jak ten. Nabranie wprawy z pewnością zajmie trochę czasu, dlatego nie zrażaj się początkowymi niepowodzeniami.

W aplikacjach WWW podstawowym zastosowaniem programowania obiektowego jest kontrola nad akcją, którą musimy dla użytkownika wykonać. Przypomnijmy sobie naszą księgę gości. Mieliśmy tam różne akcje w stylu "dodaj wpis" czy "przeglądaj wpisy", które były umieszczone w osobnych plikach oraz wybierane instrukcją **switch**. W kodzie obiektowym stworzymy sobie specjalną klasę bazową Moduł. Stwierdzamy w niej, że moduły mają akcje oraz definiujemy metody pozwalające wywołać akcję o podanej nazwie. Następnie tworzymy sobie specjalizowane klasy reprezentujące poszczególne moduły naszej strony: News, Księga gości itd. Każda klasa zawierać będzie kilka akcji związanych z danym modułem reprezentowanych przez metody. Przykładowo, w klasie Księga gości umieścimy metody wyświetl oraz dodaj wpis.

Tworzymy także kolejną klasę Nadzorca, która będzie potrafiła tworzyć obiekty określonych modułów i żądać od nich wykonania określonych akcji. W obrębie jednego żądania może zostać wykonanych kilka akcji z różnych modułów, dlatego nadzorca musi umieć także przechowywać obiekty tych, które już załadował. Działanie skryptu możemy opisać wtedy następująco:

Przypuśćmy, że chcemy wyświetlić listę wpisów w księdze gości. Dlatego tworzymy obiekt nadzorcy i przekazujemy do niego nazwę odpowiedniego modułu i akcji, po czym każemy mu rozpocząć pracę. Nadzorca załaduje odpowiednią klasę, utworzy jej obiekt i nakaże mu wykonać określoną akcję, dzięki czemu w przeglądarce ujrzymy to, co chcemy zobaczyć.

Zauważmy, że od naszych klas tworzyliśmy jedynie po jednym obiekcie. To nie przypadek i nie pomyłka. Bardzo często zdarza się, że tworzymy klasę tylko po to, by mieć dokładnie jeden jej obiekt (ba, czasami nawet wymuszamy odpowiednim kodem niemożność utworzenia większej liczby obiektów). Na początku wydaje się to dziwne, ale zwróćmy uwagę na kilka rzeczy:

  1. Jeśli tworzymy dużą aplikację, chcielibyśmy, aby była ona w miarę spójna. Skoro już utworzyliśmy kilka klas, warto wszystkie elementy aplikacji przedstawić w takiej postaci nawet, jeśli oznacza to tworzenie tylko pojedynczych ich obiektów.
  2. Obiekty posiadają pola, które są niczym innym, jak kolejnym rodzajem zmiennych. Jest to wygodny sposób organizowania danych w naszej aplikacji, bez bardzo brzydkiego tworzenia zmiennych globalnych, instrukcji **global** itd. W dodatku obiekty posiadają bardzo precyzyjne mechanizmy określające, kto i kiedy może dostać się do wybranych danych, co poprawia niezawodność.
  3. Mając klasę, możemy skorzystać z dziedziczenia. Przecież nasza aplikacja WWW może mieć kilku różnych nadzorców do różnych zastosowań. Zależność "X jest Y-kiem" jest bardzo przydatna, a nie uzyskamy jej tak łatwo przy pomocy zwykłych funkcji.

Kolejne zastosowanie to interakcja z bazami danych, do której przejdziemy tuż po rozdziale poświęconym programowaniu obiektowemu. W bazach danych istnieją dziesiątki elementów o różnorodnej strukturze: artykuły, wiadomości, profile użytkowników. Istnieją specjalne biblioteki zwane ORM (ang. Object-Relational mapper), które przenoszą te elementy do aplikacji właśnie w postaci obiektów.

Słowo końcowe

edytuj

Przedstawione tu informacje mają póki co bardziej teoretyczny charakter, który miał zaznajomić Cię z całą ideą. Począwszy od następnego rozdziału zaczniemy już pisać obiektowy kod w PHP, jednak często będziemy wracać do tego, co zostało tutaj powiedziane. Jeszcze raz przypominamy: informacje, które tu poznasz, są niezbędne do rozumienia dalszej części podręcznika. Mogą wydawać się skomplikowane, ale kiedy już załapiesz, o co w tym wszystkim chodzi, Twoje życie naprawdę stanie się o wiele prostsze.

Klasy i obiekty

edytuj

W tym rozdziale nauczymy się, jak tworzyć klasy i obiekty w PHP.

Tworzenie klas

edytuj

W językach programowania klasy traktowane są zawsze jako rodzaj typów danych. Można powiedzieć, że klasa jest definicją lub szablonem obiektów. W PHP deklarujemy je słowem kluczowym class, po którym podajemy jej unikatową nazwę. Zasady jej tworzenia są podobne, jak w przypadku nazw zmiennych, tj. nie mogą one zaczynać się od cyfry. Następnie w nawiasach klamrowych umieszczamy informacje o dozwolonych polach oraz metodach, jakie klasa będzie posiadać:

<?php
class Person
{
   public $name;
   public $surname;
                
   public function setFullName($name, $surname)
   {
      $this->name = $name;
      $this->surname = $surname;
   } // end setFullName();
                
   public function getFullName()
   {
      return $this->name.' '.$this->surname;               
   } // end getFullName();  
}

Zwyczajowo pola deklaruje się na początku klasy, natomiast później - metody, które okazują się być bardzo podobne do funkcji. Podobieństwo jest jak najbardziej uzasadnione. Właściwie funkcje także reprezentują pewne zachowanie, więc nic nie stoi na przeszkodzie, aby wykorzystać tę funkcjonalność do zdefiniowania zachowań obiektów. Zasady działania metod są bardzo podobne - pobierają one argumenty i mogą zwracać wartość. Jedyna istotna różnica to obecność specjalnego wskaźnika $this, który wskazuje zawsze na obiekt, na którym daną metodę wywołujemy. Dzięki niemu możemy dostać się do wartości przechowywanych w polach obiektu oraz wywoływać inne metody. Służy do tego specjalny operator ->. Zauważ, że odwołując się do pól, pomijamy znak dolara. Nie przejmuj się słowem kluczowym public. Jego znaczenie poznamy za chwilę.

Pola klasy zachowują się, jak zwykłe zmienne. Mogą być częścią wyrażeń, możemy do nich przypisywać wartości i wykonywać wszystkie inne operacje, które są prawidłowe dla zmiennych.

Stwórzmy teraz kilka obiektów klasy Person reprezentujących różne osoby. W przeciwieństwie do większości języków kompilowanych, a podobnie jak w Javie, obiekty nie są jednym z rodzajów wartości, ale specjalnym bytem. Wartość obiektowa to jedynie referencja do istniejącego już obiektu. Gdy wykonujemy przypisanie lub przekazujemy obiekt jako argument funkcji/metody, kopiujemy jedynie referencję, a nie obiekt. Wbrew pozorom takie zachowanie jest bardzo praktyczne. W PHP4, gdzie obiekt był jednocześnie wartością, korzystanie z programowania obiektowego było przez to bardzo problematyczne i prowadziło do wielu błędów.

Obiekty tworzymy operatorem new, po którym podajemy nazwę klasy. Zwraca on referencję do obiektu, którą możemy zapisać w zmiennej:

<?php
// Dolaczamy plik z nasza klasa
require('./Person.php');

$janusz = new Person;
$janusz->setFullName('Janusz', 'Kowalski');

$adam = new Person;
$adam->setFullName('Adam', 'Nowak');

echo 'Witaj, jestem '.$janusz->getFullName().'<br/>';
echo 'A ja jestem '.$adam->getFullName().'<br/>';

Możemy też odwołać się bezpośrednio do odpowiednich pól: $adam->name.

Wywołanie metod jest niezwykle proste. Wystarczy wziąć obiekt i po operatorze -> wywołać metodę dokładnie w taki sam sposób, jak to robiliśmy z funkcjami. Metody zawsze działają w imieniu tego obiektu, na którym zostały wywołane, a to prowadzi nas do jednej z kluczowych reguł projektowania obiektowego:

Do klasy Person nie będziemy dodawać metody w stylu polaczSieZBazaDanych(), ponieważ klasa jest definicją obiektów - przypisujemy więc właściwości i metody związane bezpośrednio z opisem człowieka. Łączenie się z bazą danych jest oddzielnym zadaniem. Co więcej, lepiej zrobić to projektując połączenie uniwersalne, na przykład w oddzielnej klasie.

Zmienne obiektowe są referencjami

edytuj

Wspomnieliśmy, że w PHP5 obiekty nie są rodzajem wartości, lecz oddzielnym bytem, do którego skrypt posiada jedynie referencje. Spójrzmy, czym to skutkuje w praktyce. Rozpatrzmy prosty skrypt:

<?php
function modify($value)
{
   $value += 5;
} // end modify();

$number = 6;
modify($number);
echo $number;

Skrypt ten wyświetli nam wartość "6". Zmienna, na której operuje funkcja modify() jest jedynie kopią zmiennej globalnej $number, dlatego jej modyfikacja nie wpływa na wartość oryginału. Inaczej jest w przypadku obiektów:

<?php
require('./Person.php');

function modify($object)
{
   $object->surname = 'Nowak';
} // end modify();

$janusz = new Person;
$janusz->setFullName('Janusz', 'Kowalski');
modify($janusz);
echo $janusz->getFullName();

Tym razem skrypt wyświetlił nam wartość Janusz Nowak, co oznacza, że zmiana stanu obiektu wprowadzona przez funkcję jest widoczna globalnie. Zachowanie jest jak najbardziej prawidłowe. Funkcja modify() operuje nie na kopii, ale na referencji do obiektu. Obie referencje: globalna $janusz oraz lokalna $object są oddzielne, ale wskazują na dokładnie ten sam obiekt. Dlatego wykonanie operacji poprzez jedną z nich sprawi, że druga także zauważy zmiany.

Spróbuj w ramach ćwiczenia wykonać taką samą sztuczkę z operatorem przypisania.

Kontrola dostępu (hermetyzacja)

edytuj

W przeciwieństwie do funkcji i programowania strukturalnego, klasy posiadają precyzyjne mechanizmy kontroli dostępu do swojego wnętrza. Proces ukrywania części funkcjonalności przed programistą nosi nazwę hermetyzacji i pomaga zwiększyć niezawodność oprogramowania. Tworząc klasę, będziemy zawsze starali się określić tzw. publiczny interfejs, z którego może korzystać programista, odwołując się do tworzonych obiektów, jednocześnie ukrywając wszystkie wewnętrzne aspekty działania klasy. Interpreter będzie pilnować, aby użytkownicy nie wywołali żadnej "wewnętrznej" metody, co mogłoby doprowadzić do błędów w działaniu lub użycia jej niezgodnie z przeznaczeniem.

Poznaliśmy już jeden z modyfikatorów dostępu, public, który podaliśmy przed każdą metodą oraz polem. Jest on wyjątkowy, ponieważ w przypadku metod można go pominąć, zostawiając samo function nazwa(), a w przypadku pól zastąpić synonimem var. My jednak będziemy stosować konwencję, w której zawsze jawnie określamy widzialność elementu.

Pozostałe modyfikatory dostępu to protected oraz private. Pierwszy z nich dotyczy dziedziczenia, dlatego zajmiemy się nim później, a tymczasem przyjrzymy się drugiemu z nich. Mówi on, że dane pole lub metoda jest prywatnym elementem klasy. W praktyce oznacza to, że odwołać się do niego możemy wyłącznie z poziomu innej metody w naszej klasie, a z zewnątrz jest on niedostępny. W naszych przykładach będziemy stosować konwencję, w której prywatne elementy będą posiadać nazwy zaczynające się od podkreślenia.

<?php
class SomeClass
{
   private $_field;

   public function getField()
   {
      // Poprawne odwołanie
      return $this->_field;
   } // end getField();

} // end SomeClass;

$someObject = new SomeClass;

// dostęp jest możliwy poprzez metodę
echo $someObject->getField();

// błąd, próba dostępu do pola prywatnego!
echo $someObject->_field;

Powyższy skrypt się nie wykona. W ostatniej linijce PHP zgłosi nam błąd, jakim jest próba dostania się do prywatnego elementu klasy spoza obiektu. Jednocześnie działa odwołanie umieszczone wewnątrz metody getField(). Taki rodzaj metody zwie się po angielsku getter, a oprócz niego mamy też setter, który pozwala ustawić wartość określonemu polu. Jest to jedna z konwencji stosowanych w hermetyzacji, w myśl której klasa nie powinna, poza szczególnymi wyjątkami, zawierać publicznych pól. Jeżeli użytkownik ma mieć dostęp do jakiegoś pola, musi to robić za pośrednictwem dodatkowych metod, czyli getterów i setterów. Zauważmy, że takie podejście pozwala nam tworzyć pola dostępne publicznie tylko do odczytu bez możliwości ich modyfikacji. Wystarczy zadeklarować je jako prywatne i stworzyć publiczny getter, bez settera. Przepiszmy zatem naszą klasę Person zgodnie z regułami hermetyzacji, ograniczając nieco dostęp:

<?php
class Person
{
   private $_name = null;
   private $_surname = null;
                
   public function setName($name)
   {
      if($this->_name === null)
      {
         $this->_name = $name;
         return true;
      }
      return false;
   } // end setName();

   public function setSurname($surname)
   {
      if($this->_surname === null)
      {
         $this->_surname = $surname;
         return true;
      }
      return false;
   } // end setSurname();
                
   public function getName()
   {
      return $this->_name;            
   } // end getName();

   public function getSurname()
   {
      return $this->_surname;            
   } // end getSurname();

   public function getFullName()
   {
      return $this->_name.' '.$this->_surname;
   } // end getFullName();
} // end Person;

Teraz pola $_name oraz $_surname są prywatne, a ich wartość można odczytać wyłącznie poprzez gettery getName() oraz getSurname(). Dostępne są także analogiczne settery setName() oraz setSurname(), jednak zauważmy, że w praktyce można je wywołać tylko raz. Oznacza to, że gdy raz ustawimy danemu obiektowi nazwisko, nie jesteśmy w stanie go już później zmienić, gdyż metoda będzie zawsze zwracała wartość false, odmawiając modyfikacji. Jest to typowy przykład kontroli dostępu i ma on szczególne znaczenie w dużych aplikacjach liczących sobie setki klas, gdzie umożliwia to wymuszenie stosowania się do określonych konwencji i pomaga zapobiegać bałaganowi w kodzie.

Praktyczne zastosowanie

edytuj

Spróbujmy teraz napisać kod, który będzie przydatny w aplikacji WWW. Stworzymy prosty, obiektowy i rozszerzalny mechanizm konfiguracji, na przykładzie którego pokażemy kilka technik projektowania obiektowego. Będzie on składać się z dwóch klas:

  1. Config - zarządza konfiguracją i udostępnia ją skryptowi.
  2. ConfigLoader - rodzaj ładowarki, czyli klasy potrafiącej załadować skądś konfigurację.

Zacznijmy od napisania klasy głównej. Programista będzie mógł dodawać do niej różne ładowarki, a ona będzie udostępniała wczytaną konfigurację pozostałej części skryptu. Opcje konfiguracyjne będą wczytywane leniwie, tj. gdy zajdzie taka potrzeba. Domyślnie ładowarka będzie jedynie kolejkowana w tablicy $_awaitingLoaders. Dopiero przy próbie odczytu nieistniejącej opcji, skrypt będzie prosić kolejne ładowarki o wczytanie swojej części konfiguracji, dopóki nie trafi na taką, która wczyta to, czego potrzebujemy.

<?php

class Config
{
   private $_config = array();
   private $_awaitingLoaders = array();

   public function get($option)
   {
      // Jeśli podana opcja istnieje, zwróć jej wartość
      if(isset($this->_config[$option]))
      {
         return $this->_config[$option];
      }

      // Opcja nie istnieje, sprawdzamy, czy któraś z oczekujących ładowarek ją ma.
      foreach($this->_awaitingLoaders as $id => $loader)
      {
         $this->_config = array_merge($this->_config, $loader->load());
         unset($this->_awaitingLoaders[$id]);
         if(isset($this->_config[$option]))
         {
            return $this->_config[$option];
         }
      }
      return null;
   } // end get();

   public function addLoader(ConfigLoader $loader)
   {
      $this->_awaitingLoaders[] = $loader;
   } // end addLoader();
} // end Config;

Pojawił się tu nowy element składni: public function addLoader(ConfigLoader $loader) - przed nazwą zmiennej pojawiła się nazwa obiektu. Jest to jedno z kolejnych udoskonaleń programowania obiektowego w PHP, czyli określanie typów argumentów. Działa ono zarówno w metodach, jak i funkcjach i mówi, że dany argument może przyjąć wyłącznie obiekt klasy ConfigLoader. Próba podania dowolnego innego rodzaju wartości zakończy się błędem. W PHP określanie typów argumentów ograniczone jest wyłącznie do klasy, a od PHP 5.1 również do tablic (typ Array). Nie można wymuszać typów skalarnych (np. liczby całkowite) ani zasobów. W naszym przypadku daje to nam pewność, że programista nie będzie próbował nakarmić naszego systemu konfiguracji jakimiś dziwnymi danymi, które mogłyby doprowadzić do błędu.

Nasz system konfiguracji stosuje leniwe ładowanie opcji, lecz zwróćmy uwagę, że jego użytkownik wcale nie musi o tym wiedzieć. Dla niego korzystanie z tej klasy ogranicza się do pamiętania, że musi zdefiniować przynajmniej jedną ładowarkę oraz że dostęp do opcji możliwy jest do odczytu poprzez metodę get(). Sposób wczytywania opcji nie jest już przedmiotem jego zainteresowań, dlatego wewnętrzne aspekty działania klasy oraz faktyczny sposób reprezentacji danych został przed nim ukryty, by nie mógł przy nim majstrować.

Napiszmy teraz ładowarkę, która będzie wczytywać opcje z pliku INI:

<?php

class ConfigLoader
{
   private $_fileName = '';

   public function setFilename($filename)
   {
      $this->_fileName = $filename;
   } // end setFilename();

   public function load()
   {
      if(file_exists($this->_fileName))
      {
         return parse_ini_file($this->_fileName);
      }

      // jeśli pliku nie ma, zwróć pustą tablicę
      return array();
   } // end load();
} // end ConfigLoader;

Tu również zastosowaliśmy hermetyzację. Można, a nawet trzeba ustawić nazwę pliku, który chcemy odczytać, jednak nie musi być ona później dostępna, dlatego pominęliśmy całkowicie getter. Metoda load() wywoływana przez naszą klasę Config musi zwrócić tablicę z opcjami wczytanymi z pliku.

To wszystko, spójrzmy teraz, jak wykorzystać nasz system w praktyce:

<?php
require('./Config.php');
require('./ConfigLoader.php');

$config = new Config;

// utworz ladowarki wczytujace rozne fragmenty konfiguracji
$basicConfig = new ConfigLoader;
$basicConfig->setFilename('./config/basic.ini.php');

$securityConfig = new ConfigLoader;
$securityConfig->setFilename('./config/security.ini.php');

$layoutConfig = new ConfigLoader;
$layoutConfig->setFilename('./config/layout.ini.php');

$config->addLoader($basicConfig);
$config->addLoader($securityConfig);
$config->addLoader($layoutConfig);

// zaladujmy pare opcji
echo $config->get('website_name');
echo $config->get('session_time');

Naszemu systemowi brakuje wciąż parę rzeczy. Przykładowo, można by pokusić się o zrobienie różnych rodzajów ładowarek: z plików, z bazy danych itd., lecz do tego potrzebna jest nam znajomość dziedziczenia. Ponadto przydałoby się, aby system potrafił odpowiednio raportować błędy. Póki co, jeśli pomylimy się w nazwie pliku konfiguracji, dowiemy się o tym dopiero po wnikliwym śledztwie, ponieważ jedynym śladem w ładowarce jest zwrócenie pustej tablicy. Odpowiednie mechanizmy poznamy w dalszej części podręcznika.

Stworzony system konfiguracji wykorzystuje jeszcze jedną technikę programistyczną o nazwie kompozycja. Jest to alternatywny do dziedziczenia sposób rozszerzania funkcjonalności obiektów, który jednak nie wymaga żadnej dodatkowej składni. Obrazowo mówiąc, polega on na tym, że jeden obiekt przechowuje referencje do innych obiektów, które potrafi wykorzystywać lub których funkcjonalność potrafi udostępnić na zewnątrz. W przeciwieństwie do dziedziczenia, kompozycja ma charakter dynamiczny. Możemy napisać algorytm, który w locie skomponuje nam gotowy obiekt, niczym z klocków lego, np. na podstawie konfiguracji lub opisu w bazie danych. Kompozycja jest bardzo często stosowana w praktyce obok dziedziczenia.

Zakończenie

edytuj

Mamy już solidne podstawy programowania obiektowego, a także pokazaliśmy, w jaki sposób wykorzystuje się jego własności podczas tworzenia oskryptowania stron internetowych, pisząc modularny i łatwy w rozbudowie system konfiguracji. W następnym rozdziale pokażemy, jak sterować tworzeniem i niszczeniem obiektów.

Poprzedni rozdział: Klasy i obiekty
Następny rozdział: Dziedziczenie

Konstruktory i destruktory

edytuj

Metody klas nie muszą być wywoływane wyłącznie przez programistę tworzącego dany skrypt. Istnieje pewna grupa metod, które są wywoływane automatycznie przez interpreter w momencie zajścia jakiegoś zdarzenia - metody takie nazywamy magicznymi, a w PHP możemy poznać je po tym, że ich nazwy rozpoczynają się od dwóch podkreśleń: __.

Pierwszymi magicznymi metodami, jakie poznamy, będą konstruktor i destruktor, wywoływane odpowiednio w momencie tworzenia oraz niszczenia obiektu.

Konstruktor

edytuj

Konstruktor jest metodą o nazwie __construct(), która może pobierać parametry, lecz nie wolno jej zwracać wartości. Jej zadaniem jest wykonanie pewnych akcji tuż po utworzeniu obiektu tak, aby można było od razu zacząć z nim pracę. Spójrzmy na nasz przykład z osobami, który analizowaliśmy ostatnio. Tuż po utworzeniu pola $_name oraz $_surname miały wartość pustą i należało ręcznie przypisać im wartość, a do tego czasu obiekt Person znajdował się w stanie, który możemy uznać za błędny. Może się zdarzyć, że wskutek pomyłki ktoś zapomni zainicjować odpowiednio obiekt po utworzeniu i przekaże go do dalszego przetwarzania. Gdy błąd się ujawni, moglibyśmy stracić dużo czasu na znalezienie błędu, a nawet potencjalnie zagrozić bezpieczeństwu aplikacji. Dzięki konstruktorom mamy pewność, że nasz obiekt zawsze będzie poprawnie inicjowany. W naszym przypadku chcemy, aby tworzona osoba od razu posiadała imię i nazwisko.

<?php
class Person
{
   private $_name = null;
   private $_surname = null;
 
   public function __construct($name, $surname)
   {
      $this->_name = $name;
      $this->_surname = $surname;
   } // end __construct();
 
   public function getName()
   {
      return $this->_name;            
   } // end getName();
 
   public function getSurname()
   {
      return $this->_surname;            
   } // end getSurname();
 
   public function getFullName()
   {
      return $this->_name.' '.$this->_surname;
   } // end getFullName();
} // end Person;

Settery są już nam niepotrzebne. Jak pamiętamy, mogły być one wywołane tylko raz, a skoro teraz imię i nazwisko jest przypisywane przez konstruktor, nie trzeba dodatkowych metod, które i tak nie zadziałają.

Popatrzmy teraz, jak przekazywać argumenty do konstruktora. Robimy to tuż po nazwie klasy przy operatorze new. Gdy klasa nie posiada konstruktora lub konstruktor nie pobiera argumentów, nawiasy wyjątkowo można w tym wypadku pominąć tak, jak to dotąd robiliśmy. Jednak po dokonanych zmianach musimy już napisać:

$janusz = new Person('Janusz', 'Kowalski');

Ćwiczenie: W poprzednim rozdziale podaliśmy zbudowany na OOP system konfiguracji. Jedną z klas wchodzących do zestawu była ConfigLoader. Dodaj do niej konstruktor, który pobiera nazwę z plikiem konfiguracyjnym tak, aby można go było podać już w momencie tworzenia obiektu. Przerób przykładowy kod tak, aby wykorzystywał możliwości konstruktora.

Destruktor

edytuj

Dotąd nie zajmowaliśmy się zagadnieniem niszczenia obiektów. Jest ono dość specyficzne, ponieważ PHP nie tylko nie narzuca obowiązku niszczenia niepotrzebnych obiektów, ale wręcz programiści często to ignorują. Większość skryptów PHP działa na zasadzie "uruchom się, wygeneruj odpowiedź, zakończ pracę", więc nie ma sensu niepotrzebnie komplikować skrypt i bawić się w zarządzanie cyklem życia obiektów, kiedy i tak wszystkie przestaną istnieć podczas kończenia pracy.

Podobnie jak w większości dynamicznych języków, obiekt przestaje istnieć w momencie usunięcia wszystkich prowadzących do niego referencji. W PHP mamy możliwość zaprogramowania operacji, która ma się wtedy wykonać, dzięki destruktorom. Przykładowo, gdy nasz obiekt reprezentuje otwarty plik, w destruktorze możemy go automatycznie zamknąć. Destruktor jest metodą o nazwie __destruct(), która nie może ani pobierać żadnych argumentów, ani też zwracać wartości. Poniższy przykład ilustruje działanie destruktorów:

<?php

class Destructable
{
   public function __construct()
   {
      echo 'Obiekt klasy Destructable został stworzony.<br/>';
   } // end __construct();

   public function __destruct()
   {
      echo 'Obiekt klasy Destructable został zniszczony.<br/>';
   } // end __destruct();
} // end Destructable;

$firstObject = new Destructable;
$secondObject = new Destructable;
unset($firstObject);

echo 'Kończymy pracę...<br/>';

Wynikiem jego działania jest:

Obiekt klasy Destructable został stworzony.
Obiekt klasy Destructable został stworzony.
Obiekt klasy Destructable został zniszczony.
Kończymy pracę...
Obiekt klasy Destructable został zniszczony.

Pierwsza z informacji o zniszczeniu obiektu powstała w wyniku jawnego wywołania unset($firstObject), które zniszczyło jedyną istniejącą w skrypcie referencję do niego. Drugi napis wygenerowała sekwencja kończenia pracy skryptu, podczas której kolejno niszczone są wszystkie obiekty.

Destruktory są wyjątkowymi metodami z powodu kilku dodatkowych ograniczeń, które ich dotyczą. Jeżeli wykonują się w momencie kończenia pracy skryptu, nie możemy z ich poziomu wysłać nagłówków HTTP, ponieważ te zostały już wysłane do przeglądarki. Ponadto niektóre serwery (np. Apache) zmieniają wtedy katalog roboczy, przez co wszystkie dotychczasowe ścieżki względne przestają wtedy działać. Rozwiązaniem jest wcześniejsze pozyskanie ścieżek bezwzględnych funkcją realpath() (uwaga: jest to operacja dyskowa i nie nadużywaj jej) i zapamiętanie ich do czasu zniszczenia obiektu.

Więcej o niszczeniu obiektów

edytuj

Podczas automatycznego niszczenia obiektów na podstawie licznika referencji pojawia się poważny problem. Przypuśćmy, że mamy obiekt A, który w jednym z pól przechowuje referencję do obiektu B. Jednocześnie B w swoim polu posiada referencję do obiektu A. W ogólnodostępnych zmiennych posiadamy jedną referencję do A, którą kasujemy, przez co oba obiekty stają się nieosiągalne dla naszego skryptu, lecz mimo to nie można ich usunąć, ponieważ liczniki wciąż wskazują, że istnieje do nich po jednej referencji. Problem ten nosi nazwę wykrywania cyklicznych referencji. Odśmiecacze pamięci większości języków (np. Java) potrafią poprawnie rozpoznawać takie sytuacje i mimo wszystko usunąć niedostępne obiekty, lecz PHP aż do wersji 5.3.0 pozbawiony był takiej możliwości. Większość skryptów wykonuje się krótko, dlatego zazwyczaj nikomu to nie przeszkadzało, jednak przy skomplikowanych, obiektowych strukturach danych, które w połowie działania trzeba było usuwać, aby zwolnić trochę pamięci dla reszty skryptu, programista musiał się nieźle nagimnastykować.

Sprawdźmy działanie poniższego skryptu:

<?php

class CircularReference
{
	private $_secondary;
	private $_name;
	
	public function __construct($name)
	{
		$this->_name = $name;
	} // end __construct();
	
	public function setSecondary(CircularReference $object)
	{
		$object->_secondary = $this;
		$this->_secondary = $object;
	} // end setSecondary();
	
	public function __destruct()
	{
		echo 'Obiekt '.$this->_name.' znika.<br/>';
	} // end __destruct();
} // end CircularReference;

// Tworzymy pierwszy obiekt i zapamietujemy referencje w $a
$a = new CircularReference('A');

// Dodajemy drugi obiekt, lecz nie zostawiamy sobie referencji
// Powstaje nam cykl: A ma dostep do B, B ma dostep do A.
$a->setSecondary(new CircularReference('B'));

// Usun jedyna posiadana referencje
unset($a);

echo 'Koniec pracy skryptu.<br/>';

Gdy uruchomimy ten skrypt, jego wynikiem działania powinno być:

Koniec pracy skryptu.
Obiekt A znika.
Obiekt B znika.

Czyli mimo, iż nie mamy do obiektu dostępu, nie jest on niszczony natychmiast. W PHP 5.3 automatyczne wykrywanie cykli może być włączone w pliku php.ini lub poprzez wywołanie funkcji gc_enable(), lecz najprawdopodobniej także i wtedy nasze wyjście będzie wyglądało tak, jak powyżej. Odśmiecacz po prostu czeka, aż uzbiera się wystarczająca liczba referencji i dopiero wtedy przegląda pamięć w poszukiwaniu obiektów. Możemy to wymusić, przerabiając lekko nasz skrypt. Aby PHP nie zawalił nas komunikatami o niszczonych obiektach, dodajmy flagę sygnalizującą, czy obiekt ma nas informować o swoim zniszczeniu, a następnie utwórzmy dodatkową pętlę, która będzie w kółko tworzyć obiekty z cyklami:

<?php
// Uwaga: tylko PHP 5.3.

class CircularReference
{
	private $_secondary;
	private $_name;
	private $_noMsg;
	
	public function __construct($name, $noMsg = false)
	{
		$this->_name = $name;
		$this->_noMsg = $noMsg;
	} // end __construct();
	
	public function setSecondary(CircularReference $object)
	{
		$object->_secondary = $this;
		$this->_secondary = $object;
	} // end setSecondary();
	
	public function __destruct()
	{
		if(!$this->_noMsg)
		{
			echo 'Obiekt '.$this->_name.' znika.<br/>';
		}
	} // end __destruct();
} // end CircularReference;

gc_enable();

// Tworzymy pierwszy obiekt i zapamietujemy referencje w $a
$a = new CircularReference('A');

// Dodajemy drugi obiekt, lecz nie zostawiamy sobie referencji
// Powstaje nam cykl: A ma dostep do B, B ma dostep do A.
$a->setSecondary(new CircularReference('B'));

// Usun jedyna posiadana referencje
unset($a);

// Tworz duzo obiektow z cyklami
for($i = 0; $i < 10000; $i++)
{
	$a = new CircularReference('A', true);
	$a->setSecondary(new CircularReference('B', true));
}

echo 'Koniec pracy skryptu.<br/>';

Tym razem odśmiecacz pamięci zareagował, co poznajemy po zmienionym wyniku:

Obiekt A znika.
Obiekt B znika.
Koniec pracy skryptu.

Spróbuj zmniejszyć ilość iteracji do 1000. Czy wtedy też włącza się odśmiecacz?

Oczywiście czasami nie chcemy czekać, aż PHP zorientuje się, że powinien wyczyścić pamięć. Odnalezienie cykli możemy wymusić, wywołując funkcję gc_collect_cycles(). Przerób powyższy skrypt, usuwając pętlę i zastępując ją wywołaniem tej funkcji. Zauważysz, że nasze obiekty ponownie zostały prawidłowo usunięte przed końcem pracy.

Zakończenie

edytuj

Umiemy już zarządzać tworzeniem oraz niszczeniem obiektu, a także wiemy, jak wykorzystać konstruktory do wymuszenia poprawnej inicjacji tworzonego obiektu. Ponadto poznaliśmy nieco zasady zarządzania pamięcią w PHP, które wprawdzie nie przydają się aż tak często, lecz na pewno warto je znać. W następnym rozdziale zajmiemy się dziedziczeniem.

Poprzedni rozdział: Konstruktory i destruktory
Następny rozdział: Interfejsy

Dziedziczenie klas

edytuj

W naszych rozważaniach z początku rozdziału mówiliśmy o dziedziczeniu, które wyrażało nam, że "X jest Y-kiem". Dziedziczenie pozwala nam rozszerzyć już istniejącą klasę poprzez przejęcie jej pól oraz metod i dołożenie nowych. Jednak współdzielenie kodu to nie wszystko. Obiekty klasy dziedziczącej są jednocześnie obiektami wszystkich klas dziedziczonych, co oznacza, że możemy ich użyć tam, gdzie program spodziewa się dostać ogólniejszy obiekt.

Implementacja dziedziczenia

edytuj

Przypuśćmy, że chcemy zbudować formularz WWW. W jego skład wchodzą różne elementy, lecz podstawowa zasada ich działania jest identyczna. Każdemu możemy ustawić nazwę i przypisać wartość. Skorzystamy z dziedziczenia, aby utworzyć klasę bazową Element zawierającą ogólny interfejs, a następnie stworzymy jej specjalizacje reprezentujące pole tekstowe, listę rozwijaną itd.

<?php

class FormElement
{
   private $_name;
   private $_value;

   public function setName($name)
   {
      $this->_name = $name;
   } // end setName();

   public function getName()
   {
      return $this->_name;
   } // end getName();
   
   public function setValue($value)
   {
      $this->_value = $value;
   } // end setValue();

   public function getValue()
   {
      return $this->_value;
   } // end getValue();
} // end FormElement;

class FormInput extends FormElement
{
   public function display()
   {
      echo '<input type="text" name="'.$this->getName().'" value="'.htmlspecialchars($this->getValue()).'" />';
   } // end display();
} // end FormInput;

class FormTextarea extends FormElement
{
   private $_width = 60;
   private $_height = 15;

   public function setDimensions($width, $height)
   {
      if($width < 1 || $height < 1)
      {
         return false;
      }
      $this->_width = $width;
      $this->_height = $height;

      return true;
   } // end setDimensions();

   public function display()
   {
      echo '<textarea name="'.$this->getName().'" rows="'.$this->_height.'" cols="'.$this->_width.'">'.htmlspecialchars($this->getValue()).'</textarea>';
   } // end display();
} // end FormTextarea;

Aby wykonać dziedziczenie, po nazwie klasy dopisujemy słowo kluczowe extends i podajemy nazwę klasy, którą chcemy rozszerzyć. Dzięki temu zarówno FormInput, jak i FormTextarea mogą korzystać z metod getName() oraz getValue() mimo, iż nigdzie ich nie deklarowaliśmy. Ponadto dołożyły one własne metody umożliwiające wyświetlanie danego pola w określony sposób, a FormTextarea dołożył dodatkowo możliwość ustawiania rozmiarów pola tekstowego. Poniżej pokazany jest przykład wykorzystania naszych klas:

<?php
require('./FormElements.php');

$fields = array();

$field = new FormInput;
$field->setName('nickname');
$field->setValue('Wpisz tu swoją nazwę');

$fields[] = $field;

$field = new FormTextarea;
$field->setName('comment');
$field->setDimensions(60, 5);

$fields[] = $field;

// Wyswietl formularz
foreach($fields as $field)
{
   $field->display();
}

Kontrola dostępu w dziedziczeniu

edytuj

Do tej pory poznaliśmy dwa modyfikatory dostępu: public oraz private. W naszym poprzednim przykładzie zauważyliśmy, że w metodzie display() odwoływaliśmy się do nazwy elementu poprzez getName(), zamiast odczytywać ją bezpośrednio z pola $_name. Wynika to z tego, że klasa pochodna (tj. FormInput, FormTextarea itd.) nie ma dostępu do prywatnych pól i metod klasy bazowej. Modyfikator private ogranicza nam dostęp do obiektów aktualnej i tylko aktualnej klasy, bez żadnych wyjątków. My tymczasem potrzebujemy czegoś pośredniego - zablokować dostęp z zewnątrz, a jednocześnie umożliwić go klasom potomnym. Dlatego wykorzystamy trzeci modyfikator, protected. Dzięki niemu możemy rozbudować naszą klasę FormElement o kilka przydatnych rzeczy.

Chronione elementy klasy są często wykorzystywane do tworzenia metod pomocniczych, które może wykorzystać programista rozszerzający klasę. U nas przydałaby się jakaś szybka możliwość tworzenia listy atrybutów dla znaczników formularzy. Wpisywanie ich na sztywno jest kiepskim pomysłem, ponieważ nie zawsze wszystkie atrybuty są potrzebne. Dlatego dodamy metodę, która przyjmie listę atrybutów jako tablicę i wyświetli tylko te niepuste, a następnie wykorzystamy ją w klasie potomnej.

<?php

class FormElement
{
   protected $_name = null;
   protected $_value = null;

   public function setName($name)
   {
      $this->_name = $name;
   } // end setName();

   public function getName()
   {
      return $this->_name;
   } // end getName();
   
   public function setValue($value)
   {
      $this->_value = $value;
   } // end setValue();

   public function getValue()
   {
      return $this->_value;
   } // end getName();

   protected function _buildAttributes(array $attributes)
   {
      $code = '';
      foreach($attributes as $name => $value)
      {
         if($value !== null)
         {
            $code .= ' '.$name.'="'.htmlspecialchars($value).'"';
         }
      }
      return $code;
   } // end _buildAttributes();
} // end FormElement;

class FormInput extends FormElement
{
   public function display()
   {
      echo '<input'.$this->_buildAttributes(array(
         'type' => 'text',
         'name' => $this->getName(),
         'value' => $this->getValue()
      )).' />';
   } // end display();
} // end FormInput;

W ramach ćwiczenia analogicznie zmodyfikuj klasę FormTextarea.

Stwórz obiekt klasy FormInput i ustaw mu nazwę, ale nie ustawiaj wartości. Wyświetl go i sprawdź źródło. Możemy zauważyć, że metoda pomocnicza _buildAttributes() pominęła nam atrybut value, gdyż jego wartość była pusta i nie było sensu go wyświetlać. Ponadto tym razem możemy już z poziomu naszych klas potomnych odwoływać się bezpośrednio do $_name oraz $_value, ponieważ zadeklarowaliśmy je jako pola chronione.

Unieważnianie metod

edytuj

PHP pozwala na tworzenie w klasach potomnych pól oraz metod, które już istnieją w klasie bazowej. Działanie to nazywa się unieważnianiem, ponieważ nowa wersja metody zastępuje (unieważnia) dotychczasową i nie wymaga żadnej dodatkowej składni. Po prostu tworzymy nową metodę o identycznej nazwie i argumentach, jak istniejąca.

<?php

class FormCheckboxList extends FormElement
{
   private $_options = array();
   protected $_value = array();

   public function setOptions(array $options)
   {
      $this->_options = $options;
      foreach($options as $name => $description)
      {
         $this->_value[$name] = false;
      }
   } // end setOptions(); 

   public function setValue($value)
   {
      if(!is_array($value))
      {
         return false;
      }
      foreach($value as $name => $checked)
      {
         if(isset($this->_options[$name]))
         {
            $this->_value[$name] = (boolean)$checked;
         }
      }
      return true;
   } // end setValue();

   public function display()
   {
      echo '<ul>';
      foreach($this->_options as $name => $description)
      {
         echo '<li><input'.$this->_buildAttributes(array(
            'type' => 'checkbox',
            'name' => $this->_name.'['.$name.']',
            'checked' => ($this->_value[$name] ? 'checked' : null)
         )).' /> '.$description.'</li>';
      }
      echo '</ul>';
   } // end display();
} // end FormCheckboxList;

Nasz nowy rodzaj elementów formularzy, FormCheckboxList, wyświetla listę checkbox-ów. Wartość elementu nie jest już pojedynczą liczbą czy tekstem, ale zbiorem wartości, który musi być odpowiednio przetworzony. Dlatego stworzyliśmy nową wersję metody setValue(), która upewnia się, że dostała tablicę, a później odpowiednio pakuje jej zawartość do wewnętrznych struktur obiektu. Zmieniliśmy także deklarację pola $_value tak, aby początkowo zawsze zawierało tablicę.

Przejrzystość wymaga, aby klasa FormElement dostała pustą metodę display(). Chcemy dzięki temu powiedzieć, że elementy formularzy mogą się wyświetlać, lecz nie precyzujemy jak. Dokładne zasady wyświetlania ustala sobie każdy rodzaj elementu z osobna. Utwórz pustą, publiczną metodę display() w klasie FormElement.

Przyjrzymy się teraz, jak podczas unieważniania zachowują się modyfikatory dostępu oraz argumenty.

Stosowanie się do tej zasady zapewni nam maksymalną zgodność ze standardami PHP. Poniższy przykład ilustruje poprawne konstrukcje:

<?php

class Foo
{
	public function metoda1($foo)
	{
		echo $foo;
	} // end metoda1();

	public function metoda2($foo, $bar = '')
	{
		echo $foo;
	} // end metoda2();

	public function metoda3($foo)
	{
		echo $foo;
	} // end metoda3();
} // end Foo;

class Bar extends Foo
{
	protected $_bar = array();

	public function metoda1($foo)
	{
		echo 'Nowe wywołanie';
	} // end metoda1();

	public function metoda2($foo, $bar = 'Nowa wartość domyślna')
	{
		echo 'Nowe wywołanie';
	} // end metoda2();

	public function metoda3($foo, $bar = 'Nowy argument opcjonalny')
	{
		echo 'Nowe wywołanie';
	} // end metoda3();
} // end Bar;

Zobaczmy teraz, co się stanie, gdy złamiemy jedną z tych reguł. Sprawmy, by nowy argument w metodzie metoda3() był wymagany (tj. usuwamy domyślną wartość):

public function metoda3($foo, $bar)

Jeżeli nie grzebaliśmy nic przy ustawieniach raportowania błędów, które zostały zaproponowane na początku podręcznika, dostaniemy niemiły komunikat:

Strict Standards: Declaration of Bar::metoda3() should be compatible with that of Foo::metoda3() in /sciezka/do/pliku.php on line 35

Błędy Strict Standards to upomnienia, że naruszyliśmy jakąś regułę. Mimo iż skrypt pozornie działa dalej, nigdy nie należy ich ignorować, ponieważ ich łamanie może spowodować, że nasz skrypt przestanie działać na przyszłych wersjach PHP lub że jego wykonywanie może prowadzić do błędów w innym miejscu. Zauważmy: obiekty Bar są jednocześnie obiektami klasy Foo. Jeżeli w pewnym miejscu skrypt oczekuje obiektów Foo, ma prawo zakładać, że metoda3() posiada jeden wymagany argument i tak też ją wywoła. Tymczasem my beztrosko wprowadzamy mu obiekt klasy Bar, który teoretycznie powinien tu zadziałać, lecz powoduje nam niespodziewany błąd, gdyż tutaj metoda3() ma już dwa wymagane argumenty. To jest cena naszej ignorancji.

Modyfikatory dostępu mogą być zmieniane w ograniczonym zakresie. Możemy osłabiać dostęp (np. zastąpić metodę prywatną chronioną oraz chronioną przez publiczną), ale nie możemy czynić go bardziej restrykcyjnym. Jeśli w klasie bazowej metoda zadeklarowana jest jako chroniona, próba zastąpienia jej metodą prywatną skończy się natychmiastowym błędem krytycznym.

Unieważniając metody, mamy dostęp do jeszcze jednej przydatnej możliwości. Przypuśćmy, że chcemy jedynie rozszerzyć istniejącą funkcjonalność, a nie całkowicie ją zastępować. Na szczęście PHP pozwala na wywołanie pierwotnej wersji metody:

public function metoda($argument)
{
   echo 'Trochę dodatkowych operacji';
   parent::metoda($argument);
} // end metoda();

Konstruktory i destruktory w dziedziczeniu

edytuj

Konstruktory oraz destruktory zachowują się prawie identycznie, jak zwykłe metody. Gdy klasa potomna nie będzie zawierać konstruktora albo destruktora, PHP po prostu odziedziczy go po klasie bazowej. Jednak jeśli zdecydujemy się go unieważnić, nie wykona się on, dopóki tego nie powiemy explicite poprzez znaną już nam konstrukcję parent::metoda(). Jedyna różnica polega na tym, że nie ma ograniczeń co do zmiany ilości argumentów w konstruktorach klas potomnych. Jest tak, ponieważ konstruktor jest wywoływany automatycznie tylko podczas tworzenia obiektu konkretnej klasy, do której należy.

Dodajmy konstruktor do klasy FormElement tak, aby można było natychmiast ustawić nazwę:

<?php

class FormElement
{
   protected $_name;
   protected $_value;

   public function __construct($name)
   {
      $this->_name = $name;
   } // end __construct();

   // dalsza część klasy
} // end FormElement;

To wystarczy, aby przy tworzeniu wszystkich elementów trzeba było określić ich nazwę. Klasy potomne odziedziczą ten konstruktor i wykorzystają go u siebie. Jednak gdy zdecydujemy się wprowadzić nowy konstruktor, musimy jawnie powiedzieć, że chcemy wywołać także stary:

<?php

class FormCheckboxList extends FormElement
{
   private $_options = array();
   protected $_value = array();

   public function __construct($name, array $options)
   {
      parent::__construct($name);
      $this->setOptions($options);
   } // end __construct();

   // dalsza część klasy
} // end FormCheckboxList;

Jak widać w konstruktorze dodaliśmy nowy argument wymagany.

Aby uczynić klasę bardziej elastyczną, możemy dodać wartosć domyślną do argumentu konstruktora:

<?php

class FormElement
{
   protected $_name;
   protected $_value;

   public function __construct($name = null)
   {
      $this->_name = $name;
   } // end __construct();

   // dalsza część klasy
} // end FormElement;

Dzięki temu określenie nazwy elementu bezpośrednio podczas tworzenia obiektu jest możliwe, ale nie jest już wymagane. Analogicznie możesz zmodyfikować klasę FormCheckboxList

Klasy abstrakcyjne

edytuj

W naszej rzeczywistości nie wszystkie pojęcia muszą mieć praktyczne zastosowanie. Niektóre pozwalają po prostu wygodnie odnosić się do grupy wielu różnych rzeczy. W historyjce z początku rozdziału mówiliśmy o towarach: chlebie, napojach, warzywach itd., ale nigdzie nie pojawił się towar, który był tylko towarem i niczym więcej. Dlatego powiemy, że towar jest pojęciem abstrakcyjnym. Nie może występować samodzielnie, ale może pomagać określać inne pojęcia. W świecie programowania obiektowego towar byłby tzw. klasą abstrakcyjną. Jest to specjalny rodzaj klasy stworzony specjalnie po to, aby po nim dziedziczyć. Tworzenie obiektów klas abstrakcyjnych jest zabronione, za to można tworzyć obiekty ich klas potomnych.

Klasę abstrakcyjną deklaruje się, dodając przed słowem class dodatkowy modyfikator abstract. Ponownie przyjrzyjmy się naszemu systemowi wyświetlania formularzy. Takim abstrakcyjnym bytem jest tam FormElement. Sam w sobie nie nadaje się do niczego, ponieważ nie ma zdefiniowanego wyświetlania, za to stanowi podbudowę dla innych klas. Przepiszmy tę klasę po raz kolejny:

<?php

abstract class FormElement
{
   protected $_name = null;
   protected $_value = null;

   public function __construct($name)
   {
      $this->_name = $name;
   } // end __construct();

   public function setName($name)
   {
      $this->_name = $name;
   } // end setName();

   public function getName()
   {
      return $this->_name;
   } // end getName();
   
   public function setValue($value)
   {
      $this->_value = $value;
   } // end setName();

   public function getValue()
   {
      return $this->_value;
   } // end getName();

   abstract public function display();

   protected function _buildAttributes(array $attributes)
   {
      $code = '';
      foreach($attributes as $name => $value)
      {
         if($value !== null)
         {
            $code .= ' '.$name.'="'.htmlspecialchars($value).'"';
         }
      }
      return $code;
   } // end _buildAttributes();
} // end FormElement;

Teraz próba napisania new FormElement('nazwa') zakończy się błędem wykonania skryptu.

W podanym kodzie możemy zauważyć jeszcze jedną rzecz, a mianowicie metodę abstrakcyjną. Analogicznie jak w przypadku klasy, jest to metoda, która jest przeznaczona do tego, aby ją unieważnić, dlatego też nie może zawierać żadnej treści. Po nawiasach zamykających listę argumentów możemy postawić już jedynie średnik. Idealnie nadaje się to dla naszej metody display(). Chcemy powiedzieć, że elementy mogą być wyświetlane, ale szczegóły pozostawiamy do zaimplementowania klasom potomnym.

Definicja ta mówi nam jeszcze jedno. Przypuśćmy, że stworzyliśmy klasę dziedziczącą po FormElement, która nie zaimplementowała metody display(). Dopóki jej nie dodamy, nowa klasa także musi być zadeklarowana jako abstrakcyjna!

Do koncepcji abstrakcji będziemy często wracać w następnych rozdziałach.

Słowo "final"

edytuj

Słowo abstract wymusza na programiście konieczność dziedziczenia po klasie oraz unieważnienia metody. W PHP istnieje także jego odwrotność, czyli słowo final, która uniemożliwia takie działanie. Jedyna różnica polega na tym, że klasa posiadająca metody finalne, nie musi być deklarowana jako finalna.

Metody finalne należy stosować tam, gdzie obecność jakiegoś algorytmu w takiej postaci, w jakiej go napisaliśmy, jest krytyczna dla działania całości, a próba jego unieważnienia mogłaby się dla skryptu skończyć tragicznie.

Obiekt potomka jest obiektem klasy bazowej

edytuj

Przyjrzyjmy się teraz praktycznej obserwacji dotyczącej dziedziczenia. Do tej pory używaliśmy go wyłącznie jako wygodny sposób na współdzielenie kodu, jednak mechanizm ten sięga o wiele głębiej. Napiszmy klasę zarządcy dla naszego formularza:

<?php

class FormBuilder
{
   protected $_elements = array();

   final public function addElement(FormElement $element)
   {
      $this->_elements[] = $element;
      return $element;
   } // end addElement();

   public function display()
   {
      foreach($this->_elements as $element)
      {
         $element->display();
      }
   } // end display();
} // end FormBuilder;

Metoda addElement() została zadeklarowana jako finalna, ponieważ nie przewidujemy możliwości zmiany sposobu dodawania nowych elementów do formularza. Programista może rozszerzyć tę klasę, by np. udoskonalić wyświetlanie, ale proces dodawania nowych elementów ma zostawić w spokoju. Zwróćmy także uwagę na argument, który musi być obiektem klasy FormElement. Spójrzmy teraz, jak używać nowej klasy:

<?php

require('./FormElements.php');
require('./FormBuilder.php');

$form = new FormBuilder;
$form->addElement(new FormInput('name'));
$form->addElement(new FormInput('email'));
$form->addElement(new FormTextarea('comment'))->setDimensions(70, 15);

$form->display();

Ukazuje on praktyczne znaczenie dziedziczenia. Choć tworzymy obiekty klasy FormInput, możemy je przekazać jako argument FormElement, ponieważ dziedziczenie mówi nam jasno: FormInput używa interfejsu z FormElement, posiada wszystkie jego zachowania oraz właściwości, dlatego obiekt FormInput jest jednocześnie obiektem FormElement. Przy okazji spójrzmy na następującą linijkę:

$form->addElement(new FormTextarea('comment'))->setDimensions(70, 15);

Jest to ilustracja, że referencja do obiektu wcale nie musi być przechowywana w zmiennej. Jeżeli jakaś funkcja lub metoda zwraca obiekt, możemy tuż po niej napisać -> i dostać się do jego elementów bez potrzeby stosowania zmiennej tymczasowej. Zastosowaliśmy tu pewną sztuczkę: metoda addElement() po prostu zwraca element przekazany jako argument po to, aby nie trzeba było przepisać obiektu do zmiennej, gdy chcemy ustawić jeszcze jakieś dodatkowe właściwości. Możemy to rozbudować jeszcze dalej i sprawić, że metody takie, jak setValue() czy setDimensions() będą po prostu zwracać $this, dzięki czemu taki łańcuszek wywołań metod można ciągnąć w nieskończoność. Technika ta nosi nazwę fluent interface i spotkamy się z nią jeszcze w dalszej części podręcznika.

Ćwiczenie

edytuj

Aby przećwiczyć poznane wiadomości, wróćmy do przykładu z systemem konfiguracji. Nadaje się on idealnie do dalszej rozbudowy poprzez dziedziczenie. Zauważmy, że konfiguracji nie musimy ładować wyłącznie z plików INI. Zamiast tego, możemy utworzyć abstrakcyjną klasę ConfigLoader definiującą interfejs ładowania konfiguracji, która następnie będzie rozszerzana przez rozmaite specjalizacje, np. IniFileConfigLoader, PhpConfigLoader itd. Twoim zadaniem jest odpowiednia przebudowa tamtego kodu. Zdefiniuj interfejs klasy abstrakcyjnej ConfigLoader. Określ, które metody muszą być abstrakcyjne, a które finalne oraz gdzie zastosować słowo protected. Dodaj niezbędne konstruktory i destruktory oraz interfejs fluent interface do głównej klasy. W IniFileConfigLoader dodaj możliwość wyłączenia sprawdzania, czy plik konfiguracyjny istnieje. Przetestuj swoje modyfikacje na podanym przykładzie:

<?php

$config = new Config;

$config->addLoader(new IniFileConfigLoader('./basic.ini'))->setFileExistsCheck(true);
$config->addLoader(new PhpConfigLoader('./extra.php'));

echo $config->get('basic_option');
echo $config->get('php_option');

Zakończenie

edytuj

Po poznaniu dziedziczenia potrafimy już całkiem sporo wyrazić przy pomocy obiektów. Dziedziczenie ma jednak pewne ograniczenie: nie można dziedziczyć po dwóch klasach naraz:

<?php

class Klasa extends A, B
{
   // ...
} // end Klasa;

W następnym rozdziale poznamy mechanizm pozwalający częściowo poradzić sobie z tym problemem, czyli interfejsy.

Poprzedni rozdział: Dziedziczenie
Następny rozdział: Wyjątki

Interfejsy

edytuj

Dziedziczenie jest jednym z najważniejszych mechanizmów programowania obiektowego, gdyż pozwala rozszerzać istniejące klasy oraz traktować je bardziej ogólnie. Podobnie jak większość języków programowania, PHP nie zezwala jednak na tzw. dziedziczenie wielokrotne, czyli możliwość rozszerzenia więcej niż jednej klasy naraz:

<?php
class MultipleInheritance extends FirstClass, SecondClass
{

} // end MultipleInheritance;

Przy tradycyjnej konstrukcji systemu obiektowego w języku dziedziczenie wielokrotne wprowadza wiele patologii, a jego poprawne używanie wymaga od programisty ścisłej dyscypliny i szczegółowej wiedzy o hierarchii klas. Zamiast niego, PHP wykorzystuje zaczerpniętą z Javy ideę interfejsów.

Z definicji wynika, że interfejs możemy traktować, jak coś w rodzaju klasy czysto abstrakcyjnej. Może on zawierać wyłącznie stałe klasowe, z którymi zapoznamy się w dalszej części podręcznika, oraz abstrakcyjne prototypy metod pozbawione implementacji. Tę dostarcza dopiero klasa, dlatego mówimy, że implementuje ona interfejs. Interfejsów nie dotyczą ograniczenia dziedziczenia. Programista może jednocześnie rozszerzać inną klasę oraz obok implementować dowolną liczbę interfejsów.

Przykładowe użycie

edytuj

Rozpatrzmy prosty system uprawnień. Główna klasa stanowi interfejs dostępu, który potrafi odpowiadać na pytania, czy użytkownik powinien mieć dostęp do wskazanego zasobu. Oprócz tego chcemy mieć zestaw innych klas, które potrafiłyby generować listę uprawnień dla naszego systemu. Nie chcemy ograniczać możliwości ich dziedziczenia, dlatego zamiast tego utworzymy interfejs opisujący, czego system uprawnień wymaga od twórcy generatora uprawnień, aby mógł on poprawnie działać.

<?php

interface AclGeneratorInterface
{
   public function generate();
   public function isAllowed($resource);
} // end AclGenerator;

class AclSystem
{
   private $_permissions = array();

   public function loadGenerator(AclGeneratorInterface $generator)
   {
      foreach($generator->generate() as $resource => $access)
      {
         if(!isset($this->_permissions[$resource]))
         {
            if($access == 2)
            {
               // Tutaj dostęp będzie generowany dynamicznie
               $this->_permissions[$resource] = $generator;
            }
            elseif($access == 0 || $access == 1)
            {
               $this->_permissions[$resource] = $access;
            }
         }
      }
   } // end loadGenerator();

   public function isAllowed($resource)
   {
      if(!isset($this->_permissions[$resource]))
      {
         return false;
      }
      if(is_object($this->_permissions[$resource]) && $this->_permissions[$resource] instanceof AclGeneratorInterface)
      {
         return $this->_permissions[$resource]->isAllowed($resource);
      }
      return (bool)$this->_permissions[$resource];
   } // end isAllowed();
} // end AclSystem;

Aby utworzyć interfejs, po słowie kluczowym interface wpisujemy jego unikalną nazwę, a następnie w nawiasach klamrowych wymieniamy listę wszystkich prototypów metod, których ma on dostarczać. Wszystkie metody interfejsu muszą być z założenia publiczne, dlatego nie można tu używać innych modyfikatorów dostępu, jednak powszechną konwencją jest dopisywanie słowa kluczowego public dla celów czytelności. Powyższy skrypt nie przedstawia póki co żadnej klasy, która by implementowała AclGeneratorInterface, jednak mamy pokazany kod odpowiedzialny za jego wykorzystanie. Jak widać w linii 13, interfejsy mogą być stosowane do typowania argumentów, identycznie jak klasy. W naszym przykładzie chcemy mieć pewność, że wszystkie obiekty, które przekażemy jako argument do loadGenerator() posiadały metody generate() oraz isAllowed() bez względu na ich położenie w hierarchii klas.

W linii 38 widoczna jest jeszcze jedna konstrukcja, czyli specjalny operator instanceof. Pozwala on testować czy dany obiekt jest instancją wybranej klasy lub implementuje określony interfejs.

Zobaczmy teraz, jak zaimplementować interfejs w klasie tak, by PHP o tym wiedział. Napiszemy przykładowy generator, który wczyta uprawnienia z pliku.

<?php

class FileGenerator implements AclGeneratorInterface
{
   private $_role = '';

   public function __construct($role)
   {
      $this->_role = (string)$role;
   } // end __construct();

   public function generate()
   {
      $acl = parse_ini_file('./access/'.$this->_role.'.ini');
      foreach($acl as &$permission)
      {
         if($permission != 0 && $permission != 1)
         {
            $permission = 0;
         }
      }
      return $acl;
   } // end generate();

   public function isAllowed($resource)
   {
      return false;
   } // end isAllowed();
} // end FileGenerator;

Aby zaimplementować interfejs, wystarczy po nazwie klasy podać słowo kluczowe implements i wymienić listę interfejsów, oddzielając je przecinkami. Musimy pamiętać o następujących ograniczeniach:

  1. Implementowane metody muszą mieć dokładnie taki sam nagłówek, jak w interfejsie.
  2. Klasa nie może implementować dwóch (lub więcej) interfejsów, które mają metodę o tej samej nazwie (od PHP 5.3.9 może, pod warunkiem, że zduplikowane metody mają taki sam nagłówek).
  3. Jeżeli klasa odziedziczyła jakąś metodę, której wymaga implementowany interfejs, jej nagłówek musi być zgodny z zawartością interfejsu.

Poniżej pokazany jest przykład błędnego nazewnictwa metod:

<?php
interface GooInterface
{
	public function bar($arg1, $arg2);
} // end GooInterface;

class Foo
{
	public function foo()
	{
		echo 'foo';
	} // end foo();
	
	public function bar($joe = 'joe')
	{
		echo 'bar';
	} // end bar();
} // end Foo;

class Bar extends Foo implements GooInterface
{
	public function joe()
	{
		echo 'joe';
	} // end joe();
} // end GooInterface;

Po uruchomieniu tego skryptu zobaczymy:

Fatal error: Declaration of Foo::bar() must be compatible with that of GooInterface::bar() in /test.php on line 22

Mówi on, że odziedziczona metoda bar() jest niezgodna z interfejsem, który właśnie próbujemy zaimplementować.

Dziedziczenie interfejsów

edytuj

Interfejsy można również dziedziczyć dokładnie tak samo, jak klasy, przy pomocy słowa kluczowego extends. Tutaj jednak wielokrotne dziedziczenie jest jak najbardziej dozwolone pod warunkiem, że nie ma konfliktów metod. Dziedziczenie interfejsów jest rzadko spotykane w rzeczywistych skryptach, lecz warto wiedzieć, że ono istnieje. Poniżej przedstawiony jest przykładowy skrypt pokazujący sposób wykorzystania i działanie:

<?php
interface Foo
{
	public function foo();
} // end Foo;

interface Bar
{
	public function bar();
} // end Bar;

interface Joe extends Foo, Bar
{
	public function joe();
} // end Joe;

class Abc implements Joe
{
	public function foo()
	{
		echo 'foo';
	} // end foo();
	
	public function bar()
	{
		echo 'bar';
	} // end foo();
	
	public function joe()
	{
		echo 'joe';
	} // end foo();
} // end Abc;

$class = new Abc;
if($class instanceof Foo)
{
	echo 'Ten obiekt implementuje interfejs Foo<br/>';
}
if($class instanceof Bar)
{
	echo 'Ten obiekt implementuje interfejs Bar<br/>';
}
if($class instanceof Joe)
{
	echo 'Ten obiekt implementuje interfejs Joe<br/>';
}

Interfejsy wbudowane

edytuj

Język PHP posiada kilkanaście specjalnych interfejsów rozpoznawanych przez interpreter i przeznaczonych do dodatkowych zastosowań. Wchodzą one w skład tzw. Standard PHP Library, czyli nowej standardowej biblioteki PHP zbudowanej w całości w oparciu o programowanie obiektowe. Będziemy się z nią zapoznawać po kawałku również w dalszej części podręcznika.

W poprzednich rozdziałach poznaliśmy funkcję sizeof() (znaną też jako count()), która zwraca ilość elementów w tablicy. Może ona współpracować także z obiektami, zwracając ilość publicznych pól:

<?php
class Counter
{
	public $foo;
	protected $bar;
}

$counter = new Counter;
echo sizeof($counter);

Wiedza o ilości pól jest rzadko potrzebna w praktyce, jednak nic nie stoi na przeszkodzie, aby to przeprogramować. Tutaj przyda nam się pierwszy specjalny interfejs o nazwie Countable. Dostarcza on metodę count(), którą PHP wywołuje, gdy chce uzyskać informacje o ilości elementów.

Pierwszym specjalnym interfejsem, jaki poznamy, jest Countable, który dostarcza metodę count(). Informuje on interpreter, że klasa, która go implementuje, jest zbiorem elementów, które można policzyć (identycznie, jak elementy w tablicy). Rozbudujmy klasę Config z pierwszego rozdziału tak, aby można było uzyskać informacje o ilości aktualnie załadowanych opcji. Zakładamy, że wykonałeś ćwiczenie z poprzedniego rozdziału dotyczące jej rozbudowy:

<?php
class Config implements Countable
{
   private $_config = array();
   private $_awaitingLoaders = array();

   public function count()
   {
      return sizeof($this->_config);
   } // end count();

   // pozostała część klasy
} // end Config;

Teraz możemy prosto dowiedzieć się, ile opcji aktualnie znajduje się w konfiguracji:

<?php
require('./Config.php');
$config = new Config;
echo 'Ilość elementów w konfiguracji: '.sizeof($config);

Obiekty można jeszcze bardziej upodobnić do tablic dzięki interfejsowi ArrayAccess. Dostarcza on czterech metod:

  • offsetGet($key) - wywoływana przy próbie odczytu: $obiekt['klucz']
  • offsetSet($key, $value) - wywoływana przy próbie zapisu: $obiekt['klucz'] = 5
  • offsetExists($key) - wywoływana przy próbie sprawdzenia, czy element o podanym kluczu istnieje: isset($obiekt['klucz'])
  • offsetUnset($key) - wywoływana przy próbie usunięcia elementu o podanym kluczu: unset($obiekt['klucz'])

Aby przećwiczyć interfejsy w praktyce, cofnijmy się do przykładu z systemem formularzy z poprzedniego rozdziału. Do klasy abstrakcyjnej FormElement dodamy specjalne chronione pole $_attributes będące tablicą przechowującą dodatkowe atrybuty dla elementu formularza (np. klasa CSS, ID itd.). Klasa musi implementować interfejsy ArrayAccess oraz Countable, które pozwolą na proste zarządzanie atrybutami tak, jakby obiekt elementu był tablicą. Zaimplementuj wszystkie wymagane metody tak, aby odpowiednio modyfikowały pole $_attributes. Zmodyfikuj klasy odpowiednich elementów tak, aby uwzględniały dodane atrybuty w generowanym kodzie HTML. Poniżej znajduje się przykładowy plik testowy:

<?php
 
require('./FormElements.php');
require('./FormBuilder.php');
 
$form = new FormBuilder;
$element = $form->addElement(new FormInput('name'));
$element['class'] = 'name';
$element['id'] = 'f_name';

if(isset($element['id']))
{
   unset($element['id']);
}
 
$form->display();

Interfejsów specjalnych jest jeszcze więcej. Wśród nich specjalną grupę stanowią tzw. iteratory. Poświęcimy im cały osobny rozdział.

Kiedy stosować?

edytuj

Nie ma jednej, uniwersalnej reguły mówiącej, kiedy należy stosować interfejsy, a kiedy dziedziczenie klas. Zasadniczo gdy chcemy dostarczyć klasie użytkownika pewną, gotową część implementacji, jesteśmy skazani na dziedziczenie, gdyż PHP nie udostępnia żadnych innych mechanizmów jej wstrzykiwania. Jednak gdy pragniemy jedynie zdefiniować listę zachowań, których oczekujemy, bez wnikania w szczegóły ich działania, interfejsy są o wiele lepszym pomysłem, gdyż mogą być implementowane niezależnie oraz nie zamykają drogi do dziedziczenia. To do nas, jako projektantów architektury aplikacji, należy odpowiedź na jakiej funkcjonalności najbardziej nam zależy i odpowiednio wybrać dostępne środki. Polecamy analizować obiektowo napisane skrypty i biblioteki, aby zapoznać się z ich budową. Naśladowanie dobrych wzorców to jedna z najlepszych szkół.

Musimy mieć świadomość, że interfejsy w połączeniu z dziedziczeniem nie gwarantują nam pełnej swobody wielokrotnego wykorzystania kodu. Dzięki interfejsom możemy swobodnie przenosić listę wymaganych zachowań, lecz nie da się przenieść implementacji metod bez użycia dziedziczenia, które ma ograniczenia. Istnieje szansa, że przyszłe wersje PHP będą oferować jeszcze jeden mechanizm do obejścia tego problemu.

Zakończenie

edytuj

Poznaliśmy już prawie wszystkie główne mechanizmy obiektowe, które dostarcza nam PHP. Interfejsy znacząco poszerzyły nasze możliwości wyrażania zależności między klasami. Kolejny rozdział poświęcony będzie profesjonalnym mechanizmom raportowania błędów przy pomocy wyjątków. Od strony technicznej nie są one częścią programowania obiektowego, lecz w PHP silnie na nim bazują i dlatego ich omówienie znajduje się właśnie tutaj.

Poprzedni rozdział: Interfejsy
Następny rozdział: Elementy statyczne

Wyjątki

edytuj

Błędy nie muszą wynikać wyłącznie z nieuwagi programisty. Może je powodować użytkownik poprzez dziwne działania, niewłaściwą konfigurację lub nawet problemy systemowe. Obsługa błędów jest to jeden z istotniejszych elementów współczesnych aplikacji. Początkujący programiści często ją bagatelizują, kompletnie nie przejmując się tym, że przy złych ustawieniach na ekranie przeglądarki pojawia się 500 ostrzeżeń PHP, albo załatwiając sprawę najprostszą komendą die(). Wykorzystują to hakerzy, dla których ścieżki dostępu, nazwy plików i numery linii wyświetlane przy komunikatach PHP to znakomite źródło informacji o serwerze, skrypcie oraz umiejętnościach programisty.

Ignorowanie zagrożenia wynika także z tego, że PHP przez długi czas nie miał zadowalających mechanizmów obsługi błędów, a opracowane wtedy prymitywne rozwiązania funkcjonują do dnia dzisiejszego. Zastanówmy się zatem, czego będziemy wymagać od systemu obsługi błędów:

  1. Priorytet - nie wszystkie błędy są krytyczne dla pracy skryptu. Gdy nie uda nam się połączyć z bazą danych, najprawdopodobniej nie będziemy w stanie nic wyświetlić, dlatego informujemy internautę, że tym razem musi obejść się smakiem. Jednak brak pliku językowego nie jest już aż tak krytyczny. Prawdopodobnie tłumacze nie zakończyli jeszcze swej pracy, dlatego tymczasowo możemy obejść problem, wczytując komunikaty w domyślnym języku, które już są gotowe.
  2. Miejsce wystąpienia - sposób obsługi błędu często zależy też od miejsca jego wystąpienia. Brak pliku konfiguracyjnego to poważna sprawa, lecz inne funkcje brak potrzebnych im plików mogą interpretować inaczej. Jako programiści musimy mieć możliwość decydowania, co zrobić z błędnymi sytuacjami w konkretnym miejscu skryptu.
  3. Przerwanie pracy - gdy wystąpi błąd, dalsze wykonywanie funkcji zazwyczaj przestaje mieć sens, jednak nie zawsze chcemy przy tym przerwać cały skrypt. System obsługi błędów musi mieć możliwość przerwania tych partii wykonywanego kodu, dla których wykryty problem jest krytyczny, nie zakłócając przy tym reszty skryptu.

Te trzy właściwości posiadają wyjątki. W PHP każdy wyjątek jest specjalnym obiektem klasy Exception lub jej pochodnych. Wyjątki można rzucać oraz obsługiwać. Skrypt rzuca wyjątek podczas wystąpienia sytuacji, którą uznajemy za błędną. Powoduje on przerwanie wykonywania bieżącego fragmentu kodu. Kod zawarty jest w specjalnych blokach informujących, jak wyjątki danego rodzaju obsłużyć. Interpreter przerywa wszystkie aktualnie wykonywane funkcje, dopóki nie trafi na blok, który wie, jak obsłużyć rzucony wyjątek i jemu przekazuje sterowanie. W kodzie obsługi wyjątku możemy sprawdzić, co to jest za błąd oraz np. wyświetlić internaucie informacje o problemie.

Wyjątki w praktyce

edytuj

Przyjrzyjmy się teraz, jak suchy opis prezentuje się w praktyce. Poniżej prezentujemy zmodyfikowaną wersję klasy FileConfigLoader z naszego systemu konfiguracji omówionego w poprzednich rozdziałach. Dotychczas brak pliku obsługiwaliśmy, po prostu zwracając pustą tablicę, jednak logiczniejszym jest rzucenie wtedy wyjątku z informacją o problemie. W przykładzie zakładamy, że wykonałeś ćwiczenie z rozdziału Dziedziczenie.

<?php
 
class FileConfigLoader extends FileLoader
{
   private $_fileName = '';
 
   public function setFilename($filename)
   {
      $this->_fileName = $filename;
   } // end setFilename();
 
   public function load()
   {
      if(file_exists($this->_filename))
      {
         return parse_ini_file($this->_filename);
      }
 
      // jeśli pliku nie ma, rzuć wyjątek
      throw new Exception('Cannot read the configuration file: '.$this->_filename);
   } // end load();
} // end FileConfigLoader;

Wyjątki rzucamy przy pomocy słowa kluczowego throw, którego argumentem jest wyrażenie dające obiekt klasy Exception lub pochodnych. Może on być wczytany ze zmiennej lub zwrócony przez inną metodę, lecz najczęściej spotykaną konstrukcją jest podana w przykładzie throw new, która w locie tworzy potrzebny obiekt. Konstruktor klasy Exception domyślnie pobiera w argumencie komunikat błędu, który możemy dobrać według upodobań.

Na razie tak rzucony wyjątek spowoduje co najwyżej wyświetlenie komunikatu Fatal error z informacją, że PHP nie znalazł żadnego bloku potrafiącego go przechwycić i obsłużyć. Przechwytywaniem wyjątków zajmuje się blok try... catch:

<?php
require('./Config.php');
require('./ConfigLoader.php');
 
$config = new Config;

try
{
   $config->addLoader(new FileConfigLoader('./config/basic.ini.php'));
   // nieistniejący plik
   $config->addLoader(new FileConfigLoader('./config/unexisting.ini.php'));

   // spróbujmy odczytać nieistniejącą opcję. Jak pamiętamy, spowoduje to
   // próbę wczytania wszystkich plików oraz odkrycie, że jeden z nich nie
   // istnieje. Rzucony zostanie wyjątek.
   echo $config->get('unexisting_option');

   // to się już nie wykona
   echo $config->get('website_name');
}
catch(Exception $exception)
{
   echo 'Wystąpił błąd w linii '.$exception->getLine().': '.$exception->getMessage();
}

Jak pamiętamy, nasz system konfiguracji obsługuje leniwe ładowanie, czyli pliki odczytywane są w momencie odwołania do nieznanej opcji. Przy tej okazji obiekt stworzony dla pliku unexisting.ini.php odkryje, że taki plik nie istnieje i rzuci wyjątek, przerywając kolejne poziomy wykonywania skryptu, dopóki nie natrafi na blok try, który przechwytuje wszystkie wyjątki, jakie wystąpiły w podanym kodzie. Następnie porównuje je z blokiem catch, gdzie precyzujemy, jakie klasy wyjątków chcemy obsługiwać oraz do jakiej zmiennej należy zapisać przechwycony wyjątek. Jeśli dany blok nie może odnaleźć dopasowania, interpreter wraca do przerywania pracy kolejnych funkcji i metod, dopóki nie natrafi na następny try...catch. Kiedy PHP zlokalizuje już kod wiedzący, jak obsłużyć dany wyjątek, przeskakuje do odpowiadającego mu bloku catch i wykonuje go.

W naszym przypadku przepływ sterowania będzie wyglądać następująco:

  1. Próbujemy odczytać opcję unexisting_option.
  2. Opcja jest niedostępna. Próbujemy załadować pliki.
  3. Ładujemy plik basic.ini.php.
  4. Opcja się nie pojawiła, więc przechodzimy dalej.
  5. Ładujemy plik unexisting.ini.php
  6. Taki plik nie istnieje. Rzucamy wyjątek.
  7. PHP przerywa wykonywanie metody load() w klasie FileConfigLoader.
  8. PHP przerywa wykonywanie metody get() w klasie Config, która odpowiada za pobranie wartości nieistniejącej opcji.
  9. PHP przerywa wykonywanie dalszego kodu w bloku try.
  10. PHP znajduje blok catch i odkrywa, że potrafi on obsługiwać wyjątki klasy Exception.
  11. PHP zapisuje obiekt wyjątku do zmiennej $exception i rozpoczyna wykonywanie kodu należącego do bloku catch.
  12. Wyświetlamy komunikat i linię, w której wystąpił błąd.
  13. Kontynuujemy pracę skryptu od pierwszej linijki po bloku try ... catch.

Zagnieżdżanie bloków try

edytuj

Bloki try... catch można zagnieżdżać. PHP próbuje dopasować rzucony wyjątek do najniższego z bloków, który potrafi go obsłużyć i tam przekierowuje działanie skryptu. Zazwyczaj aplikacja posiada jeden główny blok, którego zadaniem jest przechwycenie wszystkich wyjątków i potraktowanie ich jako błędy krytyczne. Wszędzie, gdzie chcemy potraktować je łagodniej, stosujemy dodatkowe bloki, w których określamy inny sposób obsługi.

Poniżej pokazany jest przykład zagnieżdżania. Za pomocą argumentu URL where można ustawić, w którym miejscu skrypt ma rzucić wyjątek:

<?php

if(!isset($_GET['where']))
{
   $_GET['where'] = 0;
}

try
{
   if($_GET['where'] == 0)
   {
     throw new Exception('Błąd 0');
   }
   echo 'Dalsza część bloku...<br/>';
   try
   {
      if($_GET['where'] == 1)
      {
        throw new Exception('Błąd 1');
      }
      echo 'Dalsza część bloku podrzędnego...<br/>';
   }
   catch(Exception $exception)
   {
      echo 'Problem: '.$exception->getMessage().'<br/>';
   }
  
   echo 'Dalsza część skryptu...<br/>';
}
catch(Exception $exception)
{
   echo 'Błąd krytyczny: '.$exception->getMessage().'<br/>';
}

Wywołaj skrypt z argumentem ?where=0 oraz ?where=1. Pierwsze wywołanie rzuci wyjątek Błąd 0, który zostanie przechwycony przez główny blok, który leży najbliżej, i potraktowane jak błąd krytyczny. Wyjątek z drugiego wywołania znajduje się w podrzędnym bloku, dlatego to właśnie on zostanie użyty do jego obsługi. Zauważmy, że po wykonaniu klauzuli catch skrypt kontynuuje działanie od końca bloku - w drugim przypadku wciąż wyświetli nam się napis "Dalsza część skryptu". Instrukcje echo umieszczone tuż po throw nie wykonają się nigdy.

Pokażemy teraz przykład praktycznego zastosowania, w którym wykorzystamy klasę SplFileObject z biblioteki Standard PHP Library. Udostępnia ona obiektowy interfejs do operacji na plikach, a ewentualne błędy raportuje jako wyjątki RuntimeException. Naszym zadaniem jest wczytanie konfiguracji strony WWW oraz tekstu powitalnego:

  1. Brak konfiguracji jest krytyczny - bez niej nie jesteśmy w stanie wyświetlić strony.
  2. Brak tekstu powitalnego możemy potraktować łagodniej - być może osoba odpowiedzialna za treść merytoryczną jeszcze go nie dostarczyła, dlatego zamiast niego wystarczy wyświetlić tekst domyślny.
<?php
try
{
   $config = new Config;

   // Ładowanie konfiguracji strony
   $manualLoader = new ManualConfigLoader();
   $configFile = new SplFileObject('./config/website.conf');
   foreach($configFile as $line)
   {
      $option = explode('=', $line);
      $manualLoader->addOption(trim($option[0]), trim($option[1]));
   }
   $config->addLoader($manualLoader);

   // Ładowanie tekstu powitalnego.
   $text = '';
   try
   {
      $introFile = new SplFileObject($config->get('intro_file'));
      foreach($introFile as $line)
      {
         $text .= $line;
      }
   }
   catch(RuntimeException $exception)
   {
      $text = 'Brak zdefiniowanego tekstu powitalnego.';
   }

   // Wyświetl tekst powitalny
   echo $text;
}
catch(RuntimeException $exception)
{
   echo 'Błąd krytyczny: '.$exception->getMessage();
}

Aby przykład zadziałał, należy dołączyć do niego stworzony w poprzednich rozdziałach system konfiguracji oraz napisać klasę ManualConfigLoader. Jest to kolejny system ładowania, który umożliwia ręczne definiowanie opcji konfiguracyjnych z poziomu skryptu poprzez metodę addOption($nazwa, $wartosc). Pozostawiamy to jako ćwiczenie.

Obiekty klasy SplFileObject reprezentują pojedynczy plik tekstowy, który jest otwierany w konstruktorze. Wszystkie błędy sygnalizowane są rzuceniem wyjątku. Najprostszy odczyt z pliku polega na umieszczeniu obiektu w pętli foreach, dzięki czemu w zmiennej $line pojawi się treść kolejnych linijek. Przykład pokazuje także, jak wykorzystać try ... catch jako instrukcję sterowania przepływem wykonania skryptu. Przyjrzyjmy się, jak ładowany jest tekst powitalny. Przed wejściem do sekcji krytycznej tworzymy pustą zmienną $text. W bloku try próbujemy ją wypełnić na podstawie treści z pliku. Jeśli proces ten zostanie z jakiegoś powodu przerwany, PHP rzuci wyjątek, który sprawi, że do zmiennej zostanie zapisana wartość domyślna. W ten sposób tuż po wykonaniu bloku mamy pewność, że w zmiennej $text zawsze coś się znajdzie - albo wczytane z pliku, albo uzupełnione domyślną treścią. Wyjątek potrzebny jest nam jedynie do przerwania wykonywania aktualnego kodu i przeskoczenia do catch. Nie zajmujemy się jego zawartością oraz informacjami.

Klasa Exception

edytuj

Klasa Exception jest podstawą wszystkich wyjątków, jakie można rzucać w PHP. Udostępnia ona zbiór podstawowych metod do zarządzania informacjami o wyjątku i często jest rozszerzana poprzez dziedziczenie. Programiści często nie dodają żadnej nowej funkcjonalności w klasach pochodnych. Chodzi o to, aby móc przechwytywać tylko pewien rodzaj wyjątków pochodzących z określonego źródła:

<?php

class CustomException extends Exception { }

try
{
   // kod ...

}
catch(CustomException $exception)
{
  // Przechwytujemy jedynie wyjątki CustomException, a
  // innymi się nie zajmujemy.
}

Konstruktor klasy Exception przyjmuje do trzech argumentów:

  1. Komunikat $message
  2. Kod błędu $code
  3. Poprzedni wyjątek $previous

Ponadto mamy do dyspozycji szereg metod służących do pobierania informacji o wyjątku, gdy już go przechwycimy:

  • getMessage() - zwraca komunikat błędu.
  • getCode() - zwraca kod błędu.
  • getPrevious() - zwraca poprzedni wyjątek.
  • getFile() - zwraca nazwę pliku, w którym wyjątek został rzucony.
  • getLine() - zwraca numer linii, w której wyjątek został rzucony.
  • getTrace() - zwraca tablicę zawierającą ślad stosu, czyli informacje o wszystkich funkcjach i metodach wywołanych w momencie rzucenia wyjątku.
  • getTraceAsString() - zwraca ślad stosu, ale jako tekst.

Biblioteka SPL dostarcza zbiór gotowych klas wyjątków dla najczęstszych problemów. Programista powinien je stosować wszędzie tam, gdzie to jest potrzebne, zachowując ich znaczenie.

  • RuntimeException - reprezentuje błędy, które mogą być wykryte jedynie w trakcie wykonywania (brak pliku, błąd połączenia itd.).
  • OutOfRangeException - reprezentuje błędy przekroczenia przez wartość ustalonego zakresu.
  • RangeException - podanie niewłaściwego zakresu wartości.
  • OutOfBoundsException - brak wartości o podanym kluczu.
  • DomainException - próba użycia niewłaściwych danych (np. spodziewamy się listy opcji konfiguracyjnych, a dostaliśmy zawartość menu).
  • LengthException - wartość ma niewłaściwą długość.
  • InvalidArgumentException - argument ma niewłaściwą wartość (np. spodziewaliśmy się liczby, a dostaliśmy tablicę).
  • OverflowException - próba dodania nowej wartości do przepełnionego pojemnika na dane (np. gdy ustawiliśmy odgórny limit na 100 elementów).
  • UnderflowException - przeciwieństwo poprzedniego, czyli próba pobrania wartości z pustego.

Dobrą praktyką jest pisanie tzw. kodu idiotoodpornego, który nie sypie się przy podaniu niewłaściwych danych, lecz rozsądnie to sygnalizuje, dzięki czemu reszta aplikacji ma szansę odpowiednio zareagować. Nietrudno zauważyć, że powyższe klasy wyjątków dotyczą najbardziej podstawowych problemów, jakie mogą wystąpić w naszym skrypcie, dlatego powinny być wykorzystane przy sprawdzaniu warunków początkowych i brzegowych.

Problem sprzątania

edytuj

Przerywanie pracy w połowie skomplikowanego procesu nie zawsze jest dobrym pomysłem. Metoda mogła przydzielić sobie sporo zasobów (np. otwarte pliki), a rzucając wyjątek uniemożliwiamy jej ich zwolnienie. Może to doprowadzić do kolejnych problemów w dalszej części skryptu lub nawet uszkodzenia danych. Problem ten znany jest pod nazwą problemu sprzątania, czyli jak zwolnić przydzielone zasoby po rzuceniu wyjątku. Niektóre języki oferują dla niego wbudowane wsparcie poprzez dodatkową klauzulę finally w bloku try. Wykonuje się ona zawsze, niezależnie od tego czy pojawi się wyjątek czy nie, a jej zadaniem jest posprzątanie po całym procesie. Brakuje jej jednak w PHP, dlatego programiści muszą radzić sobie inaczej. Często spotykanym rozwiązaniem jest dodanie "fikcyjnego" bloku try ... catch, który przechwytuje wyjątek tylko po to, by posprzątać, po czym rzuca go ponownie:

public function complexMethod()
{
   try
   {
      echo 'Skomplikowane obliczenia';

      // domyślne sprzątanie.
      $this->_cleanObject();
   }
   catch(Exception $exception)
   {
      $this->_cleanObject();
      throw $exception;
   }
} // end complexMethod();

Podświetlona linijka ukazuje dopiero co przechwycony wyjątek, który rzucamy ponownie, by zajął się nim nadrzędny blok try, ponieważ my chcieliśmy tylko posprzątać.

Wyjątki w PHP

edytuj

W PHP wyjątki pojawiły się stosunkowo późno. O ile we własnym kodzie nie ma problemu z ich stosowaniem, to większość dostępnych wbudowanych rozszerzeń nie potrafi raportować błędów przy ich pomocy. Obsługę wyjątków posiada jedynie kilka najnowszych, obiektowych modułów:

  • Standard PHP Library
  • Biblioteka PHAR
  • Biblioteka PDO

Niestety i to nie jest regułą. Twórcy PHP nie są konsekwentni i wciąż zdarza im się wprowadzać nowe rozszerzenia z autorskimi mechanizmami obsługi błędów (np. biblioteka Intl wprowadzona w PHP 5.3.0). Aby pisać w PHP, trzeba do tego po prostu przywyknąć.

Język posiada jednak możliwość tłumaczenia wielu standardowych błędów na wyjątki. Sztuczka polega na napisaniu specjalnej funkcji obsługi błędów i zainstalowaniu jej w skrypcie:

<?php
function ExceptionErrorHandler($errno, $errstr, $errfile, $errline)
{
   if(in_array($errno, array(E_WARNING, E_RECOVERABLE_ERROR)))
   {
      throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
   }
   return false;
}

set_error_handler('ExceptionErrorHandler');

Powyższy kod sprawi, że ostrzeżenia (Warning) oraz przechwytywalne błędy krytyczne (Catchable fatal error) zostaną przekonwertowane na wyjątki.

Zakończenie

edytuj

Wyjątki to elegancki mechanizm obsługi błędów, który świetnie współpracuje z programowaniem obiektowym. W następnym rozdziale powrócimy do niego z powrotem, aby omówić elementy statyczne klas, z których można korzystać bez konieczności posiadania obiektów.

Poprzedni rozdział: Wyjątki
Następny rozdział: Metody magiczne

Elementy statyczne

edytuj

Poznane do tej pory mechanizmy programowania obiektowego bazowały w całości na obiektach, czyli rzeczywistych, niezależnych bytach reprezentujących odpowiednie klasy. Jednak w niektórych sytuacjach tworzenie obiektu tylko po to, by móc wykonać jakąś metodę jest przerostem formy nad treścią. Dlatego PHP, podobnie jak większość innych języków obiektowych, oferuje możliwość tworzenia tzw. elementów statycznych klasy. Do działania nie potrzebują one jej obiektów, ale ponieważ są powiązane z klasą, wciąż mogą wykorzystywać np. dziedziczenie. Możemy to traktować jako rozszerzenie zwykłych funkcji i zmiennych globalnych o niektóre właściwości obiektów.

Tworzenie statycznych pól oraz metod

edytuj

Aby utworzyć statyczne pole lub metodę, dodajemy do jego deklaracji słowo kluczowe static. Następnie możemy wykorzystać operator zakresu :: poprzedzony nazwą klasy, by dostać się do nich. Nie potrzebujemy przy tym żadnego obiektu tej klasy. W poniższym przykładzie używamy elementów statycznych do kontrolowania, ile obiektów klasy zostało już utworzonych.

<?php

class TypicalClass
{
   private $_value;
   static private $_objectCount = 0;

   public function __construct($value)
   {
      $this->_value = $value;
      self::$_objectCount++;
   } // end __construct();

   public function getValue()
   {
      return $this->_value;
   } // end getValue();

   static public function getObjectCount()
   {
      return self::$_objectCount;
   } // end getObjectCount();
} // end TypicalClass;

$object1 = new TypicalClass('foo');
$object2 = new TypicalClass('bar');
$object3 = new TypicalClass('joe');

echo 'Do tej pory utworzona została następująca ilość ';
echo 'obiektów TypicalClass: '.TypicalClass::getObjectCount();

self jest odpowiednikiem $this dla metod statycznych i wskazuje na aktualną klasę. Oprócz tego mamy też parent, z którego korzystaliśmy już przy omawianiu dziedziczenia. Jak łatwo się domyślić, wskazuje on na klasę bazową do naszej. Zauważmy, że w przeciwieństwie do ->, po operatorze zakresu musimy podać znak dolara, odwołując się do statycznego pola klasy.

Metody statyczne posiadają pewne ograniczenie w stosunku do ich zwykłych odpowiedników. Nie są wywoływane w kontekście obiektu, dlatego nie można w nich korzystać ze zmiennej specjalnej $this. Pomimo tego, PHP dopuszcza ich wywoływanie na dwa sposoby:

// sposob 1
SomeClass::staticMethod();

// sposob 2
$object = new SomeClass;
$object->staticMethod();

Okazuje się, że nic nie przeszkadza, aby metodę statyczną wywołać jak zwykłą, lecz nie będzie to miało żadnego znaczenia. Zalecamy, aby unikać takiego mieszania, gdyż wprowadza ono nieład w kodzie i utrudnia jego analizę innym osobom. Działanie w drugą stronę, tj. wywoływanie metod niestatycznych jako statyczne powoduje wygenerowanie komunikatu E_STRICT.

Statyczne elementy w dziedziczeniu

edytuj

Elementy statyczne także podlegają dziedziczeniu. Z poziomu klasy B rozszerzającej A możemy dostać się do wszystkich statycznych metod i pól zadeklarowanych w tej drugiej, a także nadpisać je. Modyfikatory dostępu takie, jak protected i private pozwalają ograniczyć możliwość stosowania wyłącznie do innych metod klasy aktualnej oraz klas pochodnych. Przydaje się to, gdy potrzebujemy zamknąć jakąś często wykonywaną operację w postaci funkcji, lecz jednocześnie nie chcemy udostępniać jej użytkownikom naszej klasy.

W poniższym przykładzie wykorzystamy tzw. wzorzec projektowy, czyli ogólny przepis na osiągnięcie pewnego efektu w programowaniu obiektowym. Wzorcom projektowym przyjrzymy się bliżej w dalszej części podręcznika, dlatego nie będziemy ich tutaj dokładnie objaśniać. Jednym ze wzorców jest fabryka. Czasami zwykły konstruktor oraz operator new jest zbyt mało elastyczny, aby móc utworzyć obiekt. Przykładowo, chcielibyśmy, aby był on już od razu prawidłowo skonfigurowany do pracy lub aby na podstawie dostarczonych danych utworzony był obiekt jednej z klas rozszerzających wybieranej dynamicznie. Najogólniej ujmując, rozwiązanie polega na utworzeniu statycznej metody factory(), która tworzy nowy obiekt, konfiguruje go i udostępnia skryptowi. Nasza uproszczona fabryka będzie musiała uwzględniać to, że klasa może być dziedziczona i wtedy programista musi mieć możliwość poprawienia jej, aby tworzyć także obiekty nowego rodzaju.

<?php

class BaseItem
{
   static protected $_instances = 0;

   private function __construct()
   {
      echo 'Tworzymy obiekt podstawowy<br/>';
   } // end __construct();

   public function doSomeConfig()
   {
      echo 'Konfigurujemy obiekt '.((get_class($this) == 'BaseItem') ? 'podstawowy' : 'specjalny').'<br/>';
   } // end doSomeConfig();

   public function work()
   {
      echo 'Ja działam: '.get_class($this).'<br/>';
   } // end work();

   public static function factory()
   {
      $object = new BaseItem;
      $object->doSomeConfig();
      self::$_instances++;

      return $object;
   } // end factory();
} // end BaseItem;

class SpecialItem extends BaseItem
{
   private function __construct()
   {
      echo 'Tworzymy obiekt specjalny<br/>';
   } // end __construct();

   public static function factory()
   {
      $object = new SpecialItem;
      $object->doSomeConfig();
      parent::$_instances++;

      return $object;
   } // end factory();
} // end SpecialItem;

$baseObject = BaseItem::factory();
$specialObject = SpecialItem::factory();

$baseObject->work();
$specialObject->work();

Zwróćmy uwagę, że obie klasy posiadają prywatny konstruktor. Oznacza to, że programista może utworzyć ich obiekty jedynie za pośrednictwem naszej fabryki, która jest częścią klasy i dzięki temu może wywołać konstruktor (ograniczenia widoczności działają na podst. porównywania klas, a nie obiektów). W klasie BaseItem zadeklarowany został chroniony licznik obiektów współdzielony także przez wszystkie klasy rozszerzające. Jednak rozszerzając ją, nie możemy zostawić fabryki niezmienionej. W metodzie factory() mamy jasno powiedziane, jakiej klasy obiekt tworzymy, przez co wywołanie SpecialItem::factory() dalej tworzyłoby nam w rzeczywistości obiekty BaseItem. Musimy nadpisać wspomnianą metodę, zmieniając tę jedną linijkę i pozostawiając resztę kodu niezmienioną. Przy okazji widać także, że do statycznych elementów klasy bazowej można odwoływać się poprzez parent.

Jako ćwiczenie sprawdź następujące rzeczy:

  1. Czy zastąpienie parent przez self także zadziała i czy nie zmieni wyniku? Dodaj statyczną metodę count(), która zwróci wartość licznika i użyj jej, aby się o tym przekonać.
  2. Zakomentuj metodę factory() w klasie SpecialItem i spróbuj wyjaśnić zachowanie skryptu pamiętając o tym, że konstruktory obu klas są prywatne.

Wnikliwi czytelnicy powinni dostrzec w kodzie wywołanie funkcji get_class(). Zwraca ona nazwę klasy obiektu podanego w argumencie. W szczególności, jeśli za argument podamy $this, możemy dowiedzieć się wszystkiego o obiekcie, który wywołał aktualną metodę. Spróbuj pomyśleć, jak metoda może wykorzystać tę funkcję do dowiedzenia się, czy została wywołana na obiekcie klasy bazowej czy pochodnej?

Stałe klasowe

edytuj

PHP 5.1.0 wprowadził koncepcję stałych klasowych. Zachowują się one dokładnie tak samo, jak poznane już zwykłe stałe, lecz są powiązane z konkretną klasą, a dostęp do nich odbywa się za pośrednictwem operatora :: poprzedzonego nazwą klasy. Stałą deklarujemy przy pomocy słowa kluczowego const, po którym podajemy jej nazwę oraz żądaną wartość. Stałe klasowe są stosowane do nazywania różnych specjalnych wartości, które są przeznaczone do wykorzystywania z obiektami naszej klasy. Ponieważ ich częścią jest nazwa klasy, nie musimy obawiać się o konflikt nazw oraz tworzyć bardzo długich, złożonych identyfikatorów. Przykład zastosowania podany jest poniżej:

<?php

class File
{
   const READ = 1;
   const WRITE = 2;

   private $_ptr;
   private $_fileName;
   private $_mode;

   public function __construct($fileName, $mode = self::READ)
   {
      $this->_fileName = $fileName;
      $this->_mode = $mode;

      if(!file_exists($fileName))
      {
         throw new FileException('Podany plik nie istnieje: '.$fileName);
      }

      switch($mode)
      {
         case self::READ:
            $this->_ptr = fopen($fileName, 'r');
            break;
         case self::WRITE:
            $this->_ptr = fopen($fileName, 'w');
            break;
         case self::READ | self::WRITE:
            $this->_ptr = fopen($fileName, 'rw');
      }
   } // end __construct();

   // pozostałe metody
} // end File;

try
{
   $file = new File('./plik.txt', File::READ);

   // inne działania
}
catch(FileException $exception)
{
   die('Nie można otworzyć pliku');
}

W przykładzie tworzymy klasę File, która ma reprezentować otwarty plik. Chcielibyśmy w przejrzysty sposób reprezentować tryby otwarcia, dlatego utworzyliśmy dla nich kilka stałych klasowych wykorzystywanych jako binarne flagi. Aby powyższy przykład zadziałał, należy we własnym zakresie dopisać klasę wyjątku FileException.

Stałe klasowe mogą być też deklarowane w interfejsach. Wtedy można się do nich odwoływać zarówno za pośrednictwem nazwy interfejsu, jak i nazw klas go implementujących.

Późne wiązanie statyczne

edytuj

Wróćmy do naszej fabryki. Ma ona pewien drobny mankament. Jeśli programista chce utworzyć więcej klas pochodnych, musi dla każdej z nich od zera napisać fabrykę. Pół biedy, gdy jest ona prosta, ale wyobraźmy sobie, że składa się z ponad 100 linii kodu. Jaką mamy gwarancję, że programista nie popsuje czegoś? Co zrobić, gdy będziemy chcieli dodać funkcjonalność do klasy bazowej, która będzie musiała być uwzględniona przez wszystkie fabryki potomne? Nie zawsze musimy mieć wpływ na to, co napisze użytkownik naszej klasy.

Problem polega na tym, że metoda factory() jest monolitem, który nie rozróżnia samego utworzenia obiektu od jego konfiguracji. Dlatego wprowadźmy dodatkową, chronioną metodę _concreteFactory(), której zadaniem jest wyłącznie utworzenie obiektu i przekazanie go ogólnej fabryce, która dokona reszty. Programista musi jedynie zmodyfikować _concreteFactory(), pozostawiając konfigurację w naszych rękach.

<?php

class BaseItem
{
   static protected $_instances = 0;

   private function __construct()
   {
      echo 'Tworzymy obiekt podstawowy<br/>';
   } // end __construct();

   public function doSomeConfig()
   {
      echo 'Konfigurujemy obiekt '.((get_class($this) == 'BaseItem') ? 'podstawowy' : 'specjalny').'<br/>';
   } // end doSomeConfig();

   public function work()
   {
      echo 'Ja działam: '.get_class($this).'<br/>';
   } // end work();

   public static function factory()
   {
      $object = self::_concreteFactory();
      $object->doSomeConfig();
      self::$_instances++;

      return $object;
   } // end factory();

   protected static function _concreteFactory()
   {
      return new BaseItem;
   } // end _concreteFactory();
} // end BaseItem;

class SpecialItem extends BaseItem
{
   private function __construct()
   {
      echo 'Tworzymy obiekt specjalny<br/>';
   } // end __construct();

   protected static function _concreteFactory()
   {
      return new SpecialItem;
   } // end _concreteFactory();
} // end SpecialItem;

$baseObject = BaseItem::factory();
$specialObject = SpecialItem::factory();

$baseObject->work();
$specialObject->work();

Ups, okazuje się, że skrypt nie do końca działa tak, jak chcemy. Ci, którzy odpowiedzieli na pytania z podrozdziału "Statyczne elementy w dziedziczeniu", powinni już znać wyjaśnienie tego, co się stało. Rezultatem działania skryptu jest:

Tworzymy obiekt podstawowy
Konfigurujemy obiekt podstawowy
Tworzymy obiekt podstawowy
Konfigurujemy obiekt podstawowy
Ja działam: BaseItem
Ja działam: BaseItem

Innymi słowy, wywołanie SpecialItem::factory() całkowicie zignorowało istnienie nadpisanej metody SpecialItem::_concreteFactory() i ponownie wywołało jej odpowiednik w klasie bazowej. Okazuje się, że PHP wiąże wywołanie z konkretną metodą w kodzie już w fazie kompilacji. Kompilując metodę factory(), od razu powiązał ją na sztywno z _concreteFactory() w tej samej klasie, bez oglądania się na dziedziczenie. W pierwszym rozdziale o programowaniu obiektowym wspominaliśmy o polimorfiźmie oraz o tym, że w PHP wszystkie metody są polimorficzne z definicji i nie trzeba się tym przejmować. Doprecyzujmy teraz: wszystkie niestatyczne metody są polimorficzne, a w przypadku statycznych, musimy skorzystać z tzw. późnego wiązania statycznego (ang. late static binding) wprowadzonego w PHP 5.3.0. Chcemy poinformować PHP, że odwołanie do _concreteFactory() może prowadzić do różnych metod w zależności od tego czy wywołamy BaseItem::factory() czy SpecialItem::factory(). Dokonujemy tego poprzez zastąpienie słowa kluczowego self przez static:

<?php

class BaseItem
{
   static protected $_instances = 0;

   private function __construct()
   {
      echo 'Tworzymy obiekt podstawowy<br/>';
   } // end __construct();

   public function doSomeConfig()
   {
      echo 'Konfigurujemy obiekt '.((get_class($this) == 'BaseItem') ? 'podstawowy' : 'specjalny').'<br/>';
   } // end doSomeConfig();

   public function work()
   {
      echo 'Ja działam: '.get_class($this).'<br/>';
   } // end work();

   public static function factory()
   {
      $object = static::_concreteFactory();
      $object->doSomeConfig();
      self::$_instances++;

      return $object;
   } // end factory();

   protected static function _concreteFactory()
   {
      return new BaseItem;
   } // end _concreteFactory();
} // end BaseItem;

class SpecialItem extends BaseItem
{
   private function __construct()
   {
      echo 'Tworzymy obiekt specjalny<br/>';
   } // end __construct();

   protected static function _concreteFactory()
   {
      return new SpecialItem;
   } // end _concreteFactory();
} // end SpecialItem;

$baseObject = BaseItem::factory();
$specialObject = SpecialItem::factory();

$baseObject->work();
$specialObject->work();

Teraz nasz kod działa tak, jak tego chcemy. Późnego wiązania należy używać wtedy, gdy przewidujemy możliwość rozszerzania klasy z metodami statycznymi, i to w taki sposób, by robić z tego dziedziczenia prawdziwy użytek.

Zastosowanie

edytuj

Programiści najczęściej wykorzystują elementy statyczne do tworzenia dodatkowych mechanizmów związanych z inicjacją obiektów oraz ogólnych operacji powiązanych z klasą, które:

  • nie wymagają obecności obiektu,
  • lub których wyniki muszą być dostępne dla wszystkich obiektów.

Innym zastosowaniem jest pozostawienie klasy wyłącznie w charakterze pojemnika chroniącego dostęp do danych i traktowanie metod statycznych jako bardziej rozbudowanych funkcji. Pokażemy teraz jedno z rozwiązań programistycznych stosowanych w wielu skryptach, które bazuje w całości na elementach statycznych klas.

W rozdziale o funkcjach poznaliśmy słowo kluczowe global przenoszące zmienną z globalnej do lokalnej przestrzeni funkcji. W aplikacjach obiektowych korzystanie z global jest traktowane jako zła praktyka, która może prowadzić do nieprzewidzianych zachowań. Wszystko powinno być udostępniane za pośrednictwem obiektowych interfejsów, które pilnują, aby w danym miejscu programista miał dostęp wyłącznie do określonych usług. Wbrew pozorom, ma to sens, ponieważ ogranicza samowolę i wymusza stosowanie się do wytycznych twórcy systemu, zmniejszając ryzyko popełnienia błędu. Powagę sytuacji podkreśla fakt, że toczone były dyskusje czy nie usunąć global z PHP 6.0.

Pomimo tego, czasami potrzebny jest taki publicznie dostępny rejestr najważniejszych obiektów, z których moglibyśmy korzystać. Skoro tak, to trzeba go napisać:

<?php

class Registry
{
   private static $_objects = array();

   public static function set($name, $value)
   {
      if(!is_object($value))
      {
         throw new RuntimeException('Trying to assign a non-object value to '.$name.' in the registry.');
      }
      self::$_objects[$name] = $value;
   } // end set();

   public static function get($name)
   {
      if(!isset(self::$_objects[$name]))
      {
         throw new OutOfBoundsException($name.' is not a valid registry key.');
      }
      return self::$_objects[$name];
   } // end get();
} // end Registry;

$config = new Config;
$config->addLoader(new FileConfigLoader('./config.ini.php'));

Registry::set('config', $config);

// gdzies indziej

$config = Registry::get('config');

Dzięki takiemu rejestrowi mogliśmy dodać raportowanie błędów przy pomocy wyjątków oraz nałożyć różne ograniczenia. Nasza klasa Registry może przechowywać wyłącznie obiekty. Tablice oraz wartości skalarne nie są dozwolone.

Zakończenie

edytuj

Elementy statyczne to potężne narzędzie, jednak należy z niego korzystać rozważnie. Ponieważ zachowują się one bardziej jak funkcje i zwykłe zmienne, znacznie trudniej je testować. Zawsze dwa razy zastanów się, czy dana metoda lub pole naprawdę musi być statyczne, zanim dopiszesz do jego prototypu słowo static.

Poprzedni rozdział: Elementy statyczne
Następny rozdział: Iteratory

Metody magiczne

edytuj

W każdej klasie możemy utworzyć szereg metod, które będą traktowane w specjalny sposób przez PHP. Zwyczajowo nazywa się je metodami magicznymi, gdyż wywołuje je interpreter w odpowiedzi na różne zdarzenia, a nie programista. Można je poznać po tym, że ich nazwy zaczynają się od dwóch podkreśleń, co oznacza, że dwie takie metody powinniśmy już kojarzyć. Są to __construct() oraz __destruct() wywoływane automatycznie przy tworzeniu i niszczeniu obiektu. To jednak tylko czubek góry lodowej.

Dostęp do pól obiektu

edytuj

Gdy próbujemy odwołać się do nieistniejącego pola klasy, PHP zazwyczaj generuje komunikat E_NOTICE. Dzięki metodom __get() oraz __set() możemy zaprogramować własną akcję i wykorzystać to do swoich celów.

Przyjrzyjmy się naszemu stale ulepszanemu systemowi konfiguracji. Odwoływanie się do opcji konfiguracyjnych przez $config->get('nazwa') jest odrobinę niewygodne. Dlatego zasymulujemy, że poszczególne opcje są dostępne jako pola klasy. Poniżej pokazany jest fragment klasy Config z nową metodą:

<?php
class Config implements Countable
{
   private $_config = array();
   private $_awaitingLoaders = array();
 
   public function __get($name)
   {
      return $this->get($name);
   } // end __get();
 
   // pozostała część klasy
} // end Config;

$config = new Config;
$config->addLoader(new FileConfigLoader('./config.ini.php'));
echo $config->websiteTitle;

Metoda __get wywoływana jest w podświetlonej linijce, gdy próbujemy dostać się do nieistniejącego pola. PHP pobiera jej wynik i zwraca skryptowi jako jego wartość. Jednocześnie ponieważ nie chcemy, aby ktoś modyfikował konfigurację, nie udostępniamy metody __set() wywoływanej przy próbie przypisania nieistniejącemu polu jakiejś wartości.

Wywoływanie metod

edytuj

Magiczne metody potrafią także przechwytywać wywołania metod. Zasada działania jest analogiczna - jeśli PHP stwierdzi, że nie istnieje metoda o podanej nazwie, ale w klasie zdefiniowana jest operacja __call(), zrzuca całą robotę na nią. __call() dostaje dwa argumenty: nazwę metody oraz tablicę argumentów, z którymi programista próbował ją wywołać. Wykorzystajmy to do stworzenia mechanizmu zdarzeń. Zasada działania jest bardzo prosta. Jak pamiętamy, klasy posiadają pewne zachowania reprezentowane poprzez metody. Chcielibyśmy mieć możliwość rejestrowania dodatkowych czynności do wykonania, gdy zajdzie określone zdarzenie. Możemy dodatkowe czynności zapisać w postaci specjalnej klasy, której obiekt "wstrzykniemy" do właściwego obiektu. Znajdujący się tam kod sam zadba o to, aby czynności zostały wykonane.

Opis ten brzmi nieco abstrakcyjnie, ale spójrzmy na przykładowe zastosowanie. Mamy klasę User, która m.in. potrafi dodawać nowych użytkowników do naszej aplikacji. Chcemy, aby dodanie użytkownika z poziomu panelu administracyjnego zostało odnotowane w logach. Nic prostszego - piszemy klasę, w której implementujemy metodę onAdd(), która będzie zapisywać informację do logów, a utworzony od niej obiekt dodamy do obiektu User już w trakcie działania skryptu, gdy zorientujemy się, że wyświetlamy panel administracyjny. Nie musimy modyfikować kodu oryginalnej klasy. Co więcej, gdybyśmy chcieli wykonać przy tej okazji jeszcze inne czynności, wystarczy że napiszemy jeszcze więcej klas zdarzeń i podepniemy je pod User.

Najpierw zapoznajmy się z interfejsem EventDispatcher. Musi go implementować każda klasa, która będzie chciała obsługiwać zdarzenia. Przy okazji napiszemy też korzystającą z niego klasę User:

<?php
interface EventDispatcher
{
   public function addEventHandler(EventHandler $handler);
}

class User implements EventDispatcher
{
   private $_handlers = array();

   public function addEventHandler(EventHandler $handler)
   {
      $this->_handlers[] = $handler;
   } // end addEventHandler();

   public function addUser($login, $password)
   {
      foreach($this->_handlers as $handler)
      {
         $handler->onAdd($login, $password);
      }
      echo 'Użytkownik '.$login.' został dodany<br/>';
   } // end addUser();
} // end User;

Teraz pora na klasę EventHandler tworzącą podstawę dla wszystkich klas obsługi zdarzeń, jakie będziemy chcieli stworzyć. Zauważmy, że nie zawsze będziemy chcieli implementować wszystkie możliwe zdarzenia. Klasa User na razie obsługuje jedynie onAdd(), ale przecież w rzeczywistej aplikacji może tez posiadać zdarzenia onEdit(), onDelete() i inne. Po co tworzyć puste implementacje, kiedy można wszystko elegancko przechwycić poprzez __call() i odpowiednio przetworzyć przed użyciem? Ponadto, przed wywołaniem zdarzenia warto sprawdzić, czy spełnione są wszystkie warunki. EventHandler będzie zawierać dodatkową metodę _checkConditions(), która musi zwrócić true, aby wyrazić zgodę na wykonanie zdarzenia.

<?php

class EventHandler
{
   protected function _checkConditions()
   {
      return true;
   } // end _checkConditions();

   public function __call($name, $arguments)
   {
      if($this->_checkConditions())
      {
         $name = '_'.$name.'Event';
         if(method_exists($this, $name))
         {
            $this->$name($arguments);
         }
      }
   } // end __call();
} // end EventHandler;

Zwróćmy uwagę na linijkę 17 - oto odpowiedź, dlaczego nazwa pola klasy nie jest poprzedzona znakiem dolara. W tym miejscu PHP odczytuje nazwę metody do wywołania wprost ze zmiennej, dzięki czemu możemy dynamicznie decydować, co wykonać! Oczywiście warto wcześniej upewnić się, że w ogóle posiadamy odpowiednią metodę do obsługi zdarzenia i temu służy funkcja method_exists(). Zauważmy, że jeśli nie chcemy obsługiwać zdarzenia, nie grozi to nam teraz żadnymi konsekwencjami w postaci Fatal error. Wszystko przechwyci __call() i jeśli stwierdzi, że nie potrafi zająć się zdarzeniem, po prostu je zignoruje.

Poniżej pokazujemy przykładowe zastosowanie. Stworzymy sobie dwie klasy do obsługi zdarzeń. Pierwsza będzie rejestrować fakt dodania nowego użytkownika, a druga pomoże nam w organizacji konkursu, gdzie pięciu losowo wybranych nowo zarejestrowanych użytkowników wygrywa nagrodę.

<?php
class LoggingEvent extends EventHandler
{
   protected function _onAddEvent($arguments)
   {
      $f = fopen('./logs/user_log.log', 'a');
      fwrite($f, date('r').': '.$arguments[0].' zostal zarejestrowany.');
      fclose($f);
   } // end _onAddEvent();
} // end LoggingEvent;

class CompetitionEvent extends EventHandler
{
   protected $_winners = 0;

   protected function _checkConditions()
   {
      // sprawdzmy, czy w ogole mozna jeszcze wygrywac.
      $this->_winners = trim(file_get_contents('./winner_count.txt'));
      return ($this->_winners < 5);
   } // end _checkConditions();

   protected function _onAddEvent($arguments)
   {
      // Losuj zwyciezce z prawdopodobienstwem 0,2%
      if(rand(0, 1000) < 2)
      {
         echo $arguments[0].' wygrał!<br/>';
         file_put_contents('./winner_count.txt', ++$this->_winners);
      }
   } // end _onAddEvent();
} // end CompetitionEvent;

$user = new User;

// Tworzymy normalnie, bez zadnych bajerow.
$user->addUser('wacek', 'foo');

// Teraz bedziemy rejestrowac fakt utworzenia w logach.
$user->addEventHandler(new LoggingEvent);
$user->addUser('franek', 'bar');

// A ten uzytkownik wezmie udzial w konkursie.
$user->addEventHandler(new CompetitionEvent);
$user->addUser('jacek', 'joe');

Oto potęga programowania obiektowego - przy odrobinie pomysłowości możemy rozszerzać istniejące klasy bez zmiany ani jednej linijki ich kodu. Wszystko odbywa się w pełni dynamicznie, a pomogła nam w tym magiczna metoda __call(). W ramach ćwiczenia przyjrzyj się dokładnie kodowi do losowania zwycięzców. W pewnej sytuacji nie zadziała on prawidłowo. Czy potrafisz powiedzieć kiedy i dlaczego?

Klonowanie obiektów

edytuj

Jak pamiętamy, operator przypisania kopiuje jedynie referencję do obiektu. Są jednak sytuacje, gdy chcielibyśmy fizycznie skopiować cały obiekt. Najprostsze wyjście to wykorzystanie operatora clone:

$kopia = clone $obiekt;

Jednak nasz obiekt może mieć otwarte różne zasoby (np. pliki) lub przechowywać referencje do innych obiektów. Standardowe klonowanie po prostu skopiuje same referencje, dlatego musimy mieć możliwość "poprawienia" sklonowanej kopii tak, by była ona w pełni samodzielna wtedy, gdy tego potrzebujemy. Tu do akcji wkracza magiczna metoda __clone(). Gdy znajduje się ona w klasie, PHP wywołuje ją tuż po sklonowaniu obiektu, dzięki czemu ma ona możliwość wprowadzenia poprawek.

Aby zademonstrować działanie wspomnianej funkcji, napiszemy implementację struktury danych zwanej kolejką. Struktury danych są jednym z filarów algorytmiki. Służą do przechowywania dowolnie dużej liczby informacji ograniczonej wyłącznie rozmiarami pamięci, oferując dostęp do nich w pewien ściśle określony sposób. Działanie kolejki jest podobne do kolejek w sklepie - klienci przychodzą z jednej strony, a z drugiej są obsługiwani. W informatyce klientów zastępują dane dodawane z jednej strony, a zdejmowane z drugiej. Będziemy potrzebować dwóch klas: pierwszej do reprezentowania pojedynczego elementu kolejki oraz drugiej do zarządzania całą strukturą. Nas szczególnie będzie interesować proces klonowania. Klonując kolejkę, musimy także sklonować wszystkie obiekty reprezentujące jej elementy, dlatego musimy skorzystać z magicznej metody __clone().

Zaczynamy od napisania klasy pojedynczego elementu kolejki. Będzie on przechowywać informację oraz referencję do następnego elementu w kolejce. Następny element może mieć namiary na kolejny i tak dalej, aż do końca - tworzy się w ten sposób łańcuch elementów prowadzących od pierwszego do ostatniego czekającego elementu.

<?php

class QueueElement
{
   private $_data;
   private $_next = null;

   public function __construct($data)
   {
      $this->_data = $data;
   } // end __construct();

   public function getData()
   {
      return $this->_data;
   } // end getData();

   public function setNext(QueueElement $next)
   {
      $this->_next = $next;
   } // end setNext();

   public function getNext()
   {
      return $this->_next;
   } // end getNext();
} // end QueueElement;

Nasza klasa składa się praktycznie wyłącznie z getterów i setterów. Nie ma tutaj jeszcze operacji klonowania, ponieważ na tym poziomie nie mamy jeszcze nic do zrobienia. Dopiero główna klasa kolejki zawierać będzie całe sterowanie. Znajdować się tu będą namiary na pierwszy i ostatni element kolejki tak, aby można było się do nich szybko dostać podczas odczytu lub zapisu.

<?php

class Queue
{
   private $_first = null;
   private $_last = null;

   public function enqueue($data)
   {
      // Nic nie ma w kolejce.
      if($this->_first === null)
      {
         $this->_first = $this->_last = new QueueElement($data);
      }
      else
      // Coś jest w kolejce, doklejamy się do ostatniego.
      {
         $element = new QueueElement($data);
         $this->_last->setNext($element);
         $this->_last = $element;
      }
   } // end enqueue();

   public function dequeue()
   {
      if($this->_first === null)
      {
         throw new UnderflowException('Nie można pobrać elementu z pustej kolejki.');
      }
      $dequeued = $this->_first;
      $this->_first = $this->_first->getNext();
      
      if($this->_first === null)
      {
         $this->_last = null;
      }

      return $dequeued->getData();
   } // end dequeue();

   public function __clone()
   {
      // Zalozmy, ze sklonowana kolejka jest pusta
      $top = $this->_first;      
      $this->_first = $this->_last = null;

      // Przejedźmy się po dotychczasowej kolejce i skopiujmy
      // wszystkie jej elementy
      while($top != null)
      {
         $this->enqueue($top->getData());
         $top = $top->getNext();
      }
   } // end __clone();
} // end Queue;

Przykładowe działanie:

<?php

$queue = new Queue;
$queue->enqueue(5);
$queue->enqueue(4);
$queue->enqueue(3);

$cloned = clone $queue;
$cloned->enqueue(2);

try
{
   while(true)
   {
      echo $queue->dequeue().'<br/>';
   }
}
catch(UnderflowException $exception)
{
   // pusto
}

W przykładzie dodajemy do kolejki trzy elementy, a następnie ją klonujemy i dorzucamy do kopii jeszcze jeden element. Na końcu wyświetlamy elementy oryginału. Gdyby podczas klonowania pozostawić niezmienione elementy, wyświetliłyby nam się cztery liczby. Elementy kolejki są obiektami, zatem w skrypcie dostępne są jedynie referencje do nich. Operacja enqueue() sprawiłaby, że czwarta liczba zostałaby doczepiona do wspólnego elementu, a co gorsza - oryginał wcale by o tym nie wiedział! Mogłoby to doprowadzić do poważnych błędów w działaniu obu list. Zakomentuj operację __clone() i prześledź wszystko samodzielnie.

Przy implementowaniu własnej wersji __clone() musimy pamiętać, że operuje ona na już sklonowanym obiekcie, a nie na oryginale. Innymi słowy, PHP najpierw wykonuje dokładną kopię całego obiektu, a dopiero później wykonuje na niej __clone(), by pozwolić jej pozmieniać te rzeczy, które powinny być zmodyfikowane.

Serializacja obiektów

edytuj

Pisząc w PHP nie musimy przejmować się, jak interpreter reprezentuje tablice i obiekty w pamięci komputera. Jednak zdarza się, że chcielibyśmy zapisać je w całości do pliku lub przesłać komuś przez sieć. Za przetłumaczenie złożonego typu do postaci tekstowej i złożenie z niej z powrotem oryginału po drugiej stronie odpowiada tzw. serializacja. Spróbujmy zobaczyć, jak ten proces przebiega:

<?php
$array = array(
   'foo' => 'foo',
   'bar' => 'bar',
   'joe' => 'joe'
);

// Sprawdźmy, jak wygląda tablica normalnie
var_dump($array);

// Zserializujmy ją
$serialized = serialize($array);
echo $serialized.'<br/>';

// A teraz przywracamy oryginał
var_dump(unserialize($serialized));

Wynikiem działania skryptu powinno być:

array
  'foo' => string 'foo' (length=3)
  'bar' => string 'bar' (length=3)
  'joe' => string 'joe' (length=3)
a:3:{s:3:"foo";s:3:"foo";s:3:"bar";s:3:"bar";s:3:"joe";s:3:"joe";}
array
  'foo' => string 'foo' (length=3)
  'bar' => string 'bar' (length=3)
  'joe' => string 'joe' (length=3)

Funkcja serialize() przekonwertowała tablicę na specjalnie sformatowany ciąg tekstowy, w którym zawarte są informacje o wszystkich jej elementach. Gdy wprowadzimy go do funkcji unserialize(), PHP z powrotem odtworzy naszą tablicę. Serializować można także obiekty, a konwersji podlegają wszystkie pola. Nie zawsze jest to zjawisko pożądane. Jeśli nasz obiekt zawiera uchwyt do pliku, zostanie on zgubiony podczas przesyłania, dlatego dobrze by było, gdyby zamiast niego w zserializowanym tekście znalazła się po prostu ścieżka do niego. Podczas deserializacji musielibyśmy otworzyć go ponownie.

Niestandardowe reguły serializacji takie, jak powyżej opisana, mogą zostać zaprogramowane dzięki magicznym metodom __sleep() oraz __wakeup(), które umieszczamy w naszej klasie. Nie pobierają one żadnych argumentów. Pierwsza z nich powinna zwrócić tablicę z nazwami pól, które mają podlegać serializacji, zaś w drugiej możemy umieścić cokolwiek. Zobaczmy zatem, jak przesłać tekstem obiekt reprezentujący połączenie z plikiem:

<?php

class File
{
   private $_handle;
   private $_name;

   public function __construct($filename)
   {
      $this->_name = $filename;
      $this->_handle = fopen($filename, 'r');
   } // end __construct();

   public function __destruct()
   {
      fclose($this->_handle);
   } // end __destruct();

   public function getFilename()
   {
      return $this->_name;
   } // end getFilename();

   public function read($bytes)
   {
      return fread($this->_handle, (int)$bytes);
   } // end read();

   public function __sleep()
   {
      return array('_name');
   } // end __sleep();

   public function __wakeup()
   {
      echo 'Odtwarzam połączenie z plikiem...<br/>';
      $this->_handle = fopen($this->_name, 'r');
   } // end __wakeup();
} // end File;

W metodzie magicznej __sleep() mówimy: serializacja pola $_handle nie ma sensu, wystarczy nam nazwa pliku na odtworzenie obiektu. Odtworzenie w __wakeup() polega po prostu na ponownym otwarciu pliku. Możemy teraz przetestować całość:

<?php
require('./File.php');

$file = new File('plik.txt');
echo $file->read(100).'<br/>';

$file2 = unserialize(serialize($file));
echo $file2->read(100).'<br/>';

Pierwszy, oryginalny obiekt tworzymy w tradycyjny sposób i odczytujemy z pliku 100 bajtów. Następnie całość serializujemy i odserializowujemy, by spróbować coś odczytać poprzez "nowy" obiekt. Po uruchomieniu zauważymy, że oba odczyty się udały. PHP wywołał __wakeup() podczas deserializacji i ponownie otworzył plik, a świadczy o tym komunikat "Odtwarzam połączenie z plikiem...".

Jest kilka zastosowań dla serializacji. Może posłużyć jako szybki cache złożonych struktur. Zamiast mozolnie budować dużą kolekcję obiektów przy każdym wejściu na stronę, możemy zbudować ją raz, zserializować i zapisać do pliku, a przy kolejnych żądaniach HTTP ładować z powrotem. Jednak musimy tutaj uważać. Prostota serializacji wielu programistom zupełnie przesłania kwestię bezpieczeństwa. Przede wszystkim nie wolno zserializowanych danych pokazywać użytkownikom naszej witryny w sposób jawny (nawet w ciastkach!). Spróbuj wyświetlić sobie zserializowaną postać obiektu klasy File. Zauważysz, że zapisana jest tam informacja o tym, do jakiej klasy obiekt należy. Jeśli będziesz bezmyślnie odserializowywać wszystko, co przyjdzie do Ciebie z przeglądarki, sprytny hacker może tak spreparować ciąg, by powstał jakiś kluczowy dla aplikacji obiekt i wykorzystać go do swoich niecnych celów. Oto przykład: mamy klasę Mailer służącą do wysyłania e-maili i wpadliśmy na pomysł, by dane o zalogowanych użytkownikach trzymać w ciastkach jako zserializowane tablice. Agresor jednak modyfikuje takie ciastko tak, aby po odczytaniu przez skrypt tworzyło obiekt klasy Mailer i rozesłało spam w Twoim imieniu. Musisz przyznać, że nie jest to ciekawa perspektywa. Dlatego trzy razy pomyśl, zanim zserializowane dane udostępnisz komukolwiek poza serwerem.

Rozszerzony dostęp do pól obiektów

edytuj

Rozdział ten zaczęliśmy od omówienia metod __get() i __set(), pisząc prosty system konfiguracyjny. Możemy odczytywać i zapisywać nowe wartości do różnych opcji, ale nie mamy jak sprawdzić czy konkretna opcja istnieje. Uzupełnimy teraz to niedopatrzenie dzięki dwóm kolejnym metodom magicznym: __isset() oraz __unset(). Pierwsza z nich wywoływana jest, gdy na danym polu zadziałamy komendą isset():

if(isset($config->property))
{
   ...
}

Metoda __unset() wywoływana jest podczas wykonywania komendy unset(), czyli próby usunięcia pola z obiektu:

unset($config->property);

Obie pobierają jeden argument będący nazwą pola. Dodajmy zatem do naszej klasy Config metodę __isset().

public function __isset($name)
{
   return isset($this->_config[$name]);
} // end __isset();

Jak widać, jest to bardzo proste. Jedyne, co robimy, to każemy sprawdzić czy opcja istnieje w naszej tablicy z opcjami.

Konwersja do ciągu tekstowego

edytuj

W PHP każdy obiekt można przekonwertować na ciąg tekstowy, a dzięki magicznym metodom możemy oprogramować tę operację. Na tapetę weźmiemy naszą klasę File. Nie chcemy ciągle pisać skomplikowanych odwołań w stylu $file->getFilename() za każdym razem, gdy próbujemy wyświetlić nazwę pliku. Dlatego dodamy metodę __toString(), która pozwoli nam to nieco uprościć. Dodaj poniższy kod do ciała wspomnianej klasy:

public function __toString()
{
   return $this->_name;
} // end __toString();

Od metody tej oczekujemy, że wygeneruje tekstową reprezentację obiektu. W naszym przypadku ma to być po prostu nazwa pliku. Dzięki temu możemy teraz wyświetlić ją dużo prościej:

<?php
require('./File.php');

$file = new File('plik.txt');
echo 'Otworzyłem plik '.$file.'<br/>';

Zakończenie

edytuj

Metody magiczne są dobrym mechanizmem do reagowania na różne zdarzenia, które wykonujemy na obiektach, a które nie są związane z wywoływaniem metod. Jednak pamiętaj, że wszystko wymaga umiaru. Nie stosuj ich do dodawania obiektom kompletnie nieintuicyjnych zachowań, gdyż inni programiści będą mieć później spore problemy ze zrozumieniem Twojego kodu. Metody magiczne nie są też dobrze wspierane przez zaawansowane edytory z dynamicznym podpowiadaniem składni. Jeśli jakaś metoda jest normalnie zaprogramowana w klasie jako metoda, edytor pokaże ją na podglądzie. Jeśli będzie ona wybierana przez skomplikowany algorytm ukryty w __call(), część programistów nawet nie zauważy jej istnienia, a ich praca nad kodem będzie żmudniejsza.

Poprzedni rozdział: Metody magiczne
Następny rozdział: Automatyczne ładowanie

Iteratory

edytuj

Na samym początku naszej nauki poznaliśmy pętlę foreach, która przechodzi po wszystkich elementach tablicy. Okazuje się, że możemy ją stosować także do iterowania po obiektach, a co więcej - możemy kontrolować, co właściwie nasz obiekt będzie wtedy generować! Obiekty z taką spersonalizowaną obsługą pętli foreach nazwiemy iteratorami, a omówienie zagadnienia rozpoczniemy od pokazania paru przykładów ze znanej już nam biblioteki Standard PHP Library, która posiada całkiem pokaźną kolekcję domyślnych iteratorów.

Iteratory z biblioteki SPL

edytuj

Przed zabraniem się za programowanie obiektowe omawialiśmy m.in. funkcje dostępu do plików. Korzystanie z nich było średnio wygodne. Przykładowo, aby odczytać zawartość katalogu, musieliśmy manipulować dużą ilością funkcji, a na dokładkę otrzymywaliśmy jedynie nazwę, nie wiedząc nawet czy mamy do czynienia z plikiem czy z katalogiem. Iteratory doskonale nadają się do operacji na systemie plików. W SPL-u mamy klasę DirectoryIterator, która pozwoli nam pobrać zawartość katalogu:

<?php
try
{
   $dir = new DirectoryIterator('./katalog/');
   foreach($dir as $file)
   {
      // Pomiń pozycje "." oraz ".."
      if($file->isDot())
      {
         continue;
      }
      if($file->isDir())
      {
         echo 'Katalog '.$file.'<br/>';
      }
      else
      {
         echo 'Plik '.$file.'<br/>';
      }
   }
}
catch(UnexpectedValueException $exception)
{
   echo 'Błąd: '.$exception->getMessage();
}

Teraz iteracja po katalogach jest wyjątkowo prosta, a na dodatek zyskujemy czytelne, obiektowe API. Gdyby katalog nie istniał, zamiast dziwacznych komunikatów dostaniemy normalny wyjątek, który możemy przechwycić i oprogramować według naszych potrzeb.

Zastanówmy się teraz, jak wyświetlić katalogi wraz z zawartością ich podkatalogów, czyli innymi słowy - kompletne drzewo systemu plików. Jest to możliwe dzięki dwóm kolejnym iteratorom: RecursiveDirectoryIterator oraz RecursiveIteratorIterator. Powinny być one używane razem - pierwszy bowiem sam z siebie do podkatalogów nie wejdzie, ale udostępnia metody getChildren() i hasChildren(), które są wykorzystywane przez drugi. RecursiveIteratorIterator służy do rekurencyjnego przechodzenia po dowolnej strukturze, która implementuje powyższe dwie metody (także naszej własnej). My jednak wróćmy do naszych katalogów:

<?php
$dirIterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('./s3/'), RecursiveIteratorIterator::SELF_FIRST);
foreach($dirIterator as $file)
{
   if($file->isDir())
   {
      echo str_repeat('---', $dirIterator->getDepth()).' Katalog '.$file.'<br/>';
   }
   else
   {
      echo str_repeat('---', $dirIterator->getDepth()).' Plik '.$file.'<br/>';
   }
}

Konstruktor klasy RecursiveIteratorIterator pobiera jako pierwszy argument obiekt iteratora, po którym należy przejść rekurencyjnie. W naszym przypadku jest to iterator do przechodzenia po katalogach. W drugim, opcjonalnym parametrze możemy ustawić sposób przechodzenia przy pomocy stałych klasowych:

  1. RecursiveIteratorIterator::LEAVES_ONLY - wyświetli jedynie liście (pliki). Wartość domyślna.
  2. RecursiveIteratorIterator::SELF_FIRST - najpierw zwróć katalog, a później to, co się w nim znajduje.
  3. RecursiveIteratorIterator::CHILD_FIRST - najpierw zwróć zawartość katalogu, a później sam katalog.

Iterator produkuje nam obiekty poszczególnych plików, lecz nie ma w nich informacji o ich głębokości w strukturze katalogowej. Tę informację uzyskujemy bezpośrednio z iteratora poprzez metodę getDepth(), dzięki czemu wiemy, ile pauz należy wyświetlić przed nazwą elementu, aby go odpowiednio wciąć.

Powyższy przykład pokazał jeszcze jedną ciekawą właściwość iteratorów SPL, a mianowicie możliwość ich komponowania w czasie wykonywania. Zauważmy, że nie mamy jednego iteratora do rekurencyjnego przechodzenia po katalogach, ale mamy dwa mniejsze - jeden oferujący rekurencję, drugi - wędrówki po systemie plików. Dopiero łącząc je ze sobą uzyskujemy to, co chcemy. Gdy przejdziemy do późniejszych rozdziałów podręcznika zauważymy, że takie postępowanie ma swoją fachową nazwę dekorator. Oto inne zastosowanie kompozycji iteratorów. Mamy tablicę z sześcioma elementami, ale chcemy wyświetlić jedynie elementy od 2 do 4, przy czym nie znamy ich indeksów (pętla for odpada). Wykorzystajmy zatem iterator tablicowy ArrayIterator i połączmy go z LimitIterator, który doda odpowiednie limity:

<?php
$array = new ArrayIterator(
   array('jabłko', 'banan', 'gruszka', 'wisienka', 'czereśnia', 'truskawka')
);

echo '<ul>';
foreach(new LimitIterator($array, 2, 2) as $item)
{
   echo '<li>'.$item.'</li>';
}
echo '</ul>';

Chociaż tablice współpracują z foreach, ale nie współpracują z innymi iteratorami, dlatego w tym przypadku musimy ją opakować w specjalny obiekt klasy ArrayIterator. Domyślnie pokazałby on nam wszystkie owoce, ale my chcemy dostać jedynie "gruszkę" oraz "wisienkę". Tu do akcji wkracza LimitIterator, któremu mówimy, że chcemy dostać dwa elementy (trzeci argument) począwszy od drugiego (drugi argument - liczone od zera!). Uruchom ten przykład i spróbuj pobawić się ustawieniami, aby zobaczyć, co się stanie.

Interfejs IteratorAggregate

edytuj

Gdy wiemy już, ile można osiągnąć dzięki iteratorom, pora nauczyć się samodzielnie je tworzyć. Sprowadza się to do zaimplementowania w naszej klasie jednego z dwóch specjalnych interfejsów. Zaczniemy od omówienia prostszego IteratorAggregate, który wymaga dodania dokładnie jednej metody: getIterator(). Jej zadaniem jest... utworzenie iteratora, który zajmie się procesem iteracji w imieniu naszej klasy. Przypomnijmy sobie naszą klasę Config, która opcje konfiguracyjne przechowuje w tablicy. Dla celów debugowych chcielibyśmy mieć możliwość prostego wyświetlenia wszystkich opcji. Przypomnijmy sobie strukturę klasy:

<?php
class Config implements Countable
{
   private $_config = array();
   private $_awaitingLoaders = array();
 
   // metody klasy

} // end Config;

Opcje przechowywane są w polu $_config, dlatego to po nim powinniśmy iterować. Dodajmy do klasy interfejs IteratorAggregate oraz zaimplementujmy niezbędną metodę:

<?php
class Config implements Countable, IteratorAggregate
{
   private $_config = array();
   private $_awaitingLoaders = array();
 
   public function getIterator()
   {
      return new ArrayIterator($this->_config);
   } // end getIterator();

   // metody klasy
} // end Config;

Ponieważ lista opcji jest tablicą, w naszym imieniu iterować będzie po niej ArrayIterator, który tworzymy w momencie wywołania getIterator(). Możemy teraz wyświetlić wszystkie opcje:

<?php
require('./Config.php');

$config = new Config;
$config->addLoader(new FileConfigLoader('./config.ini.php'));

echo '<ul>';
foreach($config as $name => $value)
{
   echo '<li>'.$name.': '.$value.'</li>';
}
echo '</ul>';

Interfejs Iterator

edytuj

Interfejs IteratorAggregate jest przydatny w najprostszych sytuacjach, gdy mamy pasujący iterator, ale pojawia się problem, skąd taki wziąć gdy żaden nam nie pasuje? Pełną kontrolę nad przebiegiem procesu iteracji zapewnia dopiero interfejs Iterator, który wymaga dodania do naszej klasy pięciu metod:

  1. reset() - ustawia kursor na początku kolekcji.
  2. valid() - sprawdza czy kursor znajduje się we właściwym położeniu (np. czy nie wyszedł poza rozmiar tablicy).
  3. next() - przesuwa kursor na kolejną pozycję.
  4. key() - zwraca klucz aktualnej pozycji.
  5. current() - zwraca wartość aktualnej pozycji.

Aby lepiej zrozumieć, jak PHP z nich korzysta, spróbujmy przetłumaczyć sobie wywołanie pętli foreach na odpowiadające jej wywołanie pętli while:

<?php
// Tworzymy dowolny iterator
$iterator = new SomeIterator;

// Foreach
foreach($iterator as $name => $value)
{
   ...
}

// Równoważny mu while
$iterator->reset();
while($iterator->valid())
{
   $name = $iterator->key();
   $value = $iterator->value();

   ...

   $iterator->next();
}

Wyposażeni w taką informację spróbujmy napisać iterator zliczający. Będzie on odliczać od podanej liczby zadaną ilość razy. Jako wartość będzie zwracana aktualna liczba, a jako klucz - numer iteracji.

<?php
class CountingIterator implements Iterator
{
   private $_start;
   private $_end;
   private $_key;
   private $_value;

   public function __construct($start, $offset)
   {
      $this->_start = $start;
      $this->_offset = $offset;
   } // end __construct();

   public function reset()
   {
      $this->_key = 0;
      $this->_value = $this->_start;
   } // end reset();

   public function valid()
   {
      return $this->_key < $this->_offset;
   } // end valid();

   public function next()
   {
      $this->_key++;
      $this->_value++;
   } // end next();

   public function key()
   {
      return $this->_key;
   } // end key();

   public function current()
   {
      return $this->_value;
   } // end current();
} // end CountingIterator;

echo '<ul>';
foreach(new CountingIterator(5, 10) as $idx => $value)
{
   echo '<li>'.$idx.'. '.$value.'</li>';
}
echo '</ul>';

Powyższy przykład wyświetli dziesięć kolejnych liczb, począwszy od wartości 5. Zwróćmy uwagę, że w ten sposób zmieniliśmy foreach w pętlę for. Oczywiście w prawdziwych aplikacjach WWW nie będzie to mieć zbyt wielkiego sensu, ale dobrze pokazuje istotę procesu iteracji. W ramach ćwiczenia spróbuj zaimplementować interfejs Iterator w miejsce IteratorAggregate w klasie Config. Będziesz musiał iterować po tablicy asocjacyjnej, dlatego wszystkie metody interfejsu będą zwykłymi nakładkami na analogiczne funkcje obsługi tablic: reset(), next(), key() oraz current(). Jeśli nie pamiętasz ich działania, posłuż się dokumentacją i spróbuj rozwiązać problem, jak w metodzie valid() sprawdzić, że dotarliśmy do końca tablicy.

Zakończenie

edytuj

Iteratory są bardzo wygodnym elementem PHP pozwalającym ukryć przed programistą wiele zbędnych szczegółów technicznych. Jednak i tutaj należy korzystać z nich z umiarem i nie stosować, gdy nie ma ku temu żadnego logicznego uzasadnienia.

Poprzedni rozdział: Iteratory
Następny rozdział: Ćwiczenia

Automatyczne ładowanie

edytuj

Wraz z rozbudową projektu pojawia się w nim coraz więcej plików i bibliotek, które musimy dołączać. W PHP 4 i wcześniejszych wersjach początek większych skryptów prezentował się mniej więcej następująco:

<?php

define('DIR_INC', './includes/');

require(DIR_INC.'config.php');
require(DIR_INC.'base.php');
require(DIR_INC.'validation.php');
require(DIR_INC.'forms.php');
require(DIR_INC.'database.php');
require(DIR_INC.'i18n.php');
require(DIR_INC.'controller.php');
require(DIR_INC.'helpers.php');
// itd.

Oczywiście nie trzeba wielkiej filozofii, by zauważyć, że zarządzanie taką listą jest niezwykle trudne, a przy tym niewydajne. Nie wszystkie podstrony muszą przecież korzystać z całej funkcjonalności oferowanej przez silnik strony, w związku z czym ładowanie wszystkiego to niepotrzebna strata czasu. Remedium na te bolączki był wprowadzony w PHP 5.0.0 mechanizm automatycznego ładowania klas. Jego działanie jest bardzo proste. W momencie gdy próbujemy utworzyć obiekt nieznanej klasy, PHP wywołuje specjalną, napisaną przez nas funkcję, której zadaniem jest odnalezienie i załadowanie pliku zawierającego tę klasę. Dopiero gdy nie zostanie on znaleziony, generowany jest błąd.

Automatyczne ładowanie ułatwia życie programiście. Nie musimy już zastanawiać się czy dana klasa została załadowana czy jeszcze nie, a gdyby nie - szukać miejsca, w którym można ją bezpiecznie załadować. Z naszej perspektywy kod wygląda tak, jakby PHP miał już "z definicji" załadowane wszystkie możliwe klasy. Co więcej, ponieważ ładujemy tylko to, czego faktycznie potrzebujemy, możemy często zyskać na wydajności, mimo iż musimy przy okazji wykonać operację tłumaczenia nazwy klasy na ścieżkę do pliku.

Automatyczne ładowanie - PHP 5.0

edytuj

Pierwotny mechanizm automatycznego ładowania wprowadzony w PHP 5.0 był prosty, lecz dość prymitywny. Wszystko, co musieliśmy zrobić, to utworzyć magiczną funkcję o nazwie __autoload(), która jako argument pobierała nazwę klasy do załadowania. Utwórzmy najpierw dwa pliki testowe:

Klasa1.php:

<?php
class Klasa1
{
   public function __construct()
   {
      echo 'Tworzę obiekt klasy Klasa1<br/>';
   } // end __construct();
} // end Klasa1;

Klasa2.php:

<?php
class Klasa2
{
   public function __construct()
   {
      echo 'Tworzę obiekt klasy Klasa2<br/>';
   } // end __construct();
} // end Klasa2;

Nazwa pliku jednoznacznie mówi nam, jaka klasa się w nim znajduje, zatem pliki mogą zawierać tylko po jednej klasie bądź interfejsie. Spróbujmy teraz napisać skrypt, który będzie potrafił ładować je automatycznie:

<?php
function __autoload($className)
{
   echo 'Ładuję klasę '.$className.'<br/>';
   require('./'.$className.'.php');
} // end __autoload();

// Możemy od razu korzystać z klas
$obiekt1 = new Klasa1;
$obiekt2 = new Klasa2;
$obiekt3 = new Klasa1;

Wynikiem działania naszego skryptu powinno być:

Ładuję klasę Klasa1
Tworzę obiekt klasy Klasa1
Ładuję klasę Klasa2
Tworzę obiekt klasy Klasa2
Tworzę obiekt klasy Klasa1

W linijce 9 od razu próbujemy utworzyć obiekt klasy Klasa1 bez jej uprzedniego ładowania. PHP szybko odkrywa, że nie ma jej jeszcze w swojej pamięci, dlatego wywołuje funkcję __autoload(), prosząc ją o jej odnalezienie. Funkcja wyświetla na ekranie informację, że coś ładuje, a następnie dokleja do nazwy klasy rozszerzenie i ścieżkę, a całość przesyła do instrukcji require. Gdy dwie linijki niżej próbujemy ponownie utworzyć obiekt tej samej klasy, PHP wie już, jak ona działa i nie musi nic doładowywać, tylko od razu przystępuje do tworzenia obiektu.

Automatyczne ładowanie - SPL

edytuj

Funkcja __autoload() ma jedną, ale bardzo poważną wadę. Wyobraźmy sobie sytuację, w której korzystamy z dwóch bibliotek rozwijanych przez niezależne ekipy programistów. Każda biblioteka posiada swój własny mechanizm ładowania klas, a my także w naszym projekcie chcemy mieć jeszcze jeden. Występuje konflikt, ponieważ w pamięci nie może być trzech funkcji __autoload(), lecz co najwyżej jedna. Pojawia się konflikt nazw między bibliotekami.

Problem szybko dostrzegli również twórcy PHP i już w wersji 5.1 w znanym już nam pakiecie Standard PHP Library pojawił się nowy mechanizm automatycznego ładowania, który umożliwia równoczesną pracę kilku ładowarek. Jeśli zdecydujemy się na jego wykorzystanie, PHP automatycznie wyłączy funkcję __autoload(). Wykorzystujmy te same przykładowe klasy, zmieniając jedynie korzystający z nich skrypt:

<?php
function myClassLoader($className)
{
   echo 'Ładuję klasę '.$className.'<br/>';
   require('./'.$className.'.php');

   // Zwróć "true", informując, że klasa została znaleziona.
   return true;
} // end myClassLoader();

spl_autoload_register('myClassLoader');

// Możemy od razu korzystać z klas
$obiekt1 = new Klasa1;
$obiekt2 = new Klasa2;
$obiekt3 = new Klasa1;

Kod powinien dać dokładnie taki sam wynik, jak poprzednio, jednak w jego budowie jest zasadnicza różnica. Nasza funkcja ładująca może mieć teraz dowolną nazwę, jednak przez to musimy poinformować PHP, że ma ją wykorzystywać. Służy do tego funkcja spl_autoload_register(), do której podajemy nazwę funkcji jako argument.

Jak wspomnieliśmy, w projekcie może być użyte kilka ładowarek. Razem tworzą one stos. Zauważmy, że nasza funkcja zwraca wartość true. To informacja dla PHP, że plik został znaleziony. Jeśli nasza funkcja zwróci co innego, PHP wywoła kolejny autoloader i tak aż do skutku lub wyczerpania zapasu. Założenie jest takie, że funkcja ładująca powinna po nazwie klasy rozpoznać czy potrafi ją załadować i jeśli nie, natychmiast zakończyć działanie, by przekazać sterowanie innej. Możemy dodać do naszych klas unikalne prefiksy, np. MojProjekt i funkcją strpos() sprawdzać czy są one obecne:

<?php
function myClassLoader($className)
{
   if(strpos($className, 'MojProjekt') !== 0)
   {
      // Te klase obsluguje jakas inna ladowarka - nie ma prefiksu!
      return false;
   }
   echo 'Ładuję klasę '.$className.'<br/>';
   require('./'.$className.'.php');

   // Zwróć "true", informując, że klasa została znaleziona.
   return true;
} // end myClassLoader();

Teraz zadanie dla Ciebie. Dodaj do klas oraz ich plików prefiks MojProjekt, utwórz analogiczne klasy z prefiksem InnyProjekt i napisz dla nich drugą ładowarkę. Zarejestruj ją obok pierwszej, użyj nowych klas i obserwuj działanie skryptu.

Jeżeli jakaś ładowarka nagle nam przeszkadza, możemy ją w każdej chwili usunąć funkcją spl_autoload_unregister().

Ładowarki obiektowe

edytuj

Ładowarki SPL nie muszą być funkcjami, ale także metodami klas. Umożliwia to budowę całkiem przyjemnego i łatwego w konfiguracji interfejsu wielokrotnego użytku do ładowania klas. Zamienimy teraz nasze ładowarki z poprzedniego przykładu na takie właśnie klasy, w których będziemy mogli ustawić ścieżki dostępu oraz rozpoznawane prefiksy.

<?php
class Loader
{
   private $_prefix;
   private $_directory;
   private $_valid = false;

   public function __construct($prefix, $directory)
   {
      $this->_prefix = (string)$prefix;
      if(!is_dir($directory))
      {
         throw new RuntimeException($directory.' is not a valid directory path.');
      }
      $this->_directory = $directory;
      $this->_valid = true;
   } // end __construct();

   public function autoload($className)
   {
      if(strpos($className, $this->_prefix) !== 0)
      {
         return false;
      }
      require($this->_directory.$className.'.php');
      return true;
   } // end autoload();

   public function register()
   {
      if($this->_valid)
      {
         spl_autoload_register(array($this, 'autoload'));
         $this->_valid = false; // aby się nie dało drugi raz zarejestrować
      }
   } // end register();
} // end Loader;

$firstLoader = new Loader('MojProjekt', './MojProjekt/');
$secondLoader = new Loader('InnyProjekt', './InnyProjekt/');

$firstLoader->register();
$secondLoader->register();

// Tutaj reszta skryptu

Cały proces rejestracji ukryty jest teraz wewnątrz klasy ładowarki. Argumenty takie, jak prefiks czy katalog, przechowywane są w obiekcie, który dzięki metodzie register() potrafi się sam zarejestrować jako ładowarka SPL, wywołując w linii 33 funkcję spl_autoload_register(). Rejestrowanie metod realizujemy poprzez przekazanie jako argument tablicy z obiektem oraz nazwą metody.

Nasza ładowarka ma charakter uniwersalny - za pomocą tego samego kodu zainicjowanego różnymi parametrami możemy obsługiwać teraz dwie różne biblioteki znajdujące się w różnych katalogach. Spróbuj przy jej pomocy załadować klasy z poprzedniego rozdziału.

Standard nazewnictwa klas

edytuj

W dużych projektach niezbędne jest przestrzeganie pewnych konwencji, aby nie pogubić się we własnym kodzie. Jedna z nich może dotyczyć sposobu nadawania klasom nazwy. Także i w tym podręczniku stosowaliśmy jednolitą konwencję cechującą się używaniem angielskich słów oraz rozpoczynaniem każdego z nich dużą literą, np. FormInput czy ConfigLoader. Na nasze potrzeby wystarczała ona dotąd w zupełności, ale zauważmy, że klasa ConfigLoader może być użyta w wielu różnych bibliotekach. Gdybyśmy chcieli użyć ich jednocześnie, wystąpiłby konflikt nazw. Co więcej, nie jest to jedyna konwencja i inni programiści mogą swoje klasy nazywać inaczej. Utrudnia to tworzenie przenośnych ładowarek, a także wprowadza zamieszanie w kodzie, gdy część klas nazywa się np. Foo_Bar_Joe, drugą część FooBarJoe, a trzecią jeszcze inaczej.

Problem dostrzegli niedawno programiści pracujący przy największych projektach PHP i powołali stowarzyszenie, którego celem było rozwiązanie tej sytuacji i wypracowanie jednolitego standardu nazewnictwa. Po przyjęciu, miał on być stopniowo wdrożony we wszystkich projektach. Zapoznamy się teraz z jego założeniami i od tego momentu będziemy go konsekwentnie stosować w dalszej części podręcznika.

Zgodnie z założeniami nowego standardu, nazwy klas składają się z co najmniej dwóch członów oddzielonych od siebie znakiem _ lub \ (separator przestrzeni nazw, które poznamy już wkrótce). Nazwa każdego członu pisana jest w konwencji FooBar. Pierwszy człon powinien zawsze określać tzw. dostawcę, czyli np. nazwę projektu lub grupy programistycznej odpowiedzialnej za jego rozwój. Przypuśćmy, że tworzymy projekt o nazwie "Ala". Wtedy nasze klasy powinny dostać nazwy:

  1. Ala_Klasa1
  2. Ala_Klasa2
  3. Ala_Klasa3
  4. Ala_JakisElement_Klasa1
  5. Ala_JakisElement_Klasa2
  6. itd.

Podkreślenia pełnią bardzo ważną rolę, ponieważ podczas ładowania są zamieniane na separator katalogów w ścieżce do pliku. Oznacza to, że klasa Ala_Klasa1 będzie znajdować się w pliku Ala/Klasa1.php, a Ala_JakisElement_Klasa1 - w Ala/JakisElement/Klasa1.php. Jak widać, możemy w ten sposób nasze pliki grupować w podkatalogi. Ładowarka po nazwie dostawcy może rozpoznać czy dana klasa należy do niej i w jakim katalogu powinna szukać plików.

Pełen tekst standardu można znaleźć na liście dyskusyjnej Grupy Standaryzacyjnej PHP (j. angielski).

Uniwersalne ładowarki

edytuj

Zauważmy, że standard uwzględnia sytuację, w której korzystamy z kilku różnych bibliotek. Jeśli ich konwencja nazewnictwa jest identyczna, szybko dochodzimy do wniosku, że można by stworzyć jedną, uniwersalną ładowarkę podobną do tej, którą napisaliśmy wyżej, ale uwzględniającą potrzeby wszystkich projektów. Takie ładowarki faktycznie powstają i najczęściej wchodzą w skład różnych projektów.

Oprócz tego, także sama grupa standaryzacyjna PHP rozpoczęła opracowywanie domyślnej ładowarki, która w przyszłości ma szansę stać się częścią pakietu SPL, a co za tym idzie - być obecna w każdym interpreterze PHP. Obecnie dostępna jej jest implementacja w PHP, a także rozszerzenie w języku C do samodzielnej kompilacji jako moduł PHP. Jej użycie jest bardzo proste:

<?php
$classLoader = new SplClassLoader('Ala', '/ścieżka/do/projektu/Ala');
$classLoader->register();

Zakończenie

edytuj

Automatyczne ładowanie klas jest powszechnie spotykane w niemal wszystkich nowoczesnych projektach i bibliotekach programistycznych napisanych w PHP. Rozwiązuje problem zależności między poszczególnymi klasami, zapanowania nad ich ładowaniem, a dzięki dodatkowej standaryzacji może być także remedium na konflikty nazw. Pamiętaj, że automatyczne ładowanie działa wyłącznie dla klas oraz interfejsów. Nie da się przy jego pomocy ładować zwyczajnych funkcji, tak więc decydując się na jego użycie, najlepiej będzie w całości przejść na programowanie obiektowe i zrezygnować z ich stosowania.

To był już ostatni temat związany bezpośrednio z programowaniem obiektowym w języku PHP. W rozdziale tym poznaliśmy po kolei wszystkie elementy tego paradygmatu programowania dostępne w języku, od podstawowych pojęć takich, jak klasy, poprzez dziedziczenie aż do iteratorów i automatycznego ładowania. Jest to duża dawka materiału, i to niezbyt łatwego. O programowaniu obiektowym powstają grube książki, a jego pełną potęgę można poznać dopiero dzięki praktyce. Nie zrażaj się, jeśli zapomniałeś niektórych rzeczy lub wciąż nie do końca rozumiesz, jak wykorzystać je w praktyce. Niemal cała dalsza część podręcznika będzie bazować na poznanych tu informacjach, dlatego będzie jeszcze sporo okazji do przećwiczenia wszystkiego. Nie obawiaj się wracania do tych rozdziałów; z pewnością Ci się jeszcze przydadzą. Tymczasem przed nami zestaw ćwiczeń, które już teraz pomogą Ci utrwalić zdobyte informacje.

Poprzedni rozdział: [[../../Automatyczne ładowanie|Automatyczne ładowanie]]
Następny rozdział: [[../../Domknięcia|Domknięcia]]

Ćwiczenia

edytuj

Ćwiczenia utrwalające materiał z rozdziału o programowaniu obiektowym.

[[../../Odpowiedzi/Programowanie obiektowe|Odpowiedzi]]

Podstawy

edytuj
  1. Czym różni się klasa od obiektu?
  2. Spójrz na Twój komputer i wyobraź sobie, że masz go zamodelować w Twoim programie przy pomocy programowania obiektowego. Co będzie klasą, a co obiektem i dlaczego?
  3. Wymień elementy wchodzące w skład klasy i powiedz krótko, do czego służą.
  4. Co oznacza, że zmienna obiektowa jest referencją do obiektu?
  5. Do czego służy słowo kluczowe public przy deklarowaniu elementów klasy? Jakie znasz jeszcze inne podobne do niego?
  6. W jaki sposób możemy utworzyć obiekt jakiejś klasy?
  7. W jaki sposób możemy oprogramować proces tworzenia nowego obiektu lub jego niszczenia?
  8. Kiedy obiekt jest niszczony przez PHP?

Dziedziczenie

edytuj
  1. Podaj praktyczny przykład zastosowania dziedziczenia.
  2. Mamy klasę Foo rozszerzającą Bar oraz funkcję, która przyjmuje jeden argument. Opisz, jak zachowa się PHP, gdy za argument podamy obiekt klasy Foo, a funkcja będzie wymagać:
    • Obiektu klasy Foo
    • Obiektu klasy Bar
    • Obiektu klasy Foo, a my wprowadzamy obiekt klasy Bar.
  3. Do czego służy słowo kluczowe protected?
  4. Podczas dziedziczenia możemy unieważnić jedną z dotychczasowych metod. Przypuśćmy, że w klasie pochodnej unieważniliśmy jedną z metod, lecz zmieniliśmy listę argumentów. Jak zachowa się PHP?
  5. Do czego służą klasy abstrakcyjne?
  6. Jak zabezpieczyć jakąś metodę przed unieważnieniem?

Zaawansowane programowanie

edytuj
 

Sekcja „PHP/Domknięcia” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Przestrzenie nazw” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Archiwa PHAR” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Podstawy przetwarzania XML-a” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Wzorce projektowe” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/XDebug” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Ćwiczenia/Zaawansowane_programowanie” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

Bazy danych

edytuj
Poprzedni rozdział: Ćwiczenia
Następny rozdział: Projekt bazy danych

Wstęp do baz danych

edytuj

Aplikacje bazodanowe to specjalistyczne aplikacje, których głównym celem jest przechowywanie złożonych informacji, zarządzanie nimi oraz ich udostępnianie. Pod pojęciem "zarządzania" rozumie się ich modyfikację, kasowanie i dodawanie, natomiast "udostępnianie" oznacza możliwość ich pobierania na wszystkie możliwe sposoby we wszystkich kombinacjach, posortowane według dowolnego parametru. Bazy danych wykorzystywane są wszędzie tam, gdzie mamy do czynienia ze złożoną organizacją informacji. Również witryny WWW swą zawartość przechowują najczęściej w bazach, podczas gdy skrypty służą jedynie do ich wyświetlania i przetwarzania. Dzięki temu programista nie musi się martwić pisaniem kodu odpowiedzialnego np. za sortowanie wyników - całą pracę wykonali już za niego twórcy bazy danych.

Przegląd terminologii

edytuj

Przy pracy z bazami danych będziemy stosowali pewną terminologię. Pora, by się z nią zapoznać. Program, który zarządza bazami danych jest nazywany serwerem baz danych. W tym podręczniku będziemy pisali w skrócie serwer DB (DB od angielskiego słowa database oznaczającego po prostu bazę danych). Dane są w nim ułożone w hierarchiczny sposób, który ilustruje poniższy schemat:

 

Widzimy tu, że serwer DB może utrzymywać kilka różnych baz danych należących do różnych aplikacji. Pojedyncza baza zawiera tabele o określonej strukturze, w których przechowywane są rekordy zawierające dane. Struktura definiuje pola oraz ich typy, w jakich rekordy mogą przechowywać informacje. Poniżej znajduje się graficzna ilustracja zawartości prostej tabeli. Zawiera ona następujące pola:

  • id - unikalny identyfikator rekordu, który pozwala go odnaleźć. Nadawany automatycznie przez serwer DB.
  • name - nazwa produktu.
  • description - opis produktu
  • category - numer kategorii, do jakiej należy produkt.
  • counter - ilość produktów, jaką mamy w magazynie.

 

Oto kilka dodatkowych informacji o terminach użytych powyżej:

  • tabela - ang. table
  • rekord - zwany też wierszem. Ang. row
  • pole - zwana też kolumną. Ang. field

Pomiędzy tabelami w obrębie bazy mogą występować pewne zależności zwane relacjami. W powyższym przykładzie istnieje relacja między produktami, a kategoriami, do których są przypisane (pole category). Wyróżnia się kilka rodzajów relacji:

  • Jeden do wielu - jednemu rekordowi z tabeli A przypisanych jest kilka rekordów z tabeli B. Przykładem są nasze kategorie i produkty.
  • Wiele do wielu - jednemu rekordowi z tabeli A przypisanych jest kilka rekordów z tabeli B oraz jednemu rekordowi z tabeli B przypisanych jest kilka rekordów z tabeli A. Przykład to książki oraz ich autorzy. Jedna książka może być napisana przez wielu ludzi, jednocześnie pojedynczy człowiek może napisać kilka książek.
  • Jeden do jednego - jednemu rekordowi z tabeli A przypisany jest dokładnie jeden rekord z tabeli B. Relacja ta jest rzadko wykorzystywana.

Relacje można odzwierciedlać w strukturze bazy, a także pobierać dane z ich wykorzystaniem (np. pobrać produkty posortowane według tytułów kategorii, które mieszczą się przecież w innej tabeli). Bazy, w których dozwolone są takie operacje, nazywamy relacyjnymi bazami danych, w przeciwieństwie do płaskich baz danych.

Wszystkie operacje na bazach danych wykonujemy, wysyłając do serwera zapytania (ang. query) sformułowane w specjalnym języku SQL (Structured Query Language). Jego podstawy poznamy w niniejszym podręczniku, lecz po bardziej zaawansowane jego możliwości będziesz musiał sięgnąć do innych źródeł. Terminem ANSI SQL określamy nazwę standardu definiującego język SQL. Różne serwery DB implementują jego założenia lepiej lub gorzej, ale w przypadku korzystania ze złożonych możliwości kompatybilność między nimi nie jest zadowalająca.

Przegląd serwerów DB

edytuj

Oto krótki przegląd niektórych najczęściej spotykanych serwerów DB:


  • MySQL - najpopularniejszy serwer DB do zastosowań WWW stworzony przez szwedzką firmę MySQL AB, a obecnie rozwijany przez potentata branży bazodanowej, Oracle Corporation. Można go używać bez żadnych opłat. MySQL słynie ze swej olbrzymiej wydajności, a najnowsza wersja 5, z której będziemy korzystać, obsługuje już prawie cały standard ANSI SQL. Początkowo PHP posiadał wbudowaną obsługę tego serwera, lecz w wyniku zmian licencyjnych musiał zrezygnować z tego i obecnie moduł dla MySQL-a należy dodawać ręcznie.
  • PostgreSQL - główny konkurent MySQL-a dostępny na licencji open-source. Jego wydajność jest nieco mniejsza, ale wciąż jest to jedyny darmowy serwer DB, który posiada pełną obsługę standardu ANSI SQL.
  • SQLite - ten serwer DB jest dość specyficzny, ponieważ w rzeczywistości jest to biblioteka wbudowywana w aplikacje, które go wykorzystują (np. w interpreter PHP). Stąd też do korzystania z niego nie potrzeba żadnych dodatkowych programów. SQLite jest wbudowany domyślnie w PHP, odkąd zmienił się sposób licencjonowania MySQL-a.

W przeszłości PHP posiadał osobne funkcje do komunikacji z każdą z tych baz, dlatego powstawało wiele napisanych w PHP bibliotek unifikujących interfejs (np. ADODB, Creole). Ponadto dodawały one kilka zwiększających wydajność opcji takich, jak cache'owanie wyników zapytań do plików. W PHP 5.1.0 pojawiła się wreszcie wbudowana biblioteka PHP Data Objects, która także udostępnia jednolite API. W tym podręczniku skupimy się właśnie na niej.

Spis treści

edytuj

Ta część podręcznika rozpocznie się od nauki podstaw języka SQL. Następnie nauczysz się podstaw programowania obiektowego, którego znajomość jest niezbędna, aby korzystać z biblioteki PDO. Dowiemy się także, jak w przeszłości komunikowało się z bazami danych, jak stworzyć system sesji oparty o bazy danych oraz napiszemy przykładową aplikację: system newsów.

  1. Podstawy języka SQL
    1. Projekt bazy danych
    2. Zarządzanie rekordami
    3. Pobieranie rekordów
    4. Relacje i indeksy
  2. Biblioteka PDO
  3. Bazy danych i sesje
  4. Jak to się robiło kiedyś?
  5. phpMyAdmin
  6. Studium przypadku: System newsów
  7. Bazy danych - co dalej?
Poprzedni rozdział: Wstęp do baz danych
Następny rozdział: Zarządzanie rekordami

Projekt bazy danych

edytuj

Odstawimy teraz na chwilę PHP i nauczymy się pracować z serwerem baz danych MySQL. Poznamy również podstawy języka SQL do komunikacji z nim.

Wiersz poleceń serwera DB

edytuj

Aby wykonywać operacje na bazie danych, nie jest konieczne samodzielne tworzenie odpowiednich aplikacji. MySQL posiada specjalny wiersz poleceń, w którym możemy wprowadzać zapytania i oglądać ich wyniki. Serwer DB powinien być już uruchomiony przy starcie systemu operacyjnego, dlatego przystąpimy teraz do rzeczy.

  • Jeżeli pracujesz w systemie uniksowym, odszukaj katalog z danymi binarnymi, w którym mogła po instalacji zostać zlokalizowana aplikacja mysql. Uruchom ją poleceniem:
mysql -u root -p

Lub, jeżeli łączysz się poprzez sockety:

mysql -u root -p -S /tmp/mysql.sock
  • Użytkownik systemu Windows musi otworzyć systemowy wiersz poleceń i komendą cd przełączyć się na katalog z instalacją MySQLa, a następnie na znajdujący się w nim folder bin. Tutaj należy uruchomić program mysql.exe z parametrami -u root -p. Przykładowe postępowanie wygląda tak:
C:\> D:
D:\> cd Serwer/mysql/bin
D:\Serwer\mysql\bin> mysql.exe -u root -p

Podstawą bezpieczeństwa w bazie danych MySQL są użytkownicy, którzy mają jasno określone przywileje dostępu do baz. Najważniejszym z nich jest root, służący na serwerach produkcyjnych jedynie do administracji. Jednak w domowych warunkach można go wykorzystać także do projektowania własnych baz oraz testowania skryptów. W naszym przypadku podaliśmy podczas instalacji, że hasło tego użytkownika to także root. Podaj je, gdy zostaniesz o to zapytany przez wiersz poleceń.

Jeżeli po wpisaniu hasła ujrzysz znak zachęty mysql>, wszystko poszło OK i jesteśmy gotowi do pracy. W wierszu poleceń obowiązują następujące reguły:

  1. Zapytania kończymy średnikiem lub sekwencją \g. Jeżeli zabraknie tego elementu, ENTER zwyczajnie będzie Cię przenosił do nowej linijki!
  2. Pomoc włączamy sekwencją \h
  3. Opuszczamy wiersz poleceń sekwencją \q

Tworzenie bazy danych

edytuj

Standardowo po instalacji MySQL posiada stworzone dwie domyślne bazy: test oraz mysql. Pierwsza jest pusta, a druga zawiera ustawienia serwera i lepiej nic tam nie grzebać bez dokładnej znajomości jej budowy. My jednak nie skorzystamy z żadnej z nich.

CREATE DATABASE produkty;

To zapytanie utworzy nam nową bazę danych o nazwie produkty. Musimy się teraz na nią przełączyć:

USE produkty;

Po każdym wykonanym zapytaniu wiersz poleceń wyświetla nam informacje o jego rezultacie. Napis Query OK oznacza, że zostało ono zaakceptowane i poprawnie wykonane. Dalej mogą wystąpić informacje diagnostyczne (ilość dokonanych zmian, czas wykonywania lub lista wyników).

Tworzenie tabeli

edytuj

Jak powiedzieliśmy, właściwe dane przechowywane są w tabelach. Ich tworzenie polega na definiowaniu szczegółowej struktury rekordów. Utworzymy teraz tabelę produkty przechowującą informacje o różnych produktach:

CREATE TABLE `produkty` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`nazwa` VARCHAR(60) NOT NULL,
`opis` TEXT NOT NULL,
`ilosc` SMALLINT DEFAULT '0',
`cena` FLOAT NOT NULL,
`jakosc` TINYINT NOT NULL 
) ENGINE = MYISAM;

W pierwszej linijce nakazujemy utworzenie tabeli o podanej nazwie. Nawias rozpoczyna definicję jej struktury. Opisy budowy poszczególnych pól oddzielone są przecinkami, a informacje do każdego z nich podawane są w następującej kolejności:

  1. Nazwa pola
  2. Typ pola
  3. Czy pole może być puste?
  4. Wartość domyślna
  5. Parametry dodatkowe
  6. Klucze i indeksy

Przyjrzyjmy się zatem poszczególnym polom:

  1. id - pole to będzie przechowywało liczbowy, jednoznaczny identyfikator rekordu. Dwa rekordy nie mogą posiadać tego samego ID. Pole to powinny posiadać w zasadzie wszystkie tabele, gdyż jest ono podstawą relacji oraz systemów zarządzania bazą. Parametry to:
    • INT - typ liczbowy
    • NOT NULL - pole nie może być puste
    • AUTO_INCREMENT - MySQL automatycznie będzie dbał o nadawanie nowo dodawanym rekordom kolejnych ID.
    • PRIMARY KEY - klucz główny określający przeznaczenie tego pola jako podstawy do identyfikacji rekordów.
  2. nazwa - tutaj będziemy umieszczali nazwę produktu. Parametry to:
    • VARCHAR(60) - typ tekstowy. Maksymalna długość to 60 znaków.
    • NOT NULL - pole nie może być puste
  3. opis - opis produktu z dodatkiem wazeliny. Parametry to:
    • TEXT - typ tekstowy (maksymalna długość: 64 kB)
    • NOT NULL - pole nie może być puste
  4. ilosc - określa ilość sztuk produktu w magazynie. Parametry to:
    • SMALLINT - typ liczbowy
    • DEFAULT '0' - pole może pozostać puste, a domyślnie nowym rekordom nadajemy tutaj wartość 0.
  5. cena - cena produktu. Parametry to:
    • FLOAT - typ liczbowy z obsługą ułamków (aby można było także grosze uwzględniać)
    • NOT NULL - pole nie może być puste
  6. jakosc - liczbowe oznaczenie jakości produktu. Parametry to:
    • TINYINT - typ liczbowy
    • NOT NULL - pole nie może być puste

Ostatnia linijka zawiera zamknięcie definicji struktury oraz określenie typu tabeli. Nie musisz się nim teraz przejmować. Wystarczy na razie wiedzieć, że domyślnym jest obecnie InnoDB, ale do większości zastosowań używa się wydajniejszego MyISAM.

Zauważyłeś już pewnie, że w zapytaniu wykorzystaliśmy kilka różnych typów liczbowych. Oto dokładniejsza specyfikacja ważniejszych typów:

  1. TINYINT - liczba jednobajtowa. Wartości od -128 do 127.
  2. TINYINT UNSIGNED - liczba jednobajtowa bez znaku. Wartości od 0 do 255. Słowo UNSIGNED po kolejnych nazwach typów liczbowych robi to samo, co w tym przypadku.
  3. SMALLINT - liczba dwubajtowa. Wartości od -32768 do 32767, a bez znaku od 0 do 65535.
  4. MEDIUMINT - liczba trzybajtowa. Wartości od -8388608 do 8388607, a bez znaku od 0 do 16777215.
  5. INT - liczba czterobajtowa. Wartości od -2147483648 do 2147483647, a bez znaku od 0 do 2147483647.
  6. BIGINT - liczba ośmiobajtowa. Wartości od -9223372036854775808 do 9223372036854775807, a bez znaku od 0 do 18446744073709551615.
  7. FLOAT - liczba zmiennoprzecinkowa czterobajtowa (tak, jak w PHP).
  8. VARCHAR(M) - tekst o długości N od 0 do M znaków, gdzie M < 256. W pamięci zajmuje N + 1 bajtów (dodatkowy zawiera długość tekstu).
  9. CHAR(M) - tekst o długości od 0 do M znaków, gdzie M < 256. W przeciwieństwie do poprzedniego typu, zajmuje w pamięci zawsze M bajtów nawet, jeżeli znajdujący się w nim tekst jest krótszy.
  10. TEXT - typ tekstowy doskonały do przechowywania dłuższych treści. Można w nim zmieścić aż 64 kB danych (65535 znaków).
  11. BLOB - typ do przechowywania danych binarnych, np. plików. Maksymalna wielkość to także 64 kB.
  12. BOOL - typ logiczny, równoważnik zapisu TINYINT(1).
  13. DATE - wyspecjalizowany typ do przechowywania daty. Bardzo rzadko wykorzystywany w poważniejszych aplikacjach PHP ze względu na jego niewygodne przetwarzanie i formatowanie.

W przypadku typów liczbowych bardzo często spotyka się zapisy np. INT(8) lub MEDIUMINT(6). Liczby w nawiasach nie mają nic wspólnego z wielkością danych, jakie można w nich trzymać. Mówią one bazie danych MySQL, że jeśli dana liczba jest krótsza niż np. 8 znaków, to należy ją dopełnić spacjami do tej długości, kiedy będziemy na niej operować, jak na tekście.

Specyfika języka SQL

edytuj

Napisaliśmy już kilka zapytań i widać z nich, że SQL w wielu miejscach przypomina zwyczajny mówiony angielski. Pora poznać kilka jego cech:

  1. Wielkość liter nie odgrywa żadnej roli, lecz programiści używają ich ze względów estetycznych. Zapis create table `produkty` także jest poprawny.
  2. Odwrócone apostrofy są używane do określania nazw tabel, baz i pól, ale nie są one niezbędne. Zapisy create table produkty oraz id int not null są poprawne, lecz trzeba tu zwrócić uwagę na jedną rzecz, którą zademonstrujemy na przykładzie. Otóż chcielibyśmy mieć w tabeli pole "order". Piszemy więc order int not null, ale nagle okazuje się, że MySQL zgłasza w tej linijce błąd. Co jest nie tak? "order" jest jednym ze słów kluczowych języka SQL i aby tak nazwać tabelę, musimy koniecznie umieścić ją w odwróconych apostrofach. Wielu programistów, aby zapytania nie przypominały grochu z kapustą, z definicji używa ich zatem wszędzie, unikając w ten sposób wszystkich niespodzianek, a także niekompatybilności z nowymi wersjami, które dodają coraz więcej słów kluczowych.

Ćwiczenia

edytuj

W ramach treningu utwórz następujące tabele:

  1. Tabela klientów sklepu z polami id, imie, nazwisko, wiek, miejscowosc, ulica, numer_domu, numer_mieszkania, telefon.
  2. Tabela dostawców sklepu z polami id, nazwa, dzien_tyg_dostawy, naleznosc
  3. Tabela kategorii produktów z polami id, nazwa, opis, ilosc_produktow.

Pamiętaj, że możesz sprawdzić strukturę już utworzonej tabeli poleceniem:

DESCRIBE tabela;

Jeżeli coś Ci nie wyszło, wykonaj

DROP TABLE tabela;

aby skasować tabelę.

Poprzedni rozdział: Projekt bazy danych
Następny rozdział: Pobieranie rekordów

Zarządzanie rekordami

edytuj

Wiemy już, jak utworzyć bazę danych oraz strukturę tabel. Niewątpliwie bez tych operacji nie można zacząć, lecz później podstawowymi operacjami stają się te, dzięki którym możemy zarządzać danymi. Właśnie w tym odcinku dowiemy się, jak dodawać, modyfikować oraz usuwać niepotrzebne rekordy. Na początek proste polecenie, które pozwoli nam na wyświetlenie obecnej zawartości tabeli:

SELECT * FROM `tabela`

Gdzie zamiast tabela wstawiamy nazwę naszej tabeli. Szczegółowy opis tego zapytania poznamy w następnych dwóch odcinkach. Teraz posłuży nam on jedynie do sprawdzania, czy wszystko przebiega poprawnie.

Dodawanie rekordów

edytuj

Aby dodać rekord do tabeli, należy wysłać zapytanie INSERT. Ma ono generalnie dwie możliwe składnie:

INSERT INTO `tabela` VALUES('Wartość pola 1', 'Wartość pola 2', 'Wartość pola 3');
INSERT INTO `tabela` (`pole1`, `pole2`, `pole3`) VALUES('Wartość pola 1', 'Wartość pola 2', 'Wartość pola 3');

Oba powodują utworzenie nowego rekordu w podanej tabeli, lecz istnieje między nimi pewna różnica. W pierwszym zapytaniu musimy bezwzględnie podać wartości wszystkich pól nowego rekordu, jakie mamy zdefiniowane w strukturze tabeli, w identycznej kolejności. Drugie zapytanie pozwala nam w pierwszym z nawiasów wymienić listę pól, jakie nas interesują i dopiero potem podać ich wartości. W praktyce znacznie częściej używa się właśnie jego, gdyż nie trzeba podawać wartości pól ID, które nadawane są przez bazę automatycznie. Wróćmy zatem do naszej tabeli produkty. Wstawmy do niej kilka rekordów:

INSERT INTO `produkty`
  (`nazwa`, `opis`, `ilosc`, `cena`, `jakosc`) VALUES(
   'Długopisy niebieskie',
   'Długopisy z niebieskim wkładem firmy XXX',
   100,
   2.15,
   3);
INSERT INTO `produkty`
  (`nazwa`, `opis`, `ilosc`, `cena`, `jakosc`) VALUES(
   'Długopisy czerwone',
   'Długopisy z czerwonym wkładem firmy XXX',
   50,
   2.15,
   3);
INSERT INTO `produkty`
  (`nazwa`, `opis`, `ilosc`, `cena`, `jakosc`) VALUES(
   'Zszywacze',
   'Metalowy zszywacz + 100 zszywek.',
   30,
   9.50,
   4);
INSERT INTO `produkty`
  (`nazwa`, `opis`, `ilosc`, `cena`, `jakosc`) VALUES(
   'Karteczki samoprzylepne',
   'Samoprzylepne kartki koloru żółtego 10x10 cm w kompletach po 100 sztuk',
   200,
   3.60,
  2);

Istnieje także możliwość wstawienia kilku rekordów naraz za pomocą jednego zapytania INSERT:

INSERT INTO `produkty` (`nazwa`, `opis`, `ilosc`, `cena`, `jakosc`) VALUES
('Strugaczki', 'Czerwone, do dwóch rozmiarów ołówków', 60, 0.90, 4),
('Gumki do ścierania', 'Gumki do ścierania ołówków firmy ZZZ', 97, 0.50, 3),
('Spinacze do papieru', 'Metalowe spinacze do papieru w kompletach po 50 sztuk.', 68, 0.50, 4);

Tutaj po VALUES podajemy kilka bloków wartości dla kolejnych rekordów, każdy z nich w nawiasach i oddzielony przecinkiem.

Pamiętaj, że wartości tekstowe musimy zawsze podawać w apostrofach. Liczby można podawać zarówno z nimi, jak i bez, lecz podczas programowania apostrofami obejmuje się najczęściej wszystkie wstawiane ze skryptu wartości.

Modyfikowanie rekordów

edytuj

Czasem zachodzi konieczność zmodyfikowania niektórych informacji. Tu pomocne będzie zapytanie UPDATE. Ma ono bardzo prostą składnię:

UPDATE `tabela` SET `pole1` = 'Nowa wartość', `pole2` = 'Nowa wartość';

Jednak uważaj! Gdybyś wykonał powyższe zapytanie, podstawiając odpowiednie dane, okazałoby się, że zmiany zostały wprowadzone we wszystkich rekordach! Bardzo rzadko jest to pożądana rzecz, ponieważ o wiele częściej chcemy zmodyfikować pewną, konkretną grupę. Do jej uwzględnienia użyjemy nowej klauzuli: WHERE wyrażenie umieszczanej po liście pól do podmiany. Wyrażenie jest dowolnym poprawnym wyrażeniem języka SQL, a układa się je podobnie, jak te w PHP. Szczegółowe informacje o budowaniu wyrażeń poznamy w następnym odcinku, teraz ograniczymy się do kilku prostych sztuczek.

Na początek zmienimy opis i cenę produktu o ID 1:

UPDATE `produkty` SET `opis` = 'Długopisy niebieskie firmy YYY', `cena` = '2.45' WHERE `id` = '1';

Teraz coś trudniejszego: nasz sklep planuje podwyżkę cen najlepszych produktów (jakość 4) o 50 groszy. Wiele osób próbuje robić to, pobierając ceny wszystkich interesujących je produktów i zmieniając je setkami zapytań UPDATE, po jednym dla jednego rekordu. Jest to bardzo niepraktyczne, ponieważ język SQL jest na tyle zaawansowanym narzędziem, że radzi sobie z tym bez trudu:

UPDATE `produkty` SET `cena` = (`cena` + 0.5) WHERE `jakosc` = 4;

Powinniśmy teraz ujrzeć informację, że zmodyfikowane zostały trzy rekordy.

Usuwanie rekordów

edytuj

Niepotrzebne rekordy kasujemy zapytaniem DELETE. Podobnie, jak w poprzednim przypadku, należy zastosować klauzulę WHERE, aby określić, które z nich chcemy usunąć. Inaczej możemy pożegnać się z całą zawartością tabeli.

DELETE FROM `produkty` WHERE `id` = 3;

Zdarza się, że tabelę trzeba rzeczywiście wyczyścić, np. z danych testowych, aby aplikacja mogła być używana na normalnym serwerze. Jednak wtedy nie powinno się wykorzystywać polecenia DELETE FROM `produkty`. Do tego celu służy specjalne zapytanie:

TRUNCATE `produkty`;

Różnica pomiędzy pierwszym i drugim jest podobna, jak pomiędzy zaznaczeniem wszystkich plików na dysku i kliknięciu "Delete", a uruchomieniem jego formatowania.

Wykonując wiele zapytań DELETE oraz INSERT zauważysz, że pozostają Ci luki w numeracji. Spróbuj dodać teraz jakiś rekord. Jeżeli wykonałeś wcześniej zapytanie kasujące ten o ID 3, MySQL pozostawi tam lukę, nadając nowemu rekordowi następny w kolejności ID - 4. Prawdopodobnie z przyczyn estetycznych niektórym nowym programistom to zachowanie przeszkadza, lecz jest to zupełnie błędne podejście do problemu. Jak wspomnieliśmy bowiem, MySQL jest relacyjną bazą danych, w której rekordy z jednej tabeli mogą być połączone odpowiednimi relacjami z rekordami w drugiej. Wyobraźmy sobie więc, co by się stało, gdybyśmy skasowali jakąś kategorię np. newsów w naszym serwisie, a potem dodali nową i okazało się, że MySQL zaliczył w jej poczet wszystkie newsy z tej usuniętej, gdyż nowy rekord zajął pustą lukę w numeracji. Działanie takie stwarza poważne zagrożenie synchronizacji bazy i wprowadza w nią element losowości. Dlatego zaufaj twórcom serwerów DB, oni naprawdę znają się na rzeczy i nie podejmuj zbędnych prób zmieniania na gorsze tego, co wyraźnie służy zarówno tobie, jak i twojej bazie.

Zamiana rekordów

edytuj

Bardzo przydatną operacją jest automatyczne podmienianie rekordów. Polega ono na tym, że jeżeli wstawiamy rekord A do tabeli, w której znajduje się już rekord B o podobnym kluczu, jest on automatycznie nadpisywany przez system. MySQL posiada dwa zapytania, które są pomocne przy podmienianiu rekordów, jednak zanim się do nich dobierzemy, musimy utworzyć sobie nową tabelę.

Aby MySQL mógł zdecydować, czy rekord należy nadpisać, musi wiedzieć, które pola tabeli przechowują wartości unikalne, czyli takie, że nie można znaleźć dwóch rekordów o identycznej wartości w tym polu. Wiemy już, że taką właściwość powinno mieć pole id, i rzeczywiście - informujemy o tym bazę, tworząc dla niego tzw. klucz główny (PRIMARY KEY). Dba on o to, aby wartości się nie powtarzały. Jednak klucz ten można nałożyć tylko na jedno pole naraz, a operacja REPLACE przy rekordach mających z definicji nie tylko unikalne, ale też nadawane automatycznie ID, zbyt dużego sensu nie ma. Dlatego istnieje indeks UNIQUE, który może być nałożony na dowolnie dużo pól i także sprawi, że zyskają one tę właściwość. Aby pokazać to w praktyce, utwórzmy tabelę dla elektronicznego słownika (w wersji uproszczonej, oczywiście):

CREATE TABLE `slownik` (
`id` MEDIUMINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`haslo` VARCHAR(40) NOT NULL,
`znaczenie` VARCHAR(255) NOT NULL,
  UNIQUE(
   `haslo` 
  )
) ENGINE = MYISAM;

Mamy tu trzy pola: id, haslo oraz znaczenie. ID służy jedynie celom administracyjnym (wyszukiwanie względem liczby jest zawsze szybsze, niż względem tekstu). Dla użytkownika naszego słownika najważniejsze będzie jednak pole haslo, które także powinno mieć wartości unikalne. Indeks UNIQUE tworzymy tak, jak na przykładzie: po liście pól wstawiamy słowo kluczowe UNIQUE, w którym w nawiasie wymieniamy pola posiadające taką właściwość.

Jeżeli jesteśmy administratorami słownika, mogącymi dodawać do niego nowe hasła, unikalność obsługiwana przez MySQL ma dla nas duże znaczenie. Kiedy będzie w nim bowiem już dużo słów, ze zwykłej pomyłki możemy spróbować dodać po raz drugi to samo. Tradycyjne polecenie INSERT zareagowałoby błędem powiadamiającym, że próbujemy utworzyć rekord, w którym powtarza się unikalna wartość. Dlatego możemy zastosować inne zapytanie: REPLACE. Wstawiamy nim pierwsze trzy hasła:

REPLACE slownik (haslo, znaczenie) VALUES('tree', 'drzewo');
REPLACE slownik (haslo, znaczenie) VALUES('house', 'dom (budynek)');
REPLACE slownik (haslo, znaczenie) VALUES('sign', 'znak');

W tym momencie działają one identycznie, jak znane nam zapytanie INSERT (mają nawet podobną składnię, chociaż dozwolone jest stosowanie także składni polecenia UPDATE). Możemy zapytać się, jaka jest zawartość tabeli i pokażą nam się wszystkie trzy wymienione rekordy. Spróbujmy teraz po raz drugi dodać istniejące hasło (np. tree):

REPLACE slownik (haslo, znaczenie) VALUES('tree', 'drzewo, drzewko');
Query OK, 2 rows affected (0.00 sec)

Zwróć uwagę, jaki komunikat kontrolny pokazał MySQL: zmodyfikowane zostały dwa rekordy. Wyświetlmy zawartość tabeli: okazuje się, że pierwotny rekord tree (z ID równym 1) został skasowany, a na jego miejscu pojawił się nowy, któremu serwer nadał ID 4. Mamy zatem sformułowaną zasadę działania polecenia REPLACE:

  1. Spróbuj dodać nowy rekord.
  2. Jeśli nie powiedzie się z powodu duplikacji unikalnej wartości, usuń stary rekord powodujący kolizję.
  3. I ponownie dodaj nowy rekord.

REPLACE nie jest jedynym poleceniem realizującym zamianę rekordów. Od wersji 4.1.0 MySQL obsługuje także rozszerzenie zapytania INSERT o element ON DUPLICATE KEY UPDATE, zgodne ze standardem ANSI SQL. Dzięki niemu nie trzeba kasować kolidującego rekordu, lecz jedynie nadpisać jego wartości nowymi. Dodajmy ponownie jakieś istniejące hasło:

INSERT INTO slownik (haslo, znaczenie) VALUES('house',  'dom (budynek), rodzaj obiektu mieszkalnego.')
ON DUPLICATE KEY UPDATE znaczenie=VALUES(znaczenie);
Query OK, 2 rows affected (0.02 sec)

Pierwsza część tego zapytania to znany nam już dobrze INSERT. Próbuje on wstawić nowy rekord. Kiedy jednak zajdzie kolizja, do akcji wkracza nowa część: ON DUPLICATE KEY UPDATE, w której możemy zdefiniować sposób nadpisania starego rekordu według składni nazwa_pola = nowa_wartosc, nazwa_pola = nowa_wartosc, .... Możemy tutaj ustawiać statyczne wartości, np. pole = 1, lub też odwołać się do wartości pola, która miała być wstawiona poleceniem INSERT. Służy do tego funkcja VALUES(nazwa_pola). Wykorzystaliśmy ją w powyższym przykładzie, aby wprowadzić nową definicję hasła na miejsce starej.

Dzięki automatycznej możliwości nadpisywania rekordów z unikalnymi kluczami, nasza baza jest wygodniejsza. Jeżeli jednak tworzona aplikacja ma pracować z różnymi systemami DB, musimy sprawdzić, czy obsługują one podaną operację i w razie potrzeby napisać odpowiedni emulator.

Błędy w zapytaniach

edytuj

Także w języku SQL można popełnić błędy, które uniemożliwią wykonanie zapytania. Każdy serwer DB raportuje je inaczej. Ponieważ sztandarową bazą w tym podręczniku jest MySQL, pokażemy sposoby debugowania zapytań właśnie dla niego.

Zacznijmy od błędów składni. Wykonaj następujące zapytanie:

UPDATE `produkty` SET `opis` = 'Długopisy niebieskie firmy YYY' `cena` = '2.45' WHERE `id` = '1';

Zawiera ono celowo wprowadzony błąd, który powoduje pokazanie się komunikatu

You have an error in your SQL syntax; check the manual that
corresponds to your MySQL server version for the right syntax to use near
'cena` = '2.45' WHERE `id` = '1'' at line 1

Komunikat pokazał nam użyteczną informację o miejscu lokalizacji problemu. Według niego coś jest nie tak w okolicy ciągu cena` = '2.45' WHERE `id` = '1'. Niepisana zasada mówi, że najczęściej błąd popełniliśmy bezpośrednio przed nim. Zobaczmy więc, co znajduje się w zapytaniu przed odwołaniem do pola "cena". Okazuje się, że brakuje przecinka oddzielającego je od porzedniej definicji nowej wartości. Kiedy go tam umieścimy, wszystko zaczyna prawidłowo działać.

Inny rodzaj problemu może powstać przy pracy z polami "PRIMARY KEY" oraz "UNIQUE" (o nich dalej). Oba te atrybuty nadają polu właściwość unikalności, czyli nie mogą istnieć dwa rekordy o takich samych wartościach w tym miejscu. Przykładowo wywołajmy takie zapytanie INSERT:

INSERT INTO `produkty`
  VALUES(
   '3',
   'Długopisy niebieskie',
   'Długopisy z niebieskim wkładem firmy XXX',
   100,
   2.15,
   3);

Jest w nim wymieniony ID produktu. Jeżeli wykonywałeś wszystkie zapytania w tym odcinku, prawdopodobnie nic się teraz nie stanie, ponieważ rekord o ID 3 niedawno skasowaliśmy. W takim wypadku wykonaj je jeszcze raz, a baza pokaże ci wtedy

Duplicate entry '3' for key 1

Oznacza on tyle, że w kluczu pierwszym (w naszym przypadku jest to pole "id") próbujemy po raz drugi umieścić wartość 3, co jest niedozwolone.

Poprzedni rozdział: Zarządzanie rekordami
Następny rozdział: Relacje i indeksy

Pobieranie rekordów

edytuj

Osobnego omówienia wymaga operacja pobierania rekordów z bazy danych, która jest esencją pracy z tego typu aplikacjami. W poprzednim rozdziale poznaliśmy dla potrzeb testowych zapytanie SELECT * FROM tabela, lecz było ono podane wyłącznie, aby można było sprawdzić, czy modyfikacja zawartości tabel rzeczywiście się powiodła. Tymczasem jego możliwości są dużo bardziej skomplikowane i umożliwiają wykonywanie wielu ciekawych rzeczy. Teraz przyjrzymy się im dokładniej.

Filozofia zapytania SELECT

edytuj

Zapytanie SELECT to w zasadzie zbiór klauzul, które możemy dodawać i odejmować, działających jak filtry dla danych. Jako rezultat działania otrzymujemy zawsze to, co przejdzie przez wszystkie z nich. Podstawową i jedyną obowiązkową klauzulą jest oczywiście SELECT dane pokazująca, co należy pobrać. Za dane możemy podstawić listę wyrażeń odseparowanych przecinkami, które są nam potrzebne. Oto malutki przykład:

SELECT 1

Jego wykonanie w wierszu poleceń spowoduje wyświetlenie się tekstowej tabelki z wynikiem:

+---+
| 1 |
+---+
| 1 |
+---+
1 row in set (0.00 sec)

Pierwszy wiersz zawiera zawsze nazwy wszystkich kolumn w rekordzie. Nazwy te wykorzystamy, kiedy zaczniemy pisanie skryptów łączących się z bazą, aby pobrać wyniki z rekordów. Kolejne wiersze obrazują wszystkie pasujące rekordy wyników.

Z doświadczenia możemy powiedzieć, że pozostawianie bazie danych MySQL spraw nazewnictwa pól na liście wyników nie należy do najszczęśliwszych i w wielu przypadkach zachodzi potrzeba ręcznego określenia nazwy. Można to uczynić, dodając po każdej wartości do pobrania dodatku AS `nowa_nazwa`:

SELECT 1 AS `pole`
+------+
| pole |
+------+
|    1 |
+------+
1 row in set (0.00 sec)

Pobieranie statycznych danych nie wnosi jednak dużo do funkcjonalności baz danych. O wiele więcej zyskamy, kiedy będziemy mogli pobierać rekordy z tabel. Aby móc wymienić nazwy pól w klauzuli SELECT, musimy dołożyć do zapytania taki dodatek: FROM lista_tabel. lista_tabel to lista tabel odseparowanych przecinkami, z których zamierzamy korzystać. W tym odcinku poprzestaniemy na pojedynczej tabeli i dopiero później powiemy sobie, jak można obsłużyć więcej. Spróbujmy dowiedzieć się zatem czegoś o stworzonej ostatnio tabeli produkty:

SELECT id, nazwa FROM produkty;
+----+-------------------------+
| id | nazwa                   |
+----+-------------------------+
|  1 | Dlugopisy niebieskie    |
|  2 | Dlugopisy czerwone      |
|  3 | Zszywacze               |
|  4 | Karteczki samoprzylepne |
+----+-------------------------+
4 rows in set (0.02 sec)

Przypominamy, że zapytania kończymy średnikiem. Tabelka powyżej to wynik jego działania. Widzimy w niej pobrane pola id oraz nazwa wszystkich rekordów w tabeli produkty. Gdybyśmy chcieli pobrać wartości wszystkich pól, moglibyśmy w wykazie danych wpisać po prostu gwiazdkę:

SELECT * FROM produkty;

Okazuje się jednak, że paradoksalnie takie rozwiązanie ma mniejszą wydajność, niż wypisanie wszystkich pól ręcznie! Miej to na uwadze podczas projektowania twoich zapytań.

Klauzula WHERE

edytuj

Do tej pory MySQL zwracał nam wszystkie rekordy bez wyjątku, lecz w codziennej praktyce na dane nakłada się rozmaite warunki pełniące rolę filtrów. Jeżeli chcemy wiedzieć, którzy użytkownicy naszego serwisu napisali już ponad 200 postów, nie musimy pobierać wszystkiego i dokonywać ręcznej analizy informacji. Wystarczy nałożyć na zapytanie warunek, który nie dopuści do listy wyników tych rekordów, gdzie ilość postów jest mniejsza niż podana wartość. Wszystko to należy do kompetencji klauzuli WHERE warunek, która pojawia się również przy zapytaniach UPDATE oraz DELETE. Najprostszą operacją jest bez wątpienia zwrócenie konkretnego rekordu:

SELECT id, nazwa FROM produkty WHERE id = 3;
+----+-------------------------+
| id | nazwa                   |
+----+-------------------------+
|  3 | Zszywacze               |
+----+-------------------------+
1 row in set (0.01 sec)

Identycznie, jak w przypadku PHP, warunek jest zbiorem wyrażeń połączonych operatorami, których kolejnością także możemy manewrować wykorzystując nawiasy. Oto lista operatorów logicznych oraz operatorów porównania:

Operator Nazwa Składnia Opis
= Równość wyrażenie = wyrażenie Zwraca prawdę, jeżeli oba wyrażenia mają identyczną wartość.
!= Nierówność wyrażenie != wyrażenie Zwraca prawdę, jeżeli oba wyrażenia mają różne wartości.
<> Nierówność wyrażenie <> wyrażenie Zwraca prawdę, jeżeli oba wyrażenia mają różne wartości.
< Mniejsze niż wyrażenie < wyrażenie Zwraca prawdę, jeżeli lewe wyrażenie ma mniejszą wartość od prawego.
> Większe niż wyrażenie > wyrażenie Zwraca prawdę, jeżeli lewe wyrażenie ma większą wartość od prawego.
<= Mniejsze lub równe wyrażenie <= wyrażenie Zwraca prawdę, jeżeli lewe wyrażenie ma mniejszą lub równą wartość prawemu.
>= Większe lub równe wyrażenie >= wyrażenie Zwraca prawdę, jeżeli lewe wyrażenie ma większą lub równą wartość prawemu.
! Negacja (nie) !wyrażenie Zwraca prawdę, jeżeli wyrażenie jest fałszywe i fałsz, jeśli prawdziwe.
NOT Negacja (nie) NOT wyrażenie Zwraca prawdę, jeżeli wyrażenie jest fałszywe i fałsz, jeśli prawdziwe.
AND Koniunkcja logiczna (i) wyrażenie AND wyrażenie Zwraca prawdę, jeżeli oba wyrażenia są prawdziwe.
OR Alternatywa logiczna (lub) wyrażenie OR wyrażenie Zwraca prawdę, jeżeli przynajmniej jedno z wyrażeń jest prawdziwe.

Operatory AND oraz OR posiadają także warianty && oraz || (tak samo PHP posiada and i or). Oprócz tego, dostępne są tradycyjne operatory arytmetyczne, a także kilka specjalnych, niewystępujących nigdzie indziej:

pole IN(wartosci)

edytuj

Prawda, jeśli wartość pola znajduje się na liście wartości podanej w nawiasach. Spróbujemy pobrać nim rekordy o ID 1, 2 oraz 4.

SELECT id, nazwa FROM produkty WHERE id IN(1,2,4);

Nie możemy wśród wartości wymienić NULL

pole NOT IN(wartosci)

edytuj

Prawda, jeśli wartość pola NIE znajduje się na liście wartości podanej w nawiasach. Poniżej to samo zapytanie, ale omijające rekordy o podanych ID.

SELECT id, nazwa FROM produkty WHERE id NOT IN(1,2,4);

pole BETWEEN mniejszy AND wiekszy

edytuj

Wartość pola znajduje się w przedziale od mniejszy do wiekszy. Spróbujemy pobrać nim produkty, których ilość w magazynie waha się od 0 do 100:

SELECT id, nazwa FROM produkty WHERE ilosc BETWEEN 0 AND 100;

pole IS NULL

edytuj

Podczas tworzenia tabel powiedzieliśmy sobie nieco o polach z dozwolonymi wartościami pustymi (null). Za pomocą tego operatora oraz jego przeczenia IS NOT NULL możemy sprawdzać, czy dane pole zawiera wartość pustą, czy nie. W naszej tabeli tylko pole ilosc zezwala na użycie wartości pustych. Sprawdźmy więc, które rekordy takowe posiadają:

SELECT id, nazwa FROM produkty WHERE ilosc IS NULL;

Klauzula ORDER BY

edytuj

Ta klauzula dodaje możliwość sortowania wyników według określonego kryterium. Tradycyjnie podajemy ją po WHERE. Jej składnia to: ORDER BY lista_wyrazen. lista_wyrazen to lista oddzielonych przecinkami wyrażeń, według wartości których zostaną posortowane rekordy. Domyślnie obowiązuje kolejność od najmniejszego do największego, ale możemy ją odwrócić, dodając po wyrażeniu słowo DESC. Spróbujmy posegregować nasze rekordy względem ceny.

SELECT id, nazwa, cena FROM produkty ORDER BY cena;
+----+-------------------------+------+
| id | nazwa                   | cena |
+----+-------------------------+------+
|  6 | Gumki do scierania      |  0.5 |
|  7 | Spinacze do papieru     |  0.5 |
|  5 | Strugaczki              |  0.9 |
|  1 | Dlugopisy niebieskie    | 2.15 |
|  2 | Dlugopisy czerwone      | 2.15 |
|  4 | Karteczki samoprzylepne |  3.6 |
|  3 | Zszywacze               |  9.5 |
+----+-------------------------+------+
7 rows in set (0.00 sec)

A teraz w odwrotnej kolejności:

SELECT id, nazwa, cena FROM produkty ORDER BY cena DESC;
+----+-------------------------+------+
| id | nazwa                   | cena |
+----+-------------------------+------+
|  3 | Zszywacze               |  9.5 |
|  4 | Karteczki samoprzylepne |  3.6 |
|  1 | Dlugopisy niebieskie    | 2.15 |
|  2 | Dlugopisy czerwone      | 2.15 |
|  5 | Strugaczki              |  0.9 |
|  6 | Gumki do scierania      |  0.5 |
|  7 | Spinacze do papieru     |  0.5 |
+----+-------------------------+------+
7 rows in set (0.00 sec)

Możemy też przyjąć kolejne kryterium sortowania, jeśli dwa rekordy będą miały identyczną cenę. Przyjmijmy, że wtedy będą one sortowane pod względem tytułu.

SELECT id, nazwa, cena FROM produkty ORDER BY cena DESC, nazwa;
+----+-------------------------+------+
| id | nazwa                   | cena |
+----+-------------------------+------+
|  3 | Zszywacze               |  9.5 |
|  4 | Karteczki samoprzylepne |  3.6 |
|  2 | Dlugopisy czerwone      | 2.15 |
|  1 | Dlugopisy niebieskie    | 2.15 |
|  5 | Strugaczki              |  0.9 |
|  6 | Gumki do scierania      |  0.5 |
|  7 | Spinacze do papieru     |  0.5 |
+----+-------------------------+------+
7 rows in set (0.00 sec)

Widzimy teraz, że rekordy "Długopisy czerwone" oraz "Długopisy niebieskie" zamieniły się miejscami. Jeśli połączymy wszystko z klauzulą WHERE, poczujemy prawdziwą potęgę baz danych. Posortujmy według ceny tylko te rekordy, których jakość oznaczona jest jako 3:

SELECT id, nazwa, cena FROM produkty WHERE jakosc = 3
ORDER BY cena DESC,  nazwa;
+----+-----------------------+------+
| id | nazwa                 | cena |
+----+-----------------------+------+
|  2 | Dlugopisy czerwone    | 2.15 |
|  1 | Dlugopisy niebieskie  | 2.15 |
|  6 | Gumki do scierania    |  0.5 |
+----+-----------------------+------+
3 rows in set (0.00 sec)

Klauzula LIMIT

edytuj

Na stronach internetowych często prezentowane są olbrzymie ilości informacji. Aby wyświetlenie ich spisu nie przeciążało łącza, listę wyników dzieli się na strony (inaczej: porcjuje) tak, że naraz wyświetla się jedynie niewielka jej część, a do reszty możemy się dostać poprzez odpowiednie linki nawigacyjne. Oczywiste jest, że wybieranie tego małego kawałka danych powinno zachodzić po stronie bazy danych, a nie PHP. Tak jest w istocie, dzięki klauzuli LIMIT. Pozwala nam ona na zażądanie jedynie określonego kawałka rekordów pasujących do podanego wyrażenia. Pobiera ona dwie informacje: numer (nie mylić z ID!) rekordu w wynikach, od którego należy zacząć pobieranie oraz interesującą nas ilość rekordów.

Istnieje kilka składni tego polecenia. Pokażemy je na przykładach.

SELECT id, nazwa FROM produkty LIMIT 3;
+----+-----------------------+
| id | nazwa                 |
+----+-----------------------+
|  1 | Dlugopisy niebieskie  |
|  2 | Dlugopisy czerwone    |
|  3 | Zszywacze             |
+----+-----------------------+
3 rows in set (0.02 sec)

LIMIT 3 spowodowało, że zostały pokazane pierwsze trzy rekordy, począwszy od pierwszego. Aby zmienić punkt rozpoczęcia, ilość rekordów poprzedzamy "numerem startowym":

SELECT id, nazwa FROM produkty LIMIT 2, 3;
+----+-------------------------+
| id | nazwa                   |
+----+-------------------------+
|  3 | Zszywacze               |
|  4 | Karteczki samoprzylepne |
|  5 | Strugaczki              |
+----+-------------------------+
3 rows in set (0.00 sec)

Tym razem wyświetliła się nam dalsza część zbioru wyników. Przypominamy, że rasowi informatycy zaczynają liczenie od zera i tak samo jest w przypadku numerów startowych. Dlatego "2" w przykładzie oznacza w rzeczywistości rozpoczęcie od rekordu trzeciego. W poprzednim przykładzie start od pierwszego rekordu moglibyśmy zapisać jako LIMIT 0, 3.

LIMIT nie jest częścią standardu ANSI SQL, dlatego też inne systemy baz danych mogą używać innej składni lub nawet innych sposobów do wykonywania porcjowania danych. Aby zapewnić pewną kompatybilność, MySQL udostępnia niektóre z nich jako alternatywę. Oto powyższy przykład zapisany z wykorzystaniem składni bazy PostgreSQL, który działa także i tu:

SELECT id, nazwa FROM produkty LIMIT 3 OFFSET 2;
+----+-------------------------+
| id | nazwa                   |
+----+-------------------------+
|  3 | Zszywacze               |
|  4 | Karteczki samoprzylepne |
|  5 | Strugaczki              |
+----+-------------------------+
3 rows in set (0.00 sec)

Dlatego jeśli planujesz tworzenie aplikacji pod oba te systemy naraz, pamiętaj o różnicach w implementacji języka SQL między nimi. Dla większej ilości baz danych może być konieczne zupełne zrezygnowanie z klauzuli LIMIT na rzecz odpowiednio konstruowanych warunków WHERE oraz dodatkowych pól w tabelach.

Funkcje grupujące

edytuj

Język SQL umożliwia stosowanie funkcji do częściowej obróbki danych po stronie serwera. Specyficzną grupą funkcji są tzw. funkcje grupujące. W przeciwieństwie do reszty, operują one na zbiorach rekordów, podając o nich różne istotne informacje. Wiąże się z tym kilka ograniczeń użycia, lecz póki co nie będą nas one dotyczyć, gdyż nie potrafimy pobierać jeszcze danych z kilku tabel naraz. Do tego zagadnienia wrócimy w następnym rozdziale.

Pierwszą funkcją, z jaką się zapoznamy, będzie COUNT(). Podaje ona ilość rekordów, które pasują do warunku.

SELECT COUNT(id) FROM produkty;
+-----------+
| COUNT(id) |
+-----------+
|         7 |
+-----------+
1 row in set (0.00 sec)

Teraz wiemy, że w naszej tabeli jest siedem rekordów. Niektórzy programiści stosują składnię COUNT(*) (pamiętasz, co oznacza gwiazdka w zapytaniach SELECT?), jednak my zdecydowanie odradzamy jej użycie ze względów niższej wydajności.

Zadajmy sobie pytanie, dlaczego COUNT() wymaga podawania konkretnego pola, zamiast np. nazwy tabeli? Wszystko wyjaśni się, kiedy zobaczymy, jak funkcja ta reaguje na pola zezwalające obecność wartości NULL. W naszej tabeli jedynie ilosc zezwala na jej obecność. Wstawmy więc ją do rekordu o ID 5, aby mieć na czym eksperymentować:

UPDATE `produkty` SET `ilosc` = NULL WHERE `id` = 5;

Zobaczmy, co się teraz stanie po wykonaniu funkcji COUNT() na polu ilosc:

SELECT COUNT(ilosc) FROM produkty;
+--------------+
| COUNT(ilosc) |
+--------------+
|            6 |
+--------------+
1 row in set (0.00 sec)

Niespodzianka, zwróciło nam informację o sześciu rekordach, chociaż żadnego nie kasowaliśmy. Spokojnie, wszystko jest w porządku. COUNT() celowo opuszcza wartości NULL, gdyż tak wynika ich definicji. Po co liczyć coś, czego na dobrą sprawę nie ma? Zauważmy, jak mądrze postąpiliśmy, wprowadzając NULL do naszej bazy. Teoretycznie przy braku towaru w magazynie można by ustawiać rekordom wartości 0, lecz wtedy do pobrania gatunków produktów będących jeszcze na stanie musimy zastosować klauzulę WHERE:

SELECT COUNT(id) FROM produkty WHERE ilosc > 0;

Jeżeli zamiast 0 wprowadzimy wartości NULL, ulegnie ono skróceniu:

SELECT COUNT(ilosc) FROM produkty;

Nie tylko ta funkcja grupująca reaguje na wartość NULL. Udowodnimy to na przykładzie liczenia średniej ilości towaru w magazynie. Służy do tego funkcja grupująca AVG(). Hipoteza jest następująca: jeśli funkcja ta jest wrażliwa na obecność NULL, powinna dawać różne wyniki w zależności od tego, czy w rekordzie towaru niewystępującego w magazynie oznaczymy ilość przez NULL, czy przez 0. Sprawdźmy to. Oto ciąg wykonywanych przez nas operacji:

mysql> UPDATE `produkty` SET `ilosc` = NULL WHERE `id` = 5;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT AVG(ilosc) FROM produkty;
+------------+
| AVG(ilosc) |
+------------+
|    90.8333 |
+------------+
1 row in set (0.00 sec)

mysql> UPDATE `produkty` SET `ilosc` = 0 WHERE `id` = 5;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT AVG(ilosc) FROM produkty;
+------------+
| AVG(ilosc) |
+------------+
|    77.8571 |
+------------+
1 row in set (0.00 sec)

Oto opis poczynionych kroków:

  1. Ustawiamy w rekordzie 5 pole ilosc na NULL.
  2. Obliczamy średnią ilość towarów w magazynie. Wynik: 90,8(3)
  3. Ustawiamy w rekordzie 5 pole ilosc na 0.
  4. Obliczamy średnią ilość towaru w magazynie. Wynik: 77,86

Jak widać, AVG() dało nam różne wyniki w zależności od tego, co mieliśmy w polu ilosc, potwierdzając tym samym naszą hipotezę. Zjawisko to bardzo łatwo wytłumaczyć. Wartość 0 traktowana jest jak normalna ilość. Zgodnie ze wzorem na średnią arytmetyczną, sumujemy sześć wartości, lecz dzielimy już przez siedem, z uwzględnieniem naszego zera. Wartość NULL wyraźnie mówi bazie danych MySQL: nie licz mnie, ja nie istnieję. Bądźmy świadomi tych różnic w działaniu, gdyż tyczą się one także pozostałych funkcji grupujących.

Ostatnimi funkcjami grupującymi, które poznamy, będą MAX(), MIN() oraz SUM(). Zwracają one kolejno: największą wartość użytą w danym polu, najmniejszą oraz sumę wszystkich wartości.

Ciekawe sztuczki

edytuj

Na sam koniec pragniemy pokazać pewną ciekawą sztuczkę, która ukaże potęgę języka SQL i być może zachęci wielu z Was do dalszego pogłębiania swej wiedzy o systemach bazodanowych.

Sytuacja prezentuje się następująco: mamy tabelę, w niej pięć pól mogących przyjmować wartości 1 lub 0. Czy da się pobrać rekordy posortowane według tego, ile pól zostało ustawionych na 1? Odpowiedź brzmi: tak.

Zaczynamy od utworzenia tabeli i wypełnienia jej danymi:

CREATE TABLE `stany` (
`id` SMALLINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`stan1` TINYINT(1) NOT NULL DEFAULT '0',
`stan2` TINYINT(1) NOT NULL DEFAULT '0',
`stan3` TINYINT(1) NOT NULL DEFAULT '0',
`stan4` TINYINT(1) NOT NULL DEFAULT '0',
`stan5` TINYINT(1) NOT NULL DEFAULT '0'
) ENGINE = MYISAM;

Następnie wypełniamy ją testowymi danymi:

INSERT INTO stany (stan1,stan2,stan3,stan4,stan5) VALUES
(1,0,1,1,0),
(0,0,1,1,0),
(0,1,1,1,1),
(1,0,0,0,1),
(0,0,0,0,0),
(0,0,1,0,0),
(1,1,1,1,1),
(0,1,1,0,1);

Możemy już zacząć zabawę:

SELECT id FROM stany ORDER BY (stan1+stan2+stan3+stan4+stan5) DESC;
+----+
| id |
+----+
|  7 |
|  3 |
|  1 |
|  8 |
|  2 |
|  4 |
|  6 |
|  5 |
+----+
8 rows in set (0.00 sec)

Zadziwiające? Nie do końca. Jeżeli dokładnie czytałeś rozdział o klauzuli ORDER BY, być może zauważyłeś, że nigdzie nie ma tam wzmianki o konieczności wymieniania pól. Jest wręcz przeciwnie - czarno na białym było tam napisane "lista wyrażeń".

Aby uczynić nasz wynik bardziej przyjaznym, spróbujmy wyświetlić obok ID sumę "włączonych" pól:

mysql> SELECT id, (stan1+stan2+stan3+stan4+stan5) AS `suma` FROM stany ORDER BY
`suma` DESC;
+----+------+
| id | suma |
+----+------+
|  7 |    5 |
|  3 |    4 |
|  1 |    3 |
|  8 |    3 |
|  2 |    2 |
|  4 |    2 |
|  6 |    1 |
|  5 |    0 |
+----+------+
8 rows in set (0.00 sec)

Czyżby kolejne zaskoczenie? Wymieniając na liście danych do zwrócenia sumę, oznaczyliśmy ją etykietą "suma". Jak się okazuje, całej operacji sumowania nie musimy później powtarzać w klauzuli ORDER BY, ani żadnej innej. Wystarczy, że odwołamy się do utworzonej wcześniej etykietki.

Opisem języka SQL można by zapełnić całą książkę. To, co podaliśmy w tym podręczniku, jest jedynie wstępem do tematu, który ma nam wystarczyć na początek przy programowaniu w PHP. Zanim jednak pokażemy, jak wykorzystać potęgę baz danych w tym języku, czeka nas jeszcze jeden rozdział poświęcony relacjom oraz indeksom.

Poprzedni rozdział: Pobieranie rekordów
Następny rozdział: Biblioteka PDO

Relacje i indeksy

edytuj

Ten rozdział jest już ostatnim na naszej drodze poznawania bazy danych MySQL. Po zapoznaniu się z relacjami oraz istotą działania indeksów, powrócimy do PHP, by nauczyć się wykorzystywać tę moc w naszych skryptach.

Indeksy

edytuj

Podczas wykonywania polecenia SELECT serwer musi wykonać bardzo dużo operacji: wybieranie danych, sortowanie wyników itd. Ponieważ przeznaczony on jest nie tylko do pracy z tak mikroskopijnymi ilościami rekordów, z jakimi mamy do czynienia (MySQL pracuje stabilnie nawet przy dziesiątkach milionów rekordów w tabeli), bardzo ważną rolę odgrywa tu optymalizacja wszelkiego rodzaju wyszukiwania oraz sortowania. Popatrzmy: rekordy ułożone są w tabeli w takiej kolejności, w jakiej zostały dodane. Oznacza to, że jeżeli próbujemy wykonać w naszej stworzonej wcześniej bazie wyszukiwanie względem ceny, MySQL musi za każdym razem na nowo przetrząsać od początku do końca wszystko, co tam się znajduje. Kiedy produktów będzie parę tysięcy, może to trwać znacznie dłużej, niż teraz. Tutaj do akcji wkraczają indeksy. Indeks nałożony na pole A jest kopią zawartości tego pola, tyle że posortowaną i odpowiednio ułożoną. Podczas robienia wszelkiego rodzaju poszukiwań względem znajdujących się w nim wartości, MySQL może teraz gwałtownie przyspieszyć. Oto przykład, co mogą nam dać posortowane rekordy: mamy tysiąc wartości posortowanych rosnąco (są one wszystkie większe od zera, górna granica nieustalona). Powiedzmy, że chcemy wiedzieć, jakie rekordy mają wartości z zakresu od 700 do 900. Jako że są one posortowane, MySQL może od razu strzelić sobie w środek tego zbioru i sprawdzić:

  1. Jeżeli wartość środkowego elementu jest mniejsza od podanego zakresu, przeszukujemy tylko późniejsze rekordy.
  2. Jeżeli wartość jest większa od podanego zakresu - tylko wcześniejsze.
  3. Jeżeli mieści się w podanym zakresie - poruszamy się jednocześnie w obu kierunkach, dopóki nie wypadniemy poza zakres.

Dzięki posortowaniu, MySQL odrzuca na wejściu całe tony rekordów, które po prostu nie pasują do kryteriów z samej definicji. Także sortowanie jest prostsze - wystarczy przejechać się po indeksie; nawet nie trzeba żadnej funkcji sortującej uruchamiać. Ceną za takie udogodnienia jest zwiększenie rozmiarów bazy, gdyż sam indeks też potrzebuje trochę miejsca. Dlatego powinniśmy się zastanowić, na których polach indeks nam będzie potrzebny, a na których nie. Ba! Po ich utworzeniu będziemy się nawet mogli przekonać, czy MySQL je wykorzystuje. Wystarczy zapytanie SELECT poprzedzić słowem EXPLAIN, a otrzymamy pełną informację diagnostyczną o jego wykonywaniu. Dodajmy indeks do istniejącej już tabeli:

ALTER TABLE `produkty` ADD INDEX (`cena`);

Zapytanie ALTER służy do modyfikowania struktury już istniejących tabel, lecz nie będziemy poświęcać mu zbyt wiele czasu. Kiedy poznamy pakiet phpMyAdmin służący do zarządzania bazami danych, będzie on wykonywać takie operacje za nas automatycznie. Na razie wystarczy nam wiedzieć, że w ten sposób stworzyliśmy indeks dla pola cena w tabeli produkty. MySQL powinien teraz korzystać z niego, kiedy np. będziemy chcieli sprawdzać przedziały cenowe:

SELECT id, nazwa FROM produkty WHERE cena BETWEEN 2 AND 10;
+----+-------------------------+
| id | nazwa                   |
+----+-------------------------+
|  1 | Dlugopisy niebieskie    |
|  2 | Dlugopisy czerwone      |
|  4 | Karteczki samoprzylepne |
|  3 | Zszywacze               |
+----+-------------------------+
4 rows in set (0.00 sec)

A oto informacje diagnostyczne o zapytaniu:

EXPLAIN SELECT id, nazwa FROM produkty WHERE cena BETWEEN 2 AND 10;
+----+-------------+----------+-------+---------------+------+---------+------+------+-------------+
| id | select_type | table    | type  | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+----------+-------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | produkty | range | cena          | cena | 4       | NULL |    3 | Using where |
+----+-------------+----------+-------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

Możemy stąd dowiedzieć się m.in., jakie klucze MySQL mógł wykorzystać (kolumna "possible_keys"), a jakich faktycznie użył (kolumna "key").

Indeksy można tworzyć także przy tworzeniu tabeli. Tak wyglądałoby polecenie utworzenia tabeli produkty, gdybyśmy od razu chcieli wprowadzić tutaj jakiś indeks:

CREATE TABLE `produkty` (
  `id` int(11) NOT NULL auto_increment,
  `nazwa` varchar(60) NOT NULL,
  `opis` text NOT NULL,
  `ilosc` smallint(6) default '0',
  `cena` float NOT NULL,
  `jakosc` tinyint(4) NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `cena` (`cena`)
) ENGINE=MyISAM;

Piszemy po prostu słowo key, po nim podajemy nazwę indeksu, a w nawiasie wymieniamy listę należących do niego pól (indeks może składać się z więcej, niż jednego pola, podobnie też w tabeli może być kilka indeksów).

Relacje jeden do wielu

edytuj

Dotychczas każda tworzona tabela funkcjonowała niezależnie od innych, lecz takie podejście nie wykorzystuje nawet połowy potęgi baz danych. Prawdziwa moc tkwi w tym, że między tabelami mogą istnieć powiązania, czyli relacje, odzwierciedlające różne zależności między danymi. Pokażemy to na przykładzie internetowej biblioteki. Mamy zbiór kategorii książek zawarty w tabeli kategorie. Przechowuje on informacje o nazwie kategorii oraz ilości dostępnych tam pozycji. W prawdziwej bazie danych biblioteki oczywiście dodalibyśmy jeszcze tutaj różne ozdabiacze, w stylu opisu kategorii, możliwości wyboru ikonki, różnych dodatkowych informacji statystycznych, lecz na razie nie w tym rzecz. Oprócz tego istnieje druga tabela, ksiazki z polami nazwa, wydawnictwo, cena itp. Jednak posiada ona coś jeszcze: pole kategoria_id, czyli ID kategorii, do której dana książka jest przypisana. W ten sposób utworzyliśmy relację pomiędzy książkami a kategoriami, a ze względu na rodzaj zależności będziemy ją nazywać relacją jeden do wielu: do jednej kategorii możemy przypisać wiele książek, ale do jednej książki może być przypisana tylko jedna kategoria. Jest to najczęściej spotykany typ relacji.

Przejdźmy do utworzenia tabel:

CREATE TABLE `kategorie` (
  `id` mediumint(9) NOT NULL auto_increment,
  `nazwa` varchar(40) NOT NULL,
  `il_ksiazek` mediumint(9) default NULL,
  PRIMARY KEY  (`id`),
  KEY `il_ksiazek` (`il_ksiazek`)
) ENGINE=InnoDB;

INSERT INTO `kategorie` VALUES (1, 'Literatura polska', 4);
INSERT INTO `kategorie` VALUES (2, 'Literatura zagraniczna', 2);

CREATE TABLE `ksiazki` (
  `id` int(11) NOT NULL auto_increment,
  `nazwa` varchar(100) NOT NULL,
  `wydawnictwo` varchar(50) NOT NULL,
  `cena` float NOT NULL default '0',
  `kategoria_id` mediumint(9) NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `kategoria_id` (`kategoria_id`)
) ENGINE=InnoDB;

INSERT INTO `ksiazki` VALUES (1, 'Hamlet', 'AAA', 6.5, 2);
INSERT INTO `ksiazki` VALUES (2, 'Makbet', 'AAA', 6.8, 2);
INSERT INTO `ksiazki` VALUES (3, 'Potop', 'BBB', 18.4, 1);
INSERT INTO `ksiazki` VALUES (4, 'Quo vadis', 'BBB', 17.99, 1);
INSERT INTO `ksiazki` VALUES (5, 'Pan Tadeusz', 'CCC', 13.78, 1);
INSERT INTO `ksiazki` VALUES (6, 'Nad Niemnem', 'CCC', 15.45, 1);

Zwróćmy uwagę na kilka rzeczy:

  1. W tabeli ksiazki dla pola kategoria_id jest utworzony indeks. Nakładanie indeksów na pola relacji jest bardzo dobrą praktyką i tutaj zysk wydajności związany z ich użyciem będzie najlepiej widoczny.
  2. Przy każdym rekordzie w tabeli ksiazki pole kategoria_id przechowuje numeryczny ID rekordu kategorii, do którego dana książka jest przypisana.

Naszą przygodę z relacjami zaczniemy od prostego przykładu: chcemy dowiedzieć się, jaka jest nazwa kategorii, do której przypisana jest książka o ID 3:

SELECT kategorie.nazwa FROM ksiazki, kategorie WHERE kategorie.id = ksiazki.kategoria_id 
AND ksiazki.id = 3;
+-------------------+
| nazwa             |
+-------------------+
| Literatura polska |
+-------------------+
1 row in set (0.02 sec)

W klauzuli FROM wymieniamy aż dwie tabele - w takim wypadku nazwa każdego pola musi być poprzedzona kropką i nazwą stosownej tabeli (dodajmy, że wtedy nie możemy brać takich nazw w odwrócone apostrofy!). Istotą całego zapytania jest ten fragment: kategorie.id = ksiazki.kategoria_id - to właśnie on wiąże ze sobą kategorię z książką.

Powyższe zapytanie można nieco uprościć, stosując aliasy. Alias tworzy się w klauzuli FROM i jest to nic innego, jak skrócona nazwa tabeli, aby zapytanie było krótsze:

SELECT ka.nazwa FROM ksiazki ks, kategorie ka WHERE ka.id = ks.kategoria_id AND ks.id = 3;
+-------------------+
| nazwa             |
+-------------------+
| Literatura polska |
+-------------------+
1 row in set (0.02 sec)

Teraz zamiast pełnej nazwy ksiazki możemy pisać ks, a zamiast kategorie - ka. Aliasy są bardzo przydatne w bardzo dużych zapytaniach, zajmujących niejednokrotnie wiele linijek. Jednak niosą one w sobie coś więcej, niż tylko poprawiają estetykę. Zagłębmy się trochę bardziej w filozofię zadeklarowania chęci użycia danej tabeli w zapytaniu. Zapis FROM ksiazki mówi, że w danym momencie przetwarzania zapytania możemy pracować maksymalnie na jednym rekordzie z podanej tabeli, do którego odnoszą się warunki podane w WHERE. Ale są sytuacje, kiedy naraz musimy pobrać dane z dwóch rekordów w jednej tabeli - aliasy są w tym wypadku konieczne, aby odróżnić jeden rekord od drugiego: FROM ksiazki ks1, ksiazki ks2. Naturalnie może się zdarzyć, że oba aliasy wskażą nam ten sam rekord, ale równie dobrze mogą być one różne, w przeciwieństwie do poprzedniej sytuacji:

SELECT ks1.nazwa AS `ks1_nazwa`, ks2.nazwa AS `ks2_nazwa` FROM ksiazki ks1, ksiazki ks2 
WHERE ks1.id = 3 AND ks2.id = 4;
+-----------+-----------+
| ks1_nazwa | ks2_nazwa |
+-----------+-----------+
| Potop     | Quo vadis |
+-----------+-----------+
1 row in set (0.00 sec)

Widać teraz bardzo wyraźnie, że zapisy ksiazki ks1 oraz ksiazki ks2 są od siebie niezależne, chyba że powiążemy je relacją (tak, możliwe jest tworzenie relacji między rekordami tej samej tabeli!). Na koniec najbardziej wymowny przykład korzystania z relacji jeden do wielu. Radzimy przyjrzeć mu się uważnie, ponieważ podobne zapytania pojawiają się naprawdę często. A mowa jest o pobraniu listy książek wraz z nazwą kategorii, do której są przypisane.

SELECT ks.id, ks.nazwa, ks.wydawnictwo, ks.cena, ka.nazwa AS `kat_nazwa`
FROM ksiazki ks, kategorie ka WHERE ka.id = ks.kategoria_id ORDER BY ks.cena;
+----+-------------+-------------+-------+------------------------+
| id | nazwa       | wydawnictwo | cena  | kat_nazwa              |
+----+-------------+-------------+-------+------------------------+
|  1 | Hamlet      | AAA         |   6.5 | Literatura zagraniczna |
|  2 | Makbet      | AAA         |   6.8 | Literatura zagraniczna |
|  5 | Pan Tadeusz | CCC         | 13.78 | Literatura polska      |
|  6 | Nad Niemnem | CCC         | 15.45 | Literatura polska      |
|  4 | Quo vadis   | BBB         | 17.99 | Literatura polska      |
|  3 | Potop       | BBB         |  18.4 | Literatura polska      |
+----+-------------+-------------+-------+------------------------+
6 rows in set (0.00 sec)

Relacje wiele do wielu

edytuj

Kolejny typ relacji, którym się zajmiemy, nosi nazwę wiele-do-wielu. Jest on dokładnie tym, co mówi nazwa. Patrząc na naszą księgarnię, możemy nim przypisać autorów do książek, co jest zgodne z intuicją: książka mogła być napisana przez wielu autorów, a jednocześnie każdy autor mógł napisać wiele książek.

Zacznijmy od stworzenia tabeli z autorami:

CREATE TABLE `autorzy` (
`id` SMALLINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`imie` VARCHAR(30) NOT NULL,
`nazwisko` VARCHAR(40) NOT NULL 
) ENGINE = InnoDB;

INSERT INTO `autorzy` (`imie` , `nazwisko`) VALUES
('William', 'Shakespeare'),
('Henryk', 'Sienkiewicz'),
('Adam', 'Mickiewicz'),
('Eliza', 'Orzeszkowa'),
('Jan', 'Kowalski');

Zauważmy, że w relacji wiele-do-wielu nasze tabele nie zawierają pól-kluczy, które mogłyby je spinać. Bynajmniej nie znikają one, a wręcz przeciwnie - istnieją, tyle że w trzeciej tabeli pomocniczej:

CREATE TABLE `autorzy_to_ksiazki` (
`ksiazka_id` INT NOT NULL,
`autor_id` INT NOT NULL,
`udzial` VARCHAR(16) NOT NULL,
INDEX (`ksiazka_id`, `autor_id`) 
) ENGINE = InnoDB;

INSERT INTO `autorzy_to_ksiazki` VALUES (1, 1, 'autor');
INSERT INTO `autorzy_to_ksiazki` VALUES (1, 5, 'tłumacz');
INSERT INTO `autorzy_to_ksiazki` VALUES (2, 1, 'autor');
INSERT INTO `autorzy_to_ksiazki` VALUES (2, 5, 'tłumacz');
INSERT INTO `autorzy_to_ksiazki` VALUES (3, 2, 'autor');
INSERT INTO `autorzy_to_ksiazki` VALUES (4, 2, 'autor');
INSERT INTO `autorzy_to_ksiazki` VALUES (5, 3, 'autor');
INSERT INTO `autorzy_to_ksiazki` VALUES (6, 4, 'autor');

Relacja wiele-do-wielu może przenosić dodatkowe informacje w tabeli pomocniczej, np. w tym przypadku mówi nam także, czy osoba przypisana do danej książki jest autorem, czy tłumaczem. Teraz, kiedy tabele są już gotowe, możemy pokazać parę przykładów obrazujących korzystanie z tej relacji.

Zaczniemy od prostego wybrania wszystkich autorów "Hamleta":

SELECT a.imie, a.nazwisko, ak.udzial FROM autorzy a, autorzy_to_ksiazki ak 
WHERE a.id=ak.autor_id AND ak.ksiazka_id=1;
+---------+-------------+----------+
| imie    | nazwisko    | udzial   |
+---------+-------------+----------+
| William | Shakespeare | autor    |
| Jan     | Kowalski    | tlumacz  |
+---------+-------------+----------+
2 rows in set (0.00 sec)

Autora z książką łączy nam ten warunek: a.id=ak.autor_id AND ak.ksiazka_id=1. Na liście pól pobieramy imię i nazwisko z tabeli autorzy z dołączoną informacją o udziale przy opracowywaniu akurat tej pozycji: ak.udzial. Bierzemy ją z tabeli pomocniczej.

W podobny sposób można dowiedzieć się, przy jakich książkach pracował podany autor (np. Jan Kowalski).

SELECT k.nazwa, ak.udzial FROM ksiazki k, autorzy_to_ksiazki ak WHERE k.id=ak.ksiazka_id 
AND ak.autor_id=5;
+--------+----------+
| nazwa  | udzial   |
+--------+----------+
| Hamlet | tlumacz  |
| Makbet | tlumacz  |
+--------+----------+
2 rows in set (0.00 sec)

Zapytanie to działa identycznie, jak poprzednie. Podstawiliśmy do niego jedynie inną tabelę. Teraz może coś bardziej zaawansowanego. Dowiedzmy się, kto pracował dla nas jako autor i przy jakich książkach:

SELECT a.imie, a.nazwisko, k.nazwa FROM autorzy a, ksiazki k, autorzy_to_ksiazki ak 
WHERE a.id = ak.autor_id AND k.id = ak.ksiazka_id AND ak.udzial='autor';
+---------+-------------+-------------+
| imie    | nazwisko    | nazwa       |
+---------+-------------+-------------+
| William | Shakespeare | Hamlet      |
| William | Shakespeare | Makbet      |
| Henryk  | Sienkiewicz | Potop       |
| Henryk  | Sienkiewicz | Quo vadis   |
| Adam    | Mickiewicz  | Pan Tadeusz |
| Eliza   | Orzeszkowa  | Nad Niemnem |
+---------+-------------+-------------+
6 rows in set (0.00 sec)

Relacje wiele-do-wielu pojawiają się w bardziej złożonych bazach danych, które muszą odzwierciedlać dużą liczbę zależności. Są bardzo wszechstronnym narzędziem i ich znajomość jest wręcz niezbędna.

Optymalizacja

edytuj

Im większe zapytanie z większą ilością skomplikowanych instrukcji, tym wolniej jest wykonywane. Dlatego dobra struktura bazy danych nie tylko musi być elastyczna, ale też zoptymalizowana w taki sposób, aby jak najwięcej danych dało się pobrać prostymi zapytaniami, korzystającymi z elementarnych funkcji. Brak tej cechy jest mankamentem baz projektowanych nie tylko przez początkujących, ale i wielu zaawansowanych programistów, co negatywnie rzutuje na wydajność ich aplikacji.

Pamiętajmy, że wolna przestrzeń dyskowa jest aktualnie najtańsza w historii ludzkości, a dane liczbowe zajmują śmiesznie małą jej ilość. Podstawową zasadą jest właśnie buforowanie tego, co da się buforować, a pokażemy to na przykładzie systemu newsów z możliwością komentowania. W systemie takim mamy dwie tabele: newsy oraz komentarze połączone relacją jeden-do-wielu. Zazwyczaj przy wyświetlaniu newsów pragniemy podać też, ile jest w nich komentarzy. Pobranie takich informacji jednym zapytaniem wymaga sprytnego zastosowania funkcji grupującej COUNT(), której użycie w relacjach jest obwarowane paroma ograniczeniami. Jednak to nie wszystko. Kiedy tabele zepniemy "sztywno" w warunku WHERE, na liście pokażą się nam tylko te newsy, w których ktoś już dodał jakiś komentarz! Dlatego musimy zastosować specjalną klauzulę służącą do warunkowego spinania tabel: LEFT JOIN. Jest ona użyteczna, ale dość powolna w działaniu. Ostatecznie otrzymujemy coś takiego:

SELECT n.id, n.tytul, COUNT(k.id) AS `il_komentarzy` FROM newsy n LEFT JOIN komentarze k 
ON k.news_id=n.id GROUP BY k.news_id ORDER BY n.id;

Zapytanie to było wykorzystywane praktycznie na stronie WWW autora podręcznika. Działa, lecz przy większej ilości rekordów zaczynają się problemy w stylu zrywania połączenia czy nawet blokowania serwera DB. Naprawdę, nie życzymy nikomu, aby znalazł się w podobnej sytuacji, zwłaszcza że problemowi można zaradzić w bardzo prosty sposób. Dlaczego bowiem zliczać ilość komentarzy przy każdym wyświetlaniu strony? Przecież liczba ta nie zmienia się tak znowu często. Dorzućmy do tabeli newsy pole il_komentarzy. Przy dodawaniu newsa inicjujemy je wartością 0. Przy dodawaniu komentarza zwiększamy jego wartość o 1, przy usuwaniu - zmniejszamy. W ten sposób uzyskaliśmy takie zapytanie końcowe:

SELECT id, tytul, il_komentarzy FROM newsy ORDER BY id;

Wykorzystuje ono elementarną składnię, przez co może wytrzymać znacznie większe obciążenie. Tego typu optymalizacja spotykana jest w wielu zaawansowanych aplikacjach, np. systemie forów dyskusyjnych Invision Power Board, który w ten sposób zapamiętuje sobie ilości postów oraz tematów.

O kolejnym rodzaju optymalizacji już wspominaliśmy - jest to prawidłowe użycie indeksów. Zakładamy je wszędzie tam, gdzie dane będą sortowane lub wybierane według różnych kryteriów. Indeksy zawierają informacje ułożone w pewnym porządku, dlatego po ich wstawieniu w takie miejsca MySQL wykorzysta je do przyspieszenia całego procesu.

Optymalizację można przeprowadzać także z poziomu języka programowania, za pomocą którego komunikujemy się z bazą. Jeżeli nasze dane zmieniają się niezbyt często, nie ma potrzeby pobierania ich na nowo za każdym razem. Wystarczy raz pobrane zapisać gdzieś na serwerze w pliku i przy dalszych wejściach czytać właśnie z niego. Jest to jednak optymalizacja typowo programowa, dlatego zajmiemy się nią później.

Zakończenie

edytuj

Na tym kończymy naszą przygodę z poznawaniem języka SQL. Jeżeli pragniesz poznać go lepiej, bardzo przydatna okaże się z pewnością dokumentacja serwera MySQL. Poświęć trochę czasu na zapoznanie się z jej rozbudowaną strukturą, gdyż jest to istna kopalnia cennych informacji. Przydatne może być także analizowanie cudzych skryptów oraz własne poszukiwania z użyciem Google. Każdy sposób jest dobry, a znajomości SQL-a nigdy za wiele.

Poprzedni rozdział: Relacje i indeksy
Następny rozdział: ORM i biblioteka Doctrine

Biblioteka PDO

edytuj

Kiedyś programiści pragnący komunikować się z bazą danych poprzez PHP musieli zmagać się z wieloma problemami. Każdy serwer DB udostępniał inne API do komunikacji, które zostały na nasze nieszczęście wiernie odtworzone w interpreterze. Jeżeli ktoś chciał napisać elastyczny projekt do uruchamiania na kilku bazach, musiał pisać samodzielnie odpowiednie nakładki, które wybiorą odpowiednią funkcję w zależności od tego, czym się łączymy. Pozostawały też różne gotowe skrypty robiące to zadanie za nas.

PDO to skrót od PHP Data Objects. Jest to nowoczesny interfejs języka PHP przeznaczony do komunikacji z bazami danych, po raz pierwszy napisany wyłącznie w OOP. Jego najważniejszą zaletą jest to, że możemy za jego pomocą łączyć się zarówno z bazą danych MySQL, jak i z bazą danych PostgreSQL (o innych systemach DB nie wspominając). Wersji beta PDO można było używać już w PHP 5.0, natomiast stabilna wersja pojawiła się wraz z PHP 5.1. Gorąco zachęcamy do jego stosowania, gdyż nie tylko jest wygodniejszy od starych rozwiązań, ale też szybszy i bezpieczniejszy - do tego zagadnienia niedługo wrócimy.

Nawiązywanie połączenia

edytuj

Aby nie być gołosłownym, przejdźmy od słów do czynów. Połączymy się z naszą bazą utworzoną podczas wcześniejszych lekcji:

<?php

   try
   {
      $pdo = new PDO('mysql:host=localhost;dbname=produkty', 'root', 'root');
      echo 'Połączenie nawiązane!';
   }
   catch(PDOException $e)
   {
      echo 'Połączenie nie mogło zostać utworzone: ' . $e->getMessage();
   }
?>

Nawiązywanie połączenia polega po prostu na utworzeniu obiektu klasy PDO. Jako parametry startowe podajemy:

  • DSN - specjalny ciąg znaków identyfikujący rodzaj serwera DB (np. mysql), host na jakim jest ona uruchomiona (dla nas localhost) oraz nazwę bazy, z którą chcemy się połączyć. Opcjonalnie można dodać także parametr port(przykład poniżej) Inne serwery DB mogą wymagać innych parametrów połączeń; po szczegóły odsyłamy do dokumentacji PHP.
<?php
$mysql_host = 'localhost'; //lub jakiś adres: np sql.nazwa_bazy.nazwa.pl
$port = '3307'; //domyślnie jest to port 3306
$username = 'login';
$password = 'hasło';
$database = 'nazwa_bazy'; //'produkty'

try{
	$pdo = new PDO('mysql:host='.$mysql_host.';dbname='.$database.';port='.$port, $username, $password );
	echo 'Połączenie nawiązane!';
}catch(PDOException $e){
	echo 'Połączenie nie mogło zostać utworzone.<br />';
}
?>
  • nazwa użytkownika
  • hasło użytkownika

Host, nazwę użytkownika i hasło powinieneś dostać od swojego hostingu, kiedy będziesz chciał umieścić swoją stronę. Jeżeli podczas nawiązywania połączenia wystąpi błąd, zostanie on zgłoszony jako wyjątek PDOException, który musimy przechwycić (to ważne - jeśli wyjątek nie zostanie przechwycony, domyślny komunikat o błędzie wygenerowany przez PHP ujawni nazwę użytkownika i nazwę bazy!).

Chcąc ustawić od razu system porównań dla bazy danych wystarczy użyć takiego połączenia:

<?php

  try
   {
      $pdo = new PDO('mysql:host=localhost;dbname=produkty;charset=utf8', 'root', 'root');
      echo 'Połączenie nawiązane!';
   }
   catch(PDOException $e)
   {
      echo 'Połączenie nie mogło zostać utworzone: ' . $e->getMessage();
   }
?>

W miejsce utf8 wstaw swoje kodowanie i voila! Już nie musisz pamiętać o zmianie kodowania na UTF-8 dla przykładu. W ten sposób można przekazać jeszcze inne zapytania już zaraz po połączeniu.

Pobieranie danych

edytuj

Pobieranie danych w sterownikach baz danych realizuje się w następujący sposób: najpierw wysyłamy zapytanie i uzyskujemy od serwera zbiór wyników. Przelatując po nim pętlą, otrzymujemy kolejne rekordy w postaci tablic. W bibliotece PDO zbiorem wyniku jest obiekt klasy PDOStatement. Naraz możemy mieć otwarty tylko jeden zbiór wyników. Zabezpiecza to przed próbami tworzenia zapytań rekurencyjnych oraz wynika ze specyfiki pracy bibliotek komunikujących się z serwerem. W starszych bibliotekach ograniczenia takiego nie było dzięki emulacji, która jednak zmniejszała wydajność. Przyjrzyjmy się, jak możemy pobrać zawartość tabeli produkty:

<?php

   try
   {
      $pdo = new PDO('mysql:host=localhost;dbname=produkty', 'root', 'root');
      $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
      
      $stmt = $pdo->query('SELECT id, nazwa, opis FROM produkty');
      echo '<ul>';
      foreach($stmt as $row)
      {
          echo '<li>'.$row['nazwa'].': '.$row['opis'].'</li>';
      }
      $stmt->closeCursor();
      echo '</ul>';
   }
   catch(PDOException $e)
   {
      echo 'Połączenie nie mogło zostać utworzone: ' . $e->getMessage();
   }
?>

Metoda query() zwraca obiekt zbioru wyników odpowiadający wykonanemu zapytaniu (zauważ, że w tym przypadku nie kończymy go średnikiem!). Jedną z technik uzyskania kolejnych rekordów jest przepuszczenie tego obiektu przez pętlę foreach. Kolejne rekordy zostaną zapisane do tablicy asocjacyjnej $row, z której możemy pobrać wyniki. Po zakończeniu pobierania niezbędne jest zamknięcie zbioru wyników poleceniem closeCursor() - inaczej nie będziemy w stanie wysłać następnego zapytania.

Zauważ, że zaraz po połączeniu się z bazą danych korzystamy z metody setAttribute(). Pozwala ona skonfigurować niektóre aspekty pracy z biblioteką PDO - w tym przypadku żądamy, aby ewentualne błędy w zapytaniach raportowane były jako wyjątki.

Powyższy przykład można zapisać także w inny sposób:

<?php

   try
   {
      $pdo = new PDO('mysql:host=localhost;dbname=produkty', 'root', 'root');
      $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
      
      $stmt = $pdo -> query('SELECT id, nazwa, opis FROM produkty');
      echo '<ul>';
      while($row = $stmt->fetch())
      {
          echo '<li>'.$row['nazwa'].': '.$row['opis'].'</li>';
      }
      $stmt->closeCursor();
      echo '</ul>';
   }
   catch(PDOException $e)
   {
      echo 'Połączenie nie mogło zostać utworzone: ' . $e->getMessage();
   }
?>

W tym wypadku wykorzystaliśmy pętlę while i jawnie zażądaliśmy zwrócenia rekordu metodą fetch(). Jest ona, wbrew pozorom bardzo użyteczna - można ją wywołać wszędzie, np. w instrukcji if (sytuacja, gdy zawsze pobieramy jeden rekord), a także ustawić tryb pobierania.

Uwaga! PHP Data Objects ma czasem problemy z działaniem z MySQL 4.1. Aby uniknąć problemów na tej wersji (sporo firm hostingowych wciąż ją oferuje), musisz pamiętać o tym, aby po zamknięciu zbioru wyników metodą closeCursor() dodatkowo ręcznie skasować obiekt $stmt:

unset($stmt);

Inaczej próba przypisania do niej nowego zbioru wyników spowoduje wygenerowanie przez MySQL komunikatu General Error 2050.

Aktualizacja danych

edytuj

Zapytania typu INSERT czy UPDATE służące do modyfikacji zawartości bazy lub inne, niezwracające zbioru wyników, wysyła się za pomocą metody exec(). Wynikiem jej działania jest liczba określająca liczbę zmodyfikowanych rekordów. W poniższym przykładzie zakodujemy dodawanie pewnego konkretnego produktu do naszej listy produktów:

<?php

	try
	{
		$pdo = new PDO('mysql:host=localhost;dbname=produkty;port=3305', 'root', 'root');
		$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

		$liczba = $pdo->exec('INSERT INTO `produkty` (`nazwa`, `opis`, `ilosc`, `cena`, `jakosc`)	VALUES(
			\'Miotacz ognia na dezodorant\',
			\'Rewelacyjny miotacz ognia dla kazdej domowej gospodyni!
			Nie martw sie o paliwo - wystarczy zwykly dezodorant!\',
			\'54\',
			\'40.99\',
			\'5\')');

		if($liczba > 0)
		{
			echo 'Dodano: '.$liczba.' rekordow';
		}
		else
		{
			echo 'Wystąpił błąd podczas dodawania rekordów!';
		}
	}
	catch(PDOException $e)
	{
		echo 'Wystąpił błąd biblioteki PDO: ' . $e->getMessage();
	}
?>

Podpinanie

edytuj

Rzadko kiedy zdarza się, aby wszystkie informacje potrzebne do zmodyfikowania bazy były na sztywno zakodowane w zapytaniu tak, jak to zrobiliśmy w powyższym przykładzie. W codziennej praktyce modyfikujemy dane za pomocą formularzy, ikonek podpowiadających, co trzeba zmienić i jak. Rozwiązanie jest pozornie banalne: składamy zapytanie z predefiniowanych części, którymi opakowujemy dane z formularza, a później wysyłamy ten miks do bazy. Ilustruje to kolejny przykład, który udostępnia prosty formularz do dodawania nowych produktów:

<?php

	try
	{
		if($_SERVER['REQUEST_METHOD'] == 'POST')
		{	
			$pdo = new PDO('mysql:host=localhost;dbname=produkty;port=3305', 'root', 'root');
	
			$ilosc = $pdo -> exec('INSERT INTO `produkty` (`nazwa`, `opis`, `ilosc`, `cena`, `jakosc`)	VALUES(
				\''.$_POST['nazwa'].'\',
				\''.$_POST['opis'].'\',
				\''.$_POST['ilosc'].'\',
				\''.$_POST['cena'].'\',
				\''.$_POST['jakosc'].'\')');
	
			if($ilosc > 0)
			{
				echo 'Pomyślnie dodano: '.$ilosc.' rekordów';
			}
			else
			{
				echo 'Wystąpił błąd podczas dodawania rekordów!';
			}
		}
		else
		{
			echo '
			<form method="post" action="pdo_5.php">
			<p>Nazwa: <input type="text" name="nazwa"/></p>
			<p>Opis: <input type="text" name="opis"/></p>
			<p>Ilosc: <input type="text" name="ilosc"/></p>
			<p>Cena: <input type="text" name="cena"/></p>
			<p>Jakosc: <select name="jakosc">
			<option value="1">1</option>
			<option value="2">2</option>
			<option value="3">3</option>
			<option value="4">4</option>
			<option value="5">5</option>
			<option value="6">6</option>
			</select></p>
			<p><input type="submit" value="Dodaj"/></p>
			</form>
			';	
		}
	}
	catch(PDOException $e)
	{
		echo 'Wystapił blad biblioteki PDO: ' . $e->getMessage();
	}
?>

Kiedy został nadesłany formularz (metoda POST), nawiązywane jest połączenie z bazą. W szkielet zapytania wstawiamy wprowadzone przez użytkownika dane, wykorzystując zwyczajny operator łączenia ciągów. Później metoda exec() umieszcza nam nowy rekord w bazie. Na pierwszy rzut oka wszystko wygląda wspaniale - mamy formularz, redaktorzy mogą dodawać produkty, a internauci je oglądać. Lecz pewnego dnia jeden z redaktorów zgłasza problem: nie może wpisać do opisu produktu apostrofy, gdyż skrypt generuje wtedy jakieś tajemnicze błędy. Co jest grane? Testujemy kopię skryptu na lokalnym komputerze i działa, ale na właściwym serwerze WWW już nie. Przyczyną problemu jest złamanie dwóch podstawowych zasad obsługi formularzy:

  • Nigdy nie ufaj danym zewnętrznym
  • Nigdy nie ufaj magic quotes

Efekt jest taki, że stworzyliśmy aplikację podatną na włamania typu SQL Injection, które polegają na wykorzystaniu dziur w kontroli danych z formularzy. Zauważ, jak bezbronny jest nasz formularz: baza danych wymaga, aby ilość była liczbą. Gdzie to sprawdzamy w skrypcie? Nigdzie. Gdy jakiś inteligent wpisze nam zamiast ilości "miecio jest niepoważny", skrypt beztrosko umieści to w zapytaniu nie patrząc na sens tego, co robi. Co więcej, zwróć uwagę na pewną rzecz: w języku SQL znak apostrofu jest czymś więcej, niż tylko znakiem - kończy on lub zaczyna sekwencję ciągu tekstowego. Dlatego wprowadzając jakikolwiek tekst, który ma zawierać apostrofy, musimy poddać je zabiegowi escapingu, czyli mówiąc po polsku - poprzedzić znakiem backslash, aby MySQL wiedział, że są one integralną częścią tekstu i nie kończą wprowadzanej sekwencji. Kiedy PHP był jeszcze niewielkim projektem, ktoś wpadł na pomysł wspomożenia programistów i wymyślił tzw. magic quotes. Opcja ta, jeżeli jest włączona, powoduje, że we wszystkich danych z tablic $_GET, $_POST oraz $_COOKIE apostrofy są automatycznie poprzedzane backslashem, dzięki czemu nie trzeba się tym zajmować samodzielnie. Brzmi ciekawie? Niezupełnie! Zwróćmy uwagę, że nie tylko baza danych może służyć do przechowywania informacji. Niektóre z nich ktoś zechce umieścić w pliku i wtedy z kolei musi się sam tych niepotrzebnych backslashów pozbywać. Kolejną kontrowersyjną rzeczą dotyczącą magic quotes jest fakt, że nie wszystkie serwery miały tę opcję włączoną, tak samo nie wszyscy programiści wiedzieli, że coś takiego w ogóle istnieje. Czy widziałeś w sieci serwisy, gdzie apostrofy w artykułach poprzedzane były setkami backslashów? To właśnie efekt tego - programista miał u siebie w domu wyłączoną opcję magic quotes, więc ręcznie dodawał sobie backslashe przy danych umieszczanych w zapytaniach SQL. Później wrzucił skrypt na serwer, gdzie magic quotes dla odmiany było włączone, przez co backslashe doklejane były dwa razy - jeden z nich faktycznie escape'ował apostrofy, ale drugi był uznawany przez MySQL za integralną część tekstu. Z drugiej strony, jeśli ktoś miał w domu serwer lokalny z włączonymi magicznymi apostrofami, a później wrzucił swoja stronę WWW na serwer bez nich, stawał się łatwym celem dla hackerów, którzy bez trudu mogą włamać się atakiem SQL Injection. Atak ten polega na tym, że skoro apostrof nie jest escape'owany, to jego wprowadzenie tak naprawdę powoduje, że dalsza część ciągu jest uznawana za fragment zapytania! Możemy więc sobie zupełnie legalnie dopisać własne warunki. Wyobraźmy sobie teraz, że ktoś manipuluje w ten sposób zapytaniami związanymi z bezpieczeństwem za pomocą formularza logowania. To nie fikcja: do wyobraźni powinien przemówić ten film (j. ang).

Ostatecznie sami twórcy PHP doszli do wniosku, że magic quotes jest rozwiązaniem bezsensownym. W tworzonym właśnie PHP 6 tej opcji już nie ma i należy samodzielnie escape'ować wszystkie dane. Jednak póki co pracujemy na PHP 5.1 - choć w tym podręczniku podczas instalacji zalecaliśmy wyłączenie magic quotes, nie mamy pewności, że serwer docelowy dla naszych stron WWW posiada identyczne ustawienia. Jeśli korzystamy z PDO i mechanizmu podpinania, problem nas nie dotyczy, ponieważ biblioteka automatycznie dostosuje się wtedy do ustawień, lecz w każdym innym przypadku powinniśmy zastosować specjalny filtr, który zniweluje nam efekt niewłaściwych ustawień i nada danym pożądaną przez nasz skrypt postać.

<?php
	if(version_compare(phpversion(), '6.0.0-dev', '<'))
	{
		// Dla PHP 5 i wcześniejszych wyłączmy magic quotes

		function removeSlashes(&$value){
			if(is_array($value))
			{
				return array_map('removeSlashes', $value);
			}
			else
			{
				return stripslashes($value);
			}
		} // end rmGpc();

		set_magic_quotes_runtime(0);
		
		if(get_magic_quotes_gpc())
		{
			$_POST = array_map('removeSlashes', $_POST);
			$_GET = array_map('removeSlashes', $_GET);
			$_COOKIE = array_map('removeSlashes', $_COOKIE);	
		}
	}

?>

Przejdźmy teraz do właściwego tematu niniejszej sekcji. Skoro magic quotes jest wyłączone, rozsądek podpowiada, że dane musimy sami escape'ować. W starych rozszerzeniach do komunikacji z bazą danych służyły do tego specjalne funkcje udostępniane przez sterownik, przez które musieliśmy przepuścić wszystkie dane - nadal jednak konieczne było samodzielne spajanie tego z zapytaniem. PDO promuje filozofię przeniesienia tego zadania na bazę danych, co udostępniają najnowsze biblioteki komunikacji z serwerami DB. W języku polskim proces ten doczekał się niezbyt szczęśliwej nazwy bindowanie od angielskiego określenia data binding, jednak w tym podręczniku będziemy konsekwentnie stosować termin podpinanie, naszym zdaniem znacznie lepiej oddający jego charakter.

Podpinanie polega na przeniesieniu spajania danych z zapytaniem z języka programowania na serwer DB. Do bazy wysyłamy tutaj tak naprawdę szkielet zapytania ze specjalnymi wstawkami, do których później podpinamy interesujące nas dane za pomocą specjalnej metody, gdzie możemy dodatkowo określić ich typ (tekst, liczba itd.). Zobaczmy, jak wygląda podany na początku przykład przepisany z wykorzystaniem podpinania:

<?php

	try
	{
		if($_SERVER['REQUEST_METHOD'] == 'POST')
		{	
			$pdo = new PDO('mysql:host=localhost;dbname=produkty;port=3305', 'root', 'root');
			$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
	
			$stmt = $pdo -> prepare('INSERT INTO `produkty` (`nazwa`, `opis`, `ilosc`, `cena`, `jakosc`)	VALUES(
				:nazwa,
				:opis,
				:ilosc,
				:cena,
				:jakosc)');	// 1
			
			$stmt -> bindValue(':nazwa', $_POST['nazwa'], PDO::PARAM_STR); // 2
			$stmt -> bindValue(':opis', $_POST['opis'], PDO::PARAM_STR);
			$stmt -> bindValue(':ilosc', $_POST['ilosc'], PDO::PARAM_INT);
			$stmt -> bindValue(':cena', (float)$_POST['cena'], PDO::PARAM_STR);
			$stmt -> bindValue(':jakosc', $_POST['jakosc'], PDO::PARAM_INT);
			
			$ilosc = $stmt -> execute(); // 3
	
			if($ilosc > 0)
			{
				echo 'Dodano: '.$ilosc.' rekordow';
			}
			else
			{
				echo 'Wystapil blad podczas dodawania rekordow!';
			}
		}
		else
		{
			echo '
			<form method="post" action="pdo_6.php">
			<p>Nazwa: <input type="text" name="nazwa"/></p>
			<p>Opis: <input type="text" name="opis"/></p>
			<p>Ilosc: <input type="text" name="ilosc"/></p>
			<p>Cena: <input type="text" name="cena"/></p>
			<p>Jakosc: <select name="jakosc">
			<option value="1">1</option>
			<option value="2">2</option>
			<option value="3">3</option>
			<option value="4">4</option>
			<option value="5">5</option>
			<option value="6">6</option>
			</select></p>
			<p><input type="submit" value="Dodaj"/></p>
			</form>
			';	
		}
	}
	catch(PDOException $e)
	{
		echo 'Wystapil blad biblioteki PDO: ' . $e->getMessage();
	}
?>

Opis:

  1. Na początek wysyłamy do bazy danych szkielet zapytania, wykorzystując metodę prepare(). Zamiast danych, umieszczamy w ich miejscu wstawki, np. :nazwa, :opis. Jako rezultat otrzymujemy obiekt klasy PDOStatement, który wykorzystamy do podpięcia danych.
  2. Tutaj podpinamy dane z formularza pod konkretne wstawki metodą bindValue() obiektu PDOStatement. Określamy także ich typ: stała PDO::PARAM_STR określa podpinanie danych tekstowych, PDO::PARAM_INT - liczb całkowitych.
  3. Właściwe wykonanie zapytania metodą execute().

Podpinanie jest odporne na ataki SQL Injection. MySQL ma jasno określone, co jest danymi, a co zapytaniem i ściśle się tego trzyma. Ponadto jest także wydajniejsze, niż samodzielne spinanie wszystkiego po stronie PHP.

Szczególnie ciekawa właściwość podpinania polega na możliwości podpięcia kilku zestawów danych do tego samego szkieletu zapytania, dzięki czemu wydajność wzrasta jeszcze bardziej. Zademonstruje to poniższy przykład, w którym rozszerzyliśmy nasz formularz tak, aby naraz można nim było wprowadzać kilka produktów. Gdy zostanie on wysłany, połączymy się z MySQL'em, przekazując szkielet naszego zapytania. Następnie będziemy podpinali do niego dane kolejnych produktów i wykonywali.

<?php

	try
	{
		if($_SERVER['REQUEST_METHOD'] == 'POST')
		{	
			$pdo = new PDO('mysql:host=localhost;dbname=produkty;port=3305', 'root', 'root');
			$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
	
			$stmt = $pdo -> prepare('INSERT INTO `produkty` (`nazwa`, `opis`, `ilosc`, `cena`, `jakosc`)	VALUES(
				:nazwa,
				:opis,
				:ilosc,
				:cena,
				:jakosc)'); // 1
			
			$ilosc = 0;
			foreach($_POST['produkty'] as $produkt)
			{
				if(strlen($produkt['nazwa']) > 0)
				{
					$stmt -> bindValue(':nazwa', $produkt['nazwa'], PDO::PARAM_STR); // 2
					$stmt -> bindValue(':opis', $produkt['opis'], PDO::PARAM_STR);
					$stmt -> bindValue(':ilosc', $produkt['ilosc'], PDO::PARAM_INT);
					$stmt -> bindValue(':cena', (float)$produkt['cena'], PDO::PARAM_STR);
					$stmt -> bindValue(':jakosc', $produkt['jakosc'], PDO::PARAM_INT);
					
					$ilosc += $stmt -> execute(); // 3
				}			
			}
	
			if($ilosc > 0)
			{
				echo 'Dodano: '.$ilosc.' rekordow';
			}
			else
			{
				echo 'Wystapil blad podczas dodawania rekordow!';
			}
		}
		else
		{
			echo '<form method="post" action="pdo_7.php">';
			for($i = 1; $i <= 4; $i++)
			{
				echo '<hr/>
					<p>Nazwa: <input type="text" name="produkty['.$i.'][nazwa]"/></p>
					<p>Opis: <input type="text" name="produkty['.$i.'][opis]"/></p>
					<p>Ilosc: <input type="text" name="produkty['.$i.'][ilosc]"/></p>
					<p>Cena: <input type="text" name="produkty['.$i.'][cena]"/></p>
					<p>Jakosc: <select name="produkty['.$i.'][jakosc]">
					<option value="1">1</option>
					<option value="2">2</option>
					<option value="3">3</option>
					<option value="4">4</option>
					<option value="5">5</option>
					<option value="6">6</option>
					</select></p>';
			}
			echo '<p><input type="submit" value="Dodaj"/></p></form>';
		}
	}
	catch(PDOException $e)
	{
		echo 'Wystapil blad biblioteki PDO: ' . $e->getMessage();
	}
?>

Opis:

  1. Szkielet zapytania wysyłany tylko raz.
  2. Zestawy danych ładowane są w pętli.
  3. W pętli wykonujemy też metodę execute().

Podpinanie nie ogranicza się tylko do zapytań typu INSERT. Z powodzeniem można stosować je także przy SELECT. Napiszemy teraz skrypt wyświetlający listę produktów oraz umożliwiający nam zobaczenie szczegółów każdego z nich. Dlatego do drugiego zapytania, pobierającego szczegółowe informacje, musimy podpiąć ID produktu, który chcemy obejrzeć.

<?php

	try
	{
		$pdo = new PDO('mysql:host=localhost;dbname=produkty;port=3305', 'root', 'root');
		$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
			
		$stmt = $pdo -> query('SELECT id, nazwa FROM produkty ORDER BY id');
		echo '<ul>';
		while($row = $stmt -> fetch())
		{
			echo '<li><a href="pdo_8.php?id='.$row['id'].'">'.$row['nazwa'].'</a></li>';		
		}
		$stmt -> closeCursor();
		echo '</ul>';
		
		if(isset($_GET['id'])) // 1
		{
			$stmt = $pdo -> prepare('SELECT `nazwa`, `opis`, `ilosc`, `cena`, `jakosc` FROM `produkty` WHERE `id` = :id'); // 2
			$stmt -> bindValue(':id', $_GET['id'], PDO::PARAM_INT);
			$stmt -> execute(); // 3
			
			if($details = $stmt -> fetch()) // 4
			{
				echo '<hr/>
				<p><b>Nazwa:</b> '.$details['nazwa'].'</p>
				<p><b>Opis:</b> '.$details['opis'].'</p>
				<p><b>Ilosc:</b> '.$details['ilosc'].'</p>
				<p><b>Cena:</b> '.$details['cena'].'</p>
				<p><b>Jakosc:</b> '.$details['jakosc'].'</p>';			
			}
			else
			{
				echo '<hr/><p>Przepraszamy, podany rekord nie istnieje!</p>';
			}
			$stmt -> closeCursor();
		}
	}
	catch(PDOException $e)
	{
		echo 'Wystapil blad biblioteki PDO: ' . $e->getMessage();
	}
?>

Opis:

  1. Oczywiście wyświetlanie szczegółów przeprowadzamy tylko, jeśli podaliśmy ID.
  2. Przygotowujemy szkielet zapytania SELECT.
  3. Wykonujemy zapytanie metodą execute(). Zauważmy, że obiektem $stmt dysponujemy już od momentu wywołania metody prepare(), dlatego execute() nam już nic tu nie zwraca.
  4. Dalej postępujemy już tradycyjnie, po prostu pobierając kolejne rekordy (w tym wypadku tylko jeden) i zamykając kursor.

Jedyną wadą podpinania jest wydłużenie kodu PHP. Jeśli dotychczas wysyłaliśmy zapytanie DELETE zwyczajnie wykonując metodę exec(), teraz musimy to rozpisać na kilka linijek. Jednak jeszcze w tym rozdziale poznamy nakładkę na PDO zwaną Open Power Driver, dzięki której kod z powrotem stanie się krótki i czytelny.

Uwaga! Biblioteka PDO działa nieco inaczej na wersjach MySQL 5.0 i 4.1 także w przypadku podpinania. Wersja 5.0 jest bardziej elastyczna, jeśli chodzi o konwersję typów i nic jej nie zaszkodzi, kiedy spróbujemy wstawić do pola TINYINT(1) wartość oznaczoną w skrypcie jako PDO::PARAM_BOOL. Na MySQL 4.1 takie zapytanie nie zostanie wykonane, a ponadto serwer DB nie wygeneruje żadnego ostrzeżenia czy komunikatu.

Ćwiczenie: Zaprogramować formularz do edycji danych produktów z wykorzystaniem podpinania. Skrypt musi wczytywać do formularza dane edytowanego produktu oraz po jego wysłaniu, zmodyfikować wskazany rekord zapytaniem UPDATE. Pamiętaj: wraz z wysłanym formularzem musisz przesłać także ID rekordu, który modyfikujesz!

Obsługa relacji

edytuj

Potrafimy już pobierać wyniki pojedynczego zapytania, potrafimy też wewnątrz jednego zapytania tworzyć relacje. Przejdźmy się jednak do naszej bazy danych księgarni i załóżmy, że chcemy wyświetlić listę kategorii oraz znajdujące się w każdej z nich książki. Sporo początkujących programistów podchodziło do tego zadania z marszu: wysyłali zapytanie żądające pobrania listy kategorii, a następnie w pętli kolejne, które dla aktualnej kategorii pobierało książki. Od razu przestrzegamy przed takim sposobem myślenia! Łamie on podstawową zasadę pracy z bazami danych mówiącą, że generalnie im mniej zapytań, tym lepiej. Ilość wysyłanych zapytań musi być względnie stała i poważnym błędem jest dopuszczenie do sytuacji, gdy zależy ona wprost proporcjonalnie od ilości pobieranych danych. PHP Data Objects (PDO) niejako wymusza rezygnację z tej techniki, ponieważ wspominaliśmy, że nie można wysłać innego zapytania, kiedy nie skończyliśmy pobierać wyników jednego i nie zamknęliśmy jego kursora. Jak więc zatem poradzić sobie z tym zadaniem? Jest to bardzo proste - nasz skrypt będzie bez względu na ilość kategorii wykonywać dwa zapytania, których wynik będzie ładowany do tablicy. Dopiero z niej będzie wyświetlany kod HTML.

<?php

	try
	{
		$pdo = new PDO('mysql:host=localhost;dbname=produkty;port=3305', 'root', 'root');
		$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
			
		$stmt = $pdo -> query('SELECT id, nazwa FROM kategorie ORDER BY id');
		
		$wynik = array();
		while($row = $stmt -> fetch())
		{
			$wynik[$row['id']] = array( // 1
				'nazwa' => $row['nazwa'],
				'ksiazki' => array() // 2		
			);
		}
		$stmt -> closeCursor();

		$stmt = $pdo -> query('SELECT tytul, wydawnictwo, kategoria_id
			FROM ksiazki ORDER BY kategoria_id, id'); // 3
		
		while($row = $stmt -> fetch())
		{
			$wynik[$row['kategoria_id']]['ksiazki'][] = array( // 4
				'tytul' => $row['tytul'],
				'wydawnictwo' => $row['wydawnictwo']			
			);
		}
		$stmt -> closeCursor();

		// 5
		foreach($wynik as &$kategoria)
		{
			echo '<h3>'.$kategoria['nazwa'].'</h3>';
			foreach($kategoria['ksiazki'] as &$ksiazka)
			{
				echo '<p><i>'.$ksiazka['tytul'].'</i>
				(Wyd. '.$ksiazka['wydawnictwo'].')</p>';			
			}
		}
	}
	catch(PDOException $e)
	{
		echo 'Wystapil blad biblioteki PDO: ' . $e->getMessage();
	}

Sztuczka jest tu bardzo prosta - wykorzystujemy ID kategorii jako indeks tablicy (1). Ładujemy do niej nazwę kategorii oraz tworzymy pustą tablicę ksiazki (2) - tutaj będą trafiały książki należące do tej kategorii. Następnie wysyłamy zapytanie służące do pobrania wszystkich książek (3). Zauważmy, że sortujemy je najpierw według ID kategorii, a ponadto tenże ID pobieramy. Wykorzystany zostaje jako klucz dostępu, dzięki czemu jesteśmy w stanie ulokować daną książkę w odpowiedniej kategorii (4). Na końcu dwoma zagnieżdżonymi pętlami wyświetlamy wszystko.

Sposób opiera się na wcześniejszym zbuforowaniu danych. W praktyce programiści bardzo często tworzą takie bufory, ponieważ pozwalają one na dodatkową obróbkę pobranych danych, a w przypadku korzystania z systemu szablonów są nawet niezbędne.

Na dobrą sprawę przy założeniu, że nie chcemy wyświetlać kategorii, które nie zawierają żadnych książek, powyższe zadanie można zrealizować nawet za pomocą jednego zapytania do bazy.

<?php

	try
	{
		$pdo = new PDO('mysql:host=localhost;dbname=produkty;port=3305', 'root', 'root');
		$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
		$stmt = $pdo->query(
			'SELECT nazwa, tytul, wydawnictwo FROM kategorie AS ka, ksiazki AS ks ' .
			'WHERE ka.id=ks.kategoria_id ORDER BY ka.id, ks.tytul');
 
		$kategorie = array();
		while($row = $stmt -> fetch())
		{
			$kategorie[$row['nazwa']][] = array( // 1
				'tytul' => $row['tytul'],
				'wydawnictwo' => $row['wydawnictwo']
			);
		}
		$stmt -> closeCursor();
 
		foreach($kategorie as $nazwa => &$ksiazki)
		{
			echo '<h3>' . $nazwa . '</h3>';
			foreach($ksiazki as &$ksiazka)
			{
				echo '<p><i>'.$ksiazka['tytul'].'</i>
				(Wyd. '.$ksiazka['wydawnictwo'].')</p>';			
			}
		}
	}
	catch(PDOException $e)
	{
		echo 'Wystapil blad biblioteki PDO: ' . $e->getMessage();
	}

Tym razem pobieramy z bazy już połączone wg id kategorii obie tabele, gdzie każdy rekord powstałego złączenia zawiera nazwę kategorii i odpowiednie dane książki. Jako indeksy tablicy (1) wykorzystujemy nazwy kategorii, a wartości to tablice z książkami dla danej kategorii. Wynik ponownie wyświetlamy w pętlach, tym razem do wyświetlenia nazwy wykorzystując indeks.

Dobrze jest pamiętać, że wiele rzeczy można załatwić za pomocą samego silnika bazy danych. Język SQL oferuje bardzo bogate mechanizmy pobierania i obróbki pobieranych danych takie jak: podzapytania, triggery, a nawet pisanie procedur. Warto te mechanizmy dobrze poznać, aby oszczędzić sobie generowania niepotrzebnych zapytań do bazy i oprogramowywania rzeczy, które może załatwić SQL.

Ćwiczenie: Zmodyfikować powyższy skrypt tak, aby przy nazwie książki wymienieni byli również wszyscy jej autorzy, tłumacze, itd. Skrypt może wysyłać najwyżej cztery zapytania.

Zaawansowane techniki bazodanowe

edytuj

W bardziej rozbudowanych projektach zachodzi potrzeba stosowania bardziej rozbudowanych narzędzi, niż zwyczajnych sterowników bazodanowych. Potrzebne są tam biblioteki oferujące mechanizmy cache'owania czy debugowania. Co więcej, bezpośrednie operowanie na bazie przy pomocy języka SQL, szczególnie przy wielu prostych operacjach jest wyjątkowo niewygodne. Tutaj z pomocą przychodzą nam systemy ORM (Object-Relational Mapping). Mówiąc ogólnie, odwzorowują one strukturę bazy danych po stronie skryptu w postaci obiektów. W ten sposób, utworzenie nowego wiersza np. w tabeli z użytkownikami sprowadza się tam do utworzenia nowego obiektu klasy User i przypisania do jego pól żądanych wartości.

Jednym z kilku dostępnych systemów ORM dla PHP5 jest Doctrine. W jego skład wchodzi szereg narzędzi:

  1. Parser autorskiego wariantu języka SQL, DQL-a, zarówno w postaci tekstowej, jak i obiektowej.
  2. Generator struktury bazy danych.
  3. Generator modeli.
  4. System cache.
  5. i wiele innych.

Jest on szczególnie przydatny w grupowej pracy nad projektem, gdzie pojawia się problem przenoszenia zmian poczynionych w strukturze bazy przez jednego programistę do pozostałych uczestników. W przypadku Doctrine, cała baza opisana jest w formie pliku tekstowego z wykorzystaniem prostego i czytelnego formatu YAML, który może być łatwo umieszczony w systemie kontroli wersji. Programista może na jego podstawie wygenerować strukturę bezpośrednio w bazie danych oraz pliki modeli, które pozwalają na manipulację zawartością bazy poprzez obiekty PHP.

Zakończenie

edytuj

Nareszcie umiemy komunikować się z bazami danych z poziomu PHP, wykorzystując do tego celu bibliotekę PHP Data Objects. Nasze skrypty mogą dzięki temu w pełni czerpać z oferowanych przez bazy danych możliwości. Jednak temat baz danych nie został jeszcze zamknięty. Ponieważ PDO jest relatywnie nową biblioteką, wiele skryptów wciąż korzysta ze starych technik wywodzących się jeszcze z prehistorycznych czasów PHP 2.0/FI - dlatego też następny z rozdziałów poświęcony zostanie krótkiemu powrotowi do przeszłości.

 

Sekcja „PHP/ORM i biblioteka Doctrine” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

Poprzedni rozdział: ORM i biblioteka Doctrine
Następny rozdział: phpMyAdmin

Jak to się robiło kiedyś?

edytuj

PHP Data Objects jest bardzo młodą biblioteką i mimo swoich zalet, wciąż tysiące skryptów napisanych wcześniej korzystają ze starych oraz niewygodnych funkcji komunikacji z bazami danych. Dlatego podręcznik ten zawiera także im poświęcony rozdział.

Pobieranie wyników

edytuj

Ten zestaw funkcji w ogóle nie korzysta z dobrodziejstw programowania obiektowego - kiedy powstawał, w PHP po prostu jeszcze takowego nie było! Ponieważ znamy już się nieco na pracy z bazami danych, zaczniemy od razu od pobrania listy naszych produktów:

<?php

	mysql_connect('localhost:3305', 'root', 'root'); // 1
	mysql_select_db('produkty'); // 2
	
	$r = mysql_query('SELECT `id`, `nazwa`, `ilosc` FROM `produkty` ORDER BY `ilosc`'); // 3
	
	echo '<ul>';
	while($row = mysql_fetch_assoc($r)) // 4
	{
		echo '<li>'.$row['id'].' - '.$row['nazwa'].' - '.$row['ilosc'].'</li>';	
	}
	echo '</ul>';
	
	mysql_close(); // 5

?>

Opis:

  1. Funkcja mysql_connect(), która przyjmuje parametry: serwer, nazwa użytkownika, hasło, powoduje nawiązanie połączenia z bazą danych.
  2. Funkcja mysql_select_db() wybiera bazę danych, na której będziemy pracować.
  3. Funkcja mysql_query() wysyła zapytanie do bazy. W zależności od jego rodzaju generuje:
    • Zbiór wyników - dla zapytań SELECT.
    • true - dla zapytań typu INSERT jeśli wykonanie zapytania się powiodło
    • false - w przypadku jakiegokolwiek błędu w zapytaniu.
  4. mysql_fetch_assoc() pobiera kolejny rekord ze zbioru wyników $r jako tablicę asocjacyjną. Istnieją jeszcze mysql_fetch_num() (numeryczne indeksy tablicy) oraz mysql_fetch_array() (połączenie obu tych sposobów).
  5. mysql_close() zamyka połączenie z bazą.

Zauważ, że nie ma tutaj w ogóle czegoś takiego, jak zamykanie kursora. Jeśli korzystasz z tych funkcji, jest ono niepotrzebne, ponieważ to rozszerzenie tak naprawdę oszukuje. Wszystkie rekordy w są pobierane automatycznie przez mysql_query() i zapisywane do specjalnego bufora, skąd odczytuje je mysql_fetch_assoc(). Analogiczna metoda PDOStatement::fetch() pobierała dane bezpośrednio z serwera DB, umożliwiając ich natychmiastowe przetwarzanie. Obie techniki mają swoje plusy i minusy. PDO dzięki temu jest znacznie wydajniejsze, szczególnie przy większej liczbie rekordów, lecz nie można w nim sprawdzić, ile wyników ostatecznie dało nam zapytanie, dopóki ich wszystkich nie pobierzemy.

Obsługa błędów

edytuj

Spróbujmy dodać do naszej listy produktów sortowanie:

<?php

	mysql_connect('localhost:3305', 'root', 'root');
	mysql_select_db('produkty');
	
	$r = mysql_query('SELECT `id`, `nazwa`, `ilosc` FROM `produkty` ORDER BY `ilosc` DECS');
	
	echo '<ul>';
	while($row = mysql_fetch_assoc($r))
	{
		echo '<li>'.$row['id'].' - '.$row['nazwa'].' - '.$row['ilosc'].'</li>';	
	}
	echo '</ul>';
	
	mysql_close();

?>

Po uruchomieniu skryptu dostajemy dziwny komunikat:

Warning: mysql_fetch_assoc(): supplied argument is not a valid MySQL result resource in D:\Serwer\www\mysql\skrypt.php on line 9

Zobaczmy: mamy literówkę w zapytaniu; napisaliśmy DECS zamiast DESC, jednak mysql_query() w ogóle nie zgłosił żadnego komunikatu! Jedynym sygnałem, że coś jest nie tak, było zwrócenie wartości false zamiast zbioru wyników, co po wstawieniu do funkcji mysql_fetch_assoc() zaowocowało komunikatem o podaniu niewłaściwego parametru. Czy więc jest tu w ogóle jakaś obsługa błędów? Oczywiście, tyle że zakłada ona, że programista lubi monotonię i pisanie w kółko:

mysql_query('zapytanie') or die('Blad MySQL: '.mysql_error().'<br/>');

W praktyce bardziej opłacało się tu napisać własny wariant mysql_query(), który automatyzuje tę czynność i samodzielnie zgłasza nam błędy, jak trzeba.

Wstawianie danych

edytuj

Do wykonywania zapytań INSERT czy UPDATE także używana jest funkcja mysql_query(), lecz tym razem będzie ona za każdym razem zwracała wartość true. Aby sprawdzić, ile rekordów zostało zmodyfikowanych, musimy wywołać dodatkowo mysql_affected_rows(). Oto przepisany przykład z poprzedniego rozdziału dodający nowe produkty do bazy:

<?php

	if($_SERVER['REQUEST_METHOD'] == 'POST')
	{
		mysql_connect('localhost:3305', 'root', 'root');
		mysql_select_db('produkty');

		mysql_query('INSERT INTO `produkty` (`nazwa`, `opis`, `ilosc`, `cena`, `jakosc`)	VALUES(
			\''.mysql_real_escape_string($_POST['nazwa']).'\',
			\''.mysql_real_escape_string($_POST['opis']).'\',
			\''.mysql_real_escape_string($_POST['ilosc']).'\',
			\''.mysql_real_escape_string($_POST['cena']).'\',
			\''.mysql_real_escape_string($_POST['jakosc']).'\')');
		$ilosc = mysql_affected_rows();

		if($ilosc > 0)
		{
			echo 'Dodano: '.$ilosc.' rekordow';
		}
		else
		{
			echo 'Wystąpił błąd podczas dodawania rekordów!';
		}
		
		mysql_close();
	}
	else
	{
		?>
		<form method="post" action="mysql_4.php">
		<p>Nazwa: <input type="text" name="nazwa"/></p>
		<p>Opis: <input type="text" name="opis"/></p>
		<p>Ilosc: <input type="text" name="ilosc"/></p>
		<p>Cena: <input type="text" name="cena"/></p>
		<p>Jakosc: <select name="jakosc">
		<option value="1">1</option>
		<option value="2">2</option>
		<option value="3">3</option>
		<option value="4">4</option>
		<option value="5">5</option>
		<option value="6">6</option>
		</select></p>
		<p><input type="submit" value="Dodaj"/></p>
		</form>
		<?php	
	}

?>

Zauważ, jak musimy tutaj umieszczać dane w zapytaniu. Nie tylko wymaga to zabawy operatorem łączenia ciągów, ale też konieczność wywoływania funkcji mysql_real_escape_string() do escape'owania danych i zapobiegania atakom SQL Injection. Oczywiście, gdy magic quotes było włączone, funkcji tej nie powinno się używać.

Informacje dodatkowe

edytuj

Ze względu na charakter pobierania rekordów przez to rozszerzenie, umożliwia ono policzenie ilości zwróconych wyników jeszcze przed rozpoczęciem ich pobierania. Aby to wykonać, należy skorzystać z funkcji mysql_num_rows() z podanym jako parametr zbiorem wyników.

<?php

	mysql_connect('localhost:3305', 'root', 'root');
	mysql_select_db('produkty');
	
	$r = mysql_query('SELECT `id`, `nazwa`, `ilosc` FROM `produkty` ORDER BY `ilosc`');
	
	echo '<p>Pobrano '.mysql_num_rows($r).' wyników</p>';
	
	echo '<ul>';
	while($row = mysql_fetch_assoc($r))
	{
		echo '<li>'.$row['id'].' - '.$row['nazwa'].' - '.$row['ilosc'].'</li>';	
	}
	echo '</ul>';
	
	mysql_close();

?>

Jeśli dodaliśmy nowy rekord, możemy pobrać jego ID funkcją mysql_insert_id() wykonaną zaraz po funkcji mysql_query() z zapytaniem INSERT.

Zakończenie

edytuj

Jak wspomnieliśmy, rozszerzenie to ma charakter historyczny. Twórcy PHP stopniowo ograniczają wsparcie dla niego; nie ma w nim np. żadnej implementacji mechanizmu podpinania, choć biblioteki klienckie MySQL jak najbardziej na to zezwalają. Jest ono także niewygodne w użyciu oraz na dłuższą metę mało efektywne. W codziennej praktyce chyba żaden szanujący się programista nie stosował żadnej z tych funkcji bezpośrednio, lecz korzystał z dodatkowej, napisanej w PHP nakładki automatyzującej wszystkie nużące czynności i zapewniającej wsparcie programowania obiektowego. Teraz, gdy do dyspozycji jest biblioteka PDO, sens korzystania z tych funkcji jest bardzo wątpliwy.

phpMyAdmin

edytuj

Dotychczas wszystkie nasze bazy danych i tabele tworzyliśmy ręcznie z wiersza poleceń. Na dłuższą metę taka praca jest bardzo niewygodna i nieefektywna i nikt nie administruje w ten sposób bazami danych. Do tego celu wykorzystywane są specjalne, zaawansowane narzędzia, przestawiające całą strukturę w postaci graficznej oraz udostępniające narzędzia do np. importu czy eksportu zawartości. Jeśli tworzysz dynamiczne strony WWW, prędzej czy później spotkasz się z aplikacją phpMyAdmin, napisanym w PHP darmowym i niezwykle rozbudowanym menedżerem bazy danych MySQL wchodzącym w skład podstawowego wyposażenia niemal każdej szanującej się firmy hostingowej czy serwera WWW. Dzięki niemu możesz w parę minut skopiować stworzoną przez Ciebie na własnym komputerze bazę danych na właściwy serwer, a mnóstwo programistów wykorzystuje go do projektowania baz dla swych projektów WWW. W tym rozdziale nauczysz się podstaw pracy z phpMyAdminem.

Instalacja

edytuj

Przebieg instalacji phpMyAdmina jest bardzo prosty:

  1. Wejdź na stronę [2] i pobierz z działu DOWNLOADS najnowszą dostępną wersję (obecnie: 4.2.6).
  2. phpMyAdmina nie będziemy instalować wraz z naszymi projektami, lecz do katalogu htdocs serwera Apache. Warto wprowadzić taki podział na swoim komputerze, aby oddzielić narzędzia od tworzonych przez nas stron. Rozpakuj zatem ściągnięte archiwum do katalogu (w zależności od systemu operacyjnego) : D:/Serwer/Apache2/htdocs , /usr/local/apache2/htdocs/ lub /etc ( dla Linuksa Ubuntu)
  3. Pojawi się tam katalog phpMyAdmin lub nieco dłuższy. Skróć go do zwykłego pma. W ten sposób aplikacja będzie dostępna z przeglądarki po wpisaniu adresu http://localhost/pma/ - dzięki temu szybciej się go wpisuje.
  4. Otwórz (lub utwórz w folderze głównym plik) config.inc.php. Jest to skrypt konfiguracyjny phpMyAdmina i możesz tu zmieniać wszystkie ustawienia.
  5. Odnajdź dyrektywę $cfg['PmaAbsoluteUri'] i wprowadź do niej pełen adres URL do aplikacji, np. http://localhost/pma.
  6. Pora na ustawienie połączenia z bazą. phpMyAdmin obsługuje zarówno automatyczną konfigurację, gdzie wszystkie wartości podane są w bazie, jak i ręczne logowanie z użyciem formularza WWW (rozwiązanie spotykane u firm hostingowych). My pracujemy u siebie w domu, dlatego wszystkie wartości wypełnimy w pliku konfiguracyjnym. Zmodyfikuj następujące dyrektywy:
    • $cfg['Servers'][$i]['host'] - wpisz nazwę hosta, na jakim pracuje MySQL (przeważnie localhost).
    • $cfg['Servers'][$i]['port'] - domyślnie MySQL pracuje na porcie 3306, ale jeśli masz inny, ustaw go tutaj.
    • $cfg['Servers'][$i]['user'] - nazwa użytkownika MySQL (u nas: root), na którym będziemy pracować. Niepodanie tej wartości włączy konieczność ręcznego logowania się do phpMyAdmina.
    • $cfg['Servers'][$i]['password'] - hasło podanego użytkownika.
  7. Zapisz skrypt i spróbuj uruchomić w przeglądarce http://localhost/pma. phpMyAdmin powinien automatycznie ustawić się do pracy w języku polskim (jeśli nie, otwórz jeszcze raz config.inc.php i zmień wartość dyrektywy $cfg['DefaultLang'] na pl-iso-8859-2). Jeśli otrzymasz komunikat błędu o niemożności nawiązania połączenia, musisz sprawdzić jego parametry w pliku config.inc.php.

Uruchomienie

edytuj

W przeglądarce otwórz :

http://localhost/pma

lub :

http://localhost/phpmyadmin/

jeśli nie skróciłeś katalogu.

Zaloguj się i program jest już gotowy do pracy.

Rzut okiem

edytuj
 
Ekran startowy phpMyAdmina
 
Podgląd bazy danych
 
Struktura tabeli

Po uruchomieniu ujrzysz ekran startowy phpMyAdmina. Podczas pracy okno przeglądarki podzielone jest na dwie części: po lewej znajduje się wąski i ciemny pasek. Domyślnie znajduje się w nim lista rozwijana, z której wybieramy interesującą nas bazę danych. Gdy ją wybierzemy, pod spodem wyświetli się również lista znajdujących się w niej tabel. Szersza i jaśniejsza część ekranu to część operacyjna - tu wykonujemy wszystkie operacje.

Ekran startowy zawiera różne informacje o stanie połączenia oraz daje dostęp do niektórych opcji. Po prawej stronie możemy zmienić motyw graficzny oraz język interfejsu, natomiast pośrodku ekranu znajdują się rzeczy bardziej powiązane z samym MySQL-em. Pod napisem Utwórz nową bazę danych widzimy dwa obiekty formularza. W pierwszy z nich wpisujemy nazwę, w drugi kodowanie i po kliknięciu na przycisk "Utwórz" stworzona zostanie nowa baza danych o takich parametrach. Poniżej mamy dostęp do różnych ekranów administracyjnych, np. listy procesów serwera czy edytora uprawnień.

Przejdźmy za pomocą części nawigacyjnej do naszej bazy produkty. Zarówno w części nawigacyjnej, jak i na ekranie roboczym pojawi nam się spis tabel, które już utworzyliśmy. W części roboczej dodatkowo widoczna jest belka nawigacyjna z następującymi elementami:

  • Struktura - pokazuje listę tabel, czyli to, co właśnie oglądamy.
  • SQL - ekran do tworzenia i wykonywania zapytań SQL.
  • Szukaj - wyszukiwarka danych
  • Zapytanie przez przykład - graficzny edytor zapytań dla nieznających języka SQL (wzorowany na edytorach dostępnych np. w Microsoft Access).
  • Eksport - narzędzia eksportu bazy danych do pliku SQL lub innych.
  • Import - narzędzia do importu bazy danych z pliku SQL. W starszych wersjach phpMyAdmina ekran ten był częścią ekranu SQL.
  • Operacje - narzędzia administracyjne: zmiana nazwy bazy danych, kopiowanie, zmiana kodowania.
  • Uprawnienia - pokazuje użytkowników uprawnionych do dostępu do wybranej bazy.
  • Usuń - usuwa bazę danych.

Na właściwym serwerze WWW ilość zakładek może być ograniczona ze względu na brak uprawnień.

Pod listą tabel znajduje się niewielki formularz inicjujący edytor nowej tabeli. Niebawem z niego skorzystamy, lecz najpierw zapoznamy się jeszcze z widokiem pojedynczej tabeli. Możemy do niego przejść, wybierając nazwę z panelu nawigacyjnego lub klikając na drugą z ikonek w ekranie roboczym przy interesującej nas tabeli. Ujrzymy wtedy ekran podobny do tego przedstawionego na ilustracji 3.

Widok tabeli składa się ze szczegółowej listy wszystkich pól pokazującej informacje m.in. o ich typie czy o wartości domyślnej. Poniżej znajduje się prosty formularz umożliwiający dodanie nowych pól do tabeli, a jeszcze niżej informacje dodatkowe: spis indeksów, rozmiar danych w tabeli i statystyka rekordów. Zauważmy, że zmianie uległa belka nawigacyjna. Zniknęła opcja Zapytanie przez przykład, za to doszły dwie nowe:

  • Przeglądaj - wyświetla zawartość tabeli. Jeżeli trzymasz w tabeli dużo rekordów, nie musisz się przejmować - wyniki są porcjowane.
  • Wyczyść - czyści tabelę zapytaniem TRUNCATE (po uprzednim potwierdzeniu).

Dodatkowo niektóre zakładki zmieniają swoje działanie, np. opcja Eksport umożliwia teraz eksportowanie tylko aktualnej tabeli.

Tworzenie tabeli

edytuj
 
Edytor struktury tabeli

Aby rozpocząć tworzenie nowej tabeli, przełączmy się na widok bazy danych. Pod listą tabel widoczny jest niewielki formularz zatytułowany "Utwórz nową tabelę". Wpisujemy w nim nazwę nowej tabeli oraz ilość pól, które zamierzamy do niego dodać. Jeśli się pomylisz, nie przejmuj się. Edytor będzie można łatwo powiększyć jeszcze w trakcie edycji, a ponadto można dodawać nowe pola do tabeli już po jej utworzeniu. Po kliknięciu na "Wykonaj" ukaże się rozbudowany edytor umożliwiający dokładne zdefiniowanie struktury. Oto omówienie poszczególnych kolumn:

  • Nazwa - nazwa pola.
  • Typ - z listy wybieramy typ danych, jakie mają być w nim przechowywane.
  • Długość/wartości - dla pól VARCHAR czy CHAR podajemy tu maksymalną dozwoloną długość ciągu tekstowego. Dla liczb nie trzeba w sumie nic wpisywać, phpMyAdmin zaproponuje wtedy domyślne wartości, które usatysfakcjonują każdego. Dla pól ENUM oraz SET wpisujemy tu listę dozwolonych wartości ujętych w apostrofy i odseparowanych przecinkami.
  • Metoda porównywania napisów - tylko dla pól VARCHAR, TEXT itp. - wybieramy tutaj, według jakiego kodowania mają być porównywane znajdujące się tu dane. Przykładowo, jeżeli zamierzasz stworzyć witrynę wykorzystującą kodowanie Unicode, musisz odnaleźć tu zbiór np. utf8_polish_ci.
  • Atrybuty - dodatkowe atrybuty, np. dla liczb można wybrać atrybut UNSIGNED, co spowoduje, że będzie można przechowywać tu tylko liczby dodatnie, ale za to w dwukrotnie większym dozwolonym zakresie (przestrzeń zwolniona po wywaleniu części ujemnej).
  • Null - czy pole może przyjmować wartości NULL.
  • Domyślne - domyślna wartość tego pola w nowych rekordach.
  • Dodatkowo - dla pola ID możemy tu wybrać atrybut AUTO_INCREMENT.

Kolejne cztery pola wyboru pozwalają zdefiniować rodzaj indeksu. Od lewej strony mamy:

  • PRIMARY KEY - ustawić dla pola ID.
  • INDEX - normalny indeks.
  • UNIQUE - pole z unikalnymi wartościami (nie mogą się powtarzać w dwóch rekordach)
  • --- - brak indeksu.

Pole pod ikonką "T" umożliwia stworzenie indeksu FULLTEXT ułatwiającego przeszukiwanie zawartości tekstów. Można go utworzyć tylko w tabelach MyISAM, a ponadto aby mieć z niego jakiś pożytek, trzeba umieć pisać odpowiednie zapytania wykorzystujące tzw. "fulltext searching".

Uwaga: zaznaczenie dla kilku pól pozycji INDEX nie spowoduje utworzenia kilku indeksów, tylko jeden indeks łączony! Dlatego jeśli zamierzasz stworzyć więcej indeksów, musisz to zrobić z pomocą dodatkowego edytora już po utworzeniu tabeli. Włączamy go w widoku tabeli w pozycji "Indeksy". Widoczny jest tam formularz zatytułowany "Utwórz indeks dla X kolumn". Po wybraniu liczby kolumn w indeksie, przejdziemy do szczegółowego widoku, gdzie możemy wybrać:

  • Nazwę indeksu
  • Jego rodzaj
  • Określić pola mające wejść w jego skład.

Dalszą część formularza można zignorować. Pamiętajmy też o określeniu globalnego kodowania dla całej tabeli oraz wybraniu typu (domyślny w MySQL-u to mający większe możliwości InnoDB, ale na co dzień korzysta się głównie z wydajniejszego MyISAM).

Modyfikacja tabeli

edytuj

Istnieje możliwość modyfikacji struktury tabeli już po jej utworzeniu. Typowe operacje to:

  • Dodawanie nowych pól: pod listą pól w widoku struktury tabeli znajduje się niewielki formularz, w którym określamy, ile kolumn chcemy dodać oraz w którym miejscu. Po kliknięciu na "Wykonaj" zostaniemy przeniesieni do identycznego edytora, jak w przypadku tworzenia tabeli.
  • Modyfikacja już istniejących pól. Klikamy na ikonę ołówka przy interesującym nas polu lub zaznaczamy grupę pól i klikamy na ołówek pod spisem. Modyfikacja odbywa się w identycznym edytorze, jak w przypadku tworzenia tabeli.
  • Usuwanie pól - za pomocą ikonki krzyżyka. Wcześniej musimy potwierdzić naszą chęć.

Zarządzanie rekordami

edytuj
 
Lista zawartości tabeli
 
Dodawanie nowego rekordu

W zakładce Przeglądaj możemy obejrzeć zawartość aktualnej tabeli. Na ekranie ukaże się lista wszystkich rekordów wraz z wartościami wszystkich pól. Jest ona porcjowana: naraz pokazywane jest tylko 30, a do następnych stron przełączamy się za pomocą strzałek. Ikonki przy każdym rekordzie umożliwiają edycję danych lub jego usunięcie. Analogiczna przeglądarka ukaże nam się, gdy za pomocą zakładki SQL wykonamy zapytanie SELECT lub inne generujące jakąś listę wyników.

Zarówno edycja, jak i dodawanie nowego rekordu odbywa się w specjalnym edytorze pokazanym na screenie. Formularz składa się z pięciu kolumn:

  • Pole - nazwa pola
  • Typ - informacja o typie możliwych do przechowania danych
  • Funkcja - z tej listy możemy wybrać funkcję, przez jaką zostanie przepuszczona wartość wpisana w polu "Wartość". Uwaga: niektóre funkcje nie wymagają podawania żadnego dodatkowego parametru w tamtym polu (np. UNIX_TIMESTAMP() będący odpowiednikiem time() w PHP).
  • Null - jeśli pole zezwala, możemy tutaj zaznaczyć, że wstawiamy wartość NULL.
  • Wartość - dokładna wartość, jaką chcemy wstawić w wybrane pole.

Formularz dodawania umożliwia tworzenie do dwóch rekordów naraz. Uważaj, gdyż w przypadku tabel o dużej liczbie pól, phpMyAdmin wstawia co kilkanaście pasek z przyciskiem Wykonaj, jednak nie korzystaj z niego, lecz z przycisku znajdującego się pod całym formularzem. Inaczej zaakceptujesz tylko część wprowadzonych wartości, co może doprowadzić do nieprzewidywalnych zachowań.

Jeśli chcemy dodawać rekordy seryjnie, możemy z listy pod formularzem przy napisie "a następnie" wybrać "dodaj nowy rekord" zamiast "wróć". Spowoduje to, że po dodaniu rekordów z powrotem zostaniemy przeniesieni do formularza dodawania.

Import i eksport zawartości

edytuj
 
Ekran eksportu zawartości

phpMyAdmin jest szczególnie lubiany przez programistów, gdyż najczęściej to za jego pomocą bazy danych przenoszone są z lokalnego komputera na właściwy serwer WWW. Służy do tego zakładka Eksport. Aby prawidłowo wyeksportować zawartość bazy danych, musimy wpierw spędzić chwilkę na konfiguracji:

  1. W ramce "Eksport" wybieramy interesujące nas tabele. Jeżeli eksportujemy całą bazę, możemy kliknąć na "Zaznacz wszystkie".
  2. Wybieramy format. Domyślnie phpMyAdmin zaproponuje eksport do pliku SQL, który jest niczym innym, jak listą zapytań CREATE TABLE oraz INSERT, które po uruchomieniu odtworzą dokładną kopię naszej bazy.
  3. W ramce "Opcje eksportu" zaznaczamy, co chcemy eksportować: samą strukturę, same dane, czy obie rzeczy naraz.
  4. Dodatkowo, phpMyAdmin umożliwia zachowanie kompatybilności ze starszymi wersjami bazy danych, a nawet innych serwerów DB! Jeśli stworzyłeś na MySQL 5.0 bazę danych, ale na serwerze jest MySQL 4.0, skorzystaj z listy "Kompatybilność eksportu SQL", a aplikacja wygeneruje zapytania dla wybranej przez Ciebie wersji serwera. Podobnie możesz postąpić, jeśli chcesz przenieść swoją bazę np. na PostgreSQL (dodajmy, że ten serwer DB także posiada swój "webowy" menedżer zwany oczywiście phpPgAdmin).
  5. Jeśli nie mamy ochoty, aby naszym monitorem zawładnęły zapytania SQL, zaznaczamy jeszcze opcję "Zapisz jako plik", co spowoduje, że generowany wynik będziemy mogli od razu ściągnąć na nasz komputer, zamiast wyświetlać jego zawartość w przeglądarce.
  6. Klikamy przycisk "Wykonaj".

Importowanie bazy danych odbywa się za pomocą zakładki Import. Wskazujemy w niej plik SQL na naszym dysku ze strukturą bazy, wybieramy kodowanie i klikamy "Wykonaj". Pamiętaj, że PHP ma limit wykonywania ograniczony do 30 sekund. Jeśli twój plik SQL ma naprawdę potężne rozmiary, będziesz mógł kontynuować później jego wgrywanie od wybranego zapytania (ramka "Import częściowy"). W starszych wersjach phpMyAdmina import odbywał się za pomocą zakładki SQL, ale według niemal identycznej procedury.

Zakończenie

edytuj

Od tej pory wszystkie bazy danych i tabele będziemy tworzyć już z użyciem phpMyAdmina. Dzięki temu narzędziu praca z bazami jest naprawdę przyjemna, i co ważniejsze, wygodna. phpMyAdmin stał się tak popularny, że na polu menedżerów dla bazy MySQL w zasadzie nie ma żadnej konkurencji, a twórcy menedżerów dla innych serwerów DB bardzo mocno się na nim wzorują (podobna nawigacja, układ menusów, formularze itd.):

  • phpPgAdmin - menedżer dla baz PostgreSQL.
  • SQLiteManager - menedżer dla baz SQLite.

W następnym rozdziale sprawdzimy nasze umiejętności w praktyce, tworząc system newsów oparty o MySQL.

Poprzedni rozdział: [[../../phpMyAdmin/]]
Następny rozdział: [[../../Bazy danych - Co dalej?/]]

System newsów

edytuj

Spróbujmy teraz zaimplementować zdobytą przez nas wiedzę w praktyce. Napiszemy prosty system newsów z podziałem na kategorie i możliwością dodawania komentarzy. Podręcznik ten pokaże, jak zacząć, natomiast twoim zadaniem będzie dopisanie (z pomocą wskazówek) wszystkich dodatków niezbędnych do tego, aby system był w pełni funkcjonalny. Nowością w porównaniu do księgi gości będzie to, iż nie będziemy już umieszczać kodu HTML bezpośrednio we właściwym pliku, lecz umieścimy go jako zbiór funkcji w osobnym. Dzięki temu poprawi się czytelność kodu, a ty poznasz zalety separacji tych dwóch elementów jeszcze przed przejściem do omawiania systemów szablonów.

Projekt bazy danych

edytuj

System newsów oparty będzie o bazę danych MySQL, w której ulokujemy trzy tabelki. Będą one połączone ze sobą relacją jeden do wielu:

  1. Istnieje grupa kategorii
  2. Każda kategoria może zawierać dowolną liczbę newsów, ale pojedynczy news może należeć tylko do jednej z nich.
  3. Każdy news może zawierać dowolną ilość komentarzy.

Układ tabelek zostanie tak zoptymalizowany, aby jak najrzadziej zmuszać bazę do wykonywania funkcji COUNT() w celu zliczenia ilości wpisów.

Zacznijmy od tabeli kategorii:

CREATE TABLE `categories` (
  `id` int(11) NOT NULL auto_increment,
  `title` varchar(40) NOT NULL,
  `description` varchar(255) NOT NULL,
  `news_num` int(11) NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Oprócz typowych pól informacyjnych: title oraz description (tytuł i opis), mamy też pole news_num przechowujące aktualną liczbę newsów wewnątrz kategorii. Musimy pamiętać, aby nasz system poprawnie zmniejszał i zwiększał jego zawartość w trakcie edycji i wprowadzania nowych elementów do bazy.

Następne w kolejności są newsy:

CREATE TABLE `news` (
  `id` int(11) NOT NULL auto_increment,
  `title` varchar(128) NOT NULL,
  `body` text NOT NULL,
  `date` int(11) NOT NULL,
  `author` varchar(30) NOT NULL,
  `category_id` int(11) NOT NULL,
  `comment_num` int(11) NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `category_id` (`category_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Tabela zaczyna się od pól informacyjnych. Z punktu widzenia projektanta bazy najistotniejsze są jednak dwa ostatnie. category_id przechowuje ID kategorii, do której należy news; w tym miejscu tworzymy naszą relację. comment_num działa na podobnej zasadzie, jak w kategoriach. Dzięki temu nie trzeba będzie wykonywać skomplikowanych zapytań przy pobieraniu listy newsów, aby pokazać tam jednocześnie ilość komentarzy.

Na samym końcu zapoznamy się z tabelą komentarzy:

CREATE TABLE `comments` (
  `id` int(11) NOT NULL auto_increment,
  `news_id` int(11) NOT NULL,
  `author` varchar(30) NOT NULL,
  `date` int(11) NOT NULL,
  `body` text NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `news_id` (`news_id`,`date`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Zwróć uwagę, w jaki sposób założony jest podwójny indeks na pola news_id oraz date. Gdyby utworzyć tutaj dwa osobne indeksy, w zasadzie nic byśmy nie uzyskali. Popatrzmy na to tak: nigdy nie wyświetlamy wszystkich komentarzy, jakie mamy w bazie. Zawsze są one powiązane z jakimś konkretnym newsem, dlatego tak istotne jest odzwierciedlenie tego w strukturze indeksów. Przy osobnych indeksach pole date zostanie posortowane globalnie, a wybierając komentarze tylko dla pojedynczego newsa, system DB i tak będzie musiał przeskanować całą tabelę, aby je względem tejże daty wybrać.

Od strony bazy danych to tyle. Przejdźmy do kodowania.

Funkcje podstawowe

edytuj

Napiszemy teraz plik functions.php. Umieścimy w nim różne podstawowe funkcje. W tej chwili jest ich stosunkowo niewiele: łączenie się z bazą, kontrola długości wprowadzonego tekstu, prymitywne zabezpieczenie przed floodem. Plik ten przyda Ci się jednak podczas samodzielnej rozbudowy; prawdopodobnie rozrośnie się wtedy znacznie. Jego zawartość wygląda następująco:

<?php

	function initSystem()
	{
		global $sql;
		
		$sql = new pdo('host=localhost;port=3305;dbname=artykuly', 'root', 'root');
		$sql -> exec('SET NAMES `utf8`');
		
		ob_start();
	} // end initSystem();
	
	function doneSystem()
	{
		ob_end_flush();
	} // end doneSystem();
	
	function commentsAllowed()
	{
		if(isset($_COOKIE['a84skljf']))
		{
			return false;		
		}
		setcookie('a84skljf', 1, time() + 3600);
		return true;	
	} // end commentsAllowed();
	
	function validTextField($text, $min, $max)
	{
		if(strlen($text) > $min && strlen($text) < $max)
		{
			return true;
		}
		return false;	
	} // end validTextField();

?>

Opis poszczególnych funkcji:

  1. initSystem() - inicjacja systemu; nawiązuje połączenie z bazą danych i włącza buforowanie wyjścia.
  2. doneSystem() - aktualnie tylko kończy buforowanie wyjścia. Być może znajdziesz dla niej jakieś ciekawe dodatkowe zastosowania.
  3. commentsAllowed() - funkcja zwraca true, jeżeli internauta ma prawo dodawać komentarze i false, jeżeli już takowy niedawno dodał.
  4. validTextField() - prosta funkcja do kontroli danych. Zwraca true, jeżeli długość tekstu mieści się w podanym zakresie.

Kod HTML

edytuj

Składamy system w całość

edytuj

Ćwiczenia

edytuj

Zakończenie

edytuj

Bazy danych - co dalej?

edytuj

Ostatnie rozdziały nauczyły nas nie tylko podstaw zarządzania i tworzenia baz danych, ale także odwoływania się do nich z poziomu PHP. Nie powinieneś jednak poprzestać tylko i wyłącznie na zawartych w niniejszym podręczniku informacjach. Ilu programistów, tyle możliwych organizacji danych na stronie WWW, a to musi znaleźć swe odzwierciedlenie w projekcie bazy danych. Zagadnieniu temu można poświęcić niejedną książkę, dlatego ćwicz i eksperymentuj, ile się da.

Generalnie, przymierzając się do zaprojektowania bazy danych dla jakiejś witryny, możemy skupić się albo na jak najlepszym dopasowaniu jej do wymagań, albo na elastyczności. Pierwsze podejście zazwyczaj nie wymaga tworzenia skomplikowanej architektury, a znajdujące się tam dane można względnie szybko odczytywać. Niestety, jakakolwiek zmiana lub dodanie nowych opcji pociąga za sobą konieczność modyfikacji bazy danych przez programistę, stąd też jest to wysoce niewydajne przy pisaniu pakietów wielokrotnego użytku. Odwrotna filozofia zakłada tworzenie bardziej uniwersalnych tabel - nie odpowiadają one dokładnie strukturze danych, które mają przechowywać, ale dzięki obecności dodatkowych pól kontrolnych, bardzo łatwo dopasowują się do zmian. Przykładem może być tutaj system CMS skoncentrowany na prezentowaniu w sieci informacji tekstowej w postaci artykułów, recenzji oraz opisów. W gruncie rzeczy te trzy rzeczy wymagają tego samego: pola na treść, tytułu, jakichś linków dodatkowych. Dlaczego więc nie utworzyć zbiorczej tabeli z dodatkowym polem typ? Przyjmujemy konwencję, że jeżeli zawiera ono wartość 0, mamy do czynienia z artykułem, gdy 1 - z recenzją itd. Od strony PHP możemy teraz niezwykle łatwo napisać jednolity panel do zarządzania nimi. Jeżeli w przyszłości klient zażyczy sobie dodania kolejnego rodzaju tekstu, sprowadzi się to do przydzielenia mu kolejnego numeru i dodania do formularzy edycyjnych.

Nie wszystkie informacje da się tak prosto przechowywać w bazie danych i trzeba mieć tego świadomość. Do odwzorowania za pomocą SQL-owych tabel hierarchicznego drzewa, do którego zawartości jest szybki dostęp, musimy zastosować pewne sztuczki oraz dodatkowe algorytmy. Innymi słowy, aby uzyskać to, co chcemy, czasem musimy ruszyć głową i wymyślić jakąś ciekawą koncepcję. W jej realizacji przydatne będą bardziej zaawansowane możliwości języka SQL takie, jak klauzule JOIN, unie, widoki czy wyzwalacze (triggery). Ich dokładny opis znajduje się w dokumentacji MySQL-a pod adresem http://dev.mysql.com/doc. W sieci znajdziemy także wiele artykułów wyjaśniających ich praktyczne zastosowanie.

Równie istotnym czynnikiem jest wydajność. Niektóre algorytmy korzystania z bazy danych oraz możliwości języka SQL są bardzo czasożerne i ich stosowanie na większych bazach mija się z celem. Nie wolno Ci myśleć kategoriami, że skoro działa, to jest dobrze. W środowisku testowym bazy liczą zazwyczaj po kilka-kilkanaście rekordów i nijak nie można z taką zawartością sprawdzić, czy w rzeczywistości witryna nie padnie klientowi przy tysiącu rekordów z powodu przeciążenia. Poprawianie niezoptymalizowanych, nieprzemyślanych baz jest trudnym procesem i na pewno narazi klienta na dodatkowe koszty. Analogiczne rozumowanie można przeprowadzić dla języka PHP.

Na zakończenie rozdziału o bazach danych, jeszcze raz podkreślamy: poznaliśmy dopiero podstawy pracy z tym narzędziem. Jego szersze omówienie przekracza możliwości tego podręcznika, dlatego nie poprzestawaj na zawartych tu informacjach. Ćwicz, eksperymentuj, nie bój się sięgać po Google i dodatkowe źródła. W następnej części podręcznika poznamy systemy szablonów.

 

Sekcja „PHP/Ćwiczenia/Bazy_danych” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

Systemy szablonów

edytuj
Poprzedni rozdział: Ćwiczenia

Czym jest system szablonów?

edytuj

W omówionych do tej pory zagadnieniach i przykładach milcząco zakładaliśmy, że nasz kod PHP bezpośrednio generował HTML dla przeglądarki. Pobierając dane z bazy natychmiast obudowywaliśmy je znacznikami i posyłaliśmy w świat. Jest to cecha charakteryzująca skrypty początkujących programistów PHP, która jednak rodzi dużo problemów z przenośnością i elastycznością tworzonego kodu. W następnych rozdziałach poznamy tzw. systemy szablonów, czyli biblioteki umożliwiające ich rozwiązanie.

Idea systemu szablonów

edytuj

Od poznania programowania obiektowego powoli przestajemy patrzeć na aplikację WWW jak na monolityczny blok, w którym wszystkie elementy są ściśle i nierozerwalnie ze sobą związane. Przekonaliśmy się już, że o wiele lepszym podejściem jest podział aplikacji na warstwy zajmujące się różnymi zadaniami i komunikujące między sobą. Przyjrzyjmy się zatem procesowi generowania kodu HTML. Opisując go możemy powiedzieć, że skrypt musi najpierw pobrać skądś dane, a następnie opakować je czymś, co umożliwi ich wyświetlenie w przeglądarce w zamierzony sposób. Wyraźnie widać tutaj dwa etapy, które - jak się okazuje - świetnie nadają się do rozdzielenia na dwie warstwy.

 
Zasada działania systemów szablonów.

System szablonów (ang. template engine) to biblioteka, która pozwala oddzielić warstwę prezentacji od logiki biznesowej. Podstawowym pojęciem jest tutaj szablon, czyli prosty plik tekstowy ze szkieletem wynikowego kodu HTML oraz zestawem instrukcji mówiących, co, gdzie i jak wyświetlić. Aplikacja WWW najpierw pobiera wszystkie dane, przetwarza je i umieszcza w systemie szablonów wraz z informacją o tym, jakiego szablonu użyć, jednak bez zwracania uwagi na szczegóły procesu wyświetlania. System szablonów wczytuje szablon i wykonuje go, osadzając w nim dane ze skryptu. Wynikiem jest gotowy kod, który jest posyłany do przeglądarki.

Systemy szablonów wykorzystywane są w zdecydowanej większości dużych projektów programistycznych nawet, jeśli nie są jawnie nazwane w ten sposób. Separacja pozwala na bardziej niezależny rozwój obu warstwy aplikacji oraz większą niezawodność; zajmując się algorytmami przetwarzania nie plączą nam się wokół wstawki kodu HTML, a pracując nad warstwą prezentacji nie ma obawy, że spowodujemy błąd w mechanizmach pobierania z bazy. Migracja na nową szatę graficzną sprowadza się do napisania nowych szablonów, a co więcej - możemy przechowywać kilka zestawów szablonów i dać użytkownikowi wybór, którego z nich chce użyć.

Nie odpowiedzieliśmy na jeszcze jedno cisnące się na usta pytanie - czym właściwie jest szablon? Jest to pewien plik tekstowy, rodzaj skryptu zawierający statyczny kod wysyłany do przeglądarki oraz reguły mówiące, jakie dane gdzie umieścić. Oczywiście do wyrażania tych reguł potrzebny jest jakiś dodatkowy meta-język i ze względu na niego możemy wyróżnić dwa główne rodzaje systemów szablonów:

  1. PHP jako język szablonów - reguły osadzania danych opisywane są przy pomocy zwykłych wstawek kodu PHP.
  2. Dedykowany język szablonów - system szablonów wprowadza specjalny język do zapisu reguł oraz dostarcza parser do jego przetwarzania.

Każde z rozwiązań ma swoje wady i zalety, które zaraz omówimy.

PHP jako język szablonów

edytuj

Przykładowy szablon napisany w PHP może wyglądać następująco:

<html>
 <head>
  <title><?=$title?></title>
 </head>
 <body>
  <?if($user == 'Adam'){?>
  Hello, <strong><?=$user?></strong>
  <?}else{?>
  Hello, <?=$user?>
  <?}?>
 </body>
</html>

Zamiast korzystania z konstrukcji <?php echo $zmienna;?>, użyliśmy tu krótszego odpowiednika dającego ten sam efekt, a mianowicie <?=$zmienna?>. Jednak aby takie rozwiązanie działało, musimy w pliku php.ini serwera ustawić short_open_tag na "on".

Korzystamy tutaj ze znanych nam już pętli, instrukcji warunkowych i zmiennych, co umożliwia bardzo szybkie wgryzienie się i wykorzystanie doświadczeń, które już znamy. Z drugiej strony czysty PHP poza pętlami i warunkami nie oferuje praktycznie nic rzeczywiście przydatnego do pisania dużej liczby szablonów i co więcej, bywa dość niewygodny. Dlatego systemy tego typu udostępniają dodatkowo pokaźny zbiór tzw. helperów, np. w postaci funkcji lub metod statycznych jakiejś klasy, które zamykają skomplikowane fragmenty kodu w prostym wywołaniu. Przykładowo, zamiast pisać skomplikowany kod do sklejania adresu URL z danych, możemy mieć funkcję link(), do której podamy jedynie argumenty i tytuł odnośnika, a od razu otrzymamy gotowy znacznik <a>.

Zalety:

  1. Prostota użycia - możemy wykorzystać doświadczenia, które już znamy.
  2. Potężna siła wyrazu - PHP to pełnoprawny język programowania, dlatego w szablonach możemy posługiwać się wszystkimi elementami składni do osiągnięcia żądanego celu.

Wady:

  1. Bez helperów praktycznie niezdatny do użycia na dłuższą metę. Ponadto same helpery są czasem wyjątkowo skomplikowane, szczególnie gdy próbujemy dostosować je do własnych potrzeb.
  2. Bardzo rozwlekła składnia i brak rozumienia struktury dokumentu HTML. Bez ścisłej dyscypliny bardzo łatwo stworzyć mocno nieczytelny kod.
  3. Bierzemy język PHP ze wszystkimi jego niekonsekwencjami, których nie mamy szansy zmienić i poprawić.

Dedykowany język szablonów

edytuj

Pod nazwą dedykowany język szablonów kryje się cała rodzina różnorodnych języków używanych przez różne systemy szablonów. Poniżej możemy zobaczyć przykład szablonu utworzonego w takim hipotetycznym języku:

<html>
 <head>
  <title>{TITLE}</title>
 </head>
 <body>
  {IF:USER == 'Adam'}
  Hello, <strong>{USER}</strong>
  {ELSE}
  Hello, {USER}
  {/IF}
 </body>
</html>

Cel przyświecający ich powstawaniu jest jeden: PHP jest zbyt nieczytelny, zbyt rozwlekły i ma zbyt dużo niekonsekwencji, by można było w nim łatwo tworzyć szablony, dlatego spróbujemy stworzyć coś lepszego, pozbawionego określonych wad. PHP powstał dlatego, że ktoś stwierdził, iż C czy C++ nie nadaje się na dłuższą metę do wygodnego tworzenia stron WWW. C i C++ powstały, gdyż uznano, że wygodniej jest pisać programy w ten sposób, niż w czystym assemblerze. Przykłady można mnożyć i analogiczna sytuacja ma miejsce tutaj. Czy się to udaje, zależy już od pomysłowości oraz umiejętności programisty tworzącego określony system. Teoretycznie tworząc własny język jesteśmy ograniczeni jedynie przez prawa fizyki i matematyki oraz przez własną wyobraźnię. W praktyce wiele z powstających języków ogranicza się głównie do udostępnienia wąskiego podzbioru PHP z <?php ?> zamienionym na klamerki, a w wielu popełniono równie poważne błędy projektowe, jak w PHP. Z tego właśnie powodu nie cieszą się one uznaniem części programistów patrzących przez pryzmat tych nieudanych prób.

Zalety:

  1. Dobrze zaprojektowany dedykowany język szablonów może być dużo efektywniejszy w użyciu, niż PHP.
  2. Możliwość pozbycia się wad i niedoróbek PHP, a także wielu detali technicznych, ukrywając je za prostymi konstrukcjami języka.

Wady:

  1. W praktyce rzadko kiedy to się udaje: brak umiejętności, pomysłowości, doświadczenia.
  2. Nawet jeśli konstrukcje języka są proste w użyciu, trzeba się ich najpierw nauczyć, zwłaszcza jeżeli odbiegają pod względem zasady działania od znanych już nam wzorców.

Kontrowersje

edytuj

Systemy szablonów to jedne z najbardziej kontrowersyjnych rodzajów bibliotek. W Internecie oraz w publikacjach można bardzo łatwo spotkać mnóstwo wzajemnie wykluczających się poglądów. Poniżej przedstawiony jest przegląd najważniejszych kontrowersji, argumentów krytyków oraz punktów widzenia, a także komentarz, jak będziemy traktować je w tym podręczniku.

Problem: PHP to system szablonów.

Wielu programistów nie stosuje rozróżnienia na systemy szablonów oraz języki szablonów, nazywając np. PHP systemem szablonów, albo degradując pojęcie systemu szablonów do języka. Powiedzieliśmy na początku, że system szablonów jest biblioteką, czyli zawiera jakieś klasy, funkcje, interfejsy, które umożliwiają przekazywanie danych z warstwy logiki do szablonów. PHP sam w sobie nic takiego nie posiada - dostarcza on pętli, instrukcji warunkowych, jakichś funkcji do przetwarzania danych, czyli ewidentnych narzędzi do pisania reguł i skryptów. Dopiero gdy dodamy do niego odpowiednie klasy i kod pozwalający uruchomić jakiś szablon z określonymi danymi, możemy mówić o systemie szablonów, natomiast bez tego jest to tylko język. W drugim kierunku sytuacja jest analogiczna. Język szablonów to jedynie sposób zapisu reguł osadzania danych, który bez odpowiedniego API nic nie znaczy.

W tym podręczniku będziemy konsekwentnie trzymać się zasady, że system szablonów to biblioteka, a język szablonów to język.

Problem: zawężanie pojęcia systemów szablonów do systemów z dedykowanym językiem.

Niektórzy programiści mówią, że nie używają systemów szablonów, lecz PHP, ponieważ nie widzą sensu w uczeniu się kolejnego języka. Przypomnijmy sobie, co powiedzieliśmy w poprzednim problemie - PHP to język szablonów, a nie system. Różnice w działaniu systemów szablonów korzystającego z PHP oraz ze swojego własnego języka sprowadzają się jedynie do tego, że w tym drugim przypadku trzeba szablony przetworzyć we własnym zakresie. Poza tym ich zasada funkcjonowania, zakres obowiązków i zadań jest dokładnie taki sam. Co więcej, PHP jest językiem takim, jak każdy inny. To, że akurat biblioteka jest też w nim napisana, nie wyróżnia go w żaden konkretny sposób - ostatecznie kompilator np. dla języka C pozostaje kompilatorem bez względu na to, w czym go napisano.

W tym podręczniku przyjmujemy zasadę: jeśli wygląda jak kaczka i kwacze jak kaczka, to jest to kaczka.

Problem: systemy szablonów to dodatkowa warstwa abstrakcji i dodatkowy narzut.

Odpowiedź na to pytanie wiąże się bezpośrednio z dwoma powyższymi podpunktami. System szablonów, a język szablonów to dwie różne rzeczy i nie należy ich mylić. Jeśli chcemy oddzielić warstwę logiki od warstwy prezentacji w aplikacji WWW, nie ma innego sposobu, jak użyć szablonów. Do przetwarzania szablonów zawsze potrzebny jest nam jakiś interfejs bez względu na to czy piszemy je w PHP czy w innym języku. Ponadto nie wolno na aplikację patrzeć nigdy jedynie przez pryzmat wydajności i narzutów tego czy innego elementu. Aplikacja w pierwszej kolejności ma spełniać określone wymagania i udostępniać określoną funkcjonalność. Jeżeli jednym z wymagań jest elastyczna budowa i podział na warstwy, wtedy ten dodatkowy narzut jest narzutem, który musimy ponieść, aby zrealizować cel.

W tym podręczniku przekonamy się, że każdy system szablonów to pewna warstwa abstrakcji bez względu na to, z jakiego języka korzysta oraz że są one powszechnie stosowane nawet, jeśli nie nazywają się systemami szablonów.

Problem: przetwarzanie dedykowanego języka szablonów jest niewydajne.

Argument ten jest słuszny jedynie w przypadku systemów szablonów, które od początku do końca zajmują się samodzielnie przetwarzaniem takiego języka. Sęk w tym, że takie systemy można policzyć na palcach jednej ręki. Prawie wszystkie istniejące obecnie systemy szablonów, które oferują swój własny język, korzystają z pewnej sztuczki: zamiast wykonywać szablon od zera, kompilują go do postaci... kodu PHP i wykonują. Co więcej, kompilacja wykonywana jest tylko wtedy, gdy zmieniony zostanie oryginalny szablon źródłowy; w przeciwnym wypadku używana jest od razu zapisana na dysku skompilowana wersja, dokładnie tak samo jak w systemach korzystających z PHP. Różnice w wydajności sprowadzają się tylko do tego, kto taki szablon PHP potrafi napisać lepiej: programista czy komputer, a to już zależy od konkretnej implementacji.

Zakończenie

edytuj

Dowiedzieliśmy się już, czym są systemy szablonów i dlaczego są one takie ważne. Pora przejść od teorii do praktyki. Na początku zilustrujemy to, co zostało tu powiedziane, próbując napisać prosty, edukacyjny system szablonów. Pomoże nam to zrozumieć zasadę działania i kryjące się za tym mechanizmy. Następnie pokażemy wybrane trzy systemy szablonów oraz sposób ich użycia.

Prosty edukacyjny system szablonów

edytuj

Celem tego rozdziału jest zbudowanie prostego systemu szablonów, aby zapoznać się w praktyce z zasadami działania tego typu bibliotek. Nasz system opierać się będzie na języku PHP, gdyż budowa odpowiedniego kompilatora to temat, którym można by zapełnić całą książkę i który dość mocno odbiega od właściwego zagadnienia.

Projekt

edytuj

Zanim zaczniemy cokolwiek pisać, zastanówmy się, co system szablonów musi robić. W poprzednim rozdziale powiedzieliśmy, że jest to warstwa pośrednia między resztą aplikacji, a przeglądarką: musi odebrać dane z warstwy logiki biznesowej oraz na podstawie odpowiedniego szablonu wygenerować kod, który zostanie posłany do przeglądarki. Stąd znamy już pierwszą funkcjonalność: możliwość przekazania danych ze skryptu do szablonu.

Szablon jest plikiem, który musi być zlokalizowany gdzieś na dysku. Aplikacja WWW, przekazując dane, musi także wybrać określony szablon. Potrzebna jest nam zatem jakaś prosta konfiguracja, w której programista będzie mógł poinformować, gdzie na dysku zlokalizowane są szablony oraz ustawić ewentualne inne opcje. Na końcu warto pomyśleć o udostępnieniu jakichś helperów, które pozwolą twórcy szablonu skupić się na merytorycznej treści szablonu, zamiast na technicznych szczegółach każdego możliwego zadania.

Nasz projekt napiszemy w oparciu o programowanie obiektowe. Poniżej przedstawiona jest lista klas:

  1. Wikibooks\Tpl\Engine - główna klasa zarządzająca konfiguracją.
  2. Wikibooks\Tpl\View - klasa reprezentująca szablon z przypisanymi do niego danymi i umożliwiająca jego wykonanie, który na nasze potrzeby nazwiemy sobie widokiem.
  3. Wikibooks\Tpl\Helper - klasa z helperami.

Stosujemy się tutaj do standardu nazewnictwa i lokowania klas w plikach omówionego w rozdziale Automatyczne ładowanie.

Klasa Wikibooks\Tpl\Engine

edytuj

Pierwsza z klas nie będzie zbyt rozbudowana - jedynie, co będzie robić, to przechowywać konfigurację oraz służyć za fabrykę widoków.

<?php
namespace Wikibooks\Tpl;

class Engine
{
private $_templateDir;
private $_extension;
	
public function __construct($templateDir, $extension = 'php')
{
	$this->setTemplateDir($templateDir);
	$this->setExtension($extension);
} // end __construct();
	
public function setTemplateDir($dir)
{
	if(!is_dir($dir))
	{
	   throw new RuntimeException('Podany katalog szablonów '.$dir.' jest niedostępny.');
	}
	   if(strlen($dir) > 0 && $dir[strlen($dir) - 1] != '/')
	{
	$dir .= '/';
	}
	$this->_templateDir = $dir;
} // end setTemplateDir();
	
public function getTemplateDir()
{
 return $this->_templateDir;
} // end getTemplateDir();
	
public function setExtension($extension)
{
	if(!ctype_alnum($extension))
	{
	   throw new DomainException('Nazwa rozszerzenia może zawierać wyłącznie litery i cyfry.');
	}
	$this->_extension = $extension;
} // end setExtension();

public function getExtension()
{
	return $this->_extension;
} // end getExtension();

public function createView($viewName)
{
	return new View($viewName, $this->_templateDir.$viewName.'.'.$this->_extension);
} // end createView();
} // end Engine;

W tej klasie mamy cztery metody do zarządzania konfiguracją. setTemplateDir() pozwala ustawić programiście katalog, w którym przechowywane są szablony, zaś setExtension() pozwala wybrać domyślne rozszerzenie plików. Towarzyszą im odpowiednie gettery. Zgodnie z regułami programowania obiektowego nie upychamy całej funkcjonalności w jednej klasie, lecz rozdzielamy zadania między kilka mniejszych. Dlatego w tym miejscu jedyna rzecz, która ma coś więcej wspólnego z szablonami, to metoda createView() służąca za fabrykę widoków. W imieniu konkretnego widoku buduje mu odpowiednią ścieżkę do pliku, bazując na swojej konfiguracji.

Klasa Wikibooks\Tpl\View

edytuj

Klasa View reprezentować będzie pojedynczy szablon oraz przypisane do niego dane z warstwy logiki. Jeśli chcemy skomponować wyjście HTML z kilku mniejszych szablonów, np. aby mieć wspólny szablon z nagłówkiem i stopką strony, powinniśmy utworzyć dla każdego z nich odpowiednie widoki.

<?php
namespace Wikibooks\Tpl;

class View
{
	private $_template;
	private $_path;
	private $_data = array();

	public function __construct($template, $path)
	{
		$this->_template = $template;
		
		if(!file_exists($path))
		{
			throw new RuntimeException('Określony szablon '.$template.' nie istnieje.');
		}
		$this->_path = $path;
	} // end __construct();
	
	public function getTemplate()
	{
		return $this->_template;
	} // end getTemplate();
	
	public function __set($name, $value)
	{
		$this->_data[$name] = $value;
	} // end __set();
	
	public function __get($name)
	{
		return $this->_data;
	} // end __get();

	public function render()
	{
		extract($this->_data);
		require($this->_path);
	} // end render();
} // end View;

Każdy widok przechowuje informacje o szablonie, jaki reprezentuje, a także o danych, jakie już zdążyliśmy do niego przypisać. Służy do tego tablica $_data, a obsługiwana jest poprzez metody magiczne __set() i __get(). Gdy chcemy wyświetlić szablon, korzystamy z metody render(). Pojawia się w niej wywołanie funkcji extract(). Rozpakowuje ona zawartość tablicy jako zmienne, czyli np. z elementu foo powstanie nam zmienna $foo. Ułatwi nam to odwoływanie się do danych skryptu w szablonie. Ostatnia rzecz to operacja require i wykonanie szablonu. Jak widać, nie ma tu nic skomplikowanego.

Klasa Wikibooks\Tpl\Helpers

edytuj

Na koniec pozostało nam napisać kilka helperów. Będą one zapakowane jako metody statyczne w klasie Helpers:

<?php
namespace Wikibooks\Tpl;

class Helpers
{
	static public function linkTo($url, $title)
	{
		return '<a href="'.htmlspecialchars($url).'">'.$title.'</a>';
	} // end linkTo();
	
	static public function pluralPl($number, $singular, $plural1, $plural2)
	{
		if($number == 1)
		{
			return $singular;
		}
		elseif($number > 1 && $number < 5)
		{
			return $plural1;
		}
		return $plural2;
	} // end pluralPl();
} // end Helpers;

Omówienie:

  1. linkTo() - generuje odnośnik.
  2. pluralPl() - pozwala w prosty sposób utworzyć liczbę mnogą (aby móc wyświetlać np. "1 produkt"/"2 produkty").

Oczywiście jest to tylko zalążek - duże projekty korzystają z dziesiątek różnych helperów, np. do generowania formularzy, nawigacji i wielu innych elementów. Zauważmy, że nawet te, które podaliśmy, można by rozbudować. Przykładowo, do linkTo() moglibyśmy dorobić metodę, która pozwoli na automatyczne dodanie odpowiedniej klasy CSS wszystkim odnośnikom tworzonym za jej pomocą. Pozostawiamy to jako ćwiczenie.

Użycie

edytuj

Jak widać, nasz prosty system szablonów nie był zbyt trudny do napisania. Rzeczywiście, podstawowe mechanizmy stojące za tego typu skryptami nie są zbyt wyszukane i bazują na jedynie kilku elementach języka. Oczywiście jeśli chcielibyśmy dodać więcej funkcjonalności, stopień zaawansowania kodu znacznie by się skomplikował, ale nie jest to celem tego rozdziału.

Przyjrzyjmy się teraz, jak korzystać z naszego systemu. Na początku napiszemy szablon, który umieścimy w katalogu /templates:

<?php use \Wikibooks\Tpl\Helpers as Helpers; ?>
<html>
 <head>
  <title>Mój pierwszy szablon</title>
 </head>
 <body>
  <h1>Witaj,</h1>
  <p>Witaj, <?php echo $name; ?></p>
  <p>Aktualnie na stronie <?php echo Helpers::pluralPl($userNum, 'jest 1 użytkownik', <br />
'są '.$userNum.' użytkownicy', 'jest '.$userNum.' użytkowników'); ?>.</p>

  <?php if(is_array($products) && sizeof($products) > 0){ ?>
  <p>Co chciałbyś dziś kupić?</p>
  <ul>
  <?php foreach($products as $product){ ?>
    <li><?php echo $product['nazwa']; ?></li>
  <?php } ?>
  </ul>
  <p><?php echo Helpers::linkTo('zamowienie.php', 'Złóż zamówienie!'); ?></p>
  <?php }else{ ?>
  <p>Niestety nie mamy żadnych produktów do zaoferowania.</p>
  <?php } ?>
 </body>
</html>

W pierwszej linijce musieliśmy umieścić import klasy helperów z przestrzeni nazw, aby skrócić jej nazwę. Poza tym w pliku mamy niemal wyłącznie kod HTML z paroma wstawkami PHP. Widzimy, że wszystkie dane ze skryptu: $name, $userNum oraz $products zostały rozpakowane do postaci zmiennych. Helpery umożliwiły nam skrócenie niektórych typowych operacji. Do wyświetlenia listy produktów użyliśmy pętli foreach obudowanej warunkiem sprawdzającym czy nie jest ona pusta.

A oto i skrypt korzystający z tego szablonu. Dane pobieramy z tabeli produkty, którą poznaliśmy w rozdziale o bazach danych. Jeśli jej nie masz, skocz tam i zainstaluj ją. Zakładamy także, że w pliku SplClassLoader.php znajduje się automatyczna ładowarka klas SPL omówiona w rozdziale Automatyczne ładowanie.

<?php
require('./SplClassLoader.php');
$loader = new SplClassLoader('Wikibooks', './');
$loader->register();
header('Content-type: text/html;charset=utf-8');
try
{
	// Inicjujemy, co trzeba
	$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', 'root');
	$pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

	$tpl = new \Wikibooks\Tpl\Engine('./templates/');
	

	// Warstwa logiki aplikacji - pobieranie danych	
	$productList = array();
	$stmt = $pdo->query('SELECT * FROM produkty');
	while($row = $stmt->fetch(PDO::FETCH_ASSOC))
	{
		$productList[] = $row;
	}
	$stmt->closeCursor();
	
	// Przekazujemy dane do warstwy prezentacji
	$view = $tpl->createView('szablon');
	$view->products = $productList;	
	$view->name = 'Adam Kowalski';
	$view->userNum = rand(0, 10);
	
	// Wyświetlamy wszystko
	$view->render();
}
catch(Exception $exception)
{
	die('Wystąpił błąd: '.$exception->getMessage());
}

W naszym skrypcie znalazły się tylko instrukcje dotyczące przetwarzania danych. Jest on o wiele czytelniejszy niż gdyby ten sam kod HTML próbować umieścić tutaj. Możemy skupić się na samym procesie przetwarzania, bez zajmowania się wyświetlaniem. Jedyne co robimy, to kierujemy wszystko, co wygenerowaliśmy, do systemu szablonów.

Czy warto tworzyć własny system?

edytuj

Przedstawiony powyżej system szablonów jest bardzo prymitywny i z pewnością nie nadaje się do żadnych poważniejszych zastosowań. Programiści często zadają pytania czy lepiej użyć gotowej biblioteki czy też napisać własną. Za argument często podawana jest "zbyt duża funkcjonalność" gotowych rozwiązań, która budzi pewne przerażenie. Zwłaszcza początkujący mają tendencję do twierdzenia, że jest im ona niepotrzebna. Tymczasem rzeczywistość ma się zupełnie inaczej. Patrząc na artykuły w sieci oraz nawet na ten rozdział łatwo dojść do wniosku, że jedyne, co nam potrzeba do szczęścia, to if i foreach, jednak nic bardziej mylnego! Już na typowym internetowym blogu pojawia się wiele elementów, których wyrażanie za pomocą tych dwóch konstrukcji jest co najmniej męczące. Mamy stronicowanie, wszelkiego rodzaju elementy nawigacyjne, a ilość zależności "jeśli X, wyświetl Y" jest bardzo duża. Bazując jedynie na najprymitywniejszych elementach tracimy mnóstwo czasu na wymyślanie koła od zera, podczas gdy tzw. "gotowe" systemy szablonów oferują od razu gotowe, sprawdzone rozwiązania. Twierdzenie, że wystarczy mi podstawowa funkcjonalność to mit, na którym przejechało się już wiele osób. Nawet jeśli nie wykorzystamy wszystkiego, o wiele lepiej mieć w zanadrzu odpowiednie narzędzia, niż zorientować się, że klient oczekuje od nas czegoś, a my nie możemy mu tego dać, ponieważ nasze biblioteki są zbyt prymitywne. Jeśli chodzi o wydajność, w przypadku systemów szablonów nie do końca sprawdza się zasada większy znaczy wolniejszy. Jak wspomnieliśmy, wiele systemów szablonów to w rzeczywistości typowe API do przekazywania danych ze skryptu oraz kompilator, który jest ładowany tylko wtedy, gdy jest potrzebny. Duża funkcjonalność jest uzyskiwana praktycznie zerowym kosztem, a główna różnica wydajnościowa między takim prostym systemem, a gotowym pakietem będzie wynikać z różnych czasów ładowania samej biblioteki przez interpreter.

System szablonów warto napisać samodzielnie jedynie w dwóch sytuacjach:

  1. W celach edukacyjnych tak, jak to zrobiliśmy powyżej.
  2. Gdy faktycznie mamy ciekawy pomysł na system szablonów i chcemy go realizować.

Przy czym zadanie drugie jest znacznie trudniejsze zwłaszcza, gdy będziemy chcieli napisać własny kompilator. W większości przypadków skończy się to jedynie na udostępnieniu małego podzbioru PHP w postaci pętli i prostych warunków, który przy pierwszym poważniejszym zadaniu będzie bardziej ograniczał, niż pomagał. Najlepsze systemy szablonów powstawały latami rozwijane przez doświadczonych programistów i szansa, że zrobimy coś choć w połowie tak funkcjonalnego "z marszu", jest minimalna.

Zakończenie

edytuj

Poznaliśmy już, jak systemy szablonów działają w praktyce i jak są zbudowane, a także dowiedzieliśmy się, dlaczego w rzeczywistych projektach nie warto jest tworzyć ich samodzielnie. Pora zatem poznać kilka gotowych systemów.

Następny rozdział: Savant

Wybrane systemy szablonów

edytuj

W tym rozdziale pragniemy przedstawić bliżej wybrane gotowe systemy szablonów. Zostały one wybrane w taki sposób, by stworzyć jak najlepszy przekrój dostępnych rozwiązań, a jednocześnie by reprezentowały one najlepsze praktyki programistyczne i sposób podejścia do problemu.

Savant

edytuj

Savant to lekki, obiektowy system szablonów dla PHP 5. Wykorzystuje PHP jako język szablonów, dostarczając API do komunikacji ze skryptem oraz zestaw użytecznych helperów pomocnych np. przy generowaniu formularzy. Biblioteka jest rozwijana już od kilku lat, a najnowsze wersje wspierają również PHP 5.3.0. Programista dostaje do swojej dyspozycji elegancki, dobrze udokumentowany interfejs programisty, który można szybko opanować.

Open Power Template

edytuj

Open Power Template to obiektowy system szablonów dla PHP 5.2/5.3, wykorzystujący XML do tworzenia szablonów. Cecha szczególna to deklaratywne podejście do tworzenia szablonów: powiedz co chcesz wyświetlić, a nie jak to ma działać oraz udostępnienie dużej liczby pionierskich rozwiązań.

PHPTAL

edytuj

PHPTAL to implementacja języka szablonów Template Attribute Language z systemu szablonów Zope napisanego w Pythonie. Podobnie, jak w Open Power Template, język ten bazuje na XML-u. Implementacja obsługuje wszystkie elementy oryginału i udostępnia obiektowy interfejs programistyczny dla PHP 5.

Smarty

edytuj

Smarty to łatwy i prosty system szablonów który można się szybko nauczyć i wykorzystać. Pozwala na separację logiki aplikacji (PHP) od jej warstwy prezentacyjnej (HTML). Smarty działa poprzez umieszczanie w szablonach znaczników, które następnie są zastępowane generowaną przez aplikację treścią. Pakiet umożliwia stosowanie struktur kontrolnych (decyzyjnych, pętli, itp). System Smarty cechuje się wysoką wydajnością dzięki kompilowaniu szablonów do postaci skryptów PHP, a także wbudowanemu systemowi buforowania.


Inne systemy szablonów

edytuj

Powyższe systemy zostały szerzej opisane na dalszych stronach podręcznika, lecz oprócz nich w sieci można znaleźć wiele innych bibliotek prezentujących różny poziom i jakość.

 

Sekcja „PHP/Savant” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

Poprzedni rozdział: Savant
Następny rozdział: PHPTAL

Open Power Template

edytuj

Omówiony w poprzednim rozdziale Savant to doskonały przykład biblioteki wykorzystującej PHP jako język szablonów. W Open Power Template sytuacja odwraca się o 180 stopni. Ten system udostępnia swój własny, XML-owy język, którego cechą szczególną jest deklaratywne podejście do tworzenia szablonów. Zamiast skupiać się na szczegółach implementacyjnych, twórca szablonu powinien jedynie powiedzieć, co chce osiągnąć i zlecić kompilatorowi wygenerowanie odpowiedniego kodu, który spełni jego życzenia. Pomaga w tym rozumienie struktury HTML szablonu oraz unikalna cecha, tzw. formaty danych. Aby nie być gołosłownym, zaprezentujemy poniżej przykładowy szablon:

<?xml version="1.0" ?>
<opt:root>
<opt:prolog />
<opt:dtd template="html5" />
<html>
 <head>
  <title>{$pageTitle}</title>
 </head>
 <body>
  <p>Polecamy nasze produkty:</p>
  <ul>
   <li opt:section="products"><a parse:href="$products.url">{$products.name}</a></li>
  </ul>
 </body>
</html>
</opt:root>

Oprócz tego, Open Power Template udostępnia obiektowy interfejs programistyczny dla PHP 5.2/5.3 dostosowany do potrzeb integracji z popularnymi frameworkami, które niebawem poznamy. Do naszej dyspozycji są trzy najważniejsze elementy interfejsu:

  1. Opt_Class - główna klasa zarządzająca konfiguracją.
  2. Opt_View - klasa reprezentująca tzw. widok, czyli określony szablon i towarzyszące mu dane ze skryptu:
  3. Opt_Output_Interface - interfejs systemu wyjścia, który mówi, gdzie wysłać wygenerowany kod.

Podobnie jak w naszym prostym systemie edukacyjnym, nasza aplikacja WWW tworzy sobie odpowiednią liczbą widoków i przekazuje do nich dane. Jednak zamiast wykonywać je od razu, przekazuje je do jednego z systemów wyjścia, który zajmuje się wykonaniem szablonu i wysłaniem wyniku w określone miejsce. Przykładowo, jeśli skorzystamy z systemu wyjścia Opt_Output_Http, dane polecą do przeglądarki, a oprócz tego możemy też stworzyć sobie system wyjścia Email, który podany szablon potraktuje jako np. treść wiadomości e-mail, którą trzeba wysłać pod określony adres.

Instalacja

edytuj

Bibliotekę można pobrać ze strony www.invenzzia.org - pobieramy najnowsze wydanie z gałęzi 2.0.x, które jest omówione w tym podręczniku. W ściągniętym archiwum znajdziemy katalog /lib z właściwym kodem źródłowym biblioteki. Pozostałe katalogi to przykłady, dokumentacja oraz testy. Przenosimy wspomniany katalog gdzieś do drzewa katalogowego naszej aplikacji WWW.

Druga czynność to stworzenie folderów na szablony. Pierwszy z nich nazwijmy /templates - będą tam źródłowe wersje szablonów. W drugim, /templates_c Open Power Template będzie zapisywać skompilowane wersje, dlatego upewnijmy się, że PHP posiada do niego prawa zapisu.

Open Power Template jest częścią większej rodziny bibliotek Open Power Libs, dla których potrzeb opracowane zostało wspólne mikrojądro oferujące m.in. automatyczną ładowarkę czy system obsługi błędów. Znajdziemy je w katalogu /lib/Opl, podczas gdy właściwy kod systemu szablonów będzie w /lib/Opt. Gdybyśmy chcieli użyć w przyszłości innych bibliotek z tej rodziny, wystarczy wgrać je tuż obok, gdyż będą one korzystać z tego samego jądra.

Kolejna rzecz to ustawienie automatycznej ładowarki:

<?php
require('/sciezka/do/opl/Opl/Base.php');
Opl_Loader::setDirectory('/sciezka/do/opl/');
Opl_Loader::register();

Autoloader OPL ma charakter uniwersalny, tzn. można go także wykorzystać do ładowania innych bibliotek z kompatybilnym schematem nazewnictwa (np. Doctrine), aczkolwiek powyższy sposób jest dobry jedynie dla bibliotek OPL. Poniżej jest przedstawione bardziej uniwersalne podejście:

<?php
require('/sciezka/do/bibliotek/Opl/Base.php');
Opl_Loader::addLibrary('Opl', array('basePath' => '/sciezka/do/bibliotek/'));
Opl_Loader::addLibrary('Opt', array('basePath' => '/sciezka/do/bibliotek/'));
Opl_Loader::addLibrary('InnaBiblioteka', array('basePath' => '/sciezka/do/bibliotek/', 'handler' => null));
Opl_Loader::register();

Opcja handler ustawiona na null pozwala wyłączyć dodatkowe opcje ładowania klas specyficzne dla OPL-a, które mogą nie znaleźć zastosowania przy innych bibliotekach. I to wszystko. Jeśli poustawialiśmy dobrze ścieżki, możemy już zacząć korzystać z OPT.

Pierwszy szablon

edytuj

Przypomnijmy sobie jeszcze raz szablon z początku rozdziału:

<?xml version="1.0" ?>
<opt:root xmlns:opt="http://xml.invenzzia.org/opt">
<opt:prolog />
<opt:dtd template="html5" />
<html>
 <head>
  <title>{$pageTitle}</title>
 </head>
 <body>
  <p>Polecamy nasze produkty:</p>
  <ul>
   <li opt:section="products"><a parse:href="$products.url">{$products.name}</a></li>
  </ul>
 </body>
</html>
</opt:root>

Możemy tu zauważyć kilka charakterystycznych dla OPT zachowań. Szablon jest domyślnie traktowany jak dokument XML, dlatego powinien posiadać prolog <?xml version="1.0" ?>, a co więcej - jest on podany wyłącznie do wiadomości parsera. Jeśli chcemy wygenerować prolog w kodzie wyjściowym, korzystamy z instrukcji opt:prolog, która podana bez argumentów nadaje im domyślne wartości. Oprócz tego chcielibyśmy dodać odpowiedni DTD. Komendy DTD także idą na cele parsera biblioteki, stąd też bierze się instrukcja opt:dtd. Ma ona predefiniowanych kilka szablonów DTD dla najpopularniejszych języków, np. html5, dzięki czemu ich wstawianie jest łatwiejsze.

XML wymaga od nas jeszcze jednej ważnej rzeczy, mianowicie w dokumencie nie może być więcej niż jednego głównego znacznika. W tradycyjnym HTML-u rolę głównego znacznika pełnił element <html>, ale ponieważ mamy obok niego jeszcze dwa inne, OPT oddaje nam do dyspozycji opt:root. Oprócz bycia głównym znacznikiem, można w nim poustawiać kilka rzeczy odnoszących się do całego szablonu.

Jak widać, przejście na język XML-owy w momencie, gdy XHTML oraz HTML same albo wywodzą się, albo są podobne do niego, spowodowało konieczność specjalnego traktowania niektórych elementów, jednak w praktyce musimy je wykonać tylko raz, na samym początku. Gdy zaczniemy pracować z właściwą treścią, sytuacja znacząco się odwraca. Zerknijmy na linijkę 7. Pokazane jest tam, jak osadzać zmienne ze skryptu w tekście międzyznacznikowym. Używamy do tego klamerek, w którym zapisujemy nasze wyrażenie. Zapis ten oznacza, że tytuł strony zostanie załadowany ze zmiennej $pageTitle.

W linijce 12 pragniemy wyświetlić kilka produktów jako listę wypunktowaną. Do wyświetlania list służą tzw. sekcje, czyli rodzaj inteligentnych pętli. Jednak zamiast obudowywać element <li> pętlą, wystarczy że dokleimy do niego atrybut opt:section z podaną nazwą sekcji. Jest to prosty sposób powiedzenia: ten kawałek ma być użyty jako wzorzec do wyświetlenia pojedynczego elementu listy, przy czym nie obchodzi mnie teraz, jak ta lista będzie dokładnie działać. Nazwa sekcji umożliwia nam później dostęp do zmiennych elementu listy, np. {$products.name} - zauważmy, że do separacji służy nam kropka.

W tej samej linijce widać też, że aby wczytać zawartość atrybutu ze zmiennej, nie używamy klamerek, lecz do nazwy doklejamy parse:, a wyrażenie zapisujemy bezpośrednio w cudzysłowach. Plik zapisujemy do katalogu ./templates/.

A oto i kod PHP, który powyższy szablon uruchomi:

<?php
require('/sciezka/do/bibliotek/Opl/Base.php');
Opl_Loader::addLibrary('Opl', array('basePath' => '/sciezka/do/bibliotek/'));
Opl_Loader::addLibrary('Opt', array('basePath' => '/sciezka/do/bibliotek/'));
Opl_Loader::register();

$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', 'root');

try
{
   // Konfiguracja OPT
   $tpl = new Opt_Class;
   $tpl->sourceDir = './templates/';
   $tpl->compileDir = './templates_c/';
   $tpl->stripWhitespaces = false;
   $tpl->setup();

   // Tworzymy widok
   $view = new Opt_View('szablon.tpl');
   $view->pageTitle = 'Tytuł strony';

   $list = array();
   $stmt = $pdo->query('SELECT * FROM produkty');
   while($row = $stmt->fetch(PDO::FETCH_ASSOC))
   {
      $list[] = array(
         'name' => $row['nazwa'],
         'url' => 'produkt.php?id='.$row['id']
      );
   }
   $stmt->closeCursor();
   $view->products = $list;

   // Renderujemy
   $output = new Opt_Output_Http;
   $output->setContentType(Opt_Output_Http::HTML, 'utf-8');
   $output->render($view);
}
catch(Opt_Exception $exception)
{
   $handler = new Opt_ErrorHandler;
   $handler->display($exception);
}

Do raportowania błędów Open Power Template używa mechanizmu wyjątków, dlatego cały kod zawarty jest w bloku try...catch. Błędów nie musimy wyświetlać samodzielnie - do dyspozycji jest specjalna klasa Opt_ErrorHandler, która dodatkowo potrafi podawać różne informacje o błędzie, dzięki czemu łatwiej jest usunąć przyczynę. Sam skrypt składa się z trzech części. W pierwszej musimy skonfigurować bibliotekę, tworząc obiekt Opt_Class i ustawiając niektóre właściwości:

  1. sourceDir - katalog z szablonami źródłowymi (tj. ./templates/)
  2. compileDir - katalog ze skompilowanymi szablonami (tj. ./templates_c/)
  3. stripWhitespaces - czy usuwać ze źródeł niepotrzebne białe znaki, utrudniając analizę kodu wynikowego. Wartość false wyłącza usuwanie.

Po skonfigurowaniu wywołujemy metodę setup() i możemy zabrać się za tworzenie widoków. Obsługuje się je podobnie, jak w naszym edukacyjnym systemie z tą różnicą, że nie używamy tutaj metody fabrycznej, lecz samodzielnie tworzymy cały obiekt. W argumencie konstruktora podajemy nazwę szablonu, a później możemy do widoku przypisywać różne dane przy pomocy getterów i setterów. Przy okazji spójrzmy na linijki 23 do 33, gdzie wypełniana jest lista produktów dla sekcji. Lista jest zwykłą tablicą z elementami numerowanymi od zera, a każdy element jest tablicą asocjacyjną zawierającą kilka zmiennych.

Na samym końcu musimy szablon wykonać. W tym celu potrzebny jest nam jakiś system wyjścia, który zdecyduje, co zrobić z wynikiem. OPT dostarcza domyślnie dwóch takich systemów:

  1. Opt_Output_Return - zwraca kod jako wynik wykonania z powrotem do skryptu.
  2. Opt_Output_Http - wysyła kod do przeglądarki i udostępnia opcje zarządzania nagłówkami.

Skorzystamy z tego drugiego. Ma on dodatkową pomocniczą metodę setContentType(), która wysyła nagłówek Content-type z informacją dla przeglądarki, co dokładnie przysyłamy z serwera i ew. w jakim kodowaniu. Później wystarczy już tylko wywołać metodę render(), której podajemy za argument widok i to wszystko.

Sekcje

edytuj

Sekcje to rodzaj inteligentnych pętli i służą do wyświetlania różnego rodzaju list. Zacznijmy od wyświetlenia prostej listy produktów:

<?xml version="1.0" ?>
<opt:root xmlns:opt="http://xml.invenzzia.org/opt">
<h1>Lista produktów</h1>

<table class="list">
 <thead>
   <tr>
     <td>#</td>
     <td>Nazwa</td>
     <td>Cena</th>
   </tr>
 </thead>
 <tbody>
   <opt:section name="products">
   <tr>
      <td>{$products.id}</td>
      <td>{$products.name}</td>
      <td>{$products.price}</td>
   </tr>
   </opt:section>
 </tbody>
</table>
</opt:root>

Aby utworzyć sekcję, stosujemy znacznik opt:section pokazany w linii 14. Jego zawartość określa wygląd pojedynczego elementu listy. Każda sekcja musi posiadać swoją własną nazwę; w szablonie wykorzystujemy ją do odwoływania się do zmiennych elementu: $products.id, zaś w skrypcie posłuży ona do przypisania danych listy. Zauważmy, że w szablonie nie ma żadnych informacji odnośnie szczegółów implementacyjnych. Jest to jedna z charakterystycznych cech sekcji i nie tylko; w założeniu twórca szablonów powinien skupić się jedynie na końcowym efekcie bez zajmowania się tym, jak do niego dojść. Przyjrzyjmy się zatem, jak wygenerować dane dla naszej listy. Ponownie skorzystamy z pomocy bazy danych z poprzedniego rozdziału, zatem należy pamiętać o wcześniejszym połączeniu się z nią poprzez PDO.

$view = new Opt_View('product_list.tpl');
$products = array();
$stmt = $pdo->query('SELECT id, nazwa, cena FROM produkty ORDER BY id');
while($row = $stmt->fetch(PDO::FETCH_ASSOC))
{
   // Dodajemy nowy element
   $products[] = array(
      'id' => $row['id'],
      'name' => $row['nazwa'],
      'price' => $row['cena']
   );
}
$stmt->closeCursor();
// Dodaj listę do widoku
$view->products = $products;

Jak widzimy, pojedynczy element listy to zwykła tablica asocjacyjna, w której klucze odpowiadają indeksom użytym w szablonie. Aby utworzyć listę, wystarczy wszystkie elementy zgrupować w kolejną tablicę i przekazać do widoku pod nazwą identyczną, jak nazwa sekcji. Po uruchomieniu OPT automatycznie rozwinie je w podaną nam listę.

W szablonie nie mamy żadnych informacji o tym czy dane naszej listy umieszczone są w tablicy czy nie. Skąd zatem OPT wie, jak po niej iterować? Odpowiadają za to tzw. formaty danych. Jest to pewien rodzaj przepisu informujący OPT, jak radzić sobie z określonym rodzajem elementów. Przeróbmy nasz skrypt tak, aby umieszczał elementy w obiekcie klasy SplDoublyLinkedList (lista dwukierunkowa) i nauczmy OPT z niego korzystać:

$view = new Opt_View('product_list.tpl');
$products = new SplDoublyLinkedList;
$stmt = $pdo->query('SELECT id, nazwa, cena FROM produkty ORDER BY id');
while($row = $stmt->fetch(PDO::FETCH_ASSOC))
{
   // Dodajemy nowy element
   $products->push(array(
      'id' => $row['id'],
      'name' => $row['nazwa'],
      'price' => $row['cena']
   ));
}
$stmt->closeCursor();
// Dodaj listę do widoku
$view->products = $products;
$view->setFormat('products', 'SplDatastructure');

Widzimy, że obiekt SplDoublyLinkedList to coś zupełnie innego niż tablica. Okazuje się, że poinformowanie o tym OPT to kwestia dodania jednej linijki (nr 16). Metoda setFormat() ustawia format danych dla określonego elementu. SplDatastructure to format dostosowany do pracy z listami, stosami i kolejkami wchodzącymi w skład znanego nam już pakietu SPL.

Jeśli sekcja obejmuje pojedynczy znacznik HTML, nie musimy tworzyć całego znacznika, ponieważ mamy do dyspozycji skróconą formę atrybutową:

<tr opt:section="products">
   <td>{$products.id}</td>
   <td>{$products.name}</td>
   <td>{$products.price}</td>
</tr>

A co zrobić, gdy chcielibyśmy wyświetlić alternatywny tekst w razie otrzymania pustej listy? Pomoże nam znacznik opt:show:

<?xml version="1.0" ?>
<opt:root xmlns:opt="http://xml.invenzzia.org/opt">
<h1>Lista produktów</h1>
<opt:show name="products">
<table class="list">
 <thead>
   <tr>
     <td>#</td>
     <td>Nazwa</td>
     <td>Cena</th>
   </tr>
 </thead>
 <tbody>
   <opt:section>
   <tr>
      <td>{$products.id}</td>
      <td>{$products.name}</td>
      <td>{$products.price}</td>
   </tr>
   </opt:section>
 </tbody>
</table>
<opt:showelse>
 <p>Przykro nam, ale nie dodano jeszcze żadnych produktów.</p>
</opt:showelse>
</opt:show>
</opt:root>

Zauważmy, że w tym przypadku nazwa sekcji i inne ewentualne atrybuty trafiają do znacznika opt:show, pozostawiając opt:section pusty. Należy pamiętać o tej subtelności, ponieważ w przeciwnym razie efekty mogą być odmienne od zamierzonych.

Jednak prawdziwa elastyczność sekcji pojawia się dopiero przy próbie utworzenia list zagnieżdżonych. Pamiętamy, że w PHP i Savancie musieliśmy zajmować się tym samodzielnie, a wszelkie zmiany w strukturze danych generowanych przez aplikację zmuszały nas do przepisywania szablonów. W OPT wszystkimi detalami zajmują się formaty danych, my natomiast musimy jedynie umieścić jedną sekcję w drugiej i to wszystko. Wykorzystajmy tę właściwość do wypisania listy tagów skojarzonych z każdym produktem.

<?xml version="1.0" ?>
<opt:root xmlns:opt="http://xml.invenzzia.org/opt">
<h1>Lista produktów</h1>
<opt:show name="products">
<table class="list">
 <thead>
   <tr>
     <td>#</td>
     <td>Nazwa</td>
     <td>Cena</th>
   </tr>
 </thead>
 <tbody>
   <opt:section>
   <tr>
      <td>{$products.id}</td>
      <td><span class="name">{$products.name}</span>
        <span class="tags"><opt:section name="tags" str:separator=", ">
         <a parse:href="$tags.url">{$tags.name}</a>
        <opt:section></span>
      </td>
      <td>{$products.price}</td>
   </tr>
   </opt:section>
 </tbody>
</table>
</opt:show>
</opt:root>

Utworzenie sekcji zagnieżdżonej nie wymaga od nas absolutnie żadnej dodatkowej czynności oprócz osadzenie jednej sekcji w drugiej. Przy okazji przykład pokazuje inną ciekawą właściwość. Chcielibyśmy, aby nasze tagi były odseparowane przecinkiem, przy czym po ostatnim ma go już nie być. W tym celu dodajemy do sekcji atrybut str:separator. Przestrzeń nazw str informuje OPT, że wartość atrybutu to zwykły tekst, ponieważ domyślnie kompilator oczekuje wczytywania jego kształtu ze zmiennej lub innego wyrażenia.

Cały proces składania odbywać się będzie po stronie skryptu. Jednak ponieważ nasza baza danych nie zawiera tagów, zapiszemy je na sztywno w kodzie, a stworzenie dynamicznej listy tagów dla każdego produktu pozostawiamy jako ćwiczenie.

$view = new Opt_View('product_list.tpl');
$products = array();
$stmt = $pdo->query('SELECT id, nazwa, cena FROM produkty ORDER BY id');
while($row = $stmt->fetch(PDO::FETCH_ASSOC))
{
   // Dodajemy nowy element
   $products[] = array(
      'id' => $row['id'],
      'name' => $row['nazwa'],
      'price' => $row['cena'],
      // tagi dla tego produktu.
      'tags' => array(0 =>
         array('url' => '/tag1', 'name' => 'tag1'),
         array('url' => '/tag2', 'name' => 'tag2'),
         array('url' => '/tag3', 'name' => 'tag3'),
      );
   );
}
$stmt->closeCursor();
// Dodaj listę do widoku
$view->products = $products;
$view->setFormat('tags', 'SingleArray');

Ponieważ lista tagów jest zapisana dla każdego produktu z osobna, musimy poinformować o tym OPT, wybierając dla sekcji tags format danych SingleArray. Zachowanie domyślnego formatu jest nieco inne i wymaga od nas, aby sekcja tags posiadała swoją własną tablicę, lecz z podwójnym indeksowaniem (pierwsze - produkty; drugie - tagi konkretnego produktu).

Sekcje to bardzo wygodne narzędzie do wyświetlania wszekiego rodzaju list. Oprócz opt:section istnieją też trzy inne rodzaje sekcji:

  1. opt:selector - pozwala zdefiniować kilka różnych możliwych wyglądów dla elementów, które są wybierane na podstawie ich typu.
  2. opt:grid - wyświetlanie elementów w kolumnach z obsługą dopełniania ostatniego wiersza pustymi elementami.
  3. opt:tree - wyświetlanie drzew o dowolnej głębokości.

Wrócimy do nich w kolejnych rozdziałach.

Bloki i sortowanie listy produktów

edytuj

W panelach administracyjnych często spotyka się możliwość sortowania listy poprzez klikanie na nagłówkach kolumn. Choć Open Power Template nie dostarcza nam gotowego rozwiązania, udostępnia narzędzia, które umożliwią nam łatwą jego implementację. Są to tzw. bloki. Każdy blok składa się zawsze z dwóch elementów:

  1. Obiektu pewnej klasy PHP implementującej interfejs Opt_Block_Interface. Nazywa się on obiektem bloku.
  2. Grupy znaczników w szablonie, w których dany obiekt będzie uruchamiany. Nazywa się ona portem bloku.

Zaczniemy od umieszczenia w naszym szablonie stosownego portu:

<?xml version="1.0" ?>
<opt:root xmlns:opt="http://xml.invenzzia.org/opt">
<h1>Lista produktów</h1>
<opt:show name="products">
<table class="list">
 <thead>
   <tr>
     <td><opt:sort-list str:name="products:id" str:selected="sel">#</opt:sort-list></td>
     <td><opt:sort-list str:name="products:name" str:selected="sel">Nazwa</opt:sort-list></td>
     <td><opt:sort-list str:name="products:price" str:selected="sel">Cena</opt:sort-list></th>
   </tr>
 </thead>
 <!-- treść tabeli -->
</table>
</opt:show>
</opt:root>

Jak widać, port jest zwykłym znacznikiem. Jego nazwę wybraliśmy sami i niebawem poinformujemy OPT, że ma on go skojarzyć z odpowiednim typem bloków. Do portu możemy przekazać dowolną liczbę argumentów. U nas definiują one etykiety, po których rozpoznamy, jaką kolumnę sortujemy, a także klasy CSS, jakie należy użyć np. gdy dana kolumna jest wybrana.

Powyższy rodzaj portów to tzw. porty statyczne. OPT podczas wykonywania automatycznie utworzy dla nich odpowiedni obiekt danego typu. Oprócz tego, istnieją też porty dynamiczne, w których obiekt wczytywany jest ze zmiennej. Oznacza to, że możemy utworzyć taki obiekt po stronie skryptu, skonfigurować go tam, a następnie pchnąć do szablonu, gdzie zostanie odpalony:

<opt:block from="$obiektBloku" argument="wartość">
  ... treść ...
</opt:block>

Zanim zaczniemy implementować Opt_Block_Interface, napiszemy sobie interfejs, który pozwoli skryptowi na skonfigurowanie kolumn, po których będziemy sortować:

<?php
class Sorter
{
   const ASC = 0;
   const DESC = 1;

   private $_columns = array();
   private $_default = null;
   private $_defaultOrder = 0;

   private $_selected;
   private $_order;
   private $_url;

   static private $_sorters = array();

   public function __construct($name, $url)
   {
      $this->_sorters[$name] = $this;
      $this->_url = $url;
   } // end __construct();

   static public function get($name)
   {
      if(!isset(self::$_sorters[$name]))
      {
         throw new RuntimeException('Podany zestaw reguł sortowania: '.$name.' nie istnieje.');
      }
      return self::$_sorters[$name];
   } // end get();

   public function addColumn($id, $dbField)
   {
      $this->_columns[$id] = $dbField;
      if($this->_default === null)
      {
         $this->_default = $id;
      }
   } // end addColumn();

   public function setDefault($id, $order)
   {
      $this->_default = (string)$id;
      $this->_defaultOrder = (int)$order;
   } // end setDefault();

   public function process()
   {
      $this->_selected = $this->_default;
      // Pobierz z adresu URL informację o kolumnie, po której sortujemy.
      if(isset($_GET['col']))
      {
         if(isset($this->_columns[$_GET['col']))
         {
            $this->_selected = $_GET['col'];
         }
      }
      // Pobierz informację o kierunku sortowania
      if(isset($_GET['ord']))
      {
         if($_GET['ord'] == 0 || $_GET['ord'] == 1)
         {
            $this->_order = $_GET['ord'];
         }
      }
      // Zwróć kawałek zapytania SQL
      return $this->_columns[$this->_selected].' '.($this->_order == 0 ? 'ASC' : 'DESC');
   } // end process();

   public function isSelected($id)
   {
      if($this->_selected != $id)
      {
         // ten element nie został wybrany.
         return null;
      }
      return $this->_order;
   } // end isSelected();

   public function getUrl()
   {
      return $this->_url;
   } // end getUrl();
} // end Sorting;

Opis metod jest następujący:

  1. __construct() - tworzy nowy zestaw reguł sortowania.
  2. addColumn() - dodaje informację o nowej kolumnie. Pierwszy argument to nasz identyfikator, drugi - nazwa kolumny w zapytaniu SQL
  3. setDefault() - ustawia domyślne sortowanie.
  4. process() - wczytuje z adresu URL informacje o aktualnym sortowaniu i generuje kawałek zapytania SQL.
  5. isSelected() - metoda ta będzie używana przez nasz obiekt bloku do rozpoznania czy aktualna kolumna jest wybrana.
  6. get() - metoda statyczna zwracająca określony zestaw reguł na potrzeby obiektu bloku.

Teraz pora na klasę implementującą Opt_Block_Interface. Musimy w niej zaimplementować trzy metody:

  1. onOpen($attributes) - wywoływana w momencie otwarcia znacznika portu. Powinna zwrócić true, jeśli chcemy wyświetlić zawartość bloku.
  2. onClose() - wywoływana w momencie zamykania znacznika bloku.
  3. onSingle($attributes) - wywoływana, gdy mamy do czynienia z portem w znaczniku pojedynczym: <znacznik />.

Kod źródłowy:

<?php
class Sorter_Block implements Opt_Block_Interface
{
   private $_order;

   public function onOpen(array $attributes)
   {
      $data = explode(':', $attributes['name']);
      if(sizeof($data) != 2)
      {
         throw new DomainException('Nieprawidłowa nazwa bloku.');
      }
      $sorter = Sorter::get($data[0]);

      // Sprawdź czy sortujemy według tej kolumny
      $this->_order = $sorter->isSelected($data[1]);
      $url = $sorter->getUrl();
      $url .= (strpos($url, '?') !== null ? '?' : '&');

      // Dodaj trochę CSS-a i wygeneruj kod HTML
      $class = (isset($attributes['class']) ? $attributes['class'] : null);
      $selected = (isset($attributes['selected']) ? $attributes['selected'] : null);

      if($this->_order != null)
      {
         echo '<a href="'.$url.'col='.$data[1].'&ord='.(int)(!$this->_order).'" '.($selected !== null ? 'class="'.$selected.'" : '').'>';
      }
      else
      {
         echo '<a href="'.$url.'col='.$data[1].'&ord=0" '.($class !== null ? 'class="'.$class.'" : '').'>';
      }
      return true;
   } // end onOpen();

   public function onClose()
   {
      if($this->_order !== null)
      {
         if($this->_order == 0)
         {
            echo '↓</a>';
         }
         else
         {
            echo '↑</a>';
         }
      }
      else
      {
         echo '</a>';
      }
   } // end onClose();

   public function onSingle(array $arguments)
   {
      /* pusto */
   } // end onSingle();
} // end Sorter_Block;

Nasz mechanizm sortowania będzie przekazywać informacje o aktualnym sortowaniu za pośrednictwem dodatkowych argumentów w adresie URL. Dlatego nasz blok będzie generować wokół nazwy kolumny znacznik <a>, który po kliknięciu spowoduje posortowanie elementów według danej kolumny, a w przypadku już wybranej - odwróci kolejność sortowania. Oczywiście wybraną kolumnę należy wyróżnić odpowiednią klasą CSS oraz dodatkową strzałką pokazującą kierunek sortowania.

Zauważmy, że OPT nie zabrania nam generowania HTML-a przez kod PHP. Oczywiście generowanie w ten sposób większego kawałka kodu mijałoby się z celem i byłoby wyjątkowo nieczytelne, ale dla pojedynczych znaczników nie ma żadnych przeciwwskazań przeciwko programowaniu w ten sposób.

Pora na podłączenie naszego systemu sortowania pod listę. Dla zachowania czytelności pozbyliśmy się obsługi tagów:

$view = new Opt_View('product_list.tpl');

$sorter = new Sorter('products', 'product_list.php');
$sorter->addColumn('id', '`id`');
$sorter->addColumn('name', '`nazwa`');
$sorter->addColumn('price', '`cena`');

$products = array();
$stmt = $pdo->query('SELECT id, nazwa, cena FROM produkty ORDER BY '.$sorter->process());
while($row = $stmt->fetch(PDO::FETCH_ASSOC))
{
   // Dodajemy nowy element
   $products[] = array(
      'id' => $row['id'],
      'name' => $row['nazwa'],
      'price' => $row['cena'],
   );
}
$stmt->closeCursor();
// Dodaj listę do widoku
$view->products = $products;

Wygląda na to, że to już wszystko. Reguły sortowania są ustawione, odpowiedni kawałek zapytania SQL generowany, w szablonie oznaczone kolumny... jednak po uruchomieniu nagłówki kolumn znikają. Oczywiście - zapomnieliśmy poinformować OPT, że opt:sort-list jest blokiem. W przypadku takich nieznanych znaczników kompilator po prostu je ignoruje wraz z zawartością, stąd zniknięcie tekstu w nagłówkach. Musimy zarejestrować naszą klasę Sorter_Block w OPT, dlatego w momencie inicjowania biblioteki dodajemy:

$tpl = new Opt_Class;
$tpl->sourceDir = './templates/';
$tpl->compileDir = './templates_c/';
$tpl->stripWhitespaces = false;
$tpl->register(Opt_Class::OPT_BLOCK, 'opt:sort-list', 'Sorter_Block');
$tpl->setup();

Po ponownym skompilowaniu szablonów nasza lista będzie już mogła być sortowana po nagłówkach kolumn.

Przekonaliśmy się właśnie, że kluczem do efektywnego pisania szablonów jest przygotowanie sobie odpowiedniego zaplecza, bowiem większość napisanego przed chwilą kodu to rozmaite klasy PHP. Moglibyśmy osiągnąć identyczny efekt przy pomocy pętli oraz instrukcji warunkowych bezpośrednio w szablonie, ale przecież w prawdziwej aplikacji nie mamy jednej listy, tylko co najmniej kilkanaście. Dlatego tak ważne jest, aby poświęcić chwilkę czasu na zaprogramowanie obsługi takich fragmentów, jak sortowanie czy stronicowanie oraz aby system szablonów udostępniał odpowiednie narzędzia do ich osadzania w szablonach. W przypadku OPT są to bloki oraz działające na podobnej zasadzie komponenty, które mają jednak dużo bardziej rozbudowany interfejs dostosowany do wyświetlania formularzy. Zauważmy, że gdy przyjdzie nam tworzyć np. listę użytkowników, w szablonie będzie to już kwestia dodania dodatkowego znacznika w nagłówku każdej kolumny i nic więcej!

Zaawansowane sekcje i stronicowanie

edytuj

Upiększanie listy produktów

edytuj

Zakończenie

edytuj

W rozdziale tym pokazaliśmy, jak zbudować i obsłużyć szablon dla dynamicznej listy produktów przy pomocy biblioteki Open Power Template. Jest to jednak tylko wycinek jej możliwości, ponieważ jest ona projektowana, by poradzić sobie nawet z najtrudniejszymi wymaganiami. Filozofia tworzenia szablonów jest tutaj zupełnie inna, niż w poznanym wcześniej Savancie, dlatego nie powinniśmy tutaj polegać na swojej intuicji i przenosić tego, co funkcjonowało w PHP z nadzieją, że zadziała i tutaj. Zauważmy, że w podanych przykładach nie użyliśmy po stronie szablonów ani klasycznej pętli, ani zwykłej instrukcji warunkowej, chociaż oczywiście OPT udostępnia takie instrukcje, jak opt:if czy opt:foreach.

Następny i zarazem ostatni system szablonów, jaki poznamy, to PHPTAL. Jest on ogniwem pośrednim między OPT, a Savantem, z nowoczesnym językiem szablonów, który jednak jest nieco bardziej zbliżony do klasycznego programowania.

 

Sekcja „PHP/PHPTAL” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Ćwiczenia/Systemy_szablonów” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

Frameworki

edytuj
Poprzedni rozdział: Ćwiczenia

Czym jest framework?

edytuj

W tym rozdziale poznamy kolejną kategorię pakietów programistycznych zwanych frameworkami. Zadaniem frameworka jest dostarczenie szkieletu do tworzenia aplikacji (lub pewnej jej części), a także zbioru ogólnych funkcjonalności, które programista może rozszerzać, by dopasować je do konkretnych potrzeb. Framework może być rozumiany zatem jako szczególny przypadek biblioteki programistycznej, od której jednak odróżnia go kilka szczegółów:

  1. Odwrócenie sterowania - przepływ sterowania w aplikacji jest narzucany przez framework, nie przez programistę.
  2. Rozszerzalność - funkcjonalność frameworka może być rozszerzana przez programistę poprzez nadpisywanie lub dodawanie nowych elementów.
  3. Niemodyfikowalny kod - kod frameworka nie powinien być modyfikowany przez programistę podczas rozszerzania.

Przyjrzyjmy się, jak tworzyliśmy aplikacje WWW do tej pory. Dostawaliśmy do rąk język i musieliśmy samodzielnie zaprogramować całą strukturę aplikacji od początku. Nie chodziło tu tylko o napisanie kodu poszczególnych akcji, ale też rozwiązanie takich kwestii, jak moment ładowania konfiguracji, łączenie się z bazą danych, sposób generowania wynikowego kodu HTML itd. Zauważmy, że wymyślanie tego za każdym razem od nowa mija się z celem i jest wysoce nieefektywne. Przecież moglibyśmy wydzielić ogólną funkcjonalność, zapakować ją w zbiór funkcji i klas, a później na jej podstawie tworzyć kolejne aplikacje. Jeśli dokonaliśmy takiego podziału, brawo - stworzyliśmy właśnie framework.

Oczywiście taki framework byłby prawdopodobnie dość prymitywny, ale od strony technicznej spełnia wszystkie cechy tego rodzaju pakietów. Mamy ogólną funkcjonalność związaną z komunikacją z bazami danych czy obsługą formularzy, którą następnie dostosowujemy do potrzeb konkretnej aplikacji. Przepływ sterowania, czyli mechanizm wykonywania akcji, też najczęściej zaszyty będzie we wnętrzu frameworka. Możemy pójść jednak o krok dalej, przeanalizować strukturę istniejących aplikacji i zaprojektować framework od zera już jako framework, a nie jako pochodna procesu tworzenia aplikacji. I faktycznie - dla PHP istnieje wiele gotowych frameworków, które wystarczy ściągnąć i zastosować. To właśnie jest powodem ich wyjątkowej popularności. Firmy informatyczne nie muszą wydawać pieniędzy na opracowywanie autorskich rozwiązań, lecz po prostu mogą użyć dobrze zaprojektowane i przetestowane frameworki. Co więcej, oszczędzają w ten sposób pieniądze na szkoleniu nowych pracowników. Wystarczy w ogłoszeniu o pracę napisać, że wymagana jest znajomość frameworka XXX i nowy programista może niemal od razu zacząć pracę.

Co wchodzi w skład typowego frameworka?

edytuj

Mamy już ogólne pojęcie, czym framework jest i czym nie jest, pora zatem zastanowić się, w czym może nas framework wyręczyć. Podstawowe elementy większości frameworków to:

  1. Mechanizm uruchamiania i przetwarzania akcji.
  2. Mechanizm tworzenia logiki biznesowej aplikacji.
  3. Zarządzanie konfiguracją.
  4. Zarządzanie komunikacją z bazą danych.
  5. Obsługa formularzy.
  6. System szablonów.
  7. Obsługa błędów.
  8. Mechanizmy bezpieczeństwa, uwierzytelniania i kontroli dostępu.
  9. Generatory kodu.

Dlaczego powinienem używać frameworka?

edytuj

Framework to gotowe komponenty do budowy aplikacji WWW zaprojektowane przez doświadczonych programistów. Są one dobrze udokumentowane i dzięki temu łatwe w użyciu. Jako programista oszczędzasz czas na przysłowiowe "wymyślanie koła od nowa", ponieważ jedyne co musisz zrobić, to nauczyć się nimi posługiwać i możesz skupić się w całości na budowaniu aplikacji.

Ponadto dzięki ogólnej dostępności frameworków, programuje w nich wielu ludzi. Nowy programista w zespole, który umie korzystać z frameworka, na którym pracujesz, bez większych problemów zrozumie już napisany kod i będzie mógł od razu rozpocząć w nim pracę. Autorskie rozwiązania, zwłaszcza pisane przez początkujących, pełne są poważnych błędów projektowych i trudne w analizie, przez co przejęcie do dalszego rozwoju takiej aplikacji to koszmar.

Dlaczego nie powinienem używać frameworka?

edytuj

W przypadku większości typów aplikacji WWW nie ma żadnych przeciwwskazań do korzystania z gotowych frameworków. Mają one wszystko czego potrzeba i są dobrą szkołą programowania. Zdecydowana większość frameworków WWW napisanych w PHP (i nie tylko) działa według jednego i tego samego schematu, a różnice są minimalne. Problem pojawia się dopiero wtedy, gdy ten schemat z jakiegoś powodu nam nie odpowiada, ponieważ prawdopodobnie nie znajdziemy wtedy niczego, co spełniałoby nasze potrzeby.

Ogólnie mówiąc, sytuacja wygląda identycznie, jak z systemami szablonów i bibliotekami ORM. Aby napisać dobry framework, potrzebny jest czas i doświadczenie. Aby napisać dobry framework, który nie byłby powielaniem tego, co już zostało napisane, potrzebna jest dodatkowo kreatywność oraz umiejętność planowania. Te kryteria spełnia niewiele zespołów projektowych, dlatego lepiej jest nauczyć się korzystać z gotowych projektów.

Podsumowanie

edytuj

W następnym rozdziale poznamy złożone wzorce projektowe MVC, MVP oraz ich pochodne. Są one podstawą działania większości frameworków i dlatego ich zrozumienie jest niezbędne. Następnie napiszemy prosty, minimalistyczny framework oparty o wzorzec MVC, aby pokazać w praktyce zasadę jego działania, zaś na końcu omówimy trzy wybrane frameworki PHP i pokażemy, jak za ich pomocą zbudować blog internetowy.

 

Sekcja „PHP/Wzorce złożone: MVC, MVP i pochodne” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Prosty framework edukacyjny” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Wybrane frameworki” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Yii Framework” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Symfony Framework” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Zend Framework” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Studium_przypadku/Prosty_blog” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Ćwiczenia/Frameworki” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

Bezpieczeństwo

edytuj
 

Sekcja „PHP/Wstęp do zagadnień bezpieczeństwa” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Techniki ataków” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Zabezpieczanie sesji” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Bezpieczne zarządzanie danymi” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Kontrola formularzy” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Obrona przed botami” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Podstawy kryptografii” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Mechanizmy uwierzytelniania” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Mechanizmy kontroli uprawnień” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Połączenia szyfrowane” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Ćwiczenia/Bezpieczeństwo” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

Dobre praktyki

edytuj
 

Sekcja „PHP/Standardy kodowania” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Dokumentowanie kodu” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

 

Sekcja „PHP/Testowanie aplikacji” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

Archiwum

edytuj
Poprzedni rozdział: Operacje na plikach
Następny rozdział: Szyfrowanie

SQL Injection

edytuj

Atak typu SQL Injection polega na takiej zmianie jednego lub kilku parametrów zapytania (query) wysyłanego do bazy danych typu SQL, że polecenie to staje się niezgodne z zamierzeniem autora skryptu.

Załóżmy, że mamy stronę, która wyświetla dane klientów naszej firmy. Użytkownik po wejściu na stronę listaklientow.php otrzymuje listę klientów wyświetlająca wszystkich klientów firmy, którzy w bazie danych w kolumnie wyswietl mają wartość 1. Strona daneklienta.php, do której prowadzą łącza ze strony listaklientow.php wyświetla natomiast dane klienta dostarczone za pomocą zmiennej klient, a więc np. daneklienta.php?klient=Kowalski wyświetla dane dowolnego (ważne dla dalszej części przykładu) klienta o ID Kowalski. Co jednak, kiedy dane jakiegoś klienta nie powinny być oglądane przez niepowołane osoby? Administrator strony ustawia co prawda wartość 1 w kolumnie wyswietl bazy danych tylko dla określonych klientów, ale ktoś wpada na pomysł zrobienia czegoś takiego: daneklienta.php?klient=Tajny, gdzie klient o ID Tajny to klient, do którego profilu link nie jest wyświetlany przez plik listaklientow.php, jednak skrypt wyświetla dane klienta o dowolnym ID! Wtedy już mamy problem. Baza danych dostaje rozkaz wyświetlenia informacji dla ID Tajny, a skrypt jej tego nie zabrania. Właśnie wtedy mamy do czynienia z prostym SQL Injection.

Groźniejszym typem SQL Injection jest wypadek, w którym wspomniany ktoś (potencjalny cracker) wyśle w zmiennej klient instrukcję:

'; TRUNCATE TABLE klienci

I problem gotowy. Cała tabela klienci jest czyszczona, ponieważ zapytanie SQL wyglądało w sposób:

mysql_query("SELECT zamowienia FROM klienci WHERE id='". $_GET['id'] ."'");

Jak widać, apostrof (') jest zamykany i wydawane jest nowe, niebezpieczne polecenie SQL.

Jednakże najgroźniejsza sytuacja jest w formularzach logowania. Wystarczy, że cracker poda login:

' or 1=1 --

i ma dostęp do konta administratora.

Zabezpieczanie się przed SQL Injection

edytuj

Zabezpieczyć się przed atakiem typu SQL Injection można bardzo łatwo - wystarczy przefiltrować cudzysłowy oraz apostrofy z parametru wysyłanego do zapytania SQL. Wiele opcji hostingowych ma domyślnie wyłączony znienawidzony przez programistów parametr magic_quotes_gpc (dla serwerów Apache), która dodaje znak \ przed każdym potencjalnie niebezpiecznym znakiem parametru. Używając PDO, wystarczy używać funkcji prepare() i ustawiać zmienne. W starych funkcjach mysql_* można dokonać tego korzystając z funkcji mysql_escape_string(), która robi to samo, co wspomniane magic_quotes_gpc, np.

$id = mysql_escape_string($_GET['id']);

Jeśli parametr, który chcemy pobrać jest np liczbą, i chcemy żeby był wartością tylko tego typu możemy zastosować rzutowanie albo użyć funkcji intval, np.

$id = (int)$_GET["id"]; //Wersja z rzutowaniem
$id = intval($_GET["id"]); //Wersja z intval

Spowoduje to, że jeśli wpiszemy coś innego niż liczbę zostanie ona zamieniona na 0

Dodatkowe informacje

edytuj

Wartym odnotowana jest jeszcze przypadek, w którym mamy do czynienia z serwerami z włączonym magic_quotes_gpc(). Jeśli nie chcemy/nie możemy go wyłączyć, możemy skorzystać z funkcji stripslashes(), która działa odwrotnie do funkcji mysql_escape_string():

$id = stripslashes($_GET['id']);

PHP/Hashowanie

Poprzedni rozdział: Szyfrowanie
Następny rozdział: JS/HTML Injection

PHP Injection polega na dopisywaniu przez złośliwych użytkowników fragmentów kodu do przesyłanych zmiennych m.in. za pośrednictwem formularzy znajdujących się na stronach WWW.

Przed PHP Injection można zabezpieczyć się filtrowaniem przekazywanych parametrów - wygląda to dokładnie tak samo, jak w przypadku SQL Injection, jednak atak nie ma bezpośredniego (lub nie ma w ogóle) wpływu na bazę danych.

Przykładowy atak

edytuj

Załóżmy że użytkownik może wpisać swoje imię w formularzu, które zostanie wyświetlone. W formularzu jest jedynie pole do wpisania imienia, przesyłany metodą POST. Wyświetlanie:

<?php
$imie = $_POST['imie'];
echo("Witaj $imie !");
?>

Lecz jeżeli użytkownik wpisze:

<?php phpinfo(); ?>

To wygenerowany kod HTML będzie wyglądał tak:

Witaj <?php phpinfo(); ?> !


Jak się zabezpieczyć

edytuj

Ulepszenie poprzedniego skryptu:

<?php
$imie = htmlspecialchars($_POST['imie']);
echo("Witaj $imie !");
?>

I już atak tego typu nam nie straszny - ostre nawiasy zostaną zastąpione kodami &lt oraz &gt, co uniemożliwi wykonanie funkcji w ten sposób.

Poprzedni rozdział: Biblioteka PDO
Następny rozdział: Jak to się robiło kiedyś?

Przypomnijmy sobie najpierw wcześniejszy materiał. Używane były w nim tablice z użytkownikami.

Zaczniemy od utworzenia tabeli sesje (korzystając z bazy danych produkty).

CREATE TABLE `produkty`.`sesje` (
 `id` INT( 100 ) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
 `login` VARCHAR( 100 ) NOT NULL ,
 `haslo` VARCHAR( 1000 ) NOT NULL
 ) ENGINE = MYISAM ;

Teraz, kiedy mamy gotowy skrypt, musimy zmienić pierwszą część skryptu sesji - wszystko do komentarza Wlasciwy skrypt zamieniamy na:

<?php
	function czyIstnieje($login, $haslo)
	{
		
		$haslo = sha1($haslo);
		
   try
   {
      $pdo = new PDO('mysql:host=localhost;dbname=produkty', 'root', 'root');
      $pdo -> setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
      $stmt = $pdo -> query('SELECT id, login, haslo FROM sesje');
      foreach($stmt as $dane)
      {
 if($dane['login'] == $login && $dane['haslo'] == $haslo)
 {
 // O, jest ktos taki - zwroc jego ID
 return $dane['id'];
 /* Chcesz uzyc loginu uzytkownika? dodaj po inicjacji sesji
 $_SESSION['user'] = $user;
 oraz przed tym nawiasem klamrowym ponizej, ale po tym komentarzu
$user = $dane['user'];
 */
 }
      }
      $stmt -> closeCursor();
   }
   catch(PDOException $e)
   {
      echo 'Połączenie nie mogło zostać utworzone: ' . $e->getMessage();
   }

		
		// Jeżeli doszedłeś a tutaj, to takiego użytkownika nie ma
		return false;
	} // end czyIstnieje();

Aby utworzyć konta, należy utworzyć taki plik (pamiętaj, każdy może sobie utworzyć konto aż do kasacji):

 <?php

	try
	{
		if($_SERVER['REQUEST_METHOD'] == 'POST')
		{	
			$pdo = new PDO('mysql:host=localhost;dbname=produkty', 'root', 'root');
	      $stmt = $pdo -> query('SELECT id, login, haslo FROM sesje');
 $id = '1';
 foreach($stmt as $dane)
 {
 $id++;
 }
 $haslo = sha1($_POST['haslo']);
 $magic = get_magic_quotes_gpc();
 if ($magic != "1") $login = mysql_escape_string($_POST['login']);
      $stmt -> closeCursor();
			$add = $pdo -> exec('INSERT INTO `sesje` (`id`, `login`, `haslo`)	VALUES(
				\''.$id.'\',
				\''.$login.'\',
				\''.$haslo.'\')');
	
			if($add > 0)
			{
				echo 'Dodano? '.$ilosc;
			}
			else
			{
				echo 'Wystąpił błąd podczas dodawania rekordów!';
			}
		}
		else
		{
			echo '
			<form method="post" action="sesje_5.php">
			<p>Login: <input type="text" name="login"/></p>
			<p>Hasło: <input type="password" name="haslo"/></p>
			<p><input type="submit" value="Dodaj"/></p>
			</form>
			';	
		}
	}
	catch(PDOException $e)
	{
		echo 'Wystapil blad biblioteki PDO: ' . $e->getMessage();
	}
 ?>
Poprzedni rozdział: Czym jest system szablonów?
Następny rozdział: Open Power Template

Smarty

edytuj

Nasze praktyczne zmagania z systemami szablonów rozpoczniemy od biblioteki Smarty.

Instalacja

edytuj

W przeciwieństwie do dotąd objaśnianych zestawów funkcji oraz rozszerzeń, Smarty napisany jest w całości w PHP, dlatego musimy samodzielnie przeprowadzić proces jego instalacji. Nie jest on jednak trudny. W praktyce sprowadza się on jedynie do skopiowania gdzieś plików i dołączenia do skryptu jednego z nich. Dokładna procedura opisana jest poniżej:

  1. Wchodzimy na stronę http://www.smarty.net/ i pobieramy stamtąd najnowszą dostępną wersję (w chwili pisania tego tekstu - 2.6.16).
  2. Zakładamy, że nasze skrypty są w katalogu kursphp. Tworzymy w nim nowy podkatalog, np. smarty .
  3. Otwieramy ściągnięte archiwum i kopiujemy do naszego nowo stworzonego katalogu zawartość folderu libs.
  4. Aby załadować bibliotekę do naszej aplikacji, dołączamy plik Smarty.class.php.

Zanim zaczniemy, musimy jeszcze utworzyć dwa dodatkowe katalogi:

  1. /templates - tu trzymać będziemy nasze szablony. PHP musi mieć uprawnienia do odczytu.
  2. /templates_c - aby zwiększyć wydajność, Smarty najpierw kompiluje każdy szablon do postaci kodu PHP, a dopiero później go wykonuje. Raz skompilowany kod jest przechowywany na HDD w tym właśnie katalogu. Programista nie powinien tam nic zmieniać - po prostu należy przydzielić dla PHP prawa do zapisu i nic więcej.

Pierwszy skrypt

edytuj

Szablon to nic innego, jak plik z kodem HTML, który zawiera dodatkowe znaczniki określające, gdzie mają pojawić się dane ze skryptu. Nasz pierwszy szablon (szablon1.tpl) wygląda następująco:

<html>
 <head>
  <title>Smarty: pierwszy skrypt</title>
 </head>
 <body>
   <p>Hello world! Dzisiaj jest {$data}!</p>
 </body>
</html>

{$data} - ten fragment oznacza, że w tym miejscu ma pojawić się zawartość zmiennej $data. (Możliwe jest jednak zmiana systemu oznaczenia wprowadzania zmiennych Smarty. Są to dwa pola z klasy smarty z pliku libs/Smarty.class.php. var $left_delimiter, oraz var $right_delimiter) Jednak uważaj: to, że w szablonie stosujemy taką zmienną, nie znaczy wcale, że jeżeli stworzymy w naszym skrypcie analogiczną zmienną, to ją nam w tym miejscu wyświetli. Zmienne wewnątrz szablonów i zmienne PHP są dwiema zupełnie osobnymi rzeczami. Zmienne szablonowe należy utworzyć odpowiednią funkcją i przypisać im jakąś wartość ze skryptu. Popatrzmy więc, jak się to robi:

<?php

	require('./smarty/Smarty.class.php'); // 1
	
	$tpl = new Smarty; // 2
	$tpl -> template_dir = './templates/';
	$tpl -> compile_dir = './templates_c/';

	$tpl -> assign('data', date('d.m.Y')); // 3
	$tpl -> display('szablon1.tpl'); //4

?>

Opis skryptu (szablon1.php):

  1. Na początku ładujemy bibliotekę.
  2. Smarty ma budowę obiektową. Aby rozpocząć zabawę z szablonami, musimy utworzyć obiekt klasy Smarty i go skonfigurować. Najprostsza konfiguracja polega na określeniu ścieżek do katalogów z szablonami oraz ich skompilowanymi wersjami, ale dyrektyw jest znacznie więcej.
  3. Tutaj ustawiamy, jaką wartość ma mieć szablonowa zmienna $data.
  4. Kiedy wszystkie dane przenieśliśmy już do parsera, możemy nakazać mu przetworzenie konkretnego szablonu.

Po uruchomieniu powyższego skryptu zobaczysz, że na ekranie przeglądarki pojawił się napis "Hello world! Dzisiaj jest (tu aktualna data)!"

Więcej o zmiennych

edytuj

Smarty to coś więcej, niż zwykłe umieszczanie danych w kodzie HTML. W zasadzie po stronie szablonów mamy do dyspozycji całkiem rozbudowany język programowania obsługujący m.in. tablice i obiekty z PHP, a także rozmaite formy manipulacji danymi. Załóżmy, że mamy jakąś listę wiadomości, jednak z oczywistych przyczyn na stronie głównej pragniemy wyświetlić jedynie nagłówki i kilkanaście początkowych wyrazów. Możemy cały mechanizm przetwarzania zrealizować po stronie PHP, lecz z tym różnie bywa, zwłaszcza gdy kod PHP do pobierania listy wykorzystywany jest jeszcze w paru innych miejscach, gdzie taka właściwość jest niewskazana. To jednak nie problem, ponieważ możemy odpowiednie przycinanie dodać bezpośrednio w szablonie, który tego wymaga.

<?php

	require('./smarty/Smarty.class.php');
	
	$tpl = new Smarty;
	$tpl -> template_dir = './templates/';
	$tpl -> compile_dir = './templates_c/';

	$tpl -> assign('wiadomosc', array(
		'tytul' => 'Premier podaje się do dymisji!',
		'data' => date('d.m.Y'),
		'autor' => 'Jan Nowak',
		'tresc' => 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
		Cras nec diam. In hac habitasse platea dictumst. Donec id leo. Ut
		feugiat augue at metus. In hac habitasse platea dictumst. Donec
		pulvinar sollicitudin tellus. Quisque mattis faucibus nulla. Praesent
		in mauris. Maecenas erat nisi, laoreet in, porta nec, varius ut, turpis.
		Suspendisse pretium nibh at tellus placerat venenatis. Vestibulum
		ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia
		Curae; Etiam felis arcu, ringilla a, commodo quis, blandit id, est.
		Fusce nec sapien nec libero dignissim volutpat.'		
		));
	$tpl -> display('szablon2.tpl');

?>

Od strony skryptu za wiele nowego nie ma, poza faktem przypisywania do zmiennej szablonowej całej tablicy z danymi. Zwróćmy uwagę na długość oryginalnej wiadomości.

<html>
 <head>
  <title>SmartyNews!</title>
 </head>
 <body>
  <h3>{$wiadomosc.tytul}</h3>
  <p>Napisał {$wiadomosc.autor} dnia {$wiadomosc.data}</p>
  <p>{$wiadomosc.tresc|truncate:200:"..."}</p> 

 </body>
</html>

W tym przykładzie odwołujemy się do poszczególnych wartości w tablicy za pomocą kropki, po której podajemy nazwę indeksu. Smarty oferuje także alternatywną składnię, zbliżoną bardziej do PHP: $wiadomosc[tytul]. W ostatniej zmiennej, pojawiają się jeszcze dodatkowe znaki. Jest to tzw. modyfikator i służy, jak nazwa wskazuje, do końcowej obróbki danych, najczęściej związanej bezpośrednio z procesem wyświetlania. Tutaj nakazujemy przyciąć długość wiadomości do 200 znaków z zaokrągleniem do pełnych słów. Jeżeli wiadomość faktycznie była dłuższa, niż nakazujemy, na jej końcu mają być wstawione trzy kropki.

Sekcje

edytuj

W systemie Smarty sekcja jest jednym z rodzajów pętli. Najczęściej wykorzystuje się ją do tworzenia wszelkiego rodzaju list. Pokażemy teraz, jak wykorzystać omawianą bibliotekę do wygenerowania listy newsów, a następnie jak połączyć system szablonów oraz bazy danych, w których wykorzystujemy relację jeden do wielu.

Zacznijmy od listy. Tak wygląda szablon HTML listy newsów:

<html>
 <head>
  <title>SmartyNews!</title>
 </head>
 <body>
  {section name=i loop=$newsy}
  <h3>{$newsy[i].tytul}</h3>
  <p>Dnia {$newsy[i].data}</p>
  <p>{$newsy[i].tresc|truncate:200:"..."}</p> 
  {/section}
 </body>
</html>

Zwróćmy uwagę na dwa znaczniki: otwierający {section name=i loop=$newsy} oraz zamykający {/section}. Cały kod HTML między nimi jest powtarzany w kółko i służy za szablon pojedynczego newsa. W znaczniku otwierającym sekcję podajemy dwa parametry. Pierwszy z nich, name, określa nazwę sekcji. Identycznie będzie też nazywać się jej iterator wskazujący, na którym elemencie aktualnie jesteśmy. Drugi parametr to loop. Podajemy w nim nazwę zmiennej, w której mieści się tablica z danymi (możemy też podać cyfrę, np. loop=10 będzie oznaczało, że pętla wykona się 10 razy). Aby odwołać się np. do tytułu newsa w obrębie sekcji, stosujemy składnię {$newsy[i].tytul}, czyli najpierw nazwa zmiennej, w nawiasie kwadratowym podajemy iterator i po kropce dopiero nazwę zmiennej wewnętrznej, której wartość jest unikalna dla każdego elementu listy.

Tablica z danymi musi być w odpowiedni sposób wygenerowana po stronie PHP. Zobaczymy teraz skrypt, który się tym zajmuje.

<?php

	require('./smarty/Smarty.class.php');
	
	$tpl = new Smarty;
	$tpl -> template_dir = './templates/';
	$tpl -> compile_dir = './templates_c/';

	$newsy = array();

	for($i = 0; $i < 5; $i++)
	{
		$newsy[] = array(
				'tytul' => 'Tytuł wiadomości',
				'data' => date('d.m.Y'),
				'tresc' => 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
				Cras nec diam. In hac habitasse platea dictumst. Donec id leo. Ut
				feugiat augue at metus. In hac habitasse platea dictumst. Donec
				pulvinar sollicitudin tellus. Quisque mattis faucibus nulla. Praesent
				in mauris. Maecenas erat nisi, laoreet in, porta nec, varius ut, turpis.
				Suspendisse pretium nibh at tellus placerat venenatis. Vestibulum
				ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia
				Curae; Etiam felis arcu, ringilla a, commodo quis, blandit id, est.
				Fusce nec sapien nec libero dignissim volutpat.'	
			);
	}

	$tpl -> assign('newsy', $newsy);
	$tpl -> display('szablon3.tpl');

?>

Widzimy tutaj, że tablica $newsy zawiera mniejsze tablice asocjacyjne przypisujące konkretnym zmiennym wewnętrznym odpowiednie wartości. Następnie przekazujemy ją do parsera metodą assign(). Jako ćwiczenie spróbuj utworzyć taką tablicę, pobierając jej zawartość z bazy danych.

Sekcje można zagnieżdżać, dzięki czemu możliwe jest wyświetlanie danych w określonym porządku, np. kategorii oraz przypisanych do każdej z nich produktów. Wróćmy się do naszej bazy danych biblioteki z rozdziału o PHP Data Objects. Spróbujemy wyświetlić jej zawartość na ekranie, korzystając z pakietu Smarty. Jest to nieco trudniejsze, niż w wypadku zwykłych, płaskich list, ale również wykonalne. Rozpocznijmy od szablonu:

<html>
 <head>
  <title>Biblioteka</title>
 </head>
 <body>
  <ul>
  {section name=i loop=$kategorie}
   <li>{$kategorie[i].nazwa} <ul>
     {section name=j loop=$ksiazki[i]}
     <li>{$ksiazki[i][j].nazwa}</li>
     {/section}
   </ul></li>
  {/section}
  </ul>
 </body>
</html>

Zwróć uwagę, że w podrzędnej sekcji dotyczącej książek, musimy "podpiąć się" pod sekcję nadrzędną, aby Smarty wiedział, w jaki sposób powiązane są ich elementy. Odwołując się do rekordów książek, musimy podać w nawiasach kwadratowych najpierw iterator kategorii, a później książek.

Od strony PHP musimy przygotować dwie listy: po jednej dla kategorii i książek, z tym że druga musi uwzględniać istnienie dwóch indeksów identyfikujących rekordy. Przypomnij sobie, w jaki sposób wiązaliśmy te elementy w rozdziale poświęconym PDO - wykorzystaliśmy tam po prostu ID kategorii jako element wiążący, jednak w tym przypadku jest to niemożliwe. Gdybyśmy bowiem skasowali jakiś rekord, powstałaby dziura w numeracji, w którą sekcja z pewnością by się zaplątała, wyświetlając w tym miejscu pusty rekord. Musimy zatem użyć innej techniki. Zaproponujemy teraz nieco inne rozwiązanie tego problemu. Dane dla kategorii będziemy pobierać tak, jak poprzednio, natomiast dane książek posortujemy w pierwszej kolejności według nazw kategorii, a dopiero później według ich własnych tytułów. Zauważmy, że dzięki temu mamy zagwarantowane, że książki znajdujące się w tej samej kategorii znajdą się na liście wyników obok siebie. Teraz wystarczy podczas pobierania utrzymywać dwie zmienne: $i jako iterator oraz $kid - ID ostatniej znanej kategorii. Zanim wprowadzimy dane rekordu do tablicy, sprawdzamy prosty warunek: jeżeli $kid jest różne od identyfikatora kategorii, do której należy dana książka, to znaczy, że przeszliśmy już do książek z następnej kategorii i musimy w tym celu zwiększyć $i o 1, tak aby dane te trafiły do kolejnego rekordu. Dodatkowo zapamiętujemy ID nowej kategorii. Oto ilustracja tego algorytmu w kodzie PHP:

<?php

	require('./smarty/Smarty.class.php');
	
	$tpl = new Smarty;
	$tpl -> template_dir = './templates/';
	$tpl -> compile_dir = './templates_c/';

	$pdo = new PDO('mysql:host=localhost;dbname=podrecznik_php', 'root', 'root');

	$kategorie = array();
	$ksiazki = array();

	$stmt = $pdo -> query('SELECT id, nazwa, il_ksiazek FROM kategorie ORDER BY nazwa');
	while($row = $stmt -> fetch())
	{
		$kategorie[] = $row;
	}
	$stmt -> closeCursor();
	unset($stmt);

	$stmt = $pdo -> query('SELECT x.id, x.nazwa, k.id AS `kategoria_id` FROM ksiazki x, kategorie k
		WHERE k.id = x.kategoria_id ORDER BY k.nazwa, x.nazwa');
	$i = -1;
	$kid = 0;
	while($row = $stmt -> fetch())
	{
		if($row['kategoria_id'] != $kid)
		{
			$i++;
			$kid = $row['kategoria_id'];
		}

		$ksiazki[$i][] = $row;
	}
	$stmt -> closeCursor();

	$tpl -> assign('kategorie', $kategorie);
	$tpl -> assign('ksiazki', $ksiazki);
	$tpl -> display('szablon4.tpl');

?>

Sekcje w systemie Smarty mają duże możliwości i poznanie ich wszystkich wykracza poza ramy tego podręcznika. Więcej przykładów ich wykorzystania można znaleźć w dokumentacji biblioteki.

Instrukcje warunkowe

edytuj

Smarty posiada także instrukcję warunkową if, dzięki której można testować różne warunki i wyświetlać fragmenty kodu HTML warunkowo. Wróćmy do naszej listy newsów. Dodamy do każdego jej elementu nową zmienną: news_dnia. Jeżeli będzie ona ustawiona na 1, oznacza to, że mamy do czynienia z newsem dnia i wypadałoby to jakoś specjalnie zaznaczyć. Aby nie komplikować kodu, przyjmiemy w przykładzie, że newsem dnia jest pierwszy z elementów:

<?php

	require('./smarty/Smarty.class.php');
	
	$tpl = new Smarty;
	$tpl -> template_dir = './templates/';
	$tpl -> compile_dir = './templates_c/';

	$newsy = array();

	for($i = 0; $i < 5; $i++)
	{
		$newsy[] = array(
				'tytul' => 'Tytuł wiadomości',
				'data' => date('d.m.Y'),
				'tresc' => 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
				Cras nec diam. In hac habitasse platea dictumst. Donec id leo. Ut
				feugiat augue at metus. In hac habitasse platea dictumst. Donec
				pulvinar sollicitudin tellus. Quisque mattis faucibus nulla. Praesent
				in mauris. Maecenas erat nisi, laoreet in, porta nec, varius ut, turpis.
				Suspendisse pretium nibh at tellus placerat venenatis. Vestibulum
				ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia
				Curae; Etiam felis arcu, ringilla a, commodo quis, blandit id, est.
				Fusce nec sapien nec libero dignissim volutpat.',
				'news_dnia' => ($i == 0 ? 1 : 0)
			);
	}

	$tpl -> assign('newsy', $newsy);
	$tpl -> display('szablon5.tpl');

?>

I czas na szablon:

<html>
 <head>
  <title>SmartyNews!</title>
 </head>
 <body>
  {section name=i loop=$newsy}
  <h3>{$newsy[i].tytul}</h3>
  {if $newsy[i].news_dnia eq 1}
   <p><strong>News dnia!</strong></p>
  {/if}
  <p>Dnia {$newsy[i].data}</p>
  <p>{$newsy[i].tresc|truncate:200:"..."}</p> 
  {/section}
 </body>
</html>

Instrukcję warunkową tworzą znaczniki {if warunek} ... {/if}. Za warunek uznawane jest dowolne poprawne wyrażenie - jeżeli idzie Ci układanie takowych po stronie PHP, również w Smartym nie będziesz mieć z nimi problemów. Jedynie trzeba przyzwyczaić się do nowych operatorów. Biblioteka preferuje użycie tekstowych zamienników, aczkolwiek symboliczny zapis z PHP także jest dozwolony. Oto lista najważniejszych operatorów:

Smarty PHP
eq ==
neq, ne !=
gt >
lt <
ge, gte >=
le, lte <=
not !

Dostęp do zmiennych sesyjnych

edytuj

Z poziomu Smarty możemy mieć dostęp do zmiennych sesyjnych, które ustawiliśmy sobie z poziomu skryptów PHP. Dostęp do tych zmiennych odbywa się przez odwołanie do tablicy $smarty.session, np. $smarty.session.login_name.

Jak można to praktycznie wykorzystać, to już zupełnie inna sprawa. Przykładem może być tu proces logowania i nadawania uprawnień, tj. podczas logowania tworzymy w zmiennej sesyjnej obraz uprawnień nadanych użytkownikowi, a potem już z poziomu szablonu, bez konieczności przekazywania za każdym razem przez metodę assign(), możemy sterować tym, co pokazujemy użytkownikowi:

<?php
	// to jest power-user ustawiamy to
	$_SESSION['prawa_zapisu'] = 1;
?>

a potem w szablonie

{if $smarty.session.prawa_zapisu}
kod tylko dla power-usera
{/if}

W analogiczny sposób można odwoływać się do innych rodzajów danych wejściowych. Wartości pól formularza zapisane są w $smarty.post, z adresu URL w $smarty.get. Istnieje też możliwość dostania się do ciasteczek: $smarty.cookie. Znowu jednak sprawy bezpieczeństwa ... pod żadnym pozorem nie wolno na produkcyjnych, publicznych serwerach używać odwołań do typu $smarty.post czy $smarty.get ponieważ to zaproszenie do ataków XSS.

Zakończenie

edytuj

Biblioteka Smarty ma dużo większe możliwości, niż są w stanie pomieścić założenia tego podręcznika. W sieci znaleźć można dużo artykułów na temat tego systemu szablonów, także w języku polskim. Oprócz przetwarzania szablonów, Smarty posiada również zaawansowany moduł cache'owania wyników, szczególnie przydatny na stronach z dużym ruchem.

Przejdziemy teraz do omówienia alternatywnej biblioteki Open Power Template.

 

Sekcja „PHP/Inne/Konfiguracja PHP” znajduje się w budowie

 

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

Poprzedni rozdział: Konfiguracja PHP
Następny rozdział: Autorzy

< PHP

Prosimy nie traktować tej strony jako darmowej reklamy dla tych czy innych edytorów. Opisy niespełniające zasady neutralnego punktu widzenia są usuwane!


Choć kod PHP można tworzyć już w zwykłym notatniku, o wiele lepiej jest zaopatrzyć się w specjalny edytor wyposażony w wiele dodatkowych opcji, m.in. konwersję między systemami kodowań czy podświetlanie składni. Oto alfabetyczna lista edytorów PHP, zarówno tych darmowych, jak i komercyjnych. Jeżeli znasz jakiś warty umieszczenia, sporządź odpowiedni opis i dołącz tu. Prosimy jedynie pamiętać o zachowaniu zasady neutralnego punktu widzenia.


Wieloplatformowe

edytuj

Edytory dostępne dla systemów rodziny Windows, dla MacOS, systemów unikso-podobnych i czasem jeszcze innych.

Eclipse

edytuj

free software
Aplikacja rozwijana przez IBM. Za pomocą pluginów można ją rozszerzać o dowolne funkcjonalności. Eclipse jest aplikacją wieloplatformową napisaną w Javie. Dla PHP cały czas rozwijany jest specjalny plugin: PHPEclipse. Ma wbudowany parser PHP. Umożliwia m.in. debugowanie kodu, auto uzupełnianie i wiele innych. Przy pomocy Eclipse można także projektować bazy danych. W tym celu należy zainstalować plugin Azzurri Clay. Umożliwia on tworzenie projektów baz danych (PostgreSQL, MySQL). Narzędzie pracuje w trybie WYSIWYG.

Do Eclipse'a zostało napisanych bardzo dużo pluginów, pozwalających m.in. na obsługę AJAX, (X)HTML, CSS i innych. Program udostępniany na warunkach Eclipse Public License.

Więcej informacji na oficjalnej stronie programu i stronach domowych pluginów: PHPEclipse i Azzurri Clay

Eclipse PHP Development Tool

edytuj

free software
Narzędzie firmowane przez Zend - twórców języka PHP. Podświetla kod, zaznacza błędy w czasie rzeczywistym, wyświetla podpowiedzi (także dla własnych klas i funkcji, opisanych przy pomocy składni PHP Documentatora), umożliwia zaawansowane debugowanie (zatrzymywanie skryptu, sprawdzanie wartości zmiennych). Publikowane na Eclipse Public License.

Więcej informacji na stronie domowej programu.

NetBeans IDE

edytuj

free software
NetBeans IDE to wieloplatformowe, zintegrowane środowisko programistyczne napisane w Javie. Jest rozwijane przez firmę Sun Microsystems (Oracle) i obecnie posiada obsługę m.in. C/C++, baz danych, Java, Ruby, PHP i wiele innych. Wspiera także większość popularnych systemów kontroli wersji. NetBeans jest porównywany często do Eclipse, choć ma mniej możliwości konfiguracji, co dla niektórych może być zaletą, gdyż przez to IDE jest mniej złożone oferując jednocześnie całą potrzebną funkcjonalność. Dla developerów PHP istnieje specjalnie przygotowana wersja, którą można pobrać z tej strony. Aktualna, stabilna wersja środowiska to 7.0. Od tej wersji IDE posiada pełne wsparcie dla systemu kontroli wersji GIT, podpowiadanie znaczników dla HTML5 i trochę mniejszych zmian.

W poprzednich wersjach środowiska zaimplementowano m. in. obsługę frameworka Symfony i zend framework, usprawnioną integrację z MySQL oraz jeszcze lepszą obsługę testów PHPUnit. Wsparcie dla PHP w NetBeans jest ciągle ulepszane o czym zawsze można przeczytać na NetBeans for PHP Weblog
Więcej informacji na stronie NetBeans PHP Development.

free software
Edytor ten jest bardzo znany w środowisku użytkowników Linuksa. Pracuje w trybie tekstowym i obsługuje się go za pomocą klawiatury (można też używać myszki, jeśli nasz terminal ją obsługuje), ale posiada za to rewelacyjne możliwości kolorowania składni oraz personalizacji. Osoby korzystające z graficznych interfejsów użytkownika mogą używać gVima (wykorzystującego GTK), posiadającego prosty interfejs okienkowy oraz możliwość używania myszki. Program wydawany na licencji Vima zgodnej z GNU GPL.

Dostępne wersje dla systemów: unikso-podobnych, MS-DOS i MS-Windows, AmigaOS, OS/2, MacOS, MorphOS i kilku innych.

Więcej informacji na oficjalnej stronie programu.

Zend Studio

edytuj

komercyjny
Bardzo dobry edytor PHP dla zaawansowanych programistów sieciowych. Ma wiele funkcji ułatwiających pracę na dużymi projektami m.in.: auto uzupełnianie kodu, debugger dla skryptów PHP. Wbudowany w program serwer daje możliwość analizowania skryptów na swoim komputerze. Program przeznaczony jest dla dobrze znających język PHP programistów sieciowych.

Więcej informacji na oficjalnej stronie programu.


Unikso-podobne

edytuj

Edytory pod systemy Linuksowe, GNU, *BSD i wszelkie inne systemy unikso-podobne.

Bluefish

edytuj

free software
Autorzy Bluefisha chwalą się, że ich program używa 40-45% mniej zasobów od konkurencji. Pozwala otworzyć naraz ponad 500 dokumentów. Zapewnia kolorowanie składni m.in. dla HTML, PHP, Java, JavaScript, SQL, XML, Python, Perl, CSS. Edytor jest na licencji GNU GPL. Istnieją wersje dla Linuksa, FreeBSD, MacOS-X, OpenBSD, Solaris.

Więcej informacji na oficjalnej stronie programu.

free software
Kde Advanced Text Editor jest wygodnym edytorem tekstowym pozwalającym na wygodną pracę z dziesiątkami plików jednocześnie. Posiada obsługę plików przez ftp, zarządzanie projektami, kolorowanie składni częściej używanych języków, wybór stron kodowych, makra, obsługę wyrażeń regularnych i wiele innych udogodnień. Jest on dostępny w większości dystrybucji. Jest sztandarowym edytorem środowiska graficznego KDE. Kate jest edytorem opublikowanym na licencji GNU LGPL.

Więcej informacji na stronie oficjalnej edytora.

Quanta Plus

edytuj

free software
Zaawansowane środowisko programistyczne działające w systemach unikso-podobnych (środowisko KDE). Posiada wiele funkcji przydatnych przy pisaniu dużych aplikacji takich jak zarządzanie projektami, zakładki w kodzie, wbudowany debugger, przeglądarkę dokumentacji PHP, auto uzupełnianie i wiele innych. Program udostępniany na warunkach GNU GPL.

Więcej informacji na oficjalnej stronie programu.


Windows

edytuj

Edytory przeznaczone dla systemów Windows.

Crimson Editor

edytuj

freeware (free software?)
Zaawansowany i wygodny w obsłudze edytor, umożliwia m. in.: obsługę FTP, zaznaczanie kolumn, definiowanie własnych makr, podpinanie zewnętrznych programów (np. kompilatorów), zarządzanie projektami. Zauważone błędy: słaba obsługa dużych plików (> 0.5 MB), słaba obsługa otwierania plików w otoczeniu sieciowym w Windows 2000, "gryzie się" z niektórymi wersjami pgAdmina. Zapewnia kolorowanie składni m.in. dla HTML-a, PHP, Javy, JavaScriptu, SQL-a, XML-a, Pythona, Perla, CSS, ASP, Fortrana, LaTeX-a i wielu innych. Wersja darmowa (freeware, choć z dopiskiem Now it is open source zostały opublikowane źródła niewydanej oficjalnie wersji 3.71) do wszelakich zastosowań. Sporym mankamentem jest istnienie wersji tylko dla Windows.

Więcej informacji na oficjalnej stronie programu.

Delphi for PHP

edytuj

shareware
Środowisko Delphi jest potężnym RAD-em, bardzo przyjemnym w użytkowaniu. Wersja dla PHP została wydana 22 lipca 2007. Możliwe pobranie 30-dniowego triala.

Więcej informacji na oficjalnej stronie programu, polskim cenniku.

Dev-PHP IDE

edytuj

free software
Dev-PHP ma ciekawą funkcję podświetlania składni języka, w którym aktualnie piszemy. Dev-PHP potrafi podświetlać składnię w HTML, JS, CSS, PHP, MySQL oraz XML. Bez żadnych innych edytorów, program ten świetnie nadaje się do pisania kompletnych stron WWW. Dev-PHP posiada także funkcję numerowanie linii, podpowiedzi (Dev-PHP podpowiada, jakie parametry trzeba przekazać do poszczególnych funkcji). Dev-PHP oferuje także parsowanie skryptu PHP przez wskazany interpreter. Program ten również wspiera pisanie interfejsów graficznych z wykorzystaniem PHP-GTK. Program dostępny na licencji GNU GPL, niestety tylko na systemy Windows.

Więcej informacji na oficjalnej stronie programu.

Dreamweaver

edytuj

shareware
Znany na całym świecie, potężny program firmy Adobe, umożliwiający pisanie rozbudowanych stron HTML, a także łatwe tworzenie aplikacji internetowych w popularnych językach programowania skryptowego, m.in. PHP i ASP. Posiada takie udogodnienia jak: kolorowanie składni, numerowanie wierszy, auto uzupełnianie kodu. Program dostępny jest w angielskiej wersji językowej i jest płatny. Istnieją wersje pod Windows i MacOS.

30-dniowy trial można pobrać ze strony producenta.

freeware
Darmowy edytor stron internetowych wspomagający tworzenie stron z zastosowaniem HTML, CSS, JavaScript, PHP. Instaluje zintegrowany z programem serwer WWW Apache oraz parser języka PHP, co w znaczący sposób przyspiesza i ułatwia pisanie dokumentów internetowych. Dla ułatwienia i przyspieszenia generowania kodu istnieje system podpowiedzi dla HTML, CSS, JavaScript i PHP.

Więcej informacji na oficjalnej stronie programu.

HateML Pro

edytuj

freeware
Godny uwagi pretendent do ulubionego narzędzia zawodowego webmastera. Oprócz standardowych funkcji edytorów tego typu (kolorowanie, wstawianie tagów itp.), wyróżnia się kilkoma przemyślanymi rozwiązaniami (np. podgląd bazy danych na serwerze MySQL - bardzo pomocne przy edycji skryptów PHP, podgląd i edycja atrybutów i wartości dla każdego tagu w aktualnych dokumencie – włącznie ze zdarzeniami itp., otwieranie plików, na których aktualnie znajduje się kursor - świetna funkcja w przypadku dołączonych w skrypcie innych plików, biblioteka plików dołączonych w bieżącym dokumencie i wiele innych).

Więcej informacji na oficjalnej stronie programu.

freeware
Polski edytor, nadaje się zarówno dla webmasterów, jak i dla programistów. Pozwala na edycję programów i dokumentów w językach: HTML, PHP, Python, Assembler, Perl, Pascal, C++, Java. Można w nim również tworzyć skrypty VBScript i JavaScript, lub korzystać z gotowej biblioteki skryptów i programów. Istnieje także możliwość edycji plików ini oraz zwykłych tekstów w formacie txt. Program posiada gotowe fragmenty skryptów i składni, dzięki czemu nawet początkujący programiści i webmasterzy utworzą swój własny serwis lub program. W edytorze zawarte zostały kursy oraz linki do kursów. Program edytuje również kaskadowe arkusze stylów CSS.

Program nie jest rozwijany, można go pobrać z działu download serwisu internetowego pisma Komputer ŚWIAT.

Notatnik SP

edytuj

freeware
Uniwersalny edytor (najprawdopodobniej w zamierzeniu programistyczny) z kolorowaniem składni dla wielu języków programowania, nada się również doskonale do PHP. Niestety nie można w jednym oknie programu pracować z wieloma plikami. Innym problemem jest to, że program rozpoczyna numerowanie składni od wiersza nr 0, co może utrudnić wyszukiwanie błędów na podstawie komunikatów parsera. Posiada rozległe opcje "przerabiania" tekstu (usuwanie pustych wierszy, zamiana na małe litery itp). Niestety nie da się zapisać pliku o kodowaniu UTF-8. Duże pliki (np. binarne) otwiera szybko, nie "zawieszając" się.

Strona producenta jest nieaktualna, program można ściągnąć ze strony programypc.pl.

Notepad ++

edytuj

free software
Darmowy, uniwersalny edytor o otwartym kodzie, oferujący podświetlanie składni niemal 40 języków programowania. W oknie programu można uruchomić wiele (dziesiątki czy nawet setki przy odpowiedniej ilości RAM) osobnych plików, a także pracować w trybie sklonowanego widoku. Obsługuje kodowania UTF-8, UCS-2, ANSI (windows-1250), niestety brak wsparcia dla Latin2 (iso-8859-2). Na stronie programu dostępnych jest też ponad 20 pluginów oraz spolszczenie. Program udostępniany na licencji GNU GPL.
Pliki instalacyjne przygotowywane są dla Windows, istnieje możliwość uruchomienia także na Linuksie za pomocą Wine'a.

Więcej informacji na oficjalnej stronie programu.

Pajączek

edytuj

shareware
Pajączek to rozbudowany polski edytor WWW napisany przez firmę Cream Software. Program jest płatny, ale najnowsza wersja NxG umożliwia bezproblemową współpracę z PHP - posiada m.in. dołączony manual PHP. Zaletą aplikacji jest kilka predefiniowanych ustawień okienek dialogowych – czy wolimy mieć wszystkie pokazane, czy np. wszystkie ukryte, mając więcej miejsca na kod.

Więcej informacji na oficjalnej stronie programu.

PHP Designer

edytuj

shareware
PHP Designer jest zaawansowanym edytorem przystosowanym zarówno do edycji, debugowania, analizowania i publikowania skryptów PHP. Oprócz kolorowania składni umożliwia auto uzupełnianie kodu, jest przystosowany do edycji wielu innych technologii wykorzystywanych na stronach WWW. Edytor wyposażony jest również w możliwość wstawiania gotowych struktur kontrolnych, serwerowych zmiennych PHP wraz z opisem i możliwość dołączenia Manuala PHP.

Więcej informacji na oficjalnej stronie programu.

freeware
PSPad to czeski darmowy edytor przeznaczony do programowania w różnych językach, m.in. PHP. Dostępny jest także w polskiej wersji językowej. Posiada kolorowanie składni, dobrą obsługę tabulacji oraz przyjemny w użyciu mechanizm konwersji między różnymi kodowaniami (wspierane m.in. ISO-8859-2, Windows-1250 i Unicode). Wady to istnienie kilku kombinacji klawiszy identycznych, jak te do wstawiania niektórych polskich znaków. Należy je samemu zlokalizować i usunąć z konfiguracji. Dodatkowo, jeżeli z programu chcą na tym samym komputerze korzystać dwie osoby, każda musi zainstalować swą własną kopię, ponieważ PSPad nie przewiduje możliwości personalizacji ustawień.

Więcej informacji na oficjalnej stronie edytora.

WebSite Pro

edytuj

freeware
Jest to polski, prosty edytor stron posiadający kolorowanie składni m.in. php. Doskonale nadaje się do szybkiej edycji bądź pisania prostych skryptów.

Autor porzucił tworzenie tego edytora i na oficjalnej stronie programu dostępny jest już jedynie klucz potrzebny do korzystania z niego, natomiast ściągnąć go można ze strony dobreprogramy.pl.

freeware
Jest to polski w pełni darmowy edytor przeznaczony do XHTML, CSS, PHP i innych. Koloruje składnię dokumentów, konwertuje strony kodowe (Windows-1250, ISO-8859-2, UTF-8, UTF-16), ułatwia wstawianie tabel, grafik, odsyłaczy, umożliwia współpracę z walidatorem Tidy oraz lokalnym serwerem WWW; zawiera spis znaczników wraz z ich atrybutami, a ponadto pełną listę właściwości CSS2 i listę kilkudziesięciu funkcji PHP. Wersja instalacyjna (ma domyślnie włączone automatyczne zapisywanie konfiguracji, zawiera walidator Tidy oraz umożliwia skojarzenie z kEDem plików htm, html, php, css, xml, js i vbs). Autor porzucił tworzenie tego edytora ze względu na brak wolnego czasu i środków. Na oficjalnej stronie programu od niedawna została usunięta możliwość download-u lecz można go pobrać z tego mirroru.

Edytory przeznaczone dla systemów Apple'a.

Bluefish

edytuj

free software
Autorzy Bluefisha chwalą się, że ich program używa 40-45% mniej zasobów od konkurencji. Pozwala otworzyć naraz ponad 500 dokumentów. Zapewnia kolorowanie składni m.in. dla HTML, PHP, Java, JavaScript, SQL, XML, Python, Perl, CSS. Edytor jest na licencji GNU GPL. Istnieją wersje dla Linuksa, FreeBSD, MacOS-X, OpenBSD, Solaris.

Więcej informacji na oficjalnej stronie programu.

Dreamweaver

edytuj

shareware
Znany na całym świecie, potężny program firmy Adobe, umożliwiający pisanie rozbudowanych stron HTML, a także łatwe tworzenie aplikacji internetowych w popularnych językach programowania skryptowego, m.in. PHP i ASP. Posiada takie udogodnienia jak: kolorowanie składni, numerowanie wierszy, auto uzupełnianie kodu. Program dostępny jest w angielskiej wersji językowej i jest płatny. Istnieją wersje pod Windows i MacOS.

30-dniowy trial można pobrać ze strony producenta.

Poprzedni rozdział: Edytory PHP
Następny rozdział: Dla twórców podręcznika

Autorzy

edytuj

Choć Wikibooks może edytować każdy, istnieje pewna grupa ludzi aktywnie opiekujących się tym podręcznikiem przez cały proces jego powstawania. Nie tylko stworzyli wiele rozdziałów, ale także dbali o jednolitość całości oraz poprawność. W grupie tej znajduje się aktualnie jedna osoba:

Jeżeli chcesz pomóc, po prostu włącz się do akcji. Dotychczasowi członkowie grupy udzielą wszystkich wskazówek na temat edycji.

GNU Free Documentation License

edytuj

Version 1.2, November 2002

Copyright (C) 2000,2001,2002  Free Software Foundation, Inc.
51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.

0. PREAMBLE

edytuj

The purpose of this License is to make a manual, textbook, or other functional and useful document "free" in the sense of freedom: to assure everyone the effective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modifications made by others.

This License is a kind of "copyleft", which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software.

We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference.

1. APPLICABILITY AND DEFINITIONS

edytuj

This License applies to any manual or other work, in any medium, that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. Such a notice grants a world-wide, royalty-free license, unlimited in duration, to use that work under the conditions stated herein. The "Document", below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as "you". You accept the license if you copy, modify or distribute the work in a way requiring permission under copyright law.

A "Modified Version" of the Document means any work containing the Document or a portion of it, either copied verbatim, or with modifications and/or translated into another language.

A "Secondary Section" is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Document's overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (Thus, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical or political position regarding them.

The "Invariant Sections" are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License. If a section does not fit the above definition of Secondary then it is not allowed to be designated as Invariant. The Document may contain zero Invariant Sections. If the Document does not identify any Invariant Sections then there are none.

The "Cover Texts" are certain short passages of text that are listed, as Front-Cover Texts or Back-Cover Texts, in the notice that says that the Document is released under this License. A Front-Cover Text may be at most 5 words, and a Back-Cover Text may be at most 25 words.

A "Transparent" copy of the Document means a machine-readable copy, represented in a format whose specification is available to the general public, that is suitable for revising the document straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup, or absence of markup, has been arranged to thwart or discourage subsequent modification by readers is not Transparent. An image format is not Transparent if used for any substantial amount of text. A copy that is not "Transparent" is called "Opaque".

Examples of suitable formats for Transparent copies include plain ASCII without markup, Texinfo input format, LaTeX input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML, PostScript or PDF designed for human modification. Examples of transparent image formats include PNG, XCF and JPG. Opaque formats include proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML, PostScript or PDF produced by some word processors for output purposes only.

The "Title Page" means, for a printed book, the title page itself, plus such following pages as are needed to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, "Title Page" means the text near the most prominent appearance of the work's title, preceding the beginning of the body of the text.

A section "Entitled XYZ" means a named subunit of the Document whose title either is precisely XYZ or contains XYZ in parentheses following text that translates XYZ in another language. (Here XYZ stands for a specific section name mentioned below, such as "Acknowledgements", "Dedications", "Endorsements", or "History".) To "Preserve the Title" of such a section when you modify the Document means that it remains a section "Entitled XYZ" according to this definition.

The Document may include Warranty Disclaimers next to the notice which states that this License applies to the Document. These Warranty Disclaimers are considered to be included by reference in this License, but only as regards disclaiming warranties: any other implication that these Warranty Disclaimers may have is void and has no effect on the meaning of this License.

2. VERBATIM COPYING

edytuj

You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in section 3.

You may also lend copies, under the same conditions stated above, and you may publicly display copies.

3. COPYING IN QUANTITY

edytuj

If you publish printed copies (or copies in media that commonly have printed covers) of the Document, numbering more than 100, and the Document's license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects.

If the required texts for either cover are too voluminous to fit legibly, you should put the first ones listed (as many as fit reasonably) on the actual cover, and continue the rest onto adjacent pages.

If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a computer-network location from which the general network-using public has access to download using public-standard network protocols a complete Transparent copy of the Document, free of added material. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public.

It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document.

4. MODIFICATIONS

edytuj

You may copy and distribute a Modified Version of the Document under the conditions of sections 2 and 3 above, provided that you release the Modified Version under precisely this License, with the Modified Version filling the role of the Document, thus licensing distribution and modification of the Modified Version to whoever possesses a copy of it. In addition, you must do these things in the Modified Version:

  • A. Use in the Title Page (and on the covers, if any) a title distinct from that of the Document, and from those of previous versions (which should, if there were any, be listed in the History section of the Document). You may use the same title as a previous version if the original publisher of that version gives permission.
  • B. List on the Title Page, as authors, one or more persons or entities responsible for authorship of the modifications in the Modified Version, together with at least five of the principal authors of the Document (all of its principal authors, if it has fewer than five), unless they release you from this requirement.
  • C. State on the Title page the name of the publisher of the Modified Version, as the publisher.
  • D. Preserve all the copyright notices of the Document.
  • E. Add an appropriate copyright notice for your modifications adjacent to the other copyright notices.
  • F. Include, immediately after the copyright notices, a license notice giving the public permission to use the Modified Version under the terms of this License, in the form shown in the Addendum below.
  • G. Preserve in that license notice the full lists of Invariant Sections and required Cover Texts given in the Document's license notice.
  • H. Include an unaltered copy of this License.
  • I. Preserve the section Entitled "History", Preserve its Title, and add to it an item stating at least the title, year, new authors, and publisher of the Modified Version as given on the Title Page. If there is no section Entitled "History" in the Document, create one stating the title, year, authors, and publisher of the Document as given on its Title Page, then add an item describing the Modified Version as stated in the previous sentence.
  • J. Preserve the network location, if any, given in the Document for public access to a Transparent copy of the Document, and likewise the network locations given in the Document for previous versions it was based on. These may be placed in the "History" section. You may omit a network location for a work that was published at least four years before the Document itself, or if the original publisher of the version it refers to gives permission.
  • K. For any section Entitled "Acknowledgements" or "Dedications", Preserve the Title of the section, and preserve in the section all the substance and tone of each of the contributor acknowledgements and/or dedications given therein.
  • L. Preserve all the Invariant Sections of the Document, unaltered in their text and in their titles. Section numbers or the equivalent are not considered part of the section titles.
  • M. Delete any section Entitled "Endorsements". Such a section may not be included in the Modified Version.
  • N. Do not retitle any existing section to be Entitled "Endorsements" or to conflict in title with any Invariant Section.
  • O. Preserve any Warranty Disclaimers.

If the Modified Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modified Version's license notice. These titles must be distinct from any other section titles.

You may add a section Entitled "Endorsements", provided it contains nothing but endorsements of your Modified Version by various parties--for example, statements of peer review or that the text has been approved by an organization as the authoritative definition of a standard.

You may add a passage of up to five words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modified Version. Only one passage of Front-Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one.

The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsement of any Modified Version.

5. COMBINING DOCUMENTS

edytuj

You may combine the Document with other documents released under this License, under the terms defined in section 4 above for modified versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodified, and list them all as Invariant Sections of your combined work in its license notice, and that you preserve all their Warranty Disclaimers.

The combined work need only contain one copy of this License, and multiple identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but different contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work.

In the combination, you must combine any sections Entitled "History" in the various original documents, forming one section Entitled "History"; likewise combine any sections Entitled "Acknowledgements", and any sections Entitled "Dedications". You must delete all sections Entitled "Endorsements."

6. COLLECTIONS OF DOCUMENTS

edytuj

You may make a collection consisting of the Document and other documents released under this License, and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects.

You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document.

7. AGGREGATION WITH INDEPENDENT WORKS

edytuj

A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, is called an "aggregate" if the copyright resulting from the compilation is not used to limit the legal rights of the compilation's users beyond what the individual works permit. When the Document is included in an aggregate, this License does not apply to the other works in the aggregate which are not themselves derivative works of the Document.

If the Cover Text requirement of section 3 is applicable to these copies of the Document, then if the Document is less than one half of the entire aggregate, the Document's Cover Texts may be placed on covers that bracket the Document within the aggregate, or the electronic equivalent of covers if the Document is in electronic form. Otherwise they must appear on printed covers that bracket the whole aggregate.

8. TRANSLATION

edytuj

Translation is considered a kind of modification, so you may distribute translations of the Document under the terms of section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation of this License, and all the license notices in the Document, and any Warranty Disclaimers, provided that you also include the original English version of this License and the original versions of those notices and disclaimers. In case of a disagreement between the translation and the original version of this License or a notice or disclaimer, the original version will prevail.

If a section in the Document is Entitled "Acknowledgements", "Dedications", or "History", the requirement (section 4) to Preserve its Title (section 1) will typically require changing the actual title.

9. TERMINATION

edytuj

You may not copy, modify, sublicense, or distribute the Document except as expressly provided for under this License. Any other attempt to copy, modify, sublicense or distribute the Document is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.

10. FUTURE REVISIONS OF THIS LICENSE

edytuj

The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. See http://www.gnu.org/copyleft/.

Each version of the License is given a distinguishing version number. If the Document specifies that a particular numbered version of this License "or any later version" applies to it, you have the option of following the terms and conditions either of that specified version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation.

How to use this License for your documents

edytuj

To use this License in a document you have written, include a copy of the License in the document and put the following copyright and license notices just after the title page:

Copyright (c)  YEAR  YOUR NAME.
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.2
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
A copy of the license is included in the section entitled "GNU
Free Documentation License".

If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, replace the "with...Texts." line with this:

with the Invariant Sections being LIST THEIR TITLES, with the
Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST.

If you have Invariant Sections without Cover Texts, or some other combination of the three, merge those two alternatives to suit the situation.

If your document contains nontrivial examples of program code, we recommend releasing these examples in parallel under your choice of free software license, such as the GNU General Public License, to permit their use in free software.