Professional Documents
Culture Documents
PHP I MySQL. Tworzenie Stron WWW. Vademecum Profesjonalisty. Wydanie V
PHP I MySQL. Tworzenie Stron WWW. Vademecum Profesjonalisty. Wydanie V
Laura Thomson
PHP
i MySQL
7ZRU]HQLHVWURQ:::
9DGHPHFXPSURIHVMRQDOLVW\
:\GDQLH9
5R]ZLĉ]DQLHGODQLH]DZRGQ\FKG\QDPLF]Q\FKZLWU\Q
Tytuł oryginału: PHP and MySQL Web Development (5th Edition)
ISBN: 978-83-283-3257-7
Authorized translation from the English language edition, entitled: PHP AND MYSQL WEB
DEVELOPMENT, Fifth Edition, ISBN 0321833899; by Luke Welling; and by Laura Thomson;
published by Pearson Education, Inc, publishing as Addison Wesley.
Copyright © 2017 by Pearson Education, Inc.
All rights reserved. No part of this book may be reproduced or transmitted in any form
or by any means, electronic or mechanical, including photocopying, recording or by any
information storage retrieval system, without permission from Pearson Education Inc.
Polish language edition published by HELION S.A. Copyright © 2017.
Wydawnictwo HELION
ul. Kościuszki 1c, 44-100 GLIWICE
tel. 32 231 22 19, 32 230 98 63
e-mail: helion@helion.pl
WWW: http://helion.pl (księgarnia internetowa, katalog książek)
Drogi Czytelniku!
Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres
http://helion.pl/user/opinie/phmsv5_ebook
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
„Książka Wellinga i Thomson jest jedyną, o której mogę powiedzieć, że nie można się bez niej obejść.
Tekst jest klarowny i zrozumiały, a jednocześnie czytelnik nie ma poczucia zmarnowanego czasu. Struktura
książki jest bardzo dobrze przemyślana. Rozdziały są ani za krótkie, ani za długie, a ich tytuły od razu
wskazują, czego dane fragmenty dotyczą”.
— Wright Sullivan, prezes firmy A&E Engineering, Inc. z Greer South Karolina
„Chciałem tylko powiedzieć, że PHP i MySQL. Tworzenie stron WWW. Vademecum programisty jest
doskonała! Ma logiczną strukturę, odpowiedni poziom zaawansowania, przynajmniej jak dla mnie (a je-
stem średnio zaawansowany), jest interesująca i łatwo się ją czyta. I oczywiście zawiera same przydatne
informacje!”.
— CodE-E, Austria
„Na rynku dostępnych jest kilka książek wprowadzających w tematykę PHP, lecz publikacja Wellinga
i Thomson to doskonała pozycja dla osób, które chcą nauczyć się tworzyć złożone i niezawodne syste-
my informatyczne. Od razu widać, że autorzy mają duże doświadczenie w tworzeniu profesjonalnych
aplikacji i potrafią nauczyć nie tylko samego języka, ale również sposobów jego używania z wykorzysta-
niem odpowiednich praktyk programistycznych”.
— Javier Garcia, starszy inżynier telekomunikacji,
Laboratorium Badawczo-Rozwojowe firmy Telefonica, Madryt
„Książka wpadła mi w ręce dwa dni temu i dotarłem do jej połowy. Nie mogę się od niej oderwać!
Układ książki i kolejność poruszanych zagadnień jest bez zarzutu. Informacje podawane są w bardzo
przystępny sposób. Wszystko jest dla mnie od razu zrozumiałe. Cudowne są również przykłady.
Oderwałem się od lektury tylko na chwilę, żeby powiedzieć wam, jak doskonale można spędzać czas,
czytając tę książkę”.
— Jason B. Lancaster
„Książka zyskała już sobie rzesze wiernych fanów, między innymi dzięki zamieszczeniu w niej krót-
kiego kursu języka PHP i doskonałego opisu MySQL pod kątem ich wykorzystania w aplikacjach inter-
netowych. W publikacji prezentowanych jest kilka kompletnych aplikacji, które stanowią doskonały
przykład ilustrujący sposoby tworzenia modularnych, skalowalnych aplikacji w języku PHP.
Bez względu na to, czy jest się początkującym programistą PHP, czy też weteranem poszukującym
jak najlepszego źródła informacji, ta książka na pewno nikogo nie zawiedzie!”.
— WebDynamic
„Bez dwóch zdań biblia PHP i MySQL. Książka PHP i MySQL. Tworzenie stron WWW. Vademecum
profesjonalisty Luke’a Wellinga i Laury Thomson przekonała mnie, że programowanie i korzystanie z baz
danych jest dostępne dla każdego. Dowiedziałem się dopiero 1/10000 tego, co warto wiedzieć, a już jestem
oczarowany”.
— Tim Luoma, TnTLuoma.com
„Książka Wellinga i Thomson jest doskonałym przewodnikiem dla osób, które od razu chciałyby się
zmierzyć z tworzeniem praktycznych projektów. Znajdują się w niej przykładowe aplikacje: obsługi
poczty, koszyka na zakupy, kontroli sesji, a także proste forum internetowe i weblog, zaś na początku
prezentowany jest opis języka PHP, po którym następuje charakterystyka podstawowych zagadnień
dotyczących serwera bazodanowego MySQL”.
— twilight30 w Slashdot
„Książka jest po prostu doskonała, żeby nie zanudzać… Luke Welling i Laura Thomson doskonale tłuma-
czą takie zagadnienia, jak wyrażenia regularne, klasy i obiekty, sesje i im podobne. Dzięki tej książce
bez trudu uzupełniłem luki w znajomości zagadnień, których dotychczas zupełnie nie mogłem
pojąć… Publikacja skupia się na funkcjach i rozwiązaniach najczęściej używanych w PHP, po czym
następuje prezentacja praktycznych rozwiązań, sposobu integracji z MySQL, a także zagadnień bezpie-
czeństwa podanych z perspektywy kierownika projektów. Nawet najmniejszy fragment książki został
przemyślany i podany w zrozumiały sposób”.
— notepad w codewalkers.com
„Ta książka nie ma sobie równych! Jestem doświadczonym programistą, dlatego nie potrzebuję już
właściwie pomocy w zakresie składni PHP, która nie odbiega przecież za bardzo od składni C/C++.
Nie miałem jednak zielonego pojęcia o bazach danych, dlatego gdy przyszło do stworzenia aplikacji
do recenzowania książek, stwierdziłem, że potrzebuję szczegółowego przewodnika po PHP i MySQL.
Mam już publikację mSQL and MySQL Wydawnictwa O’Reilly; zapewne jest ona bardziej przydatnym
źródłem informacji na temat języka SQL, lecz ta książka zasłużyła sobie na ważne miejsce na mojej
półce… Z czystym sumieniem polecam”.
— Paul Robichaux
„Książka jest ciekawie napisana i przeznaczona dla osób, które chcą nauczyć się tworzenia aplikacji
internetowych w dwóch najpopularniejszych technologiach o otwartym dostępie do kodu źródłowego…
Projekty praktyczne są perełką tej publikacji. Są one opisane i stworzone w sposób logiczny i modułowy,
a jednocześnie stanowią wybór rozwiązań, które najczęściej wykorzystuje się w wielu prawdziwych
witrynach internetowych”.
— Craig Cecil
„Książka krok po kroku, w przystępny sposób wprowadza nawet zupełnie niedoświadczonego progra-
mistę w tajniki programowania w PHP. W trakcie lektury często wracałem pamięcią do własnych
początków tworzenia aplikacji WWW. Cały czas uczę się PHP, lecz ta książka dała mi solidną podstawę,
od której mogę rozpocząć dalszą naukę, i przez cały czas stanowi dla mnie bezcenną pomoc”.
— Stephen Ward
„Jest to jedna z niewielu książek, która rzeczywiście do mnie przemówiła i przekonała do siebie. Nie
mogę odłożyć jej na półkę; cały czas leży w zasięgu ręki, ponieważ co chwila z niej korzystam.
Książka ma logiczną strukturę, wywody napisano prostym i zrozumiałym językiem, zaś przykła-
dy są jasne i prezentowane krok po kroku. Przed rozpoczęciem lektury nie wiedziałem niczego na temat
PHP i MySQL. Natomiast po zakończeniu lektury mam wrażenie, że jestem w stanie stworzyć nawet
bardzo skomplikowaną aplikację WWW”.
— Power Wong
„Ta książka jest boska… Szczerze ją polecam każdemu, kto chce się zagłębić w zagadnienia dotyczące
programowania aplikacji WWW opartych na bazach danych. Szkoda, że inne publikacje na temat pro-
gramowania nie są podobne…”
— Sean C. Schertell
Spis treści
O autorach ............................................................................................ 19
O współautorach .................................................................................. 19
Wprowadzenie ...................................................................................... 21
Rozdział 11. Łączenie się z bazą MySQL za pomocą PHP .................................... 273
Jak działa internetowa baza danych? .................................................................................273
Wykonywanie zapytań do bazy danych z poziomu strony WWW ....................................276
Sprawdzenie poprawności wpisanych danych .............................................................277
Ustanawianie połączenia z bazą danych ......................................................................278
Wybór używanej bazy danych .....................................................................................279
Spis treści 11
Rozdział 16. Implementacja metod uwierzytelniania przy użyciu PHP ............... 365
Identyfikacja użytkowników .............................................................................................365
Implementacja kontroli dostępu ........................................................................................366
Przechowywanie haseł dostępu ...................................................................................368
Zabezpieczanie haseł ...................................................................................................369
Zabezpieczanie więcej niż jednej strony .....................................................................370
Podstawowa metoda uwierzytelniania ...............................................................................371
Wykorzystanie podstawowej metody uwierzytelniania w PHP .........................................371
Wykorzystanie podstawowej metody uwierzytelniania na serwerze Apache
przy użyciu plików .htaccess ..........................................................................................373
Implementacja własnej metody uwierzytelniania ..............................................................376
Propozycje dalszych lektur ................................................................................................376
W następnym rozdziale .....................................................................................................376
Luke Welling jest architektem oprogramowania i regularnie bierze udział w konferencjach po-
święconych programowaniu aplikacji internetowych oraz oprogramowaniu z darmowym dostę-
pem do kodu źródłowego, takich jak OSCON, ZendCon, MySQLUC, PHPCon, OSDC i LinuxTag.
Wcześniej pracował w OmniTI, w firmie analiz internetowych Hitwise.com, w MySQL AB —
producencie serwerów bazodanowych, był też niezależnym konsultantem w firmie Tangled Web
Design. Luke był również wykładowcą inżynierii oprogramowania oraz e-commerce na Wydziale
Inżynierii Systemów Elektrycznych i Komputerowych Uniwersytetu Melbourne w Australii. Uzy-
skał stopień magistra w dziedzinie stosowanych nauk komputerowych. W wolnym czasie stara się
doprowadzić do perfekcji swoją bezsenność.
O współautorach
Julie C. Meloni jest dyrektorem technicznym i konsultantką techniczną, mieszkającą w Waszyng-
tonie, D.C. Jest autorką wielu książek i artykułów na temat języków programowania dla sieci
WWW oraz baz danych, w tym popularnej książki Sams Teach Yourself PHP, MySQL and Apache
All in One.
John Coggeshall jest właścicielem firmy Internet Technology Solutions, LLC, zajmującej się kon-
sultowaniem rozwiązań internetowych opartych na PHP i mającej klientów na całym świecie, jak
również właścicielem CoogleNet — sieci Wi-Fi działającej na zasadzie subskrypcji. Wcześniej był
starszym członkiem zespołu Global Services w firmie Zend Technologies. Pracę z PHP rozpoczął
w 1997 roku; jest autorem czterech książek i ponad 100 artykułów poświęconych tej technologii.
Jennifer Kyrnin jest autorką książek i projektantką, działająca w branży internetowej od roku 1995.
Napisała między innymi następujące książki: Sams Teach Yourself Bootstrap in 24 Hours, Sams
Teach Yourself Responsive Web Design in 24 Hours oraz Sams Teach Yourself HTML5 Mobile
Application Development in 24 Hours.
20 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty
Wprowadzenie
PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty przedstawia szczegółowo wiedzę
nabytą dzięki doświadczeniu autorów w wykorzystywaniu PHP i MySQL, dwóch najważniej-
szych i najczęściej stosowanych narzędzi służących do tworzenia aplikacji internetowych.
A zatem zaczynajmy!
Odbiorcami PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty są czytelnicy znają-
cy przynajmniej podstawy HTML-a i posiadający pewne doświadczenie w dziedzinie nowocze-
snych języków programowania, choć niekonieczne piszący programy dla internetu lub wykorzy-
stujący relacyjne bazy danych. Początkujący programiści również mogą skorzystać z tej książki,
ale wówczas zrozumienie jej treści może im zająć nieco więcej czasu. Autorzy starali się nie
opuszczać żadnych podstawowych pojęć, zostały one jednak przedstawione w znacznym skrócie.
Typowym adresatem książki jest osoba pragnąca opanować PHP i MySQL w celu tworzenia du-
żej lub komercyjnej witryny WWW. Jeżeli Czytelnik zna już inny język programowania WWW,
publikacja powinna być znacznie łatwiejsza w odbiorze.
Autorzy postawili sobie za cel napisanie książki o PHP, która nie byłaby jedynie przeglądem
funkcji. Na rynku są wprawdzie dostępne inne pożyteczne publikacje na ten temat, lecz okazują się
one nieprzydatne w dziedzinie praktycznych zastosowań (np. utworzenie koszyka na zakupy).
Dołożono wszelkich starań, aby każdy przedstawiony przykład był instruktywny. Wiele przy-
kładów kodu może zostać bezpośrednio zastosowanych w witrynie WWW, inne zaś wymagają
jedynie nieznacznych modyfikacji.
22 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty
Wykorzystanie języka (takiego jak PHP) i bazy danych (takiej jak MySQL) pozwala na two-
rzenie witryn dynamicznych — mogą one być dostosowywane i zawierać aktualne informacje.
Autorzy książki celowo skupiają się na rzeczywistych aplikacjach, nawet w rozdziałach wprowa-
dzających. Na początku opisują proste systemy, po czym omawiają różne elementy PHP i MySQL.
Każdy z nich powinien być użyteczny w formie, w jakiej zostanie opisany, lub może ulec modyfi-
kacji w celu dostosowania do konkretnych potrzeb. Zostały one wybrane, ponieważ zdaniem auto-
rów przedstawiają osiem najczęściej tworzonych przez programistów aplikacji WWW. Nawet jeżeli
potrzeby odbiorców są inne, książka powinna pomóc w osiągnięciu tych celów.
PHP, utworzony w 1994 roku, jest dziełem jednego człowieka, Rasmusa Lerdorfa. Język ten uległ
w późniejszym czasie trzem poważnym modyfikacjom, których wynikiem jest obecny dojrzały
produkt o szerokich zastosowaniach. Zgodnie z informacjami, które podał Greg Michillie, pra-
cownik firmy Google, w maju 2013 roku język PHP wykorzystywało ponad 75% wszystkich istnie-
jących witryn WWW, a w czerwcu 2016 roku liczba ta wzrosła do 82%.
PHP jest produktem Open Source. Oznacza to dostęp do jego kodu źródłowego, który można bez-
płatnie wykorzystywać, zmieniać i redystrybuować.
Skrót PHP początkowo oznaczał Personal Home Page (Osobista Strona Domowa), został jednak
zmieniony zgodnie z rekursywną konwencją nadawania nazw GNU (GNU = Gnu's Not Unix — Gnu
Nie Unix) i oznacza obecnie PHP Hypertext Preprocessor (PHP Preprocesor Hipertekstu).
Wprowadzenie 23
Obecnie główną wersją PHP jest wersja 7. W wersji tej od nowa napisano moduł Zend, a także
dokonano istotnych ulepszeń w samym języku. Wszystkie kody prezentowane w tej książce zostały
przetestowane w najnowszej wersji języka PHP 7 dostępnej w trakcie przygotowywania książ-
ki, jak również w najnowszej wersji linii PHP 5.6, która wciąż jest oficjalnie obsługiwana.
Główna strona PHP jest dostępna pod adresem http://www.php.net (strona w Polsce: http://pl.php.net).
MySQL jest dostępny w dwóch licencjach. Można go używać na podstawie licencji Open Source
(GPL), czyli za darmo, jeśli tylko spełnione zostaną wszystkie warunki zawarte w licencji tego typu.
Jeżeli natomiast MySQL ma być dystrybuowany wraz z aplikacją, która nie podlega warunkom
licencji GPL, można uzyskać jego licencje komercyjne.
W efekcie może powstać architektura hybrydowa zawierająca wiele magazynów danych. Niektóre
z powyższych wyborów są zależne od innych. Na przykład nie wszystkie systemy operacyjne pra-
cują na dowolnym sprzęcie, nie wszystkie serwery internetowe potrafią obsługiwać wszystkie języki
programowania itd.
Większość kodu PHP można napisać w taki sposób, by był on przenośny między różnymi syste-
mami operacyjnymi i serwerami internetowymi. Niektóre funkcje PHP odnoszą się do konkretnych
systemów plików i systemów operacyjnych, jednak odpowiednia informacja na ten temat zawsze
jest podawana w podręcznikach, a także w tej książce.
Wydajność
PHP jest bardzo wydajny. Za pomocą pojedynczego niedrogiego serwera można obsłużyć miliony
odwiedzin dziennie. Bez problemu można go używać do obsługi zarówno jednego prostego for-
mularza wysyłającego wiadomości poczty elektronicznej, jak i ogromnych witryn, takich jak
Facebook czy Etsy.
Skalowalność
Jak zwykł powtarzać Rasmus Lerdorf, PHP charakteryzuje się architekturą „współużytkowania
niczego”. Inaczej mówiąc, tanim kosztem można efektywnie wdrożyć skalowanie poziome z wyko-
rzystaniem większej liczby serwerów.
Wprowadzenie 25
Oprócz bibliotek natywnych PHP zawiera abstrakcyjną warstwę dostępu do baz danych o nazwie
PHP Database Objects (PDO), która zapewnia spójny mechanizm dostępu do baz danych i ułatwia
stosowanie bezpiecznych praktyk programistycznych.
Wbudowane biblioteki
Ponieważ PHP został zaprojektowany do stosowania w sieci WWW, posiada wiele wbudowanych
funkcji służących do wykonywania zadań z nią związanych. Za pomocą zaledwie kilku wierszy
kodu możliwe jest tworzenie „w locie” obrazków GIF, łączenie z innymi usługami sieciowymi,
parsowanie języka XML, wysyłanie poczty elektronicznej, praca z cookies oraz generowanie
dokumentów PDF.
Koszt
PHP jest bezpłatne. Można w dowolnym momencie pobrać najnowszą wersję ze strony http://www.
php.net bez żadnych opłat.
Przenośność
PHP jest dostępne dla wielu systemów operacyjnych. Kod PHP może być tworzony na darmowym,
podobnym do Uniksa systemie operacyjnym, takim jak Linux i FreeBSD, na komercyjnych wer-
sjach Uniksa, Mac OS X oraz na różnych wersjach Microsoft Windows.
26 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty
Kod zazwyczaj będzie działał, bez konieczności dokonywania zmian, w różnych systemach obsłu-
gujących PHP.
Kod źródłowy
Użytkownik posiada dostęp do kodu źródłowego PHP. Inaczej niż w przypadku programów
komercyjnych, nie ma przeciwwskazań, jeżeli zachodzi konieczność modyfikacji lub dodania wła-
sności do języka.
Nie trzeba czekać, aż producent udostępni programy korygujące. Nie ma również znaczenia, że
zaprzestanie działalności lub zdecyduje się na zakończenie obsługi produktu.
Dokumentacja i społeczność PHP osiągnęły już pełną dojrzałość, co skutkuje bogactwem dostęp-
nych zasobów i informacji.
W ramach prac nad PHP 7 za kulisami przeprowadzona została modernizacja Zend Engine, której
efektem jest znaczący wzrost wydajności działania wielu aplikacji internetowych — czasami wyno-
szący nawet 100%! Choć zwiększenie wydajności oraz zmniejszenie zużycia pamięci były klu-
czowymi wymaganiami stawianymi PHP 7, to nie można zapomnieć o innym ważnym wymogu
— zachowaniu pełnej zgodności wstecz. Okazuje się, że w tej najnowszej wersji języka wprowa-
dzono stosunkowo mało zmian, które powodują niezgodność z wcześniejszymi wersjami. Zmiany
te zostały przedstawione w odpowiednich miejscach książki i w odpowiednich kontekstach, dzięki
czemu informacje zamieszczone w książce są przydatne zarówno dla osób używających PHP 5.6,
Wprowadzenie 27
jak i dla osób korzystających z PHP 7, gdyż całkiem sporo komercyjnych serwisów hostingowych
nie zdecydowało się jeszcze na włączenie do swojej oferty obsługi najnowszej wersji języka PHP.
Wydajność
MySQL jest niewątpliwie szybki. Strona testów programistycznych jest dostępna pod adresem
http://www.mysql.com/why-mysql/benchmarks/.
Niski koszt
MySQL jest dostępny bezpłatnie, na licencji Open Source, lub za niską cenę w ramach licencji
komercyjnej. Jeżeli MySQL ma być rozprowadzany wraz z aplikacją, która nie będzie podlegać
licencji Open Source, trzeba zakupić licencję na serwer. Jeśli jednak tworzona aplikacja nie będzie
dystrybuowana — co dotyczy większości aplikacji internetowych — lub oprogramowanie będzie
miało charakter darmowy, kupno takiej licencji nie będzie konieczne.
Łatwość wykorzystania
Większość nowoczesnych baz danych wykorzystuje SQL. Osoby, które stosowały wcześniej inne
RDBMS, nie powinny mieć żadnych problemów z jego przyswojeniem. MySQL jest również
łatwiejszy w konfiguracji niż wiele podobnych produktów.
Przenośność
MySQL może być wykorzystywany w różnych systemach uniksowych, a także w Microsoft
Windows.
28 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty
Kod źródłowy
Podobnie jak w przypadku PHP, można pobrać i modyfikować kod źródłowy MySQL. Zwykle nie
ma to specjalnego znaczenia dla większości użytkowników, lecz sam fakt posiadania dostępu do
kodu źródłowego pozwala ze spokojem patrzeć w przyszłość, ponieważ stanowi gwarancję, że
serwer będzie nadal rozwijany, a w nagłych przypadkach znacznie łatwiej przyjdzie rozwiązywać
ewentualne problemy.
Obecnie dostępnych jest kilka różnych kopii i zamienników MySQL, których wykorzystanie
można rozważyć; wśród nich jest także MariaDB, napisana przez twórców MySQL, w tym Michaela
„Montyʼego” Wideniusa (https://mariadb.org/).
Dostępność wsparcia
Nie wszystkie produkty o otwartym dostępie do kodu źródłowego mają swą firmę macierzystą,
która zapewniałaby wsparcie techniczne, szkolenia, konsultacje i certyfikacje. W przypadku MySQL
wszystkie te usługi można uzyskać od firmy Oracle (pozyskała ona MySQL wraz z wykupieniem
firmy Sun Microsystems, która wcześniej zakupiła firmę MySQL AB — pierwotnego właści-
ciela bazy).
Inne zmiany związane są ze zwiększeniem zakresu zgodności ze standardem ANSI oraz przyspie-
szeniem pracy serwera.
Wprowadzenie 29
Jeżeli nadal używana jest wczesna wersja 4.x lub któraś z wersji 3.x serwera MySQL, należy pa-
miętać, że począwszy od wersji 4.0, produkt ten wzbogacono o następujące mechanizmy:
obsługa widoków,
procedury składowane,
wyzwalacze i kursory,
obsługa podzapytań,
typy GIS służące do przechowywania danych geograficznych,
ulepszona obsługa różnych wersji lokalizacyjnych,
standardowa dostępność modułu InnoDB, zapewniającego przechowywanie danych
z wykorzystaniem transakcji,
pamięć podręczna zapytań MySQL, dzięki której znacznie zwiększono prędkość
wykonywania powtarzalnych zapytań, tak często używanych w ramach aplikacji WWW.
Układ książki
Książka została podzielona na pięć części.
Część I, „Stosowanie PHP”, zawiera przegląd głównych elementów języka PHP wraz z przykła-
dami. Każdy jest przykładem rzeczywistym, wykorzystywanym przy tworzeniu witryny związanej
z e-commerce, nie zaś kodem „zabawkowym”. Część ta rozpoczyna się od rozdziału 1., „Podsta-
wowy kurs PHP”. Osoby, które posługują się PHP, mogą ograniczyć się do pobieżnego przejrzenia
tej części. Czytelnicy nieznający tego języka lub rozpoczynający dopiero naukę programowania
powinni poświęcić jej nieco więcej uwagi.
Część II, „Stosowanie MySQL”, zawiera opis pojęć i projektowania związanych z wykorzystaniem
systemów relacyjnych baz danych, takich jak MySQL, wykorzystaniem SQL, łączeniem bazy
danych MySQL ze światem za pomocą PHP oraz zaawansowanymi tematami dotyczącymi MySQL,
takimi jak bezpieczeństwo i optymalizacja.
Część IV, „Zaawansowane techniki PHP”, zawiera szczegółowy opis niektórych podstawowych,
wbudowanych funkcji PHP. Autorzy wybrali grupy funkcji wykorzystywane zazwyczaj do two-
rzenia aplikacji internetowej. Opisane zostały: interakcja z serwerem, interakcja z siecią, gene-
rowanie obrazków, manipulacje datą i godziną oraz obsługa sesji.
Część V, „Tworzenie praktycznych projektów PHP i MySQL”, to nasza ulubiona część. Zawiera
ona opis kwestii praktycznych, takich jak zarządzanie dużymi projektami i usuwanie błędów.
Przedstawia także przykładowe projekty świadczące o efektywności i wszechstronnych możli-
wościach PHP i MySQL.
30 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty
Kody źródłowe
Kody źródłowe przykładowych programów prezentowanych w książce, można pobrać z serwera
FTP wydawnictwa Helion — ftp://ftp.helion.pl/przyklady/phmsv5.zip.
Uwagi końcowe
Autorzy mają nadzieję, że Czytelnicy, za ich przykładem, polubią naukę PHP i MySQL. Korzystanie
z tych produktów jest naprawdę satysfakcjonujące. Czytelnik, który przyswoi sobie wiedzę przed-
stawioną w tej książce, może dołączyć do tysięcy programistów WWW sięgających po te solidne
i skuteczne narzędzia, aby bez trudu tworzyć rzeczywiste, dynamiczne aplikacje internetowe.
Część I
Stosowanie PHP
32 Część I Stosowanie PHP
Rozdział 1. Podstawowy kurs PHP 33
Rozdział 1.
Podstawowy kurs PHP
Rozdział ten to krótki przegląd składni i konstrukcji języka PHP. Dlatego też osoby posługujące się
tym językiem mogą uzupełnić ewentualne luki w wiedzy. Użytkownicy, którzy znają inne języki
programowania, takie jak C, Perl lub Python, z pewnością nabiorą wprawy w stosowaniu PHP.
Książka ta wprowadza w arkana PHP poprzez analizę wielu przykładów z życia codziennego,
zaczerpniętych z doświadczeń autorów w tworzeniu witryn internetowych. Podręczniki progra-
mowania często uczą podstawowej składni na bardzo prostych przykładach, prezentują opisy składni
i funkcji. My zaś pokazujemy stosowanie języka PHP w praktyce.
Rozdział ten rozpoczyna się od przykładowego elektronicznego formularza zamówień i na jego pod-
stawie przedstawiono sposób używania zmiennych, operatorów i wyrażeń w PHP. Opisane są
również typy zmiennych i reguły pierwszeństwa operatorów. Prezentujemy także sposoby dostępu
do zmiennych formularza oraz metody manipulacji nimi w celu obliczenia sumy zamówienia
klienta i podatku pobieranego od tej kwoty.
W dalszej części przykładowy formularz zamówienia został rozwinięty poprzez dodanie skryptu
PHP, który sprawdza poprawność wprowadzonych danych. Omówiona została idea wartości
boolowskich oraz przykłady użycia instrukcji if i else, operatora ?: oraz instrukcji switch.
W końcowej części rozdziału znajduje się opis pętli — kod PHP generujący powtarzające się ta-
blice HTML.
Zastosowanie PHP
Praca nad przykładami przedstawionymi w tym rozdziale oraz w pozostałej części książki wymaga
dostępu do serwera WWW z zainstalowanym PHP. Aby maksymalnie wykorzystać przykłady
i opisy konkretnych przypadków, należy je uruchomić i podjąć próbę modyfikacji. Żeby to zrobić,
potrzebne jest miejsce do przeprowadzania testów.
Jeżeli PHP nie jest zainstalowane, trzeba rozpocząć od zainstalowania go samodzielnie lub przez
administratora systemu. Instrukcja znajduje się w dodatku A, „Instalacja Apache, PHP i MySQL”.
Formularz zamówienia
Programista HTML pracujący dla firmy Janka stworzył formularz zamówienia na części, które
Janek sprzedaje. Ten stosunkowo prosty formularz widoczny na rysunku 1.1 jest podobny do
wielu innych istniejących w sieci WWW. Na początku Janek chciałby wiedzieć, co zamówił jego
klient, obliczyć sumę tego zamówienia oraz ustalić kwotę podatku VAT od tego zamówienia.
Część kodu HTML dla tego formularza jest przedstawiona na listingu 1.1.
Listing 1.1. formularz.html — kod HTML dla podstawowego formularza zamówienia Janka
<form action="przetworzzamowienie.php" method="post">
<table style="border: 0px;">
<tr style="background: #cccccc;">
<td style="width: 150px; text-align: center;">Produkt</td>
<td style="width: 15px; text-align: center;">Ilość</td>
</tr>
<tr>
<td>Opony</td>
<td><input type="text" name="iloscopon" size="3" maxlength="3" /></td>
</tr>
Rozdział 1. Podstawowy kurs PHP 35
Rysunek 1.1.
Na początkowym
formularzu zamówienia
Janka zapisano jedynie
produkty i ich ilość
<tr>
<td>Olej</td>
<td><input type="text" name="iloscoleju" size="3" maxlength="3" /></td>
</tr>
<tr>
<td>Świece Zapłonowe</td>
<td><input type="text" name="iloscswiec" size="3" maxlength="3" /></td>
</tr>
<tr>
<td colspan="2" style="align: center"><input type="submit" value="Złóż zamówienie" /></td>
</tr>
</table>
</form>
Należy zwrócić uwagę, że akcja formularza została ustawiona na nazwę skryptu PHP, który będzie
przetwarzał zamówienie klienta (zostanie on napisany później). Ogólnie rzecz biorąc, wartość
atrybutu ACTION to URL, który zostanie załadowany, kiedy użytkownik naciśnie przycisk Złóż
zamówienie. Dane, które wprowadził użytkownik do formularza, zostaną przesłane do tego
URL-a za pomocą metody protokołu HTTP podanej w atrybucie method — czy będzie to get
(dodane na końcu URL-a), czy też post (wysłane jako osobny komunikat).
Drugą kwestią wartą uwagi są nazwy pól formularza: iloscopon, iloscoleju, iloscswiec, użyte
ponownie w skrypcie PHP. Z tego powodu ważne jest nadawanie polom formularza nazw, które
coś znaczą i mogą być łatwo zapamiętane po rozpoczęciu pisania skryptu PHP. Niektóre edytory
HTML domyślnie tworzą nazwy pól, takie jak pole23, które niełatwo zapamiętać. Zadanie pro-
gramisty PHP jest o wiele łatwiejsze, jeżeli nazwy opisują wpisywane w te pola dane.
Możliwe jest takie zaadaptowanie standardu kodowania nazw pól, aby wszystkie nazwy na stronach
witryny używały identycznego formatu. Ułatwia to zapamiętanie na przykład skrótów wyrazów
w nazwie pola formularza lub stosowanie znaku podkreślenia jako spacji.
36 Część I Stosowanie PHP
Przetwarzanie formularza
Aby przetworzyć formularz, trzeba utworzyć skrypt wymieniony w atrybucie ACTION znacznika
form, o nazwie przetworzzamowienie.php. Należy otworzyć edytor tekstów, utworzyć ten plik
i wpisać następujący kod:
<!DOCTYPE html>
<html>
<head>
<title>Części samochodowe Janka — wyniki zamówienia</title>
</head>
<body>
<h1>Części samochodowe Janka</h1>
<h2>Wyniki zamówienia</h2>
</body>
</html>
Warto podkreślić, że wszystko, co zostało napisane do tej pory, to czysty kod HTML. Teraz nad-
szedł czas, aby dodać prosty kod PHP do skryptu.
Rysunek 1.2.
Tekst przekazany
w konstrukcji PHP
echo został pokazany
przez przeglądarkę
Warto zauważyć, że kod PHP został osadzony w zwyczajnie wyglądającym pliku HTML. W dalszej
kolejności należy obejrzeć źródło w przeglądarce. Powinien pojawić się następujący kod:
<!DOCTYPE html>
<html>
<head>
<title>Części samochodowe Janka — wyniki zamówienia</title>
Rozdział 1. Podstawowy kurs PHP 37
</head>
<body>
<h1>Części samochodowe Janka</h1>
<h2>Wyniki zamówienia</h2>
<p>Zamówienie przyjęte.</p></body>
</html>
Żaden fragment czystego kodu PHP nie jest widoczny, ponieważ interpreter języka PHP przetworzył
skrypt i zamienił go na jego wyniki. Oznacza to, że z PHP można utworzyć czysty kod HTML,
możliwy do przeglądania w każdej przeglądarce — innymi słowy, przeglądarka użytkownika nie
musi rozumieć języka PHP.
Powyższy przykład ukazuje ideę przetwarzania skryptów przez serwer. Kod PHP jest interpreto-
wany i wykonywany przez serwer WWW, natomiast JavaScriptu i innych technologii — przez
przeglądarkę WWW na komputerze użytkownika.
Kod znajdujący się w tym pliku składa się z czterech części tekstowych:
kodu HTML,
znaczników PHP,
instrukcji PHP,
odstępów.
Istnieją dwa różne style znaczników PHP. Wszystkie poniższe fragmenty kodu są równoznaczne.
Styl XML
<?php echo '<p>Zamówienie przyjęte.</p>'; ?>
Takiego stylu znaczników będziemy używać w książce, gdyż jest to preferowany styl
znaczników PHP. Administrator serwera nie ma możliwości jego wyłączenia, a więc mamy
gwarancję, że będzie on dostępny na każdym serwerze. Nabiera to szczególnego znaczenia
w przypadku aplikacji, które prawdopodobnie będą używane w różnych środowiskach.
Ten styl znaczników może być używany z dokumentami XML (Extensible Markup
Language — Rozszerzalny Język Oznaczania). Ogólnie rzecz biorąc, zaleca się stosowanie
tego właśnie stylu znaczników PHP.
Styl krótki
<? echo '<p>Zamówienie przyjęte.</p>'; ?>
38 Część I Stosowanie PHP
Instrukcje PHP
Interpreter PHP działa dzięki instrukcjom umieszczonym między znacznikami otwierającymi
i zamykającymi. W poprzednim przykładzie wystąpił tylko jeden typ instrukcji:
echo '<p>Zamówienie przyjęte.</p>';
Jak łatwo się domyślić, zastosowanie konstrukcji echo daje bardzo prosty rezultat: wyświetlenie
łańcucha znaków przekazanych do przeglądarki. Na rysunku 1.2 przedstawiono wynik — w oknie
pojawia się tekst Zamówienie przyjęte.
Należy zauważyć średnik pojawiający się na końcu instrukcji echo. Jego funkcją jest odseparowa-
nie instrukcji PHP, podobnie jak w języku polskim rozdziela się zdania kropką. Średnik spełnia
podobne funkcje w językach Java i C.
Do często popełnianych błędów składni należy opuszczenie średnika. Na szczęście, równie łatwo
go odnaleźć i naprawić.
Odstępy
Znaki tworzące przerwy — takie jak nowe wiersze (powrót karetki), spacje i znaki tabulacji —
noszą nazwę odstępów. Z połączenia akapitów powyższego i poniższego wynika jeden spójny akapit
wyjaśniający, w jaki sposób odstępy są ignorowane w PHP i HTML.
Jak wiadomo, przeglądarki ignorują odstępy w HTML-u. Podobnie działa mechanizm PHP. Warto
przeanalizować dwa poniższe fragmenty kodu HTML:
<h1>Witamy w "Częściach samochodowych Janka"!</h1><p>Co Państwo zechcą zamówić dzisiaj?</p>
i
<h1>Witamy w "Częściach samochodowych
Janka"!</h1>
<p>Co Państwo zechcą
zamówić dzisiaj?</p>
Przedstawione fragmenty kodu HTML dają identyczne rezultaty, ponieważ są tak samo interpre-
towane przez przeglądarkę. Można jednak — co jest zalecane — używać odstępów w HTML-u
jako pomocy w celu poprawienia czytelności kodu HTML. Podobne zasady obowiązują w PHP.
Nie ma potrzeby wstawiania odstępu między instrukcjami PHP, ale kod będzie o wiele bardziej
zrozumiały, jeżeli każda z nich zostanie umieszczona w osobnym wierszu. Na przykład:
echo 'witaj';
echo 'świecie';
i
echo 'witaj ';echo 'świecie';
Komentarze
Komentarze znaczą dokładnie tyle, co ich nazwa: w kodzie spełniają funkcję notatek dla czytają-
cych. Mogą być one użyte w celu wyjaśnienia przeznaczenia skryptu, podania jego autora, wyja-
śnienia sposobu zapisu, wskazania daty ostatniej modyfikacji itd. Znajdują się we wszystkich, poza
najprostszymi, skryptach PHP.
Interpretator PHP zignoruje każdy tekst zawarty w komentarzu. Czyni tak zwłaszcza analizator skła-
dniowy, który omija komentarze, traktując je jak odstępy.
Poniżej znajduje się wielowierszowy komentarz w stylu C, który może pojawić się na początku
skryptu:
/* Autor: Janek Nowak
Ostatnia modyfikacja: 10 kwietnia
Ten skrypt przetwarza zamówienia klientów
*/
Komentarze wielowierszowe powinny rozpoczynać się symbolem /*, a kończyć */. Tak jak
w języku C, nie mogą być one zagnieżdżane.
W obu stylach wszystko, co następuje po symbolu komentarza (# lub //), jest traktowane jako
komentarz. Dzieje się tak, dopóki nie zostanie osiągnięty koniec wiersza bądź kończący znacznik
PHP, w zależności od tego, co pojawi się wcześniej.
W poniższym wierszu kodu tekst oto komentarz, występujący przed znacznikiem zamykającym,
stanowi część komentarza. Tekst a to już nie, znajdujący się za znacznikiem zamykającym, będzie
natomiast traktowany jako zwykły kod HTML, ponieważ nie występuje on między znacznikami:
// oto komentarz ?> a to już nie
Podstawowym powodem stosowania przetwarzanego przez serwer języka skryptowego jest moż-
liwość dostarczania użytkownikom stron o dynamicznej treści. Jest to ważna funkcja, ponieważ
zawartość dostosowująca się do potrzeb użytkownika bądź ulegająca nieustannym zmianom przy-
kuwa na trwałe uwagę odwiedzających stronę. PHP pozwala na łatwe zastosowanie tej funkcji.
Na początek prosty przykład. Należy zamienić kod PHP w pliku przetworzzamowienie.php nastę-
pującym kodem:
<?php
echo '<p>Zamówienie przyjęte o ';
echo date('H:i, jS F Y');
echo '</p>';
?>
40 Część I Stosowanie PHP
Ten sam kod można by również zapisać w jednym wierszu, wykorzystując do tego operator kon-
katenacji (.), na przykład:
<?php
echo '<p>Zamówienie przyjęte o '.date('H:i, jS F Y').'</p>';
?>
W powyższym kodzie wbudowana w PHP funkcja date() została zastosowana po to, aby prze-
kazać klientowi datę i godzinę przyjęcia jego zamówienia. Dane te ulegną zmianie przy każdym
uruchomieniu skryptu. Wynik jednego z uruchomień jest przedstawiony na rysunku 1.3.
Rysunek 1.3.
Funkcja PHP date()
zwraca sformatowaną
datę
Wywoływanie funkcji
Warto zwrócić uwagę na ogólny sposób wywoływania funkcji date(). PHP posiada bardzo roz-
budowaną bibliotekę funkcji używanych do tworzenia aplikacji WWW. Większość z nich wymaga
dostarczenia im pewnych danych oraz zwraca inne.
Oto wywołanie funkcji:
echo date('H:i, jS F')
Należy zauważyć, że w nawiasach znajduje się łańcuch znaków przekazywany funkcji. Element
znajdujący się w nawiasach nazywany jest argumentem lub parametrem funkcji. Argumenty są
używane przez funkcję na wejściu, aby zwrócić określone wyniki.
Jeśli funkcja date() wyświetli ostrzeżenie informujące o tym, że nie została określona
strefa czasowa, to do pliku php.ini należy dodać dyrektywę konfiguracyjną date.timezone.
Więcej informacji na ten temat można znaleźć w przykładowym pliku php.ini zamieszczonym
w dodatku A.
Pełna lista formatów rozpoznawanych przez funkcję date() znajduje się w rozdziale 19.
Rozdział 1. Podstawowy kurs PHP 41
Zmienne formularza
W skrypcie PHP można uzyskać dostęp do każdego pola formularza, traktując je jako zmienne
o nazwach identycznych jak nazwy pól. Nazwy zmiennych PHP można rozpoznać po tym, że
zaczynają się od znaku $. (Zapominanie o znaku dolara jest częstym błędem programistycznym).
Zależnie od wersji PHP i ustawień można stosować kilka różnych sposobów uzyskiwania dostępu
do danych za pośrednictwem zmiennych. W ostatnich wersjach PHP wszystkie z tych metod
oprócz jednej zostały uznane za przestarzałe, dlatego osoby, które używały PHP w przeszłości, mu-
szą zwrócić na to uwagę.
Dostęp do zawartości pola iloscopon można uzyskać w następujący sposób:
$_POST['iloscopon']
$_POST jest tablicą zawierającą dane przesłane żądaniem HTTP POST, czyli wysłane z formularza
używającego metody POST. Istnieją trzy takie tablice, które mogą zawierać dane przesyłane z formu-
larzy: $_POST, $_GET oraz $_REQUEST. Jedna z tablic — $_GET lub $_POST — zawiera wszystkie
szczegółowe dane o wszystkich zmiennych formularza. To, która z tych tablic zostanie użyta,
zależy od tego, czy metodą wykorzystywaną w formularzu była metoda GET, czy POST. Ponadto
wszystkie dane przesłane którąkolwiek z tych dwóch metod są dostępne w tablicy $_REQUEST.
Jeśli formularz zostanie przesłany metodą POST, to dane wpisane w polu iloscopon będą przecho-
wywane w $_POST['iloscopon']. Jeżeli natomiast zostanie on przesłany metodą GET, dane będą
się znajdować w $_GET['iloscopon'].
Tablice te są nowymi tak zwanymi tablicami superglobalnymi. Wrócimy do nich, gdy będziemy
omawiać zasięg zmiennych.
Zajmijmy się przykładem, w którym utworzone zostaną łatwiejsze w użyciu kopie zmiennych.
W celu skopiowania wartości jednej zmiennej do drugiej zmiennej należy zastosować operator
przypisania, którym w PHP jest znak równości (=). Poniższy wiersz kodu utworzy nową zmienną
o nazwie $iloscopon i skopiuje zawartość $_POST['iloscopon'] do nowej zmiennej:
$iloscopon = $_POST['iloscopon'];
Umieśćcie ten blok kodu na początku skryptu przetwarzającego formę. Wszystkie pozostałe skrypty
z tej książki obsługujące dane z formy będą zawierać na początku analogiczny blok. Kod ten nie
spowoduje wygenerowania żadnych danych wyjściowych, dlatego nie będzie miało znaczenia,
czy umieścicie go powyżej, czy poniżej znacznika <html> i innych znaczników HTML rozpoczyna-
jących stronę. Generalnie umieszczamy takie bloki na samym początku skryptu, aby łatwiej było
je znaleźć.
<?php
//utwórz krótkie nazwy zmiennych
$iloscopon = $_POST['iloscopon'];
$iloscoleju = $_POST['iloscoleju'];
$iloscswiec = $_POST['iloscswiec'];
?>
42 Część I Stosowanie PHP
Kod ten spowoduje utworzenie trzech nowych zmiennych: $iloscopon, $iloscoleju oraz
$iloscswiec, a następnie przypisze im dane wysłane z formy metodą POST.
Aby wyświetlić wartości tych zmiennych na stronie w przeglądarce, należy użyć następującego zapisu:
echo $iloscopon.' opon<br />';.
Na tym etapie nie sprawdzamy jeszcze zawartości zmiennych, aby upewniać się, że w każdym
polu formularza wpisano sensowne dane. Można samodzielnie wpisać jakieś zupełnie nieprawi-
dłowe dane i sprawdzić, co się stanie. Po przeczytaniu całego rozdziału nic jednak nie stanie na
przeszkodzie, by dodać do skryptu fragment kodu weryfikującego poprawność danych.
Na razie wystarczy wiedzieć, że dane wpisane przez użytkownika można wyświetlić w przeglą-
darce, przekazując je wcześniej do funkcji htmlspecialcharc(). Na przykład w poniższym przykła-
dzie moglibyśmy to zrobić w następujący sposób:
echo htmlspecialchars($iloscopon).' opon<br />';
Aby skrypt zaczął dawać widoczne efekty, należy umieścić na jego końcu poniższy fragment
kodu PHP:
echo '<p>Przyjęto następujące zamówienie:</p>';
echo htmlspecialchars($iloscopon).' opon<br />';
echo htmlspecialchars($iloscoleju).' butelek oleju<br />';
echo htmlspecialchars($iloscswiec).' świec zapłonowych<br />';
Po odświeżeniu zawartości okna przeglądarki skrypt powinien dać rezultat podobny do przedstawio-
nego na rysunku 1.4. Konkretne wartości zależą oczywiście od danych wpisanych do formularza.
Rysunek 1.4.
W skrypcie
przetworzzamowienie.php
łatwo uzyskać dostęp
do zmiennych formularza
wpisanych przez
użytkownika
W kolejnych podrozdziałach poruszymy kilka spraw, które są w tym przykładzie godne uwagi.
Rozdział 1. Podstawowy kurs PHP 43
Kropka, nazywana operatorem łączenia łańcuchów znaków, jest używana do dodawania do siebie
łańcuchów (fragmentów tekstu). Często stosuje się ją przy wyświetlaniu wyników w przeglądarce
z użyciem instrukcji echo. W ten sposób można uniknąć konieczności wpisywania kilku takich
instrukcji.
Każdą zmienną niebędącą tablicą można również umieścić w łańcuchu znaków otoczonym cudzy-
słowami w celu jej wyświetlenia (tablice są nieco bardziej skomplikowane, dlatego łączeniem
tablic i łańcuchów znaków zajmiemy się w rozdziale 4.). Na przykład:
$iloscopon = htmlspecialchars($iloscopon);
echo "$iloscopon opon<br />";
Wyrażenie to jest równoznaczne z pierwszą instrukcją przedstawioną w tym punkcie. Każdy z tych
formatów jest uznawany, a wybór jednego z nich — całkowicie dowolny. Proces taki, czyli zastę-
powanie wewnątrz łańcucha znaków zmiennej przez jej zawartość, nazywa się interpolacją.
Zwróćcie uwagę, że interpolacja jest cechą wyłącznie łańcuchów znaków otoczonych cudzysłowa-
mi. Nie można w taki sam sposób umieszczać nazw zmiennych w łańcuchach znaków otoczonych
apostrofami. Uruchomienie poniższego wiersza kodu:
echo '$iloscopon opon<br />';
W PHP istnieją dwa typy łańcuchów znaków: zapisane w cudzysłowach i zapisane w apostrofach.
PHP próbuje interpretować łańcuchy (czyli wyszukiwać nazwy zmiennych i zamieniać je na ich
wartości) w cudzysłowach, dając rezultat, który można zobaczyć powyżej. Łańcuchy w apostro-
fach zostaną potraktowane jako prawdziwe surowe dane.
Istnieje również trzeci sposób definiowania łańcuchów. Jest to składnia heredoc (<<<), znana
już użytkownikom języka Perl. Pozwala ona na zgrabne definiowanie długich łańcuchów znaków
poprzez używanie znacznika zamykającego, który oznacza koniec łańcucha. Poniższy przykładowy
kod tworzy łańcuch złożony z trzech wierszy, a następnie go wyświetla:
44 Część I Stosowanie PHP
echo <<<koniec
wiersz 1
wiersz 2
wiersz 3
koniec
Identyfikatory
Identyfikatory to nazwy zmiennych. (Nazwy funkcji i klas również są identyfikatorami;
funkcje i klasy zostaną omówione w rozdziałach 5. i 6.). Istnieje kilka prostych zasad dotyczących
identyfikatorów:
Identyfikatory, które mogą mieć dowolną długość, składają się z liter, cyfr, znaków
podkreślenia i znaków dolara.
Identyfikatory nie mogą rozpoczynać się cyfrą.
W PHP rozróżniana jest wielkość liter identyfikatorów; $iloscopon to nie to samo
co $IloscOpon. Próba zamiennego ich stosowania to częsty błąd programistyczny.
Wyjątkiem są funkcje wbudowane w PHP — ich nazwy mogą być używane w każdej formie.
Identyfikatory zmiennych mogą mieć nazwę identyczną z wbudowaną funkcją. Należy
jednak unikać tego kłopotliwego rozwiązania. Ponadto nie można utworzyć funkcji
o identyfikatorze takim samym jak funkcja wbudowana.
Jedną z cech PHP jest to, że nie trzeba deklarować zmiennych przed ich użyciem. Zmienna zostanie
utworzona po pierwszym przypisaniu jej wartości. Więcej na ten temat w następnym podrozdziale.
Każdy z tych dwóch wierszy tworzy zmienną i przypisuje jej dosłowną wartość. Można również
przyporządkować zmiennym wartości innych zmiennych, jak w poniższym przykładzie:
$ilosc = 0;
$wartosc = $ilosc;
Typy zmiennych
Typ zmiennej odnosi się do rodzaju danych w niej zapisanych. PHP udostępnia cały zestaw typów
danych. Dane różnego rodzaju mogą być przechowywane przy użyciu różnych typów danych.
Rozdział 1. Podstawowy kurs PHP 45
Dostępne są również trzy specjalne typy danych: NULL, resource oraz callable.
Zmienne, którym nie nadano wartości, które nie są ustawione lub którym nadano wartość NULL, są
typu NULL. Pewne funkcje wbudowane (na przykład funkcje obsługi baz danych) zwracają zmienne
typu resource.
Reprezentują one zasoby zewnętrzne (na przykład połączenie z bazą danych). Niemal na pewno
nigdy nie będziecie operować bezpośrednio na zmiennych typu resource, lecz są one często zwra-
cane przez funkcje i muszą być przekazywane do innych funkcji jako ich parametry.
Dane typu callable są w zasadzie funkcjami, które mogą być przekazywane do innych funkcji.
Siła typu
Typy w PHP są słabo zaznaczone. W większości języków programowania, na przykład w C,
zmienne mogą przechowywać tylko jeden typ danych, który musi zostać zadeklarowany przed ich
użyciem. W PHP typ zmiennej jest określany przez dane do niej przypisane.
Na przykład kiedy zmienne $ilosc i $wartosc zostały zadeklarowane powyżej, nastąpiło również
określenie ich początkowego typu:
$ilosc = 0;
$wartosc = 0.00;
Ponieważ $ilosc przypisano 0, liczbę całkowitą, jest ona w tym momencie zmienną typu integer.
Podobnie $wartosc jest zmienną typu float.
Zmienna $wartosc jest teraz typu string. PHP dostosowuje typ zmiennej do danych przechowy-
wanych w niej w danym momencie.
Zdolność do niewidocznej zmiany typów „w locie” może okazać się niezwykle przydatna. Należy
pamiętać, że PHP „wie”, jaki typ danych zostaje umieszczony w zmiennej. Zwróci on dane o tym
samym typie przy odzyskiwaniu ich ze zmiennej.
46 Część I Stosowanie PHP
Rzutowanie typu
Stosując rzutowanie typu, można udawać, że zmienna bądź wartość są innego typu niż w rzeczy-
wistości. Sposób ten działa identycznie jak w C. Należy po prostu podać w nawiasach przed wła-
ściwą zmienną nazwę pożądanego typu.
Drugi wiersz oznacza: „Weź wartość zapisaną w $ilosc, zinterpretuj ją jako float i zapisz
w $wartosc”. Zmienna $wartosc jest w tym wypadku typu float. Rzutowane zmienne nie zmieniają
typu, a zatem $ilosc pozostaje typem integer.
Można także użyć wbudowanej funkcji służącej do sprawdzania i ustawiania konkretnego typu,
która zostanie przedstawiona w dalszej części tego rozdziału.
Zmienne zmiennych
PHP dostarcza zmiennych jeszcze jednego typu, zwanych zmiennymi zmiennych. Pozwalają one na
dynamiczną zmianę nazwy zmiennej.
Jak widać, PHP daje dużo swobody w tej dziedzinie. Wszystkie języki pozwalają na zmianę
wartości zmiennej, lecz niewiele umożliwia zmianę jej typu, a jeszcze mniej — zmianę nazwy.
Zmienna zmiennej działa poprzez użycie wartości jednej zmiennej jako nazwy drugiej. A oto
przykład:
$nazwazmiennej = "iloscopon";
Można teraz użyć zmiennej $$nazwazmiennej zamiast $iloscopon. Przykładowo można nadać
wartość $iloscopon w następujący sposób:
$$nazwazmiennej = 5;
Metoda ta wydaje się nieco niejasna, lecz w dalszej części książki zostanie omówiona dokładniej.
Zamiast wypisywania i oddzielnego używania każdej ze zmiennych formularza można użyć
pętli i zmiennej do automatycznego przetworzenia ich wszystkich. W podrozdziale poświęconym
pętli for, znajdującym się w dalszej części rozdziału, podaliśmy przykład zastosowania tej metody.
W przykładowej aplikacji można przechować ceny każdej sprzedawanej części jako stałe. Defi-
niuje się je, stosując funkcję define:
Rozdział 1. Podstawowy kurs PHP 47
define("CENAOPON", 100);
define("CENAOLEJU", 10);
define("CENASWIEC", 4);
Powyższe wiersze należy dodać do kodu skryptu. W ten sposób w przykładowej aplikacji będą
istnieć teraz trzy stałe, które będą mogły zostać użyte do obliczenia wartości zamówienia klienta.
Łatwo zauważyć, że nazwy stałych są zapisane w całości wielkimi literami. Jest to konwencja
zapożyczona z C, która pozwala na łatwe odróżnienie zmiennych od stałych. Nie ma ona charak-
teru obligatoryjnego, lecz jej stosowanie czyni kod łatwiejszym do odczytania i utrzymania.
Jedna z ważnych różnic pomiędzy stałymi i zmiennymi polega na tym, że przy odwołaniu do stałej
nie umieszcza się przed jej nazwą znaku dolara, lecz jedynie samą nazwę. Na przykład aby użyć
jednej z powyższych stałych, można napisać:
echo CENAOPON;
Oprócz stałych zdefiniowanych przez użytkownika PHP tworzy dużą liczbę własnych stałych.
Prostym sposobem na ich obejrzenie jest użycie polecenia phpinfo():
phpinfo();
Polecenie to da wynik w postaci listy predefiniowanych przez PHP zmiennych i stałych, jak i innych
pożytecznych informacji. Niektóre z nich zostaną wyjaśnione w dalszej części książki.
Jeszcze jedna różnica między zmiennymi i stałymi polega na tym, że stałe mogą przechowywać
jedynie dane typów boolean, integer, float lub string. Typy te ogólnie nazywa się mianem warto-
ści skalarnych.
Zasięg zmiennych
Termin zasięg odnosi się do części skryptu, w której widoczna jest dana zmienna. Istnieje sześć
podstawowych typów zasięgów w PHP:
Wbudowane zmienne superglobalne są widoczne w całym skrypcie.
Stałe, po ich zadeklarowaniu, są zawsze widoczne globalnie. Oznacza to, że można ich
używać zarówno wewnątrz, jak i na zewnątrz funkcji.
Zmienne globalne zadeklarowane w skrypcie są widoczne w całym skrypcie, ale nie
wewnątrz funkcji.
Zmienne używane w obrębie funkcji, które są deklarowane jako globalne, odnoszą się
do zmiennej globalnej o tej samej nazwie.
Zmienne utworzone wewnątrz funkcji i zadeklarowane jako statyczne są niewidoczne
na zewnątrz funkcji, lecz zachowują swą wartość w czasie między wykonaniem tej
i następnej funkcji (więcej na ten temat w rozdziale 5.).
Zmienne utworzone wewnątrz funkcji są jej zmiennymi lokalnymi i kończą swój żywot
w momencie zakończenia wykonywania tej funkcji.
Tablice $_GET i $_POST, a także niektóre inne specjalne zmienne, posiadają własne zasady rządzące
ich zasięgiem. Są to tak zwane zmienne superglobalne i mogą być widoczne wszędzie — zarów-
no wewnątrz funkcji, jak i poza nimi.
48 Część I Stosowanie PHP
Zasięg omówimy dokładniej w dalszej części tego rozdziału, przy okazji opisu funkcji i klas. Do tego
czasu wszystkie używane zmienne będą z definicji globalne.
Używanie operatorów
Operatory to symbole używane do manipulowania wartościami i zmiennymi poprzez wykonywanie
na nich operacji. Niektóre będą potrzebne do obliczenia wartości i podatku od zamówienia klienta.
Dwa z nich zostały już opisane — operator przypisania (=) i operator łączenia łańcuchów znaków
(.). W następnym punkcie przedstawiona zostanie ich pełna lista.
Ogólnie rzecz biorąc, operatory działają na jednym, dwóch bądź trzech argumentach, przy
czym większość — na dwóch. Na przykład operator przypisania ingeruje w dwa — miejsce prze-
chowywania po lewej stronie symbolu = i wyrażenie po prawej stronie. Argumenty te są nazywane
operandami, to znaczy wartościami, na których się operuje.
Operatory arytmetyczne
Operatory arytmetyczne działają na bardzo prostej zasadzie — są zwykłymi operatorami mate-
matycznymi. Zostały one przedstawione w tabeli 1.1.
Dodawanie i odejmowanie działa tak, jak należy się tego spodziewać. Wynikiem działań jest, od-
powiednio, dodawanie bądź odejmowanie wartości zapisanych w zmiennych $a i $b.
Rozdział 1. Podstawowy kurs PHP 49
- Różnica $a - $b
* Iloczyn $a * $b
/ Iloraz $a / $b
% Modulo $a % $b
Można również użyć symbolu różnicy (-) jako operatora jednoargumentowego — tzn. takiego,
który posługuje się tylko jednym argumentem lub operandem — w celu oznaczenia liczb ujem-
nych. Na przykład:
$a = -1;
Mnożenie i dzielenie również działa w przeważającej liczbie wypadków tak, jak należy się tego
spodziewać. Warto jedynie zauważyć symbol gwiazdki jako operator iloczynu, zamiast symbolu
zwykle stosowanego, oraz symbol ukośnika jako operator ilorazu, również w miejsce symbolu
standardowego.
Operator reszty zwraca pozostałość po podzieleniu zmiennej $a przez zmienną $b. Należy rozwa-
żyć następujący fragment kodu:
$a = 27;
$b = 10;
$wynik = $a%$b;
Wartość zachowana w zmiennej $wynik jest resztą z dzielenia 27 przez 10; wynosi ona 7.
Operatory łańcuchowe
Powyżej omówiliśmy przykład zastosowania jedynego operatora łańcuchowego. Operatora łączenia
(konkatenacji) używa się do dodawania łańcuchów oraz tworzenia i przechowywania wyniku
w podobny sposób, jak w przypadku operatora sumy, aby dodać dwie cyfry.
$a = "Części samochodowe ";
$b = 'Janka';
$wynik = $a.$b;
Operatory przypisania
Opisany już został podstawowy operator przypisania (=). Symbol = zawsze traktuje się jako operator
przypisania i należy go odczytywać: „jest przypisana wartość”. Na przykład:
$ilosc = 0;
50 Część I Stosowanie PHP
Powyższa instrukcja powinna być przeczytana następująco: „zmiennej $ilosc jest przypisana
wartość zero”. Powody tego zostaną podane w dalszej części rozdziału, podczas opisywania operato-
rów porównania.
Działanie to spowoduje przypisanie zmiennej $b wartości 11. Ogólna zasada działania operatorów
przypisania polega na tym, że wartością całego wyrażenia jest wartość przypisana lewemu
operandowi.
Jak pokazano powyżej, podczas obliczania wartości wyrażenia można, identycznie jak w mate-
matyce, używać nawiasów w celu ustanowienia kolejności wykonywania poszczególnych części
wyrażenia.
jest równoznaczny z:
$a = $a + 5;
Istnieją łączone operatory przypisania dla każdego operatora arytmetycznego i dla operatora łączenia
łańcuchów. Zestawienie wszystkich łączonych operatorów przypisania wraz z ich wynikami jest
przedstawione w tabeli 1.2.
-= $a -= $b $a = $a - $b
*= $a *= $b $a = $a * $b
/= $a /= $b $a = $a / $b
%= $a %= $b $a = $a % $b
.= $a .= $b $a = $a . $b
Rozdział 1. Podstawowy kurs PHP 51
Drugi wiersz zawiera operator preinkrementacji — nazywany tak, ponieważ symbol ++ pojawia się
przed znakiem $a. Operator ten, po pierwsze, zwiększa $a o 1, a po drugie — zwraca powiększoną
wartość. W tym przypadku wartość zmiennej $a rośnie do 5, po czym zostaje ona zwrócona i wy-
drukowana. Wartość całego wyrażenia wynosi 5 (należy zauważyć, że nie zostaje zwrócona wartość
$a + 1, lecz na stałe zwiększona wartość $a).
Natomiast gdy symbol ++ występuje po $a, użyty zostanie operator postinkrementacji, co daje
odmienny rezultat. Rozważmy następujący zapis:
$a=4;
echo $a++;
W tym przypadku efekty zostają odwrócone. Po pierwsze, zwrócona i wydrukowana zostaje war-
tość $a, która później ulega powiększeniu. Wartość całego wyrażenia wynosi 4, i właśnie ona zostaje
zwrócona. Natomiast po wykonaniu tej instrukcji wartość $a zostanie zwiększona do 5.
Jak łatwo się domyślić, działanie operatora -- (dekrementacji) jest podobne, lecz wartość $a zostaje
zmniejszona, a nie zwiększona.
Operatory referencji
Operator referencji (&, czyli ampersand), może być stosowany w połączeniu z przypisaniem.
W większości przypadków, kiedy jedna zmienna jest przypisywana do drugiej, zostaje utworzona
kopia pierwszej zmiennej, zapisana w drugiej. Na przykład:
$a = 5;
$b = $a;
Powyższe wiersze kodu tworzą kopię wartości zmiennej $a i zapisują ją w $b. Jeżeli później zostanie
zmieniona wartość $a, $b nie ulegnie modyfikacji:
$a = 7; // $b łańcuch będzie miało wartość 5
Odwołania mogą sprawiać nieco kłopotów. Należy pamiętać, że odwołanie jest rodzajem nazwy
zastępczej (określanej potocznie mianem aliasu), a nie wskaźnikiem. Z tego względu $a i $b
wskazują na ten sam fragment pamięci. Można to zmienić, usuwając jedną z tych zmiennych w na-
stępujący sposób:
unset($a);
Usunięcie jej nie spowoduje zmiany wartości zmiennej $b (7), lecz zniszczy połączenie między
zmienną $a i wartością 7 przechowywaną w pamięci.
52 Część I Stosowanie PHP
Operatory porównań
Operatorów porównań używa się w celu porównania dwóch wartości. Wyrażenia, w których
występują te operatory, zwracają jedną z dwóch wartości logicznych, true lub false, zależnie od
wyniku porównania.
Operator równości
Operator równości, symbol == (dwa znaki równości) pozwala na sprawdzenie równości dwóch
wartości. Na przykład można zastosować wyrażenie:
$a == $b
aby sprawdzić, czy wartości zmiennych $a i $b są sobie równe. Wynik zwrócony przez to wyrażenie
będzie wynosił true, jeżeli są równe, lub false, jeżeli nie.
Łatwo jest pomylić ten znak z symbolem =, operatorem przypisań. Takie wyrażenie zadziała bez
zwrócenia błędu, lecz raczej nie da pożądanego wyniku. Ogólnie rzecz biorąc, wartości niezerowe są
przyjmowane jako true, a zerowe jako false. Przy założeniu, że zmienne zostały zainicjowane
w następujący sposób:
$a = 5;
$b = 7;
Testowanie za pomocą wyrażenia $a = $b zwróci wynik true. Dzieje się tak dlatego, że wartość
$a = $b jest wartością przypisaną do lewej strony tego wyrażenia, która w tym wypadku wynosi 7.
Jest to wartość niezerowa, tak więc wyrażenie zostaje przyjęte jako true. Jeżeli założeniem było
przetestowanie $a == $b, którego wynikiem jest false, został wprowadzony do kodu błąd logiczny,
niekiedy bardzo trudny do odnalezienia. Należy zawsze sprawdzać, czy zastosowany został właści-
wy operator.
Użycie operatora przypisania zamiast operatora porównania jest błędem, który łatwo popełnić i który
zdarza się niejednokrotnie w karierze każdego programisty.
Operatory logiczne
Operatory logiczne stosuje się w celu uzyskania wyników operacji logicznych. Aby na przykład
ustalić, czy wartość zmiennej $a mieści się w zakresie od 0 do 100, należy sprawdzić warunki
$a >= 0 i $a <= 100, stosując operator AND:
$a >= 0 && $a <= 100
PHP rozpoznaje warunki logiczne AND, OR, XOR (wyłączne OR) i NOT.
!= Nierówność $a != $b
|| OR $a || $b Zwraca true, jeżeli $a lub $b, lub oba są true, w przeciwnym wypadku
false
and AND $a and $b Tak samo jak &&, lecz z mniejszym pierwszeństwem
or OR $a or $b Tak samo jak ||, lecz z mniejszym pierwszeństwem
xor XOR $a xor $b Zwraca true, jeżeli $a albo $b wynosi true, oraz false, jeżeli obie
zmienne mają wartość true albo obie mają wartość false.
Operatory and i or posiadają mniejsze pierwszeństwo niż operatory && i ||. Zasady pierwszeństwa
zostaną opisane szerzej w dalszej części tego rozdziału.
Operatory bitowe
Operatory bitowe pozwalają na traktowanie zmiennej typu integer jako ciągu bitów użytych do
jej przedstawienia. W PHP operatory bitowe nie mają wprawdzie zbyt szerokiego zastosowania,
lecz mimo to ich zestawienie przedstawione jest w tabeli 1.5.
Pozostałe operatory
Oprócz operatorów opisanych powyżej istnieje jeszcze kilka innych.
Przecinek (,), jest stosowany do oddzielania argumentów funkcji i innych list składników.
Dwa operatory specjalne, new i ->, są używane odpowiednio do tworzenia obiektów klas i dostępu
do składowych klasy. Zostaną one opisane w rozdziale 6.
Istnieje poza tym kilka innych typów operatorów, które zostaną krótko scharakteryzowane poniżej.
54 Część I Stosowanie PHP
Operator trójkowy
Operator trójkowy, symbol ?:, działa według następującego wzoru:
warunek ? wartość, jeżeli prawdziwy : wartość, jeżeli fałszywy
Operator trójkowy jest podobny do wersji wyrażenia instrukcji if – else, które opiszemy w dalszej
części tego rozdziału.
Bez operatora @ ten wiersz wygeneruje ostrzeżenie o dzieleniu przez zero (warto spróbować). Kiedy
operator występuje, błąd zostaje stłumiony.
Przy stosowaniu takiego tłumienia ostrzeżeń należy stosować kod obsługujący błędy, aby sprawdzić,
kiedy wystąpiło ostrzeżenie. Jeżeli w konkretnym egzemplarzu PHP w pliku php.ini jest włą-
czona opcja track_errors, komunikat o błędzie będzie przechowywany w zmiennej globalnej
$php_errormsg.
Operator wykonania
Operator wykonania to w istocie para operatorów (``). Symbol ten można znaleźć zazwyczaj na
tym samym klawiszu, co tyldę (~).
PHP będzie próbował wykonać dowolny kod zawarty pomiędzy tymi symbolami, interpretując go
jako polecenie wykonywane w wierszu poleceń serwera. Wartością wyrażenia jest wynik polecenia.
Rozdział 1. Podstawowy kurs PHP 55
Każde z tych poleceń utworzy wydruk katalogów i zapisze go w zmiennej $out. Może on być
później wyświetlony przez przeglądarkę lub obrabiany w dowolny sposób.
Istnieją również inne sposoby wykonywania poleceń przy użyciu serwera. Zostaną one opisane
w rozdziale 17.
Operatory tablicowe
Istnieje szereg operatorów tablicowych. Operatory elementu tablicy ([]) umożliwiają uzyskanie
dostępu do elementów tablicy. W kontekście niektórych tablic można także używać operatora =>.
Operatory te zostaną bliżej przedstawione w rozdziale 3.
Można również używać wielu innych operatorów tablicowych. Zostaną one również szczegółowo
opisane w rozdziale 3., lecz dla kompletności wywodu zostaną przedstawione także poniżej.
Nietrudno zauważyć, że wszystkie operatory tablicowe przedstawione w tabeli 1.6 mają od-
powiadające sobie operatory działające na zmiennych skalarnych. Wystarczy tylko zapamiętać,
że + wykonuje operację dodawania na danych skalarnych i unię na tablicach — nawet jeśli nie
przejawia się szczególnego zainteresowania zasadami arytmetyki różniącymi te dwa działania
— a działanie operatorów szybko nabierze sensu. Nie istnieje natomiast możliwość użytecznego
porównywania tablic z danymi skalarnymi.
Operator typu
Dostępny jest jeden operator typu: instanceof. Jest on używany w programowaniu zorientowanym
obiektowo, lecz wspominamy o nim tutaj dla zapewnienia kompletności wywodu (programowanie
zorientowane obiektowo zostanie szerzej opisane w rozdziale 6.).
Operator instanceof pozwala na sprawdzanie, czy obiekt jest egzemplarzem konkretnej klasy,
na przykład:
56 Część I Stosowanie PHP
class przykladowaKlasa();
$mojObiekt = new przykladowaKlasa();
if ($mojObiekt instanceof przykladowaKlasa)
echo "mojObiekt jest egzemplarzem klasy przykladowaKlasa";
$wartosc = 0.00;
define('CENAOPON', 100);
define('CENAOLEJU', 10);
define('CENASWIEC', 4);
Po odświeżeniu zawartości strony w oknie przeglądarki powinien pojawić się wynik podobny do
przedstawionego na rysunku 1.5.
Rysunek 1.5.
Wszystkie sumy
dla zamówienia klienta
zostały obliczone,
sformatowane
i wyświetlone
Jak łatwo zauważyć, w powyższym kodzie zastosowano kilka operatorów. Użyto operatorów su-
my (+) i iloczynu (), aby obliczyć wartość, oraz operatora łączenia łańcuchów (.) w celu sfor-
matowania wyniku dla przeglądarki.
Rozdział 1. Podstawowy kurs PHP 57
Zastosowano również funkcję number_format(), aby sformatować sumy jako łańcuch znaków
z dwoma miejscami po przecinku. Funkcja ta pochodzi z biblioteki matematycznej (Math) PHP.
Po bliższym przyjrzeniu się obliczeniom można zastanawiać się, dlaczego zostały one wykonane
właśnie w takim porządku. Rozważmy na przykład następującą instrukcję:
$wartosc = $iloscopon * CENAOPON
+ $iloscoleju * CENAOLEJU
+ $iloscswiec * CENASWIEC;
Całkowita wartość wydaje się poprawna, ale dlaczego operacje mnożenia zostały wykonane przed
dodawaniem? Otóż należy zwrócić uwagę na pierwszeństwo operatorów, to znaczy porządek,
w którym są one brane pod uwagę.
Pierwszeństwo i kolejność
Ogólnie rzecz biorąc, operatory posiadają zdefiniowane pierwszeństwo, czyli porządek, w jakim
są obliczane. Operatory posiadają również kolejność, to znaczy porządek, w którym są obliczane
operatory o takim samym pierwszeństwie. Generalnie istnieją trzy rodzaje kolejności: od lewej
do prawej (nazywane w skrócie lewą), od prawej do lewej (nazywane w skrócie prawą), a do niektó-
rych operatorów kolejność w ogóle nie odnosi się.
Tabela 1.7 przedstawia pierwszeństwo i kolejność operatorów w PHP. W tabeli tej operatory o naj-
niższym pierwszeństwie umieszczone są na szczycie, a pierwszeństwo wzrasta w miarę prze-
mieszczania się w dół tabeli.
Kolejność Operatory
Lewa ,
Lewa or
Lewa xor
Lewa and
Prawa print
Lewa ? :
Lewa ||
Lewa &&
Lewa |
Lewa ^
Lewa &
Lewa + - .
58 Część I Stosowanie PHP
Kolejność Operatory
Lewa ,
Lewa * / %
Prawa !
Prawa [ ]
Należy zauważyć, że operatorem o najwyższym pierwszeństwie jest ten, o którym jeszcze nic nie
zostało powiedziane — zwyczajne nawiasy. Efektem tego operatora jest podniesienie pierwszeń-
stwa dowolnego kodu zawartego pomiędzy nawiasami. W razie konieczności to sposób na omi-
nięcie reguł pierwszeństwa.
to operator iloczynu, posiadający wyższe pierwszeństwo niż operator dodawania, zostałby wyko-
nany wcześniej, i w związku z tym całe wyrażenie dałoby nieprawidłowy wynik. Stosując nawiasy,
można wymusić wcześniejsze obliczenie wyrażenia 1 + $stawkavat.
W wyrażeniu można zastosować dowolną liczbę nawiasów. Jako pierwsze obliczone zostaną te
najbardziej wewnętrzne.
Należy zwrócić uwagę na jeszcze jeden operator z tej tabeli, o którym dotąd nie wspomniano:
konstrukcję print, stanowiącą odpowiednik instrukcji echo. Obydwie konstrukcje generują dane
wyjściowe.
Generalnie w tej książce używamy instrukcji echo, lecz można też używać instrukcji print, jeśli
okaże się to bardziej czytelne. Ani print, ani echo nie są tak naprawdę funkcjami, obie jednak
można wywoływać tak jak funkcje: podając parametry w nawiasach. Obydwie można również trak-
tować podobnie jak operatory: wystarczy tylko umieścić przetwarzany łańcuch znaków za słowem
kluczowym echo lub print.
Jeśli wywołamy print jak funkcję, zwróci ona wartość (1). Własność ta może okazać się użyteczna,
gdy dane wyjściowe będą miały być generowane wewnątrz bardziej złożonego wyrażenia, lecz
z drugiej strony print jest wolniejsza niż echo.
Rozdział 1. Podstawowy kurs PHP 59
Aby zastosować funkcję gettype(), trzeba przekazać jej zmienną. Funkcja ta określi jej typ i zwróci
łańcuch zawierający nazwę typu: bool, integer, double (dla wartości typu float, co ze względów
historycznych może być nieco mylące), string, array, object, resource lub NULL. Jeżeli nie będzie
to żaden z wymienionych typów standardowych, funkcja zwróci wyrażenie "unknown type".
Aby zastosować settype(), trzeba przekazać jej zmienną, której typ ma zostać zmieniony, oraz
łańcuch zawierający nazwę pożądanego typu zmiennej, wybraną z powyższej listy.
Konwencja stosowana w tej książce oraz w dokumentacji php.net odnosi się do typu
danych mixed. Taki typ danych nie istnieje, ale ponieważ PHP jest tak elastyczny
w zakresie obsługi typów, wiele funkcji może pobierać jako argumenty różne (lub dowolne)
typy danych. Argumenty, które mogą różne typy danych, są oznaczane jako mixed.
PHP posiada również pewne funkcje testujące typ zmiennej, zależne od tego typu. Każda z nich
pobiera zmienną jako argument i zwraca wartość true lub false. Funkcje te są następujące:
is_array() — sprawdza, czy zmienna jest tablicą;
is_double(), is_float(), is_real() (funkcje równoznaczne) — sprawdza, czy zmienna
jest liczbą zmiennoprzecinkową;
is_long(), is_int(), is_integer() (funkcje równoznaczne) — sprawdza, czy zmienna
jest liczbą całkowitą;
is_string() — sprawdza, czy zmienna jest łańcuchem znaków;
is_bool() — sprawdza, czy zmienna ma wartość logiczną;
is_object() — sprawdza, czy zmienna jest obiektem;
60 Część I Stosowanie PHP
Funkcja ta pobiera zmienną jako argument i zwraca wartość true, jeżeli dana zmienna istnieje,
lub false w przeciwnym wypadku. Można do niej przekazać także listę zmiennych oddzielonych
znakiem przecinka. Wówczas isset() zwróci wartość true, jeżeli wszystkie zmienne będą istnieć.
Można usunąć z pamięci zmienną, stosując funkcję towarzyszącą powyższej, unset(). Posiada ona
następujący prototyp:
void unset(mixed zmienna [,mixed zmienna[,...]]);
Funkcja ta pozbywa się z pamięci przekazanej jej zmiennej i zwraca wartość true.
Istnieje również funkcja empty(). Sprawdza ona, czy dana zmienna istnieje i czy posiada wartość
pustą i zerową, i zwraca odpowiednio true lub false. Posiada następujący prototyp:
boolean empty(mixed zmienna);
Zmienna $iloscopon powinna zwrócić wartość 1 (true) dla funkcji isset() niezależnie od tego, czy
w formularzu została do niej wprowadzona wartość i ile ona wynosi. Wynik funkcji empty()
zależy od wprowadzonej wartości.
Zmienna $niema nie istnieje, tak więc funkcja isset() zwróci wartość pustą (false), a funkcja
empty() wynik 1 (true).
Funkcje powyższe mogą być przydatne przy sprawdzaniu, czy użytkownik wypełnił odpowiednie
pola formularza.
Rozdział 1. Podstawowy kurs PHP 61
Reinterpretacja zmiennych
Można osiągnąć wyniki podobne do rzutowania typu zmiennej poprzez wywołanie odpowiedniej
funkcji. Oto trzy funkcje przydatne do tego zadania:
int intval(mixed zmienna[, int baza=10]);
float floatval(mixed zmienna);
string strval(mixed zmienna);
Każda z nich pobiera na wejściu zmienną i zwraca jej wartość przekształconą na odpowiedni typ.
Funkcja intval() pozwala dodatkowo na wskazanie bazy konwersji, gdy konwertowana zmienna
jest łańcuchem znaków. W ten sposób można na przykład konwertować łańcuchy szesnastkowe
na liczby całkowite.
Podejmowanie decyzji
za pomocą instrukcji warunkowych
Struktury kontrolujące to struktury w obrębie języka, pozwalające na kontrolę przebiegu wykona-
nia programu bądź skryptu. Można podzielić je na struktury warunkowe (inaczej zwane rozgałę-
zionymi) i struktury powtarzalne, czyli pętle. Specyficzne zastosowania tych struktur w PHP
zostaną omówione poniżej.
Aby właściwie odpowiedzieć na życzenia klienta, kod musi być zdolny do podejmowania decyzji.
Konstrukcje dające programowi możliwość ich podejmowania są nazywane instrukcjami
warunkowymi.
Instrukcja if
Do podejmowania decyzji może być zastosowana instrukcja if, która powinna otrzymać warunek
użycia. Jeżeli wartość warunku wynosi true, zostanie wykonany następny fragment kodu. Warunki
w instrukcji if muszą być oznaczone nawiasami ().
Na przykład jeżeli klient nie zamówi ani opon, ani oleju, ani świec, zazwyczaj oznacza to przypad-
kowe naciśnięcie przycisku Złóż zamówienie jeszcze przed wypełnieniem formularza. W tym
wypadku powinna zostać wyświetlona wiadomość znacząca więcej niż „Zamówienie przyjęte”.
Kiedy odwiedzający stronę nic nie zamawia, powinna pojawić się wiadomość w rodzaju: „Na
poprzedniej stronie nie zostało złożone żadne zamówienie!”. Można to łatwo wykonać za pomocą
następującej instrukcji if:
if( $ilosc == 0 )
echo 'Na poprzedniej stronie nie zostało złożone żadne zamówienie!<br />';
Bloki kodu
W poleceniu warunkowym, takim jak if, często występuje konieczność wykonania więcej niż
jednej instrukcji. Nie ma wtedy potrzeby stosowania większej liczby warunków if. Zamiast tego
można zebrać kilka instrukcji, tworząc blok. Aby zadeklarować blok, należy zamknąć go nawiasami
klamrowymi:
if($ilosc == 0) {
echo '<p style="color:red">';
echo 'Na poprzedniej stronie nie zostało złożone żadne zamówienie!<br />';
echo '</p>';
}
Powyższe trzy wiersze kodu otoczone nawiasami klamrowymi są teraz traktowane jako blok kodu.
Kiedy wartość warunku wynosi true, zostaną wykonane wszystkie trzy wiersze. Kiedy warunek
wynosi false, wszystkie zostaną zignorowane.
Jak już wspomniano, PHP nie zwraca uwagi na wygląd kodu. Zalecane jest wcinanie kodu
w celu zwiększenia jego czytelności. Ogólnie rzecz biorąc, wcinanie stosuje się w celu
łatwego sprawdzenia, które wiersze kodu zostaną wykonane w przypadku spełnienia
warunków, które instrukcje pogrupowane są w bloki, a które są częścią pętli bądź funkcji.
W powyższych przykładach wcięta została instrukcja zależna od instrukcji if oraz instrukcje
tworzące blok.
Instrukcja else
Zazwyczaj podejmuje się decyzje nie tylko co do warunków, w których zostanie wykonana instruk-
cja, lecz są opisane wszystkie możliwe działania.
Polecenie else pozwala na zdefiniowanie działania alternatywnego, podejmowanego wtedy, gdy
wartość instrukcji if wynosi false. Należy ostrzec klientów Janka, kiedy nic nie zamawiają, w prze-
ciwnym wypadku trzeba im pokazać złożone przez nich zamówienie.
Jeżeli kod przykładu ulegnie zmianie i zostanie dodana instrukcja else, to jest możliwe wyświe-
tlenie albo ostrzeżenia, albo podsumowania zamówienia.
if($ilosc == 0) {
echo "Na poprzedniej stronie nie zostało złożone żadne zamówienie!<br />";
} else {
echo htmlspecialchars($iloscopon).' opon<br />';
echo htmlspecialchars($iloscoleju).' butelek oleju<br />';
echo htmlspecialchars($iloscswiec).' świec zapłonowych<br />';
}
Instrukcja elseif
Dla wielu podejmowanych decyzji istnieją więcej niż dwie opcje wyboru. Można utworzyć sekwen-
cję wielu opcji, stosując instrukcję elseif, która jest połączeniem instrukcji else i if. Przy użyciu
sekwencji warunków program sprawdza każdy z nich, dopóki nie znajdzie tego, którego warunek
wynosi true.
Janek wprowadził system zniżek dla zamówień na duże ilości opon. Poniżej przedstawiono schemat
tych zniżek:
zamówiono mniej niż 10 opon — brak zniżki,
zamówiono 10 – 49 opon — zniżka 5%,
zamówiono 50 – 99 opon — zniżka 10%,
zamówiono powyżej 100 opon — zniżka 15%.
Za pomocą instrukcji if i elseif można utworzyć fragment kodu obliczający wielkość zniżki.
W takim przypadku należy użyć operatora AND (&&), aby połączyć dwa warunki w jeden.
if( $iloscopon < 10 )
$znizka = 0;
elseif( $iloscopon >= 10 && $iloscopon <= 49 )
$znizka = 5;
elseif( $iloscopon >= 50 && $iloscopon <= 99 )
$znizka = 10;
elseif( $iloscopon > 100 )
$znizka = 15;
Warto zauważyć, że można zamiennie stosować zapis elseif oraz else if — oba są poprawne.
Przy tworzeniu listy instrukcji elseif należy pamiętać, że tylko jedna z instrukcji lub ich bloków zo-
stanie wykonana. W powyższym przykładzie nie miało to znaczenia, ponieważ warunki wykluczały
się wzajemnie — tylko jeden z nich mógł być spełniony w tym samym czasie. Jeżeli nastąpi sytuacja,
w której więcej niż jeden warunek okaże się prawdziwy, zostanie wykonany tylko pierwszy z nich.
Instrukcja switch
Instrukcja switch działa na zasadach podobnych do instrukcji if, lecz w jej przypadku warunek
może mieć więcej niż dwie wartości. W przypadku instrukcji if wartość warunku wynosi jedynie
true lub false. W instrukcji switch warunek może posiadać dowolną ilość wartości. Muszą one
jednak należeć do jednego z typów prostych (integer, string lub float). Należy wprowadzić
instrukcję case dla każdej wartości oraz opcjonalnie wartość domyślną, dla której nie trzeba two-
rzyć instrukcji case.
Janek chciałby wiedzieć, jaki rodzaj reklamy jest najbardziej skuteczny. Można w tym celu dodać
do formularza zamówień odpowiednie pytanie. Wprowadźmy do formularza następujący kod
HTML. Powinno to dać wynik podobny do przedstawionego na rysunku 1.6:
<tr>
<td>Jak dowiedzieli się Państwo o sklepie Janka?</td>
<td><select name="jak">
<option value = "a">Jestem stałym klientem
<option value = "b">Reklama telewizyjna
<option value = "c">Książka telefoniczna
<option value = "d">Znajomy
</select>
</td>
</tr>
64 Część I Stosowanie PHP
Rysunek 1.6.
Formularz zamówienia
„zapytuje” klientów
o źródło informacji
na temat sklepu
Powyższy kod HTML dodał nową zmienną formularza (o nazwie jak), której wartość wynosi
'a', 'b', 'c' lub 'd'. W następujący sposób można obsługiwać tę zmienną za pomocą serii instrukcji
if i elseif:
if($jak == "a") {
echo "<P>Stały klient.</p>";
} elseif($jak == "b") {
echo "<P>Reklama telewizyjna. </p>";
} elseif($jak == "c") {
echo "<P>Książka telefoniczna. </p>";
} elseif($jak == "d") {
echo "<P>Znajomy. </p>";
} else {
echo "<P>Źródło nieznane.</P>";
}
(Zwróćmy uwagę, że w obu tych przykładach zakłada się, iż wartość zmiennej $jak odczytano
z tablicy $_POST).
Rozdział 1. Podstawowy kurs PHP 65
Instrukcja switch funkcjonuje w nieco inny sposób niż instrukcje if i elseif. Polecenie if działa
tylko na jedną instrukcję, chyba że zostaną użyte nawiasy klamrowe w celu utworzenia bloku
instrukcji. Instrukcja switch działa w odwrotny sposób. Kiedy w switch zostanie włączona instrukcja
case, PHP będzie wykonywał kolejne polecenia, aż napotka instrukcję break. Bez instrukcji
przerywającej switch wykona cały kod następujący po prawdziwej wartości case. Kiedy osiągnie
instrukcję break, przeskoczy do wykonywania pierwszego wiersza kodu po poleceniu switch.
Na to pytanie nie sposób odpowiedzieć wprost. Nie ma zadań wykonywanych za pomocą jednej
lub kilku instrukcji else, elseif lub switch, których nie dałoby się wykonać przy użyciu serii
instrukcji if. Należy stosować takie instrukcje, które czynią kod jak najbardziej czytelnym. Wraz ze
wzrostem doświadczenia decyzje takie są coraz łatwiejsze.
Janek chciałby mieć tabelę wyświetlającą koszty transportu, które zostaną dodane do wartości
zamówienia klienta. Kurier, z którego usług korzysta, uzależnia koszty od odległości, na jaką prze-
słany zostaje dany towar. Koszt może być obliczony za pomocą prostego wzoru.
Rysunek 1.7.
Tabela pokazuje koszt
transportu, zmieniający
się wraz ze wzrostem
odległości
Na listingu 1.2 przedstawiony jest kod HTML tworzący powyższą tabelę. Warto zwrócić uwagę na
to, że jest on długi i posiada powtarzające się elementy.
66 Część I Stosowanie PHP
Do wpisania powyższego kodu znacznie wygodniej byłoby użyć taniego i niemęczącego się kom-
putera niż płatnego, nudzącego się pracownika.
Instrukcje pętli nakazują PHP powtarzające się wykonywanie instrukcji lub bloku.
Pętle while
Pętla while jest najprostszym typem pętli w PHP. Podobnie jak instrukcja if, zależy ona od
warunku. Różnica pomiędzy pętlą while a instrukcją if polega na tym, że instrukcja if wykonuje
następujący po niej blok kodu jednokrotnie, jeżeli jej warunek ma wartość true. Pętla while będzie
powtarzać wykonywanie bloku tak długo, jak jej warunek będzie miał wartość true.
Pętla while jest zazwyczaj używana w przypadkach, gdy nie wiadomo, ile iteracji trzeba wykonać,
by warunek pętli został spełniony. Jeśli liczba tych iteracji jest ściśle określona, to warto zastanowić
się nad zastosowaniem pętli for.
Podstawowa struktura pętli while jest następująca:
while( warunek ) wyrażenie
$cyfra = 1;
while ($cyfra <= 5 ) {
echo $cyfra."<br />";
$cyfra++;
}
Na początku każdej iteracji sprawdzany jest warunek. Jeżeli jego wartość wynosi false, blok
nie zostanie wykonany i pętla zakończy się. Zostanie wtedy wykonana pierwsza instrukcja nastę-
pująca po pętli.
Pętla while może być zastosowana do bardziej użytecznego zadania, na przykład do wyświetlenia
tabeli kosztów transportu pokazanej na rysunku 1.7. Kod na listingu 1.3 stosuje pętlę while do
utworzenia tabeli kosztów transportu.
Listing 1.3. transport.php — tworzenie tabel kosztów transportu Janka za pomocą PHP
<!DOCTYPE>
<html>
<head>
<title>Części samochodowe Janka - zestawienie kosztów przesyłek</title>
</head>
<body>
<table style="border: 0px; padding: 3px">
<tr>
<td style="background: #cccccc; text-align: center;">Odległość</td>
<td style="background: #cccccc; text-align: center;">Koszt</td>
</tr>
<?php
$odleglosc = 50;
while ($odleglosc <= 250) {
echo "<tr>
<td style=\"text-align: right;\">".$odleglosc."</td>
<td style=\" text-align: right;\">". ($odleglosc / 10) ."</td>
</tr>\n";
$odleglosc += 50;
}
?>
</table>
</body>
</html>
Aby zwiększyć czytelność kodu HTML generowanego przez ten skrypt, należy dołączyć nowe
wiersze i znaki spacji. Jak już wspomniano, przeglądarki zignorują je, lecz okażą się one ważne
dla użytkowników. Kod HTML trzeba przeglądać często — zwłaszcza gdy dane wyjściowe nie są
takie, jak oczekiwano.
Na listingu 1.3 umieszczono wewnątrz niektórych łańcuchów znaków znaki \n. W łańcuchach
znaków ograniczonych cudzysłowem sekwencja ta reprezentuje znak nowego wiersza.
Ten typ pętli może być zapisany w krótszy sposób, przy użyciu pętli for. Podstawowa struktura
pętli for jest następująca:
for( wyrażenie1; warunek; wyrażenie2)
wyrażenie3;
Pętla while z listingu 1.3 może zostać zmieniona na pętlę for. W takim przypadku kod PHP jest
następujący:
<?php
for($odleglosc = 50; $odleglosc <= 250; $odleglosc += 50) {
echo "<tr>
<td style=\"text-align: right;\">".$odleglosc."</td>
<td style=\" text-align: right;\">". ($odleglosc / 10) ."</td>
</tr>\n";
}
?>
Funkcjonalnie wersje z pętlą while i pętlą for są identyczne. Pętla for jest jednak nieco krótsza,
dokładnie o dwa wiersze.
Oba typy pętli są równoważne — żadna z nich nie jest ani lepsza, ani gorsza od drugiej. W kon-
kretnej sytuacji używa się intuicyjnie wygodniejszej.
Nawiasem mówiąc, z pętlą for można łączyć zmienne zmiennych w celu iteracji serii podobnych
pól formularza. Na przykład jeżeli dane są pola formularza o nazwach nazwa1, nazwa2, nazwa3
itd., można wykonać na nich działanie, takie jak:
for($i=1; $i <= $ilosc_pol; $i++) {
$temp= "nazwa$i";
echo htmlspecialchars($$temp).'<br />'; // czy też jakiekolwiek inne działanie
}
Stosując dynamiczne tworzenie nazw zmiennych, można uzyskać po kolei dostęp do każdego
z tych pól.
Oprócz pętli for istnieje również pętla foreach, przeznaczona do stosowania względem tablic.
Jej zastosowanie omówimy w rozdziale 3.
Pętle do..while
Ostatni omówiony tutaj typ pętli działa w trochę inny sposób. Ogólna struktura pętli do..while
wygląda następująco:
do {
wyrażenie
}
while( warunek );
Rozdział 1. Podstawowy kurs PHP 69
Pętla do..while różni się od pętli while tym, że warunek jest w niej testowany na końcu. Oznacza
to, że wyrażenie zawarte w pętli do..while zostanie wykonane co najmniej raz.
W poniższym przykładzie wartość warunku wynosi na początku false i nigdy nie zmieni się na
true, lecz pętla zostanie wykonana raz, zanim sprawdzony zostanie warunek i pętla zakończy się.
$cyfra = 100;
do {
echo $cyfra."<br />";
} while($cyfra < 1);
Aby zatrzymać wykonywanie pętli, należy zastosować instrukcję break, tak jak to zostało opisane
w podrozdziale na temat instrukcji switch. Po napotkaniu instrukcji break zostanie wykonany
pierwszy wiersz kodu za końcem pętli.
Aby zakończyć wykonywanie całego skryptu PHP, stosuje się instrukcję exit, szczególnie przy
wykrywaniu błędów. Na przykład można zmodyfikować powyższy przykład w następujący
sposób:
if($ilosc == 0) {
echo "Na poprzedniej stronie nie zostało złożone żadne zamówienie!<br />";
exit;
}
Wywołanie funkcji exit powoduje zaprzestanie wykonywania pozostałej części skryptu PHP.
Struktury tej używa się w celu ustawienia dyrektyw wykonania dla bloku kodu, to znaczy reguł
opisujących sposób, w jaki znajdujący się dalej kod powinien zostać wykonany. Na razie zaim-
plementowano tylko dwie takie dyrektywy: ticks oraz encoding.
Pierwszej z nich, dyrektywy ticks, używa się poprzez wpisanie ticks=n. Dzięki temu możliwe
jest wykonywanie konkretnej funkcji co n-ty wiersz kodu znajdującego się wewnątrz bloku
kodu, co jest szczególnie przydatne dla celów profilowania i debugowania.
Dyrektywa encoding służy do określania sposobu kodowania konkretnego skryptu, a stosuje się
ją następująco:
declare(encoding='UTF-8');
W tym przypadku, jeśli korzystamy z przestrzeni nazw, po instrukcji declare nie możemy
umieścić bloku kodu. Więcej informacji dotyczących przestrzeni nazw podano w dalszej części
książki.
Struktura sterująca declare została tutaj wspomniana jedynie w celu zachowania kompletności.
Niektóre przykłady ukazujące sposoby używania funkcji tick zostaną przedstawione w roz-
działach 25. i 26.
W następnym rozdziale
Po lekturze tego rozdziału wiecie już, jak przyjąć złożone przez klienta zamówienie i jak nim
manipulować. W następnym zostaną przedstawione metody zachowywania zamówienia, tak aby
mogło ono być później odczytane i zrealizowane.
Rozdział 2.
Przechowywanie
i wyszukiwanie danych
W poprzednim rozdziale omówiliśmy sposoby dostępu do danych umieszczonych w formularzu
HTML i metody manipulowania nimi. Teraz przedstawiamy metody zapisywania informacji w celu
późniejszego ich wykorzystania. W większości przypadków, włączając w to przykład z poprzed-
niego rozdziału, celem jest przechowanie danych i późniejsze ich załadowanie. W tym przykładzie
należy zapamiętać zamówienie klienta, aby później je zrealizować.
Plik jednorodny może mieć wiele różnych formatów, lecz zazwyczaj terminem tym oznacza się
prosty plik tekstowy. W opisywanym przykładzie dane są zapisywane w pliku tekstowym, jedno
zamówienie w jednym wierszu.
Zapisywanie zamówień w taki właśnie sposób jest rozwiązaniem bardzo prostym w realizacji, ale
zarazem jest ono obarczone pewnymi ograniczeniami, co zostanie pokazane w dalszej części
rozdziału. Przy obróbce danych znacznej wielkości stosuje się zazwyczaj bazy danych. Mimo to
72 Część I Stosowanie PHP
pliki jednorodne znajdują zastosowania i istnieją przypadki, w których wiedza na ich temat jest
konieczna.
Proces zapisu i odczytu plików w PHP przebiega tak samo jak w wielu innych podobnych języ-
kach programowania. Osoby znające język C lub skrypty powłoki Uniksa powinny bez trudu
rozpoznać podobieństwa między stosowanymi w nich procedurami.
Formularz został zmodyfikowany w celu łatwego uzyskania adresu klienta. Nowa wersja formularza
jest przedstawiona na rysunku 2.1.
Rysunek 2.1.
Wersja formularza
zamówień pobierająca
również adres klienta
Pole formularza zawierające adres klienta nosi nazwę adres. Podczas przetwarzania w PHP
daje ono zmienną, do której można się odwołać za pomocą jednego z następujących wyrażeń:
$REQUEST['adres'], $_POST['adres'] lub $_GET['adres'], zależnie od metody przesyłania formula-
rza (szczegóły opisaliśmy w rozdziale 1.).
W tym rozdziale każde nadchodzące zamówienie zostanie zapisane w tym samym pliku. Skon-
struowany później interfejs WWW pozwoli pracownikom Janka na przeglądanie przyjętych
zamówień.
Przetwarzanie plików
Zapisywanie danych w pliku następuje w trzech etapach. Są to:
1. Otwarcie pliku. Jeżeli dany plik nie istnieje, należy go utworzyć.
2. Zapisanie danych w pliku.
3. Zamknięcie pliku.
Rozdział 2. Przechowywanie i wyszukiwanie danych 73
Przy odczytywaniu danych z pliku dostępnych jest wiele sposobów ustalania ilości pobieranych
naraz danych. Rozwiązania najczęściej stosowane zostaną opisane bardziej szczegółowo w poniż-
szych punktach. Na początek przedstawimy mechanizm otwierania plików.
Otwieranie pliku
Aby otworzyć plik w PHP, stosuje się funkcję fopen(). Otwierając plik, należy zadeklarować
sposób, w jaki będzie on używany. Sposób ten nosi nazwę trybu otwarcia pliku.
Przy wywołaniu funkcja fopen spodziewa się dwóch lub trzech parametrów. Zazwyczaj stosuje
się dwa, jak pokazano w powyższym przykładzie.
Pierwszy parametr to nazwa pliku, który ma zostać otwarty. Można tu określić ścieżkę dostępu
do pliku, jak w powyższym przykładzie; plik zamowienia.txt znajduje się w katalogu zamówień.
74 Część I Stosowanie PHP
Podobnie jak w przypadku nadawania zmiennym formy krótkich nazw, na początku skryptu
należy umieścić następujący wiersz:
$document_root = $_SERVER['DOCUMENT_ROOT'];
w celu skopiowania zawartości zmiennej noszącej nazwę długą do zmiennej o krótkiej nazwie.
Można także podać bezwzględną ścieżkę dostępu do pliku. Jest to ścieżka określana względem
głównego katalogu systemu plików (w systemie Unix jest to /, a w systemie Windows zazwyczaj
jest to C:\). W przypadku serwera działającego w systemie Unix ścieżka ta mogłaby mieć nastę-
pującą postać: /dane/zamowienia. Jeżeli ścieżka nie zostanie podana, PHP będzie szukał pliku
i ewentualnie utworzy go w tym samym katalogu, w którym znajduje się skrypt. Może się to różnić
w zależności od faktu, czy PHP jest uruchamiany poprzez jakiś skrypt CGI, i zależy od konfiguracji
serwera.
W środowisku Uniksa stosuje się ukośniki (/), natomiast w środowisku Windows można używać
lewych (\) lub prawych ukośników (/), które muszą jednak zostać oznaczone jako znaki specjalne,
aby funkcja fopen właściwie je zinterpretowała. W tym celu należy po prostu dodać przed każdym
symbolem jeszcze jeden lewy ukośnik, jak pokazano w poniższym przykładzie:
$wp = fopen("$document_root\\..\\zamowienia\\zamowienia.txt", 'w');
Niewiele osób stosuje w ścieżkach dostępu w kodzie PHP znaki odwrotnych ukośników, ponieważ
oznaczałoby to, że kod ten będzie działał tylko w systemach Windows. Stosowanie zwykłych
ukośników pozwala na przenoszenie kodu między maszynami pracującymi w systemach Unix
i Windows bez konieczności wprowadzania w nim zmian.
Drugim parametrem funkcji fopen() jest tryb otwarcia pliku, określający jego przeznaczenie. Powi-
nien on zostać podany jako łańcuch znaków. W powyższym przykładzie funkcji fopen() zostaje
przekazana wartość w, co oznacza otwarcie pliku do zapisu. Podsumowanie trybów otwarcia pliku
przedstawiono w tabeli 2.1.
Tryb otwarcia pliku zależy od sposobu, w jaki system zostanie użyty. W przykładzie został zasto-
sowany tryb 'w', co oznacza, że w pliku będzie mogło być zapamiętane tylko jedno zamówienie.
Każde nowo przyjęte zamówienie nadpisze poprzednie. Nie jest to rozwiązanie zbyt rozsądne,
więc lepiej użyć trybu dodawania (oraz, zgodnie z zaleceniem, trybu binarnego):
$wp = fopen("$document_root/../zamowienia/zamowienia.txt", 'ab');
Istnieje również trzeci, opcjonalny parametr funkcji fopen(). Stosuje się go w celu szukania pliku
w lokalizacjach podanych w opcji include_path (ustawianej w konfiguracji PHP — szczegóły
w dodatku A). Aby użyć tej opcji, należy nadać temu parametrowi wartość true. Nie trzeba wtedy
podawać ścieżki dostępu do pliku.
$wp = fopen('zamowienia.txt', 'ab', true);
Rozdział 2. Przechowywanie i wyszukiwanie danych 75
Czwarty parametr również jest opcjonalny. Funkcja fopen() dopuszcza, by nazwy plików były
poprzedzone nazwą protokołu (na przykład http://), a same pliki były otwierane ze zdalnych
lokalizacji. Niektóre protokoły pozwalają ponadto na przekazywanie dodatkowych parametrów.
Taki sposób użycia funkcji fopen() zostanie opisany bardziej szczegółowo w dalszej części tego
rozdziału.
Jeżeli funkcji fopen() uda się otwarcie pliku, zwraca ona zasób będący w rzeczywistości uchwy-
tem albo wskaźnikiem pliku i przechowuje go w zmiennej, w powyższym przykładzie: $wp.
Zmienna ta jest stosowana przy kolejnych próbach dostępu do pliku, to znaczy przy odczytywa-
niu lub zapisywaniu danych.
Jeżeli wprowadzona nazwa pliku rozpoczyna się od ftp://, otwarte zostanie pasywne połączenie
FTP z serwerem, którego adres został wprowadzony, a funkcja zwróci wartość wskaźnika na
początek pliku.
76 Część I Stosowanie PHP
Jeżeli wprowadzona nazwa pliku rozpoczyna się od http://, otwarte zostanie pasywne połączenie
HTTP z serwerem, którego adres został wprowadzony, a funkcja zwróci wartość wskaźnika na
odpowiedź.
Należy pamiętać, że nazwy domen w URL-ach nie są różnicowane ze względu na wielkość liter,
w przeciwieństwie do ścieżek i nazw plików.
Rysunek 2.2.
Podczas nieudanej
próby otwarcia pliku
PHP wyświetla
specyficzne
ostrzeżenie
Po popełnieniu takiego błędu należy upewnić się, czy skrypt, który jest stosowany, posiada prawo
dostępu do danego pliku. Zależnie od konfiguracji serwera, skrypt może być uruchomiony z pra-
wami użytkownika serwera WWW lub z prawami właściciela swojego katalogu.
W większości systemów skrypt zostanie uruchomiony jako użytkownik serwera WWW. Jeżeli
na przykład skrypt znajduje się w systemie uniksowym w katalogu ~/public_html/rozdział2,
należy utworzyć ogólnodostępny katalog, w którym przechowywane będą zamówienia. Aby to
uczynić, można wpisać:
mkdir sciezka/do/zamowienia
chgrp apache sciezka/do/zamowienia
chmod 777 sciezka/do/zamowienia/
Można by także zmienić właściciela pliku na użytkownika, którego używa serwer WWW. Są rów-
nież osoby, które w takim przypadku w celu ułatwienia sobie zadania zmieniłyby prawa dostępu do
pliku, tak by każdy mógł w nim zapisywać dane. Należy jednak pamiętać, że katalogi i pliki z takim
ogólnym prawem zapisu są bardzo niebezpieczne. W szczególności nie powinno się używać katalogów
Rozdział 2. Przechowywanie i wyszukiwanie danych 77
dostępnych bezpośrednio z poziomu WWW, które posiadają możliwość zapisu. Z tego powodu
przykładowy katalog zamowienia został umieszczony poza drzewem katalogów serwera WWW.
Szczegółowe informacje na temat bezpieczeństwa są przedstawione w rozdziale 15.
Złe ustawienia dostępu do plików to najpopularniejszy, lecz nie jedyny błąd popełniany przy
otwieraniu plików. Jeżeli plik nie może zostać otwarty, trzeba koniecznie o tym wiedzieć, aby nie
próbować odczytywać ani zapisywać w nim danych.
Jeżeli wywołanie funkcji fopen() nie powiedzie się, zwróci ona wartość false. Dodatkowo
PHP wygeneruje także ostrzeżenie (komunikat na poziomie E_WARNING). Można wtedy zastąpić
oryginalny komunikat o błędzie PHP innym, bardziej przyjaznym dla użytkownika:
@$wp = fopen("$document_root/../zamowienia/zamowienia.txt", 'ab');
if (!$wp) {
echo "<p><strong> Zamówienie Państwa nie może zostać przyjęte w tej chwili.
Proszę spróbować później.</strong></p></body></html>";
exit;
}
Symbol @ umieszczony przed wywołaniem funkcji fopen() nakazuje PHP wytłumienie wszystkich
błędów wynikłych z tego wywołania. Zazwyczaj warto wiedzieć, kiedy występuje błąd. Kwestię
tę rozważymy później.
jednak w takiej sytuacji nie widać wyraźnie, że stosowane jest ukrywanie błędów, co może utrudnić
debugowanie kodu.
Ogólnie rzecz biorąc, stosowanie operatora tłumienia błędów nie jest uważane za przejaw
dobrego stylu programowania, więc należy go potraktować jedynie jako chwilowe ułatwienie.
Opisana metoda stanowi najprostszy sposób radzenia sobie z błędami. Bardziej elegancki
sposób obsługi błędów zostanie przedstawiony w rozdziale 7.
Instrukcja if sprawdza wartość zmiennej $wp, aby ustalić, czy wywołanie funkcji fopen() zwróciło
prawidłowy wskaźnik. Jeżeli nie, wyświetla komunikat o błędzie i kończy działanie skryptu.
Rysunek 2.3.
Stosowanie własnych
komunikatów
o błędach zamiast tych
wbudowanych w PHP
jest bardziej przyjazne
dla użytkownika
Alternatywą dla funkcji fwrite() jest funkcja file_put_contents(). Jej prototyp przedstawia się
następująco:
int file_put_contents ( string nazwa_pliku,
mixed dane
[, int znaczniki
[, resource kontekst]])
Trzeci parametr, dlugosc, zawiera maksymalną możliwą do zapisania liczbę bajtów. Jeżeli para-
metr ten został umieszczony w wywołaniu funkcji, fwrite() będzie zapisywać ciag w pliku
wskazanym przez wskaznik pliku, dopóki nie osiągnie końca ciagu lub zapisze dlugosc bajtów,
zależnie od tego, co wystąpi wcześniej.
Długość łańcucha znaków można odczytać, używając funkcji PHP o nazwie strlen() w następujący
sposób:
fwrite($wp, $ciagwyjsciowy, strlen($ciagwyjsciowy));
Trzeciego parametru używa się w trakcie zapisywania w trybie binarnym, ponieważ można
dzięki niemu uniknąć pewnych komplikacji związanych ze zgodnością między platformami.
Rozdział 2. Przechowywanie i wyszukiwanie danych 79
Formaty plików
Tworząc plik danych podobny do przykładowego, można określić dowolny format przechowy-
wania danych. Jeżeli jednak dane te będą wykorzystywane później przez jakąś aplikację, należy
zastosować się do zasad określonych przez tę aplikację.
W tym prostym przykładzie każdy rekord jest zapisany w osobnym wierszu pliku. Metodę tę
zastosowano, ponieważ występuje w niej prosty separator rekordów: znak nowego wiersza. Znaki
te przedstawia się za pomocą sekwencji "\n", gdyż są niewidzialne.
W całej książce pola danych będą zapisywane za każdym razem w jednakowym porządku i od-
dzielane znakami tabulacji. Ponieważ znak ten również jest niewidzialny, przedstawia się go za
pomocą sekwencji "\t". Można wybrać dowolny, czytelny znak podziału.
Znak podziału powinien być znakiem, który nie występuje pośród wprowadzanych danych, lub
też dane powinny zostać przekształcone w celu usunięcia występujących w nich znaków podziału.
Analiza pełnego kodu przykładu pokaże, że problematyczne znaki są usuwane przy wykorzysta-
niu wyrażenia regularnego (funkcji preg_replace()). Wyrażenia regularne zostały wyczerpująco
opisane w rozdziale 4.
Zamykanie pliku
Po zakończeniu korzystania z pliku należy go zamknąć. Stosuje się w tym celu funkcję fclose()
w następujący sposób:
fclose($wp);
Funkcja ta zwraca wartość true, jeżeli zamykanie powiodło się, lub false, w przeciwnym wypadku.
Działanie to ma znacznie większe szanse powodzenia niż otwieranie pliku — i w tym przypadku
nie zdecydowano się na jego sprawdzenie.
$ilosc = 0;
$wartosc=0.00;
define('CENAOPON', 100);
define('CENAOLEJU', 10);
define('CENASWIEC', 4);
if($ilosc == 0) {
echo "Na poprzedniej stronie nie zostało złożone żadne zamówienie!<br />";
} else {
if ($iloscopon > 0) {
echo htmlspecialchars($iloscopon).' opon<br />';
}
if ($iloscoleju > 0) {
echo htmlspecialchars($iloscoleju).' butelek oleju<br />';
}
if ($iloscswiec > 0) {
echo htmlspecialchars($iloscswiec).' świec zapłonowych<br />';
}
}
echo "Wartość netto zamówienia: ". number_format($wartosc, 2, '.', ' ') ."zł<br />";
if (!$wp) {
echo "<p><strong> Państwa zamówienie nie może zostać przyjęte w tej chwili.
Proszę spróbować później.</strong></p></body></html>";
exit;
}
flock($wp, LOCK_EX);
Odczyt z pliku
Klienci Janka mogą już składać swoje zamówienia przez sieć WWW, lecz jeżeli pracownicy firmy
chcą je obejrzeć, muszą otworzyć plik własnoręcznie.
Można stworzyć interfejs WWW pozwalający pracownikom na łatwe odczytywanie plików. Kod
tego interfejsu został przedstawiony na listingu 2.3.
Listing 2.3. zobaczzamowienia.php — interfejs pozwalający pracownikom Janka na oglądanie zawartości plików
<?php
// utworzenie krótkich nazw zmiennych
$document_root = $_SERVER['DOCUMENT_ROOT'];
?>
<!DOCTYPE html>
<html>
<head>
<title>Części samochodowe Janka — wyniki przetwarzania zamówienia</title>
</head>
<body>
<h1>Części samochodowe Janka</h1>
<h2>Zamówienia klientów</h2>
<?php
if (!$wp) {
echo "<p><strong>Brak zamówień.<br />
Proszę spróbować później.</strong></p>";
exit;
}
82 Część I Stosowanie PHP
while (!feof($wp)) {
$zamowienie = fgets($wp);
echo htmlspecialchars($zamowienie)."<br />";
}
?>
</body>
</html>
Skrypt ten działa na zasadzie opisanej powyżej — otwarcie pliku, odczyt z pliku, zamknięcie pliku.
Wynik uruchomienia powyższego skryptu, wykorzystującego plik danych z listingu 2.1, jest przed-
stawiony na rysunku 2.4.
Rysunek 2.4.
Skrypt zobacz
zamowienia.php
wyświetla w oknie
przeglądarki
wszystkie zamówienia
znajdujące się
w pliku zamowienia.txt
Funkcja feof() używa wskaźnika pliku jako swojego jedynego parametru. Zwraca ona wartość true,
jeżeli wskaźnik pliku znajduje się na jego końcu. Chociaż nazwa funkcji może wydawać się dziwna,
łatwo ją zapamiętać, wiedząc, że feof znaczy w skrócie File End Of File (plik koniec pliku).
Istnieje wiele różnych funkcji stosowanych do odczytywania danych z pliku. Na przykład funkcja
fgets() jest przydatna przy obróbce plików zawierających zwykły tekst, który odczytujemy
fragmentami.
Interesującą odmianą fgets() jest funkcja fgetss(), która posiada następujący prototyp:
string fgetss(int wskaznik_pliku[, int dlugosc[, string dozwolone_znaczniki]]);
Funkcja ta podobna jest do fgets(), z wyjątkiem tego, że usuwa z czytanego łańcucha wszystkie
znaczniki PHP i HTML, poza wyszczególnionymi w parametrze dozwolone_znaczniki. Stosuje
się ją w celach bezpieczeństwa przy odczytywaniu plików napisanych przez innych programistów
lub zawierających informacje wprowadzone przez użytkowników. Niekontrolowany obcy kod
HTML może zniszczyć dokładnie zaplanowany proces formatowania strony. Niekontrolowany
obcy kod PHP lub JavaScript może dać złośliwemu użytkownikowi sposobność do spowodowa-
nia zagrożenia bezpieczeństwa serwera.
Inną odmianą funkcji fgets() jest funkcja fgetcsv(). Posiada ona następujący prototyp:
array fgetcsv(resource wskaznik_pliku, int dlugosc, string [znak_podziału
[, string załącznik
[, string escape]]]);
Funkcja ta służy do rozdzielania wierszy pliku w celu zrekonstruowania zmiennych, kiedy wcze-
śniej zastosowany został znak podziału (taki jak zaproponowany powyżej) lub przecinek (używany
w większości arkuszy kalkulacyjnych i innych aplikacji). Oznacza to, że przy jej stosowaniu plik
jest odczytywany nie wiersz po wierszu, lecz od znaku podziału do znaku podziału. Wywołanie
tej funkcji następuje podobnie jak w przypadku fgets(), lecz przekazuje się jej również znak
podziału użyty do odseparowania pól. Na przykład:
$zamowienie = fgetcsv($wp, 0, "\t");
Powyższe polecenie odczyta wiersz z pliku i podzieli ją tam, gdzie natrafi na znak tabulacji (\t).
Wyniki zwracane są w postaci tablicy (w powyższym przykładowym kodzie: $zamowienie). Tablice
zostaną opisane dokładniej w rozdziale 3.
Parametr dlugosc powinien mieć większą wartość niż długość (wyrażoną w liczbie znaków) najdłuż-
szego wiersza odczytywanego pliku lub wartość 0, jeśli długość wiersza nie ma być ograniczana.
Parametr załącznik służy do wskazywania znaków, jakimi jest otoczone każde pole w wierszu.
Jeśli nie zostanie on podany, przyjęta zostanie domyślnie wartość " (cudzysłów).
Pierwsza z nich polega na zastosowaniu funkcji readfile(). Można zastąpić niemal cały powyższy
skrypt jednym wierszem kodu:
readfile("$document_root/../zamowienia/zamowienia.txt");
Wywołanie funkcji readfile() otwiera plik, wyświetla zawartość w oknie przeglądarki, po czym
zamyka plik. Prototyp funkcji readfile() jest następujący:
int readfile(string nazwa_pliku, [bool użycie_opcji_include_path[, resource kontekst]]);
Opcjonalny drugi parametr określa, czy PHP powinien szukać pliku przez opcję include_path,
i działa w sposób identyczny jak fopen(). Opcjonalny parametr kontekst używany jest jedynie
wówczas, gdy pliki są otwierane zdalnie na przykład za pośrednictwem HTTP; ten sposób używania
funkcji zostanie szerzej opisany w rozdziale 18. Funkcja zwraca całkowitą liczbę bajtów odczyta-
nych z pliku.
Drugą funkcją tego typu jest fpassthru(). W celu jej zastosowania należy najpierw otworzyć plik za
pomocą fopen(), a potem przekazać wartość wskaźnika pliku funkcji fpassthru(), która wyświetli
zawartość tego pliku w okienku przeglądarki. Po zakończeniu działania funkcja zamyka plik.
Funkcja fpassthru zwraca wartość true, jeżeli odczyt powiedzie się, w przeciwnym wypadku —
false.
Trzecią metodą odczytu całego pliku jest zastosowanie funkcji file(). Działa ona w identyczny
sposób jak readfile() z jednym wyjątkiem — zamiast wyświetlić zawartość pliku w przeglądarce,
zamienia ją na tablicę. Kwestia ta zostanie szczegółowo opisana w rozdziale 3. Tymczasem poni-
żej zostało przedstawione jej przykładowe zastosowanie:
$tablicapliku = file("$document_root/../zamowienia/zamowienia.txt");
Polecenie to wczytuje cały plik w tablicę o nazwie $tablicapliku. Każdy wiersz pliku zostanie
zachowany jako osobny element tablicy. Warto zwrócić uwagę, że we wcześniejszych wersjach PHP
funkcja file() nie była binarnie bezpieczna.
Czwartą dostępną opcją jest wykorzystanie funkcji file_get_contents(). Działa ona tak samo jak
funkcja readfile(), z tą tylko różnicą, że zwraca zawartość pliku w postaci łańcucha znaków,
a nie przesyła jej do przeglądarki.
Powyższy kod odczytuje za pomocą funkcji fgetc() pojedynczy znak z pliku i zapisuje go w zmien-
nej $znak, dopóki nie zostanie osiągnięty koniec pliku. Później zastosowana zostaje mała sztuczka
zamieniająca znaki końca wiersza (\n), na złamania wierszy HTML (<br />).
Dzieje się tak jedynie w celu czystego sformatowania strony. W przypadku próby wyświetlenia
pliku ze znakami nowych wierszy między rekordami cały plik zostałby wyświetlony jako jeden
wiersz (warto sprawdzić). Przeglądarki nie generują znaków niewidocznych, dlatego trzeba je
zastępować znakami HTML oznaczającymi nowy wiersz (<br />). W celu przeprowadzenia tej
zamiany zastosowany został operator trójkowy.
Pomniejszym efektem ubocznym stosowania funkcji fgetc() zamiast fgets() jest fakt, że w prze-
ciwieństwie do funkcji fgets() zwraca ona znak EOF. Dlatego po przeczytaniu znaku należy ponow-
nie użyć feof(), aby znak ten nie został wyświetlony w przeglądarce.
Jeżeli nie istnieje wyraźny powód odczytywania pliku znak po znaku, stosowanie tej metody nie
jest polecane.
Funkcja fread() odczytuje przekazaną jej liczbę bajtów, chyba że wcześniej napotka znak końca
pliku lub pakietu sieciowego.
Funkcja ta zwraca wielkość pliku w bajtach i może zostać zastosowana w połączeniu z funkcją
fread() w celu odczytania jednorazowo całego pliku (lub jakiejś jego części). Można zastąpić cały
przykładowy skrypt następującymi wierszami kodu:
$wp = fopen("$document_root/../zamowienia/zamowienia.txt", 'rb');
echo nl2br(fread($wp, filesize("$document_root/../zamowienia/zamowienia.txt"));
fclose($wp);
86 Część I Stosowanie PHP
Funkcja nl2br() przekształca w generowanym kodzie HTML znaki \n w znaki nowego wier-
sza (<br />).
Powyższa funkcja zwraca wartość false, jeżeli plik nie mógł zostać usunięty. Sytuacja taka zdarza
się zazwyczaj z powodu niewystarczających praw do pliku bądź jeżeli plik nie istnieje.
Rysunek 2.5.
Kiedy zamówienia są
przeczytane, wskaźnik
pliku wskazuje na jego
koniec, offset 315
bajtów. Wywołanie
przewinięcia ustawia
go na pozycji 0
(na początku pliku)
Funkcja fseek() stosowana jest do ustawiania wskaźnika pliku w dowolnym punkcie pliku. Jej
prototyp wygląda następująco:
int fseek(resource wskaznik_pliku, int offset [, int skąd]);
Wywołanie funkcji fseek() ustawia wskaźnik pliku wp w punkcie wskazywanym przez skąd i prze-
suwa o offset bajtów, licząc od początku pliku. Opcjonalny parametr skąd posiada domyślną
wartość SEEK_SET, czyli początek pliku. Parametr ten może również mieć wartości SEEK_CUR (bieżąca
pozycja wskaźnika pliku) oraz SEEK_END (koniec pliku).
Rozdział 2. Przechowywanie i wyszukiwanie danych 87
Funkcja rewind() jest równoznaczna z wywołaniem funkcji fseek() z zerowym offsetem. Na przy-
kład można zastosować funkcję fseek() w celu znalezienia środkowego rekordu w pliku danych
lub po to, aby przeprowadzić przeszukiwanie binarne. Zazwyczaj po osiągnięciu poziomu złożoności
wymagającego stosowania takich mechanizmów polecane jest wykorzystanie bazy danych zapro-
jektowanej z myślą o konkretnym rozwiązaniu.
Blokowanie pliku
Można wyobrazić sobie sytuację, w której dwóch klientów stara się zamówić produkt w tym
samym czasie (dzieje się tak często, zwłaszcza gdy strona jest licznie odwiedzana). Co się stanie,
gdy jeden z klientów wywoła funkcję fopen() i zacznie zapis w pliku, a drugi uczyni to samo?
Jak będzie wyglądać ostateczna zawartość pliku? Czy najpierw zostanie zapisane pierwsze zamó-
wienie, a później drugie, czy też odwrotnie? A może dojdzie do sytuacji niepożądanej, na przy-
kład oba zamówienia wymieszają się ze sobą? Odpowiedź na powyższe pytania zależy od konkret-
nego systemu operacyjnego, ale zazwyczaj jest to wielka niewiadoma.
W celu uniknięcia powyższych problemów stosuje się blokowanie plików. W PHP wykorzystuje
się w tym celu funkcję flock(). Powinna ona zostać wywołana po otwarciu pliku, lecz przed odczy-
taniem go lub zapisaniem w nim danych.
Funkcji flock() należy przekazać wskaźnik otwartego pliku i cyfrę określającą wymagany rodzaj
zamka. Funkcja ta zwraca true, jeżeli zamek został prawidłowo założony, a false w przeciwnym
wypadku. Opcjonalny trzeci parametr będzie zawierał wartość true, jeśli założenie zamka spowo-
duje zablokowanie bieżącego procesu (czyli zmuszenie go do przejścia w stan oczekiwania).
Stosując funkcję flock(), należy dodać ją do wszystkich skryptów korzystających z pliku. W innym
przypadku jest ona bezwartościowa.
Należy zwrócić uwagę, iż flock() nie działa w systemie NFS i innych sieciowych systemach
plików. Nie działa ona również w starszych systemach plików, które nie obsługują blokowania
plików — systemem takim jest na przykład FAT. W niektórych systemach operacyjnych funkcja
ta jest zaimplementowana na poziomie procesu i nie będzie działać prawidłowo, jeśli stosowany
będzie wielowątkowy API serwera.
if (!$wp) {
echo "<p><strong> Państwa zamówienie nie może zostać przyjęte w tej chwili.
Proszę spróbować później.</strong></p></body></html>";
exit;
}
Kod jest teraz dużo solidniejszy, ale ciągle niedoskonały. Co by się stało, gdyby dwa skrypty jed-
nocześnie usiłowały założyć blokadę? Spowodowałoby to „wyścig” o bardzo niepewnym wyniku,
co wywołałoby wiele kolejnych problemów. Lepszą metodą jest zastosowanie systemu zarządzania
bazami danych.
Prawdopodobnie głównym powodem używania RDBMS jest to, że możliwości funkcjonalne, które
powinny posiadać systemy przechowywania danych, zostały już w nich zaimplementowane (a przy-
najmniej większość z nich). Oczywiście można napisać własną bibliotekę funkcji PHP, lecz po
co ponownie wymyślać koło?
W części II tej książki opiszemy ogólną zasadę działania relacyjnych baz danych, a w szczególno-
ści konfigurację i zastosowanie MySQL w tworzeniu stron WWW opartych na bazach danych.
Jeżeli tworzony jest prosty system, który nie wymaga pełnowymiarowej bazy danych, natomiast
chcemy uniknąć zakładania zamków i innych komplikacji związanych z używaniem plików pła-
skich, można użyć nowego rozszerzenia PHP o nazwie SQLite. Udostępnia ono SQL-owy interfejs do
pliku płaskiego. W tej książce skupimy się na używaniu serwera MySQL, natomiast więcej informa-
cji na temat SQLite można znaleźć pod adresami http://sqlite.org/ oraz http://www.php.net/sqlite.
Zalecamy również lekturę rozdziału na temat systemów plików w podręczniku elektronicznym PHP,
dostępnym pod adresem http://www.php.net/filesystem.
W następnym rozdziale
W kolejnym rozdziale przedstawimy tablice — czym są i jak mogą zostać zastosowane w skryptach
PHP do przetwarzania danych.
90 Część I Stosowanie PHP
Rozdział 3.
Stosowanie tablic
W tym rozdziale opiszemy zasady stosowania ważnych konstrukcji programistycznych — tablic.
Zmienne przedstawione w poprzednich rozdziałach to zmienne skalarne, tzn. takie, które prze-
chowują pojedynczą wartość. Z kolei tablica to zmienna, która przechowuje zbiór lub sekwencję
wartości. Tablica może posiadać wiele elementów, a każdy z nich może przechowywać pojedyn-
czą wartość, taką jak tekst bądź liczby, lub też inną zmienną. Tablice zawierające w sobie inne tabli-
ce są nazywane tablicami wielowymiarowymi.
W tym rozdziale do przykładowej aplikacji „Części samochodowe Janka” zostaną wprowadzone ta-
blice w celu łatwiejszego operowania powtarzalnymi informacjami, takimi jak zamówienia klientów.
Podobnie, do operacji na plikach może zostać zastosowany kod krótszy i bardziej uporządkowany.
Czym są tablice?
Zmienne skalarne zostały omówione w rozdziale 1. Zmienna skalarna to opatrzona nazwą pozycja,
w której przechowuje się wartość. Z kolei tablica to nazwana pozycja, w której przechowuje się
zbiór wartości, w ten sposób grupując zwykłe zmienne.
Przykładową tablicą jest lista produktów Janka z przykładu przytoczonego w poprzednim roz-
dziale. Na rysunku 3.1 jest przedstawiona lista trzech produktów przechowywana w formacie
tablicowym. Te trzy produkty są przechowywane w jednej zmiennej o nazwie $produkty (w dalszej
części zostaną przedstawione sposoby tworzenia podobnych zmiennych).
92 Część I Stosowanie PHP
Rysunek 3.1.
Produkty Janka mogą
być przechowywane
w tablicy
Dzięki informacjom przechowanym jako tablica można wykonać na nich wiele użytecznych działań.
Stosując przedstawione w rozdziale 1. konstrukcje pętli, można zaoszczędzić wysiłku poprzez
wykonywanie tego samego działania na każdej wartości w tablicy. Cały zbiór danych może być
przemieszczany jako jedna jednostka. W ten sposób, przy użyciu jednego wiersza kodu, wszystkie
wartości zostaną przekazane funkcji. Na przykład aby posortować produkty alfabetycznie, można
przekazać całą tablicę funkcji PHP sort().
Wartości przechowywane w tablicy nazywane są jej elementami. Każdy element tablicy posiada
przypisany indeks (zwany także kluczem), który umożliwia dostęp do niego. Tablice w wielu języ-
kach programowania posiadają indeksy numeryczne, zaczynające się zwykle od zera lub jedynki.
PHP również rozpoznaje ten typ tablic.
PHP pozwala na to, by indeksami tablic były liczby lub łańcuchy znaków. W tradycyjny sposób
można używać tablic indeksowanych numerycznie, lecz kluczami mogą być również dowolne
inne wartości, dzięki którym indeksy będą bardziej użyteczne i opisowe. (Własność taka powinna
być już znana programistom piszącym w innych językach, w których dozwolone jest używanie
asocjacyjnych tablic, słowników lub map). Podejście programistyczne może się nieco różnić
w zależności od tego, czy używane są standardowe tablice indeksowane numerycznie, czy też
zdecydowano się na użycie indeksów o bardziej interesujących wartościach.
Powyższy wiersz utworzy tablicę o nazwie produkty, zawierającą trzy podane wartości — 'Opony',
'Olej' i 'Świece Zapłonowe'. Należy zauważyć, że podobnie jak echo, array() to konstrukcja języ-
ka, a nie funkcja.
Począwszy od PHP w wersji 5.4, tablice można także tworzyć przy użyciu nowego, skróconego
zapisu. Polega on na zastosowaniu nawiasów kwadratowych [ oraz ] zamiast operatora array().
Na przykład aby utworzyć tablicę z rysunku 3.1 przy pomocy zapisu skróconego, należałoby zasto-
sować poniższy wiersz kodu:
$produkty = ['Opony', 'Olej', 'Świece Zapłonowe'];
Rozdział 3. Stosowanie tablic 93
Zależnie od pożądanej zawartości tablicy nie zawsze jest potrzebne ręczne inicjowanie tej zawar-
tości. Jeżeli potrzebne dane znajdują się w innej tablicy, można bezpośrednio skopiować jedną
tablicę do drugiej, stosując operator =.
Aby przechować w tablicy rosnącą sekwencję liczb, warto zastosować funkcję range(), która
automatycznie utworzy tablicę. Poniższy wiersz kodu utworzy tablicę o nazwie liczby o elemen-
tach od 1 do 10:
$liczby = range(1,10);
Funkcja range() posiada opcjonalny trzeci parametr, który pozwala na definiowanie długości kroku
między kolejnymi wartościami. Jeżeli na przykład chcemy utworzyć tablicę zawierającą liczby
nieparzyste od 1 do 10, możemy to zrobić za pomocą następującego polecenia:
$nieparzyste = range(1, 10, 2);
Funkcja ta może być również używana w odniesieniu do znaków, jak w poniższym przykładzie:
$litery = range('a', 'z');
Jeżeli informacje zawarte są w pliku na dysku, tablica może zostać załadowana bezpośrednio
z pliku. Kwestię tę omówimy w dalszej części tego rozdziału, w podrozdziale „Wczytywanie tablic
z plików”.
Jeżeli dane, które powinny zostać umieszczone w tablicy, są przechowywane w bazie danych,
można załadować je bezpośrednio z bazy. Kwestia ta zostanie opisana w rozdziale 11.
Można również zastosować różne funkcje w celu odczytania części tablicy lub zmiany jej porządku.
Niektóre z tych funkcji zostaną przedstawione w dalszej części tego rozdziału, w podrozdziale
„Wykonywanie innych działań na tablicach”.
Innymi słowy, aby użyć poszczególnych wartości z tablicy produktów, należy wpisać, odpowiednio,
$produkty[0], $produkty[1] i $produkty[2].
Istnieje także możliwość odwoływania się do elementów tablicy przy użyciu nawiasów klamrowych
({}) zamiast kwadratowych ([]). W takim przypadku wartość pierwszego elementu tablicy produk-
tów można by pobrać, wykorzystując wyrażenie $produkty{1}.
Domyślnie element zero to pierwszy element tablicy. Taki sam schemat numerowania stosuje się
w C, C++, Javie i w kilku innych językach. Osoby nieznające go będą musiały poświęcić trochę
czasu na zapoznanie się z tym elementem.
Podobnie jak w przypadku innych zmiennych, zawartość elementów tablicy może być modyfikowa-
na przy użyciu operatora =. Następujący wiersz kodu zastąpi pierwszy element w powyższej tablicy,
'Opony', na 'Bezpieczniki'.
$produkty[0] = 'Bezpieczniki';
Następujący wiersz może zostać zastosowany w celu dodania nowego elementu — 'Bezpieczniki'
— na końcu tablicy, co w efekcie dałoby cztery elementy:
$produkty[3] = 'Bezpieczniki';
94 Część I Stosowanie PHP
Należy zwrócić uwagę, iż o ile parsowanie łańcuchów znaków jest w PHP rozwiązane bardzo spryt-
nie, to można wprowadzić w nim dużo zamieszania. W razie wystąpienia kłopotów polegających
na nieprawidłowej interpretacji tablic lub innych zmiennych osadzonych w łańcuchu znaków
otoczonym cudzysłowem, można je wyjąć poza cudzysłowy lub zastosować bardziej złożoną
składnię, o której więcej powiemy w rozdziale 4. Powyższa instrukcja echo zadziała prawidłowo,
jednak w wielu bardziej złożonych przypadkach umieszczonych w dalszej części tego rozdziału
zmienne będą umieszczane poza cudzysłowami.
Podobnie jak inne zmienne w PHP, tablice nie muszą być inicjowane ani tworzone z wyprzedze-
niem. Powstają one automatycznie przy pierwszym zastosowaniu.
Poniższy kod utworzy identyczną tablicę $produkty jak tablica utworzona wcześniej przy użyciu
instrukcji array():
$produkty[0] = 'Opony';
$produkty[1] = 'Olej';
$produkty[2] = 'Świece Zapłonowe';
Jeżeli tablica $produkty wcześniej nie istniała, pierwszy wiersz utworzy nową tablicę z tylko
jednym elementem. Następne wiersze dodadzą do niej wartości. W momencie dodawania wierszy
rozmiar tablicy jest automatycznie zmieniany. Własność taka w wielu językach programowania
nie jest dostępna.
Pętla powyższa da wynik podobny do poprzedzającego ją kodu, lecz nie wymaga tyle wysiłku,
co ręczne wpisywanie kodu w trakcie pracy z każdym elementem dużej tablicy. Zdolność stosowa-
nia prostych pętli w celu dostępu do każdego elementu to przydatna właściwość tablic. Można
również zastosować pętlę foreach, przeznaczoną specjalnie do przetwarzania tablic. W tym przykła-
dzie można by ją zastosować w następujący sposób:
foreach ($produkty as $biezacy) {
echo $biezacy." ";
}
Powyższy kod zapisze każdy kolejny element tablicy $produkty w zmiennej $biezacy i wyświetli ją.
Inicjowanie tablicy
Następujący kod tworzy tablicę, w której nazwy produktów są kluczami, a ceny wartościami:
$ceny = array('Opony'=>100, 'Olej'=>10, 'Świece Zapłonowe'=>4);
Symbol znajdujący się między kluczami i wartościami (=>) to po prostu znak równości oraz
znak „większe niż”.
Podany niżej kod utworzy taką samą tablicę $ceny. Zamiast tablicy trzyelementowej ta wersja
tworzy tablicę z tylko jednym elementem, a potem dodaje kolejne dwa.
$ceny = array('Opony'=>100);
$ceny['Olej'] = 10;
$ceny['Świece Zapłonowe'] = 4;
Poniżej przedstawiono nieco inny, lecz równoznaczny fragment kodu. W tym wariancie tabela
nie jest tworzona ręcznie, lecz automatycznie przy dodaniu do niej pierwszego elementu.
$ceny['Opony'] = 100;
$ceny['Olej'] = 10;
$ceny['Świece Zapłonowe'] = 4;
Stosowanie pętli
Ponieważ indeksy w powyższej tablicy asocjacyjnej nie są liczbami, do pracy z tablicą nie można
zastosować zwykłego licznika w pętli for. Można natomiast wykorzystać pętlę foreach lub kon-
strukcję list() albo each().
Pętla foreach ma nieco odmienną strukturę, gdy jest stosowana do przeglądania tablic z indeksami
innymi niż numeryczne. Możemy zastosować ją dokładnie tak samo jak w poprzednim przykładzie
lub uwzględnić w niej również klucze:
foreach ($ceny as $klucz => $wartosc) {
echo $klucz." - ".$wartosc."<br />";
}
W rozdziale 1. przedstawiliśmy pętlę while oraz instrukcję echo. Powyższy kod stosuje funkcję
each(), która nie została wcześniej przedstawiona. Zwraca ona bieżący element tablicy oraz
nadaje następnemu atrybut bieżącego. Ponieważ w powyższym przykładzie funkcja each() jest
wywoływana wewnątrz pętli while, zwraca ona po kolei każdy element tablicy i zatrzymuje się po
osiągnięciu jej końca.
96 Część I Stosowanie PHP
Rysunek 3.2.
Instrukcja each() może
zostać użyta do pracy
z tablicami
W powyższym kodzie zmienna $element to tablica. Przy wywołaniu funkcja each() zwraca tablicę
z czterema wartościami oraz cztery indeksy wskazujące na ich pozycję. Pozycje key i 0 zawierają
klucz bieżącego elementu, a pozycje value i 1 — jego wartość. W tym przykładzie występują
nazwane pozycje, lecz jest to wyłącznie kwestia wyboru.
Istnieje również częściej spotykany i elegantszy sposób wykonania powyższych działań. Konstruk-
cję list() stosuje się do rozdzielenia tablicy na kilka wartości. Można rozdzielić dwie wartości
zwracane przez funkcję each() w następujący sposób:
while (list($produkt, $cena) = each($ceny)) {
echo $produkt ." - ". $cena."<br />";
}
W powyższym wierszu zastosowano funkcję each() w celu pobrania bieżącego elementu z tablicy
$ceny, zwrócenia go jako tablicy oraz uczynienia następnego elementu bieżącym. Funkcja list()
jest zastosowana po to, aby zamienić elementy 0 i 1 tablicy zwróconej przez each() na dwie nowe
zmienne o nazwach $produkt i $cena.
Warto zwrócić uwagę na to, że podczas stosowania funkcji each() tablica pamięta obecnie
przetwarzany element. Jeśli więc ta sama tablica będzie musiała zostać użyta dwukrotnie w tym
samym skrypcie, to konieczne będzie ponowne ustawienie jej bieżącego elementu na pierwszy
element tablicy. Służy do tego funkcja reset(). A zatem aby po raz kolejny przejrzeć zawartość
tablicy cen, należy użyć poniższego fragmentu kodu:
reset($ceny);
while (list($produkt, $cena) = each($ceny)) {
echo $produkt ." - ". $cena."<br />";
}
Powyższy kod ustawia bieżący element na początku tablicy i pozwala na ponowne przejście
przez nią.
Operatory tablicowe
Istnieje jeden zestaw specjalnych operatorów dotyczących wyłącznie tablic. Większość z nich
ma swoje odpowiedniki wśród operatorów skalarnych, o czym można się przekonać, analizując
tabelę 3.1.
Rozdział 3. Stosowanie tablic 97
Działanie większości z tych operatorów jest oczywiste, szerszych wyjaśnień wymaga jedynie ope-
rator unii. Próbuje on dodać elementy tablicy $b na koniec tablicy $a. Jeżeli elementy z $b mają
takie same klucze jak niektóre elementy obecne już w $a, nie zostaną one dodane. Oznacza to,
że żaden element tablicy $a nie zostanie nadpisany.
Tablice wielowymiarowe
Tablice to niekoniecznie proste listy kluczy i wartości — każda pozycja tablicy może przechowy-
wać inną tablicę. W ten sposób można utworzyć tablicę dwuwymiarową, traktowaną jako macierz
bądź siatkę posiadającą szerokość i wysokość, czyli rzędy i kolumny.
Aby przechować więcej niż jedną cechę charakterystyczną dla każdego towaru firmy Janka, można
zastosować tablicę dwuwymiarową. Rysunek 3.3 ukazuje towary zapisane w postaci tablicy dwuwy-
miarowej, w której każdy rząd przedstawia pojedynczy produkt, a każda kolumna — jego atrybuty.
Rysunek 3.3.
W tablicy dwuwymiarowej
można przechować
więcej informacji
na temat produktów
sprzedawanych
w firmie Janka
98 Część I Stosowanie PHP
Stosując PHP, można utworzyć następujący kod zapisujący dane w tablicy przedstawionej na
rysunku 3.3:
$produkty = array ( array( 'OPO', 'Opony', 100 ),
array( 'OLE', 'Olej', 10 ),
array( 'SWI', 'Świece Zapłonowe', 4 ) );
Należy zauważyć, że według powyższej definicji tablica produktów zawiera trzy inne tablice.
Aby uzyskać dostęp do danych znajdujących się w tablicy jednowymiarowej, trzeba podać jej nazwę
oraz indeks elementu. W tablicy dwuwymiarowej należy postąpić podobnie, poza tym że każdy
element posiada dwa indeksy — rząd i kolumnę (szczytowy rząd to rząd 0, a kolumna pierwsza
z lewej to kolumna 0).
Aby wyświetlić zawartość powyższej tablicy, można ręcznie uzyskać dostęp do każdego elementu
w poniższym porządku:
echo '|'.$produkty[0][0].'|'.$produkty[0][1].'|'.$produkty[0][2].'|<br />';
echo '|'.$produkty[1][0].'|'.$produkty[1][1].'|'.$produkty[1][2].'|<br />';
echo '|'.$produkty[2][0].'|'.$produkty[2][1].'|'.$produkty[2][2].'|<br />';
Alternatywnie, można umieścić pętlę for wewnątrz drugiej pętli for, co da identyczny wynik:
for($rzad = 0; $rzad < 3; $rzad++) {
for ($kolumna = 0; $kolumna < 3; $kolumna++) {
echo '|'.$produkty[$rzad][$kolumna];
}
echo '|<br />';
}
Jedyna różnica pomiędzy dwoma powyższymi przykładami polega na tym, że przy pracy z więk-
szymi tablicami druga wersja jest znacznie krótsza.
Można również nadać kolumnom nazwy zamiast numerów, jak przedstawiono na rysunku 3.3.
W celu przechowania tego samego zbioru produktów w kolumnach o nazwach jak na rysunku 3.3
trzeba zastosować następujący kod:
$produkty = array( array( 'Kod' => 'OPO',
'Opis' => 'Opony',
'Cena' => 100
),
array( 'Kod' => 'OLE',
'Opis' => 'Olej',
'Cena' => 10
),
array( 'Kod' => 'SWI',
'Opis' => 'Świece Zapłonowe',
'Cena' => 4
)
);
Z powyższej tablicy łatwiej jest wyciągnąć pojedynczą wartość, a także zapamiętać, że opis
produktu przechowywany jest w kolumnie Opis, a nie w kolumnie 1. Przy zastosowaniu tablic
asocjacyjnych nie trzeba pamiętać, że jednostka zapamiętana jest w [x][y]. Można bez trudu
odnaleźć dane, odnosząc się do pozycji o znaczących nazwach rzędów i kolumn.
Rozdział 3. Stosowanie tablic 99
W ten sposób jednak traci się możliwość zastosowania zwykłej pętli for w celu przeskoczenia
za jednym razem jednej kolumny. Poniżej przedstawiono jeden ze sposobów wyświetlenia tej tablicy:
for($rzad = 0; $rzad < 3; $rzad++) {
echo '|'.$produkty[$rzad]['Kod']. '|'.$produkty[$rzad]['Opis'].
'|'.$produkty[$rzad]['Cena'].'|<br />';
}
Stosując pętlę for, można pracować z zewnętrzną, indeksowaną numerycznie tablicą $produkty,
której każdy rząd jest tablicą z indeksami opisowymi. Stosując funkcje each() i list() wewnątrz
pętli while, można pracować z tablicami wewnętrznymi. Można zatem wewnątrz pętli for umieścić
drugą pętlę — while — jak pokazano w poniższym przykładzie:
for ($rzad = 0; $rzad < 3; $rzad++) {
while (list($klucz, $wartosc) = each($produkty[$rzad])) {
echo '|'.$wartosc";
}
echo "|<br />";
}
Dwa wymiary nie są granicą — elementy tablicy mogą przechowywać nowe tablice, te zaś —
więcej tablic.
Jeżeli Janek podzieliłby produkty swojej firmy na kategorie, to mógłby w celu ich przechowania
zastosować tablicę trójwymiarową (zobacz rysunek 3.4).
Rysunek 3.4.
Tablica trójwymiarowa
pozwala na dzielenie
produktów na kategorie
100 Część I Stosowanie PHP
Ponieważ zawiera ona jedynie indeksy numeryczne, w celu wyświetlenia jej zawartości można
wykorzystać zagnieżdżone pętle for.
for ($warstwa = 0; $warstwa < 3; $warstwa++) {
echo 'Warstwa '.$warstwa."<br />";
for($rzad = 0; $rzad < 3; $rzad++) {
for ($kolumna = 0; $kolumna < 3; $kolumna++) {
echo '|'.$kategorie[$warstwa][$rzad][$kolumna];
}
echo '|<br />';
}
}
Sortowanie tablic
Zazwyczaj przydatne jest uporządkowanie związanych ze sobą danych znajdujących się w tablicy.
Posortowanie tablicy jednowymiarowej nie nastręcza trudności.
Elementy powyższej tablicy występują teraz w porządku: Olej, Opony, Świece Zapłonowe.
Wartości można również posortować w porządku numerycznym. Tablica zawierająca ceny produk-
tów Janka może zostać uporządkowana rosnąco numerycznie w następujący sposób:
$ceny = array( 100, 10, 4 );
sort($ceny);
Należy zauważyć, że funkcja sort() zwraca uwagę na wielkość liter. Wszystkie wielkie litery
zostają umieszczone przed małymi; tak więc A to mniej niż Z, ale Z to mniej niż a.
Funkcja ta posiada również drugi, opcjonalny parametr. Można do niej przekazać jedną ze stałych
SORT_REGULAR (jest to stała domyślna), SORT_NUMERIC, SORT_STRING, SORT_LOCALE_STRING,
SORT_NATURAL lub SORT_FLAG_CASE.
Możliwość definiowania rodzaju sortowania okazuje się użyteczna, gdy porównujemy łańcuchy
znaków mogące zawierać liczby, na przykład 2 i 12. Pod względem liczbowym 2 jest mniejsze
od 12, jednak łańcuch znaków '12' jest mniejszy od '2'.
Użycie tej stałej sprawi, że funkcja sort() nie będzie uwzględniać wielkości liter, a zatem 'a'
oraz 'A' zostaną potraktowane jako ten sam znak.
Poniższy kod tworzy tablicę zawierającą trzy produkty i związane z nimi ceny, a potem porządkuje
ją rosnąco według cen.
$ceny = array( 'Opony'=>100, 'Olej'=>10, 'Świece Zapłonowe'=>4 );
asort($ceny);
Funkcja asort() porządkuje tablicę według wartości każdego elementu. W powyższej tablicy
wartościami są ceny, a kluczami tekstowe opisy produktów. Aby zamiast według cen posortować
tablicę według opisów, należy zastosować funkcję ksort(), która porządkuje według kluczy, a nie
wartości. Poniższy kod da w efekcie tablicę posortowaną alfabetycznie — Olej, Opony, Świece
Zapłonowe.
$ceny = array( 'Opony'=>100, 'Olej'=>10, 'Świece Zapłonowe'=>4 );
ksort($ceny);
102 Część I Stosowanie PHP
Sortowanie odwrotne
Trzy różne funkcje sortujące — sort(), asort() i ksort() — porządkują tablice rosnąco. Każda
z nich posiada odpowiadającą jej funkcję sortującą odwrotnie, w porządku malejącym. Są to odpo-
wiednio: rsort(), arsort() i krsort().
Funkcje sortujące odwrotnie są stosowane w ten sam sposób jak zwykłe funkcje sortujące rosnąco.
Funkcja rsort() porządkuje jednowymiarową tablicę indeksowaną numerycznie w porządku
malejącym. Funkcja arsort() sortuje jednowymiarową tablicę w porządku malejącym według
wartości każdego elementu. Funkcja krsort() porządkuje malejąco jednowymiarową tablicę według
klucza każdego elementu.
Poniżej przedstawiona została definicja tablicy stosowanej już wcześniej. Przechowuje ona produkty
sprzedawane w firmie Janka — każdemu z nich przypisane są kod, opis i cena:
$produkty = array(array('OPO', 'Opony', 100 ),
array('OLE', 'Olej', 10 ),
array('SWI', 'Świece Zapłonowe', 4 ));
Jak się okazuje, tablica $produkty zostanie posortowana na podstawie pierwszego elementu każdej
z tablic, z zastosowaniem zwyczajnego sortowania w kolejności rosnącej:
'OLE', 'Olej', 10
'OPO', 'Opony', 100
'SWI', 'Świece Zapłonowe', 4
Parametr kolejność może przyjmować wartości SORT_ASC lub SORT_DESC, dzięki czemu możliwe
jest sortowanie w kolejności, odpowiednio, rosnącej i malejącej.
Z kolei parametr typsortowania może przyjmować takie same wartości jak w przypadku funkcji
sort().
Rozdział 3. Stosowanie tablic 103
Należy zwrócić uwagę na to, że choć funkcja array_multisort() będzie zachowywać skojarzenia
klucz-wartość w przypadku kluczy łańcuchowych, to w przypadku kluczy numerycznych, takich
jak te w powyższym przykładzie, skojarzenia te nie będą zachowywane.
usort($produkty, 'porownaj');
Dotychczas w tej książce stosowane były wyłącznie funkcje wbudowane w PHP. Oprócz nich
została zdefiniowana osobna funkcja — ma ona za zadanie uporządkować powyższą tablicę.
Tworzenie funkcji przedstawimy dokładnie w rozdziale 5., tymczasem poniżej znajduje się krótkie
wprowadzenie.
Funkcje definiuje się za pomocą słowa kluczowego function. Należy im nadać nazwę, która
powinna być znacząca, tak więc powyższa funkcja nosi na przykład nazwę porownaj(). Wiele
funkcji używa parametrów lub argumentów. Funkcja porownaj() pobiera dwa argumenty — jeden
nazwany został $x, a drugi $y. Celem tej funkcji jest pobranie dwóch wartości i określenie ich
porządku.
W tym przykładzie parametry x i y są dwiema z tablic zawartych w głównej tablicy; każda z nich
opisuje jeden produkt. Aby uzyskać dostęp do pola Opis tablicy $x, należy wprowadzić $x[1],
ponieważ Opis jest drugim w kolejności elementem tych tablic, a numerowanie zaczyna się od
zera. Wyrażenia $x[1] i $y[1] stosuje się do porównania pól Opis w tablicach przekazanych
funkcji.
Kiedy funkcja kończy swoje działanie, może dać odpowiedź wyrażeniu, które ją wywołało. Dzia-
łanie to nazywa się zwracaniem wartości. Aby zwrócić wartość, należy zastosować w funkcji
słowo kluczowe return. Na przykład wiersz return 1; wysyła do kodu wywołującego funkcję
wartość 1.
Aby być użyta przez funkcję usort(), funkcja porownaj() musi porównać $x i $y. Zwraca war-
tość 0, jeżeli $x jest równe $y, liczbę ujemną, jeżeli $x jest mniejsze, a dodatnią, jeżeli większe.
Powyższa funkcja zwróci 0, 1 lub –1, zależnie od wartości $x i $y.
Ostatni wiersz powyższego kodu wywołuje wbudowaną funkcję usort() z tablicą, która ma zostać
uporządkowana ($produkty), i z nazwą funkcji porównującej (porownaj()).
104 Część I Stosowanie PHP
Jeżeli tablica ma zostać uporządkowana według odmiennego klucza, należy po prostu napisać
inną funkcję porównującą. Aby sortować według ceny, należy wziąć pod uwagę trzecią kolumnę
tablicy i utworzyć następującą funkcję porównującą:
function porownaj($x, $y) {
if ($x[2] == $y[2]) {
return 0;
} else if ($x[2] < $y[2]) {
return –1;
} else {
return 1;
}
}
Znak u w nazwie funkcji usort() znaczy użytkownik (user), ponieważ funkcja ta wymaga zdefi-
niowanej przez użytkownika funkcji porównującej. Podobnie dzieje się w wypadku funkcji
uasort() i uksort() — wersje asort() i ksort().
Tak jak asort(), funkcja uasort() powinna być używana do porządkowania tablic nienumerycznych
według wartości. Należy zastosować funkcję asort(), jeżeli wartości są zwykłymi liczbami
lub wartościami tekstowymi. Jeśli są obiektami bardziej skomplikowanymi, np. tablicami, trzeba
zdefiniować własną funkcję porównującą i zastosować uasort().
Podobnie jak ksort(), funkcja uksort() powinna być wykorzystywana do porządkowania tablic
nienumerycznych według kluczy. Należy zastosować funkcję ksort(), jeżeli klucze są zwykłymi
liczbami lub wartościami tekstowymi. Jeśli chcemy posortować tablicę według kluczy, uwzględniając
kryteria, których nie obsługują standardowe funkcje sortujące, należy zdefiniować własną funkcję
porównującą i zastosować uksort() (klucze tablic PHP mogą być typu integer lub string).
shuffle($obrazki);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Części samochodowe Janka</title>
</head>
<body>
<h1>Części samochodowe Janka</h1>
<div align="center">
<table style="width: 100%; border: 0">
<tr>
<?php
for ($i = 0; $i < 3; $i++) {
echo "<td style=\"width: 33%; text-align: center\">
<img src=\"";
echo $obrazki[$i];
echo "\"/></td>";
}
?>
</tr>
</table>
</div>
</body>
</html>
Ponieważ kod wybiera obrazki losowo, tworzy inną stronę prawie za każdym jej załadowaniem,
jak przedstawiono na rysunku 3.5.
106 Część I Stosowanie PHP
Rysunek 3.5.
Funkcja shuffle()
pozwala na wyświetlenie
obrazu trzech losowo
wybranych produktów
Zastosowanie funkcji range() powoduje zazwyczaj utworzenie sekwencji liczb zapisanych w ko-
lejności rosnącej:
$liczby = range(1,10);
Alternatywnie, można stworzyć tablicę element po elemencie poprzez napisanie pętli for:
$liczby = array();
for($i=10; $i>0; $i--) {
array_push($liczby, $i);
}
Pętla for może działać w powyższy sposób, pomniejszając elementy. W tym celu należy ustawić
wysoką wartość początkową, po czym zastosować operator --, aby za każdym razem zmniejszać
licznik o jeden.
Powyżej została utworzona pusta tablica, po czym dla każdego elementu zastosowano funkcję
array_push(), która dodaje element na końcu tablicy. Nawiasem mówiąc, przeciwieństwem funkcji
array_push() jest array_pop(). Funkcja ta usuwa i zwraca ostatni element tablicy.
Warto zwrócić uwagę na to, że jeżeli tablica, którą należy utworzyć, ma zawierać ciąg liczb zapisa-
nych w kolejności malejącej, to można ją utworzyć, przekazując do funkcji range() opcjonalny,
trzeci parametr o wartości –1:
$liczby = range(10, 1, -1);
Rozdział 3. Stosowanie tablic 107
Aby przetworzyć lub wypełnić zamówienie, można wczytać je do tablicy. Listing 3.2 wyświetla
aktualny plik zamówień.
$number_of_orders = count($orders);
if ($number_of_orders == 0) {
echo "<p><strong>Brak zamówień.<br />
Proszę spróbować później.</strong></p>";
}
Powyższy skrypt daje rezultat identyczny do listingu 2.3 z poprzedniego rozdziału, przedstawionego
na rysunku 2.4. Tym razem zastosowano funkcję file(), która wczytuje cały plik do tablicy.
Każdy wiersz pliku staje się jednym elementem tablicy. Powyższy kod stosuje również funkcję
count(), aby sprawdzić liczbę elementów w tablicy.
Co więcej, można wczytać każdą część wiersza zamówienia do osobnego elementu tablicy, aby
przetwarzać poszczególne części osobno lub sformatować je w bardziej atrakcyjny sposób. Dzia-
łania te wykonuje na przykład listing 3.3.
<head>
<title>Części samochodowe Janka - Zamówienia klientów</title>
<style type="text/css">
table, th, td {
border-collapse: collapse;
border: 1px solid black;
padding: 6px;
}
th {
background: #ccccff;
}
</style>
</head>
<body>
<h1>Części samochodowe Janka</h1>
<h2>Zamówienia klientów</h2>
<?php
// Odczytanie całego pliku
// Każde zamówienie staje się elementem tablicy
$zamowienia= file("$document_root/../zamowienia/zamowienia.txt");
if ($ilosc_zamowien == 0) {
echo "<p><strong>Brak zamówień.<br />
Proszę spróbować później.</strong></p>";
}
echo "<table>\n";
echo "<tr>
<th>Data zmówienia</th>
<th>Opony</th>
<th>Olej</th>
<th>Świece zapłonowe</th>
<th>Suma</th>
<th>Adres</th>
<tr>";
echo "</table>";
?>
</body>
</html>
Kod zawarty w listingu 3.3 wczytuje cały plik do tablicy, lecz w tym przypadku, inaczej niż w listin-
gu 3.2, zastosowana została funkcja explode(), która dzieli wiersz na poszczególne pola. Pozwala to
na przetworzenie i sformatowanie danych przed wyświetleniem. Wynik działania powyższego
skryptu został przedstawiony na rysunku 3.6.
Rysunek 3.6.
Po rozbiciu zamówień
funkcją explode() każda
część zamówienia może
zostać umieszczona
w osobnej komórce
tablicy, aby dać bardziej
uporządkowany wynik
Powyższe wywołanie „rozbija” na części przekazany mu łańcuch znaków. Każdy znak tabulacji
staje się przerwą pomiędzy dwoma elementami. Na przykład łańcuch:
"19:49, 4th December 2016\t4 opon\t1 butelek oleju\t6 świec zapłonowych\
t434.00PLN\tul. Główna 33, Gliwice"
zostaje podzielony na następujące części: "19:49, 4th December 2016", "4 opon", "1 butelek
oleju", "6 świec zapłonowych", "434.00PLN", "ul. Główna 33, Gliwice". Należy zwrócić uwagę,
że opcjonalny argument limit może być zastosowany do ograniczenia maksymalnej liczby zwra-
canych części.
W przykładzie tym nie została wykonana duża ilość przetwarzania. Jedynie zamiast wyświetlania
w każdej linii opon, oleju i świec zapłonowych pokazywana jest tylko ilość każdego z tych elemen-
tów oraz dodany został dodatkowy rząd nagłówkowy podający znaczenie liczb.
Istnieje kilka sposobów wyciągania liczb z łańcuchów znaków. W powyższym przykładzie zasto-
sowana została funkcja intval(). Jak wspomnieliśmy w rozdziale 1., funkcja intval() zmienia typ
string w typ integer. Konwersja przebiega dość inteligentnie i w związku z tym ignoruje takie
części, jak opis w powyższym przykładzie (nie mogą być one zamienione na format integer).
Różne sposoby manipulacji łańcuchami znaków zostaną przedstawione w następnym rozdziale.
110 Część I Stosowanie PHP
Jeżeli tworzona jest nowa tablica, aktualny wskaźnik jest inicjowany w ten sposób, że wskazuje
pierwszy element tablicy. Wywołanie funkcji current($nazwa_tablicy) zwraca pierwszy element.
Wywołanie funkcji next() lub each() przesuwa wskaźnik o jeden element do przodu. Wywołanie
each($nazwa_tablicy) zwraca aktualny element przed przeskokiem do następnego. Funkcja next()
działa nieco inaczej — wywołanie next($nazwa_tablicy) przesuwa wskaźnik, po czym zwraca
nowy element aktualny.
Powyżej pokazane zostało, że funkcja reset() przesuwa wskaźnik do pierwszego elementu tablicy;
podobnie wywołanie end($nazwa_tablicy) przesuwa wskaźnik na koniec tablicy. Funkcje te zwracają
odpowiednio pierwszy i ostatni element tablicy.
Aby poruszać się po tablicy w odwrotnej kolejności, można zastosować end() i prev().
Funkcja prev() jest przeciwieństwem next(). Przesuwa ona wskaźnik o jeden element wstecz,
po czym zwraca nowy aktualny element.
Stosując funkcje each(), current(), reset(), end(), next(), pos() i prev(), można napisać własny
kod poruszający się po tablicy w dowolny sposób.
Rozdział 3. Stosowanie tablic 111
Podobnie jak wyżej opisana funkcja usort(), array_walk() spodziewa się przekazania funkcji
zdeklarowanej przez użytkownika. Funkcja array_walk pobiera trzy parametry. Pierwszym
z nich jest tablica, która ma zostać przetworzona. Drugi to nazwa zdefiniowanej przez użyt-
kownika funkcji, która zostanie zastosowana do każdego elementu tablicy. Trzeci parametr,
dane_uzytkownika, jest opcjonalny. Zastosowanie go przekazuje jego wartość jako parametr zdefi-
niowanej funkcji. Poniżej przedstawiono zasady jej działania.
Na przykład zdefiniowaną przez użytkownika podręczną funkcją może być funkcja wyświetlająca
każdy element w określonym jednym formacie. Poniższy kod wyświetla każdy element w nowym
wierszu, wywołując zdefiniowaną przez użytkownika funkcję moj_drukuj() przy każdym elemencie
tablicy $tablica.
function moj_drukuj($wartosc) {
echo "$wartosc<br />";
}
array_walk($tablica, 'moj_drukuj');
Należy również zauważyć pewien subtelny punkt powyższego kodu, to znaczy sposób przeka-
zywania wartości $wartosc. Symbol & przed nazwą zmiennej w definicji funkcji mojeMnozenie()
oznacza, że $wartość zostanie przekazana przez referencję. Przekazanie przez referencję pozwala
funkcji na modyfikację zawartości tablicy.
Klucz Wartość
4 1
5 1
1 3
2 2
3 1
Powyższa tabela pozwala stwierdzić, że wartości 4, 5 i 3 wystąpiły w tablicy $tablica raz, 1 — trzy
razy, a 2 — dwukrotnie.
Przeznaczeniem funkcji extract() jest pobranie tablicy i stworzenie wartości skalarnych o nazwach,
takich jak klucze tablicy. Wartości tych zmiennych zostają nadane według wartości tablicy.
Rozdział 3. Stosowanie tablic 113
Powyższa tablica posiadała trzy elementy o kluczach: klucz1, klucz2 i klucz3. Po użyciu funkcji
extract() utworzone zostały trzy zmienne: $klucz1, $klucz2 i $klucz3. Powyższy listing pokazu-
je, że wartości zmiennych $klucz1, $klucz2 i $klucz3 to odpowiednio: 'wartosc1', 'wartosc2'
i 'wartosc3'. Wartości te zostały przeniesione z pierwotnej tablicy.
Typ Znaczenie
EXTR_OVERWRITE W wypadku kolizji nadpisanie istniejącej zmiennej
EXTR_SKIP W wypadku kolizji ominięcie elementu
EXTR_PREFIX_SAME W wypadku kolizji utworzenie zmiennej $przedrostek_klucz. Musi zostać podana
wartość parametru przedrostek
EXTR_PREFIX_ALL Przed wszystkimi nazwami zmiennych zostaje umieszczony przedrostek.
Musi zostać podana wartość parametru przedrostek
EXTR_PREFIX_INVALID Poprzedza prefiksem przedrostek nazwy zmiennych, które w przeciwnym
wypadku byłyby nieprawidłowe (na przykład zmienne o nazwach będących liczbami).
Przedrostek musi być podany
EXTR_IF_EXISTS Wyodrębnia tylko te zmienne, które już istnieją (tzn. wypełnia istniejące
zmienne wartościami z tablicy). Funkcja jest przydatna na przykład do
przekształcania $_REQUEST w zbiór poprawnych zmiennych
EXTR_PREFIX_IF_EXISTS Tworzy tylko wersję z przedrostkiem, jeśli istnieje już wersja bez przedrostka.
EXTR_REFS Wyodrębnia zmienne jako odwołania.
Należy zauważyć, że aby funkcja extract() mogła dokonać ekstrakcji elementu, jego klucz musi
być zgodną z wymaganiami nazwą zmiennej. Oznacza to, że klucze rozpoczynające się od cyfr bądź
zawierające spacje zostaną ominięte.
W następnym rozdziale
Kolejny rozdział zawiera opis funkcji przetwarzających łańcuchy znaków. Przedstawione zostaną
również funkcje wyszukujące, zamieniające, dzielące i łączące ciągi, a także efektywne funkcje
wyrażeń regularnych, które potrafią przeprowadzić niemal każde działanie na ciągach.
Rozdział 4.
Manipulowanie łańcuchami
znaków i wyrażenia regularne
W tym rozdziale omówimy sposoby manipulowania tekstem i formatowania go za pomocą funkcji
służących do obsługi łańcuchów znaków dostępnych w PHP. Przedstawimy w nim również sposób
zastosowania tych funkcji i wyrażeń regularnych do wyszukiwania (i zamiany) słów, fraz i innych
fragmentów łańcuchów.
Funkcje te znajdują wiele zastosowań. Często koniecznie jest oczyszczenie lub przeformatowanie
danych wpisanych przez użytkownika, które mają zostać umieszczone w bazie danych. Funkcje
wyszukujące są m.in. niezwykle przydatne do tworzenia mechanizmów aplikacji wyszukujących.
Przykładowa aplikacja
— Inteligentny Formularz Pocztowy
W tym rozdziale funkcje do obsługi łańcuchów znaków i wyrażeń regularnych będą rozpatrywane
w kontekście aplikacji Inteligentnego Formularza Pocztowego. Skrypty te zostaną dodane do opi-
sywanej w poprzednich rozdziałach strony „Części samochodowych Janka”.
Tym razem utworzony zostanie bezpośredni i często stosowany formularz komentarzy klientów,
aby mogli oni przesyłać skargi bądź pochwały. Formularz ten jest podobny do przedstawionego
na rysunku 4.1. Jednak aplikacja ta będzie posiadać jedno ulepszenie, rzadko spotykane w sieci
WWW. Zamiast wysyłać komentarze pocztą na ogólny adres pocztowy, taki jak na przykład
komentarze@przyklad.com, będzie wybierać docelowego odbiorcę komentarza poprzez poszukiwa-
nie kluczowych słów i znaków. Następnie wysyła komentarz bezpośrednio do odpowiedniego
pracownika firmy Janka. Na przykład jeżeli list zawiera słowo „reklama”, należy przesłać go do
działu marketingu. Jeśli pochodzi od największego klienta, powinien zostać przekazany bezpo-
średnio do Janka.
116 Część I Stosowanie PHP
Rysunek 4.1.
Formularz komentarzy
firmy Janka „prosi”
klientów o podanie
nazwiska, adresu
poczty elektronicznej
i komentarza
Na początku przedstawiony zostanie bardzo prosty skrypt z listingu 4.1, który będzie rozbudowy-
wany w dalszej części rozdziału.
$adresod = "serwerwww@przyklad.com";
?>
<!DOCTYPE html>
<html>
<head>
<title>Części samochodowe Janka — komentarz przyjęty</title>
</head>
<body>
<h1>Komentarz przyjęty</h1>
<p>Państwa komentarz został wysłany.</p>
</body>
</html>
Rozdział 4. Manipulowanie łańcuchami znaków i wyrażenia regularne 117
W powyższym skrypcie pola formularza zostały połączone i przy użyciu funkcji PHP mail()
wysłane na adres komentarze@przyklad.com. Jest to tylko przykładowy adres pocztowy. Aby
móc przetestować kody prezentowane w tym rozdziale, w jego miejsce trzeba wstawić rzeczywisty
adres poczty elektronicznej. Ponieważ jest to pierwsze w tej książce zastosowanie funkcji mail(),
poniżej znajduje się jej dokładniejszy opis.
Jak łatwo przewidzieć, funkcja ta wysyła pocztę elektroniczną. Prototyp funkcji mail() wygląda
następująco:
bool mail(string do, string temat, string wiadomosc, string [dodatkowe_naglowki [, string
dodatkowe_parametry]]);
Pierwsze trzy parametry są wymagane i opisują odpowiednio adres pocztowy odbiorcy, temat
wiadomości i samą wiadomość. Czwarty parametr może być zastosowany do wysłania dodatkowych
standaryzowanych nagłówków pocztowych. Standaryzowane nagłówki pocztowe są opisane
w dokumencie RFC822, dostępnym w formie elektronicznej (RFC, czyli prośby o komentarz —
Requests for Comment). Stanowią one źródło wielu standardów internetowych, opisanych szerzej
w rozdziale 18. Powyżej czwarty parametr został zastosowany do dodania do listu adresu From:.
W ten sposób można również dodać m.in. pola Reply-to: i Cc:. Aby dorzucić więcej niż jeden
nagłówek, należy rozdzielić te nagłówki w łańcuchu znaków symbolami nowego wiersza i powrotu
karetki (\r\n), jak przedstawiono poniżej:
$dodatkowe_naglowki="From: serwerwww@przyklad.com\r\n"
."Reply-to: janek@przyklad.com";
Opcjonalny piąty parametr może zostać użyty do przekazywania parametru do programu pocztowe-
go, za pomocą którego będzie wysłana wiadomość.
Aby mieć możliwość korzystania z funkcji mail(), należy ustawić instalację PHP tak, aby posiadała
dane programu wysyłającego pocztę. Jeżeli powyższy skrypt nie działa poprawnie, przyczyną
może być błąd instalacji — trzeba wówczas dokładnie sprawdzić dodatek A, „Instalacja Apache,
PHP i MySQL”.
W dalszej części rozdziału powyższy podstawowy skrypt zostanie rozwinięty poprzez wykorzy-
stanie funkcji łańcuchowych i funkcji obsługi wyrażeń regularnych PHP.
PHP posiada trzy funkcje o takim przeznaczeniu. Na początku skryptu, gdy zmiennym z formularza
wejściowego nadawane są krótkie nazwy, można uporządkować wprowadzone dane przy użyciu
funkcji trim() w następujący sposób:
$nazwa=trim($_POST['nazwa']);
$adres=trim($_POST['adres']);
$komentarz=trim($_POST['komentarz']);
Funkcja trim() usuwa znaki odstępu z początku i końca łańcucha, po czym zwraca łańcuch wyni-
kowy. Znaki, które usuwa, to symbole nowego wiersza i powrotu karetki (\n i \r), poziome
i pionowe znaki tabulacji (\t i \x0B), znaki końca łańcucha (\0) i spacje. Można również
przekazać do niej drugi parametr zawierający listę znaków, które należy usunąć zamiast listy
domyślnej. Zależnie od pożądanego rezultatu, możliwe jest zastosowanie zamiast funkcji trim()
funkcji ltrim() lub rtrim(). Obie podobne są do trim(); pobierają łańcuch znaków i zwracają go
w postaci sformatowanej. Różnica między nimi polega na tym, że trim() usuwa znaki odstępu
z początku i końca łańcucha, ltrim() jedynie z początku (czyli z lewej strony), a rtrim() tylko
z końca (czyli z prawej strony).
Zamiast rtrim() można także używać tej samej funkcji pod inną nazwą: chop(). Podobne funkcje
są dostępne w języku Perl, choć działają one nieco inaczej; dlatego osoby mające doświadczenie
w stosowaniu tego języka powinny zachować ostrożność i uważać, by nie przenosić swoich przy-
zwyczajeń z Perla na grunt PHP.
Ogólnie rzecz biorąc, funkcja ta przekształca znaki przedstawione w tabeli 4.1 na odpowiadające
im symbole HTML.
Znak Przekształcenie
& &
" "
' '
< <
> >
Jeśli chodzi o działanie tej funkcji, to domyślnie koduje ona wyłącznie cudzysłowy. Znaki apostrofu
nie są domyślnie konwertowane, przy czym można to zmienić przy użyciu parametru flagi.
Uwaga: Jeśli wejściowy łańcuch znaków nie jest prawidłowy w stosowanym sposobie kodowania,
to funkcja zwróci pusty łańcuch znaków, a jednocześnie nie będzie zgłaszać jakichkolwiek
błędów. Taki sposób działania jest celowy i ma zapobiegać problemom związanym ze wstrzyki-
waniem kodu.
Pierwszy opcjonalny parametr funkcji, flagi, określa sposób przeprowadzania konwersji. Jego
wartością jest maska bitowa reprezentująca połączone ze sobą, dostępne stałe. Wartością domyślną
tego parametru, co pokazuje prototyp funkcji, jest ENT_COMPAT | ENT_HTML401. Stała ENT_COMPAT
wskazuje na to, że znaki cudzysłowu powinny być konwertowane, a znaki apostrofu nie. Z kolei
stała ENT_HTML401 informuje funkcję, że przekazany łańcuch powinien być traktowany jako kod
w języku HTML 4.01.
Drugi opcjonalny parametr funkcji, kodowanie, określa sposób kodowania używany podczas kon-
wersji. Począwszy od wersji PHP 5.4, domyślnie stosowane jest kodowanie UTF-8. Wcześniej
było to kodowanie ISO-8859-1, określane także jako Latin-1. Kompletna lista obsługiwanych
sposobów kodowania dostępna jest w dokumentacji PHP.
Trzeci opcjonalny parametr funkcji, podwojne_kodowanie, określa, czy należy kodować symbole
HTML umieszczone w przekazanym łańcuchu znaków. Domyślna wartość tego parametru (true)
nakazuje, by były one (ponownie) kodowane.
Tabela 4.2 zawiera kompletną listę stałych, które mogą być używane w parametrze flagi funkcji
htmlspecialchars().
120 Część I Stosowanie PHP
Flaga Znaczenie
ENT_COMPAT Konwertowane będą znaki cudzysłowu, lecz nie apostrofy.
ENT_NOQUOTES Nie będą konwertowane ani znaki cudzysłowu, ani apostrofy.
ENT_QUOTES Konwertowane będą zarówno cudzysłowy, jak i apostrofy.
ENT_HTML401 Łańcuch wejściowy będzie traktowany jako kod HTML 4.01.
ENT_XML1 Łańcuch wejściowy będzie traktowany jako kod XML 1.
ENT_XHTML Łańcuch wejściowy będzie traktowany jako kod XHTML.
ENT_HTML5 Łańcuch wejściowy będzie traktowany jako kod HTML5.
ENT_IGNORE Eliminuje nieprawidłowe sekwencje jednostek kodowych, zamiast zwracać pusty
łańcuch znaków. Ze względów bezpieczeństwa rozwiązanie to nie jest zalecane.
ENT_SUBSTITUTE Zastępuje nieprawidłowe sekwencje jednostek kodowych znakiem zastępczym
Unicode (ang. Unicode Replecement Character).
ENT_DISALLOWED Zastępuje nieprawidłowe punkty kodowe znakiem zastępczym Unicode.
Podobnie jak wiele innych problemów związanych z przetwarzaniem łańcuchów znaków, także i ten
można rozwiązać na kilka sposobów. Jednym z nich jest zastosowanie funkcji str_replace(),
przedstawione w poniższym przykładzie:
$mailcontent = "Nazwa klienta: ".str_replace("\r\n", "", $name)."\n".
"Adres e-mail klienta: ".str_replace("\r\n", "",$email)."\n".
"Komentarze klienta:\n".str_replace("\r\n", "",$feedback)."\n";
Jeśli konieczne będzie zastosowanie bardziej złożonych reguł dopasowywania oraz zamiany, można
skorzystać z wyrażeń regularnych, opisanych w dalszej części tego rozdziału. Jednak w prostych
przypadkach, takich jak chęć całkowitego zastąpienia jednego łańcucha znaków drugim, należy
zastosować funkcję str_replace(). Funkcja str_replace() zostanie szczegółowo omówiona
w dalszej części rozdziału.
Rozdział 4. Manipulowanie łańcuchami znaków i wyrażenia regularne 121
Należy pamiętać, że HTML nie zwraca uwagi na odstępy, tak więc jeżeli komentarz nie zostanie
przefiltrowany przez funkcję nl2br(), zostanie wyświetlony w jednym wierszu (poza podziałem
wymuszonym przez wielkość okna przeglądarki). Wynik tego jest przedstawiony na rysunku 4.2.
Rysunek 4.2.
Zastosowanie funkcji
PHP nl2br() poprawia
wygląd długich
łańcuchów w HTML
Należy zwrócić uwagę na to, że w powyższym przykładzie najpierw wywoływana jest funkcja
htmlspecialchars(), a następnie funkcja nl2br(). Taka kolejność wywołań jest konieczna, gdyż
w razie jej odwrócenia znaczniki <br /> wstawione przez wywołanie funkcji nl2br() zosta-
łyby przekształcone przez funkcję htmlspecialchars() na symbole HTML, co nie dałoby zamie-
rzonego efektu.
<?php
// utworzenie krótkich nazw zmiennych
$nazwa = trim($_POST['nazwa']);
$email = trim($_POST['email']);
$komentarz = trim($_POST['komentarz']);
$adresod = "serwerwww@przyklad.com";
<h1>Komentarz przyjęty</h1>
<p>Państwa komentarz (przedstawiony poniżej) został wysłany.</p>
<p><?php echo nl2br(htmlspecialchars($zawartosc)); ?> </p>
</body>
</html>
Istnieje także wiele innych funkcji służących do obsługi łańcuchów znaków, które mogą się oka-
zać przydatne. Zostaną one dokładniej opisane w dalszej części tego podrozdziału.
Obie techniki wyświetlają łańcuch „na surowo”. Aby zastosować bardziej złożone formatowa-
nie, należy skorzystać z funkcji printf() i sprintf(). Działają one w zasadzie w identyczny
sposób, z tym że printf() wyświetla sformatowany łańcuch znaków, a sprintf() po prostu go
zwraca.
Osoby znające język C szybko zorientują się, że funkcje te pod względem koncepcyjnym są
podobne do ich wersji w C. Trzeba jednak od razu zaznaczyć, że ich składnia nie jest identyczna.
Osoby nieznające tego języka będą musiały przyzwyczaić się do nich, lecz warto podjąć ten wysiłek,
gdyż są one użyteczne i efektywne.
Aby osiągnąć ten sam efekt za pomocą funkcji printf(), należy napisać:
printf ("Wartość zamówienia wynosi %s", $wartosc);
Rozdział 4. Manipulowanie łańcuchami znaków i wyrażenia regularne 123
Znak %s w formatowanym łańcuchu znaków zwany jest specyfikatorem konwersji. Ten konkretny
znak oznacza „zamień na łańcuch”. W tym przypadku zostanie on zamieniony na zmienną $wartosc,
interpretowaną jako łańcuch znaków. Jeżeli wartość zmiennej $wartosc wynosiła na przykład 12.4,
obie techniki wyświetlą ją jako 12.4.
Przewagą funkcji printf() jest możliwość zastosowania bardziej przydatnego specyfikatora kon-
wersji, wskazującego na przykład, że zmienna $wartosc jest w rzeczywistości liczbą zmienno-
przecinkową i że powinna mieć dwa miejsca po przecinku. Przedstawiono to poniżej:
printf ("Wartość zamówienia wynosi %.2f", $wartosc);
Po podaniu takiego sposobu formatowania powyższa instrukcja wyświetli wartość 12,4 przechowy-
waną w zmiennej $wartosc w postaci 12.40.
W formatowanym łańcuchu można zastosować kilka specyfikatorów konwersji. Jeżeli istnieje n
specyfikatorów konwersji, to po formatowanym łańcuchu znaków powinno znaleźć się n argumen-
tów. Każdy specyfikator konwersji zostanie zamieniony przeformatowanym argumentem w porząd-
ku, w jakim są one wyszczególnione. Na przykład:
printf ("Wartość zamówienia wynosi %.2f (z dostawą %.2f) ", $wartosc, $transport);
W tym przypadku pierwszy specyfikator konwersji zastosuje zmienną $wartosc, a drugi zmienną
$transport.
Każdy specyfikator konwersji jest skonstruowany według jednego formatu, który wygląda nastę-
pująco:
%[+]['znak_dopełnienia][–][szerokosc][.dokładnosc]typ
Gdy funkcja printf() jest wywoływana wraz z kodami konwersji, można wykorzystać wyliczanie
argumentów. Oznacza to, że argumenty nie muszą być wymieniane w takiej samej kolejności,
w jakiej zapisane są w specyfikacji konwersji. Na przykład:
printf ("Wartość zamówienia wynosi %.2\$.2f (z dostawą %1\$.2f) ", $wartosc, $transport, $wartosc);
124 Część I Stosowanie PHP
Typ Znaczenie
% Znak %
b Zinterpretować jako integer i wyświetlić jako liczbę binarną
c Zinterpretować jako integer i wyświetlić jako znak
d Zinterpretować jako integer i wyświetlić jako liczbę dziesiętną
e Zinterpretować jako liczbę zmiennoprzecinkową o podwójnej precyzji (double) i wyświetlić w zapisie
naukowym. W tym przypadku precyzja określa liczbę cyfr po przecinku dziesiętnym
E To samo co e, lecz zostanie wyświetlona wielka litera E
f Zinterpretować jako liczbę zmiennoprzecinkową (float) i wyświetlić w sposób odpowiedni dla liczb
tego typu z uwzględnieniem ustawień lokalnych
F Zinterpretować jako liczbę zmiennoprzecinkową (float) i wyświetlić w sposób odpowiedni dla liczb
tego typu bez uwzględniania ustawień lokalnych
g Krótszy z zapisów, jakie można uzyskać, używając specyfikatorów e lub f
G Krótszy z zapisów, jakie można uzyskać, używając specyfikatorów E lub F
o Zinterpretować jako integer i wyświetlić jako liczbę ósemkową
s Zinterpretować jako string i wyświetlić jako łańcuch znaków
u Zinterpretować jako integer i wyświetlić jako liczbę dziesiętną bez znaku
x Zinterpretować jako integer i wyświetlić jako liczbę szesnastkową z małymi literami a – f
X Zinterpretować jako integer i wyświetlić jako liczbę szesnastkową z wielkimi literami A – F
Wystarczy jedynie wskazać pozycję na liście bezpośrednio po znaku % i wpisać po nim znak ukośni-
ka oraz dolara — w naszym przykładzie 2\$ oznacza „wymień na drugi argument z listy”. Metoda
ta może być również użyta do powtarzania argumentów.
Istnieją dwie alternatywne wersje tej funkcji: vprintf() i vsprintf(). Obydwie zamiast zmiennej
liczby parametrów przyjmują dwa argumenty: łańcuch formatujący oraz tablicę argumentów.
Tabela 4.4. Funkcje wielkości liter w łańcuchach znaków i efekty ich zastosowania
Funkcja ta bierze łańcuch znaków o nazwie wpis i rozbija go na części według separatora,
po czym zwraca części w formie tablicy. Można ograniczyć liczbę części za pomocą opcjonalnego
parametru limit.
Aby otrzymać nazwę domeny z adresu poczty elektronicznej klienta, należy zastosować następu-
jący fragment kodu:
$tablica_email = explode('@', $email);
To wywołanie explode() rozdziela adres poczty elektronicznej klienta na dwie części — nazwę
użytkownika, która jest przechowywana w $tablica_email[0], oraz nazwę domeny, przechowy-
waną w $tablica_email[1]. Następnie można napisać kod sprawdzający nazwę domeny i przesy-
łający komentarz na odpowiedni adres:
if ($tablica_email[1] == "duzyklient.com") {
$adresdo = "janek@przyklad.com";
} else {
$adresdo = "komentarze@przyklad.com";
}
126 Część I Stosowanie PHP
Należy zauważyć, że jeżeli nazwa domeny jest zapisana wielkimi literami bądź literami różnej
wielkości, ta metoda nie będzie działać. Można ominąć ten problem poprzez wykonanie w pierw-
szej kolejności konwersji domeny w całości na wielkie lub małe litery, a później sprawdzenie
warunku:
if (strtolower($tablica_email[1]) == "duzyklient.com") {
$adresdo = "janek@przyklad.com";
} else {
$adresdo = "komentarze@przyklad.com";
}
Możliwe jest odwrócenie efektów explode() przez zastosowanie funkcji implode() bądź join(),
które są identyczne. Na przykład:
$nowy_email = implode('@', $tablica_email);
Separatorem może być zarówno znak, jak i łańcuch znaków. Należy jednak zauważyć, że wejściowy
łańcuch znaków zostanie rozbity przy każdym z elementów łańcucha separacyjnego, a nie przy
całym łańcuchu, jak to się dzieje w przypadku explode().
Wywoływanie strtok() nie jest, wbrew jej prototypowi, tak proste. Aby otrzymać pierwszy żeton
z łańcucha, należy wywołać strtok() z łańcuchem znaków, który ma zostać rozbity, oraz z sepa-
ratorem. W celu uzyskania następnych żetonów wystarczy podać jeden parametr — separator.
Funkcja utrzymuje swój wewnętrzny wskaźnik w odpowiednim miejscu łańcucha. Aby wyzero-
wać wskaźnik, należy ponownie podać funkcji łańcuch znaków do rozbicia.
Jak zwykle dobrym pomysłem jest sprawdzenie, czy klient w ogóle wpisał jakiś komentarz.
Można to zrobić, stosując na przykład funkcję empty(). W powyższym przykładzie działanie to
zostało ominięte w celu zachowania zwięzłości.
Powyższy kod wyświetla każdy żeton z komentarza klienta w osobnym wierszu i robi pętle,
dopóki są dostępne żetony.
Rozdział 4. Manipulowanie łańcuchami znaków i wyrażenia regularne 127
Jeżeli funkcja ta zostanie wywołana jedynie z dodatnią wartością parametru start, wynikiem
będzie łańcuch rozpoczynający się od pozycji start do końca łańcucha. Na przykład:
substr($test, 1);
zwraca Wasza obsługa klientów jest wspaniała. Należy zauważyć, że pierwsza pozycja w łań-
cuchu nosi numer 0, podobnie jak w wypadku tablic.
Jeżeli funkcja ta zostanie wywołana jedynie z ujemną wartością parametru start, wynikiem będzie
końcówka łańcucha wejściowego o długości start znaków. Na przykład:
substr($test, -9);
zwraca wspaniała.
Parametr dlugosc może być zastosowany do określenia liczby znaków, którą funkcja ma zwrócić
(jeżeli jest dodatni). Służy też do wyznaczenia liczby znaków, licząc od końca łańcucha wejścio-
wego, którą funkcja ma pominąć (jeżeli jest ujemny). Na przykład
substr($test, 0, 5);
zwraca pierwsze pięć znaków łańcucha, w tym przypadku Wasza. Następujący kod:
echo substr($test, 5, -15);
zwraca znaki pomiędzy piątym od początku i piętnastym od końca, to jest obsługa klientów.
Pierwszy znak ma numer 0, a zatem na pozycji piątej znajduje się znak szósty.
Funkcja pobiera dwa łańcuchy znaków, które porównuje. Jeżeli są równe, zwróci 0. Jeśli ciag1
zostaje uporządkowany za (lub jest większy niż) ciag2 w porządku leksykograficznym, strcmp()
zwróci liczbę większą od zera. Jeżeli ciag1 jest mniejszy od ciag2, strcmp() zwróci liczbę
mniejszą od zera. Funkcja uwzględnia wielkość liter.
Warto zwrócić uwagę na to, że takie działanie jest pod pewnymi względami intuicyjne, gdyż
sprawdzanie, czy wynikiem funkcji jest true, czy false, nie pozwoli uzyskać oczekiwanych
efektów. Jeśli oba porównywane łańcuchy są identyczne, to funkcja zwraca 0, więc w razie użycia
kodu o następującej postaci:
if (strcmp($a,$b)) {
...
}
okaże się, że powyższa instrukcja warunkowa zostanie wykonana wyłącznie w przypadku, gdy oba
łańcuchy są różne.
Funkcja strcasecmp() jest identyczna, tyle że nie zwraca uwagi na wielkość liter.
Wykorzystując poprzednio opisane funkcje, można użyć explode() lub strtok(), aby wycią-
gnąć pojedyncze słowa z wiadomości, po czym porównać ją za pomocą operatora == lub funkcji
strcmp().
Można jednak wykonać te same działania, stosując pojedyncze odwołanie do funkcji dopasowującej
łańcuchy znaków lub wyrażenia regularne. Służą one do wyszukiwania pewnego wzorca w łańcu-
chu. W poniższych punktach zostały one kolejno opisane.
Funkcja strstr(), najbardziej ogólna, może być stosowana do odnajdywania łańcucha lub
znaku w dłuższym łańcuchu znaków. Należy zauważyć, że w PHP funkcja strchr() działa tak
samo jak strstr(), chociaż z jej nazwy wynikałoby, że jest ona używana do odnajdywania znaku
w łańcuchu, podobnie jak wersja tej funkcji w języku C. W PHP każda z tych funkcji może być
stosowana do odnajdywania łańcucha znaków w innym, włączając w to łańcuchy zawierające
tylko jeden znak.
Funkcji należy przekazać łańcuch znaków stog, który ma zostać przeszukany, oraz łańcuch igla,
który ma być odnaleziony. Jeżeli zostanie odnaleziony łańcuch dokładnie pasujący do igla,
funkcja zwraca fragment stog rozpoczynający się od igla, a w przeciwnym wypadku — false.
Jeżeli igla pojawia się więcej niż jeden raz, zwrócony łańcuch znaków będzie się rozpoczynał
od pierwszego jej wystąpienia. Jeśli parametr przed_igla przyjmie wartość true, to funkcja zwróci
fragment łańcucha przed wystąpieniem łańcucha określonego parametrem igla.
Powyższy kod szuka konkretnych słów kluczowych w komentarzu i rozsyła listy do odpowiednich
osób. Jeżeli na przykład w komentarzu klienta znajduje się zdanie: „Wciąż nie otrzymałem mojej
dostawy”, zostanie wykryty łańcuch "dostawy". Komentarz zostanie następnie przesłany na adres
dostawy@przyklad.com.
Istnieją dwa warianty funkcji strstr(). Pierwszy z nich to stristr(), która to funkcja działa
praktycznie identycznie, lecz nie zwraca uwagi na wielkość liter. Własność ta jest przydatna w apli-
kacjach, w których klient może napisać na przykład "dostawy", "Dostawy" lub "DOSTAWY" — albo to
samo słowo zapisane dowolną inną kombinacją znaków o różnej wielkości.
Drugim wariantem jest funkcja strrchr(). Działa ona podobnie, lecz zwraca fragment stog, który
rozpoczyna się od ostatniego wystąpienia igla. Poza tym poszukuje ona tylko jednego znaku
(pierwszego znaku łańcucha przekazanego jako parametr igla), co jest nieco zagadkowe.
Zwracana liczba (integer) opisuje pozycję pierwszego wystąpienia igla w stog. Pierwszy znak
jak zwykle posiada pozycję 0.
W tym przypadku funkcji jako igla została przekazana jedna litera, lecz może być to także łańcuch
znaków dowolnej długości.
Opcjonalny parametr offset jest stosowany w celu podania pozycji, począwszy od której stóg ma
być przeszukiwany. Na przykład
echo strpos($test, 'ś', 4);
Funkcja strrpos() jest niemal identyczna, lecz zwraca pozycje ostatniego wystąpienia igla w stog.
W każdym z tych przypadków, jeżeli igla nie jest łańcuchem, strpos() i strrpos() zwrócą false.
Może to stwarzać problemy, ponieważ w językach o słabym typowaniu, takich jak PHP, false
jest w wielu kontekstach równoznaczne z 0 — położeniem pierwszego znaku łańcucha.
Rozdział 4. Manipulowanie łańcuchami znaków i wyrażenia regularne 131
Można uniknąć tego problemu, stosując operator ===, który sprawdza zwracane wartości:
$wynik = strpos($test, "C");
if ($wynik === false) {
echo "Nie znaleziono";
} else {
echo "Znaleziono na pozycji 0";
}
Funkcja ta zamienia wszystkie elementy igla znalezione w stog na nowa_igla i zwraca nową wersję
stog. Opcjonalny czwarty parametr ile wskazuje liczbę znaków, które mają zostać zastąpione.
Na przykład klienci mogą używać Inteligentnego Formularza, wnosząc skargi, w których wystę-
pują niecenzuralne słowa. Aby chronić pracowników Janka przed obrazą, można zdefiniować
tablicę $cenzura, zawierającą słowa niecenzuralne. Poniżej znajduje się przykład, w którym użyto
funkcji str_replace() i tej tablicy:
$komentarz = str_replace($cenzura, '%!@*', $komentarz);
Funkcja zamienia część łańcucha znaków ciag na łańcuch zamiennik. To, którą z nich obejmie
modyfikacja, zależy od parametru start i opcjonalnego dlugosc.
Wartość parametru start przedstawia offset w łańcuchu, gdzie powinna rozpocząć się zamiana.
Jeżeli wynosi ona 0 lub jest wartością dodatnią, jej pozycję liczy się od początku łańcucha; jeśli
jest ujemna, offset liczy się od końca łańcucha. Na przykład poniższy wiersz kodu zamieni ostatni
znak w $test na X:
$test = substr_replace($test, 'X', -1);
Parametr dlugosc jest opcjonalny i opisuje punkt, w którym PHP powinien zakończyć zamianę.
Jeżeli nie zostanie on podany, łańcuch znaków zostanie zamieniony od pozycji start do końca.
132 Część I Stosowanie PHP
Jeżeli dlugosc wynosi 0, zamiennik zostanie wstawiony do łańcucha bez nadpisania istnieją-
cej zawartości. Dodatnia wartość parametru dlugosc przedstawia liczbę znaków, które mają być
zamienione na nowy łańcuch. Ujemna wartość parametru dlugosc opisuje pozycję, liczoną od końca
łańcucha, w której powinna zakończyć się zamiana.
Podobnie jak funkcji str_replace(), także substr_replace() można używać, przekazując do niej
zestaw tablic.
Podstawy
Wyrażenie regularne to sposób opisywania wzorca we fragmencie tekstu. Prezentowane wcześniej
dosłowne dopasowania są pewną formą wyrażeń regularnych. Na przykład powyżej jest przed-
stawione poszukiwanie wyrażeń regularnych, takich jak sklep i dostawy.
Dostosowywanie wyrażeń regularnych w PHP jest bardziej podobne do funkcji strstr() niż do do-
kładnego porównania, ponieważ dopasowuje się pewien łańcuch znaków wewnątrz innego (może się
on znajdować gdziekolwiek w dłuższym łańcuchu, chyba że zostanie to ustawione). Na przykład
łańcuch sklep pasuje do wyrażenia regularnego sklep, ale również do wyrażeń regularnych k, kl itp.
Możliwe jest również stosowanie, oprócz dokładnego dopasowania, specjalnych znaków wska-
zujących metaznaczenie. Na przykład za pomocą znaków specjalnych można wskazać, że wzo-
rzec musi pojawić się na początku lub na końcu łańcucha, że część wzorca może się powtarzać
lub że znaki wzorca muszą należeć do określonego typu. Można również połączyć dokładne
występowania znaków specjalnych.
Ograniczniki
W przypadku wyrażeń regularnych PCRE każde wyrażenie musi zostać zapisane pomiędzy parą
specjalnych znaków — tak zwanych ograniczników. Mogą to być dowolne znaki, z wyjątkiem liter,
cyfr, znaku lewego ukośnika oraz odstępu. Ogranicznik umieszczony na końcu wyrażenia musi
być taki sam jak ten umieszczony na początku.
Najczęściej używanym ogranicznikiem jest znak ukośnika (/). A zatem wyrażenie regularne pasujące
do słowa sklep należałoby napisać tak, jak pokazano w poniższym przykładzie:
/sklep/
Jeśli konieczne jest dopasowanie znaku ukośnika (/), to trzeba go poprzedzić znakiem lewego
ukośnika (\), jak w poniższym przykładzie:
/http:\/\//
Rozdział 4. Manipulowanie łańcuchami znaków i wyrażenia regularne 133
Jeżeli w tworzonym wyrażeniu wybrany ogranicznik występuje wiele razy, to można się zastanowić
nad wybraniem innego. W przykładzie zaprezentowanym powyżej zamiast ukośnika można by
użyć jako ogranicznika znaku #, a wtedy wyrażenie regularne mogłoby przyjąć następującą postać:
#http://#
Czasami może się także pojawić potrzeba dodania za ogranicznikiem modyfikatora wzorca. Oto
przykład:
/sklep/i
W tym przypadku wyrażenie byłoby dopasowywane bez uwzględniania wielkości liter. Bez
wątpienia jest to najczęściej używany modyfikator; inne modyfikatory można znaleźć w doku-
mentacji PHP.
Po pierwsze, znak . może być stosowany jako wieloznacznik dla każdego innego znaku poza zna-
kiem nowego wiersza (\n). Na przykład wyrażenie regularne:
/.łot/
pasuje do wyrażeń "płot", "młot" itp. Ten rodzaj dostosowywania wieloznaczników jest często
używany do dopasowywania nazw plików w systemach operacyjnych.
Stosując wyrażenia regularne, można jednak znacznie dokładniej opisać typ znaku, do którego
będzie tworzone dopasowanie. Można również określić zbiór, do którego znak musi należeć. W po-
przednim przykładzie wyrażenie regularne pasuje do wyrażeń "młot" i "płot", ale także do "#łot".
Aby ograniczyć dopasowywane znaki do tych zakresu od a do z, należy dodać następujący kod:
/[a–z]ot/
Można również określić zakres, jak zostało to uczynione w poprzednim przykładzie, stosując
specjalny znak łącznika, a także zbiór zakresów:
/[a–zA–Z]/
Ten zbiór zakresów oznacza każdy znak alfabetu niezależnie od wielkości litery.
Można również zastosować zbiory do określenia, które znaki nie mogą być elementami zbioru.
Na przykład:
/[^a–z]/
134 Część I Stosowanie PHP
dopasowuje każdy znak, który nie pochodzi z zakresu pomiędzy a i z. Znak karetki (^, nazywany
także akcentem przeciągłym) oznacza nie, kiedy jest umieszczony w nawiasach kwadratowych.
Poza nimi posiada inne znaczenie, opisane poniżej.
Ponadto oprócz wyliczania zbiorów i zakresów istnieje pewna ilość predefiniowanych klas znaków,
które mogą być użyte w wyrażeniach regularnych. Zostały one przedstawione w tabeli 4.5.
Klasa Dopasowania
[[:alnum:]] Znaki alfanumeryczne
[[:alpha:]] Znaki alfabetu
[[:ascii:]] Znaki ASCII
[[:lower:]] Małe litery
[[:upper:]] Wielkie litery
[[:word:]] Znaki tworzące słowa (litery, cyfry i znak podkreślenia)
[[:digit:]] Liczby dziesiętne
[[:xdigit:]] Liczby szesnastkowe
[[:punct:]] Znaki przestankowe
[[:blank:]] Tabulatory i spacje
[[:space:]] Znaki odstępu
[[:cntrl:]] Znaki kontrolne
[[:print:]] Wszystkie możliwe do wyświetlenia znaki
[[:graph:]] Wszystkie możliwe do wyświetlenia znaki poza spacjami
Koniecznie należy zapamiętać, że zewnętrzna para nawiasów klamrowych ogranicza klasę, a we-
wnętrzna para wchodzi w skład jej nazwy. Na przykład:
/[[:alpha]1-5]/
To wyrażenie opisuje klasę, która może zawierać literę lub cyfrę z zakresu od 1 od 5.
Powtarzalność
Często konieczne jest określenie kilkukrotnego występowania konkretnego łańcucha bądź klasy
znaków. Można to przedstawić, stosując trzy znaki specjalne w wyrażeniach regularnych. Symbol *
oznacza, że wzorzec może powtórzyć się zero bądź więcej razy, a symbol + — jeden lub więcej
razy. W końcu symbol ? określa, że wzorzec powinien wystąpić dokładnie jeden raz lub nie
powinien wystąpić w ogóle. Symbol ten powinien pojawić się bezpośrednio po tej części wyraże-
nia, do której się odnosi. Na przykład:
/[[:alnum:]]+/
Podwyrażenia
Często użyteczna jest możliwość rozdzielenia wyrażenia na podwyrażenia, na przykład w celu
przedstawienia „co najmniej jednego z tych łańcuchów, a następnie dokładnie tego”. Wyrażenia
można rozdzielić stosując nawiasy, w taki sam sposób jak w wyrażeniach arytmetycznych. Na
przykład
/(bardzo )*dużo/
Podwyrażenia policzalne
Możliwe jest określenie liczby powtórzeń danego łańcucha znaków poprzez zastosowanie wyra-
żenia numerycznego w nawiasach klamrowych ({}). Można przekazać dokładną liczbę powtórzeń
({3} oznacza dokładnie trzy powtórzenia), zakres powtórzeń ({2, 4} oznacza od dwu do czterech
powtórzeń) lub otwarty zakres powtórzeń ({2,} oznacza co najmniej dwa powtórzenia).
Na przykład:
/(bardzo ){1,3}/
Można również określić, czy konkretne podwyrażenie powinno pojawić się na początku, na
końcu, czy też w obu miejscach. Jest to bardzo użyteczne przy upewnianiu się, czy w łańcuchu
nie pojawia się nic więcej poza szukaną frazą.
Symbol ^ jest stosowany na początku wyrażenia regularnego w celu wskazania, że musi się ono
pojawić na początku szukanego łańcucha znaków. Symbol $ jest umieszczany na końcu wyrażenia
regularnego, aby pokazać, że musi się ono pojawić na końcu łańcucha.
Rozgałęzianie
Możliwość wyboru w wyrażeniach regularnych oznacza się znakiem |. Na przykład aby wyrażenie
pasowało do com, edu lub net, można zastosować wyrażenie:
/com|edu|net/
136 Część I Stosowanie PHP
Analogicznie, jeżeli chcemy umieścić znak lewego ukośnika w łańcuchu znaków umieszczo-
nym w cudzysłowie, również z tego samego powodu trzeba wpisać dwa takie znaki. Ostatecznie
trochę niecodziennym efektem stosowania tych reguł jest to, że w PHP łańcuch znaków reprezentu-
jący wyrażenie regularne zawierające zwykły znak lewego ukośnika musi zawierać cztery lewe
ukośniki. Interpreter PHP odczyta cztery takie znaki jako dwa lewe ukośniki, natomiast interpreter
wyrażeń regularnych potraktuje obydwa lewe ukośniki jako jeden.
Również znak dolara jest znakiem specjalnym w łańcuchach znaków PHP zapisanych w cudzy-
słowach oraz wyrażeniach regularnych. Aby móc dopasowywać znak $ we wzorcu, należałoby
napisać "\\\$". Łańcuch ten znajduje się w cudzysłowach, dlatego PHP odczyta go jako \$,
a interpreter wyrażeń regularnych dopasuje go do znaku dolara.
Podsumowanie metaznaków
Podsumowanie wszystkich znaków specjalnych — nazywanych także metaznakami — jest przed-
stawione w tabelach 4.6 i 4.7. Tabela 4.6 ukazuje znaczenie znaków specjalnych umieszczonych
poza nawiasami kwadratowymi, a tabela 4.7 znaczenie znaków w nawiasach.
Tabela 4.6. Podsumowanie znaków specjalnych stosowanych w wyrażeniach regularnych PCRE umieszczonych
poza nawiasami kwadratowymi
Znak Znaczenie
\ Znak ucieczki
^ Dopasowanie na początku łańcucha
$ Dopasowanie na końcu łańcucha
. Dopasowanie do każdego znaku oprócz nowego wiersza
| Start alternatywnych rozgałęzień (jak OR)
( Początek fragmentu łańcucha
) Koniec fragmentu łańcucha
* Powtórzenie zero lub więcej razy
+ Powtórzenie jeden lub więcej razy
{ Początek minimalnego/maksymalnego kwantyfikatora
} Koniec minimalnego/maksymalnego kwantyfikatora
? Oznaczenie podwzorca jako opcjonalnego
Rozdział 4. Manipulowanie łańcuchami znaków i wyrażenia regularne 137
Znak Znaczenie
\ Znak ucieczki
^ NOT, jeżeli użyte przed wyrażeniem
Sekwencje specjalne
Sekwencje specjalne są częścią wzorca zaczynającą się od znaku lewego ukośnika. Istnieje kilka
kategorii tych sekwencji.
Po pierwsze, znak lewego ukośnika może zostać użyty do oznaczenia jednego z metaznaków, zgod-
nie z informacjami podanymi w dwóch poprzednich punktach rozdziału.
Po drugie, znak lewego ukośnika jest używany jako początek zestawu sekwencji znakowych repre-
zentujących znaki, które nie posiadają reprezentacji graficznej. Kilka takich znaków zostało już
przedstawionych w innych kontekstach; należą do nich między innymi \n (znak nowego wiersza), \r
(znak powrotu karetki) czy też \t (znak tabulacji). Inne popularne sekwencje znakowe to \cx,
reprezentująca kombinację Ctrl+x (gdzie x jest dowolnym znakiem), oraz \e, reprezentująca znak
ucieczki.
I w końcu po trzecie, znak lewego ukośnika może stanowić początek jednego z ogólnych typów
znaków, które zostały przedstawione w tabeli 4.8.
\d Cyfra dziesiętna
\h Odstęp poziomy
\H Wszystko, co nie jest odstępem poziomym
\s Odstęp
\S Dowolny znak niebędący odstępem
\v Odstęp pionowy
Dwa typy znaków wymienione na samym końcu tabeli 4.8 to tak zwane znaki tworzące słowa.
Jednak trzeba zauważyć, że w razie stosowania dopasowywania korzystającego z ustawień lokal-
nych ten typ będzie rozszerzany o znaki charakterystyczne dla danych ustawień lokalnych, na
przykład o znaki diakrytyczne lub znaki z akcentami.
Znak lewego ukośnika ma jeszcze dwa inne zastosowania. Pierwszym z nich są odwołania wsteczne,
a drugim asercje; zostaną one opisane w kolejnych punktach rozdziału.
138 Część I Stosowanie PHP
Odwołania wsteczne
Odwołania wsteczne to coś, przy czym poddają się wszyscy, z wyjątkiem wytrawnych programi-
stów języka Perl. Okazuje się jednak, że nie są one aż tak bardzo złożone.
Odwołania wsteczne we wzorcu są oznaczane znakiem lewego ukośnika oraz cyfrą (może nawet
więcej niż jedną, zależnie od kontekstu). Są one używane do dopasowywania tego samego podwyra-
żenia w więcej niż jednym miejscu łańcucha bez jawnego określania postaci tego podwyrażenia.
Na przykład w poniższym wzorcu:
/^([a-z]+) \1 czarna owca/
Wynika to z tego, że odwołanie wsteczne sprowadza się w zasadzie do stwierdzenia: „znajdź frag-
ment dopasowany do poprzedniego podwyrażenia i dopasuj go w identycznej postaci jeszcze raz”.
Jeśli we wcześniejszej części wyrażenia regularnego pojawiło się więcej podwyrażeń, to są one
numerowane kolejno, zaczynając od 1, i takiej numeracji należy używać w odwołaniach wstecznych.
Asercje
Asercje służą do sprawdzania, czy jakieś znaki w łańcuchu zostały dopasowane bez faktycznego
dopasowywania jakichkolwiek znaków. Zasadę działania asercji trudno jest zrozumieć bez
podania odpowiedniego przykładu. Dostępne asercje rozpoczynające się od znaku lewego ukośni-
ka zostały podane w tabeli 4.9.
Asercja Znaczenie
\b Granica słowa
\B Dowolny znak oprócz granicy słowa
\A Początek tematu
\z Koniec tematu
\Z Koniec tematu lub znak nowego wiersza na końcu
\G Pierwsza dopasowana pozycja w temacie
Granice słów to miejsca, w których znaki tworzące słowa (zdefiniowane w poprzednim punkcie
rozdziału) sąsiadują z innymi znakami.
Asercje początku i końca przypominają nieco metaznaki ^ oraz $, z tą różnicą, że niektóre opcje
konfiguracyjne zmieniają znaczenie tych metaznaków, a działanie asercji \A, \z oraz \Z nigdy
nie ulega zmianie.
Rozdział 4. Manipulowanie łańcuchami znaków i wyrażenia regularne 139
Asercja pierwszej dopasowanej pozycji (\G) jest podobna do asercji początku, lecz używa się jej
w przypadku, gdy dopasowywanie zaczyna się z jakimś przesunięciem, na co pozwalają niektóre
funkcje obsługi wyrażeń regularnych. Na przykład jeśli przesunięcie wynosi 5, a pierwszy fragment
odnaleziony na pozycji 5. jest dopasowaniem, to asercja \G zostanie spełniona, spełnienie asercji \A
będzie natomiast zależeć od tego, co zostanie znalezione na pozycji 1.
Kombinacja \. Oznacza po prostu kropkę (.). Znak kropki został użyty poza klasą znaków, zatem
trzeba go poprzedzić znakiem ucieczki w celu dopasowania go wyłącznie do dosłownego znaku
kropki.
Podwyrażenie [a-zA-Z0-9\-.]+$ pasuje do reszty nazwy domeny — zawiera litery, cyfry, łączniki
oraz, jeżeli to konieczne, więcej kropek, i tak do końca łańcucha.
Nietrudno jednak zauważyć, że niektóre niepoprawne adresy pocztowe nadal pasują do tego wyra-
żenia regularnego. Właściwie nie sposób je wszystkie wyłapać, lecz powyższe wyrażenie poprawi
nieco sytuację. Można je dalej ulepszać na wiele sposobów. Można na przykład podać listę po-
prawnych domen najwyższego poziomu (ang. Top Level Domains — TLD). Wprowadzając
restrykcyjne ograniczenia, trzeba jednak uważać, gdyż funkcja odrzucająca 1% poprawnych danych
może narobić o wiele więcej szkód niż funkcja, która zaakceptuje 10% danych niewłaściwych.
Teraz, po opisaniu wyrażeń regularnych PHP, przedstawimy funkcje PHP z ich zastosowaniem.
140 Część I Stosowanie PHP
Funkcja ta przeszukuje łańcuch znaków szukaj w celu odnalezienia fragmentów pasujących do wy-
rażenia regularnego zawartego w łańcuchu wzorzec. Jeśli uda się dopasować całe wyrażenie regular-
ne, to dopasowany fragment zostanie zapisany jako $dopasowania[0]; natomiast w kolejnych
elementach tej tablicy zostaną zapisane fragmenty łańcucha szukaj, do których zostały dopasowane
kolejne podwyrażenia.
Jedyną możliwą wartością parametru flags jest stała PREG_OFFSET_CAPTURE. W przypadku jej
użycia tablica dopasowania przyjmie inną postać. Każdy jej element będzie tablicą zawierającą
dopasowane podwyrażenie oraz pozycję łańcucha szukaj, na której dopasowany fragment został
znaleziony.
Funkcja preg_match() zwraca wartość 1, jeśli udało się dopasować wyrażenie regularne, 0, jeśli
się to nie udało, oraz FALSE w razie wystąpienia błędu. Oznacza to, że aby uniknąć błędnej interpreta-
cji wyniku — potraktowania wartości 0 i FALSE w taki sam sposób — trzeba będzie zastosować
operator porównania ===.
Rozdzielanie łańcuchów
za pomocą wyrażeń regularnych
Inną użyteczną funkcją wyrażeń regularnych jest split() o następującym prototypie:
array preg_split(string wzorzec, string szukaj [, int limit=-1 [, int flagi=0]]);
Funkcja ta dzieli łańcuch znaków szukaj na fragmenty według wyrażenia regularnego wzorzec,
po czym zwraca te fragmenty w tablicy. Parametr limit ogranicza liczbę fragmentów, jakie zostaną
zapisane w tablicy wynikowej. (Wartość domyślna, -1, oznacza, że liczba zwracanych fragmentów
nie będzie w żaden sposób ograniczana).
Parametr flags może być jedną z poniższych stałych (można też połączyć dwie lub więcej tych
wartości, używając operatora alternatywy bitowej (|)):
PREG_SPLIT_NO_EMPTY — oznacza, że zwracane będą wyłącznie niepuste fragmenty;
PREG_SPLIT_DELIM_CAPTURE — oznacza, że zwracane będą także fragmenty użyte
do podzielenia łańcucha wejściowego na części;
PREG_SPLIT_OFFSET_CAPTURE — oznacza, że będą zwracane również informacje o miejscach
wystąpienia poszczególnych wyznaczonych fragmentów łańcucha wejściowego, podobnie
jak w przypadku użycia funkcji preg_match().
Własności te mogą być użyteczne przy rozdzielaniu adresów poczty elektronicznej, nazw domen
lub dat. Na przykład:
$domena = 'uzytkownik@przyklad.com';
$tab = preg_split("/\.|@/", $domena);
while (list($klucz, $wartosc) = each($tab)) {
echo "<br />".$wartosc;
}
Powyższe wyrażenie rozbija nazwę komputera na trzy części i wyświetla każdą z nich w osobnym
wierszu:
uzytkownik
przyklad
com
142 Część I Stosowanie PHP
Generalnie funkcje wyrażeń regularnych pracują mniej wydajnie niż funkcje łańcuchowe
o podobnych możliwościach funkcjonalnych. Jeżeli dana aplikacja jest wystarczająco prosta,
aby zastosować w niej funkcje łańcuchowe, należy to uczynić. Nie dotyczy to jednak sytuacji,
gdy realizowane przez nią zadania można wykonać przy użyciu jednego wyrażenia regularnego
i większej liczby funkcji do obsługi łańcuchów znaków.
Istnieje ogromny materiał na temat wyrażeń regularnych. Użytkownicy Uniksa mogą rozpocząć od
strony regexp w man.
Zrozumienie wyrażeń regularnych zabiera nieco czasu — im jednak więcej obejrzanych i urucho-
mionych przykładów, tym większa biegłość.
W następnym rozdziale
W kolejnym rozdziale przedstawimy kilka sposobów zastosowania PHP. Dzięki ponownemu
wykorzystaniu istniejącego kodu będziemy mogli zaoszczędzić czas i wysiłek.
Rozdział 5.
Ponowne wykorzystanie
kodu i tworzenie funkcji
W tym rozdziale wyjaśnimy, w jaki sposób ponowne stosowanie istniejącego kodu pozwala uczynić
programy bardziej niezawodnymi, zwięzłymi i łatwiejszymi w utrzymaniu. Przedstawimy techniki
dzielenia na moduły i ponowne stosowanie kodu, począwszy od prostych zastosowań funkcji
require() i include() w celu użycia danego kodu na więcej niż jednej stronie. Wyjaśnimy, dlaczego
takie działania są lepsze niż funkcje Server Side Include. Przykład pomoże zrozumieć stosowanie
łączonych plików dla uzyskania spójnego wyglądu całej strony. Wyjaśnione zostaną także sposoby
tworzenia i wywoływania własnych funkcji, jako przykłady zostaną podane funkcje tworzenia
strony i formularza.
Koszt
Poprzez całe „życie” fragmentu oprogramowania jego utrzymywanie, modyfikowanie, testowanie
i dokumentowanie zajmie znacznie więcej czasu niż jego początkowe utworzenie. Piszący komer-
cyjny kod powinni ograniczać liczbę wierszy kodu stosowanych wewnątrz organizacji. Jedną z naj-
bardziej praktycznych metod osiągnięcia tego celu jest ponowne użycie już stosowanego kodu,
a nie pisanie jego nieco innej wersji dla nowego zadania. Mniejsza ilość kodu oznacza niższe
koszty. Jeżeli istnieje oprogramowanie spełniające wymagania nowego projektu, należy go użyć.
Wydatki związane z nabyciem już istniejącego oprogramowania są prawie zawsze niższe niż koszt
tworzenia równoważnego produktu. Dotyczy to zarówno kupowania produktu, jak i korzystania
z oprogramowania o otwartym kodzie źródłowym. Trzeba jednak wziąć pod uwagę możliwość, że
istniejące oprogramowanie nie spełnia wszystkich wymagań.
Modyfikacja istniejącego kodu niekiedy okazuje się trudniejsza niż napisanie nowego. W przypadku
korzystania z projektu o otwartym kodzie źródłowym warto poszukać takiego, którego architek-
tura opiera się na wtyczkach, gdyż programy tego typu pozwalają na łatwe rozszerzanie możliwości
funkcjonalnych. W przeciwnym razie, jeśli pojawi się konieczność wprowadzenia zmian w sposobie
działania programu, trzeba będzie albo przekazać zmiany do głównego projektu (co jest w takim
przypadku preferowanym rozwiązaniem), albo utrzymywać własną wersję kodu całego projektu
(co nie jest zalecane).
Niezawodność
Jeżeli dany moduł kodu jest już stosowany w jakiejś organizacji, przypuszczalnie został on dokładnie
przetestowany. Nawet jeżeli moduł ten składa się z kilku wierszy, istnieje prawdopodobieństwo,
że w razie napisania go od nowa mogą zostać przeoczone dane dopisane przez autora, na przykład
po znalezieniu błędów. Istniejący kod jest zazwyczaj bardziej niezawodny niż całkiem nowy.
Wyjątkiem od tej reguły są sytuacje, kiedy moduł kodu jest na tyle stary, że można go uznać
za przestarzały. Jeśli chodzi o starsze biblioteki, to czasami ich naturalny rozwój doprowadza
do pojawienia się niepotrzebnego, błędnego kodu. W takich przypadkach warto zastanowić się
nad opracowaniem ich zamiennika, który będzie się nadawał do użycia.
Spójność
Interfejsy zewnętrzne systemu — czy to interfejsy użytkownika, czy dla systemów zewnętrznych
— powinny być spójne. Napisanie nowego kodu spójnego z istniejącymi częściami systemu zabiera
dużo czasu i wysiłku. Zastosowanie działającego już kodu automatycznie zapewnia jego spójność.
Jednak najważniejszą z wymienionych zalet jest to, że ponowne stosowanie kodu oznacza mniejszą
ilość pracy dla autora. Musi jednak zostać spełniony warunek, że istniejący kod jest złożony
z modułów i dobrze napisany. Podczas pracy należy rozpoznawać poszczególne części swojego
kodu, aby można było się do nich odwołać w przyszłości.
Instrukcje te działają podobnie do funkcji Server Side Include serwerów WWW i dyrektywa #include
w C lub C++.
Instrukcje require() i include() są niemal identyczne. Jedyna różnica między nimi polega na tym,
że w przypadku niepowodzenia require() wygeneruje błąd krytyczny, natomiast include() wygene-
ruje tylko ostrzeżenie.
Rysunek 5.1.
Uruchomienie pliku
glowny.php pokazuje
również skrypt
ponownie.php
146 Część I Stosowanie PHP
Aby zastosować instrukcję require(), trzeba mieć plik. W poprzednim przykładzie został zastoso-
wany plik ponownie.php. Po uruchomieniu skryptu instrukcja:
require('ponownie.php');
ulega zamianie na zawartość żądanego pliku, po czym skrypt zostaje wykonany. Oznacza to, że po
uruchomieniu glowny.php będzie pracował tak, jak gdyby został napisany w następujący sposób:
<?php
echo "Oto główny plik.<br />";
echo "Oto bardzo prosta instrukcja PHP.<br />";
echo "Skrypt się zakończy.<br />";
?>
PHP nie zwraca uwagi na rozszerzenie żądanego pliku. Oznacza to, że można dowolnie nazywać
pliki dopóty, dopóki nie zostają one wywołane bezpośrednio. Kiedy do załadowania pliku stosuje
się require(), stanie się on częścią pliku PHP i jako taki zostanie wykonany.
Zazwyczaj instrukcje PHP nie zostałyby przetworzone, gdyby znajdowały się w pliku o nazwie
na przykład strona.html. PHP zwykle pracuje jedynie z plikami o określonych rozszerzeniach,
takich jak .php. (Ustawienie to można zmienić w pliku konfiguracyjnym serwera WWW). Jednak
załadowanie strony strona.html poprzez require() spowoduje, że cały PHP wewnątrz niej zostanie
wykonany. Tak więc pliki ponownie używane mogą mieć dowolne rozszerzenie, lecz z powodów
wymienionych poniżej dobrze jest konsekwentnie stosować rozszerzenie .php.
Przede wszystkim trzeba wziąć pod uwagę to, że jeżeli pliki o rozszerzeniu .inc (bądź innym
niestandardowym) przechowywane są w katalogu dokumentów WWW i użytkownik załaduje
je bezpośrednio do przeglądarki, będzie on mógł zobaczyć cały kod jako zwykły tekst, w tym także
wszystkie hasła. Tak więc ważne jest przechowywanie ponownie użytych plików w katalogu poza
drzewem katalogów serwera WWW lub używanie standardowych rozszerzeń.
Kod PHP w pliku znalazł się wewnątrz znaczników PHP. Konwencja ta jest konieczna, aby w żą-
danym pliku został potraktowany jako kod PHP. Jeżeli znajdzie się poza znacznikami, zostanie
ujęty jako tekst lub HTML i nie zostanie wykonany.
Na przykład witryna WWW fikcyjnej firmy TLA Consulting posiada pewną liczbę stron o wyglą-
dzie przedstawionym na rysunku 5.2. Kiedy potrzebna jest nowa strona, programista może otworzyć
już istniejącą, wyciąć niepotrzebne fragmenty tekstu, wprowadzić nowy tekst i zapisać plik pod
nową nazwą.
Rozdział 5. Ponowne wykorzystanie kodu i tworzenie funkcji 147
Rysunek 5.2.
TLA Consulting posiada
standardowy wygląd
wszystkich swoich
stron WWW
Przeanalizujmy następujący scenariusz. Witryna WWW istnieje już od pewnego czasu, a w niej
dziesiątki, setki lub nawet tysiące stron o podobnym stylu. Zapada decyzja o zmianie standardowego
wyglądu — może to być niewielka modyfikacja, jak dodanie adresu poczty elektronicznej w stopce
każdej strony lub pojedynczej opcji w menu nawigacyjnym. Czy ma sens ręczna zmiana dziesiątek,
setek, a może nawet tysięcy stron?
Bezpośrednie ponowne stosowanie fragmentów HTML-a wspólnych dla wszystkich stron jest
dużo lepszym rozwiązaniem niż wycinanie i wklejanie w dziesiątkach, setkach, a nawet tysiącach
stron. Kod źródłowy strony głównej (glowna.html), widocznej na rysunku 5.2, został przedstawiony
na listingu 5.1.
Listing 5.1. glowna.html — kod HTML tworzący stronę główną firmy TLA Consulting
<!DOCTYPE html>
<html>
<head>
<title>TLA CONSULTING</title>
<link href="styles.css" type="text/css" rel="stylesheet">
</head>
<body>
<div class="menuitem">
<a href="contact.html">
<img src="s-logo.gif" alt="" height="20" width="20" />
<span class="menutext">Kontakt</span>
</a>
</div>
148 Część I Stosowanie PHP
<div class="menuitem">
<a href="services.html">
<img src="s-logo.gif" alt="" height="20" width="20" />
<span class="menutext">Usługi</span>
</a>
</div>
<div class="menuitem">
<a href="about.html">
<img src="s-logo.gif" alt="" height="20" width="20" />
<span class="menutext">O nas</span>
</a>
</div>
</nav>
</body>
</html>
Jak pokazano na listingu 5.1, plik ten posiada kilka wyraźnych części kodu. Nagłówek strony
— element <head> — zawiera odwołanie do pliku kaskadowych arkuszy stylów (Cascading Style
Sheet — CSS), wykorzystywanych przez stronę. Część o nazwie naglowek wyświetla nazwę i logo
firmy, menu tworzy pasek nawigacyjny, a zawartosc to tekst właściwy dla strony. Poniżej znajduje
się stopka strony. Możliwe jest przydatne rozdzielenie tego pliku i nazwanie części naglowek.php,
glowna.php i stopka.php. Zarówno naglowek.php, jak i stopka.php zawierają kod, który zostanie
ponownie wykorzystany na innych stronach.
Plik glowna.php to zamiennik pliku glowna.html, zawierający właściwą zawartość strony oraz
dwie instrukcje require(), jak przedstawiono na listingu 5.2.
Jak wspomniano wcześniej, nazwy nadane plikom nie wpływają na sposób ich przetwarzania
po wywołaniu przez require(). Zwyczajową konwencją jest nazywanie plików częściowych,
które zostaną umieszczone w innych plik.inc (gdzie inc znaczy include — „dołącz”). Ogólnie
rzecz biorąc, stosowanie takiej konwencji nie jest zalecane, ponieważ domyślnie pliki z rozszerze-
niem .inc nie będą interpretowane jako pliki z kodem PHP, dopóki nie skonfiguruje się w odpowied-
ni sposób serwera WWW.
Jeżeli jednak przytoczoną konwencję planuje się zastosować, powszechnym i użytecznym pomy-
słem jest wówczas umieszczanie plików częściowych w katalogu widzianym przez skrypty, ale
niepozwalającym na ładowanie poszczególnych plików przez serwer WWW, czyli w katalogu
umieszczonym poza drzewem katalogów serwera WWW. Uniemożliwia to bezpośrednie ładowa-
nie poszczególnych plików, co prawdopodobnie:
1. dałoby błędne wykonanie plików oznaczonych jako .php, lecz zawierających jedynie
część strony bądź skryptu lub
2. pozwoliło użytkownikom na odczytanie kodu źródłowego plików o innych rozszerzeniach.
Plik naglowek.php zawiera definicje CSS używane przez tę stronę, tabele wyświetlające nazwę
firmy oraz menu nawigacyjne, jak to zostało przedstawione na listingu 5.3.
Listing 5.3. naglowek.php — stosowany wielokrotnie nagłówek wszystkich stron WWW TLA
<!DOCTYPE html>
<html>
<head>
<title>TLA CONSULTING</title>
<link href="styles.css" type="text/css" rel="stylesheet">
</head>
<body>
<div class="menuitem">
<a href="contact.html">
<img src="s-logo.gif" alt="" height="20" width="20" />
<span class="menutext">Kontakt</span>
</a>
</div>
<div class="menuitem">
<a href="services.html">
<img src="s-logo.gif" alt="" height="20" width="20" />
<span class="menutext">Usługi</span>
</a>
</div>
<div class="menuitem">
<a href="about.html">
150 Część I Stosowanie PHP
Plik stopka.php zawiera tabelę wyświetlającą stopkę na dole każdej strony. Plik ten jest przedsta-
wiony na listingu 5.4.
Listing 5.4. stopka.php — Stosowana wielokrotnie stopka dla wszystkich stron WWW TLA
<!-- stopka strony -->
<footer>
<p>© TLA Consulting<br />
Prosimy odwiedzić <a href ="prawne.php">stronę informacji prawnych</a>.</p>
</footer>
</body>
</html>
Takie ujęcie pozwala łatwo uzyskać spójnie wyglądające strony WWW. Nowa strona w tym stylu
może być utworzona na przykład następująco:
<? require('naglowek.php'); ?>
Oto zawartość tej strony
<? require('stopka.php'); ?>
Co najważniejsze, nawet po utworzeniu za pomocą tego nagłówka i stopki wielu stron WWW
łatwo jest zmienić pliki nagłówka i stopki. Niezależnie od tego, czy jest to drobna modyfikacja
w tekście, czy też całkowite przeprojektowanie wyglądu witryny, zmiany należy dokonać tylko
jednym miejscu. Nie trzeba oddzielnie zmieniać każdej strony w witrynie, ponieważ każda strona
ładuje ten sam nagłówek i stopkę.
W powyższym przykładzie użyto czystego kodu HTML w zawartości, nagłówku i stopce. Nie
jest to wymogiem. W plikach tych można stosować instrukcje PHP w celu dynamicznego genero-
wania części strony.
Aby mieć pewność, że plik rzeczywiście będzie traktowany jak zwykły tekst lub HTML, a nie
zostanie wykonany przez PHP, można zastosować funkcję readfile(). Wyświetla ona zawartość
pliku bez parsowania go. Rozwiązanie takie może być niezbędne z punktu widzenia bezpieczeń-
stwa, jeśli przetwarzany jest tekst wpisany przez użytkownika.
Stosując te dyrektywy, nie trzeba wpisywać instrukcji require(), ale nagłówki i stopki nie będą
już opcją dla strony.
Korzystający z serwera WWW Apache mogą zmieniać podobne do powyższych opcje konfigura-
cyjne oddzielnie dla każdego katalogu. Aby użytkownicy mogli to uczynić, serwer musi być usta-
wiony tak, aby pozwalał na ominięcie głównych plików konfiguracyjnych. W celu ustawienia
w katalogu automatycznego dołączania na początku i końcu plików należy w nim utworzyć
plik o nazwie .htaccess. Musi on zawierać dwa następujące wiersze:
php_value auto_prepend_file "/sciezka/do/pliku/naglowek.php"
php_value auto_append_file "/sciezka/do/pliku/stopka.php"
Należy zauważyć drobne różnice w składni między powyższymi opcjami a ich odpowiednikami
w pliku php.ini: nie dość, że na początku wiersza umieszczona jest fraza php_value, to jeszcze nie
ma w nim znaku równości. Istnieje także kilka innych ustawień konfiguracyjnych php.ini, które
można zmieniać w ten sposób.
Ustawianie opcji w pliku .htaccess zamiast w php.ini lub w plikach konfiguracyjnych serwera
daje dużą elastyczność. Można zmieniać ustawienia współdzielonego serwera, które obejmują
tylko własne katalogi. Nie trzeba restartować serwera WWW ani mieć uprawnień administratora.
Wadą metody .htaccess jest to, że pliki konfiguracyjne muszą być odczytywane i analizowane przy
każdym otwarciu pliku w danym katalogu, a nie raz na starcie, co daje nieco niższą wydajność.
Pod względem formalnym funkcja jest kompletnym modułem kodu, który opisuje interfejs wywo-
łania, wykonuje jakieś zadanie i opcjonalnie zwraca wartość.
Wywoływanie funkcji
Poniższe wiersze to najprostsze możliwe wywołanie funkcji:
nazwa_funkcji();
Wywołuje ona funkcję o nazwie nazwa_funkcji, która nie wymaga parametrów. Kod ten ignoruje
również wszystkie wartości, które mogą być przez funkcję zwrócone.
152 Część I Stosowanie PHP
Część funkcji jest wywoływana w dokładnie taki sposób. Funkcja phpinfo() okazuje się często
przydatna w różnego rodzaju testach, ponieważ wyświetla ona: informacje o zainstalowanej wersji
PHP i o PHP, ustawienia serwera WWW oraz wartości różnych zmiennych PHP i serwera. Choć
funkcja phpinfo() ma parametr wywołania i zwraca wartość, to jednak rzadko kiedy są one stoso-
wane; dlatego też zazwyczaj jej wywołanie ma następującą postać:
phpinfo();
Jednak większość funkcji wymaga jednego lub więcej parametrów. Parametry przekazuje się przez
umieszczenie danych lub nazwy zmiennej je przechowującej w nawiasach po nazwie funkcji.
Wywołanie funkcji z parametrem wygląda następująco:
nazwa_funkcji('parametr');
W tym przypadku zastosowanym parametrem był łańcuch znaków zawierający jedynie słowo
parametr, lecz poniższe wywołania są również prawidłowe, zależnie od tego, jakich parametrów
funkcja oczekuje:
nazwa_funkcji(2);
nazwa_funkcji(7.993);
nazwa_funkcji($zmienna);
Występująca w ostatnim wierszu $zmienna może być zmienną należącą do dowolnego typu w PHP,
w tym tablicą, obiektem, a nawet inną funkcją.
Parametr jest informacją dowolnego typu, lecz poszczególne funkcje zazwyczaj wymagają konkret-
nego typu danych.
Na podstawie prototypu funkcji można poznać liczbę parametrów funkcji, co poszczególne z nich
przedstawiają i jakiego typu danych potrzebują. W tej książce prototypy często są ukazywane
przy opisywaniu funkcji.
Na podstawie prototypu można dowiedzieć się kilku informacji i ważna jest prawidłowa interpre-
tacja tej specyfikacji. W tym przypadku słowo resource przed nazwą funkcji sygnalizuje, że zwraca
ona zasób resource (to znaczy uchwyt otwartego pliku). Parametry funkcji znajdują się w nawia-
sach. W przypadku funkcji fopen() w prototypie przedstawione są cztery parametry. Parametry
nazwapliku i tryb są łańcuchami znaków, parametr uzycie_sciezki ma wartość logiczną, a kontekst
jest zasobem. Nawiasy kwadratowe wokół uzycie_sciezki i kontekst wskazują, że są to parame-
try opcjonalne.
Można zatem podać ich wartości albo je zignorować — w tym drugim przypadku użyte zostaną ich
domyślne wartości. Należy jednak pamiętać, że w przypadku funkcji z więcej niż jednym parame-
trem opcjonalnym parametry można ignorować tylko od prawej strony. Na przykład używając
funkcji fopen(), można pozostawić pusty parametr kontekst albo zignorować od razu obydwa
parametry uzycie_sciezki i kontekst. Nie można natomiast zignorować parametru uzycie_sciezki,
a podać wartość parametru kontekst.
Po przeczytaniu prototypu tej funkcji wiadomo, że poniższy fragment kodu prawidłowo wywoła
fopen():
$nazwa = 'mojplik.txt';
$trybotwarcia = 'r';
$wp = fopen($nazwa, $trybotwarcia);
Rozdział 5. Ponowne wykorzystanie kodu i tworzenie funkcji 153
Powyższy kod wywołuje funkcję o nazwie fopen(). Wartość zwrócona przez nią zostanie prze-
chowana w zmiennej $wp. W tym przypadku funkcji została przekazana zmienna $nazwa zawiera-
jąca łańcuch znaków. Łańcuch ten zawiera z kolei nazwę pliku, który ma zostać otwarty. Funkcji
tej została również przekazana zmienna $trybotwarcia, zawierająca łańcuch znaków przedsta-
wiający tryb, w którym plik ma zostać otwarty. Parametry opcjonalne nie zostały podane.
Rysunek 5.3.
Wiadomość o błędzie
jest wynikiem wywołania
nieistniejącej funkcji
Wiadomości o błędach podawane przez PHP są zazwyczaj bardzo przydatne. Wiadomość przed-
stawiona na rysunku powyżej wskazuje dokładną nazwę pliku, w którym wystąpił błąd, numer
wiersza oraz nazwę funkcji. Informacja ta powinna pomóc w odnalezieniu i naprawieniu błędu.
Nie zawsze łatwo zapamiętać dokładną nazwę funkcji. Na przykład niektóre funkcje o nazwach
dwuwyrazowych posiadają znak podkreślenia między elementami nazwy, a niektóre nie.
Funkcja stripslashes() łączy dwa słowa w jedno, podczas gdy strip_tags() oddziela je znakiem
podkreślenia. Złe przeliterowanie nazwy funkcji podczas jej wywoływania daje w wyniku błąd
przedstawiony na rysunku 5.3. Niespójne nazewnictwo funkcji jest jednym z dziwactw PHP.
Wynika ono z tego, że nazwy wielu funkcji PHP odpowiadają nazwom funkcji w używanych
bibliotekach języka C, które także są bardzo niespójne. Niemniej jednak wszystkie te rozbieżności
mogą być bardzo denerwujące.
Niektóre funkcje występujące w tej książce nie istnieją w starszych wersjach języka PHP, ponie-
waż założono, że obecnie stosuje się przynajmniej wersję 5.5. W kolejnych wersjach definiowa-
ne są nowe funkcje i dlatego używającym starszych zaleca się uaktualnienie, chociażby z powodu
zwiększonych możliwości funkcjonalnych i większej wydajności. Aby dowiedzieć się, kiedy została
dodana określona funkcja, należy sięgnąć po podręcznik elektroniczny dostępny pod adresem
www.php.net/array.
154 Część I Stosowanie PHP
Próba wywołania funkcji niezadeklarowanej w stosowanej wersji PHP da taki wynik, jak pokazany
na rysunku 5.3. Kolejnym powodem pojawienia się takiego komunikatu o błędzie może być to,
że wywoływana funkcja jest częścią rozszerzenia PHP, które nie zostało załadowane. Na przykład
jeżeli spróbujemy użyć funkcji z biblioteki gd (pozwalającej na manipulowanie obrazkami), która
nie została wcześniej zainstalowana, na ekranie wyświetlony zostanie właśnie taki komunikat.
Należy pamiętać, że nazwy funkcji zachowują się w inny sposób niż nazwy zmiennych. W nazwach
zmiennych zwraca się uwagę na wielkość liter, tak więc $Nazwa i $nazwa to dwie różne zmienne,
ale nazwa() i Nazwa() to ta sama funkcja.
Funkcje wbudowane w PHP pozwalają na interakcję z plikami, używanie baz danych, tworzenie
grafiki i łączenie się z innymi serwerami. Jednak w karierze każdego programisty jest wiele sytuacji,
których twórcy języka nie przewidzieli.
Na szczęście programista nie musi ograniczać się do stosowania funkcji wbudowanych, ponieważ
może tworzyć własne funkcje wykonujące dowolne zadania. Większość kodów to mieszanka funkcji
już istniejących i tworzonych przez użytkownika. Jeżeli tworzy się blok kodu wykonujący zadanie,
które będzie ponownie wykorzystane w innych miejscach skryptu lub w innych skryptach, powinno
się zdeklarować ten blok jako funkcję.
Deklarowanie funkcji pozwala na stosowanie własnego kodu w taki sam sposób jak funkcji wbudo-
wanych. Należy po prostu wywołać własną funkcję i dostarczyć potrzebne jej parametry. Oznacza
to, że dana funkcja może być wywoływana i wykorzystywana wiele razy w jednym skrypcie.
Powyższa deklaracja funkcji rozpoczyna się słowem function, aby czytelnicy i analizator skła-
dniowy PHP wiedzieli, że następne dane będą zdefiniowaną przez użytkownika funkcją o nazwie
moja_funkcja. Wywołuje się ją za pomocą następującego wyrażenia:
moja_funkcja();
Jak łatwo zgadnąć, wynikiem wywołania funkcji jest wyświetlenie w oknie przeglądarki tekstu:
Moja funkcja została wywołana.
Funkcje wbudowane są dostępne dla wszystkich skryptów PHP, ale funkcje zdeklarowane —
jedynie dla skryptów, w których zostały zdeklarowane. Dobrym pomysłem jest posiadanie pliku
lub zbioru plików zawierających własne funkcje. Można wtedy zastosować we wszystkich
skryptach instrukcję require(), aby funkcje stały się dostępne tam, gdzie będzie to konieczne.
Wewnątrz funkcji nawiasy klamrowe zawierają kod wykonujący żądane zadanie. Pomiędzy na-
wiasami można umieścić wszystko, co jest prawidłowe dla każdego innego miejsca w skrypcie
PHP, w tym: wywołania funkcji, deklaracje nowych zmiennych i funkcji, instrukcji require()
i include(), deklaracje klas oraz czysty HTML. Aby zakończyć skrypt i pisać zwykły HTML,
należy zastosować tę samą metodę, jak w innych miejscach skryptu — zamykający znacznik PHP,
a następnie HTML. Następujący fragment kodu jest prawidłową modyfikacją poprzedniego przy-
kładu o identycznym wyniku:
<?php
function moja_funkcja() {
?>
Moja funkcja została wywołana
<?php
}
?>
Należy zauważyć, że kod PHP jest zawarty pomiędzy pasującymi do siebie otwierającymi i zamy-
kającymi znacznikami PHP. W większości krótkich przykładowych fragmentów kodu, przed-
stawionych w tej książce, znaczniki te nie są pokazywane. Tutaj występują, ponieważ są konieczne
w przykładzie oraz przed i po nim.
Wiele języków programowania umożliwia ponowne stosowanie nazw funkcji. Własność ta nazywa-
na jest przeciążaniem funkcji (ang. function overloading). PHP nie pozwala jednak na przeciążanie
funkcji, tak więc funkcja nie może mieć nazwy takiej jak którakolwiek z funkcji wbudowanych
156 Część I Stosowanie PHP
lub wcześniej zdefiniowanych. (Pewna forma przeciążania jest dostępna w klasach, które zosta-
ną opisane w rozdziale 6., „Obiektowy PHP”, niemniej jednak różni się ona od formy przeciążania
stosowanej w innych językach programowania). Należy również zwrócić uwagę na to, że cho-
ciaż każdy skrypt PHP zna wszystkie funkcje wbudowane dostępne w zasięgu globalnym, to funkcje
zdefiniowane przez użytkownika istnieją jedynie w skryptach, w których zostały zdeklarowane.
Oznacza to, że nazwa funkcji może zostać ponownie użyta w innym pliku. Działania te prowadzą
jednak do pomyłek i powinno się ich unikać.
(Ostatnia z nazw funkcji byłaby prawidłowa, gdyby taka funkcja nie istniała wcześniej).
Należy zwrócić uwagę, że choć $nazwa nie jest prawidłową nazwą funkcji, wywołanie funkcji
postaci
$nazwa();
może zostać wykonane zależnie od wartości zmiennej $nazwa. Wynika to z faktu, że PHP pobiera
wartość przechowywaną w $nazwa, odszukuje funkcję o właśnie takiej nazwie i próbuje ją wywołać.
Funkcja tego typu jest nazywana funkcją zmienną.
Parametry
Aby wykonywać swoją pracę, większość funkcji potrzebuje jednego parametru lub większej ich
liczby. Parametr pozwala na przekazywanie funkcji danych. Poniżej znajduje się przykład funkcji
wymagającej parametru, która pobiera tablicę jednowymiarową i wyświetla ją jako tabelę.
function stworz_tabele($dane)
{
echo '<table>';
reset($dane);
$wartosc = current($dane);
while ($wartosc) {
echo "<tr><td>$wartosc</td></tr>\n";
$wartosc = next($dane);
}
echo '</table>';
}
Przekazanie parametru pozwoliło na pobranie danych stworzonych poza funkcją — w tym przy-
padku tablicy $moja_tablica — do użytku wewnątrz funkcji.
Rozdział 5. Ponowne wykorzystanie kodu i tworzenie funkcji 157
Rysunek 5.4.
Wywołanie funkcji
stworz_tabele()
generuje
taką tabelę HTML
Podobnie jak funkcje wbudowane, funkcje zdefiniowane przez użytkownika mogą mieć wiele
parametrów, w tym parametry opcjonalne. Funkcję stworz_tabele() można usprawnić na wiele
sposobów; jednym z nich jest umożliwienie użytkownikowi określenia innych atrybutów tabeli.
Poniżej została przedstawiona ulepszona wersja funkcji, wprawdzie bardzo podobna, lecz pozwala-
jąca na opcjonalne ustawienie nagłówka oraz tytułu tabeli.
function stworz_tabele($dane, $naglowek=NULL, $tytul=NULL)
{
echo '<table>';
if ($caption) {
echo "<caption>$tytul</caption>";
}
if ($header) {
echo "<tr><th>$naglowek</th></tr>";
}
reset($data);
$value = current($data);
while ($value) {
echo "<tr><td>$value</td></tr>\n";
$value = next($data);
}
echo '</table>';
}
Pierwszy parametr funkcji stworz_tabele() jest ciągle wymagany. Następne dwa są opcjonalne,
ponieważ zostały dla nich zdefiniowane wartości domyślne. Wynik bardzo podobny do rysunku 5.4
można uzyskać za pomocą poniższego odwołania do stworz_tabele():
stworz_tabele($moja_tablica);
Jeżeli te same dane należy wyświetlić w tablicy bardziej rozciągniętej, można wywołać nową funk-
cję w następujący sposób:
stworz_tabele($moje_dane, 'Nagłówek tabelki');
Wartości opcjonalne nie muszą być w ogóle podawane — można na przykład wskazać część z nich.
Parametry są przypisywane od lewej do prawej strony.
158 Część I Stosowanie PHP
Należy pamiętać, że nie można nie wpisać jednego z parametrów opcjonalnych, ograniczając się
jedynie do późniejszego. W tym przykładzie aby przekazać wartość caption, trzeba podać również
header. Jest to częsty powód błędów w programowaniu. Dlatego też parametry opcjonalne są
umieszczane na końcu listy parametrów.
jest całkowicie prawidłowe i w jego wyniku zmienna $naglowek przyjmie wartość 'Chciałbym,
by to był tytuł tabelki', a zmienna $tytul przyjmie swoją wartość domyślną, czyli NULL.
Można jednak przekazać jako nagłówek wartość NULL, jak w poniższym przykładzie:
stworz_tabele($moje_dane, NULL, 'Chciałbym, by to był tytuł tabelki');
Można również deklarować funkcje przyjmujące zmienną liczbę parametrów. Liczbę parametrów
przekazanych do funkcji oraz ich wartości można sprawdzić, używając trzech funkcji pomocni-
czych: func_num_args(), func_get_arg() i func_get_args().
Powyższa funkcja zwraca liczbę przekazanych do niej parametrów i wyświetla każdy z nich.
Funkcja func_num_args() zwraca liczbę przekazanych argumentów. Funkcja func_get_args()
zwraca tablicę tych argumentów. Alternatywnie można uzyskać dostęp do argumentów jeden po
drugim, również używając funkcji func_get_arg(), lecz przekazując do niej numer argumentów,
jaki ma zostać zwrócony. (Argumenty są numerowane od zera).
Zasięg
Jak można było wcześniej zauważyć, kiedy konieczne było użycie zmiennych w dołączanym
pliku, należało je po prostu zadeklarować w skrypcie przed instrukcjami require() bądź include().
Jednakże przy stosowaniu funkcji po prostu przekazywaliśmy im zmienne. Dzieje się tak m.in.
dlatego, że nie istnieje mechanizm bezpośredniego przekazywania zmiennych dołączanym plikom,
a także z tego powodu, że zasięg zmiennych zachowuje się w inny sposób w przypadku funkcji.
Zasięg zmiennej kontroluje miejsca, w których zmienna ta jest widzialna i zdatna do użytku. W róż-
nych językach programowania obowiązują odmienne zasady dotyczące zasięgów zmiennych.
W PHP są one raczej proste:
Zmienne zadeklarowane wewnątrz funkcji posiadają zasięg od miejsca, w którym zostały
zadeklarowane, do końca funkcji. Nazywa się to zasięgiem funkcji, a zmienne są nazywane
zmiennymi lokalnymi.
Rozdział 5. Ponowne wykorzystanie kodu i tworzenie funkcji 159
Przedstawiony niżej kod nie daje żadnego wyniku. Zadeklarowana tu została zmienna o nazwie
$zmienna wewnątrz funkcji fn(). Ponieważ zmienna ta została zadeklarowana wewnątrz funkcji,
posiada zasięg funkcji, a zatem istnieje jedynie od miejsca, w którym została zadeklarowana,
czyli do końca funkcji. Kiedy ponownie odwołujemy się do $zmienna poza funkcją, tworzona jest
nowa zmienna $zmienna. Posiada ona zasięg globalny i będzie widoczna do końca pliku. Niestety,
jeżeli jedynym wyrażeniem zastosowanym do nowej $zmienna jest echo, nigdy nie będzie ona
posiadać wartości.
function fn() {
$zmienna = "zawartość";
}
fn();
echo $zmienna;
Funkcje nie są wykonywane do czasu ich wywołania, tak więc pierwsza wykonana instrukcja to
$zmienna = 1;. Tworzy ona zmienną o nazwie $zmienna, o zasięgu globalnym i zawartości 1. Na-
stępna wykonana instrukcja to wywołanie funkcji fn(). Wiersze kodu wewnątrz funkcji wykony-
wane są po kolei. Pierwszy wiersz kodu wewnątrz funkcji odnosi się do zmiennej $zmienna. Kiedy
wiersz ten jest wykonywany, funkcja tworzy w swoim zasięgu nową zmienną o nazwie $zmienna
i wyświetla ją. W ten sposób zostaje utworzony pierwszy wiersz wyniku.
160 Część I Stosowanie PHP
Następny wiersz kodu wewnątrz funkcji ustawia zawartość $zmienna na 2. Ponieważ dzieje się to
wewnątrz funkcji, wiersz ten zmienia wartość lokalnej, a nie globalnej wersji $zmienna. Drugi
wiersz wyniku pokazuje, że modyfikacja się powiodła.
Funkcja kończy się w tym momencie, tak więc zostaje wykonany ostatni wiersz skryptu. Ta instruk-
cja echo pokazuje, że zawartość zmiennej globalnej nie uległa zmianie.
Jeżeli zmienna utworzona wewnątrz funkcji ma posiadać zasięg globalny, można zastosować słowo
kluczowe global w następujący sposób:
function fn() {
global $zmienna;
$zmienna='zawartość';
echo 'Wewnątrz funkcji, $zmienna = '.$zmienna.'<br />';
}
fn();
echo 'Na zewnątrz funkcji, $zmienna = '.$zmienna;
W tym przykładzie zmienna $zmienna została bezpośrednio zdefiniowana jako globalna, czyli
po wywołaniu funkcji będzie widoczna również poza nią. Wynik powyższego skryptu jest
następujący:
Wewnątrz funkcji, $zmienna = zawartość
Na zewnątrz funkcji, $zmienna = zawartość
Należy zauważyć, że zmienna posiada zasięg od momentu wykonania wiersza global $zmienna;.
Funkcja mogła być zadeklarowana poniżej lub powyżej jej wywołania. (Warto zauważyć, że zasięg
funkcji jest różny od zasięgu zmiennej). Miejsce deklaracji funkcji nie ma znaczenia; ważne jest
miejsce wywołania funkcji, a więc wykonania kodu znajdującego się wewnątrz niej.
Słowa kluczowego global można również użyć na początku skryptu, kiedy zmienna jest po raz
pierwszy wywołana. W ten sposób zostaje złożona deklaracja stosowania jej w całym skrypcie.
Jest to prawdopodobnie częstsze zastosowanie słowa kluczowego global.
Z powyższych przykładów wynika, że całkowicie prawidłowe jest ponowne użycie tej samej
nazwy dla zmiennej wewnątrz i na zewnątrz funkcji bez zakłóceń pomiędzy nimi. Rozwiązanie to
ma jednak poważne wady, ponieważ bez dokładnego przeczytania kodu i przemyślenia zasięgów
inni użytkownicy mogą sądzić, że jest to jedna i ta sama zmienna.
Powyższy kod jest całkowicie bezużyteczny. Przy takiej definicji poniższy kod da błędny wynik 10.
$wartosc = 10;
powieksz ($wartosc);
echo $wartosc;
Rozdział 5. Ponowne wykorzystanie kodu i tworzenie funkcji 161
Zawartość zmiennej $wartosc nie zmieniła się. Dzieje się tak z powodu zasad zasięgów. Powyższy
kod tworzy zmienną o nazwie $wartosc, która zawiera 10, a następnie wywołuje funkcję powieksz().
Zmienna $wartosc w funkcji jest tworzona przy jej wywołaniu. Zostaje do niej dodane 1, tak więc
wartość zmiennej $wartosc wynosi 11 wewnątrz funkcji, dopóki funkcja nie zakończy się i nastąpi
powrót do wywołującego ją kodu. W tym kodzie zmienna $wartosc jest inną zmienną, o zasięgu
globalnym, w związku z czym pozostaje niezmieniona.
Zwykły sposób wywołania parametrów funkcji nosi nazwę przekazania przez wartość. Kiedy
przekazuje się parametr, utworzona zostaje nowa zmienna zawierająca podaną wartość. Jest ona
kopią oryginału. Można ją dowolnie modyfikować, lecz wartość oryginalnej zmiennej poza funkcją
pozostaje niezmieniona. (Jest to tak naprawdę dość uproszczony opis czynności, które PHP wyko-
nuje wewnętrznie).
Jeśli niezbędne jest zmodyfikowanie wartości wewnątrz funkcji, to konieczne będzie zastosowa-
nie przekazania przez referencję. W tym przypadku, kiedy zmiennej przekazywany jest parametr,
funkcja — zamiast tworzyć nową zmienną — otrzymuje referencję (odwołanie) do oryginalnej.
Referencja ta posiada nazwę zmiennej, rozpoczynającą się od znaku dolara, i może być stosowana
w dokładnie taki sam sposób jak inna zmienna. Różnica polega na tym, że nie posiada własnej war-
tości, lecz odwołuje się do oryginału. Każda modyfikacja referencji dotyczy również oryginału.
Parametr wykorzystujący przekazanie przez referencję oznacza się poprzez umieszczenie przed
jego nazwą w definicji funkcji znaku &. Nie istnieje konieczność zmiany wywołania funkcji.
Powyższy przykład funkcji powieksz() może być zmodyfikowany tak, aby posiadał jeden parametr
przekazywany z odwołaniem i aby nadal pracował poprawnie.
function powieksz(&$wartosc, $wielkosc = 1) {
$wartosc = $wartosc + $wielkosc;
}
Powyższa funkcja naprawdę działa, a użytkownik może dowolnie nazywać zmienną przeznaczoną do
powiększenia. Jak wspomniano powyżej, z powodu niedoskonałości umysłów ludzkich niekiedy
występują problemy ze współpracą ze zmiennymi o tych samych nazwach wewnątrz i na zewnątrz
funkcji, tak więc zmiennej w głównym skrypcie zostanie nadana nowa nazwa. Następujący kod
testowy wyświetli teraz wartość 10 przed wywołaniem funkcji powieksz() i 11 po wywołaniu:
$a = 10;
echo $a.'<br />';
powieksz($a);
echo $a.'<br />';
return;
echo "Ta instrukcja nigdy nie zostanie wykonana";
}
Oczywiście nie jest to zbyt użyteczne zastosowanie return. Zazwyczaj powroty z funkcji dokonują
się po spełnieniu pewnego warunku.
Warunek błędu to częsty powód użycia instrukcji return do przerwania wykonywania funkcji
przed jej właściwym końcem. Jeżeli na przykład funkcja ma za zadanie znajdować większą liczbę
z dwóch, przerwanie może nastąpić, jeśli któraś z liczb nie istnieje.
function wiekszy($x,$y) {
if ((!isset($x)) || (!isset($y))) {
echo "Ta funkcja wymaga dwóch liczb";
return;
}
if ($x>=$y) {
echo $x."<br />";
} else {
echo $y."<br />";
}
}
Wbudowana funkcja isset() pokazuje, czy dana zmienna została utworzona i obdarzona war-
tością. W powyższym kodzie wiadomość o błędzie pojawia się, jeżeli któryś z parametrów nie
ma nadanej wartości. Sprawdzenie tego dokonuje się za pomocą wyrażenia !isset(), to znaczy
„NIE isset()”, tak więc całe wyrażenie może być rozumiane jako „jeżeli x nie jest ustawione
bądź y nie jest ustawione”. Funkcja zakończy działanie, jeśli którykolwiek z tych warunków okaże
się prawdziwy.
Jeżeli instrukcja return zostanie wykonana, kolejne wiersze kodu funkcji zostaną zignorowane.
Wykonywanie programu powróci do punktu, w którym nastąpiło wywołanie funkcji. Jeżeli oba
parametry są ustawione, funkcja wyświetli większy z nich.
jest następujący:
2.5
1.9
Ta funkcja wymaga dwóch liczb
W powyższym kodzie zwrócona zostaje większa z dwóch wartości przekazanych funkcji. W przy-
padku błędu powinna zostać zwrócona zupełnie inna liczba. Jeżeli brakuje jednej z liczb, zwró-
cona zostanie wartość false. (Stosując takie podejście, należy tylko pamiętać, że programista
wywołujący tę funkcję musi sprawdzać typ zwracanej wartości przy użyciu operatora === aby
uzyskać pewność, że wartość false nie zostanie pomylona z 0).
Dla porównania: wbudowana funkcja max() nie zwraca nic, jeżeli brakuje obu zmiennych, a jeżeli
tylko jedna była ustawiona, zwraca tę jedną.
Poniższy kod:
$a = 1;
$b = 2.5;
$c = 1.9;
$d = NULL;
echo wiekszy($a, $b).'<br />';
echo wiekszy($c, $a).'<br />';
echo wiekszy($d, $a).'<br />';
Funkcje wykonujące pewne zadanie, lecz bez konieczności zwracania wartości, często zwracają
true lub false, aby wskazać, czy ich działanie się powiodło.
Implementacja rekurencji
PHP obsługuje funkcje rekurencyjne. Funkcja rekurencyjna to taka, która wywołuje samą siebie.
Funkcje te są szczególnie przydatne przy nawigacji w dynamicznych strukturach danych, takich
jak połączone listy bądź drzewa.
Mimo to niewiele aplikacji opartych na WWW wymaga struktur danych o takiej złożoności. Dla-
tego też rekurencja ma niewiele zastosowań. W wielu przypadkach może zastąpić iterację, ponie-
waż oba te mechanizmy pozwalają na pewną powtarzalność danych. Jednak funkcje rekurencyjne
są wolniejsze i zużywają więcej pamięci niż iteracja. Należy więc stosować iterację, gdzie tylko
jest to możliwe.
Listing 5.5. rekurencja.php — łatwo jest odwrócić łańcuch znaków przy użyciu rekurencji — pokazana
została również wersja iteracyjna
<?php
function odwroc_r($lancuch) {
if (strlen($lancuch)>0) {
odwroc_r(substr($lancuch, 1));
}
echo substr($lancuch, 0, 1);
return;
}
function odwroc_i($lancuch) {
for ($i=strlen($lancuch); $i>=0; --$i) {
echo substr($lancuch, $i, 1);
}
return;
}
odwroc_r('Cześć');
echo '<br />';
odwroc_i('Cześć');
?>
Na powyższym listingu zaimplementowano dwie funkcje. Obie z nich wyświetlą odwrócony łań-
cuch. Funkcja odwroc_r() jest rekurencyjna, a funkcja odwroc_i iteracyjna.
Funkcja odwroc_r pobiera jako parametr łańcuch znaków. Po jej wywołaniu przystąpi do wywoły-
wania siebie, za każdym razem przekazując łańcuch od miejsca drugiego do ostatniego. Na przykład
wywołanie:
odwroc_r('Cześć');
Każde wywołanie funkcji przez siebie samą tworzy w pamięci serwera nową kopię kodu funkcji,
lecz z różnym parametrem. Kod „oszukuje”, że za każdym razem jest wywoływana inna funkcja.
Przeciwdziała to pomyłkom pomiędzy kopiami funkcji.
Następnie kopia funkcji zwraca kontrolę kopii, przez którą została wywołana, to znaczy
odwroc_r('ść'). Funkcja ta wyświetli najpierw pierwszy znak łańcucha — 'ś' — po czym
kontrola przejdzie do kolejnej kopii funkcji.
Proces ten — wyświetlanie znaku i powrót do kopii nadrzędnej — trwa, dopóki kontrola nie zostanie
przekazana głównemu programowi.
Rozdział 5. Ponowne wykorzystanie kodu i tworzenie funkcji 165
Rozwiązanie rekurencyjne może zostać wybrane, kiedy jego kod jest znacznie krótszy i bardziej
elegancki niż wersja iteracyjna. W tej dziedzinie aplikacji nie zdarza się to jednak zbyt często.
Chociaż rekurencja wydaje się bardziej elegancka, programiści często zapominają o postawieniu
warunku zakończenia rekurencji. Znaczy to, że funkcja będzie się kopiowała, aż serwerowi zabrak-
nie pamięci lub zostanie przekroczony maksymalny czas wykonania, zależnie od tego, co wystąpi
szybciej.
Jednym z przykładów funkcji zwrotnych jest przedstawione w rozdziale 3., „Stosowanie tablic”,
wywołanie funkcji array_walk(). Jak wiadomo, funkcja array_walk() ma następujący prototyp:
bool array_walk(array tablica, callable funkcja[, mixed daneużytkownika])
W zaprezentowanym przykładzie została zdefiniowana także funkcja, która miała być wywoływana
dla każdego elementu tablicy i która została przekazana w wywołaniu funkcji array_walk();
miała ona następującą postać:
function moj_drukuj($wartosc) {
echo "$wartosc<br />";
}
array_walk($tablica, 'moj_drukuj');
Domknięcia mają także dostęp do zmiennych w zasięgu globalnym, jednak trzeba je jawnie zdefi-
niować w definicji domknięcia, używając słowa kluczowego use. Prosty przykład takiego rozwiąza-
nia został przedstawiony na listingu 5.6.
166 Część I Stosowanie PHP
$wspolczynnik = 0.20;
array_walk($produkty, $przeliczenie);
array_walk($produkty, $wyswietlacz);
?>
W następnym rozdziale
Wiadomo już, jak dołączać pliki i funkcje i czynić kod łatwiejszym w utrzymaniu i możliwym do
ponownego wykorzystania. W następnym rozdziale opiszemy programowanie obiektowe, a szcze-
gólnie jego zastosowanie w PHP. Stosowanie obiektów pozwala na osiągnięcie celów przedstawio-
nych w tym rozdziale, z większymi nawet zyskami dla złożonych projektów.
Rozdział 6.
Obiektowy PHP
W rozdziale tym wyjaśnimy koncepcje programowania zorientowanego obiektowo (ang. Object
Oriented — OO) oraz przedstawimy sposoby stosowania ich w PHP.
Mechanizmy obiektowe dostępne w PHP posiadają wszystkie własności, jakich oczekuje się od
języka w pełni zorientowanego obiektowo. W niniejszym rozdziale omówimy po kolei każdy z nich.
Klasy i obiekty
W kontekście programowania obiektowego obiektem może być prawie każda rzecz bądź koncepcja
— obiekt fizyczny, taki jak biurko lub klient, ale także konceptualny, istniejący jedynie w oprogra-
mowaniu, np. pole tekstowe lub plik. Generalnie najbardziej interesujące są obiekty konceptualne
zawierające obiekty świata rzeczywistego i koncepcje obiektowości, które muszą zostać wprowa-
dzone do programu.
Możliwości funkcjonalne obiektu są związane z używanymi przez niego danymi. Możliwa jest łatwa
zmiana szczegółów implementacji obiektu w celu poprawienia wydajności, dodania nowych wła-
sności lub naprawienia błędów bez konieczności zmiany interfejsu, która mogłaby poważnie
wpłynąć na cały projekt. Dzięki enkapsulacji możliwe jest dokonywanie zmian i naprawianie
błędów bez wpływania na pozostałe części projektu.
Rzeczywiście, istnieje ku temu kilka powodów. Wiele projektów WWW jest względnie małych
i prostych. Nie ustalając planu, można za pomocą piły zbić skrzynkę; podobnie bez większego
trudu można stworzyć większość projektów oprogramowania WWW z powodu ich niewielkich
rozmiarów. Jednakże próba zbudowania domu bez uprzedniego planu da marne rezultaty, jeżeli
w ogóle się powiedzie — podobnie jest z dużymi projektami programów.
Wiele projektów WWW ewoluuje od zbioru stron powiązanych hiperłączami do złożonej aplikacji
i zależnie od tego, czy wykorzystują okna dialogowe, czy też dynamicznie tworzone strony HTML,
wymagają dobrze przemyślanej metodologii rozwoju. Zorientowanie obiektowe może pomóc
w kontroli złożonych projektów, zwiększyć możliwości ponownego wykorzystywania kodu,
a w związku z tym ograniczyć koszty utrzymania.
Obiekty mogą być pogrupowane w klasy. Klasy reprezentują zbiór obiektów, które mogą różnić
się nieco między sobą, lecz posiadają określoną liczbę podobieństw. Klasa zawiera obiekty posiada-
jące takie same operacje, działające w identyczny sposób, i takie same atrybuty, opisujące identyczne
własności, chociaż wartości tych atrybutów mogą różnić się pomiędzy poszczególnymi obiektami.
Rozdział 6. Obiektowy PHP 169
O rzeczowniku „rower” można myśleć jako o klasie obiektów, opisującej wiele konkretnych rowe-
rów o wielu cechach wspólnych (atrybutach), jak dwa koła, kolor i wielkość, oraz operacje, takie
jak ruch.
Konkretny rower można ująć jako obiekt pasujący do klasy rowerów. Posiada on wszystkie wspólne
cechy rowerów, również operację ruchu, która działa tak jak ruch w innych rowerach. Atrybuty
konkretnego roweru mają unikatowe wartości, na przykład kolor zielony, ponieważ nie wszystkie
rowery są zielone.
Polimorfizm
Obiektowy język programowania musi rozpoznawać polimorfizm, co oznacza, że różne klasy
mogą się różnić zachowaniami dla tej samej operacji. Jeżeli na przykład istnieje klasa „samochód”
i klasa „rower”, to każda z nich ma różne operacje ruchu. W wypadku obiektów ze świata realne-
go ten problem rzadko występuje. Rowery raczej nie zaczną stosować operacji ruchu zapoży-
czonej od samochodów. Jednak język programowania nie odznacza się zdrowym rozsądkiem
świata realnego, tak więc musi rozpoznawać polimorfizm, aby „wiedzieć”, której operacji ruchu
użyć na konkretnym obiekcie.
Polimorfizm to charakterystyka raczej zachowań niż obiektów. W PHP tylko funkcje składowe
klas mogą być polimorficzne. Można to porównać do czasowników ze świata realnego, gdyż są one
równoważne funkcjom składowym. Roweru na przykład można używać na wiele sposobów,
m.in. czyścić go, wprawiać w ruch, rozkładać, naprawiać i malować.
Czasowniki te opisują ogólne działania, ponieważ nie wiadomo, na jakich obiektach są one doko-
nywane (ten typ abstrakcji dotyczącej obiektów i działań należy do charakterystycznych wyróżni-
ków ludzkiej inteligencji).
Na przykład przemieszczanie się rowerem wymaga zupełnie innych działań niż poruszanie się
samochodem, chociaż ogólne założenia są podobne. Czasownik poruszać się może być skojarzony
z konkretnymi działaniami dopiero wtedy, gdy znany jest obiekt tych działań.
Dziedziczenie
Dziedziczenie pozwala na tworzenie hierarchicznych związków pomiędzy klasami za pomocą
klas pochodnych. Klasa pochodna dziedziczy atrybuty i operacje po swojej klasie bazowej. Na przy-
kład rower i samochód mają pewne wspólne cechy. Możliwe jest zastosowanie klasy „pojazd”
zawierającej cechy, takie jak atrybut koloru, i operację ruchu, które to cechy posiadają wszystkie
pojazdy. Można więc przyjąć, że „rower” i „samochód” dziedziczą po pojeździe.
Często wymiennie stosowane są terminy klasa pochodna, podklasa i potomek. Analogicznie termin
klasa bazowa często używany jest naprzemiennie z terminami nadklasa oraz przodek.
Poprzez dziedziczenie możliwe jest budowanie nowych klas i dodawanie do starych. Z podsta-
wowej prostej klasy można według potrzeb wywieść klasy bardziej złożone i wyspecjalizowa-
ne. Dzięki temu kod jest łatwiejszy do ponownego wykorzystania, co zalicza się do ważnych zalet
ujęcia obiektowego.
Stosowanie dziedziczenia zaoszczędza sporo pracy, jeżeli operacje mogą zostać napisane raz
w klasie bazowej, a nie wielokrotnie w osobnych klasach pochodnych. Może również umożliwić
dokładniejsze modelowanie relacji ze świata realnego. Jeżeli zdanie o dwóch klasach z wyrazem
„jest” w środku jest sensowne, dziedziczenie jest przypuszczalnie właściwe. Zdanie: „samochód
jest pojazdem” ma sens, przeciwnie zaś zdanie: „pojazd jest samochodem”, ponieważ nie wszyst-
kie pojazdy to samochody. Tak więc „samochód” może dziedziczyć po „pojeździe”.
170 Część I Stosowanie PHP
Struktura klasy
Minimalna definicja klasy jest następująca:
class nazwaklasy
{
}
Klasy wymagają atrybutów i operacji, aby stać się użyteczne. Atrybuty tworzy się poprzez zade-
klarowanie zmiennych wewnątrz definicji klasy, stosując słowa kluczowe określające konkretny
zasięg widoczności: public, private albo protected. Więcej informacji na temat tych słów kluczo-
wych znajduje się w dalszej części tego rozdziału. Poniższy kod tworzy klasę o nazwie nazwaklasy
i dwa jej atrybuty, $atrybut1 i $atrybut2.
class nazwaklasy
{
public $atrybut1;
public $atrybut2;
}
Operacje tworzy się poprzez zadeklarowanie funkcji wewnątrz definicji klasy. Poniższy kod
utworzy klasę o nazwie nazwaklasy i dwie operacje niewykonujące żadnych działań. Operacja
operacja1() nie pobiera żadnych parametrów, a operacja2() pobiera dwa.
class nazwaklasy
{
function operacja1()
{
}
function operacja2($param1, $param2)
{
}
}
Konstruktory
Większość klas posiada specjalny typ operacji, zwany konstruktorem. Konstruktor jest wywoły-
wany przy tworzeniu obiektu, może więc wykonywać przydatną inicjalizację zadań, takich jak
nadawanie atrybutom sensownych wartości początkowych czy też tworzenie innych obiektów
wymaganych przez ten obiekt.
Konstruktor jest deklarowany w identyczny sposób jak inne operacje, ale posiada specjalną nazwę
__construct(). Chociaż istnieje możliwość ręcznego wywołania konstruktora, jego głównym
celem jest automatyczne wywołanie podczas tworzenia obiektu. Poniższy kod deklaruje klasę
z konstruktorem:
class nazwaklasy
{
function __construct($param)
{
echo "Konstruktor wywołany z parametrem ".$param."<br />";
}
}
Rozdział 6. Obiektowy PHP 171
PHP obsługuje już przeciążanie funkcji wewnątrz klas, co oznacza, że można zdefiniować więcej
niż jedną funkcję o danej nazwie i różnej liczbie parametrów lub z parametrami o różnych typach.
(Mechanizm taki jest obsługiwany w wielu językach zorientowanych obiektowo). Więcej informacji
na ten temat znajdzie się w dalszej części rozdziału.
Destruktory
Przeciwieństwem konstruktora jest destruktor. Dzięki niemu można implementować możliwości
funkcjonalne, które będą mogły być wykonywane bezpośrednio przed zniszczeniem klasy realizo-
wanym automatycznie w momencie, gdy wszystkie odwołania do klasy zostaną usunięte lub wypad-
ną poza zasięg.
Analogicznie do zasad nazewnictwa konstruktorów, destruktor klasy powinien nosić nazwę
__destruct(). Destruktory nie mogą przyjmować żadnych parametrów.
Tworzenie egzemplarzy
Po zadeklarowaniu klasy należy stworzyć obiekt — konkretne indywiduum, które jest członkiem
klasy — aby na nim pracować. Nazywane jest to tworzeniem egzemplarza bądź tworzeniem instancji
klasy. Obiekt tworzy się, stosując słowo kluczowe new. Należy wskazać klasę, której członkiem będzie
nowy obiekt, oraz podać jego nazwę, a także wszystkie parametry wymagane przez konstruktor.
Poniższy kod deklaruje klasę nazwaklasy z konstruktorem, po czym tworzy dwa obiekty typu
nazwaklasy:
class nazwaklasy
{
function __construct($param)
{
echo "Konstruktor wywołany z parametrem ".$param."<br />";
}
}
$a = new nazwaklasy("Pierwszy");
$b = new nazwaklasy("Drugi");
Ponieważ konstruktor zostaje wywołany, ilekroć powstaje nowy obiekt, powyższy kod wyświetla
następujący wynik:
Konstruktor wywołany z parametrem Pierwszy
Konstruktor wywołany z parametrem Drugi
1
Ostrzeżenie: Brak argumenty 1 w nazwaklasy::__construct(), wywołanej w pliku .../klasatestowa.php w wierszu 16
i zdefiniowanej w pliku .../klasatestowa.php w wierszu 8. — przyp. tłum.
2
Uwaga: Niezdefiniowana zamienna: param w pliku .../klasatestowa.php w wierszu 10 — przyp. tłum.
172 Część I Stosowanie PHP
Warto zauważyć, że po ostrzeżeniu i uwadze obiekt i tak zostanie utworzony, lecz parametr
konstruktora będzie pusty.
Dostępność atrybutów spoza klasy jest wyznaczana przez modyfikatory dostępu, przedstawione
w dalszej części rozdziału. W powyższym przykładzie nie ograniczono w żaden sposób dostępu
do atrybutów, można więc uzyskać do nich dostęp także spoza klasy w następujący sposób:
class nazwaklasy
{
public $atrybut;
}
$a = new nazwaklasy();
$a->$atrybut = "wartość";
echo $a->$atrybut;
Po utworzeniu obiektu operacje można wywoływać tak samo jak normalne funkcje: podając ich
nazwę i umieszczając wszelkie parametry w nawiasach. Jednak ponieważ w tym przypadku ope-
racje należą do obiektu, a nie są zwyczajnymi funkcjami, konieczne jest określenie, do którego
obiektu będzie należeć wywoływana operacja. Do tego celu używana jest nazwa obiektu, a zapis
wygląda tak samo jak w odwołaniach do atrybutów:
Rozdział 6. Obiektowy PHP 173
$a->operacja1();
$a->operacja2(12, "test");
Jeśli operacje coś zwracają, to tę wartość wynikową można przechwycić tak, jak pokazano w po-
niższym przykładzie:
$x = $a->operacja1();
$y = $a->operacja2(12, "test");
Należy zauważyć, że funkcja __get() ma jeden parametr — nazwę atrybutu — i zwraca wartość
wskazanego atrybutu. Podobnie funkcja __set() pobiera dwa parametry: nazwę atrybutu oraz nową
wartość, którą należy mu przypisać.
Funkcji tych nie trzeba wywoływać bezpośrednio. Poprzedzające ich nazwę podwójne podkre-
ślenie wskazuje PHP, że mają one specjalne znaczenie, podobnie jak funkcje __construct()
i __destruct().
możemy następnie wywołać funkcje __get() i __set(), aby odczytać i ustawić wartość dowolnego
atrybutu, który inaczej nie jest dostępny. W przypadku atrybutów zadeklarowanych jako publiczne
żadna z tych funkcji nie będzie używana, nawet jeśli wszystkie zostaną zdefiniowane.
Jeżeli napiszemy
$a->atrybut = 5;
Funkcja __get() działa w podobny sposób. Jeżeli w naszym kodzie zawrzemy odwołanie postaci
$a->$atrybut;
wyrażenie to niejawnie wywoła funkcję __get() z parametrem $nazwa równym "atrybut". Napi-
sanie funkcji __get(), która będzie zwracać wartość, pozostaje zadaniem programisty.
Rozdział 6. Obiektowy PHP 175
Na pierwszy rzut oka powyższy kod może się wydawać mało lub wręcz bezwartościowy. W jego
obecnej formie to prawdopodobnie prawda, lecz powód tworzenia funkcji dostępowych jest prosty:
istnieje teraz tylko jeden fragment kodu posiadający dostęp do tego konkretnego atrybutu.
Przy jednym tylko punkcie dostępu można zaimplementować testy sprawdzające, czy przecho-
wywane są tylko sensowne dane. Jeżeli okaże się później, że wartość $atrybut powinna mieścić
się pomiędzy zero i sto, należy w jednym miejscu dodać kilka wierszy kodu i sprawdzać wartości
przed pozwoleniem na zmiany. Funkcja __set() powinna zostać zmieniona w następujący sposób:
function __set($nazwa, $wartosc)
{
if(($nazwa="atrybut") && ($wartosc >= 0) && ($wartosc <= 100)) {
$this->$atrybut = $wartosc;
}
}
Mając tylko jeden punkt dostępu, można dowolnie zmieniać jego implementację. Jeżeli z jakiegoś
powodu należy zmienić sposób przechowywania $this->atrybut, funkcje dostępowe pozwalają na
wykonanie tego poprzez zmianę kodu tylko w jednym miejscu.
prawidłowe byłyby wszystkie poniższe próby dostępu do operacji i atrybutów obiektu typu B:
$b = new B();
$b->operacja1();
$b->atrybut1 = 10;
$b->operacja2();
$b->atrybut2 = 10;
176 Część I Stosowanie PHP
Należy zauważyć, że ponieważ klasa B dziedziczy po klasie A, można odnosić się w niej do
$b->operacja1() i $b->atrybut1, chociaż zostały one zdeklarowane w klasie A. B — jako klasa
pochodna A — posiada te same możliwości funkcjonalne i dane. Dodatkowo B ma własny atrybut
i operację.
Warto zapamiętać, że dziedziczenie działa tylko w jedną stronę. Klasa pochodna, czyli dziecko,
dziedziczy własności po swoim rodzicu, czyli klasie bazowej, lecz rodzic nie posiada własności
dziecka. Oznacza to, że dwa ostatnie z poniższych wierszy kodu nie mają sensu:
$a = new A();
$a->operacja1();
$a->atrybut1 = 10;
$a->operacja2();
$a->atrybut2 = 10;
class B extends A
{
function __construct()
{
$this->operacja1();
$this->operacja2();
$this->operacja3();
}
}
$b = new B;
?>
Rozdział 6. Obiektowy PHP 177
Kod ten tworzy w klasie A trzy operacje: public, protected i private. B dziedziczy po A. W kon-
struktorze klasy B następuje próba wywołania operacji przodka.
Wiersz
$this->operacja1();
Przykład ten wskazuje, że operacje prywatne nie mogą być wywoływane z klasy potomnej.
Jeżeli wykomentujemy ten wiersz, wywołania pozostałych dwóch funkcji zadziałają. Funkcja
protected jest dziedziczona, lecz można jej używać jedynie z wnętrza klasy potomnej, co właśnie
zrobiliśmy. Jeżeli na końcu pliku dodamy nowy wiersz
$b->operacja2();
Przesłanianie
Powyższa klasa pochodna posiada własne zadeklarowane atrybuty i operacje. Możliwe jest jednak
również, i czasem użyteczne, ponowne zadeklarowanie tych samych atrybutów i operacji. Robi
się to w celu nadania atrybutowi klasy pochodnej innej domyślnej wartości niż ta, którą posiada
on w klasie bazowej, lub w celu zapewnienia operacji klasy pochodnej innych możliwości funkcjo-
nalnych niż te, które posiada ona w klasie bazowej. Działania takie nazywa się przesłanianiem
(ang. overriding).
w której należy zmienić domyślną wartość atrybutu $atrybut i nadać nowe działanie operacji
operacja(), można stworzyć następującą klasę B, która przesłoni atrybut $atrybut i operację
operacja():
class B extends A
{
public $atrybut = 'inna wartość';
function operacja()
{
echo 'Coś innego<br />';
echo 'Wartość $atrybut wynosi '.$this->atrybut.'<br />';
}
}
178 Część I Stosowanie PHP
Utworzony został nowy obiekt typu A, po czym wywołano jego funkcję operacja(). Da to nastę-
pujący wynik:
Coś
Wartość $atrybut wynosi domyślna wartość
Powyższy wynik pokazuje, że zadeklarowanie B nie zmieniło A. Jeżeli utworzony zostanie obiekt
typu B, da on inne wyniki.
Poniższy kod:
$b = new B();
$b->operacja();
da następujący wynik:
Coś innego
Wartość $atrybut wynosi inna wartość
Podobnie jak tworzenie nowych atrybutów i operacji w klasie pochodnej nie ma wpływu na klasę
bazową, przesłanianie atrybutów lub operacji w klasie pochodnej nie oddziałuje na klasę bazową.
Klasa pochodna dziedziczy wszystkie atrybuty i operacje po swojej klasie bazowej, chyba że zo-
staną jej dostarczone zamienniki. Jeżeli zamiennik zostanie dostarczony, uzyskuje on pierwszeństwo
i przesłania oryginalną definicję.
Słowo kluczowe parent umożliwia wywoływanie oryginalnych wersji operacji z klasy przodka.
Na przykład aby wywołać A::operacja z wnętrza klasy B, należałoby napisać:
parent::operacja();
Dane wynikowe będą jednak inne. Pomimo wywołania operacji z klasy przodka PHP użyje wartości
atrybutów z klasy bieżącej. Dlatego wynik wywołania będzie następujący:
Coś
Wartość $atrybut wynosi inna wartość
Dziedziczenie może posiadać kilka warstw. Można zadeklarować klasę C, która dziedziczy własności
B i zarazem rodzica B, A. Klasa C również posiada możliwość wyboru, które atrybuty oraz operacje
rodziców zamienić i przesłaniać.
Dzięki takiemu rozwiązaniu operacja() nie zostanie przesłonięta w klasie B. Jeżeli taka próba
zostanie podjęta, wygenerowany będzie następujący błąd:
Fatal error: Cannot override final method A::operacja()3
Dzięki słowu kluczowemu final można w ogóle zapobiegać dziedziczeniu po klasie. Aby unie-
możliwić dziedziczenie klasy A, należy napisać:
final class A
{…}
Wielokrotne dziedziczenie
Niektóre języki obiektowe (przede wszystkim C++, Python i Smalltalk) oferują możliwość praw-
dziwego dziedziczenia wielokrotnego, ale większość z nich, w tym również PHP, nie ma takiej
własności. Oznacza to, że każda klasa może dziedziczyć tylko po jednym rodzicu. Nie ma
ograniczeń co do liczby dzieci, jakie może posiadać rodzic. Na początku może nie wydawać się
to zbyt jasne. Rysunek 6.1 przedstawia trzy różne sposoby dziedziczenia przez klasy A, B i C.
Rysunek 6.1.
PHP nie udostępnia
dziedziczenia
wielokrotnego
Lewa część rysunku przedstawia klasę C dziedziczącą po B, która z kolei dziedziczy po klasie A.
Każda klasa posiada co najwyżej jednego rodzica, tak więc jest to całkowicie prawidłowe dziedzi-
czenie w PHP.
Środkowa część rysunku ukazuje klasy B i C dziedziczące po klasie A. Każda klasa posiada co naj-
wyżej jednego rodzica, a zatem jest to również prawidłowe dziedziczenie.
3
Nie można przesłonić sfinalizowanej metody A::operacja() — przyp. tłum.
180 Część I Stosowanie PHP
Prawa część rysunku prezentuje klasę C dziedziczącą zarówno po klasie A, jak i klasie B. W tym
przypadku klasa C posiada dwoje rodziców, tak więc jest to dziedziczenie wielokrotne, nieprawi-
dłowe w PHP.
Dziedziczenie wielokrotne może być niezwykle złożone i trudne w kontekście utrzymania i pielę-
gnacji kodu, dlatego też opracowane zostały różne mechanizmy pozwalające na korzystanie z jego
zalet bez konieczności ponoszenia kosztów, które się wiążą z jego stosowaniem. PHP udostępnia
dwa takie mechanizmy, które w pewnym stopniu zastępują możliwości funkcjonalne dziedziczenia
wielokrotnego: interfejsy oraz cechy (ang. trait).
Implementowanie interfejsów
Gdy pojawia się konieczność zaimplementowania mechanizmu podobnego do dziedziczenia wie-
lokrotnego, można w PHP wykorzystać w tym celu interfejsy. Można je traktować jako rozwią-
zanie zastępujące dziedziczenie wielokrotne, analogiczne do implementacji interfejsów obsługi-
wanej przez inne języki zorientowane obiektowo, takie jak Java.
Idea interfejsu polega na tym, że wskazuje on zestaw funkcji, które muszą implementować klasy
implementujące ten interfejs. Można na przykład dojść do wniosku, że potrzebny jest zestaw klas,
które powinny umieć wyświetlać same siebie. Zamiast tworzyć klasę przodka zawierającą funkcję
wyswietl(), która będzie dziedziczona i przesłaniana przez wszystkie pozostałe klasy, można
zaimplementować interfejs w następujący sposób:
interface Wyswietlane
{
function wyswietl();
}
Przykład ten ilustruje pewne rozwiązanie zastępcze dla dziedziczenia wielokrotnego, ponieważ
klasa stronaWWW może dziedziczyć po jednej klasie i implementować jeden lub wiele interfejsów.
Jeżeli nie zostaną zaimplementowane wszystkie metody wskazane w interfejsie (w naszym przy-
padku metoda wyswietl()), wygenerowany zostanie błąd krytyczny.
Cechy
Cechy (ang. traits) są mechanizmem pozwalającym na korzystanie z najlepszych cech dziedziczenia
wielokrotnego bez konieczności narażania się na wszystkie związane z nim problemy. Cecha
pozwala na grupowanie możliwości funkcjonalnych, które następnie mogą być wykorzystywane
w wielu klasach. Klasa może używać wielu cech, a same cechy mogą dziedziczyć po sobie. Cechy
stanowią zatem wspaniały zestaw elementów pozwalających na wielokrotne stosowanie kodu.
Podstawowa różnica pomiędzy interfejsami a cechami polega na tym, że cechy zawierają imple-
mentację, a interfejsy jedynie określają, co należy zaimplementować.
Rozdział 6. Obiektowy PHP 181
Cechy tworzy się niemal identycznie jak klasy, jednak zamiast słowa kluczowego class należy
użyć słowa trait, jak pokazano w poniższym przykładzie:
trait dziennik
{
public function rejestrujkomunikat($tresc, $poziom='DEBUG')
{
// zapisuje $komunikat do dziennika
}
}
function zachowaj($dane) {
// ...
$this->rejestrujkomunikat($msg);
}
}
trait dziennikSystemowy
{
public function rejestrujkomunikat($message, $level='ERROR')
{
// zapisuje $komunikat do dziennika
}
}
class magazynPlikowy
{
use dzinnikPlikowy, dziennikSystemowy
{
dzinnikPlikowy::rejestrujkomunikat insteadof dziennikSystemowy;
dziennikSystemowy::rejestrujkomunikat as private rejestrujkomunikatsystemowy;
}
function store($data)
{
182 Część I Stosowanie PHP
// ...
$this->rejestrujkomunikat($komunikat);
$this->logsysmessage($komunikat);
}
}
?>
W tym przykładzie zostały zastosowane dwie różne cechy służące do rejestrowania komuni-
katów — ich nazwy podano w klauzuli use. Ponieważ każda z nich implementuje metodę
rejestrujkomunikat(), konieczne jest określenie, której z nich należy użyć. W przeciwnym razie
PHP zgłosi błąd krytyczny, gdyż nie będzie w stanie rozwiązać tego konfliktu.
W celu określenia konkretnej metody, która ma być stosowana, należy użyć słowa kluczowego
insteadof w sposób przedstawiony w poniższym przykładzie:
dziennikPlikowy::rejestrujkomunikat insteadof dziennikSystemowy;
Ten wiersz kodu jawnie informuje PHP, że należy zastosować metodę rejestrujkomunikat()
zdefiniowaną w cesze dziennikPlikowy. Jednak w tym przykładzie konieczne jest także wywołanie
metody rejestrujkomunikat() zdefiniowanej w cesze dziennikSystemowy. W tym celu można
użyć słowa kluczowego as, aby zmienić nazwę metody; oto jak można to zrobić:
dziennikSystemowy::rejestrujkomunikat as private rejestrujkomunikatsystemowy;
Projektowanie klas
Powyżej przedstawione zostały koncepcje obiektów i klas oraz składnia służąca do implemento-
wania ich w PHP. Teraz należy poznać sposoby tworzenia użytecznych klas.
Wiele klas w kodzie przedstawia klasy lub kategorie obiektów ze świata realnego. Przykładami klas,
które można zastosować w tworzeniu stron WWW, mogą być strony, składniki interfejsu użyt-
kownika, koszyki na zakupy, obsługa błędów, kategorie produktów lub klienci.
Klasa musi stanowić elastyczny szkielet, na podstawie którego będzie można tworzyć nowe strony
WWW, i który w żaden sposób nie będzie nas ograniczał.
Ponieważ strona generowana jest przez skrypt, a nie statyczny HTML, istnieje możliwość dodania
kilku „sprytnych” własności, które pozwolą na:
zmianę elementów strony w jednym miejscu. Jeżeli zmodyfikowana zostanie informacja
o prawach autorskich lub dodany dodatkowy przycisk, wystarczy dokonać zmiany w jednym
miejscu.
posiadanie domyślnej zawartości dla większości elementów strony, które jednak mogą
być modyfikowane zgodnie z potrzebami poprzez ustawianie wartości elementów, takich
jak tytuł i metaznaczniki.
rozpoznawanie, która strona jest aktualnie wyświetlana, a także na odpowiednią zmianę
elementów nawigacyjnych — nie jest konieczne wyświetlanie przycisku „strona główna”
na stronie głównej.
umożliwienie zamiany standardowych elementów poszczególnych stron. Jeżeli na przykład
w poszczególnych częściach witryny mają znajdować się różne przyciski nawigacyjne,
powinna istnieć możliwość zamiany przycisków standardowych.
Klasa wymaga pewnych atrybutów. Elementy, które mogą różnić się między pojedynczymi stro-
nami, zostaną zdefiniowane jako atrybuty klasy. Podstawowa zawartość strony, będąca kombi-
nacją znaczników HTML i tekstu, zostanie nazwana $zawartosc. Deklaruje się ją poprzez wpisanie
poniższego wiersza wewnątrz definicji klasy:
public $zawartosc;
Atrybuty mogą przechowywać również tytuł strony. Prawdopodobnie będzie on ulegał zmianie
w celu jasnego objaśnienia aktualnie wyświetlanej strony. Domyślny tytuł zostanie zdefiniowany
za pomocą poniższej deklaracji:
public $tytul = "TLA Consulting";
Przyciski nawigacyjne przedstawione na rysunku 5.2 (zobacz poprzedni rozdział) nie powinny
zmieniać się w zależności od strony w celu ułatwienia nawigacji użytkownikom. Aby można je
było łatwo zmieniać, również zostaną przechowane w atrybucie. Ponieważ liczba przycisków może
się zmieniać, zostaną one zachowane w tablicy łącznie z URL-ami, na które powinny wskazywać.
184 Część I Stosowanie PHP
W celu nadania określonych możliwości funkcjonalnych klasa wymaga również operacji. Można
rozpocząć od utworzenia funkcji dostępowych, ustawiających i pobierających wartości zdefi-
niowanych atrybutów:
public function __set($nazwa, $wartosc)
{
$this->$nazwa = $wartosc;
}
Funkcja __set() nie zawiera mechanizmów sprawdzania błędów (dla prostoty), lecz w razie
potrzeby własność taką łatwo można dodać. Ponieważ raczej żadna z tych wartości nie będzie
wywoływana spoza klasy, można pominąć tworzenie funkcji __get(), jak zresztą uczyniono.
Głównym przeznaczeniem tej klasy jest wyświetlanie strony HTML, do czego konieczna jest
następująca funkcja, nazwana Wyswietl():
public function Wyswietl()
{
echo "<html>\n<head>\n";
$this -> WyswietlTytul();
$this -> WyswietlSlowaKluczowe();
$this -> WyswietlStyle();
echo "</head>\n<body>\n";
$this -> WyswietlNaglowek();
$this -> WyswietlMenu($this->przyciski);
echo $this->zawartosc;
$this -> WyswietlStopke();
echo "</body>\n</html>\n";
}
Funkcja ta zawiera kilka prostych instrukcji wyświetlających HTML, ale głównie składa się z od-
wołań do innych funkcji klasy. Jak można się domyślić na podstawie nazw tych funkcji, wy-
świetlają one poszczególne części strony.
Rozbijanie funkcji nie jest obowiązkowe. Wszystkie osobne funkcje mogły zostać połączone
w jedną, dużą. Zostały jednak rozdzielone z kilku powodów.
Każda funkcja powinna mieć zdefiniowane zadanie. Im ono prostsze, tym prostsze jest pisanie
i testowanie funkcji. Nie należy jednak przesadzić — rozbicie programu na zbyt wiele małych
fragmentów powoduje trudności w czytaniu go.
Stosując dziedziczenie, można przesłaniać operacje. Istnieje możliwość wymiany jednej dużej
funkcji Wyswietl(), lecz mało prawdopodobna jest chęć zmiany wyglądu całej strony. Znacznie
lepiej rozbić funkcję wyświetlającą na kilka mniejszych, kompletnych zadań i przesłonić tylko
te, które należy zmienić.
Listing 6.1 przedstawia kompletną klasę, zapisaną jako strona.php, aby mogła być dołączona
do innych plików.
Listing 6.1. strona.inc — klasa Strona dostarcza łatwego i elastycznego sposobu tworzenia stron TLA
<?php
class Strona
{
// atrybuty klasy Strona
public $zawartosc;
public $tytul = "TLA CONSULTING";
public $slowa_kluczowe = "TLA Consulting, Tutaj Lubią Atrybuty,
niektóre z moich najlepszych przyjaciółek to wyszukiwarki";
public $przyciski = array("Strona główna" => "glowna.php",
"Kontakt" => "kontakt.php",
"Usługi" => "uslugi.php",
"Mapa strony" => "mapa.php"
);
<h1>TLA Consulting</h1>
</header>
<?php
}
Operacja CzyToAktualnyURL() określa, czy URL przycisku jest taki sam jak URL obecnej strony.
W celu sprawdzenia tej informacji można zastosować wiele różnych technik. W tym przypadku
została zastosowana funkcja strpos(), która sprawdza, czy podany URL jest zawarty w jednej
z ustawionych przez serwer zmiennych. Wywołanie strpos($_SERVER['PHP_SELF'], $url) zwróci
liczbę, jeżeli łańcuch $url znajduje się wewnątrz zmiennej superglobalnej $_SERVER['PHP_SELF'],
a w przeciwnym wypadku — false.
Aby zastosować tę klasę strony, należy dołączyć do skryptu plik strona.php i wywołać Wyswietl().
Kod przedstawiony na listingu 6.2 utworzy stronę główną TLA Consulting i wyświetli wynik
podobny do tego pokazanego na rysunku 5.2.
Listing 6.2. glowna.php — strona główna stosuje klasę Strona do wykonania większości pracy związanej
z generowaniem strony
<?php
require("strona.php");
Listing 6.2 pokazuje, że utworzenie nowych stron przy użyciu klasy Strona wymaga niewielkiego
nakładu pracy. Stosowanie klasy gwarantuje podobny wygląd wszystkich stworzonych za jej
pomocą stron.
188 Część I Stosowanie PHP
Jeżeli niektóre części strony powinny być wariantem strony standardowej, można po prostu skopio-
wać plik strona.php do nowego pliku strona2.php, po czym dokonać pewnych zmian. Oznacza to,
że przy każdej zmianie w pliku strona.php należy także dokonać modyfikacji w strona2.php.
Lepszym sposobem jest zastosowanie dziedziczenia. Polega on na utworzeniu nowej klasy, dziedzi-
czącej większość możliwości funkcjonalnych po Strona, ale przesłaniającej części, które powinny
być inne. Na przykład strona usług w TLA powinna zawierać drugi pasek nawigacyjny. Skrypt
przedstawiony na listingu 6.3 czyni to poprzez utworzenie nowej klasy o nazwie StronaUslug, która
dziedziczy po Strona. Utworzona została nowa tablica o nazwie $przyciski2, zawierająca przyciski
i łącza, które powinny znaleźć się w drugim rzędzie. Ponieważ klasa ta powinna działać prze-
ważnie tak jak poprzednia, przesłaniana jest jedynie zmieniona część — operacja Wyswietl().
Listing 6.3. uslugi.php — strona usług dziedziczy po klasie Strona, ale przesłania Wyswietl(),
aby zmienić wygląd
<?php
require ("strona.php");
$uslugi=new StronaUslug();
$uslugi->zawartosc="<p>Firma TLA Consulting oferuje kilka rodzajów usług.
Być może produktywność pracowników Państwa poprawiłaby się,
jeżeli Państwa firma zostałaby poddana inżynierii,
być może cały system potrzebuje właściwego doboru celów,
lub zmiany sposobu zarządzania zasobami ludzkimi.</p>";
$uslugi->Wyswietl();
?>
Przesłonięta metoda Wyswietl() jest bardzo podobna do pierwotnej, tyle że zawiera jeden dodatkowy
wiersz:
$this -> WyswietlMenu($this->przyciski2);
Wiersz ten wywołuje powtórnie metodę WyswietlMenu() i tworzy drugi pasek menu.
Jak przedstawiono na rysunku 6.2, utworzyliśmy nowy wariant strony standardowej. Musieliśmy
w tym celu napisać tylko te części kodu, które różniły się od strony standardowej.
Rysunek 6.2.
Strona usług
jest tworzona
przez zastosowanie
dziedziczenia w celu
ponownego użycia
jak najmniejszej
ilości kodu
Tworzenie stron za pomocą klas PHP posiada oczywiste zalety. Klasa wykonuje większość pracy
za programistę, który nie musi wkładać tyle wysiłku w tworzenie nowych stron — wszystkie
bowiem mogą być zaktualizowane w jednym miejscu poprzez proste zaktualizowanie klas. Stosując
dziedziczenie, można stworzyć różne wersje klas bez utraty powyższych zalet.
Jednak, jak można się domyślić, aby uzyskać te korzyści, trzeba ponieść pewne koszty. Tworze-
nie strony przez skrypt wymaga większej pracy procesora niż zwykłe załadowanie statycznej
strony HTML z dysku i wysłanie jej do przeglądarki. Jest to ważna wiadomość dla zatłoczonych
serwerów. W takich przypadkach poleca się mimo wszystko stosowanie statycznego kodu HTML
lub przechowywanie stron w pamięci podręcznej, aby zmniejszyć przeładowanie serwera.
Dostęp do stałej klasowej uzyskuje się przy użyciu operatora ::, wskazując klasę, do której należy
stała — jak w powyższym przykładzie.
190 Część I Stosowanie PHP
Trzeba zaznaczyć, że słowa kluczowego static nie można używać wewnątrz metody statycznej,
ponieważ obiekt, do którego należy się odwołać może nie istnieć.
W przykładzie tym wskazano, że $jakasklasa powinna być egzemplarzem klasy B. Jeżeli wówczas
przekazany zostanie egzemplarz klasy A
sprawdzenie_wskazania($a)
wygenerowany zostanie następujący błąd krytyczny:
Fatal error: Argument 1 must be an instance of B
Zwróćmy jeszcze uwagę, że gdyby wskazano typ A i do funkcji przekazano egzemplarz klasy B,
błąd nie pojawiłby się, ponieważ B dziedziczy po A.
Rozdział 6. Obiektowy PHP 191
Tego samego mechanizmu można używać w przypadku stosowania interfejsów, tablic oraz elemen-
tów wywoływalnych, czyli w przypadku przekazywania funkcji. Choć mechanizm ten działa w in-
terfejsach, to jednak nie można z niego korzystać w cechach.
class B extends A {
public static function ktoraklasa() {
echo __CLASS__;
}
}
A::test();
B::test();
?>
Jakie są oczekiwane wyniki generowane przez ten skrypt? Zapewne dosyć zaskakujące będzie
odkrycie, że w efekcie wykonania powyższego skryptu dwa razy zostanie wyświetlona litera A.
Wynika to z tego, że choć klasa B przesłania metodę ktoraklasa() klasy A, to jednak wywo-
łanie metody test() na rzecz klasy B spowoduje, że będzie ona wykonana w kontekście klasy A.
A w jaki sposób można sprawić, żeby metoda test() wywołała implementację metody ktoraklasa()
zdefiniowaną w klasie B, czyli implementację dostępną w obecnie używanej klasie?
Klonowanie obiektów
PHP zawiera słowo kluczowe clone, dzięki któremu można skopiować istniejący obiekt. Na przy-
kład polecenie:
$c = clone $b;
spowodowałoby utworzenie kopii obiektu $b tej samej klasy i z tymi samymi wartościami atrybutów.
192 Część I Stosowanie PHP
Zachowanie to również można zmienić. Jeżeli clone powinno wykonywać działania inne od
domyślnego, należy w tym celu w klasie bazowej napisać metodę o nazwie __clone(). Metoda
ta przypomina konstruktor i destruktor, ponieważ jej również nie wywołuje się bezpośrednio. Jest
ona wywoływana w momencie, gdy użyte zostaje słowo kluczowe clone w sposób przedstawiony
powyżej. Wewnątrz metody __clone() można zdefiniować taką operację kopiowania, jaka jest
rzeczywiście potrzebna.
Wygodną rzeczą związaną z metodą __clone() jest to, że jest ona wywoływana dopiero po utwo-
rzeniu dokładnej kopii obiektu przy użyciu mechanizmu domyślnego. W tym momencie wystarczy
więc zmieniać tylko te elementy, które rzeczywiście wymagają zmian.
Można również nic nie zmieniać, a jedynie dodać jakieś dodatkowe czynności, na przykład uaktu-
alnianie w używanej bazie danych rekordu powiązanego z klasą.
Każda klasa, która zawiera metody abstrakcyjne, sama musi być abstrakcyjna, co widać na poniż-
szym przykładzie:
abstract class A
{
abstract function operacjaX($param1, $param2);
}
Można także deklarować klasy abstrakcyjne, które nie będą zawierać żadnych abstrakcyjnych metod.
Metod i klas abstrakcyjnych używa się najczęściej w złożonych hierarchiach klas, w których
trzeba zapewnić, że każda klasa potomna będzie zawierać i przesłaniać konkretne metody. Ten
sam efekt można również osiągnąć przy użyciu interfejsów.
Aby móc używać przeciążania, należy zaimplementować metodę __call(), jak w poniższym
przykładzie:
Rozdział 6. Obiektowy PHP 193
Metoda __call() powinna przyjmować dwa parametry. Pierwszy musi zawierać nazwę wywoły-
wanej metody, natomiast drugi tablicę parametrów przekazywanych do tej metody. Można wówczas
samodzielnie decydować, która metoda powinna zostać wywołana. W naszym przykładzie jeśli do
metody wyswietl() przekazywany jest obiekt, wywołana zostanie metoda wyswietlObiekt(); jeżeli
przekazana zostanie tablica, nastąpi wywołanie metody wyswietlTablice(); natomiast jeśli prze-
kazana zostanie wartość innego rodzaju, wywołana będzie metoda wyswietlSkalar().
Aby wywołać ten kod, należy najpierw utworzyć egzemplarz klasy zawierającej metodę __clone()
(nazwijmy ją przeciazanie), po czym wywołać metodę wyswietl(), jak w poniższym przykładzie:
$pr = new przeciazanie;
$pr->wyswietl(array(1, 2, 3));
$pr->wyswietl('kot');
Zwróćmy uwagę, że aby ten kod zadziałał, nie trzeba implementować metody wyswietl().
W PHP 5.3 została wprowadzona podobna magiczna metoda, o nazwie __callStatic(). Działa ona
podobnie jak __call(), lecz jest wywoływana w momencie, gdy niedostępna metoda zostanie
wywołana w kontekście statycznym, na przykład:
przeciazanie::wyswietl();
Główną rolą __autoload() jest podjęcie próby dołączenia funkcją include() lub require() plików
niezbędnych do utworzenia wymaganej klasy. Rozważmy następujący przykład:
function __autoload($nazwa)
{
include_once $nazwa.".php";
}
Tak skonstruowany kod podejmie próbę dołączenia pliku o takiej samej nazwie jak nazwa klasy.
194 Część I Stosowanie PHP
Jeżeli wymagany jest mechanizm bardziej wyrafinowany niż prezentowany powyżej, można zaim-
plementować tak zwany iterator. W tym celu klasa, która ma podlegać iteracji, powinna implemen-
tować interfejs IteratorAggregate i zawierać metodę o nazwie getIterator(), zwracającą eg-
zemplarz klasy iteratora. Klasa ta z kolei musi implementować interfejs Iterator, posiadający
zestaw metod koniecznych do zaimplementowania. Przykładową klasę i iterator przedstawiono
na listingu 6.4.
private $ob;
private $ile;
private $biezacyIndeks;
function __construct($ob)
{
$this->ob = $ob;
$this->ile = count($this->ob->dane);
}
function rewind()
{
$this->biezacyIndeks = 0;
}
function valid()
{
return $this->biezacyIndeks < $this->ile;
}
function key()
{
return $this->biezacyIndeks;
}
function current()
{
return $this->ob->dane[$this->biezacyIndeks];
}
function next()
{
$this->biezacyIndeks++;
}
}
Rozdział 6. Obiektowy PHP 195
function __construct($wejscie)
{
$this->dane = $wejscie;
}
function getIterator()
{
return new IteratorObiektu($this);
}
}
$mojIterator = $mojObiekt->getIterator();
for($mojIterator->rewind(); $mojIterator->valid(); $mojIterator->next())
{
$klucz = $mojIterator->key();
$wartosc = $mojIterator->current();
echo $klucz." => ".$wartosc."<br />";
}
?>
Powodem, dla którego warto używać tego rodzaju klasy iteratora, jest fakt, że interfejs do danych
nie zmieni się nawet wówczas, gdy zmieni się wewnętrzna implementacja. W naszym przykładzie
klasa IteratorAggregate jest zwykłą tablicą. Jeżeli zostanie ona zmieniona w tabelę mieszaną
lub listę połączoną, nadal będzie można przez nią przechodzić przy użyciu standardowej klasy
Iterator, choć sam kod źródłowy tej klasy powinien ulec odpowiednim zmianom.
Generatory
Generatory są pod wieloma względami podobne do iteratorów. Są one także stosowane w kilku
innych językach programowania, takich jak Python. Jednym ze sposobów myślenia o generatorach
jest wyobrażenie sobie, że ich definicje wyglądają jak funkcje, lecz działają jak iteratory.
Funkcje generatora należy wywoływać wewnątrz pętli foreach. Spowoduje to utworzenie obiektu
Generator, który praktycznie zapamięta wewnętrzny stan funkcji generatora. Każda iteracja
zewnętrznej pętli foreach sprawi, że generator wykona jedną iterację swojej wewnętrznej funkcji.
foreach(fizzbuzz(1,20) as $liczba) {
echo $liczba.'<br />';
}
?>
Funkcja generatora zostaje wywołana w pętli foreach. W momencie pierwszego wywołania tej
funkcji tworzony jest wewnętrzny obiekt generatora. Po wywołaniu funkcja jest wykonywana
do miejsca, w którym zostanie napotkana pierwsza instrukcja yield. Wykonanie tej instrukcji
spowoduje zwrócenie wartości i przekazanie sterowania do miejsca, w którym funkcja została
wywołana.
Najważniejszą rzeczą, na jaką należy zwrócić uwagę, jest to, że generator zachowuje stan.
Oznacza to, że podczas następnej iteracji zewnętrznej pętli foreach wykonywanie generatora
zostanie wznowione w miejscu, w którym wcześniej zostało przerwane, i będzie kontynuowane
aż do miejsca, w którym zostanie napotkana następna instrukcja yield. W ten sposób sterowanie
jest przekazywane tam i z powrotem pomiędzy główną linią kodu a funkcją generatora. Podczas
każdej iteracji zewnętrznej pętli foreach z generatora pobierana jest następna wartość sekwencji.
Jeśli ktoś chciałby sobie wyobrazić model generatora, to można by go porównać do wymyślnej
tablicy zawierającej wszystkie potencjalne wartości. Jednak podstawowa różnica pomiędzy gene-
ratorem oraz, dajmy na to, funkcją wypełniającą tablicę wszystkimi potencjalnymi wartościami
polega na tym, że generatory stosują tak zwane wykonywanie leniwe (ang. lazy execution).
Chodzi o to, że w danej chwili jest pobierana i przechowywana w pamięci tylko jedna wartość.
To sprawia, że generatory doskonale nadają się do operowania na dużych zbiorach danych, których
nie da się łatwo zmieścić w dostępnej pamięci.
Rozdział 6. Obiektowy PHP 197
Instrukcja echo wyświetli wszystkie dane zwrócone przez __toString(). Funkcję tę można na
przykład zaimplementować w następujący sposób:
class Drukowane;
{
var $testjeden;
var $testdwa;
public function __toString()
{
return(var_export($this, TRUE));
}
}
API to jest wysoce skomplikowane, dlatego zajmiemy się tylko prostym przykładem ilustrującym
ogólną ideę jego używania. Wróćmy na przykład do klasy Strona, którą zdefiniowano wcześniej
w tym rozdziale. Dzięki API Reflection można odczytać wszystkie informacje na temat tej klasy
tak, jak uczyniono to na listingu 6.6.
require_once("strona.inc");
?>
W przykładzie tym użyto metody __toString() klasy Reflection do wyświetlenia danych. Znacz-
niki <pre> wyświetlane są przez oddzielne wiersze kodu, aby wyraźnie uwidocznić moment wywo-
łania metody __toString().
Część danych wynikowych zwróconych przez powyższy kod przedstawiono na rysunku 6.3.
198 Część I Stosowanie PHP
Rysunek 6.3.
Dane wynikowe
zwracane przez API
Reflection
są zadziwiająco
szczegółowe
Przestrzenie nazw
Przestrzenie nazw to mechanizm pozwalający na grupowanie klas bądź funkcji. Przestrzenie nazw
doskonale nadają się do łączenia powiązanych ze sobą elementów i tworzenia bibliotek.
Przed wprowadzeniem przestrzeni nazw jedynym dostępnym sposobem grupowania klas lub funkcji
na podstawie nazwy było dodawanie do nich odpowiednich prefiksów. Na przykład w przypadku
posiadania biblioteki klas do obsługi poczty elektronicznej nazwy wszystkich wchodzących w jej
skład klas mogły być poprzedzane słowem Mail. Przestrzenie nazw stanowią jednak znacznie
lepszy sposób tworzenia kolekcji powiązanego ze sobą kodu, a przy okazji rozwiązują dwa dodat-
kowe, powszechnie występujące problemy.
Przede wszystkim grupowanie klas i funkcji w przestrzeniach nazw pozwala unikać konfliktów
nazw. Załóżmy, że na potrzeby projektu została napisana klasa do obsługi pamięci podręcznej,
której nadano nazwę Cache. Jeśli w tym samym projekcie jest używany jakiś framework, to może
się zdarzyć, że także w nim będzie używana klasa o takiej samej nazwie. W takim przypadku twórcy
projektu będą mieli spory problem. Gdyby jednak każda z tych klas została umieszczona we własnej
przestrzeni nazw, to problemu dałoby się uniknąć.
Ponadto w starych systemach można było spotkać się z nazwami klas typu Tworca_Projekt_Cache_
Memcache. Już na pierwszy rzut oka widać, że taka nazwa jest dosyć dziwna. Dzięki przestrzeniom
nazw taką nazwę klasy można skrócić do nazwy Memcache, umieszczonej w odpowiedniej prze-
strzeni nazw.
Rozdział 6. Obiektowy PHP 199
W celu utworzenia przestrzeni nazw należy użyć słowa kluczowego namespace, a za nim podać
jej nazwę. (Strasznie tu dużo tych nazw!). Cały kod umieszczony w pliku poniżej deklaracji prze-
strzeni nazw automatycznie będzie należeć do tej przestrzeni. Koniecznie trzeba zapamiętać, że
jeśli w jakimś pliku ma zostać zadeklarowana przestrzeń nazw, to jej deklaracja musi być umiesz-
czona w pierwszym wierszu kodu danego pliku.
W poniższym przykładzie utworzymy przestrzeń nazw Zamowienie, zawierającą cały kod związa-
ny z obsługą zamówień. Można w tym celu utworzyć plik o nazwie zamowienia.php (choć nazwa
samego pliku nie ma tu żadnego znaczenia), posiadający następującą zawartość:
<?php
namespace zamowienia;
class zamowienia
{
// ...
}
class pozycjaZamowienia
{
// ...
}
?>
Dostęp do klas umieszczonych w powyższej przestrzeni można uzyskać w taki oto sposób:
include 'zamowienia.php';
$mojeZamowienie = new zamowienia\zamowienie();
Trzeba zatem zwrócić uwagę na to, że chcąc użyć w kodzie klasy należącej do przestrzeni nazw
zamowienia, należy najpierw zapisać nazwę przestrzeni nazw, następnie znak lewego ukośnika,
a dopiero za nim nazwę klasy, którą należy zastosować. Znak lewego ukośnika jest także nazywany
separatorem przestrzeni nazw.
Podawanie nazwy przestrzeni w opisany powyżej sposób jest określane jako stosowanie w pełni
kwalifikowanej przestrzeni nazw. Klasy zamowienie można też używać bez określania przestrzeni
nazw, do której ona należy; można to zrobić w następujący sposób:
<?php
namespace zamowienia;
include 'zamowienia.php';
$mojeZamowienie = new zamowienie();
?>
Przede wszystkim, jak widać, ta sama deklaracja przestrzeni nazw została użyta w kilku plikach.
Jest to całkowicie poprawne rozwiązanie. Oczywiście można by zadeklarować w tym pliku
kolejne klasy i funkcje i także one należałyby do przestrzeni nazw zamowienia. Ta możliwość
stanowi kolejną metodę wygodnego organizowania kodu w modularny sposób. W tym przypadku
umieszczenie deklaracji przestrzeni nazw na początku pliku oznacza dołączenie dalszego kodu
znajdującego się w pliku do przestrzeni nazw, co z kolei sprawia, że klas czy też funkcji należących
do tej przestrzeni będzie można używać bez poprzedzania ich pełną nazwą przestrzeni nazw.
Aby wyobrazić sobie przestrzenie nazw, można porównać je na przykład do katalogów w syste-
mie plików. Umieszczenie deklaracji przestrzeni nazw na początku pliku przenosi jego zawartość
do kontekstu przestrzeni zamowienia, dzięki czemu nie trzeba już określać ścieżki do innych ele-
mentów tej przestrzeni.
200 Część I Stosowanie PHP
Koniecznie należy zapamiętać, że wszelkie klasy, do których odwołujemy się wewnątrz przestrzeni
nazw bez podawania w pełni kwalifikowanej nazwy przestrzeni nazw, są traktowane jako klasy
należące do tej przestrzeni. Jednakże funkcje i stałe podane bez w pełni kwalifikowanej nazwy
przestrzeni nazw będą najpierw poszukiwane w bieżącej przestrzeni, a jeśli nie uda się ich znaleźć,
to PHP spróbuje je odszukać w globalnej przestrzeni nazw. Ten sposób działania nie dotyczy
jednak klas.
W powyższym przykładzie została zadeklarowana klasa Strona (używana już w jednym z wcze-
śniejszych przykładów), należąca do przestrzeni nazw janek\html\strona. Taki wzorzec określa-
nia struktury przestrzeni nazw jest stosowany całkiem często. W takim przypadku, aby skorzystać
z klasy Strona poza przestrzenią nazw, należy odwołać się do niej w następujący sposób:
$uslugi = new janek\html\strona\Strona();
Gdyby jednak kod należał do przestrzeni nazw janek, to odwołanie do klasy Strona musiałoby
przyjąć następującą postać:
$uslugi = new html\strona\Strona();
Zastosowanie powyższej instrukcji use pozwala na użycie skrótu czy też nazwy zastępczej
strona zamiast w pełni kwalifikowanej nazwy przestrzeni janek\html\strona. Poza tym nazwa
zastępcza przestrzeni nazw może mieć całkowicie inną postać, na przykład:
use janek\html\strona as www;
$uslugi = new www\Strona();
W następnym rozdziale
W następnym rozdziale przedstawione zostaną mechanizmy obsługi wyjątków w PHP. Wyjątki są
wygodnym narzędziem obsługi błędów fazy wykonania.
202 Część I Stosowanie PHP
Rozdział 7.
Obsługa błędów i wyjątków
W tym rozdziale omówione są zasady obsługi wyjątków i sposoby ich implementacji w PHP.
Wyjątki są nową, ważną funkcją w PHP. Zapewniają one zunifikowany obiektowy mechanizm
obsługi błędów w sposób rozszerzalny i łatwy w utrzymaniu.
Jeżeli wewnątrz bloku try coś zostanie wykonane nieprawidłowo, można wykonać operację
nazywaną zgłoszeniem wyjątku. W niektórych językach, na przykład Java, wyjątki w pewnych
przypadkach są zgłaszane automatycznie. W PHP wyjątki muszą być zgłaszane ręcznie. Operacja
ta jest wykonywana w następujący sposób:
throw new Exception($komunikat, kod);
Słowo kluczowe throw wywołuje mechanizm obsługi wyjątków. Jest to konstrukcja języka, a nie
funkcja, trzeba do niej jednak przekazać wartość. Oczekuje ona, że otrzyma obiekt jako parametr.
W najprostszym przypadku można utworzyć obiekt wbudowanej klasy Exception, tak jak pokazano
w powyższym przykładzie.
Konstruktor tej klasy wymaga podania do trzech parametrów: komunikatu, kodu oraz wcześniej-
szego wyjątku. Pierwsze dwa są przeznaczone do reprezentowania komunikatu błędu oraz nu-
meru kodu błędu. Trzeci parametr służy do przekazania wcześniej zgłoszonego wyjątku i jest uży-
wany w przypadku obsługi łańcuchów wyjątków. Wszystkie te parametry są opcjonalne.
204 Część I Stosowanie PHP
Następnie, poniżej bloku try potrzebny jest co najmniej jeden blok catch. Blok catch jest zbu-
dowany następująco:
catch (typ wyjątek)
{
// obsługa wyjątku
}
Dla jednego bloku try można mieć kilka bloków catch. Zastosowanie więcej niż jednego bloku
catch ma sens, jeżeli każdy z nich ma za zadanie przechwycić inny typ wyjątku. Na przykład
jeżeli chcemy przechwycić wyjątki klasy Exception, blok catch będzie wyglądał następująco:
catch (Exception $e)
{
// obsługa wyjątku
}
Obiekt przekazany do bloku catch (i przechwycony przez niego) jest tym samym obiektem, który
jest przekazany do instrukcji throw, powodującej zgłoszenie wyjątku. Wyjątek może być dowol-
nego typu, ale najlepszym rozwiązaniem jest wykorzystywanie obiektów klasy Exception lub
obiektów własnych klas wyjątków dziedziczących po klasie Exception (sposób definiowania wła-
snych wyjątków jest przedstawiony w dalszej części rozdziału).
Po zgłoszeniu wyjątku PHP wyszukuje odpowiadający mu blok catch. Jeżeli utworzony jest więcej
niż jeden blok catch, obiekty przekazywane do tych bloków powinny być różnych typów, dzięki
czemu PHP może rozpoznać, do którego bloku przekazać sterowanie.
I w końcu poniżej wszystkich bloków catch można umieścić blok finally. Ten blok zostanie
wykonany zawsze po wykonaniu bloku try oraz bloków catch, i to niezależnie od tego, czy jakiś
wyjątek został zgłoszony oraz obsłużony, czy nie. Blok finally umieszczony poniżej bloków
try i catch ma następującą postać:
try {
// jakieś operacje, które potencjalnie mogą zgłosić wyjątek
} catch (Exception $e) {
// obsługa wyjątku
} finally {
echo 'Ten blok zawsze zostanie wywołany!';
}
Należy również wspomnieć, że wewnątrz bloku catch można zgłaszać kolejne wyjątki. Aby wyja-
śnić tę kwestię, przeanalizujmy poniższy przykład. Na listingu 7.1. pokazany jest prosty przykład
obsługi wyjątków.
Na listingu 7.1. pokazane jest zastosowanie różnych metod klasy Exception, które zostaną krótko
omówione poniżej. Wynik działania tego kodu jest pokazany na rysunku 7.1.
Rozdział 7. Obsługa błędów i wyjątków 205
Rysunek 7.1.
Blok catch pozwala
wyświetlać komunikaty
błędów oraz miejsce
ich wystąpienia
W przykładowym kodzie zgłoszony został wyjątek klasy Exception. Ta klasa wbudowana zawiera
metody, które — wykorzystane w bloku catch — pozwalają na tworzenie pomocnych komuni-
katów błędów.
Klasa Exception
PHP zawiera wbudowaną klasę o nazwie Exception. Jej konstruktor posiada trzy opcjonalne para-
metry, o których już wcześniej wspominaliśmy: komunikat, kod błędu oraz poprzedni wyjątek.
Oprócz konstruktora klasa zawiera następujące metody:
Stos wywołań pokazuje, które funkcje były wykonywane w czasie, gdy został zgłoszony wyjątek.
206 Część I Stosowanie PHP
/* Metody do przesłaniania */
public function __toString(); // sformatowany łańcuch do wyświetlania
}
?>
Podstawowym powodem, dla którego zamieszczamy definicję tej klasy, jest pokazanie, że więk-
szość metod publicznych jest oznaczona jako final. Oznacza to, że nie można ich przedefiniować.
Można tworzyć własne klasy dziedziczące po Exception, ale nie da się zmieniać działania pod-
stawowych metod. Trzeba zauważyć, że metoda __toString() może zostać przedefiniowana, dzięki
czemu możliwa jest zmiana sposobu wyświetlania wyjątku. Można również dodawać własne
metody.
try
{
throw new mojException("Wystąpił okropny błąd", 42);
}
catch (mojException $m)
{
echo $m;
}
?>
W przedstawionym kodzie zadeklarowana została nowa klasa wyjątku o nazwie moj Exception,
która rozszerza podstawową klasę Exception. Różnica pomiędzy tą klasą a klasą Exception jest
taka, że zmieniona została metoda __toString(), aby zapewnić elegancki sposób wyświetlania
danych wyjątku. Wynik działania tego kodu jest pokazany na rysunku 7.2. Jak widać, jest on podob-
ny do wyników wygenerowanych w poprzednim przykładzie.
Rysunek 7.2.
Klasa mojException
udostępnia wyjątki
z eleganckim
wyświetlaniem
Przykład ten jest dosyć prosty. W następnym podrozdziale zapoznamy się ze sposobami różnych
wyjątków mających za zadanie obsługę różnych kategorii błędów.
Wracając do oryginalnego kodu, można zauważyć trzy miejsca, gdzie operacja zapisu do pliku
może się nie udać: plik nie da się otworzyć, nie można nałożyć blokady lub nie można zapisać do
pliku. Utworzyliśmy klasy wyjątków dla każdej z tych możliwości.
208 Część I Stosowanie PHP
Następnie w taki sposób zmieniliśmy plik przetworzzamowienie.php z rozdziału 2., aby skorzystać
w nim z obsługi wyjątków. Nowa wersja tego pliku jest zamieszczona na listingu 7.5.
Listing 7.5. przetworzzamowienie.php — skrypt przetwarzający zamówienia Janka z dodaną obsługą wyjątków
<?php
require_once("wyjatki_plikowe.php");
<!DOCTYPE html>
<html>
<head>
<title>Części samochodowe Janka — wyniki zamówienia</title>
</head>
<body>
<h1>Części samochodowe Janka</h1>
<h2>Wyniki zamówienia</h2>
<?php
echo "<p>Zamówienie przyjęte o ".date('H:i, jS F Y')."</p>";
echo "<p>Państwa zamówienie wygląda następująco:</p>";
$ilosc = 0;
$wartosc=0.00;
define('CENAOPON', 100);
define('CENAOLEJU', 10);
define('CENASWIEC', 4);
if( $ilosc == 0 ) {
echo "Na poprzedniej stronie nie zostało złożone żadne zamówienie!<br />";
} else {
if ( $iloscopon>0 ) {
echo htmlspecialchars($iloscopon)." opon<br />";
}
if ( $iloscoleju>0 ) {
echo htmlspecialchars($iloscoleju)." butelek oleju<br />";
}
if ( $iloscswiec>0 ) {
echo htmlspecialchars($iloscswiec)." świec zapłonowych<br />";
}
}
if (!flock($p, LOCK_EX)) {
throw new blokadaPlikuException();
}
210 Część I Stosowanie PHP
if (!fwrite($wp, $ciagwyjsciowy)) {
throw new zapisPlikuException();
}
flock($wp, LOCK_UN);
fclose($wp);
echo '<p>Zamówienie zapisane.</p>';
}
catch (otwarciePlikuException $ope)
{
echo "<p><strong> Nie można otworzyć pliku zamówień.
Proszę skontaktować się z administratorem systemu.</strong></p>";
}
catch (Exception $e)
{
echo "<p><strong>Państwa zamówienie nie może zostać przyjęte w tej chwili.
Proszę spróbować później.</strong></p>";
}
?>
</body>
</html>
Jak łatwo zauważyć, część skryptu odpowiedzialna za operacje wejścia-wyjścia jest umieszczona
w bloku try. Tworzenie małych bloków try oraz catch z odpowiednimi wyjątkami powinno być
traktowane jako dobra praktyka kodowania. Powoduje to, że kod obsługi wyjątków jest łatwiejszy
do napisania i modyfikacji, ponieważ łatwo sprawdzić, jakie błędy należy obsłużyć.
Jeżeli nie można otworzyć pliku, zgłaszany jest wyjątek otwarciePlikuException; jeżeli nie można
zablokować pliku, zgłaszany jest wyjątek blokadaPlikuException; a jeżeli nie można zapisać
danych do pliku, zgłaszany jest wyjątek zapisPlikuException.
Przyjrzyjmy się blokom catch. Aby tylko zilustrować problem, dodaliśmy dwa takie bloki: jeden
do obsługi otwarciePlikuException, a drugi do obsługi Exception. Ponieważ pozostałe wyjątki dzie-
dziczą po Exception, mogą być przechwycone przez drugi blok catch. Bloki catch są dopasowywane
za pomocą identycznego mechanizmu, jak zastosowany w operatorze instanceof. Jest to jeden
z powodów, dla których powinno się tworzyć własne klasy wyjątków na podstawie jednej klasy.
Należy jeszcze pamiętać, że jeżeli zostanie zgłoszony wyjątek, dla którego nie został utworzony
odpowiedni blok catch, PHP zgłosi błąd krytyczny.
Na listingu 7.5 można zauważyć, że wywołanie funkcji fopen() jest nadal poprzedzone operato-
rem @, którego zadaniem jest wyłączenie wyświetlania komunikatów błędów. Jeżeli funkcja nie
zostanie prawidłowo zakończona, w zależności od ustawień w pliku php.ini, komunikat błędu
może być wyświetlony lub zarejestrowany w dzienniku zdarzeń. Ustawienia te są opisane w roz-
dziale 26., ale należy pamiętać, że ostrzeżenie to pojawi się niezależnie od tego, czy został zgło-
szony wyjątek, czy nie.
Rozdział 7. Obsługa błędów i wyjątków 211
W następnym rozdziale
Następna część książki jest poświęcona systemowi MySQL. Opiszemy, jak tworzyć i wypełniać
bazy danych MySQL, a następnie połączymy to, czego się nauczymy, z PHP, dzięki czemu moż-
liwe stanie się korzystanie z bazy danych poprzez WWW.
212 Część I Stosowanie PHP
Część II
Stosowanie MySQL
214 Część II Stosowanie MySQL
Rozdział 8. Projektowanie internetowej bazy danych 215
Rozdział 8.
Projektowanie
internetowej bazy danych
Gdy już znamy podstawy PHP, możemy przystąpić do łączenia skryptów z bazą danych. Jak
pamiętamy, w rozdziale 2. przedstawiono korzyści wynikające z używania systemów zarządza-
nia relacyjnymi bazami danych (RDBMS) zamiast plików jednorodnych:
Bazy danych pozwalają na szybszy dostęp do danych niż pliki jednorodne.
Bazy danych pozwalają na wykonywanie zapytań o dane spełniające konkretne kryteria.
Bazy danych posiadają wbudowany mechanizm zapewniania równoległego dostępu,
dzięki czemu my, programiści, nie musimy się tym przejmować.
Bazy danych pozwalają na swobodny dostęp do danych.
Bazy danych mają wbudowany system przywilejów.
Ujmując rzecz bardziej konkretnie, korzystanie z baz danych pozwala na szybkie i łatwe uzyski-
wanie odpowiedzi na pytania dotyczące miejsca zamieszkania klientów, najlepiej sprzedającego
się produktu czy typu klientów wydających najwięcej pieniędzy. Informacje te pomogą ulepszyć
stronę WWW, aby przyciągnąć i zatrzymać większą liczbę użytkowników, lecz ich odczytywanie
z plików płaskich byłoby bardzo trudnym zadaniem.
Bazą danych, z której będziemy korzystać w tej części książki, jest MySQL. Zanim jednak zaj-
miemy się konkretnymi zagadnieniami dotyczącymi bazy danych, powinniśmy rozważyć nastę-
pujące kwestie:
koncepcję i terminologię relacyjnych baz danych,
projektowanie internetowych baz danych,
architekturę internetowych baz danych.
wykonania tego zadania: przy wykorzystaniu rodzimego sterownika PHP oraz przy użyciu
biblioteki PDO niezależnej od stosowanej bazy danych.
W rozdziale 12. przedstawimy zaawansowane sposoby administrowania serwerem MySQL,
w tym również szczegółowe informacje na temat systemu uprawnień, bezpieczeństwa
serwera i jego optymalizacji.
W rozdziale 13. bardziej szczegółowo omówimy mechanizmy składowania danych
wchodzące w skład serwera MySQL, w tym również transakcje, przeszukiwanie
pełnotekstowe oraz procedury składowane.
Tabele
Relacyjne bazy danych składają się z relacji, zwanych zazwyczaj tabelami. Tabela jest dokładnie tym,
co oznacza — tabelą danych. Jeśli używaliście arkusza kalkulacyjnego, używaliście również tabeli.
Rozważmy następujący przykład.
Spójrzmy na przykładową tabelę przedstawioną na rysunku 8.1. Zawiera ona nazwiska i adresy
klientów księgarni o nazwie „Książkorama”.
Rysunek 8.1.
Informacje na temat
klientów „Książkoramy”
są przechowywane
w tabeli
Tabela posiada nazwę (Klienci), kilka kolumn, z których każda zawiera inny rodzaj danych, oraz
wiersze odpowiadające poszczególnym klientom.
Kolumny
Każda kolumna tabeli posiada wyróżniającą ją nazwę i zawiera inny rodzaj danych. Ponadto każdej
kolumnie przypisany jest typ danych. Na przykład z tabeli Klienci na rysunku 8.1 można wywnio-
skować, iż KlientID jest typu całkowitoliczbowego, natomiast pozostałe trzy kolumny zawierają
ciągi znaków. Kolumny są czasem nazywane polami lub atrybutami.
Wiersze
Każdy wiersz tabeli odpowiada innemu klientowi. Format tabelaryczny powoduje, że każdy wiersz
ma te same atrybuty. Wiersze są również nazywane rekordami lub krotkami.
Rozdział 8. Projektowanie internetowej bazy danych 217
Wartości
Każdy wiersz zawiera zbiór pojedynczych wartości odpowiadających kolumnom. Każda z tych
wartości musi być tego samego typu, jaki przypisano kolumnie, w której się znajduje.
Klucze
Konieczne jest znalezienie sposobu jednoznacznej identyfikacji każdego klienta. Nazwiska nie
na wiele się tu zdadzą — łatwo zgadnąć, dlaczego, szczególnie w przypadku popularnych. Roz-
patrzmy przykład Julii Kowalskiej z tabeli Klienci: w książce telefonicznej figuruje mnóstwo
osób o tym nazwisku.
Moglibyśmy wyróżnić Julię na kilka sposobów. Zapewne jest ona jedyną osobą o tym nazwisku
mieszkającą pod przypisanym jej adresem, jednak mówienie o „Julii Kowalskiej z ulicy Wierz-
bowej 25 w Warszawie” jest dosyć niewygodne i brzmi chyba za bardzo urzędowo. Ponadto
metoda ta wymaga korzystania z więcej niż jednej kolumny tabeli.
Metoda, którą posłużyliśmy się w tym przykładzie i którą zapewne większość Czytelników będzie
wykorzystywać w swoich aplikacjach, polega na dodaniu unikalnego pola KlientID. Na tej samej
zasadzie jest nadawany numer konta w banku czy numer członkostwa w klubie. Dzięki temu
przechowywanie szczegółowych danych w bazie staje się znacznie prostsze. Sztucznie nadawany
numer identyfikacyjny gwarantuje unikalność poszczególnych rekordów, co rzadko zapewniają
nam prawdziwe dane, a nawet ich kombinacje.
Pole identyfikujące poszczególne rekordy nazywane jest kluczem bądź też kluczem podstawowym.
Klucz może się też składać z kilku pól. Jeśli na przykład zdecydowalibyśmy się wyróżniać Julię
jako „Julia Kowalska z ulicy Wierzbowej 25 w Warszawie”, to klucz składałby się z pól Nazwisko,
Adres i Miejscowosc, co jednak nie gwarantowałoby unikalności.
Bazy danych składają się najczęściej z więcej niż jednej tabeli i wykorzystują klucze do uwidocz-
nienia odwołań między dwiema oddzielnymi tabelami. Rysunek 8.2 przedstawia drugą tabelę,
którą dołączyliśmy do naszej bazy danych. W nowej tabeli przechowywane są informacje doty-
czące zamówień złożonych przez klientów księgarni. Każdy wiersz tabeli Zamowienia odpowiada
jednemu zamówieniu dokonanemu przez jednego klienta. Tabela ta zawiera również kolumnę
KlientID, dzięki czemu wiemy, kim jest klient, który złożył dane zamówienie. Przeanalizujmy
zamówienie, dla którego ZamowienieID jest równe 2: widzimy, iż zostało ono złożone przez klienta,
którego KlientID wynosi 1. Z tabeli Klienci wynika, iż tym klientem jest Julia Kowalska.
218 Część II Stosowanie MySQL
Rysunek 8.2.
Każde zamówienie
w tabeli Zamowienia
zawiera odwołanie
do klienta z tabeli
Klienci
Relację tego typu określa się mianem klucza obcego. KlientID jest kluczem podstawowym w tabeli
Klienci, lecz jeśli pole to pojawi się również w innej tabeli, na przykład Zamowienia, to nosi nazwę
klucza obcego.
Można by zadać pytanie, dlaczego zostały utworzone dwie oddzielne tabele, zamiast zapamiętywać
adres Julii w tabeli Zamowienia. Odpowiedzi udzielimy w następnym podrozdziale.
Schematy
Zbiór projektów wszystkich tabel wchodzących w skład bazy danych nazywamy schematem bazy
danych — jest to całościowy projekt bazy. Schemat powinien zawierać projekty tabel wraz z ozna-
czeniem kolumn, typów danych przypisanych poszczególnym kolumnom oraz wskazywać klucze
podstawowe i klucze obce. Generalnie schemat nie powinien zawierać żadnych danych, można
jednak przedstawić przykładowe dane w celu lepszego objaśnienia projektu. Schematy baz danych
mogą mieć formę nieformalnych diagramów, takich jak na rysunkach 8.1 i 8.2, diagramów encji
i relacji (tym rodzajem diagramów nie będziemy się zajmować) bądź też formę tekstową, np.:
Klienci(KlientID, Nazwisko, Adres, Miasto)
Zamowienia(ZamowienieID, KlientID, Wartosc, Data)
Pogrubienie nazwy kolumny oznacza, iż będą w niej zapamiętywane klucze podstawowe tabeli,
w której ta kolumna się znajduje. Natomiast kursywą wyróżnione są te kolumny, w których zapa-
miętywane będą klucze obce.
Relacje
Klucze obce ukazują relacje pomiędzy danymi z dwóch różnych tabel. Na przykład połączenie
tabeli Zamowienia z tabelą Klienci odzwierciedla relację między konkretnym wierszem tabeli
Zamowienia i określonym wierszem tabeli Klienci.
Teoria relacyjnych baz danych uwzględnia istnienie trzech typów relacji. Są one klasyfikowane
zależnie od liczby wartości, które mogą wystąpić po każdej stronie relacji. Wyróżnia się więc relacje
„jeden do jednego”, „jeden do wielu” i „wiele do wielu”.
Rozdział 8. Projektowanie internetowej bazy danych 219
Relacja „jeden do jednego” oznacza, iż po każdej stronie może występować tylko jedna wartość.
Na przykład gdybyśmy wydzielili z tabeli Klienci odrębną tabelę zawierającą adresy, wówczas
byłyby one powiązane relacją „jeden do jednego”. Tabela Klienci mogłaby więc posiadać klucz
obcy z tabeli Adresy lub odwrotnie.
W relacji „jeden do wielu” jeden wiersz tabeli jest połączony z jednym wierszem lub wieloma
wierszami drugiej. W naszym przykładzie jeden klient może złożyć kilka zamówień. W przypadku
tego typu relacji tabela z wieloma wierszami będzie zawierać kolumnę z kluczem obcym pochodzą-
cym z tabeli z jednym wierszem. Dlatego też w tabeli Zamowienia umieściliśmy kolumnę KlientID
w celu zastosowania tej relacji.
W przypadku relacji „wiele do wielu” wiele wierszy jednej tabeli jest połączonych z wieloma
wierszami innej. Gdybyśmy na przykład mieli dwie tabele: Ksiazki i Autorzy, mogłaby zaist-
nieć sytuacja, w której jedna z książek miałaby dwóch współautorów, przy czym każdy z nich
mógłby być również autorem lub współautorem innych książek. Tego typu relacje są zazwyczaj
upraszczane poprzez utworzenie dodatkowej, trzeciej tabeli. Mielibyśmy więc tabele Ksiazki,
Autorzy oraz Autorzy_Ksiazki. Ta ostatnia zawierałaby tylko pary kluczy obcych pochodzących
z dwóch pozostałych tabel i określałaby, jaki autor napisał (samodzielnie lub z innym autorem)
daną książkę.
Ogólnie można przyjąć, iż każda klasa modelowanych obiektów świata realnego powinna mieć
odpowiadającą jej tabelę. Chcemy na przykład przechowywać informacje o wszystkich klientach
naszej księgarni. Jeśli istnieje zbiór danych mający tę samą strukturę, można utworzyć tabelę odpo-
wiadającą tym danym.
Na podstawie tak opisanego zbioru informacji łatwo zatem zauważyć, iż w projektowanej bazie
są potrzebne co najmniej trzy tabele: Klienci, Zamowienia oraz Ksiazki. Początkowy schemat tej
bazy danych jest przedstawiony na rysunku 8.3.
220 Część II Stosowanie MySQL
Rysunek 8.3.
Schemat początkowy
zawiera tabele Klienci,
Zamowienia i Ksiazki
Na tym etapie nie jesteśmy w stanie określić na podstawie modelu, które książki zostały zamówione
i w jakim zamówieniu. Zajmiemy się tym w następnym podrozdziale.
Jeśli Julia często będzie zamawiać książki w księgarni „Książkorama”, na co oczywiście liczymy,
wówczas jej dane zostaną zapisane wiele razy. Tabela Zamowienia wyglądałaby więc tak jak na
rysunku 8.4.
Rysunek 8.4.
Baza danych
przechowująca dane
redundantne zajmuje
znacznie więcej pamięci
i może powodować
przekłamania danych
Tak zaprojektowana tabela może stać się źródłem problemów dwojakiego rodzaju:
Marnotrawstwo pamięci. Po co mamy zapisywać dane Julii czterokrotnie, jeśli wystarczy,
że zapiszemy je tylko raz?
Rozdział 8. Projektowanie internetowej bazy danych 221
Możliwość powstania tzw. anomalii uaktualniania, czyli sytuacji, w której zmiana danych
zawartych w bazie prowadzi do utraty ich spójności. Naruszona zostaje integralność danych
i nie mamy pewności, które dane są poprawne, a które nie. Skutkuje to zazwyczaj utratą
informacji.
Należy unikać trzech rodzajów anomalii uaktualniania bazy danych: modyfikacji, wstawiania
i usuwania.
Jeśli Julia zmieni miejsce zamieszkania po złożeniu zamówienia, a przed jego realizacją, wówczas
należy uaktualnić jej adres w czterech miejscach bazy zamiast w jednym, co wymaga cztero-
krotnie więcej pracy. Nietrudno jest przeoczyć ten fakt i dokonać uaktualnienia tylko w jednym
miejscu, a w konsekwencji doprowadzić do utraty spójności danych (niedopuszczalne!). Zagrożenia
tego typu nazywane są anomaliami modyfikacji, gdyż pojawiają się przy próbie dokonania zmian
w bazie danych.
Gdy mamy tak zaprojektowaną bazę danych, będziemy musieli wstawić dane Julii, ilekroć złoży ona
zamówienie. Za każdym razem trzeba będzie również sprawdzić spójność tych danych z wcześniej
zapisanymi do bazy. Niedopełnienie tego obowiązku może prowadzić do sytuacji, w której dane
dotyczące adresu Julii, zawarte w dwóch różnych wierszach, są ze sobą sprzeczne. W jednym wier-
szu na przykład może znaleźć się informacja, iż Julia mieszka w Warszawie, inny natomiast wska-
zywałby, że jej miejscem zamieszkania jest np. Wrocław. Sytuacja taka jest nazywana anomalią
wstawiania, gdyż pojawia się przy zapisywaniu nowych danych do bazy.
Powinniśmy zatem projektować bazy danych w taki sposób, aby nie wystąpiła żadna z powyższych
anomalii.
Jednym z rozwiązań może być dodanie kolumny do tabeli Zamowienia, w której zapisywane będą
wszystkie zamówione książki. Przypadek taki jest przedstawiony na rysunku 8.5.
Rysunek 8.5.
W tym przypadku pole
Ksiazki_zamowione
zawiera wartości
wielokrotne
Ten pomysł jest wadliwy z kilku powodów. Prowadzi on bowiem do zagnieżdżania w jednej kolum-
nie całej tabeli wiążącej zamówienia z książkami. Zastosowanie tej metody znacznie utrudni zna-
lezienie odpowiedzi na pytanie typu: „Ile egzemplarzy książki Java 2 dla profesjonalistów zostało
222 Część II Stosowanie MySQL
zamówionych?”. System nie może po prostu zliczyć odpowiednich pól, lecz musi przeanalizować
wartość każdego atrybutu, aby sprawdzić, czy zawiera on szukane wartości.
Zatem tak naprawdę tworzymy tabelę w tabeli. Zamiast tego powinniśmy utworzyć nową tabelę
o nazwie Pozycje_zamowione. Jest ona pokazana na rysunku 8.6.
Rysunek 8.6.
Tak zaprojektowana
tabela ułatwia szukanie
zamówionych książek
Nowa tabela jest łącznikiem między tabelami Zamowienia i Ksiazki. Tabela tego typu jest wyko-
rzystywana najczęściej w przypadku istnienia pomiędzy dwoma obiektami relacji „wiele do
wielu” — w rozważanym przypadku jedno zamówienie może opiewać na kilka książek, a każda
książka może być zamówiona przez wielu klientów.
Kiedy trzeba rozwiązać problem, który naprawdę wymaga korzystania z nieatomowych wartości
kolumn, to warto rozważyć zastosowanie bazy danych przeznaczonej do przechowywania wartości
tego typu, a nie relacyjnej bazy danych. Takie bazy danych nie są bazami relacyjnymi i często
określa się je jako bazy NoSQL lub magazyny danych (ang. datastore). (Tanie nierelacyjne magazy-
ny danych nie będą opisywane w tej książce).
Rysunek 8.7.
W celu przechowywania
recenzji możemy dodać
kolumnę Recenzja
do tabeli Ksiazki lub
utworzyć dodatkową
tabelę Recenzje_ksiazek
Pierwszy sposób polega na dodaniu kolumny Recenzja do tabeli Ksiazki. Każda książka będzie
więc miała dodatkowe pole Recenzja. Jeśli baza danych przechowuje dane wielu książek, a recenzent
ocenia tylko niektóre z nich, wówczas w znacznej liczbie wierszy atrybut ten nie ma żadnej war-
tości. Pola mają wówczas wartość NULL.
Występowanie wielu pustych pól w bazie danych jest niewskazane. Prowadzi to do marnotrawstwa
pamięci, może być także przyczyną błędnych wyników zwracanych przez funkcje sumujące
wartości pól lub inne funkcje obliczeniowe. Użytkownik, widząc puste pole, nie jest pewien, czy
sytuacja ta jest spowodowana nieprawidłowością atrybutu, błędem w bazie danych, czy też po prostu
brakiem danych w bazie.
Można uniknąć problemów związanych z występowaniem wielu pustych pól, stosując drugi ze
sposobów pokazanych na rysunku 8.7. W tym przypadku tabela Recenzje_ksiazek zawiera tylko
te pozycje, które zostały już ocenione, oraz samą recenzję.
Zauważmy, iż założeniem tego projektu jest istnienie pracującego dla naszej księgarni recenzenta,
czyli inaczej mówiąc, obecność relacji jeden do jednego między tabelami Ksiazki i Recenzje_ksiazek.
Gdybyśmy chcieli przechowywać wiele recenzji jednej książki, mielibyśmy do czynienia z relacją
jeden do wielu, a więc trzeba by zastosować drugie rozwiązanie. Ponadto mając tylko jedną recenzję
każdej książki jako klucz główny tabeli Recenzje_ksiazek, można by wykorzystać numer ISBN.
W przypadku wielu recenzji niezbędne jest wprowadzenie unikatowego identyfikatora każdej z nich.
Podstawowa operacja wykonywana przez serwer WWW jest pokazana na rysunku 8.8. Przed-
stawiony system składa się z dwóch obiektów: przeglądarki internetowej oraz serwera WWW,
pomiędzy którymi wymagane jest istnienie połączenia umożliwiającego komunikację. Przeglądarka
wysyła żądanie do serwera, serwer natomiast przesyła odpowiedź. Taka architektura sprawdza się
w przypadku przesyłania przez serwer stron statycznych. System dostarczający przeglądarce strony
WWW oparte na bazie danych ma bardziej złożoną konstrukcję.
Rysunek 8.8.
Relacja klient-serwer między
przeglądarką i serwerem WWW
wymaga komunikacji
Internetowa aplikacja bazodanowa, którą utworzymy, będzie się opierać na klasycznej architekturze
internetowej bazy danych, pokazanej na rysunku 8.9. Większość przedstawionych obiektów zapew-
ne jest już znana Czytelnikom.
Rysunek 8.9. Relacja klient – serwer pomiędzy przeglądarką WWW a serwerem WWW wymaga ich
wzajemnej komunikacji
Proces ten przebiega zawsze tak samo, niezależnie od typu interpretera skryptów czy używanego
serwera bazy danych. Czasami serwer WWW, interpreter PHP oraz serwer bazy danych pracują na
tym samym komputerze, nierzadko jednak spotyka się rozwiązania, w których serwer bazy danych
działa na oddzielnej maszynie. Metodę tę stosuje się najczęściej do celów bezpieczeństwa lub dla
Rozdział 8. Projektowanie internetowej bazy danych 225
zmniejszenia obciążenia komputerów. Z punktu widzenia projektanta fakt ten nie zmieni sposobu
jego pracy, może jednak przyczynić się do istotnego wzrostu wydajności całego systemu.
Wzrost rozmiaru i złożoności aplikacji prowadzi do tego, że zaczyna dzielić się je na warstwy.
Zazwyczaj warstwa bazodanowa stanowi interfejs do bazy MySQL, druga warstwa realizuje logikę
biznesową i jest tak naprawdę jądrem całej aplikacji, z kolei warstwa prezentacji jest odpowie-
dzialna za generowanie wynikowego kodu HTML. Nie zmienia to jednak faktu, że punktem wyjścia
jest architektura przedstawiona na rysunku 8.9 — rozszerzeniu ulega jedynie kod źródłowy PHP.
W następnym rozdziale
W następnym rozdziale zaczniemy tworzyć bazę danych opartą na MySQL. Najpierw przedsta-
wimy kolejne etapy tworzenia bazy danych udostępnianej w internecie, a następnie sposób prze-
kazywania zapytań do bazy oraz metody wysyłania zapytań z poziomu skryptów PHP.
226 Część II Stosowanie MySQL
Rozdział 9.
Tworzenie internetowej
bazy danych
W rozdziale tym przedstawimy sposób tworzenia bazy danych MySQL, przeznaczonej do udo-
stępniania na stronach WWW.
Również i w tym rozdziale posłużymy się znanym już przykładem księgarni internetowej „Książko-
rama”. Przypomnijmy schemat jej bazy danych:
Klienci(KlientID, Nazwisko, Adres, Miejscowosc)
Zamowienia (ZamowienieID, KlientID, Wartosc, Data)
Ksiazki (ISBN, Autor, Tytul, Cena)
Pozycje_zamowione (ZamowienieID, ISBN, Ilosc)
Recenzje_ksiazek (ISBN, Recenzja)
W celu przeanalizowania materiału zawartego w tym rozdziale należy zapewnić sobie dostęp do
serwera MySQL. Oznacza to, że trzeba przeprowadzić instalację MySQL na serwerze WWW,
obejmującą następujące czynności:
instalację plików,
rejestrację użytkownika na serwerze MySQL, jeśli jest to wymagane przez używany
system operacyjny,
zdefiniowanie odpowiedniej ścieżki dostępu,
uruchomienie aplikacji mysql_install_db, jeśli jest to wymagane przez używany
system operacyjny,
określenie hasła administratora,
228 Część II Stosowanie MySQL
Po wykonaniu wymienionych czynności można przystąpić do lektury tego rozdziału. Jeśli jednak
instalacja nie powiodła się, należy kierować się instrukcjami zawartymi w dodatku A.
Ewentualne błędy, które mogą wystąpić w trakcie wykonywania czynności opisywanych w tym
rozdziale, spowodowane będą prawdopodobnie nieprawidłową pracą serwera MySQL. W takim
przypadku należy ponownie przeanalizować czynności opisane powyżej oraz przedstawione
w dodatku A w celu upewnienia się, iż MySQL został prawidłowo zainstalowany.
Może się zdarzyć, że Czytelnik będzie miał dostęp do serwera MySQL zainstalowanego na kompu-
terze, którym Czytelnik nie zarządza, na przykład udostępnianym przez dostawcę usług interne-
towych na zdalnym komputerze, w miejscu pracy itp.
W takim przypadku wykonanie zawartych w tym rozdziale przykładów lub utworzenie własnej
bazy danych będzie wymagało zarejestrowania przez administratora odpowiedniego użytkownika
oraz znajomości jego nazwy, hasła i nazwy przypisanej mu bazy danych. Fragmenty tego rozdziału
poświęcone konfiguracji użytkowników i baz danych można pominąć lub przeczytać, by móc
lepiej wyjaśnić administratorowi przedstawione wcześniej wymagania. Zwykli użytkownicy nie
będą wszakże mogli samodzielnie tworzyć własnych użytkowników i baz danych.
Wszystkie przykłady zawarte w tym rozdziale były uruchamiane na serwerze MySQL Community
Edition w wersji 5.6, która w trakcie pisania tej książki była wersją najnowszą. Niektóre wcze-
śniejsze wersje serwera posiadają nieco mniejsze możliwości, dlatego też przed rozpoczęciem
lektury należy zastąpić starszą wersję nową instalacją MySQL. Najnowszą wersję serwera MySQL
można pobrać ze strony http://www.mysql.com.
W książce komunikację z serwerem MySQL prowadzimy przy użyciu klienta wiersza poleceń
o nazwie MySQL monitor, stanowiącego składnik każdej instalacji. Nic jednak nie stoi na prze-
szkodzie, by używać innego klienta. Jeśli na przykład używany serwer MySQL znajduje się na
dzierżawionym serwerze WWW, administratorzy systemu zwykle udostępniają użytkownikom
działający przez przeglądarkę interfejs phpMyAdmin. Różne klienty GUI działają nieco inaczej,
niż zostanie to przedstawione w tym rozdziale, lecz odpowiednie zaadaptowanie przedstawionych
instrukcji nie powinno sprawić większego kłopotu.
Opuszczenie znaku średnika spowoduje, że nastąpi przejście do nowego wiersza przed zakończeniem
wpisywania tekstu komendy. Pozwoliło to nam na zwiększenie czytelności przykładów. Odnalezie-
nie miejsca, w którym nastąpiło przejście do nowego wiersza, nie powinno sprawić żadnych proble-
mów — MySQL wyświetli tam znak kontynuacji w formie strzałki przedstawionej poniżej:
mysql> grant select
->
Rozdział 9. Tworzenie internetowej bazy danych 229
Symbol ten oznacza, że MySQL czeka na wprowadzenie dalszej części polecenia. Za każdym
razem, gdy zostanie naciśnięty klawisz Enter bez wcześniejszego wpisania znaku średnika, nastąpi
przejście do nowego wiersza i wyświetlenie symbolu strzałki.
Warto również zapamiętać, że MySQL nie rozróżnia małych i wielkich liter w poleceniach języka
SQL, może natomiast je rozróżniać w nazwach baz danych i tabel (kwestia ta zostanie poruszona
w dalszej części rozdziału).
Polecenie mysql wywołuje monitor MySQL, mający postać wiersza poleceń łączącego klienta
z serwerem.
Opcji –h używa się w celu wskazania komputera, do którego ma nastąpić połączenie (na maszynie
tej pracuje serwer MySQL). Jeśli polecenie to ma być wykonane na tym samym komputerze, na
którym znajduje się MySQL, to opcja –h oraz parametr nazwa_komputera mogą zostać pominięte.
W przeciwnym wypadku konieczne jest wpisanie w miejsce parametru nazwa_komputera nazwy
maszyny, na której uruchomiony jest serwer bazy danych.
Opcja –u jest stosowana w celu wskazania identyfikatora użytkownika, na którego następuje logo-
wanie. Jeśli identyfikator ten nie zostanie podany, wówczas MySQL domyślnie użyje identyfikatora
tego użytkownika, który jest aktualnie zalogowany do systemu operacyjnego.
Jeśli MySQL został zainstalowany na komputerze lub serwerze czytelnika, będzie on zmuszony
zalogować się jako użytkownik root oraz samodzielnie utworzyć bazę danych, którą będzie wyko-
rzystywał w trakcie uruchamiania zawartych w tym rozdziale przykładów. Przy pierwszym uru-
chomieniu serwera użytkownik root jest jedynym zarejestrowanym użytkownikiem. W trakcie
pracy na serwerze zainstalowanym na komputerze administrowanym przez inną osobę należy przy
logowaniu użyć identyfikatora użytkownika, podanego przez administratora.
Opcja –p informuje serwer o logowaniu się z użyciem hasła. Może ona zostać pominięta, jeśli dla
danego użytkownika hasło nie zostało ustanowione.
Jeżeli czytelnik loguje się jako użytkownik root bez konieczności podania hasła, zalecane jest
określenie hasła dostępu zgodnie z instrukcjami zawartymi w dodatku A. Jego brak powoduje, iż
system będzie niedostatecznie zabezpieczony.
Podawanie hasła w tym samym wierszu co polecenie mysql nie jest obowiązkowe. Jeżeli nie zostanie
ono podane, to serwer MySQL sam o nie „zapyta”. Tak naprawdę pominięcie hasła jest właściwie
lepszym rozwiązaniem, gdyż wpisane w wierszu poleceń będzie miało formę zwykłego tekstu,
co umożliwi osobom postronnym jego wykrycie.
(Jeżeli polecenie to nie zadziała, trzeba sprawdzić, czy serwer MySQL jest uruchomiony, a komenda
mysql może być zrealizowana w bieżącej ścieżce dostępu).
230 Część II Stosowanie MySQL
Następnie należy podać hasło dostępu. Jeśli wszystko przebiegnie prawidłowo, powinien zostać
wyświetlony komunikat podobny do poniższego:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 559
Server version: 5.6.19-log MySQL Community Server (GPL)
Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
Jeżeli czytelnik pracuje na własnym komputerze i komunikat nie będzie przypominał powyższego,
wówczas należy upewnić się, czy uruchomiona jest aplikacja mysql_install_db (jeżeli jest wyma-
gana) i czy zostało określone oraz wpisane prawidłowo hasło dostępu dla użytkownika root. Jeśli
praca odbywa się na zdalnej maszynie, trzeba upewnić się, iż podane hasło jest prawidłowe.
Po zalogowaniu się do serwera kursor powinien znajdować się przy znaku zachęty, umożliwiając
stworzenie nowej bazy danych. Czytelnik pracujący na własnym komputerze powinien stosować
się do instrukcji podanych w dalszej części rozdziału. Czytelnik korzystający z maszyny admi-
nistrowanej przez inną osobę powinien mieć utworzoną i przypisaną mu bazę danych, może
więc od razu przejść do lektury podrozdziału „Używanie odpowiedniej bazy danych”. Można
oczywiście zapoznać się z poprzedzającymi go podrozdziałami, nie będzie jednak możliwości
(a przynajmniej nie powinno być) uruchamiania wyszczególnionych w nich poleceń.
Tworzenie bazy danych to czynność najprostsza. W wierszu poleceń MySQL należy wpisać:
mysql> create database nazwa_bazy;
W miejsce nazwa_bazy należy podać nazwę bazy danych, która ma zostać utworzona. W naszym
przykładzie jest to baza o nazwie Ksiazki.
I to wszystko! Otrzymana odpowiedź powinna wyglądać mniej więcej tak (czas wykonania polece-
nia może się różnić):
Query OK, 1 row affected (0.06 sec)
Oznacza to, że operacja została wykonana bez błędów. Jeśli taki komunikat się nie pojawi, należy
upewnić się, że na końcu komendy został wpisany znak średnika. Informuje on serwer MySQL
o zakończeniu wpisywania polecenia i o konieczności jego wykonania.
Dla każdego użytkownika, który będzie pracował w systemie, trzeba utworzyć konto i nadać mu hasło
dostępu. Nie muszą one być takie same, jak identyfikator użytkownika i hasło wykorzystywane do za-
logowania się do systemu operacyjnego. Ta sama zasada odnosi się do użytkownika root. Pożądane
jest stosowanie różnych haseł dostępu w razie logowania się do systemu operacyjnego i serwera
MySQL, szczególnie w przypadku użytkownika root.
Nadawanie hasła nowym użytkownikom nie jest obowiązkowe, jednak ze względów bezpieczeń-
stwa pożądane jest, aby każdy użytkownik posiadał własne hasło dostępu. W przypadku tworzenia
internetowej bazy danych wskazane jest utworzenie co najmniej jednego użytkownika, który będzie
wykorzystywany tylko przez daną aplikację bazodanową. Można by zadać pytanie o cel tej operacji.
Odpowiedź związana jest z systemem przywilejów.
Zasadę tę należy stosować wszędzie, nie tylko w odniesieniu do MySQL. Na przykład do wysłania
zapytania do bazy danych ze strony WWW użytkownik nie będzie potrzebował wszystkich przywi-
lejów, które posiada użytkownik root. Należy więc utworzyć nowego użytkownika i nadać mu
tylko przywileje niezbędne do uzyskania dostępu do bazy danych.
W tym rozdziale zostaną opisane pierwsze cztery z nich. Uprawnienia związane z procedu-
rami składowanymi zostaną opisane w rozdziale 13., „Zaawansowane programowanie w MySQL”.
Z kolei uprawnienia związane z użytkownikami pośredniczącymi w ogóle nie zostaną dokładniej
232 Część II Stosowanie MySQL
opisane w tej książce, gdyż są one stosowane bardzo rzadko. Informacje na ich temat można
znaleźć w dokumentacji MySQL.
Polecenie CREATE USER, jak można się spodziewać, tworzy użytkownika. Ogólna postać tego pole-
cenia została przedstawiona poniżej:
CREATE USER identyfikator_uzytkownika
IDENTIFIED BY [PASSWORD] haslo | IDENTIFIED WITH [wtyczka_uwierzytelniania]
[AS lancuch_uwierzytelniania]
Nazwa użytkownika to nazwa, której użytkownik chce używać podczas logowania się do serwera
MySQL. Trzeba pamiętać, że nie musi ona być taka sama jak nazwa użytkownika stosowana do
logowania się w systemie operacyjnym. Identyfikator użytkownika może także zawierać informacje
o nazwie komputera. Można jej używać do odróżniania użytkownika laura (rozumianego jako
laura@localhost) od użytkownika laura@inny.host.com. To bardzo przydatna możliwość, gdyż
użytkownicy z różnych domen mogą mieć takie same imiona. Co więcej, określanie komputera
zwiększa także bezpieczeństwo, bo pozwala określać, z którego komputera dany użytkownik może
się zalogować, a nawet do których tablic i kolumn będzie on miał dostęp z konkretnej lokalizacji.
Element haslo powinien określać hasło, którego użytkownik będzie używał do zalogowania się
na serwerze MySQL. Oczywiście obowiązują tu standardowe reguły związane z określaniem ha-
seł. Zagadnieniom bezpieczeństwa poświęcimy nieco więcej uwagi w dalszej części książki; na
pewno trzeba jednak zadbać o to, by hasła nie dało się łatwo odgadnąć. Oznacza to, że hasło nie
może być słowem pochodzącym ze słownika ani nie może być takie samo jak nazwa użytkownika.
W optymalnym przypadku powinno ono stanowić kombinację małych i wielkich liter oraz znaków,
które nie są literami.
Począwszy od wersji MySQL 5.5.7, jako alternatywę dla hasła można stosować tak zwaną wtycz-
kę uwierzytelniania (ang. authentication plugin). W tym celu trzeba skorzystać z alternatywnej
składni polecenia: IDENTIFIED WITH [wtyczka_uwierzytelniania]. Stosowanie wtyczek uwierzytel-
niania nie zostanie opisane w niniejszej książce, ale zostało ono omówione w dokumentacji MySQL.
Polecenie GRANT służy do określania uprawnień użytkowników. Poza tym, jeśli konto użytkownika
jeszcze nie istnieje, wydanie polecenia GRANT spowoduje jego utworzenie. Oznacza to, że w celu
uproszczenia procedury tworzenia użytkownika można się ograniczyć do zastosowania samego
polecenia GRANT.
Parametr kolumny jest opcjonalny. Używa się go w celu wskazania kolumn, do których podane
przywileje zostaną zastosowane. Można podać nazwę pojedynczej kolumny lub listę nazw kolumn
oddzielonych przecinkami.
Parametr obiekt wskazuje bazę lub tabelę, do której zostaną zastosowane podane przywileje. W celu
nadania przywilejów na wszystkie bazy danych w systemie obiekt powinien przyjąć wartość *.*.
Wówczas przywileje zostały nadane na poziomie globalnym. Ten sam efekt można osiągnąć, przypi-
sując parametrowi obiekt wartość *, jeśli nie jest używana żadna baza danych. Częściej jednak wskazuje
się wszystkie tabele w bazie, wpisując nazwa_bazy.*, konkretną tabelę (nazwa_bazy.nazwa_tabeli)
lub pojedyncze kolumny w danej tabeli poprzez wpisanie nazwa_bazy.nazwa_tabeli i nadanie odpo-
wiedniej wartości parametrowi kolumny. Za pomocą tych metod nadaje się przywileje na pozo-
stałych trzech poziomach uprzywilejowania, odpowiednio: baza danych, tabela i kolumna. Jeżeli
w trakcie wykonywania tego polecenia używana jest konkretna baza danych, to podanie samej
nazwy tabeli będzie zinterpretowane jako nadanie przywilejów tabeli o tej nazwie, znajdującej się
w używanej bazie danych.
Klauzula REQUIRE pozwala wskazać, że użytkownik musi łączyć się poprzez protokół Secure Sockets
Layer (SSL), a także wskazać opcje SSL. Więcej informacji na temat połączeń z serwerem MySQL
za pośrednictwem SSL można znaleźć w podręczniku MySQL.
Dodanie opcji WITH GRANT OPTION spowoduje, że wskazany użytkownik będzie mógł nadawać innym
użytkownikom takie przywileje, jakie sam posiada.
lub
MAX_UPDATES_PER_HOUR n
lub
MAX_CONNECTIONS_PER_HOUR n
lub
MAX_USER_CONNECTIONS n
Dzięki tym klauzulom można ograniczyć liczbę zapytań, uaktualnień lub połączeń, jakie jeden
użytkownik może zrealizować w ciągu godziny, bądź też liczbę jednoczesnych połączeń, jakie dany
użytkownik może otworzyć. Rozwiązanie to może przydać się do ograniczania obciążenia systemów
współużytkowanych generowanego przez jednego użytkownika.
Informacje dotyczące nadanych uprawnień zapamiętywane są w sześciu tabelach systemowych,
znajdujących się w bazie danych o nazwie mysql. Tabele te noszą nazwy: mysql.user, mysql.db,
mysql.host, mysql.tables_priv, mysql.columns_priv oraz mysql.procs_priv. Zamiast wykorzysty-
wać polecenie GRANT, można zmieniać dane bezpośrednio w wymienionych tabelach. Szczegółowe
informacje na temat sposobu działania tych tabel oraz metod wprowadzania zmian bezpośrednio
w nich zostaną przedstawione w rozdziale 12.
Użytkownikom należy nadawać takie przywileje, które umożliwią korzystanie tylko z potrzebnych
im baz i tabel. Nikomu, z wyjątkiem administratora, nie należy udostępniać systemowej bazy mysql,
gdyż w niej właśnie przechowywane są identyfikatory wszystkich użytkowników, ich hasła dostępu
itp. (szerzej zagadnienie to jest opisane w rozdziale 12.).
Oprócz przywilejów opisanych w tabeli 9.1 stosowany jest także przywilej GRANT, nadawany częściej
poprzez dodanie opcji WITH GRANT OPTION niż przez dołączanie go do listy wartości parametru
przywileje.
Przywilej Opis
CREATE TABLESPACE Pozwala administratorowi na tworzenie, modyfikowanie i usuwanie przestrzeni tabel
CREATE USER Pozwala administratorowi na tworzenie użytkowników (w sposób pokazany wcześniej)
CREATE TEMPORARY TABLES Pozwala administratorowi na używanie słowa kluczowego TEMPORARY w instrukcjach
CREATE TABLE
Przywilej FILE jest dość szczególny. Jest on bardzo przydatny dla użytkowników, gdyż możli-
wość wstawiania danych bezpośrednio z plików do tabel pozwala zaoszczędzić czas, konieczny
na ręczne wpisywanie kolejnych wartości. Z drugiej strony jednak może zostać wykorzystany do
załadowania wszelkich plików, które widzi serwer MySQL, np. pliku bazy należącej do innego
użytkownika czy pliku zawierającego hasła dostępu. Przywilej ten powinien więc być nadawany
z rozwagą lub też administrator powinien sam dokonywać ładowania danych z pliku do wskaza-
nej przez użytkownika tabeli.
Przywilej Opis
ALL Nadaje wszystkie przywileje opisane w tabelach 9.1 i 9.2. Jest on równoważny przywilejowi ALL
PRIVILEGES
USAGE Nie nadaje żadnych przywilejów. Powoduje zarejestrowanie użytkownika i pozwala mu na zalogowanie
się, lecz jakiekolwiek inne czynności są dla niego niedostępne. Odpowiednie przywileje nadawane
są zwykle później. Zastosowanie polecenia GRANT w celu utworzenia użytkownika z przywilejem
USAGE jest odpowiednikiem wykonania polecenia CREATE USER
Polecenie REVOKE
Przeciwieństwem GRANT jest polecenie REVOKE, używane w celu odebrania użytkownikowi okre-
ślonych przywilejów. Jego składnia jest bardzo podobna do składni polecenia GRANT:
REVOKE przywileje [kolumny]
ON obiekt
FROM identyfikator_uzytkownika
Jeżeli do polecenia GRANT dołączono opcję WITH GRANT OPTION, przywilej ten można cofnąć (podob-
nie jak wszystkie inne przywileje) w następujący sposób:
REVOKE ALL PRIVILEGES, GRANT OPTION
FROM identyfikator_uzytkownika
Niekiedy konieczne okazuje się wyeliminowanie tego użytkownika. Można to zrobić w następujący
sposób:
mysql> revoke all privileges, grant option
-> from 'fred';
Po rozmowie z Zosią wiadomo już, w jaki sposób chce korzystać z bazy, można więc nadać jej
odpowiednie przywileje:
mysql> grant select, insert, update, delete, index, alter, create, drop
-> on ksiazki.*
-> to 'zosia'@'localhost';
Jak widać, nie ma już potrzeby podawania hasła dostępu, jakim posługuje się Zosia.
Rozdział 9. Tworzenie internetowej bazy danych 237
Jeżeli administrator dojdzie do wniosku, że Zosia wykonała już część swoich zadań, może ograni-
czyć nadane jej przywileje:
mysql> revoke alter, create, drop
-> on ksiazki.*
-> from 'zosia'@'localhost';
Kiedy Zosia nie będzie już potrzebowała dostępu do bazy danych, można odebrać jej wszystkie
pozostałe przywileje:
mysql> revoke all
-> on ksiazki.*
-> from 'zosia'@'localhost';
Rejestrowanie użytkownika
łączącego się z internetu
Konieczne jest zarejestrowanie użytkownika, który łączy się z bazą danych poprzez stronę WWW
zawierającą skrypty PHP. Również w tym przypadku należy zastosować zasadę najmniejszego
przywileju, trzeba więc rozważyć, jakie operacje ma wykonywać skrypt PHP.
W większości przypadków wystarczą przywileje SELECT, INSERT, DELETE i UPDATE. Nadaje się je
w następujący sposób:
mysql> grant select, insert, delete, update
-> on ksiazki.*
-> to 'ksiazkorama' identified by 'ksiazkorama123';
Oczywiście ze względów bezpieczeństwa hasło powinno być bardziej skomplikowane niż podane
w przykładzie.
Użytkownik korzystający z usług zewnętrznej firmy internetowej otrzyma zapewne szersze przy-
wileje na pracę z przydzieloną mu bazą danych. Identyfikator użytkownika i jego hasło dostępu
będą prawdopodobnie umożliwiały zarówno wydawanie poleceń serwerowi MySQL (tworzenie
tabel itp.), jak i łączenie się z bazą z poziomu strony WWW (np. wyszukiwanie danych w tabeli).
Użycie takiego samego identyfikatora użytkownika i hasła mogłoby obniżyć nieznacznie stopień
zabezpieczenia systemu, zalecane jest więc zarejestrowanie nowego użytkownika z następującym
zbiorem przywilejów:
mysql> grant select, insert, update, delete, index, alter, create, drop
-> on ksiazki.*
-> to 'ksiazkorama' identified by 'ksiazkorama123';
Utwórz więc drugą wersję użytkownika, gdyż będziemy jej potrzebować w następnych sekcjach.
Należy zauważyć, że w tym przypadku nie została określona nazwa komputera. Oczywiście w razie
potrzeby można ją dodać. Dodawana wartość zależy od tego, gdzie będzie działał kod PHP. Jeśli
będzie on wykonywany na tym samym komputerze, to można dodać 'localhost', jeśli natomiast
będzie on wykonywany na innym komputerze, to konieczne będzie dodanie odpowiedniego ad-
resu IP lub nazwy komputera.
Wylogowanie się z serwera MySQL jest dokonywane za pomocą polecenia quit. Zalecane jest
ponowne zalogowanie się jako użytkownik internetowy (zarejestrowany w poprzednim punkcie)
i sprawdzenie poprawności działania bazy danych. Jeżeli wpisana instrukcja GRANT została wyko-
nana, lecz nadal nie można się zalogować, zwykle oznacza to, że w trakcie procesu instalacji nie
238 Część II Stosowanie MySQL
usunięto anonimowego użytkownika. Z powrotem więc trzeba zalogować się jako użytkownik
root i wykonać instrukcje zawarte w dodatku A, zmierzające do usunięcia kont anonimowych. Po
wykonaniu tej operacji nie powinno już być problemów z zalogowaniem się jako użytkownik
internetowy.
Pierwszą czynnością, jaką należy wykonać, jest wskazanie bazy danych, która zostanie wykorzy-
stana. Służy do tego następujące polecenie:
mysql> use nazwa_bazy;
Tę samą operację można wykonać, podając nazwę bazy danych w trakcie logowania się do serwera:
> mysql –D nazwa_bazy –h nazwa_komputera –u identyfikator_uzytkownika –p
Jeżeli przed przystąpieniem do pracy nie zostanie wybrana żadna baza danych, serwer wyświetli
komunikat o błędzie:
ERROR 1046 (3D000): No Database Selected
Niektórzy Czytelnicy mogą już wiedzieć, że MySQL udostępnia więcej niż jeden typ tabel czy
mechanizm przechowywania danych. Więcej na temat typów tabel napiszemy w rozdziale 13.
Na razie wszystkie tabele znajdujące się w naszych bazach danych będą używały domyślnego
mechanizmu przechowywania danych, którym od wersji MySQL 5.5.5 jest InnoDB.
Parametr nazwa_tabeli powinien określać nazwę tabeli, która ma zostać utworzona. Z kolei para-
metr kolumny powinien mieć postać listy kolumn oddzielonych przecinkami, jakie będzie zawierać
nowa tabela. Każda z kolumn musi być określona przez podanie jej nazwy i typu danych.
Na listingu 9.1 przedstawione są polecenia SQL, za pomocą których zostaną utworzone powyższe
tabele (przy założeniu, że istnieje już baza danych Ksiazki). Kod ten znajduje się również w przy-
kładach dołączonych do tej książki, zamieszczonych w katalogu rozdzial_09, w pliku o nazwie
ksiazkorama.sql.
Listing 9.1. ksiazkorama.sql — kod SQL tworzy tabele w bazie danych księgarni „Książkorama”
CREATE TABLE Klienci
( KlientID INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
Nazwisko char(50) NOT NULL,
Adres char(100) NOT NULL,
Miejscowosc char(30) NOT NULL
);
Kod SQL zawarty w pliku ksiazkorama.sql zostanie wykonany po wpisaniu następującego pole-
cenia (zakładamy przy tym, że plik ksiazkorama.sql ma tę samą lokalizację, co aplikacja mysql):
> mysql –h nazwa_komputera –u ksiazkorama –D ksiazki –p < ksiazkorama.sql
(Pamiętaj, by parametr nazwa_komputera zastąpić nazwą swego komputera oraz aby wskazać pełną
ścieżkę dostępu do pliku ksiazkorama.sql).
Mechanizm przekierowania do pliku zawierającego kod SQL jest bardzo poręczny, gdyż pozwala
na edycję kodu za pomocą dowolnego edytora tekstowego przed jego wykonaniem.
240 Część II Stosowanie MySQL
Pojedyncza tabela jest tworzona za pomocą odrębnego polecenia CREATE TABLE. Jak widać, każda
z tabel zawiera kolumny wyszczególnione w schemacie bazy danych, utworzonym w poprzednim
rozdziale. Ponadto każdej kolumnie nadano nazwę oraz przypisano typ danych. Niektóre kolumny
są opisywane przez dodatkowe atrybuty, objaśnione w następnym punkcie.
Atrybut PRIMARY KEY oznacza, że wskazana kolumna jest kluczem podstawowym tabeli, a wartości
w niej zapisywane muszą być unikalne. MySQL automatycznie indeksuje takie kolumny. Zauważ-
my, że w pliku ksiazkorama.sql wszystkim kolumnom z atrybutem AUTO_INCREMENT (np. KlientID
w tabeli Klienci) przypisano również atrybut PRIMARY KEY, dzięki czemu został spełniony wymóg
indeksowania pól z atrybutem AUTO_INCREMENT.
Atrybut PRIMARY KEY może być wyszczególniony zaraz za nazwą nowej kolumny wyłącznie w przy-
padku, gdy tylko ta jedna kolumna jest kluczem podstawowym tabeli. Inny sposób określenia klucza
podstawowego został zaprezentowany dla tabeli Pozycje_zamowione. Wykorzystanie omawianej
metody jest wymuszone tym, że w skład klucza podstawowego tabeli Pozycje_zamowione wchodzi
nie jedna, ale dwie kolumny jednocześnie. (To prowadzi również do utworzenia indeksu bazują-
cego na obydwóch kolumnach).
Na końcu definicji tabeli można także podać klauzulę FOREIGN KEY wraz z nazwą tabeli oraz ko-
lumny. Ograniczenie to oznacza, że wartości zapisywane w podanej tabeli muszą posiadać odpo-
wiedniki we wskazanej kolumnie innej tabeli, tak zwanej tabeli klucza obcego. Przy okazji można
też określić różne operacje, jakie zostaną wykonane w przypadku usunięcia danych, do których
odwołują się wartości z kolumny. Na przykład umieszczenie na końcu zapisu ON DELETE CASCADE
będzie oznaczać, że „jeśli zostanie usunięty wiersz z tabeli klucza obcego, to także w tej tabeli
należy usunąć odpowiednie wiersze”. W powyższym przykładzie zastosowana została jednak
opcja domyślna: RESTRICT. Oznacza ona, że usunięcie wierszy z tabeli klucza obcego lub ich mody-
fikacja nie będą możliwe, jeśli wcześniej nie zostaną wprowadzone w tej tabeli odpowiednie
modyfikacje.
Warto zwrócić uwagę na to, że zapis FOREIGN KEY zostanie uwzględniony wyłącznie w przypadku,
gdy używany mechanizm składowania będzie obsługiwał klucze obce. Jednym z mechanizmów
spełniających ten warunek jest InnoDB. We wcześniejszych wersjach serwera MySQL domyślnym
mechanizmem składowania był MyISAM, który nie obsługiwał kluczy obcych. Więcej informacji
na temat różnych mechanizmów składowania można znaleźć w rozdziale 13.
Typy kolumn
Rozważmy przykład pierwszej utworzonej tabeli:
CREATE TABLE Klienci
( KlientID INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
Nazwisko char(50) NOT NULL,
Adres char(100) NOT NULL,
Miejscowosc char(30) NOT NULL
);
Tworząc nową tabelę, należy każdej jej kolumnie przypisywać odpowiedni typ danych.
Według schematu tabela Klienci składa się z czterech kolumn. Pierwsza z nich, KlientID, jest
kluczem podstawowym o wartościach typu całkowitoliczbowego (typ INT). Co więcej, wszystkie
pola ID (KlientID, ZamowienieID) mogą mieć tylko wartości nieujemne, gdyż nie ma w planach
używania identyfikatorów klientów czy zamówień o wartościach ujemnych. Wykorzystano również
cechy atrybutu AUTO_INCREMENT, dzięki któremu MySQL sam dba o spełnienie powyższych wymo-
gów — mamy więc jeden kłopot z głowy.
Pozostałe kolumny będą przechowywać dane mające postać łańcuchów znaków — nadano im więc
typ CHAR o określonej dla każdej kolumny długości łańcucha, zawartej w nawiasach okrągłych.
Na przykład w polu Nazwisko można zapisywać łańcuchy zawierające nie więcej niż 50 znaków.
Nawet jeśli nazwisko klienta liczy mniej niż 50 liter wartość pola Nazwisko, zawsze będzie zaj-
mować tyle pamięci, ile trzeba do zapisania 50 znaków. MySQL automatycznie doda do nazwiska
tyle znaków spacji, ile brakuje do wykorzystania całej przydzielonej polu pamięci. Alternatywą
dla typu CHAR jest VARCHAR — wartości pól tego typu zajmują zawsze o jeden bajt więcej niż
trzeba do zapamiętania podanego łańcucha. Stosowanie pól typu VARCHAR generalnie oszczędza
więc pamięć, może jednak negatywnie wpływać na wydajność całego systemu.
Większość kolumn została zadeklarowana jako NOT NULL. Zastosowanie takiego niewielkiego roz-
wiązania optymalizacyjnego wszędzie tam, gdzie to możliwe, może nieznacznie polepszyć wydaj-
ność pracy systemu. Zagadnienia dotyczące optymalizacji pracy serwera MySQL zostaną poru-
szone w rozdziale 12.
W obrębie niektórych poleceń CREATE TABLE zawarte są elementy, które nie zostały jeszcze omówio-
ne. Rozważmy kolejny przykład:
CREATE TABLE Zamowienia
( Zamowienieid INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
Klientid INT UNSIGNED NOT NULL,
Wartosc FLOAT(6,2),
Data DATE NOT NULL,
Kolumna Wartosc jest zadeklarowana jako liczba zmiennoprzecinkowa typu FLOAT. Dla większości
typów liczbowych zmiennoprzecinkowych istnieje możliwość określenia maksymalnej liczby
cyfr, jakie będą wyświetlane, oraz ilości miejsc znaczących po przecinku. W przykładzie księgarni
internetowej wartość zamówień jest określana w złotówkach, zadeklarowano więc, iż ma ona nie
więcej niż sześć cyfr plus dwie cyfry znaczące po przecinku.
W rozważanej tabeli wszystkie kolumny, oprócz kolumny Wartosc, są zadeklarowane jako NOT NULL.
Dlaczego? Otóż wpisując nowe zamówienie należy najpierw zapisać je w tabeli Zamowienia,
następnie dodać odpowiednie rekordy do tabeli Pozycje_zamowione, po czym obliczyć wartość
całego zamówienia. W momencie wpisywania nowego zamówienia jego wartość nie zawsze jest
znana, dlatego wartości pola Wartosc mogą być nieokreślone.
W tym przypadku nie ma potrzeby tworzenia klucza podstawowego, ponieważ każda publikacja
ma własny, unikalny numer ISBN. Nie licząc kolumny ISBN, wszystkie pozostałe kolumny tej
tabeli mogą posiadać wartości NULL. Można bowiem wyobrazić sobie sytuację, w której znany jest
tylko numer ISBN książki, zanim jeszcze zostanie ujawniony jej tytuł, autor i cena.
Pole Ilosc, w którym zapamiętywana jest liczba zamówionych egzemplarzy jednej książki, jest
typu TINYINT UNSIGNED, a więc może przyjmować wartości liczb całkowitych z zakresu od 0 do 255.
Jak już wspomnieliśmy, klucze podstawowe składające się z więcej niż jednej kolumny należy
deklarować za pomocą klauzuli PRIMARY KEY. Przykład jej użycia zaprezentowano powyżej.
Użyto w nim nowego typu danych, który nie został jeszcze omówiony — to typ text. Jest on
stosowany w przypadku dłuższych tekstów, np. artykułów prasowych. Istnieje kilka jego wariantów,
które zostaną szczegółowo opisane w dalszej części tego rozdziału.
Dla jeszcze lepszego zrozumienia procesu tworzenia tabel podane zostaną zasady nadawania nazw
kolumnom oraz ogólne informacje na temat identyfikatorów. Najpierw jednak powrócimy na chwilę
do utworzonej bazy danych.
Rozdział 9. Tworzenie internetowej bazy danych 243
Polecenia SHOW można również użyć do wyświetlenia wszystkich baz danych udostępnianych przez
serwer:
mysql> show databases;
Użytkownicy, którzy nie posiadają przywileju SHOW DATABASES, zobaczą jedynie listę baz danych,
do których udzielono im dostępu.
MySQL wyświetli wszystkie informacje, które należało podać przy tworzeniu tabeli:
+-------+------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+------------+------+-----+---------+-------+
| ISBN | char(13) | NO | PRI | NULL | |
| Autor | char(50) | YES | | NULL | |
| Tytul | char(100) | YES | | NULL | |
| Cena | float(4,2) | YES | | NULL | |
+-------+------------+------+-----+---------+-------+
4 rows in set (0.01 sec)
Oba polecenia są szczególnie przydatne do przypomnienia sobie np. typu danych przechowywanych
w konkretnej kolumnie czy przeanalizowania struktury bazy utworzonej przez innego projektanta.
Tworzenie indeksów
Już wcześniej krótko wspominaliśmy o indeksach, ponieważ definiowanie kluczy głównych jest
równoznaczne z utworzeniem indeksów na tych kolumnach.
Jednym z częstych problemów, który sygnalizują niedoświadczeni użytkownicy serwera MySQL,
jest to, że narzędzie, które jest uznawane za niezwykle szybkie, na ich bazach danych działa
wolno. Przyczyną tego problemu jest po prostu to, że na bazie danych nie utworzono indeksów.
(Wszak możliwe jest utworzenie tabel, które nie posiadają kluczy głównych ani indeksów).
Indeksy zostaną utworzone automatycznie na kolumnach, które wskazano jako klucze. Jeśli okaże
się, że wiele zapytań wykonywanych jest na kolumnie, która nie jest kluczem, można pokusić
244 Część II Stosowanie MySQL
się o utworzenie na niej indeksu w celu poprawienia wydajności. Można tego dokonać instrukcją
CREATE INDEX. Jej ogólna postać jest następująca:
CREATE [UNIQUE|FULLTEXT|SPATIAL] INDEX nazwa_indeksu
ON nazwa_tabeli (nazwa_kolumny_indeksowanej [(dlugosc)] [ASC|DESC, …])
Indeksy FULLTEXT, czyli pełnotekstowe, służą do indeksowania pól tekstowych; więcej na ich temat
napiszemy w rozdziale 13. Z kolei indeksy typu SPATIAL służą do indeksowania danych przestrzen-
nych, lecz ich opis wykracza poza ramy tematyczne tej książki.
Indeksy typu UNIQUE zapewniają, że wszystkie wartości, czy też wszystkie kombinacje wartości
w przypadku indeksów wielokolumnowych, będą unikalne. (Tak się właśnie dzieje w przypadku
indeksów głównych).
Opcjonalne pole dlugosc służy do wskazania, aby tylko pierwszych dlugosc znaków pola podle-
gała indeksowaniu. Można również wskazać, by indeks był ułożony w porządku rosnącym (ASC)
lub malejącym (DESC). Porządkiem domyślnym jest porządek rosnący.
Identyfikatory MySQL
MySQL rozróżnia wiele rodzajów identyfikatorów: bazy danych, tabele, kolumny i indeksy
(wszystkie są już Czytelnikowi znane), aliasy (zostaną one szerzej omówione w następnym rozdzia-
le), widoki oraz procedury składowane (które zostaną opisane dokładniej w rozdziale 13.) i jeszcze
kilka innych.
Bazy danych w MySQL stają się katalogami w strukturze plików systemu operacyjnego, a tabele
są reprezentowane przez jeden lub kilka plików. W starszych wersjach MySQL miało to bezpośredni
wpływ na zasady nazewnictwa baz i tabel, ale obecnie wszystkie problematyczne znaki są kodowane.
Niemniej jednak ten sposób odwzorowywania baz danych i tabel na elementy systemu plików ma
wpływ na rozróżnianie wielkości liter. Jeżeli system operacyjny rozróżnia wielkie i małe litery,
wówczas czyni to również serwer baz danych (tak jest w przypadku większości wersji systemu
Unix), w przeciwnym razie wielkość liter nie będzie miała znaczenia (np. w przypadku systemu
Windows lub Mac OS). W nazwach kolumn i aliasów wielkość liter nie odgrywa roli, jednak
w obrębie jednego polecenia nazwy te muszą być zapisywane w taki sam sposób.
Co więcej, aby dodatkowo skomplikować sprawę, wpływ na uwzględnianie wielkości znaków
w identyfikatorach ma także opcja konfiguracyjna lower_case_table_names.
Ogólnie rzecz biorąc, w celu zapewnienia jak najlepszej przenaszalności wszystkie identyfikatory
warto zapisywać małymi literami.
Warto wspomnieć, że lokalizacja katalogów i plików z danymi jest ustawiana w plikach konfi-
guracyjnych serwera MySQL. Można ją odnaleźć, wpisując w wierszu poleceń systemu operacyj-
nego następującą komendę:
> mysqladmin –h nazwa_komputera –u root –p variables
Podane zasady ulegają jednak częstym zmianom. Identyfikatory mogą nawet zawierać słowa za-
rezerwowane i wszelkiego rodzaju znaki specjalne. Jedynym wymogiem jest wówczas koniecz-
ność zamknięcia ich w odwrotnych apostrofach. Można na przykład napisać:
create database `create database`;
Oczywiście z tak szerokich możliwości należy korzystać z umiarem. To, że baza może nosić nazwę
`create database`, wcale nie oznacza, że powinna zostać tak nazwana. Stosuje się tu standardową
zasadę nazewnictwa, według której identyfikatory powinny mieć sensowne brzmienie.
246 Część II Stosowanie MySQL
Typy liczbowe
Typy liczbowe dzielą się na dwie kategorie: całkowitoliczbowe, o ustalonej precyzji, zmiennoprze-
cinkowe oraz pola bitowe. W przypadku liczb zmiennoprzecinkowych istnieje możliwość zadekla-
rowania liczby cyfr znaczących po przecinku — w tabelach wielkość ta jest oznaczona symbolem D.
Maksymalna wartość parametru D jest wyznaczona przez mniejszą spośród dwóch liczb: 30 i M – 2
(tj. maksymalna szerokość wyświetlania minus 2 — jeden znak przecinka i jeden znak dla całko-
witej części danej liczby).
W przypadku typów całkowitoliczbowych można je zawęzić do typu UNSIGNED, jak pokazano na
listingu 9.1.
Dla wszystkich typów liczbowych można ustawić atrybut ZEROFILL. Powoduje on, że przy wyświe-
tlaniu zawartości kolumn posiadających ten atrybut zapisane w nich liczby zostaną poprzedzone
zerami. Jeżeli kolumna zostanie zdefiniowana jako ZEROFILL, automatycznie będzie ona również
mieć typ UNSIGNED.
Typy całkowitoliczbowe zostały przedstawione w tabeli 9.5. W drugiej kolumnie tabeli zawarto
dopuszczalny zakres danych w przypadku braku atrybutu UNSIGNED (pierwszy wiersz) i z ustawio-
nym atrybutem UNSIGNED (drugi wiersz).
Wykorzystanie
Typ Zakres Opis
pamięci (w bajtach)
TINYINT[(M)] –127 – 128 lub 0 – 255 1 Bardzo małe liczby całkowite
BIT Synonim TINYINT
BOOL Synonim TINYINT
SMALLINT[(M)] –32 768 – 32 767 lub 0 – 65535 2 Małe liczby całkowite
MEDIUMINT[(M)] –8 388 608 – 8 388 607 3 Średnie liczby całkowite
lub 0 – 16 777 215
Rozdział 9. Tworzenie internetowej bazy danych 247
Wykorzystanie
Typ Zakres Opis
pamięci (w bajtach)
INT[(M)] –231 – 231–1 lub 0 – 232–1 4 Zwykłe liczby całkowite
INTEGER[(M)] Synonim typu INT
63 63 64
BIGINT[(M)] –2 – 2 –1 lub 0 – 2 –1 8 Duże liczby całkowite
Wykorzystanie
Typ Zakres Opis
pamięci (w bajtach)
FLOAT(precyzja) Zależnie od precyzji różny Używany
do deklarowania liczb
zmiennoprzecinkowych
o pojedynczej
lub podwójnej precyzji
FLOAT[(M, D)] ±1,175494351E–38 4 Liczby
±3,402823466E+38 zmiennoprzecinkowe
o pojedynczej precyzji.
Typ ten jest
równoznaczny z typem
FLOAT(4), pozwala przy
tym na określenie
szerokości wyświetlania
i liczby cyfr znaczących
po przecinku
DOUBLE[(M, D)] ±1,7976931348623157E–308 8 Liczby
±2,2250738585072014E+308 zmiennoprzecinkowe
o podwójnej precyzji.
Typ ten jest równoznaczny
z typem FLOAT(8), pozwala
przy tym na określenie
szerokości wyświetlania
i liczby cyfr znaczących
po przecinku
DOUBLE PRECISION[(M, D)] Jak wyżej Synonim typu
DOUBLE[(M, D)]
Wykorzystanie
Typ Zakres Opis
pamięci (w bajtach)
DECIMAL[(M[, D])] Różny M+2 Liczba
zmiennoprzecinkowa
przechowywana jako
zmienna typu CHAR. Zakres
zależy od wartości M —
szerokości wyświetlania
NUMERIC[(M, D)] Jak wyżej Synonim typu DECIMAL
DEC[(M, D)] Jak wyżej Synonim typu DECIMAL
FIXED[(M, D)] Jak wyżej Synonim typu DECIMAL
Istnieje jeszcze jeden typ liczbowy: BIT(M). Pola tego typu pozwalają na przechowywanie do M bitów,
gdzie M jest liczbą z zakresu od 1 do 64.
Tabela 9.9 prezentuje możliwe dostępne formaty wyświetlania wartości typu TIMESTAMP.
Rozdział 9. Tworzenie internetowej bazy danych 249
Typy łańcuchowe
Typy łańcuchowe dzielą się na cztery grupy. Pierwsza z nich to klasyczne (zwykłe) łańcuchy zna-
ków, czyli krótkie fragmenty tekstu. Do tej grupy zaliczamy typy CHAR (łańcuchy o stałej długości)
oraz VARCHAR (łańcuchy o zmiennej długości). Dla każdego z nich można określić długość łańcucha.
Łańcuchy zapisywane w kolumnach typu CHAR zostaną uzupełnione spacjami w celu pełnego
wykorzystania dopuszczalnego limitu ich długości. Natomiast w kolumnach typu VARCHAR łań-
cuchy są zapisywane w ich pierwotnej formie, dzięki czemu zajmują one tylko tyle pamięci, ile
potrzeba (tak więc w przypadku wartości CHAR MySQL usunie końcowe znaki spacji przy ich
pobieraniu z bazy, natomiast ewentualne znaki spacji znajdujące się na końcu łańcucha VARCHAR
zostaną wyeliminowane w momencie zapisania go do bazy). Wybór któregoś z tych typów spro-
wadza się więc do rozstrzygnięcia dylematu między zwiększeniem szybkości działania kosztem
używanej pamięci a ograniczeniem zużycia pamięci i spadkiem wydajności systemu. Zagadnienie
to zostanie wyczerpująco omówione w rozdziale 12.
Do drugiej grupy należą typy BINARY oraz VARBINARY. Są to raczej łańcuchy bajtów, a nie znaków.
Do trzeciej grupy należą typy TEXT i BLOB. Wartości obu wymagają zróżnicowanych ilości pamięci.
Typy te są przeznaczone do przechowywania, odpowiednio, dłuższych tekstów oraz danych bi-
narnych. Wartości typu BLOB to tzw. duże obiekty binarne (ang. binary large objects), stąd też wywo-
dzi się jego nazwa. Obiekty te mogą przechowywać każdy rodzaj danych, np. obrazy, dźwięki itp.
Ponieważ oba typy pozwalają na zapisywanie dużych ilości danych, sposób ich wykorzystania jest
nieco bardziej skomplikowany. Zagadnienie to zostanie szerzej przedstawione w rozdziale 12.
Czwarta grupa składa się z dwóch typów specjalnych SET i ENUM. Typ SET jest używany w celu
zawężenia wartości danych, które mogą być zapisane w danej kolumnie, do z góry określonego zbio-
ru wartości, przy czym dane zapisane w kolumnie mogą mieć więcej niż jedną wartość z tego zbioru.
Podany zbiór wartości może zawierać co najwyżej 64 elementy.
ENUM to inaczej typ wyliczeniowy. Jest on podobny do typu SET, z jedną różnicą: kolumny typu
ENUM mogą zawierać tylko jedną spośród określonego zbioru wartości lub wartość NULL, a zbiór
wartości dopuszczalnych może zawierać do 65 535 elementów.
Tabele 9.10, 9.11 i 9.12 zawierają podsumowanie właściwości typów łańcuchowych. Tabela 9.10
przedstawia zwykłe typy łańcuchowe.
250 Część II Stosowanie MySQL
Tabela 9.12 prezentuje typy TEXT i BLOB. Maksymalna długość pola typu TEXT w znakach odpowia-
da maksymalnej długości (liczonej w bajtach) plików, które można zapisać w tym polu.
Maksymalna ilość
Typ Opis
wartości w zbiorze
ENUM('wartość1','wartość2',…) 65 535 W kolumnie tego typu może znajdować się tylko jedna
wartość ze zbioru wartości dopuszczalnych lub NULL
SET('wartość1','wartość2',…) 64 W kolumnie tego typu może znajdować się podzbiór
zbioru wartości dopuszczalnych lub NULL
Rozdział 9. Tworzenie internetowej bazy danych 251
W następnym rozdziale
Ponieważ znamy już metody rejestrowania użytkowników, zakładania baz danych i tworzenia
tabel, możemy przejść do sposobów korzystania z bazy danych. W następnym rozdziale zostaną
przedstawione metody zapisywania danych do tabel, ich modyfikacji i usuwania oraz wysyłania
zapytań do bazy.
252 Część II Stosowanie MySQL
Rozdział 10.
Praca z bazą danych MySQL
W tym rozdziale zaprezentujemy strukturalny język zapytań SQL oraz sposoby wykorzystania go
do formułowania zapytań do bazy danych. Na przykładzie internetowej księgarni „Książkorama”
zostaną pokazane sposoby zapisywania danych w bazie, ich usuwania i modyfikacji oraz wysyłania
zapytań do bazy.
W pierwszej kolejności przedstawimy język SQL oraz przyczyny, dla których należy się z nim
zaznajomić.
Przed przystąpieniem do wykonania poleceń SQL zawartych w tym rozdziale należy założyć
bazę danych dla przykładowej księgarni „Książkorama”. Dokładne instrukcje na ten temat za-
warte są w rozdziale 9.
Podstawowym standardem języka SQL jest ANSI, obsługiwany przez MySQL. Większość syste-
mów baz danych zawiera również własne rozszerzenia tego standardu. Istnieją nieznaczne różnice
między standardem SQL a SQL bazy MySQL. Niektóre z tych różnic mają zniknąć w kolejnych
wersjach MySQL, niektóre z nich natomiast wynikają z celowego działania. Ważniejsze z nich
254 Część II Stosowanie MySQL
będziemy wskazywać w dalszych częściach książki. Pełną listę różnic między SQL w poszczegól-
nych wersjach bazy MySQL a standardem ANSI SQL można znaleźć w podręczniku on-line MySQL.
Stronę tę można znaleźć pod poniższym adresem URL, a także w wielu innych lokalizacjach:
http://dev.mysql.com/doc/refman/5.7/en/compatibility.html
Część Czytelników zetknęła się prawdopodobnie z takimi określeniami, jak język definiowania
danych (ang. Data Definition Language — DDL), służący do definiowania baz danych, oraz język
manipulowania danymi (ang. Data Manipulation Language — DML), przeznaczony do zadawania
zapytań do bazy. SQL odgrywa rolę obu języków. W rozdziale 9. zdefiniowaliśmy dane właśnie
za pomocą SQL (a właściwie jego części odpowiadającej DDL), tak więc był on już wcześniej
używany. Język DDL jest przeznaczony do tworzenia nowych baz danych.
Polecenia języka SQL odpowiadające poleceniom językowi DML będą znacznie częściej wykorzy-
stywane, gdyż realizują one funkcje zapisywania i wyszukiwania danych z baz danych.
Jak wiadomo, systemy zarządzania relacyjnymi bazami danych zawierają tabele, w których z kolei
znajdują się wiersze danych zapisanych w odpowiednich kolumnach. Każdy wiersz tabeli opisuje
zazwyczaj jakiś obiekt świata rzeczywistego lub związek pomiędzy takimi obiektami, natomiast
w kolumnach przechowywane są informacje na temat tych obiektów. Polecenie INSERT umożliwia
zapisanie do bazy wiersza danych.
Na przykład w celu wstawienia nowego rekordu do tabeli Klienci bazy danych księgarni „Książko-
rama” należy wpisać:
INSERT INTO Klienci VALUES (NULL, 'Julia Kowalska', 'Wierzbowa 25', 'Warszawa');
Jak widać, w miejsce nazwa_tabeli wprowadzono nazwę tabeli, do której ma zostać wpisany nowy
rekord, natomiast wartosc1, wartosc2 itd. zastąpiono odpowiednimi wartościami. W powyższym
przykładzie wszystkie podane wartości są ujęte w apostrofy. MySQL wymaga, aby łańcuchy znaków
były zawsze podawane w cudzysłowach lub apostrofach (w dalszej części książki używane będą
obie te formy zapisu). Liczby i daty nie wymagają stosowania ani cudzysłowów, ani apostrofów.
Warto zwrócić uwagę na kilka ciekawych własności polecenia INSERT. Zawarte w poleceniu wartości
zostaną wstawione do kolumn w tej kolejności, w jakiej je podano. Jeżeli wystarczy wypełnić
danymi tylko niektóre kolumny bądź też wygodniej jest podawać dane w innej kolejności, można
wypisać nazwy odpowiednich kolumn zaraz za nazwą tabeli. Na przykład:
INSERT INTO Klienci (nazwisko, adres, miejscowosc) VALUES
('Maria Janowska', 'Kolejowa 15', 'Katowice');
Metoda ta jest szczególnie przydatna, gdy brakuje niektórych danych potrzebnych do zapełnie-
nia rekordu lub gdy wypełnienie niektórych pól jest opcjonalne. Ten sam efekt zostanie osiągnięty
po wpisaniu równoważnego polecenia:
INSERT INTO Klienci SET nazwisko='Marian Strzelec', adres='Aroniowa 12', miejscowosc='Lublin';
Rozdział 10. Praca z bazą danych MySQL 255
Istnieje także możliwość wpisania więcej niż jednego wiersza danych za pomocą pojedynczego
polecenia INSERT. Każdy wiersz musi być wówczas ujęty w nawiasy i oddzielony od sąsiednich
nawiasów przecinkami.
Instrukcja INSERT jest dostępna tylko w kilku innych wariantach. Po słowie INSERT można dodać
słowo LOW_PRIORITY, DELAYED lub HIGH_PRIORITY. Pierwsze oznacza, że system może poczekać
i wstawić dane później, gdy na tabeli nie będzie wykonywany odczyt. Słowo kluczowe DELAYED
z kolei oznacza, że wstawiane dane będą podlegać buforowaniu. Jeśli serwer będzie zajęty, można
kontynuować wykonywanie zapytań, nie czekając na zakończenie wykonywania operacji INSERT.
Słowo kluczowe HIGH_PRIORITY ma znaczenie wyłącznie w przypadku, gdy MySQL został urucho-
miony z opcją --low-priority-updates. Jego działanie polega na unieważnieniu tej opcji podczas
wykonywania bieżącego zapytania. Jeśli opcja --low-priority-updates nie została użyta, to także
zastosowanie słowa kluczowego HIGH_PRIORITY nie da żadnych efektów.
Zaraz po tych słowach kluczowych można wpisać słowo IGNORE. Oznacza ono, że jeśli nastąpi próba
wstawienia wiersza, który spowodowałby duplikację kluczy unikatowych, zostanie ona niejaw-
nie zignorowana. Kolejną alternatywą jest umieszczenie na końcu instrukcji INSERT wyrażenia
ON DUPLICATE KEY UPDATE wyrazenie. Umożliwi to dokonanie zmiany wartości zduplikowanego
klucza przy użyciu zwykłej instrukcji UPDATE (omówionej w dalszej części rozdziału).
Do bazy danych księgarni „Książkorama” zostaną teraz wpisane przykładowe dane. Operacja
ta zostanie wykonana za pomocą kolejnych poleceń INSERT, wykorzystujących możliwość zapi-
sywania więcej niż jednego wiersza naraz. Realizujący to kod SQL można znaleźć w pliku o nazwie
ksiazki_insert.sql, znajdującym się w przykładach dołączonych do tej książki, w katalogu rozdzial_10.
Jest on również zaprezentowany na listingu 10.1.
Listing 10.1. ksiazki_insert.sql — kod SQL wstawiający przykładowe dane do bazy danych księgarni
„Książkorama”
use ksiazki;
Polecenia zawarte w pliku ksiazki_insert.sql można wykonać, wpisując w wierszu poleceń syste-
mu operacyjnego:
> mysql –h nazwa_komputera –u ksiazkorama –p < ścieżka/do/ksiazki_insert.sql
Wszystkie klauzule polecenia SELECT zostaną omówione w dalszej części rozdziału. Najpierw jednak
przedstawimy zapytanie nieposiadające żadnych dodatkowych opcji. Służy ono do wyszukania
odpowiednich pozycji ze wskazanej tabeli.
Zazwyczaj pozycjami tymi są po prostu kolumny wchodzące w skład wskazanej tabeli (jednak mogą
to być także np. rezultaty wykonania jakiegoś wyrażenia — więcej na ten temat w dalszej części
rozdziału). Wykonanie poniższego zapytania spowoduje wyświetlenie zawartości kolumn
Nazwisko i Miejscowosc z tabeli Klienci:
SELECT Nazwisko, Miejscowosc
FROM Klienci;
Jeżeli wcześniej zostały wykonane polecenia zawarte w pliku ksiazki_insert.sql, to MySQL powi-
nien wygenerować następujące zestawienie:
+-----------------+-------------+
| Nazwisko | Miejscowosc |
+-----------------+-------------+
| Julia Kowalska | Warszawa |
| Adam Pawlak | Szczecin |
| Michalina Nowak | Gliwice |
+-----------------+-------------+
3 rows in set (0.00 sec)
Rozdział 10. Praca z bazą danych MySQL 257
Jak widać, otrzymana lista zawiera zatem dane tylko z tych kolumn, które wybrano, tj. Nazwisko
i Miejscowosc, zawartych w tabeli Klienci. Są to dane ze wszystkich rekordów zapisanych w tabeli.
Po słowie kluczowym SELECT można wypisać nazwy dowolnej liczby kolumn. Oprócz tego można
również podać inne pozycje niebędące kolumnami. Jedną z nich jest np. symbol wieloznaczny *,
który zastępuje nazwy wszystkich kolumn wchodzących w skład podanej tabeli lub tabel. Na
przykład aby wyszukać wartości wszystkich pól wszystkich rekordów przechowywanych w tabeli
Pozycje_zamowione, należy użyć następującego polecenia:
SELECT *
FROM Pozycje_zamowione;
Wyszukiwanie danych
spełniających określone kryteria
W celu ograniczenia liczby wyświetlanych wierszy należy określić kryteria wyszukiwania. Doko-
nuje się tego za pomocą klauzuli WHERE. Na przykład wykonanie następującego polecenia:
SELECT *
FROM Zamowienia
WHERE Klientid=3;
spowoduje wyświetlenie wartości wszystkich pól tych wierszy, w których KlientID ma wartość
równą 3. Oto efekt:
+--------------+----------+---------+------------+
| ZamowienieID | KlientID | Wartosc | Data |
+--------------+----------+---------+------------+
| 1 | 3 | 69.98 | 2007-04-02 |
| 4 | 3 | 6.99 | 2007-05-01 |
+--------------+----------+---------+------------+
2 rows in set (0.01 sec)
Klauzula WHERE służy do określania kryteriów, które będą podstawą wyszukiwania. W powyższym
przykładzie zażądano wyświetlenia tylko tych wierszy, w których wartość pola KlientID jest
równa 3. Należy zwrócić uwagę, że w MySQL operatorem równości jest pojedynczy znak =, co
jest istotną różnicą w porównaniu z operatorem równości stosowanym w PHP. Warto o tym pamię-
tać, gdyż może to powodować pewne zamieszanie, szczególnie w sytuacji, gdy skrypty PHP
współpracują z bazą danych.
Oprócz operatora równości MySQL obsługuje pełen zestaw operatorów i wyrażeń regularnych.
Te najczęściej wykorzystywane w klauzuli WHERE zostały przedstawione w tabeli 10.1, jednak
nie jest to ich pełna lista — jeżeli istnieje potrzeba wykorzystania innego operatora lub wyraże-
nia, należy zajrzeć do podręcznika MySQL.
258 Część II Stosowanie MySQL
Nazwa
Operator Przykład Opis
(jeśli jest stosowana)
= równy KlientID=3 Sprawdza, czy dwie wartości są sobie
równe
> większy od Wartosc>60.00 Sprawdza, czy pierwsza wartość
jest większa od drugiej
< mniejszy od Wartosc<60.00 Sprawdza, czy pierwsza wartość
jest mniejsza od drugiej
>= większy lub równy Wartosc>=60.00 Sprawdza, czy pierwsza wartość
jest większa lub równa drugiej
<= mniejszy lub równy Wartosc<=60.00 Sprawdza, czy pierwsza wartość
jest mniejsza lub równa drugiej
!= lub <> różny Ilosc!=0 Sprawdza, czy dwie wartości różnią
się od siebie
IS NOT NULL Adres IS NOT NULL Sprawdza, czy pole ma nadaną wartość
IS NULL Adres IS NULL Sprawdza, czy pole jest puste
BETWEEN Ilosc BETWEEN 0 AND 60.00 Sprawdza, czy wartość jest większa
lub równa wartości minimalnej
i jednocześnie mniejsza lub równa
wartości maksymalnej
IN Miejscowosc IN('Warszawa', Sprawdza, czy wartość należy
'Lublin') do określonego zbioru wartości
NOT IN Miejscowosc NOT Sprawdza, czy wartość nie należy
IN('Warszawa', 'Lublin') do określonego zbioru wartości
LIKE wyszukiwanie wzorca Nazwisko LIKE 'Fred %' Sprawdza, czy wartość pasuje
do określonego wzorca
NOT LIKE wyszukiwanie wzorca Nazwisko NOT LIKE 'Fred %' Sprawdza, czy wartość nie pasuje
do określonego wzorca
REGEXP wyrażenie regularne Nazwisko REGEXP '^Ma.*' Sprawdza, czy wartość pasuje
do określonego wyrażenia regularnego
Ostatnie trzy pozycje zawarte w tabeli odwołują się do operatorów LIKE i REGEXP, które służą do po-
równywania wartości z podanym wzorcem.
Operator LIKE wykorzystuje wzorce udostępniane przez język SQL. Wzorce mogą zawierać zwykły
tekst oraz znak %, będący symbolem wieloznacznym, który zastępuje dowolną liczbę dowolnych
znaków, a także znak _, zastępujący dowolny pojedynczy znak.
Słowo kluczowe REGEXP stosuje się do przyrównywania wartości do wyrażeń regularnych. MySQL
obsługuje wyrażenia regularne w stylu POSIX. Słowo kluczowe REGEXP można zastąpić słowem
RLIKE, będącym jego synonimem. Styl POSIX jest nieco odmienny od stosowanych domyślnie
w PHP wyrażeń regularnych PCRE. (W języku PHP wcześniej także był używany styl POSIX, lecz
został on uznany za przestarzały). Więcej informacji na ten temat przedstawiliśmy w rozdziale 4.
W poleceniu SELECT można określać większą liczbę kryteriów wyszukiwania przy użyciu prostych
operatorów oraz składni dopasowywania do wzorca połączonych operatorami logicznymi AND i OR,
jak pokazano w poniższym przykładzie.
Rozdział 10. Praca z bazą danych MySQL 259
SELECT *
FROM Zamowienia
WHERE KlientID = 3 OR KlientID = 2;
Dane te zawarte są w oddzielnych tabelach, ponieważ odnoszą się do odrębnych obiektów świata
rzeczywistego. Jest to jedna z zasad prawidłowej konstrukcji bazy danych, o której wspomniano
w rozdziale 8.
Aby uzyskać wszystkie potrzebne informacje za pomocą poleceń języka SQL, należy wykonać
operację nazywaną łączeniem (ang. join). Prowadzi ona do połączenia dwóch lub więcej tabel
w celu odnalezienia związków między zapisanymi w nich danymi. Jeśli na przykład poszukiwane
są informacje na temat zamówień złożonych przez Julię Kowalską, trzeba najpierw w tabeli
Klienci sprawdzić wartość pola KlientID tego klienta, a następnie w tabeli Zamowienia odszukać
wiersze z tą samą wartością zapisaną w polu KlientID.
Choć zrozumienie zasady operacji łączenia nie sprawia trudności, należy ona do bardziej wyrafi-
nowanych i złożonych operacji języka SQL. MySQL obsługuje kilka różnych typów operacji łącze-
nia, z których każdy służy do innych celów.
Występuje tu kilka elementów, które należy omówić. Przede wszystkim znalezienie żądanych in-
formacji wymagało danych pochodzących z dwóch tabel, dlatego też nazwy obu zawarto w tekście
zapytania.
Co więcej, dokonano operacji łączenia tabel, czego Czytelnik mógł nawet nie zauważyć. Prze-
cinek oddzielający nazwy dwóch tabel jest równoważny użyciu słów kluczowych INNER JOIN
lub CROSS JOIN. Jest to typ połączenia określany niekiedy mianem full join lub też produktu
kartezjańskiego obu tabel. Oznacza to w praktyce: „Weź wymienione tabele i utwórz jedną, dużą.
Duża tabela powinna zawierać po jednym wierszu dla każdej możliwej kombinacji wierszy z każdej
z wymienionych tabel, niezależnie od tego, czy ta kombinacja ma sens, czy nie”. Innymi słowy,
w wyniku tej operacji otrzymano tabelę zawierającą każdy wiersz tabeli Klienci połączony z każ-
dym wierszem tabeli Zamowienia, bez względu na to, czy dany klient złożył to zamówienie, czy nie.
260 Część II Stosowanie MySQL
W większości przypadków na nic się zdaje poprzestanie na wykonaniu tylko takiej mało wyrafino-
wanej operacji. Najczęściej poszukiwane są wiersze, które do siebie pasują. W rozważanym
przypadku są to rekordy z zamówieniami złożonymi przez wskazanego klienta wraz z rekordem
zawierającym jego dane.
Rekordy te zostały odnalezione dzięki użyciu warunku połączenia (ang. join condition) w klauzuli
WHERE. Jest to specjalny typ wyrażenia warunkowego, określającego, które pola ustanawiają
związek między dwiema tabelami. W rozważanym przykładzie warunek połączenia miał postać:
Klienci.KlientID=Zamowienia.KlientID
Dzięki niemu MySQL zawarł w tabeli wynikowej tylko te wiersze, w których wartość pola KlientID
z tabeli Klienci odpowiada wartości pola o tej samej nazwie z tabeli Zamowienia.
Dodanie do zapytania warunku połączenia spowodowało zmianę typu połączenia na typ określany
po angielsku jako equi-join.
W przykładowym zapytaniu użyto separatora mającego postać kropki w celu wyraźnego wskazania,
z której tabeli ma pochodzić kolumna o podanej nazwie. Tak więc wyrażenie Klienci.KlientID
odnosi się do kolumny KlientID z tabeli Klienci, a Zamowienia.KlientID odnosi się do kolumny
KlientID z tabeli Zamowienia.
Użycie separatora jest niezbędne w przypadku, gdy w dwóch lub więcej tabelach istnieją kolumny
o identycznych nazwach. Separator w postaci kropki może być również użyty w celu rozróżnienia
kolumn pochodzących z rozmaitych baz danych. W analizowanym przykładzie została zastosowana
notacja typu tabela.kolumna. Oprócz tego można również wskazać bazę danych, wykorzystując
notację baza.tabela.kolumna, np. w celu sprawdzenia prawdziwości wyrażenia:
ksiazki.Zamowienia.KlientID=innabaza.Zamowienia.KlientID
Kolumna Nazwisko występuje co prawda tylko w tabeli Klienci, nie obowiązuje więc wymóg
zastosowania separatora. MySQL prawidłowo odczyta tę klauzulę. Jednak dla ludzi samo nazwisko
jest mało znaczące, dlatego zastosowanie konstrukcji Klienci.Nazwisko znacznie zwiększy
czytelność zapytania.
Na przykład w celu odnalezienia klientów, którzy nabyli książki dotyczące języka Java (choćby
po to, by rozesłać im informacje o wydaniu nowej pozycji o tej tematyce), należy prześledzić
związki między danymi z kilku tabel.
Rozdział 10. Praca z bazą danych MySQL 261
Trzeba więc znaleźć klientów, którzy złożyli co najmniej jedno zamówienie obejmujące pozycję
na temat języka Java. Aby powiązać dane z tabeli Klienci z danymi z tabeli Zamowienia, trzeba
przeanalizować wartości pola KlientID, tak jak to uczyniliśmy w poprzednim przykładzie. Tabela
Zamowienia natomiast powiązana jest z tabelą Pozycje_zamowione za pomocą pola ZamowienieID,
ta z kolei związana jest z tabelą Ksiazki numerem ISBN. Po odnalezieniu wymienionych połączeń
można przystąpić do wyszukania wszystkich książek z wyrazem „Java” w tytule, po czym wyświe-
tlić nazwiska tych klientów, którzy nabyli co najmniej jedną z nich.
W celu odnalezienia tego klienta trzeba było prześledzić dane z czterech tabel, natomiast aby
powiązać je za pomocą połączenia typu equi-join, należało zastosować trzy różne warunki po-
łączenia. W przypadku łączenia tabel prawidłowością jest, że na każdą ich parę przypada jeden
warunek. W konsekwencji suma wszystkich nałożonych warunków połączenia jest o jeden mniejsza
niż liczba łączonych tabel. Zasadę tę warto pamiętać szczególnie wtedy, gdy trzeba znaleźć
błąd w zapytaniu zwracającym nieprawidłowe wyniki. Należy wówczas sprawdzić nałożone
warunki połączenia i prześledzić całą ścieżkę połączeń: od danych, które są znane, do danych,
które chce się uzyskać.
Jednym ze sposobów wykonania tego zadania w MySQL jest użycie połączenia typu left join.
Spowoduje ono wyszukanie wierszy spełniających zadany warunek połączenia między dwiema
tabelami. Jeśli jednak w drugiej tabeli nie ma odpowiadającego wiersza, do tabeli wynikowej
zostanie dodany wiersz z wartością NULL w prawej kolumnie.
W zapytaniu tym tabele Klienci i Zamowienia połączone są za pomocą left join. Jak można zauwa-
żyć, składnia warunku połączenia tego typu jest nieco inna — do określenia warunku połączenia
korzysta się ze specjalnej klauzuli ON.
262 Część II Stosowanie MySQL
Powyższe zapytanie zwraca wyłącznie tych klientów, którzy mają odpowiadające im rekordy
w tabeli zamówień.
W celu wyszukania tylko tych klientów, którzy jeszcze niczego nie zamówili, należy sprawdzić
wartości pola klucza podstawowego prawej tabeli (w tym przypadku: pole ZamowienieID) i wybrać
wiersze, dla których wartość ta jest równa NULL:
SELECT Klienci.KlientID, Klienci.Nazwisko
FROM Klienci LEFT JOIN Zamowienia
USING (KlientID)
WHERE Zamowienia.ZamowienieID IS NULL;
Obecnie wykonanie tego zapytania nie zwróci żadnych wyników, gdyż wszyscy klienci zapisani
w bazie złożyli zamówienia.
Ponieważ Jan jest nowym klientem, tak jak można się było tego spodziewać, tylko on nie złożył
jeszcze żadnego zamówienia.
Należy zwrócić uwagę na to, że w tym przykładzie została zastosowana jeszcze inna składnia
warunku połączenia. Definiując left join, można użyć klauzuli ON (jak we wcześniejszym przykła-
dzie) lub klauzuli USING, wykorzystanej ostatnio. Trzeba jednak pamiętać, że w przypadku USING
nie można wskazać nazwy tabeli, z której ma pochodzić pole łączące. Pola te muszą mieć takie same
nazwy w obu łączonych tabelach.
Tego typu zapytania można również konstruować przy użyciu podzapytań. Więcej na ich temat
powiemy w dalszej części tego rozdziału.
SELECT Kl.Nazwisko
FROM Klienci AS Kl, Zamowienia AS Z, Pozycje_zamowione AS Pz, Ksiazki AS Ks
WHERE Kl.KlientID = Z.KlientID
AND Z.ZamowienieID = Pz.ZamowienieID
AND Pz.ISBN = Ks.ISBN
AND Ks.Tytul LIKE '%Java%';
Wskazując tabele, które mają być przeszukane, użyto klauzuli AS w celu zadeklarowania dla nich
aliasów. Również kolumny mogą mieć własne aliasy (do tego zagadnienia powrócimy w punkcie
dotyczącym funkcji agregujących).
Użycie aliasów jest konieczne w przypadku łączenia tabeli z samą sobą. Na szczęście, operacja
ta jest o wiele łatwiejsza, niż wskazuje nazwa. Jest ona szczególnie użyteczna na przykład wtedy,
gdy trzeba wyszukać w tabeli dane mające te same wartości. Aby wyszukać klientów mieszkających
w tych samych miejscowościach — na przykład w celu założenia klubów czytelników — można
nadać jednej tabeli (w tym przypadku Klienci) dwa różne aliasy:
SELECT K1.Nazwisko, K2.Nazwisko, K1.Miejscowosc
FROM Klienci AS K1, Klienci AS K2
WHERE K1.Miejscowosc = K2.Miejscowosc
AND K1.Nazwisko != K2.Nazwisko;
W przypadku tego zapytania MySQL traktuje tabelę Klienci, jakby były to dwie różne tabele o na-
zwach K1 i K2, a następnie przeprowadza ich łączenie na podstawie wartości pola Miejscowosc.
Nałożono również drugi warunek — K1.Nazwisko != K2.Nazwisko — w celu pominięcia wierszy
zawierających podwójne dane jednego klienta.
Nazwa Opis
Produkt kartezjański Wszystkie możliwe kombinacje wszystkich wierszy łączonych tabel. Stosowane przez
użycie przecinków oddzielających nazwy tabel i pominięcie klauzuli WHERE
Full join Jak wyżej
Cross join Jak wyżej. Może być również stosowane przez użycie słów kluczowych CROSS JOIN
pomiędzy nazwami łączonych tabel
Inner join Semantycznie równoważne przecinkowi. Może być również stosowane przez użycie
słów kluczowych INNER JOIN. W razie pominięcia klauzuli WHERE równoważne połączeniu
typu full join, częściej jednak stosowane w klasycznej formie (tj. z klauzulą WHERE)
Equi-join Stosowane z wyrażeniem warunkowym zawierającym znak równości w celu wyszukania
pasujących do siebie wierszy z różnych tabel. W języku SQL odpowiada mu klauzula WHERE
Left join Powoduje wyszukanie pasujących do siebie wierszy ze wskazanych tabel oraz wypełnienie
wierszy nieodpowiadających sobie wartościami NULL. W SQL stosowane za pomocą słów
kluczowych LEFT JOIN. Używane w celu wyszukania brakujących wartości. Łączenie tabel
w odwrotnym kierunku można uzyskać przez użycie słów RIGHT JOIN
264 Część II Stosowanie MySQL
Klauzula ORDER BY jest przeznaczona do sortowania wierszy danych na podstawie wartości jednej
lub więcej kolumn, wyszczególnionych w ramach polecenia SELECT. Na przykład:
SELECT Nazwisko, Adres
FROM Klienci
ORDER BY Nazwisko;
Wykonanie tego polecenia spowoduje utworzenie tabeli zawierającej nazwiska klientów, uszerego-
wane w kolejności alfabetycznej, wraz z ich adresami:
+-----------------+---------------+
| Nazwisko | Adres |
+-----------------+---------------+
| Adam Pawlak | Szeroka 1/47 |
| Jan Nowakowski | Krakowska 234 |
| Julia Kowalska | Wierzbowa 25 |
| Michalina Nowak | Zachodnia 357 |
+-----------------+---------------+
4 rows in set (0.00 sec)
W tym przypadku kolumna Nazwisko zawiera zarówno imię, jak i nazwisko klienta, tak więc rekordy
szeregowane są najpierw pod względem imion, a dopiero potem, jeśli istnieje taka potrzeba,
pod względem nazwisk; aby przeprowadzić sortowanie pod względem samych nazwisk, należałoby
utworzyć dwie oddzielne kolumny: Imie i Nazwisko.
Domyślnym porządkiem sortowania jest porządek rosnący (od a do z lub od liczby najmniejszej
do największej). Porządek taki można również narzucić w treści zapytania — służy do tego
słowo kluczowe ASC:
SELECT Nazwisko, Adres
FROM Klienci
ORDER BY Nazwisko ASC;
Można również zastosować malejący porządek sortowania, wpisując słowo kluczowe DESC:
SELECT Nazwisko, Adres
FROM Klienci
ORDER BY Nazwisko DESC;
Dodatkowo sortowanie może odbywać się na podstawie wartości jednej lub więcej kolumn.
Można je wskazywać, podając ich nazwy, aliasy, a nawet pozycje, na których występują w tabeli.
Nazwa Opis
AVG(kolumna) Oblicza wartość średnią we wskazanej kolumnie
COUNT(elementy) Zwraca liczbę niezerowych wartości w podanej kolumnie. Jeżeli nazwa kolumny jest
poprzedzona słowem kluczowym DISTINCT, funkcja zwróci tylko ilość występujących
w niej odrębnych wartości. Jeżeli zamiast nazwy konkretnej kolumny wpisany
zostanie symbol *, funkcja zwróci wartość równą liczbie wierszy w tabeli
MIN(kolumna) Podaje najmniejszą wartość w kolumnie
MAX(kolumna) Podaje największą wartość w kolumnie
STD(kolumna) Oblicza odchylenie standardowe wartości w kolumnie
STDDEV(kolumna) Jak wyżej
SUM(kolumna) Sumuje wszystkie wartości z kolumny
Bardziej szczegółowe dane otrzymać można dzięki zastosowaniu klauzuli GROUP BY. Umożliwia
ona obliczenie średniej wartości grup zamówień wyodrębnionych na podstawie jakiegoś kryte-
rium — np. numeru klienta. Będzie więc wiadomo, który z nich składa największe zamówienia.
SELECT KlientID, AVG(Wartosc)
FROM Zamowienia
GROUP BY KlientID;
Użycie klauzuli GROUP BY wraz z funkcją agregującą faktycznie zmienia jej działanie. Zapytanie
sformułowane w powyższy sposób nie zwróci średniej wartości wszystkich zamówień zapisa-
nych w tabeli Zamowienia, lecz obliczy średnią wartość zamówień złożonych przez każdego
klienta (a ściślej: przypadających na każdą wartość pola KlientID):
+----------+--------------+
| KlientID | AVG(Wartosc) |
+----------+--------------+
| 1 | 12.990000 |
| 2 | 74.000000 |
| 3 | 38.485002 |
+----------+--------------+
3 rows in set (0.00 sec)
Stosując funkcje agregujące i grupowanie, należy pamiętać o bardzo ważnej kwestii. Otóż we-
dług standardu ANSI SQL w klauzuli SELECT zapytań wykorzystujących agregację danych lub
klauzulę GROUP BY mogą znajdować się tylko funkcje agregujące oraz nazwy tych kolumn, które
będą podstawą grupowania (ponadto nazwy te muszą być wyszczególnione w klauzuli SELECT).
266 Część II Stosowanie MySQL
MySQL umożliwia w tym przypadku większą swobodę. Obsługuje on składnię rozszerzoną, która
pozwala na pominięcie nazw kolumn w klauzuli SELECT, jeśli nie ma potrzeby ich wyświetlania.
Ważną zaletą grupowania i agregowania danych jest możliwość określania dodatkowych kryte-
riów wyszukiwania. Służąca do tego klauzula HAVING powinna znaleźć się zaraz za klauzulą
GROUP BY. Jej działanie jest takie samo, jak w przypadku użycia WHERE w zapytaniach niezawie-
rających grupowania ani funkcji agregacji.
Przytoczone przed chwilą zapytanie można rozszerzyć tak, aby wyszukać klientów, którzy złożyli
zamówienia o przeciętnej wartości co najmniej 50 złotych.
SELECT KlientID, AVG(Wartosc)
FROM Zamowienia
GROUP BY KlientID
HAVING AVG(Wartosc)>50;
Przypomnijmy, że klauzula HAVING odnosi się tylko do grup danych. Efekt wykonania zapytania
będzie następujący:
+----------+--------------+
| Klientid | AVG(Wartosc) |
+----------+--------------+
| 2 | 74.000000 |
+----------+--------------+
1 row in set (0.06 sec)
Jeśli jednak w klauzuli LIMIT zostaną podane dwa parametry, to pierwszy z nich będzie określał
numer wiersza, od którego będą zwracane wyniki, a drugi — liczbę zwracanych wierszy.
Zapytanie to oznacza: „Wyszukaj nazwiska wszystkich klientów, a następnie wyświetl trzech z nich,
począwszy od drugiego”. Należy pamiętać o tym, że wiersze numerowane są od zera, a więc
pierwszy wiersz w tabeli ma numer 0. Klauzula ta jest przydatna szczególnie w aplikacjach interne-
towych. Potencjalny klient może zażądać, by na jednej stronie przeglądanego przez siebie katalogu
Rozdział 10. Praca z bazą danych MySQL 267
produktów znajdowało się nie więcej niż 10 pozycji. Należy jednak pamiętać, że klauzula LIMIT
nie jest częścią standardu ANSI SQL. Stanowi ona rozszerzenie MySQL, dlatego jej używanie czyni
kod SQL niezgodnym z większością innych systemów zarządzania relacyjnymi bazami danych.
Używanie podzapytań
Podzapytanie to zapytanie SQL zagnieżdżone w innym zapytaniu. Co prawda, takie same efekty
można osiągnąć umiejętnie używając złączeń i tabel tymczasowych, lecz same podzapytania są
zwykle łatwiejsze do skonstruowania i bardziej czytelne.
Podzapytania podstawowe
Najczęstszy sposób użycia podzapytań polega na wykorzystaniu wyników jednego zapytania
jako danych porównawczych dla drugiego zapytania. Gdybyśmy na przykład chcieli znaleźć
zamówienia, których wartość była najwyższa, można by napisać następujące zapytanie:
SELECT KlientID, Wartosc
FROM Zamowienia
WHERE Wartosc = (SELECT MAX(Wartosc) FROM Zamowienia);
W tym przykładzie podzapytanie zwraca jedną wartość (wartość maksymalną), która następnie
stanowi wartość porównawczą dla zapytania zewnętrznego. Jest to bardzo dobry przykład użycia
podzapytania, ponieważ tego właśnie zapytania nie można zgrabnie napisać w ANSI SQL, korzy-
stając ze złączeń.
Ten sam wynik zostanie jednak zwrócony przez poniższe zapytanie wykonujące złączenie:
SELECT KlientID, Wartosc
FROM Zamowienia
ORDER BY Wartosc Desc
LIMIT 1;
W zapytaniu tym użyto klauzuli LIMIT, dlatego nie jest ono zgodne z większością systemów zarzą-
dzania relacyjnymi bazami danych.
Jednym z powodów, dla których MySQL przez tak długi czas nie obsługiwał podzapytań, jest
to, że tak naprawdę niewiele jest rzeczy, których bez podzapytań w ogóle nie można zrobić.
Podzapytania i operatory
Istnieje pięć specjalnych operatorów podzapytań. Cztery z nich są używane również w zwykłych
zapytaniach, a jeden (EXISTS) zazwyczaj jest używany w podzapytaniach skorelowanych i zostanie
przedstawiony w następnym punkcie. Cztery standardowe operatory podzapytań przedstawiono
w tabeli 10.4.
268 Część II Stosowanie MySQL
SOME SELECT k1 FROM t1 WHERE k1 > Alias dla ANY; czasami okazuje się bardziej zrozumiałe
SOME(SELECT k1 FROM t2); dla użytkowników
ALL SELECT k1 FROM t1WHERE k1 > Zwraca wartość true, jeżeli porównanie będzie miało wartość true
ALL (SELECT k1 FROM t2); dla wszystkich wierszy z podzapytania
Każdy z tych operatorów może wystąpić jedynie za operatorem porównania. Wyjątek stanowi
operator IN, w którym operator porównania (=) jest niejako „zagnieżdżony”.
Podzapytania skorelowane
Istnieją także specjalne podzapytania, nazywane podzapytaniami skorelowanymi. W podzapyta-
niach tego rodzaju elementów z zapytania zewnętrznego można używać w zapytaniu wewnętrznym.
Na przykład:
SELECT ISBN, Tytul
FROM Ksiazki
WHERE NOT EXISTS
(SELECT * FROM Pozycje_zamowione WHERE Pozycje_zamowione.ISBN = Ksiazki.ISBN);
Operator EXISTS zwraca wartość true, jeśli w podzapytaniu znajduje się co najmniej jeden pasujący
wiersz. I odwrotnie: NOT EXISTS zwróci true, jeśli żaden wiersz z podzapytania nie będzie pasował.
Podzapytania wierszowe
Jak dotąd wszystkie analizowane podzapytania zwracały pojedyncze wartości, choć w wielu
przypadkach były to jedynie wartości true i false (jak choćby w ostatnim przykładzie z operatorem
EXISTS). Podzapytania wierszowe zwracają całe wiersze, które następnie można porównywać
z wierszami z zapytania zewnętrznego. Z podejścia takiego korzysta się zazwyczaj w celu odnale-
zienia wierszy z jednej tabeli, które istnieją również w drugiej tabeli. W naszej bazie danych książek
trudno jest skonstruować odpowiedni przykład, który ilustrowałby to rozwiązanie, dlatego poniżej
przedstawiono ogólną składnię tego typu konstrukcji:
SELECT k1, k2, k3
FROM t1
WHERE (k1, k2, k3) IN (SELECT k1, k2, k3 FROM t2)
Rozdział 10. Praca z bazą danych MySQL 269
Działanie tego polecenia polega na zamianie dotychczasowych wartości określonych kolumn wska-
zanej tabeli na nowe, podane wartości. Zakres modyfikacji może zostać ograniczony do konkretnego
zbioru wierszy za pomocą klauzuli WHERE, a ich liczbę można limitować poprzez klauzulę LIMIT.
Klauzuli ORDER BY zwykle używa się tylko w połączeniu z klauzulą LIMIT. Na przykład jeżeli
chcemy zmienić tylko pierwszych dziesięć wierszy, najpierw trzeba je odpowiednio uporządkować.
Jeśli podane zostaną klauzule LOW_PRIORITY i IGNORE, zadziałają one tak samo jak w instrukcji INSERT.
Rozważmy dwa przykłady. W celu podniesienia cen wszystkich książek o 10% należy użyć polece-
nia UPDATE bez klauzuli WHERE:
UPDATE Ksiazki
SET Cena = Cena*1.1;
Jeśli z kolei trzeba zmienić tylko jeden, konkretny wiersz, na przykład uaktualnić adres klienta,
należy wpisać:
UPDATE Klienci
SET Adres='Olkuska 250'
WHERE KlientID = 4;
Standard ANSI SQL przewiduje możliwość dokonania tylko jednej zmiany naraz, jednak MySQL
nie posiada takiego ograniczenia. Każda klauzula zmiana może prowadzić do innego rodzaju
modyfikacji.
Jeśli podana zostanie klauzula IGNORE i podejmiemy próbę dokonania zmiany, która spowoduje
duplikację kluczy głównych, pierwszy z nich zostanie wstawiony do tablicy podlegającej zmianie,
natomiast wszystkie pozostałe zostaną usunięte. Jeżeli klauzula ta nie zostanie podana (domyślnie
ona nie występuje), zmiana się nie powiedzie i zostanie anulowana.
Najczęściej używane rodzaje zmian, których można dokonać tą instrukcją, zostały zaprezentowa-
ne w tabeli 10.5.
Składnia Opis
ADD [COLUMN] (opis_kolumny) Dodaje nową kolumnę na określonej pozycji (domyślnie na końcu tabeli).
[FIRST | AFTER kolumna] Parametr opis_kolumny wymaga podania nazwy kolumny i jej typu
(jak w przypadku polecenia CREATE TABLE)
ADD [COLUMN] (opis_kolumny, Dodaje jedną lub więcej nowych kolumn na końcu tabeli
opis_kolumny,…)
ADD INDEX [indeks] (kolumna, …) Tworzy indeks w określonej kolumnie lub kolumnach
ADD [CONSTRAINT [symbol]] PRIMARY Deklaruje wskazaną kolumnę lub kolumny jako klucz podstawowy tabeli.
KEY (kolumna, …) Zapis CONSTRAINT odnosi się do tabel, w których użyto kluczy obcych.
Więcej informacji na ten temat znajduje się w rozdziale 13.
ADD UNIQUE [CONSTRAINT [symbol]] Tworzy unikatowy indeks w podanej kolumnie lub kolumnach. Zapis
[indeks] (kolumna, …) CONSTRAINT odnosi się do tabel, w których użyto kluczy obcych. Więcej
informacji na ten temat znajduje się w rozdziale 13.
ADD [CONSTRAINT [symbol]] FOREIGN Dodaje klucz obcy do tabeli. Więcej informacji na ten temat znajduje
KEY [indeks] (kolumna_indeksu, się w rozdziale 13.
…) [definicja_odwolania)
ALTER [COLUMN] kolumna [SET Przypisuje lub usuwa domyślną wartość kolumny
DEFAULT wartość | DROP DEFAULT]
Tabela 10.5. Możliwe rodzaje zmian dokonywanych poleceniem ALTER TABLE (ciąg dalszy)
Składnia Opis
CONVERT TO CHARACTER SET zestaw Przekształca wszystkie kolumny tekstowe do wskazanego zestawu
COLLATE c znaków ze wskazaną metodą porządkowania
[DEFAULT] CHARACTER SET zestaw Ustawia domyślny zestaw znaków wraz z metodą porządkowania
COLLATE c
Często okazuje się, że któraś z już istniejących kolumn nie ma na tyle dużej „pojemności”, aby
pomieścić odpowiednie dane. Na przykład tabelę Klienci zdefiniowano w taki sposób, że długość
przechowywanego w niej nazwiska klienta ograniczono do 50 znaków. W trakcie zapisywania
danych kolejnych klientów może okazać się, że niektóre nazwiska przekraczają ten limit i są auto-
matycznie skracane. Problem ten można rozwiązać, zmieniając typ kolumny, aby mogła ona pomie-
ścić nazwiska mające nawet 70 znaków.
ALTER TABLE Klienci
MODIFY Nazwisko CHAR(70) NOT NULL;
Równie często zachodzi konieczność dodania nowej kolumny do tabeli. Wyobraźmy sobie, że
wprowadzony został nowy podatek od wartości sprzedanych książek i musi on być naliczany
oddzielnie dla każdego zamówienia. Można więc w tabeli Zamowienia utworzyć nową kolumnę
Podatek:
ALTER TABLE Zamowienia
ADD Podatek FLOAT(6,2) AFTER Wartosc;
Czasem też trzeba usunąć z tabeli kolumnę, która nie jest już potrzebna. Można na przykład
usunąć kolumnę, która przed chwilą została dodana do tabeli Zamowienia:
ALTER TABLE Zamowienia
DROP Podatek;
a z tabeli zostaną usunięte wszystkie wiersze. Zazwyczaj poprzestaje się na usunięciu tylko części
wierszy, które określa się w klauzuli WHERE. Operacja taka może być konieczna, na przykład gdy
nakład którejś z książek został już wyczerpany albo w trakcie robienia „porządków” w bazie okaże się,
że jeden z klientów już od dawna nie złożył żadnego zamówienia:
DELETE FROM Klienci
WHERE KlientID = 4;
Klauzula LIMIT jest stosowana do określenia maksymalnej liczby wierszy, które mogą być usunięte.
Klauzula ORDER BY jest zwykle używana w połączeniu z klauzulą LIMIT.
Klauzule LOW_PRIORITY i IGNORE działają tak jak w innych przypadkach. Klauzula QUICK może przy-
spieszyć działanie tabel MyISAM.
Usuwanie tabel
Czasami trzeba usunąć całe tabele. Operację tę wykonuje się za pomocą polecenia DROP TABLE.
Jego składnia jest bardzo prosta:
DROP TABLE tabela;
Polecenie to spowoduje usunięcie wszystkich wierszy z tabeli, a także samej tabeli, należy więc
wykonywać je z rozwagą.
Spowoduje ono usunięcie wszystkich wierszy, tabel i indeksów oraz samej bazy danych, nie trzeba
więc chyba przypominać o zachowaniu ostrożności przy jego używaniu.
W celu pogłębienia wiedzy na temat SQL można zajrzeć do opisu standardu ANSI SQL do-
stępnego pod adresem http://www.ansi.org.
Więcej informacji na temat rozszerzeń standardu ANSI SQL oferowanych przez MySQL znajduje się
w elektronicznym podręczniku, dostępnym pod adresem http://www.mysql.com.
W następnym rozdziale
W następnym rozdziale przedstawimy metody udostępniania bazy danych księgarni „Książkorama”
w internecie.
Rozdział 11.
Łączenie się z bazą MySQL
za pomocą PHP
W poprzednich rozdziałach tej książki wykorzystanie PHP ograniczało się do przechowywania
i zapisywania danych do plików jednorodnych. W rozdziale 2. zasygnalizowaliśmy, że zadania te
wykonywane są znacznie efektywniej przez systemy relacyjnych baz danych, które ponadto charak-
teryzują się nieporównanie większą stabilnością pracy, tak ważną szczególnie w przypadku tworze-
nia aplikacji internetowych. Obecnie, po utworzeniu bazy danych za pomocą MySQL, możemy przy-
stąpić do ustanowienia połączenia między bazą a stroną WWW.
W tym rozdziale wyjaśnimy, w jaki sposób za pomocą PHP można udostępnić bazę danych księ-
garni „Książkorama” na stronie WWW. Pokażemy metody odczytu danych z bazy i sposób ich
zapisu do bazy oraz powiemy, jak ustrzec się przed błędami mogącymi wystąpić w przypadku
wpisania nieprawidłowych danych.
W poprzednim rozdziale utworzyliśmy bazę danych, możemy więc rozpocząć pisanie skryptów PHP
realizujących kolejne kroki z powyższej listy. Zaczniemy od zaprojektowania formularza HTML
do wyszukiwania informacji. Odpowiedni kod przedstawiony jest na listingu 11.1.
<body>
<h1>Wyszukiwanie książek w księgarni "Książkorama"</h1>
</body>
</html>
A zatem jest to prosty kod HTML. W przeglądarce formularz wyszukiwania będzie wyglądał jak
ten z rysunku 11.1.
Po naciśnięciu przycisku Szukaj zostanie wykonany skrypt zapisany w pliku rezultaty.php. Jego
pełny tekst jest przedstawiony na listingu 11.2. W dalszej części tego rozdziału zostaną opisane
działanie i efekty wykonania skryptu.
Rozdział 11. Łączenie się z bazą MySQL za pomocą PHP 275
Rysunek 11.1.
Formularz wyszukiwania
jest dosyć ogólnikowy,
można więc szukać
książek na podstawie
ich tytułu, autora
lub numeru ISBN
Listing 11.2. rezultaty.php — skrypt odczytujący i wyświetlający wyniki wyszukiwania danych w bazie
<!DOCTYPE html>
<html>
<head>
<title>"Książkorama"- Rezultaty wyszukiwania</title>
</head>
<body>
<h1>"Książkorama"- Rezultaty wyszukiwania </h1>
<?php
// utworzenie krótkich nazw zmiennych
$metoda_szukania=$_POST['metoda_szukania'];
$wyrazenie=trim($_POST['wyrazenie']);
if (!$metoda_szukania || !$wyrazenie) {
echo '<p>Brak parametrów wyszukiwania. <br />
Wróć do poprzedniej strony i spróbuj ponownie.</p>';
exit;
}
$zapytanie = "SELECT ISBN, Autor, Tytul, Cena FROM Ksiazki WHERE $metoda_szukania = ?";
$polecenie = $db->prepare($zapytanie);
$polecenie->bind_param('s', $wyrazenie);
$polecenie->execute();
276 Część II Stosowanie MySQL
$polecenie->store_result();
while($polecenie->fetch()) {
echo "<p><strong>Tytuł: ".$tytul."</strong>";
echo "<br />Autor: ".$autor;
echo "<br />ISBN: ".$isbn;
echo "<br />Cena: ".number_format($cena,2)."</p>";
}
$polecenie->free_result();
$db->close();
?>
</body>
</html>
Rysunek 11.2.
Rezultaty
wyszukiwania książek
o języku Java
są wyświetlane
na stronie WWW
przez skrypt
rezultaty.php
Kolejny krok polega na sprawdzeniu, czy użytkownik wybrał metodę szukania oraz wpisał wyra-
żenie, które ma zostać odnalezione. Zauważmy, że czynność ta jest wykonywana dopiero po usu-
nięciu niepotrzebnych znaków spacji z początku i końca wyrażenia. W razie zastosowania odwrotnej
kolejności mogłoby dojść do sytuacji, w której podane przez użytkownika wyrażenie nie jest
puste, a więc nie nastąpiłoby wytworzenie błędu. Mogłoby ono jednak składać się z samych spacji,
które zostałyby potem usunięte przez funkcję trim().
if (!$metoda_szukania || !$wyrazenie) {
echo '<p>Brak parametrów wyszukiwania. <br />
Wróć do poprzedniej strony i spróbuj ponownie.</p>';
exit;
}
Następnie sprawdzana jest zmienna $metoda_szukania w celu upewnienia się, czy jej wartość jest
poprawna. Operacja ta wykonywana jest pomimo tego, że wartość tej zmiennej pochodzi z elementu
<select> formularza HTML:
switch ($metoda_szukania) {
case 'Tytul':
case 'Autor':
case 'ISBN':
break;
default:
echo '<p>Nieprawidłowy typ wyszukiwania. <br/>
Wróć i spróbuj jeszcze raz.</p>';
exit;
}
Wydawałoby się więc, że nie ma możliwości pozostawienia tej zmiennej pustej. Należy jednak
pamiętać, że może istnieć więcej niż jeden interfejs służący do obsługi omawianej bazy danych.
Na przykład księgarnia internetowa Amazon udostępnia swoją bazę wielu różnym serwisom,
wykorzystującym własne interfejsy. Najlepszym rozwiązaniem jest więc każdorazowe sprawdze-
nie danych wejściowych w celu uniknięcia ewentualnych problemów spowodowanych korzysta-
niem z różnych interfejsów.
Kolejnym ważnym etapem weryfikacji poprawności danych wejściowych jest sprawdzenie, czy
zawierają one znaki kontrolne. (Być może Czytelnik pamięta, że zagadnienie to zostało opisane
w rozdziale 4.). W momencie przesyłania danych pochodzących od użytkownika do bazy danych
typu MySQL należy posłużyć się znakami ucieczki.
W tym przypadku należy wykonać dwie operacje. Pierwsza z nich to upewnienie się, że typ wyszu-
kiwania jest prawidłowy; to zadanie realizuje kod przedstawiony powyżej. Druga operacja, wyko-
nywana w celu ochronienia się przed wszelkimi problematycznymi znakami w łańcuchu wyszuki-
wania, polega na użyciu konstrukcji MySQL określanej jako zapytanie składowane. Zagadnienie
to zostanie już niebawem opisane znacznie szerzej.
278 Część II Stosowanie MySQL
Funkcja ta zwróci zasób, a nie obiekt. Zasób ten będzie reprezentował połączenie z bazą danych,
a jeśli używany będzie interfejs proceduralny, zasób ten trzeba będzie przekazywać do wszystkich
pozostałych funkcji mysqli. Przypomina to sposób działania funkcji obsługujących pliki, takich
jak fopen().
Większość funkcji mysqli posiada zarówno interfejs zorientowany obiektowo, jak i proceduralny.
Podstawowa różnica między nimi polega na tym, że nazwy proceduralnych wersji funkcji roz-
poczynają się od mysqli_ i trzeba do nich przekazywać uchwyt zasobu, który został zwrócony
przez mysqli_connect(). Wyjątkiem od tej reguły są połączenia z bazą danych, które można usta-
nawiać przy użyciu konstruktora obiektu mysqli.
Konieczne jest sprawdzenie, czy połączenie z bazą zostało nawiązane, gdyż od tego zależy poprawne
działanie reszty skryptu. Oto właściwy fragment kodu:
if (mysqli_connect_errno()) {
echo '<p>Błąd: Połączenie z bazą danych nie powiodło się.<br />
Spróbuj jeszcze raz później.</p>';
exit;
}
(Kod ten wygląda identycznie w wersji zorientowanej obiektowo oraz proceduralnej). Funkcja
mysqli_connect_errno() zwróci numer błędu, jeżeli taki wystąpi, lub zero, jeśli nawiązanie połącze-
nia zakończy się powodzeniem.
Zauważmy, że wiersz kodu mającego na celu ustanowienie połączenia z bazą danych rozpoczyna
się od operatora tłumienia błędów @. Dzięki temu można zgrabnie obsłużyć wszelkie zaistniałe
błędy. (Ten sam efekt można by osiągnąć przy użyciu wyjątków, których w tym prostym przykła-
dzie nie użyto).
Nie można zapominać, że jednorazowo może istnieć tylko ograniczona liczba połączeń do serwera
MySQL. Limit liczby połączeń jest wyznaczany przez wartość zmiennej MySQL o nazwie
max_connections. Ten parametr oraz odpowiadająca mu zmienna MaxClients serwera Apache okre-
ślają granicę, po której przekroczeniu zostaną odrzucone wszystkie żądania otwarcia nowego
połączenia. Dzięki temu możliwa jest ochrona zasobów przed nadmiernym obciążeniem czy też
ich pełne zablokowanie w razie wystąpienia awarii.
Rozdział 11. Łączenie się z bazą MySQL za pomocą PHP 279
Wartość obu parametrów można zmienić, dokonując odpowiednich modyfikacji w plikach kon-
figuracyjnych. Wartość zmiennej MaxClients serwera Apache jest zapisana w pliku o nazwie
httpd.conf, natomiast max_connections — w pliku my.conf.
Tak samo należy postąpić podczas pracy z poziomu strony WWW. Nazwa bazy danych, która ma
zostać użyta, jest wskazywana jako parametr konstruktora mysqli lub funkcji mysqli_connect().
Jeśli zajdzie konieczność zmiany domyślnej bazy danych, będzie można wykonać tę operację,
wywołując funkcję mysqli_select_db(). Można tego dokonać na dwa sposoby:
$db->select_db(nazwa_bazy);
lub wpisując:
mysqli_select_db(zasob_db, nazwa_bazy);
Przy okazji można tu zauważyć zależność między funkcjami, o której wspomniano już wcześniej:
wersje proceduralne rozpoczynają się od mysqli_ i wymagają podania dodatkowego parametru
w postaci uchwytu bazy danych.
W powyższym wierszu kodu należy zwrócić uwagę na dwa zagadnienia. Przede wszystkim zmienna
$metoda_szukania została umieszczona bezpośrednio w zapytaniu. Poza tym w miejscu, gdzie
można by się spodziewać zmiennej $wyrazenie, został umieszczony znak zapytania (?). O co w tym
wszystkim chodzi?
W tym przypadku mogą pojawić się zagrożenia określane ogólnie jako wstrzykiwanie SQL (ang.
SQL injection), które zostaną znacznie bardziej szczegółowo opisane w trzeciej części książki,
„E-commerce i bezpieczeństwo”. Najprościej rzecz ujmując, chodzi o to, że użytkownik mo-
że w polu formularza wpisać coś, co zostanie potraktowane jako kod SQL — takiej sytuacji za
wszelką cenę trzeba unikać.
280 Część II Stosowanie MySQL
W przedstawionym zapytaniu został umieszczony znak zapytania, gdyż posłuży ono do utworzenia
tak zwanego polecenia przygotowanego (ang. prepared statement). Znak zapytania odgrywa w nim
rolę zamiennika — przekazuje bazie danych następującą informację: „cokolwiek zostanie umiesz-
czone w miejscu znaku zapytania, należy to potraktować wyłącznie jako dane, a nie jako kod”.
Już za moment zostanie opisany sposób tworzenia i stosowania poleceń przygotowanych.
Można się zastanawiać, dlaczego takie samo rozwiązanie nie zostało wykorzystane w przypadku
zmiennej $metoda_szukania. Wynika to z tego, że takich zamienników można używać wyłącz-
nie w odniesieniu do danych, a nie nazw kolumn, tabel lub całych baz danych. W celu dodatkowego
zabezpieczenia w powyższym przykładzie zostało zastosowane rozwiązanie określane jako biała
lista, które pozwala sprawdzić, czy w zmiennej $metoda_szukania została podana jedna z dopusz-
czalnych wartości. (Jak można zauważyć, sprawdzenie tej zmiennej wcale nie było takie krótkie).
W następnej części rozdziału zostanie pokazane, w jaki sposób zapytanie z zamiennikiem można
zamienić na prawdziwe zapytanie.
Trzeba pamiętać, że w odróżnieniu od zapytań wpisywanych za pośrednictwem monitora MySQL
zapytania wysyłane do serwera bazy danych nie muszą być zakończone znakiem średnika.
Podstawowa idea poleceń przygotowanych polega na tym, że szablon polecenia jest przesyłany
do serwera MySQL, a następnie osobno są do niego przesyłane dane. Do jednego zapytania można
wielokrotnie przesyłać dane; możliwość ta jest szczególnie użyteczna w przypadku grupowego
wstawiania danych do bazy.
Podczas przygotowywania zapytania w miejscu, gdzie mają się pojawić dane, został umieszczony
znak zapytania. Wokół tego typu znaków zapytania nie należy umieszczać żadnych cudzysłowów,
apostrofów ani innych separatorów.
W drugim wierszu kodu została wywołana metoda $db->prepare(), która w przypadku stosowania
proceduralnego stylu programowania nosi nazwę mysqli_stmt_prepare(). Ten wiersz odpowiada
za utworzenie obiektu polecenia czy też zasobu, który będzie następnie użyty do wykonania zapyta-
nia oraz przetworzenia wyników.
Ten obiekt polecenia udostępnia metodę o nazwie bind_param(). (W wersji proceduralnej odpowiada
ona funkcji mysqli_stmt_bind_param()). Metoda ta służy do określania wartości, które mają zo-
stać wstawione w miejscu znaków zapytania. Pierwszym parametrem tej metody jest łańcuch zna-
ków określający format, przypominający nieco łańcuch formatujący stosowany w funkcji printf().
Rozdział 11. Łączenie się z bazą MySQL za pomocą PHP 281
Wartość 's' przekazywana w powyższym przykładzie oznacza, że dany parametr jest łańcuchem
znaków. Innymi dostępnymi wartościami łańcucha formatującego są: i, oznaczająca liczbę całko-
witą, d, oznaczająca liczbę zmiennoprzecinkową o podwójnej precyzji (double), oraz b, oznaczająca
duży obiekt binarny (BLOB). Po tym parametrze należy podać zmienne, przy czym ich liczba powinna
odpowiadać liczbie pytajników w zapytaniu. Wartości te będą podstawiane w podanej kolejności.
Na czym zatem polega przydatność poleceń przygotowanych? Otóż ważne jest to, że pozwalają
one na zmianę wartości powiązanych zmiennych i ponowne wykonanie zapytania bez konieczności
jego ponownego przygotowywania. Możliwość ta jest szczególnie użyteczna w przypadku grupo-
wego wstawiania danych do bazy w pętlach.
W tym przykładzie pobierane są zarówno liczba zwróconych wierszy, jak i wszystkie kolumny
dla każdego zwróconego wiersza.
umieszczając jej wywołanie w pętli. Każde wywołanie metody fetch() spowoduje pobranie kolej-
nego wiersza wyników i zapisanie umieszczonych w nim wartości w powiązanych zmiennych.
Kolejna operacja to pobranie i wyświetlenie każdego wiersza ze zbioru wyników; jest ona wykony-
wana w pętli w następujący sposób:
while($polecenie->fetch()) {
echo "<p><strong>Tytuł: ".$tytul."</strong>";
echo "<br />Autor: ".$autor;
echo "<br />ISBN: ".$isbn;
echo "<br />Cena: ".number_format($cena,2)." zł</p>";
}
Dane z wyników zapytania można pobierać nie tylko przy użyciu funkcji mysqli_stmt_fetch(),
ale także na inne sposoby. Aby z nich skorzystać, w pierwszej kolejności należy pobrać z polecenia
zasób zbioru wyników. Służy do tego funkcja mysqli_stmt_get_result() bądź też wywołanie
o następującej postaci:
$wynik = $polecenie->get_result();
Każde z powyższych wywołań zwraca obiekt mysqli_result, który udostępnia kilka użytecznych
funkcji do pobierania danych. Poniżej przedstawione zostały najbardziej przydatne z nich:
mysqli_fetch_array() (oraz związana z nią mysqli_fetch_assoc()) — zwraca następny
wiersz ze zbioru wyników w formie tablicy. Wersja mysqli_fetch_assoc() używa nazw
kolumn jako kluczy poszczególnych elementów tablicy, choć podobne możliwości
posiada także funkcja mysqli_fetch_array(). Funkcja ta może bowiem pobierać drugi,
opcjonalny argument, określający typ zwracanej tablicy. Przekazanie wartości MYSQLI_ASSOC
spowoduje zwrócenie tablicy, w której kluczami będą nazwy kolumn, przekazanie wartości
MYSQLI_NUM spowoduje zwrócenie tablicy indeksowanej liczbami, a użycie wartości
MYSQLI_BOTH spowoduje zwrócenie tablicy zawierającej dwa komplety danych: jednego,
w którym kluczami będą nazwy kolumn, oraz drugiego — indeksowanego liczbami.
mysqli_fetch_all() — zwraca wszystkie wiersze pobrane z bazy danych w formie tablicy
tablic, w której każda z wewnętrznych tablic reprezentuje jeden zwrócony wiersz.
mysqli_fetch_object() — zwraca następny wiersz ze zbioru wyników w formie obiektu,
w którym każda kolumna jest zapisana jako atrybut o nazwie odpowiadającej nazwie
kolumny.
lub
mysqli_free_result($wynik);
Rozdział 11. Łączenie się z bazą MySQL za pomocą PHP 283
lub
mysqli_close($db);
aby zamknąć połączenie z bazą danych. Użycie tej funkcji nie jest jednak konieczne, gdyż połącze-
nie i tak zostanie zamknięte z chwilą zakończenia wykonywania skryptu.
Choć operacja ta przebiega podobnie, warto jednak przeanalizować przykład. Na rysunku 11.3
zaprezentowano formularz HTML, który służy do zapisywania informacji o nowych książkach.
Rysunek 11.3.
Taki formularz
mógłby służyć
personelowi
księgarni „Książkora
ma” do zapisywania
informacji o nowych
książkach
<style type="text/css">
fieldset {
width: 75%;
border: 2px solid #cccccc;
}
label {
width: 75px;
284 Część II Stosowanie MySQL
float: left;
text-align: left;
font-weight: bold;
}
input {
border: 1px solid #000;
padding: 3px;
}
</style>
</head>
<body>
<h1>"Książkorama" - Wstawianie nowej książki</h1>
<fieldset>
<p><label for="isbn">ISBN</label>
<input type="text" id="isbn" name="isbn" maxlength="13" size="13" /></p>
<p><label for="autor">Author</label>
<input type="text" id="autor" name="autor" maxlength="30" size="30" /></p>
<p><label for="tytul">Tytuł</label>
<input type="text" id="tytul" name="tytul" maxlength="60" size="30" /></p>
<p><label for="cena">Cena</label>
<input type="text" id="cena" name="cena" maxlength="7" size="7" /></p>
</fieldset>
</form>
</body>
</html>
Listing 11.4. wstaw_ksiazke.php — skrypt zapisujący informacje o nowej książce do bazy danych
<!DOCTYPE html>
<html>
<head>
<title>"Książkorama" - rezultat wstawiania nowej książki</title>
</head>
<body>
<h1>"Książkorama" - rezultat wstawiania nowej książki </h1>
<?php
if (!isset($_POST['isbn']) || !isset($_POST['autor'])
|| !isset($_POST['tytul']) || !isset($_POST['cena'])) {
echo "<p>Nie podano informacji we wszystkich wymaganych polach.<br />
Proszę wrócić na poprzednią stronę i spróbować jeszcze raz.</p>";
exit;
}
Rozdział 11. Łączenie się z bazą MySQL za pomocą PHP 285
if (mysqli_connect_errno()) {
echo "<p>Błąd: Połączenie z bazą danych nie powiodło się.<br />
Spróbuj jeszcze raz później.</p>";
exit;
}
if ($polecenie->affected_rows > 0) {
echo "<p>Książka została zapisana w bazie danych.</p>";
} else {
echo "<p>Wystąpił błąd. <br />
Książka nie została dodana do bazy.</p>";
}
$db->close();
?>
</body>
</html>
Rysunek 11.4.
W razie poprawnego
wykonania skryptu
informacja o zapisaniu
nowej książki zostanie
wyświetlona na stronie
WWW
Kolejną czynnością jest nawiązanie połączenia z bazą danych poprzez utworzenie obiektu mysqli
oraz przygotowanie zapytania, które zostanie przesłane do bazy. W tym przypadku zapytanie będzie
zawierać polecenie SQL INSERT:
$query = "INSERT INTO Ksiazki VALUES (?, ?, ?, ?)";
$polecenie = $db->prepare($query);
$polecenie->bind_param('sssd', $isbn, $autor, $tytul, $cena);
$polecenie->execute();
Generalne zasady łączenia się z bazami danych i wykonywania zapytań pozostają niezmienione.
Różnice mogą wystąpić np. w nazwach poszczególnych funkcji, inne mogą też być możliwości
poszczególnych systemów. Jeżeli jednak praca z MySQL nie sprawia Czytelnikowi kłopotów,
to korzystanie z innych baz również nie powinno przysparzać żadnych problemów.
Jeżeli zaistnieje potrzeba wykorzystania bazy danych, która nie jest obsługiwana przez żadną
z bibliotek PHP, można skorzystać z ODBC (Open Database Connectivity), będącego standardo-
wym interfejsem dostępu do baz danych. Udostępnia on tylko podstawowe metody dostępu do
baz danych, z oczywistych zresztą powodów. Jeśli bowiem interfejs ma być zgodny ze wszystkimi
typami baz, to nie ma możliwości, by udostępniał funkcje charakterystyczne tylko dla jednego czy
kilku systemów zarządzania nimi.
Oprócz bibliotek dostarczanych wraz z PHP istnieje także rozszerzenie o nazwie PDO, stanowiące
abstrakcję dostępu do danych — pozwala ono na używanie dokładnie tego samego interfejsu do
obsługi wielu różnych baz danych.
Dla porównania spójrzmy, w jaki sposób napisalibyśmy skrypt wykonujący przeszukiwanie, wy-
korzystując do tego PDO (patrz listing 11.5).
Listing 11.5. rezultaty_pdo.php — pobiera wyniki wyszukiwania w bazie MySQL i formatuje je w celu ich
wyświetlenia
<!DOCTYPE html>
<html>
<head>
<title>"Książkorama"- Rezultaty wyszukiwania</title>
</head>
Rozdział 11. Łączenie się z bazą MySQL za pomocą PHP 287
<body>
<h1>"Książkorama"- Rezultaty wyszukiwania </h1>
<?php
// utworzenie krótkich nazw zmiennych
$metoda_szukania=$_POST['metoda_szukania'];
$wyrazenie="%{$_POST['wyrazenie']}%";
if (!$metoda_szukania || !$wyrazenie) {
echo '<p>Brak parametrów wyszukiwania. <br />
Wróć do poprzedniej strony i spróbuj ponownie.</p>';
exit;
}
// koniguracja DSN
$dsn = "mysql:host=$host;dbname=$nazwa_bazy";
// wykonanie zapytania
$zapytanie = "SELECT ISBN, Autor, Tytul, Cena FROM Ksiazki WHERE $metoda_szukania =
:szukanewyrazenie";
$polecenie = $db->prepare($zapytanie);
$polecenie->bindParam(':szukanewyrazenie', $wyrazenie);
$polecenie->execute();
?>
</body>
</html>
Funkcja ta akceptuje łańcuch połączenia, określany także jako DNS lub nazwa źródła danych, zawie-
rający wszystkie parametry niezbędne do nawiązania połączenia z bazą danych. Można się o tym
przekonać, analizując format łańcucha połączenia:
$dsn = "mysql:host=$host;dbname=$nazwa_bazy";
Jak łatwo zauważyć, cały kod wchodzący w interakcję z bazą danych jest umieszczony wewnątrz
bloku try:catch. Domyślnie, jeśli wystąpią jakieś problemy, PDO zgłosi wyjątek. Prawie na samym
końcu skryptu umieszczona jest instrukcja przechwytująca zgłaszane błędy:
catch (PDOException $e) {
echo "Błąd: ".$e->getMessage();
exit;
}
Jak widać, zgłaszane wyjątki będą typu PDOException i będą zawierać komunikat informujący
o przyczynie problemów.
Zakładając, że wszystko poszło dobrze, następną wykonywaną operacją jest utworzenie i wyko-
nanie zapytania:
$zapytanie = "SELECT ISBN, Autor, Tytul, Cena FROM Ksiazki
WHERE $metoda_szukania = :szukanewyrazenie";
$polecenie = $db->prepare($zapytanie);
$polecenie->bindParam(':szukanewyrazenie', $wyrazenie);
$polecenie->execute();
Warto zwrócić uwagę na to, że w PDO liczba pobranych wierszy jest zwracana przez metodę, a nie
udostępniana jako atrybut obiektu, dlatego też za rowCount została umieszczona para nawiasów.
Ogólna metoda fetch() może zwracać wiersze w wielu różnych formatach — parametr
PDO::FETCH_OBJ wskazuje, że wiersze mają być zwracane w formie anonimowego obiektu.
Po wyświetleniu zwróconych wierszy kończymy skrypt, zwalniając zasób bazy danych, co spowo-
duje także zamknięcie połączenia z bazą danych:
$db = NULL;
Rozdział 11. Łączenie się z bazą MySQL za pomocą PHP 289
Jak widać, powyższy ogólny przykład jest bardzo podobny do naszego pierwszego skryptu.
Zalety stosowania PDO polegają na tym, że wystarczy znać tylko jeden zestaw funkcji baz danych,
a napisany kod będzie wymagał co najwyżej niewielkich zmian w sytuacji, gdybyśmy zdecydo-
wali się na zmianę używanej bazy danych.
W następnym rozdziale
W następnym rozdziale przedstawimy szczegółowe informacje na temat administrowania serwerem
MySQL. Omówimy w nim również zagadnienia dotyczące optymalizacji działania baz danych
oraz ich replikacji.
290 Część II Stosowanie MySQL
Rozdział 12.
Administrowanie MySQL
dla zaawansowanych
W tym rozdziale przedstawimy bardziej zaawansowane aspekty pracy z MySQL, w tym zaawanso-
wany system przywilejów, bezpieczeństwo i optymalizację pracy serwera.
Wykonanie polecenia GRANT powoduje zmiany w tabelach systemowej bazy danych o nazwie mysql.
Informacje dotyczące przywilejów są przechowywane w siedmiu tabelach tej bazy. Z tego powodu
nadając przywileje na poziomie bazy danych, należy zachować ostrożność z udostępnianiem
bazy mysql.
Aby przejrzeć zawartość systemowej bazy danych mysql, należy zalogować się jako administrator
i wpisać polecenie:
USE mysql;
Następnie można sprawdzić, jakie tabele zawiera wybrana baza. W tym celu należy wpisać:
SHOW TABLES;
292 Część II Stosowanie MySQL
Wszystkie powyższe tabele przechowują informacje systemowe. Siedem z nich — user, host, db,
tables_priv, columns_priv, proxies_priv oraz procs_priv — przechowuje dane na temat przy-
dzielonych przywilejów. Są one czasami nazywane tabelami przywilejów. Każda z nich spełnia
odrębną funkcję, jednak ich najważniejszym zadaniem jest określenie, jakie operacje dany użyt-
kownik może wykonywać, a do jakich nie jest uprawniony. Wszystkie tabele zawierają pola kilku
typów: pola zakresu identyfikują użytkownika, komputer i jakąś część bazy danych, której do-
tyczą przywileje, a pola przywilejów określają, jakie działania może wykonać dany użytkownik
na wskazanym obiekcie bazy danych. Istnieją także pola bezpieczeństwa, zawierające informacje
związane z zabezpieczeniami, oraz kolumny kontroli kolumn, ograniczające liczbę zasobów, które
mogą być używane.
Tabela o nazwie user służy do określania tego, czy użytkownik w ogóle może nawiązać połączenie
z serwerem MySQL oraz czy ma jakieś przywileje administracyjne. Tabela host była używana
wcześniej, lecz obecnie jest uznawana za przestarzałą i nie jest stosowana. Tabela db określa, do któ-
rych baz danych użytkownik ma dostęp. W tabeli tables_priv zapisane są nazwy tabel, które mogą
być wykorzystywane w ramach dostępnych użytkownikowi baz danych. Tabela columns_priv
Rozdział 12. Administrowanie MySQL dla zaawansowanych 293
zawiera nazwy tych kolumn, w tabeli proxies_priv znajdują się informacje pozwalające wskazać
użytkowników mogących działać jako pośrednicy czy też przekazywać przywileje innym użytkow-
nikom, do których wskazani użytkownicy mają dostęp, a w tabeli procs_priv znajdują się nazwy
procedur, które można wykonywać.
Tabela user
W tabeli tej zawarte są szczegółowe dane o przywilejach o zasięgu globalnym. Określają one, czy
użytkownik w ogóle może zalogować się do serwera MySQL, a jeśli tak, to czy posiada on jakie-
kolwiek przywileje globalne, tzn. odnoszące się do wszystkich baz danych w systemie.
Pole Typ
Host char(60)
User char(16)
Password char(16)
Tabela 12.1. Schemat tabeli user systemowej bazy danych mysql (ciąg dalszy)
Pole Typ
Repl_client_priv enum('N', 'Y')
ssl_cipher blob
x509_issuer blob
x509_subject blob
plugin char(64)
authentication_string text
password_expired enum('Y','N')
Większość wierszy tej tabeli odnosi się do zbioru przywilejów nadanych użytkownikowi User
łączącemu się z komputera Host i logującemu się za pomocą hasła Password. Te trzy pola to tak
zwane pola zakresu tabeli, gdyż określają one zakres pól przywilejów. Nazwy pól przywilejów
kończą się ciągiem znaków _priv.
Wszystkie przywileje zawarte w tabeli user mają charakter globalny, a więc odnoszą się do
wszystkich baz danych w systemie (z bazą mysql włącznie). W przypadku administratorów niektóre
(lub wszystkie) pola tej tabeli przyjmą więc wartość Y. Jednak dla większości użytkowników
wszystkie pola powinny mieć wartość N, gdyż zwykłym użytkownikom należy nadawać przywileje
odnoszące się tylko do wyznaczonych im baz danych, a nie do wszystkich tabel w systemie.
Dwoma kolejnymi grupami pól dostępnymi w tej tabeli są pola bezpieczeństwa oraz pola kontroli
zasobów.
Rozdział 12. Administrowanie MySQL dla zaawansowanych 295
Tabela db
Większość przywilejów nadanych zwykłym użytkownikom jest przechowywana w tabeli db.
Tabela db wyznacza, którzy użytkownicy i z jakich komputerów mają dostęp do określonych baz
danych. Przywileje z tej tabeli odnoszą się do bazy danych o nazwie wyszczególnionej w tym samym
wierszu.
Pole Typ
Host char(60)
Db char(64)
User char(16)
Struktura tych tabel jest nieco inna niż struktura user oraz db. Schematy tabel tables_priv, columns_
priv, procs_priv oraz proxies_priv są przedstawione, odpowiednio, w tabelach 12.3, 12.4, 12.5
i 12.6.
Pole Typ
Host char(60)
Db char(64)
User char(16)
Table_name char(60)
Grantor char(77)
Timestamp timestamp
Pole Typ
Host char(60)
Db char(64)
User char(16)
Table_name char(64)
Column_name char(64)
Timestamp timestamp
Pole Typ
Host char(60)
Db char(64)
User char(16)
Routine_name char(64)
Grantor char(77)
Timestamp timestamp
Rozdział 12. Administrowanie MySQL dla zaawansowanych 297
Pole Typ
Host char(60)
User char(16)
Proxied_host char(64)
Proxied_user char(16)
With_grant tinyint(1)
Grantor char(77)
Timestamp timestamp
W polu Grantor tabel tables_priv oraz procs_priv przechowywany jest identyfikator użytkownika,
który przyporządkował określone przywileje, natomiast w polu Timestamp jest zapisywana data
i godzina ich nadania. Jeśli chodzi o tabelę proxies_priv, to obecnie kolumny te nie są w niej
używane.
Konieczne jest wówczas nakazanie serwerowi, aby na nowo odczytał dane z tabel przywilejów.
Można to zrobić na trzy sposoby. Pierwszy z nich polega na wpisaniu polecenia
mysql> flush privileges;
w wierszu poleceń MySQL (aby polecenie to zostało wykonane, trzeba posiadać uprawnienia admi-
nistratora). Ten sposób jest najczęściej używany w celu zastosowania nowych przywilejów.
lub
> mysqladmin reload
Poza tym warto tak uruchamiać serwer MySQL w wewnętrznej sieci, by pracował on za zaporą
sieciową (ang. firewall). W ten sposób można uniknąć połączeń wychodzących z nieautoryzowa-
nych komputerów — przedtem należy sprawdzić, czy istnieje możliwość połączenia z zewnętrznego
komputera przez port 3306. Jest to domyślny port wykorzystywany przez serwer MySQL i jako
taki powinien być zablokowany przez zaporę sieciową.
Rozdział 12. Administrowanie MySQL dla zaawansowanych 299
Hasła
Każdemu użytkownikowi (szczególnie użytkownikowi root) należy nadać hasło dostępu, które
dodatkowo powinno być często zmieniane i trudne do odgadnięcia — tak jak w przypadku haseł
dostępu do systemu operacyjnego. Podstawowa zasada jest taka, że hasła te nie mogą być zwykłymi
słowami, które można znaleźć w słowniku. Najlepszym rozwiązaniem jest zastosowanie kombi-
nacji liter, cyfr oraz innych symboli.
Jeżeli hasła dostępu mają być przechowywane w plikach skryptów, to pliki te powinny być dostępne
tylko dla użytkowników, do których te hasła należą. Zasada ta odnosi się jedynie do współużyt-
kowanych środowisk hostingowych, które obecnie nie są zbyt popularne.
W skryptach PHP inicjujących połączenie z serwerem baz danych trzeba podać hasło dostępu
odpowiedniego użytkownika. Hasło to oraz identyfikator użytkownika można zapisać w oddzielnym
pliku, np. dbpolaczenie.php, który w razie potrzeby dołącza się do skryptu za pomocą polecenia
include(). Plik ten można zapisać w innej lokalizacji niż dokumenty WWW i udostępnić tylko
jednemu, wskazanemu użytkownikowi.
Niewskazane jest przechowywanie haseł dostępu w formie zwykłego tekstu w bazie danych.
Oczywiście MySQL utrwala hasła zarejestrowanych użytkowników w inny sposób, jednak w przy-
padku aplikacji internetowych czasami trzeba zapisać identyfikatory i hasła dostępu użytkowników
z niej korzystających w bazie danych. Dane te można przed zapisaniem zaszyfrować (jednostron-
nie, tzn. bez możliwości ich odszyfrowania) za pomocą funkcji password(). Należy jednak pamiętać,
że po zapisaniu tak zaszyfrowanego hasła poprzez polecenie INSERT w tekście polecenia SELECT,
wykonywanego w celu zalogowania użytkownika, również trzeba użyć tej samej funkcji szyfrują-
cej, aby sprawdzić, czy podane hasło dostępu jest prawidłowe.
Sposób ten zostanie wykorzystany w czasie tworzenia aplikacji PHP w części V tej książki, zatytu-
łowanej „Tworzenie praktycznych projektów PHP i MySQL”.
Przywileje użytkowników
Wiedza to moc. Czytelnik powinien się więc upewnić, że dobrze zrozumiał system przywilejów
MySQL i poznał następstwa nadania poszczególnych typów. Nie należy przyznawać użytkowni-
kowi szerszych przywilejów, niż jest to potrzebne — trzeba je sprawdzić, przeglądając tabele
przywilejów.
Należy zacząć od tego, by nie nadawać przywilejów do zarządzania bazą danych mysql żadnemu
użytkownikowi, który nie posiada uprawnień administracyjnych.
W szczególności nie powinno się nadawać przywilejów PROCESS, FILE, SHUTDOWN i RELOAD żadnemu
użytkownikowi, który nie jest administratorem, o ile nie ma takiej konieczności, a jeśli już taka
konieczność się pojawi, to należy spróbować przydzielić te uprawnienia na możliwie jak najkrót-
szy czas. Przywileju PROCESS można używać do podglądania czynności wykonywanych przez innych
użytkowników oraz wpisywanych przez nich poleceń i danych, a więc także ich haseł dostępu.
300 Część II Stosowanie MySQL
Również przywilej GRANT powinno nadawać się z rozwagą, gdyż użytkownik, który go posiada, może
nadawać innym użytkownikom takie przywileje, jakie sam ma.
I dodatkowo, co zapewne nie jest wcale oczywiste, także przywilej ALTER należy przydzielać z roz-
wagą. Użytkownicy mogą go wykorzystać do zmiany nazw tabel, a co za tym idzie, do oszukania
systemu przywilejów.
Zakres ochrony serwera MySQL można dodatkowo zwiększyć, zapisując w tabeli host adresy
IP komputerów zamiast nazw ich domen. Dzięki temu unikamy problemów spowodowanych np.
błędami serwera DNS czy działaniami hakerów. W tym celu należy uruchomić demona MySQL
z parametrem --skip-name-resolve, dzięki któremu w polu Host będzie można zapisać tylko adres
IP lub wartość localhost.
Kolejną ważną kwestią, o której należy pamiętać, jest zablokowanie możliwości uruchamiania
programu mysqladmin przez użytkowników niemających statusu administratora. Program ten
uruchamiany jest z poziomu wiersza poleceń, a więc należy przeprowadzić odpowiednie zmiany
w systemie przywilejów systemu operacyjnego.
MySQL i internet
Łączenie serwera MySQL ze stronami WWW wymaga zastosowania dodatkowych środków
ostrożności.
Wszystkie dane pochodzące od użytkownika powinny podlegać weryfikacji. Nawet gdy formularz
HTML składa się z samych pól wyboru i przycisków opcji, ktoś może próbować zmienić URL
w celu dokonania zmian w działaniu skryptu. Warto również za każdym razem sprawdzać roz-
miar danych wysyłanych przez użytkownika.
Jeżeli użytkownicy mają wysyłać do bazy poufne dane lub podawać hasła dostępu, należy zastoso-
wać protokół SSL (Secure Sockets Layer), dzięki czemu dane te będą przekazywane z przeglądarki
do serwera w formie zaszyfrowanej, a nie jako zwykły tekst. Praca z SSL zostanie omówiona
w jednym z następnych rozdziałów.
Rozdział 12. Administrowanie MySQL dla zaawansowanych 301
Polecenie
mysql> SHOW DATABASES;
powoduje wyświetlenie listy wszystkich dostępnych na serwerze baz danych. Można więc użyć
polecenia SHOW TABLES w celu uzyskania listy tabel zawartych w jednej z dostępnych baz danych:
mysql> SHOW TABLES FROM Ksiazki;
Jeżeli w poleceniu SHOW TABLES nie zostanie wyspecyfikowana nazwa bazy danych, wówczas
wyświetlone zostaną tabele bazy aktualnie używanej.
Jeśli znane są nazwy tabel, można uzyskać listę kolumn w niej zawartych:
mysql> SHOW COLUMNS FROM Zamowienia FROM Ksiazki;
Jeśli nazwa bazy danych nie zostanie podana, to polecenie SHOW COLUMNS będzie się odnosiło do
bazy aktualnie używanej. Możliwe jest również użycie notacji baza.tabela:
mysql> SHOW COLUMNS FROM Ksiazki.Zamowienia;
Jeszcze inna forma polecenia SHOW pozwala na sprawdzenie przywilejów nadanych wskazanemu
użytkownikowi. Na przykład w wyniku wykonania poniższego polecenia
mysql> SHOW GRANTS FOR 'ksiazkorama';
zwrócony zostanie następujący wynik:
+------------------------------------------------------------------------- +
| Grants for ksiazkorama@% |
+--------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'ksiazkorama'@'%' IDENTIFIED BY PASSWORD
'*709DAE5A09B166086E25836CAD952AC6380691C7' |
| GRANT SELECT, INSERT, UPDATE, DELETE ON `ksiazki`.* TO 'ksiazkorama'@'%' |
+-------------------------------------------------------------------------+
2 rows in set (0.00 sec)
Istnieje wiele innych form polecenia SHOW, których można używać. W praktyce używanych
jest ponad 30 odmian polecenia, a najbardziej popularne z nich przedstawiono w tabeli 12.7. Pełna
302 Część II Stosowanie MySQL
lista wszystkich form polecenia SHOW znajduje się w podręczniku MySQL pod adresem http://dev.
mysql.com/doc/refman/5.6/en/show.html. We wszystkich przypadkach gdy w tabeli występuje wyra-
żenie [like_lub_where], dopasowanie wzorca można wykonać instrukcją LIKE albo przy użyciu
klauzuli WHERE.
Forma Opis
SHOW DATABASES [like_lub_where] Wyświetla nazwy wszystkich dostępnych baz danych
SHOW [OPEN] TABLES [FROM Wyświetla listę tabel z aktualnie używanej bazy danych lub z bazy
nazwa_bazy] [like_lub_where] o nazwie nazwa_bazy
SHOW [FULL] COLUMNS FROM Wyświetla wszystkie kolumny z podanej tabeli w aktualnie używanej
nazwa_tabeli [FROM nazwa_bazy] bazie danych lub w bazie o nazwie nazwa_bazy
[like_lub_where]
SHOW INDEX FROM nazwa_tabeli Wyświetla dane o wszystkich indeksach ustawionych na wskazanej tabeli
[FROM nazwa_bazy] z aktualnie używanej bazy danych lub z bazy o nazwie nazwa_bazy, jeśli
została podana. Równoważnym poleceniem jest SHOW KEYS
SHOW [GLOBAL | SESSION] STATUS Wyświetla informacje o liczbie składników systemowych, np. liczbę
[like_lub_where] aktywnych wątków. Klauzula LIKE służy do określania wzorców nazw
tych składników, np. 'Thread%' dotyczy składników 'Threads_cached',
'Threads_connected', 'Threads_created' i 'Threads_running'
SHOW [GLOBAL|SESSION] VARIABLES Wyświetla nazwy i wartości zmiennych systemowych MySQL, np. numer
[LIKE nazwa_zmiennej] wersji. Klauzula LIKE służy do określania wzorców nazw tych zmiennych
w taki sam sposób, jak w przypadku polecenia SHOW STATUS
SHOW [FULL] PROCESSLIST Wyświetla informacje o wszystkich aktywnych procesach w systemie,
tj. aktualnie wykonywanych zapytaniach. Większość użytkowników
będzie widzieć tylko ich własny proces, jednak użytkownicy, którym nadano
przywilej PROCESS, widzą wszystkie procesy wykonywane przez system,
a więc również hasła, jeśli są zawarte w zapytaniach. Domyślnie
wyświetlanych jest 100 znaków zapytania. Użycie opcji FULL powoduje
wyświetlenie pełnych tekstów zapytań
SHOW TABLE STATUS [FROM Wyświetla informacje o wszystkich tabelach z aktualnie używanej bazy
nazwa_bazy] [like_lub_where] danych lub z bazy o nazwie nazwa_bazy, jeśli została podana, opcjonalnie
— z bazy, której nazwa pasuje do wzorca nazwa_bazy. Informacje te zawierają
określenie typu tabeli oraz czas przeprowadzenia ostatniej zmiany
SHOW GRANTS FOR Wyświetla polecenia GRANT, jakich należałoby użyć, by użytkownikowi
identyfikator_użytkownika o identyfikatorze identyfikator_użytkownika nadać aktualnie
przypisane mu przywileje
SHOW PRIVILEGES Wyświetla przywileje obsługiwane przez serwer
SHOW CREATE DATABASE db Wyświetla instrukcję CREATE DATABASE, przez którą można utworzyć
wskazaną bazę danych
SHOW CREATE TABLE nazwa_tabeli Wyświetla instrukcję CREATE TABLE, przez którą można utworzyć
wskazaną tabelę
SHOW [STORAGE] ENGINES Wyświetla listę mechanizmów składowania danych, które są dostępne
w bieżącej instalacji, oraz wskazuje domyślny z nich (mechanizmy
składowania danych zostaną szczegółowo przedstawione w rozdziale 13.)
SHOW ENGINE To polecenia ma kilka wariantów, lecz najczęściej jest stosowane w postaci
SHOW ENGINE INNODB STATUS (wcześniej miało ono postać SHOW INNODB
STATUS), która zwraca informacje o bieżącym stanie mechanizmu
składowania danych InnoDB
Rozdział 12. Administrowanie MySQL dla zaawansowanych 303
Forma Opis
SHOW WARNINGS [LIMIT Wyświetla wszystkie błędy, ostrzeżenia i uwagi wygenerowane w trakcie
[przesuniecie,] liczba_wierszy] wykonywania ostatniej instrukcji
SHOW ERRORS [LIMIT Wyświetla wyłącznie błędy wygenerowane w trakcie wykonywania
[przesuniecie,] liczba_wierszy] ostatniej instrukcji
działa podobnie jak polecenie DESCRIBE nazwa_tabeli lub SHOW COLUMNS FROM nazwa_tabeli.
Drugi, ciekawszy sposób, w jaki można użyć tego polecenia, pozwala na uzyskanie informacji
na temat sposobu wykonania zapytań SELECT przez serwer MySQL. W tym celu należy wpisać
słowo explain przed poleceniem SELECT.
Polecenie EXPLAIN znajduje zastosowanie w przypadku, gdy bardziej złożona kwerenda nie
zwraca oczekiwanych rezultatów lub też jej wykonanie zajmuje znacznie więcej czasu niż powinno.
Tworząc złożone zapytanie, można sprawdzić jego działanie poprzez wydanie polecenia EXPLAIN
jeszcze przed wykonaniem tego zapytania. Na podstawie otrzymanych wyników można tak
zmodyfikować zapytanie, by poprawić lub zoptymalizować jego działanie. Otrzymany rezultat służy
również celom poznawczym.
Na pierwszy rzut oka otrzymane zestawienie może wydawać się bardzo skomplikowane, lecz tak
naprawdę jest ono niezmiernie użyteczne. Przeanalizujmy po kolei wszystkie kolumny wyświetlonej
tabeli.
Pierwsza kolumna o nazwie id zawiera numer identyfikacyjny instrukcji SELECT wewnątrz zapytania,
do którego odnosi się analizowany wiersz.
Kolumna select_type prezentuje typ użytego zapytania. Wartości, jakie mogą znaleźć się w tej
kolumnie, przedstawiono w tabeli 12.8.
Rozdział 12. Administrowanie MySQL dla zaawansowanych 305
Tabela 12.8. Typy instrukcji SELECT, jakie mogą się pojawić w danych wynikowych polecenia EXPLAIN
Typ Opis
SIMPLE Stary dobry SELECT, jak w powyższym przykładzie
PRIMARY Zewnętrzne (pierwsze) zapytanie, w którym użyto podzapytań i unii
UNION Drugie lub dalsze zapytanie w unii
DEPENDENT UNION Drugie lub dalsze zapytanie w unii, zależne od zapytania głównego
UNION RESULT Wynik wykonania polecenia UNION
SUBQUERY Podzapytanie wewnętrzne
DEPENEDENT SUBQUERY Podzapytanie wewnętrzne zależne od zapytania głównego (czyli podzapytanie
skorelowane)
DERIVED Podzapytanie użyte w klauzuli FROM
MATERIALIZED Podzapytanie wyznaczane wcześniej
UNCACHEABLE SUBQUERY Podzapytanie, którego wyniku nie należy zapisywać w buforze i dla każdego
wiersza trzeba je wykonywać na nowo
UNCACHEABLE UNION Drugie lub kolejne polecenie UNION należące do podzapytania, którego nie
należy zapisywać w buforze
W kolumnie table figurują nazwy wszystkich tabel potrzebnych do wykonania zapytania. Każdy
z wierszy zawiera szczegółowe informacje na temat sposobu, w jaki dana tabela jest używana
w czasie przetwarzania zapytania. W naszym przykładzie jest oczywiste, że do wykonania zapytania
zostaną użyte tabele zamowienia, pozycje_zamowione, klienci i ksiazki (te informacje są już zresztą
zawarte w samym tekście zapytania).
Kolumna type określa sposób, w jaki dana tabela jest łączona z innymi w czasie przetwarzania
zapytania. Zestaw wszystkich wartości, jakie mogą być wyświetlone w tej kolumnie, jest ukazany
w tabeli 12.9. Wartości te uszeregowane są w kolejności od najszybciej do najwolniej wykonywa-
nych połączeń. Dzięki temu wiadomo, ile wierszy tabeli musi zostać odczytanych w celu wyko-
nania zapytania.
Typ Opis
const lub system Dane z tabeli są odczytywane tylko jeden raz. Dzieje się tak w przypadku, gdy tabela
zawiera tylko jeden wiersz. Wartość system wskazuje, iż jest to tabela systemowa;
w przeciwnym wypadku wyświetlana jest wartość const
eq_ref Dla każdego zbioru wierszy z tabeli dołączanej jest odczytywany tylko jeden wiersz
wskazanej tabeli. Sytuacja taka ma miejsce wtedy, gdy połączenie dokonywane jest na
podstawie wszystkich pól indeksu ustawionego na tej tabeli i jest to indeks typu UNIQUE
lub klucz podstawowy
fulltext Połączenie jest wykonywane przy użyciu indeksu fulltext
ref Dla każdego zbioru wierszy z tabeli dołączanej odczytywany jest zbiór wszystkich
pasujących wierszy ze wskazanej tabeli. Do sytuacji tej dochodzi wtedy, gdy połączenie
nie może zostać przeprowadzone na podstawie tylko jednego wiersza spełniającego
warunek połączenia, tzn. gdy połączenie odbywa się tylko na podstawie części klucza
lub gdy klucz ten nie ma atrybutu UNIQUE lub nie jest kluczem podstawowym
306 Część II Stosowanie MySQL
Tabela 12.9. Możliwe typy połączeń wyświetlanych przez polecenie EXPLAIN (ciąg dalszy)
Typ Opis
ref_or_null Podobne do zapytania ref, lecz w tym przypadku MySQL wyszuka również wiersze
mające wartość NULL (ten typ jest używany najczęściej w podzapytaniach)
index_merge Użyto optymalizacji Index Merge
unique_subquery Połączeń tego typu używa się w celu zastąpienia zapytania ref podzapytaniami IN,
w których zwracany jest jeden unikatowy wiersz
index_subquery Połączenie tego typu jest podobne do połączenia unique_subquery, lecz używa się go
w przypadku indeksowanych podzapytań, które nie są unikatowe
range Dla każdego zbioru wierszy z tabeli dołączanej odczytywany jest zbiór wierszy
ze wskazanej tabeli, których wartości należą do określonego zbioru
index Cały indeks zostaje odczytany
ALL Każdy wiersz tabeli zostaje odczytany
W analizowanym przykładzie widać, że jedna tabela łączona jest połączeniem typu eq_ref (tabela
Klienci), w jednej używane jest połączenie typu ref (tabela Pozycje_zamowione), następna jest łączona
połączeniem typu index (tabela Zamowienia), a w przypadku kolejnej następuje połączenie typu ALL,
czyli odczytywane są wszystkie wiersze tych tabel.
Uzupełnieniem tych danych są informacje zawarte w kolumnie rows. W kolumnie tej wyświetlona
jest przybliżona liczba wierszy, które należy odczytać w celu dokonania połączenia. Wartości te
można pomnożyć przez siebie dla otrzymania liczby wszystkich wierszy, które zostaną odczytane
w trakcie przetwarzania zapytania. Wykonujemy w tym przypadku mnożenie, gdyż połączenie
jest w istocie kombinacją wszystkich wierszy poszczególnych tabel (zobacz rozdział 10.). Należy
zwrócić uwagę, że otrzymana wartość będzie liczbą wierszy odczytanych, a nie zwróconych
w odpowiedzi, oraz że będzie to tylko wartość przybliżona — MySQL nie może rozpoznać dokład-
nej liczby wierszy bez wykonania zapytania.
Oczywiście im mniejsza będzie ta liczba, tym lepiej. Na razie baza księgarni „Książkorama”
zawiera niewielką liczbę danych, lecz kiedy jej rozmiar zacznie się zwiększać, wydłużać się będzie
również czas wykonania zapytania. Powrócimy do tej sprawy w dalszej części rozdziału.
Jak można się spodziewać, kolumna possible_keys wyświetla klucze, które mogą zostać wykorzy-
stane do łączenia tabel.
W kolumnie key wypisane są te klucze, które rzeczywiście zostały użyte przez serwer MySQL
lub wartości NULL w przypadku, gdy nie użyto żadnego klucza.
Kolumna key_len wyświetla długości wykorzystanych kluczy. Na podstawie tych wartości można
stwierdzić, czy użyto klucza w całości, czy tylko jego części. Jest to szczególnie przydatne
w sytuacji, gdy klucze podstawowe składają się z więcej niż jednego pola tabeli. W naszym przykła-
dzie zastosowano klucze o pełnej długości.
W kolumnie ref zawarte są nazwy kolumn, których wraz z kluczem użyto do wyszukania wierszy
w tabeli.
Ostatnia kolumna, Extra, może zawierać szereg innych informacji dotyczących sposobu przeprowa-
dzenia łączenia. Wybrane wartości, które mogą zostać zawarte w tej kolumnie, są przedstawione
w tabeli 12.10. Pełna lista ponad 30 dostępnych wartości znajduje się w podręczniku MySQL
pod adresem http://dev.mysql.com/doc/refman/5.6/en/using-explain.html.
Rozdział 12. Administrowanie MySQL dla zaawansowanych 307
Tabela 12.10. Wybrane możliwe wartości kolumny Extra wyświetlane po wykonaniu polecenia EXPLAIN
Wartość Znaczenie
Distinct Po znalezieniu pierwszego pasującego wiersza MySQL kończy wyszukiwanie pozostałych
wierszy
Not exists Zapytanie zostało zoptymalizowane w celu użycia LEFT JOIN
Range checked for Dla każdego wiersza ze zbioru wierszy, odczytanego z tabel dołączanych, nastąpi próba
each record znalezienia i wykorzystania najlepszego indeksu, jeśli istnieje
Using filesort Sortowanie danych będzie wymagało dwukrotnego przeanalizowania wszystkich wierszy
(co oczywiście zajmie dwukrotnie więcej czasu)
Using index Wszystkie informacje z tabeli zostaną odczytane z indeksu — a więc wiersze nie będą
odczytywane
Using join buffer Tabele są odczytywane w częściach przy użyciu bufora połączenia, następnie w celu zakończenia
wykonywania zapytania z bufora pobierane są odpowiednie wiersze
Using temporary W celu wykonania zapytania konieczne jest utworzenie dodatkowej, tymczasowej tabeli
Using WHERE Wybór wierszy zostanie dokonany z wykorzystaniem klauzuli WHERE
I w końcu warto także zwrócić uwagę na informacje umieszczone w kolumnie Extra. Jeśli pojawi się
w niej tekst: Using temporary, to często będzie to oznaczało, że w klauzulach GROUP BY oraz ORDER BY
są używane inne kolumny. Jeżeli uda się zmienić zapytanie tak, by tego uniknąć, to może ono
zadziałać lepiej. Jeśli w kolumnie Extra pojawi się tekst: Using filesort, będzie to oznaczać,
że baza wykonuje zapytanie w dwóch przebiegach — w pierwszym pobiera dane, a w drugim je
308 Część II Stosowanie MySQL
porządkuje (zazwyczaj na podstawie klauzuli ORDER BY). W takim przypadku warto przeczytać do-
skonały (choć długi) artykuł poświęcony optymalizacji zapytań z klauzulą ORDER BY, dostępny
w dokumentacji MySQL zamieszczonej na stronie http://dev.mysql.com/doc/refman/5.6/en/
order-by-optimization.html.
W tabelach typu MyISAM tylko w ostateczności dopuszczalne jest tworzenie kolumn o zmiennej
długości danych (typy VARCHAR, TEXT, BLOB). Tabele zawierające kolumny o z góry określonej długo-
ści danych zwiększą wydajność pracy bazy, choć może to spowodować nieznaczny wzrost używanej
przestrzeni dyskowej.
Przywileje
Kolejnym sposobem na poprawienie wydajności systemu jest uproszczenie przywilejów nadawa-
nych użytkownikom. Sposób, w jaki dokonywane jest sprawdzenie przywilejów przed wykona-
niem zapytania, został omówiony w jednym z poprzednich podrozdziałów. W tym przypadku
obowiązuje zasada, według której im mniej skomplikowany jest proces odczytywania przywile-
jów użytkownika, tym szybciej zapytanie zostanie przetworzone.
Optymalizacja tabel
Długotrwałe korzystanie z tabeli bazy danych prowadzi do coraz większej fragmentacji zawar-
tych w niej danych, spowodowanej wykonywaniem operacji DELETE i UPDATE. Występowanie
fragmentacji zwiększa czas potrzebny do wyszukania odpowiednich wierszy w tabeli. Negatywnym
skutkom fragmentacji danych można zapobiec, wykonując polecenie:
OPTIMIZE TABLE nazwa_tabeli;
Stosowanie indeksów
Tam, gdzie jest to możliwe, należy stosować indeksy usprawniające proces przetwarzania zapytań.
Indeksy te powinny być jak najprostsze; trzeba również pamiętać o tym, by nie tworzyć indeksów,
które nie będą w ogóle wykorzystywane. Aby sprawdzić, które z nich są rzeczywiście przydat-
ne, należy wykonać polecenie EXPLAIN, jak pokazano w jednym z poprzednich podrozdziałów.
Warto także dążyć do minimalizacji długości kluczy głównych.
Rozdział 12. Administrowanie MySQL dla zaawansowanych 309
Więcej wskazówek
Istnieje wiele pomniejszych sposobów na usprawnienie pracy serwera MySQL w określonych
sytuacjach. Serwis internetowy poświęcony MySQL zawiera szereg takich wskazówek, dostępnych
pod adresem: http://www.mysql.com.
Pierwszy z nich polega na zablokowaniu tabeli podczas kopiowania fizycznych plików. Służy
do tego polecenie LOCK TABLES o następującej składni:
LOCK TABLES tabela typ_blokady [, tabela typ_blokady ...]
Parametr tabela powinien wskazywać nazwę tabeli, a typ_blokady może przyjmować wartości
READ lub WRITE. W przypadku tworzenia kopii zapasowej powinno wystarczyć zastosowanie blokady
READ. Przed rozpoczęciem tworzenia kopii zapasowej należy wykonać polecenie FLUSH TABLES;, aby
zyskać pewność, że wszystkie zmiany dokonane w indeksach zostały zapisane na dysku.
W trakcie tworzenia kopii użytkownicy i skrypty będą wciąż mieć możliwość wykonywania
zapytań tylko do odczytu. Jeżeli jednak istnieje duża liczba zapytań wprowadzających zmiany
w bazie danych, jak na przykład zamówienia od klientów, rozwiązanie takie nie będzie zbyt
praktyczne.
Drugą, nadrzędną metodą jest zastosowanie polecenia mysqldump. Polecenie to wpisuje się w wierszu
poleceń systemu operacyjnego i zazwyczaj wykorzystuje się je mniej więcej w taki sposób:
> mysqldump --all-databases -> wszystkie sql
Polecenie to spowoduje zapisanie w pliku zrzutu wszystkie.sql wszystkich poleceń SQL niezbęd-
nych do zrekonstruowania bazy danych.
Trzecia metoda polega na użyciu skryptu mysqlhotcopy. Można go wywołać, wpisując polecenie:
> mysqlhotcopy database /sciezka/do/kopii
Ostatnią metodą tworzenia kopii zapasowej jest utrzymywanie repliki bazy danych. Replikacja
zostanie opisana w dalszej części rozdziału.
Jeżeli wykonano kopię zapasową, wykorzystując pierwszą metodę z poprzedniego punktu, wów-
czas można z powrotem skopiować pliki danych do tej samej lokalizacji w nowej instalacji MySQL.
Jeśli kopia zapasowa została wykonana drugim sposobem, należy wykonać kilka czynności.
Najpierw konieczne jest wykonanie zapytań zapisanych w pliku zrzutu. Spowoduje to rekonstrukcję
bazy danych do postaci, jaką miała ona w chwili tworzenia tego pliku. Następnie należy uaktual-
nić bazę danych do takiej postaci, jaka przechowywana jest w plikach dziennika. Można tego
dokonać, wykonując następujące polecenie:
> mysqlbinlog bin.[0-9]* | mysql
Więcej informacji na temat wykonywania kopii zapasowych MySQL oraz przywracania baz
danych można znaleźć na witrynie MySQL pod adresem http://www.mysql.com.
Implementowanie replikacji
Replikacja jest technologią, dzięki której możliwe jest utrzymywanie większej liczby serwerów
bazodanowych udostępniających takie same dane. W ten sposób można rozkładać obciążenie
i zwiększać niezawodność systemu. Jeśli jeden z serwerów odmówi posłuszeństwa, cały czas
można wykonywać zapytania na pozostałych maszynach. Po uruchomieniu replikacji można
również za jej pomocą tworzyć kopie zapasowe.
Rozwiązanie takie stosuje się najczęściej w ten sposób, że zapytania zapisujące dane są wykonywane
na serwerze nadrzędnym, natomiast na odbiorcach wykonywane są zapytania odczytujące dane.
Takie rozwiązanie nazywane jest podziałem odczytu-zapisu i można je zaimplementować bądź
to przy użyciu takiego narzędzia jak MySQL Proxy, bądź też bezpośrednio w logice aplikacji.
Możliwe jest także konstruowanie bardziej skomplikowanych architektur, w których może wystąpić
na przykład większa liczba serwerów nadrzędnych, my jednak zajmiemy się tylko przypadkiem
standardowym — serwer nadrzędny – odbiorca.
Należy zdać sobie sprawę, że odbiorcy zwykle nie posiadają danych równie aktualnych jak serwer
nadrzędny. Zjawisko to ma miejsce w każdej rozproszonej bazie danych.
Rozdział 12. Administrowanie MySQL dla zaawansowanych 311
Aby przystąpić do budowania architektury serwera nadrzędnego i odbiorcy, należy najpierw upewnić
się, że na serwerze nadrzędnym włączone jest zapisywanie do dziennika binarnego. Włączanie tego
mechanizmu zostało opisane w dodatku A.
Na obydwu serwerach (nadrzędnym i odbiorcy) należy wyedytować plik my.ini lub my.cnf. Na
serwerze nadrzędnym musi znaleźć się następujące ustawienie:
[mysqld]
log-bin
server-id=1
Pierwszy wiersz włącza zapisywanie do dziennika binarnego (powinien on już być obecny; jeżeli
tak nie jest, wiersz ten trzeba dopisać). Drugi wiersz natomiast nadaje serwerowi nadrzędnemu
unikatowy identyfikator. Każdy z odbiorców również potrzebuje identyfikatora, dlatego na każ-
dym z nich należy dodać analogiczny wiersz w pliku my.ini/my.cnf. Należy się upewnić, że liczby
te są unikatowe! Na przykład pierwszy odbiorca mógłby mieć server-id=2, drugi server-id=3
i tak dalej.
Nałożenie blokady READ jest konieczne ze względu na to, że należy zapamiętać pozycję serwera
w dzienniku binarnym w momencie tworzenia migawki. Można tego dokonać wykonując instrukcję:
mysql> SHOW MASTER STATUS;
312 Część II Stosowanie MySQL
Wartości z kolumn File i Position należy zapamiętać, ponieważ będą one potrzebne w trakcie
konfigurowania serwerów odbiorców.
Zamiast parametrów zapisanych kursywą należy podać rzeczywiste dane. Parametr serwer oznacza
nazwę serwera nadrzędnego. Parametry uzytkownik i haslo pochodzą z instrukcji GRANT, którą
wykonano wcześniej na serwerze nadrzędnym. Wartości plik_dziennika i pozycja_w_dzienniku
pochodzą natomiast z instrukcji SHOW MASTER STATUS wykonanej na serwerze nadrzędnym.
Wiele cennych informacji znajduje się w książce Paula DuBois pt. MySQL (Fifth Edition), wydanej
przez Addison-Wesley.
W następnym rozdziale
W następnym rozdziale przedstawione zostaną niektóre zaawansowane możliwości serwera MySQL
przydatne w trakcie tworzenia aplikacji internetowych. Powiemy m.in. o różnych rodzajach mecha-
nizmów składowania danych, o transakcjach oraz procedurach składowanych.
Rozdział 13.
Zaawansowane
programowanie w MySQL
W tym rozdziale poznamy bardziej zaawansowane zagadnienia dotyczące programowania w MySQL,
w tym typy tabel, transakcje i procedury składowane.
Wiersz ten pozwala na odczytanie danych z pliku noweksiazki.txt i zapisanie ich w tabeli ksiazki.
(Polecenie to zakłada, że obecnie stosowana jest baza danych ksiazki. Tabelę docelową można
także określić przy użyciu zapisu bazadanych.tabela). Domyślnie pola danych w pliku muszą
być rozdzielane znakiem tabulacji, dane umieszczane w apostrofach, a każdy wiersz powinien
być zakończony znakiem nowego wiersza (\n). Znaki specjalne muszą być poprzedzone lewym
ukośnikiem (\). Parametry te mogą być modyfikowane za pomocą różnych opcji polecenia LOAD;
dodatkowe informacje można znaleźć w podręczniku MySQL.
Aby skorzystać z polecenia LOAD DATA INFILE, użytkownik musi posiadać prawo FILE, omówione
w rozdziale 9., „Tworzenie internetowej bazy danych”.
314 Część II Stosowanie MySQL
W czasie tworzenia tabeli wybór mechanizmu składowania jest realizowany w następujący sposób:
CREATE TABLE tabela TYPE = typ ....
Tabel InnodDB należy zawsze używać w aplikacjach, w których duże znaczenie mają transakcje,
na przykład podczas gromadzenia danych finansowych, czy też w aplikacjach, w których operacje
INSERT i SELECT często przeplatają się ze sobą, takich jak internetowe tablice ogłoszeniowe bądź
fora dyskusyjne. Poza tym tabele te należy stosować wszędzie tam, gdzie ważne jest zachowanie
integralności odwołań (poprzez używanie kluczy obcych), czyli w większości aplikacji korzystają-
cych z danych relacyjnych.
W niektórych przypadkach można także wykorzystywać tabele typu MyISAM. Typowym rozwią-
zaniem jest stosowanie tych tabel w aplikacjach magazynów danych. Co więcej, w czasie gdy pisana
była niniejsza książka, tabele MyISAM posiadały bardziej zaawansowane możliwości korzystania
z indeksów pełnotekstowych niż tabele InnoDB, choć można się spodziewać, że w najbliższej
przyszłości sytuacja ta ulegnie zmianie.
Tabele typu MEMORY najlepiej nadają się na tabele tymczasowe lub do implementacji perspektyw,
natomiast tabele MERGE można stosować, jeżeli wykorzystywane są bardzo duże tabele MyISAM.
Po utworzeniu tabeli jej typ można zmienić za pomocą polecenia ALTER TABLE, tak jak na poniższym
przykładzie:
ALTER TABLE Zamowienia ENGINE=innodb;
ALTER TABLE Pozycje_zamowione ENGINE=innodb;
W większości przykładów w tej książce będziemy korzystać z tabel MyISAM. W kolejnym punkcie
omówimy wykorzystanie transakcji i sposób, w jaki są one implementowane w tabelach InnoDB.
Transakcje
Transakcje to mechanizm pomagający w utrzymaniu spójności bazy danych, szczególnie w przy-
padku wystąpienia błędu bądź załamania serwera. W poniższym podpunkcie zdefiniujemy, czym
są transakcje, i w jaki sposób można je zaimplementować za pomocą InnoDB.
Aby pokazać, dlaczego taka własność może być ważna, przeanalizujmy przykład aplikacji banko-
wej. Wyobraźmy sobie sytuację, gdy chcemy przesłać pieniądze z jednego konta bankowego na
drugie. Wymaga to zmniejszenia stanu jednego konta i zwiększenia o taką samą kwotę stanu drugie-
go, co można zrealizować co najmniej dwoma zapytaniami. Niezwykle ważne jest, aby oba te
zapytania były wykonane w całości lub nie było wykonane żadne z nich. Jeżeli po zmniejszeniu
stanu pierwszego konta, a przed zwiększeniem stanu drugiego, zostanie wyłączone zasilanie, to jaki
otrzymamy wynik? Czy pieniądze po prostu znikną?
Być może spotkałeś się z określeniem zgodność z ACID. ACID odnosi się do czterech wymagań,
które powinny być spełniane przez transakcje:
Atomowość (ang. Atomicity) — transakcja powinna być atomowa, czyli powinna być
wykonywana w całości lub nie wykonywana w ogóle.
Spójność (ang. Consistency) — transakcja powinna pozostawiać spójną bazę danych.
316 Część II Stosowanie MySQL
Izolacja (ang. Isolation) — niedokończone transakcje nie powinny być widoczne dla
pozostałych użytkowników bazy danych, czyli do zakończenia transakcji powinna ona
pozostawać odizolowana (od reszty bazy danych).
Trwałość (ang. Durability) — po zapisaniu w bazie danych transakcja powinna być trwała.
Transakcja, która została trwale zapisana w bazie danych, jest nazywana transakcją zatwierdzoną.
Transakcja, która nie została zapisana do bazy danych — czyli baza danych została przywrócona
do stanu sprzed rozpoczęcia transakcji — jest nazywana transakcją wycofaną.
Jeżeli automatyczne zatwierdzanie jest włączone, należy rozpoczynać każdą transakcję instrukcją
START TRANSACTION;
Jeżeli automatyczne zatwierdzanie jest wyłączone, instrukcja ta nie jest potrzebna, ponieważ trans-
akcja jest automatycznie uruchamiana przy wykonywaniu instrukcji SQL.
Po zakończeniu wykonywania instrukcji składających się na transakcję można zapisać w bazie danych
wyniki działania, wpisując:
COMMIT;
Jeżeli zmieniłeś zdanie, możesz przywrócić poprzedni stan bazy danych, wpisując:
ROLLBACK;
Do momentu zatwierdzenia transakcji wyniki jej działania nie są widoczne dla innych użytkowników
lub innych sesji.
Aby go wykonać, należy otworzyć dwa połączenia do bazy danych ksiazki. W oknie pierwszego
połączenia dodaj nowy rekord zamówienia:
INSERT INTO Zamowienia VALUES(5, 2, 69.98, '2008-06-18');
INSERT INTO Pozycje_zamowione VALUES(5, '0-672-31697-8', 1);
Pozostawmy to połączenie otwarte i przejdźmy do okna drugiego połączenia, w którym należy wyko-
nać to samo zapytanie SELECT. W efekcie nie powinniśmy zobaczyć wpisanego wcześniej zamówienia:
Empty set (0.00 sec)
(Jeżeli jednak uda nam się je zobaczyć, będzie to oznaczało, że najprawdopodobniej zapomnieliśmy
wyłączyć automatyczne zatwierdzanie transakcji; sprawdźmy to, jak również to, czy tabela
jest typu InnoDB).
Dzieje się tak dlatego, że transakcja nie została jeszcze zatwierdzona (jest to jednocześnie ilustracja
działania mechanizmu izolacji transakcji).
Klucze obce
Mechanizm InnoDB zawiera również obsługę kluczy obcych. Klucze obce zostały opisane w roz-
dziale 8., „Projektowanie internetowej bazy danych”.
W jaki sposób należy to skonfigurować? Aby utworzyć tabelę korzystającą od początku z kluczy
obcych, należy zmienić instrukcję DDL w następujący sposób:
CREATE TABLE Pozycje_zamowione
( ZamowienieID INT UNSIGNED NOT NULL,
ISBN CHAR(13) NOT NULL,
Ilosc TINYINT UNSIGNED,
Aby sprawdzić, czy zmiany te dały efekt, możesz spróbować wstawić wiersz z wartością kolumny
zamowienieid, która nie odpowiada żadnemu wierszowi z tabeli zamowienia.
INSERT INTO Pozycje_zamowione VALUES(77, '0-672-31697-8', 7);
Procedury składowane
Procedura składowana jest podprogramem utworzonym i zapisanym w MySQL. Może składać się
z instrukcji SQL oraz struktur sterujących. Procedury składowane są przydatne, jeżeli kilka różnych
aplikacji wykonuje te same operacje na bazie danych lub jeżeli chcemy hermetyzować pewien zakres
funkcji. Procedury składowane w bazie danych mogą być traktowane analogicznie do programo-
wania zorientowanego obiektowo — pozwalają sterować sposobem dostępu do danych.
Prosty przykład
Na listingu 13.1 pokazana jest deklaracja procedury składowanej.
DELIMITER ;
Pierwsza instrukcja
DELIMITER //
zmienia znak końca wiersza z bieżącej wartości — zwykle średnika — na dwa znaki ukośnika.
Jest ona potrzebna, ponieważ znak średnika jest znacznikiem końca wiersza w kodzie procedury;
w przeciwnym razie MySQL próbowałby wykonać niekompletny kod procedury w czasie jego
wprowadzania.
Następny wiersz,
CREATE PROCEDURE Suma_zamowien (OUT Suma FLOAT)
rozpoczyna tworzenie procedury. Nazwą procedury jest Suma_zamowien. Posiada ona pojedynczy
parametr o nazwie Suma, który będzie zawierał obliczoną wartość. Słowo kluczowe OUT wskazuje,
że jest to parametr zwracany z funkcji.
Parametry mogą być również deklarowane jako IN, co oznacza, że wartości są przekazywane do
procedury, lub INOUT, co z kolei znaczy, że wartości są przekazywane do procedury, ale mogą być
przez nią zmieniane.
Słowo FLOAT oznacza typ parametru. W tym przypadku zwracana jest suma wszystkich zamówień
z tabeli Zamowienia. Typem kolumny Wartosc jest FLOAT, więc i typem zwracanej wartości musi być
FLOAT. Akceptowane typy danych muszą odpowiadać typom kolumn.
Jeżeli potrzebny jest więcej niż jeden parametr, można użyć listy parametrów rozdzielonej prze-
cinkami, identycznie jak w PHP.
Rozdział 13. Zaawansowane programowanie w MySQL 319
Treść procedury znajduje się między instrukcjami BEGIN i END. Są one analogiczne do nawiasów
klamrowych w PHP ({}), ponieważ ograniczają blok instrukcji.
W treści procedury wykonujemy po prostu instrukcję SELECT. Jedyną różnicą w stosunku do zwy-
kłego zapytania jest użycie klauzuli INTO Suma, co pozwala załadować wynik zapytania do para-
metru Suma.
Po zadeklarowaniu procedury można ją wywołać, korzystając ze słowa kluczowego CALL, jak poniżej:
CALL Suma_zamowien(@t);
W sposób podobny do tworzenia procedury możemy utworzyć funkcję. Funkcja zwraca pojedynczą
wartość i pozwala na użycie tylko parametrów wejściowych.
Podstawowa składnia dla tego zadania jest niemal identyczna. Przykładowa funkcja jest zamiesz-
czona na listingu 13.2.
DELIMITER ;
Jak widać, w przykładzie tym użyliśmy słowa kluczowego FUNCTION zamiast PROCEDURE. Występuje
również kilka innych różnic.
Parametry nie muszą być deklarowane jako IN lub OUT, ponieważ wszystkie są parametrami wej-
ściowymi, czyli IN. Po liście parametrów znajduje się klauzula RETURNS FLOAT. Określa ona typ
zwracanej wartości. I tutaj może być to jeden z dopuszczalnych typów MySQL.
Za klauzulą określającą typ wyniku znajdują się słowa kluczowe NO SQL. Określają one cechę danej
funkcji. W tym miejscu mogą się pojawić także inne zapisy, takie jak:
DETERMINISTIC lub NOT DETERMINISTIC — funkcje deterministyczne w razie przekazywania
tych samych parametrów zawsze będą zwracać takie same wyniki.
320 Część II Stosowanie MySQL
NO SQL, CONTAINS SQL, READS SQL DATA lub MODIFIES SQL DATA — określają zawartość
funkcji. W kodzie powyższych funkcji nie są wykonywane żadne zapytania SQL,
dlatego został użyty zapis NO SQL.
Komentarz zapisany pomiędzy znakami apostrofu (').
Deklaracja używanego języka: LANGUAGE SQL.
Frazy SQL SECURITY DEFINER oraz SECURITY INVOKER określają, czy należy używać
poziomu przywilejów użytkownika, który zdefiniował funkcję (zadeklarowanego
w funkcji), czy też użytkownika, który ją wykonuje.
Wspomnieliśmy o tych charakterystykach, gdyż jeśli jest włączone rejestrowanie operacji w binar-
nym pliku dziennika, to podczas definiowania funkcji konieczne będzie umieszczenie tu jednej
z fraz: DETERMINISTIC, NO SQL bądź też READS SQL DATA. Wynika to z tego, że funkcje, które zapisują
dane, mogą być niebezpieczne w kontekście odzyskiwania danych oraz replikacji bazy i dlatego nie
wolno ich stosować. (Więcej informacji na ten temat można znaleźć w dokumentacji MySQL).
Warto zwrócić uwagę na to, że w powyższej funkcji nie zostały zastosowane słowa kluczowe BEGIN
oraz END. Można by ich użyć, lecz w tym przypadku nie jest to konieczne. Podobnie jak w PHP, jeśli
blok kodu zawiera tylko jedną instrukcję, to oznaczanie jego początku i końca nie jest wymagane.
Wywołanie funkcji różni się od wywoływania procedury. Można wywoływać funkcję składowaną
w taki sam sposób jak funkcję wbudowaną. Na przykład:
SELECT Dodaj_podatek(100);
Po zdefiniowaniu procedur i funkcji można wyświetlić kod użyty do ich zdefiniowania za pomocą
następujących instrukcji:
SHOW CREATE PROCEDURE Suma_zamowien;
lub
SHOW CREATE FUNCION Dodaj_podatek;
lub funkcję
DROP FUNCION Dodaj_podatek;
Zmienne lokalne
Zmienne lokalne mogą być deklarowane wewnątrz bloku BEGIN...END przy użyciu instrukcji DECLARE.
Na przykład zmienimy funkcję Dodaj_podatek, aby korzystała ze zmiennej lokalnej do przecho-
wywania stopy podatku, co jest pokazane na listingu 13.3.
DELIMITER ;
Jak widać, zmienne są deklarowane za pomocą słowa kluczowego DECLARE, po którym występują
nazwa zmiennej i jej typ. Klauzula DEFAULT jest opcjonalna i definiuje wartość domyślną zmiennej.
Następnie można korzystać z zadeklarowanej zmiennej w ogólnie przyjęty sposób.
OPEN C1;
REPEAT
FETCH C1 INTO Biez_id, Biez_suma;
IF NOT Gotowe THEN
IF Biez_suma > N_suma THEN
SET N_suma=Biez_suma;
SET N_id=Biez_id;
END IF;
322 Część II Stosowanie MySQL
END IF;
UNTIL Gotowe END REPEAT;
CLOSE C1;
SET Id_najwiekszego=N_id;
END
//
DELIMITER ;
W powyższym kodzie użyte są struktury sterujące (instrukcje warunkowe i pętle), kursory oraz
instrukcje obsługi. Przeanalizujmy ten przykład wiersz po wierszu.
Następną deklarowaną zmienną jest Gotowe, która jest inicjowana wartością 0 (false). Zmienna
ta jest wykorzystywana jako znacznik pętli. Po wyczerpaniu wszystkich zmiennych przypisujemy
jej wartość 1 (true).
Kolejnym elementem powyższej procedury jest kursor. Kursor przypomina nieco tablicę i służy
do pobrania wyników zapytania (takich jak te zwracane przez funkcję mysqli_query()). Pozwala on
na przetwarzanie tych wyników wiersz po wierszu (podobnie jak w przypadku użycia funkcji
mysqli_fetch_row()). Poniżej przedstawiony został sposób tworzenia kursora:
DECLARE C1 CURSOR FOR SELECT ZamowienieID, Wartosc FROM Zamowienia;
Utworzony w ten sposób kursor nosi nazwę C1. Powyższy wiersz jest jedynie definicją tego, co
kursor będzie zawierać. Samo zapytanie nie zostanie jeszcze wykonane.
Kolejny wiersz:
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET Gotowe = 1;
jest nazywany deklaracją podprogramu obsługi. Jest on podobny do obsługi wyjątków w proce-
durach składowanych. Dodatkowo można także tworzyć podprogramy CONTINUE oraz EXIT. Pod-
program CONTINUE, który jest użyty w opisywanym kodzie, powoduje wykonanie zdefiniowanej akcji
i dalsze wykonywanie procedury. Podprogram obsługi EXIT kończy wykonywanie wykonywanego
bloku BEGIN...END.
Następna część deklaracji podprogramu obsługi określa moment jego wywołania. W tym przypadku
będzie on wywołany, gdy zostanie spełniony warunek SQLSTATE '02000'. Być może zastanawiasz
się, co on znaczy, ponieważ wygląda bardzo tajemniczo. Oznacza, że podprogram obsługi będzie
wywołany, gdy nie zostaną znalezione kolejne wiersze. Przetwarzamy zbiór wynikowy wiersz po
wierszu, a gdy wyczerpane zostaną wiersze do przetwarzania, wywołany zostanie podprogram
obsługi. Można również użyć tu frazy FOR NOT FOUND. Innymi możliwościami są SQLWARNING oraz
SQLEXCEPTION.
Następny wiersz,
OPEN C1;
powoduje właśnie wykonanie zapytania. Aby odczytać kolejne wiersze danych, należy skorzystać
z instrukcji FETCH. Wykonujemy to w pętli REPEAT. W tym przypadku pętla ta jest skonstruowana
w następujący sposób:
Rozdział 13. Zaawansowane programowanie w MySQL 323
REPEAT
...
UNTIL Gotowe END REPEAT;
Zwróćmy uwagę, że warunek (UNTIL Gotowe) jest sprawdzany dopiero na końcu. W procedurach
składowanych można również korzystać z pętli WHILE, w postaci:
WHILE warunek DO
...
END WHILE;
Pętle te nie mają wbudowanego warunku, ale mogą być przerywane za pomocą instrukcji LEAVE.
Kontynuując nasz przykład — kolejny wiersz kodu powoduje odczytanie wiersza danych:
FETCH C1 INTO Biez_id, Biez_suma;
Wiersz ten powoduje odczytanie danych z kursora zapytania. Wartości dwóch atrybutów odczyta-
nych przez zapytanie są zapisywane w podanych zmiennych lokalnych.
Następnie sprawdzamy, czy wiersz został odczytany, po czym, korzystając z dwóch instrukcji IF,
porównujemy bieżącą wartość z największą zapisaną wartością:
IF NOT Gotowe THEN
IF Biez_suma > N_suma THEN
SET N_suma=Biez_suma;
SET N_id=Biez_id;
END IF;
END IF;
Na koniec obliczoną wartość przypisujemy do parametru OUT. Nie można użyć parametru jako
zmiennej tymczasowej, a jedynie do zapisania końcowej wartości (jest to podejście podobne do
stosowanego w niektórych językach programowania, na przykład Ada).
Wyzwalacze
Wyzwalacze to jak gdyby procedury składowane sterowane zdarzeniami albo, jeśli ktoś woli, wy-
wołania zwrotne. To kod skojarzony z konkretną tabelą, który jest wywoływany w momencie
wykonania na danej tabeli określonej akcji.
Pierwszy wiersz określa nazwę tworzonego wyzwalacza. Połączenie określenia czasu (BEFORE
lub AFTER) oraz zdarzenia powodującego wykonanie wyzwalacza (zapytania INSERT, UPDATE lub
DELETE wykonanego na konkretnej tabeli) definiuje, kiedy kod wyzwalacza zostanie wykonany.
Opcjonalna klauzula kolejnosc pozwala na wykonanie więcej niż jednego wyzwalacza dla konkret-
nej kombinacji czasu i zdarzenia. Poniżej przedstawiony został format tej klauzuli:
{FOLLOWS | PRECEDES} inny_wyzwalacz
Kolejna klauzula, FOR EACH ROW, oznacza, że wyzwalacz należy wykonać dla każdego wiersza,
który został objęty efektami działania zapytania.
Listing 13.5. wyzwalacz.sql — przed usunięciem zamówienia najpierw trzeba usunąć każdą z pozycji
zamówienia
# Przykład wyzwalacza
DELIMITER //
Rozdział 13. Zaawansowane programowanie w MySQL 325
DELIMITER ;
Jak widać, operacja ta jest wykonywana przed usunięciem rekordu z tabeli Zamowienia. Sam kod
umieszczony w ciele wyzwalacza usuwa z tabeli Pozycje_zamowione każdy wiersz zawierający
odpowiednią wartość w kolumnie ZamowienieID. W tym zapytaniu został użyty specjalny element
składni: słowo kluczowe OLD. Oznacza ono: „zastosuj wartość tej kolumny z chwili przed wykona-
niem zapytania”. Dostępne jest także słowo kluczowe NEW.
Jeśli teraz sprawdzimy zawartość tabeli Pozycje_zamowione, przekonamy się, że zniknęły z niej
wszystkie wiersze, w których pole ZamowienieID miało wartość 3:
+--------------+---------------+-------+
| ZamowienieID | ISBN | Ilosc |
+--------------+---------------+-------+
| 1 | 0-672-31697-8 | 2 |
| 2 | 0-672-31769-9 | 1 |
| 4 | 0-672-31745-1 | 3 |
| 5 | 0-672-31697-8 | 1 |
+--------------+---------------+-------+
4 rows in set (0.00 sec)
Kolejnym często spotykanym zastosowaniem wyzwalaczy jest formatowanie danych oraz rejestro-
wanie danych do audytów, takich jak informacje o tym, jakie zmiany zostały wprowadzone w bazie,
kto ich dokonał i kiedy.
326 Część II Stosowanie MySQL
Więcej informacji na temat instrukcji LOAD DATA INFILE, pozostałych mechanizmów składowania
oraz procedur składowanych znajduje się w podręczniku MySQL.
Jeżeli chcesz się dowiedzieć więcej na temat transakcji i spójności bazy danych, sięgnij po wybraną
pozycję na temat podstaw baz danych, na przykład An Introduction to Database Systems, której
autorem jest C.J. Date.
W następnym rozdziale
Omówiliśmy już podstawy PHP oraz MySQL. W trzeciej części książki, „E-commerce i bezpie-
czeństwo”, przedstawione zostaną zagadnienia związane z bezpieczeństwem aplikacji interneto-
wych, na które należy zwracać uwagę podczas tworzenia i wykonywania tego typu aplikacji.
Część III
E-commerce
i bezpieczeństwo
328 Część III E-commerce i bezpieczeństwo
Rozdział 14. Zagrożenia bezpieczeństwa aplikacji internetowych 329
Rozdział 14.
Zagrożenia bezpieczeństwa
aplikacji internetowych
W tym rozdziale przyjrzymy się bezpieczeństwu w kontekście szerszego zagadnienia, jakim jest
zabezpieczanie całych aplikacji internetowych. Oczywiście każdą z części aplikacji trzeba będzie
zabezpieczyć na wypadek jej nieprawidłowego użycia (nieumyślnego lub celowego), jednak dodat-
kowo warto wypracować pewne strategie tworzenia aplikacji w sposób, który pozwoli zadbać o ich
bezpieczeństwo.
Identyfikacja zagrożeń
Na początku rozdziału przedstawione zostaną konkretne zagrożenia bezpieczeństwa występujące
w nowoczesnych aplikacjach internetowych. Pierwszym krokiem na drodze do stworzenia bez-
piecznej aplikacji internetowej jest zrozumienie natury zagrożeń, tak by można było wypracować
sposoby ochrony przed nimi.
Jeżeli implementowany jest system do składania zleceń giełdowych online albo wykonywania trans-
akcji walutowych, osoby trzecie mające dostęp do konta danego użytkownika mogłyby odczytać
jego numer identyfikacji podatkowej (NIP), dane dotyczące aktywów posiadanych przez użytkow-
nika i ich ilości, a w skrajnych przypadkach nawet dane jego rachunku bankowego.
Nawet udostępnienie tabeli imion i nazwisk oraz adresów użytkowników jest pogwałceniem zasad
bezpieczeństwa. Użytkownicy bardzo sobie cenią własną prywatność, a lista ich imion i nazwisk
wraz z adresami oraz dodatkowymi danymi marketingowymi (na przykład że wszystkie dziesięć
330 Część III E-commerce i bezpieczeństwo
tysięcy osób znajdujących się na liście dokonały zakupów w sieciowym sklepie z wyrobami
tytoniowymi) jest bardzo łakomym kąskiem dla firm handlowych i marketingowych, które nie
przejmują się regułami.
Wszystkie takie zdarzenia są przykre i mogą mieć fatalne następstwa, istnieją jednak dwa rodzaje
zdarzeń, które występują bardzo często i nastręczają ogromnych problemów. Są to ujawnienie
numerów kart kredytowych oraz ujawnienie haseł.
Znaczenie numerów kart kredytowych jest oczywiste — każdy, kto dysponuje ważnymi numerami
kart kredytowych, nazwiskami ich właścicieli, datami wygaśnięcia kart itd., może z tych danych sko-
rzystać sam lub, co jest znacznie częstsze, może sprzedać je komuś, kto zaoferuje za nie wysoka
cenę.
Atrakcyjność haseł nie jest aż tak oczywista. Można by się zastanawiać, do czego przydadzą się
hasła, skoro napastnikowi udało się już uzyskać dostęp do atrakcyjnych danych w aplikacji.
Otóż hasła są atrakcyjne z tego względu, że użytkownicy lubią je stosować wielokrotnie — w róż-
nych witrynach. Istnieje zatem spora szansa na to, że nazwa użytkownika oraz hasło, którego
Janek Kowalski użył do założenia konta w witrynie służącej do udostępniania zdjęć, będą takie
same jak te, których używa do logowania się w witrynie banku.
Takie dane z codziennego działania aplikacji bądź serwerów mogą być źródłem wielu interesujących
informacji. Jeśli z takimi dziennikami będą skojarzone adresy IP, to bez trudu będzie można zidenty-
fikować wzorce działania konkretnych użytkowników i w miarę precyzyjnie odgadnąć ich lokali-
zację. Jeżeli pliki dzienników serwera zawierają adresy URL, a tak właśnie jest zazwyczaj, to
adresy te mogą zawierać nazwy użytkowników, hasła oraz informacje o punktach końcowych
(potencjalnie prywatnych) dostępnych w danej witrynie.
Nie trzeba chyba tłumaczyć, że ujawnienie jakichkolwiek informacji tego typu jest związane
z utratą reputacji firmy, co prowadzi do utraty klientów, którzy boją się zaufać firmie mającej
problemy z zapewnieniem bezpieczeństwa danych.
Ograniczanie ryzyka
Aby zminimalizować ryzyko ujawnienia poufnych informacji, należy ograniczyć sposoby uzy-
skiwania do nich dostępu oraz określić grupę osób uprawnionych do ich przeglądania. Wiąże się
z tym zaprojektowanie systemu w stosowny sposób, właściwe skonfigurowanie serwera, napisanie
odpowiedniego oprogramowania, przetestowanie wszystkich składników systemu, usunięcie niepo-
trzebnych usług serwera WWW oraz wykorzystanie mechanizmu uwierzytelniania.
Należy z dużą uwagą projektować, konfigurować, implementować i testować system, dzięki czemu
możliwe będzie zmniejszenie ryzyka włamania i, co równie ważne, ograniczenie prawdopodobień-
stwa wystąpienia błędu, który mógłby doprowadzić do ujawnienia przechowywanych informacji.
Należy usunąć niepotrzebne usługi serwera WWW, aby zmniejszyć liczbę potencjalnych słabych
punktów systemu. Każda z udostępnianych usług może mieć swoje słabe miejsca, dlatego też należy
regularnie uaktualniać oprogramowanie w celu ich eliminacji. Poważne niebezpieczeństwo sta-
nowią zwłaszcza usługi, które w ogóle nie są używane. Jeśli nie przewiduje się wykonywania
Rozdział 14. Zagrożenia bezpieczeństwa aplikacji internetowych 331
Ryzyko ujawnienia danych istnieje również w przypadku przesyłania ich w sieci. Protokół TCP/IP
posiada wiele cech, dzięki którym stał się standardem łączącym różne rodzaje sieci w jeden wielki
twór — internet, jednak bezpieczeństwo nie jest jego mocną stroną. Działanie TCP/IP opiera się
na dzieleniu przesyłanych danych na części zwane pakietami, które następnie są przekazywane
od jednego komputera do drugiego do momentu, gdy dotrą do miejsca przeznaczenia. Oznacza to,
że dane przechodzą przez wiele różnych komputerów (zobacz rysunek 14.1). Każdy z nich mógłby
posłużyć do podejrzenia przesyłanych za jego pośrednictwem informacji.
Rysunek 14.1.
Transmisja danych
w internecie polega
na przesyłaniu ich
za pośrednictwem wielu
różnych komputerów,
które mogą nie być
godne zaufania
W celu prześledzenia drogi, jaką przebywają dane wysłane z określonej maszyny, można użyć
polecenia traceroute (w systemie Unix). Komenda ta spowoduje wypisanie adresów wszystkich
komputerów, przez które będą przechodzić pakiety danych, zanim dotrą do komputera docelowego.
Jeśli maszyna docelowa znajduje się w tym samym kraju, pakiety te przejdą przez około 10 kompu-
terów pośredniczących. W przypadku gdy komputer docelowy jest zlokalizowany poza grani-
cami kraju, przesyłanie danych może odbywać się za pośrednictwem nawet 20 maszyn. Jeśli firma
posiada rozległą i skomplikowaną sieć komputerową, to wysłane z niej dane mogą przejść przez
5 innych maszyn, zanim w ogóle zostaną wysłane na zewnątrz budynku przedsiębiorstwa.
Ataki polegające na odczytaniu lub modyfikacji danych w trakcie ich przesyłania siecią są nazywane
atakami typu „man in the middle” (w skrócie: MITM).
1
Nawet jeśli usługa rcp jest używana, to lepszym rozwiązaniem będzie jej usunięcie i zainstalowanie w jej miejsce
bezpieczniejszej usługi scp.
332 Część III E-commerce i bezpieczeństwo
Modyfikacje danych
Całkowita utrata danych pociąga za sobą trudne do przewidzenia konsekwencje, jednak modyfi-
kacje istniejących danych mogą stanowić jeszcze większe zagrożenie. Co może się stać, gdy ktoś
niepowołany uzyska dostęp do systemu i dokona zmian w plikach? Usunięcie dużej ilości danych
zostanie zapewne szybko zauważone i dzięki kopiom zapasowym będzie możliwe ich odzyskanie.
Ile jednak czasu minie, zanim ktokolwiek zorientuje się, że dane uległy modyfikacji?
Modyfikacje plików mogą dotyczyć plików z danymi lub plików wykonywalnych. Motywem
działania napastnika może być chęć zostawienia własnego znaku na stronie internetowej lub
zdobycie informacji w celu dokonania oszustwa. Zastąpienie któregoś z plików wykonywalnych
jego odpowiednio zmodyfikowaną wersją może umożliwić włamywaczowi nieograniczony dostęp
do systemu w przyszłości bądź też stanowić furtkę do uzyskania w systemie znacznie szerszych
uprawnień.
Jedną z metod zabezpieczenia się przed modyfikacją danych przesyłanych w sieci jest obliczanie
ich sumy kontrolnej. Co prawda nie uniemożliwi to dokonania zmian przez kogoś nieuprawnione-
go, jednak osoba, dla której dane te są przeznaczone, może łatwo sprawdzić, czy uległy one modyfi-
kacji, wyliczając nową sumę kontrolną i porównując ją z sumą kontrolną danych oryginalnych.
Zaszyfrowanie danych również utrudni dokonanie w nich jakichkolwiek zmian, gdyż znacznie
trudniej będzie je niepostrzeżenie zmodyfikować.
W celu ochrony plików przechowywanych na serwerze przed ewentualnymi zmianami ich zawar-
tości należy wykorzystać możliwość nadawania uprawnień, udostępnianą przez system opera-
cyjny, i zabezpieczyć sam system przed dostępem z zewnątrz. Nadając odpowiednie prawa dostępu,
można zezwolić użytkownikowi na korzystanie z zasobów systemu, ograniczając mu jednocześnie
możliwość dokonywania w nim zmian czy też modyfikacji plików należących do innych użytkow-
ników.
Wykrycie dokonanych zmian jest bardzo trudne. Jeśli w którymś momencie dojdziemy do wnio-
sku, że zabezpieczenia systemu zostały złamane, to skąd wiadomo, czy zmianom nie uległy ważne
pliki? Przecież niektóre pliki, jak choćby te wykorzystywane przez bazy danych do przechowy-
wania zawartości tabel, zmieniają się niemal co chwila. Inne z kolei pozostają niezmienione przez
cały okres od momentu ich zapisania na dysku, chyba że celowo zostały uaktualnione. Zarówno
dane, jak i programy mogą zostać podstępnie zmienione, o ile jednak programy można zainstalować
na nowo, o tyle w przypadku plików z danymi nigdy nie ma pewności, która wersja jest „czysta”.
Narzędzia służące do analizy integralności plików, takie jak Tripwire, zapisują informacje na
temat ważnych plików zaraz po ich instalacji. Informacje te mogą być później wykorzystane do
sprawdzenia, czy pliki te nie uległy zmianie.
Należy w tym miejscu zaznaczyć, że przyczyną utraty lub zniszczenia danych nie musi być wcale
nieprawidłowe lub bezprawne użycie systemu. Jeżeli na przykład spłonie budynek, w którym
znajdują się serwery systemu, a wraz z nim wszystkie dyski twarde, będzie to oznaczać utratę
wszystkich danych. Pozostanie wówczas mieć nadzieję, że wcześniej utworzono odpowiednie
kopie bezpieczeństwa i opracowano plany przywrócenia systemu po awarii.
Znacznie poważniejsze następstwa mogą wyniknąć z utraty danych niż z ich ujawnienia. Po
spędzeniu długich miesięcy na tworzeniu witryny internetowej, a następnie gromadzeniu informacji
o klientach i złożonych przez nich zamówieniach utrata tych danych może być bardzo kosztowna —
zarówno w wymiarze finansowym, jak i ze względu na zmarnowany czas i nadszarpniętą reputację.
Jeśli nie istnieją żadne kopie utraconych danych, konieczne będzie utworzenie nowej witryny od
podstaw i rozpoczęcie działalności właściwie od zera. Na pewno zniechęciłoby to również klientów
oraz zachęciło użytkowników twierdzących, że zamówili towary, których potem nie otrzymali.
Nie można wykluczyć, że crackerzy włamią się do systemu i zniszczą przechowywane dane. Być
może jakiś beztroski administrator czy programista przypadkiem usunie jakieś dane. Jest niemal
pewne, że pewnego dnia zepsuje się dysk twardy — talerze dysków obracają się przecież tysiące
razy na minutę i od czasu do czasu ulegają awariom. Parafrazując prawo Murphy’ego, uszkodzeniu
ulegnie dysk zawierający najważniejsze dane, których kopia zapasowa została wykonana dawno
temu.
Ograniczanie ryzyka
W celu zapobieżenia tego typu sytuacjom można zastosować szereg środków ostrożności. Przede
wszystkim należy zabezpieczyć serwery przed atakiem ze strony crackerów. Liczba pracowników
mających dostęp do systemu powinna być jak najmniejsza, a wszyscy powinni również posiadać
fachową wiedzę i doświadczenie. Ważne, by wykorzystywane dyski charakteryzowały się wysoką
jakością; zalecane jest też stosowanie macierzy RAID (ang. Redundant Array of Inexpensive Disks),
dzięki której kilka dysków funkcjonuje jak jeden szybszy i bardziej niezawodny nośnik danych.
Tak naprawdę istnieje tylko jeden niezawodny sposób ochrony przed utratą danych: tworzenie
kopii zapasowych. Nie jest to żadne wielkie przedsięwzięcie, wręcz odwrotnie — czynność nużąca,
nudna i, pozostaje mieć nadzieję, bezużyteczna, ale o ogromnym znaczeniu. Należy dbać o regularne
wykonywanie kopii zapasowych, wcześniej jednak konieczne jest przetestowanie używanych
do tego celu narzędzi, by upewnić się, że zapasowe dane będzie można łatwo odzyskać. Zapasowe
kopie danych nie powinny być przechowywane w komputerach. Chociaż prawdopodobieństwo
tego, że siedziba firmy doszczętnie spłonie czy też dosięgnie jej inny żywioł, jest bardzo małe,
to przechowywanie kopii zapasowych w innym budynku jest tanim, ale niezwykle skutecznym
środkiem zabezpieczającym.
Blokada usługi
Jednym z zagrożeń, przed którym najtrudniej ustrzec witrynę internetową, jest blokada usługi,
zwana po angielsku Denial of Service (DoS). Do blokady tej dochodzi wtedy, gdy czyjeś działania
powodują powstanie utrudnień lub całkowicie uniemożliwiają uzyskanie dostępu do usługi — albo
też opóźniają dostęp do usług, w których czas ma podstawowe znaczenie.
Serwery niedziałające przez godzinę, lub nawet dłużej, mogą spowodować szkody, których skutki
będą trudne do usunięcia. Jeśli weźmiemy pod uwagę wszechobecność niektórych serwerów
w internecie oraz założenie, że będą one zawsze dostępne, to okaże się, że ich wyłączenie może
być poważnym problemem.
334 Część III E-commerce i bezpieczeństwo
Warto zauważyć, że ataki DoS, podobnie jak inne zagrożenia, mogą być spowodowane nie tylko
wrogimi działaniami. Może je też wywołać błędnie skonfigurowana sieć lub gwałtowny napływ
użytkowników (na przykład po publikacji informacji o witrynie na jakimś popularnym blogu
technologicznym).
Na początku 2013 roku została przeprowadzona seria ataków przeprowadzonych przy użyciu tak
zwanej blokady rozproszonej (ang. Distributed Denial of Service — DDoS), skierowanych
przeciwko amerykańskim instytucjom finansowym, takim jak American Express czy też Wells
Fargo. Witryny wymienionych firm dysponują łączami o ogromnej przepustowości, a nad ich
zabezpieczeniem pracują zespoły składające się z doskonałych specjalistów, jednak mimo to ich
systemy mogą zostać zablokowane na długie godziny w wyniku DoS. Trudno wskazać konkretne
korzyści, jakie potencjalny cracker mógłby osiągnąć z powodu zablokowania popularnego serwi-
su internetowego, niemniej właściciel zablokowanej witryny traci zarówno pieniądze, czas, jak
i dobrą opinię.
Jednym z powodów, dla których tak trudno ustrzec się przed atakami tego typu, jest fakt, że mogą
one być przeprowadzone na wiele różnych sposobów. Polegają np. na zainstalowaniu na atako-
wanym serwerze WWW programu, którego wykonanie będzie czasochłonne dla procesora, na
wykorzystaniu tzw. odwrotnego spammingu (ang. reverse spamming) czy użyciu zautomatyzo-
wanych programów. Odwrotny spamming polega na wysyłaniu ogromnych ilości listów elektro-
nicznych, w których jako nadawca zostaje wymieniony właśnie serwis mający być obiektem
ataku. Witryna zostanie wówczas zalana odpowiedziami niezadowolonych internautów, do których
te listy trafiły.
Ograniczanie ryzyka
Przeciwdziałanie atakom DoS jest niezwykle trudne. Można w tym celu sprawdzić, jakie porty
są najczęściej wykorzystywane w trakcie tych ataków, a następnie je zablokować. Router może
zawierać mechanizmy, które ograniczą liczbę żądań wykorzystujących określony protokół, np.
ICMP, do procentowo określonej wielkości. Generalnie znalezienie komputerów, z których atak
jest przeprowadzany, jest znacznie łatwiejsze niż ochrona własnych maszyn przed blokadą. Gdyby
każdy administrator poświęcał odpowiednio wiele uwagi monitorowaniu zarządzanej przez siebie
sieci, blokady DDoS nie sprawiłyby aż tylu problemów.
Ponieważ istnieje mnóstwo metod przeprowadzenia ataku, najlepszym rozwiązaniem wydaje się
ciągłe monitorowanie ruchu w sieci, a w razie zauważenia jakichś nieprawidłowości natychmiastowe
podjęcie odpowiednich działań.
Ogólnie rzecz biorąc, dobrze jest mieć plan określający, co zrobić w przypadku ogromnego wzrostu
ruchu sieciowego, niezależnie od jego przyczyny.
Rozdział 14. Zagrożenia bezpieczeństwa aplikacji internetowych 335
Innym rozwiązaniem jest stworzenie mechanizmu, który pozwoli na przekształcenie całej witryny
lub jej części na statyczne strony i skopiowanie jej do sieci dystrybucji treści (CDN — Content
Distribution Network). Takie rozwiązanie doskonale sprawdza się wtedy, kiedy trzeba sobie radzić
ze wzrostem ruchu sieciowego, który nie jest wynikiem ataku.
Niektórzy dostawcy usług hostingu w chmurze, na przykład Amazon Web Services (AWS),
udostępniają mechanizm automatycznego skalowania, który umożliwia dodawanie dodatkowych
serwerów w celu obsługi ruchu sieciowego. Także to rozwiązanie dobrze się sprawdza w sytuacjach,
gdy zwiększony ruch nie jest następstwem ataku. Jednak w razie ataku DDoS nie zaleca się stoso-
wania tej metody, głównie z powodu związanych z tym wysokich kosztów.
Ze względu na ogromną liczbę możliwych sposobów przeprowadzania ataków DoS jedyną efek-
tywną metodą przeciwdziałania im jest monitorowanie normalnego ruchu sieciowego i przygotowa-
nie się na podjęcie odpowiednich działań w razie wystąpienia sytuacji odbiegającej od normy.
Jednak w przypadku ataku DDoS nawet takie rozwiązanie może się okazać niewystarczające.
Przedstawiony przykład był trywialny, lecz możliwości ataków typu Cross Site Scripting są niezwy-
kle szerokie.
336 Część III E-commerce i bezpieczeństwo
Istnieją także inne formy ataków polegających na wstrzykiwania kodu. Na przykład w poprzed-
niej części książki wspomniano o atakach polegających na wstrzykiwaniu kodu SQL.
Jest również możliwość wykorzystania słabych punktów naszego kodu, aplikacji zainstalowanych
na komputerze czy też usterek w konfiguracji w celu przesłania na serwer i uruchomienia dowolnego
kodu, tak by doprowadzić do złamania jego zabezpieczeń. To zagadnienie zostanie opisane w na-
stępnym punkcie rozdziału.
Ograniczanie ryzyka
Unikanie różnorakich form wstrzykiwania kodu i poleceń wymaga szczegółowej wiedzy i zwracania
uwagi na detale. Niektóre techniki i narzędzia, które można w tym celu zastosować, zostaną opisane
w rozdziale 15., „Tworzenie bezpiecznych aplikacji internetowych”.
Na tego typu ataki trzeba być szczególnie wyczulonym, ponieważ pierwszą czynnością, jaką wy-
konują napastnicy po włamaniu się do serwera, jest zatarcie śladów i ukrycie wszelkich oznak
ich działania.
Ograniczanie ryzyka
Zabezpieczanie przed złamaniem zabezpieczeń dostępu do serwera wymaga zastosowania rozwią-
zań nazywanych obroną w głąb (ang. defence-in-depth), które zostaną dokładnie opisane w roz-
dziale 15. Najkrócej rzecz ujmując, oznacza to wyobrażenie sobie wszystkiego, co może pójść źle,
i to w każdym aspekcie działania systemu, a następnie przygotowanie odpowiedniej warstwy zabez-
pieczającej wszystkie wykryte słabe punkty.
Jedną ze strategii ograniczania ryzyka, o której warto tu wspomnieć, jest wykorzystanie systemu
wykrywania włamań (ang: Intrusion Detection System, w skrócie IDS), takiego jak Snort. Systemy
tego typu służą do monitorowania ruchu sieciowego i ostrzegania przed takim, który może być
zagrożony atakiem.
Najlepszym rozwiązaniem byłoby, gdyby obie strony transakcji nie miały możliwości zaprze-
czenia, że wzięły w niej udział lub też mogły udowodnić swoje racje, na przykład w sądzie.
W praktyce jednak rzadko istnieje taka możliwość.
Rozdział 14. Zagrożenia bezpieczeństwa aplikacji internetowych 337
Ograniczanie ryzyka
Uwierzytelnianie daje do pewnego stopnia pewność co do tożsamości osoby biorącej udział
w transakcji. Jeszcze większe zaufanie można mieć w przypadku korzystania z cyfrowych certyfi-
katów autentyczności. System uwierzytelniania certyfikatów ma pewne luki, niemniej jednak
jest on obecnie standardem.
Istotną kwestią jest również zablokowanie możliwości dokonywania jakichkolwiek zmian w wia-
domościach wymienianych przez obie strony. Nic nie da posiłkowanie się treścią wiadomości
wysłanej przez jakąś firmę, jeśli nie można udowodnić, że jest to wiadomość oryginalna, a jej treść
nie została zmieniona. Również w tym przypadku wykorzystanie podpisu cyfrowego czy zaszy-
frowanie wiadomości znacznie utrudnia dokonanie w niej jakichś zmian.
Firmy prowadzące działalność w internecie zapewne bez oporów wydadzą kilkaset dolarów na
uzyskanie certyfikatu autentyczności od odpowiedniej organizacji, na przykład Symantec
(http://www.symantec.com/), Thawte (http://www.thawte.com) czy też Comodo (http://www.comodo.com/),
aby przekonać klientów o swej rzetelności. Czy ta sama firma będzie skłonna odprawić z kwitkiem
klienta, który takiego certyfikatu nie posiada? W przypadku transakcji o niewielkiej wartości
sklepy internetowe pogodzą się raczej z ryzykiem oszustwa lub zaprzeczania udziału w transakcji,
niż odmówią obsłużenia klienta.
Identyfikacja użytkowników
Wszystkich użytkowników naruszających bezpieczeństwo systemu bez namysłu traktuje się jak
użytkowników złych lub złośliwych, których celem jest wyłącznie wyrządzenie komuś szkody.
Trzeba jednak pamiętać również o innych użytkownikach, którzy mogli wykonywać dane działania
nieświadomie i dlatego zaliczanie ich od razu do grona szkodników może być nieuzasadnione.
Napastnicy i crackerzy
Najbardziej znaną grupą użytkowników są tak zwani crackerzy czy też napastnicy. Nie należy ich
mylić z hakerami, ponieważ prawdziwi hakerzy są zwykle programistami o krystalicznej uczci-
wości i z przyjaznymi intencjami, dlatego wrzucanie ich do jednego worka z crackerami może być
dla nich krzywdzące. Dążeniem crackerów jest — abstrahując od kierujących nimi motywów —
znalezienie luk w zabezpieczeniach systemu i wykorzystanie ich do własnych celów. Motywami
crackerów wykradających dane finansowe i numery kart kredytowych może być pazerność;
crackerzy opłacani przez nieuczciwe firmy mogą dla nich wykradać informacje z systemów
firm konkurencyjnych; jeszcze inni są po prostu utalentowanymi ludźmi lubiącymi ryzyko, dlatego
szukają coraz to nowych systemów, do których można by się włamać. Crackerzy są poważnym
zagrożeniem dla każdego systemu dostępnego w sieci, jednak nie znaczy to, że należy skupiać
na nich całą swoją uwagę.
338 Część III E-commerce i bezpieczeństwo
Rozczarowani pracownicy
Kolejną grupą osób, o której nie należy zapominać, są pracownicy. Z takiego albo innego powodu
niektórzy z nich mogą dążyć do wyrządzenia szkody swojej firmie. Niezależnie od motywacji,
pracownicy tacy mogą przeobrazić się w domorosłych crackerów albo zacząć korzystać z narzędzi
pochodzących z zewnątrz, aby zaatakować serwery z wewnętrznej sieci firmy. Jeżeli systemy
zostaną szczelnie zabezpieczone przed atakami z zewnątrz, natomiast zabezpieczenia wewnętrzne
praktycznie nie będą istnieć, będzie to oznaczać, że systemy nadal są niezabezpieczone. Jest to
jeden z najważniejszych argumentów za tworzeniem tak zwanych stref zdemilitaryzowanych
(ang. demilitarized zones — DMZ), o których więcej powiemy w następnym rozdziale.
My sami
Jakkolwiek zaskakująco może to zabrzmieć, jednym z najmniej pewnych punktów w zabezpie-
czeniach systemów są sami programiści i napisany przez nich kod. Jeżeli programista nie podejdzie
z należytą uwagą do bezpieczeństwa, napisze dziurawy kod i nie przyłoży się do jego odpowiedniego
przetestowania i zweryfikowania jego bezpieczeństwa, automatycznie udostępni złośliwym użytkow-
nikom możliwość złamania zabezpieczeń systemu.
Każdy, kto bierze się za pisanie kodu źródłowego, musi czynić to zgodnie ze sztuką. Internet jest
szczególnie bezlitosny dla tych, którzy są beztroscy i leniwi. Najtrudniejszą rzeczą jest jednak
w tym przypadku przekonanie o tym fakcie własnego szefa albo osoby odpowiedzialnej za budżet
projektu. Jednak kilka minut poglądowej prezentacji negatywnych następstw będących wynikiem
zaniedbań w projektowaniu zabezpieczeń systemu powinno wystarczyć, by przekonać decydentów,
że w świecie, w którym dane są najważniejsze, warto czasem ponieść nieco większe koszty.
Rozdział 14. Zagrożenia bezpieczeństwa aplikacji internetowych 339
W następnym rozdziale
Doskonałym źródłem informacji dotyczących zagrożeń związanych z aplikacjami internetowymi jest
witryna projektu Open Web Application Security Project (OWASP). Każdego roku publikowany
jest w niej raport przedstawiający 10 najpoważniejszych zagrożeń dla bezpieczeństwa aplikacji
internetowych; można w niej także znaleźć listę doskonałych e-booków oraz innych zasobów
poświęconych zagadnieniom bezpieczeństwa w internecie. Witryna ta jest dostępna pod adresem
http://www.owasp.org/.
Planowanie z wyprzedzeniem
Bezpieczeństwo nie jest dodatkową funkcją. Gdy tworzy się aplikację internetową i podejmuje
decyzję dotyczącą zbioru zawartych w niej funkcji, bezpieczeństwo nie jest czymś, co czasem
dodaje się do tego zbioru i przypisuje konkretnemu programiście do zaimplementowania w ciągu
paru dni. Bezpieczeństwo musi zawsze być w centrum procesu projektowania aplikacji, a jego
zapewnienie jest zadaniem, które nigdy się nie kończy — również po wdrożeniu aplikacji i spadku
intensywności prac programistycznych, a nawet po ich całkowitym zakończeniu.
Jednym z możliwych rozwiązań jest nałożenie na użytkowników wymogu, by każdy z nich prze-
chodził przez cztery etapy logowania i na każdym podawał inne hasło. Można także zobowiązać
użytkowników do zmiany wszystkich czterech haseł co najmniej raz w miesiącu i odrzucać nowe
hasła, jeżeli były już używane w przeszłości. Można by przy tym również wymagać, by hasła były
bardzo długie i zawierały znaki z różnych klas (wielki i małe litery, znaki przestankowe, cyfry itd).
W ten sposób system stałby się o wiele bezpieczniejszy, a potencjalni crackerzy musieliby poświęcić
bardzo dużo czasu, aby złamać mechanizm logowania i dostać się do systemu.
Niestety, tak skonstruowany system stałby się aż tak bezpieczny, że praktycznie nikt nie chciałby
go używać. Użytkownicy prędzej czy później doszliby do wniosku, że stopień skomplikowania
systemu jest zbyt duży. W ten sposób dochodzimy do reguły, według której o bezpieczeństwo
trzeba dbać tak jak o użyteczność. Prosty w użyciu system z tylko symbolicznymi zabezpiecze-
niami może zdobyć sobie sympatię użytkowników, lecz z drugiej strony będzie również zdecy-
dowanie bardziej narażony na problemy z bezpieczeństwem oraz wynikające z nich przymusowe
przerwy w działaniu. I odwrotnie: system z najbardziej wymyślnymi zabezpieczeniami, które będą
niezrozumiałe dla zwykłych śmiertelników, nie zyska sobie zbyt wielu użytkowników, co także
negatywnie odbije się na prowadzonej działalności biznesowej.
Monitorowanie bezpieczeństwa
Nawet po zakończeniu implementacji aplikacji i udostępnieniu jej użytkownikom na serwerach
produkcyjnych praca projektanta się nie kończy. Częścią procesu zapewnienia bezpieczeństwa
jest monitorowanie pracy systemu, sprawdzanie zawartości dzienników zdarzeń i innych plików
oraz analiza wydajności systemu oraz sposobów pracy z nim. Jedynie na podstawie bieżącej
analizy pracy systemu (albo przez regularne uruchamianie programów wykonujących tego typu
czynności analityczne) można stwierdzić, czy zachodzą jakiekolwiek zagrożenia dla bezpieczeń-
stwa systemu, oraz identyfikować obszary, w których być może powinno się pomyśleć o zaimple-
mentowaniu bezpieczniejszych rozwiązań.
Bezpieczeństwo jest niestety nieustanną walką, której — przynajmniej w przenośni — nigdy nie
można wygrać. Ceną za bieżące prawidłowe działanie systemu jest konieczność jego ciągłego
monitorowania i natychmiastowego reagowania na wszelkie zidentyfikowane problemy.
Druga faza podejścia do zagadnień bezpieczeństwa to faza od szczegółu do ogółu. To właśnie nią
zajmiemy się w tym rozdziale. W tej fazie bierze się pod uwagę wszystkie komponenty, z jakich
składa się aplikacja, takie jak serwer bazy danych, sam serwer czy sieć, w której serwer się znaj-
duje. Trzeba spróbować zapewnić bezpieczeństwo korzystania z poszczególnych komponentów,
jak również zagwarantować, że komponenty te zostaną zainstalowane i skonfigurowane w sposób
zgodny z wymaganiami bezpieczeństwa. Wiele produktów jest instalowanych domyślnie z ustawie-
niami konfiguracyjnymi, przy których bardzo łatwo się do nich włamać, dlatego koniecznie trzeba
się zapoznać ze wszystkimi znajdującymi się w nich lukami i sposobami ich eliminowania.
Twórcy aplikacji muszą filtrować wszystkie dane wejściowe pochodzące ze źródeł zewnętrznych.
Nie oznacza to wcale, że systemy należy projektować z myślą, iż wszyscy jego użytkownicy
344 Część III E-commerce i bezpieczeństwo
będą mieli złe intencje. Wręcz odwrotnie: użytkowników powinno się zachęcać do korzystania
z aplikacji, trzeba jedynie jak najlepiej przygotować się na wszelkiego rodzaju ataki i błędne korzy-
stanie z systemu.
Jeżeli filtrowanie będzie wykonywane efektywnie, znacząco powinna zmniejszyć się liczba zagro-
żeń z zewnątrz, a możliwości pracy samego systemu powinny zdecydowanie wzrosnąć. Nawet
jeżeli można zaufać wszystkim użytkownikom systemu, to nigdy nie ma się pewności, że na ich
komputerach nie znajduje się oprogramowanie typu spyware lub podobne, które będzie samo-
dzielne wysyłać żądania na serwer lub zmieniać treść żądań wysyłanych przez użytkownika.
Zatem tak naprawdę nigdy nie można ufać użytkownikom.
Ze względu na to, jak ważnym elementem jest filtrowanie danych wejściowych pochodzących od
użytkowników, warto przyjrzeć się bliżej sposobom filtrowania.
<!DOCTYPE html>
<html>
<head>
<title>Kim jesteś?</title>
</head>
<body>
<h1>Kim jesteś ślicznotko?</h1>
<form action="formularz.php" method="POST">
<p>
<input type="radio" name="plec" id="plec_m" value="Mężczyzną"/>
<label for="plec_m">Mężczyzną</label><br />
</p>
<button type="submit" name="wyslij">Wyślij formularz</button>
</form>
</body>
</html>
Wygląd tego formularza pokazano na rysunku 15.1. W celu obsłużenia danych pochodzących
z formularza można założyć, że za każdym razem, gdy w skrypcie formularz.php odczytywana jest
wartość $_POST['plec'], powinien zostać zwrócony łańcuch znaków 'Mężczyzna', 'Kobieta' lub
'Nie twój interes'. Byłoby to jednak założenie bardzo ryzykowne.
Rozdział 15. Tworzenie bezpiecznych aplikacji internetowych 345
Rysunek 15.1.
Najprostszy formularz
z pytaniem o płeć
Jak wspomniano już wcześniej, działanie sieci WWW opiera się na protokole HTTP — prostym
protokole tekstowym. Przesłanie powyższego formularza spowoduje wysłanie do serwera komuni-
katu tekstowego o strukturze podobnej do przedstawionej poniżej:
POST /formularz.php HTTP/1.1
Host: www.domenakomputera.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 14
plec=Mężczyzną
Nic jednak nie stoi na przeszkodzie, by ktoś połączył się z naszym serwerem i wysłał do niego
dowolną inną wartość w ramach formularza. Można by spreparować na przykład żądanie nastę-
pującej treści:
POST /formularz.php HTTP/1.1
Host: www.domenakomputera.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49:0) Gecko/20100101 Firefox/49.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 18
plec=Lubię+ciastka
Bez trudu mogłoby w takiej sytuacji dojść do zamieszania. Znacznie lepszym rozwiązaniem jest
sprawdzanie za każdym razem, czy otrzymana wartość należy do zbioru wartości dozwolonych,
tak jak pokazano na listingu 15.2.
<?php
switch ($_POST['plec']) {
case 'Mężczyzną':
case 'Kobietą':
case 'Inny':
echo "<h1>Gratulacje!
Jesteś: ".$_POST['plec'].".</h1>";
346 Część III E-commerce i bezpieczeństwo
break;
default:
Ilość wymaganego kodu jest w tym przypadku nieco większa — zawiera on bowiem listę dopusz-
czalnych wartości — lecz przynajmniej zyskuje się dzięki temu pewność, że do formularza
przekazywane są prawidłowe wartości. Fakt ten zdecydowanie zyskuje na znaczeniu, gdy przetwa-
rzane są na przykład dane finansowe, znacznie bardziej poufne niż płeć użytkownika. Według
ogólnej zasady nigdy nie należy oczekiwać, że wartość pochodząca z formularza zawsze będzie
należeć do zbioru wartości oczekiwanych — zawsze trzeba ją najpierw zweryfikować.
Prosty sposób zapewnienia, że dana wartość jest odpowiedniego typu, polega na zrzutowaniu lub
przekształceniu jej do pożądanego typu, a następnie jej użyciu, na przykład:
$liczba_nocy = (int)$_POST['liczba_nocy'];
if ($liczba_nocy == 0) {
echo "BŁĄD: Nieprawidłowa liczba nocy!";
exit;
}
Jeżeli użytkownik ma za zadanie wpisać datę w jakimś formacie narodowym, na przykład w for-
macie mm/dd/yy używanym w Stanach Zjednoczonych, można wówczas napisać kod, który przy
użyciu funkcji PHP o nazwie checkdate() zapewni, że mamy do czynienia z prawidłową datą.
Funkcja checkdate przyjmuje wartości miesiąc, dzień i rok (rok w formacie czterocyfrowym)
i wskazuje, czy po połączeniu tych wartości uzyskuje się prawidłową datę:
$mmddrr = explode('/', $_POST['data_wyjazdu']);
if (count($mmddrr) != 3)
{
echo "BŁĄD: Podano nieprawidłową datę!";
exit;
}
Przefiltrowanie i sprawdzenie danych wejściowych wymaga czasu i wysiłku, ale dzięki wykona-
niu tych czynności nie tylko spełnia się wymóg weryfikacji poprawności tych danych, co zresztą
powinno się robić zawsze (czyli na przykład sprawdzać, czy data wylotu podana w momencie zaku-
pu biletu na samolot rzeczywiście jest prawidłową datą), ale także zwiększa ogólne bezpieczeń-
stwo całego systemu.
Aby uniknąć ataku polegającego na wstrzykiwaniu kodu SQL, można jednocześnie zastosować
dwa rozwiązania przedstawione poniżej:
Używanie zapytań sparametryzowanych. Zapytania tego typu oddzielają kod SQL od
danych. To rozwiązanie nie może jednak zabezpieczyć przed problemami z nazwami
kolumn i tabel, gdyż ich nie można określać za pomocą parametrów. Niemniej ponieważ
z góry znamy strukturę bazy danych, nazwy kolumn i tabel można filtrować, dopuszczając
użycie wyłącznie prawidłowych wartości.
Upewnianie się, że wszystkie dane wejściowe są zgodne z oczekiwaniami. Jeżeli
nazwy użytkowników nie mogą przekraczać 50 znaków długości i powinny zawierać
wyłącznie litery i liczby, z miejsca można założyć, że znajdujący się na końcu nazwy
użytkownika fragment łańcucha "; DELETE FROM użytkownicy" należy usunąć. Jeżeli
napisany zostanie kod PHP, który jeszcze przed wysłaniem danych wejściowych do
serwera bazodanowego zagwarantuje, że dane wejściowe są zgodne z oczekiwaniami,
pozwoli na wyświetlenie użytkownikowi o wiele bardziej konkretnego komunikatu o błędzie
niż ten, który zostałby zwrócony przez bazę danych (o ile dane wejściowe byłyby w bazie
sprawdzane). Tym samym znacznie zmniejszone zostanie ryzyko naruszenia bezpieczeństwa.
konsekwencji. W tym celu wykorzystuje się kilka funkcji, dzięki którym przeglądarka klienta po-
traktuje dane wynikowe po prostu jako tekst wynikowy i nie pomyli takich danych z żadnymi
innymi.
W wielu aplikacjach internetowych pobiera się dane od użytkownika i wyświetla się je z powrotem
na stronie. Doskonałym przykładem są strony, na których użytkownicy mogą komentować
opublikowane artykuły. W takich przypadkach trzeba zapewnić, że żaden złośliwy użytkownik nie
wstrzyknie we wpisywanym tekście dodatkowego kodu HTML.
Ponieważ wszystkie elementy znaczników języka HTML są ograniczane znakami < i >, trudno
byłoby je zawrzeć w wynikowym łańcuchu znaków przeznaczonym do wyświetlenia w przeglą-
darce (ponieważ domyślnie przeglądarka przyjmie, że wskazują one znaczniki języka HTML).
Aby obejść problem używa się elementów HTML < oraz >. Analogicznie, aby w wyniko-
wym kodzie HTML zawrzeć znak ampersanda, należy użyć elementu &. Apostrof i cudzy-
słów są reprezentowane odpowiednio przez elementy ' oraz ". Elementy są przekształcane
w odpowiednie znaki wynikowe przez klienta HTML (przeglądarkę internetową) i nie są wówczas
traktowane jak znaczniki.
Obydwie funkcje przyjmują drugi parametr, który określa, jak mają działać apostrofy, cudzysłowy
i nieprawidłowe sekwencje kodu. Obie przyjmują również trzeci parametr, wyznaczający zestaw
znaków, w jakim zakodowano łańcuch wejściowy przekazany do funkcji (co jest dla nas bardzo
ważne, ponieważ funkcja powinna prawidłowo działać na łańcuchach znaków UTF-8). Poniżej
przedstawionych zostało pięć najczęściej używanych wartości drugiego parametru tych funkcji:
ENT_COMPAT (wartość domyślna) — Cudzysłowy są przekształcane w element ",
apostrofy natomiast pozostają w oryginalnej postaci.
ENT_QUOTES — Apostrofy i cudzysłowy są przekształcane odpowiednio w elementy '
oraz ".
ENT_NOQUOTES — Funkcja nie przekształca w elementy HTML ani apostrofów,
ani cudzysłowów.
ENT_IGNORE — Nieprawidłowe sekwencje kodu są milcząco pomijane.
ENT_SUBSTITUTE — Nieprawidłowe sekwencje kodu zą zastępowane znakiem zamiennika
Unicode, a nie pustym łańcuchem znaków.
ENT_DISALLOWED — Nieprawidłowe sekwencje kodu zą zastępowane znakiem zamiennika
Unicode, a nie pozostawiane w początkowej postaci.
Rozdział 15. Tworzenie bezpiecznych aplikacji internetowych 349
Tak skonstruowany łańcuch znaków zostanie przetworzony przez poniższy skrypt PHP (na łańcuchu
wynikowym wykonywana jest funkcja nl2br w celu zapewnienia czytelności wynikowego łań-
cucha znaków):
<?php
?>
Łańcuch wynikowy będzie mieć wówczas (po wyświetleniu w przeglądarce źródła strony) nastę-
pującą postać:
<p align"center">Czy użytkownik dał "15000?".</p><br />
<br />
<script type="text/javascript"><br />
// złośliwy kod JavaScript<br />
</script><p align"center">Czy u¿ytkownik da³ "15000?".</p><br />
<br />
<script type="text/javascript"><br />
// z³ośliwy kod JavaScript<br />
</script>
<script type="text/javascript">
// złośliwy kod JavaScript
</script>
Trzeba zauważyć, że wywołanie funkcji htmlentities() zastąpiło symbol waluty euro (€) odpo-
wiednim symbolem HTML (€), a wywołanie htmlspecialchars() nie wprowadziło w nim
żadnych dodatkowych modyfikacji.
W sytuacjach, gdy użytkownicy powinni mieć możliwość wpisywania znaczników HTML (na przy-
kład w biuletynach elektronicznych, w których użytkownicy zwykle mogą formatować czcionkę,
kolor i styl znaków), konieczne będzie zaimplementowanie własnego mechanizmu przetwarzania
łańcuchów znaków, który znajdzie dozwolone znaczniki i pozostawi je w łańcuchach wynikowych.
Przede wszystkim należy się zastanowić, co się stanie, gdy złośliwy użytkownik odwoła się do
pliku, który nie ma rozszerzenia .php ani .html. Wiele serwerów WWW domyślnie przesyła wów-
czas do strumienia wyjściowego zawartość wywołanego pliku, o ile nie zostaną prawidłowo skon-
figurowane. Jeżeli więc plik obiekt_uzytkownik.php będzie się znajdował gdzieś w drzewie kata-
logu głównego witryny i użytkownik odwoła się do niego, w przeglądarce może pojawić się
treść kod źródłowego zawartego w tym pliku. Użytkownik może w ten sposób zobaczyć dane
lub ścieżki dostępu do plików na serwerze, jak również — ewentualnie — znaleźć luki w systemie
zabezpieczeń.
W celu zapobieżenia takim sytuacjom serwer WWW należy skonfigurować w taki sposób, aby
przetwarzane były wyłącznie żądania skierowane do plików .html i .php, a żądania do plików
z innymi rozszerzeniami (takimi jak *.inc, *.mo czy *.txt) powodowały zwrócenie błędu.
Poza tym, jeśli wszystkie używane pliki mają rozszerzenie .php, może się zdarzyć, że bezpośred-
nie odwołanie do pliku przeznaczonego wyłącznie do dołączania do innych może pociągać za
sobą niezamierzone konsekwencje. Przykładem może być biblioteka z kodem administracyjnym.
Używając jej w zwyczajnym kontekście, możemy sprawdzać, czy użytkownik jest uwierzytelniony;
jeśli jednak taka biblioteka zostanie wczytana niezależnie, to proces uwierzytelniania może zostać
pominięty.
Analogicznie, wszystkie inne pliki takie jak pliki z hasłami, pliki tekstowe i konfiguracyjne albo
katalogi specjalne, najlepiej jest przechowywać poza katalogiem z dokumentami udostępnianymi
publicznie. Nawet jeżeli wydaje się, że serwer WWW jest skonfigurowany prawidłowo, być może
zapomniano o czymś w trakcie konfigurowania albo w przyszłości serwer może zostać zastąpiony
nową wersją, z konfiguracją nieprawidłową, a więc zwiększy się wówczas poziom zagrożenia.
Jeżeli w pliku php.ini włączona jest opcja allow_url_fopen, teoretycznie możliwe będzie dołączanie
plików znajdujących się na serwerach zdalnych. Jest to kolejne potencjalne zagrożenie dla aplikacji
i dlatego trzeba unikać dołączania plików znajdujących się na innych komputerach — zwłaszcza
na tych, nad którymi nie ma się pełnej kontroli. Dodatkowo nigdy nie należy uzależniać tego, jaki
plik zostanie dołączony, od zawartości danych pochodzących od użytkowników, ponieważ dane
wejściowe mogą zostać odpowiednio spreparowane i zagrozić w ten sposób bezpieczeństwu
systemu.
Wprawdzie jest to rozwiązanie wygodne, lecz nie do końca bezpieczne — jeśli jakiś cracker zdoła
dostać się do tego pliku, od razu uzyska również dane uwierzytelniające do bazy danych wraz
ze wszystkimi prawami, jakie posiada użytkownik jan.
Rozdział 15. Tworzenie bezpiecznych aplikacji internetowych 351
Lepszym rozwiązaniem jest umieszczenie nazwy użytkownika i hasła w oddzielnym pliku, który
będzie się znajdował poza strukturą katalogu głównego aplikacji i będzie dołączany do skryptu
w następujący sposób:
<?php
// plik polaczenie_bd.php
$serwer_bd = 'localhost';
$nazwa_uzytkownika_db = 'jan';
$haslo_bd = 'hasło';
$nazwa_bd = 'baza_danych';
?>
include('../kod/polaczenie_bd.php');
?>
W podobny sposób najlepiej jest używać danych o podobnym poziomie poufności, które zawsze
dobrze jest dodatkowo zabezpieczyć.
Trzeba uważać, aby nie zapisywać plików ze zbyt szerokimi uprawnieniami odczytu ani nie
umieszczać ich w lokalizacjach, do których — we współużytkowanych środowiskach hostingo-
wych — dostęp mają inni użytkownicy.
W ten sposób nieuprawniony użytkownik zyskałby komplet informacji o instalacji PHP oraz
ewentualnych lukach w zabezpieczeniach, które mógłby wykorzystać. Również tego problemu
łatwo można uniknąć: jeżeli użytkownicy mają podawać nazwy plików, które chcą uzyskać,
trzeba podawane nazwy restrykcyjnie filtrować. W sytuacji z przykładu wystarczy eliminować
wszelkie wystąpienia sekwencji ..\, a także uniemożliwiać wywołania plików wskazanych
przez podanie ścieżki bezwzględnej, na przykład c:\mysql\my.ini lub /etc/my.cnf.
352 Część III E-commerce i bezpieczeństwo
Gdy użytkownik łączy się z witryną, wpisuje słowo w oknie wyszukiwania (na przykład „defene-
stracja”), i klika przycisk Szukaj, szybko straci zaufanie do witryny i poziomu jej zabezpieczeń,
jeżeli w odpowiedzi w przeglądarce pojawi się komunikat:
AJAJAJ!!! To się nigdy nie powinno zdarzyć. BŁĄD!!! BŁĄD!!! BŁĄD!!!
Jeżeli stabilność aplikacji będzie brana pod uwagę od początku procesu tworzenia aplikacji,
znacznie łatwiej będzie zmniejszyć prawdopodobieństwo wystąpienia problemów mających swój
początek w błędach ludzkich. Najskuteczniejsze sposoby to:
Przeprowadzenie pełnej fazy projektowania aplikacji, wraz z tworzeniem jej prototypów.
Im więcej osób będzie mogło wypowiedzieć się na temat planowanych działań, tym łatwiej
będzie wychwycić potencjalne problemy jeszcze przed ich wystąpieniem. Jest to zresztą
doskonały moment na przeprowadzenie testów użyteczności docelowego interfejsu
użytkownika.
Przydzielenie do projektu zasobów kontroli jakości. W wielu projektach oszczędza się
właśnie na testowaniu, przydzielając na przykład jednego testera na 50 programistów.
Programiści zazwyczaj nie są dobrymi testerami! Doskonale radzą sobie z zapewnianiem
tego, że ich kod będzie działać prawidłowo w przypadku podawania właściwych danych
wejściowych, lecz znajdowanie innych problemów przychodzi im już z dużo większym
trudem. W największych firmach programistycznych testerów jest mniej więcej tyle
samo co programistów. Stosunkowo wysokie jest ryzyko, że szef naszej firmy nie będzie
chciał opłacać armii testerów, jednak przeprowadzenie odpowiedniego procesu testowania
jest kluczowe dla powodzenia aplikacji.
Zobowiązanie programistów do stosowania zautomatyzowanych testów. Trudno się
spodziewać, że w ten sposób znalezione zostaną wszystkie błędy, które mógłby znaleźć
doświadczony tester, lecz na pewno zapobiegnie się w ten sposób zjawisku regresji — czyli
sytuacji, w której problem lub błąd naprawiony już wcześniej pojawia się w systemie
ponownie w wyniku zmian w innej części kodu. Programiści powinni mieć zakaz dołączania
do projektu własnego kodu źródłowego lub wdrażania kodu, jeżeli wcześniej nie przeszedł
on z powodzeniem testów.
Monitorowanie działania aplikacji po jej wdrożeniu w środowisku produkcyjnym. Dzięki
regularnemu przeglądaniu dzienników zdarzeń aplikacji oraz komentarzy użytkowników
lub klientów można będzie zidentyfikować poważniejsze błędy i problemy oraz ewentualne
luki w zabezpieczeniach systemu. Zidentyfikowane problemy i błędy będzie można od razu
wyeliminować, zanim doprowadzą do poważniejszych konsekwencji.
Wykonywanie poleceń
Wcześniej wspomniano o tak zwanym operatorze wykonawczym. Ma on postać operatora języka,
za którego pośrednictwem w wierszu poleceń powłoki (na przykład sh w systemach z rodziny
Unix albo cmd.exe w Windows) można wykonać wskazane polecenie zawarte między apostrofami
odwrotnymi (`); trzeba przy tym zwrócić uwagę na to, że różnią się one od zwykłych apostrofów (').
Rozdział 15. Tworzenie bezpiecznych aplikacji internetowych 353
Klawisz z apostrofem odwrotnym znajduje się zwykle w lewym górnym rogu klawiatury o układzie
angielskojęzycznym, natomiast na klawiaturach o innych układach językowych znalezienie klawisza
może być już trudniejsze.
Jeżeli w pliku tekstowym znajduje się lista nazwisk wraz z ich numerami telefonów, można użyć
polecenia grep, aby znaleźć w pliku listę nazwisk zawierających słowo „Kowalski”. Polecenie
grep jest poleceniem systemów z rodziny Unix i przyjmuje łańcuch znaków stanowiący wzorzec
do znalezienia oraz listę plików, w których ten wzorzec ma zostać odnaleziony, po czym zwraca
te wiersze z plików, w których wzorzec rzeczywiście został znaleziony.
grep [argumenty] wzorzec lista_plików …
Istnieją odmiany programu grep dla systemu Windows; zresztą sam Windows udostępnia program
o nazwie findstr.exe, który działa w podobny sposób. Aby znaleźć osoby o nazwisku „Kowalski”,
można by wykonać skrypt o następującej treści:
<?php
?>
Co gorsza, zazwyczaj serwer WWW i PHP działa w kontekście o jak najbardziej ograniczonych
uprawnieniach (więcej na ten temat powiemy w następnych punktach rozdziału), które jednak
być może trzeba będzie poszerzyć, aby zyskać możliwość wykonywania niektórych poleceń.
To z kolei osłabi bezpieczeństwo całego systemu. Dlatego w środowiskach produkcyjnych
odwrotne apostrofy powinny być stosowane z wyjątkową ostrożnością. Ogólnie rzecz biorąc, ich
stosowanie w ogóle nie jest zalecane. Możliwość używania funkcji shell_exec() oraz odwrotnych
apostrofów można wyłączyć w pliku konfiguracyjnym PHP lub poprzez uruchomienie PHP w trybie
bezpiecznym.
Polecenie exec i funkcje systemowe działają bardzo podobnie do operatora wykonywania poleceń
powłoki, a różnią się jedynie tym, że wskazane polecenie wykonują w sposób bezpośredni, a nie
z poziomu powłoki. Ponadto w przeciwieństwie do apostrofów odwrotnych nie zawsze zwracają
354 Część III E-commerce i bezpieczeństwo
pełen zbiór danych wynikowych. Wykorzystanie polecenia exec i funkcji systemowych również
naraża na szereg zagrożeń bezpieczeństwa i powinno się wobec nich stosować te same środki
ostrożności.
W ramach przyjętego przez nas idealistycznego podejścia do bezpieczeństwa trzeba się upewnić,
że używane serwery WWW oraz PHP są prawidłowo skonfigurowane. Nie ma możliwości, by
szczegółowo opisać metody zabezpieczania każdego serwera WWW i każdego rozszerzenia języka
PHP, dlatego opis zostanie ograniczony do przedstawienia najważniejszych zagadnień, które trzeba
wziąć pod uwagę, oraz kierunku poszukiwań dodatkowych wskazówek i sugestii.
Bardzo ważne jest, aby przygotować sobie prosty „skrypt instalacyjny”, który będzie realizowany
w trakcie instalacji każdej nowej wersji używanego oprogramowania. W ten sposób będzie się mieć
większą pewność, że nie zapomniało się o niczym istotnym, a czego brak mógłby sprawić problemy
w późniejszym czasie. Doskonałym pomysłem będzie zautomatyzowanie takich czynności.
Rysunek 15.2.
Proces
uaktualniania
oprogramowania
na serwerze
W pliku php.ini znajduje się wiele ustawień dotyczących modułów, których w zasadzie nigdy nie
będzie się używać, lecz jeśli moduły te są wyłączone, to ich ustawieniami w ogóle nie trzeba się
przejmować — zostaną one zignorowane. Natomiast w przypadku modułów, które będą używane
na co dzień, należy przejrzeć ich dokumentację dostępną w podręczniku PHP (pod adresem
http://www.php.net/manual) i sprawdzić, jakie opcje konfiguracyjne są dostępne i jakie są dostępne
dla nich wartości.
Zdecydowanie zalecane jest przechowywanie pliku php.ini w systemie kontroli wersji albo w syste-
mie do zarządzania konfiguracją bądź też, w najgorszym razie, ręczne rejestrowanie wprowadzanych
modyfikacji, tak by po zainstalowaniu nowej wersji oprogramowania można było mieć pewność,
że wciąż będą używane prawidłowe ustawienia.
Oczywiście, jak już wcześniej wspomniano, pliki konfiguracyjne powinno się umieścić poza
katalogiem przeznaczonym na dokumenty wchodzące w skład danej witryny.
Skupić się na dostawcach, którzy udostępniają całe drzewa katalogów, a nie tylko katalog
dokumentów. Niektórzy dostawcy informują po prostu, że prywatnym obszarem dostępnym
dla klienta jest wyłącznie katalog dokumentów, inni z kolei udostępniają całą hierarchię
katalogów, w której znajduje się także katalog public_html/ przeznaczony na dokumenty
i skrypty PHP.
Spróbować dowiedzieć się, jakie są ustawienia pliku php.ini dostawcy. prawdopodobnie
większość dostawców nie publikuje informacji o ustawieniach ani nawet nie przesyła ich
potencjalnym klientom, można jednak zapytać personel dostawcy, czy włączony jest tryb
bezpieczny oraz które funkcje i klasy są wyłączone. Można także wywołać funkcję ini_get
i sprawdzić odpowiednie wartości. Dostawcy, którzy nie używają trybu bezpiecznego albo
nie wyłączają żadnych funkcji będą mieli większe szanse na wybór niż ci, którzy oferują
bardziej przemyślaną konfigurację.
Sprawdzić wersje poszczególnych oferowanych przez dostawcę komponentów
oprogramowania. Czy są to wersje najnowsze?
Poszukać usługodawców, którzy oferują okres próbny, gwarancję zwrotu pieniędzy lub inne
metody, dzięki którym będzie można przetestować działanie własnej aplikacji, zanim
podejmie się ostateczną decyzję o związaniu się na dłużej z konkretnym usługodawcą.
Skorzystać z jednej z wielu doskonałych usług w chmurze, takich jak Amazon AWS,
czy też dostawców platformy jako usługi (PaaS), takich jak Heroku. Choć niektórzy
programiści wciąż preferują korzystanie z usług hostingowych, gdyż nie chcą zawracać
sobie głowy zadaniami administracyjnymi, to jednak warto zastanowić się nad takim
rozwiązaniem. Wykonywanie samodzielnych działań w takich środowiskach jest dużo
prostsze od zarządzania własnym komputerem, a poza tym w internecie dostępnych jest
wiele wspaniałych poradników i samouczków. Usługi tego typu znacznie ułatwiają
korzystanie z najnowszych wersji oprogramowania.
Należy się upewnić, że dla wszystkich kont w bazie danych zdefiniowano hasła dostępu. Jedną
z pierwszych czynności, jaką wykonuje się na serwerze bazodanowym, jest nadanie hasła su-
perużytkownikowi (użytkownikowi root). Podczas jego określania najlepiej jest unikać słów, które
można znaleźć w słowniku. Nawet hasła typu 44piesA są znacznie mniej bezpieczne niż hasło
w postaci FI93!!xl2@. Jeżeli ktoś ma trudności z zapamiętaniem tego typu haseł, najlepiej jest
użyć metody polegającej na skonstruowaniu hasła z pierwszych liter słów znanego zdania lub po-
wiedzenia, z uwzględnieniem jakiegoś wzorca występowania wielkich i małych liter w tym zdaniu.
358 Część III E-commerce i bezpieczeństwo
Wiele serwerów baz danych (nie wyłączając starszych wersji serwera MySQL) jest instalowanych
domyślnie z użytkownikiem anonimowym, którego zakres uprawnień jest zwykle większy, niż
byśmy chcieli. W trakcie analizy i poznawania systemu uprawnień należy zwrócić uwagę, by wszel-
kie domyślne konta funkcjonowały zgodnie z wybranym dla nich przeznaczeniem, zaś konta
niepotrzebne zostały usunięte.
Wyłącznie superużytkownik powinien mieć dostęp do tabel z uprawnieniami oraz bazami admini-
stracyjnymi. Inne konta powinny mieć uprawnienia wyłącznie do odczytywania lub modyfikowa-
nia tych baz danych i tabel, które są dla nich przeznaczone.
Aby przetestować system uprawnień, można spróbować wykonać czynności przedstawione poniżej
i upewnić się, że w każdym z tych przypadków baza danych zwraca błąd:
Połączyć się z bazą danych bez podania nazwy użytkownika i hasła.
Połączyć się jako użytkownik root bez podawania hasła.
Podać nieprawidłowe hasło dla użytkownika root.
Połączyć się w kontekście wybranego użytkownika i odczytać zawartość tabeli, do której
ten użytkownik nie powinien mieć dostępu.
Połączyć się w kontekście wybranego użytkownika i uzyskać dostęp do systemowych
baz danych lub tabel z uprawnieniami.
Dopóki nie wykona się wszystkich czynności wskazanych powyżej, dopóty nie będzie się mieć
pewności, że system uwierzytelniania bazy danych jest rzeczywiście właściwie zabezpieczony.
Jednak jak już wcześniej zaznaczano, takie zabezpieczenie nie powinno być jedyną stosowaną formą
ochrony — dodatkowo należy sprawdzać wartość każdego pola przesłanego z formularza. Jeżeli
pole tekstowe jest przeznaczone do wpisywania nazwy użytkownika, warto się upewnić, że nie
wpisano w nim kilobajtów danych ani znaków, które w nazwach użytkownika są zabronione.
Dzięki wykonaniu takiej weryfikacji w kodzie źródłowym zyskuje się od razu możliwość bardziej
precyzyjnego informowania o błędach, a także zmniejsza się ryzyko naruszenia bezpieczeństwa
bazy danych. Analogicznie w przypadku danych dotyczących daty i czasu można sprawdzać ich
poprawność przed wysłaniem do serwera.
Poza tym na wszystkich pozwalających na to serwerach zawsze należy używać poleceń przy-
gotowywanych, w których wszelkie potencjalnie niebezpieczne elementy zostaną automatycznie
zabezpieczone, a odpowiednie części instrukcji będą umieszczone w cudzysłowach.
Aby upewnić się, że baza danych prawidłowo przetwarza dane, można wykonać następujące
czynności:
Spróbować wpisać dane typu '; DELETE FROM nieużywana_tabela' i tym podobne.
W polach przeznaczonych na liczby lub daty spróbować wpisać nieprawidłowe dane,
na przykład '55#$888ABC', i upewnić się, że zwrócony zostanie błąd.
Rozdział 15. Tworzenie bezpiecznych aplikacji internetowych 359
Spróbować wpisać dane, których długość wykracza poza zdefiniowany dopuszczalny limit
i sprawdzić, czy zwrócony zostanie błąd.
Praca serwera
Gdy serwer bazodanowy już działa, wciąż można wykonywać szereg czynności zwiększających
jego bezpieczeństwo. Pierwsza i najważniejsza czynność to zapewnienie, że serwer nigdy nie powi-
nien działać w kontekście superużytkownika (czyli jako root w systemie Unix albo administrator
w Windows). Tak naprawdę serwer MySQL nie pozwoli się uruchomić w kontekście superużyt-
kownika, chyba że w momencie jego uruchamiania jawnie wskaże się takie wymaganie.
Zabezpieczanie sieci
Istnieje co najmniej kilka sposobów zabezpieczenia sieci, w której działa aplikacja internetowa.
Szczegółowy opis dostępnych rozwiązań wykracza poza zakres niniejszej książki, lecz techniki te
są stosunkowo proste i nie służą tylko ochronie aplikacji internetowych.
360 Część III E-commerce i bezpieczeństwo
Zapory sieciowe
Podobnie, jak obowiązkowo należy filtrować dane wejściowe przeznaczone dla aplikacji interne-
towej napisanej w języku PHP, konieczne jest filtrowanie całości danych wchodzących do sieci —
bez względu na to, czy dane są przeznaczone dla biur firmy, czy też dla centrum przetwarzania,
w którym znajdują się serwery i aplikacje.
Protokół TCP/IP, na którym bazuje sieć internet, korzysta z portów. Poszczególne porty są domyśl-
nie dedykowane do obsługi konkretnego rodzaju ruchu sieciowego (na przykład protokół HTTP
jest przekazywany domyślnie na porcie numer 80). Znaczna liczba portów jest przeznaczona
wyłącznie do przekazywania danych sieci wewnętrznej i generalnie nie mają zastosowania
w zakresie interakcji ze światem zewnętrznym. Jeżeli na wybranych portach wyłączone zostanie
przyjmowanie lub wysyłanie danych z sieci, zmniejszy się w ten sposób ryzyko naruszenia zabez-
pieczeń komputerów i serwerów (a w konsekwencji również aplikacji internetowych).
Jedną z metod obniżania ryzyka takiego ataku jest wykorzystanie tak zwanej strefy zdemilitary-
zowanej (ang. demilitarized zone — DMZ). Izoluje się w niej serwery, na których działa aplikacja
internetowa (a także inne serwery, na przykład poczty korporacyjnej) od zewnętrznej sieci inter-
netowej oraz wewnętrznych sieci korporacyjnych, co przedstawiono na rysunku 15.3.
Rysunek 15.3.
Konfiguracja strefy
zdemilitaryzowanej
(DMZ)
Projekt, instalacja i utrzymanie strefy zdemilitaryzowanej to czynności, które wykonuje się w poro-
zumieniu z administratorem sieci we wszystkich lokalizacjach, w których znajdować się będą serwe-
ry przeznaczone dla aplikacji internetowej.
Niestety, przed tego typu atakami bardzo trudno jest się zabezpieczyć i adekwatnie na nie odpo-
wiedzieć. Niektórzy producenci urządzeń sieciowych oferują rozwiązania, których zadaniem
jest zmniejszenie ryzyka i ograniczenie ewentualnych szkód spowodowanych przez atak DoS. Na
razie nie istnieją jednak żadne kompletne rozwiązania chroniące przed atakami DoS.
Bezpieczeństwo komputerów
i systemów operacyjnych
Ostatnim elementem, jaki bezwzględnie należy brać pod uwagę w trakcie zabezpieczania infra-
struktury, są fizyczne komputery pełniące rolę serwerów, na których działa aplikacja internetowa.
Odpowiednie bezpieczeństwo można zapewnić na kilka sposobów.
Jeżeli oprócz wymienionych żadne inne składniki oprogramowania nie są potrzebne, należy je
całkowicie wyłączyć. W ten sposób nie trzeba się będzie dodatkowo martwić o ich odpowiednie
zabezpieczenie. W razie wątpliwości warto poszukać dodatkowych informacji, ponieważ najpraw-
dopodobniej ktoś zadał już w internecie pytanie, czy dana usługa rzeczywiście jest niezbędna,
i otrzymał na to pytanie konkretną odpowiedź.
Dlatego tak ważne jest, aby serwery, na których działają aplikacje internetowe, znajdowały się
w bezpiecznym miejscu, do którego dostęp mają tylko uprawnione osoby, oraz wdrożone zostały
odpowiednie procedury przydzielania i odbierania prawa dostępu poszczególnym osobom.
Jeśli witryna lub usługa działa w chmurze, to taki problem może się objawić jako działanie apli-
kacji wyłącznie w jednym regionie. Z kolei po stronie bazy danych jego przejawem mogą być
trudności ze sporządzaniem kopii zapasowych.
Planowanie działań na wypadek awarii (ang. disaster planning lub recovery planning) jest niezwykle
istotnym, lecz niestety często lekceważonym elementem świadczenia usług informatycznych,
bez względu na to, czy usługą tą jest udostępnianie aplikacji internetowej, czy też ma ona inny
charakter (włączając w to świadczenie bieżącego wsparcia dla działalności biznesowej). Odpowiedni
plan składa się zwykle ze zbioru dokumentów lub procedur, które wyznaczają sposób postępowania
w sytuacji, gdy wystąpiło jedno z poniższych zdarzeń (zdarzeń takich może być znacznie więcej):
Część lub całe centrum przetwarzania danych uległo zniszczeniu w wyniku katastrofy,
na przykład trzęsienia ziemi.
Wszyscy członkowie zespołu programistycznego wyszli na obiad i wpadli pod autobus,
w wyniku czego odnieśli poważne obrażenia (albo ponieśli śmierć).
Główna siedziba firmy spłonęła.
Napastnik z zewnątrz lub złośliwy (bądź jedynie niekompetentny) pracownik zniszczył
wszystkie znajdujące się na serwerach dane używane przez aplikację internetową.
Wiele osób z różnych powodów wzbrania się nawet przed poruszaniem tematu katastrof i ataków,
lecz smutna prawda jest taka, że podobne zdarzenia się zdarzają, choć rzeczywiście niezbyt
często. Zwykle jednak firmy nie mogą sobie pozwolić nawet na chwilę przestoju w świadczeniu
usługi spowodowanego jednym z takich zdarzeń losowych, a taki przestój na pewno wystąpi,
jeżeli firma nie będzie przygotowana na każdą ewentualność. Firma, która ma miliony złotych
dziennego obrotu, od razu by zbankrutowała, gdyby używane przez nią aplikacje internetowe stały
się niedostępne na ponad tydzień, a zadanie ponownego udostępnienia aplikacji zostałoby powie-
rzone osobom, które nie mają odpowiedniego doświadczenia w instalacji używanych przez tę firmę
systemów.
W następnym rozdziale
W rozdziale 16. odejdziemy od teoretycznych zagadnień bezpieczeństwa i przedstawimy informacje
dotyczące uwierzytelniania, czyli sposobu identyfikacji użytkowników. Zaprezentujemy kilka metod
uwierzytelniania, w tym również wykorzystujące PHP i MySQL.
Rozdział 16.
Implementacja metod
uwierzytelniania
przy użyciu PHP
W rozdziale tym zostanie przedstawionych kilka sposobów wykorzystywania PHP i MySQL do
przeprowadzania procesu uwierzytelniania.
Identyfikacja użytkowników
Internet jest środkiem łączności zapewniającym anonimowość, często jednak bardzo przydatna
jest wiedza na temat osób odwiedzających daną stronę internetową. Serwer internetowy bez trudu
jednak może zebrać wiele informacji na temat komputerów i sieci nawiązujących z nim połączenie.
Przeglądarka internetowa zazwyczaj sama się zidentyfikuje, wysyłając do serwera informacje na
temat swego typu, wersji i systemu operacyjnego, na którym pracuje. Istnieje również możliwość
rozpoznania rozdzielczości ekranu użytkownika, używanej palety kolorów, a nawet rozmiaru okna
przeglądarki z wykorzystaniem kodu JavaScript.
Każdy komputer podłączony do internetu posiada własny, niepowtarzalny adres IP. Na jego
podstawie można uzyskać część danych związanych z odwiedzającym. Dzięki nim można dowie-
dzieć się, kto jest właścicielem adresu IP, a niekiedy określić, w której części świata się on znajduje.
Niektóre adresy mogą być bardziej przydatne od innych. Zazwyczaj osoby posiadające stałe połą-
czenie z internetem mają również stały IP. Klientom dostawców internetowych, łączącym się za
pomocą linii telefonicznej, zazwyczaj zostaje przydzielony na czas połączenia jeden z adresów IP
dostawcy. Kolejnym razem ten sam adres IP może być już przyporządkowany innej maszynie.
Urządzenia mobilne dodatkowo komplikują sprawę. Jak zatem widać, adresy IP nie są aż tak przy-
datne do identyfikowania użytkowników, jak by się to mogło wydawać na pierwszy rzut oka.
366 Część III E-commerce i bezpieczeństwo
Wiele witryn w różnorodny sposób zachęca odwiedzających do podania swych danych. Na przy-
kład coraz więcej witryn udostępnia bezpłatne treści, lecz jedynie tym osobom, które zechcą założyć
konto i zalogować się na nim. Większość witryn komercyjnych zapisuje dane klienta składającego
zamówienie po raz pierwszy. Dzięki temu w razie kolejnych zamówień użytkownik nie musi
ponownie podawać informacji o sobie.
W przypadku części użytkowników założenie to okaże się jednak chybione — często zdarza się,
że kilka osób korzysta z jednego komputera, a jedna używa kilku różnych maszyn i urządzeń
mobilnych. Od czasu do czasu trzeba więc ponownie zapytać użytkownika o jego dane, a ponadto
konieczne jest wykazanie, że odwiedzający rzeczywiście jest tą osobą, za którą się podaje.
Listing 16.1. zastrzezone.php — kod źródłowy skryptu realizującego najprostszą metodę uwierzytelniania
<!DOCTYPE html>
<html>
<head>
<title>Zastrzeżona strona</title>
</head>
<body>
<?php
if ((!isset($_POST['uzytkownik'])) || (!isset($_POST['haslo']))) {
//Użytkownik musi podać swój identyfikator i hasło
?>
<h1>Zaloguj się</h1>
<p>Ta strona jest zastrzeżona.</p>
<form method="post" action="zastrzezone.php">
<p><label for="uzytkownik">Użytkownik:</label>
<input type="text" name="uzytkownik" id="uzytkownik" size="15" /></p>
<p><label for="haslo">Hasło:</label>
<input type="password" name="haslo" id="haslo" size="15" /></p>
<p><button type="submit" name="submit">Zaloguj się</button> </p>
</form>
Rozdział 16. Implementacja metod uwierzytelniania przy użyciu PHP 367
<?php
} else if(($_POST['uzytkownik']=='uzytkownik') && ($_POST['haslo']=='haslo')) {
// kombinacja: identyfikator i haslo jest poprawna
echo "<h1>Oto ona!</h1>
<p>Na pewno jesteś szczęśliwy, że możesz zobaczyć tę stronę.</p>";
} else {
// kombinacja: identyfikator i haslo jest nieprawidlowa
echo "<h1>Odejdź stąd!</h1>
<p>Nie jesteś uprawniony do przeglądania tych zasobów.</p>";
}
?>
Rysunek 16.1.
W formularzu HTML
użytkownik powinien
podać swój identyfikator
oraz hasło w celu
uzyskania dostępu
Rysunek 16.2.
Jeśli użytkownik wpisze
błędne dane, zostanie
wyświetlony odpowiedni
komunikat
Jeśli parametry podane przy wywołaniu skryptu okażą się prawidłowe, zostanie wyświetlona sekretna
strona. W naszym przypadku wygląda ona jak ta z rysunku 16.3.
Listing 16.1 zawiera kod źródłowy skryptu realizującego operacje przedstawione na rysunkach 16.1,
16.2 oraz 16.3.
368 Część III E-commerce i bezpieczeństwo
Rysunek 16.3.
Po podaniu właściwych
danych skrypt wyświetli
zawartość strony
Skrypt zaprezentowany na listingu 16.1 realizuje prostą metodę uwierzytelniania, dzięki której
zastrzeżona strona jest dostępna tylko dla uprawnionych użytkowników. Niestety, skrypt obarczony
jest również poważnymi wadami:
akceptuje tylko jeden identyfikator użytkownika i jedno hasło dostępu zapisane bezpośrednio
w kodzie źródłowym,
przechowuje hasło dostępu w formie otwartego tekstu,
chroni tylko jedną stronę internetową,
przesyła hasło dostępu w formie otwartego tekstu.
Każdą z tych wad można wyeliminować, jednak kosztem różnego nakładu pracy i z różnym
powodzeniem.
Użycie bazy danych do przechowywania identyfikatorów użytkowników i ich haseł dostępu nie
skomplikuje zbytnio skryptu, znacznie natomiast przyspieszy wykonywanie operacji uwierzy-
telniania. Dzięki temu możliwe jest również napisanie skryptów pozwalających na zapisywanie
danych o nowych użytkownikach, usuwanie użytkowników oraz modyfikację wcześniej zapisanych
Rozdział 16. Implementacja metod uwierzytelniania przy użyciu PHP 369
haseł dostępu. W przypadku przechowywania haseł w bazie danych trzeba jednak pamiętać o tym,
by nie zapisywać ich otwartym tekstem. Zamiast tego przeważnie przechowywane są jedynie
skróty haseł (utworzone na przykład przy użyciu wbudowanej funkcji PHP o nazwie md5()),
które podczas uwierzytelniania są porównywane z wyznaczonym skrótem wartości wpisanej przez
użytkownika. Dzięki zastosowaniu takiego rozwiązania wciąż można uwierzytelniać użytkowników
pragnących uzyskać dostęp do chronionego zasobu, a jednocześnie w bazie danych nie są prze-
chowywane faktyczne hasła.
Zabezpieczanie haseł
Niezależnie od tego, czy dane użytkowników będą przechowywane w pliku, czy w bazie danych,
lepiej nie decydować się na ryzykowne zapisywanie haseł dostępu w formie otwartego tekstu.
Jednokierunkowy algorytm mieszający zapewni większe bezpieczeństwo praktycznie bez dodat-
kowego nakładu pracy.
W starszych wersjach PHP typowym rozwiązaniem było stosowanie jednej z kilku udostępnianych
jednokierunkowych funkcji mieszających. Najstarszym, ale też najmniej bezpiecznym rozwiąza-
niem jest algorytm Unix Crypt, realizowany przez funkcję crypt(). Algorytm Message Digest 5
(MD5) zaimplementowany w funkcji md5() jest silniejszy. Ostatnio jednak przeważnie stosowane są
funkcje używające algorytmu Secure Hashing Algorithm, takie jak sha1() lub sha256(), bądź inne.
Jawne określanie używanej funkcji mieszającej ma tę wadę, że wraz z upływem czasu algorytmy
mieszające stają się coraz mniej bezpieczne. Jak to możliwe? Otóż naukowcy zajmujący się zagad-
nieniami bezpieczeństwa znajdują sposoby na złamanie algorytmu. Staje się to coraz łatwiejsze wraz
ze wzrostem mocy obliczeniowej komputerów.
W PHP 5.5, jak również w PHP 7, istnieje funkcja password_hash(), która używa silnej, jednokie-
runkowej funkcji mieszającej do wygenerowania tak zwanego skrótu i zapisania go w łańcuchu
znaków. Prototyp tej funkcji ma następującą postać:
string password_hash(string $haslo, integer $algorytm [, array $opcje] )
Funkcja ta ma jeszcze jedną przydatną cechę: pozwala na przekazanie stałej o postaci PASSWORD_DEFAULT,
którą można przekazać w jej wywołaniu jako nazwę używanego algorytmu mieszającego. W kolej-
nych wersjach PHP faktyczna wartość tej stałej jest zmieniana, tak by zawsze określała bezpiecz-
ny algorytm. Domyślnie używanym algorytmem jest CRYPT_BLOWFISH, który można także wskazać
jawnie, podając stałą PASSWORD_BCRYPT jako wartość parametru $algorytm.
Użycie stałej PASSWORD_BCRYPT sprawi, że funkcja wygeneruje również ziarno. W tym kontekście
ziarno oznacza losowo wygenerowaną daną, która jest dodawana do hasła przed wyznaczeniem
jego skrótu. Dzięki tej danej odgadnięcie hasła jest znacznie trudniejsze.
Tego łańcucha nie można w żaden sposób odszyfrować lub przekształcić z powrotem na począt-
kowy łańcuch znaków (nie może tego zrobić nawet jego twórca), więc na pierwszy rzut oka
mogłoby się wydawać, że nie jest on zbyt użyteczny. Jednak jest on przydatny ze względu na swój
deterministyczny charakter — w przypadku przekazania tego samego wejściowego łańcucha zna-
ków użycie skrótu zwróconego przez funkcję password_hash() zawsze zwróci ten sam wynik.
370 Część III E-commerce i bezpieczeństwo
Nie trzeba znać pierwotnego hasła, które zostało przekazane do funkcji mieszającej. Wystarczy
tylko wiedzieć, że skrót wpisanego hasła jest taki sam jak skrót hasła zapisany wcześniej w pliku
lub bazie danych.
Jak już wspomniano, nie zaleca się zapisywania bezpośrednio w kodzie źródłowym identyfikato-
rów użytkowników i ich haseł dostępu. Dane te powinny być zapisywane w oddzielnych plikach lub
przechowywane w bazie danych.
Należy pamiętać, że funkcje mieszające zwykle zwracają dane o stałym rozmiarze. W przypadku
PASSWORD_BCRYPT dane te mają długość 60 znaków, jeśli funkcja zwraca zwykły łańcuch znaków.
Algorytmy, które będą stosowane w przyszłości, mogą zwracać dłuższe skróty, dlatego też należy
się upewnić, że kolumna w bazie danych będzie uwzględniać tę zmianę wielkości danych. Z tego
względu użycie kolumny pozwalającej na zapis 256 znaków wydaje się uzasadnione.
Aby samodzielnie wykorzystać ten mechanizm, można by dołączyć odpowiednie fragmenty kodu
z listingu 16.1 do każdej ze stron, które mają być zastrzeżone. Użycie opcji auto_prepend_file
i auto_append_file spowoduje automatyczne dodanie odpowiedniego fragmentu kodu na początku
i na końcu wszystkich plików znajdujących się we wskazanych katalogach. Opcje te zostały szerzej
omówione w rozdziale 5.
Co jednak stanie się po zastosowaniu tego rozwiązania, gdy użytkownik zapragnie przejrzeć
więcej stron na naszej witrynie? Konieczność podawania identyfikatora i hasła dostępu przed
wyświetleniem każdej kolejnej strony zapewne szybko zniechęci odwiedzającego.
Niestety, również ten sposób nie jest wolny od wad. Ponieważ dane identyfikacyjne zostałyby
dołączone do każdej strony wysyłanej użytkownikowi oraz do każdego adresu URL, zastrzeżone
strony byłyby jawne dla każdej osoby, która na tym samym komputerze zaczęłaby przeglądać
zawartość pamięci podręcznej przeglądarki lub jej historię. Co więcej, hasło dostępu i identyfikator
Rozdział 16. Implementacja metod uwierzytelniania przy użyciu PHP 371
Istnieją dwa sposoby, dzięki którym można uniknąć takich problemów: podstawowe uwierzy-
telnianie protokołu HTTP oraz sesje. Podstawowe uwierzytelnianie likwiduje problem związany
z zapisem odwiedzanych stron w pamięci podręcznej przeglądarki; jednak również w tym przy-
padku hasło dostępu jest przesyłane do serwera wraz z każdym żądaniem. Dopiero wykorzystanie
mechanizmu sesji pozwala uniknąć obu niedogodności. Uwierzytelnianie protokołu HTTP zostanie
omówione w następnym punkcie. Sesje natomiast będą przedmiotem rozdziału 22., a bardziej
szczegółowo zostaną opisane w rozdziale 27.
Co prawda serwer WWW będzie wymagał podania danych uwierzytelniających przed wykonaniem
każdego kolejnego żądania, nie oznacza to jednak, że użytkownik musi co chwila podawać swe
dane identyfikacyjne. Po ich pierwszym wpisaniu przeglądarka zapamięta je i automatycznie
prześle do serwera wraz z każdym żądaniem. Dane podane przez użytkownika zostaną usunięte
z pamięci z chwilą zamknięcia okna przeglądarki.
Opisany mechanizm protokołu HTTP znany jest jako podstawowe uwierzytelnianie. Można go
wykorzystać na przykład w skrypcie PHP lub za pomocą narzędzi udostępnianych przez serwer.
W kolejnych podrozdziałach zostaną przedstawione sposoby użycia tej metody w skryptach PHP
i na serwerze Apache.
list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) =
explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
}
Działanie kodu źródłowego ukazanego na listingu 16.2 jest bardzo podobne jak w przypadku
wcześniej przedstawionych przykładowych skryptów. Jeśli nie zostaną podane żadne dane
uwierzytelniające, użytkownik musi je wpisać. Jeżeli podane informacje okażą się nieprawidłowe,
nastąpi wyświetlenie odmowy dostępu do zasobów. Natomiast po podaniu prawidłowego identyfi-
katora i hasła dostępu wyświetlony zostanie zastrzeżony dokument.
W takim przypadku użytkownik zobaczy interfejs, który wyglądem odbiega nieco od tego pokaza-
nego w poprzednim przykładzie. Tym razem nie jest bowiem wyświetlany formularz HTML,
w którym użytkownik wpisywał dane w poprzednim przykładzie. Zamiast niego przeglądarka
wyświetli własne okno dialogowe. Niektórzy uznają, że jest to lepsze rozwiązanie, inni natomiast
skłaniają się ku utrzymaniu pełnej kontroli nad wizerunkiem strony internetowej. Okno dialogowe
wyświetlane przez przeglądarkę Internet Explorer zostało zaprezentowane na rysunku 16.4.
Rozdział 16. Implementacja metod uwierzytelniania przy użyciu PHP 373
Rysunek 16.4.
Przeglądarka wyświetli
własne okno dialogowe,
jeśli jest przeprowadzane
uwierzytelnianie
protokołu HTTP
W celu zapewnienia takiego samego efektu, jak po wykonaniu wcześniej przedstawionych skryp-
tów, konieczne jest utworzenie dwóch plików HTML: jednego z treścią zastrzeżoną i drugiego
wyświetlającego informację o odmowie dostępu. W poprzednich przykładach niektóre znaczniki
zostały pominięte, w istocie jednak powinny one zawierać również znaczniki <html> i <body>.
Na listingu 16.3 przedstawiona jest zawartość strony, udostępniana tylko uprawnionym użytkow-
nikom. Nadano jej nazwę zawartosc.html. Listing 16.4 zawiera natomiast kod strony wyświe-
tlającej odmowę dostępu, która nosi nazwę odmowa.html. Utworzenie takiej strony nie jest
obowiązkowe, lecz może okazać się bardzo przydatne, gdy chcemy zamieścić na niej jakieś dodat-
kowe wiadomości. Gdy wiemy, że zostanie ona wyświetlona w razie odmowy udzielenia dostępu
użytkownikowi, można na niej zawrzeć informacje na temat sposobów zarejestrowania się
i otrzymania hasła dostępu lub odzyskania hasła (gdyby użytkownik je zapomniał).
<title>Zastrzeżona strona</title>
</head>
<body>
<h1>Odejdź stąd!</h1>
<p>Nie jesteś uprawniony do przeglądania tych zasobów.</p>
</body>
</html>
W tych dwóch ostatnich przykładach nie ma niczego nowego. Znacznie bardziej interesujący będzie
plik przedstawiony na listingu 16.5. Plik ten nosi nazwę .htaccess i jest odpowiedzialny za kon-
trolę dostępu do plików i podkatalogów katalogu, w którym się znajduje.
Listing 16.5. .htaccess — w pliku tym można zawrzeć szereg opcji konfiguracyjnych serwera Apache, w tym
dotyczące uwierzytelniania
AuthUserFile /var/www/.htpass
AuthType Basic
AuthName "Strefa chroniona"
AuthBasicProvider file
Require valid-user
ErrorDocument 401 /var/www/php_html/chapter_16/odmowa.html
Listing 16.5 przedstawia zawartość pliku .htaccess, włączającego stosowanie podstawowego uwie-
rzytelniania w katalogu. Plik ten może określać szereg ustawień, a sześć wierszy przedstawionych
na listingu odnosi się bezpośrednio do uwierzytelniania.
Pierwszy wiersz:
AuthUserFile /var/www/.htpass
informuje, gdzie znajduje się plik z prawidłowymi identyfikatorami i hasłami dostępu. Zazwyczaj
jest on nazywany .htpass, można mu jednak nadać dowolną inną nazwę. Istotna jest jedynie jego
lokalizacja, która powinna znajdować się poza strukturą katalogu głównego witryny — w prze-
ciwnym razie użytkownicy mogliby bez trudu pobrać go z serwera. Zawartość przykładowego
pliku .htpass zaprezentowano na listingu 16.6.
Listing 16.6. .htpass — zawartość pliku przechowującego identyfikatory użytkowników i zaszyfrowane hasła
dostępu
uzytkownik1:$apr1$2dTEuqf0$ok6jSPLkWoswioQyqTwdv.
uzytkownik2:$apr1$9aA0xUxC$pphrV4GqGahOwGI5qTerE1
uzytkownik3$apr1$c2xbFr5F$dOLbi4NG8Ton0bOmRBw/11
uzytkownik4$apr1$vjxonbG2$PPZyfInUnu2vDcpiO.lPZ0
Ponieważ obsługiwanych jest kilka różnych metod uwierzytelniania, konieczne jest określenie tej,
która ma być używana. W tym przykładzie jest to uwierzytelnianie typu Basic, określane przy
użyciu poniższej dyrektywy:
AuthType Basic
Podobnie jak w skrypcie, w celu dokonania uwierzytelnienia należy nadać nazwę chronionemu
zasobowi w następujący sposób:
AuthName "Strefa chroniona"
Nazwa ta może być dowolna, trzeba jednak pamiętać, że użytkownik będzie ją widział. W przedsta-
wionym przykładzie nazwa ta brzmi Strefa chroniona.
Rozdział 16. Implementacja metod uwierzytelniania przy użyciu PHP 375
Konieczne jest także określenie używanego dostawcy uwierzytelniania. W tym przykładzie jest to
plik, co należy określić przy pomocy poniższej dyrektywy:
AuthBasicProvider file
Poza tym trzeba określić, kto ma prawo dostępu do chronionego zasobu. Można tu podać konkret-
nych użytkowników lub ich grupy bądź też, jak zrobiono w przykładzie, można pozwolić na dostęp
wszystkim uwierzytelnionym użytkownikom:
Require valid-user
Każdy wiersz pliku .htpass zawiera identyfikator użytkownika, znak dwukropka i zaszyfrowane
hasło dostępu.
Zawartość różnych plików .htpass jest oczywiście różna. Aby go utworzyć, należy uruchomić
niewielki program o nazwie htpasswd, dołączany do każdej dystrybucji serwera Apache.
Program htpasswd udostępnia wiele opcji, lecz najczęściej jest używany w sposób pokazany
poniżej:
htpasswd -b[c] plik_haseł identyfikator_użytkownika
Przełącznik –c informuje program o tym, że należy utworzyć nowy plik. Należy go użyć podczas
zapisywania hasła pierwszego użytkownika. Nie należy jednak stosować tego przełącznika
przy dodawaniu następnych użytkowników, gdyż spowoduje to usunięcie wcześniej utworzonego
pliku i utworzenie nowego.
Przełącznik b informuje, że hasło zostanie podane jako parametr w wywołaniu programu i nie
powinien on prosić o jego podanie. Jest to przydatne zwłaszcza wówczas, gdy htpasswd jest
wywoływany w trakcie wykonywania procesu wsadowego. W przypadku wywołania programu
z poziomu wiersza poleceń przełącznik b nie powinien być używany, gdyż wówczas hasła zostaną
zapisane w historii wykonywanych poleceń.
Plik przedstawiony na listingu 16.6 został utworzony w wyniku wykonania następujących poleceń:
htpasswd –bc /var/www/.htpass uzytkownik1 haslo1
htpasswd –bc /var/www/.htpass uzytkownik2 haslo2
htpasswd –bc /var/www/.htpass uzytkownik3 haslo3
htpasswd –bc /var/www/.htpass uzytkownik4 haslo4
Trzeba zaznaczyć, że htpasswd może nie znajdować się w bieżącej ścieżce. Jeśli tak się stanie,
konieczne będzie wskazanie pełnej ścieżki dostępu do niego. W wielu systemach plik ten znajduje
się w katalogu /usr/local/apache/bin.
Ten typ uwierzytelniania jest łatwy do wykorzystania, wiążą się z nim jednak pewne niedogodności.
przeanalizować zawartość tego pliku, a następnie odczytać dane z pliku haseł i sprawdzić, czy
informacje podane w celu uwierzytelnienia są prawidłowe. Zamiast w pliku .htaccess można by
te same dane zapisać w głównym pliku konfiguracyjnym serwera httpd.conf. Plik .htaccess jest
odczytywany za każdym razem, gdy jest wysyłane żądanie udostępnienia zastrzeżonej strony,
natomiast httpd.conf tylko raz — w trakcie uruchamiania serwera. Ta druga metoda działa więc
znacznie szybciej, jednak w razie dokonywania jakichkolwiek zmian konieczne jest zatrzyma-
nie i ponowne uruchomienie serwera WWW.
Jednak niezależnie od tego, w którym pliku zostaną umieszczone odpowiednie dyrektywy serwera,
w dalszym ciągu konieczne jest odczytywanie danych z pliku haseł przed realizacją każdego
żądania. Oznacza to, że podobnie jak w przypadku innych metod korzystających z plików jednorod-
nych także ten sposób nie jest zbyt przydatny, gdy liczbę uprawnionych użytkowników mierzy
się w setkach czy tysiącach.
W przypadku wielu prostych witryn wykorzystanie modułu mod_auth_basic wraz z plikiem haseł
jest idealnym rozwiązaniem. Jest ono szybkie, można je zaimplementować stosunkowo łatwo
i pozwala na dodawanie wpisów dla nowych użytkowników za pomocą dowolnego, wygodnego
mechanizmu. Można je stosować wraz z bazą danych, jednak w celu zapewnienia większej elastycz-
ności i kontroli dostępu do poszczególnych części stron można zaimplementować własny mecha-
nizm uwierzytelniania, działający w oparciu o PHP i MySQL.
W rozdziale 27. opiszemy włączenie tego mechanizmu do rzeczywistego projektu. Zostanie przed-
stawiony sposób jego wykorzystania do kontroli dostępu do różnych części jednej witryny.
W następnym rozdziale
Następna część książki zawiera informacje o bardziej zaawansowanych technikach wykorzysty-
wania języka PHP, takich jak używanie systemu plików, stosowanie i kontrola sesji, implementacja
lokalizacji i inne.
Część IV
Zaawansowane techniki PHP
378 Część IV Zaawansowane techniki PHP
Rozdział 17. Interakcja z systemem plików i serwerem 379
Rozdział 17.
Interakcja z systemem
plików i serwerem
Rozdział 2. zawierał informacje na temat zapisywania i odczytywania danych z plików na serwerze
WWW. W tym rozdziale znajduje się opis innych funkcji PHP, które pozwalają na interakcję z sys-
temem plików serwera WWW.
Przed dokładnym omówieniem funkcji systemu plików należy przyjrzeć się działaniu systemu
wysyłania plików.
Jak można zauważyć, formularz zawiera pole tekstowe, w którym użytkownik wpisuje nazwę pliku.
Można też nacisnąć przycisk Przeglądaj, który pozwala na przeglądanie dostępnych lokalnie
plików. Poniżej przedstawiona jest implementacja formularza.
380 Część IV Zaawansowane techniki PHP
Rysunek 17.1.
Formularz HTML
zastosowany
w przykładzie do
wysyłania plików
posiada inne pola
i typy pól niż zwykły
formularz HTML
Po wprowadzeniu nazwy pliku użytkownik może nacisnąć przycisk Wyślij, po czym plik zostanie
wysłany na serwer, gdzie „czeka” na niego skrypt PHP.
Zanim przedstawiony zostanie kod skryptu, który wysyła plik na serwer, trzeba zaznaczyć, że w pliku
php.ini występują cztery dyrektywy sterujące sposobem wysyłania plików na serwer w PHP. Dyrek-
tywy wraz z ich wartościami domyślnymi oraz krótkim opisem przedstawiono w tabeli 17.1.
Tabela 17.1. Dostępne w pliku php.ini ustawienia konfiguracyjne operacji wysyłania plików na serwer
Warto zauważyć, że powyższy formularz używa metody POST — do przesyłania plików nie można
używać metody GET.
Wartość tego pola określa maksymalną wielkość pliku, jaki użytkownicy będą mogli przesłać
na serwer. W przedstawionym przykładzie rozmiar ten określiliśmy na 1 000 000 bajtów
(w przybliżeniu jeden megabajt). W swojej własnej aplikacji można go zwiększyć lub
zmniejszyć. Jeśli wartość MAX_FILE_SIZE zostanie określona przy użyciu pola ukrytego
formularza, to przesłoni ona ustawienia konfiguracyjne, jeżeli wartość MAX_FILE_SIZE
będzie mniejsza od wartości upload_max_filesize oraz post_max_size, podanych
w pliku php.ini.
Należy wprowadzić pole tekstowe typu plikowego, co w powyższym przykładzie wygląda
następująco:
<input name="plikuzytkownika" type="file" id="plikuzytkownika" />
Plik może mieć dowolną nazwę, lecz należy pamiętać, że będzie ona stosowana w celu
uzyskania dostępu do pliku przez odbierający skrypt PHP
Kiedy plik zostaje wysłany, trafia do tymczasowego katalogu na serwerze WWW, wskazywanego
przez dyrektywę upload_tmp_dir w pliku php.ini. Zgodnie z opisem, który zawiera tabela 17.1,
jeżeli wartość dyrektywy nie jest zdefiniowana, domyślnie użyty zostanie główny tymczasowy
katalog serwera WWW, na przykład /tmp. Jeżeli nie nastąpi przeniesienie lub zmiana nazwy pliku,
nim skrypt się zakończy, plik ten zostanie skasowany w momencie zakończenia skryptu.
382 Część IV Zaawansowane techniki PHP
Zakładając, że znana jest nazwa i położenie pliku, można go teraz skopiować w odpowiednie miejsce.
Na końcu wykonywania skryptu plik tymczasowy zostanie usunięty. Tak więc jeżeli ma on zostać
zachowany, należy wcześniej przenieść go lub zmienić jego nazwę.
W tym przykładzie przesyłane będą obrazy PNG, które skrypt będzie umieszczał w nowym katalogu
o nazwie /uploads/. Co więcej, konieczne będzie utworzenie tego katalogu w głównym katalogu
serwera WWW. Jeżeli katalog ten nie będzie istniał, przesyłanie pliku na serwer nie powiedzie się.
Listing 17.2. wyslij.php — kod PHP odbierający pliki wysłane przez formularz HTML
<!DOCTYPE html>
<html>
<head>
<title>Wysyłanie...</title>
</head>
<body>
<h1>Wysyłanie pliku...</h1>
<?php
if ($_FILES['plikuzytkownika']['error'] > 0)
{
echo 'Problem: ';
switch ($_FILES['plikuzytkownika']['error'])
{
case 1:
echo 'Rozmiar pliku przekroczył wartość upload_max_filesize';
break;
case 2:
echo 'Rozmiar pliku przekroczył wartość max_file_size';
break;
case 3:
echo 'Plik przesłany tylko częściowo';
break;
case 4:
echo 'Nie wysłano żadnego pliku';
break;
case 6:
echo 'Nie można wysłać pliku: Nie wskazano katalogu tymczasowego.';
break;
Rozdział 17. Interakcja z systemem plików i serwerem 383
case 7:
echo 'Wysłanie pliku nie powiodło się: Nie zapisano pliku na dysku.';
break;
case 8:
echo 'Rozszerzenie PHP zablokowało odebranie pliku na serwerze.';
break;
}
exit;
}
if (is_uploaded_file($_FILES['plikuzytkownika']['tmp_name']))
{
if (!move_uploaded_file($_FILES['plikuzytkownika']['tmp_name'], $lokalizacja))
{
echo 'Problem: Plik nie może być skopiowany do katalogu';
exit;
}
}
else
{
echo 'Problem: możliwy atak podczas wysyłania pliku. Nazwa pliku: ';
echo $_FILES['plikuzytkownika']['name'];
exit;
}
Co interesujące, większość tego skryptu ma za zadanie sprawdzić błędy. Wysyłanie pliku pociąga
za sobą potencjalne zagrożenie bezpieczeństwa, której to sytuacji należy w miarę możliwości unikać.
Należy sprawdzić prawidłowość wysłanego pliku na wszelkie sposoby, aby być pewnym, że można
bezpiecznie wyświetlić go odwiedzającym stronę.
Poniżej omówione są poszczególne części skryptu. Najpierw sprawdzany jest kod błędu zwracany
w $_FILES['plikuzytkownika']['error']. Istnieje również stała związana z każdym z kodów. Stałe
i wartości są następujące:
UPLOAD_ERROR_OK, wartość 0 — oznacza, że nie pojawił się żaden błąd.
UPLOAD_ERR_INI_SIZE, wartość 1 — oznacza, że rozmiar wysłanego pliku przekracza
wartość maksymalną wskazywaną w pliku php.ini za pomocą dyrektywy
upload_max_filesize.
UPLOAD_ERR_FORM_SIZE, wartość 2 — oznacza, że rozmiar wysłanego pliku przekracza
wartość maksymalną określoną w kodzie formy HTML, w elemencie MAX_FILE_SIZE.
384 Część IV Zaawansowane techniki PHP
Można także sprawdzić typ MIME, by zagwarantować, że na serwer przesyłane będą wyłącznie
pliki określonego typu. W naszym przykładzie chcemy wysyłać jedynie obrazki w formacie PNG.
W tym celu weryfikujemy typ MIME, odczytując $_FILES['plikuzytkownika']['type'] i spraw-
dzając, czy ma on wartość image/png. Tak naprawdę w ten sposób unikamy błędu, gdyż nie jest to
żadne zabezpieczenie. Typ MIME jest określany przez przeglądarkę na podstawie rozszerzenia pliku
oraz informacji w samym pliku, a następnie przekazywany na serwer.
Później należy sprawdzić, czy plik, który ma zostać otwarty, w istocie został wysłany i nie jest to
plik lokalny, jak na przykład /etc/passwd. Do tego zagadnienia wrócimy jeszcze na końcu tej
części rozdziału.
Wynik jednego (udanego) uruchomienia powyższego skryptu jest przedstawiony na rysunku 17.2.
Rysunek 17.2.
Po skopiowaniu
i przeformatowaniu
wysłany plik jest
wyświetlany w celu
potwierdzenia
prawidłowości procesu
Rozdział 17. Interakcja z systemem plików i serwerem 385
Skrypt umożliwiający wysyłanie plików musi być napisany bardzo przemyślnie, ponieważ w prze-
ciwnym wypadku złośliwy użytkownik mógłby podać własną tymczasową nazwę pliku i sprawić,
że skrypt obsłuży ten plik jako wysłany. Ponieważ wiele skryptów wysyłających pliki wyświetla
użytkownikowi wysłane dane lub przechowuje je w miejscu, z którego mogą one być skopiowane,
istnieje niebezpieczeństwo umożliwienia użytkownikom dostępu do każdego pliku na serwerze
WWW. Mogłyby to być między innymi tak ważne pliki, jak /etc/passwd lub kod źródłowy PHP
zawierający hasła do bazy danych.
Przykład przedstawiony na listingach 17.1 oraz 17.2 obsługuje jedynie przesyłanie jednego pliku,
ale w prosty sposób można zmodyfikować formularz i skrypt w taki sposób, by obsługiwały
przesyłanie wielu plików. W takim przypadku zamiast jednego pola typu file należałoby
umieścić na formularzu więcej pól, jak również zmienić ich nazwę na tablicę, w której
będą przechowywane informacje o poszczególnych przesłanych plikach. Oto przykład:
<input type="file" name="plikiuzytkownika[]" id="plikiuzytkownika"/>
W skrypcie do poszczególnych elementów tej tablicy należy się odwoływać
przy użyciu ich indeksów, na przykład: $_FILES['plikiuzytkownika']['name'][0],
$_FILES['plikiuzytkownika']['name'][1] itd.
Tabela 17.2. Ustawienia konfiguracyjne mechanizmu sesyjnych postępów przesyłania w pliku php.ini
Tabela 17.2. Ustawienia konfiguracyjne mechanizmu sesyjnych postępów przesyłania w pliku php.ini
Powyższe dane informują, że żądanie POST zostało przesłane o godzinie określonej znacznikiem
czasu o wartości 1424047703 (czyli w poniedziałek 16 lutego 2015 roku o godzinie 0:48 i 23 sekundy
czasu uniwersalnego — GMT) i zawierało dane o wielkości 43 837 bajtów. Całkowita liczba
przesłanych bajtów wyniosła 43837, a wartością pola done jest 1, co oznacza, że obsługa żądania
została zakończona. Gdyby wartość tego ostatniego pola wyniosła 0, oznaczałoby to, że żądanie
POST jeszcze nie zostało zakończone.
Wartości pól content_length oraz bytes_processed mają znaczenie w przypadku tworzenia i pre-
zentowania paska postępów przesyłania po stronie klienta, który to pasek będzie pozwalał użytkow-
nikowi na śledzenie przebiegu operacji. Ponieważ informacje o postępach przesyłania są zapisywane
Rozdział 17. Interakcja z systemem plików i serwerem 387
w sesji użytkownika, po rozpoczęciu sesji i operacji przesyłania pliku w jednym skrypcie (obsłu-
gującym przesłanie) można je odczytywać w innym — takim, który będzie odczytywał wartości
pól content_length oraz bytes_processed z sesji dla konkretnego pliku lub plików. Gdyby taki
skrypt był cyklicznie i asynchronicznie wywoływany, na przykład przy użyciu żądań AJAX, to
wystarczyłoby zastosować proste matematyczne obliczenia, by określić w procentach, jaka
część pliku została przesłana (w tym celu wystarczyłoby podzielić liczbę przesłanych bajtów
przez całkowitą wielkość pliku i wynik pomnożyć przez 100).
Odczyt z katalogów
W pierwszej kolejności zostanie zaimplementowany skrypt pozwalający na przeglądanie ka-
talogu wysłanych plików. Przeglądanie katalogów w PHP nie powinno nastręczać problemów.
Na listingu 17.3 przedstawiono prosty skrypt wykonujący to zadanie.
<?php
$obecny_kat = '/sciezka/do/wyslane/';
$kat = opendir($obecny_kat);
</body>
</html>
Funkcja opendir() jest stosowana do otwierania katalogu do odczytu. Jej użycie jest podobne do
zastosowania funkcji fopen() do odczytywania z plików. Zamiast przekazywać funkcji nazwę
pliku, wysyła się jej nazwę katalogu:
$kat = opendir($obecny_kat);
Rozdział 17. Interakcja z systemem plików i serwerem 389
Funkcja zwraca uchwyt katalogu w sposób bardzo podobny do zwracania przez funkcję fopen()
uchwytu pliku.
Kiedy katalog zostaje otwarty, można odczytać z niego nazwę pliku poprzez wywołanie
readdir($kat), jak przedstawiono w przykładzie. Funkcja ta zwraca false, kiedy wszystkie pliki
zostaną już odczytane. Należy zapamiętać, że funkcja ta zwróci false również wtedy, gdy odczyta
plik o nazwie „0”. Aby uchronić się przed takim przypadkiem, w kodzie jawnie sprawdzany jest
wynik odczytu katalogu, co pozwala zapewnić, że zwracana wartość nie jest równa false:
while (false !== ($plik = readdir($kat)))
Kiedy odczytywanie danych z katalogu zostanie zakończone, należy wywołać closedir ($kat),
aby je zakończyć. Funkcja ta jest podobna do funkcji fclose() zamykającej plik.
Przykładowy wynik działania skryptu przeglądającego katalogi jest przedstawiony na rysunku 17.3.
Rysunek 17.3.
Listing zawartości
katalogu wyświetla
wszystkie pliki
wybranego katalogu
Na liście widocznej na rysunku 17.3 zwykle wyświetlane są także katalogi . (katalog bieżący) oraz
.. (katalog nadrzędny). W przykładowym kodzie obydwa katalogi zostały jednak pominięte, za
co odpowiada następujący wiersz:
if ($plik != "." && $plik != "..")
Jeżeli wiersz ten zostanie z kodu usunięty, wówczas na prezentowanej liście katalogów pojawią się
również katalogi . i ...
Jeżeli przeglądanie katalogów ma być realizowane poprzez ten mechanizm, rozsądne jest ograni-
czenie katalogów udostępnionych do przeglądu. Chodzi o to, by użytkownik nie mógł przeglądać
katalogów w obszarach, do których zwykle nie ma dostępu.
Powiązaną i często przydatną funkcją jest rewinddir($kat), która powraca do odczytywania nazw
plików od początku katalogu.
Alternatywą dla tych funkcji jest klasa dir dostarczana przez PHP. Posiada ona własności handle
(uchwyt) i path (ścieżka) oraz metody read(), close() i rewind(), które zachowują się identycznie
jak ich nieklasowe odpowiedniki.
Na listingu 17.4 znajduje się podobny przykład, w którym użyto klasy dir.
390 Część IV Zaawansowane techniki PHP
Listing 17.4. przegkat2.php — wyświetlenie zawartości katalogu wysłanych plików przy użyciu klasy dir
<!DOCTYPE html>
<html>
<head>
<title>Przeglądanie katalogów</title>
</head>
<body>
<h1>Przeglądanie</h1>
<?php
$kat = dir("/sciezka/do/wyslane/");
echo '</ul>';
$kat->close();
?>
</body>
</html>
Pliki wyświetlane przez przykładowy skrypt nie są w żaden sposób porządkowane, dlatego aby
uzyskać posortowaną listę, można użyć funkcji o nazwie scandir(). Można z niej skorzystać
w celu umieszczenia nazw plików w tablicy i posortowania nazw w porządku alfabetycznym
— rosnąco lub malejąco, jak na listingu 17.5.
Listing 17.5. scandir.php — wykorzystanie funkcji scandir() do alfabetycznego uporządkowania nazw plików
<!DOCTYPE html>
<html>
<head>
<title>Przeglądanie katalogów</title>
</head>
<body>
<h1>Przeglądanie</h1>
<?php
$kat = '/sciezka/do/wyslane/';
$pliki1 = scandir($kat);
$pliki2 = scandir($kat, 1);
foreach($pliki1 as $plik)
{
if($plik != "." && $plik != "..")
{
echo '<li>'.$plik.'</li>';
}
}
Rozdział 17. Interakcja z systemem plików i serwerem 391
echo '</ul>';
foreach($pliki2 as $plik)
{
if($plik != "." && $plik != "..")
{
echo '<li>'.$plik.'</li>';
}
}
echo '</ul>';
?>
</body>
</html>
Otrzymywanie informacji
na temat aktualnego katalogu
Pewne dodatkowe informacje na temat systemu plików można uzyskać po określeniu ścieżki
dostępu do pliku. Na przykład funkcje dirname($sciezka) i basename($sciezka) zwracają części
ścieżki, odpowiednio: katalogową i plikową. Może to być użyteczne dla skryptu przeglądającego
katalogi, szczególnie jeżeli istnieje skomplikowana struktura katalogów o zawartości opartej na
znaczących nazwach katalogów i plików.
Do listingu zawartości katalogu można dodać również wskazanie ilości wolnego miejsca na wysłane
pliki, stosując funkcję disk_free_space($sciezka). Po przekazaniu tej funkcji ścieżki do katalogu
zwróci ona liczbę wolnych bajtów na dysku (Windows) lub w systemie plików (UNIX), w którym
znajduje się katalog.
Stosowanie funkcji mkdir() jest bardziej skomplikowane, niż mogłoby się wydawać. Pobiera ona
dwa parametry, ścieżkę pożądanego katalogu (włączając w to nazwę nowego katalogu) oraz upraw-
nienia, które ten katalog powinien posiadać. Oto odpowiedni przykład:
mkdir("/tmp/test", 0777);
Możliwe jest wyzerowanie parametru umask przed stworzeniem katalogu, aby uniknąć jego dzia-
łania. Odbywa się to poprzez wpisanie:
$staryumask = umask(0);
mkdir("/tmp/test", 0777);
umask($staryumask);
392 Część IV Zaawansowane techniki PHP
Powyższy kod stosuje funkcję umask(), które może być użyta do sprawdzania i zmiany aktualnej
wartości umask. Zmieni ona wartość umask na dowolną wartość jej przekazaną i zwróci poprzednią
wartość umask. Jeżeli zaś zostanie wywołana bez parametrów, zwróci aktualną wartość umask.
Należy zaznaczyć, że funkcja umask() nie działa w systemie Windows.
Funkcja rmdir() usuwa katalog następująco:
rmdir("/tmp/test");
lub
rmdir("c:\\tmp\\test");
Teraz można utworzyć kolejny skrypt, własciwoscipliku.php, który będzie wyświetlał dodatkowe
informacje dotyczące tego pliku; jego kod został przedstawiony na listingu 17.6.
if (!isset($_GET['plik']))
{
echo "Nie podano nazwy pliku.";
}
else {
$aktualny_kat='/sciezka/do/wyslane/';
$plik=basename($_GET['plik']); //usunięcie dla bezpieczeństwa informacji na temat katalogu
$bezpieczny_plik = $aktualny_kat.$plik;
$uzytkownik=posix_getpwuid(fileowner($bezpieczny_plik));
echo 'Właściciel pliku: '.$uzytkownik['name'].'<br>';
$grupa=posix_getgrgid(filegroup($bezpieczny_plik));
echo 'Grupa pliku: '.$grupa['name'].'<br>';
Przed zaprezentowaniem tego skryptu Czytelnikom należy się ważna informacja: niektóre użyte
w nim funkcje, między innymi posix_getpwuid(), fileowner() i filegroup(), nie działają w systemie
Windows bądź działają w nim nieprawidłowo.
Wynik przykładowego uruchomienia skryptu z listingu 17.6 jest przedstawiony na rysunku 17.4.
Rysunek 17.4.
Właściwości pliku
pokazują informacje
systemu plików
na temat tego pliku.
Należy zauważyć,
że uprawnienia
są wyświetlane
w formacie ósemkowym
394 Część IV Zaawansowane techniki PHP
Poniżej zostały omówione funkcje użyte w listingu 17.6. Jak wspomniano, funkcja basename()
pobiera nazwę pliku bez katalogu. (Można również zastosować funkcję dirname() wyświetlającą
nazwę katalogu bez nazwy pliku).
Funkcje fileatime() i filemtime() zwracają znacznik czasu, wskazujący odpowiednio czas ostatnie-
go otworzenia i modyfikacji pliku. Znaczniki czasu zostały przeformatowane za pomocą funkcji
date(), tak aby były łatwiejsze do odczytania. Funkcje te zwrócą identyczne wartości w niektórych
systemach operacyjnych (jak w przykładzie), zależnie od informacji w nich przechowywanych.
Funkcje fileowner() i filegroup() zwracają identyfikator użytkownika (uid) i grupy (gid) tego
pliku. Identyfikatory te mogą zostać przekonwertowane na nazwy za pomocą odpowiednich
funkcji posix_getpwuid() i posix_getgrgid(), które pozwalają na ich łatwiejsze odczytanie. Funk-
cje te pobierają jako parametr uid lub gid i zwracają tablicę asocjacyjną zawierającą informacje
o użytkowniku lub grupie, włączając w to nazwę użytkownika lub grupy (zobacz listing 17.4).
Funkcja fileperms() pokazuje uprawnienia pliku. W powyższym przykładzie zostały one przekon-
wertowane na format ósemkowy za pomocą funkcji decoct(), tak aby użytkownikom Uniksa wydały
się bardziej znajome.
Funkcja filetype() zwraca informację na temat typu badanego pliku. Możliwe wyniki to fifo,
char, dir, block, link, file lub unknown.
Można również zastosować funkcję stat(), która zbiera bardzo podobne dane. Kiedy zostanie jej
przekazany plik, zwraca ona tablicę zawierającą dane podobne do zwracanych przez powyższe funk-
cje. Funkcja lstat() działa w podobny sposób, z tym że stosuje się ją do dowiązań symbolicznych.
Wszystkie funkcje sprawdzające stan pliku potrzebują na wykonanie tej operacji sporo czasu,
dlatego też ich wyniki są przechowywane w pamięci podręcznej. Aby sprawdzić informacje o pliku,
który został tymczasem zmodyfikowany, należy wywołać funkcję:
clearstatcache();
która usuwa poprzednie wyniki. Jeżeli powyższy skrypt powinien być stosowany przed modyfikacją
danego pliku i po niej, należy na jego początku umieścić wywołanie funkcji clearstatcache(), aby
zapewnić aktualność otrzymywanych informacji.
Funkcja chgrp() jest wykorzystywana do zmiany grupy pliku. Może być ona stosowana jedynie
do zmiany grupy pliku na grupę, której członkiem jest aktualny użytkownik, chyba że jest on użyt-
kownikiem root.
Rozdział 17. Interakcja z systemem plików i serwerem 395
Funkcja chmod() zmienia uprawnienia pliku. Przekazywane uprawnienia powinny posiadać stan-
dardową uniksową formę chmod. Na ich początku należy umieścić 0, aby wskazać, że posiadają one
format ósemkowy. Na przykład:
chmod('jakisplik.txt', 0777);
Funkcja chown() jest wykorzystywana do zmiany właściciela pliku. Może ona być stosowana jedynie
wtedy, gdy skrypt posiada uprawnienia użytkownika root, co nie powinno się nigdy zdarzyć,
chyba że skrypt jest uruchamiany z poziomu wiersza poleceń w celu wykonania jakichś zadań
administracyjnych.
Pierwszą i najprostszą czynnością jest stworzenie lub zmiana czasu ostatniej modyfikacji pliku za
pomocą funkcji touch(). Działa ona podobnie do uniksowego polecenia touch. Funkcja ta posiada
następujący prototyp:
bool touch(string plik, [int czas [, int czas_dostepu]])
Jeżeli plik już istnieje, czas ostatniej modyfikacji zostanie zmieniony na aktualny lub podany
w drugim, opcjonalnym parametrze. Drugi parametr powinien być podany w formacie znacznika
czasu. Jeżeli plik nie istnieje, zostanie utworzony. Zmieni się również czas dostępu do pliku:
domyślnie na aktualny czas w systemie lub na znacznik czasu wskazany w opcjonalnym parametrze
czas_dostepu.
Pliki mogą być usuwane za pomocą funkcji unlink() (należy zauważyć, że funkcja ta nie nazywa
się delete() — taka bowiem nie istnieje). Stosuje się ją w następujący sposób:
unlink($nazwapliku);
Pliki mogą być kopiowane i przenoszone za pomocą funkcji copy() i rename() w następujący sposób:
copy($sciezka_zrodlowa, $sciezka_przeznaczenia);
rename($staryplik, $nowyplik);
Funkcja rename() odgrywa podwójną rolę jako funkcja przenosząca pliki z miejsca na miejsce,
ponieważ PHP nie posiada typowej funkcji przenoszącej, oprócz funkcji move_uploaded_file(),
która jednak ma ściśle określone zastosowanie. To, czy pliki mogą być przenoszone z jednego
systemu plików do drugiego i czy są nadpisywane przy korzystaniu z rename(), zależy od systemu
operacyjnego, tak więc należy sprawdzić efekty na swoim serwerze. Proszę również uważać na
ścieżkę stosowaną w nazwie pliku. Jeżeli jest to ścieżka względna, będzie ona odnoszona do pozycji
skryptu, a nie oryginalnego pliku.
Funkcji tej przekazuje się polecenie, które powinno zostać wykonane. Na przykład:
exec("ls –la");
Funkcja exec() nie zwraca bezpośredniego wyniku, lecz ostatni wiersz wyniku wykonania
polecenia.
Jeżeli funkcji zostanie przekazana zmienna jako wynik, zwróci ona tablicę łańcuchów znaków
przedstawiającą każdy wiersz wyniku wykonania. Jeżeli przekaże się jej zmienną jako
zwroc_wartosc, wynikiem będzie zwracana wartość.
passthru() — Funkcja passthru() posiada następujący prototyp:
void passthru(string polecenie [, int zwroc_wartosc])
Funkcja ta wyświetla wynik polecenia w przeglądarce. „Stara się” ona ukazać wynik
po każdym wierszu (zakładając, że PHP działa jako moduł serwera), co odróżnia ją
od funkcji passthru(). Zwraca ona ostatni wiersz wyniku (jeżeli uruchomienie powiedzie się)
lub false (w razie niepowodzenia).
Parametry działają w ten sam sposób co w innych funkcjach przedstawionych powyżej.
Odwrócone apostrofy (``) — Symbole te zostały opisane w skrócie w rozdziale 1.
Jest to właściwie operator wywołania.
Nie wyświetla on bezpośredniego wyniku. Wynik wykonania polecenia jest zwracany
jako łańcuch znaków, który może zostać wyświetlony lub mogą być na nim wykonane
dowolne operacje.
W przypadku bardziej złożonych potrzeb można również zastosować funkcje popen(), proc_open()
oraz proc_close(), służące do rozwidlania zewnętrznych procesów i przesyłania danych między
nimi.
Analizując skrypt przedstawiony na listingu 17.7, warto prześledzić zastosowanie każdej z po-
wyższych technik.
chdir('/sciezka/do/wyslane/');
// wersja exec
echo '<h1>Stosowanie funkcji exec()</h1>';
echo '<pre>';
// unix
exec('ls -la', $wynik);
// windows
// exec('dir', $wynik);
{
echo $wiersz.PHP_EOL;
}
echo '</pre>';
echo '<hr />';
// wersja passthru
echo '<h1>Stosowanie funkcji passthru()</h1>';
echo '<pre>';
// unix
passthru('ls -la');
// windows
// passthru('dir');
echo '</pre>';
echo '<hr />';
// wersja system
echo '<h1>Stosowanie funkcji system()</h1>';
echo '<pre>';
//unix
$wynik=system('ls -la');
// windows
// $wynik=system('dir');
echo '</pre>';
echo '<hr />';
// unix
$wynik=`ls -la`;
// windows
// $wynik = `dir`;
echo $wynik;
echo '</pre>';
?>
Jedna z tych technik mogła zostać zastosowana jako alternatywa dla opisanego wcześniej skryptu
przeglądającego katalogi. Zwróćmy uwagę, że jeden z ubocznych efektów stosowania funkcji
zewnętrznych jest uwidoczniony w powyższym przykładzie — nie jest on już przenośny. Zastoso-
wano w nim polecenia systemu Unix, tak więc polecenia systemu Windows (umieszczone w kodzie
w komentarzach) mogą nie dawać zamierzonych efektów.
Jeżeli częścią polecenia, które ma zostać wywołane, są dane przekazane przez użytkownika, należy
zawsze uruchamiać je poprzez funkcję escapeshellcmd(). Uniemożliwia to użytkownikom złośliwe
(i nie tylko) wykonywanie poleceń w systemie. Funkcję tę stosuje się w następujący sposób:
system(escapeshellcmd($polecenie));
Należy również stosować funkcję escapeshellarg(), aby wykonać operację ucieczki na argumen-
tach, które zostaną przekazane do poleceń powłoki.
398 Część IV Zaawansowane techniki PHP
Interakcja ze środowiskiem:
funkcje getenv() i putenv()
Koniec tego rozdziału jest poświęcony sposobom zastosowania zmiennych środowiskowych w PHP.
Służą do tego celu dwie funkcje — getenv(), która umożliwia odczytanie zmiennych środowisko-
wych, oraz putenv(), pozwalająca na ich ustawianie. Należy zauważyć, że omawiane jest środo-
wisko, w którym PHP działa na serwerze.
Jeśli jesteś administratorem systemu i chciałbyś z góry określić, które zmienne środowiskowe mogą
być ustawiane przez programistę, możesz wykorzystać dyrektywę safe_mode_allowed_env_vars
w pliku php.ini. Po uruchomieniu PHP w bezpiecznym trybie użytkownicy będą mogli ustawiać tylko
te zmienne środowiskowe, których przedrostki są wymienione w tej dyrektywie.
W następnym rozdziale
W rozdziale 18. zastosujemy funkcje sieci i protokołu PHP w celu interakcji z systemami innym
niż własny serwer WWW. Jest to kolejne rozszerzenie możliwości działania skryptów.
Rozdział 18.
Stosowanie funkcji sieci
i protokołu
W tym rozdziale opiszemy funkcje PHP związane z obsługą i wykorzystaniem sieci, które pozwalają
na interakcję skryptów z pozostałą częścią internetu. Znajduje się tam wiele zasobów, a także liczne
różnorodne protokoły, umożliwiające dostęp do nich. W tym rozdziale zostaną poruszone nastę-
pujące zagadnienia:
przegląd dostępnych protokołów,
wysyłanie i odczytywanie poczty elektronicznej,
korzystanie z danych pochodzących z innych wtitryn WWW,
stosowanie funkcji połączeń sieciowych,
korzystanie z FTP.
Przegląd protokołów
Protokoły to zasady komunikacji w danej sytuacji. Na przykład znany jest protokół obowiązujący
podczas spotkania z inną osobą — przywitanie się, uściśnięcie dłoni, chwila rozmowy, po czym
pożegnanie. W różnych sytuacjach wymagane są różne protokoły. Poza tym osoby wywodzące się
z różnych kręgów kulturowych mogą oczekiwać odmiennych protokołów, co może utrudnić inter-
akcję. Protokoły komputerowe działają na podobnych zasadach.
Protokoły i inne standardy internetowe są opisane w dokumentach zwanych RFC, czyli Prośby
o Komentarz (ang. Requests for Comments). Protokoły są definiowane przez grupę IETF (ang. Internet
Engineering Task Force — Specjalna Grupa Inżynierii Internetu). Dokumenty RFC są powszechnie
dostępne w internecie. Podstawowym źródłem jest Wydawca RFC, znajdujący się pod adresem
http://www.rfc-editor.org/.
400 Część IV Zaawansowane techniki PHP
W razie problemów z pracą danego protokołu definiujący go dokument RFC stanowi godne zaufania
źródło, często wykorzystywane podczas usuwania błędów. Takie dokumenty są bardzo szczegółowe
i nierzadko liczą po kilkaset stron.
Do dobrze znanych dokumentów RFC należą: RFC2616, opisujący protokół HTTP/1.1, oraz
RFC822, opisujący format internetowych wiadomości poczty elektronicznej.
W tym rozdziale opiszemy pewne aspekty PHP korzystające z tych protokołów. Wyrażając się ściślej,
zostanie przedstawione wysyłanie poczty przez SMTP, odczytywanie poczty przez POP i IMAP4,
łączenie się z innymi serwerami WWW przez HTTP i HTTPS oraz przesyłanie plików przez FTP.
Aby zwiększyć różnorodność zastosowań funkcji mail(), można zastosować różne powszechnie
dostępne klasy. SMTP służy jedynie do wysyłania wiadomości. Protokoły IMAP4 (ang. Internet
Message Access Protocol — protokół dostępu do wiadomości internetowych, opisany w RFC2060)
oraz POP (ang. Post Office Protocol — protokół pocztowy, opisany w RFC1939 lub w STD0053)
są wykorzystywane do odczytywania wiadomości z serwera. Nie mogą one wysyłać poczty.
PHP jest dostarczany z zewnętrzną biblioteką, której można używać do obsługi IMAP4; jej doku-
mentacja jest dostępna na stronie http://php.net/manual/en/book.imap.php.
Wyobraźmy sobie, że pewna firma chciałaby wyświetlać na swojej stronie głównej aktualny kurs
własnych akcji. Informacja ta jest dostępna gdzieś na pewnej stronie giełdowej — lecz jak do niej
dotrzeć?
Należy rozpocząć od znalezienia oryginalnego URL-a tej informacji. Kiedy jest on już znany, za
każdym otwarciem strony firmy można połączyć się z tym URL-em, odczytać stronę i uzyskać
potrzebne informacje.
Poniżej umieszczony jest skrypt, który odczytuje i przeformatowuje kurs akcji z witryny Yahoo!
Finance; na stronie każdego papieru wartościowego umieszcza ona odnośnik „Download Data”1,
1
Pobierz dane — przyp. tłum.
Rozdział 18. Stosowanie funkcji sieci i protokołu 401
tworzony konsekwentnie według tego samego wzorca. Odczytano na przykład aktualną cenę akcji
firmy Google. (Informacje odczytywane z innych stron mogą mieć dowolny charakter, ale zasady
pozostają te same).
Przykładowy skrypt pobiera dane pochodzące z innej witryny, a następnie wyświetla je na gene-
rowanej stronie WWW. Skrypt ten jest przedstawiony na listingu 18.1.
Listing 18.1. polaczenie.php — skrypt odczytujący kurs akcji o symbolu podanym w zmiennej $symbol
z NASDAQ
<!DOCTYPE html>
<html>
<head>
<title>Kurs akcji z NASDAQ</title>
</head>
<body>
<?php
//wybór symbolu akcji
$symbol='GOOG';
echo '<h1>Kurs akcji o symbolu '.$symbol.'</h1>';
$url = 'http://download.finance.yahoo.com/d/quotes.csv' .
'?s='.$symbol.'&e=.csv&f=sl1d1t1c1ohgv';
if(!($zawartosc = file_get_contents($url))) {
die('Otwarcie URL '.$url.' niemożliwe');
}
//powiadomienie o źródle
echo '<p>Ta informacja została uzyskana z <br />'
.'<a href="'.$url.'">'.$url.'</a></p>';
?>
</body>
</html>
Sam skrypt jest bardzo prosty — nie zostały w nim zastosowane żadne nieopisane poprzednio
funkcje, są tylko nowe ich zastosowania.
zwraca całą zawartość pliku dostępnego pod adresem URL i zapisuje go w zmiennej $zawartosc.
402 Część IV Zaawansowane techniki PHP
Rysunek 18.1.
Skrypt stosuje wyrażenia
regularne do wyciągania
kursu akcji z informacji
odczytanych z giełdy
papierów wartościowych
Funkcje plikowe PHP posiadają wiele możliwości. Przedstawiony przykład wczytuje zawartość
pliku pobranego przy użyciu protokołu HTTP, lecz w dokładnie taki sam sposób można by wcho-
dzić w interakcję z innymi serwerami, również przez HTTPS, FTP czy inne protokoły. W przypad-
ku niektórych zadań może jednak zajść konieczność zastosowania nieco bardziej specjalizowane-
go podejścia. Na przykład pewne możliwości protokołu FTP są dostępne tylko za pośrednictwem
konkretnych funkcji FTP i nie można z nich skorzystać przy użyciu fopen() ani innych funkcji
plikowych. Co więcej, w przypadku niektórych zadań realizowanych za pośrednictwem protoko-
łów HTTP i HTTPS konieczne może okazać się użycie biblioteki cURL. Dzięki niej można zalogo-
wać się na stronę WWW i symulować przechodzenie do kolejnych stron przez użytkownika.
Wróćmy jednak do skryptu: po uzyskaniu tekstu strony dzięki funkcji file_get_contents(), moż-
na zastosować funkcję list() w celu odnalezienia interesującej części strony:
list($symbol, $kurs, $data, $godzina) = explode(',', $zawartosc);
$data = trim($data, '"');
$godzina = trim($godzina, '"');
Kiedy wartości zostaną już zapisane w zmiennych przedstawionych powyżej, wystarczy je wyświe-
tlić na stronie:
echo '<p>Ostatni kurs akcji '.$symbol.': '.$kurs.'</p>';
echo '<p>Kurs na dzień '.$data.', z godziny '.$godzina.'</p>';
I oto jest!
Ujęcie to może być stosowane do wielu różnych celów. Innym dobrym przykładem jest odczytanie
lokalnej prognozy pogody i osadzenie jej na stronie.
Najlepszym zastosowaniem tego podejścia jest połączenie informacji z różnych źródeł w celu
otrzymania określonych rezultatów. Za doskonały przykład zastosowania tej techniki moż-
na uznać osławiony skrypt Philipa Greenspuna tworzący Miernik Bogactwa Billa Gatesa:
http://philip.greenspun.com/WealthClock.
Rozdział 18. Stosowanie funkcji sieci i protokołu 403
Strona ta pobiera informacje z dwóch źródeł. Odczytuje aktualną liczbę ludności Stanów Zjedno-
czonych ze strony Biura Spisu Ludności USA, później wyszukuje aktualną wartość akcji Microsoftu,
po czym łączy te dwie informacje, dodaje sporą dawkę opinii autora i tworzy nową informację
— oszacowanie aktualnego majątku Billa Gatesa.
Przy tworzeniu takiego skryptu niekiedy konieczne okazuje się stworzenie możliwości przekazy-
wania informacji. Na przykład przy łączeniu się z zewnętrznym URL-em można przekazać pewne
parametry, które w zwykłej sytuacji zostałyby podane przez użytkownika. Powinno się wtedy zasto-
sować funkcję urlencode(), która pobiera łańcuch znaków i dokonuje jego konwersji do formatu
właściwego URL-om, na przykład zamieniając spacje w znaki plus. Wywołuje się ją w następują-
cy sposób:
$zakodowanyparametr=urlencode($parametr);
Problemem w takim ogólnym podejściu jest to, że na witrynie, z której pobierane są informacje,
może zmienić się format danych, co doprowadzi do tego, że skrypt nie będzie działał. Jak już
wcześniej wspomniano, zawsze należy zwracać uwagę na źródła danych, od których będą zależeć
pisane skrypty.
Listing 18.2 przedstawia kod HTML służący do tworzenia formularza zgłoszenia do takiego katalogu.
Jest to bardzo prosty formularz — jego renderowana wersja z wprowadzonymi danymi przykła-
dowymi jest zamieszczona na rysunku 18.2.
404 Część IV Zaawansowane techniki PHP
Rysunek 18.2.
Zgłoszenia katalogowe
zazwyczaj wymagają
podania URL-a
i szczegółów
dotyczących
możliwości kontaktu,
aby administratorzy
katalogu mogli
powiadomić właściciela
strony o dodaniu jej
do katalogu
Po naciśnięciu przycisku zgłoszenia należy sprawdzić, czy, po pierwsze, zgłoszony URL znajduje
się na prawdziwym komputerze, a po drugie, czy istnieje także komputer macierzysty adresu
pocztowego. Wynik przykładowego skryptu wykonującego te działania jest przedstawiony na
rysunku 18.3.
Rysunek 18.3.
Ta wersja skryptu
ukazuje wyniki
sprawdzenia nazwy
komputera URL-a
i adresu pocztowego
— wersja ostateczna
nie musi tego robić,
interesujące jest jednak
wyświetlenie wyników
sprawdzania
Skrypt wykonujący działania sprawdzające korzysta z dwóch funkcji sieci PHP — gethostbyname()
i getmxrr(). Pełny skrypt jest przedstawiony na listingu 18.3.
<?php
Rozdział 18. Stosowanie funkcji sieci i protokołu 405
// Wyodrębnienie z pól
$url = $_REQUEST['url'];
$email = $_REQUEST['email'];
// Sprawdzenie URL-a
$url=parse_url($url);
$komp=$url['host'];
if(!($ip=gethostbyname($komp)))
{
echo 'Komputer macierzysty URL-a nie posiada prawidłowego adresu IP';
exit;
}
if(!getmxrr($kompemail, $kompmx))
{
echo 'Adres pocztowy nie posiada prawidłowego komputera macierzystego';
exit;
}
foreach($kompmx as $mx)
{
echo '<li>'.$mx.'</li>';
}
echo '</ul>';
Przede wszystkim z superglobanej tablicy $_POST zostaje pobrany URL, po czym wykonana jest na
nim funkcja parse_url(). Zwraca ona tablicę asocjacyjną zawierającą poszczególne części URL-a.
Dostępne części danych to: scheme (schemat), user (użytkownik), pass (hasło), host (komputer ma-
cierzysty), port, path (ścieżka), query (zapytanie) i fragment. Zazwyczaj korzysta się tylko z nie-
których z nich, lecz poniżej przedstawiono przykład tworzenia przez nie URL-a.
pass: sekret
host: przyklad.com
port: 80
path: /skrypt.php
query: zmienna=wartosc
fragment: kotwica
W skrypcie zgloszenie_katalog.php potrzebna jest jedynie informacja host, która zostaje wycią-
gnięta z tablicy w następujący sposób:
$url=parse_url($url);
$komp=$url['host'];
Po wykonaniu tej czynności można uzyskać adres IP tego komputera macierzystego, jeżeli jego
nazwa znajduje się w DNS-ie. Służy do tego funkcja gethostbyname(), która zwraca adres IP,
jeżeli on występuje, a w przeciwnym wypadku wyświetla false:
$ip=gethostbyname($komp)
Można również wykonać działanie odwrotne poprzez zastosowanie funkcji gethostbyaddr(), która
pobiera jako parametr adres IP i zwraca nazwę komputera. Jeżeli obie te funkcje zostaną wywoła-
ne jedna po drugiej, wynikiem może być inna nazwa komputera niż początkowa. Oznacza to, że
witryna stosuje usługę wirtualnego komputera macierzystego, w przypadku której jeden fizyczny
komputer i adres IP posiadają więcej niż jedną nazwę domenową.
Jeżeli URL jest prawidłowy, można sprawdzić adres poczty elektronicznej. Na początku należy go
rozbić na nazwę użytkownika i nazwę komputera poprzez wywołanie funkcji explode():
$email=explode('@', $email);
$kompemail=$email[1];
Gdy posiadamy część adresu zawierającą nazwę komputera, możemy sprawdzić, czy wiadomości
wysłane na ten adres dotrą na miejsce. Stosuje się w tym celu funkcję getmxrr():
getmxrr($kompemail, $kompmx)
Funkcja ta dla danego adresu zapisuje zbiór rekordów MX (ang. Mail Exchange — wymiana poczty)
do tablicy dostarczonej przez argument $kompmx.
Rekord MX jest przechowywany DNS-ie i łączy się z nim podobnie jak z nazwą komputera.
Komputer wymieniony w rekordzie MX jest niekoniecznie tym, do którego ostatecznie trafi wiado-
mość — „wie” on natomiast, gdzie ma ją skierować. (Może być ich więcej niż jeden, tak więc funkcja
getmxrr() zwraca tablicę nazw komputerów, a nie pojedynczy łańcuch znaków). Jeżeli w DNS-ie
nie istnieje rekord MX, wiadomość nie zostanie wysłana nigdzie.
Jeśli wszystkie te testy zostaną pomyślnie wykonane, to dane z formularza będzie można zapisać
w bazie w celu późniejszego przejrzenia ich przez pracownika. Ta operacja nie jest wykonywana
w skrypcie, lecz na podstawie komentarzy umieszczonych na samym końcu skryptu można się
zorientować, gdzie informacje te należałoby umieścić.
Oprócz przedstawionych wcześniej funkcji można także wykorzystać funkcję o bardziej ogólnym
charakterze, checkdnserr(); pobiera ona nazwę komputera i zwraca wartość true, jeśli w systemie
DNS jest dostępny jakikolwiek wpis, który dotyczy tego komputera. Wyniki generowane przez tę
funkcję nie udostępniają jednak żadnych informacji, które można by zaprezentować użytkownikowi,
jak to było w przypadku funkcji getmxrr(); z powodzeniem można by ich natomiast użyć do
szybkiego sprawdzenia poprawności adresu.
Rozdział 18. Stosowanie funkcji sieci i protokołu 407
Funkcje te nie są domyślnie wbudowane w standardową instalację. Aby używać ich w systemie
Unix, należy włączyć program PHP configure z opcją --enable-ftp, po czym uruchomić ponow-
nie make. Jeśli wykorzystywana jest standardowa instalacja dla Windows, funkcje FTP będą udo-
stępnione automatycznie (więcej informacji na temat konfiguracji PHP znajduje się w dodatku A).
Listing 18.4. ftplustro.php — skrypt pobierający nowe wersje pliku z serwera FTP2
<!DOCTYPE html>
<html>
<head>
<title>Uaktualnienie kopii lustrzanej</title>
</head>
<body>
<h1>Uaktualnienie kopii lustrzanej</h1>
<?php
// ustawienie zmiennych - należy je zmienić, aby pasowały do aplikacji
$komp = 'apache.cs.utah.edu';
$uzytkownik = 'anonymous';
$haslo = 'ja@przyklad.com';
$plikzdalny = '/apache.org/httpd/httpd-2.4.16.tar.gz';
$pliklokalny = '/sciezka/do/plikow/httpd-2.4.16.tar.gz';
// łączenie z komputerem
$lacz=ftp_connect($komp);
if(!$lacz)
{
echo 'Błąd: Nie można nawiązać połączenia z komputerem '. $komp;
exit;
}
// logowanie do komputera
$wynik = @ftp_login($lacz, $uzytkownik, $haslo);
2
Plik dostępny pod adresem: ftp://ftp.helion.pl/przyklady/phmsv5.zip — przyp. red.
408 Część IV Zaawansowane techniki PHP
if (!$wynik)
{
echo 'Błąd: Nie można zalogować się jako '.$uzytkownik;
ftp_quit($lacz);
exit;
}
$czaszdalny=ftp_mdtm($lacz, $plikzdalny);
if (!($czaszdalny >= 0))
{
// To nie oznacza, że plik nie istnieje, serwer może nie dostarczać czasu modyfikacji
echo 'Dostęp do czasu pliku zdalnego niemożliwy.<br />';
$czaszdalny=$czaslokalny+1; //zapewnienie uaktualnienia
}
else
{
echo 'Plik zdalny zmodyfikowany ';
echo date('G:i j-M-Y', $czaszdalny);
echo '<br />';
}
// pobieranie pliku
echo 'Pobieranie pliku z serwera...<br />';
$wp=fopen($pliklokalny, 'wb');
fclose($wp);
echo 'Plik pobrany pomyślnie';
Rozdział 18. Stosowanie funkcji sieci i protokołu 409
?>
</body>
<html>
Rysunek 18.4.
Skrypt tworzący kopię
lustrzaną przez FTP
sprawdza, czy lokalna
wersja pliku jest
aktualna, i ewentualnie
pobiera nową
Skrypt ftplustro.php jest dość ogólny. Można zauważyć, że rozpoczyna się on od ustawienia
pewnych zmiennych:
$komp = 'apache.cs.utah.edu';
$uzytkownik = 'anonymous';
$haslo = 'ja@przyklad.com';
$plikzdalny = '/apache.org/httpd/httpd-2.4.23.tar.gz';
$pliklokalny = '/sciezka/do/plikow/httpd-2.4.23.tar.gz';
Zmienna $komp powinna zawierać nazwę serwera FTP, z którym ma nastąpić połączenie. Z kolei
$uzytkownik i $haslo odpowiadają nazwie użytkownika i hasłu, jakie zostaną podane przy logowaniu.
Wiele witryn FTP pozwala na anonimowe logowanie, czyli powszechnie dostępną nazwę użytkow-
nika, którą każdy może użyć do zalogowania. Nie wymaga się żadnego hasła, ale powszechnym
zwyczajem jest podawanie własnego adresu poczty elektronicznej, aby administratorzy systemu
mogli poznać pochodzenie swoich użytkowników. W powyższym skrypcie została zastosowana ta
konwencja.
Zmienna $plikzdalny zawiera ścieżkę do pliku, który ma zostać pobrany. W tym przypadku jest
to serwer Apache dla systemu Unix.
Zmienna $pliklokalny zawiera ścieżkę do pliku na lokalnym komputerze, w którym ma zostać prze-
chowany pobrany plik. Niezależnie od tego, w którym katalogu zostanie umieszczony pobierany
plik, PHP musi mieć możliwość zapisania w nim tego pliku. Bez względu na rodzaj systemu
operacyjnego przedstawiony skrypt zadziała tylko po uprzednim utworzeniu tego katalogu.
Co więcej, jeżeli konfiguracja systemu jest dość restrykcyjna, trzeba będzie się dodatkowo upew-
nić, czy skrypt ma prawo do zapisu. Zmienne te mogą być modyfikowane w celu zaadaptowania
skryptu do własnych potrzeb.
410 Część IV Zaawansowane techniki PHP
Podstawowe etapy tego skryptu są identyczne jak te stosowane przy ręcznym pobieraniu pliku
przez FTP za pomocą wiersza poleceń:
1. Połączenie ze zdalnym serwerem FTP.
2. Logowanie (jako użytkownik lub anonimowe).
3. Sprawdzenie, czy plik zdalny został uaktualniony.
4. Jeżeli tak, pobranie go.
5. Zamknięcie połączenia FTP.
w wierszu poleceń platformy Windows lub Unix. W PHP jest on wykonany za pomocą nastę-
pującego kodu:
$lacz=ftp_connect($komp);
if(!$lacz)
{
echo 'Błąd: Nie można nawiązać połączenia z komputerem '. $komp;
exit;
}
Funkcja wywoływana w tym skrypcie to ftp_connect(). Pobiera ona jako parametr nazwę kom-
putera, a zwraca uchwyt połączenia lub false, jeżeli połączenie nie powiodło się. Funkcja ta może
również pobrać numer portu komputera, z którym należy się połączyć, jako opcjonalny drugi para-
metr (nie został on zastosowany w powyższym skrypcie). Jeżeli numer portu nie zostanie podany,
domyślnym portem jest 21, czyli domyślny port FTP.
Należy zauważyć, że jeżeli próba zalogowania nie powiedzie się, połączenie FTP zostanie zamknięte
za pomocą funkcji ftp_quit(). Więcej informacji na ten temat znajduje się w dalszej części rozdziału.
Zanim przejdziemy do opisu wykonywania operacji na zdalnym systemie plików, chcielibyśmy zwró-
cić uwagę Czytelnika na funkcję ftp_pasv(), wywoływaną w poniższym wierszu kodu:
ftp_pasv($lacz, true);
W tym przypadku wywołanie to zapewnia, że będzie używany tryb pasywny; oznacza to, że
wszystkie połączenia danych będą inicjowane przez klienta (skrypt), a nie przez zdalny serwer FTP.
Gdyby tryb pasywny nie został włączony, mogłoby się okazać, że skryptowi nie uda się pomyśl-
nie przejść dalej niż do zalogowania się na zdalnym serwerze.
Właśnie czasy plików decydują o tym, że używane są funkcje FTP, a nie o wiele prostsze od nich
funkcje plikowe. Te ostatnie bez trudu radzą sobie z odczytywaniem i, w niektórych przypadkach,
również zapisywaniem plików za pośrednictwem interfejsów sieciowych, lecz większość funkcji
statusu, takich jak filemtime(), nie działa w sposób zdalny.
Na początku sprawdzamy istnienie lokalnej kopii pliku za pomocą funkcji file_exists(). Jeżeli
plik nie istnieje, pobranie go jest oczywiście konieczne. W przeciwnym wypadku należy pobrać czas
ostatniej modyfikacji pliku za pomocą funkcji filemtime() i przechować go w zmiennej, na przy-
kład $czaslokalny. Jeżeli plik nie istnieje, wartość zmiennej $czaslokalny zostaje ustawiona na 0,
tak aby był on „starszy” niż wszystkie możliwe czasy modyfikacji pliku zdalnego:
echo 'Sprawdzanie daty pliku...<br />';
if(file_exists($pliklokalny))
{
$czaslokalny=filemtime($pliklokalny);
echo 'Plik lokalny zmodyfikowany';
echo date('G:i j-M-Y', $czaslokalny);
echo '<br />';
}
else
$czaslokalny=0;
Po ustaleniu czasu lokalnego należy pobrać czas modyfikacji pliku zdalnego. Można to wykonać
za pomocą funkcji ftp_mdtm():
$czaszdalny=ftp_mdtm($lacz, $plikzdalny);
Pobiera ona dwa parametry — uchwyt połączenia FTP i ścieżkę do pliku zdalnego — oraz
zwraca uniksowy znacznik czasu modyfikacji pliku lub –1, jeżeli występuje jakiś błąd. Nie
wszystkie serwery FTP zapewniają tę własność, tak więc można nie otrzymać użytecznego wyniku
tej funkcji. W tym przypadku istnieje możliwość sztucznego ustawienia zmiennej $czaszdalny,
aby była „nowsza” niż zmienna $czaslokalny, poprzez dodanie do niej 1. Zapewni to wykonanie
próby pobrania pliku:
412 Część IV Zaawansowane techniki PHP
Kiedy dostępne są już oba czasy, można je porównać, aby zobaczyć, czy pobranie pliku jest
konieczne, czy nie:
if(!($czaszdalny > $czaslokalny))
{
echo 'Kopia lokalna jest aktualna.<br />';
exit;
}
Pobieranie pliku
Na tym etapie następuje próba pobrania pliku z serwera:
echo 'Pobieranie pliku z serwera...<br />';
$wp=fopen($pliklokalny, 'wb');
if(!$sukces=ftp_fget($lacz, $wp, $plikzdalny, FTP_BINARY))
{
echo 'Błąd: pobranie pliku niemożliwe';
ftpclose($wp);
ftp_quit($lacz);
exit;
}
fclose($wp);
echo 'Plik pobrany pomyślnie';
Jak opisaliśmy wcześniej, plik lokalny jest otwierany za pomocą funkcji fopen(). Po wykonaniu
tej czynności należy wywołać funkcję ftp_fget(), która dokonuje próby pobrania pliku i przecho-
wania go w pliku lokalnym. Funkcja ta pobiera cztery parametry. Pierwsze trzy są proste — połą-
czenie FTP, uchwyt pliku lokalnego oraz ścieżka do pliku zdalnego. Parametr czwarty to tryb FTP.
Istnieją dwa tryby przesyłania plików przez FTP: ASCII i binarny. Tryb ASCII jest stosowany do
przekazywania plików tekstowych (to znaczy takich, które składają się jedynie ze znaków ASCII),
a tryb binarny do przesyłania wszystkich innych typów plików. W trybie binarnym pliki są trans-
ferowane bez zmian, natomiast w trybie ASCII znaki powrotu karetki i nowego wiersza są prze-
kształcane w znaki odpowiednie dla używanego systemu operacyjnego (\n w Uniksie, \r\n
w Windows i \r w Macintoshu).
Biblioteka FTP PHP posiada dwie predefiniowane zmienne, FTP_ASCII i FTP_BINARY, które przed-
stawiają te dwa tryby. Należy wybrać tryb odpowiedni dla danego pliku i przekazać właściwą
zmienną funkcji ftp_fget() jako czwarty parametr. W przykładowym skrypcie jest przesyłany plik
gzip, tak więc został zastosowany tryb FTP_BINARY.
Funkcja ftp_fget() zwraca true, jeżeli pobranie powiedzie się, lub false, jeżeli wystąpi błąd.
W przykładzie wynik zostaje przechowany w zmiennej $sukces, po czym użytkownik otrzymuje
wiadomość o wyniku pobierania.
Rozdział 18. Stosowanie funkcji sieci i protokołu 413
Alternatywą dla ftp_fget() jest funkcja ftp_get(), która posiada następujący prototyp:
int ftp_get(int połączenie_ftp, string ścieżka_pliku_lokalnego,
string ścieżka_pliku_zdalnego, int tryb)
Funkcja ta działa w bardzo podobny sposób do ftp_fget(), lecz nie wymaga otwarcia pliku lokal-
nego. Przekazuje się jej systemową nazwę pliku lokalnego, w którym ma zostać wykonany zapis,
a nie jego uchwyt.
Należy zauważyć, że nie istnieje funkcja równoważna do polecenia FTP mget, które może być
zastosowane do pobierania wielu plików jednocześnie. Zamiast niego należy użyć kilku wywo-
łań ftp_fget() lub ftp_get().
Zamykanie połączenia
Po zakończeniu połączenia FTP należy je zamknąć za pomocą funkcji ftp_quit():
ftp_quit($lacz);
Wysyłanie plików
Aby przebyć odwrotną drogę, to znaczy skopiować pliki z własnego serwera do komputera zdal-
nego, należy zastosować dwie funkcje, które zasadniczo są odwrotnościami ftp_fget() i ftp_get().
Funkcje te to ftp_fput() i ftp_put(). Posiadają one następujące prototypy:
int ftp_fput(int połączenie_ftp, string ścieżka_pliku_lokalnego,
int ścieżka_pliku_zdalnego, int tryb)
int ftp_put(int połączenie_ftp, string ścieżka_pliku_lokalnego,
string ścieżka_pliku_zdalnego, int tryb)
Domyślna wartość maksymalnego czasu wykonania wszystkich skryptów PHP jest zdefiniowana
w pliku php.ini i wynosi domyślnie 30 sekund. Wartość ta została zaprojektowana do wychwy-
tywania skryptów wyłamujących się spod kontroli. Jednak przy przesyłaniu plików przez FTP
i wolnym połączeniu — lub dużym pliku — przesłanie pliku może zabrać więcej czasu.
pozwala skryptowi na działanie przez kolejne 90 sekund od momentu wywołania tej funkcji.
414 Część IV Zaawansowane techniki PHP
Funkcja ta zwraca wielkość pliku zdalnego lub –1, jeżeli wystąpi błąd. Nie działa ona jednak na
niektórych serwerach FTP.
Jednym z użytecznych zastosowań funkcji ftp_size() jest obliczenie maksymalnego czasu wyko-
nania, ustawianego dla konkretnego transferu. Znając wielkość pliku i szybkość połączenia, można
w przybliżeniu obliczyć czas potrzebny na pobranie pliku i odpowiednio zastosować funkcję
set_time_limit().
Za pomocą następującego kodu można uzyskać wykaz plików mieszczących się w katalogu na
zdalnym serwerze WWW:
$wydruk=ftp_nlist($lacz, dirname($sciezka_katalogu));
foreach($wydruk as $nazwapliku)
{
echo $nazwapliku.'<br />’;
}
Kod ten korzysta z funkcji ftp_nlist(), aby uzyskać listę nazw plików w konkretnym katalogu.
Jeżeli chodzi o pozostałe funkcje FTP, to niemal wszystko, co można wykonać z wiersza poleceń
FTP, można też uzyskać za pomocą funkcji FTP, z wyjątkiem polecenia mget (pobrania wielu
plików). W jego przypadku można zastosować funkcję ftp_nlist(), aby uzyskać listę plików
i potem kolejno je pobierać.
Kompletną listę funkcji PHP odpowiadających poleceniom FTP można znaleźć w internetowej
dokumentacji PHP, dostępnej na stronie: http://php.net/manual/pl/book.ftp.php.
Interesujące mogą wydać się również niektóre informacje na temat protokołów World Wide
Web Consortium: http://www.w3.org/Protocols/.
Warto również zerknąć do książki opisującej TCP/IP — Computer Networks autorstwa Andrew
Tanenbauma3.
W następnym rozdziale
Teraz jesteśmy już gotowi, by przejść do kolejnego rozdziału książki. Będzie on poświęcony biblio-
tekom PHP zawierającym funkcje dat i funkcje kalendarzowe. Przedstawimy w nim sposoby
konwersji formatów wprowadzonych przez użytkownika na formaty MySQL i odwrotnie.
3
Polskie wydanie ukazało się pod tytułem Sieci komputerowe. Wydanie V (Helion 2012).
Rozdział 19.
Zarządzanie datą i czasem
W tym rozdziale opiszemy sprawdzanie i formatowanie daty i czasu oraz konwersje pomiędzy
formatami daty. Jest to szczególnie ważne podczas konwersji między formatami daty MySQL i PHP,
formatami daty Uniksa i PHP oraz dat wpisywanych przez użytkownika w formularzu HTML.
Strefy czasowe
Można się spotkać z opiniami, że najprostszym sposobem radzenia sobie ze strefami czasowymi
w aplikacjach internetowych jest całkowite ich pominięcie, a to ze względu na nieuchronny atak
serca, który ich obsługa może wywołać u przeciętnego programisty. Jednak z drugiej strony zigno-
rowanie stref czasowych nie jest najlepszym sposobem obsługi dat i godzin w aplikacjach inter-
netowych, a zwłaszcza w tych, które mogą być stosowane przez użytkowników z całego świata.
Oczywiście istnieje możliwość przechowywania wszystkich informacji o datach i godzinach w jed-
nej, uniwersalnej strefie czasowej (takiej jak UTC) i konwertowania ich w razie konieczności
na informacje dotyczące strefy konkretnego użytkownika, przechowywania ich we własnej strefie
czasowej i analogicznego konwertowania (choć w tym przypadku sprawa może być nieco trud-
niejsza, jeśli nie wiadomo, w jakiej strefie czasowej działa serwer, bądź jeśli serwery są zlokalizo-
wane w innych strefach czasowych niż ta, w której przebywamy) czy też w końcu zapisywania
dat i godzin z wykorzystaniem strefy czasowej użytkownika.
W tym rozdziale Czytelnik pozna między innymi funkcje PHP date(), time() oraz strtotime(),
przy czym koniecznie należy zaznaczyć, że wykorzystują one strefę czasową określoną w pliku
php.ini przy użyciu dyrektywy konfiguracyjnej date.timezone. Domyślnie wartość tej dyrektywy
nie jest ustawiona, a PHP stosuje ustawienia domyślne. Zalecane jest jednak jawne ustawienie
wartości dyrektywy date.timezone w pliku php.ini na używanym serwerze WWW poprzez podanie
w niej jednej z dostępnych stref czasowych wymienionych na stronie http://php.net/manual/en/
timezones.php.
416 Część IV Zaawansowane techniki PHP
W przypadku stosowania standardowej strefy czasowej, takiej jak UTC, do zapisywania wszyst-
kich dat i godzin w aplikacji można także wykorzystać wbudowaną funkcję MySQL o nazwie
UTC_TIMESTAMP(). Jednak nawet jeśli zastosujemy rozwiązanie polegające na zapisywaniu dat
i godzin z użyciem standardowego formatu, lecz będziemy chcieli konwertować je na bieżąco
na podstawie ustawień użytkownika, będziemy to mogli robić, korzystając z funkcji MySQL
o nazwie CONVERT_TZ().
Powyższy wiersz kodu wyświetli datę formatu 17th February 2015. Kody formatów akceptowane
przez funkcję date() są przedstawione w tabeli 19.1.
Kod Opis
a Przed- lub popołudnie, przedstawione jako dwie małe litery, am lub pm
A Przed- lub popołudnie, przedstawione jako dwie wielkie litery, AM lub PM
B Czas internetowy Swatch, uniwersalny schemat czasu, reprezentujący bieżącą godzinę w formie liczby
z zakresu od 000 do 999. Więcej informacji na ten temat można znaleźć pod adresem:
http://www.swatch.com/en/internet-time
c Data w formacie ISO 8601. Data prezentowana jako RRRR-MM-DD. Duża litera T oddziela datę od godziny.
Godzina jest przedstawiana w formacie GG:MM:SS. Na końcu wyświetlana jest strefa czasowa w postaci
odchylenia od czasu Greenwich (GMT) — na przykład 2015-02-17T01:38:35+00:00
d Dzień miesiąca jako dwucyfrowa liczba z wiodącym zerem. Zasięg od 01 do 31
D Dzień tygodnia w trójznakowym skróconym formacie tekstowym. Zasięg od Mon do Sun
e Identyfikator strefy czasowej, taki jak UTC lub GMT
F Miesiąc roku w pełnym formacie tekstowym. Zasięg od January do December
g Godzina dnia w formacie dwunastogodzinnym bez zer wiodących. Zasięg od 1 do 12
G Godzina dnia w formacie dwudziestoczterogodzinnym bez zer wiodących. Zasięg od 0 do 23
h Godzina dnia w formacie dwunastogodzinnym z zerami wiodącymi. Zasięg od 01 do 12
H Godzina dnia w formacie dwudziestoczterogodzinnym z zerami wiodącymi. Zasięg od 01 do 23
i Minuty po pełnej godzinie z zerami wiodącymi. Zasięg od 00 do 59
I Czas letni, przedstawiony jako wartość boolowska. Zwróci ona 1, jeżeli aktualnie jest czas letni, i 0, jeżeli nie
j Dzień miesiąca jako liczba bez zer wiodących. Zasięg od 1 do 31
l Dzień tygodnia w pełnym formacie tekstowym. Zasięg od Monday do Sunday
L Rok przestępny, przedstawiony jako wartość boolowska. Zwróci ona 1, jeżeli aktualny rok jest rokiem
przestępnym, i 0, jeżeli nie
Rozdział 19. Zarządzanie datą i czasem 417
Tabela 19.1. Kody formatów dla funkcji PHP date() (ciąg dalszy)
Kod Opis
m Miesiąc roku jako liczba dwucyfrowa z zerami wiodącymi. Zasięg od 01 do 12
M Miesiąc roku w trójznakowym skróconym formacie tekstowym. Zasięg od Jan do Dec
n Miesiąc roku jako liczba bez zer wiodących. Zasięg od 1 do 12
N Dzień tygodnia jako jedna cyfra; zgodny z ISO-8601. Zakres wartości wynosi od 1 (poniedziałek) do 7
(niedziela)
o Rok zgodnie z ISO-8601. Jest to taka sama wartość jak w przypadku kodu Y, tyle że jeżeli numer tygodnia
ISO (W) należy do roku poprzedniego lub następnego, wówczas ten właśnie rok zostanie użyty.
O Mierzona w godzinach różnica między bieżącą strefą czasową a Greenwich Mean Time, na przykład +1600
P Różnica pomiędzy bieżącą strefą czasową a GMT, z dwukropkiem rozdzielającym godziny i minuty, na
przykład +05:00
r Data i czas sformatowane zgodnie z dokumentem RFC822, na przykład Tue, 17 Feb 2015 01:41:42 +0000
s Sekundy po pełnej minucie z zerami wiodącymi. Zasięg od 00 do 59
S Przyrostek porządkowy dat w formacie dwuznakowym. Zależnie od poprzedzającej cyfry może być to
st, nd, rd lub th
Znaczniki czasu Uniksa są skrótowym sposobem przechowywania daty i czasu, lecz warto zauwa-
żyć, że nie dotyczy ich problem roku 2000, w przeciwieństwie do niektórych innych skrótowych
formatów daty. Mogą w nich jednak wystąpić podobne problemy, ponieważ przy użyciu
32-bitowych liczb całkowitych można zaprezentować jedynie ograniczony zakres czasu. Jeżeli two-
rzone oprogramowanie będzie miało obsługiwać daty sprzed roku 1902 lub po roku 2038, może
to sprawić niemałe kłopoty.
418 Część IV Zaawansowane techniki PHP
W niektórych systemach, w tym również w systemie Windows, zakres jest jeszcze bardziej ogra-
niczony. Znacznik czasu nie może mieć wartości ujemnej, zatem nie można używać znaczników
sprzed roku 1970. Trzeba o tym pamiętać, myśląc o przenośności kodu.
Niemal na pewno nie trzeba się martwić o to, czy napisany program będzie nadal używany w roku
2038. Znaczniki czasu nie mają z góry określonego rozmiaru, lecz zależą od rozmiaru typu long
języka C, który wynosi co najmniej 32 bity. Jeżeli napisany program będzie używany jeszcze w roku
2038, jest bardzo prawdopodobne, że kompilator będzie używał typu o większym rozmiarze.
Konwencja ta jest standardem systemu Unix, lecz mimo to format taki jest używany przez funkcję
date() i wiele innych funkcji nawet w PHP dla Windows. Jedyna różnica polega na tym, że w sys-
temie tym znacznik czasu musi mieć wartość dodatnią.
Aby przekonwertować czas i datę do znacznika czasu Uniksa, należy zastosować funkcję mktime().
Posiada ona następujący prototyp:
int mktime([int godzina[, int minuta[, int sekunda[, int miesiąc[,
int dzień[, int rok[, int czas_letni]]]]]]])
Same parametry nie wymagają dodatkowych wyjaśnień, ale podstawową pułapką, na którą należy
zwracać uwagę, jest ich kolejność — sprawia ona bowiem, że chcąc określić datę, nie można
pominąć parametrów określających czas. Jeśli czas nie ma znaczenia, to jako liczbę godzin,
minut i sekund można podać 0. Oczywiście można pomijać parametry z prawej strony ich listy.
Jeżeli funkcji nie przekaże się żadnych parametrów, zostaną one ustawione na wartości aktualne.
Tak więc wywołanie takie jak:
$znacznikczasu = mktime();
zwróci znacznik czasu Uniksa dla aktualnej daty czasu (choć takie wywołanie spowoduje wygene-
rowanie ostrzeżenia E_STRICT, jeśli pozwala na to obecnie wybrany poziom raportowania błędów).
Podobny rezultat można uzyskać, używając następującego wywołania:
$znacznikczasu = time();
Funkcja time() nie przyjmuje żadnych parametrów i zawsze zwraca znacznik czasu Unix dla ak-
tualnej daty i godziny.
Kolejną opcją jest funkcja date(), o której już wspominano. Łańcuch formatujący "U" oznacza
znacznik czasu. Poniższa instrukcja odpowiada dwóm zaprezentowanym powyżej:
$znacznikczasu=date("U");
Funkcji mktime() można przekazać rok w formacie dwu- lub czterocyfrowym. Wartości dwucy-
frowe od 0 do 69 zostaną zinterpretowane jak lata od 2000 do 2069, a wartości od 70 do 99 — jako
lata od 1970 do 1999.
Poniżej znajduje się kilka dodatkowych przykładów ilustrujących działanie funkcji mktime():
$czas = mktime(12, 0, 0);
oznacza południe bieżącego dnia;
$czas = mktime(0,0,0,1,1);
to pierwszy stycznia bieżącego roku. Warto zauważyć, że parametr wskazujący północ jest okre-
ślany przez liczbę 0 zamiast 24.
Funkcji mktime() można również używać wraz z prostymi działaniami arytmetycznymi. Na przykład
$czas = mktime(12,0,0,$pon,$dzien+30,$rok);
Rozdział 19. Zarządzanie datą i czasem 419
spowoduje dodanie 30 dni do daty wskazanej przez poszczególne parametry, mimo że wyrażenie
($dzien+30) w większości przypadków będzie miało wartość większą niż liczba dni we wskaza-
nym miesiącu.
Aby wyeliminować problemy związane z czasem letnim i zimowym, najlepiej jest używać godziny
12 zamiast godziny 0. Jeśli do północy dnia 25-godzinnego dodana zostanie wartość (246060),
dzień się nie zmieni. Dodanie tej samej wartości do południa zwróci godzinę 11 rano, lecz przy-
najmniej będzie to cały czas prawidłowy dzień.
Pobiera ona znacznik czasu i zwraca tablicę asocjacyjną przedstawiającą części daty i czasu, jak
pokazano w tabeli 19.2.
Mając te pary informacji o dacie i godzinie w tablicy, łatwo można przetwarzać je w dowolne
wymagane formaty. Element 0 (znacznik czasu) może się wydawać bezużyteczny w tablicy, lecz
gdy funkcję getdate() wywoła się bez parametrów, zwróci ona bieżący znacznik czasu.
Sprawdza ona, czy rok jest poprawną liczbą z zakresu od 0 do 32767, czy miesiąc jest liczbą z zakre-
su od 1 do 12 i czy dany dzień istnieje w danym miesiącu. Funkcja w trakcie sprawdzania poprawno-
ści daty uwzględnia lata przestępne.
Na przykład wyrażenie
checkdate(9, 18, 2008);
zwróci true, a wyrażenie
checkdate(9, 31, 2008);
— nie.
Parametr $format to kod formatowania, który definiuje sposób wyświetlenia znacznika czasu.
Parametr $znacznik_czasu natomiast to znacznik czasu przekazywany do funkcji. Parametr
$znacznik_czasu jest opcjonalny. Jeżeli do funkcji nie zostanie przekazany znacznik czasu, wów-
czas użyty zostanie bieżący znacznik czasu systemu (czyli znacznik na moment wykonania
skryptu). Na przykład poniższy kod:
<?php
echo strftime('%A<br />');
echo strftime('%x<br />');
echo strftime('%c<br />');
echo strftime('%Y<br />');
?>
wyświetli aktualny znacznik czasu w czterech różnych formatach. Wynik działania kodu będzie
podobny do przedstawionego poniżej:
Tuesday
02/17/15
Tue Feb 17 02:10:19 2015
2015
Pełna lista kodów formatowania dla funkcji strftime() przedstawiono w tabeli 19.3.
Rozdział 19. Zarządzanie datą i czasem 421
Klucz Wartość
%a Skrótowa nazwa dnia tygodnia
%A Pełna nazwa dnia tygodnia
%b lub %h Skrótowa nazwa miesiąca. Wartości należą do zakresu od Jan do Dec
%B Pełna nazwa miesiąca. Wartości należą do zakresu od January do December
%c Data i czas w standardowym formacie, na przykład Tue Feb 02:13:04 2015
%C Wiek w formacie dwucyfrowym, wyliczany przez podzielenie roku przez 100 i pominięcie części
dziesiętnej, na przykład 20
%d Dzień miesiąca w postaci skrótowej (przedział od 01 do 31)
%D Data w postaci skrótowej (to samo co mm/dd/yy), na przykład 02/17/15
%e Dzień miesiąca w postaci dwuznakowego łańcucha znaków (od '1' do '31')
%F Inna nazwa dla formatu "%Y-%m-%d", często stosowanego formatu zapisu dat w bazach danych.
Generuje daty takie jak 2015-02-17
%g Dwucyfrowy rok zgodny z numerem tygodnia, zgodny z ISO-8601
%G Czterocyfrowy rok zgodny z numerem tygodnia, zgodny z ISO-8601
%H Godzina (przedział od 00 do 23)
%I Godzina (przedział od 1 do 12)
%j Dzień roku (przedział od 001 do 366)
%k Godzina jako dwuznakowy łańcuch (od '1' do '23')
%l Godzina jako dwuznakowy łańcuch (od '1' do '23')
%m Miesiąc (przedział od 01 do 12)
%M Minuty (przedział od 00 do 59)
%n Znak nowego wiersza (\n)
%p Łańcuch znaków AM lub PM (wielkimi literami)
%P Łańcuch znaków am lub pm (małymi literami)
%r Czas w notacji z a.m. lub p.m., na przykład 02:22:45 AM
%R Czas w notacji 24-godzinnej, na przykład 02:22
%s Znacznik czasu epoki Uniksa, odpowiadający wywołaniu funkcji time(), na przykład 1424140235
%S Sekundy (przedział od 00 do 59)
%t Znak tabulacji (\t)
%T Godzina w formacie gg:mm:ss, na przykład 02:23:57
%u Numer dnia tygodnia (przedział od 1 do 7, gdzie 1 oznacza poniedziałek, a 7 — niedzielę), zgodny
z ISO-8601
%U Numer tygodnia aktualnego roku, począwszy od pierwszej niedzieli jako pierwszego dnia
pierwszego tygodnia
%V Numer tygodnia aktualnego roku, gdzie tydzień 1 jest pierwszym tygodniem, który ma co najmniej
4 dni w aktualnym roku, przy czym pierwszym dniem tygodnia jest poniedziałek; zgodny z ISO-8601
422 Część IV Zaawansowane techniki PHP
Klucz Wartość
%w Dzień tygodnia (przedział od 0 do 6, gdzie 0 oznacza niedzielę, a 6 oznacza sobotę)
%W Numer tygodnia aktualnego roku, gdzie pierwszy poniedziałek jest pierwszym dniem pierwszego
tygodnia
%x Data w standardowym formacie (bez czasu), na przykład 02/17/25
%X Czas w standardowym formacie (bez daty), na przykład 02:26:21
%y Rok dwucyfrowy, na przykład 15
%Y Rok czterocyfrowy, na przykład 2015
%z Przesunięcie strefy czasowej, na przykład -0500
%Z Skrót nazwy strefy czasowej, na przykład EST
Należy zaznaczyć, że za każdym razem, gdy w tabeli 19.3 jest mowa o standardowym formacie,
kod formatowania jest zastępowany przez wartość odpowiadającą ustawieniom lokalnym serwera
WWW. Funkcja strftime() doskonale nadaje się do wyświetlania dat i godzin w różnorodnych
formach, które uczynią strony internetowej najbardziej przyjaznymi.
Zależnie od docelowej grupy użytkowników, funkcja ta może się okazać niezbyt dla nich przyjazna.
W związku z tym, w celu komunikacji pomiędzy PHP i MySQL należy zazwyczaj dokonać operacji
konwersji daty. Działania te mogą zostać wykonane po każdej ze stron.
Podczas przekazywania dat z PHP do MySQL można je łatwo ułożyć w poprawny format za
pomocą funkcji date(), jak pokazano poprzednio. Należy tylko pamiętać o jednym — jeśli daty są
tworzone samodzielnie, zalecane jest stosowanie wersji dnia i miesiąca z wiodącymi zerami,
aby nie sprawiać trudności MySQL. Można używać dwucyfrowego zapisu roku, lecz lepszym
wyjściem będzie zapisywanie roku przy użyciu czterech cyfr. Jeżeli konwersja ma zostać wykonana
przez MySQL, dwie przydatne do tego celu funkcje to DATE_FORMAT() i UNIX_TIMESTAMP().
Funkcja DATE_FORMAT() działa w sposób podobny do swojego odpowiednika w PHP, lecz stosuje
inne kody formatów. Najpowszechniej używanym działaniem jest konwersja daty do formatu
amerykańskiego MM-DD-RRRR, zamiast formatu ISO właściwego MySQL — RRRR-MM-DD.
Można to wykonać poprzez wytworzenie następującego zapytania:
SELECT DATE_FORMAT(kolumna_daty, '%m %d %Y')
FROM nazwatabeli;
Kod Opis
%M Miesiąc, pełny tekst
%W Nazwa dnia tygodnia, pełny tekst
%D Dzień miesiąca, z przyrostkiem tekstowym (na przykład 1st)
%Y Rok, numeryczny, 4 cyfry
%y Rok, numeryczny, 2 cyfry
%a Nazwa dnia tygodnia, 3 znaki
%d Dzień miesiąca, numeryczny, zera wiodące
%e Dzień miesiąca, numeryczny, bez zer wiodących
%m Miesiąc, numeryczny, zera wiodące
%c Miesiąc, numeryczny, bez zer wiodących
%b Miesiąc, tekst, 3 znaki
%j Dzień roku, numeryczny
%H Godzina, format dwudziestoczterogodzinny, zera wiodące
%k Godzina, format dwudziestoczterogodzinny, bez zer wiodących
%h lub %I Godzina, format dwunastogodzinny, zera wiodące
%l Godzina, format dwunastogodzinny, bez zer wiodących
%i Minuty, numeryczny, zera wiodące
%r Czas, format dwunastogodzinny (gg:mm:ss [AM|PM])
%T Czas, format dwudziestoczterogodzinny (gg:mm:ss)
%S lub %s Sekundy, numeryczny, zera wiodące
%p AM lub PM
%w Dzień tygodnia, numeryczny, od 0 (niedziela) do 6 (sobota)
Funkcja UNIX_TIMESTAMP() działa w podobny sposób, lecz zamienia kolumnę na znacznik czasu
Uniksa. Na przykład
SELECT UNIX_TIMESTAMP(kolumna_daty)
FROM nazwatabeli;
zwróci datę w formacie znacznika czasu Uniksa. Później można z nim postąpić w podobny sposób
jak w PHP.
Obliczenia i porównania dat można łatwo wykonywać, stosując znaczniki czasu Unix. Należy
jednak pamiętać, że znaczniki czasu mogą zwykle reprezentować daty jedynie z zakresu od roku
1902 do roku 2038, podczas gdy zakres dat MySQL jest znacznie szerszy.
Zaleca się stosowanie znaczników czasu Uniksa do obliczeń dat, a standardowego formatu daty do
ich przechowywania lub wyświetlania.
424 Część IV Zaawansowane techniki PHP
Listing 19.1. oblicz_wiek.php — skrypt obliczający wiek osoby na podstawie daty urodzenia
<?php
// ustawienie daty do obliczeń
$dzien = 18;
$miesiac = 9;
$rok = 1972;
W powyższym skrypcie data służąca do obliczenia wieku została podana ręcznie. W prawdziwych
aplikacjach informacja ta zazwyczaj pochodzi z formularza HTML. W pierwszej kolejności należy
wywołać funkcję mktime() w celu obliczenia znacznika czasu dla daty urodzenia i aktualnego
czasu:
$dataurunix = mktime(0, 0, 0, $miesiac, $dzien, $rok);
$terazunix=time(); // znacznik dla dnia dzisiejszego
Kiedy daty te posiadają już ten sam format, można po prostu odjąć je od siebie:
$wiekunix=$terazunix - $dataurunix;
Teraz następuje nieco trudniejsza część — konwersja tego okresu z powrotem do formatu bliższego
potocznemu myśleniu. Nie jest to znacznik czasu, lecz wiek osoby mierzony w sekundach. Można
tę liczbę ponownie przekonwertować na lata poprzez podzielenie jej przez liczbę sekund w roku.
Następnie zostaje ona zaokrąglona w dół za pomocą funkcji floor(), aby na przykład wiek osoby
nie był określany jako 20, dopóki nie ukończy ona dwudziestego roku życia:
$wiek=floor($wiekunix / (365 * 24 * 60 * 60)); // konwersja z sekund na lata
Należy jednak zauważyć, że to ujęcie posiada wady, ponieważ jest ograniczone poprzez wielkość
znaczników czasu Uniksa (ogólnie 32-bitowych liczb typu integer). Znaczniki czasu nie są jednak
najlepszą formą prezentacji dat urodzenia. Przedstawiony przykład będzie działał na wszystkich
platformach jedynie w przypadku osób urodzonych w roku 1970 lub późniejszym. System Windows
nie obsługuje znaczników czasu wcześniejszych niż dla roku 1970. Nawet jednak biorąc ten fakt
pod uwagę, obliczenia mogą okazać się nie do końca prawidłowe, ponieważ nie uwzględnia się
w nich lat przestępnych, a dodatkowe błędy mogą wystąpić, jeśli o północy w dniu urodzin w lokal-
nej strefie czasowej następuje zmiana czasu na letni.
Rozdział 19. Zarządzanie datą i czasem 425
Innym rozwiązaniem, które nie jest widoczne od razu, jest użycie serwera MySQL. Udostępnia on
szeroki zbiór funkcji manipulowania datami, działających również dla dat i godzin wykraczających
poza zakres znaczników czasu Uniksa. W celu wykonania zapytania trzeba połączyć się z serwerem
MySQL, nie trzeba jednak w tym przypadku pobierać z bazy jakichkolwiek danych.
Poniższe zapytanie dodaje jeden dzień do 28 lutego 1700 roku i zwraca datę wynikową:
select adddate('1700-02-28', interval 1 day);
Rok 1700 nie jest rokiem przestępnym, zatem datą wynikową będzie 1700-03-01.
Wyczerpujący opis składni służącej do opisywania i modyfikowania dat można znaleźć w podręczniku
MySQL dostępnym pod adresem http://dev.mysql.com/doc/refman/5.6/en/date-and-time-functions.html.
Niestety, nie istnieje prosty sposób obliczenia liczby lat między dwiema datami, zatem przykład
z datami urodzin wciąż pozostaje problemem. Wiek osoby mierzony w dniach można bardzo łatwo
odczytać, a kod z listingu 19.2 dodatkowo obliczy na jego podstawie przybliżoną liczbę lat.
Listing 19.2. mysql_oblicz_wiek.php — skrypt obliczający wiek osoby na podstawie daty urodzenia przy użyciu
serwera MySQL
<?php
// ustawienie daty dla celów obliczeniowych
$dzien = 18;
$miesiac = 9;
$rok = 1972;
Po sformatowaniu daty do postaci znacznika czasu ISO, do serwera MySQL zostanie przesłane
następujące zapytanie:
select datediff(now(), '1972-09-18T00:00:00+02:00')
Funkcja now() serwera MySQL zwraca bieżącą datę i godzinę. Funkcja datediff() serwera odej-
muje natomiast jedną datę od drugiej i zwraca obliczoną różnicę wyrażoną w dniach.
Warto zaznaczyć, że data nie jest odczytywana z tabeli ani nawet w skrypcie nie wybiera się bazy
danych — wystarczy tylko zalogować się do serwera MySQL, używając odpowiedniej nazwy
użytkownika i hasła.
426 Część IV Zaawansowane techniki PHP
Ponieważ nie istnieje żadna wbudowana funkcja przeznaczona specjalnie do wykonywania takich
obliczeń, zapytanie SQL obliczające dokładną liczbę lat byłoby dosyć skomplikowane. W naszym
przykładzie zastosowano pewien skrót, polegający na podzieleniu liczby dni przez 365,25, co
w efekcie ma dać wiek wyrażony liczbą lat. Wynik ten może różnić się od prawidłowego o rok,
jeśli zapytanie zostanie wykonane w dniu urodzin danej osoby i zależnie od tego, ile lat przestępnych
wystąpiło w całym życiu tej osoby.
Stosowanie mikrosekund
W niektórych aplikacjach mierzenie czasu w sekundach może nie być wystarczająco precyzyjne.
Jeżeli trzeba mierzyć bardzo krótkie okresy, na przykład czas wykonania jednego lub kilku skryptów
PHP, należy użyć funkcji microtime().
Choć parametr funkcji microtime() jest opcjonalny, to jednak zalecamy, by w wywołaniach tej
funkcji nadawać mu wartość true. Dzięki podaniu tego parametru opcjonalnego funkcja microtime()
zwróci znacznik czasu wyrażony w postaci liczby zmiennoprzecinkowej, gotowej do użycia
w dowolnie wybrany sposób. Jest to ten sam znacznik czasu, co znacznik zwrócony przez mktime(),
time() i date(), zawierający jednak dodatkowo część ułamkową.
Poniższa instrukcja:
echo number_format(microtime(true), 5, '.', '');
W starszych wersjach PHP nie można zażądać zwrócenia wyniku w postaci liczby zmiennoprze-
cinkowej — jest on zwracany w postaci łańcucha znaków. Wywołanie funkcji microtime() bez
parametru spowoduje zwrócenie przez nią łańcucha znaków w postaci " 0.88679500 1424141403".
Pierwsza liczba to część ułamkowa, natomiast druga to liczba wszystkich sekund, jakie upłynęły
od 1 stycznia 1970 roku.
Przetwarzanie liczb jest bardziej użyteczne niż przetwarzanie łańcuchów znaków, zatem najłatwiej
jest wywołać funkcję microtime() z parametrem true.
Aby korzystać z tych funkcji, należy wkompilować w PHP rozszerzenie calendar używając opcji
--enable-calendar. Jest ono już wkompilowane w standardowej instalacji dla Windows.
W następnym rozdziale
O ustawieniach lokalnych wspomniano już przy okazji prezentowania funkcji służących do obsługi
dat i godzin, jednak zrozumienie zagadnień związanych z ustawieniami lokalnymi jest nieodzowne
w kontekście tworzenia wielojęzycznych aplikacji internetowych. Rozdział 20., „Umiędzynaroda-
wianie i lokalizowanie”, wyjaśnia, że lokalizacja to znacznie więcej niż samo przetłumaczenie
tekstów, i pokazuje, w jaki sposób można przygotować aplikację do tego procesu.
428 Część IV Zaawansowane techniki PHP
Rozdział 20.
Umiędzynarodawianie
i lokalizowanie
W tym rozdziale przedstawione zostaną podstawy umiędzynarodawiania aplikacji interneto-
wych w celu przygotowania ich do lokalizacji ich zawartości. Tworzenie aplikacji umiędzyna-
rodowionych — czyli posiadających wiele wersji językowych — w PHP jest proste i zapewnia
liczne korzyści użytkownikom aplikacji pochodzącym z różnych krajów. Na początku rozdzia-
łu, w celu przygotowania Czytelnika do zdobycia użytkowników z wielu różnych krajów, wyja-
śnione zostaną różnice pomiędzy umiędzynarodawianiem oraz lokalizacją aplikacji, związki między
obiema tymi operacjami oraz dotyczące ich pojęcia.
w postaci stałych, a funkcje formatujące będą mogły zmieniać postać generowanych łańcuchów
w zależności od używanych ustawień lokalnych — będzie możliwe rozpoczęcie procesu lokalizacji.
Standardowe identyfikatory ustawień lokalnych składają się zarówno z kodu języka, jak i regionu,
na przykład en_US oznacza „odmianę języka angielskiego używaną w USA”, a en_GB „odmianę
języka angielskiego używaną w Wielkiej Brytanii”.
Można by się zastanawiać, dlaczego wprowadzono takie rozróżnienie geograficzne, jednak aby
znaleźć odpowiedź na to pytanie, wystarczy sobie wyobrazić różnice w zapisie tych samych słów
w obu odmianach języka angielskiego albo różnice kontekstowe, których wprowadzenie w two-
rzonym oprogramowaniu mogłoby być pożądane. Innym przykładem mogłaby być witryna,
której treści są pisane w języku niemieckim i która jest przeznaczona wyłącznie dla obywateli
Niemiec — taka witryna mogłaby używać ustawień lokalnych de_DE („język niemiecki używany
w Niemczech”). Choć Austriacy mówiący po niemiecku mogliby używać tej witryny w identycz-
nej postaci bez najmniejszych problemów, to jednak nigdy nie byłaby ona idealnie dostosowana
do ich potrzeb, jeśli nie zostałaby zlokalizowana przy wykorzystaniu ustawień lokalnych de_AT
(„język niemiecki używany w Austrii”).
Zbiory znaków
Zbiory znaków są zazwyczaj określane jako jednobajtowe lub wielobajtowe. Określenia te wskazują
na liczbę bajtów konieczną do zdefiniowania ze znakami używanymi w danym języku. Angielski,
niemiecki, francuski albo polski (jak i wiele innych języków) są językami jednobajtowymi,
gdyż do reprezentacji liter, takich jak a, oraz cyfr, takich jak 9, potrzeba tylko jednego bajta.
Jednobajtowe zbiory znaków zawierają maksymalnie 256 znaków, w tym kompletny zestaw zna-
ków ASCII, znaki diakrytyczne oraz inne znaki niezbędne do formatowania.
Wielobajtowe zbiory znaków mogą składać się z więcej niż 256 znaków, a ich podzbiorami mogą
być jednobajtowe zbiory znaków. Do języków wielobajtowych należą między innymi tradycyj-
nych chiński, japoński, koreański, tajski, arabski czy też hebrajski. W tych językach do zapisania
jednego znaku potrzeba więcej niż jednego bajta. Doskonałym przykładem może być słowo Tokio,
czyli nazwa stolicy Japonii. W języku polskim jest ono zapisywane przy użyciu 5 różnych liter, co
wymaga użycia 5 bajtów. Niemniej jednak w języku japońskim słowo to jest reprezentowane
przez dwie sylaby — tou oraz kyou — z których każda jest zapisywana z użyciem 2 bajtów, co
oznacza, że zapisanie tego słowa wymaga w sumie 4 bajtów.
Konkretnie rzecz biorąc, chodzi tu o dwa nagłówki: Content-type oraz Content-language, które
można także określić przy użyciu odpowiednich atrybutów znacznika HTML5. PHP daje możliwość
stworzenia środowiska dynamicznego, można zatem zabezpieczyć się na wszelkie okoliczności
i zarówno przesyłać do przeglądarki odpowiednie nagłówki, jak i umieszczać w generowanym
dokumencie odpowiadające im atrybuty znacznika HTML5.
Rozdział 20. Umiędzynarodawianie i lokalizowanie 431
Z kolei w przypadku witryny w języku japońskim zbiór znaków oraz kod języka należałoby określić
przy użyciu nagłówków generowanych w następujący sposób:
header("Content-Type: text/html;charset=UTF-8");
header("Content-Language: ja");
I podobnie, jeśli japońska witryna używa zbioru znaków UTF-8, a domyślnym zbiorem znaków
przeglądarki jest ISO-8859-1, to spróbuje ona wyświetlać teksty w języku japońskim, korzystając
z jednobajtowego zbioru znaków ISO-8859-1. Jeżeli wygenerowane nagłówki nie zmienią
używanego zbioru znaków na UTF-8 oraz jeżeli w systemie operacyjnym nie będą zainstalowane
odpowiednie biblioteki i pakiety językowe niezbędne do wyświetlania tekstu w zamierzony sposób,
to skutki mogą być fatalne.
Klasycznym przykładem zagrożenia związanego z używanymi zbiorami znaków jest różnica pomię-
dzy kodowaniem wykorzystywanym przez serwer PHP a kodowaniem używanym przez MySQL,
zwłaszcza w przypadku stosowania języków wielobajtowych. Załóżmy, że PHP uważa, że przesyła
do serwera MySQL zwyczajne teksty ASCII, a domyślnym zbiorem znaków używanym przez ser-
wer MySQL jest Big5. W takim przypadku zastosowanie funkcji mysql_real_escape_string()
(lub jej obiektowego odpowiednika) do zabezpieczenia łańcuchów znaków przesyłanych do bazy
dany spowoduje, że zostaną w nich pominięte znaki kończące dwubajtowe sekwencje znaków
Big5, gdyż PHP po prostu nie będzie wiedział o konieczności ich stosowania.
432 Część IV Zaawansowane techniki PHP
Aby móc korzystać z wielobajtowych funkcji łańcuchowych w PHP, należy je włączyć na etapie
konfiguracji, przed zbudowaniem PHP. Służy do tego opcja --enable-mbstring. Użytkownicy
systemu Windows muszą włączyć w pliku konfiguracyjnym php.ini bibliotekę php_mbstring.dll.
Po takim skonfigurowaniu PHP będzie już można używać wszystkich z ponad 40 funkcji służących
do obsługi łańcuchów znaków wielobajtowych.
Więcej informacji na temat tych funkcji można znaleźć w internetowej dokumentacji PHP dostępnej
na stronie http://www.php.net/mbstring. Ogólnie rzecz biorąc, podczas operowania na łańcuchach
znaków wielobajtowych w pierwszej kolejności należy poszukać zwyczajnej funkcji łańcuchowej,
a następnie odszukać jej wielobajtowy odpowiednik (na przykład funkcji strpos() będzie odpowia-
dać funkcja mb_stripos(), przeznaczona do wyszukiwania w łańcuchach znaków wielobajtowych).
Listing 20.1 przedstawia zawartość pliku głównego używanego do przesyłania nagłówków związa-
nych z lokalizacją.
switch($jezykBiezacy) {
case "en":
define("ZESTAWZNAKOW","ISO-8859-1");
define("KODJEZYKA", "en");
break;
case "ja":
define("ZESTAWZNAKOW","UTF-8");
define("KODJEZYKA", "ja");
break;
case "pl":
define("ZESTAWZNAKOW","UTF-8");
define("KODJEZYKA", "pl");
break;
default:
define("ZESTAWZNAKOW","UTF-8");
define("KODJEZYKA", "pl");
break;
}
header("Content-Type: text/html;charset=".ZESTAWZNAKOW);
header("Content-Language: ".KODJEZYKA);
?>
Jak widać na powyższym listingu, jeśli nie ma żadnej wartości zapisanej w sesji, to domyślnie
wybierane są polskie ustawienia lokalne. Gdyby witryna miała być domyślnie prezentowana w języ-
ku japońskim, to właśnie te ustawienia powinny być wybierane domyślnie. Ten skrypt ma być
używany wraz z kolejnym, przedstawionym na listingu 20.2, który zawiera mechanizm wyboru
ustawień lokalnych pozwalający na zmianę wartości przypisywanej zmiennej $jezykBiezacy
w wierszu 6. powyższego skryptu.
case "ja":
define("POWITANIE_TXT","ようこそ!");
define("WYBOR_TXT","言語を選択");
434 Część IV Zaawansowane techniki PHP
break;
case "pl":
define("POWITANIE_TXT","Witamy!");
define("WYBOR_TXT","Wybierz język");
break;
default:
define("POWITANIE_TXT","Witamy!");
define("WYBOR_TXT","Wybierz język");
break;
}
}
?>
Instrukcja switch rozpoczynająca się w wierszu 10. zawiera trzy klauzule case oraz klauzulę
default, których zadaniem jest ustawienie odpowiednich wartości stałych ZESTAWZNAKOW i KODJEZYKA.
W wierszach 32. i 33. stałe te zostają użyte do wygenerowania odpowiednich nagłówków
HTTP: Content-type i Content-language.
Listing 20.2 przedstawia plik zawierający funkcję, która będzie używana przez skrypt z listingu 20.3
w celu przekazania do przeglądarki zlokalizowanych łańcuchów znaków. Podobnie jak w skrypcie
zaprezentowanym na listingu 20.1, także i tu została zastosowana instrukcja switch, definiująca
dwie stałe — POWITANIE_TXT oraz WYBOR_TXT — zawierające odpowiednie łańcuchy znaków, które
zostaną wyświetlone przez kolejny skrypt.
</ul>
</body>
</html>
Rysunek 20.1.
Strona w domyślnym
języku polskim
Rysunek 20.2.
Po wybraniu
japońskich ustawień
lokalnych zostaną
wyświetlone teksty
w języku japońskim
Jeśli do tworzenia oprogramowania lub późniejszego wykonywania aplikacji jest używany system
Linux albo Mac OS X, to pakiet gettext będzie już najprawdopodobniej zainstalowany. Nie
dotyczy to jednak użytkowników systemu Windows. Aby zainstalować GNU gettext w jakimkol-
wiek systemie operacyjnym, należy wyświetlić sekcję „Downloading gettext” w witrynie
http://www.gnu.org/software/gettext/ i pobrać z niej plik przeznaczony dla odpowiedniego systemu
operacyjnego. Po zainstalowaniu pakietu GNU gettext należy skonfigurować PHP, tak by go
rozpoznało i umożliwiło korzystanie z niego.
Po zainstalowaniu pakietu GNU gettext na serwerze WWW, aby włączyć funkcję gettext()
i inne powiązane z nią funkcje w PHP w systemie Linux lub Mac OS X, należy zmienić konfi-
gurację PHP i ponownie je skompilować. W przypadku systemu Windows wystarczy włączyć już
skompilowane rozszerzenie. W celu skonfigurowania PHP w systemach Linux i Mac OS X należy
dodać następującą flagę:
--with-gettext
Po jej dodaniu należy ponownie skompilować i zainstalować PHP w standardowy sposób. Trzeba
także pamiętać o ponownym uruchomieniu serwera Apache po zainstalowaniu nowego mo-
dułu PHP.
Także w tym przypadku, po dokonaniu zmiany i zapisaniu pliku, konieczne będzie ponowne uru-
chomienie serwera Apache. Po wprowadzeniu powyższych modyfikacji w wynikach generowanych
przez funkcję phpinfo() powinna być prezentowana sekcja informująca o włączeniu wsparcia dla
pakietu GNU gettext.
Po włączeniu wsparcia dla pakietu GNU gettext następną niezbędną modyfikacją będzie utworzenie
w katalogu głównym serwera podkatalogów dla treści powiązanych z konkretnymi ustawieniami
lokalnymi. W pierwszej kolejności w katalogu głównym serwera trzeba będzie utworzyć katalog;
znajdą się w nim katalogi dla poszczególnych ustawień lokalnych (wszystkich, które mają być
obsługiwane).
Rozdział 20. Umiędzynarodawianie i lokalizowanie 437
Wewnątrz tego katalogu nadrzędnego należy utworzyć katalogi dla poszczególnych ustawień
lokalnych — nazwa każdego z nich musi się składać z dwuliterowego skrótu języka, zgodnego
ze specyfikacją ISO-639-1 i zapisanego małymi literami (na przykład „en”, „pl”, „ja”) oraz
znaku podkreślenia, a także zapisanego wielkimi literami dwuliterowego kodu kraju, zgodnego
z ISO-3166-1 (na przykład „US”, „PL”, „JP”). W każdym z tych podkatalogów należy z kolei
utworzyć kolejny katalog, o nazwie LC_MESSAGES.
A zatem aby witryna obsługiwała trzy ustawienia lokalne, używając do tego pakietu GNU gettext
i funkcji PHP gettext(), trzeba będzie utworzyć strukturę katalogów o następującej postaci:
/htdocs
/locale
/pl_PL
/LC_MESSAGES
/en_US
/LC_MESSAGES
/ja_JP
/LC_MESSAGES
W kolejnym punkcie rozdziału zostały zamieszczone informacje o plikach, które należy umieścić
w takiej strukturze plików; nie są to bowiem pliki PHP, lecz raczej pliki zawierające przetłumaczone
łańcuchy znaków, które mają być używane w zlokalizowanej witrynie.
"Project-Id-Version: 0.2\n"
"POT-Creation-Date: 2016-11-20 14:00+0500\n"
"Last-Translator: Jan Kowalski <jan_ko@jakaswitryna.com.pl\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Language: en_US\n"
438 Część IV Zaawansowane techniki PHP
# komunikat powitalny
msgid "POWITANIE_MSG"
msgstr "Welcome!"
Plik PO rozpoczyna się od pustego identyfikatora komunikatu (msgid) oraz pustego łańcucha
komunikatu (msgstr). Poniżej podawane są informacje o samym pliku oraz jego twórcy: powyższy
plik ma numer wersji 0.2, został stworzony 20 listopada 2016 roku przez Jana Kowalskiego,
zapisano go z użyciem kodowania UTF-8 i jest przeznaczony dla ustawień lokalnych en_US.
Zaprezentowane rozwiązanie wydaje się proste i faktycznie takie jest, niemniej jednak two-
rzenie i utrzymywanie plików PO cechuje ukryta złożoność — jest nią bardzo długa lista do-
stępnych opcji, takich jak te podane na stronie specyfikacji, znajdującej się pod adresem
http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files.
Zapoznanie się ze specyfikacją pakietu GNU gettext jest zalecane, podobnie zresztą jak używanie
specjalnych edytorów plików PO, gdyż istnieje jeszcze jedna niezbędna czynność, którą te pro-
gramy mogą za nas wykonać: skonwertowanie plików PO na specjalne pliki MO. „MO” to akronim
pochodzący od słów „Machine Object” — są to pliki zawierające obiekty binarne, które w efekcie są
czytane i używane przez pakiet gettext. Pliki PO, choć łatwe do tworzenia i zarządzania przez
ludzi, nie są jednak stosowane bezpośrednio przez pakiet gettext, który zamiast nich używa właśnie
plików MO.
Po zebraniu tych wszystkich operacji można uzyskać skrypt podobny do tego przedstawionego
na listingu 20.5.
$domain='messages';
bindtextdomain($domain, "./locale");
bind_textdomain_codeset($domain, 'UTF-8');
textdomain($domain);
?>
<!DOCTYPE html>
<html>
<head>
<title><?php echo gettext("POWITANIE_MSG"); ?></title>
</head>
<body>
<h1><?php echo gettext("POWITANIE_MSG"); ?></h1>
<h2><?php echo gettext("WYBOR_MSG"); ?></h2>
<ul>
<li><a href="<?php echo $_SERVER['PHP_SELF']."?locale=pl_PL"; ?>">pl_PL</a></li>
<li><a href="<?php echo $_SERVER['PHP_SELF']."?locale=en_US"; ?>">en_US</a></li>
<li><a href="<?php echo $_SERVER['PHP_SELF']."?locale=ja_JP"; ?>">ja_JP</a></li>
</ul>
</body>
</html>
i potrzeby. Można także skorzystać z frameworków PHP i sprawdzić, na ile okażą się użyteczne
w przypadku konkretnego projektu albo w jaki sposób ich twórcy zaimplementowali potrzebne
możliwości. Doskonałe przykłady można znaleźć w witrynach Zend Framework (https://
framework.zend.com/manual/2.4/en/modules/zend.i18n.translating.html) oraz Symfony (http://symfony.
com/doc/current/book/translation.html).
W następnym rozdziale
Jednym z bardzo użytecznych zastosowań PHP jest generowanie obrazków „w locie”. Rozdział 21.,
„Generowanie obrazków”, opisuje, jak można używać funkcji bibliotek graficznych do uzyskiwania
interesujących i przydatnych efektów.
Rozdział 21.
Generowanie obrazków
Do najbardziej użytecznych własności PHP należy możliwość tworzenia obrazków „w locie”. PHP
posiada pewne wbudowane funkcje informacyjne obrazka; można również zastosować bibliotekę
GD do tworzenia nowych obrazków i manipulacji już istniejącymi. W rozdziale tym opisujemy
metody stosowania funkcji obrazka w celu osiągnięcia interesujących i przydatnych efektów.
Przedstawimy dwa przykłady — tworzenie „w locie” przycisków na stronie WWW oraz rysowanie
wykresu słupkowego na podstawie danych z bazy danych MySQL.
W tym rozdziale używana będzie biblioteka GD2, lecz istnieje także jeszcze jedna popularna biblio-
teka obrazków PHP, o nazwie ImageMagick (http://www.imagemagick.org). W bibliotece PECL
— PHP Extension Class Library — znajduje się też klasa udostępniająca jej możliwości funkcjonal-
ne w formie obiektowej: http://pecl.php.net/package/imagick. ImageMagick i GD2 oferują wiele
podobnych mechanizmów, lecz w niektórych obszarach ImageMagick posiada większe możliwości.
Na przykład ImageMagick pozwala na tworzenie animowanych obrazków GIF. Jeśli jednak zajdzie
konieczność pracy z obrazkami w kolorach rzeczywistych lub renderowania efektów przezroczy-
stości, należy najpierw porównać możliwości obu wspomnianych bibliotek.
W systemie Windows obrazki PNG i JPEG są obsługiwane automatycznie, jeżeli tylko zarejestro-
wano rozszerzenie php_gd2.dll. Aby zarejestrować rozszerzenie, można skopiować plik php_gd2.dll
z katalogu instalacyjnego PHP do katalogu systemowego. Dodatkowo w pliku konfiguracyjnym
php.ini należy usunąć znak komentarza z początku wiersza:
extension=php_gd2.dll
442 Część IV Zaawansowane techniki PHP
Aby móc wykorzystywać obrazki PNG w systemie Unix, trzeba będzie zainstalować libpng oraz zlib,
dostępne, odpowiednio, pod adresami:
http://www.libpng.org/pub/png/libpng.html
http://www.zip.net/
Aby w systemie Unix współpracować z plikami JPEG, należy pobrać bibliotekę ze strony http://www.
ijg.org/ i zmienić konfigurację PHP za pomocą opcji:
--with-jpeg-dir=sciezka/do/jpeg-6b
Formaty obrazków
Biblioteka GD obsługuje formaty JPEG, PNG, GIF i inne. Więcej informacji na jej temat można
uzyskać na stronie http://libgd.github.io/. W kilku następnych punktach rozdziału przyjrzymy
się wybranym formatom obrazów.
JPEG
JPEG (wymawiane „jot-peg”) znaczy właściwie Joint Photographic Experts Group (połączona grupa
ekspertów fotograficznych) i jest nazwą grupy tworzącej standardy, a nie samego formatu graficznego.
Format plików opisywany jako JPEG w istocie oficjalnie nazywa się JFIF, co odpowiada jednemu
ze standardów stworzonych przez grupę JPEG.
Pliki JPEG są zazwyczaj stosowane do przechowywania zdjęć lub innych obrazków o wielu kolorach
lub stopniach koloru. W formacie tym jest zastosowana kompresja stratna, to znaczy zmniejszenie
wielkości pliku zdjęcia odbywa się kosztem jakości obrazka. Ponieważ pliki JPEG powinny zawierać
głównie obrazki analogowe, ze stopniowaniem kolorów, oko ludzkie może tolerować pewną utratę
jakości. Format ten nie nadaje się do rysunków liniowych, tekstu i dużych jednokolorowych bloków.
Więcej informacji na temat formatu JPEG/JFIF jest dostępnych na oficjalnej witrynie JPEG:
http://www.jpeg.org/
PNG
PNG (wymawiane „ping”) oznacza Portable Network Graphics (przenośna grafika sieciowa). Format
ten stał się następcą formatu GIF (ang. Graphics Interchange Format — format wymiany grafiki)
z powodów, które omówimy w dalszej części tego rozdziału. Witryna WWW poświęcona PNG
opisywała go kiedyś jako „superwydajny format obrazków z kompresją bezstratną”. Z powodu
bezstratnej kompresji ten format obrazków jest odpowiedni dla obrazków zawierających tekst,
linie proste i proste bloki kolorów, takie jak nagłówki i przyciski na stronach WWW — są to te
same właściwości, dla których wcześniej stosowano format GIF. Format ten charakteryzuje się
lepszą kompresją niż GIF oraz zmienną przezroczystością, korekcją gamma i dwuwymiarowym
przeplotem.
Rozdział 21. Generowanie obrazków 443
GIF
GIF oznacza Graphics Interchange Format (format wymiany grafiki). Jest to format szeroko stoso-
wany w sieci WWW, z zastosowaniem kompresji bezstratnej, używany do przechowywania obraz-
ków zawierających tekst, linie proste lub bloki jednokolorowe.
Format GIF korzysta z palety 256 unikatowych kolorów z 24-bitowej przestrzeni kolorów RGB.
Obsługuje on także animacje i pozwala na wykorzystanie w każdej ramce animacji palety 256
kolorów. Ograniczona liczba kolorów sprawia, że GIF nie nadaje się do reprodukcji fotografii ani
innych obrazków z ciągłością kolorów, natomiast doskonale się sprawdza w przypadku prostszych
obrazków, takich jak grafiki czy logo z obszarami o jednolitej barwie.
Obrazki GIF są kompresowane za pomocą bezstratnej techniki kompresji LZW, która zmniejsza
rozmiar pliku bez utraty jego wizualnej jakości.
Tworzenie obrazków
Tworzenie rysunków w PHP składa się z czterech etapów:
1. Utworzenie kadru, w którym będzie umieszczony obrazek.
2. Rysowanie kształtów lub umieszczanie tekstu w tym kadrze.
3. Wyświetlanie ostatecznego wyniku.
4. Zwalnianie zasobów.
Na początku przedstawimy bardzo prosty przykład skryptu tworzącego obrazek (zobacz listing 21.1).
// rysowanie obrazka
imagefill($ob, 0, 0, $niebieski);
imageline($ob, 0, 0, $szerokosc, $wysokosc, $bialy);
imagestring($ob, 4, 50, 150, 'Sprzedaż', $bialy);
// wyświetlenie obrazka
header('Content-type: image/png');
imagepng($ob);
// porządki
imagedestroy($ob);
?>
Rysunek 21.1.
Skrypt tworzy
niebieskie tło,
po czym dodaje linię
i etykietę tekstową
Funkcji imagecreatetruecolor() należy przekazać dwa parametry. Pierwszym z nich jest szero-
kość nowego obrazka, a drugi to jego wysokość. Funkcja ta zwraca identyfikator nowego obrazka.
Identyfikatory te działają podobnie jak uchwyty plików.
Alternatywnym sposobem jest odczytanie istniejącego pliku obrazka, który można następnie filtro-
wać, zmieniać jego wielkość oraz dodawać do niego elementy. Można to uczynić za pomocą jednej
z następujących funkcji: imagecreatefrompng(), imagecreatefromjpeg() lub imagecreatefromgif(),
zależnie od formatu odczytywanego pliku. Każda z tych funkcji pobiera jako parametr nazwę pliku,
na przykład:
$ob = imagecreatefrompng('podstawowyobrazek.png');
Kolory obrazka można wybrać poprzez wywołanie funkcji imagecolorallocate(). Należy prze-
kazać jej identyfikator obrazka oraz wartości składników czerwonego, zielonego i niebieskiego
koloru (RGB — red green blue), który ma zostać użyty.
Na listingu 21.1 przedstawiono zastosowanie dwu kolorów — niebieskiego i białego, zdefiniowa-
nych przez wywołanie:
$bialy = imagecolorallocate($ob, 255, 255, 255);
$niebieski = imagecolorallocate($ob, 0, 0, 255);
Funkcja zwraca identyfikator koloru, który może zostać zastosowany do późniejszego dostępu do
tego koloru.
Następnie należy zastosować jedną z wielu funkcji umożliwiających rysowanie w obrazku, zależnie
od tego, co ma zostać narysowane — linie, obszary, wielokąty czy też tekst.
Funkcje rysujące ogólnie wymagają następujących wartości jako parametrów:
identyfikatora obrazka,
współrzędnych początku i czasami końca elementu, który ma zostać narysowany,
koloru rysowania,
w przypadku tekstu informacji o czcionce.
W powyższym przykładzie zastosowano trzy funkcje rysujące. Poniżej jest opisana każda z nich.
Ważną informacją jest to, że utworzono niebieskie tło rysowania za pomocą funkcji imagefill():
imagefill($ob, 0, 0, $niebieski);
Funkcja ta pobiera jako parametry identyfikator obrazka, początkowe współrzędne obszaru rysowa-
nia (x i y) oraz kolor wypełnienia.
Współrzędne obrazka rozpoczynają się od lewego górnego rogu, który posiada współrzędne
x=0 i y=0. Prawy dolny róg obrazka to x=$szerokosc, y=$wysokosc. W grafice komputerowej
jest to standardowe podejście, lecz w konwencjach grafiki matematycznej stosuje się podejście
odwrotne, o czym trzeba zawsze pamiętać!
Następnie zostaje narysowana linia od lewego górnego rogu (0, 0) do prawego dolnego rogu
($szerokosc, $wysokosc) obrazka:
imageline($ob, 0, 0, $szerokosc, $wysokosc, $bialy);
Funkcja ta pobiera jako parametr identyfikator obrazka, punkt początkowy x i y linii, punkt końcowy
oraz kolor.
Ostatecznie do wykresu zostaje dodana etykieta:
imagestring($ob, 4, 50, 150, 'Sprzedaż', $bialy);
Funkcja imagestring()1 pobiera nieco inne parametry. Jej prototyp jest następujący:
int imagestring(int ob, int czcionka, int x, int y, string tekst, int kolor)
1
Trzeba pamiętać, że prezentowana tu funkcja imagestring() nie obsługuje zbioru znaków UTF-8, który obecnie
jest najczęściej stosowany. Jeśli zatem nasze strony lub skrypty są pisane z wykorzystaniem tego sposobu kodowania,
to do wyświetlania łańcuchów znaków na generowanych obrazkach konieczne będzie zastosowanie funkcji
imagettftext(), która potrafi obsługiwać UTF-8. Więcej informacji na ten temat można znaleźć w dokumentacji PHP
dostępnej na stronie http://php.net/manual/en/function.imagettftext.php.
446 Część IV Zaawansowane techniki PHP
Pobiera ona jako parametry identyfikator obrazka, czcionkę, współrzędne początku zapisu tekstu
x i y, tekst oraz kolor.
Dobrym powodem stosowania jednego z alternatywnych zbiorów funkcji jest to, że tekst wpisany
za pomocą funkcji imagestring() i funkcji z nią związanych, takich jak imagechar() (zapisuje
w obrazku znak), nie posiada wygładzonych konturów. Funkcje TrueType i PostScript tworzą tekst
o wyrównanych krawędziach (ang. anti-aliasing).
Aby upewnić się co do różnic pomiędzy tymi dwoma typami zapisu, warto spojrzeć na rysunek 21.2.
W miejscach występowania w literach linii zakrzywionych tekst bez anti-aliasingu wydaje się poszar-
pany. Krzywa lub kąt są uzyskane poprzez efekt „schodków”. W obrazku z anti-aliasingiem krzywe
i kąty zostają wygładzone poprzez umieszczenie dodatkowych pikseli o kolorach pośrednich między
tłem a tekstem.
Rysunek 21.2.
Zwykły tekst sprawia wrażenie
nierównego, zwłaszcza jeśli jest
napisany czcionką o dużej wielkości.
Anti-aliasing wygładza krzywe i kąty
w literach
Zazwyczaj przy odczytywaniu pliku w przeglądarce serwer WWW przesyła najpierw typ MIME.
Dla strony HTML lub PHP (po wykonaniu) pierwszą przesłaną informacją będzie:
Content-type: text/html
W tym przypadku przeglądarka powinna otrzymać informację, że przesłany zostanie obrazek, a nie
zwyczajowy kod HTML. Stosuje się do tego funkcję header(), która przesyła nieprzetworzone
łańcuchy nagłówków HTTP. Ważną własnością funkcji header() jest to, że nie może ona zostać
wykonana, jeżeli dla strony tej został już wysłany inny nagłówek HTTP. PHP automatycznie wy-
syła nagłówek HTTP, gdy skrypt zacznie produkować dane do wyświetlenia w przeglądarce. Jeżeli
więc występuje jakakolwiek instrukcja echo lub nawet puste miejsce przed znacznikiem otwierającym
PHP, nagłówki zostaną wysłane, a przy próbie wywołania funkcji header() ukaże się ostrzeżenie.
Można jednak wysyłać kilka nagłówków HTTP poprzez wielokrotne wywołania funkcji header()
w tym samym skrypcie, chociaż wciąż wszystkie muszą występować przed wyświetleniem ja-
kichkolwiek informacji w przeglądarce.
Rozdział 21. Generowanie obrazków 447
Powyższe wywołanie wysyła do przeglądarki obrazek w formacie PNG. Jeżeli miał on być wysłany
w innym formacie, należy wywołać imagejpeg() — gdy włączona jest obsługa plików JPEG.
W tych przypadkach należy również wysłać odpowiedni nagłówek:
header('Content-type: image/jpeg');
Drugą opcją, alternatywną w stosunku do wszystkich poprzednich, jest zapisanie obrazku w pliku
zamiast wyświetlenia go w przeglądarce. Można to zrobić, dodając do funkcji imagepng() (lub po-
dobnej funkcji dla innych obsługiwanych formatów) drugi opcjonalny parametr:
imagepng($ob, $nazwapliku);
Należy pamiętać, że obowiązują tutaj wszystkie zwyczajowe zasady zapisywania pliku w PHP
(na przykład prawidłowe ustawienie uprawnień).
Metody pierwsza i druga zostały już opisane. Opiszmy więc teraz metodę trzecią. Aby zastosować
tę metodę, należy włączyć obrazek w kod HTML poprzez umieszczenie wewnątrz kodu następu-
jącego znacznika obrazka:
<img src="prostywykres.php" height="200" width="200" alt="Sprzedaż spada" />
Zamiast bezpośrednio umieszczać obrazek PNG, JPEG lub GIF, należy w znaczniku SRC ulokować
skrypt PHP tworzący obrazek. Dane te zostaną poprawnie odczytane i dodane do kodu, jak przed-
stawiono na rysunku 21.3.
448 Część IV Zaawansowane techniki PHP
Rysunek 21.3.
Obrazek tworzony
dynamicznie w oczach
użytkownika nie różni
się niczym od zwykłego
W tym przykładzie jednak przyciski zostaną utworzone za pomocą pustego szablonu przycisku.
Pozwala on na stosowanie efektów, np. spadzistych krawędzi, które łatwiej jest utworzyć za pomocą
Photoshopa, GIMP-a lub innych narzędzi graficznych, a nie w HTML-u i CSS. Stosując bibliote-
kę obrazków PHP, można rozpocząć od rysunku podstawowego i kreślić na jego powierzchni.
W przykładzie tym zostaną również zastosowane czcionki TrueType w celu uzyskania tekstu
z anti-aliasingiem. Funkcje czcionek TrueType cechują się osobliwościami, które opisano poniżej.
Podstawowy proces sprowadza się do pobrania pewnego tekstu i utworzenia przycisku z wyświe-
tlonym danym tekstem. Tekst zostanie wyśrodkowany na przycisku zarówno poziomo, jak i pio-
nowo; zostanie też zastosowany największy pasujący rozmiar czcionki.
W celu wykonania testów i eksperymentów został utworzony fronton generatora przycisków. Jego
interfejs jest przedstawiony na rysunku 21.4 i stanowi jedynie proste rozwiązanie służące do prze-
syłania paru zmiennych do skryptu PHP.
Ten typ interfejsu może zostać zastosowany w celu automatycznego generowania stron WWW.
Poniższy skrypt można również wywołać w sposób ciągły w celu generowania wszystkich przyci-
sków witryny WWW „w locie”, lecz wówczas trzeba by wykorzystać pamięć podręczną, aby skrócić
czas wykonywania.
Rozdział 21. Generowanie obrazków 449
Rysunek 21.4.
Fronton umożliwia
użytkownikowi
wybranie koloru
przycisku
oraz wprowadzenie
żądanego tekstu
Rysunek 21.5.
Przycisk generowany
przez skrypt
tworz_przycisk.php
Przycisk jest generowany przez skrypt o nazwie tworz_przycisk.php, przedstawiony na listingu 21.2.
Listing 21.2. tworz_przycisk.php — skrypt ten może zostać wywołany przez formularz projektuj_przycisk.html
lub wewnątrz znacznika obrazka HTML
<?php
// sprawdzenie, czy dostępne są odpowiednie dane zmiennych
// zmienne to tekst_przycisku oraz kolor
$tekst_przycisku = $_POST['tekst_przycisku'];
$kolor = $_POST['kolor'];
450 Część IV Zaawansowane techniki PHP
if (empty($tekst_przycisku) || empty($kolor))
{
echo '<p>Stworzenie obrazka niemożliwe - formularz wypełniony niepoprawnie.</p>';
exit;
}
$szerokosc_obrazka = imagesx($ob);
$wysokosc_obrazka = imagesy($ob);
// Natomiast w przypadku systemu Unix należy podać pełną ścieżkę do katalogu czcionki.
//putenv('GDFONTPATH=/usr/share/fonts/truetype/dejavu');
do
{
$rozmiar_czcionki--;
if ($wysokosc_tekstu>$wysokosc_obrazka_bez_marginesow ||
$szerokosc_tekstu>$szerokosc_obrazka_bez_marginesow)
{
// żaden możliwy do odczytania rozmiar czcionki nie pasuje
echo '<p>Wprowadzony tekst nie pasuje do przycisku.</p>';
}
else
{
// odnaleziono pasujący rozmiar czcionki
// teraz należy obliczyć jego współrzędne
Rozdział 21. Generowanie obrazków 451
if ($tekst_lewy < 0)
{
$tekst_x += abs($tekst_lewy); //Dodanie współczynnika do lewej pozycji
}
header('Content_type: image.png');
imagepng($ob);
}
// zwolnienie zasobów
imagedestroy($ob);
?>
Jest to jeden z najdłuższych skryptów opisanych do tej pory w tej książce. Poniżej znajduje się opis
jego poszczególnych części. Na początku przeprowadzono podstawowe sprawdzenie błędów, po
czym został ustawiony kadr.
Na początku z superglobalnej tablicy $_POST odczytywany jest kolor oraz ustawiony zostaje nowy
identyfikator obrazka, oparty na odpowiednim przycisku:
$kolor = $_POST['kolor'];
Jednak jeszcze przed utworzeniem identyfikatora obrazka skrypt sprawdza, czy zostały określone
wartości zmiennych $tekst_przycisku oraz $kolor, a jeśli którakolwiek z nich jest pusta, to skrypt
kończy działanie, wyświetlając odpowiedni komunikat. W przeciwnym razie skrypt przechodzi do
wykonania kolejnej czynności, którą jest utworzenie identyfikatora nowego obrazka:
$ob = imagecreatefrompng($kolor.'-przycisk.png');
Funkcja imagecreatefrompng() pobiera jako parametr nazwę pliku PNG, a zwraca nowy identyfi-
kator pliku zawierającego kopię tego pliku PNG. Należy zauważyć, że funkcja ta nie modyfikuje
w żaden sposób bazowego pliku PNG. W ten sam sposób stosuje się funkcje imagecreatefromjpeg()
i imagecreatefromgif(), jeżeli zainstalowana jest odpowiednia obsługa.
Na początku ustawione zostają pewne powiązane zmienne. Pierwsze dwie to wysokość i szerokość
obrazka przycisku:
$szerokosc_obrazka = imagesx($ob);
$wysokosc_obrazka = imagesy($ob);
Następne dwa przedstawiają marginesy od krawędzi przycisku. Obrazki przycisków mają ścięte
końcówki, tak więc należy zostawić miejsce dookoła końców tekstu. Przy stosowaniu różnych
obrazków liczba ta będzie inna! W podanym przykładzie margines po każdej stronie wynosi około
18 pikseli.
$szerokosc_obrazka_bez_marginesow = $szerokosc_obrazka – (2 * 18);
$wysokosc_obrazka_bez_marginesow = $wysokosc_obrazka – (2 * 18);
Należy również ustawić początkowy rozmiar czcionki. W tym przypadku jest to 32 (właściwie 33,
ale liczba ta zostanie zaraz zmniejszona), mniej więcej największy rozmiar pasujący do przycisku:
$rozmiar_czcionki = 33;
W przypadku stosowania GD2 należy wskazać, gdzie znajdują się czcionki — służy do tego
zmienna środowiskowa GDFONTPATH:
// W przypadku systemu Windows będzie to:
putenv('GDFONTPATH=C:\WINDOWS\Fonts');
// Natomiast w przypadku systemu Unix należy podać pełną ścieżkę do katalogu czcionki.
//putenv('GDFONTPATH=/usr/share/fonts/truetype/dejavu');
Konieczne jest również określenie, która czcionka ma zostać użyta. Zmierzamy użyć tę czcionkę
z funkcjami TrueType, które odszukają plik czcionki w podanej lokalizacji i dodadzą do jej nazwy
rozszerzenie .ttf (ang. True Type Font).
$nazwa_czcionki = 'arial';
Jeśli w systemie nie ma czcionki Arial (którą wykorzystujemy w tym przykładzie), można zasto-
sować inną czcionkę typu TrueType.
Następnie zostaje wykonana pętla do…while, zmniejszająca rozmiar czcionki przy każdej iteracji,
dopóki wprowadzony tekst nie będzie odpowiednio pasował do przycisku:
do
{
$rozmiar_czcionki--;
Kod ten sprawdza rozmiar tekstu przez odwołanie się do własności nazywanej ramką ograniczającą
(ang. bounding box) tekstu. Jest to wykonane za pomocą funkcji imagettfbbox(), która należy do
funkcji czcionek TrueType. Po obliczeniu wielkości zostanie wykonany napis na przycisku za
pomocą czcionki TrueType i funkcji Imagettftext().
Ramka ograniczająca fragmentu tekstu to najmniejszy prostokąt, jaki można narysować wokół tekstu.
Przykład ramki ograniczającej jest przedstawiony na rysunku 21.6.
Rysunek 21.6.
Współrzędne ramki ograniczającej podane
są względem bazowej linii pisma. Początek
układu współrzędnych jest przedstawiony
na rysunku jako (0,0)
Aby w łatwy sposób przyswoić sobie zawartość poszczególnych elementów tabeli, należy po prostu
zapamiętać, że numerowanie zaczyna się od lewego dolnego rogu ramki ograniczającej i posuwa
się przeciwnie do ruchu wskazówek zegara.
Istnieje jedna osobliwa własność wartości zwracanych przez funkcję imagettfbbox() — koordy-
naty podane względem początku układu współrzędnych. Przeciwnie do obrazków, w których
współrzędne liczone są od prawego górnego rogu, w tym przypadku są one obliczane względem
bazowej linii pisma.
Spójrzmy ponownie na rysunek 21.6. Można zauważyć, że wzdłuż dolnej części tekstu została nary-
sowana linia. Jest ona znana jako bazowa linia pisma. Niektóre litery, na przykład „p”, wykraczają
poza nią. Noszą one nazwę liter zstępujących.
454 Część IV Zaawansowane techniki PHP
Lewy koniec linii bazowej jest podstawą pomiarów — ma współrzędne X i Y równe 0. Współ-
rzędne powyżej linii bazowej posiadają dodatnią współrzędną X, a współrzędne poniżej bazy —
ujemną współrzędną X.
Dodatkowo tekst może posiadać wartości współrzędnych znajdujące się poza klatką wiążącą.
Na przykład tekst może mieć początek w punkcie o współrzędnej X równej –1.
Podsumowując powyższe rozważania można stwierdzić, że należy być uważnym przy wykony-
waniu obliczeń za pomocą tych liczb.
W tym miejscu sprawdzane są dwa warunki. Najpierw należy stwierdzić, czy tekst o danej wielkości
da się odczytać — nie ma sensu tworzenie tekstu o rozmiarze mniejszym niż 8, ponieważ odczytanie
przycisku sprawia trudność. Drugi zbiór warunków sprawdza, czy tekst będzie się mieścił w prze-
znaczonej dla niego przestrzeni rysowania.
Następnie sprawdzany jest fakt, czy obliczenia iteracyjne znalazły odpowiednią wielkość tekstu,
czy nie, i ewentualnie zostanie wyświetlony komunikat o błędzie:
if($wysokosc_tekstu>$wysokosc_obrazka_bez_marginesow ||
$szerokosc_tekstu>$szerokosc_obrazka_bez_marginesow)
{
// żaden możliwy do odczytania rozmiar czcionki nie pasuje
echo '<p>Wprowadzony tekst nie pasuje do przycisku.</p>';
}
Funkcja ta pobiera dużą liczbę parametrów, kolejno: identyfikator obrazka, wielkość czcionki
w punktach, kąt, pod którym tekst ma zostać umieszczony, współrzędne X i Y początku tekstu, kolor
tekstu, nazwę czcionki i właściwy tekst.
Plik czcionki musi być udostępniony serwerowi, nie jest zaś potrzebny w komputerze
użytkownika, ponieważ do klienta zostanie przekazany jako obrazek.
Etap końcowy
W tym momencie można już wyświetlić przycisk w przeglądarce:
header('Content_type: image.png');
imagepng($ob);
I to już wszystko! Jeżeli nie wystąpił żaden błąd, w oknie przeglądarki powinien ukazać się przycisk
podobny do przedstawionego na rysunku 21.5.
W przykładzie tym na witrynie WWW zostanie przeprowadzona ankieta na temat preferencji wybor-
czych użytkowników w fikcyjnych wyborach. Wyniki ankiety zostaną przechowane w bazie danych
MySQL, po czym za pomocą funkcji obrazka narysowany zostanie wykres słupkowy.
Tworzenie wykresów to drugie z podstawowych zastosowań tych funkcji. Wykresy mogą zostać
utworzone na podstawie dowolnych danych — sprzedaży, wyników przeszukiwania sieci WWW itd.
Na potrzeby tego przykładu została utworzona baza danych MySQL o nazwie ankieta. Zawiera ona
jedną tabelę o nazwie wyniki_ankiety, która przechowuje nazwiska kandydatów w kolumnie kandydat,
a liczbę otrzymanych przez nich głosów w kolumnie liczba_glosow. Stworzono również użyt-
kownika tej bazy danych o nazwie ankieta i haśle ankieta. Wygenerowanie tej bazy zajmuje około
pięciu minut i można to uczynić poprzez uruchomienie skryptu przedstawionego na listingu 21.3.
W tym celu należy przekazać skrypt poprzez konto root w następujący sposób:
mysql –u root –pTWOJE_HASŁO < stworzankiete.sql
456 Część IV Zaawansowane techniki PHP
USE ankieta;
Powyższa baza danych zawiera trzech kandydatów. Interfejs dla niej jest dostarczony poprzez stronę
o nazwie glosuj.html. Kod strony jest przedstawiony na listingu 21.4.
Listing 21.4. glosuj.html — w tym miejscu użytkownicy mogą dodawać swoje głosy
<!DOCTYPE html>
<html>
<head>
<title>Ankieta</title>
</head>
<body>
<h1>Dodaj swój głos</h1>
<p>Na kogo będziesz głosować w wyborach?</p>
</body>
</html>
Rysunek 21.7.
W tym miejscu
użytkownicy mogą
dodawać swoje głosy,
a po kliknięciu
przycisku ukażą się
aktualne wyniki ankiety
Ogólna idea jest taka, że po naciśnięciu przycisku głos użytkownika zostanie dodany do bazy danych.
Następnie wszystkie wyniki zostaną pobrane i wyświetli się wykres słupkowy aktualnych wyników.
Typowy wynik po dodaniu pewnej liczby głosów został przedstawiony na rysunku 21.8.
Rysunek 21.8.
Wyniki głosowania,
utworzone poprzez
narysowanie w kadrze
serii linii, prostokątów
i elementów tekstowych
Skrypt generujący ten obrazek jest dość długi. Został on podzielony na cztery części, a każdą z nich
omówimy osobno. Większość skryptu wydaje się znajoma — w poprzednich rozdziałach przed-
stawiliśmy wiele podobnych przykładów MySQL. Omówiliśmy również tworzenie jednokoloro-
wego kadru i umieszczanie w nim etykiet tekstowych.
Nowe części skryptu odnoszą się do rysowania linii i prostokątów. Na nich skupimy teraz uwagę.
Część pierwsza (czteroczęściowego skryptu) jest przedstawiona na listingu 21.5.1.
Listing 21.5.1. pokaz_wyniki.php — część pierwsza aktualizuje bazę danych i pobiera nowe wyniki ankiety
<?php
if (empty($glos))
{
echo '<p>Nie zagłosowałeś na nikogo.</p>';
exit;
}
/***************************************************
Zapytanie bazy danych odczytujące wynik ankiety
***************************************************/
while ($w_polecenie->fetch())
{
$suma_glosow += $ilosc_glosow;
}
$w_polecenie->data_seek(0);
Część pierwsza, przedstawiona na listingu 21.5.1, łączy się z bazą danych MySQL, uaktualnia głosy
zależnie od wpisu użytkownika, pobiera zapisane głosy, jak również liczy sumaryczną liczbę odda-
nych głosów. Po otrzymaniu tej informacji można rozpocząć wykonywanie obliczeń w celu stwo-
rzenia wykresu. Część druga jest przedstawiona na listingu 21.5.2.
Druga część skryptu ustawia pewne zmienne, które zostaną zastosowane do narysowania wykresu.
Obliczanie wartości tego typu zmiennych może być nudne, lecz pewien stopień przewidywania
ostatecznego kształtu obrazka uczyni proces rysowania o wiele łatwiejszym. Wartości zastosowane
powyżej były obliczone na podstawie naszkicowania pożądanego wyniku na kartce papieru i wy-
liczania odpowiednich proporcji.
Zmienna $szerokosc to całkowita zastosowana szerokość kadru. Ustawione zostają również prawe
i lewe marginesy (zmienne odpowiednio $lewy_margines i $prawy_margines), grubość i odstępy
między słupkami ($wysokosc_slupka i $odleglosc_slupkow) oraz czcionka, rozmiary czcionki
i pozycja etykiet ($nazwa_czcionki, $rozmiar_tytulu, $rozmiar_podstawowy, $rozmiar_maly
i $wciecie_tekstu).
Po zdefiniowaniu tych podstawowych wartości można dokonać kilku obliczeń. Należy narysować
bazę, z której będą brały początek wszystkie słupki. Położenie tej bazy można obliczyć za pomocą
lewego marginesu i wielkości etykiet dla współrzędnej X i przewidywanej wartości ze szkicu dla
współrzędnej Y. Jeśli zależałoby nam na elastyczności, można zamiennie sprawdzić szerokość naj-
dłuższego nazwiska.
Obliczane są również dwie ważne wartości: pierwszą z nich jest odległość na wykresie przedstawia-
jąca jedną jednostkę:
$jednostka_slupka = ($szerokosc-($x+$prawy_margines))/100; //jeden "stopień" wykresu
Jest to maksymalna długość słupków — od bazy do prawego marginesu — podzielona przez 100,
ponieważ wykres będzie pokazywał wartości procentowe.
Drugą wartością jest całkowita potrzebna wysokość kadru:
$wysokosc = $ilosc_kandydatow*($wysokosc_slupka+$odleglosc_slupkow)+50;
Jest to wysokość słupka pomnożona przez liczbę słupków plus dodatkowa przestrzeń dla tytułu.
Część trzecia jest przedstawiona na listingu 21.5.3.
Listing 21.5.3. pokaz_wyniki.php — część trzecia konfiguruje gotowy na przyjęcie danych wykres
/*********************************************
Konfiguracja podstawowego obrazka
*********************************************/
// stworzenie pustego kadru
$ob = imagecreatetruecolor($szerokosc, $wysokosc);
460 Część IV Zaawansowane techniki PHP
// Przydzielenie kolorów
$bialy = imagecolorallocate($ob,255,255,255);
$niebieski = imagecolorallocate($ob,0,64,128);
$czarny = imagecolorallocate($ob,0,0,0);
$rozowy = imagecolorallocate($ob,255,78,243);
$kolor_tekstu = $czarny;
$kolor_procentow = $czarny;
$kolor_tla = $bialy;
$kolor_linii = $czarny;
$kolor_slupka = $niebieski;
$kolor_liczb = $rozowy;
// dodanie tytułu
$tytul = 'Wyniki ankiety';
$wymiary_tytulu = imagettfbbox($rozmiar_tytulu, 0, $czcionka, $tytul);
$dlugosc_tytulu = $wymiary_tytulu[2] - $wymiary_tytulu[0];
$wysokosc_tytulu = abs($wymiary_tytulu[7] - $wymiary_tytulu[1]);
$tytul_nad_linia = abs($wymiary_tytulu[7]);
$tytul_x = ($szerokosc - $dlugosc_tytulu)/2; // wyśrodkowanie w x
$tytul_y = ($y - $wysokosc_tytulu)/2 + $tytul_nad_linia; // wyśrodkowanie w y
W części trzeciej skonfigurowany zostaje podstawowy obrazek, przydzielone kolory, po czym roz-
poczyna się rysowanie wykresu.
które rysuje czarny kontur wokół krańców kadru. Funkcja ta rysuje prostokąt niewypełniony. Para-
metry są identyczne jak w przypadku poprzedniej funkcji. Należy zauważyć, że prostokąt został
narysowany do współrzędnych $szerokosc-1 i $wysokosc-1 — kadr posiada wymiary od (0,0) do
tych wartości. Jeżeli prostokąt zostałby narysowany do $szerokosc i $wysokosc, znalazłby się na
zewnątrz kadru.
W celu wyśrodkowania i wpisania tytułu na wykresie zastosowana została ta sama logika i funkcje
jak w przypadku poprzedniego przykładu.
Rozdział 21. Generowanie obrazków 461
W tym przypadku baza została narysowana od punktu nieco powyżej miejsca, w którym zostanie
narysowany pierwszy słupek, do punktu nieco powyżej końca kadru.
W tym momencie można wypełnić wykres danymi. Część czwarta jest przedstawiona na li-
stingu 21.5.4.
Listing 21.5.4. pokaz_wyniki.php — część czwarta umieszcza dane na wykresie i kończy działanie skryptu
/*********************************************
Umieszczenie danych na wykresie
*********************************************/
// Pobranie każdego wiersza bazy danych i narysowanie odpowiadających im słupków
while ($w_polecenie->fetch())
{
if ($suma_glosow > 0) {
$procent = intval(round(($ilosc_glosow/$suma_glosow)*100));
} else {
$procent = 0;
}
imagettftext($ob,$rozmiar_podstawowy, 0, $szerokosc-$dlugosc_procentow-$wciecie_tekstu,
$y+($wysokosc_slupka/2), $kolor_procentow, $czcionka, $procent.'%');
// wyświetlenie liczb
imagettftext($ob, $rozmiar_maly, 0, $x+(100*$jednostka_slupka)-50,
$y+($wysokosc_slupka/2), $kolor_liczb, $czcionka,
$ilosc_glosow.'/'.$suma_glosow);
/*********************************************
Wyświetlenie obrazka
*********************************************/
header('Content-type: image/png');
imagepng($ob);
/*********************************************
Zwalnianie zasobów
*********************************************/
$w_polecenie->free_result();
$bd_polacz->close();
imagedestroy($ob);
?>
Część czwarta bierze po kolei kandydatów dostępnych w wynikach pobranych z bazy danych,
oblicza procent oddanych głosów oraz rysuje słupki i etykiety dla każdego kandydata.
Po raz kolejny etykiety dodawane są za pomocą funkcji imagettftext(). Słupki rysowane są jako
wypełnione prostokąty za pomocą funkcji imagefilledrectangle():
imagefilledrectangle($ob, $x, $y-2, $dlugosc_slupka, $y+$wysokosc_slupka,
$kolor_slupka);
Powyższy skrypt jest dość długi, lecz może być łatwo przystosowany do konkretnych potrzeb lub
do automatycznego tworzenia ankiet poprzez interfejs. Jedną ważną własnością skryptu, której
w tym przypadku brakuje, jest mechanizm wykrywający oszustwa. Użytkownicy szybko odkryliby
możliwość wielokrotnego głosowania i wyniki stałyby się bezwartościowe.
Podobne ujęcie można zastosować do tworzenia wykresów liniowych, a nawet kołowych, pod wa-
runkiem posiadania odpowiedniej wiedzy matematycznej.
W następnym rozdziale
W następnym rozdziale opiszemy użyteczne funkcje kontroli sesji PHP, zapewniające możliwość
zachowywania stanu w aplikacjach internetowych.
Rozdział 22.
Stosowanie kontroli sesji
w PHP
W tym rozdziale opisujemy sposoby kontroli sesji w PHP, stanowiące powszechnie wykorzysty-
waną metodę przechowywania i wielokrotnego stosowania danych związanych z konkretnym
użytkownikiem w wielu punktach tej samej aplikacji internetowej.
Ideą kontroli sesji jest zapewnienie możliwości śledzenia użytkownika podczas pojedynczej sesji
w witrynie WWW. Umożliwia ona dokonanie wielu operacji: łatwą obsługę logowania użytkow-
ników oraz wyświetlanie zawartości zależnie od poziomu uwierzytelnienia lub osobistych preferen-
cji. Dodatkowo sesje pozwalają między innymi na śledzenie zachowań użytkownika czy też zaim-
plementowanie koszyków na zakupy.
PHP udostępnia obszerny zestaw wbudowanych funkcji do kontroli sesji, jak również jedną super-
globalną tablicę $_SESSION.
Identyfikator sesji działa jak klucz pozwalający na zgłoszenie konkretnych zmiennych jako tak zwa-
nych zmiennych sesyjnych. Zawartość tych zmiennych jest przechowywana na serwerze. Identy-
fikator sesji jest jedyną informacją widoczną po stronie klienta. Jeżeli podczas konkretnego
połączenia z danym serwerem identyfikator sesji jest widoczny poprzez cookie lub URL, można
uzyskać dostęp do zmiennych tej sesji przechowywanych na serwerze. Najprawdopodobniej każdy
kiedyś korzystał z witryn, które przechowują identyfikator sesji w adresach URL. Jeśli adres URL
strony zawiera fragment danych wyglądających na losowe, to najprawdopodobniej jest to jakiś
rodzaj kontroli sesji.
Powyższe wyrażenie utworzy cookie o nazwie name i wartości wartosc. Wszystkie inne parametry
są opcjonalne. Pole expires zawiera datę, po której cookie nie będzie już ważne. Jeżeli data wyga-
śnięcia nie zostanie podana, cookie działa, dopóki nie zostanie ręcznie usunięte przez użytkownika
lub administratora. Path i domain mogą być łącznie zastosowane do określenia URL-a bądź URL-ów,
dla których ważne jest cookie. Słowo kluczowe secure oznacza, że cookie nie będzie wysyłane
poprzez zwykłe połączenie HTTP. W końcu słowo kluczowe HttpOnly oznacza, że cookie będzie
dostępne jedynie przy użyciu protokołu HTTP, a nie w klienckich językach programowania, takich
jak JavaScript.
Kiedy przeglądarka łączy się z URL-em, w pierwszej kolejności poszukuje cookies przechowywa-
nych lokalnie. Jeżeli któreś z nich jest odpowiednie dla danego URL-a, zapisane w nim informacje
zostaną przekazane na serwer.
Parametry tej funkcji odpowiadają dokładnie parametrom opisanego wcześniej nagłówka Set-Cookie.
to kiedy użytkownik odwiedzi następną stronę tej samej witryny (lub przeładuje tę samą stronę),
administrator uzyska dostęp do zawartości cookie poprzez $_COOKIE['mojecookie'].
Rozdział 22. Stosowanie kontroli sesji w PHP 465
Cookie może zostać usunięte poprzez ponowne wywołanie setcookie() z tą samą nazwą cookie
i z datą wygaśnięcia pochodzącą z przeszłości. Cookie może zostać również skonfigurowane
ręcznie za pomocą funkcji header() oraz składni cookie podanej uprzednio. Należy pamiętać, że
nagłówek cookie musi zostać wysłany przed innymi nagłówkami; w przeciwnym wypadku nie
będzie działał. Ograniczenie to wynika z istoty samych cookies, nie jest zaś ograniczeniem PHP.
Podczas sesji PHP nie trzeba ręcznie ustawiać żadnych cookies. Zajmują się tym odpowiednie
funkcje PHP, które tworzą wszystkie cookies niezbędne do rozpoczęcia sesji, kiedy tylko zażąda
tego programista, wywołując jedną z nich.
W celu obejrzenia zawartości cookie ustawionego poprzez kontrolę sesji można zastosować funkcję
session_get_cookie_params(). Zwraca ona tablicę zawierającą elementy lifetime (okres życia),
path (ścieżka) i domain (domena).
Dyrektywy session.use_trans_sid należy używać z ostrożnością, gdyż może się ona przyczy-
niać do zwiększania zagrożenia witryny. W przypadku jej włączenia użytkownik może wysłać
pocztą elektroniczną do innej osoby adres URL zawierający identyfikator sesji, adres URL może
zostać zapamiętany na komputerze dostępnym publicznie — albo stanie się dostępny w historii,
albo w zakładkach przeglądarki na powszechnie dostępnym komputerze.
Alternatywnie można także samodzielnie dodawać identyfikator sesji do wszystkich łączy, aby
był przekazywany dalej. Identyfikator sesji jest przechowywany w stałej SID. Aby przekazać tę
stałą ręcznie, należy dodać ją na końcu łącza, podobnie do parametru GET:
<a href="lacze.php?<?php echo strip_tags(SID); ?>">
Funkcja strip_tags() została tu użyta w celu uniknięcia ataków ze strony skryptów z innych stron.
1
Mechanizm zarządzania stanem w HTTP — przyp. tłum.
466 Część IV Zaawansowane techniki PHP
Należy zauważyć, że etapy te nie muszą dotyczyć jednego skryptu, a niektóre z nich mogą wystę-
pować w wielu. Każdy z nich jest omówiony poniżej.
Rozpoczynanie sesji
Zanim możliwe będzie skorzystanie z możliwości, jakie zapewnia sesja, należy ją rozpocząć. Mo-
żemy to zrobić na dwa sposoby.
Sprawdza ona istnienie identyfikatora sesji. Jeżeli identyfikator ten nie istnieje, zostanie stworzony,
a także udostępniona zostanie superglobalna tablica $_SESSION. Jeżeli sesja już istnieje, funkcja
session_start() pobierze zmienne sesyjne, aby można było z nich skorzystać. Z tego względu
kluczowe jest wywoływanie session_start() na początku każdego skryptu korzystającego z sesji.
Jeżeli funkcja session_start() nie zostanie wywołana, wówczas w kodzie skryptu żadne dane zapi-
sane w ramach sesji nie będą dostępne.
Drugim sposobem zainicjowania sesji jest takie skonfigurowanie PHP, aby rozpoczynał ją automa-
tycznie, kiedy ktoś odwiedza daną stronę. Można to uczynić za pomocą dyrektywy konfiguracyjnej
session.auto_start w pliku php.ini — takie rozwiązanie zostanie opisane w dalszej części rozdzia-
łu. Z metodą tą wiąże się jedna poważna niedogodność: po włączeniu auto_start nie można używać
obiektów w charakterze zmiennych sesyjnych. Przyczyną jest to, że definicja klasy takiego obiektu
musi zostać załadowana przed rozpoczęciem sesji, ponieważ tylko wtedy można tworzyć
obiekty w ramach sesji.
Utworzona zmienna sesyjna będzie śledzona do końca sesji lub dopóki nie zostanie usunięta ręcznie.
Ważność sesji może również wygasnąć w sposób naturalny, zależnie od ustawienia dyrektywy
session.gc_maxlifetime w pliku php.ini. Dyrektywa ta wskazuje określoną w sekundach ilość
czasu, przez którą sesja będzie ważna, zanim zostanie zakończona przez mechanizm odśmiecania.
Rozdział 22. Stosowanie kontroli sesji w PHP 467
Jeżeli jako zmienna sesyjna używany jest obiekt, ważne jest, aby przed wywołaniem session_start()
dołączyć definicję klasy w celu ponownego załadowania zmiennych sesyjnych. Dzięki temu PHP
będzie wiedzieć, w jaki sposób zrekonstruować obiekt sesji.
Z drugiej strony należy być ostrożnym przy sprawdzaniu, czy zmienne sesyjne zostały ustawione
(na przykład za pomocą isset() lub empty()). Warto pamiętać, że zmienne mogą być ustawione
przez użytkownika za pomocą metody POST lub GET. Można sprawdzić zmienną w celu określenia,
czy jest to zgłoszona zmienna sesyjna, poprzez sprawdzenie wartości $_SESSION.
Nie powinno się podejmować prób usunięcia całej tablicy $_SESSION, ponieważ w efekcie całko-
wicie zablokuje to sesję. Aby usunąć wszystkie zmienne sesyjne naraz, można użyć poniższej
instrukcji, która wyczyści wszystkie elementy tablicy $_SESSION:
$_SESSION = array();
Kiedy sesja wykonała już wszystkie działania, należy usunąć wszystkie zmienne, po czym wywołać
session_destroy();
Na pierwszej stronie nastąpi rozpoczęcie sesji oraz zostanie zarejestrowana zmienna $_SESSION
['zmienna_sesyjna']. Kod tej strony przedstawiono na listingu 22.1.
?>
<p><a href="strona2.php">Następna strona</a></p>
Zmienna została zgłoszona i nadano jej wartość. Wynik uruchomienia powyższego skryptu przed-
stawiono na rysunku 22.1.
Rysunek 22.1.
Początkowa wartość
zmiennej sesyjnej
przedstawiona przez
stronę strona1.php
Ostateczna wartość zmiennej na stronie to ta, która będzie dostępna na kolejnych stronach. Na końcu
skryptu zmienna sesyjna zostaje zamrożona do czasu jej ponownego pobrania poprzez następne
wywołanie session_start().
unset($_SESSION['zmienna_sesyjna']);
?>
<p><a href="strona3.php">Następna strona</a></p>
Po zastosowaniu zmiennej usuwamy ją funkcją unset(). Sesja nadal trwa, lecz zmienna $_SESSION
['zmienna_sesyjna'] nie jest już zgłoszona.
Ostatecznie następuje przejście do strony strona3.php, ostatniego skryptu tego przykładu. Kod tego
skryptu jest przedstawiony na listingu 22.3.
Jak pokazano na rysunku 22.3, nie można już uzyskać dostępu do wartości zmiennej $_SESSION
['zmienna_sesyjna'].
Rozdział 22. Stosowanie kontroli sesji w PHP 469
Rysunek 22.2.
Wartość zmiennej
sesyjnej została
przekazana przez
identyfikator sesji
stronie strona2.php
session_destroy();
?>
Rysunek 22.3.
Usunięta zmienna
nie jest już dostępna
Pełną listę ustawień konfiguracyjnych związanych z sesjami można znaleźć w dokumentacji PHP
dostępnej na stronie http://php.net/manual/en/session.configuration.php.
470 Część IV Zaawansowane techniki PHP
Przykład ten składa się z trzech prostych skryptów. Pierwszy z nich, uwierz_glowny.php, obsługuje
formularz logowania i uwierzytelnianie członków danej witryny WWW. Drugi, tylko_czlonkowie.php,
wyświetla informacje jedynie członkom pomyślnie zalogowanym. Trzeci, wylog.php, dokonuje
wylogowania członka.
Aby zrozumieć zasadę działania przedstawionego przykładu, należy spojrzeć na rysunek 22.4, który
ukazuje początkową stronę wyświetlaną przez uwierz_glowny.php.
Rozdział 22. Stosowanie kontroli sesji w PHP 471
Rysunek 22.4.
Ponieważ użytkownik
jeszcze się nie zalogował,
należy wyświetlić stronę
logowania
Rysunek 22.5.
Użytkownicy
niezalogowani nie mogą
zobaczyć zawartości
strony; zamiast tego
otrzymają następującą
wiadomość
Jeżeli jednak użytkownik wcześniej zaloguje się (za pomocą nazwy użytkownika testowy i hasła
haslo), a następnie spróbuje zobaczyć stronę członkowską, otrzyma wynik przedstawiony na ry-
sunku 22.6.
Rysunek 22.6.
Po zalogowaniu się
użytkownik może
zobaczyć obszary
członkowskie
472 Część IV Zaawansowane techniki PHP
Najpierw przeanalizujmy kod źródłowy aplikacji. Większość kodu tej aplikacji należy do pliku
uwierz_glowny.php. Skrypt ten jest przedstawiony na listingu 22.4. Poniżej znajduje się szczegó-
łowy opis.
if (mysqli_connect_errno()) {
echo 'Połączenie z bazą danych nie powiodło się: '.mysqli_connect_error();
exit();
}
$wynik = $bd_lacz->query($zapytanie);
if($wynik->num_rows)
{
// jeżeli dane są w bazie, zarejestrowanie identyfikatora użytkownika
$_SESSION['prawid_uzyt'] = $iduzytkownika;
}
$bd_lacz->close();
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Stron główna</title>
<style type="text/css">
fieldset {
width: 50%;
border: 2px solid #ff0000;
}
legend {
font-weight: bold;
font-size: 125%;
}
label {
width: 125px;
float: left;
text-align: left;
font-weight: bold;
}
input {
border: 1px solid #000;
padding: 3px;
}
button {
margin-top: 12px;
}
</style>
Rozdział 22. Stosowanie kontroli sesji w PHP 473
</head>
<body>
<h1>Strona główna</h1>
<?php
if(isset($_SESSION['prawid_uzyt']))
{
echo '<p>Użytkownik zalogowany jako: '.$_SESSION['prawid_uzyt'].' <br />';
echo '<a href="wylog.php">Wylogowanie</a></p>';
}
else
{
if(isset($iduzytkownika))
{
// jeżeli próba logowania była nieudana
echo '<p>Zalogowanie niemożliwe.</p>';
}
else
{
// nie było próby logowania lub nastąpiło wylogowanie
echo '<p>Użytkownik niezalogowany.</p>';
}
// tworzenie formularza logowania
echo '<form method="post" action="uwierz_glowny.php">';
echo '<fieldset>';
echo '<legend>Proszę się zalogować!</legend>';
echo '<p><label for="iduzytkownika">Identyfikator użytkownika:</label>';
echo '<input type="text" name="iduzytkownika" id="iduzytkownika" size="30"/></p>';
echo '<p><label for="haslo">Hasło:</label>';
echo '<input type="password" name="haslo" id="haslo" size="30"/></p>';
echo '</fieldset>';
echo '<button type="submit" name="login">Logowanie</button>';
}
?>
<br />
<a href="tylko_czlonkowie.php">Część członkowska</a>
</body>
</html>
Działania skryptu skupiają się na zmiennej sesyjnej $prawid_uzyt. Podstawowa idea jest taka,
że jeżeli ktoś zaloguje się pomyślnie, zostanie zgłoszona zmienna sesyjna o nazwie $_SESSION
['prawid_uzyt'], zawierająca identyfikator tej osoby.
Pierwszą czynnością wykonaną przez skrypt jest wywołanie funkcji session_start(). Funkcja ta
pobierze zmienną sesyjną $prawid_uzyt, jeżeli została ona zarejestrowana.
Podczas pierwszego wykonania skryptu żaden z warunków if nie będzie spełniony, tak więc użyt-
kownik zostanie skierowany do jego części końcowej. Otrzyma on informację o braku zalogowania
oraz przedstawiony mu zostanie odpowiedni służący do tego formularz:
echo '<form method="post" action="uwierz_glowny.php">';
echo '<fieldset>';
echo '<legend>Proszę się zalogować!</legend>';
echo '<p><label for="iduzytkownika">Identyfikator użytkownika:</label>';
echo '<input type="text" name="iduzytkownika" id="iduzytkownika" size="30"/></p>';
echo '<p><label for="haslo">Hasło:</label>';
echo '<input type="password" name="haslo" id="haslo" size="30"/></p>';
474 Część IV Zaawansowane techniki PHP
echo '</fieldset>';
echo '<button type="submit" name="login">Logowanie</button>';
echo '</form>';
$wynik = $bd_lacz->query($zapytanie);
Otwarte zostaje połączenie z bazą danych MySQL i wykonane zapytanie, po czym następuje
sprawdzenie identyfikatora użytkownika i hasła. Jeżeli stanowią one pasującą do siebie parę w bazie
danych, zostaje zgłoszona zmienna $_SESSION['prawid_uzyt']. Zawiera ona identyfikator tego
konkretnego użytkownika, tak więc znana jest jego tożsamość i istnieje możliwość jego dalszego
śledzenia.
if($wynik->num_rows)
{
// jeżeli dane są w bazie, zarejestrowanie identyfikatora użytkownika
$_SESSION['prawid_uzyt'] = $iduzytkownika;
}
$bd_lacz->close();
}
Ponieważ tożsamość użytkownika jest już znana, nie trzeba wyświetlać ponownie formularza logo-
wania. Zamiast tego użytkownikowi zostanie wyświetlona jego nazwa oraz otrzyma on możliwość
wylogowania się:
if(isset($_SESSION['prawid_uzyt']))
{
echo '<p>Użytkownik zalogowany jako: '.$_SESSION['prawid_uzyt'].' <br />';
echo '<a href="wylog.php">Wylogowanie</a></p>';
}
Jeżeli nastąpiła nieudana z jakiegoś powodu próba logowania, dostępny jest identyfikator użytkow-
nika, lecz nie zmienna $_SESSION['prawid_uzyt'], tak więc można wyświetlić komunikat o błędzie.
if(isset($iduzytkownika))
{
// jeżeli próba logowania była nieudana
echo '<p>Zalogowanie niemożliwe.</p>';
}
Rozdział 22. Stosowanie kontroli sesji w PHP 475
Oto skrypt główny. Poniżej została przedstawiona strona członkowska. Kod tego skryptu zapre-
zentowano na listingu 22.5.
Listing 22.5. tylko_czlonkowie.php — kod części członkowskiej witryny WWW sprawdza uprawnionych
użytkowników
<?php
session_start();
?>
<!DOCTYPE html>
<html>
<head>
<title>Część członkowska</title>
</head>
<body>
<h1>Część członkowska</h1>
<?php
// sprawdzenie zmiennej sesyjnej
if(isset($_SESSION['prawid_uzyt']))
{
echo '<p>Użytkownik zalogowany jako '.$_SESSION['prawid_uzyt'].'.</p>';
echo '<p>Oto zawartość dostępna tylko dla członków.</p>';
}
else
{
echo '<p>Użytkownik niezalogowany.</p>';
echo '<p>Tylko zalogowani użytkownicy mogą oglądać tę stronę.</p>';
}
?>
</body>
</html>
Powyższy kod jest bardzo prosty. Wszystko, co wykonuje, to sprawdzanie za pomocą $_SESSION
['prawid_uzyt'], czy aktualna sesja zawiera zgłoszonego użytkownika. Jeżeli użytkownik jest
zalogowany, można wyświetlić mu zawartość przeznaczoną tylko dla członków; w przeciwnym
przypadku otrzymuje on komunikat o braku zalogowania.
Ostatnim skryptem jest wylog.php, który dokonuje wylogowania użytkownika z systemu. Kod tego
skryptu jest przedstawiony na listingu 22.6.
Listing 22.6. wylog.php — ten skrypt usuwa zmienną sesyjną i niszczy sesję
<?php
session_start();
<h1>Wylogowanie</h1>
<?php
if (!empty($stary_uzyt))
{
echo '<p>Zostałeś wylogowany.</p>';
}
else
{
// jeżeli brak zalogowania, lecz w jakiś sposób uzyskany dostęp do strony
echo '<p>Użytkownik niezalogowany, tak więc brak wylogowania.</p>';
}
?>
<p><a href="uwierz_glowny.php">Powrót do strony głównej</a></p>
</body>
</html>
Kod jest bardzo prosty, lecz zawiera pewien subtelny szczegół. Sesja zostaje rozpoczęta, po czym
w zmiennej zapisywana jest stara nazwa użytkownika, usuwa się zmienną prawidłowego użytkow-
nika i następuje zniszczenie sesji. Następnie użytkownik otrzymuje wiadomość zależną od tego, czy
został wylogowany, czy wylogowanie było niemożliwe lub czy poprzednio się nie zalogował.
Powyższy prosty zestaw skryptów będzie podstawą dużej części pracy przedstawionej w następ-
nych rozdziałach.
W następnym rozdziale
W następnym rozdziale odejdziemy na chwilę od głównych zagadnień opisywanych w tej książce,
by przyjrzeć się nieco dokładniej pisaniu skryptów działających w przeglądarce, a konkretnie tech-
nologii AJAX, która pozwala kodowi pisanemu w języku JavaScript na prowadzenie komunikacji
z serwerem. Komunikacja z serwerem oznacza jednocześnie komunikację ze skryptami PHP uru-
chamianymi na tym serwerze, dlatego też przyjrzymy się żądaniom generowanym przez klienty
oraz odpowiedziom przesyłanym przez serwery.
Rozdział 23.
Integracja JavaScriptu i PHP
W tym rozdziale przedstawione zostaną sposoby stosowania języka JavaScript do interakcji ze
skryptami PHP wykonywanymi na serwerze w celu realizacji akcji, które nie wymagają pełnego
odświeżania strony w przeglądarce.
Powodem, dla którego z punktu widzenia twórców aplikacji internetowych AJAX jest interesującą
i potężną technologią, jest słowo odpowiadające pierwszej literze akronimu: technologia AJAX
pozwala na wykonywanie żądań asynchronicznych. W praktyce oznacza to, że istnieje możliwość
przesyłania na serwer, na którym są wykonywane skrypty PHP, żądań generowanych przez
skrypty JavaScript, i to bez konieczności odświeżania całych stron wyświetlanych w przeglądarce.
Proces ten pozwala na tworzenie aplikacji internetowych zapewniających użytkownikom doskonałe
wrażenia i bardzo przypominających klasyczne aplikacje komputerowe, a jednocześnie pozwala
na implementację interfejsu użytkownika w sposób modularny, którego nie można byłoby uzyskać
w przypadku stosowania zwyczajnych, pełnych żądań, sprawiających, że strona za każdym razem
jest pobierana i odświeżana w całości.
Termin „AJAX” stał się popularny w roku 2003, kiedy to implementacje języka JavaScript
w większości nowoczesnych przeglądarek zaczęły obsługiwać możliwości generowania żądań
asynchronicznych, realizowanych przy użyciu klasy XMLHttpRequest (czasami określanej jako
XHR). Niemniej jednak obecnie, w nowoczesnej erze aplikacji internetowych, zamiast tych nisko-
poziomowych API powszechnie stosowane są wszechstronne i działające we wszystkich przeglądar-
kach frameworki JavaScript. Na potrzeby tego rozdziału do przedstawienia sposobów wykorzystania
technologii AJAX do komunikacji z serwerem WWW zostanie użyty bardzo popularny frame-
work JavaScript — jQuery.
478 Część IV Zaawansowane techniki PHP
Framework jQuery nie tylko sam oferuje ogromne możliwości, lecz jest także bardzo elastycz-
ny i rozszerzalny, gdyż udostępnia całą kolekcję wysokiej klasy wtyczek. Dzięki tym wtyczkom
dostępna jest większość możliwości funkcjonalnych, których programista może potrzebować w apli-
kacji. W tym rozdziale wykorzystywane będą jedynie podstawowe możliwości samego frameworka
jQuery (określane jako jQuery Core), a w szczególności te związane z technologią AJAX.
W celu zapewnienia jak największej przenaszalności kodu w tej książce wykorzystane zostanie
to drugie rozwiązanie.
A zatem zapewnienie możliwości użycia jQuery w aplikacji internetowej sprowadza się do dołącze-
nia tej biblioteki do dokumentu HTML przy wykorzystaniu poniższego znacznika <script>,
odwołującego się do jej najnowszej wersji:
<script scr="//code.jquery.com/jquery-3.1.1.min.js" />
Warto zwrócić uwagę na to, że w powyższym adresie URL został pominięty używany protokół
(na przykład http://). Jest to rozwiązanie celowe, informujące przeglądarkę, że zasób, do którego
adres URL się odwołuje, należy pobrać przy użyciu protokołu zdefiniowanego przez dokument
nadrzędny. A zatem jeśli strona została pobrana przy wykorzystaniu protokołu https://, to zostanie on
także użyty do pobrania biblioteki. Dzięki zastosowaniu takiego rozwiązania można uniknąć
ewentualnych komunikatów o zagrożeniach, które przeglądarka mogłaby wyświetlać na przykład
w przypadku pobierania niebezpiecznego zasobu na stronie pobranej przy użyciu bezpiecznego
protokołu.
Rozdział 23. Integracja JavaScriptu i PHP 479
Wczytanie tej jednej biblioteki jQuery pozwala aplikacji internetowej na korzystanie z jej pełnych
możliwości! W kilku kolejnych punktach rozdziału przedstawione zostaną podstawowe pojęcia
związane z biblioteką jQuery oraz jej możliwości.
Ten uchwyt do przestrzeni nazw jQuery jest używany za każdym razem, kiedy chcemy skorzystać
z możliwości biblioteki. Niemniej jednak wpisywanie jQuery za każdym razem byłoby dość
niewygodne, dlatego też jQuery tworzy nazwę zastępczą (tak zwany alias), która pozwala odwoły-
wać się do jQuery przy użyciu symbolu $. To właśnie ta nazwa zastępcza będzie stosowana
w tym rozdziale, gdyż to ona jest przeważnie wykorzystywana podczas tworzenia aplikacji z uży-
ciem biblioteki jQuery.
Aby lepiej wyjaśnić, jak działają selektory, w pierwszej kolejności przedstawimy (na listingu 23.1)
prosty dokument HTML, który będzie podstawą do dalszych rozważań.
<hr/>
<div id="konsolaW3">
<h3>Konsola WWW</h3>
</div>
<script src="//code.jquery.com/jquery-3.1.1.min.js"></script>
</body>
</html>
Jednak w przypadku wybierania wielu elementów zazwyczaj nie określa się listy poszczególnych
elementów przy użyciu operatora # i identyfikatorów. Znacznie częściej wybiera się grupę elemen-
tów należących do określonej klasy, niezależnie od ich identyfikatorów. Używa się do tego selekto-
rów klasy, które mają następującą postać:
var personaliaElem = $('.imieinazw');
Nie powinno być większym zaskoczeniem, że istnieje także możliwość wybierania elementów
na podstawie ich typu. Na przykład gdyby konieczne było zwrócenie całej zawartości dokumentu
HTML, można by ją pobrać przy użyciu następującego wywołania:
var cialoDokumentu = $('body');
Rozdział 23. Integracja JavaScriptu i PHP 481
Powyższe wywołanie zwraca pierwszy element <input> znaleziony w dokumencie. Gdyby koniecz-
ne było ograniczenie wyszukiwania do pierwszego elementu <input> znajdującego się w konkret-
nym formularzu, to można by to zrobić, łącząc ich selektory w następujący sposób:
var pierwszePole = $('#mojFormularz input:first');
Dzięki zastosowaniu dodatkowego pseudoselektora :even lub :odd można wybrać każdy co drugi
element ze zbioru zwracanego przez wcześniejszy selektor, przy czym mogą to być, odpowiednio,
elementy „parzyste” i „nieparzyste”:
var wierszeNP = $('tr:odd');
var wierszeP = $('tr:even');
I w końcu, choć z technicznego punktu widzenia nie ma to nic wspólnego z selektorami, w podobny
sposób można tworzyć w pamięci zupełnie nowe elementy HTML, a następnie wykonywać na
nich operacje i dodawać je do istniejącego dokumentu HTML, co właściwie odpowiada modyfiko-
waniu zawartości strony bez jej odświeżania. Na przykład załóżmy, że konieczne jest utworzenie
nowego elementu <p>. Można to zrobić błyskawicznie w poniższy sposób:
var nowyAkapit = $('<p>');
Teoretycznie przy użyciu tej techniki można by tworzyć całe sekcje dokumentu HTML, a nawet całe
dokumenty:
var nowyAkapit = $('<p>To jest <strong>bardzo ważny tekst</strong>.</p>');
W ramach przykładu przedstawiona zostanie metoda jQuery o nazwie val(), pozwalająca pro-
gramiście na pobranie lub ustawienie atrybutu elementu wejściowego:
var myInput = $('#first_name');
console.log("Wartość elementu input o identyfikatorze #imie wynosi: '
+ myInput.val());
myInput.val('Jan');
console.log("Wartość pola o identyfikatorze #imie została zmieniona na: '
+ myInput.val());
W tym przykładzie wybierany jest tylko jeden element HTML o podanym identyfikatorze, o warto-
ści imie. Niemniej jednak ponieważ selektor zawsze zwraca zbiór elementów, tej samej metody
można by używać zawsze. Aby powyższy przykład był nieco bardziej praktyczny, przedsta-
wiona zostanie druga metoda jQuery, addClass(); zgodnie z tym, co sugeruje jej nazwa, służy
ona do określania klas, do których należą elementy HTML. Poniżej zaprezentowany został
przykład użycia tej metody:
var polaPresonaliow = $('.imieinazw');
polaPresonaliow.addClass('kontrolki-formularza');
Ten przykładowy kod odnajduje wszystkie elementy HTML należące do klasy imieinazw, a następ-
nie dodaje je także do klasy kontrolki-formularza.
W bardziej praktycznych zastosowaniach, w których elementy stron mogą, lecz nie muszą ist-
nieć, bardzo dużego znaczenia nabiera sprawdzanie, czy zbiór wynikowy zwrócony na skutek
wykonania selektora zwrócił jakieś elementy, czy nie. Ponieważ z technicznego punktu widze-
nia zbiór zawierający zero elementów wciąż jest zbiorem (a zatem jego sprawdzenie w języku
JavaScript zwróciłoby wartość true), faktyczną wielkość zbioru należy odczytywać przy użyciu
właściwości length:
var nameFields = $('.imieinazw');
if(nameFields.length > 0) {
console.log("Znaleziono elementy należące do klasy 'imieinazw'.");
} else {
console.log("Nie znaleziono elementów należących do klasy 'imieinazw'.");
}
W hierarchii dokumentu HTML zdarzenia propagują z elementu źródłowego, poprzez jego elementy
nadrzędne, aż w końcu są przekazywane przez cały dokument (co wyzwala akcje we wszyst-
kich elementach), które danego zdarzenia nasłuchują. Podobnie jak w wielu innych systemach
obsługi zdarzeń, procedury ich obsługi, nazywane także funkcjami nasłuchującymi, mogą zatrzymać
propagację zdarzenia. W przypadku biblioteki jQuery podczas obsługi zdarzeń zazwyczaj
w pierwszej kolejności jest używany selektor, który pozwala wybrać odpowiednie elementy, a na-
stępnie zostaje wywołana metoda on(), pozwalająca nasłuchiwać wybranego zdarzenia i wykony-
wać odpowiednią logikę, jeśli zostanie ono zgłoszone. Jednym z najprostszych możliwych przy-
kładów obsługi zdarzeń jest nasłuchiwanie zdarzenia ready, zgłaszanego przez jQuery, kiedy
cały dokument wraz ze wszystkimi zasobami zostanie prawidłowo pobrany:
$(document).on('ready', function(event) {
// Kod do wykonania po zakończeniu pobierania dokumentu
});
Podobnie jak większość innych metod jQuery, także i metoda on() może być wywoływana na
rzecz dowolnego selektora. Na przykład aby reagować na każde kliknięcie łącza, można by
dołączyć funkcję nasłuchującą do każdego znacznika <a> z atrybutem href i nasłuchiwać zda-
rzeń click:
$('a').on('click', function(event) {
// Czynności wykonywane za każdym razem po kliknięciu elementu <a> HTML.
});
Metoda on() jest uniwersalnym sposobem kojarzenia zdarzeń z procedurami ich obsługi, jednak
zarówno dla wygody, jak i ze względów historycznych jQuery udostępnia także cały zestaw podob-
nych metod kojarzących funkcje nasłuchujące z konkretnymi zdarzeniami. Na przykład wywo-
łania $(document).on('ready', ...) oraz $(document).ready(...) dają identyczne rezultaty.
W zależności od początkowego selektora może się zdarzyć, że będziemy chcieli stworzyć jedno
zdarzenie dla wielu elementów HTML, lecz po jego zgłoszeniu operować wyłącznie na jednym
elemencie, który je zgłosił. Jak można było zauważyć w dwóch ostatnich przykładach, domknięcie
przekazane w celu obsługi zdarzenia posiadało jeden parametr: event. Parametr ten jest obiektem
zdarzenia tworzonym w momencie jego zgłaszania; jego właściwość target zawiera odwołanie
do konkretnego elementu strony, który zgłosił zdarzenie. A zatem wybraną operację, na przykład na
klikniętym przycisku, można wykonać w następujący sposób:
$('button').on('click', function(event) {
var przycisk = $(event.target);
I podobnie, w szczególności dla niektórych rodzajów zdarzeń, takich jak zdarzenie click elementu
HTML <a>, domyślna funkcja nasłuchująca może wykonywać czynności, które nie powinny
zostać wykonane. Oto przykład:
$('a').on('click', function(event) {
var link = $(event.target).attr('href');
console.log("Kliknięte łącze prowadziło do adresu URL: " + link);
});
Logicznie rzecz biorąc, powyższy fragment kodu powinien zapewniać możliwość nasłuchiwa-
nia na zdarzenia click, pobierania wartości atrybutu elementu źródłowego przy użyciu metody
attr(), a następnie wyświetlanie wartości tego atrybutu w konsoli przeglądarki. I choć powinno
tak być, to jednak powyższy kod nie będzie działał w taki sposób, gdyż istnieje domyślne działanie
skojarzone z kliknięciem elementu (a konkretnie: zmiana strony wyświetlanej w przeglądarce
na zasób o podanym adresie URL). Powyższy kod będzie działał właśnie w taki sposób, gdyż
484 Część IV Zaawansowane techniki PHP
niezależnie od tego, czy kod poprawnie nasłuchuje zdarzenia, będzie ono propagowane w górę
dokumentu HTML, co w końcu doprowadzi do zastosowania domyślnego sposobu jego obsługi.
Aby temu zaradzić, trzeba uniemożliwić propagację zdarzenia, używając do tego metody
preventDefault(), dostępnej w każdym obiekcie zdarzenia. Poniższy przykład przedstawia fragment
kodu korzystający z tej metody, który będzie działał zgodnie z oczekiwaniami:
$('a').on('click', function(event) {
preventDefault();
Jak wspomniano we wcześniejszej części rozdziału, istnieje wiele różnych zdarzeń, których można
nasłuchiwać i które można obsługiwać — jest ich zbyt wiele, by można było je wszystkie
szczegółowo opisać w tym rozdziale. Niemniej jednak tabela 23.1 zawiera listę kilku najczęściej
używanych zdarzeń, które można obsługiwać przy wykorzystaniu frameworka jQuery.
<hr/>
<div id="konsolaW3">
<h3>Konsola WWW</h3>
</div>
<script src="//code.jquery.com/jquery-3.1.1.min.js"></script>
<script>
var konsolaW3 = function(msg) {
var konsola = $('#konsolaW3');
var nowyKomunikat = $('<p>').text(msg);
konsola.append(nowyKomunikat);
}
$(document).on('ready', function() {
$('#imie').attr('placeholder', 'Jan');
$('#nazwisko').attr('placeholder', 'Kowalski');
});
$('#mojFormularz').on('submit', function(event) {
var imie = $('#imie').val();
var nazwisko = $('#nazwisko').val();
konsolaW3("Przesłano formularz!");
alert("Witaj, użytkowniku " + imie + " " + nazwisko + "!");
});
$('.imieinazw').on('focusout', function(event) {
var poleForm = $(event.target);
konsolaW3("Wartość pola o identyfikatorze '" +
poleForm.attr('id') +
"' została zmieniona na: '" +
poleForm.val() +
"'");
});
</script>
</body>
</html>
Jak widać, w powyższym dokumencie HTML zostały wprowadzone znaczące zmiany — dodano
do niego kilka procedur obsługi zdarzeń jQuery, których zadaniem jest ożywienie tego statyczne-
go dokumentu. Pierwszą z tych funkcji jest konsolaW3(), której definicja została przedstawiona
poniżej:
486 Część IV Zaawansowane techniki PHP
Ta funkcja będzie używana w innych miejscach naszej aplikacji w celu wyświetlania na bieżąco
informacji o realizacji skryptu. Jej działanie polega na dodawaniu nowego akapitu (elementu <p>)
wewnątrz początkowo pustego elementu <div> o identyfikatorze konsolaW3 za każdym razem,
gdy konieczne jest wyświetlenie nowego komunikatu. To wspaniały przykład zastosowania
jQuery do wyboru elementów, tworzenia nowej zawartości, a następnie modyfikowania wyświetlo-
nego dokumentu HTML z poziomu kodu JavaScript.
Po przedstawieniu tej funkcji pomocniczej nadszedł czas, by zająć się opisem kluczowych możliwo-
ści funkcjonalnych prezentowanej aplikacji jQuery. Pierwszą z nich będzie funkcja wykonywana
po zakończeniu wczytywania dokumentu:
$(document).on('ready', function() {
$('#imie').attr('placeholder', 'Jan');
$('#nazwisko').attr('placeholder', 'Kowalski');
});
Po usunięciu miejsca wprowadzania z pola formularza (na przykład dlatego, że użytkownik chce
wprowadzić dane w innym polu) zostanie wygenerowane zdarzenie focusout, co z kolei spowoduje
wywołanie powyższej funkcji. Funkcja ta sprawdza (przy użyciu właściwości target przekazanego
obiektu zdarzenia) element, który doprowadził do zgłoszenia zdarzenia, a następnie wyświetla
komunikat, wywołując w tym celu przedstawioną wcześniej funkcję konsolaW3(). W rezultacie stro-
na jest aktualizowana na bieżąco za każdym razem, gdy użytkownik zmieni zawartość dowolnego
pola formularza. Efekty działania tej procedury obsługi zdarzeń zostały pokazane na rysunku 23.1.
Ostatnim zdarzeniem obsługiwanym w powyższej aplikacji jest zdarzenie submit, generowane
w efekcie przesłania formularza. Zastosowany selektor określa, że funkcja nasłuchująca powinna
zostać skojarzona wyłącznie z formularzem o identyfikatorze mojFormularz:
$('#mojFormularz').on('submit', function(event) {
var imie = $('#imie').val();
var nazwisko = $('#nazwisko').val();
konsolaW3("Przesłano formularz!");
alert("Witaj, użytkowniku " + imie + " " + nazwisko + "!");
});
Rozdział 23. Integracja JavaScriptu i PHP 487
Rysunek 23.1.
Wykonywanie
czynności
na formularzu
obsługiwanym
przez jQuery
generuje rejestr
operacji
wyświetlany
u dołu strony
Ze względów demonstracyjnych także ta procedura obsługi zdarzeń jest bardzo prosta, a jej
działanie ogranicza się do pobrania wartości z obu pól formularza i pokazania ich w okienku
dialogowym przeglądarki wyświetlanym przy użyciu funkcji JavaScript alert(). Następnie funkcja
aktualizuje samą stronę WWW, wyświetlając na niej informacje podane w formularzu.
Ten przykład kończy krótką prezentację frameworka jQuery. W żadnym razie nie można jej uznać
za wyczerpującą, jednak pozwoli ona Czytelnikowi zrozumieć zagadnienia opisywane w dalszej
części rozdziału, takie jak zastosowanie jQuery do komunikacji z serwerem WWW przy użyciu
technologii AJAX.
Stosowanie jQuery,
technologii AJAX i skryptów PHP
Oprócz wszystkich ogromnych możliwości związanych z manipulowaniem dokumentami HTML
jQuery udostępnia także programistom cały zestaw możliwości funkcjonalnych przeznaczonych
do prowadzenia asynchronicznej komunikacji z serwerami WWW. Możliwości te są wbudowane
w implementację języka JavaScript w przeglądarkach WWW, jednak jQuery znacznie ułatwia ich
wykorzystywanie, gdyż implementacje stosowane w poszczególnych przeglądarkach różnią się
nieco od siebie, a jQuery wyodrębnia ich kluczowe cechy, udostępniając spójny interfejs API.
Aby zacząć korzystać z technologii AJAX przy użyciu jQuery, należy zapoznać się z przedstawio-
nym w dalszej części rozdziału prostym przykładem aplikacji do obsługi internetowych pogawędek.
Aplikacja ta będzie zapewniać wielu użytkownikom możliwość jednoczesnego prowadzenia
pogawędek oraz odbierania wiadomości, i to bez konieczności odświeżania okna przeglądarki.
A zatem przed rozpoczęciem pisania skryptu PHP konieczne jest utworzenie tabeli MySQL. Poniżej
przedstawione zostało polecenie CREATE, które tworzy bazę danych o nazwie czat, a w niej tabelę
wiadomosci_czatu:
CREATE DATABASE czat;
USE czat;
Ta bardzo prosta baza tabela przechowuje podstawowe metadane o każdej wiadomości oraz jej
treść. Tabela składa się z czterech kolumn: liczby, która w unikalny sposób identyfikuje każdy
rekord tabeli, samej wiadomości, identyfikatora sesji użytkownika, który przesłał wiadomość,
oraz liczby całkowitej reprezentującej uniksowy znacznik czasu określający, kiedy wiadomość
została wysłana. Identyfikator sesji PHP jest ważny, gdyż na jego podstawie aplikacja będzie
określać, czy dana wiadomość została wysłana przez użytkownika przeglądającego pogawędkę,
czy też przez kogoś innego.
Listing 23.3 przedstawia kompletny kod skryptu służącego do tworzenia i wyświetlania pogawędki;
poniżej został on dokładnie opisany i wyjaśniony.
date_default_timezone_set('UTC');
if (mysqli_connect_errno()) {
echo '<p>Błąd: Nie można nawiązać połączenia z bazą danych.<br />
Proszę spróbować później.</p>';
exit;
}
try {
$aktualnyCzas = time();
$idSesji = session_id();
$czasSprawdzenia = isset($_SESSION['czas_sprawdzenia']) ?
$_SESSION['czas_sprawdzenia'] : $aktualnyCzas;
switch($akcja) {
case 'pobierz':
$polecenie = $polaczenieBD->prepare($zapytanie);
$polecenie->bind_param('s', $czasSprawdzenia);
$polecenie->execute();
$polecenie->bind_result($id, $wiadomosc, $idSesji, $date_created);
$wynik = $polecenie->get_result();
$noweWiadomosci = [];
while($wiadomosc = $wynik->fetch_assoc()) {
if($idSesji == $wiadomosc['wyslana_przez']) {
$wiadomosc['wyslana_przez'] = 'ja';
} else {
$wiadomosc['wyslana_przez'] = 'inny';
}
$noweWiadomosci[] = $wiadomosc;
}
$_SESSION['czas_sprawdzenia'] = $aktualnyCzas;
print json_encode([
'sukces' => true,
'wiadomosci' => $noweWiadomosci
]);
exit;
case 'wyslij':
$polecenie = $polaczenieBD->prepare($zapytanie);
$polecenie->bind_param('ssi', $wiadomosc, $idSesji, $aktualnyCzas);
$polecenie->execute();
Powyższy prosty serwer pogawędek rozpoczyna się od włączenia sesji oraz buforowania poprzez
wywołanie funkcji, odpowiednio, session_start() i ob_start(). Następnie ustawiony jest nagłówek
odpowiedzi Content-Type, któremu zostaje przypisana wartość applicatio/json, dzięki czemu
490 Część IV Zaawansowane techniki PHP
klient będzie wiedział, że odpowiedź zawiera dokumenty JSON. Poza tym, aby wymusić stosowanie
spójnych znaczników czasu, wywoływana jest funkcja date_default_timezone_set().
Po wykonaniu tych podstawowych operacji zostaje utworzone połączenie z bazą danych MySQL,
po czym skrypt sprawdza, czy zostało ono nawiązane prawidłowo. Jeśli wystąpiły jakieś problemy,
to skrypt zostaje natychmiast zakończony, gdyż brak połączenia z bazą danych na tym etapie
oznacza pogawędkę, w której nie będzie żadnych wiadomości.
Jeśli natomiast uda się prawidłowo nawiązać połączenie z bazą, to kolejną operacją, którą zostanie
wykonana, będzie określenie sposobu obsługi bieżącego żądania. W przypadku żądań HTTP
GET zostanie pobrana lista wszystkich wiadomości, które jeszcze nie zostały wyświetlone danemu
użytkownikowi. Wiadomości te zostaną zwrócone i wyświetlone w przeglądarce. W przypadku
żądań HTTP POST (czyli przesłania formularza) skrypt zapisze nową wiadomość, która następnie zo-
stanie rozesłana do wszystkich innych użytkowników.
Bez względu na rodzaj żądania skrypt zawsze zwraca obiekt JSON, w którym jest dostępny
klucz o nazwie sukces, zawierający wartość logiczną true lub false, zależnie od tego, czy operacja
zakończyła się pomyślnie, czy nie. W razie niepowodzenia do obiektu zostanie także dodany
klucz blad, zawierający komunikat błędu. W przypadku obsługi żądania HTTP GET zwracany
obiekt będzie zawierał również klucz wiadomosci z listą wszystkich wiadomości, które powinny
zostać wyświetlone w przeglądarce danego użytkownika.
Skrypt PHP przedstawiony na listingu 23.2 wykorzystuje kilka pojęć związanych z operacjami
przeprowadzanymi na bazach danych (takich jak wstawianie i pobieranie informacji z bazy),
które zostały przedstawione we wcześniejszej części książki. Niemniej jednak jego kluczowym
aspektem jest sposób, w jaki jest on wykonywany. Prawidłowe działanie aplikacji wymaga tego,
by przeglądarka wywoływała ten skrypt cyklicznie w celu aktualizacji interfejsu użytkownika
i wyświetlania w nim nowych wiadomości. Dodatkowo strona WWW stanowiąca interfejs
użytkownika aplikacji będzie udostępniała możliwość przesyłania wiadomości na serwer przy
użyciu żądania AJAX, dzięki czemu będą one rozsyłane do wszystkich innych klientów biorących
udział w pogawędce. W kolejnym punkcie rozdziału zostaną przedstawione kliencka część aplikacji
oraz używane przez nią metody AJAX.
Pierwszym parametrem tej metody jest adres URL, na jaki ma zostać wysłane asynchroniczne
żądanie, a drugim — obiekt zawierający ustawienia żądania. Złożoność tej metody można określić
dopiero po przejrzeniu listy wszystkich dostępnych ustawień służących do określenia sposobu
jej działania, obsługi żądania oraz odpowiedzi. Ponieważ wszystkie te ustawienia są doskonale
opisane w internetowej dokumentacji jQuery dostępnej na stronie http://api.jqeury.com/jQuery.ajax/,
Rozdział 23. Integracja JavaScriptu i PHP 491
nie będziemy powielać informacji o nich w tej książce. Zamiast tego w dalszej części tego pod-
punktu przedstawionych zostanie kilka najczęściej stosowanych przypadków użycia oraz sposobów
ich realizacji z wykorzystaniem metody $.ajax().
Pierwszy przykład wykonuje proste żądanie HTTP GET. Właściwość success określa funkcję,
która będzie wywołana po prawidłowym wykonaniu żądania i do której zostaną przekazane dane
pobrane z odpowiedzi, jej status tekstowy oraz obiekt jQuery.
// Wykonanie żądania HTTP GET
$.ajax('/example.php', {
'method' : 'GET',
'success' : function(dane, status, jqXHR) {
console.log(data);
}
});
Kolejny przykład przedstawia sposób wykonywania żądania HTTP POST, które przesyła na serwer
jakieś dane. W przypadku prawidłowego obsłużenia żądania zostaje wywołana funkcja określona
przez właściwość success, tak samo jak to było w przypadku generacji żądań GET. Jednak w tym
przykładzie druga funkcja została określona we właściwości error. Ta funkcja zostanie wywoła-
na, gdy wystąpi błąd (na przykład gdy serwer zwróci kod statusu 500); informacje na jego temat
mogą zostać wyświetlone w interfejsie użytkownika:
// Wykonywanie żądań HTTP POST z obsługą błędów
$.ajax('/example.php', {
'method' : 'POST',
'data' : {
'myBoolean': true,
'myString' : 'To są przykładowe dane.'
},
'success' : function(dane, status, jqXHR) {
console.log(data);
},
'error' : function(jqXHR, status, zgloszonyBlad) {
console.log("Wystąpił błąd: " + zgloszonyBlad);
}
});
W przypadku chęci lub konieczności dodania do żądania jakichś nagłówków, takich jak wartości
niezbędne do uwierzytelnienia żądania, można użyć klucza headers, któremu przypisywany jest
obiekt zawierający pary nazwa-wartość, reprezentujące wszystkie potrzebne nagłówki:
// Dodawanie nagłówków do żądania GET
$.ajax('/example.php', {
'method' : 'GET',
'headers' : {
'X-my-auth' : 'SomeAuthValue'
}
success: function(dana, status, jqXHR) {
console.log(data);
}
});
W zależności od złożoności żądania AJAX oraz poziomu wymaganej kontroli nad nim może się
okazać, że do jego wykonania będzie można użyć jednej z metod pomocniczych, eliminujących
część złożoności związanych ze stosowaniem metody $.ajax() i wszystkich jej ustawień. Kolejny
podpunkt tego rozdziału zawiera opis tych uproszczonych metod AJAX oraz sposobów ich stosowa-
nia w celu wykonywania żądań; po tej prezentacji zostanie przedstawiona następna część aplikacji
do prowadzenia internetowych pogawędek.
Poniżej przedstawiony został dość prosty sposób wykonywania żądań HTTP GET w celu pobrania
zasobu z serwera WWW:
// Uproszczony sposób wykonywania żądań GET
$.get('/przyklad.php', {
'parametrZapytania' : 'wartośćParametru'
}, function(dane, status, jqXHR) {
console.log(dane);
});
W przypadku stosowania metody $.get() w jej wywołaniu przekazywane są adres URL żądanego
zasobu, wszelkie parametry żądania (w formie zwyczajnego obiektu JavaScript) oraz funkcja zwrot-
na, którą należy wykonać po pomyślnym zakończeniu żądania. Jeśli podczas obsługi żądania wystą-
pią jakieś błędy, metoda $.get() zakończy się bez sygnalizowania jakichkolwiek nieprawidłowości.
Jak można się było spodziewać, istnieje także metoda $.post(), która działa w identyczny sposób
(z tą różnicą, że zamiast żądań GET generuje żądania POST):
// Uproszczony sposób generowania żądań POST
$.post('/przyklad.php', {
'parametrPost' : 'wartość parametru'
}, function(dane, status, jqXHR) {
console.log(dane);
);
Istnieją również dwie dodatkowe metody pomocnicze, które mogą się okazać przydatne w pewnych
okolicznościach. Pierwsza z nich, $.getScript(), pozwala na dynamiczne pobranie dokumentu
JavaScript z serwera i wykonanie go, i to przy użyciu tylko jednego wiersza kodu:
$.getScript('/sciezka/do/skryptu/moj.js', function() {
// Skrypt moj.js został wczytany i teraz można używać
// wszelkich zdefiniowancyh w nim funkcji i obiektów.
});
Rozdział 23. Integracja JavaScriptu i PHP 493
W podobny sposób druga z metod, $.getJSON(), pozwala przesłać na podany adres URI żądanie
HTTP GET, przetworzyć zwrócony dokumenty JSON i przekazać go do określonej funkcji zwrotnej:
// Wczytanie dokumentu JSON przy użyciu żądania HTTP GET
$.getJSON('/przyklad.php', {
'parametrJSON' : 'wartośćParametru'
}, function(dame, status, jqXHR) {
console.log(dame.status);
});
Po tym krótkim wstępie prezentującym możliwości obsługi żądań AJAX przy użyciu biblioteki
jQuery możemy przejść do przedstawienia klienckiej części aplikacji do prowadzenia pogawędek
internetowych.
.bubble-recv:after
{
content: '';
494 Część IV Zaawansowane techniki PHP
position: absolute;
border-style: solid;
border-width: 15px 15px 15px 0;
border-color: transparent #AEE5FF;
display: block;
width: 0;
z-index: 1;
left: -15px;
top: 12px;
}
.bubble-recv:before
{
content: '';
position: absolute;
border-style: solid;
border-width: 15px 15px 15px 0;
border-color: transparent #000000;
display: block;
width: 0;
z-index: 0;
left: -16px;
top: 12px;
}
.bubble-sent
{
position: relative;
width: 330px;
height: 75px;
padding: 10px;
background: #00E500;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
border: #000000 solid 1px;
margin-bottom: 10px;
}
.bubble-sent:after
{
content: '';
position: absolute;
border-style: solid;
border-width: 15px 0 15px 15px;
border-color: transparent #00E500;
display: block;
width: 0;
z-index: 1;
right: -15px;
top: 12px;
}
.bubble-sent:before
{
content: '';
position: absolute;
border-style: solid;
border-width: 15px 0 15px 15px;
border-color: transparent #000000;
display: block;
width: 0;
Rozdział 23. Integracja JavaScriptu i PHP 495
z-index: 0;
right: -16px;
top: 12px;
}
.spinner {
display: inline-block;
opacity: 0;
width: 0;
.has-spinner.active {
cursor:progress;
}
.has-spinner.active .spinner {
opacity: 1;
width: auto;
}
.has-spinner.btn-mini.active .spinner {
width: 10px;
}
.has-spinner.btn-small.active .spinner {
width: 13px;
}
.has-spinner.btn.active .spinner {
width: 16px;
}
.has-spinner.btn-large.active .spinner {
width: 19px;
}
.panel-body {
padding-right: 35px;
padding-left: 35px;
}
</style>
</head>
<body>
<h1 style="text-align:center">Ajaksowe pogawędki</h1>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title">Porozmawiajmy</h2>
</div>
<div class="panel-body" id="panelCzatu">
</div>
<div class="panel-footer">
<div class="input-group">
<input type="text" class="form-control" id="trescWiadomosci" placeholder="Tu wpisz
wiadomość..."/>
<span class="input-group-btn">
496 Część IV Zaawansowane techniki PHP
Kiedy strona ta zostanie wyświetlona w przeglądarce, utworzy prosty interfejs użytkownika, podob-
ny do tego przedstawionego na rysunku 23.2; oczywiście jego konkretna postać będzie zależeć
od prowadzonej konwersacji (warto zwrócić uwagę na to, że bezpośrednio po wyświetleniu strony
nie będzie na niej żadnych dymków wiadomości).
Rysunek 23.2.
Ajaksowe
pogawędki w akcji
Aby ożywić statyczny dokument HTML wczytany i wyświetlony w przeglądarce, musimy zaim-
plementować w języku JavaScript kod, który nawiąże połączenie z serwerem PHP na serwerze
i wyświetli przesłane przez niego wiadomości. Wszystkie te operacje są wykonywane przez skrypt
JavaScript o nazwie klient.js, do którego odwołuje się przedstawiony wcześniej dokument HTML.
Aby umożliwić odpytywanie skryptu PHP, należy skorzystać z mechanizmu JavaScript nazywa-
nego licznikiem czasu. Mechanizm ten opóźnia wykonanie funkcji JavaScript o podany czas,
a następnie wywołuje ją, kiedy ten czas upłynie. W przypadku prezentowanego skryptu funkcja
ta nosi nazwę odpytajSerwer i jest zdefiniowana w następujący sposób:
var odpytajSerwer = function() {
$.get('czat.php', function(wynik) {
if(!wynik.sukces) {
console.log("Błąd podczas pobierania nowych wiadomości z serwera!");
return;
}
$.each(wynik.wiadomosci, function(idx) {
var dymekCzatu;
if(this.wyslana_przez == 'ja') {
dymekCzatu = $('<div class="row bubble-sent pull-right">' +
this.wiadomosc +
'</div><div class="clearfix"></div>');
} else {
dymekCzatu = $('<div class="row bubble-recv">' +
this.wiadomosc +
'</div><div class="clearfix"></div>');
}
$('#panelCzatu').append(dymekCzatu);
});
setTimeout(odpytajSerwer, 5000);
});
}
Funkcja odpytajSerwer() musi zostać wywołana pierwszy raz w celu zapoczątkowania cyklu
odpytywania serwera, a najlepszym momentem na zrobienie tego jest moment zakończenia
wczytywania dokumentu. Z tego względu skrypt określa procedurę obsługi zdarzenia jQuery
ready i w niej wywołuje funkcję odpytajSerwer(). W celu uatrakcyjnienia interfejsu użytkownika
skrypt określa także procedurę obsługi zdarzeń click wszystkich przycisków na stronie, która
odpowiednio dodaje do tych elementów lub usuwa z nich klasę active.
$(document).ready(function() {
odpytajSerwer();
$('button').click(function() {
$(this).toggleClass('active');
});
});
Ostatnim elementem skryptu klient.js jest przedstawiony poniżej fragment służący do przesyła-
nia nowych wiadomości na serwer. Poniższy kod stanowi procedurę obsługi zdarzeń click,
skojarzoną z przyciskiem umieszczonym na stronie. Funkcja ta odczytuje wpisany komunikat,
498 Część IV Zaawansowane techniki PHP
a następnie przesyła go na serwer przy użyciu żądania HTTP POST, umożliwiając odczytanie jej
przez inne klienty biorące udział w pogawędce.
$('#wyslijWiadomoscBtn').on('click', function(zdarzenie) {
zdarzenie.preventDefault();
$.post('czat.php', {
'wiadomosc' : wiadomosc
}, function(wynik) {
$('#wyslijWiadomoscBtn').toggleClass('active');
if(!wynik.sukces) {
alert("Błąd podczas wysyłania wiadomości!");
} else {
console.log("Wiadomość została wysłana!");
$('#trescWiadomosci').val('');
}
});
});
Umieszczenie tych wszystkich stosunkowo prostych funkcji w jednym pliku JavaScript (w przypad-
ku prezentowanej aplikacji jest to plik klient.js) oraz wczytanie go na stronie WWW to wszystko,
czego potrzeba do zapewnienia poprawnego działania aplikacji czatu. Choć aplikacja ta nie zapew-
nia natychmiastowej aktualizacji interfejsu użytkownika (trzeba na nią czekać do pięciu sekund),
to jednak pozwala na prowadzenie pogawędek z przyjaciółmi w czasie rzeczywistym, i to bez
konieczności każdorazowego odświeżania całej strony.
Jeśli Czytelnik jest zainteresowany dalszym poznawaniem biblioteki jQery, powinien przeczytać
serię artykułów i poradników dostępnych na stronie http://learn.jquery.com.
W następnym rozdziale
To już niemal koniec tej części książki. Jednak zanim w ramach jej kolejnej części zajmiemy
się projektami, najpierw przedstawimy kilka użytecznych informacji dodatkowych o możliwościach
PHP, które nie znalazły się w poprzednich rozdziałach.
Rozdział 24.
Inne przydatne własności
Niektóre przydatne funkcje i własności PHP nie należą do żadnej konkretnej kategorii. Przedsta-
wiamy je w tym rozdziale.
Wiele z przykładów przedstawionych w tym rozdziale to krótkie fragmenty kodu, których Czytelnik
z powodzeniem może używać do tworzenia większych bloków kodu we własnych aplikacjach.
pobierze zawartość łańcucha i wykona zapisaną w nim instrukcję. Powyższy wiersz da taki sam
wynik jak
echo 'Witaj, świecie';
Na pierwszy rzut oka może się wydawać, że funkcja eval() nie jest zbyt użyteczna, jednak okazuje
się, że może się ona przydać w wielu sytuacjach. Na przykład można przechowywać bloki
kodu w bazie danych, po czym odczytać je i wykonać za jej pomocą. Możliwe jest również genero-
wanie kodu w pętli, po czym zastosowanie eval() do jego wykonania.
Jednak funkcja eval() najczęściej znajduje zastosowanie w systemach szablonów, które zostaną
nieco dokładniej omówione w rozdziale 25., „Stosowanie PHP i MySQL w dużych projektach”.
W przypadku stosowania systemu szablonów istnieje możliwość wczytywania kodu HTML, PHP
i zwykłego tekstu z bazy danych; następnie system szablonów dodaje do tych treści odpowiednie
formatowanie i przetwarza treści z użyciem funkcji eval() tak, jak gdyby stanowiły one zwyczajny
kod PHP.
500 Część IV Zaawansowane techniki PHP
Funkcja eval() może być także przydatna do uaktualniania lub poprawienia istniejącego kodu. Jeżeli
istnieje duży zbiór skryptów, które wymagają przewidywalnej zmiany, można (chociaż jest to nie-
zbyt wydajne) napisać skrypt zamieniający stary skrypt w łańcuch znaków. Uruchamia on regexp
w celu przeprowadzenia zmian, po czym stosuje eval(), aby wykonać zmodyfikowany skrypt.
Możliwa jest również sytuacja, że bardzo łatwowierna osoba pozwala na wpisywanie kodu PHP
w przeglądarce, po czym jest on wykonywany na serwerze, jednak odradzamy korzystanie z takiego
rozwiązania w zwyczajnych zastosowaniach.
Ta instrukcja nie zwraca żadnej wartości. Rozwiązaniem alternatywnym jest wywołanie funkcji die().
Nieco bardziej użytecznym sposobem ukończenia skryptu jest przekazanie do funkcji exit() para-
metru. Sposób ten może być stosowany do wyświetlania komunikatu o błędzie lub wykonania
funkcji przed zakończeniem skryptu. Jest ona znana programistom Perla. Na przykład:
exit('Zakończenie skryptu...');
Częściej jednak funkcje exit() i die() są stosowane wraz z operatorem OR, łączącym je z jakimś
wyrażeniem, które może zakończyć się niepowodzeniem, takim jak otwarcie pliku lub połączenie
z bazą danych:
mysql_query(zapytanie) or die('Nie można wykonać zapytania!');
W tym przykładzie komunikat: "Nie można wykonać zapytania!" zostanie wyświetlony na ekranie,
jeśli wywołanie funkcji mysql_query() zwróci wartość false. Dodatkowo zamiast prostego
wyświetlenia komunikatu o błędzie można wywołać ostatnią funkcję przed zakończeniem skryptu:
function komunikat_blad()
{
return 'Błąd MySQL: '.mysql_error();
}
mysql_query($zapytanie) or die(komunikat_blad());
Powyższe rozwiązanie może służyć jako sposób przekazywania użytkownikowi bardziej precy-
zyjnych powodów niepowodzenia skryptu, jako metoda zamykania znaczników HTML bądź też
sposób usuwania niekompletnej strony WWW z bufora wyjściowego.
Alternatywnie można napisać funkcję, która w przypadku niepowodzenia będzie wysyłać list
elektroniczny do administratora, aby wiedział, że wystąpił poważny błąd, można też dodać błąd do
pliku z raportami lub zgłosić wyjątek.
Przydatność serializacji zmniejszyła się od czasu wprowadzenia kontroli sesji. Jest ona stosowana
głównie do typów zadań, do których aktualnie można zastosować kontrolę sesji. Faktycznie funk-
cje kontroli sesji dokonują serializacji zmiennych sesji w celu przechowania ich pomiędzy żąda-
niami HTTP.
Jednak ciągle możliwa jest konieczność przechowania tablicy PHP lub obiektu w pliku lub bazie
danych. Jeżeli taka sytuacja zachodzi, istnieją dwie funkcje, które należy znać: serialize()
i unserialize().
Aby wiedzieć, co dokładnie wykonuje serializacja, należy przeanalizować dane zwracane przez
funkcję serialize(). Zamienia ona zawartość obiektu lub tablicy na łańcuch znaków.
Jeżeli na powyższych danych dokonana zostanie serializacja, po czym ukażą się one w przeglądarce,
wynik będzie następujący:
O:9:"pracownik":2:{s:5:"nazwa";s:5:"Piotr";s:13:"id_pracownika";i:5324;}
Łatwo jest dostrzec związki pomiędzy pierwotnymi danymi obiektu i danymi po serializacji.
Ponieważ dane po serializacji to zwykły tekst, można zapisać je w bazie danych lub w dowolnym
innym miejscu. Warto jednak pamiętać, by przed zapisaniem jakichkolwiek danych tekstowych
zastosować funkcję mysqli_real_escape_string(), aby zabezpieczyć wszelkie znaki specjalne.
Wymóg przeprowadzenia tej operacji jest spowodowany obecnością cudzysłowów w poprzednim
łańcuchu po serializacji.
Kolejna ważna kwestia, o której należy wspomnieć, omawiając serializację klas lub stosowanie ich
jako zmienne sesji: PHP musi znać strukturę klasy, zanim przystąpi do przywracania jej egzem-
plarza. Dlatego też przed wywołaniem session_start() lub unserialize() trzeba będzie dołączyć
instrukcją include plik z definicją klasy.
Skrypt przedstawiony na listingu 24.1 wyświetla wszystkie funkcje dostępne dla konkretnej instalacji
PHP za pomocą tych dwóch funkcji (patrz rysunek 24.1).
Listing 24.1. lista_funkcji.php — ten skrypt wyświetla wszystkie rozszerzenia dostępne dla PHP, a także
dostarcza listy funkcji dla każdego rozszerzenia
<?php
echo 'Zbiory funkcji dostępne w tej instalacji to:<br />';
$rozszerzenia = get_loaded_extensions();
foreach($rozszerzenia as $kazde_roz)
{
echo $kazde_roz.'<br />';
echo '<ul>';
$funkcje_roz = get_extension_funcs($kazde_roz);
foreach($funkcje_roz as $funkcja)
{
echo '<li>'.$funkcja.'</li>';
}
echo '</ul>';
}
?>
Informacje uzyskiwane za pomocą powyższych funkcji mogą być przydatne przy sprawdzaniu
powodzenia instalacji rozszerzenia lub przy pisaniu przenośnego kodu generującego użyteczne
komunikaty diagnostyczne podczas procesu instalacji.
Datę ostatniej modyfikacji skryptu można sprawdzić za pomocą funkcji getlastmod() (należy
dostrzec brak znaków podkreślenia w nazwie funkcji) w następujący sposób:
echo date('g:i a, j M Y',getlastmod());
Rozdział 24. Inne przydatne własności 503
Rysunek 24.1.
Skrypt lista_funkcji.php
wyświetla wszystkie
wbudowane funkcje
PHP dostępne
w używanej instalacji
Funkcja getlastmod() zwraca znacznik czasu Uniksa, który można przekazać funkcji date(), jak
w powyższym przykładzie, aby otrzymać datę do odczytania przez człowieka.
Można uzyskać dostęp i zmienić opcje za pomocą bliźniaczych funkcji ini_get() i ini_set().
Listing 24.2 przedstawia prosty skrypt z zastosowaniem tych funkcji.
$max_execution_time = ini_get('max_execution_time');
echo 'Nowy czas maksymalny wynosi '.$max_execution_time.'<br />';
?>
Funkcja ini_set() pobiera dwa parametry. Pierwszym jest nazwa opcji konfiguracyjnej pliku php.ini,
która ma zostać zamieniona, a drugim wartość, która zostanie tej opcji nadana. Funkcja zwraca
poprzednią wartość opcji.
504 Część IV Zaawansowane techniki PHP
Nie wszystkie opcje ini można ustawiać w taki sposób. Każda z tych opcji posiada poziom, na
którym może zostać ustawiona. Dostępne są następujące poziomy:
PHP_INI_USER — Wartości tego rodzaju można zmieniać w skryptach przy użyciu ini_set().
PHP_INI_PERDIR — Wartości tego rodzaju można zmieniać w plikach php.ini lub
.htaccess albo httpd.conf, jeśli używany jest serwer Apache. Fakt, że wartości te można
zmieniać w plikach .htaccess, oznacza, że można je ustawiać dla każdego katalogu
oddzielnego — stąd właśnie nazwa.
PHP_INI_SYSTEM — Wartości tego rodzaju można zmieniać w plikach php.ini lub httpd.conf.
PHP_INI_ALL — Wartości tego rodzaju można zmieniać w dowolny z wcześniej opisanych
sposobów, to jest: w skrypcie, w pliku .htaccess lub w plikach httpd.conf albo php.ini.
Pełny zestaw opcji ini oraz poziomy, na których można je ustawiać, znajdziesz w podręczniku PHP
pod adresem http://php.net/manual/en/ini.list.php.
Podświetlanie źródeł
PHP posiada wbudowany mechanizm podświetlania składni, podobnie jak wiele edytorów. Jest on
zwłaszcza przydatny przy dzieleniu się kodem z innymi osobami lub przedstawianiu go podczas
dyskusji na stronie WWW.
Funkcja highlight_string() działa w podobny sposób, z tym że pobiera jako parametr łańcuch zna-
ków, po czym wyświetla go w przeglądarce w formacie podświetlonej składni.
Kolory podświetlanej składni mogą być ustawiane w pliku php.ini. Część za to odpowiedzialna
wygląda w sposób podobny do poniższego fragmentu:
;Colors for Syntax Highlighting mode
highlight.string = #DD0000
highlight.comment = #FF8000
highlight.keyword = #007700
highlight.bg = #FFFFFF
highlight.default = #0000BB
highlight.html = #000000
Choć w wydrukowanej wersji książki kolory te nie będą widoczne, to jednak w wersji elektronicznej
będzie można je zobaczyć. Dlatego też rysunek 24.2 przedstawia kod przykładu z listingu 24.1
wyświetlony przy użyciu funkcji show_source().
Rysunek 24.2.
Funkcja
show_source()
wyświetla kod PHP,
kolorując
składnię zgodnie
z ustawieniami
konfiguracyjnymi
Zapewne pierwszy kontakt większości z Czytelników z PHP nastąpił przy okazji tworzenia projek-
tów WWW, lecz te same mechanizmy, które czynią z niego rozbudowany język skryptów WWW,
sprawiają również, że PHP jest jednocześnie silnym programem narzędziowym działającym
w wierszu poleceń.
Skrypty PHP można wykonywać w wierszu poleceń na trzy sposoby: z pliku, poprzez potok lub
bezpośrednio w wierszu poleceń.
Aby wykonać skrypt PHP w pliku, należy się upewnić, że plik wykonawczy PHP (php lub php.exe,
zależnie od systemu operacyjnego) znajduje się w ścieżce domyślnej, i wywołać go, podając jako
argument nazwę skryptu. Na przykład:
php mojskrypt.php
Plik mojskrypt.php jest zwykłym plikiem z kodem PHP, to znaczy zawiera standardową składnię
PHP zamkniętą w odpowiednich znacznikach PHP.
Aby przekazać kod za pośrednictwem potoku, można uruchomić dowolny program generujący na
wyjściu poprawny kod PHP i przekazać go potokiem do pliku wykonawczego php. W poniższym
przykładzie użyto programu echo do wygenerowania jednowierszowego programu:
echo '<?php for($i=1; $i<10; $i++) echo $i; ?>' | php
Również w tym przypadku kod PHP jest zamknięty w znacznikach PHP (<?php i ?>). Zwróćmy też
uwagę, że wykorzystywany jest tutaj program echo z wiersza poleceń, a nie konstrukcja języka PHP.
506 Część IV Zaawansowane techniki PHP
Tego typu jednowierszowy program łatwiej byłoby uruchomić bezpośrednio z wiersza poleceń,
jak w poniższym przykładzie:
php –r 'for($i=1; $i < 10; $i++) echo $i;
Tutaj sytuacja jest nieco inna. Kod PHP przekazywany w tym łańcuchu znaków nie zawiera się
między znacznikami PHP. Gdyby łańcuch ten został otoczony znacznikami PHP, wygenerowany
zostałby błąd składni.
Zakres użytecznych programów, które można by pisać z myślą o ich uruchamianiu w wierszu pole-
ceń, jest praktycznie nieograniczony. Można na przykład tworzyć instalatory samodzielnie napisa-
nych aplikacji PHP. Można naprędce sklecić prosty skrypt odpowiednio formatujący plik tekstowy
przed zaimportowaniem go do bazy danych. Można nawet napisać skrypt wykonujący dowolne
powtarzalne zadania, które można chcieć wykonywać z poziomu wiersza poleceń. Przykładem mógłby
być skrypt kopiujący wszystkie pliki PHP, obrazki i struktury tabel MySQL z testowego serwera
WWW na serwer produkcyjny.
W następnej części
Część V, „Tworzenie praktycznych projektów PHP i MySQL”, zawiera opis kilku stosunkowo skom-
plikowanych projektów korzystających z PHP i MySQL. Projekty te są przydatnymi przykładami
dla podobnych zadań występujących w rzeczywistości. Ilustrują one zastosowanie PHP i MySQL
w większych projektach.
W rozdziale 25. omówimy niektóre kwestie, z którymi można się zetknąć przy kodowaniu więk-
szych projektów za pomocą PHP. Są to zasady tworzenia oprogramowania, takie jak: projekt,
dokumentacja i zarządzanie zmianami.
Część V
Tworzenie praktycznych
projektów PHP i MySQL
508 Część V Tworzenie praktycznych projektów PHP i MySQL
Rozdział 25. Stosowanie PHP i MySQL w dużych projektach 509
Rozdział 25.
Stosowanie PHP i MySQL
w dużych projektach
We wcześniejszych częściach opisaliśmy różne składniki i zastosowania PHP i MySQL. Chociaż
przedstawione przykłady miały być interesujące i stosowne, charakteryzowały się prostotą, gdyż
składały się z kilku skryptów, których długość rzadko kiedy przekraczała 100 wierszy kodu.
Przy tworzeniu prawdziwych aplikacji WWW sporadycznie występują tak proste zadania. W pierw-
szych latach istnienia WWW „interaktywna” witryna WWW mogła zawierać jedynie formularz słu-
żący do wysyłania wiadomości poczty elektronicznej. Jednak w obecnych czasach witryny WWW
stały się aplikacjami WWW — to znaczy programami z prawdziwego zdarzenia przesyłanymi
przez WWW. Ta zmiana oznacza rewolucję w dziedzinie skali. Witryny WWW rosną od kilku
skryptów do tysięcy wierszy kodu. Projekty tej wielkości wymagają przygotowania i zarządzania
na wzór innych projektów programistycznych.
Przed obejrzeniem projektów zawartych w tej części książki przedstawimy niektóre techniki możli-
we do zastosowania w zarządzaniu dużymi projektami WWW. Tworzenie witryn i aplikacji WWW,
zarządzanie nimi oraz gwarantowanie ich skalowalności to sztuka sama w sobie, a zapewnienie
prawidłowej realizacji tych zadań bez wątpienia jest bardzo trudne; można to zauważyć, obserwując
zarówno rynek, jak i aplikacje WWW, których używamy każdego dnia, gdyż w każdej z nich można
znaleźć jakieś problemy.
Inżynieria oprogramowania jest również podejściem, którego w oczywisty sposób brakuje w wielu
projektach WWW. Dzieje się tak z dwóch głównych powodów. Pierwszym jest częste podobień-
stwo tradycyjnych sposobów tworzenia aplikacji WWW do sposobu opracowywania pisemnego
raportu. Jest to ćwiczenie ze struktury dokumentu, projektu graficznego i produkcji. Jest to para-
dygmat zorientowany na dokument, odpowiedni dla witryn o małej lub średniej wielkości. Jednakże
w razie powiększania zawartości dynamicznej w witrynach WWW do poziomu, na którym witryny
oferują raczej usługi niż dokumenty, paradygmat ten już nie obowiązuje. Wielu programistów
w ogóle nie bierze pod uwagę zastosowania inżynierii oprogramowania do projektu.
Drugim powodem, dla którego inżynieria oprogramowania często nie znajduje zastosowania, są
liczne różnice pomiędzy tworzeniem aplikacji WWW i tworzeniem zwykłych aplikacji. W przypad-
ku tworzenia aplikacji WWW występują znacznie krótsze terminy, nieustanny nacisk, aby witryna
była gotowa już teraz. Inżynieria oprogramowania opiera się na działaniu zaplanowanym, wykony-
wanym w określonym okresie. Przy opracowywaniu projektów WWW często nie ma na to czasu.
Kiedy projekt WWW nie jest zaplanowany, na końcu pojawiają się takie same problemy jak w przy-
padku każdego innego projektu oprogramowania — aplikacje pełne błędów, niedotrzymane terminy
i niemożliwy do odczytania kod. Sposobem na rozwiązanie problemów jest odnalezienie tych czę-
ści inżynierii oprogramowania, które sprawdzają się w nowej dziedzinie tworzenia aplikacji
WWW, i odrzucenie nieodpowiednich.
Należy podjąć decyzje co do procesów. Pod pojęciem procesów trzeba rozumieć: standardy
kodowania, strukturę katalogów, zarządzanie kontrolą wersji, środowisko programistyczne,
poziom i standardy dokumentacji i przydział zadań dla poszczególnych członków zespołu.
W projektach aplikacji WWW ten etap prac jest zbyt często pomijany, przez co później
traci się zbyt wiele czasu na cofanie się do wcześniejszych etapów, dostosowywanie
kodu do standardów i tworzenie dokumentacji post factum.
Trzeba stworzyć prototyp oparty na wcześniejszych informacjach, pokazać go użytkownikom,
dokonywać bezustannych iteracji i rozsądnie podchodzić do tego, co w danej organizacji
oznacza słowo „gotowe”.
W trakcie całego tego procesu należy pamiętać, że na wszystkich etapach ważne i przydatne
jest oddzielenie zawartości i logiki aplikacji. Poniżej idea ta jest opisana szerzej.
Trzeba dokonać wszystkich potrzebnych optymalizacji.
Należy testować aplikacje WWW tak samo dokładnie jak każdy inny projekt
programistyczny.
Jedną z zalet PHP jako języka jest obszerna biblioteka wbudowanych funkcji. Zawsze powinno się
sprawdzać, czy funkcja wykonująca daną operację istnieje. Zazwyczaj jej znalezienie nie zabiera
zbyt wiele czasu. Skuteczną metodą jest przejrzenie podręcznika pod kątem grup funkcji, które
można znaleźć na stronie http://php.net/manual/en/funcref.php.
Czasami programiści przypadkowo piszą ponownie funkcje, ponieważ nie sprawdzili w dokumenta-
cji, czy istniejąca funkcja oferuje pożądane przez nich możliwości funkcjonalne. Gorąco zachęcamy,
by pamiętać o połączeniu z podręcznikiem elektronicznym lub też pobrać jego aktualną wer-
sję i przeglądać ją lokalnie, i to niezależnie od swojego doświadczenia programistycznego, gdyż
stanowi ona świetne źródło informacji. Warto pamiętać, że podręcznik elektroniczny jest często
uaktualniany i zawiera uwagi oraz komentarze dodawane przez innych użytkowników. Najpraw-
dopodobniej będzie można w nim znaleźć fragmenty kodu oraz informacje nadesłane przez innych
programistów stanowiące odpowiedzi na pytania, które mogą się nasunąć po zapoznaniu się z pod-
stawowym opisem danej funkcji. Podręcznik zawiera także informacje o odnalezionych błędach
oraz sposoby radzenia sobie z nimi; niejednokrotnie pojawiają się one, jeszcze zanim błędy zostaną
oficjalnie poprawione i udokumentowane.
Niektórzy programiści posiadający inne przygotowanie językowe mogą być skłonni do pisania pew-
nych funkcji-owijaczy, które stanowią właściwie inne nazwy funkcji PHP, ale pasują do znanego
przez nich języka. Praktyka ta nazywana jest „składniowym cukrem”. Pomysł nie należy do naj-
lepszych — powoduje, że kod jest trudniejszy do odczytania i utrzymania dla innych. Podczas
nauki nowego języka należy nauczyć się jego właściwego zastosowania. Poza tym dodanie poziomu
wywołań funkcji tego typu spowoduje spowolnienie kodu. Biorąc pod uwagę opisane powyżej kwe-
stie, jest to ujęcie, którego należy unikać.
512 Część V Tworzenie praktycznych projektów PHP i MySQL
Jeżeli okazuje się, że pożądane możliwości funkcjonalne nie znajdują się w głównej bibliotece
PHP, istnieją dwie drogi postępowania. Jeśli potrzebna jest rzecz stosunkowo prosta, można napisać
własną funkcję lub obiekt. Jeżeli jednak poszukiwane możliwości funkcjonalne stanowią skom-
plikowany fragment kodu — taki jak koszyk na zakupy, system poczty elektronicznej WWW
czy też forum WWW — nie należy się dziwić, że tego typu rzeczy zostały już utworzone. Jedną
z zalet pracy w środowisku Open Source jest darmowy dostęp do kodu między innymi takich apli-
kacji. Jeżeli znajdzie się składnik podobny do pożądanego, nawet nie identyczny, można potrak-
tować jego kod źródłowy jako punkt startowy modyfikacji lub utworzenia własnego.
Jeżeli jednak konieczne okazuje się tworzenie własnych funkcji lub składników, należy poważnie
rozważyć udostępnienie ich środowisku PHP po zakończeniu pracy. Jest to zasada, dzięki której
środowisko programistów PHP jest pomocną, aktywną i kompetentną grupą.
Standardy kodowania
Większość dużych organizacji posiada własne standardy kodowania — przewodniki wybiera-
nia nazw plików i zmiennych, komentowania i wcinania kodu itd.
Ponieważ paradygmat dokumentu był przez długi czas stosowany do tworzenia aplikacji WWW,
często nie stosowano standardów kodowania w tym obszarze. W przypadku kodowaniu na własną
ręką lub w małym zespole łatwo nie docenić ważności standardów kodowania. Nie należy popeł-
niać tego błędu, ponieważ zespół i projekt mogą się powiększyć, co więcej, może to nastąpić tak
szybko, że nie uda się w odpowiednim czasie przygotować sensownej dokumentacji. Skutkiem jest
nie tylko bałagan, lecz także problemy grupy programistów, którzy nie potrafią wykorzystać istnieją-
cego kodu, przez co będą starać się samodzielnie rozwiązywać własne problemy, dodatkowo
pogarszając sytuację.
Nazwy zmiennych powinny opisywać przechowywane w nich dane. Jeżeli zmienna przechowuje
nazwisko użytkownika, należy nadać jej nazwę $nazwisko. Ogólnie rzecz biorąc, trzeba znaleźć
równowagę pomiędzy długością i czytelnością. Na przykład przechowywanie nazwiska w zmiennej
Rozdział 25. Stosowanie PHP i MySQL w dużych projektach 513
$n sprawia, że jest ona łatwa do napisania, lecz kod stanie się trudny do zrozumienia. Przechowy-
wanie nazwiska w zmiennej $nazwisko_aktualnego_uzytkownika jest bardziej informacyjne, lecz wy-
maga pisania (łatwiej wówczas popełnić błąd). W istocie jednak nie ułatwia zbytnio zrozumienia.
Należy podjąć decyzję o stosowaniu wielkich liter. Jak już wspomniano, PHP zwraca uwagę na
wielkość liter w nazwach zmiennych. Należy zadecydować, czy nazwy zmiennych będą zawierać
wyłącznie małe litery, wyłącznie wielkie litery, czy też ich połączenie, na przykład będą się rozpo-
czynały od wielkich liter. My zazwyczaj zapisujemy nazwy małymi literami, oddzielając poszcze-
gólne słowa znakami podkreślenia (metoda ta jest czasami określana jako „snake-case”1), gdyż
ten sposób najłatwiej nam zapamiętać. Niektóre firmy i organizacje mogą jednak wybierać do
zapisu nazw zmiennych składających się z wielu słów inne notacje, takie jak lowerCamelCase
lub UpperCamelCase; tak naprawdę wybór takiego czy innego sposobu zapisu nie ma większego
znaczenia — liczy się to, czy będzie on konsekwentnie stosowany w całej bazie kodu. Można
także narzucić limit liczby słów, które można zastosować w nazwie, na przykład ograniczając
ją do dwóch lub trzech.
Dobrym pomysłem jest również rozróżnienie zmiennych i stałych za pomocą wielkości liter.
Popularnym schematem jest stosowanie wyłącznie małych liter w zmiennych (na przykład $wynik),
a wyłącznie wielkich w stałych (na przykład PI).
Jedną ze złych praktyk stosowanych przez programistów jest posiadanie dwóch zmiennych o tej
samej nazwie, ale z różnym zastosowaniem wielkich liter — tylko dlatego, że jest to możliwe, na
przykład $nazwisko i $Nazwisko. Nie trzeba chyba dodawać, że ten pomysł jest bardzo niefortunny.
Należy również unikać śmiesznych schematów stosowania wielkich liter, takich jak $RzEcZ, ponie-
waż nikt nie będzie pamiętał zasad, na jakich one pracują.
Do nazw funkcji odnosi się większość powyższych rozważań dotyczących nazw zmiennych oraz kil-
ka dodatkowych. Nazwy funkcji powinny być generalnie zorientowane na czasownik. Warto roz-
ważyć wbudowane w PHP funkcje, takie jak addslashes() czy mysqli_connect(). Konwencja taka
opisuje, jaką czynność wykonają te funkcje na parametrach lub z parametrami, które zostaną im
przekazane. Schemat ten bardzo poprawia łatwość odczytania kodu. Należy zauważyć, że powyższe
dwie funkcje posiadają różne schematy postępowania z wielowyrazowymi nazwami funkcji.
Funkcje PHP są w tej kwestii niespójne, przypuszczalnie częściowo dlatego, że zostały napisane
przez dużą grupę osób, ale przede wszystkim dlatego, że wiele nazw funkcji zostało przeniesio-
nych bez zmian z różnorodnych języków i interfejsów API.
Inaczej niż w przypadku nazw zmiennych w nazwach funkcji nie zwraca się uwagi na wielkość
liter. Zalecane jest jednak konsekwentne stosowanie wybranego formatu także do zapisu nazw
funkcji w celu uniknięcia zamieszania w kodzie (lub firmie).
Zwróćmy jednak uwagę, że w sytuacji, gdy PHP udostępnia interfejs proceduralny i zorientowany
obiektowo, nazwy funkcji się różnią. Zwykle w nazwach funkcji proceduralnych używa się notacji
snake_case (moja_funkcja()), a funkcje dostępne w interfejsie obiektowym nazywane są z zastoso-
waniem tak zwanej notacji lowerCamelCase (mojaFunkcja()).
1
Zapis wężowy — przyp. tłum.
514 Część V Tworzenie praktycznych projektów PHP i MySQL
I jeszcze końcowa uwaga — w istocie nie ma znaczenia, jakie konwencje i standardy stosuje się do
tworzenia kodu, jeżeli są one spójne w całej bazie kodu i konsekwentnie używane przez cały zespół.
Komentowanie kodu
Wszystkie programy powinny być komentowane na rozsądnym poziomie. Generalnie należy doda-
wać komentarz do każdego z następujących elementów:
Pliki, niezależnie, czy są one kompletnymi skryptami, czy plikami dołączanymi —
Każdy plik powinien posiadać komentarz, określający czym jest, jakie jest jego
przeznaczenie, kto go napisał i kiedy był uaktualniany.
Funkcje — Komentarze funkcji powinny określać, jak działa dana funkcja, jakich
parametrów się spodziewa oraz co zwraca.
Klasy — Komentarze powinny opisywać przeznaczenie klasy. Metody klasy powinny
posiadać komentarze tego samego typu i poziomu co inne funkcje.
Fragmenty kodu wewnątrz skryptu i funkcji — Często przydatne okazuje się
rozpoczęcie pisania kodu od zbioru komentarzy w stylu pseudokodu, po czym następuje
wypełnienie kodem każdej części. Tak więc początkowy skrypt może przypominać
następujący:
<?
// sprawdź prawidłowość wprowadzonych danych
// wyślij do bazy danych
// wyświetl wyniki
?>
Jest to wygodne, ponieważ kiedy wszystkie części wypełnione zostaną wywołaniami funkcji
i innymi potrzebnymi elementami, kod w pewnym stopniu będzie już zawierał komentarze.
Skomplikowane fragmenty kodu i sztuczki — Kiedy stworzenie czegoś zajmuje cały
dzień, lub jest to utworzone w niezwykły sposób, należy napisać komentarz opisujący
powody takiej konstrukcji. W ten sposób można uniknąć konsternacji: „Do czego to miało
służyć?”.
Inną ogólną zasadą postępowania jest komentowanie w trakcie pracy. Nie należy odkładać tej czyn-
ności do końca projektu, gdyż gwarantujemy, że nikt jej później nie wykona, no chyba że Czytelnik
ma znacznie mniej napięty terminarz i znacznie lepszą samodyscyplinę niż my (i większość innych
programistów).
Wcinanie
Jak w każdym innym języku programowania, należy wcinać kod w sposób rozsądny i spójny. Pisa-
nie kodu źródłowego jest działaniem podobnym do nadawania odpowiedniej formy raportowi lub
listowi biznesowemu. Wcinanie ułatwia czytanie i zrozumienie kodu.
Ogólnie rzecz biorąc, każdy blok kodu należący do struktury kontrolującej powinien być wcięty
w porównaniu do otaczającego kodu. Stopień wcięcia powinien być zauważalny (powinna to być
więcej niż jedna spacja), lecz nie nadmierny. Generalnie odradza się stosowanie znaków tabulacji,
choć spory o to trwają wśród programistów od dziesięcioleci. Chociaż są łatwe w stosowaniu,
zajmują wiele miejsca na monitorach. Zazwyczaj stosuje się wcinanie o dwie lub trzy spacje.
Kwestią otwartą jest również sposób stosowania nawiasów klamrowych. Dwa najbardziej popularne
schematy są następujące:
Rozdział 25. Stosowanie PHP i MySQL w dużych projektach 515
Schemat 1.:
if(warunek) {
// zrób coś
}
Schemat 2.:
if(warunek)
{
// zrób coś
}
Wybór pomiędzy nimi jest dowolny, należy jedynie pamiętać o konsekwentnym trzymaniu się jed-
nej konwencji w celu uniknięcia zamieszania. W tej książce staramy się stosować drugi z powyż-
szych schematów.
Dzielenie kodu
Wielki monolityczny kod wywiera bardzo niekorzystne wrażenie. Niektórzy stosują jeden ogromny
skrypt, który wykonuje wszystko w jednej ogromnej instrukcji wyboru. Choć uwielbiamy instruk-
cje wyboru i uważamy, że są one bardzo użyteczne, to jednak znacznie lepszym rozwiązaniem
byłoby dzielenie kodu na funkcje i klasy i umieszczanie związanych ze sobą elementów w plikach,
które zostaną później dołączone do głównego skryptu. Można na przykład ulokować wszystkie
funkcje związane z bazami danych w pliku funkcjebd.php.
Na początku każdego projektu warto poświęcić trochę czasu na zastanowienie się nad sposobem
podzielenia projektu na składniki. Wymaga to narysowania linii pomiędzy obszarami poszczegól-
nych możliwości funkcjonalnych, dlatego nie zawsze jest to proste i szybkie; nie należy się jednak
zniechęcać, kiedy zależności te zmienią się po rozpoczęciu pracy nad projektem. Także takie
planowanie powinno być procesem iteracyjnym. Konieczne jest również podjęcie decyzji na
temat kolejności tworzenia składników, wyznaczenia struktury ich wzajemnej zależności oraz
planu czasowego tworzenia wszystkich składników.
Nawet jeżeli wszyscy członkowie zespołu będą pracować nad wszystkim elementami kodu, dobrym
pomysłem jest przydzielenie głównej odpowiedzialności za każdy składnik konkretnej osobie.
W ostateczności będzie ona odpowiedzialna za niepowodzenie danego składnika. Ktoś musi też
podjąć obowiązki menedżera kompilacji, to znaczy osoby zapewniającej istnienie i współdziałanie
wszystkich składników. Osoba ta zazwyczaj zarządza również kontrolą wersji — zostanie ona
omówiona w dalszej części tego rozdziału. Osoba ta może być menedżerem projektu bądź też pro-
gramistą, któremu wyznaczono dodatkowe zadanie.
516 Część V Tworzenie praktycznych projektów PHP i MySQL
Należy zadecydować, jak katalogi zostaną podzielone pomiędzy składniki, logikę, zawartość
i współdzielone biblioteki kodu. Struktura powinna być udokumentowana. Należy także upewnić
się, że każda osoba pracująca nad projektem posiada dostęp do tej dokumentacji, tak by była w stanie
znaleźć wszystko, czego potrzebuje.
Musicie również pamiętać, że nawet jeżeli kod jest przechowywany w miejscu dostępnym dla reszty
zespołu, pracownicy powinni zostać o tym powiadomieni. Trzeba utworzyć system dokumentacji
wewnętrznych bibliotek funkcji oraz udostępnić go pozostałym członkom zespołu.
Wyobraźmy sobie sytuację, w której następuje próba poprawienia kodu, lecz przypadkowo zostaje
on uszkodzony i nie ma możliwości przywrócenia poprzedniej wersji, niezależnie od liczby tych
prób. Albo też klient decyduje, że poprzednia wersja witryny była lepsza. Może się również zdarzyć,
że konieczne jest przywrócenie poprzedniej wersji z powodów prawnych.
Możliwa jest także inna sytuacja, w której dwaj członkowie zespołu programistycznego pracują nad
tym samym plikiem. Mogą oni otworzyć ten plik w jednym czasie, nadpisując nawzajem swoje
zmiany. Mają kopie, na których pracują lokalnie, i modyfikują je na różne sposoby. Jeżeli zachodzi
obawa wystąpienia takiego zjawiska, jeden programista może czekać, nic nie robiąc, aż inny zakończy
edycję pliku.
Wszystkie te problemy można rozwiązać za pomocą systemu kontroli wersji. Systemy te mogą
śledzić zmiany w każdym pliku w składzie, tak więc możliwe jest ustalenie nie tylko obecnego
stanu pliku, ale również jak wyglądał on w dowolnym czasie w przeszłości. Własność ta pozwala
na zamianę uszkodzonego pliku na jego ostatnią działającą wersje. Możliwe jest oznaczenie dowol-
nego zestawu kopii plików jako wersji do opublikowania. Oznacza to, że można kontynuować roz-
wijanie kodu z dostępem w dowolnym czasie do kopii aktualnej opublikowanej wersji.
Systemy kontroli wersji pomagają również w sytuacjach, w których kilku programistów pracuje
razem nad kodem. Każdy programista może otrzymać kopię kodu ze składu (nazywane jest to
wyprowadzaniem), a kiedy dokona zmian, mogą być one zapisane ponownie w składzie (wprowa-
dzanie). Systemy kontroli wersji mogą więc śledzić, kto dokonał danej modyfikacji w systemie.
Rozdział 25. Stosowanie PHP i MySQL w dużych projektach 517
Dokumentacja projektów
Możliwe jest utworzenie wielu różnych rodzajów dokumentacji dla projektów programistycznych.
Należą do nich:
dokumentacja projektowania,
dokumentacja techniczna-przewodnik programistyczny,
słownik danych (w tym dokumentacja klas),
przewodnik użytkownika (chociaż większość aplikacji WWW powinna być
samowyjaśniająca się).
518 Część V Tworzenie praktycznych projektów PHP i MySQL
Celem tej książki nie jest nauczenie sposobu tworzenia dokumentacji technicznej, lecz zasugero-
wanie sposobu ułatwienia pracy poprzez automatyzację części procesu.
Dostępnych jest także kilka rozwiązań tego typu przeznaczonych dla PHP; należą do nich następują-
ce projekty:
phpDocumentor 2, dostępny na stronie http://www.phpdoc.org/.
Produkt ten wyświetla wyniki podobne do javadoc i wydaje się, że pracuje dość dobrze.
Poza tym pracujący nad nim zespół projektowy wydaje się być najbardziej aktywny
w porównaniu z pozostałymi dwoma wymienionymi tu rozwiązaniami.
phpDox, dostępny na stronie http://phpdox.de/.
Także ten produkt generuje obszerną dokumentację kodu źródłowego oraz dodatkowe,
przydatne metryki kodu, takie jak złożoność cyklomatyczna.
Prototypowanie
Prototypowanie jest etapem pracy często stosowanym w tworzeniu aplikacji WWW. Prototyp jest
ważnym narzędziem służącym do poznawania wymagań użytkownika. Zazwyczaj jest to uprosz-
czona, częściowo działająca wersja aplikacji. Okazuje się przydatna w dyskusji z klientem i może
stanowić podstawę ostatecznego systemu. Często kilka iteracji prototypu tworzy ostateczną aplikację.
Zaletą tego podejścia jest możliwość bliskiej współpracy z klientem lub końcowym użytkownikiem.
W rezultacie zostanie utworzony system, z którego będą oni zadowoleni oraz poczują się, w pewnym
sensie, jego właścicielami.
Istnieją dwa podstawowe problemy związane z ujęciem prototypowym. Należy być świadomym,
na czym one polegają, aby możliwe było ich uniknięcie i maksymalne wykorzystanie tego podejścia.
Pierwszy problem polega na tym, że programistom często zdarza się usunąć z jakiegoś powodu kod,
który napisali. Prototypy są często pisane w pośpiechu; spoglądając z dystansu, można stwierdzić,
że prototyp nie został utworzony w sposób optymalny, a nawet dostateczny. Gorsze części kodu mogą
zostać poprawione, lecz jeżeli cała struktura jest nieprawidłowa, oznacza to poważny problem. Istotną
niedogodnością jest zwyczajowa dla aplikacji WWW ogromna presja czasu i wynikający z niej brak
możliwości naprawy poważnych błędów. Wynikiem jest marnie zaprojektowany i trudny w utrzy-
maniu system.
Można tego uniknąć przez wykonanie pewnych planów opisanych w poprzednich częściach tego
rozdziału. Należy również pamiętać, że często łatwiejsze od naprawy jest usunięcie pewnych części
i napisanie ich od nowa. Chociaż wygląda to na proces, dla którego nie ma czasu, często dzięki niemu
można zaoszczędzić wielu problemów w późniejszych etapach pracy.
Rozdział 25. Stosowanie PHP i MySQL w dużych projektach 519
Aby uniknąć tego problemu, należy wykonać plan projektu z określoną liczbą iteracji oraz datą,
po której żadne możliwości funkcjonalne nie mogą być dodane bez kreślenia nowych planów
oraz zmiany budżetu i terminów.
W przypadku małych projektów, z niewielką liczbą wierszy kodu i skryptów, oddzielanie logiki
i zawartości może się okazać niewarte trudu. Jednak gdy projekty nabierają rozmachu, podstawowym
zadaniem jest oddzielenie logiki i zawartości. Jeżeli nie zostanie to wykonane, kod stanie się znacznie
trudniejszy w utrzymaniu. Gdy zapadnie decyzja o zastosowaniu nowego projektu graficznego dla
witryny WWW, a duża część HTML-a jest osadzona w kodzie, zmiana grafiki okaże się koszmarem.
W sieci dostępnych jest kilka systemów szablonów. Jednym z najstarszych, choć prawdopodobnie
wciąż najbardziej popularnym z nich jest Smarty, dostępny pod adresem http://www.smarty.net.
Kolejnymi interesującymi rozwiązaniami tego typu są Twig (http://twig.sensiolabs.org/) oraz Plates
(http://platesphp.com/). Warto przyjrzeć się zarówno im, jak i ich alternatywom, by przekonać
się, która z opcji będzie najbardziej użyteczna dla danej organizacji.
Optymalizacja kodu
Dla programistów pochodzących ze środowiska niesieciowego optymalizacja może wydawać się
naprawdę ważna. W przypadku stosowania PHP większość czasu, który użytkownik poświęca na
oczekiwanie na aplikacje WWW, zajmuje połączenie i pobieranie. Optymalizacja kodu nie wywiera
istotnego wpływu na ten czas.
520 Część V Tworzenie praktycznych projektów PHP i MySQL
Testowanie
Przegląd i testowanie są kolejnym podstawowym punktem inżynierii oprogramowania, który często
zostaje przeoczony podczas tworzenia aplikacji WWW. Łatwo jest dwu- lub trzykrotne przetesto-
wać program i stwierdzić, że wszystko działa bezbłędnie. To często popełniany błąd. Przed uzna-
niem programu za gotowy należy upewnić się, że został on gruntownie przetestowany pod kątem
różnych scenariuszy.
Proponowane są dwie metody zredukowania liczby błędów w kodzie (niemożliwe jest usunięcie
wszystkich, można jednak wyeliminować lub zminimalizować większość z nich).
Przede wszystkim należy przyjąć praktykę przeglądu kodu w ramach zespołu. Jest to proces, w któ-
rym inny programista lub zespół programistów ogląda kod i sugeruje ulepszenia. W toku tego typu
analizy zazwyczaj zostają zasygnalizowane następujące kwestie:
niezauważone błędy,
przypadki testowania, których nie przewidział,
optymalizacja,
poprawa bezpieczeństwa,
istniejące składniki, które można zastosować w celu poprawienia danego fragmentu kodu,
dodatkowe możliwości funkcjonalne.
Rozdział 25. Stosowanie PHP i MySQL w dużych projektach 521
Nawet pracując indywidualnie, warto znaleźć „kolegę po fachu”, który znajduje się w podobnej
sytuacji, i nawzajem dokonywać przeglądu kodów.
Drugą metodą jest odnalezienie testerów aplikacji WWW, którzy reprezentują użytkowników. Pod-
stawowa różnica pomiędzy aplikacjami WWW a biurkowymi polega na tym, że nie sposób określić
grupy użytkowników korzystających z tych pierwszych. Nie można zakładać, że wszyscy użytkow-
nicy będą dobrze znali komputery. Nie można przecież dać im grubego podręcznika ani też skróconej
karty obsługi. Zamiast tego należy sprawić, aby aplikacje WWW same w sobie były zrozumiałe
i dobrze udokumentowane. Należy pomyśleć o sposobach, w jakich użytkownicy będą korzystali
z danej aplikacji. Łatwość obsługi jest bez wątpienia najważniejszą własnością aplikacji.
Jednym z często stosowanych sposobów jest posłużenie się aplikacją WWW w wersji beta. Kiedy
można założyć, że większość błędów jest już wykluczona, należy udostępnić aplikację małej grupie
użytkowników testowych i uzyskać niewielki ruch na stronie. Można zaoferować darmowe usługi
pierwszym 100 klientom w zamian za komentarze na temat witryny. Z pewnością utworzą oni pewne
kombinacje danych lub zastosowań, których nie przewidzieli programiści. Podczas tworzenia witryny
WWW dla firmy zwykle dysponuje się licznymi niedoświadczonymi użytkownikami — firma
deleguje pracowników do przeglądania tej witryny (działanie takie ma dodatkową zaletę zwiększania
poczucia własności witryny u klienta).
W następnym rozdziale
W rozdziale 26. znajduje się opis różnych typów błędów programistycznych, komunikatów o błę-
dach PHP oraz technik odnajdywania i łatwego usuwania błędów.
522 Część V Tworzenie praktycznych projektów PHP i MySQL
Rozdział 26.
Usuwanie
i rejestracja błędów
W tym rozdziale opiszemy usuwanie błędów w skryptach PHP. Osoby próbujące uruchomić
przykłady podane w książce lub znające wcześniej PHP prawdopodobnie zdążyły już wypracować
własne umiejętności i techniki eliminacji błędów. W przypadku bardziej skomplikowanych
projektów usuwanie błędów może stać się zadaniem trudniejszym. Chociaż umiejętności programi-
styczne rosną, błędy mogą dotyczyć kilku plików lub interakcji pomiędzy kodem stworzonym
przez kilka osób.
Błędy programistyczne
Niezależnie od stosowanego języka programowania można wyróżnić trzy ogólne typy typów błędów
programistycznych:
błędy składni,
błędy wykonania,
błędy logiczne.
Każdy z powyższych typów zostanie pokrótce omówiony przed przejściem do opisu pewnych
taktyk wykrywania, obsługi, unikania i usuwania błędów.
Błędy składni
Wszystkie języki posiadają zbiór zasad nazywany składnią, które dotyczą instrukcji. Zasada ta
odnosi się zarówno do języków naturalnych, takich jak polski, jak i do języków programowania,
m.in. PHP. Jeżeli instrukcja nie spełnia reguł języka, mówi się o nim, że zawiera błąd składniowy.
524 Część V Tworzenie praktycznych projektów PHP i MySQL
Jeżeli zostaną złamane zasady składni języka polskiego, istnieje duże prawdopodobieństwo, że
posługujący się nim ludzie nadal będą się rozumieli. Często reguła ta nie dotyczy języków pro-
gramowania. Jeżeli skrypt nie przestrzega zasad składni PHP — zawiera błędy składni — analiza-
tor składniowy nie zdoła przetworzyć jego części lub nawet całości. Ludzie potrafią wydobyć
informację z częściowych lub sprzecznych danych. Komputery zaś nie.
Oprócz innych zasad składnia PHP wymaga, by instrukcje kończyły się średnikami, by łańcu-
chy znaków były zapisywane w cudzysłowach lub apostrofach oraz by parametry przekazywane
do funkcji były oddzielone przecinkami i ujęte w nawiasy. Jeżeli zasady te zostaną złamane,
skrypt PHP nie będzie działał i prawdopodobnie wyświetli przy pierwszym jego uruchomieniu
komunikat o błędzie.
Jedną z wielkich zalet PHP jest wyświetlanie przydatnych komunikatów o błędach w razie jakich-
kolwiek nieprawidłowości. Komunikat o błędzie PHP zazwyczaj zawiera informację, jaki jest
jego rodzaj, w jakim pliku wystąpił oraz w którym jego wierszu.
Można dostrzec, że powyższy skrypt próbuje przekazać łańcuch znaków funkcji date(), lecz
przypadkowo ominięty został otwierający cudzysłów, który wskazuje początek łańcucha.
Proste błędy składni, takie jak powyższy, zazwyczaj są łatwe do odnalezienia. Znacznie trudniejsze
może być odnajdywanie błędów, które pojawiają się w następstwie połączenia większej liczby
plików. Poza tym trudno szuka się błędów w dużych plikach. Komunikat o błędzie składni wystę-
pującym w 1001. wierszu pliku liczącego sobie 1000 wierszy kodu może skutecznie popsuć humor
na cały dzień, lecz jednocześnie będzie stanowić delikatny sygnał sugerujący, że może warto za-
dbać o lepszą modularyzację kodu.
Błędy wykonania
Błędy wykonania są trudniejsze do odnalezienia i naprawienia. Jeżeli skrypt zawiera błąd składni,
analizator składniowy wykryje go w trakcie wykonywania kodu. Błędy wykonawcze nie są
spowodowane jedynie przez zawartość danego skryptu. Mogą one opierać się na interakcjach
pomiędzy skryptem a innymi zdarzeniami lub warunkami.
Następująca instrukcja:
require ('nazwapliku.php');
jest prawidłową instrukcją PHP. Nie zawiera ona żadnych błędów składni.
Rozdział 26. Usuwanie i rejestracja błędów 525
Jednak może ona spowodować błąd wykonania. Jeżeli instrukcja ta zostanie wykonana, a plik
nazwapliku.php nie istnieje bądź użytkownikowi skryptu odmówiono prawa odczytu, wyświetlony
zostanie komunikat o błędzie podobny do następującego:
Fatal error: require(): Failed opening required 'nazwapliku.php' (include_path='.:/usr/local/
lib/php') in /home/ksiazka/public_html/rozdzial_26/blad.php on line 2
Chociaż w kodzie nie wystąpił żaden błąd, to jednak ponieważ opiera się on na pliku, który istnieje
lub nie istnieje podczas różnych uruchomień tego kodu, może on wywołać błąd wykonania.
Poniższe trzy instrukcje są prawidłowymi instrukcjami PHP. Niestety, występując łącznie, próbują
wykonać działanie niemożliwe — dzielenie przez zero.
$i=10;
$j=0;
$k=$i/$j;
Ostrzeżenie to umożliwia łatwą poprawę błędu. Niewielu programistów dąży do napisania kodu,
który będzie miał za zadanie dzielenie przez zero, lecz zaniechanie sprawdzenia danych wprowa-
dzonych przez użytkownika często prowadzi do błędu tego typu.
Poniższy kod również może wygenerować taki sam błąd, lecz źródło tego błędu może być o wiele
trudniej zidentyfikować, ponieważ będzie on występować tylko od czasu do czasu:
$i=10;
$j=$i/$_REQUEST['wejscie'];
Jest to jeden z wielu różnych błędów wykonania, które można napotkać w trakcie testowaniu kodu.
Nietrudno również wywołać jedną z własnych funkcji, która nie istnieje w danym skrypcie, lecz
może istnieć w innym miejscu. Jeżeli kod zawiera wywołanie nieistniejącej funkcji, na przykład:
nieistniejaca_funkcja();
lub
blednie_zapisana_funkcja();
526 Część V Tworzenie praktycznych projektów PHP i MySQL
Podobnie możliwe jest wywołanie istniejącej funkcji z niewłaściwą liczbą parametrów, czego
wynikiem będzie wyświetlenie ostrzeżenia.
Funkcja strstr() pobiera dwa łańcuchy znaków — stóg, w którym należy szukać, oraz igłę,
którą należy odnaleźć. Jeżeli zamiast tego wywoła się ją w następujący sposób:
strstr();
Jednak poza, choć rzadkimi, przypadkami, w których zmienna $zmienna będzie posiadać wartość 4,
wywołanie strstr() nie nastąpi i nie pojawi się żadne ostrzeżenie. Interpreter PHP nie traci
czasu na parsowanie fragmentów kodu, które w aktualnym procesie wykonywania nie są potrzebne.
Trzeba więc zawsze uważnie testować napisane programy!
Łatwo jest wywołać funkcję w nieprawidłowy sposób, ale ponieważ wynikłe z tego komunikaty
podają konkretny wiersz i wywołanie funkcji sprawiające problemy, błąd ten jest równie łatwo
naprawić. Są one trudne do wykrycia jedynie w przypadku niewłaściwej procedury testowej,
która nie sprawdza całego wykonywanego warunkowo kodu. Podczas testowania jedną z zasad jest
wykonanie każdego wiersza kodu dokładnie jeden raz. Inną metodą jest przetestowanie wszystkich
warunków granicznych i typów wprowadzanych danych.
Funkcje takie jak fopen(), często zawodne, generalnie posiadają zwracaną wartość, która sygnalizu-
je, że wystąpił błąd. W przypadku fopen() zwrócona wartość false oznacza niepowodzenie.
Jak można się spodziewać, funkcja mysqli_connect() zwraca wartość false w przypadku wystą-
pienia błędu. Oznacza to, że łatwo jest uchwycić i obsłużyć popularne błędy.
Jeżeli normalne wykonywanie skryptu nie zostanie zatrzymane, a błędy pozostaną, skrypt będzie
próbował kontynuować interakcję z bazą danych. Próba wykonywania zapytań i odczytania
wyników bez prawidłowego połączenia MySQL da w rezultacie nieprofesjonalnie wyglądający
ekran, na którym ukażą się liczne komunikaty o błędach, widoczne dla użytkowników.
Wiele innych, często wykorzystywanych funkcji PHP związanych z MySQL, takich jak
mysqli_query(), również zwraca false, wskazując na wystąpienie błędu.
Jeżeli błąd wystąpi, można uzyskać dostęp do komunikatu o błędzie za pomocą funkcji mysqli_error()
lub do kodu błędu za pomocą mysqli_errno(). Jeżeli ostatnia wykonana funkcja MySQL nie wy-
wołała błędu, mysqli_error() zwraca pusty łańcuch, a mysqli_errno() — 0.
Na przykład przy założeniu, że połączono się z serwerem i wybrano bazę danych, następujący
fragment kodu:
$wynik=mysqli_query($db, 'select * from nie_istnieje' );
echo mysqli_errno($db);
echo '<br />';
echo mysqli_error($db);
może wyświetlić:
1146
Table 'nazwabd.nie_istnieje' doesn't exist
Warto zauważyć, że wynik tych funkcji odnosi się do ostatniej wykonanej funkcji MySQL (innej
niż mysqli_error() i mysqli_errno()). Aby poznać wynik polecenia, należy sprawdzić go przed
wywołaniem kolejnych.
Należy pamiętać o sprawdzeniu, czy żądania bazy danych zakończyły się sukcesem, przed próbą
skorzystania z wyniku. Nie ma sensu próba wykonania zapytania po nieudanym połączeniu
się z bazą danych ani też próba pobrania i przetworzenia wyników po wykonaniu nieudanego
zapytania.
Warto zapamiętać, że istnieje różnica pomiędzy nieudanym zapytaniem a zapytaniem, które nie
jest w stanie zwrócić żadnych wyników lub wywrzeć wpływu na jakikolwiek rekord.
528 Część V Tworzenie praktycznych projektów PHP i MySQL
Zapytanie SQL zawierające błędy składni SQL lub odnoszące się do nieistniejących baz danych,
tabeli lub rzędów może zakończyć się niepowodzeniem. Na przykład następujące zapytanie:
select * from nie_istnieje;
zakończy się niepowodzeniem, ponieważ tabela o takiej nazwie nie istnieje, oraz wygeneruje numer
błędu i komunikat o błędzie, dostępne za pomocą funkcji mysqli_errno() i mysqli_error().
Zapytanie SQL, które jest prawidłowe pod względem składni, a jedynie odnosi się do nieistnie-
jących baz danych, tabeli lub rzędów, generalnie nie zakończy się niepowodzeniem. Może jednak
nie zwrócić żadnych wyników, jeżeli odnosi się do pustej tabeli lub szuka nieistniejących danych.
Na przykład przy założeniu działającego połączenia z bazą danych i istnienia tablicy o nazwie t1
oraz kolumny o nazwie k1 następujące zapytanie:
select * from t1 where k1='nie w bazie danych';
Przed skorzystaniem z wyników zapytania należy sprawdzić zarówno wystąpienie błędu, jak i brak
wyników.
Chociaż istnieje ryzyko powtarzania się, konieczne jest dokładne sprawdzenie wartości zwracanej
przez funkcję próbującą dokonać interakcji z serwisem sieciowym.
zwróci ostrzeżenie, jeżeli nie powiedzie się próba jego połączenia z portem 5000 w komputerze
localhost, lecz ostrzeżenie to zostanie wyświetlone w formacie domyślnym, a w skrypcie nie będzie
możliwości obsłużenia go w elegancki sposób.
Błędy wykonania są trudniejsze do wyeliminowania niż błędy składni, ponieważ analizator skła-
dniowy nie może ich zasygnalizować przy pierwszym uruchomieniu kodu. Ponieważ występują
one w wyniku kombinacji przypadków, mogą być trudne do wykrycia i usunięcia. Analizator
Rozdział 26. Usuwanie i rejestracja błędów 529
składniowy nie potrafi automatycznie stwierdzić, że konkretny wiersz kodu wywoła błąd. Jedynie
testowanie może spowodować jedną z sytuacji prowadzących do ujawnienia błędu.
Obsługa błędów wywołania wymaga zdolności przewidywania, którego wynikiem jest sprawdze-
nie różnych możliwych typów niepowodzenia i podjęcie odpowiedniego działania. Zakłada również
dokładne testowanie w celu symulacji każdego typu błędów wywołania.
Nie oznacza to, że konieczna jest symulacja każdego możliwego błędu. Na przykład MySQL może
zwracać setki różnych numerów i komunikatów o błędach. Zamiast tego należy symulować błąd
w każdej funkcji, która może się skończyć zgłoszeniem błędu, oraz wystąpienie błędu każdego typu,
który jest obsługiwany przez inny blok kodu.
Klasyczny błąd wykonania występuje podczas przetwarzania wpisów użytkownika, kiedy w wyniku
przeoczenia nie zostanie zastosowana funkcja addslashes(). Oznacza to, że jeżeli wystąpi użyt-
kownik o nazwisku takim jak O’Grady (dotyczy to zwłaszcza nazwisk anglojęzycznych), które
zawiera apostrof, wynikiem jest błąd funkcji baz danych, jeśli w instrukcji INSERT umieszczone
zostanie takie nazwisko otoczone tylko znakami apostrofu.
Błędy wynikające ze złych przewidywań na temat wprowadzanych danych zostaną opisane w na-
stępnym podrozdziale.
Błędy logiczne
Błędy logiczne są typem błędów najtrudniejszym do odnalezienia i wyeliminowania. Występują
one w sytuacji, w której prawidłowy kod wykonuje dokładnie to, co zostało mu nakazane, lecz
niezgodnie z zamysłem twórcy.
Błędy logiczne mogą zostać wywołane przez prostą pomyłkę podczas wpisywania kodu, na
przykład:
for($i=0; $i<10; $i++);
{
echo 'coś się dzieje<br />';
}
Powyższy fragment kodu jest prawidłowy; posiada poprawną składnię PHP. Nie opiera się na
żadnych zewnętrznych usługach, także raczej nie wywoła błędu wykonania. Jedynie dokład-
ne przeanalizowanie tego fragmentu ujawnia, że nie wykona on działania, które można przewidzieć,
ani też działania, które chciał spowodować programista.
Na pierwszy rzut oka kod wygląda tak, jakby dokonywał dziesięciokrotnej iteracji pętli for, za
każdym razem wyświetlając napis coś się dzieje. Dodanie zbytecznego średnika na końcu
pierwszego wiersza oznacza, że pętla nie będzie wywierać żadnego wpływu na następne wiersze.
Pętla for dokona dziesięciokrotnej iteracji bez żadnego wyniku, a instrukcja echo zostanie wykonana
tylko raz.
530 Część V Tworzenie praktycznych projektów PHP i MySQL
Ponieważ powyższy kod jest prawidłowym, chociaż nieskutecznym sposobem osiągnięcia tegoż
wyniku, analizator składniowy nie będzie zgłaszał błędów. Komputery sprawnie wykonują pewne
zadania, lecz nie odznaczają się ani tzw. zdrowym rozsądkiem ani inteligencją. Maszyna wykona
dokładnie to, co zostanie jej nakazane. Należy jedynie upewnić się, że o to właśnie nam chodziło.
Błędy logiczne nie są spowodowane nieprawidłowościami kodu, ale jedynie błędem programisty,
który nie napisał kodu instruującego komputer, by zrobił dokładnie to, co chciał osiągnąć. W rezul-
tacie błędy te nie mogą zostać wykryte automatycznie. Nie zostanie wyświetlona żadna informacja
o błędzie ani też numer wiersza, w którym występuje problem. Błędy logiczne mogą zostać wykryte
jedynie poprzez odpowiednie testowanie.
Błąd logiczny, taki jak w powyższym, bardzo prostym przykładzie, jest stosunkowo łatwy do
popełnienia. Nietrudno go jednak poprawić, kiedy podczas pierwszego uruchomienia kodu wyświe-
tlony wynik będzie inny niż spodziewany. Większość błędów logicznych nastręcza wszakże więcej
kłopotów.
Kłopotliwe błędy logiczne są zazwyczaj wynikiem nietrafnych przewidywań twórcy kodu. W roz-
dziale 25. opisaliśmy zalecaną technikę, polegającą na dokonywaniu przeglądu kodu przez innych
programistów w celu zasugerowania dodatkowych sytuacji testowych oraz na wynajmowaniu
jako testerów osób z kręgu docelowej klienteli. Błędne założenie, że ludzie będą wprowadzać
jedynie określone typy danych, często się zdarza, a w tym przypadku trudno jest wykryć błędy,
jeżeli jedynym testerem jest sam programista.
Tworząc na stronie handlowej pole tekstowe, należy zastanowić się nad kilkoma zagadnieniami. Czy
użytkownicy będą wpisywać jedynie wartości dodatnie? Jeżeli użytkownik wpisze wartość –10, to
czy oprogramowanie przeleje na jego kartę kredytową dziesięciokrotną wartość danego przedmiotu?
Podobnie można założyć, że utworzone zostaje pole tekstowe, w którym wpisuje się wartość
w dolarach. Czy użytkownicy powinni wpisywać wartość ze znakiem dolara, czy też bez niego?
Czy użytkownicy mogą wpisywać liczby z tysiącami oddzielonymi przecinkami? Niektóre z tych
kwestii mogą zostać sprawdzone po stronie klienta (na przykład za pomocą JavaScriptu), aby
nieco odciążyć serwer.
Listing 26.1. skladuj_zmienne.php — poniższy kod może być dołączony do strony w celu wyświetlenia
zawartości zmiennych dla usunięcia błędów1
<?php
session_start();
function skladuj_tablice($tablica) {
if(is_array($tablica)) {
$rozmiar = count($tablica);
$ciag = "";
if($rozmiar) {
$licznik = 0;
$ciag .= "{ ";
// dodanie klucza i wartości każdego elementu do łańcucha znaków
foreach($tablica as $zmienna => $wartosc) {
?>
1
Plik dostępny pod adresem: ftp://ftp.helion.pl/przyklady/phmsv5.zip — przyp. red.
532 Część V Tworzenie praktycznych projektów PHP i MySQL
Powyższy kod wyświetla cztery tablice zmiennych przekazanych do strony. Jeżeli stronę wywołano
ze zmiennymi GET, zmiennymi POST, cookies lub strona ta posiada zmienne sesji, zostaną one
wyświetlone.
Wyniki zostały umieszczone wewnątrz komentarzy HTML, tak więc są one widoczne, lecz nie
kolidują ze sposobem tworzenia widocznych elementów strony przez przeglądarkę. Jest to wy-
godny sposób tworzenia informacji pomocnych w testowaniu. Dzięki ukryciu informacji z procesu
debugowania w komentarzach, jak uczyniono to w kodzie z listingu 26.1, kod dla celów testo-
wania można pozostawić w skrypcie aż do ostatniej chwili. Jako nakładkę na funkcję print_r()
zdefiniowano funkcję skladuj_tablice(). Funkcja ta umieszcza jedynie znaki ucieczki przed
zamykającymi znacznikami komentarzy HTML.
Dokładna forma wyniku zależy od zmiennych przekazanych stronie. Kiedy zostanie on dodany
do listingu 22.4, jednego z przykładów uwierzytelniania zawartych w rozdziale 22., dorzuci nastę-
pujące wiersze do kodu HTML tworzonego przez skrypt:
<!-- ROZPOCZĘCIE SKŁADOWANIA ZMIENNYCH -->
Powyższy skrypt wyświetla zmienne POST wysłane przez formularz logowania z poprzedniej
strony — iduzytkownika i haslo. Pokazuje on również zmienną sesji, stosowaną do przechowywania
nazwy użytkownika — PHPSESSID. Jak opisano w rozdziale 22., PHP stosuje cookie w celu połącze-
nia zmiennych sesji z konkretnymi użytkownikami. Skrypt zwraca pseudolosowy numer,
PHPSESSID, przechowywany w tym cookie w celu identyfikacji konkretnego użytkownika.
Każda stała przedstawia pewien typ błędu, który może być zgłoszony lub zignorowany. Jeżeli
na przykład poziom błędów zostanie określony jako E_ERROR, zgłaszane będą tylko błędy krytyczne.
Powyższe stałe mogą być łączone za pomocą arytmetyki binarnej w celu utworzenia różnych
poziomów błędów.
Domyślny poziom błędów, który zwraca wszystkie błędy oprócz uwag, jest określony jako:
E_ALL & ~E_NOTICE
Stała E_ALL jest połączeniem wszystkich innych typów błędów z wyjątkiem E_STRICT. Może być
ona zastąpiona przez inne poziomy, połączone za pomocą bitowego operatora OR (lub) — |.
E_ERROR | E_WARNING | E_PARSE | E_NOTICE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR |
E_COMPILE_WARNING | E_USER_ERROR | E_USER_WARNING | E_USER_NOTICE
Podobnie, domyślny poziom zgłaszania błędów może być określony poprzez wszystkie poziomy
błędów, z wyjątkiem uwag połączonych razem operatorem OR.
534 Część V Tworzenie praktycznych projektów PHP i MySQL
Aby zmienić zgłaszanie błędów we wszystkich skryptach, należy zmodyfikować cztery wiersze
w domyślnym pliku php.ini:
error_reporting = E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED
display_errors = On
display_startup_errors = Off
log_errors = Off
log_errors_max_len = 1024
ignore_repeated_errors = Off
ignore_repeated_source = Off
report_memleaks = On
track_errors = Off
html_errors = On
error_log =
Podczas testowania i usuwania błędów przydatne może się okazać ustawienie wyższego poziomu
error_reporting. W kodzie ostatecznym, jeżeli dołączone są własne przydatne komunikaty o błę-
dach, bardziej profesjonalnie będzie wyglądać wyłączenie display_errors i włączenie log_errors,
przy wysokim poziomie error_reporting. Możliwe będzie później odniesienie się do dokładnych
błędów w dziennikach zdarzeń, jeżeli zostaną zgłoszone jakieś problemy.
Włączenie track_errors może pomóc w obsługiwaniu błędów we własnym zakresie, bez korzy-
stania z domyślnych możliwości PHP. Chociaż PHP dostarcza przydatnych komunikatów o błędach,
jego domyślne zachowanie wywiera niekorzystne wrażenie, kiedy wystąpią jakieś problemy.
Rozdział 26. Usuwanie i rejestracja błędów 535
po czym nastąpi przerwanie wykonywania skryptu. Podobny wynik pojawia się w przypadku błę-
dów niekrytycznych, lecz wykonanie postępuje dalej.
Powyższy kod HTML wyświetla wyraźnie błąd, lecz nie prezentuje się najlepiej. Styl komunikatu
o błędzie najprawdopodobniej różni się od wyglądu pozostałej części strony. Jego wynikiem może
być też pusty ekran użytkowników, jeżeli zawartość strony jest wyświetlona w tabeli, a przeglądarka
nie radzi sobie z wyświetleniem poprawnego kodu HTML. Dzieje się tak, ponieważ HTML
otwiera, lecz nie domyka elementów tablicy, na przykład:
<table>
<tr><td>
<br>
<b>Typ błędu</b>: komunikat o błędzie in <b>sciezka/plik.php</b>
on line <b>numerWiersza</b><br>
Nie jest konieczne zachowanie domyślnej obsługi błędów PHP, a nawet stosowanie takich samych
ustawień dla wszystkich plików. Aby zmienić poziom zgłaszania błędów w danym skrypcie,
należy wywołać funkcję: error_reporting().
Przekazanie zmiennej zgłaszającej błędy lub połączenia kilku z nich ustawia poziom w ten sam spo-
sób, jak czyni to podobna opcja w pliku php.ini. Funkcja ta zwraca poprzedni poziom zgłaszania
błędów. Popularny sposób korzystania z tej funkcji jest następujący:
// wyłączenie zgłaszania błędów
$stary_poziom=error_reporting(0);
// tu należy umieścić kod generujący ostrzeżenia
// ponowne włączenie zgłaszania błędów
error_reporting($stary_poziom);
Powyższy fragment kodu wyłącza zgłaszanie błędów, pozwalając na wykonanie pewnego kodu
skłonnego do generowania ostrzeżeń, które niekoniecznie zostaną wyświetlone.
Stałe wyłączenie zgłaszania błędów jest niefortunnym pomysłem, ponieważ znacznie utrudnia
poszukiwania i naprawę błędów kodowania.
Funkcja ta pobiera jako parametr komunikat o błędzie oraz opcjonalnie typ błędu. Typ błędu musi
być jednym z typów E_USER_ERROR, E_USER_WARNING lub E_USER_NOTICE. Jeżeli nie zostanie podany,
domyślnym typem jest E_USER_NOTICE.
Powyżej zostało opisane wyzwalanie własnych błędów. Można również utworzyć własne elementy
obsługujące błędy.
Funkcja obsługująca błędy musi pobierać dwa parametry, typ błędu i komunikat o błędzie. Na pod-
stawie tych dwu zmiennych funkcja zadecyduje o sposobie obsłużenia błędu. Typ błędu musi być
jednym ze zdefiniowanych typów stałych. Komunikat o błędzie to łańcuch zawierający opis.
Listing 26.2 zawiera skrypt, który deklaruje funkcje obsługującą błędy, wprowadza ją za pomocą
set_error_handler(), po czym wyzwala kilka błędów.
Rozdział 26. Usuwanie i rejestracja błędów 537
Listing 26.2. obsluga.php — skrypt deklaruje utworzoną przez użytkownika funkcję obsługującą błędy
oraz wyzwala różne błędy
<?php
// funkcja obsługująca błędy
function moja_obsluga_bledow($numerbl, $ciagbl, $plikbl, $liniabl) {
echo "<p><strong>BŁĄD:</strong>".$ciagbl."</p>
<p>Proszę spróbować ponownie lub skontaktować się z administratorem i
przekazać, że błąd wystąpił w linii $liniabl pliku ".$plikbl.".<br />
To nam ułatwi rozwiązanie problemu.</p>";
Rysunek 26.1.
Przy zastosowaniu
własnej obsługi błędów
możliwe jest podawanie
komunikatów o błędach
bardziej przyjaznych
niż domyślne PHP
Powyższa dodana funkcja obsługująca błędy nie czyni więcej niż mechanizmy wbudowane.
Ponieważ kod takich funkcji pisze się w dowolny sposób, funkcjom tym można narzucić każde
działanie. Pozwalają one na selekcję informacji przekazywanych odwiedzającym w wypadku
538 Część V Tworzenie praktycznych projektów PHP i MySQL
wystąpienia błędów oraz wybór sposobu ich prezentacji, aby pasował on do wyglądu pozostałej
części witryny. Co ważniejsze, zapewnia elastyczność decyzji. Czy skrypt powinien kontynuować
działanie? Czy komunikat powinien być wyświetlony, czy zapisany? Czy automatycznie powinna
zostać powiadomiona obsługa techniczna?
Należy pamiętać, że funkcja obsługująca błędy nie będzie ponosić odpowiedzialności za wszystkie
ich typy. Niektóre błędy, takie jak błędy składni i krytyczne błędy wykonania, będą obsługiwane
w sposób domyślny. Jeżeli jest to sprawa ważna w danym przypadku, należy dokładnie sprawdzić
parametry przed przekazaniem ich funkcji, która może wywołać błędy krytyczne i wyzwolić
własny poziom błędów E_USER_ERROR (jeżeli jest pewne, że parametry te spowodują błąd).
PHP zawiera jeszcze jeden nowy mechanizm: jeżeli napisana funkcja obsługi błędów jawnie
zwróci wartość false, wywołany zostanie wbudowany mechanizm obsługi błędów PHP. W ten
sposób można samodzielnie obsługiwać błędy typu E_USER_*, a w przypadku zwykłych błędów
zdać się na wbudowany mechanizm PHP.
W celu włączenia rejestrowania błędów w pliku należy zmienić ustawienia w pliku php.ini, określa-
jąc wartość dyrektywy konfiguracyjnej error_log. Na przykład aby zapisywać wszystkie zgłasza-
ne błędy w pliku /var/log/bledy-php.log, należy użyć dyrektywy o następującej postaci:
error_log = /var/log/bledy-php.log
Następnie trzeba się także upewnić, że zostanie wyłączona dyrektywa display_errors, tak aby błędy
nie były przesyłane do użytkownika końcowego:
display_errors = off
W końcu należy ponownie uruchomić serwer, by zmiany konfiguracji PHP zostały uwzględnione.
Po ponownym uruchomieniu serwera będzie już można wygodnie przeglądać dziennik błędów
(miejmy tylko nadzieję, że nie będzie w nim zbyt wielu wpisów).
W następnym rozdziale
W rozdziale 27., „Tworzenie uwierzytelniania użytkowników i personalizacji”, przedstawiony
zostanie sposób rejestrowania użytkowników w witrynie, śledzenia tego, czym są oni zaintereso-
wani, i prezentowania im odpowiednio dostosowanych treści.
Rozdział 27.
Tworzenie uwierzytelniania
użytkowników
i personalizacji
W tym projekcie umożliwimy użytkownikom rejestrację w witrynie WWW. Gdy użytkownicy
się zarejestrują, będziemy mogli śledzić ich zainteresowania oraz pokazywać właściwą dla nich
zawartość witryny. Działalność taka nosi nazwę personalizacji użytkowników.
Ten konkretny projekt pozwoli na stworzenie przez użytkowników zbioru zakładek w sieci
WWW. Zostaną im również zasugerowane, na podstawie ich poprzednich zachowań, inne łącza,
które mogą im się wydać interesujące. Uogólniając, personalizacja użytkowników może zostać
wykorzystana w niemal każdej aplikacji WWW w celu przedstawienia poszukiwanych przez nich
informacji, zaprezentowanych w odpowiednim formacie.
Ten projekt rozpocznie się od przeglądu zbioru wymagań podobnych do tych, które można
otrzymać od klienta. Wymagania te zostaną rozwinięte w zbiór składników rozwiązania. Zapro-
jektujemy ich połączenie oraz wykonamy implementację.
Składniki rozwiązania
Powinniśmy zbudować system zakładek online o nazwie ZakładkaPHP.
Wymagania są następujące:
Nieodzowna jest możliwość identyfikacji indywidualnych użytkowników. Powinien istnieć
również sposób ich uwierzytelniania.
Konieczny jest sposób przechowywania zakładek indywidualnego użytkownika.
Użytkownicy powinni mieć możliwość dodawania i usuwania zakładek.
Musi istnieć możliwość rekomendacji użytkownikom witryn, które mogą wzbudzić ich
zainteresowanie. Podstawą jest dostępna o nich wiedza.
Znając wymagania systemu, można rozpocząć projektowanie rozwiązania i jego składników. Poni-
żej przedstawiamy możliwe rozwiązania każdego z trzech wyliczonych wcześniej wymagań.
Jeżeli użytkownicy mają posiadać możliwość logowania za pomocą nazwy użytkownika i hasła,
potrzebne są następujące składniki:
Użytkownicy powinni mieć możliwość rejestracji za pomocą nazwy użytkownika i hasła.
Niezbędne są pewne ograniczenia długości i formatu nazwy użytkownika i hasła. Z powodów
bezpieczeństwa hasła powinny być przechowywane w formacie zaszyfrowanym.
Użytkownicy powinni mieć możliwość zalogowania się za pomocą szczegółów
dostarczonych przez nich w trakcie procesu rejestracji.
Użytkownicy powinni mieć możliwość wylogowania się po zakończeniu korzystania
z witryny. Nie jest to szczególnie ważne, jeżeli korzystają z witryny za pomocą swojego
domowego komputera, lecz jest istotne z punktu widzenia bezpieczeństwa w razie
korzystania z komputera wspólnego, takiego jak komputer w bibliotece.
Witryna powinna mieć możliwość sprawdzenia, czy użytkownik jest zalogowany, czy nie,
i wyświetlać informacje tylko użytkownikom zalogowanym.
Użytkownicy powinni mieć możliwość zmiany swojego hasła w celu zwiększenia
bezpieczeństwa.
Może się zdarzyć, że użytkownicy zapomną hasło. Dlatego powinni mieć możliwość
ponownego wprowadzenia hasła bez konieczności osobistej interwencji któregoś
z pracowników. Popularną metodą rozwiązania tego problemu jest wysyłanie hasła na
adres poczty elektronicznej, podany podczas rejestracji. Ponieważ hasła przechowywane
są w formie zaszyfrowanej, nie jest możliwe odczytanie hasła oryginalnego. Konieczne
jest wygenerowanie nowego, zapisanie go i wysłanie do użytkownika.
Dla celów naszego projektu w dalszej części utworzymy funkcje odpowiadające za wszystkie
powyższe możliwości funkcjonalne. Większość z nich będzie można wykorzystać ponownie
w innych projektach, ewentualnie po drobnych zmianach.
Przechowywanie zakładek
Aby przechowywać zakładki użytkowników, należy skonfigurować pewną przestrzeń w bazie danych
MySQL. Potrzebne będą następujące możliwości funkcjonalne:
Rozdział 27. Tworzenie uwierzytelniania użytkowników i personalizacji 541
Rekomendowanie zakładek
Istnieje kilka metod pozwalających na zarekomendowanie zakładki użytkownikowi. Zareko-
mendować można najbardziej popularne witryny dotyczące danego tematu. W tym projekcie
zostanie zaimplementowany system sugestii „podobnych umysłów”. Poszukuje użytkowników,
którzy posiadają zakładki takie jak użytkownik aktualnie zalogowany, oraz proponuje mu inne
zakładki tych użytkowników. Aby uniknąć zakładek osobistych, rekomendowane będą jedynie
zakładki przechowywane przez więcej niż jednego użytkownika.
Przegląd rozwiązania
Po zastanowieniu się można stworzyć diagram systemu, taki jak ten przedstawiony na rysunku 27.1.
Rysunek 27.1.
Ten diagram przedstawia
możliwe drogi przez
system ZakładkaPHP
Całe rozwiązanie zostanie przedstawione dość szczegółowo, a cały kod przykładowej aplikacji
znajduje się w katalogu rozdzial_27 w przykładach dołączonych do tej książki. Podsumowanie
zawartych plików przedstawiono w tabeli 27.1.
Najpierw zaimplementujemy bazę danych MySQL przeznaczoną dla tej aplikacji, ponieważ jest
ona niezbędna do działania praktycznie wszystkich funkcji. W dalszej kolejności przejdziemy
przez kody źródłowe w takiej kolejności, w jakiej były one tworzone. Rozpoczniemy od strony
powitalnej, następnie przeanalizujemy skrypt uwierzytelniający użytkowników, mechanizm zapisy-
wania i odczytywania zakładek, a skończymy na rekomendacjach. Porządek taki ma logiczne uza-
sadnienie. Jest to jedynie kwestia rozpoznania zależności między stronami i utworzenia w pierw-
szej kolejności tych elementów, które będą wymagane przez moduły implementowane w dalszej
kolejności.
Rozdział 27. Tworzenie uwierzytelniania użytkowników i personalizacji 543
Rysunek 27.2.
Schemat bazy danych
systemu ZakładkaPHP
Tabela uzytkownik przechowuje nazwę użytkownika, która jest kluczem podstawowym (nazwa_uz),
hasło (haslo) oraz adres poczty elektronicznej (email). Tabela zakladka przechowuje pary nazw
użytkowników (nazwa_uz) i zakładek (URL_zak). Nazwa użytkownika w tej tabeli odwołuje się
do nazwy użytkownika z tabeli uzytkownik.
SQL zastosowany do utworzenia tej bazy oraz przechowujący użytkownika łączącego się z nią
przez sieć WWW jest przedstawiony na listingu 27.1. Jeżeli planuje się skorzystanie z tego kodu
w własnym systemie, należy go zmodyfikować. Trzeba więc pamiętać, by zmienić hasło użytkowni-
ka na bezpieczniejsze!
Powyższa baza danych może zostać skonfigurowana w systemie w wyniku uruchomienia tego zbio-
ru poleceń przez użytkownika MySQL zalogowanego jako root. Można to wykonać, uruchamiając
następujące polecenie w wierszu poleceń systemu:
mysql –u nazwa_uzytkownika –p < zakladki.sql
544 Część V Tworzenie praktycznych projektów PHP i MySQL
Kiedy baza danych jest już skonfigurowana, można dokonać implementacji podstawowej witryny.
wyswietl_informacje_witryny();
wyswietl_form_log();
tworz_stopke_html();
?>
Rysunek 27.3.
Strona początkowa
systemu ZakładkaPHP
jest tworzona
przez funkcje generujące
kod HTML w pliku
logowanie.php
Jak można zauważyć, plik ten jest jedynie pojemnikiem dla pięciu innych plików, które powinny
zostać dołączone do aplikacji. Została mu nadana taka struktura, ponieważ funkcje można podzie-
lić na kilka logicznych grup. Niektóre mogą zostać wykorzystane w innych projektach, tak więc
każda została umieszczona w osobnym pliku. W wypadku późniejszej konieczności ich zastoso-
wania wiadomo, gdzie ich szukać. Plik funkcje_zakladki.php został utworzony, ponieważ większość
z pięciu plików funkcji stosowana będzie w przeważającej części pozostałych skryptów. Łatwiej jest
dołączyć ten jeden plik do wszystkich skryptów, niż w każdym z nich wpisywać pięć identycznych
wywołań require_once().
W tym konkretnym przypadku zastosowano funkcje z pliku funkcje_wyswietl.php. Są to proste funk-
cje wyświetlające prawie wyłącznie czysty kod HTML. Plik ten zawiera między innymi cztery funk-
cje wykorzystane w pliku logowanie.php, czyli tworz_naglowek_html(), wyswietl_informacje_strony(),
wyswietl_form_log() oraz tworz_stopke_html().
Nie wszystkie z powyższych funkcji zostaną opisane szczegółowo, jedna z nich przedstawiona
zostanie jako przykład. Kod funkcji tworz_naglowek_html() przedstawiono na listingu 27.4.
Listing 27.4. Funkcja tworz_naglowek_html() umieszczona w pliku funkcje_wyswietl.php — wyświetla
standardowy nagłówek pojawiający się na każdej stronie aplikacji
function tworz_naglowek_html($tytul) {
// wyświetlenie nagłówka HTML
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title><?php echo $tytul;?></title>
<style>
body { font-family: Arial, Helvetica, sans-serif; font-size: 13px }
li, td { font-family: Arial, Helvetica, sans-serif; font-size: 13px }
hr { color: #3333cc;}
a { color: #000 }
div.formblock
{ background: #ccc; width: 300px; padding: 6px; border: 1px solid #000;}
</style>
</head>
<body>
<div>
<img src="zakladka.gif" alt="Logo ZakładkaPHP" height="55" width="57" style="float: left;
padding-right: 6px;" />
<h1>ZakładkaPHP</h1>
</div>
<hr />
<?php
if($tytul) {
tworz_tytul_html($tytul);
}
}
546 Część V Tworzenie praktycznych projektów PHP i MySQL
Jak można zauważyć, jedyną logiką zawartą w funkcji tworz_naglowek_html() jest rozpoczęcie
dokumentu HTML i nadanie stronie odpowiedniego tytułu i nagłówka. Pozostałe funkcje zasto-
sowane w pliku logowanie.php są podobne. Funkcja wyswietl_informacje_witryny() dodaje pewien
ogólny tekst na temat witryny, wyswietl_form_log() wyświetla szary formularz przedstawiony na
rysunku 27.3, a tworz_stopke_html() dorzuca do strony standardową stopkę HTML.
Zalety wyizolowania lub usunięcia kodu HTML z głównego ciągu logicznego zostały omówione
w rozdziale 25. W tym rozdziale stosujemy ujęcie bazujące na zestawie funkcji.
Analizując rysunek 27.3, można dostrzec, że na stronie tej użytkownik ma do wyboru trzy opcje
— może zarejestrować się, zalogować się (jeżeli zarejestrował się już wcześniej) lub ustawić nowe
hasło, jeżeli zapomniał stare. Implementacja tych modułów zostanie przedstawiona w następnym
podrozdziale, w którym opisujemy uwierzytelnianie użytkowników.
Rejestracja użytkowników
Aby zarejestrować użytkownika, należy zebrać informacje o nim, podane za pomocą formularza,
oraz wprowadzić go do bazy danych.
Kiedy użytkownik kliknie łącze Jeszcze nie członek? na stronie logowanie.php, zostanie on
przeniesiony do formularza rejestracji tworzonego przez plik formularz_rejestracji.php. Skrypt ten
jest przedstawiony na listingu 27.5.
wyswietl_form_rej();
tworz_stopke_html();
?>
Powyższy skrypt jest również bardzo prosty; wywołuje jedynie funkcje z biblioteki wyświetla-
jącej, zawartej w pliku funkcje_wyswietl.php. Wynik uruchomienia tego skryptu jest przedstawiony
na rysunku 27.4.
Szary formularz na tej stronie jest tworzony przez funkcję wyswietl_form_rej(), umieszczoną
w pliku funkcje_wyswietl.php. Kiedy użytkownik kliknie przycisk Rejestracja, zostanie przeniesiony
do skryptu nowa_rejestracja.php (zobacz listing 27.6).
Rozdział 27. Tworzenie uwierzytelniania użytkowników i personalizacji 547
Rysunek 27.4.
Formularz rejestracji
pobiera dane potrzebne
bazie danych. Formularz
wymaga, by użytkownicy
wpisali hasło dwukrotnie
w celu uniknięcia pomyłek
Listing 27.6. nowa_rejestracja.php — skrypt sprawdza prawidłowość danych wprowadzonych przez nowego
użytkownika i umieszcza je w bazie danych
<?php
<?php
// dołączenie plików z funkcjami używanymi w aplikacji
require_once('bookmark_fns.php');
if (!wypelniony($_POST)) {
throw new Exception('Formularz wypełniony nieprawidłowo — proszę wrócić i spróbować
ponownie.');
}
// różne hasła
if ($haslo != $haslo2) {
throw new Exception('Niepasujące do siebie hasła — proszę wrócić i spróbować ponownie.');
}
548 Część V Tworzenie praktycznych projektów PHP i MySQL
// próba zarejestrowania
// także ta funkcja może zgłaszać wyjątek
rejestruj($nazwa_uz, $email, $haslo);
// rejestracja zmiennej sesji
$_SESSION['prawid_uzyt'] = $nazwa_uz;
// koniec strony
tworz_stopke_html();
}
catch (Exception $e) {
tworz_naglowek_html('Problem:');
echo $e->getMessage();
tworz_stopke_html();
exit;
}
?>
Jest to pierwszy tak złożony skrypt aplikacji spośród przedstawionych do tej pory. Skrypt ten
rozpoczyna się od dołączenia plików funkcji i rozpoczęcia sesji. Po zarejestrowaniu użytkownika
zostanie utworzona zmienna sesji zawierająca jego nazwę, jak przedstawiono w rozdziale 22.
Główny kod skryptu znajduje się w bloku try, ponieważ trzeba w nim sprawdzić szereg warunków.
Jeśli któryś z nich nie zostanie spełniony, wykonanie skryptu zostanie przeniesione do bloku catch,
który przeanalizujemy za moment.
Funkcja powyższa jest jedną z napisanych własnoręcznie. Znajduje się ona bibliotece funkcji
w pliku funkcje_prawid_dane.php. Zostanie przedstawiona w dalszej części rozdziału.
Prawidłowość podanego adresu poczty elektronicznej:
if(!prawidlowy_email($email))
oraz
if(strlen($nazwa_uz)>16)
W przykładzie tym hasło powinno mieć długość co najmniej 6 znaków, w celu zwiększenia
bezpieczeństwa, oraz mniej niż 17 znaków, aby zmieściło się w polu bazy danych
zdefiniowanym do jego przechowywania. Zwróćmy uwagę, że maksymalna długość
hasła nie jest w ten sposób ograniczona, ponieważ jest ono przechowywane jako wartość
mieszająca wygenerowana przez funkcję SHA1. Wartość ta będzie miała zawsze długość
40 znaków bez względu na długość hasła.
Listing 27.7. Funkcja wypelniony() pochodząca z pliku funkcje_prawid_dane.php — sprawdza, czy formularz
został prawidłowo wypełniony
function wypelniony($zmienne_formularza) {
Funkcja wypelniony()„spodziewa się” przekazania jej tablicy zmiennych — ogólnie będą to tablice
$_POST lub $_GET. Sprawdzi ona, czy każda z nich została wypełniona, i zwróci true, jeżeli tak, zaś
w przeciwnym wypadku — false.
Funkcja prawidlowy_email() stosuje wyrażenia regularne nieco bardziej złożone niż wyrażenie
utworzone w rozdziale 4., w celu sprawdzenia prawidłowości adresu. Zwraca ona true, jeżeli adres
wydaje się poprawny, a false w przeciwnym wypadku.
// koniec strony
tworz_stopke_html();
Jak można zauważyć, funkcja rejestruj() jest wywoływana z argumentami zawierającymi nazwę
użytkownika, adres poczty elektronicznej oraz hasło, które to argumenty zostały wprowadzone
przez użytkownika. Jeżeli działanie to powiedzie się, nazwa użytkownika jest rejestrowana jako
zmienna sesji, a on sam otrzymuje łącze do głównej strony członkowskiej. (Jeśli wywołanie się nie
powiedzie, funkcja wyrzuci wyjątek, który zostanie przechwycony w bloku catch). Wynik przed-
stawiono na rysunku 27.5.
Rysunek 27.5.
Rejestracja powiodła
się — użytkownik
może teraz przejść
do strony członkowskiej
Listing 27.9. Funkcja rejestruj() pochodząca z pliku funkcje_uwierz.php — dokonuje próby umieszczenia
informacji o nowym użytkowniku w bazie danych
function rejestruj($nazwa_uz, $email, $haslo) {
// zarejestrowanie nowej osoby w bazie danych
// zwraca true lub komunikat o błędzie
if ($lacz->num_rows>0) {
throw new Exception('Nazwa użytkownika zajęta — proszę wrócić i wybrać inną.');
}
Rozdział 27. Tworzenie uwierzytelniania użytkowników i personalizacji 551
return true;
}
W funkcji tej nie występuje nic specjalnie nowego — łączy się ona ze skonfigurowaną wcze-
śniej bazą danych. Jeżeli wybrana nazwa użytkownika jest już zajęta lub baza danych nie może
zostać uaktualniona, wyrzuci ona wyjątek. W przeciwnym przypadku uaktualni bazę danych
i zwróci true.
Należy zapamiętać, że połączenie z bazą danych jest wykonywane za pomocą własnej funkcji,
lacz_bd(). Funkcja ta jest jedynym miejscem w kodzie, które zawiera nazwę użytkownika i hasło
do połączenia z bazą danych. W ten sposób jeżeli hasło dostępu do bazy danych zostanie zmienione,
konieczna będzie modyfikacja tylko jednego pliku aplikacji. Funkcja lacz_bd() jest przedstawiona
na listingu 27.10.
Listing 27.10. Funkcja lacz_bd() zawarta w pliku funkcje_bazy.php — łączy się z bazą danych MySQL
<?php
function lacz_bd() {
$wynik = new mysqli('localhost', 'uzyt_zak', 'haslo', 'zakladki');
if (!$wynik) {
throw new Exception('Połączenie z serwerem bazy danych nie powiodło się');
} else {
return $wynik;
}
}
?>
Kiedy użytkownicy zostaną zarejestrowani, mogą zalogować się i wylogować za pomocą zwykłych
stron logowania i wylogowania. Zostaną one utworzone w następnym podrozdziale.
Logowanie
Jeżeli użytkownicy podadzą informacje na swój temat w formularzu na stronie logowanie.php
(zobacz rysunek 27.3) i wyślą je, zostaną przeniesieni do skryptu o nazwie czlonek.php. Skrypt
zaloguje ich, jeśli zostali oni przeniesieni z tego formularza; wyświetli również zalogowanym
użytkownikom wszystkie zakładki. Skrypt ten, będący centrum pozostałej części aplikacji, prze-
stawiono na listingu 27.11.
tworz_naglowek_html('Strona główna');
sprawdz_prawid_uzyt();
// odczytanie zakładek użytkownika
if ($tablica_url = pobierz_urle_uzyt($_SESSION['prawid_uzyt'])) {
wyswietl_urle_uzyt($tablica_url);
}
tworz_stopke_html();
?>
Logika powyższego skryptu może wydać się znajoma — zastosowano ponownie pewne idee
z rozdziału 22.
Przede wszystkim należy sprawdzić, czy użytkownik został przeniesiony do tej strony ze strony
początkowej — to znaczy: czy wypełnił formularz — oraz dokonać próby jego zalogowania
w następujący sposób:
if ($nazwa_uz && $haslo) {
// właśnie nastąpiła próba logowania
try {
loguj($nazwa_uz, $haslo);
// jeżeli użytkownik znajduje się w bazie danych, rejestracja identyfikatora
$_SESSION['prawid_uzyt'] = $nazwa_uz;
}
Można dostrzec, że próba logowania jest wykonana za pomocą funkcji loguj(). Funkcja ta została
zdefiniowana w bibliotece funkcje_uwierz.php, a jej kod zostanie przedstawiony w dalszej części
tego rozdziału.
Rozdział 27. Tworzenie uwierzytelniania użytkowników i personalizacji 553
Jeżeli logowanie powiodło się, można zgłosić sesję — jak uczyniono to poprzednio — przechowując
nazwę użytkownika w zmiennej sesji $prawid_uzyt.
Jeżeli nie wystąpiły żadne problemy, użytkownikowi zostaje przedstawiona strona członkowska:
tworz_naglowek_html('Strona główna');
sprawdz_prawid_uzyt();
// odczytanie zakładek użytkownika
if ($tablica_url = pobierz_urle_uzyt($_SESSION['prawid_uzyt'])) {
wyswietl_urle_uzyt($tablica_url);
}
tworz_stopke_html();
Skrypt czlonek.php kończy wyświetlanie strony, tworząc menu za pomocą funkcji wyswietl_
menu_uzyt(). Przykładowa strona utworzona przez skrypt czlonek.php jest przedstawiona na
rysunku 27.6.
Rysunek 27.6.
Skrypt czlonek.php
sprawdza, czy użytkownik
jest zalogowany,
odczytuje i wyświetla jego
zakładki oraz dostarcza
mu menu opcji
Poniżej dokładniej opisano funkcje loguj() i sprawdz_prawid_uzyt(). Funkcja loguj() jest przed-
stawiona na listingu 27.12.
554 Część V Tworzenie praktycznych projektów PHP i MySQL
Listing 27.12. Funkcja loguj() umieszczona w pliku funkcje_uwierz.php — sprawdza dane użytkownika
w bazie danych
function loguj($nazwa_uz, $haslo) {
// sprawdzenie nazwy użytkownika i hasła w bazie danych
// jeżeli się zgadza, zwraca true
// jeżeli nie, zgłasza wyjątek
if ($wynik->num_rows>0) {
return true;
} else {
throw new Exception('Logowanie nie powiodło się.');
}
}
Funkcja powyższa łączy się z bazą danych i sprawdza, czy istnieje użytkownik z podaną kombinacją
nazwy i hasła. Zwróci ona true, jeżeli taki użytkownik istnieje, oraz false, jeżeli nie istnieje lub gdy
dane nie mogły zostać sprawdzone.
Funkcja sprawdz_prawid_uzyt() nie łączy się ponownie z bazą danych, natomiast sprawdza, czy
użytkownik posiada zgłoszoną sesję, to znaczy, czy zdążył się już zalogować. Funkcja ta została
przedstawiona na listingu 27.13.
Listing 27.13. Funkcja sprawdz_prawid_uzyt() z pliku funkcje_uwierz.php — sprawdza, czy użytkownik posiada
zgłoszoną sesję
function sprawdz_prawid_uzyt() {
// sprawdzenie, czy użytkownik jest zalogowany, i powiadomienie go, jeżeli nie jest
if (isset($_SESSION['prawid_uzyt'])) {
echo "Zalogowano jako ".$_SESSION['prawid_uzyt'].".<br />";
} else {
// nie jest zalogowany
tworz_naglowek_html('Problem:');
echo 'Brak zalogowania.<br />';
tworz_HTML_URL('logowanie.php', 'Logowanie');
tworz_stopke_html();
exit;
}
}
Jeżeli użytkownik nie jest zalogowany, funkcja poinformuje go, że musi się zalogować, aby oglądać
zawartość tej strony. Dostarczy mu również łącza do strony logowania.
Wylogowanie
Można zauważyć, że w menu na rysunku 27.6 istnieje łącze o nazwie Wylogowanie. Jest to łącze
do skryptu wylog.php, przedstawionego na listingu 27.14.
Rozdział 27. Tworzenie uwierzytelniania użytkowników i personalizacji 555
if (!empty($stary_uzyt)) {
if ($wynik_niszcz) {
// jeżeli użytkownik zalogowany i niewylogowany
echo 'Wylogowano.<br />';
tworz_HTML_URL('logowanie.php', 'Logowanie');
} else {
// użytkownik zalogowany i wylogowanie niemożliwe
echo 'Wylogowanie niemożliwe.<br />';
}
} else {
// jeżeli brak zalogowania, lecz w jakiś sposób uzyskano dostęp do strony
echo 'Użytkownik niezalogowany, tak więc brak wylogowania.<br />';
tworz_HTML_URL('logowanie.php', 'Logowanie');
}
tworz_stopke_html();
?>
Powyższy kod również może wydawać się znajomy. Wynika to z faktu, że jest on oparty na kodzie
utworzonym w rozdziale 22.
Zmiana hasła
Po wybraniu opcji menu Zmiana hasła użytkownikowi zostanie wyświetlony formularz, przedsta-
wiony na rysunku 27.7.
Formularz ten tworzony jest przez skrypt zmiana_hasla_formularz.php. Ten prosty skrypt stosuje
funkcje z biblioteki wyświetlającej, tak więc jego kod nie został tu zawarty.
Kiedy formularz zostanie wysłany, wywoła skrypt zmiana_hasla.php, przedstawiony na listingu 27.15.
Rysunek 27.7.
Skrypt zmiana_hasla_
formularz.php wyświetla
formularz umożliwiający
użytkownikom zmianę
hasła
try {
sprawdz_prawid_uzyt();
if (!wypelniony($_POST)) {
throw new Exception('Formularz nie został wypełniony całkowicie. Proszę spróbować
ponownie.');
}
if ($nowe_haslo != $nowe_haslo2) {
throw new Exception('Wprowadzone hasła nie są identyczne. Hasło nie zostało zmienione.');
}
// próba uaktualnienia
zmien_haslo($_SESSION['prawid_uzyt'], $stare_haslo, $nowe_haslo);
echo 'Hasło zmienione.';
}
catch (Exception $e) {
echo $e->getMessage();
}
wyswietl_menu_uzyt();
tworz_stopke_html();
?>
Powyższy skrypt sprawdza, czy użytkownik jest zalogowany (za pomocą funkcji sprawdz_prawid_
uzyt()), czy wypełnił on formularz hasła (za pomocą wypelniony()) oraz czy nowe hasła są
identyczne i właściwej długości. Działania te nie są nowe. Jeżeli nie wystąpią żadne problemy,
zostaje wywołana funkcja zmien_haslo():
Rozdział 27. Tworzenie uwierzytelniania użytkowników i personalizacji 557
Funkcja ta pochodzi z biblioteki funkcje_uwierz.php, a jej kod jest przedstawiony na listingu 27.16.
Listing 27.16. Funkcja zmien_haslo() pochodząca z pliku funkcje_uwierz.php — uaktualnia hasło użytkownika
w bazie danych
function zmien_haslo($nazwa_uz, $stare_haslo, $nowe_haslo) {
// zmiana hasła użytkownika ze stare_haslo na nowe_haslo
// zwraca true lub false
Funkcja ta sprawdza za pomocą opisanej poprzednio funkcji loguj(), czy podane stare hasło
jest prawidłowe. Jeżeli jest prawidłowe, funkcja łączy się z bazą danych i uaktualnia hasło do
nowej wartości.
Rysunek 27.8.
Skrypt zapomnij_
formularz.php tworzy
formularz, w którym
użytkownicy mogą
poprosić o ustawienie
nowego hasła i wysłanie
go do nich
558 Część V Tworzenie praktycznych projektów PHP i MySQL
Ten prosty skrypt korzysta jedynie z funkcji wyświetlających, tak więc nie został tu przedsta-
wiony. Kiedy formularz zostaje wysłany, wywołuje skrypt zapomnij_haslo.php, znacznie bardziej
interesujący. Skrypt ten jest przedstawiony na listingu 27.17.
Listing 27.17. zapomnij_haslo.php — skrypt ustawia hasło użytkownika na losową wartość i wysyła ją
za pomocą poczty elektronicznej
<?php
require_once("funkcje_zakladki.php");
tworz_naglowek_html("Ustawianie hasła");
try {
$haslo=ustaw_haslo($nazwa_uz);
powiadom_haslo($nazwa_uz, $haslo);
echo 'Nowe hasło zostało przesłane na adres poczty elektronicznej.<br />';
}
catch (Exception $e) {
echo 'Hasło nie mogło zostać ustawione. Proszę spróbować później.';
}
tworz_HTML_URL('logowanie.php', 'Logowanie');
tworz_stopke_html();
?>
Jak można dostrzec, w celu wykonania pracy powyższy skrypt korzysta z dwóch głównych
funkcji — ustaw_haslo() i powiadom_haslo(). Zostaną one kolejno opisane.
Funkcja ustaw_haslo() generuje nowe losowe hasło dla użytkownika i umieszcza je w bazie
danych. Kod tej funkcji jest przedstawiony na listingu 27.18.
Listing 27.18. Funkcja ustaw_haslo() umieszczona w pliku funkcje_uwierz.php — ten skrypt ustawia hasło
użytkownika na losową wartość i wysyła je pocztą elektroniczną
function ustaw_haslo($nazwa_uz) {
// ustawienie hasła użytkownika na losową wartość
// zwraca nowe hasło lub false w przypadku niepowodzenia
if($nowe_haslo == false) {
// użycie domyślnej wartości hasła
$nowe_haslo = "zmienMnie!";
}
Funkcja ta generuje losowe hasło poprzez odczytanie losowego słowa ze słownika za pomocą
funkcji pobierz_losowe_slowo() i dodanie do niej losowej liczby z zakresu od 0 do 999. Jeśli słowo
nie zostanie pobrane ze słownika, to funkcja użyje domyślnego hasła o postaci "ZmienMnie!" z koń-
cówką w formie losowej liczby. Funkcja pobierz_losowe_slowo() znajduje się w bibliotece
funkcje_uwierz.php. Funkcja ta, przedstawiona na listingu 27.19, również znajduje się w bibliotece
funkcje_uwierz.php.
Aby skrypt był jeszcze bezpieczniejszy, zamiast stosować hasło o domyślnej postaci, należałoby
zgłosić wyjątek, na przykład w poniższy sposób:
if($nowe_haslo == false) {
throw new Exeption('Nie można ustawić nowego hasła!');
}
W przypadku innych systemów lub braku instalacji ispell nie należy zaprzątać sobie uwagi
tym problemem. Lista słów stosowanych przez ispell jest dostępna pod następującym adre-
sem http://wordlist.sourceforge.net/, wystarczy zatem zmienić jedynie adres w kodzie funkcji
pobierz_losowe_slowo().
Witryna ta zawiera słowniki w wielu językach, jeżeli więc losowe słowo powinno pochodzić na
przykład z norweskiego lub esperanto, można pobrać jeden ze słowników. Pliki te sformatowane
są w taki sposób, że każde słowo umieszczone jest w osobnym wierszu, oddzielone przez znaki
nowej linii.
Aby pobrać losowe słowo z tego pliku, wybierana jest losowa pozycja pomiędzy 0 i długością
pliku, po czym następuje odczyt pliku z tego miejsca. Jeżeli odczyt zostanie wykonany od losowej
pozycji do następnego znaku nowego wiersza, najprawdopodobniej wynikiem będzie niepełne
słowo, tak więc pierwszy wiersz zostanie pominięty. Jako hasło zostanie pobrane następne słowo.
Działanie to jest wykonane poprzez dwukrotne wywołanie funkcji fgets().
Listing 27.20. Funkcja powiadom_haslo() umieszczona w pliku funkcje_uwierz.php — wysyła nowe hasło
na adres poczty elektronicznej użytkownika
function powiadom_haslo($nazwa_uz, $haslo) {
// powiadomienie użytkownika o zmianie hasła
$lacz = lacz_bd();
$wynik = $lacz->query("select email from uzytkownik
where nazwa_uz='".$nazwa_uz."'");
if (!$wynik) {
throw new Exception('Nie znaleziono adresu e-mail');
} else if ($wynik->num_rows == 0) {
throw new Exception('Nie znaleziono adresu e-mail');
// nazwy użytkownika nie ma w bazie danych
} else {
$wiersz = $wynik->fetch_object();
$email = $wiersz->email;
$od = "From: obsluga@zakladkaphp \r\n";
$wiad = "Hasło systemu ZakładkaPHP zostało zmienione na $haslo \r\n"
."Proszę zmienić je przy następnym logowaniu. \r\n";
} else {
throw new Exception('Wysłanie e-maila nie powiodło się');
}
}
}
Powyższa funkcja po otrzymaniu nazwy użytkownika i nowego hasła po prostu wyszukuje adres
poczty elektronicznej tego użytkownika w bazie danych, po czym korzysta z funkcji PHP mail(),
aby wysłać hasło.
Bezpieczniejsze byłoby nadanie użytkownikom haseł losowych z prawdziwego zdarzenia —
utworzonych z dowolnej kombinacji małych i wielkich liter, cyfr i znaków interpunkcyjnych —
a nie losowych słów i liczb. Jednak hasło typu „zygzak487” będzie łatwiejsze do odczytania
i wpisania niż to prawdziwie losowe. Użytkownikom często sprawia problemy rozpoznanie, czy
znak w losowym łańcuchu to 0, czy O (zero lub wielkie O), czy też 1 lub l (jeden lub małe L).
W tym systemie plik słownika zawiera około 45 000 słów. Jeżeli włamywacz znałby sposób
tworzenia haseł oraz nazwę użytkownika, ciągle pozostałoby mu średnio 22 500 000 haseł, z których
musiałby wybrać jedno. Ten poziom bezpieczeństwa wydaje się odpowiedni do takiego typu apli-
kacji, nawet jeżeli użytkownicy nie zwrócą uwagi na przesłaną pocztą elektroniczną radę, aby je
zmienić.
Jednak lepszym rozwiązaniem pozwalającym użytkownikom na zmianę hasła byłoby przesyłanie im
łącza do strony z formularzem do zmiany hasła, zawierającego w łańcuchu zapytania jednorazowy
żeton, którego ważność wygasałaby po określonym czasie (24 godzinach, 72 godzinach itd.).
Ten jednorazowy żeton byłby kluczem służącym do uwierzytelnienia użytkownika i pozwalającym
mu na zmianę hasła, i to bez konieczności stosowania generowanego hasła przesyłanego otwartym
tekstem w wiadomości e-mail.
Implementacja przechowywania
i odczytywania zakładek
Funkcje odpowiedzialne za obsługę kont użytkowników zostały zaimplementowane, zatem w tym
podrozdziale przedstawiamy sposób przechowywania, odczytywania oraz usuwana zakładek.
Dodawanie zakładek
Użytkownicy mogą dodawać zakładki poprzez wybranie opcji menu Dodaj zakładkę. Zostaną
wtedy przeniesieni do formularza przedstawionego na rysunku 27.9.
Skrypt ten również jest prosty i wykorzystuje jedynie funkcje wyświetlające, więc nie został tu
opisany. Po wysłaniu formularz wywołuje skrypt dodaj_zak.php (zobacz listing 27.21).
Listing 27.21. dodaj_zak.php — skrypt dodaje nowe zakładki do strony osobistej użytkownika
<?php
require_once('funkcje_zakladki.php');
session_start();
Rysunek 27.9.
Skrypt dodaj_zak_
formularz.php wyświetla
formularz, za pomocą
którego użytkownicy
mogą dodawać zakładki
do strony osobistej
tworz_naglowek_html('Dodawanie zakładek');
try {
sprawdz_prawid_uzyt();
if (!wypelniony($_POST)) {
throw new Exception('Formularz wypełniony niewłaściwie. Proszę spróbować ponownie.');
}
// sprawdzenie formatu URL-a
if (strstr($nowy_url, 'http://') === false) {
$nowy_url = 'http://'.$nowy_url;
}
Powyższy skrypt również przechodzi ścieżkę sprawdzenia prawidłowości, wpisu do bazy danych
i wyświetlenia.
Aby potwierdzić prawidłowość, najpierw sprawdzane jest wypełnienie formularza przez użytkow-
nika za pomocą funkcji wypelniony(). Następnie URL przechodzi dwa testy. Przede wszystkim
funkcja strstr()sprawdza, czy URL rozpoczyna się od wyrażenia http://. Jeżeli nie, zostaje
Rozdział 27. Tworzenie uwierzytelniania użytkowników i personalizacji 563
ono dodane do początku URL-a. Po wykonaniu tego działania można sprawdzić istnienie URL-a.
Jak stwierdzono w rozdziale 18., do otwarcia URL-a rozpoczynającego się od http:// można
zastosować funkcję fopen(). Jeżeli istnieje możliwość otwarcia pliku, można przyjąć, że URL
jest prawidłowy, po czym wywołać funkcję dodaj_zak() w celu dodania go do bazy danych.
Wszystkie funkcje związane z zakładkami znajdują się w bibliotece funkcji funkcje_url.php. Kod
funkcji dodaj_zak() jest przedstawiony na listingu 27.22.
Listing 27.22. Funkcja dodaj_zak() pochodząca z pliku funkcje_url.php — dodaje nowe zakładki do bazy danych
function dodaj_zak($nowy_url) {
// dodawanie nowych zakładek do bazy danych
$lacz = lacz_bd();
return true;
}
Funkcja dodaj_zak() jest względnie prosta. Sprawdza ona, czy użytkownik nie posiada już takiej
zakładki w bazie danych. (To mało prawdopodobne, by użytkownik umieszczał zakładkę drugi
raz, lecz jest możliwe i nawet prawdopodobne, że odświeżył stronę). Jeżeli zakładka jest nowa,
wtedy zostaje umieszczona w bazie danych.
Analizując po raz kolejny plik dodaj_zak.php, można dostrzec, że ostatnim działaniem wykonywa-
nym przez ten skrypt jest wywołanie funkcji pobierz_urle_uzyt() i wyswietl_urle_uzyt(), podobnie
jak w pliku czlonek.php. Funkcje te zostały opisane poniżej.
Wyświetlanie zakładek
W skrypcie czlonek.php i funkcji dodaj_zak() zostały zastosowane funkcje pobierz_urle_uzyt()
i wyswietl_urle_uzyt(). Pobierają one odpowiednie zakładki z bazy i wyświetlają je. Funkcja
pobierz_urle_uzyt() znajduje się w bibliotece funkcje_url.php, a wyswietl_urle_uzyt() —
w bibliotece funkcje_wyswietl.php.
Listing 27.23. Funkcja pobierz_urle_uzyt() z pliku funkcje_url.php — odczytuje zakładki użytkownika z bazy danych
function pobierz_urle_uzyt($nazwa_uz) {
// pobranie z bazy danych wszystkich URL-i danego użytkownika
564 Część V Tworzenie praktycznych projektów PHP i MySQL
$lacz = lacz_bd();
$wynik = $lacz->query("select URL_zak
from zakladka
where nazwa_uz = '".$nazwa_uz."'");
if (!$wynik) {
return false;
}
Poniżej znajduje się krótki opis funkcji pobierz_urle_uzyt(). Pobiera ona jako parametr nazwę
użytkownika oraz odczytuje zakładki użytkownika z bazy danych. Zwróci ona tablicę tych URL-i
lub false, jeżeli zakładki nie mogą być odczytane.
Usuwanie zakładek
Kiedy użytkownik zaznaczy zakładki do usunięcia i wybierze opcję menu Usuwanie zakładek,
formularz zawierający URL-e zostanie wysłany. Każde z pól wyboru jest tworzone za pomocą
następującego kodu w funkcji wyswietl_urle_uzyt():
echo "<tr bgcolor=\"".$kolor."\"><td>
<a href=\"".$url."\">".htmlspecialchars($url)."</a></td>
<td><input type=\"checkbox\" name=\"usun_mnie[]\"
value=\"".$url."\"/></td>
</tr>";
Nazwą każdego pola wyboru jest usun_mnie[]. Oznacza to, że w skrypcie PHP aktywowanym
przez ten formularz będzie dostępna tablica o nazwie $usun_mnie, która zawiera wszystkie zakładki
przeznaczone do usunięcia.
Wybranie opcji Usuwanie zakładek aktywuje skrypt usun_zak.php. Skrypt ten jest przedstawiony
na listingu 27.24.
Listing 27.24. usun_zak.php — skrypt usuwa zakładki z bazy danych
<?php
require_once('funkcje_zakladki.php');
session_start();
tworz_naglowek_html('Usuwanie zakładek');
sprawdz_prawid_uzyt();
Rozdział 27. Tworzenie uwierzytelniania użytkowników i personalizacji 565
if (!wypelniony($_POST)) {
echo '<p>Nie zostały wybrane żadne zakładki do usunięcia.<br />
Proszę spróbować ponownie.</p>';
wyswietl_menu_uzyt();
tworz_stopke_html();
exit;
} else {
if (count($usun_mnie) > 0) {
foreach($usun_mnie as $url) {
if (usun_zak($prawid_uzyt, $url)) {
echo 'Usunięto '.htmlspecialchars($url).'.<br />';
} else {
echo 'Nie udało się usunięcie '.htmlspecialchars($url).'.<br />';
}
}
} else {
echo 'Nie wybrano żadnych zakładek do usunięcia';
}
}
wyswietl_menu_uzyt();
tworz_stopke_html();
?>
Właściwe usuwanie zakładki z bazy danych wykonuje więc funkcja usun_zak(), przedstawiona na
listingu 27.25.
Listing 27.25. Funkcja usun_zak() pochodząca z pliku funkcje_url.php — usuwa pojedynczą zakładkę z listy
użytkownika
function usun_zak($uzytkownik, $url) {
// usunięcie jednego URL-a z bazy danych
$lacz = lacz_bd();
// usunięcie zakładki
if (!$lacz->query("delete from zakladka
where nazwa_uz='".$uzytkownik."'
and URL_zak='".$url."'")) {
throw new Exception('Usunięcie zakładki nie powiodło się.');
}
return true;
}
566 Część V Tworzenie praktycznych projektów PHP i MySQL
A zatem jest to stosunkowo prosta funkcja. Dokonuje ona próby eliminacji zakładki konkretnego
użytkownika z bazy danych. Należy pamiętać, że trzeba usunąć parę użytkownik – zakładka. Inni
użytkownicy mogą dalej posiadać ten URL jako zakładkę.
Przykładowy wynik uruchomienia skryptu usuwającego jest przedstawiony na rysunku 27.10.
Rysunek 27.10.
Skrypt usuwający
powiadamia użytkownika
o usuniętych zakładkach
i wyświetla pozostałe
Podobnie jak w skrypcie dodaj_zak.php, kiedy zmiany w bazie danych zostaną wykonane, ukaże
się nowa lista zakładek za pomocą funkcji pobierz_urle_uzyt() i wyswietl_urle_uzyt().
Implementacja rekomendacji
Ostatnim łączem jest skrypt rekomendujący, rekomendacja.php. Istnieje wiele metod rekomendacji.
W tym przypadku zostanie ona przeprowadzona na zasadzie „podobnych umysłów”. Oznacza
to, że odszukani zostaną użytkownicy posiadający co najmniej jedną zakładkę taką samą jak dany
użytkownik.
Najprostszym sposobem implementacji tej metody jako zapytania SQL jest zastosowanie podza-
pytań. Pierwsze podzapytanie będzie miało następującą treść
select distinct(z2.nazwa_uz)
from zakladka z1, zakladka z2
where z1.nazwa_uz='".$prawid_uzyt."'
and z1.nazwa_uz!=z2.nazwa_uz
and z1.URL_zak=z2.URL_zak
Powyższe zapytanie stosuje aliasy do dołączania tabeli bazy danych do samej siebie — osobliwy,
lecz czasami przydatny pomysł. Należy wyobrazić sobie, że tak naprawdę istnieją dwie tabele
zakładek, jedna o nazwie z1, a druga — z2. W tabeli z1 jest przeglądany aktualny użytkownik
i jego zakładki. W drugiej tablicy przeglądane są zakładki pozostałych użytkowników. Poszuki-
wani są inni użytkownicy (z2.nazwa_uz), którzy posiadają URL-e takie same jak aktualny użyt-
kownik (z1.URL_zak=z2.URL_zak) i nie są nim (z1.nazwa_uz!= z2.nazwa_uz).
Rozdział 27. Tworzenie uwierzytelniania użytkowników i personalizacji 567
Jeżeli w systemie znajduje się wiele osób, można podnieść wartość zmiennej $popularnosc, aby
rekomendować jedynie URL-e zapisane przez licznych użytkowników. URL-e zapisane przez liczną
grupę osób powinny być wyższej jakości i posiadać lepszy projekt niż przeciętna strona WWW.
Pełny skrypt tworzący rekomendacje jest przedstawiony na listingach 27.26 i 27.27. Główny skrypt
tworzący rekomendacje nosi nazwę rekomendacja.php (zobacz listing 27.26). Wywołuje on
funkcję rekomendującą o nazwie rekomenduj_urle(), umieszczoną w pliku funkcje_url.php
(zobacz listing 27.27).
Listing 27.26. rekomendacja.php — skrypt sugeruje pewne zakładki, które mogą się spodobać użytkownikowi
<?php
session_start();
require_once('funkcje_zakladki.php');
tworz_naglowek_html('Rekomendacja URL-i');
try {
sprawdz_prawid_uzyt();
$urle = rekomenduj_urle($_SESSION['prawid_uzyt']);
wyswietl_rekomend_urle($urle);
}
catch (Exception $e) {
$e->getMessage();
}
wyswietl_menu_uzyt();
tworz_stopke_html();
?>
568 Część V Tworzenie praktycznych projektów PHP i MySQL
Listing 27.27. Funkcja rekomenduj_urle() pochodząca z pliku funkcje_url.php — skrypt poszukujący aktualnych
rekomendacji
function rekomenduj_urle($prawid_uzyt, $popularnosc = 1) {
// tworzenie półinteligentnych rekomendacji
// Jeżeli posiadają URL-e wspólne z innymi użytkownikami, mogą im się
// spodobać inne URL-e, które lubią inni
$lacz = lacz_bd();
if (!($wynik = $lacz->query($zapytanie))) {
throw new Exception('Nie znaleziono żadnych rekomendowanych zakładek.');
}
if ($wynik->num_rows==0) {
throw new Exception('Nie znaleziono żadnych rekomendowanych zakładek.');
}
$urle = array();
// stworzenie tablicy odpowiednich URL-i
for ($licznik=0; $rzad = $wynik->fetch_object(); $licznik++) {
$urle[$licznik] = $rzad->URL_zak;
}
return $urle;
}
Rysunek 27.11.
Skrypt zarekomendował
użytkownikowi witrynę
helion.pl. Mieli ją
również dwaj inni
użytkownicy z bazy
danych, posiadający
witrynę helion.pl
Projekt Laravel można przygotować na kilka różnych sposobów. Jeśli chodzi nam o utworzenie
jednorazowego projektu, to najlepszym rozwiązaniem będzie zastosowanie polecenia composer
create-project i określenie docelowego katalogu, w którym projekt ma się znajdować:
$ composer create-project -prefer-dist laravel/laravel rozdzial_29
Jeśli jednak zamierzamy używać frameworka Laravel często, do tworzenia wielu różnych pro-
jektów, to nieco prostszym rozwiązaniem będzie zainstalowanie narzędzia Laravel Installer —
programu obsługiwanego z poziomu wiersza poleceń i korzystającego z Composera — a następnie
używanie go do tworzenia kolejnych projektów. Także w tym przypadku należy rozpocząć od
wykorzystania Composera, lecz tym razem w celu zainstalowania programu Laravel Installer:
$ composer global require "laravel/installer"
Istnieje wiele rzeczy, które można skonfigurować lub zmienić, by dodatkowo dostosować sposób
działania nowego, ogólnego projektu Laravel po jego utworzeniu. Krytyczne znaczenie ma jednak
w rzeczywistości tylko jedna z nich — sprawdzenie, czy serwer WWW ma prawo zapisu w kilku
określonych katalogach projektu. A zatem, zaczynając od katalogu projektu (w naszym przypadku
jest to katalog rozdzial_29), trzeba sprawdzić, czy poniżej wymienione katalogi mają uprawnienia
pozwalające na zapis ich zawartości przez serwer WWW, gdyż jest to konieczne do prawidło-
wego działania frameworka Laravel:
./storage
./bootstrap/cache
Choć samo uruchomienie projektu Laravel nie wymaga zbyt zaawansowanej konfiguracji,
to jednak istnieje bardzo wiele ustawień konfiguracyjnych, które można podawać w celu
określenia sposobu działania poszczególnych komponentów frameworka. W katalogu
config/ znajduje się sporo plików konfiguracyjnych, pozwalających na konfigurację działania
pamięci podręcznej, debugowania, dostępu do bazy danych, serwerów poczty elektronicznej
itd. Zaleca się, by w najprostszym przypadku otworzyć plik config/app.php i określić prawidłowe
wartości ustawień konfiguracyjnych aplikacji (takie jak strefa czasowa lub używane ustawienia
lokalne).
Podczas pisania aplikacji z użyciem frameworka Laravel w głównej mierze będziemy się kon-
centrować na plikach znajdujących się w katalogu app, gdyż to właśnie w nim umieszczana jest
przeważająca część logiki aplikacji. Domyślnie kod przechowywany w tym katalogu należy do
przestrzeni nazw App, dzięki czemu bez trudu można go dodawać do istniejącej struktury klas.
Na przykład aby utworzyć w aplikacji klasę o nazwie Foo, jej kod można umieścić w pliku
app\Library\Foo.php, a sama klasa powinna należeć do przestrzeni nazw \App\Library\Foo.
Gdyby ktoś chciał zmienić nazwę bazowej przestrzeni nazw z domyślnej App na jakąś inną,
to powinien poszukać w dokumentacji Laravela informacji o poleceniu Artisan app:name.
W nowej instalacji projektu Laravel 5 katalog app zawiera podstawowe klasy oraz strukturę ka-
talogów wystarczającą do utworzenia prostej aplikacji jednostronicowej. Oznacza to, że strukturę
tego katalogu można dodatkowo rozbudować zgodnie z poniższymi informacjami:
Katalog Commands — zawiera wszelkie polecenia frameworka Laravel tworzone na
potrzeby aplikacji. Mogą to być polecenia wydawane w wierszu poleceń (CLI), zadania
asynchroniczne bądź też zwyczajne zadania synchroniczne wykonywane przez inne
fragmenty aplikacji. W bardzo wielu aplikacjach internetowych tworzenie takich poleceń
nie jest potrzebne.
Katalog Events — zawiera klasy używane przez menedżera zdarzeń dostarczanego przez
framework Laravel. Framework udostępnia zdarzenia implementowane zarówno w formie
klas, jak i bez ich użycia, a katalog Events jest przeznaczony do przechowywania tych
pierwszych.
Katalog Handlers — stanowi przeciwieństwo opisanego powyżej katalogu Events. Służy
on do przechowywania klas używanych do obsługi zdarzeń zgłaszanych w aplikacji oraz
wykonywania określonej logiki w reakcji na te zdarzenia. Na przykład w katalogu Events
można by umieścić kasę reprezentującą zdarzenie dodania nowego użytkownika, którego
zgłoszenie będzie powodować wykonanie klasy obsługi HandleNewUser, czyli obsługę tego
zdarzenia.
Katalog Services — zawiera wszelkie usługi zdefiniowane w aplikacji. We frameworku
Laravel usługi są mechanizmem pomocniczym umożliwiającym zachowanie wysokiego
poziomu abstrakcji głównego kodu aplikacji, co ułatwia jego wielokrotne użycie. Na przykład
można by stworzyć klasę usługi w celu zarządzania użytkownikami, która będzie używana
w innych miejscach aplikacji. Innym przykładem mogłaby być usługa zapewniająca
możliwość komunikacji z inną witryną.
Katalog Exceptions — zgodnie z przewidywaniami służy on do przechowywania wszelkich
niestandardowych klas wyjątków używanych w aplikacji, które będą zgłaszane w przypadku
występowania błędów.
Dla większości, o ile nawet nie dla wszystkich, klas umieszczanych w opisanych powyżej
katalogach można automatycznie generować prawidłowe szablony. Służy do tego polecenie
artisan frameworka Laravel. Pełną listę tych tak zwanych generatorów można wyświetlić,
wykonując w oknie terminala polecenie php artisan list make z poziomu głównego
katalogu projektu.
574 Część V Tworzenie praktycznych projektów PHP i MySQL
Jak wspomnieliśmy już wcześniej, w tym rozdziale zaczniemy tworzyć prostego internetowego
klienta poczty elektronicznej; na jego przykładzie zademonstrujemy możliwości i sposób używa-
nia frameworka Laravel, a także protokołu IMAP i udostępnianych przez PHP funkcji do jego
obsługi. W trakcie tworzenia aplikacji wiele pojęć i katalogów opisanych w tym punkcie rozdziału
zostanie opisanych dokładniej i wyjaśnionych na praktycznych przykładach.
Jądra aplikacji
W zależności od natury żądania (czy zostało ono zainicjowane przez żądanie HTTP, czy też
przez polecenie wydane w oknie konsoli) jest ono obsługiwane przez odpowiednie jądro apli-
kacji. Ze względu na charakter prezentowanej tu aplikacji skoncentrujemy się wyłącznie na jądrze
HTTP, stanowiącym pojedynczy punkt wejścia, którym żądania HTTP trafiają do frameworka
Laravel, a tym samym także do aplikacji. To jądro HTTP jest odpowiedzialne za zdefiniowanie
przeważającej większości potrzeb niezbędnych do obsługi każdego żądania, takich jak rejestracja,
obsługa błędów itd.; jeśli to konieczne, można je zmienić i dostosować do potrzeb tworzonej
aplikacji.
Poza tym, że jądro aplikacji zajmuje się ustawieniem podstawowych narzędzi i możliwości niezbęd-
nych do obsługi żądań, odpowiada także za zdefiniowanie tak zwanego oprogramowania warstwy
pośredniej używanego w aplikacji, które musi zostać wykonane w ramach obsługi każdego żądania,
zanim będzie można je przekazać do samej aplikacji. W kontekście żądań HTTP przykładami
takiego oprogramowania warstwy pośredniej mogą być obsługa danych sesji HTTP, przetwa-
rzanie żetonów zapobiegających wykradaniu danych podczas obsługi formularzy przesyłanych
pomiędzy witrynami czy też przeprowadzanie wszelkich innych operacji, które — z logicznego
punktu widzenia — muszą zostać wykonane przed logiką aplikacji. Jądro aplikacji jest także
odpowiedzialne za wczytywanie i obsługę dostawców usług (ang. Service Providers), którzy zostaną
opisani w dalszej części rozdziału.
Klasę jądra HTTP frameworka Laravel można znaleźć w pliku app/Http/Kernel.php, przy czym
rozszerza ona klasę Illuminate\Foundation\Http\Kernel. Dzięki temu twórca aplikacji dysponuje
punktem dostępu pozwalającym na dostosowywanie używanego oprogramowania warstwy po-
średniej, rejestracji, obsługi błędów itd.
Dostawcy usług
Kolejnym krokiem wykonywanym przez jądro Laravel podczas obsługi żądań jest inicjalizacja i re-
jestracja (w nim samym) wszelkich zdefiniowanych dostawców usług. Dostawcy usług są niezwykle
Rozdział 28. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela 575
Dla osoby zainteresowanej pisaniem bibliotek dla frameworka Laravel, których będzie można
następnie używać w jego aplikacjach, dostawcy usług są punktem wyjścia. Na przykład chcąc
stworzyć komponent zapewniający dostęp do serwisu Twitter, należałoby napisać bibliotekę,
a następnie dostawcę usługi, który udostępniłby tę bibliotekę oraz jej możliwości funkcjonalne
wewnątrz frameworka. Później w kodzie aplikacji można by podać tego dostawcę w ustawieniach
konfiguracyjnych, a tym samym uzyskać do niego dostęp w kodzie aplikacji.
Nawet jeśli ktoś nie jest zainteresowany pisaniem bibliotek komponentów, to musi wiedzieć, że do-
stawcy usług są kluczowym aspektem każdej aplikacji używającej frameworka Laravel. Domyślnie
każda nowa aplikacja Laravel definiuje w katalogu app\Providers dostawców, którzy są stosowani
do inicjalizacji różnych komponentów. Ci dostawcy (jak również dostawcy, których utworzymy
samodzielnie) pozwalają aplikacji na zainicjowanie dowolnych niezbędnych zasobów jeszcze przed
przekazaniem obsługi żądania do logiki aplikacji napisanej przez jej twórców.
Framework Laravel udostępnia trzy podstawowe metody służące do określania tras, odpowiadają-
ce trzem metodom HTTP, których można użyć do przesłania żądania. Na przykład poniższy
fragment kodu przedstawia prostą trasę kojarzącą żądania HTTP GET skierowane do dokumentu
głównego ('/') z najprostszym kontrolerem, który można utworzyć w Laravelu (czyli mającym
postać domknięcia), oraz drugą trasę, kojarzącą żądania HTTP typu POST odnoszące się do adresu
/submit z kolejnym domknięciem:
Route::get('/', function() {
return 'Witaj, świecie!';
});
Route::post('submit', function() {
return 'Przesłano żądanie POST.';
});
576 Część V Tworzenie praktycznych projektów PHP i MySQL
Oprócz najczęściej stosowanych metod HTTP GET i POST framework Laravel udostępnia także
metody pozwalające na obsługę innych typów żądań: Route::put(), Route::delete() itd. Jeśli
zajdzie potrzeba, by jeden zestaw kodu obsługiwał wiele różnych metod HTTP, to można to
osiągnąć przy użyciu metody Route::any() (która będzie dopasowywać trasę wyłącznie na podsta-
wie ścieżki, niezależnie od zastosowanej metody HTTP) bądź też metody Route::match() (pozwa-
lającej jawnie zdefiniować, które metody mają być obsługiwane):
Route::match(['post','put'], '/', function() {
return 'Obsługiwanym żądaniem było HTTP POST lub HTTP GET!';
});
Route::any('universal', function() {
return 'Żądanie przesłano na adres /universal, obsługuje on dowolne żądania HTTP.';
}
Parametry trasy
Oprócz stosowania w trasach prostych ścieżek router Laravela umożliwia także tworzenie tras
zawierających zmienne, definiowane przy użyciu prostej składni. Na przykład gdybyśmy chcieli
stworzyć trasę obsługującą żądania GET, która będzie pobierać i wyświetlać artykuł o identyfikatorze
podanym w ścieżce (na przykład /article/1234), moglibyśmy to zrobić w następujący sposób:
Route::get('article/{id}', function() {
return 'Zażądano wyświetlenia artykułu o identyfikatorze: ' . $id;
});
Jeśli parametr danej trasy jest opcjonalny, to w definicji trasy za nazwą zmiennej należy dodać
znak zapytania (?), jak pokazano w poniższym przykładzie:
Route::get('articles/{id?}', function() {
if (is_null($id) {
return 'Zażądano wyświetlenia wszystkich artykułów.';
} else {
return 'Zażądano wyświetlenia artykułu o identyfikatorze: ' . $id;
}
});
Czasami w trakcie stosowania parametrów w trasach może być konieczne posiadanie większej
kontroli nad tym, jakie wartości parametrów będą akceptowane. W powyższych przykładach
wartością parametru {id} mogły być liczba lub łańcuch znaków, co często może być sprzeczne
z projektem aplikacji. Aby wymusić zastosowanie określonego formatu dla konkretnego para-
metru trasy, można użyć metody Route::where(), która narzuca warunek, określony w formie
wyrażenia regularnego, na wartość danego parametru. Warunek ten musi być spełniony, by
możliwe było dopasowanie trasy dla danego żądania. Oto przykłady określania dwóch takich
warunków:
Route::get('articles/{id?}', function($id) {
return 'Podano wartość liczbową: ' . $id;
})->where('id', '[0-9]+');
Rozdział 28. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela 577
Route::get('articles/{id?}', function($id) {
return 'Podano wartość łańcuchową: ' . $id;
})->where('id', '[A-Za-z]+');
W powyższym przykładzie zostały zdefiniowane dwie odrębne trasy odwołujące się do tego
samego punktu końcowego — /articles/<ID> — przy czym pierwsza z nich obsługuje wszystkie
żądania, w których wartość parametru ID była liczbą, a druga żądania, w których parametrem
był łańcuch znaków. Jeśli wartość parametru ID nie spełniła żadnego z tych dwóch kryteriów,
to żadna trasa nie zostanie dopasowana i aplikacja wygeneruje błąd HTTP o wartości 404.
Warto także zauważyć, że trasy mogą zawierać dowolną liczbę parametrów zapisanych w ten
sam, przedstawiony wcześniej sposób. W takim przypadku wartości poszczególnych parametrów
można ograniczać, przekazując w wywołaniu metody Route::where tablicę wyrażeń regularnych:
Route::get('articles/{sekcja}/{id}', function($sekcja, $id) {
return 'Zażądano artykułu o identyfikatorze ' . $id .
' z sekcji o nazwie: ' . $sekcja;
})->where(['sekcja' => '[A-Za-z]+', 'id' => '[0-9]+']);
Grupy tras
Często podczas definiowania tras przydatna jest możliwość definiowania globalnych zachowań
dla takiego bądź innego podzbioru tras. Na przykład niektóre trasy mogą mieć sens wyłącznie
dla uwierzytelnionego użytkownika, a inne mogą być poprzedzone jedną wspólną ścieżką bazową,
na przykład authenticate/view oraz authenticated/create. Właśnie w tym celu framework Laravel
udostępnia grupy tras.
Do grupowania tras służy metoda Route::group(). Pierwszym parametrem tej metody jest seria
definicji wspólnych dla wszystkich elementów grupy, a drugim — domknięcie zawierające poszcze-
gólne trasy, które do niej należą. Wróćmy do przykładu podanego w poprzednim akapicie —
gdybyśmy chcieli stworzyć grupę tras, z których każda ma być poprzedzona wspólną ścieżką
'authenticated', to moglibyśmy to zrobić w następujący sposób:
Route::group(['prefix' => 'authenticated'], function() {
Route::get('view', function() {
return 'To jest trasa: authenticated\view';
});
Route::get('create', function() {
return 'To jest trasa: authenticated\create';
});
});
Router frameworka Laravel obsługuje wiele różnych definicji, takich jak prefix, która została
zastosowana w powyższym przykładzie. Poniżej przedstawionych zostało kilka z tych definicji,
których można używać podczas tworzenia grup:
578 Część V Tworzenie praktycznych projektów PHP i MySQL
Więcej informacji na temat definiowania tras oraz ich różnych konfiguracji można znaleźć w doku-
mentacji frameworka Laravel dostępnej na stronie https://laravel.com/docs/5.2/routing.
Stosowanie kontrolerów
W poprzednim podpunkcie rozdziału, poświęconym routerowi, cała logika obsługi danej trasy
była zaimplementowana w postaci domknięcia. Choć to rozwiązanie jest całkowicie prawidłowe,
to jednak nie jest ono najczęściej stosowanym sposobem definiowania logiki obsługi tras. Zamiast
domknięć używa się zazwyczaj metod klasy kontrolerów.
Kontrolery są bardzo użyteczne pod względem organizacji logiki aplikacji, pozwalają bowiem
na grupowanie powiązanego ze sobą kodu w pojedynczych klasach umieszczonych w katalogu
app/Http/Controllers. Każdy nowy projekt aplikacji Laravel zawiera klasę App\Http\Controllers\
Controller, która powinna stanowić klasę bazową dla wszystkich własnych kontrolerów.
W ramach przykładu przeanalizujmy poniższą klasę kontrolera (zapisaną w pliku app/Http/Controllers/
MyController.php):
namespace App\Http\Controllers;
Istnieje kilka sposobów na to, by użyć tego kontrolera jako punktu końcowego dla określonej
trasy. Jednym z najprostszych z nich jest dodanie tras do aplikacji, jak zrobiono to w poniższym
przykładzie obsługującym żądania HTTP GET:
Route::get('/', 'MyController@myAction');
Podobnie jak w poprzednim punkcie rozdziału, w którym zamiast kontrolerów były używane do-
mknięcia, także w przypadku stosowania kontrolerów można określać parametry tras. Jedyna różnica
polega na tym, że parametry trasy będą powiązane z parametrami metody, a nie domknięcia.
Choć wykonywanie operacji takich jak określanie oprogramowania warstwy pośredniej podczas
definiowania trasy jest rozsądnym i przydatnym rozwiązaniem, to jednak określanie takiego
oprogramowania warstwy pośredniej używanego w danym kontrolerze można także wykonać
jawnie — w konstruktorze klasy kontrolera. Co więcej, w zależności od sposobu organizacji
kodu takie rozwiązanie może nawet być preferowane. Na przykład oprogramowanie warstwy
pośredniej auth można dodać do kontrolera w następujący sposób:
namespace App\Http\Controllers;
Możliwości wstrzykiwania oferowane przez kontrolery frameworka Laravel obejmują także metody.
Na przykład aby uzyskać dostęp do obiektu Request (zawierającego wszelkie informacje dotyczące
obecnie obsługiwanego żądania, takie jak dane GET lub POST, parametry trasy itd.), można go
umieścić bezpośrednio w deklaracji metody, jak pokazano w poniższym przykładzie:
namespace App\Http\Controllers;
use Illuminate\Http\Request
W kontrolerach obiekt Request najczęściej jest używany w celu pobierania danych skojarzonych
z obiektem żądania, takich jak parametry żądania GET lub dane przesłane w żądaniu POST. We
frameworku Laravel dostęp do danych żądania jest zunifikowany do postaci jednej metody —
Request::input() — definiującej dwa parametry. Pierwszym z nich jest klucz, którego należy
użyć w celu pobrania danej (takiej jak nazwa zmiennej), a drugim wartość domyślna, zwracana
w przypadku, gdy dana nie jest dostępna w żądaniu. W ramach przykładu rozważmy poniższe
hipotetyczne żądanie HTTP: http://www.example.com/myaction?name=Janek. Dane przekazane
w tym żądaniu można pobrać w kodzie kontrolera w następujący sposób:
public function myAction(Request $request) {
$name = $request->input('name', 'Janek');
return "Nadane imię to $name (domyślnie jest używane: Janek)";
}
Podczas pobierania danych przy użyciu metody Request::input() nasze możliwości nie ograni-
czają się jedynie do pobierania prostych nazw zmiennych. Na przykład jeśli dane wejściowe są
jakiegoś złożonego typu (takiego jak tablica), można skorzystać ze specjalnego sposobu zapisu
z kropką (.), by pobrać wybraną wartość zapisaną w takiej tablicy. Przykład zamieszczony po-
niżej jest podobny do poprzedniego, ale tym razem imię jest umieszczone w elemencie tablicy
$myarray['mykey'][0]['name']:
public function myAction(Request $request) {
$name = $request->input('myarray.mykey.0.name', 'Janek');
}
Warto zwrócić uwagę na to, że tego zapisu można używać nawet wtedy, gdy dane wejściowe są
zapisane w formacie JSON, choć w tym przypadku żądanie musi zawierać nagłówek Content-Type
o wartości 'application/json'.
Aby sprawdzić dane wejściowe i przekonać się, czy istnieje w nich zmienna o określonej nazwie
(bez podawania jej domyślnej wartości), można skorzystać z metody Request::has():
public function myAction(Request $request)
{
if(!$request->has('name')) {
return "Nie podano imienia!";
}
$name = $request->input('name');
return "Podane imię to: $name";
}
Rozdział 28. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela 581
Istnieje kilka różnych sposobów na pobranie tablicy asocjacyjnej zawierającej wszystkie dane
dostępne w żądaniu. Najbardziej kompletne wyniki zapewnia metoda Request::all(), która zwraca
wszystkie dostępne dane. Gdyby ktoś chciał zwrócić tylko podzbiór tych danych, mógłby w tym
celu zastosować metodę Request::only(); zwraca ona wyłącznie zmienne, których nazwy zostały
podane w wywołaniu. Ewentualnie można by także użyć metody Request::except(), zwracającej
wszystkie zmienne, z wyjątkiem tych, których nazwy zostały wymienione. Poniższy przykład
ukazuje sposób wykorzystania obu tych metod:
public function myAction(Request $request)
{
$allInput = Request::all();
$onlyNameAndPhone = Request::only(['name', 'phone']);
$allButPassord = Request::except('password');
}
Ostatnim zastosowaniem obiektu Request, jakie zostanie tu przedstawione, jest użycie go do pobie-
rania danych wejściowych w formie plików przesłanych na serwer. Dostęp do plików przesłanych
w żądaniu na serwer jest zapewniany głównie za pomocą metody Request::file(), zwracającej
instancję typu Symfony\Component\HttpFoundation\File\UploadedFile, która z kolei udostępnia
metody pomocnicze pozwalające na dalszą obsługę pliku. Dwa najczęstsze wymagania związane
z obsługą plików przesłanych na serwer to sprawdzenie, czy dany plik został prawidłowo przesłany,
oraz przeniesienie pliku z tymczasowej lokalizacji, w której został początkowo umieszczony,
do docelowego miejsca, w którym powinien się znaleźć. Metody wykonujące te dwie operacje,
wraz z metodą Request::hasFile(), zostały przedstawione w kolejnym przykładzie, demonstrują-
cym prosty sposób obsługi plików przesyłanych na serwer.
public function myAction(Request $request)
{
if(!$request->hasFile('photo')) {
return "Nie przesłano żadnego pliku!";
}
$file = $request->file('photo');
if(!$file->isValid()) {
return "Plik nie jest prawidłowy!";
}
$file->move('/docelowa/sciezka/zdjecie/na/serwerze');
Stosowanie widoków
Jak pokazano w poprzednich punktach rozdziału, już samo używanie kontrolerów do imple-
mentacji logiki aplikacji (oraz zwracania jej wyników w formie łańcuchów znaków) może być
przydatne i korzystne. Niemniej jednak w aplikacjach internetowych separacja logiki widoków
jest jednym z kluczowych elementów dobrej organizacji kodu. Właśnie w tym celu stosuje się
komponent widoku — View — który pozwala na łatwe generowanie złożonych danych wynikowych
(takich jak kod HTML) z wykorzystaniem danych pobranych z żądania lub wyliczonych przez
kontroler.
We frameworku Laravel komponent View jest niezależnym narzędziem, którego można używać
wszędzie do generowania różnych wyników, takich jak kod HTML. Komponent ten jest na przykład
stosowany w komponencie poczty elektronicznej do generowania wiadomości wysyłanych do użyt-
kowników. W swojej najprostszej postaci implementacja widoku może się składać z utworzenia
582 Część V Tworzenie praktycznych projektów PHP i MySQL
szablonu widoku, a następnie użycia komponentu View do wygenerowania instancji widoku z wyko-
rzystaniem konkretnych wartości zmiennych zastosowanych w tym szablonie. W ramach przykładu
przeanalizujmy poniższy prosty szablon HTML, zapisany w pliku resources/view/welcome.php:
<html>
<head>
<title><?= $pageTitle ?></title>
</head>
<body>Witam, to jest bardzo prosty szablon.</body>
</html>
Aby wygenerować taki widok jako łańcuch znaków, wystarczy skorzystać z funkcji pomocniczej
view(), podając jako pierwszy argument wywołania nazwę widoku, a jako drugi tablicę par
klucz-wartość, reprezentującą dane, które należy udostępnić w widoku:
<?php
Route::get('/', function() {
return view('welcome', ['pageTitle' => 'Witamy w świecie widoków!']);
});
?>
Warto zwrócić uwagę na to, że nazwa widoku jest w zasadzie nazwą pliku przechowywanego
w katalogu resources/views, z której usunięto rozszerzenie .php. Szablony widoków są zazwy-
czaj zorganizowane w jakiś logiczny sposób i umieszczone w strukturze katalogów rozpoczynają-
cej się od nadrzędnego katalogu resources/views. Jedną z klasycznych technik organizacji jest
zapisywanie widoków w katalogach odpowiadających kontrolerom, w których widoki te są
używane. W takim przypadku, by określić podkatalog zawierający widok, wystarczy posłużyć
się prostą notacją z kropką. Poniższy przykład pokazuje, jak można się odwołać do szablonu
widoku zapisanego w pliku resources/views/mycontroller/index.php:
<?php
Route::get('/', function() {
return view('mycontroller.index', ['jakas_zmienna' => 'wartość zmiennej']);
});
?>
Typowy projekt widoków przewiduje, że mają one dostęp wyłącznie do tych danych, które
zostały do nich przekazane przy użyciu funkcji pomocniczej view(). Niemniej jednak istnieje
także możliwość tworzenia „zmiennych globalnych”, które będą domyślnie dostępne
we wszystkich widokach. Można to zrobić, używając następującego wywołania:
view()->share('klucz', 'wartość');
Widoki Blade
Opcjonalnym, ale bardzo użytecznym elementem widoków we frameworku Laravel jest wyko-
rzystanie mechanizmu obsługi widoków, noszącego nazwę Blade. Choć w zasadzie w Laravelu
w szablonach widoków można stosować zwyczajny kod PHP, to jednak mechanizm Blade udo-
stępnia wiele potężnych możliwości organizacyjnych, pozwalających na tworzenie szablonów
w logiczny sposób bez konieczności stosowania w nich rozbudowanej logiki. Warto zauważyć,
że szablon Blade może zawierać zwyczajny kod PHP, tak jak dowolny inny szablon widoku.
Jednak zastosowanie mechanizmu Blade daje dwie podstawowe korzyści: dziedziczenie sza-
blonów oraz definiowanie sekcji; obie zostaną opisane poniżej.
Rozdział 28. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela 583
Szablony Blade są tworzone tak samo jak każdy inny widok, z tą różnicą, że należy zapisywać
je w plikach z rozszerzeniem .blade.php, a nie .php. W ramach prezentacji sposobu działania
szablonów Blade przeanalizujemy problem powszechnie występujący w aplikacjach interneto-
wych — konieczność zapewnienia spójnego wyglądu wszystkim stronom aplikacji.
@yield('content')
</body>
</html>
@section('stylesheets')
@parent
<link href="/sciezka/do/inne/stylesheet.css" rel="stylesheet"/>
@stop
@section('sidebar')
<div class="col-and-4"›
<ul>
<li><a href="/">Strona główna</a></li>
<li><a href="/account">Moje konto</a></li>
</ul>
</div>
@stop
@section('content')
Witaj, świecie!
@stop
584 Część V Tworzenie praktycznych projektów PHP i MySQL
To tylko wybrane możliwości komponentu View frameworka Laravel oraz mechanizmu generacji
widoków Blade. Niemniej jednak te podstawowe informacje w zupełności wystarczą do napisania
internetowego klienta poczty elektronicznej. Jeśli Czytelnik chciałby dowiedzieć się czegoś
więcej na temat widoków we frameworku Laravel, to powinien zajrzeć do jego dokumentacji.
Mechanizmy typu ORM są zwykle najbardziej złożonym kodem, który można znaleźć we frame-
workach, niemniej jednak ich przeznaczenie jest całkiem proste. Chodzi o to, żeby zamiast stoso-
wać w aplikacji zapytania SQL do zapisywania i pobierania informacji z baz danych, framework
generował te zapytania automatycznie, a nam, programistom, pozwalał na wchodzenie w interakcje
z bazą danych w sposób obiektowy.
Rozdział 28. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela 585
Aby rozpocząć korzystanie z modeli Eloquent, w pierwszej kolejności musimy mieć bazę danych
(my zastosujemy bazę danych MySQL) oraz prawidłowo skonfigurowane połączenie z tą bazą
w aplikacji Laravel. Ustawienia konfiguracyjne tego połączenia można znaleźć w pliku config/
database.php w aplikacji, w elemencie o kluczu 'connection'. Poniżej przedstawiony został
przykład prawidłowo skonfigurowanego połączenia z serwerem MySQL działającym na lokalnym
komputerze:
...
'connections' => [
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'chap29'),
'username' => env('DB_USERNAME', 'myuser'),
'password' => env('DB PASSWORD', 'mypass'),
'charset' => 'UTF-8',
'collation' => 'utf8_unicode_cil',
'prefix' => '',
'strict' => false
]
]
...
Skoro połączenie z bazą danych zostało już skonfigurowane, to możemy zająć się sposobem
tworzenia modeli Eloquent, które będą go używały. Na potrzeby tego przykładu załóżmy, że
dysponujemy bazą danych MySQL składającą się z dwóch tabel: książek i autorów. Konkretne
kolumny dostępne w tych tabelach nie mają większego znaczenia dla naszych rozważań; należy
tylko wiedzieć, że pomiędzy tabelą autorów i tabelą książek istnieje relacja 1–N. Oznacza to, że
jedna z kolumn tabeli książek musi zawierać klucz obcy wskazujący jeden z wierszy tabeli autorów.
We frameworku Laravel tworzenie takiej definicji schematu bazy danych odbywa się przy użyciu
procedury migracji bazy danych. Procedura ta rozpoczyna się od utworzenia klasy migracji
przy wykorzystaniu wydawanego z poziomu okna konsoli polecenia artisan, takiego jak to
przedstawione poniżej:
$ php artisan migrate:make create_author_and_books_schema
$table->foreign('author_id')
->references('id')
->on('authors')
->onDelete('cascade');
});
}
Poniżej został przedstawiony kod klasy ORM o nazwie Authors, zapisany w pliku app/Models/
Authors.php:
<?php
namespace App\Models;
?>
Choć może to być sporym zaskoczeniem, to jednak naprawdę wszystko, czego potrzeba, by
uzyskać podstawowy dostęp do tabeli 'authors' na serwerze MySQL. Eloquent automatycznie
określi nazwę tabeli na podstawie nazwy klasy (będzie to liczba mnoga od słowa 'author', zapisana
małymi literami) i przyjmie, że do schematu można się odwołać przy użyciu domyślnego połą-
czenia z bazą danych. Dysponując tą klasą, można już w bardzo prosty sposób wstawiać dane
do tabeli:
$myModel = new Author();
$myModel->naem = 'John Coggeshall';
$myModel->save();
Wykonywanie innych operacji, takich jak odczytywanie rekordów czy też ich aktualizacja, jest
zazwyczaj równie łatwe, o czym będzie się można przekonać w dalszej części rozdziału.
Jednak elementami, które nie są zdefiniowane w mechanizmie Eloquent, są druga z tabel w naszym
schemacie, 'books', oraz związek pomiędzy tabelą książek a tabelą autorów. Aby uzupełnić te
braki, musimy zdefiniować drugą klasę Eloquent, o nazwie Book, bardzo podobną do klasy Author:
<?php
namespace App\Models;
Po utworzeniu obu tych klas możemy zacząć definiować związek pomiędzy nimi, reprezentujący
relację pomiędzy obiema tabelami bazy danych. W tym celu w każdej z tych klas należy utworzyć
metodę o nazwie odpowiadającej nazwie drugiej klasy i zwracającą obiekt zależności. W rozpatry-
wanym przypadku pomiędzy tabelą autorów i tabelą książek występuje relacja jeden-do-wielu,
a zatem w klasie Author należy utworzyć metodę books(), która zwróci obiekt zależności, a w klasie
Book — metodę zwracającą obiekt zależności. Kody obu tych metod zostały przedstawione poniżej.
Plik app/Models/Author.php
<?php
namespace App\Models;
Plik app/Models/Book.php
<?php
namespace App\Models;
Jak pokazano w jednym z wcześniejszych przykładów, tworzenie rekordu bazy danych przy wyko-
rzystaniu modeli Eloquent jest banalne i sprowadza się do utworzenia instancji danej klasy, zapi-
sania odpowiednich danych w jej polach i wywołania metody save(). Dokładnie to samo dotyczy
aktualizacji już istniejących rekordów; w zasadzie — z punktu widzenia mechanizmu Eloquent
— jedyna różnica pomiędzy operacją dodania i operacją aktualizacji sprowadza się do tego,
czy klucz główny rekordu jest określony, czy nie. Jeśli pole klucza głównego ma wartość null,
to Eloquent przyjmuje, że wykonywana jest operacja wstawienia (utworzenia) rekordu; jeżeli z kolei
pole klucza głównego ma wartość inną niż null, to Eloquent przyjmuje, że wykonywana jest
operacja aktualizacji:
<?php
$authors = \App\Models\Author::all();
foreach($authors as $author) {
print "Personalia autora to: {$author->name}.";
}
?>
Przyjrzyjmy się teraz różnym możliwościom zadawania zapytań udostępnianych przez Eloquent.
W metodach przedstawionych na poniższej liście została zastosowana ogólna klasa modelu Eloquent
— Model — którą należy zastąpić konkretną klasą. Trzeba pamiętać, że poniższa lista nie obejmuje
wszystkich metod dostępnych w klasach modeli Eloquent — te można znaleźć w dokumentacji API
frameworka Laravel.
Rozdział 28. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela 589
Często pojawia się konieczność wykonywania zapytań z użyciem zależności pomiędzy poszczegól-
nymi modelami Eloquent. Przedstawienie pełnych możliwości funkcjonalnych związanych z tym
zagadnieniem wykracza poza ramy tematyczne tego rozdziału, wspomnimy jednak, że w zasadzie
można to wykonać tego typu zapytania całkiem łatwo, wywołując zależność jako metodę, a następ-
nie używając tych samych metod zapytań, które zostały przedstawione powyżej. W takich przypad-
kach Eloquent automatycznie doda do zapytania niezbędne zależności:
<?
$author = Author::find(1); // zakładamy, że ten wiersz istnieje
Jak już wspominaliśmy wcześniej w tym rozdziale, dostępna jest rozbudowana dokumentacja
internetowa związana z Laravelem. Gorąco zachęcamy do tego, by podczas tworzenia własnych
aplikacji korzystać z niej w celu poszerzania swojej wiedzy. Dokumentację tę można znaleźć na
stronie https://laravel.com/docs/5.3.
590 Część V Tworzenie praktycznych projektów PHP i MySQL
Rozdział 29.
Tworzenie internetowego
klienta poczty elektronicznej
z użyciem Laravela
— część 2.
Skoro zaprezentowaliśmy już podstawowe pojęcia i komponenty frameworka Laravel, możemy
przejść do opisu stosowania ich w praktyce. W ramach projektu, który zostanie przedstawiony
w tym rozdziale, połączymy informacje o frameworku Laravel z możliwościami funkcjonalnymi
obsługi protokołu IMAP udostępnianymi przez PHP i stworzymy prostego, internetowego klienta
poczty elektronicznej.
W języku PHP dostęp do serwerów IMAP zapewnia rozszerzenie PHP IMAP — niezwykle solidny
zestaw narzędzi operujących na stosunkowo niskim poziomie, pozwalających na manipulowa-
nie pocztą elektroniczną i korzystanie z niej przy użyciu protokołu IMAP. Nie wszystkie możliwości
funkcjonalne udostępniane przez to rozszerzenie są potrzebne do napisania prostego klienta poczty
elektronicznej, który mamy zamiar zaimplementować w ramach tego projektu; skoncentrujemy
się więc tutaj wyłącznie na tych, z których będziemy korzystać.
Podobnie jak wiele starszych rozszerzeń PHP, także funkcje IMAP działają głównie w oparciu
o zasoby. W swojej najprostszej postaci połączenie z serwerem IMAP jest otwierane przy uży-
ciu funkcji imap_open(), która zwraca zasób PHP reprezentujący to polecenie. Zasób ten jest
następnie wykorzystywany w kolejnych interakcjach z serwerem, przy czym zazwyczaj jest
przekazywany jako pierwszy parametr funkcji.
Choć w dokumentacji PHP nie ma o nich wzmianki, dostępne są jednak także inne stałe, których
można używać w tej masce bitowej; zostały one tu jednak pominięte, gdyż albo nie są przydatne
dla programistów, albo są przeznaczone wyłącznie dla twórców rozszerzeń.
Drugi z opcjonalnych parametrów funkcji imap_open(), $n_retires, jest liczbą całkowitą opisują-
cą, ile razy funkcja ma próbować nawiązać połączenie, zanim zgłosi błąd; w końcu ostatni parametr,
$params, to tablica par klucz-wartość, pozwalająca na ustawienie dodatkowych opcji. W czasie
gdy powstawała ta książka, parametru tego można było użyć do przekazania tylko jednej opcji,
Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 593
Wszystkie flagi są określane w formie ścieżki w definicji serwera i w tej postaci można je dowolnie
łączyć. Jedną z dostępnych flag jest /novalidate-cert, która informuje o tym, że nie należy weryfi-
kować certyfikatów bezpieczeństwa w przypadku nawiązywania połączeń TLS/SSL (co byłoby
potrzebne w przypadku stosowania samodzielnie podpisanych certyfikatów serwera). Powyższy
łańcuch połączenia korzystający z tej flagi miałby następującą postać:
{imap.gmail.com:993/imap/ssl/novalidate-cert}/INBOX
Kompletny wykaz flag połączeń IMAP wraz z ich znaczeniem można znaleźć w dokumentacji
PHP dostępnej na stronie poświęconej funkcji imap_open().
Pierwszym parametrem funkcji imap_list() jest, zgodnie z oczekiwaniami, zasób serwera zwrócony
przez funkcję imap_open(). Drugim parametrem jest odwołanie do serwera, które zazwyczaj ma
taką samą postać jak parametr $mailbox_spec funkcji imap_open() (bez podawania fragmentu
określającego skrzynkę pocztową). I w końcu ostatni parametr, $search_pattern, pozwala na okre-
ślenie konkretnej części hierarchii skrzynek pocztowych, do której ma należeć pobierana lista.
Wzorzec określany przez parametr $search_pattern może stanowić ścieżkę konkretnej skrzynki
pocztowej; zapewnia on obsługę dwóch szczególnych przypadków wyszukiwania. Pierwszym z nich
jest zastosowanie wzorca o postaci '*', który zwraca wyłącznie bieżące skrzynki pocztowe dostępne
w określonej ścieżce. Poniżej przedstawiony został przykład wykorzystania tej funkcji:
<?php
// zwraca listę wszystkich skrzynek
$mailboxes = imap_list($resource, '{imap.google.com:993/imap/ssl}', '*');
Zanim będzie można pobrać listę wiadomości, trzeba będzie przejść do odpowiedniej skrzynki
pocztowej. Choć jest to dość dziwne, to jednak w rozszerzeniu IMAP PHP nie ma żadnej
oczywistej funkcji, która intuicyjnie mogłaby zostać wykorzystana do tego celu. Okazuje się, że
do zmiany skrzynki pocztowej należy użyć funkcji imap_reopen(), która „ponownie otwiera”
połączenie w kontekście nowej skrzynki pocztowej:
bool imap_reopen(resource $resource, string $new_mailbox_ref [, int $options [, int $n_retries]])
Podobnie jak wcześniej, także w tym przypadku parametr $resource jest zasobem zwróconym
przez początkowe wywołanie funkcji imap_open(). Drugi parametr, $new_mailbox_ref, jest łańcu-
chem znaków zapisanym w takim samym formacie jak parametr $mailbox_ref funkcji imap_open(),
choć odwołuje się do skrzynki pocztowej, do której chcemy przejść. Ostatnie dwa parametry,
$options oraz $n_retries, mają dokładnie takie samo przeznaczenie jak funkcja imap_open().
A zatem zmiany skrzynki pocztowej 'INBOX' na 'Archive' na serwerze IMAP należałoby dokonać
w następujący sposób:
<?php
$connection = imap_open('{imap.gmail.com:993/imap/ssl}/INBOX', $username, $password);
if(imap_reopen($connection '{imap.gmail.com:993/imap/ssl}/Archive')) {
echo "Zmieniono skrzynkę pocztową na 'Archive'";
} else {
echo "Nie udało się zmienić skrzynki pocztowej.";
}
?>
Programiści mają do dyspozycji także inne funkcje służące do wykonywania różnych operacji
na skrzynkach pocztowych (na przykład funkcje do ich tworzenia i usuwania), jednak wykraczają
one poza zakres prostego, internetowego klienta pocztowego, który mamy zamiar zaimplementować
w tym rozdziale. Gdyby jednak Czytelnik chciał skorzystać z tych funkcji, to zachęcamy do po-
szukania informacji na ich temat w dokumentacji PHP.
Można przypuszczać, że użytkownik najpierw będzie chciał pobrać listę wiadomości dostępnych
w skrzynce pocztowej, a dopiero potem pobrać i wyświetlić jedną z nich. Korzystając z rozsze-
rzenia IMAP PHP, można to zrobić przy użyciu funkcji imap_fetch_overview(), która zwraca
wskazany zakres wiadomości z określonej skrzynki pocztowej. Koniecznie należy zwrócić
uwagę na to, że funkcja ta zwraca jedynie pewien zakres wiadomości ze skrzynki, a nie wszyst-
kie z nich. Szczególnie w obecnych czasach, kiedy przestrzeń do magazynowania danych jest
tania, skrzynki pocztowe mogą zawierać nawet dziesiątki tysięcy wiadomości, dlatego pobieranie ich
wszystkich w ramach jednej operacji jest nie tylko niepraktyczne, lecz także bardzo nierozsądne.
Zamiast tego lepiej jest pobrać za jednym razem jedynie części dostępnych wiadomości i zaim-
plementować mechanizm stronicowania, pozwalający na poruszanie się do przodu i w tył listy.
Zagadnieniem tym zajmiemy się już za chwilę, na razie jednak przedstawimy samą funkcję
imap_fetch_overview() oraz sposób jej stosowania:
array imap_fetch_overview(resource $resource, string $sequence [, int $options = 0])
Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 595
Tak jak wcześniej, również w tym wywołaniu parametr $resource jest zasobem IMAP, a para-
metr $sequence łańcuchem znaków zawierającym opis tych wiadomości, które należy pobrać ze
skrzynki pocztowej. Określenie tej sekwencji może mieć kilka różnych postaci, a pisząc o sekwencji,
odwołujemy się do faktu, że każdej wiadomości w skrzynce pocztowej jest nadawany identyfi-
kator liczbowy, którego wartości sekwencyjnie rosną. Oznacza to, że pierwsza wiadomość w danej
skrzynce ma identyfikator o wartości 1, druga — identyfikator o wartości 2 itd. Podczas określania
tej sekwencji można bądź to podać identyfikatory poszczególnych wiadomości, oddzielając je od
siebie przecinkami, bądź też określić cały zakres przy użyciu formatu "X:Y", gdzie X jest początkiem
sekwencji, a Y końcem zakresu pobieranych wiadomości.
<?php
// pobranie pierwszych dziesięciu wiadomości ze skrzynki pocztowej
// przy wykorzystaniu zapisu zakresu
$messages = imap_fetch_overview($connection, "1:10");
?>
Uważny Czytelnik zapewne zauważył, że nie podaliśmy jeszcze opisu ostatniego, opcjonalnego
parametru funkcji imap_fetch_overview() — zrobiliśmy to celowo, gdyż odwołuje się on do nowego
pojęcia, które wymaga nieco bardziej rozbudowanego wprowadzenia. Przedstawiamy je poniżej.
Zgodnie z podanymi wcześniej informacjami parametr $sequence odwołuje się do wartości liczbo-
wej reprezentującej konkretne położenie danej wiadomości poczty elektronicznej w skrzynce
pocztowej. A zatem użycie sekwencji zakresu o postaci 1:10 lub 3,5,7 spowoduje, odpowiednio,
zwrócenie pierwszych dziesięciu wiadomości lub wiadomości trzeciej, piątej i siódmej. Wiado-
mości te są zwracane w formie tablicy. Jednak istnieje jeszcze inny sposób odwoływania się do
wiadomości w skrzynce pocztowej. Opiera się on na wykorzystaniu unikalnego identyfikatora
nadawanego każdej wiadomości przez serwer IMAP. Ten unikalny identyfikator pozwala na
szybkie pobranie konkretnej wiadomości bez konieczności lokalizowania jej położenia w skrzynce
pocztowej (zazwyczaj jest to możliwe dzięki temu, że wiadomość ta została odszukana już
wcześniej). Ów identyfikator jest zwracany jako jeden z elementów struktury danych (tablicy)
reprezentujących wiadomość, a konkretnie: w elemencie o kluczu 'uid'. W przypadku pobierania
wiadomości przy użyciu unikalnych identyfikatorów za pomocą funkcji imap_fetch_overview()
można przypisać opcjonalnemu parametrowi $options wartość FT_UID, a następnie w parametrze
$sequence podać listę unikalnych identyfikatorów wiadomości, oddzielonych od siebie przecin-
kami. Trzeba jednak pamiętać, że w przypadku posługiwania się tymi unikalnymi identyfikatorami
nie można określać zakresu pobieranych wiadomości (przy wykorzystaniu przedstawionego
wcześniej zapisu 'X:Y'). Zasadę tę demonstruje poniższy przykład:
<?php
// pobranie pierwszych dziesięciu wiadomości w bieżącej
// skrzynce pocztowej
$messages = imap_fetch_overview($connection, '1:10');
Jak wskazuje sama nazwa funkcji — imap_fetch_overview() — zwraca ona jedynie wybrane
informacje dotyczące pobieranych wiadomości, te, które są przeważnie potrzebne do wyświetlenia
listy wiadomości w kliencie pocztowym. Dla każdej wiadomości umieszczonej w tablicy wyników
zostanie zwrócona przedstawiona poniżej grupa par klucz-wartość, o ile tylko informacje te zostaną
zdefiniowane dla danej wiadomości. Ponieważ nie wszystkie serwery IMAP, a nawet nie wszystkie
wiadomości na danym serwerze dysponują tym samym zestawem szczegółowych informacji,
niektóre z tych kluczy mogą być niedostępne; dlatego przed ich użyciem zawsze należy sprawdzić,
czy można się do nich odwołać. Oto lista tych kluczy:
$email['subject'] — temat wiadomości;
$email['from'] — adres nadawcy wiadomości;
$email['to'] — adres odbiorcy wiadomości (zapisany w formacie RFC822);
$email['date'] — data wysłania wiadomości (zapisana w formacie RFC822);
$email['message_id'] — identyfikator wiadomości (nie należy go mylić z numerem
w sekwencji ani z unikalnym identyfikatorem wiadomości);
$email['references'] — opcjonalny identyfikator wiadomości informujący o tym,
że dana wiadomość odwołuje się do innej wiadomości o podanym identyfikatorze;
$email['in_reply_to'] — opcjonalny identyfikator wiadomości informujący o tym,
że dana wiadomość jest odpowiedzią na wiadomość o podanym identyfikatorze;
$email['size'] — wielkość wiadomości wyrażona w bajtach;
$email['uid'] — unikalny identyfikator wiadomości nadany jej przez serwer IMAP;
$email['msgno'] — identyfikator sekwencji wiadomości w skrzynce pocztowej;
$email['recent'] — flaga informująca o tym, że wiadomość została ostatnio otrzymana;
$email['flagged'] — flaga informująca o tym, że wiadomość została oflagowana jako
spam, śmieci itd.
$email['answered'] — flaga informująca o tym, że została już wysłana odpowiedź na tę
wiadomość;
$email['deleted'] — flaga informująca o tym, że wiadomość została zaznaczona
jako przeznaczona do usunięcia;
$email['seen'] — flaga informująca o tym, że wiadomość została już wcześniej
wyświetlona;
$email['draft'] — flaga informująca o tym, że wiadomość jest oznaczona jako szkic.
Obiekt zwracany przez tę funkcję jest instancją ogólnej klasy PHP — stdClass — i zawiera na-
stępujące właściwości:
$info->Date — bieżący systemowy czas skrzynki pocztowej zapisany zgodnie
z wytycznymi dokumentu RFC2822;
$info->Driver — protokół używany do uzyskania dostępu do danej skrzynki pocztowej
(dostępne wartości tej właściwości to pop3, imap oraz nntp);
$info->Mailbox — nazwa bieżącej skrzynki pocztowej;
$info->Nmsgs — liczba wiadomości dostępnych w bieżącej skrzynce pocztowej;
$info->Recent — liczba ostatnio otrzymanych wiadomości dostępnych w bieżącej
skrzynce pocztowej.
Do zaimplementowania podziału na strony potrzebna jest nam właściwość Nmsgs, która określa
całkowitą liczbę wiadomości w skrzynce pocztowej; na tej właściwości wyliczymy całkowitą
liczbę stron niezbędnych do pobrania i wyświetlenia całej zawartości skrzynki (uwzględniając
przy tym predefiniowaną, maksymalną liczbę wiadomości prezentowanych na stronie). Łącząc
ze sobą funkcje imap_check() oraz imap_fetch_overview(), można napisać kod własnej funkcji
zwracającej listę wiadomości z danej skrzynki pocztowej, które mają być wyświetlone na stronie
o konkretnym numerze. Poniżej przedstawiony został kod tej funkcji, której nadaliśmy nazwę
imap_overwiev_by_page:
<?php
function imap_overview_by_page($connection, int $page = 1, int $perPage = 25,
int $options = 0)
{
$boxInfo = imap_check($connection);
if($start < 1) {
$start = 1;
}
return $overview;
}
?>
Kod powyższej funkcji imap_overview_by_page jest bardzo prosty i nie wymaga dokładniejszego
opisu. Pierwszym parametrem jej wywołania jest zasób IMAP. Drugi parametr, $page, określa
numer strony listy wiadomości, trzeci — $perPage — liczbę wiadomości, które należy wyświetlić
na stronie, i w końcu ostatni — $options — odpowiada analogicznemu ostatniemu parametrowi
wewnętrznej funkcji imap_fetch_overview(). Wewnątrz funkcji wywoływana jest opisana
wcześniej funkcja imap_check(), za pomocą której pobieramy całkowitą liczbę wiadomości w bieżą-
cej skrzynce pocztowej, a następnie używamy tej funkcji wraz z parametrami $page i $perPage
do wyliczenia wartości określających początek i koniec sekwencji. Ze względu na charakter sekwen-
cji wiadomości po jej pobraniu musimy wywołać funkcję array_reverse(), by ułożyć zawartość
strony w logicznej kolejności. Dopiero tak przetworzoną sekwencję wiadomości zwracamy jako
wynik wywołania funkcji.
598 Część V Tworzenie praktycznych projektów PHP i MySQL
Po zapoznaniu się z opisem tych wszystkich funkcji Czytelnik powinien już wiedzieć, w jaki
sposób zamierzamy połączyć te wszystkie narzędzia tak, by stworzyły prostego klienta poczty
elektronicznej. Skoro pokazaliśmy już, jak można pobrać ogólne informacje o wiadomościach
umieszczonych w skrzynce pocztowej, musimy zająć się kolejnym zagadnieniem: pobieraniem
pełnej zawartości konkretnej wiadomości wraz z ewentualnymi załącznikami.
Parametr $connection określa zasób skrzynki pocztowej IMAP, a parametr $msgId — identyfika-
tor wiadomości, której treść chcemy pobrać. Funkcja ta pozwala na określenie pewnych użytecznych
opcji, których można używać podczas pobierania wiadomości; zostały one opisane poniżej:
FT_UID — wskazuje, że parametr $msgId jest unikalnym identyfikatorem wiadomości
zwróconym przez serwer IMAP, a nie sekwencyjnym numerem wiadomości w skrzynce
pocztowej;
FT_PEEK — wskazuje, że wiadomość ma zostać pobrana, ale serwer IMAP nie powinien
ustawiać dla niej flagi „obejrzana” („seen”). Możliwość ta jest przydatna w przypadkach,
gdy chcemy pobrać wiadomość w celach programistycznych, a nie po to, by wyświetlić
jej treść użytkownikowi.
Pobieranie zawartości wiadomości przy użyciu funkcji imap_body() jest bardzo łatwe. W przypad-
ku wiadomości o najprostszej możliwej postaci można się spodziewać, że zawartość wiadomości
będzie jednocześnie jej treścią. Choć faktycznie może tak być, to jednak niemal zawsze wiado-
mość zawiera strukturę określaną jako format MIME. Jest to sposób zapisu korzystający z kodu
ASCII, który pozwala na tworzenie wiadomości poczty elektronicznej obejmujących kilka róż-
nych wersji (na przykład wersję HTML oraz wersję tekstową) i zawierających załączniki.
Oznacza to, że zawartość nowoczesnych wiadomości poczty elektronicznej jest strukturą, którą
dopiero trzeba przetworzyć, na przykład trzeba rozdzielić treść wiadomości (zapisaną w formie
kodu HTML i zwyczajnego tekstu) oraz poszczególne dołączone do niej załączniki.
Pełna prezentacja i wyjaśnienie formatu MIME znacznie wykraczają poza ramy tematyczne tego
rozdziału, ale dzięki możliwościom, jakie zapewnia rozszerzenie IMAP w języku PHP, dysponu-
jemy pewnymi użytecznymi narzędziami, które pomagają w przetworzeniu struktury wiadomości
na sensowne komponenty. Służy do tego funkcja imap_fetchstructure():
object imap_fetchstructure(resource $connection, int $msgId [, int $options = 0]);
Podobnie jak w przypadku funkcji imap_body(), także i tutaj parametry $connection oraz $msgId
reprezentują, odpowiednio, połączenie ze skrzynką pocztową oraz identyfikator wiadomości
(bądź to numer w sekwencji, bądź też, w przypadku podania opcji FT_UID, unikalny identyfikator).
Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 599
Nawet bez zagłębiania się w szczegóły formatu MIME łatwo zauważyć, że struktura zwracana
przez funkcję imap_fetchstructure() bez trudu może stać się bardzo skomplikowana. Sama funk-
cja zwraca obiekt (klasy stdClass) zawierający drzewiastą strukturę zawartości wskazanej wia-
domości. Użyliśmy określenia „struktura drzewiasta”, gdyż — jak się Czytelnik wkrótce przekona
— zwrócony obiekt jest jedynie głównym, najwyższym węzłem tej struktury, a wewnątrz niego
mogą być potencjalnie zapisane inne, identyczne węzły podrzędne, reprezentujące każdą z części
lub jeszcze mniejsze fragmenty wiadomości. Poniżej przedstawiona została struktura obiektu
pojedynczego węzła:
type — główny typ danego węzła;
encoding — kodowanie zastosowane do przesłania zawartości tego węzła (na przykład
base64);
ifsubtype — wartość logiczna określająca, czy został podany jakiś podtyp;
subtype — podtyp MIME danego węzła;
ifdescription — wartość logiczna określająca, czy został podany opis;
description — łańcuch znaków z opisem węzła;
ifid — wartość logiczna określająca, czy został podany łańcuch identyfikujący;
id — łańcuch identyfikujący;
lines — liczba wierszy zawartości danego węzła;
bytes — liczba bajtów zajmowanych przez dany węzeł;
ifdisposition — wartość logiczna określająca, czy został podany łańcuch dyspozycji;
disposition — łańcuch zawierający dyspozycję dla danego węzła;
ifdparameters — wartość logiczna określająca, czy zostały podane jakieś parametry
dyspozycji;
dparameters — tablica obiektów reprezentujących parametry dyspozycji, z których każdy
składa się z właściwości attribute oraz value, opisujących dany parametr dyspozycji;
ifparameters — wartość logiczna określająca, czy zostały podane jakieś parametry
węzła;
parameters — tablica obiektów podobna do tej opisanej w punkcie dparameters, z tym
że zawierająca parametry węzła;
parts — tablica węzłów podrzędnych reprezentujących inne fragmenty wiadomości.
Na podstawie zawartości węzłów można się domyślić, że struktura wiadomości poczty elektro-
nicznej w formacie MIME może być bardzo złożona, a logika, którą trzeba zaimplementować
w celu jej przetworzenia, jeszcze bardziej skomplikowana. Dlatego zamiast prezentować tę logikę
już teraz, co trudno byłoby zrobić sensownie w oderwaniu od konkretnego rozwiązania, zajmiemy
się planowanym klientem poczty elektronicznej, a przetwarzanie zawartości wiadomości opisze-
my przy okazji prezentowania jego implementacji.
IMAP dla PHP, i udostępniającej je pod postacią, która lepiej będzie się nadawać do wykorzystania
w aplikacji Laravel. Zakładamy przy tym, że Czytelnik rozpocznie prace nad aplikacją od zu-
pełnie nowego projektu aplikacji Laravel.
Pierwszą rzeczą, jaką zrobimy, będzie utworzenie w aplikacji katalogu (a zatem także i przestrzeni
nazw), w którym następnie umieścimy bibliotekę IMAP. Można jej nadać zupełnie dowolną na-
zwę, jednak my stworzymy katalog app/Library/Imap, który w aplikacji Laravel będzie automatycz-
nie odpowiadał przestrzeni nazw App\Library\Imap.
Pierwszy element biblioteki, który musimy napisać, będzie odpowiadał za nawiązanie połączenia
z serwerem IMAP. Zdecydowaliśmy się na napisanie tej możliwości funkcjonalnej w taki sposób,
by ogólna logika nawiązywania połączenia z serwerem IMAP została oddzielona od szczegółów im-
plementacyjnych obsługi konkretnej implementacji serwera IMAP. Ponieważ w tym projekcie
zamierzamy napisać klienta IMAP korzystającego z serwera IMAP usługi Gmail firmy Google,
zaczniemy od utworzenia dwóch klas: App\Library\Imap\AbstractConnection, klasy abstrak-
cyjnej zawierającej ogólne szczegóły implementacyjne związane z połączeniem IMAP, oraz
App\Library\Imap\GmailConnection, klasy zawierającej implementację połączenia z serwerem
IMAP usługi Gmail.
$this->getServerRef(),
$this->getUsername(),
$this->getPassword(),
$options,
$n_retries,
$params
);
if(!is_resource($connection)) {
throw new ImapException("Nie udało się nawiązać połączenia z serwerem.");
}
if(!empty($this->port)) {
$serverRef .= ':' . $this->port;
}
if(!empty($this->path)) {
$serverRef .= $this->path;
}
<?php
namespace App\Library\Imap;
?>
<?php
$connection = new GmailConnection();
try {
$client = $connection->setUsername($username)
->setPassword($password)
->connect();
} catch(\App\Library\Imap\ImapException $e) {
echo "Nie udało się nawiązać połączenia: {$e->getMessage()}";
}
?>
Jak widać, tworzymy w tym konstruktorze instancję nieznanej jeszcze klasy Message(), do której
także przekazujemy połączenie. Będzie to kolejna klasa wchodząca w skład naszej biblioteki,
służąca jako pojemnik dla poszczególnych wiadomości pobieranych z serwera IMAP. Zastosujemy
w niej wzorzec projektowy prototypu (w którym instancja nadrzędna będzie po prostu klonowana
za każdym razem, gdy będzie konieczne utworzenie nowego obiektu wiadomości), ułatwiając
w ten sposób programistom zaimplementowanie klasy Message. Klasę tę opiszemy szczegółowo
dopiero w dalszej części rozdziału, na razie wystarczy jedynie informacja o tym, że ten obiekt pro-
totypu klasy Message można zmienić przy użyciu dwóch poniższych metod:
Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 603
Podobnie jak samo rozszerzenie IMAP dla języka PHP, także klasa App\Library\Imap\Client
będzie operować na jednej skrzynce pocztowej w danej chwili. W jej konstruktorze określili-
śmy, że początkową skrzynką pocztową ma być domyślna skrzynka wskazana przez klasę odpowia-
dającą za utworzenie klienta. Oczywiście musimy także zaimplementować metody pozwalające
na zmianę aktywnej skrzynki pocztowej oraz zwrócenie informacji o obecnie używanej skrzynce.
Operacje te będą wykonywane przez dwie metody: getCurrentMailbox() oraz setCurrentMailbox(),
których kod został przedstawiony poniżej:
public function getCurrentMailbox() : string
{
return $this->_currentMailbox;
}
if(!is_array($result)) {
return [];
}
604 Część V Tworzenie praktycznych projektów PHP i MySQL
$retval = [];
foreach($result as $mailbox) {
$retval[] = str_replace($serverRef, '', $mailbox);
}
return $retval;
}
Należy zwrócić uwagę na to, że zgodnie z informacjami podanymi wcześniej metoda imap_list()
wymaga przekazania łańcucha odwołania do serwera, podobnego do tego używanego w funkcji
imap_open(), który zdefiniuje zakres skrzynek pobieranych skrzynek pocztowych. Nas interesują
skrzynki pocztowe umieszczone na „głównym poziomie” połączenia i właśnie je pobiera przedsta-
wiona metoda. Ponieważ funkcja imap_list() zwraca listę skrzynek pocztowych w formie łańcu-
chów zawierających pełne odwołania (włącznie z nazwą serwera), te niepotrzebne dane są usuwane
z tablicy wynikowej. Poniżej zaprezentowana została metoda getServerRef(), która konstruuje
łańcuch połączenia z serwerem IMAP:
protected function getServerRef()
{
$serverRef = '{' . $this->_spec['hostname'];
if(!empty($this->_spec['port'])) {
$serverRef .= ':' . $this->_spec['port'];
}
if(!empty($this->_spec['path'])) {
$serverRef .= $this->_spec['path'];
}
$serverRef .= '}';
return $serverRef;
}
Dzięki przedstawionym metodom nasza klasa klienta IMAP zapewnia już możliwość zwracania
listy skrzynek pocztowych oraz szybkiego i prostego przełączania się pomiędzy nimi. Kolejnym
zadaniem będzie pobranie listy wiadomości dostępnych w konkretnej skrzynce pocztowej. We
wcześniejszej części rozdziału taką listę wiadomości pobieraliśmy przy użyciu funkcji PHP
imap_fetch_overview(). Jak Czytelnik zapewne pamięta, przy okazji jej prezentowania wspomi-
naliśmy także o konieczności zaimplementowania jakiegoś mechanizmu podziału na strony,
dzięki któremu będzie się ona nadawać do praktycznego zastosowania, a nawet zaimplemento-
waliśmy demonstracyjną wersję funkcji przeprowadzającej taki podział. Większą część tamtego
kodu zastosujemy ponownie w przedstawionej poniżej metodzie getPage(), która należy do naszej
klasy Client i odpowiada za pobieranie listy wiadomości.
public function getPage(int $page = 1, int $perPage = 25, $options = 0) :
\Illuminate\Support\Collection
{
$boxInfo = imap_check($this->_connection);
if($start < 1) {
$start = 1;
}
Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 605
$overview = imap_fetch_overview($this->_connection,
"$start:$end",
$options);
$overview = array_reverse($overview);
$msgObj->setSubject($msg->subject)
->setFrom($msg->from)
->setTo($msg->to)
->setDate($msg->date)
->setMessageId($msg->message_id)
->setSize($msg->size)
->setUID($msg->uid)
->setMessageNo($msg->msgno);
if(isset($msg->references)) {
$msgObj->setReferences($msg->references);
}
if(isset($msg->in_reply_to)) {
$msgObj->setInReplyTo($msg->in_reply_to);
}
$collection->put($key, $msgObj);
}
return $collection;
}
Aby zapewnić możliwość zwrócenia z danej skrzynki pocztowej pojedynczej wiadomości, a nie
całej ich listy, zaimplementujemy także metodę getMessage(). Tak samo jak metoda getPage(),
zwracająca kolekcję instancji Message, poniższa metoda getMessage() będzie zwracać jeden obiekt
tej klasy:
public function getMessage($id, int $options = 0) :
\App\Library\Imap\Message\MessageInterface
{
$overview = imap_fetch_overview($this->_connection, $id, $options);
if(empty($overview)) {
return $this->getPrototype();
}
$overview = array_pop($overview);
$retval = $this->getPrototype();
606 Część V Tworzenie praktycznych projektów PHP i MySQL
$retval->setSubject($overview->subject)
->setFrom($overview->from)
->setTo($overview->to)
->setDate($overview->date)
->setMessageId($overview->message_id)
->setSize($overview->size)
->setUID($overview->uid)
->setMessageNo($overview->msgno);
return $retval;
}
Na podstawie metod getPage() oraz getMessage() łatwo wywnioskować, że nasza klasa Message
służy głównie jako obiektowe opakowanie zapewniające możliwości zwracania różnych wartości
pobieranych przez funkcję PHP imap_fetch_overview() (informacje na jej temat można znaleźć
we wcześniejszej części rozdziału). W następnym podpunkcie rozdziału opiszemy tę klasę nieco
dokładniej, przedstawimy też kluczowe informacje na temat jej architektury.
interface MessageInterface
{
public function __construct($connection);
public function setSubject(string $subject);
public function getSubject() : string;
public function setFrom(string $from);
public function getFrom() : string;
public function setTo(string $to);
public function getTo() : string;
public function setDate(string $date);
public function getDate() : \DateTime;
public function setMessageId(string $id);
public function getMessageId() : string;
public function setReferences(string $refs);
public function getReferences() : string;
public function setInReplyTo(string $to);
public function getInReplyTo() : string;
public function setSize(int $size);
public function getSize() : int;
public function setUID(string $uid);
public function getUID() : string;
public function setMessageNo(int $no);
public function getMessageNo() : int;
}
?>
Domyślną implementacją tego interfejsu będzie przedstawiona już wcześniej klasa App\Library\
Imap\Message\Message. Niemniej jednak, wprowadzając niewielką zmianę w kodzie metody
App\Library\Imap\Client::setPrototype(), klasę tę można zamienić na dowolną inną klasę wybraną
Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 607
przez twórcę aplikacji. Na przykład można by stworzyć model Eloquent implementujący powyższy
interfejs i stosować go do pobierania wiadomości poczty elektronicznej, które jednocześnie będzie
można w prosty sposób zapisać w bazie danych.
if(!is_resource($connection)) {
throw new \InvalidArgumentException("Do konstruktora trzeba przekazać zasób IMAP.");
}
$this->_connection = $connection;
}
Jak można się było przekonać, analizując przedstawiony wcześniej kod klienta IMAP, zamierzo-
nym celem prostego użycia tego obiektu jest wykorzystanie go jako opakowania zapewniające-
go dostęp do różnych właściwości wiadomości poczty elektronicznej pobieranych za pomocą
funkcji imap_fetch_overview(). Niemniej jednak klasa ta została zaprojektowana zgodnie z założe-
niami programowania obiektowego, tak by te podstawowe informacje pobierane przy jej użyciu
można było także rozszerzyć o samą zawartość wiadomości. Konkretnie rzecz biorąc, nasza klasa
będzie implementować metodę fetch(), która — po przekazaniu podstawowych fragmentów
wiadomości przez klienta — skorzysta z możliwości klienta IMAP, by pobrać pełną treść reprezen-
towanej wiadomości. Metoda ta jest zdefiniowana w następujący sposób:
public function fetch(int $options = 0) : self
{
$structure = imap_fetchstructure($this->_connection,
$this->getMessageNo(), $options);
if(!$structure) {
return $this;
}
switch($structure->type) {
case TYPEMULTIPART:
case TYPETEXT:
$this->processStruct($structure);
break;
case TYPEMESSAGE:
break;
case TYPEAPPLICATION:
case TYPEAUDIO:
case TYPEIMAGE:
case TYPEVIDEO:
case TYPEMODEL:
608 Część V Tworzenie praktycznych projektów PHP i MySQL
case TYPEOTHER:
break;
}
return $this;
}
Jak widać, metoda fetch() używa opisanej wcześniej funkcji imap_etchstructure(), by pobrać
ogólną strukturę zawartości przetwarzanej wiadomości e-mail. Korzystając z właściwości type
węzła głównego elementu zawartości wiadomości, możemy następnie w odpowiedni sposób
przetwarzać tę zawartość. W przypadku naszej prostej aplikacji będą nas interesować jedynie
dwa typy tej „głównej zawartości” wiadomości: zwyczajny tekst oraz wieloczęściowe wiadomości
w formacie MIME. Oba te typy reprezentują przeważającą większość wszystkich wysyłanych
obecnie wiadomości poczty elektronicznej i na potrzeby naszych rozważań są one zupełnie wystar-
czające. Typy te będą obsługiwane przez metodę processStruct(), która została przedstawiona
i opisana poniżej.
if(!is_null($partId)) {
$curPartId = $partId . '.' . $curPartId;
}
$self->processStruct($part, $curPartId);
}
}
return $self;
};
if(isset($structure->parameters)) {
foreach($structure->parameters as $param) {
$params[strtolower($param->attribute)] = $param->value;
}
}
if(isset($structure->dparameters)) {
foreach($structure->dparameters as $param) {
$params[strtolower($param->attribute)] = $param->value;
}
}
Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 609
if(isset($params['name']) || isset($params['filename']) ||
(isset($structure->subtype) &&
strtolower($structure->subtype) == 'rfc822'))
{
// przetworzenie załącznika
$filename = isset($params['name']) ? $params['name'] :
$params['filename'];
$attachment = new Attachment($this);
$attachment->setFilename($filename)
->setEncoding($structure->encoding)
->setPartId($partId)
->setSize($structure->bytes);
switch($structure->type) {
case TYPETEXT:
$mimeType = 'text';
break;
case TYPEMESSAGE:
$mimeType = 'message';
break;
case TYPEAPPLICATION:
$mimeType = 'application';
break;
case TYPEAUDIO:
$mimeType = 'audio';
break;
case TYPEIMAGE:
$mimeType = 'image';
break;
case TYPEVIDEO:
$mimeType = 'video';
break;
default:
case TYPEOTHER:
$mimeType = 'other';
break;
}
$mimeType .= '/' . strtolower($structure->subtype);
$attachment->setMimeType($mimeType);
$this->_attachments[$partId] = $attachment;
return $recurse($structure);
}
if(!is_null($partId)) {
$body = imap_fetchbody($this->_connection,
$this->getMessageNo(), $partId, FT_PEEK);
} else {
$body = imap_body($this->_connection, $this->getUID(),
FT_UID | FT_PEEK);
}
$encoding = strtolower($structure->encoding);
switch($structure->encoding) {
case 'quoted-printable':
case ENCQUOTEDPRINTABLE:
$body = quoted_printable_decode($body);
break;
case 'base64':
case ENCBASE64:
$body = base64_decode($body);
610 Część V Tworzenie praktycznych projektów PHP i MySQL
break;
}
$subtype = strtolower($structure->subtype);
switch(true) {
case $subtype == 'plain':
if(!empty($this->_plainBody)) {
$this->_plainBody .= PHP_EOL . PHP_EOL . trim($body);
} else {
$this->_plainBody = trim($body);
}
break;
case $subtype == 'html':
if(!empty($this->_htmlBody)) {
$this->_htmlBody .= '<br><br>' . $body;
} else {
$this->_htmlBody = $body;
}
break;
}
return $recurse($structure);
}
Naturalną pierwszą reakcją na tak długi i skomplikowany kod jest onieśmielenie, ale spokojnie
— w dalszej części tego podpunktu rozdziału podzielimy ją na fragmenty i wyjaśnimy ich
działanie krok po kroku. Metoda zaczyna się od inicjalizacji pary zmiennych, które będą w niej
używane, oraz zdefiniowania zmiennej $recurse, zawierającej funkcję anonimową. Zadaniem
tej funkcji anonimowej jest określenie, czy obecnie analizowana „część” wiadomości MIME
sama składa się z innych części. Jeśli okaże się, że faktycznie składa się ona z kolejnych, pod-
rzędnych części, to przetwarzamy ją, wywołując w tym celu metodę processStruct() i wykonując
te same czynności dla poszczególnych części. A zatem zaczynając od struktury głównego poziomu,
schodzimy do struktur podrzędnych i rekurencyjnie pobieramy przy tym odpowiednie dane:
$recurse = function($struct) use ($partId, $self) {
if(isset($struct->parts) && is_array($struct->parts)) {
if(!is_null($partId)) {
$curPartId = $partId . '.' . $curPartId;
}
$self->processStruct($part, $curPartId);
}
}
return $self;
};
Rysunek 29.1.
Struktura
wiadomości e-mail
Jak widać, tę hipotetyczną wiadomość można podzielić na trzy główne części: część tekstową,
część dźwiękową oraz plik załącznika. Jednak każda z tych części może się składać z dalszych
części podrzędnych. W powyższym przykładzie część tekstowa wiadomości składa się z części
określanej jako zwyczajny tekst (text/plain) oraz z treści zapisanej w formacie HTML (text/html).
Podobnie część dźwiękowa wiadomości składa się z dwóch części podrzędnych: pliku wav
(audio/wav) i pliku MP3 (audio/mp3). W naszym przykładzie plik załącznika nie zawiera żadnych
części podrzędnych.
Gdybyśmy mieli przypisać każdej z części wiadomości jakiś identyfikator, to prostym rozwią-
zaniem byłoby ponumerowanie ich na podstawie względnej głębokości, w sposób przedstawiony
na rysunku 29.2.
Rysunek 29.2.
Numerowanie
każdej z części
wiadomości MIME
Takie łańcuchy znaków są nazywane identyfikatorami części, a rozszerzenie PHP IMAP używa
ich do identyfikacji fragmentu wieloczęściowej wiadomości e-mail, który jest przetwarzany
w ramach danej operacji. Podczas działania naszej metody processStruct(), która rekurencyjnie,
węzeł po węźle, analizuje zawartość wiadomości, tworzony jest także odpowiedni identyfikator
części; reprezentuje on obecnie przetwarzany fragment wiadomości i będzie używany w dalszej
części kodu.
Następną operacją wykonywaną przez metodę processStruct() jest łączenie parametrów części
oraz parametrów dyspozycji (jeśli takie istnieją) w jedną tablicę — $params. Na mocy konwen-
cji parametry części i parametry dyspozycji nigdy nie używają tych samych identyfikatorów,
nawet pomimo tego, że z technicznego punktu widzenia jest to możliwe. Ponieważ w praktyce
nie ma potrzeby rozróżniania tych parametrów, połączenie ich i sformatowanie do postaci znor-
malizowanej listy (w której wszystkie identyfikatory są zapisane małymi literami) pozwoli później
na ich szybkie odnajdywanie:
if(isset($structure->parameters)) {
foreach($structure->parameters as $param) {
$params[strtolower($param->attribute)] = $param->value;
}
}
612 Część V Tworzenie praktycznych projektów PHP i MySQL
if(isset($structure->dparameters)) {
foreach($structure->dparameters as $param) {
$params[strtolower($param->attribute)] = $param->value;
}
}
Kolejną czynnością jest przeanalizowanie obecnie przetwarzanej części i próba określenia, czy
stanowi ona plik załącznika, czy nie. W tym celu metoda sprawdza różne dostępne zmienne, ta-
kie jak wspomniane wcześniej parametry i podtyp typu danej części. W naszym przykładzie spraw-
dzamy, czy dla danej części zostały określone parametry 'name' oraz 'filename', gdyż ich obecność
w przeważającej większości przypadków oznacza, że dana część jest załącznikiem. Ponadto spraw-
dzamy, czy został określony podtyp danej części, a jeśli tak, to czy jest on łańcuchem 'rfc822', co
również oznacza, że dana część jest załącznikiem.
if(isset($params['name']) || isset($params['filename']) ||
(isset($structure->subtype) &&
strtolower($structure->subtype) == 'rfc822'))
{
// przetorzenie załącznika
$filename = isset($params['name']) ? $params['name'] :
$params['filename'];
$attachment = new Attachment($this);
$attachment->setFilename($filename)
->setEncoding($structure->encoding)
->setPartId($partId)
->setSize($structure->bytes);
switch($structure->type) {
case TYPETEXT:
$mimeType = 'text';
break;
case TYPEMESSAGE:
$mimeType = 'message';
break;
case TYPEAPPLICATION:
$mimeType = 'application';
break;
case TYPEAUDIO:
$mimeType = 'audio';
break;
case TYPEIMAGE:
$mimeType = 'image';
break;
case TYPEVIDEO:
$mimeType = 'video';
break;
default:
case TYPEOTHER:
$mimeType = 'other';
break;
}
$mimeType .= '/' . strtolower($structure->subtype);
$attachment->setMimeType($mimeType);
$this->_attachments[$partId] = $attachment;
return $recurse($structure);
}
Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 613
Jeśli na podstawie parametrów i podtypu części określimy, że dana część jest załącznikiem, to
tworzona jest instancja nieprzedstawionej jeszcze klasy Attachment — używanej do reprezentacji
załączników — którą następnie dodajemy do tablicy Message::$_attachments, stosując przy tym
identyfikator części jako klucz. Jeżeli chodzi o obiekt załącznika, to upewniamy się, że będzie
on podawał odpowiedni typ kodowania (czyli sposób, w jaki załączniki są zapisywane w wiadomo-
ści e-mail, na przykład base64); poza tym na podstawie typu części możemy określić charakter
samego załącznika. Do załączników wrócimy jeszcze nieco później, teraz interesuje nas jedynie
pobranie ich i zachowanie do późniejszego przetworzenia. Kiedy to zrobimy, przetwarzanie danej
części będzie chwilowo zakończone, dlatego możemy ponownie wywołać naszą anonimową
funkcję $recurse, aby określić, czy istnieją jakieś części podrzędne, i ewentualnie przetworzyć
je, jeśli się okaże, że faktycznie są dostępne.
Jeżeli w naszej ocenie obecnie analizowana część nie jest załącznikiem, to będzie ona musiała
stanowić fragment samej treści wiadomości. Oznacza to, że musimy pobrać zawartość tej części
i w jakiś sposób określić, co należy z nią zrobić. Dla uproszczenia prezentowanego rozwiązania
zakładamy, że jeśli część nie jest załącznikiem, to jest częścią samej wiadomości e-mail. Co
więcej, przyjmujemy, że sama wiadomość jest zapisana bądź to w formacie HTML, bądź też jako
zwyczajny tekst (ewentualnie w obu tych formatach).
Pierwszą czynnością związaną z przetwarzaniem treści wiadomości jest pobranie jej z obecnie
analizowanego segmentu wiadomości. Jeśli podczas wywoływania metody processStruct()
został do niej przekazany identyfikator części, to używamy go do pobrania zawartości tej części
przy pomocy funkcji imap_fetchbody(). Jeżeli natomiast identyfikator części nie został podany,
oznacza to, że przetwarzany e-mail nie jest wieloczęściową wiadomością MIME, a jego zawartość
jest jednocześnie treścią tej wiadomości; w takim przypadku można ją pobrać, wywołując funk-
cję imap_body(). W obu sytuacjach trzeba pamiętać, by zastosować stałą FT_PEEK, aby wiadomość
nie została oznaczona jako „przeczytana”:
if(!is_null($partId)) {
$body = imap_fetchbody($this->_connection,
$this->getMessageNo(), $partId, FT_PEEK);
} else {
$body = imap_body($this->_connection, $this->getUID(),
FT_UID | FT_PEEK);
}
Kolejną czynnością, jaką należy wykonać, jest zdekodowanie pobranej zawartości przy wykorzysta-
niu sposobu kodowania zastosowanego do jej przesłania i przywrócenie pierwotnej postaci tej
zawartości. W tym celu sprawdzamy właściwość encoding części i na podstawie odczytanego
sposobu kodowania dekodujemy zawartość części, używając odpowiedniej funkcji PHP. Dla
uproszczenia kodu nasza aplikacja obsługuje jedynie trzy sposoby kodowania: zwyczajny tekst
(nie wymaga dekodowania), quoted-printable oraz base64:
switch($structure->encoding) {
case 'quoted-printable':
case ENCQUOTEDPRINTABLE:
$body = quoted_printable_decode($body);
break;
case 'base64':
case ENCBASE64:
$body = base64_decode($body);
break;
}
Na tym etapie realizacji metody processStruct() udało nam się już zdobyć kilka informacji o prze-
twarzanej wiadomości. Przede wszystkim udało nam się ustalić, że analizowany segment nie jest
614 Część V Tworzenie praktycznych projektów PHP i MySQL
załącznikiem, co dla naszego procesora oznacza, że segment ten musi być fragmentem treści
wiadomości. Poza tym pobraliśmy już tę wiadomość i zdekodowaliśmy ją do pierwotnej posta-
ci. W ramach kolejnej czynności musimy spróbować określić charakter tej treści, a konkretnie to,
czy została ona zapisana jako zwyczajny tekst, czy w formacie HTML. W tym celu trzeba sprawdzić
właściwość subtype struktury z informacjami o danej części wiadomości i w zależności od jej warto-
ści wykonać odpowiednie operacje:
$subtype = strtolower($structure->subtype);
switch(true) {
case $subtype == 'plain':
if(!empty($this->_plainBody)) {
$this->_plainBody .= PHP_EOL . PHP_EOL . trim($body);
} else {
$this->_plainBody = trim($body);
}
break;
case $subtype == 'html':
if(!empty($this->_htmlBody)) {
$this->_htmlBody .= '<br><br>' . $body;
} else {
$this->_htmlBody = $body;
}
break;
}
Nasz prosty klient pocztowy obsługuje jedynie wiadomości zapisane jako zwyczajny tekst lub
formatowane jako HTML; wszystkie inne podtypy treści są przez niego ignorowane. W przypad-
ku wiadomości, których treść jest zapisana jako zwyczajny tekst, zapisujemy ją we właściwości
Message::$_plainBody, a treści HTML zapisujemy we właściwości Message::$_htmlBody. Trzeba
pamiętać, że ta sama wiadomość e-mail może obejmować większą liczbę segmentów, z których
jeden będzie zawierał treść w postaci zwyczajnego tekstu, a inny — treść w formacie HTML; z tego
względu, aby mieć pewność, że cała treść wiadomości zostanie prawidłowo wyświetlona, metoda
processStruct() zawiera logikę, która przypisuje wszystkie odszukane treści do odpowiednich
właściwości.
Kiedy wszystkie niezbędne czynności zostaną wykonane, ponownie wywoływana jest meto-
da $recurse, dzięki czemu możemy mieć pewność, że zostaną przetworzone wszystkie istnieją-
ce podtypy. Pod koniec przetwarzania wiadomości wszystkie rekurencyjne wywołania metody
processStruct() zostaną zakończone, a efektem ich wykonania będzie zapisanie we właściwo-
ściach Message::$_plainBody, Message::$_htmlBody oraz Message::$_attachments odpowiednio
przetworzonej zawartości wiadomości e-mail, której teraz będziemy mogli użyć w innych fragmen-
tach aplikacji.
Klasa Attachment
Efektem wywołania metody Message::fetch(), a w konsekwencji także metody processStruct(),
jest utworzenie jednego lub kilku obiektów Attachment. Każdy z tych obiektów reprezentuje
i zawiera jeden z załączników przesłanych w przetwarzanej wiadomości. Aby zakończyć tworzenie
biblioteki IMAP używanej przez aplikację tworzoną w tym rozdziale, pokrótce omówimy również
tę klasę. Podobnie jak w przypadku klasy Message, także teraz pominiemy wszystkie standardowe
metody typu „get” i „set” i skoncentrujemy się jedynie na tych, które mają większą wartość
edukacyjną.
Tak jak same wiadomości poczty elektronicznej oraz zawartość klasy Message, również załączniki
nie są pobierane z serwera aż do momentu, kiedy jawnie tego zażądamy. A zatem choć wywołanie
metody Message::fetch() spowoduje pobranie przetwarzanej wiadomości e-mail oraz zdefiniowanie
Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 615
wszystkich przesłanych w niej załączników, to jednak same załączniki nie zostaną pobrane aż do
momentu wywołania przedstawionej poniżej metody fetch():
public function fetch() : self
{
$body = imap_fetchbody(
$this->_message->getConnection(),
$this->_message->getMessageNo(),
$this->_partId,
FT_PEEK);
switch($this->getEncoding()) {
case 'quoted-printable':
case ENCQUOTEDPRINTABLE:
$body = quoted_printable_decode($body);
break;
case 'base64':
case ENCBASE64:
$body = base64_decode($body);
break;
}
$this->setData($body);
return $this;
}
Do załączników i klasy Attachment wrócimy jeszcze raz nieco później, kiedy już zaimplementu-
jemy interfejs użytkownika naszego internetowego klienta poczty elektronicznej.
Punktem wyjścia naszej pracy będzie standardowy projekt aplikacji Laravel 5, opisany w poprzed-
nim rozdziale. Pierwszą rzeczą, jaką należy zrobić po utworzeniu projektu Laravel, jest dołączenie
do niego kodu, który napisaliśmy wcześniej w tym rozdziale, czyli obiektowej biblioteki korzystającej
616 Część V Tworzenie praktycznych projektów PHP i MySQL
Dzięki możliwościom zapewnianym przez sam framework Laravel, a także dzięki pracy, którą
włożyliśmy w przygotowanie biblioteki IMAP, zaimplementowanie klienta poczty elektronicznej
sprowadzi się jedynie do podłączenia biblioteki oraz utworzenia w projekcie aplikacji odpowied-
nich kontrolerów i widoków. Zacznijmy od powiązania biblioteki IMAP z aplikacją Laravel poprzez
napisanie odpowiedniego dostawcy usług.
W tym celu z poziomu klasy naszego dostawcy usług zarejestrujemy nowy pojemnik działający jako
singleton, który będzie odpowiedzialny za skonfigurowanie połączenia i zwracanie jego instancji,
odpowiednio przygotowanej do użycia:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Library\Imap\GmailConnection;
Analiza metody register() klasy naszego dostawcy usług pokazuje, że zawiera on całkiem prostą
logikę służącą do zarejestrowania domknięcia, które będzie wykonywane za każdym razem, gdy
aplikacja zażąda instancji klasy o nazwie 'Imap\Connection\Gmail'. Kod samego domknięcia także
jest prosty — jego działanie sprowadza się do zwrócenia nowej instancji klasy GmailConnection()
i wstrzyknięcia do niej niezbędnych wartości konfiguracyjnych. Wartości te pochodzą z danych
konfiguracyjnych aplikacji Laravel, przy czym odwołania takie jak imap.gmail.retries wskazują
na plik config/imap.php, który powinien zwracać tablicę zawierającą klucz 'retries'. A zatem jeśli
ten plik będzie istnieć i zwróci tablicę zawierającą klucz 'retries', to wartość skojarzona z tym
kluczem zostanie wstrzyknięta jako drugi parametr konstruktora. W przeciwnym razie — jeśli
plik nie zostanie znaleziony lub zwrócona tablica nie będzie zawierać poszukiwanego klucza —
funkcja config() frameworka Laravel zwróci null.
Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 617
Zanim powyższy dostawca usług będzie mógł być używany w aplikacji, musimy poinformować
framework o jego istnieniu. W tym celu w pliku config/app.php należy zmodyfikować tablicę
stanowiącą zawartość klucza 'providers', a konkretnie — dodać do niej następującą wartość:
App\Providers\ImapServiceProvider::class
@yield('main')
</div>
</body>
618 Część V Tworzenie praktycznych projektów PHP i MySQL
@section('javascript')
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS"
crossorigin="anonymous"></script>
@show
</html>
Aby znacznie uprościć tworzenie niezbędnego układu oraz określanie stylów strony, wykorzysta-
my w niej framework CSS Bootstrap. Analiza kodu powyższego szablonu Blade pokazuje, że
zdefiniowaliśmy w nim kilka sekcji, które później będzie można rozszerzać w szablonach pochod-
nych. Konkretnie rzecz biorąc, zdefiniowaliśmy sekcję 'stylesheets' (umieszczoną wewnątrz
elementu <head> naszej strony), służącą do określania arkuszy stylów, których będziemy chcieli
używać w tym arkuszy frameworka Bootstrap, oraz sekcję 'javascript', umieszczoną na samym
końcu dokumentu i przeznaczoną do dołączenia niezbędnego kodu JavaScript wykorzystywanego
przez framework Bootstrap. Kody JavaScript umieściliśmy na końcu, a arkusze stylów na początku,
gdyż przeglądarki zazwyczaj wczytują zasoby w takiej kolejności, w jakiej zostają one podane
w kodzie dokumentu HTML. Dlatego też wczytywanie skryptów JavaScript po pobraniu całej
pozostałej zawartości strony jest uznawane za najlepsze rozwiązanie.
Można także zauważyć, że w układzie została umieszczona logika warunkowa, sprawdzającą
wartość zmiennej $errors. W szablonach Blade zawsze jest dostępny obiekt $errors, odgrywający
rolę standardowej struktury przeznaczonej do przechowywania komunikatów o błędach, które
należy wyświetlić w widoku wygenerowanym przez kontroler. Tym zagadnieniem zajmiemy
się za chwilę, a teraz wystarczy zwrócić uwagę na to, że dzięki dołączeniu tego fragmentu do
układu uzyskujemy standardowy mechanizm pozwalający na wyświetlanie użytkownikom komuni-
katów o błędach we wszystkich generowanych widokach.
Powyższy układ jest rozszerzany poprzez podanie konkretnych treści określających postać strony
z formularzem do logowania. Ten drugi szablon będzie zapisany w pliku resources/views/auth/
login.blade.php; jego kod przedstawiliśmy poniżej:
@extends('layouts.public')
@section('main')
<div class="col-lg-5 col-lg-offset-2">
<div class="panel panel-default">
<div class="panel-heading">
Proszę się zalogować
</div>
<div class="panel-body">
<form action="/auth/login" method="POST">
{!! csrf_field() !!}
<div class="form-group">
<label for="email">Nazwa użytkownika Gmail</label>
<input type="text" name="email" id="email" placeholder="user@gmail.com">
</div>
<div class="form-group">
<label for="password">Hasło Gmail</label>
<input type="password" name="password" id="password">
</div>
<div class="form-group">
<input type="checkbox" name="remember"> Zapamiętaj mnie
</div>
<button class="btn btn-block btn-primary" type="submit">
<i class="glyphicon glyphicon-lock"></i> Zaloguj
</button>
</form>
</div>
</div>
</div>
@stop
Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 619
Jak można się było spodziewać, powyższy szablon jest w przeważającej mierze zwyczajnym
formularzem HTML, zapewniającym użytkownikowi możliwość podania informacji uwierzytel-
niających. Warto zwrócić uwagę jedynie na dwie rzeczy: na to, iż rozszerza on szablon Blade
layouts.public, przez co zostanie on użyty podczas generowania wynikowego kodu strony,
oraz na to, że wiersz kodu o postaci {!! csrf_field() !!}} użyty został bezpośrednio poniżej
otwierającego znacznika <form>. Ten zapis stanowi wywołanie funkcji szablonów, która jest
udostępniana przez framework Laravel i służy do generowania specjalnego, ukrytego pola formula-
rza przeznaczonego do ochrony przed atakami CSRF (ang. Cross-Site Request Forgery).
Skoro widoki są już gotowe, możemy zająć się zaimplementowaniem logiki, która będzie je ob-
sługiwać. Zaczniemy od zdefiniowania niezbędnej trasy w aplikacji. W tworzonym projekcie ta
trasa oraz odpowiadająca jej trasa do wylogowania użytkownika będą jedynymi, które nie wy-
magają uwierzytelniania. Ponieważ obie te trasy mają być wykonywane w ramach obsługi żądań
HTTP, dodamy do nich także oprogramowanie warstwy pośredniej 'web' (udostępniane przez sam
framework Laravel). A zatem w pliku app/Http/routes.php musimy zdefiniować następujące trasy:
Route::group(['middleware' => ['web']], function() {
Route::get('auth/login', [
'as' => 'login',
'uses' => 'Auth\AuthController@getLogin'
]);
Route::get('auth/logout', 'Auth\AuthController@getLogout');
Route::post('auth/login', 'Auth\AuthController@postLoginGMail');
});
use App\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use App\Library\Imap\ImapException;
$connection->setUsername($request->get('email'))
->setPassword($request->get('password'));
try {
$client = $connection->connect();
620 Część V Tworzenie praktycznych projektów PHP i MySQL
} catch(ImapException $e) {
return $this->sendFailedLoginResponse($request);
}
$credentials = [
'user' => $request->get('email'),
'password' => $request->get('password')
];
\Session::put('credentials', $credentials);
return redirect('inbox');
}
}
?>
Choć w powyższym kontrolerze nie ma zbyt dużej ilości kodu, to jednak dzieje się w nim
znacznie więcej, niż widać na pierwszy rzut oka. Klasa AuthController definiuje jedynie dwie
metody, z których pierwszą jest konstruktor, ale opiera się na zdefiniowanej przez framework
Laravel cesze o nazwie AuthenticatesUsers, uzupełniającej wszelkie brakujące możliwości funkcjo-
nalne. Cecha ta implementuje wszystkie metody niezbędne do wyświetlenia formularza logowania,
obsługi logowania, wylogowania itd.
Musimy się jeszcze zastanowić nad tym, w jaki sposób aplikacja będzie mogła odróżniać uwie-
rzytelnionego użytkownika, który powinien mieć prawo dostępu do chronionych tras (zostaną
one stworzone i opisane w dalszej części rozdziału), od użytkownika nieuwierzytelnionego,
który będzie miał dostęp jedynie do strony logowania. We frameworku Laravel zajmuje się tym
odpowiednie oprogramowanie warstwy pośredniej, którego ideę przedstawiliśmy w poprzed-
nim rozdziale. Na potrzeby naszej aplikacji będziemy musieli zmodyfikować domyślną klasę
App\Http\Middleware\Authenticate dostępną w podstawowym projekcie aplikacji Laravel i zaim-
plementować w niej własną logikę określania tego, czy użytkownik jest uwierzytelniony, czy nie.
Konkretnie rzecz biorąc, dostęp powinien być udzielony bądź nie w zależności od istnienia
w sesji zmiennej 'credentials', zapisanej w metodzie postLoginGMail() kontrolera AuthController.
Poniżej przedstawiony został kod klasy Authenticate:
<?php
namespace App\Http\Middleware;
Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 621
use Closure;
use Illuminate\Support\Facades\Auth;
class Authenticate
{
public function handle($request, Closure $next, $guard = null)
{
if(!\Session::has('credentials')) {
if($request->ajax()) {
return response('Unauthorized.', 401);
}
return redirect()->guest('auth/login');
}
return $next($request);
}
}
?>
Połączenie tych wszystkich elementów stanowi dość prostą implementację uwierzytelniania z wyko-
rzystaniem konta Gmail, której będziemy używać w naszym internetowym kliencie IMAP. Teraz
możemy przejść do implementacji zestawu możliwości funkcjonalnych samego klienta.
Pod względem przebiegu działania każde z powyższych zadań będzie realizowane przez aplikację
w podobny sposób:
622 Część V Tworzenie praktycznych projektów PHP i MySQL
Route::get('read/{id}', [
'as' => 'read',
'uses' => 'InboxController@getMessage'
])->where('id', '[0-9]+');
Route::get('read/{id}/attachment/{partId}', [
'as' => 'read.attachment',
'uses' => 'InboxController@getAttachment'
])->where('partId', '[0-9]+(\.[0-9]+)*');
Route::get('compose/{id?}', [
'as' => 'compose',
'uses' => 'InboxController@getCompose'
])->where('id', '[0-9]+');
Route::get('inbox/delete/{id}', [
'as' => 'delete',
'uses' => 'InboxController@getDelete'
])->where('id', '[0-9]+');
Route::post('compose/send', [
'as' => 'compose.send',
'uses' => 'InboxController@postSend'
]);
});
Jak widać, powyższy fragment kodu definiuje nową grupę, składającą się z sześciu tras, odpowiada-
jących pięciu możliwościom funkcjonalnym, które zamierzamy zaimplementować w naszej apli-
kacji. Tras jest sześć, gdyż ze względu na załączniki do obsługi logiki służącej do wyświetlania
wiadomości potrzebne są dwie trasy.
$client = \App::make('Imap\Connection\GMail')
->setUsername($credentials['user'])
->setPassword($credentials['password'])
->connect();
return $client;
}
Teraz możemy zająć się główną stroną naszej aplikacji oraz nazwaną trasą 'inbox', odpowiedzialną
za wyświetlenie użytkownikowi wszystkich wiadomości e-mail dostępnych w danej skrzynce
pocztowej. Tworząc tę stronę główną, zadbamy o wykorzystanie odpowiednio atrakcyjnego sposobu
prezentacji oraz podziału zawartości skrzynki na strony. Poniżej przedstawiony został kod metody
App\Http\Controllers\InboxController::getInbox():
public function getInbox(Request $request)
{
$client = $this->getImapClient();
$mailboxes = $client->getMailboxes();
if($currentMailbox != $client->getCurrentMailbox()) {
if(in_array($currentMailbox, $mailboxes)) {
$client->setCurrentMailbox($currentMailbox);
}
}
Kod metody getInbox() rozpoczyna się od pobrania bieżącej skrzynki pocztowej, której zawartość
będzie prezentowana. Jeśli skrzynka ta nie została określona, to domyślnie zostanie użyta dowolna
bieżąca skrzynka określona przez klienta IMAP. Jeżeli bieżąca skrzynka pocztowa jest inna niż
ta, której używa klient IMAP, to odpowiednio zmieniamy bieżącą skrzynkę. W następnej kolejności
wywoływana jest metoda klienta getMailboxes(), która — zgodnie z wcześniejszymi wyjaśnie-
niami — zwraca listę wszystkich skrzynek pocztowych dostępnych w używanym połączeniu
624 Część V Tworzenie praktycznych projektów PHP i MySQL
IMAP. Później metoda pobiera z bieżącej skrzynki pocztowej jedną stronę wiadomości e-mail
(opierając się przy tym na numerze strony przekazanym przez użytkownika) i tworzy obiekt odpo-
wiedzialny za podział na strony, który zadba o zapewnienie możliwości poruszania się pomiędzy
stronami w interfejsie użytkownika aplikacji. I w końcu wszystkie te informacje zostają przekazane
do widoku app.inbox w celu ich wygenerowania i przesłania do przeglądarki użytkownika.
@extends('layouts.authed')
@section('stylesheets')
@parent
<link href="/css/app.css" rel="stylesheet"/>
@stop
@section('main')
<div class="row">
<div class="col-md-3">
<div class="text-center"><h2>Skrzynki pocztowe</h2></div>
<div class="panel panel-default">
<div class="panel-body">
<a href="/compose" class="btn btn-primary btn-block">Utwórz</a>
<ul class="folders">
@foreach($mailboxes as $mailbox)
<li>
<a href="/inbox?box={{{ $mailbox }}}"><i class="glyphicon
glyphicon-inbox"></i> {{{ $mailbox }}}</a>
</li>
@endforeach
</ul>
</div>
</div>
</div>
<div class="col-md-9">
<div class="text-center"><h2>Internetowy klient pocztowy - {{{ $currentMailbox }}}
</h2></div>
<div class="panel panel-default">
<div class="panel-body">
<ul class="messages">
@foreach($messages as $message)
<li>
<a href="/read/{{ $message->getMessageNo() }}" class="nohover">
<div class="header">
<span class="from">
{{{ $message->getFrom() }}}
<span class="pull-right">
{{{ $message->getDate()->format('F jS, Y h:i A') }}}
</span>
</span>
{{{ $message->getSubject() }}}
</div>
</a>
<hr/>
</li>
@endforeach
</ul>
</div>
</div>
<div class="text-center">
{{ $paginator->render() }}
</div>
</div>
</div>
@stop
Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 625
Analiza kodu widoku app.inbox pokazuje, że rozszerza on nieistniejący jeszcze szablon Blade
o nazwie layouts.authed. Jest on niemal identyczny z układem layouts.public, który przedsta-
wiliśmy wcześniej, podczas prezentowania zagadnień związanych z uwierzytelnianiem, lecz różni
się od niego pod jednym względem: zawiera grupę konstrukcji warunkowych sprawdzających
zmienne sesyjne i wyświetlających stosowne komunikaty o błędach. Pozwolą nam one, o czym
Czytelnik się niebawem przekona, w atrakcyjny wizualnie sposób przekazywać użytkownikom
komunikaty informacyjne dotyczące wykonywanych operacji. Aby nie został pominięty żaden waż-
ny element aplikacji, poniżej przedstawiamy kod tego szablonu:
<html>
<head>
@section('stylesheets')
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/
css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhj
VME1fgjWPGmkzs7" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/
bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3u
ZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
@show
</head>
<body>
<div class="container">
@if(Session::has('success'))
<div class="alert alert-success" role="alert">{{ Session::get('success') }}</div>
@endif
@if(Session::has('error'))
<div class="alert alert-danger" role="alert">{{ Session::get('error') }}</div>
@endif
@if(Session::has('warning'))
<div class="alert alert-warning" role="alert">{{ Session::get('warning') }}</div>
@endif
@if(Session::has('info'))
<div class="alert alert-info" role="alert">{{ Session::get('info') }}</div>
@endif
@yield('main')
</div>
</body>
@section('javascript')
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS"
crossorigin="anonymous"></script>
@show
</html>
626 Część V Tworzenie praktycznych projektów PHP i MySQL
Wróćmy teraz do układu app.inbox. Dzieli on interfejs aplikacji na dwie kolumny. Pierwszą z nich
jest kolumna paska bocznego, zawierająca listę dostępnych skrzynek pocztowych oraz odnośnik
umożliwiający napisanie nowej wiadomości. Druga kolumna zawiera listę wiadomości dostęp-
nych na obecnie prezentowanej stronie bieżącej skrzynki pocztowej, przy czym każda wiadomość
jest prezentowana jako odnośnik, którego kliknięcie pozwala wyświetlić jej treść. U dołu strony
wywoływana jest metoda render() obiektu $paginator, która automatycznie generuje atrakcyjny
wizualnie widżet pozwalający na przechodzenie pomiędzy kolejnymi stronami wiadomości w bie-
żącej skrzynce pocztowej (patrz rysunek 29.3).
$mailboxes = $client->getMailboxes();
if($currentMailbox != $client->getCurrentMailbox()) {
if(in_array($currentMailbox, $mailboxes)) {
Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 627
$client->setCurrentMailbox($currentMailbox);
}
}
$messageId = $request->route('id');
$message = $client->getMessage($messageId)->fetch();
@section('stylesheets')
@parent
<link href="/css/app.css" rel="stylesheet"/>
@stop
@section('main')
<div class="row">
<div class="col-md-3">
<div class="text-center"><h2>Skrzynki pocztowe</h2></div>
<div class="panel panel-default">
<div class="panel-body">
<a href="/compose" class="btn btn-primary btn-block">Utwórz</a>
<ul class="folders">
@foreach($mailboxes as $mailbox)
<li>
<a href="/inbox?box={{{ $mailbox }}}"><i class="glyphicon
glyphicon-inbox"></i> {{{ $mailbox }}}</a>
</li>
@endforeach
</ul>
</div>
</div>
</div>
<div class="col-md-9">
<div class="text-center"><h2>Internetowy klient pocztowy - {{{ $currentMailbox }}}
</h2></div>
<div class="panel panel-default">
<div class="panel-body">
<div class="header">
<span class="from">
{{{ $message->getFrom() }}}
</span>
<span class="subject">
628 Część V Tworzenie praktycznych projektów PHP i MySQL
@stop
Podobnie jak wcześniej metoda kontrolera, także przedstawiony powyżej szablon app.read pod
wieloma względami przypomina szablon app.inbox; wyjątkiem jest jedynie główny blok zawarto-
ści szablonu, który w głównej części strony prezentuje treść wiadomości, a nie ich listę. Ten wi-
dok określa też kilka operacji, które można wykonać na wiadomości, a konkretnie chodzi o napi-
sanie odpowiedzi lub usunięcie danej wiadomości. Analizując zawartość umieszczoną
bezpośrednio za treścią wiadomości, można zauważyć wywołanie metody getAttachments()
obiektu Message. Jak wiadomo z wcześniejszej części rozdziału, metoda ta zwraca tablicę obiek-
tów Attachments (zdefiniowanych w ramach naszej biblioteki IMAP), reprezentujących załączniki
odnalezione w wiadomości. Trzeba pamiętać, że podobnie jak obiekt Message, także obiekty klasy
Attachment nie pobierają zawartości załączników, jeśli im tego jawnie nie nakażemy. To ważna
cecha projektowa, gdyż wyświetlanie treści wiadomości nie wymaga wczytywania wszystkich
dołączonych do niej załączników. Zamiast tego w tym widoku jedynie przeglądamy listę wszyst-
kich załączników i dla każdego z nich generujemy odnośnik pozwalający na jego pobranie.
$messageId = $request->route('id');
$attachmentPart = $request->route('partId');
$message = $client->getMessage($messageId)->fetch();
$attachment = $message->getAttachmentByPartId($attachmentPart)->fetch();
Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 629
$messageId = $request->route('id');
$client->deleteMessage($messageId);
Pierwszą z metod związanych z wysyłaniem wiadomości poczty elektronicznej, którą się zajmiemy,
będzie metoda InboxController::getCompose(). Jej zadaniem jest wygenerowanie interfejsu użyt-
kownika pozwalającego na napisanie i wysłanie nowej wiadomości bądź też odpowiedzi na jed-
ną z wiadomości dostępnych w skrzynce pocztowej. Poniżej przedstawiony został kod tej metody:
public function getCompose(Request $request)
{
$client = $this->getImapClient();
$mailboxes = $client->getMailboxes();
$messageId = $request->route('id');
$quotedMessage = '';
$message = null;
if(!is_null($messageId)) {
$message = $client->getMessage($messageId)->fetch();
$quotedMessage = $message->getPlainBody();
foreach($messageLines as &$line) {
$line = ' > ' . $line;
}
Ponieważ chcemy mieć możliwość zarówno tworzenia nowych wiadomości, jak i odpowiadania
na wiadomości znajdujące się w skrzynce pocztowej, logika działania metody InboxController::
getCompose() jest nieco bardziej rozbudowana i nie ogranicza się jedynie do pobierania listy
skrzynek pocztowych oraz generowania formularza HTML służącego do wpisywania treści wiado-
mości. W przypadku podania identyfikatora wiadomości, na którą chcemy odpowiedzieć, stanowią-
cego opcjonalny parametr trasy, metoda musi pobrać treść tej wiadomości (zapisaną w formie
zwyczajnego tekstu) i „zacytować” ją w tradycyjny sposób, charakterystyczny dla klientów poczty
elektronicznej. Ponieważ nasza aplikacja nie obsługuje wysyłania wiadomości w formacie
HTML, „zacytowanie” wiadomości sprowadza się do wykonania prostej operacji na łańcuchach
znaków, a konkretnie: dodaniu znaku ">" na początku każdego wiersza wiadomości, na którą
odpowiadamy. Ta „zacytowana” treść wiadomości, obiekt tej wiadomości oraz lista skrzynek
pocztowych zostają następnie przekazane do szablonu resources/views/app/compose.blade.php,
którego kod został przedstawiony poniżej:
@extends('layouts.authed')
@section('stylesheets')
@parent
<link href="/css/app.css" rel="stylesheet"/>
@stop
@section('main')
Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 631
<div class="row">
<div class="col-md-3">
<div class="text-center"><h2>Skrzynki pocztowe</h2></div>
<div class="panel panel-default">
<div class="panel-body">
<a href="/compose" class="btn btn-primary btn-block">Utwórz</a>
<ul class="folders">
@foreach($mailboxes as $mailbox)
<li>
<a href="/inbox?box={{{ $mailbox }}}">
<i class="glyphicon glyphicon-inbox"></i>
{{{ $mailbox }}}
</a>
</li>
@endforeach
</ul>
</div>
</div>
</div>
<div class="col-md-9">
<div class="text-center">
@if(is_null($message))
<h2>Klient pocztowy - Nowa wiadomość</h2>
@else
<h2>Klient pocztowy - Odpowiedź</h2>
@endif
</div>
value="" class="form-control"/>
</span>
@endif
</div>
<hr/>
<div class="messageBody">
<textarea class="form-control replybox"
name="message" rows="10" >{{{ $quotedMessage }}}</textarea>
</div>
<hr/>
<input type="submit" class="btn btn-block btn-primary"
value="Wyślij wiadomość"/>
</form>
</div>
</div>
</div>
</div>
@stop
Powyższy szablon to właściwie dwa bardzo podobne szablony umieszczone w jednym pliku.
Jeśli do szablonu została przekazana wiadomość (co określamy na podstawie zmiennej $message,
która może mieć wartość null lub inną), to generujemy formularz z wypełnionymi polami nadaw-
cy, adresata i tematu, a dodatkowo w wielowierszowym polu tekstowym wyświetlamy przygotowa-
ną w kontrolerze treść wiadomości. Jeśli jednak żadna wiadomość nie zostanie przekazana do
szablonu, to wszystkie te pola będą puste, a użytkownik będzie je mógł odpowiednio wypełnić.
Poza tym także ten szablon widoku jest zgodny z naszym standardem i wyświetla w bocznej
kolumnie listę wszystkich dostępnych skrzynek pocztowych; formularz do edycji wiadomości jest
wyświetlany w głównej części strony.
$from = $request->input('from');
$to = $request->input('to');
$subject = $request->input('subject');
$message = $request->input('message');
Metoda validate() ma dwa parametry. Pierwszym z nich są weryfikowane dane, które mogą
mieć postać tablicy lub obiektu ArrayAccess. W naszym przykładzie jako ten pierwszy parametr
przekazywany jest bezpośrednio obiekt żądania, Requet, gdyż implementuje on interfejs PHP
ArrayAccess. Drugim parametrem metody validate() jest tablica par klucz-wartość, w której
kluczami są nazwy zmiennych przekazanych jako pierwszy parametr, a wartościami — łańcuchy
znaków definiujące reguły walidacji.
gdzie <reguła> to nazwa reguły (listę wszystkich dostępnych reguł można znaleźć w dokumentacji
Laravela, w sekcji poświęconej walidacji). Każda reguła może zawierać także jeden lub więcej
parametrów, przy czym pierwszy z nich należy poprzedzić znakiem dwukropka, a pozostałe
oddzielić od siebie przecinkami. Nie wszystkie reguły mają parametry, a poszczególne reguły
używane do sprawdzenia danej zmiennej wejściowej muszą być od siebie oddzielone znakami
potoku: "|".
W przedstawionej powyżej metodzie postSend() zastosowaliśmy reguły required, email oraz max.
Pierwsza z nich, required, upewnia się, że podana zmienna wejściowa istnieje. Jak można się
domyślić, reguła email sprawdza, czy dane wejściowe mogą być prawidłowym adresem e-mail
(oczywiście tego nigdy nie można być pewnym aż do momentu, gdy wiadomość zostanie przesłana
na serwer i przez niego odebrana). Reguła max określa natomiast maksymalną dopuszczalną
długość danych; w naszym przykładzie sprawdzamy, czy tytuł wiadomości nie składa się z więcej
niż 255 znaków.
Podobnie jak komponent Validation, także Mail jest bardzo solidny i przedstawienie pełnego
zestawu jego możliwości wykracza poza zakres tematyczny tego rozdziału. Śmiało można jednak
powiedzieć, że pozwala on na łatwe wysyłanie wiadomości poczty elektronicznej, zarówno w for-
macie tekstowym, jak i HTML, przy wykorzystaniu wielu różnych metod transportu, zaczynając
od klasycznego programu sendmail, a kończąc na dostawcach takich jak Mandrill. My skorzysta-
my z metody Mail::raw(), która umożliwia wysyłanie „nieprzetworzonych” wiadomości e-mail,
czyli takich, których treść nie jest tworzona przy użyciu widoku Blade. Poniżej przedstawiony
został przykład wywołania tej metody:
\Mail::raw($message, function($message) use ($to, $from, $subject) {
$message->from($from);
634 Część V Tworzenie praktycznych projektów PHP i MySQL
$message->to($to);
$message->subject($subject);
});
Jak widać, pierwszym parametrem wywołania tej metody jest łańcuch znaków zawierający treść
wiadomości; w naszym przykładzie jest to dokładnie ten sam tekst, który został przesłany przez
użytkownika. Drugim parametrem jest domknięcie pobierające obiekt wiadomości. Domknięcie
to pozwala na zaimplementowanie dowolnej logiki niezbędnej do określenia szczegółów wiadomo-
ści, takich jak jej temat lub adres nadawcy i odbiorców. Po wykonaniu domknięcia Laravel zastosuje
ten obiekt wiadomości i spróbuje ją wysłać, używając transportu skonfigurowanego w aplikacji.
W naszym przykładzie spowoduje to wysłanie napisanej przez użytkownika wiadomości na podany
adres poczty elektronicznej.
Wnioski
Jeśli udało Ci się, Czytelniku, dotrzeć do tego miejsca, to gratulujemy! Musiałeś zmierzyć się
z obszernym materiałem obejmującym wiele zagadnień — zaczynając od konstrukcji aplikacji
tworzonych z użyciem frameworka Laravel, przez korzystanie z rozszerzenia PHP IMAP, a kończąc
na tworzeniu obiektowych bibliotek. Każdemu z tych zagadnień z powodzeniem można by poświę-
cić odrębny rozdział (a w niektórych przypadkach nawet całą książkę), więc dalsze ich analizowa-
nie możesz potraktować jako zadanie do samodzielnego wykonania. Możesz przy tym korzystać
z dwóch wspaniałych źródeł informacji, którymi są:
dokumentacja frameworka Laravel: http://laravel.com/docs/;
dokumentacja rozszerzenia IMAP języka PHP: http://php.net/imap.
Rozdział 30.
Integracja z mediami
społecznościowymi
— udostępnianie
i uwierzytelnianie
Wraz z ugruntowywaniem się pozycji mediów społecznościowych jako podstawy naszych interakcji
coraz więcej aplikacji internetowych zaczyna korzystać z integracji z tymi platformami. Choć każda
z platform implementuje te mechanizmy integracji w inny sposób (używając własnych usług
internetowych, wywołań API itd.) to istnieją jednak pewne wspólne trendy i rozwiązania —
zwłaszcza związane z uwierzytelnianiem — które cieszą się dużą popularnością. W tym rozdziale
przedstawimy wybrane z nich, na przykład OAuth, oraz pokażemy, jak można wykonywać pewne
powszechne operacje z użyciem popularnej platformy społecznościowej, jaką jest Instagram.
Warto zwrócić uwagę na to, że choć w niniejszym rozdziale skoncentrujemy się na Instagramie,
to zaprezentowane tu pojęcia i techniki można zastosować także w odniesieniu do większości
innych platform społecznościowych.
Aby zrozumieć OAuth, należy zdefiniować różne jednostki lub role, które będą uczestniczyć
w interakcji z procesem uwierzytelniania. Zawiera je na poniższa lista:
Zasób — rzecz, do której chcemy uzyskać dostęp. W schematach OAuth często zdarza
się, że jedna operacja autoryzacji zapewnia dostęp do wielu zasobów, na przykład
użytkownik może chcieć udzielić komuś prawa do wyświetlania listy osób obserwujących
jego poczynania oraz wysyłania wiadomości na ich konta w serwisie Twitter (na którym
każdy jest odrębnym zasobem).
636 Część V Tworzenie praktycznych projektów PHP i MySQL
Ostatecznym celem całej operacji jest sprawienie, by serwer uwierzytelniania udzielił klientowi
dostępu do jednego lub kilku zasobów użytkownika przechowywanych na serwerze zasobów.
Cały ten proces w wizualny sposób został przedstawiony na rysunku 30.1.
Trzeba zwrócić uwagę na to, że diagram przedstawiony na rysunku 30.1 niekoniecznie musi dokład-
nie odpowiadać sekwencji przesyłanych żądań pomiędzy poszczególnymi podmiotami. Zależnie
od charakteru autoryzacji podanej przez użytkownika mogą się także zmieniać generowane żądania.
Jednak we wszystkich przypadkach ostatecznym celem OAuth jest dostarczenie klientowi żetonu
dostępu, pozwalającego mu uzyskać dostęp do zasobów, z których właściciel pozwolił korzystać.
Oczywiście stosowanie OAuth jest znacznie bardziej złożone, niż pokazuje to powyższy diagram,
dlatego opiszemy teraz kilka związanych z nim szczegółów. Przede wszystkim, aby klient mógł
uczestniczyć w transakcji OAuth, musi być znany serwerowi autoryzacji. Innymi słowy, oprócz
ewentualnego przydziału autoryzacyjnego (authorization grant) klient musi przekazać temu
serwerowi także swoje własne dane uwierzytelniające. Serwer autoryzacji przetwarza następnie
żądanie odnoszące się do konkretnego zasobu nie tylko na podstawie przedstawionego przy-
działu autoryzacji, lecz także na podstawie danych uwierzytelniających klienta. Każdy klient
musi zostać uprzednio zarejestrowany w danej usłudze (na przykład trzeba odwiedzić sekcję dla
programistów serwisu Twitter i zarejestrować w nim tworzoną aplikację) i uzyskać identyfikator
klienta (ang. Clinet ID), stanowiący informację publiczną, oraz klucz tajny klienta (ang. Client
Secret), stanowiący informację poufną, które są następnie używane w procesie autoryzacji.
Przebieg procesu OAuth po zarejestrowaniu się klienta w znacznej mierze zależy od charakteru
przydziału (ang. Grant), który właściciel autoryzuje na rzecz klienta. W przypadku protokołu
OAuth 2.0 wyróżnia się cztery podstawowe, unikalne typy udziałów:
Kod autoryzacji (ang. Authorization Code) — używany przez aplikacje serwerowe
(takie jak aplikacje pisane w języku PHP), które chciałyby uzyskać dostęp do zasobu.
Rozdział 30. Integracja z mediami społecznościowymi — udostępnianie i uwierzytelnianie 637
Przydział niejawny (ang. Implicit) — stosowany przez aplikacje mobilne czy też inne
aplikacje, które w całości są wykonywane na urządzeniu nadzorowanym przez właściciela
(mogą to być na przykład aplikacje pisane w języku JavaScript lub aplikacje iPhone’a).
Hasło właściciela zasobu (ang. Resource Owner Password Credential) — używane
wyłącznie przez zaufanych klientów, takich jak klienty w całości kontrolowane
przez właściciela zasobu.
Dane uwierzytelniające klienta (ang. Client Credentials) — stosowane podczas
korzystania z API aplikacji.
W tym rozdziale skoncentrujemy się wyłącznie na pierwszych dwóch typach udziałów: kodzie
autoryzacji oraz przydziale niejawnym, gdyż bez wątpienia są one najczęściej używane podczas
implementowania OAuth w typowych aplikacjach internetowych.
Gdy aplikacja serwerowa ma już kod autoryzacji, może wygenerować kolejne żądanie HTTP,
które ponownie jest kierowane do serwera autoryzacji; zawiera ono identyfikator klienta aplikacji,
kod tajny klienta oraz otrzymany wcześniej kod autoryzacji. Po sprawdzeniu wszystkich tych
informacji przez serwer autoryzacji aplikacja otrzymuje żeton dostępu, który powinna zapisać
i którego może następnie używać, by odwoływać się do wybranych zasobów.
638 Część V Tworzenie praktycznych projektów PHP i MySQL
Warto zauważyć, że stosowany tu termin żeton dostępu może być nieco mylący. Ogólnie rzecz
biorąc, dane zwracane przez serwer autoryzacji i określane tutaj jako „żeton dostępu” są strukturą
danych, która — między innymi — zawiera dwa niezależne żetony. Pierwszy z nich jest faktycz-
nym żetonem dostępu (ang. Access Token), a drugi — żetonem odświeżania (ang. Refresh Token).
Ściśle mówiąc, żeton dostępu nie może być używany bezterminowo, lecz jedynie przez określony
czas, po którego upłynięciu konieczne jest ponowne wykonanie procesu autoryzacji. Ponieważ
takie cykliczne żądanie od użytkownika potwierdzania zgody na dostęp byłoby niewygodne
i niepraktyczne, aplikacja serwerowa może w niezauważalny dla niego sposób poprosić o przy-
dzielenie nowego żetonu dostępu, używając do tego żetonu odświeżenia, przy czym cały ten
proces może być wykonany bez jakiejkolwiek ingerencji użytkownika. Okres ważności żetonu do-
stępu zawsze można określić na podstawie daty wygaśnięcia dostępnej w danych żetonu. Kiedy
ten okres minie, żeton dostępu można odświeżyć, korzystając z żetonu odświeżenia.
Przydziały niejawne
Czasami może się zdarzyć, że będziemy chcieli używać OAuth w sytuacjach, w których aplikacja
działająca na serwerze nie będzie dostępna (dotyczy to na przykład aplikacji mobilnych lub
aplikacji pisanych w języku JavaScript). Z myślą o takich przypadkach opracowano odrębną proce-
durę pozwalającą na uzyskanie przydziału autoryzacji, nazywaną przydziałem niejawnym (ang.
Implicit Grant). W odróżnieniu od przydziału opartego na kodzie autoryzacji, który weryfikuje
aplikację przesyłającą żądanie na podstawie jej informacji uwierzytelniających, w tym przypadku
tożsamość aplikacji jest weryfikowana wyłącznie na podstawie adresu URI. Nie jest także dostępny
żeton odświeżania. Schemat żądań generowanych podczas korzystania z przydziału niejawnego
został przedstawiony na rysunku 30.3.
Po zarejestrowaniu się klienta na Instagramie uzyskamy jego identyfikator oraz klucz tajny,
niezbędne do pomyślnego wykonania uwierzytelnienia. Obie te wartości należy zapisać, gdyż
mają one zasadnicze znaczenie dla tworzonej aplikacji.
Nasza aplikacja PHP będzie się składać z pięciu odrębnych skryptów PHP. Pierwszy z nich będzie
prostym plikiem konfiguracyjnym, zawierającym wartości konieczne do zaimplementowania
klienta OAuth. Kolejne dwa skrypty będą stanowić właściwą implementację klienta OAuth, a ostat-
nie dwa będą zawierać możliwości funkcjonalne implementowanego klienta Instagrama. Aby
zapewnić sobie możliwość łatwego wykonywania różnych żądań HTTP, skorzystamy z pakietu
Guzzle HTTP Client, pobranego za pośrednictwem Composera, a atrakcyjny wygląd aplikacji
uzyskamy dzięki wykorzystaniu frameworka CSS Bootstrap.
return [
'client_id' => 'xxx',
'client_secret' => 'xxx',
'redirect_uri' => 'http://' . $_SERVER['HTTP_HOST'] . '/completeoauth.php',
'scopes' => [
'likes',
'basic',
640 Część V Tworzenie praktycznych projektów PHP i MySQL
'public_content'
]
];
?>
Klucz scopes powyższych danych konfiguracyjnych zawiera bardzo ważną tablicę — definiuje
ona te informacje o użytkowniku, do których staramy się uzyskać dostęp podczas operacji
uwierzytelniania OAuth. W zasadzie można je sobie wyobrazić jako unikalne stałe łańcuchowe
charakterystyczne dla konkretnej platformy używanej do uwierzytelniania. Każda z tych stałych
reprezentuje odrębne możliwości funkcjonalne, dane czy też zasób, na których wykorzystanie
w aplikacji staramy się uzyskać zgodę użytkownika. Instagram udostępnia sporo takich wartości
zakresu, a pełną ich listę można znaleźć w dokumentacji serwisu zamieszczonej na stronie
https://www.instagram.com/developer/authorization/. Nas interesują zakresy likes, basic oraz
public_content, które pozwolą nam na pobieranie podstawowych informacji o koncie, informacji
o profilu publicznym oraz mediach dostępnych dla danego użytkownika, jak również możliwość
oznaczania treści jako ulubionych w imieniu użytkownika.
$authParams = [
'client_id' => $settings['client_id'],
'client_secret' => $settings['client_secret'],
'response_type' => 'code',
'redirect_uri' => $settings['redirect_uri'],
'scope' => implode(' ', $settings['scopes'])
];
?>
Rozdział 30. Integracja z mediami społecznościowymi — udostępnianie i uwierzytelnianie 641
<html>
<head>
<title>Rozdział 30. - prosty klient Instagrama</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/
bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8h
DDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-
theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzap
T0u7EYsXMjQV+0En5r" crossorigin="anonymous">
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS"
crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h1>Rozdział 30. - prosty klient Instagrama</h1>
<div class="row">
<div class="col-md-4 col-md-offset-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Logowanie z Instagramem</h3>
</div>
<div class="panel-body">
<a href="<?=$loginUrl?>" class="btn btn-block btnprimary">
Zaloguj się, używając Instagrama</a>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
Pomijając kod HTML używany do wyświetlenia strony, powyższy skrypt PHP rozpoczyna się od
dołączenia pliku autoload.php. Instrukcja ta służy do wczytania pliku autoload.php wygenerowane-
go przez Composer i zapewnia naszej aplikacji dostęp do dodatkowych modułów — w naszym
przykładzie jest to klient Guzzle HTTP. Choć dołączanie tego pliku na tej stronie nie jest absolutnie
niezbędne, to jednak dla uproszczenia aplikacji będziemy go dołączać do wszystkich tworzących
ją skryptów PHP.
Kolejny etap procesu jest obsługiwany na serwerze Instagrama, który najpierw poprosi użytkownika
o zalogowanie się na koncie Instagram, a następnie o udzielenie odpowiednich zezwoleń na
podstawie zakresu (lub zakresów) określonego w parametrach początkowego żądania GET. Zakłada-
jąc, że użytkownik uwierzytelni żądanie, Instagram przekieruje użytkownika pod podany adres
URI i doda jako jego parametr specjalny kod. Tym parametrem jest potrzebny nam kod autoryzacji,
niezbędny do pobrania żetonu dostępu.
642 Część V Tworzenie praktycznych projektów PHP i MySQL
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
require_once __DIR__ . '/../vendor/autoload.php';
if(!isset($_GET['code'])) {
header("Location: index.php");
exit;
}
try {
$response = $client->post('https://api.instagram.com/oauth/access_token',
[
'form_params' => [
'client_id' => $settings['client_id'],
'client_secret' => $settings['client_secret'],
'grant_type' => 'authorization_code',
'redirect_uri' => $settings['redirect_uri'],
'code' => $_GET['code']
]
]);
} catch(ClientException $e) {
if($e->getCode() == 400) {
$errorResponse = json_decode($e->getResponse()->getBody(), true);
die("Błąd uwierzytelniania: {$errorResponse['error_message']}");
}
throw $e;
}
$_SESSION['access_token'] = $result;
header("Location: feed.php");
exit;
Ten skrypt rozpoczyna się od opisanego już wcześniej dołączenia pliku autoload.php wygenero-
wanego przez Composer oraz wczytania tablicy ustawień, która zostaje zapisana w zmiennej
$settings. Następnie upewniamy się, czy zgodnie z oczekiwaniami dostępny jest parametr żądania
HTTP GET o nazwie code. Zakładając, że parametr ten został przekazany, możemy utworzyć instan-
cję klienta Guzzle HTTP i użyć go do wykonania żądania POST skierowanego ponownie do serwera
autoryzacji Instagrama. Korzystając z tego żądania, przesyłamy na serwer wszystkie niezbędne
dane i kojarzymy je z kluczem form_params. Wśród danych znajdują się identyfikator klienta
oraz jego klucz tajny, określenie typu przydziału, adres URI przekierowania, jak również kod
autoryzacji przekazany we wcześniejszym żądaniu.
Rozdział 30. Integracja z mediami społecznościowymi — udostępnianie i uwierzytelnianie 643
Po przesłaniu żądania Instagram uwierzytelni wszystkie przekazane dane, aby sprawdzić żądanie
autoryzacji, a następnie w odpowiedzi zwróci żeton dostępu. W razie niepowodzenia autoryzacji
Instagram zwróci kod błędu HTTP 400, który w naszym kliencie zostanie zgłoszony jako wyjątek.
Wyjątek ten jest przechwytywany i wyświetlany w celach diagnostycznych jako komunikat.
W przypadku prawidłowego obsłużenia żądania serwer przekazuje w odpowiedzi dokument w for-
macie JSON, zawierający żeton dostępu, żeton odświeżenia oraz kilka innych metainformacji,
takich jak data wygaśnięcia ważności żetonu dostępu. W standardowej aplikacji informacje te
zostałyby zapisane w jakimś trwałym magazynie związanym z danym użytkownikiem (na przykład
w bazie danych), ale w naszej prostej aplikacji zachowamy je jedynie w sesji użytkownika.
Skoro uzyskaliśmy już ważne informacje uwierzytelniające pozwalające na interakcję z Instagra-
mem w imieniu naszego użytkownika, możemy przekierować go do punktu głównej aplikacji i wy-
świetlić strumień komunikatów z jego konta na Instagramie. Zadanie to realizuje kolejny skrypt,
feed.php, który zostanie przedstawiony w następnym podpunkcie rozdziału.
Jako że ten skrypt jest nieco bardziej złożony niż poprzednie, opiszemy go w dwóch częściach.
Pierwsza z nich zawiera logikę wykonywaną przed wygenerowaniem interfejsu strony, a druga
obejmuje kod związany z generowaniem tego interfejsu.
use GuzzleHttp\Client;
if(!isset($_SESSION['access_token']) || empty($_SESSION['access_token'])) {
header("Location: index.php");
exit;
}
$requestUri = "https://api.instagram.com/v1/users/self/media/recent";
$recentPhotos = [];
$tag = '';
$response = $client->get($requestUri, [
'query' => [
'access_token' => $_SESSION['access_token']['access_token'],
'count' => 50
644 Część V Tworzenie praktycznych projektów PHP i MySQL
]
]);
if(is_array($results)) {
$recentPhotos = array_chunk($results['data'], 4);
}
?>
Skrypt rozpoczyna się od wykonania kilku zabezpieczających testów, które pozwalają upewnić
się, że faktycznie posiadamy żeton dostępu, i przekierowują użytkownika na stronę logowania,
jeśli okazuje się, że żeton ten nie jest dostępny. Po tej weryfikacji żetonu inicjujemy kilka
zmiennych: $requestUri (zawierającą adres URI, którego będziemy używać w celu pobrania me-
diów z Instagram API), $recentPhostos (której będziemy używać do przechowywania mediów
pobieranych przy użyciu żądań) oraz $tag (zawierającą znacznik stosowany do wyszukiwania
i pobierania mediów).
Domyślnie zależy nam na pobieraniu najnowszych dostępnych mediów, chyba że został podany
jakiś znacznik. A zatem inicjujemy zmienną $requestUri, zapisując w niej adres odpowiedniego
punktu końcowego, a następnie sprawdzamy, czy w żądaniu HTTP GET został podany parametr
queryTag. Jeśli nie został, to wykonamy żądanie pobierające najnowsze media, a jeśli został,
skierujemy żądanie do innego adresu URI w API Instagrama, który pozwoli nam pobrać wyłącznie
media zawierające podany znacznik. Zastosowanie takiego rozwiązania jest możliwe dlatego,
że wyniki zwracane przez Instagram API w obu przypadkach są na tyle podobne, że możemy
ich używać podczas generowania jednego interfejsu użytkownika naszej strony.
Po określeniu adresu URI punktu końcowego wykonujemy żądanie HTTP GET, przekazując jed-
nocześnie liczbę wyników, jakie należy zwrócić (w formie parametru count), oraz żeton dostępu
OAuth. Warto zauważyć, że żeton ten został zwrócony przez serwis Instagram w formie dokumentu
JSON, w którym były dostępne także inne informacje, takie jak żeton odświeżenia. Jednak w tym
żądaniu przekazujemy na serwer jedynie żeton dostępu.
Wynikiem tego żądania będzie dokument JSON zawierający kolekcję wpisów spełniających kry-
teria określone w żądaniu. Dlatego też, w celu dalszego przetwarzania, dokument ten przekształcamy
na tablicę PHP przy użyciu funkcji json_decode(). Następnie dzielimy zawartość tej tablicy na
części składające się z czterech elementów, co ułatwi nam późniejsze wyświetlanie pobranych
mediów. W efekcie uzyskamy tablicę $recentPhotos, zawierającą listę czteroelementowych tablic
z faktycznymi mediami, które musimy wyświetlić na stronie.
W kolejnym etapie działania skryptu tablica ta jest wyświetlana w atrakcyjny wizualnie sposób,
z wykorzystaniem frameworka CSS Bootstrap. Poniżej przedstawiona została druga część skryptu
feed.php, która służy właśnie do tego celu:
<html>
<head>
<title>Rozdział 30. - prosty klient Instagrama</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/
bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZl
pLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/
bootstraptheme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J
5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS"
crossorigin="anonymous"></script>
Rozdział 30. Integracja z mediami społecznościowymi — udostępnianie i uwierzytelnianie 645
<script src="//code.jquery.com/jquery-1.12.0.min.js"></script>
<script>
$(document).ready(function() {
$('.like-button').on('click', function(e) {
e.preventDefault();
var media_id = $(e.target).data('media-id');
$.get('like.php?media_id=' + media_id,
function(data) {
if(data.success) {
$(e.target).remove();
}
});
});
});
</script>
</head>
<body>
<div class="container">
<h1>Ostatnie zdjęcia z Instagrama</h1>
<div class="row">
<div class="col-md-12">
<form class="form-horizontal" method="GET" action="feed.php">
<fieldset class="form-group">
<div class="col-xs-9 input-group">
<input type="text" class="form-control"
id="tagQuery" name="tagQuery" placeholder="Szukaj znacznika...."
value="<?=$tag?>"/>
<span class="input-group-btn">
<button type="submit" class="btn btnprimary">
<i class="glyphicon glyphicon-search"></i> Szukaj</button>
</span>
</div>
</fieldset>
</form>
</div>
</div>
<div class="row">
<?php foreach($recentPhotos as $photoRow): ?>
<div class="row">
<?php foreach($photoRow as $photo): ?>
<div class="col-md-3">
<div class="card">
<div class="card-block">
<h4 class="cardtitle"><?= substr($photo['caption']
['text'], 0, 30)?></h4>
<h6 class="card-subtitle textmuted"><?= substr
($photo['caption']['text'], 30, 30)?></h6>
</div>
<img class="card-img-top"
src="<?=$photo['images']['thumbnail']['url']?>"
alt="<?=$photo['caption']['text']?>" />
<div class="card-block">
<?php foreach($photo['tags'] as $tag): ?>
<a href="feed.php?tagQuery=<?=$tag?>"
class="card-link">#<?=$tag?></a>
<?php endforeach?>
</div>
<div class="card-footer text-right">
<?php if(!$photo['user_has_liked']): ?>
<a data-media-id="<?=$photo['id']?>"
href="#" class="btn btn-xs btn-primary
like-button"><i class="glyphicon
646 Część V Tworzenie praktycznych projektów PHP i MySQL
glyphicon-thumbs-up"></i> Polub</a>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
</div>
</body>
</html>
Wyniki generowane przez skrypt feed.php stanowią kluczowy element naszej przykładowej
aplikacji — jest nimi bądź to lista ostatnich zdjęć z konta danego użytkownika na Instagramie,
bądź też lista zdjęć związanych z konkretnym znacznikiem. U góry strony widnieje pasek do
wyszukiwania znaczników, a poniżej znajdują się poszczególne zdjęcia rozmieszczone przy wy-
korzystaniu siatki zdefiniowanej przez framework CSS Bootstrap. Oprócz samych zdjęć oraz
ich tytułów pod każdym ze zdjęć, których użytkownik jeszcze nie polubił, wyświetlany jest przycisk
Polub. Jest on obsługiwany przez kod jQuery, a jego kliknięcie powoduje wygenerowanie żądania
AJAX skierowanego z powrotem do naszej aplikacji (a konkretnie: do skryptu like.php), którego
zadaniem jest oznaczenie danego zdjęcia jako „lubianego”. Jeśli użytkownik już wcześniej po-
lubił zdjęcie, przycisk ten nie jest wyświetlany.
use GuzzleHttp\Client;
header("Content-Type: application/json");
if(!isset($_SESSION['access_token']) || empty($_SESSION['access_token'])) {
header("Location: index.php");
exit;
}
if(!isset($_GET['media_id']) || empty($_GET['media_id'])) {
echo json_encode([
'success' => false
]);
return;
}
$media_id = $_GET['media_id'];
$requestUri = "https://api.instagram.com/v1/media/{$media_id}/likes";
$response = $client->post($requestUri, [
'form_params' => [
'access_token' => $_SESSION['access_token']['access_token']
Rozdział 30. Integracja z mediami społecznościowymi — udostępnianie i uwierzytelnianie 647
]
]);
Wniosek
Tak oto zakończyliśmy omawianie sposobu wykorzystania protokołu OAuth na przykładzie
serwisu Instagram. Oczywiście nie przedstawiliśmy tu wyczerpującej listy wszystkich dostępnych
wywołań API. Czytelnik może dokładniej zapoznać się z wszystkimi tymi żądaniami API we
własnym zakresie, stosując przedstawione tu techniki i narzędzia służące do uwierzytelniania
aplikacji przez użytkownika i wykonywania żądań HTTP. Pełna dokumentacja API serwisu Insta-
gram jest dostępna na stronie http://www.instagram.com/developer/, czyli w tym samym miejscu,
w którym należy zarejestrować aplikację, by móc korzystać z protokołu OAuth.
Choć każdy etap używania protokołu OAuth został tu szczegółowo opisany, to jednak warto
zwrócić uwagę na to, że w profesjonalnych aplikacjach podejmowanie takich wysiłków nie jest
konieczne. Platformy społecznościowe, takie jak Facebook czy Google, udostępniają SDK dla
języka PHP przeznaczone do korzystania z ich różnych usług internetowych, które znacznie
ułatwiają proces autoryzacji. W przypadku innych platform, które nie udostępniają SDK dla języka
PHP, takich jak Twitter, często dostępne są doskonałe, otwarte SDK, takie jak TwitterOAuth
(http://twitteroauth.com/), zapewniające podobne możliwości.
Mimo że zrozumienie sposobu działania protokołu OAuth oraz zasad integracji z mediami
społecznościowymi bez wątpienia ma duże znaczenie, to zdecydowanie zaleca się, by po zdo-
byciu niezbędnej wiedzy i opanowaniu niezbędnych umiejętności zacząć korzystać z szerokiej
gamy już istniejących narzędzi, zamiast od podstaw tworzyć własne.
648 Część V Tworzenie praktycznych projektów PHP i MySQL
Rozdział 31.
Tworzenie
koszyka na zakupy
W tym rozdziale omówimy sposoby tworzenia prostego koszyka na zakupy. Zostanie on dodany
do bazy danych „Książkorama”, zaimplementowanej w części II, „Stosowanie MySQL”. Opiszemy
również drugą opcję — konfigurację i korzystanie z istniejącego w systemie Open Source koszyka
na zakupy PHP.
Osoby, które nie znają jeszcze terminu koszyk na zakupy (zwany także wózkiem na zakupy),
powinny się dowiedzieć, że jest on stosowany do opisu specyficznego mechanizmu zakupów
online. Po zakończeniu przeglądania zostaje dokonany zakup produktów z koszyka.
Aby zaimplementować koszyk na zakupy dla celów tego projektu, należy wprowadzić następujące
funkcje:
Baza danych produktów, które mają być sprzedawane online.
Katalog produktów online, podzielony na kategorie.
Koszyk na zakupy śledzący produkty, które chce zakupić użytkownik.
Skrypt kasy, który przetwarza dane dotyczące płatności i transportu.
Interfejs administracyjny.
Składniki rozwiązania
Przypomnijmy sobie bazę danych „Książkorama”, utworzoną w części II. W tym projekcie sklep
online „Książkorama” zostanie uruchomiony. Komponenty, z jakich będzie składać się aplikacja,
powinny spełniać następujące wymagania:
Należy znaleźć sposób połączenia bazy danych z przeglądarką użytkownika. Użytkownicy
powinni mieć możliwość przeglądania produktów według kategorii.
Użytkownicy powinni mieć także możliwość wybrania produktów z katalogu w celu
dokonania późniejszych zakupów. Musi istnieć możliwość śledzenia wybranych przez
nich produktów.
Kiedy użytkownicy zakończą zakupy, konieczne będzie podsumowanie ich zamówień,
pobranie szczegółów dostawy oraz przetworzenie płatności.
Należy również utworzyć interfejs administracyjny witryny „Książkorama”,
aby administrator mógł dodawać i edytować książki i kategorie witryny.
650 Część V Tworzenie praktycznych projektów PHP i MySQL
Znamy już ogólną koncepcję witryny, a zatem można już przystąpić do projektowania aplikacji
i jej poszczególnych komponentów.
Konieczne będzie również dodanie pewnych informacji do istniejącej bazy danych na temat adresów
dostaw, szczegółów płatności itd. Wiadomo już, jak utworzyć interfejs bazy danych MySQL za
pomocą PHP, tak więc ta część rozwiązania powinna być dość łatwa.
Ponadto, składanie zamówień przez klientów powinno się odbywać w ramach jednej transakcji.
Aby było to możliwe, musimy przekształcić tabele księgarni „Książkorama” w taki sposób, by
używały one modułu InnoDB. Zadanie to jest niezbyt skomplikowane.
Zastosowanie zmiennej sesji do śledzenia decyzji od strony do strony jest łatwiejsze, ponieważ
nie wymaga ciągłego wysyłania zapytań o te informacje do bazy danych. Zastosowanie takiego
podejścia pozwoli również uniknąć sytuacji nagromadzenia się śmieci w bazie danych z powodu
użytkowników, którzy tylko przeglądają lub zmieniają zdanie.
Tak więc konieczne jest zaprojektowanie zmiennej sesji lub zbioru zmiennych w celu przecho-
wywania decyzji użytkownika. Kiedy użytkownik zakończy zakupy i zapłaci za nie, informacje
te zostaną umieszczone w bazie danych jako dowód transakcji.
Dane te mogą również zostać zastosowane do utworzenia podsumowania obecnego stanu koszyka
w jednym rogu strony, aby użytkownik wiedział, ile planuje wydać w danym czasie.
Istnieje co najmniej kilka systemów płatności, które można wykorzystać, oraz wiele interfejsów
do tych systemów płatności, jednak zakres funkcji realizowanych przez poszczególne mechanizmy
przetwarzania płatności kartami kredytowymi w czasie rzeczywistym jest zazwyczaj dość podobny.
Trzeba utworzyć konto rozliczeniowe w banku dla kart, które będą akceptowane; zazwyczaj sam
bank wskaże listę rekomendowanych dostawców systemu płatności. Dostawca danego systemu
płatności poda parametry, które należy przekazać systemowi, oraz wskaże sposób przekazywania
tych parametrów. Producenci większości systemów płatności udostępniają przykładowe fragmenty
kodu źródłowego gotowego do wykorzystania w skryptach PHP, którymi można zastąpić funkcję
zaimplementowaną w tym rozdziale.
Rozdział 31. Tworzenie koszyka na zakupy 651
System płatności przekaże dane bankowi oraz zwróci kod sukcesu lub jeden z wielu kodów porażki.
W zamian za przekazywanie danych księgarni bramka płatności pobierać błędzie opłatę startową
i opłaty roczne, a także opłatę opartą na liczbie lub wartości transakcji. Niektórzy dostawcy pobie-
rają opłaty nawet za odrzucone transakcje.
Wybrany system płatności będzie potrzebował przynajmniej informacji o użytkowniku (takich jak
numer karty kredytowej), informacji identyfikujących księgarnię (aby określić, na które konto
należy przelać pieniądze) oraz całkowitej wartości transakcji.
Wartość zamówienia może być obliczona za pomocą zmiennej sesji koszyka na zakupy. Szczegóły
ostatecznego zamówienia zostaną przechowane w bazie danych i jednocześnie system pozbędzie
się zmiennej sesji.
Interfejs administratora
Oprócz powyższych rozwiązań zostanie utworzony interfejs administratora, który pozwoli na
dodawanie, usuwanie oraz edycję książek i kategorii w bazie danych.
Jedną z częstych zmian, jakie będą przeprowadzane, jest modyfikacja ceny produktu (na przykład
oferty specjalne i wyprzedaże). Oznacza to, że przy przechowywaniu zamówienia klienta powinna
być zachowana także cena zakupu. Koszmarem księgowym byłaby sytuacja, w której przechowy-
wane są jedynie informacje o produktach zakupionych przez użytkownika oraz ich aktualne ceny.
Oznacza to również, że kiedy użytkownik będzie chciał zwrócić lub wymienić produkt, otrzyma
prawidłową kwotę.
W tym przykładzie nie zostanie utworzony interfejs śledzący wypełnienie zamówienia. W razie
konieczności można go jednak dodać później.
Przegląd rozwiązania
Poniżej wszystkie składniki zostały połączone w całość. Istnieją dwa podstawowe sposoby ujęcia
systemu — widok użytkownika i widok administratora. Po rozważeniu wymaganej funkcjo-
nalności zostały utworzone dwa projekty działania systemu, przedstawione, odpowiednio, na ry-
sunkach 31.1 i 31.2.
Rysunek 31.1.
Widok użytkownika
systemu „Książkorama”
pozwala użytkownikom
na przeglądanie książek
według kategorii,
przeglądanie szczegółów
książki, dodawanie
książek do koszyka oraz
kupowanie produktów
652 Część V Tworzenie praktycznych projektów PHP i MySQL
Rysunek 31.2. Widok administratora systemu „Książkorama” pozwala na dodawanie, edycję i usuwanie
książek i kategorii
Rysunek 31.2 przedstawia interfejs administratora, który posiada większą liczbę skryptów, lecz
niewiele nowego kodu. Skrypty te pozwalają na zalogowanie się oraz na dodawanie książek
i kategorii.
Najprostszym sposobem implementacji edycji i usuwania książek oraz kategorii jest przedsta-
wienie administratorowi nieco zmodyfikowanej wersji interfejsu użytkownika witryny. Admini-
strator będzie miał możliwość przeglądania kategorii i książek, lecz zamiast dostępu do koszyka
na zakupy będzie mógł dotrzeć do konkretnej książki lub kategorii i dokonać jej edycji lub usunięcia.
Poprzez utworzenie skryptów przydatnych zarówno dla użytkownika, jak i dla administratora
można znacznie oszczędzić czas i wysiłek.
Jak często się zdarza w przypadku systemów takich jak ten prezentowany w tym rozdziale,
zostanie utworzony i zastosowany zbiór bibliotek funkcji. Zbiór funkcji, który zastosujemy w tym
projekcie, będzie podobny do API używanych w pozostałych projektach przedstawionych w niniej-
szej książce. Części kodu wyświetlające HTML zostaną umieszczone w jednej bibliotece według
zasady oddzielania logiki i zawartości. Co ważniejsze, pozwalają one na uczynienie kodu łatwiej-
szym w odczytywaniu i utrzymywaniu.
Rozdział 31. Tworzenie koszyka na zakupy 653
Konieczne również będą drobne zmiany w bazie danych „Książkorama”. Nazwa bazy danych
została zmieniona na ksiazka_kz (Koszyk na zakupy), aby możliwe było odróżnienie jej od bazy
danych utworzonej w części II.
Pełny kod projektu znajduje się na serwerze FTP wydawnictwa Helion — ftp://ftp.helion.pl/
przyklady/phmsv5.zip. Podsumowanie plików aplikacji jest przedstawione w tabeli 31.1.
use ksiazka_kz;
dos_wojew char(20),
dos_kod_poczt char(10),
dos_kraj char(20) not null
) engine=InnoDB DEFAULT CHARSET=UTF8;
Chociaż pierwotny interfejs systemu „Książkorama” nie zawierał żadnych błędów, w tym przy-
padku istnieje kilka dodatkowych wymagań, które muszą zostać spełnione.
Usunięcie tabeli recenzje — recenzje mogą zostać dodane jako rozszerzenie tego projektu.
Natomiast każda książka posiada pole opis, zawierające krótką informację na jej temat.
Zmiana modułu przechowywania danych na InnoDB. Dzięki temu możemy używać kluczy
obcych, a w trakcie zapisywania informacji na temat zamówienia klientów — również
transakcji.
Aby skonfigurować bazę danych w swoim systemie, należy uruchomić skrypt ksiazka_kz.sql po-
przez MySQL jako użytkownik root:
mysql –u root –p < ksiazka_kz.sql
Przed podjęciem powyższych działań zalecana jest zmiana hasła użytkownika ksiazka_kz na bar-
dziej bezpieczne niż 'haslo'. Należy pamiętać, że jeśli w pliku ksiazka_kz.sql zmienione zostanie
hasło, trzeba będzie taką samą zmianę wprowadzić w pliku funkcje_bazy.php (o tym, gdzie
należy to zrobić, już za chwilę).
Dołączony został również plik przykładowych danych. Nosi on nazwę populacja.sql. Można
umieścić przykładowe dane w bazie danych, uruchamiając ten skrypt poprzez MySQL w podobny
sposób.
Początkowa strona witryny jest tworzona przez skrypt o nazwie indeks.php. Wynik uruchomienia
tego skryptu przedstawiono na rysunku 31.3.
Rysunek 31.3.
Strona początkowa
witryny przedstawia
kategorie książek
dostępne w sprzedaży
Można dostrzec, że oprócz listy kategorii w prawym górnym rogu ekranu znajdują się: łącze do
koszyka na zakupy i pewne informacje podsumowujące jego zawartość. Informacje te będą się
pojawiać na każdej stronie przeglądanej przez użytkownika.
Jeżeli użytkownik kliknie jedną z tych kategorii, zostanie przeniesiony na stronę kategorii, tworzoną
przez skrypt pokaz_kat.php. Strona kategorii Internet jest przedstawiona na rysunku 31.4.
Rozdział 31. Tworzenie koszyka na zakupy 657
Rysunek 31.4.
Każda książka danej
kategorii jest
wyświetlana
razem z fotografią
Wszystkie książki kategorii Internet są przedstawione jako łącza. Jeżeli użytkownik wybierze
któreś z nich, zostanie przeniesiony do strony zawierającej dane szczegółowe książki. Strona danych
jednej z książek jest przedstawiona na rysunku 31.5.
Rysunek 31.5.
Każda książka
posiada stronę
danych
szczegółowych,
przedstawiającą
większą ilość
informacji, w tym
długi opis książki
Na tej stronie oprócz łącza Pokaż koszyk istnieje również łącze Dodaj do koszyka, za pomocą
którego użytkownik może wybrać produkt w celu jego zakupienia. Kwestia ta zostanie omówiona
podczas opisu tworzenia koszyka na zakupy.
Przedstawianie kategorii
Pierwszy skrypt, indeks.php, wyświetla wszystkie kategorie bazy danych. Jest on przedstawiony
na listingu 31.2.
require_once ('funkcje_ksiazka_kz.php');
tworz_naglowek_html("Witamy w księgarni Książkorama");
Skrypt ten rozpoczyna się od dołączenia pliku funkcje_ksiazka_kz.php, który ładuje wszystkie
biblioteki funkcji tej aplikacji.
Skrypt indeks.php zawiera również wywołania do pewnych funkcji wyświetlających HTML, takich
jak: tworz_naglowek_html() i tworz_stopke_html() (obie znajdują się w pliku funkcje_wyswietl.php).
Zawarty w nim jest także kod sprawdzający, czy użytkownik jest zalogowany jako administrator.
W razie pozytywnej odpowiedzi udziela mu różnych opcji nawigacyjnych — kwestia ta zostanie
opisana w podrozdziale poświęconym funkcjom administracyjnym.
A zatem funkcja pobierz_kategorie() łączy się z bazą danych i odczytuje listę wszystkich identy-
fikatorów i nazw kategorii. Napisana i wykorzystana została funkcja wynik_bd_do_tablicy(),
umieszczona w pliku funkcje_bazy.php. Funkcja ta jest przedstawiona na listingu 31.4. Pobiera
ona identyfikator wyniku MySQL i zwraca indeksowaną numerycznie tablicę rzędów, w której
każdy rząd jest tablicą asocjacyjną.
$tablica_wyn = array();
return $tablica_wyn;
}
W tym przypadku tablica ta zostanie zwrócona z powrotem aż do pliku indeks.php, gdzie nastąpi
jej przekazanie funkcji wyswietl_kategorie() pochodzącej z pliku funkcje_wyswietl.php. Funkcja
ta wyświetla każdą kategorię jako łącze do strony zawierającej wszystkie książki tej kategorii.
Kod tej funkcji jest przedstawiony na listingu 31.5.
}
echo "</ul>";
echo "<hr />";
}
Powyższa funkcja dokonuje konwersji wszystkich kategorii bazy danych na łącza. Wszystkie
łącza prowadzą do tego samego skryptu — pokaz_kat.php — lecz każde z nich posiada inny para-
metr, identyfikator kategorii, czyli idkat (jest to unikatowa liczba, generowana przez MySQL
i stosowana do identyfikacji kategorii).
Parametr kolejnego skryptu decyduje, która kategoria zostanie następnie wyświetlona.
$idkat = $_GET['idkat'];
$nazwa = pobierz_nazwe_kategorii($idkat);
tworz_naglowek_html($nazwa);
wyswietl_ksiazki($tablica_ksiazek);
tworz_stopke_html();
?>
Skrypt ten ma strukturę bardzo podobną do strony indeksu, z tą różnicą, że odczytywane są książki,
a nie kategorie.
Jak zwykle na początku następuje rozpoczęcie sesji, po czym dokonuje się konwersja przekazanego
identyfikatora kategorii na nazwę kategorii za pomocą funkcji pobierz_nazwe_kategorii():
$nazwa=pobierz_nazwe_kategorii($idkat);
Funkcja ta wyszukuje nazwę kategorii w bazie danych. Jest ona przedstawiona na listingu 31.7.
Rozdział 31. Tworzenie koszyka na zakupy 661
Po odczytaniu nazwy kategorii można utworzyć nagłówek HTML i przejść do odczytania i wyświe-
tlenia książek z bazy danych należących do wybranej kategorii:
$tablica_ksiazek=pobierz_ksiazki($idkat);
wyswietl_ksiazki($tablica_ksiazek);
$isbn = $_GET['isbn'];
$cel = "indeks.php";
if($ksiazka['idkat']) {
$cel = "pokaz_kat.php?idkat=".urlencode($ksiazka['idkat']);
}
Należy zapamiętać, że funkcja wyswietl_dane_ksiazki() szuka pliku obrazka dla książki w katalogu
obrazki/{$ksiazka['isbn']}.jpg. Jeżeli plik ten nie zostanie odnaleziony, nie ukaże się żaden
obrazek. Pozostała część skryptu pokaz_ksiazke.php konfiguruje opcje nawigacyjne. Zwykły
użytkownik będzie miał do wyboru przyciski Kontynuacja zakupów, który przeniesie go z po-
wrotem do strony kategorii, oraz Dodaj do koszyka, który doda książkę do jego koszyka na zaku-
py. Jeżeli użytkownik zalogowany jest jako administrator, otrzyma inne opcje. Przedstawimy je
w podrozdziale na temat administracji.
Powyższy skrypt dopełnia podstawowy system katalogu. Poniżej zostanie przedstawiona funk-
cjonalność koszyka na zakupy.
Powyższe wyrażenie oznacza, że tablica zawiera jeden egzemplarz książki o numerze ISBN
8371973918. Kiedy w koszyku pojawią się nowe produkty, zostaną one dodane również do tablicy.
Podczas przeglądu koszyka stosuje się tablicę $koszyk w celu przejrzenia pełnych danych pro-
duktów w bazie danych.
Rozdział 31. Tworzenie koszyka na zakupy 663
W celu kontroli informacji wyświetlanych w nagłówku, który przedstawia liczbę produktów i cał-
kowitą wartość, stosuje się dwie inne zmienne sesji. Zmienne te noszą odpowiednio nazwy:
$produkty i $calkowita_wartosc.
Aby w pełni zrozumieć działanie tego skryptu, w pierwszej kolejności przyjrzyjmy się stronie przed-
stawionej na rysunku 31.6.
Rysunek 31.6.
Skrypt pokaz_kosz.php
wywołany bez żadnych
parametrów pokazuje
zawartość koszyka
W tym przypadku łącze Pokaż koszyk zostało wybrane, kiedy koszyk był pusty, czyli nie zostały
wskazane żadne produkty.
Rysunek 31.7 przedstawia koszyk na bardziej zaawansowanym etapie zakupów, kiedy dokonano
już wyboru dwu książek. W tym przypadku strona została wywołana przez kliknięcie łącza Dodaj
do koszyka na stronie pokaz_ksiazke.php książki PHP4. Biblia. Po bliższym przyjrzeniu się paskowi
URL-a można dostrzec, że tym razem skrypt został wywołany z parametrem. Parametr nosi nazwę
nowy i posiada wartość 8371973918 — to znaczy numer ISBN książki, która została właśnie dodana
do koszyka.
Nasuwa się zatem wniosek, że istnieją dwie nowe opcje. Znajduje się tam przycisk Zapisz zmiany,
którego można użyć do zmiany liczby produktów w koszyku. Aby to uczynić, należy bezpośrednio
zmienić liczby i nacisnąć Zapisz zmiany. Jest to właściwie przycisk wysyłający dane — przenosi
on użytkownika z powrotem do skryptu pokaz_kosz.php w celu uaktualnienia koszyka.
Dodatkowo występuje przycisk Do kasy, który użytkownik może nacisnąć, kiedy jest gotowy do
opuszczenia sklepu. Kwestia tę opiszemy w dalszej części tego rozdziału.
Rysunek 31.7.
Skrypt pokaz_
kosz.php wywołany
z parametrem
nowy dodaje nowy
produkt do koszyka
@ $nowy = $_GET['nowy'];
if($nowy)
{
// wybrany nowy produkt
if(!isset($_SESSION['koszyk'])) {
$_SESSION['koszyk'] = array();
$_SESSION['produkty'] = 0;
$_SESSION['calkowita_wartosc'] ='0.00';
}
if(isset($_SESSION['koszyk'][$nowy])) {
$_SESSION['koszyk'][$nowy]++;
} else {
$_SESSION['koszyk'][$nowy] = 1;
}
$_SESSION['calkowita_wartosc'] = oblicz_wartosc($_SESSION['koszyk']);
$_SESSION['produkty'] = oblicz_produkty($_SESSION['koszyk']);
if(isset($_POST['zapisz'])) {
foreach ($_SESSION['koszyk'] as $isbn => $ilosc) {
if($_POST[$isbn] == '0') {
unset($_SESSION['koszyk'][$isbn]);
} else {
$_SESSION['koszyk'][$isbn] = $_POST[$isbn];
}
}
Rozdział 31. Tworzenie koszyka na zakupy 665
$_SESSION['calkowita_wartosc'] = oblicz_wartosc($_SESSION['koszyk']);
$_SESSION['produkty'] = oblicz_produkty($_SESSION['koszyk']);
}
tworz_naglowek_html("Koszyk na zakupy");
$cel = "indeks.php";
tworz_stopke_html();
?>
Powyższy skrypt posiada trzy główne części — wyświetlanie koszyka, dodawanie produktów do ko-
szyka oraz zapisywanie zmian w koszyku. Zostaną one opisane w następnych trzech podrozdziałach.
Podgląd koszyka
Niezależnie od strony, z której użytkownik został skierowany, ukazuje się zawartość jego koszyka.
W przypadku najprostszym, kiedy użytkownik zwyczajnie kliknął przycisk Pokaż koszyk, jest to
jedyna wykonywana część kodu:
if(($_SESSION['koszyk']) && (array_count_values($_SESSION['koszyk']))) {
wyswietl_koszyk($_SESSION['koszyk']);
} else {
echo "<p>Koszyk jest pusty</p><hr/>";
}
A zatem jeżeli koszyk zawiera jakieś produkty, zostanie wywołana funkcja wyswietl_koszyk().
Jeżeli koszyk jest pusty, użytkownik otrzyma odpowiednią wiadomość.
Funkcja ta nie wydaje się zagadnieniem bardzo skomplikowanym, lecz ponieważ wykonuje ona
dużą ilość pracy, warto ją dokładnie przeanalizować.
$_SESSION['koszyk'][$nowy] = 1;
}
W tym miejscu następuje sprawdzenie, czy produkt znajduje się już w koszyku. Jeżeli tak, jego
liczba w koszyku zostaje zwiększona o jeden. Jeśli nie, zostanie on dodany do koszyka.
Po trzecie, musi być obliczona całkowita wartość i liczba produktów w koszyku. W tym celu zasto-
sowane są funkcje oblicz_wartosc() i oblicz_produkty():
$_SESSION['calkowita_wartosc'] = oblicz_wartosc($_SESSION['koszyk']);
$_SESSION['produkty'] = oblicz_produkty($_SESSION['koszyk']);
Listing 31.11. Funkcja oblicz_wartosc() pochodząca z pliku funkcje_ksiazki.php — oblicza i zwraca całkowitą
wartość produktów umieszczonych w koszyku na zakupy
function oblicz_wartosc($koszyk) {
// obliczenie całkowitej wartości produktów w koszyku
$wartosc = 0.0;
if(is_array($koszyk)) {
$lacz = lacz_bd();
foreach($koszyk as $isbn => $ilosc) {
$zapytanie = "select cena from ksiazki where isbn='".
$lacz->real_escape_string($isbn)."'";
$wynik = $lacz->query($zapytanie);
if ($wynik) {
$produkt = $wynik->fetch_object();
$cena_produktu = $produkt->cena;
$wartosc +=$cena_produktu*$ilosc;
}
}
}
return $wartosc;
}
Listing 31.12. Funkcja oblicz_produkty() pochodząca z pliku funkcje_ksiazki.php — oblicza i zwraca całkowitą
liczbę produktów znajdujących się w koszyku na zakupy
function oblicz_produkty($koszyk) {
// obliczenie całkowitej ilości produktów w koszyku na zakupy
$produkty = 0;
if(is_array($koszyk)) {
foreach($koszyk as $isbn => $ilosc) {
$produkty += $ilosc;
}
}
return $produkty;
}
Jak można dostrzec, funkcja oblicz_wartosc() działa poprzez wyszukanie w bazie danych ceny
każdego produktu w koszyku. Jest to działanie dość powolne, tak więc aby uniknąć zbyt częstego
jego wykonywania, wartość (jak również całkowita liczba produktów) będzie przechowywana ja-
ko zmienna sesji i obliczana ponownie jedynie podczas zmiany zawartości koszyka.
Funkcja oblicz_produkty() jest prostsza — po prostu dodaje do siebie ilości każdego produktu
w koszyku i oblicza ich sumę przy użyciu funkcji array_sum(). Jeżeli żadna tablica jeszcze nie
istnieje (czyli gdy koszyk jest pusty), funkcja zwraca wartość 0.
Rozdział 31. Tworzenie koszyka na zakupy 669
$_SESSION['calkowita_wartosc'] = oblicz_wartosc($_SESSION['koszyk']);
$_SESSION['produkty'] = oblicz_produkty($_SESSION['koszyk']);
}
Można dostrzec, że analizowana jest zawartość koszyka na zakupy i dla każdego numeru ISBN
w koszyku sprawdzana jest zmienna POST o tej nazwie. Są to pola formy Zapisz zmiany.
Jeżeli któreś z tych pól będzie posiadać wartość zero, produkt ten zostanie usunięty z koszyka
za pomocą funkcji unset(). W innym przypadku koszyk jest uaktualniany tak, aby jego zawartość
pasowała do pól formularza:
if($_POST[$isbn]=='0') {
unset($_SESSION['koszyk'][$isbn]);
} else {
$_SESSION['koszyk'][$isbn] = $_POST[$isbn];
}
if(!$_SESSION['produkty']) {
$_SESSION['produkty'] = '0';
}
if(!$_SESSION['calkowita_wartosc']) {
$_SESSION['calkowita_wartosc'] = '0.00';
}
Pobyt w kasie
Kiedy użytkownik naciska przycisk Idź do kasy znajdujący się w koszyku na zakupy, zostaje
wywołany skrypt kasa.php. Strona kasy i strony następujące po niej powinny być dostępne przez
SSL (Secure Socjet Layer); owa przykładowa aplikacja nie wymaga jednak tego (szczegółowe
informacje na temat SSL są dostępne w rozdziale 15., „Tworzenie bezpiecznych aplikacji interne-
towych”).
Rysunek 31.8.
Skrypt kasa.php
pobiera szczegółowe
dane klienta
Skrypt ten wymaga od użytkownika podania adresu (oraz adresu dostawy, jeżeli jest on inny). Ten
stosunkowo prosty skrypt jest przedstawiony na listingu 31.13.
tworz_naglowek_html("Kasa");
tworz_stopke_html();
?>
Powyższy skrypt nie kryje wielkich niespodzianek. Jeżeli koszyk jest pusty, skrypt powiadomi
o tym użytkownika. W przeciwnym wypadku wyświetli formularz przedstawiony na rysunku 31.8.
Jeżeli użytkownik będzie kontynuował działanie poprzez naciśnięcie przycisku Kupuj na dole
formularza, zostanie przeniesiony do skryptu zakup.php. Wynik uruchomienia tego skryptu jest
przedstawiony na rysunku 31.9.
Rysunek 31.9.
Skrypt zakup.php
oblicza koszty dostawy
i całkowitą wartość
zamówienia
oraz pobiera
od klienta
szczegóły płatności
Kod skryptu zakup.php jest nieco bardziej skomplikowany niż kod skryptu kasa.php (zobacz li-
sting 31.14).
Listing 31.14. zakup.php — skrypt przechowuje detale zamówienia w bazie danych i pobiera szczegóły płatności
<?php
include ('funkcje_ksiazka_kz.php');
// koszyk na zakupy potrzebuje sesji, zostaje więc ona rozpoczęta
session_start();
tworz_naglowek_html("Kasa");
672 Część V Tworzenie praktycznych projektów PHP i MySQL
// jeżeli wypełniony
if(($_SESSION['koszyk']) && ($nazwisko) && ($adres) && ($miasto) && ($kod_poczt) && ($kraj)) {
// możliwe umieszczenie danych w bazie
if( umiesc_zamowienie($_POST) != false ) {
//wyświetl koszyk bez możliwości zmian i bez obrazków
wyswietl_koszyk($_SESSION['koszyk'], false, 0);
wyswietl_dostawe(oblicz_koszt_dostawy());
tworz_stopke_html();
?>
Logika powyższego skryptu jest prosta — sprawdza on, czy użytkownik wypełnił formularz
i ulokował szczegóły w bazie danych za pomocą funkcji umiesc_zamowienie(). Jest to prosta
funkcja, która umieszcza szczegółowe dane klienta w bazie danych. Jej kod jest przedstawiony
na listingu 31.15.
$lacz = lacz_bd();
$lacz->autocommit(FALSE);
$wynik = $lacz->query($zapytanie);
if($wynik->num_rows>0) {
$klient = $wynik->fetch_object();
$idklienta = $klient->idklienta;
} else {
$zapytanie = "insert into klienci values
(0, '" . $conn->real_escape_string($nazwisko) ."','" .
$conn->real_escape_string($adres) .
"','". $conn->real_escape_string($miasto) ."','" .
$conn->real_escape_string($wojew) .
"','". $conn->real_escape_string($kod_poczt) ."','" .
$conn->real_escape_string($kraj)."')";
$wynik = $lacz->query($zapytanie);
if (!$wynik) {
return false;
}
}
$idklienta = $lacz->insert_id;
$data = date("Y-m-d");
$wynik = $lacz->query($zapytanie);
if($wynik->num_rows>0) {
$zamowienie = $wynik->fetch_object();
$idzam = $zamowienie->idzamowienia;
} else {
return false;
}
// koniec transakcji
$lacz->commit();
$lacz->autocommit(TRUE);
return $idzam;
}
?>
Funkcja ta jest stosunkowo długa, ponieważ konieczne jest umieszczenie danych klienta, szczegółów
zamówienia oraz danych każdej książki, która została zakupiona.
Jest to jedyna część aplikacji, w której trzeba wykonywać transakcje. A jak udało nam się ich dotąd
unikać? Przeanalizujmy kod funkcji lacz_db():
function lacz_bd() {
$wynik = new mysqli('localhost', 'ksiazka_kz', 'haslo', 'ksiazka_kz');
if (!$wynik) {
return false;
}
$wynik->autocommit(TRUE);
return $wynik;
}
Rozdział 31. Tworzenie koszyka na zakupy 675
Oczywiście, funkcja ta różni się nieco od tej samej funkcji, którą wykorzystywaliśmy w poprzed-
nich rozdziałach. Po zestawieniu połączenia z serwerem MySQL powinno się włączyć tryb auto-
matycznego zatwierdzania transakcji autocommit. Zagwarantuje to, że każda instrukcja SQL zostanie
automatycznie zatwierdzona, jak już wcześniej wspominaliśmy. Jeżeli zachodzi potrzeba wyko-
nania transakcji złożonej z większej liczby instrukcji, tryb automatycznego zatwierdzania trzeba
wyłączyć, następnie należy wykonać serię instrukcji INSERT, zatwierdzić dane, po czym przywrócić
tryb automatycznego zatwierdzania.
Następnie obliczane są koszty dostawy na adres klienta i wartość ta zostaje wyświetlona za pomocą
następującego wiersza kodu:
wyswietl_dostawe(oblicz_koszt_dostawy());
Implementacja płatności
Kiedy klient wybierze przycisk Kupuj, szczegóły jego zakupu zostaną przetworzone za pomocą
skryptu przetworz.php. Wyniki zakończonej pomyślnie płatności są przedstawione na rysunku 31.10.
Rysunek 31.10.
Transakcja zakończyła
się sukcesem, produkty
zostaną teraz wysłane
do klienta
Listing 31.16. przetworz.php — skrypt przetwarza zamówienie klienta i wyświetla wynik tych działań
<?php
include ('funkcje_ksiazka_kz.php');
// koszyk na zakupy potrzebuje sesji, zostaje więc ona rozpoczęta
session_start();
tworz_naglowek_html('Kasa');
676 Część V Tworzenie praktycznych projektów PHP i MySQL
$typ_karty = $_POST['typ_karty'];
$numer_karty = $_POST['numer_karty'];
$miesiac_karty = $_POST['miesiac_karty'];
$rok_karty = $_POST['rok_karty'];
$nazwa_karty = $_POST['nazwa_karty'];
wyswietl_dostawe(oblicz_koszt_dostawy());
if(przetworz_karte($_POST)) {
// opróżnij koszyk
session_destroy();
echo "<p>Dziękujemy za dokonanie zakupów. Zamówienie zostało przyjęte.</p>";
wyswietl_przycisk("indeks.php", "kontynuacja", "Kontynuacja zakupów");
} else {
echo "<p>Dokonanie operacji niemożliwe. Proszę skontaktować się z wystawcą karty lub
spróbować ponownie.</p>";
wyswietl_przycisk("zakup.php", "powrot", "Powrót");
}
} else {
echo "<p>Nie zostały wypełnione wszystkie pola, proszę spróbować ponownie.</p><hr />";
wyswietl_przycisk("zakup.php", "powrot", "Powrót");
}
tworz_stopke_html();
?>
W skrypcie tym następuje przetworzenie danych karty kredytowej klienta, po czym, jeżeli działania
zakończą się sukcesem, jego sesja zostaje zniszczona.
Utworzona w tym miejscu funkcja przetwarzająca dane karty po prostu zwraca true. Gdyby
przetwarzanie karty miało zostać rzeczywiście zaimplementowane, funkcja ta musiałaby weryfi-
kować dane (sprawdzać, czy data ważności karty jest prawidłowa oraz czy numer karty ma prawi-
dłowy format), a następnie przetwarzać płatność.
Rysunek 31.11.
Użytkownicy muszą
przejść stronę logowania,
aby dotrzeć do funkcji
administratora
Rysunek 31.12.
Menu administratora
pozwala na uzyskanie
dostępu do funkcji
administracyjnych
$nazwa_uz = $_POST['nazwa_uz'];
$haslo = $_POST['haslo'];
if (loguj($nazwa_uz, $haslo)) {
// jeżeli w bazie danych, zgłoszenie identyfikatora użytkownika
$_SESSION['uzyt_admin'] = $nazwa_uz;
} else {
// niepomyślne logowanie
tworz_naglowek_html("Problem:");
echo "<p>Zalogowanie niemożliwe.<br />Należy być zalogowanym, aby przeglądać tę stronę.</p>";
tworz_html_url('logowanie.php', 'Logowanie');
tworz_stopke_html();
exit;
}
}
678 Część V Tworzenie praktycznych projektów PHP i MySQL
tworz_naglowek_html('Administracja');
if (sprawdz_uzyt_admin()) {
wyswietl_menu_admin();
} else {
echo "<p>Brak autoryzacji do wejścia na obszar administracyjny.</p>";
}
tworz_stopke_html();
?>
Kiedy administrator dotrze do tego miejsca, może zmienić swoje hasło lub wylogować się — kod
ten jest taki sam jak kod zamieszczony w poprzednich rozdziałach tej książki.
Administrator jest identyfikowany po zalogowaniu się poprzez zmienną sesji $uzyt_admin i funk-
cję sprawdz_uzyt_admin(). Funkcja ta i inne wykorzystywane przez skrypty administracyjne mogą
zostać odnalezione w bibliotece funkcji funkcje_admin.php.
Jeżeli administrator wybierze dodanie nowej kategorii lub książki, zostanie skierowany do pliku —
odpowiednio dodaj_kat_form.php lub dodaj_ksiazke_form.php. Każdy z tych skryptów przed-
stawia administratorowi formularz do wypełnienia. Każdy z nich jest przetwarzany przez odpo-
wiadający mu skrypt (dodaj_kat.php i dodaj_ksiazke.php), który sprawdza, czy formularz jest
wypełniony i umieszcza nowe dane w bazie danych. Ponieważ są one do siebie podobne, zostaną
tu opisane tylko wersje skryptów odpowiedzialne za książki.
Rysunek 31.13.
Ten formularz pozwala
administratorowi
na dodawanie nowych
książek do katalogu
online
Można dostrzec, że pole Kategoria dla książek jest elementem HTML SELECT. Opcje tego pola
pochodzą z wywołania funkcji pobierz_kategorie(), która została poprzednio opisana.
Kiedy zostaje wybrany przycisk Dodaj książkę, następuje aktywacja skryptu dodaj_ksiazke.php.
Kod tego skryptu jest przedstawiony na listingu 31.18.
Listing 31.18. dodaj_ksiazke.php — skrypt sprawdza prawidłowość danych nowej książki i umieszcza ją
w bazie danych
<?php
tworz_naglowek_html("Dodawanie książki");
if (sprawdz_uzyt_admin()) {
if (wypelniony($_POST)) {
$isbn = $_POST['isbn'];
$tytul = $_POST['tytul'];
$autor = $_POST['autor'];
$idkat = $_POST['idkat'];
$cena = $_POST['cena'];
$opis = $_POST['opis'];
tworz_stopke_html();
?>
Oprócz dodawania nowych kategorii i książek administrator może edytować i usuwać te elementy.
Funkcje te zostały zaimplementowane przy maksymalnym ponownym wykorzystaniu kodu. Kiedy
administrator wybierze łącze Strona główna w menu administracyjnym, zostanie przeniesiony do
pliku indeks.php i może poruszać się po witrynie w ten sam sposób, co zwykły użytkownik, za po-
mocą tych samych skryptów.
Istnieje jednak pewna różnica — administratorzy będą mieli do dyspozycji inne opcje, wynikające
z faktu, że posiadają zgłoszoną zmienną sesji $uzyt_admin. Jeżeli na przykład wejdzie on na opisaną
wcześniej stronę pokaz_ksiazke.php, ujrzy inne opcje menu (zobacz rysunek 31.14).
Administrator posiada dostęp do dwóch nowych opcji na tej stronie — Edycja produktu i Menu
administratora. Można również zauważyć, że w prawym górnym rogu nie jest wyświetlony koszyk
na zakupy — zamiast tego znajduje się tam przycisk Wylogowanie.
Kod wzmiankowanych nowości znajduje się także w tym pliku (zobacz listing 31.8).
if( sprawdz_uzyt_admin() ) {
wyswietl_przycisk("edycja_ksiazki_form.php?isbn=". urlencode($isbn), 'edycja-produktu',
'Edycja Produktu');
wyswietl_przycisk('admin.php', 'menu-admin', 'Menu administratora');
wyswietl_przycisk($cel, 'kontynuuj', 'Kontynuacja');
}
Jeżeli spojrzy się na skrypt pokaz_kosz.php, można zauważyć, że również te opcje zostały w niego
wbudowane.
680 Część V Tworzenie praktycznych projektów PHP i MySQL
Rysunek 31.14.
Skrypt pokaz_ksiazke.php
wyświetla inną zawartość
administratorowi
Jeżeli administrator kliknie przycisk Edycja produktu, zostanie przeniesiony do skryptu edycja_
ksiazki_form.php. Wynik uruchomienia tego skryptu jest przedstawiony na rysunku 31.15.
Rysunek 31.15.
Skrypt edycja_
ksiazki_form.php
umożliwia edycję
szczegółowych danych
książki oraz jej usunięcie
Jest to w istocie ten sam formularz, który został zastosowany do pobrania szczegółowych infor-
macji o książce. W formularz ten została wbudowana opcja przekazywania i wyświetlania danych
istniejącej książki. Podobnie wygląda kwestia formularza kategorii. Aby to zrozumieć, należy
przeanalizować listing 31.19.
<?php
if ($edycja)
// potrzebny jest stary isbn, aby znaleźć książkę w bazie danych
// jeżeli isbn jest uaktualniany
echo "<input type=\"hidden\" name=\"staryisbn\"
value=\"". htmlspecialchars($ksiazka['isbn'])."\" />";
?>
<input type="submit"
value="<?php echo $edycja?'Uaktualnienie':'Dodanie'; ?> książki" />
</form></td>
<?php
if ($edycja) {
echo "<td>
<form method=\"post\" action=\"usun_ksiazke.php\">
<input type=\"hidden\" name=\"isbn\"
value=\"". htmlspecialchars($ksiazka['isbn'])."\" />
<input type=\"submit\" value=\"Usuń książkę\" />
</form></td>";
}
?>
</td>
</tr>
</table>
</form>
<?php
}
Po przekazaniu funkcji tablicy zawierającej szczegółowe dane książki formularz zostanie wy-
świetlony w trybie edycji, a pola zostaną wypełnione danymi z tablicy:
<input type=text name=cena
value="<?php echo htmlspecialchars($edycja ? $ksiazka['cena'] : ''); ?>" />
Wyświetlone zostaną nawet różne przyciski wysyłające. W istocie formularz edycji wywołuje dwa
przyciski — jeden uaktualniający książkę i drugi ją usuwający. Wywołują one skrypty edycja_
ksiazki.php i usun_ksiazke.php, które odpowiednio uaktualniają bazę danych.
Wersje kategorii powyższych skryptów działają w bardzo podobny sposób poza jedną kwestią.
Jeżeli administrator próbuje usunąć kategorię, która zawiera jakieś książki, nie zostanie ona usu-
nięta. (Jest to sprawdzane przez zapytanie bazy danych). Pozwala to uniknąć problemów, które
może spowodować przypadkowe usunięcie — kwestia ta została omówiona w rozdziale 8. W tym
przypadku, jeżeli usuwana kategoria ciągle zawierałaby książki, stałyby się one „sierotami”.
Nie byłaby znana ich kategoria, tak więc nie istniałaby możliwość dotarcia do nich!
Rozwijanie projektu
Utworzyliśmy stosunkowo prosty system koszyka na zakupy. Istnieje wiele możliwych dodatków
i ulepszeń:
W rzeczywistym sklepie online konieczne jest utworzenie pewnego systemu śledzenia
i wypełniania zamówień — do tej pory nie wskazano metody przeglądania zamówień,
które zostały przyjęte.
Rozdział 31. Tworzenie koszyka na zakupy 683
Klienci powinni mieć możliwość sprawdzenia postępu swoich zamówień bez konieczności
kontaktu z administracją. Według autorów ważne jest, by klienci nie musieli logować się
w celu przeglądania strony. Jednak dostarczenie klientom metody uwierzytelniania daje
im możliwość obejrzenia poprzednich zamówień, a administratorom — metody połączenia
podobnych przyzwyczajeń w profile.
Obrazki książek muszą być wysyłane przez FTP do katalogu obrazki XXX, musi im zostać
także nadana odpowiednia nazwa. Aby to ułatwić, można dodać opcję wysyłania plików
do strony dodającej książkę.
Można dodać logowanie użytkowników, personalizację oraz rekomendacje książek, recenzje
online, programy stałych użytkowników, sprawdzanie zasobów magazynu itd. Możliwości
są nieograniczone.
684 Część V Tworzenie praktycznych projektów PHP i MySQL
Dodatki
686 Dodatki
Dodatek A Instalacja Apache, PHP i MySQL 687
Dodatek A
Instalacja Apache,
PHP i MySQL
Apache, PHP i MySQL dostępne są w wersjach przeznaczonych na różne platformy systemowe
i dla różnych serwerów WWW. W tym dodatku wyjaśnimy, w jaki sposób należy instalować Apache,
PHP i MySQL w systemie Unix (prawie) od zera, jak również zamieścimy wskazówki dotyczące
instalacji tych technologii w systemach Windows oraz Mac OS X.
Celem tego dodatku jest zaprezentowanie pełnego procesu instalacji serwera WWW, umożliwia-
jącego udostępnianie wielu witryn internetowych. Niektóre z nich będą wymagać protokołu SSL
do prowadzenia za ich pośrednictwem działalności komercyjnej, a niemal wszystkie będą za-
wierały skrypty realizujące połączenie z serwerem baz danych oraz wyszukujące i przetwarzające
dane. Właśnie z tego względu w niniejszym dodatku zamieścimy informacje o przygotowaniu
standardowej konfiguracji PHP, MySQL oraz serwera Apache na komputerze działającym pod
kontrolą systemu operacyjnego Unix.
Wielu użytkowników PHP nigdy nie staje przed koniecznością instalowania PHP na komputerze,
dlatego też zagadnienia te umieszczone zostały w dodatku, a nie w rozdziale 1. Najłatwiejszym
sposobem uzyskania dostępu do dobrego serwera posiadającego szybkie połączenie z internetem
i mającego zainstalowane PHP jest założenie konta na jednym z tysięcy serwisów oferujących usługi
hostingowe lub u sprzedawców tego typu usług działających na całym świecie. Jeśli ktoś chce
skorzystać z tej metody, to powinien się upewnić, że wybrana firma hostingowa udostępnia aktualne
wersje Apache, PHP i MySQL, gdyż w przeciwnym razie mogą wystąpić problemy z bezpieczeń-
stwem, które trudno będzie rozwiązać (nie wspominając nawet o tym, że nie będzie można korzy-
stać z najnowszych i najlepszych możliwości poszczególnych technologii).
688 Dodatki
Zależnie od powodów, dla których PHP ma zostać zainstalowane na komputerze, różne będą też
podejmowane decyzje. Na przykład jeżeli komputer przez cały czas utrzymuje połączenie z siecią
i będzie miał odgrywać rolę serwera z prawdziwego zdarzenia, to należy zastanowić się nad kwe-
stią jego wydajności. Jeśli tworzony jest serwer dla celów programistycznych, na którym będą
się odbywać testy kodu, to najważniejszą rzeczą będzie zapewnienie zgodności jego konfiguracji
z konfiguracją prawdziwego serwera.
Interpreter PHP może być uruchomiony jako moduł serwera lub jako CGI. Zazwyczaj używa się
go jako modułu ze względu na większą wydajność. Z drugiej strony wersja CGI jest czasami
stosowana na serwerach, na których wersja w postaci modułu jest niedostępna, lub
dlatego, że pozwala ona użytkownikom serwera Apache na uruchamianie różnych stron
PHP jako użytkownikom o różnych identyfikatorach.
W tym dodatku zostanie przedstawiona metoda instalacji PHP jako modułu.
Choć instalacja z kodów źródłowych wymaga dodatkowego czasu na ich pobranie, zainstalowanie
i skonfigurowanie, co na początku może odstraszyć wiele osób, to jednak taki sposób daje pełną
kontrolę nad konfiguracją przygotowywanego środowiska. Stosując ten sposób instalacji, mamy
pełną kontrolę nad tym, co zostanie zainstalowane, jakie wersje technologii będą użyte oraz jakie
dyrektywy konfiguracyjne zostaną wykorzystane.
Jedną z wad instalacji przy użyciu binariów jest to, że rzadko zawierają one najnowsze wersje
programu. Zależnie od tego, jak ważne były ostatnio opublikowane poprawki, nie musi to stanowić
aż tak dużego problemu. Niemniej jednak jeśli ktoś planuje skorzystać z tych prekonfigurowanych,
binarnych dystrybucji PHP, MySQL, Apache oraz bibliotek dodatkowych, to sugerujemy, by naj-
pierw zaktualizować je przy użyciu metody aktualizacji typowej dla zainstalowanej wersji systemu
operacyjnego Linux (na przykład apt-get, yum lub innego menedżera pakietów).
Największą wadą instalacji z użyciem binariów jest to, że nie ma możliwości wyboru opcji zasto-
sowanych podczas ich kompilacji. Najbardziej elastycznym i niezawodnym sposobem jest skompi-
lowanie wszystkich programów z kodów źródłowych. Zastosowanie tego rozwiązania zajmuje
nieco więcej czasu niż instalacja przy użyciu menedżera pakietów, dlatego doskonale rozumiemy
osoby, które korzystają z możliwości instalacji pakietów binarnych, jeśli tylko jest ona dostępna;
poza tym jeżeli wszystkim, czego potrzebujemy, jest podstawowa konfiguracja, to istnieje całkiem
spore prawdopodobieństwo, że prekompilowane pakiety binarne w zupełności nam wystarczą.
Dodatek A Instalacja Apache, PHP i MySQL 689
Dla celów tej książki zainstalujemy standardową wersję PHP, oprócz tego jednak zostanie poka-
zane, w jaki sposób można udostępniać dodatkową bibliotekę PHP o nazwie gd2.
Biblioteka gd2 to tylko jedna z wielu bibliotek przeznaczonych dla PHP. Zostanie ona dołączona
w celu pokazania ogólnej zasady kompilacji z kodów źródłowych oraz udostępniania dodatko-
wych bibliotek PHP. Proces kompilacji większości programów przeznaczonych dla systemów
Unix przebiega w podobny sposób, jednak jak pokażemy w dalszej części tego dodatku, mogą się
zdarzyć sytuacje, w których łatwiejszym rozwiązaniem będzie zainstalowanie gotowych, pre-
kompilowanych pakietów.
Zaprezentowany proces instalacji odnosi się do serwera Ubuntu, ale opis wykonywanych czynności
jest na tyle ogólny, że można go zastosować na dowolnym uniksowym serwerze.
Jeśli konieczne będzie używanie funkcji mail(), trzeba będzie zainstalować agenta transferu poczty
elektronicznej (mail transfer agent — MTA), lecz tego zagadnienia nie będziemy poruszać.
Zakładamy, że Czytelnik ma dostęp do serwera jako użytkownik root oraz zainstalował wcześniej
następujące narzędzia:
gzip lub gunzip,
gcc oraz make GNU.
Pierwszym etapem instalacji będzie pobranie wszystkich plików .tar z kodami źródłowymi do kata-
logu tymczasowego. Należy się upewnić, że znajduje się w nim wystarczająco dużo miejsca. Wybrali-
śmy katalog /usr/src na katalog tymczasowy. Wszystkie pliki najlepiej pobrać jako root, by uniknąć
ewentualnych problemów z dostępem do nich w przyszłości.
690 Dodatki
Instalowanie MySQL
W tym popunkcie pokażemy, jak należy przeprowadzić binarną instalację MySQL. Instalowanie
oficjalnej binarnej dystrybucji MySQL w rzeczywistości jest zalecanym rozwiązaniem, zwłaszcza
wziąwszy pod uwagę niewielką liczbę opcji konfiguracyjnych, których prawdopodobnie Czytelnik
może potrzebować, oraz liczbę kroków procesu konfiguracyjnego, które od lat nastręczają pro-
blemów. Choć absolutnie nic nie stoi na przeszkodzie, by pobrać pliki źródłowe i zbudować na
ich podstawie serwer MySQL, a instrukcje opisujące ten proces są całkiem dobre, to jednak instalu-
jąc pliki binarne przeznaczone dla konkretnego systemu, uzyskamy ten sam efekt znacznie
szybciej, i to praktycznie bez żadnych różnic w możliwościach funkcjonalnych serwera.
W tym przykładzie użyjemy repozytorium apt dla systemu Ubuntu 16.04, niemniej jednak
można pobrać repozytorium binarne odpowiednie dla stosowanego systemu, dostępne na stronie
http://www.mysql.com/downloads/. Ten rodzaj instalacji automatycznie umieszcza pliki w różnych
miejscach i obsługuje konfigurację serwera MySQL.
Rysunek A.1.
Opcje konfiguracyjne
MySQL
W przedstawionym oknie należy wybrać opcję Server, a w kolejnym kroku — wersję serwera. Na-
stępnie należy kliknąć OK, by zapisać konfigurację. W tym momencie system będzie gotowy
do wykonania polecenia apt w celu zainstalowania dokładnie takiej aplikacji, jakiej sobie życzyli-
śmy; można więc wydać następujące polecenie:
# sudo apt-get install
Podczas instalowania serwera powinno zostać wyświetlone okno z prośbą o ustawienie hasła użyt-
kownika root bazy danych, takie jak to przedstawione na rysunku A.2.
Rysunek A.2.
Prośba o podanie
hasła użytkownika
root serwera MySQL
Po podaniu hasła zostaniemy poproszeni o jego powtórzenie, a gdy to zrobimy, proces instalacji
będzie kontynuowany.
W trakcie instalacji MySQL automatycznie tworzone są trzy bazy danych1. Jedna z nich to mysql,
w której zapisane są dane na temat użytkowników, komputerów i przywilejów na serwerze. Można
to sprawdzić, wykonując następujące polecenie w wierszu poleceń:
# mysql –u root –p
Enter password:
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0,00 sec)
1
Począwszy od wersji MySQL 5.7.7, tworzona jest jeszcze jedna baza danych, o nazwie sys. Zawiera ona
obiekty ułatwiające programistom oraz administratorom interpretowanie wyników gromadzonych w bazie
danych performance_schema — przyp. tłum.
692 Dodatki
Teraz będziemy już posiadać działający, zabezpieczony serwer MySQL, którego będziemy mogli
używać wraz z PHP.
Ta druga flaga konfiguracyjna została użyta po to, by mod_so został wkompilowany w serwer
Apache. Moduł ten, którego nazwa odwołuje się do uniksowego formatu wspólnych obiektów
(nag. shared objects; *.so), pozwala na stosowanie w Apache dynamicznych modułów, takich
jak PHP.
Po wykonaniu ostatniego polecenia, jeśli w trakcie procesu nie wystąpiły żadne błędy, biblioteka
JPEG powinna już być zainstalowana w katalogu /usr/local/lib. Jeżeli jednak w trakcie procesu bu-
dowania i instalacji biblioteki wystąpiły jakieś błędy, to trzeba będzie zajrzeć do jej dokumentacji.
Taki sam proces, na który składa się wykonanie poleceń gunzip, tar, configure, make i make install,
należy powtórzyć w celu zbudowania z plików źródłowych dwóch kolejnych bibliotek: PNG
oraz zlib.
Jeśli chodzi o instalowanie biblioteki IMAP-C, to także ją można pobrać i zbudować z kodów
źródłowych, niemniej jednak proces ten może się okazać problematyczny ze względu na „dziwactwa”
tej biblioteki i różnice, jakie mogą występować podczas jej budowania w różnych systemach opera-
cyjnych. Dlatego też zalecamy, by postępować zgodnie z najnowszymi wytycznymi, opublikowa-
nymi w dokumentacji PHP dostępnej na stronie http://php.net/manual/en/imap.requirements.php.
Kiedy wszystkie biblioteki będą już przygotowane, można zająć się budowaniem samego PHP.
W pierwszej kolejności trzeba rozpakować kody źródłowe do wybranego katalogu, a następnie
przejść do niego:
# cd /usr/src
# sudo gunzip php-WERSJA.tar.gz
# sudo tar -xvf php-WERSJA.tar
# cd php-WERSJA
Istnieje bardzo wiele opcji, które można zastosować w poleceniu configure, przygotowującym
PHP do kompilacji i zbudowania. Aby je wyświetlić i zdecydować, które warto wykorzystać,
należy wykonać polecenie ./configure --help. W naszym przykładzie zdecydowaliśmy się na
dodanie obsługi MySQL, Apache oraz gd2.
Trzeba zwrócić uwagę na to, że poniższe polecenie stanowi jedną całość. Można je zapisać w jed-
nym wierszu bądź też, jak pokazano poniżej, można skorzystać ze znaku kontynuacji (\), który
pozwala na wpisywanie jednego polecenia w kilku wierszach, co bardzo poprawia jego czytelność:
#./configure --prefix=/usr/local/php
--with-mysqli=mysqlnd
--with-apxs2=/usr/local/apache2/bin/apxs
--with-jpeg-dir=/usr/local/lib
--with-png-dir=/usr/local/lib
--with-zlib-dir=/usr/local/lib
--with-imap=/usr/lib
--with-kerberos
--with-imap-ssl
--with-gd
694 Dodatki
Pierwsza flaga konfiguracyjna określa położenie instalacyjnego katalogu PHP; w tym przypadku
jest to /usr/local/php. Druga zapewnia, że środowisko PHP zostanie zbudowane z wykorzystaniem
natywnych sterowników MySQL. Trzecia flaga informuje PHP o położeniu apxs2 (narzędzia
Apache Extensions — rozszerzenia serwera Apache), używanego do budowy modułów serwera
Apache, do których zalicza się także PHP. Określenie położenia apxs2 w ramach procesu budowy
PHP wynika z konieczności przygotowania odpowiedniej wersji modułu PHP, a do tego przed
rozpoczęciem budowania PHP konieczne jest wstępne skonfigurowanie i zainstalowanie przynajm-
niej podstawowej wersji serwera Apache.
Będzie to oznaczać, że system jest gotowy do zbudowania PHP oraz zainstalowania plików binar-
nych; musimy zatem dokończyć proces, wydając dwa poniższe polecenia:
# make
# make install
Po tym ostatnim kroku, jeśli w trakcie procesu nie wystąpiły żadne błędy, plik binarny PHP zostanie
zbudowany i zainstalowany, podobnie jak moduł PHP dla serwera Apache, który także zostanie
zbudowany, zainstalowany i umieszczony w odpowiednim miejscu struktury katalogów serwera
Apache. Ostatnią czynnością konfiguracyjną, którą można wykonać, jest upewnienie się, że plik
php.ini zostanie umieszczony w stabilnym i standardowym miejscu:
# sudo cp php.ini-development /usr/local/php/lib/php.ini
Dodatek A Instalacja Apache, PHP i MySQL 695
ewentualnie:
# sudo cp php.ini-production /usr/local/php/lib/php.ini
Obie wersje pliku php.ini wymienione w powyższych poleceniach zawierają odmienne zestawy
opcji konfiguracyjnych. Pierwsza z nich, php.ini-development, jest przeznaczona dla komputerów
używanych przez programistów do tworzenia aplikacji. Na przykład w tym pliku dyrektywie
display_errors przypisywana jest wartość On. Choć ustawienie to ułatwia pisanie i testowanie
kodu, to jednak nie nadaje się raczej do użycia na komputerach produkcyjnych. Wszystkie odwoła-
nia do domyślnych ustawień php.ini w niniejszej książce odnoszą się do ustawień podanych wła-
śnie w tym pliku. Druga wersja pliku, o nazwie php.ini-production, zawiera ustawienia przezna-
czone do użycia na serwerze produkcyjnym.
Plik php.ini można edytować, by zmienić zapisane w nim ustawienia konfiguracyjne. Istnieje wiele
opcji godnych uwagi, jednak kilka z nich jest szczególnie istotnych. Na przykład aby móc wysy-
łać wiadomości e-mail, trzeba będzie podać w opcji sendmail_path ścieżkę do programu sendmail.
Jeśli wszystko zostanie wykonane prawidłowo i nie wystąpią żadne błędy, to po zakończeniu
budowy serwera Apache wrócimy do wiersza poleceń i będziemy gotowi do wykonania finalnych
zmian w konfiguracji środowiska. Jeżeli jednak w trakcie procesu instalacji wystąpią jakieś błędy,
to trzeba będzie zajrzeć do dokumentacji serwera Apache HTTP Server 2.4 (http://httpd.apache.
org/docs/2.4/) i tam poszukać informacji o napotkanych anomaliach.
Teraz będzie już można uruchomić serwer i sprawdzić, czy PHP działa. W pierwszej kolejności
uruchomimy serwer bez obsługi protokołu SSL i sprawdzimy, czy w ogóle działa. W następnym
punkcie rozdziału zajmiemy się sprawdzeniem obsługi PHP oraz SSL.
Do sprawdzenia tego, czy konfiguracja serwera Apache jest prawidłowa, można użyć programu
configtest:
# cd /usr/local/apache2/bin
# sudo ./apachectl configtest
Syntax OK
# sudo ./apachectl start
./apachectl start: httpd started
Jeśli konfiguracja jest prawidłowa, to po odwołaniu się do serwera w przeglądarce WWW powin-
niśmy uzyskać rezultat podobny do tego przedstawionego na rysunku A.3.
Rysunek A.3.
Domyślna strona testowa
wyświetlana przez serwer
Apache
Można nawiązać połączenie z serwerem, podając nazwę domeny lub adres IP komputera.
Należy sprawdzić oba przypadki, aby upewnić się, że wszystko działa poprawnie.
W efekcie wywołania pliku test.php przeglądarka powinna wyświetlić stronę podobną do tej przed-
stawionej na rysunku A.4.
Dodatek A Instalacja Apache, PHP i MySQL 697
Rysunek A.4.
Funkcja phpinfo()
powoduje wyświetlenie
informacji dotyczących
konfiguracji
Aby stworzyć samodzielnie podpisany certyfikat, należy wykonać umieszczone poniżej polecenie,
które utworzy sam certyfikat i odpowiedni klucz i umieści je w miejscu oczekiwanym przez serwer
Apache:
# sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /usr/local/apache2/conf/server.key \
-out /usr/local/apache2/conf/server.crt
Istnieje odrębny plik konfiguracyjny zawierający ustawienia związane z SSL; jest to plik
httpd-ssl.conf, umieszczony w katalogu /usr/local/apache2/conf/. Na razie można pozostawić
go w pierwotnej postaci i ponownie uruchomić serwer Apache, aby zostały uwzględnione
wprowadzone modyfikacje. Można także kontynuować lekturę, by go zmodyfikować, a tym samym
698 Dodatki
W przypadku serwera Apache 2.4, aby włączyć obsługę SSL, wystarczy jedynie usunąć znak
komentarza umieszczony w pliku httpd.conf, na początku reguły dołączającej plik httpd-ssl.conf.
A zatem wiersz, który początkowo ma następującą zawartość:
# Include conf/extra/httpd-ssl.conf
Działanie serwera można sprawdzić, odwołując się do niego w przeglądarce z wykorzystaniem pro-
tokołu https:
https://nazwaserwera.nazwadomeny.com.pl
albo:
https://xxx.xxx.xxx.xxx:443
Instalacja Apache,
PHP i MySQL w systemie Windows
Dysponując odpowiednimi narzędziami programistycznymi PHP, Apache oraz MySQL można
zainstalować z plików źródłowych także w systemie Windows i Mac OS X. Można też zainstalo-
wać PHP, Apache oraz MySQL osobno, postępując zgodnie z instrukcjami dostępnymi w doku-
mentacji PHP zamieszczonej na stronie http://php.net/manual/en/install.php. Niemniej jednak
aby szybko i sprawnie uruchomić całe środowisko na komputerze przeznaczonym do pisania apli-
kacji, można także skorzystać z jednego z kilku dostępnych pakietów instalacyjnych oferujących
całościowe rozwiązanie.
Rysunek A.5.
W przypadku
używania samodzielnie
podpisanych certyfikatów
przeglądarka poprosi
o dodanie wyjątku
oznaczającego,
że ufamy danej witrynie
Istnieją także dwa inne dobre pakiety tego typu, które również zawierają Apache, MySQL i PHP;
oto one:
WAMP — stanowi instalację Apache, MySQL oraz PHP przeznaczoną dla systemów
Windows; dostępny na stronie http://www.wampserver.com/.
MAMP — stanowi instalację Apache, MySQL oraz PHP przeznaczoną dla komputerów
Mac; dostępny na stronie http://www.mamp.info/.
Aby zainstalować któryś z tych pakietów na komputerze z systemem Windows lub Mac OS X, należy
postępować zgodnie instrukcjami opublikowanymi przez twórców danego pakietu. Jest całkiem praw-
dopodobne, że cały proces sprowadzi się do jednego kliknięcia bądź ewentualnie przejścia krótkie-
go kreatora instalacyjnego, który przeprowadzi nas przez wszystkie etapy instalacji i konfiguracji.
Plik ten powinien zostać zapisany w katalogu główny dokumentów udostępnianych przez ser-
wer (zwykle c:\Program Files\Apache Software Foundation\Apache2.4\htdocs). Należy wywołać
go w przeglądarce, wpisując:
http://localhost/test.php
lub
http://adres_IP/test.php
Jeśli w przeglądarce zostanie wyświetlona strona podobna do przedstawionej na rysunku A.2, będzie
to oznaczało, że serwer prawidłowo obsługuje PHP.
700 Dodatki
Instalowanie PEAR
PEAR, PHP Extension And Application Repository2, jest systemem rozpowszechniania pisanych
w PHP komponentów wielokrotnego użytku, tworzonych przez społeczność programistów używają-
cych tego języka. Obecnie jest w nim dostępnych ponad 200 pakietów przeznaczonych dla języka
PHP 5 i nowszych, które rozszerzają możliwości funkcjonalne podstawowej instalacji PHP.
Aby korzystać z PEAR, trzeba posiadać odpowiedni pakiet instalacyjny. W przypadku opisanego
wcześniej sposobu instalacji PHP w systemie Unix pakiet ten będzie już dostępny, a żeby zainstalo-
wać go w systemie Windows, trzeba będzie wyświetlić okno wiersza poleceń, a następnie wpisać
w nim polecenie: c:\php\go-pear.
Skrypt go-pear zada kilka prostych pytań na temat tego, gdzie ma zostać zainstalowany instalator
pakietu oraz standardowe klasy PEAR, a następnie sam je pobierze i zainstaluje. W tym momencie
powinien już być zainstalowany instalator pakietu PEAR oraz podstawowe biblioteki PEAR.
Można teraz przystąpić do instalacji poszczególnych pakietów, wpisując
pear install pakiet
gdzie pakiet powinien być zastąpiony przez nazwę pakietu, który ma zostać zainstalowany.
Listę dostępnych pakietów otrzymamy, wpisując
pear list-all
Aby sprawdzić, czy dostępna jest nowsza wersja któregoś z zainstalowanych pakietów, należy wpisać
pear upgrade nazwa_pakietu
Jeżeli przedstawiona procedura z jakiegoś powodu nie zadziała, proponujemy pobrać pakiety
PEAR bezpośrednio. Wszystkie te pakiety można znaleźć w witrynie http://pear.php.net/
packages.php. Można je tam przeglądać tak długo, aż znajdziemy ten, który nas interesuje.
Wybrany pakiet można następnie pobrać i ręcznie umieścić w katalogu PEAR na komputerze —
jest to zazwyczaj podkatalog pear katalogu instalacyjnego PHP.
Alternatywne konfiguracje, obejmujące inne serwery działające w systemach Unix, zostały opisane
w dokumentacji PHP zamieszczonej na stronie http://php.net/manual/en/install.unix.php.
2
Repozytorium rozszerzeń i aplikacji PHP — przyp. tłum.
Skorowidz
A blokada
usługi, 333
agregowanie danych, 264 pliku, 87
AJAX, 477, 487 błędy, 203, 352, 523
aliasy, 262 logiczne, 529
analiza ustawień, 355 programistyczne, 523
Apache, 373, 687 składni, 523
Apache HTTP Server, 355 w zmiennych, 530
API Reflection, 197 wykonania, 524
aplikacje WWW, 510
architektura internetowej bazy danych, 224
asercje, 138 C
asynchroniczne żądania, 477
atak cechy, 180
DDoS, 361 cookie, 464
CSS, Cascading Style Sheets, 519
DoS, 361
czcionki, 448
atomowe wartości kolumn, 221
atomowość, 315
atrybuty, 170 D
klasy, 172
kolumn, 240 dane wrażliwe, 329
automatyczne generowanie obrazków, 447 data i czas, 415
autoryzacja, 635 DDL, Data Definition Language, 254
OAuth, 642 definiowanie
awaria, 362 użytkowników, 230
własnych funkcji, 154
dekrementacja, 51
B destruktory, 171
DMZ, demilitarized zone, 360
baza danych, 88, 215 dodawanie
MySQL, 253 zakładek, 561
bezpieczeństwo, 327 zawartości dynamicznej, 39
aplikacji, 329 dokumentacja projektów, 517
komputerów, 361 dołączanie kodu, 145
serwera bazy danych, 357 domknięcia, 165
serwera WWW, 354 dopasowanie łańcuchów znaków, 129, 452
sieci, 359 dostawcy usług, 574
systemów operacyjnych, 361 dostęp
aplikacji, 341 do danych żądania, 580
biblioteka do elementów tablicy, 95
GD, 442 do serwera, 336
jQuery, 478, 492 do tablic, 94
do zawartości tablicy, 93
do zmiennych formularza, 41
702 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty
metody
jQuery, 490
O
statyczne, 190 OAuth, 635
mikrosekundy, 426 obiekty, 168
modele, 584 obliczanie
modyfikacje danych, 332 dat, 424
modyfikator sum, 56
private, 173 obrazki, 441
protected, 173 obsługa
public, 173 błędów i wyjątków, 203
monitor MySQL, 228 obrazków, 441
monitorowanie bezpieczeństwa, 343 żądań, 574
MySQL, 23, 213 żądań AJAX, 492
agregowanie danych, 264 ochrona bazy danych, 298
identyfikatory, 244 ODBC, 286
kod źródłowy, 28 odczyt
kopia zapasowa, 309 z katalogów, 388
koszt, 27 z pliku, 81
obliczanie dat, 425 odczytywanie zakładek, 561
ochrona, 298 odstępy, 38
optymalizacja, 308 odwołania wsteczne, 138
procedury składowane, 318 ograniczanie ryzyka, 334
przenośność, 27 opakowywanie funkcji IMAP, 599
przywileje, 233 opcja
przywracanie bazy, 310 auto_append_file, 150
replikacja, 310 auto_prepend_file, 150
składowania danych, 314 opcje konfiguracyjne sesji, 470
struktury sterujące, 321 operacje, 170
szeregowanie danych, 264 CRUD, 588
tworzenie bazy danych, 227 operator, 48
typy połączeń, 263 równości, 52
usuwanie rekordów, 271 tłumienia błędów, 54
usuwanie tabel, 272 trójkowy, 54
wsparcie, 28 typu, 55
wstawianie danych, 283 wykonania, 54
wydajność, 27 wykonawczy, 352
operatory
wyszukiwanie danych, 256, 259, 261
arytmetyczne, 48
wyzwalacze, 324
bitowe, 53
zaawansowane programowanie, 313
łańcuchowe, 49
zapisywanie danych, 254
podzapytań, 268
zmiana rekordów, 269
porównania, 258
zmiana struktury tabel, 269
MySQL 5.x, 28 porównań, 52
przypisania, 49
referencji, 51
N tablicowe, 55, 96
optymalizacja
nazwy kodu, 519, 520
funkcji, 154, 155 bazy danych, 308
tabel, 262 organizacja kodu źródłowego, 349
osadzanie usług, 400
otwieranie pliku, 73
oznaczanie zdjęć, 646
706 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty
T
U
tabela, 216, 238
columns_priv, 296 uaktualnianie
db, 295 oprogramowania, 354
procs_priv, 296 systemu operacyjnego, 361
tables_priv, 296 udostępnianie usług, 362
user, 293 uprawnienia, 357
tablice, 91 usługa uwierzytelniająca, 635
indeksowane numerycznie, 92 ustawianie typów zmiennych, 59
wielowymiarowe, 97, 102 usuwanie
z innymi indeksami, 94 błędów, 523
zmiana kolejności elementów, 105 baz danych, 272
technologia AJAX, 477 katalogów, 391
tekst przycisku, 455 rekordów, 271
testowanie, 520 tabel, 272
transakcje, 315 wiadomości, 629
transfer danych początkowych, 311 zakładek, 564
transmisja danych, 331 utrata danych, 332
trasy, 576 uwierzytelnianie, 365, 371, 470
trwałość, 316 aplikacji, 617
tworzenie użytkowników, 539, 546
egzemplarzy, 171 użytkownicy, 357
funkcji, 143 używanie bazy danych, 238
Skorowidz 709
W Z
wartości, 217 zabezpieczanie
wcinanie, 514 haseł, 369
wdrażanie nowych wersji, 354 kodu źródłowego, 343
widok, 575, 581 strony, 370
administratora systemu, 652 zakładki, 540
użytkownika systemu, 651 dodawanie, 561
widoki Blade, 582 implementacja przechowywania, 561
wielokrotne dziedziczenie, 179 odczytywanie, 561
wiersz poleceń, 505 usuwanie, 564
wiersze, 216 wyświetlanie, 563
właściciel
zalety
skryptu, 502
MySQL, 27
pliku, 394
PHP, 24
wskazywanie typu, 190
zamiana
współużytkowane serwery hostingowe, 356
wstrzykiwanie kodu, 335 fragmentów łańcuchów, 141
wybór łańcuchów znaków, 129
bazy danych, 279 zamykanie
środowiska programistycznego, 517 pliku, 79
wyjątki, 203 połączenia, 413
użytkownika, 206 połączenia z bazą, 282
wykresy, 455 zapisywanie danych, 254
wylogowanie, 554 zapory sieciowe, 360
wyrażenia regularne, 132 zapytania do bazy danych, 276, 279
asercje, 138 zarządzanie
dopasowywanie znaków specjalnych, 136 datą i czasem, 415
klasy znaków, 133 zmiennymi, 59
kotwiczenie, 135 zasada
metaznaki, 136 działania sesji, 463
odnajdywanie fragmentów łańcuchów, 140 najmniejszego przywileju, 231
zasięg zmiennych, 47, 158
odwołania wsteczne, 138
zastosowanie
ograniczniki, 132
PHP, 34
podwyrażenia, 135
znaczników PHP, 37
podwyrażenia policzalne, 135 zawartość kodu źródłowego, 350
powtarzalność, 134 zbiory, 133
rozdzielanie łańcuchów, 141 wynikowe selektorów, 481
rozgałęzianie, 135 znaków, 430
sekwencje specjalne, 137 zgłaszanie błędów, 532, 534
style składni, 132 zintegrowane środowisko programistyczne, 517
zamiana fragmentów łańcuchów, 141 zmiana
wysyłanie hasła, 555
plików, 379, 380, 385, 413 przywilejów, 298
wiadomości, 629 struktury tabel, 269
wyszukiwanie danych, 71, 256, 259, 261 ustawień zgłaszania błędów, 534
wyświetlanie wielkości liter, 124
grafiki, 446 właściwości pliku, 394
strumienia, 643 zmienne, 43
zakładek, 563 formularza, 41
wywoływanie lokalne, 321
funkcji, 40, 151 sesyjne, 466
funkcji niezdefiniowanej, 153 skalarne, 112
operacji klas, 172 zmiennych, 46
wyzwalacze, 324 znacznik czasu, 420
wyzwalanie własnych błędów, 535 Uniksa, 417
wzorzec MVC Laravela, 574 znaczniki PHP, 37