PHP/Klasy i obiekty
Z Wikibooks, biblioteki wolnych podręczników.
Spis treści |
[edytuj] Klasy i obiekty
Podstawy programowania obiektowego poznaliśmy już w rozdziale "Wstęp do programowania obiektowego", ponieważ potrzebowaliśmy ich do prawidłowego korzystania z PDO oraz systemów szablonów. Teraz nauczymy się o nim wszystkiego, co tylko jest możliwe i pokażemy, jak prawidłowo wykorzystywać je w codziennej praktyce programistycznej.
Przypomnijmy, czego nauczyliśmy się do tej pory:
- Klasa jest elementem opisowym. Informuje PHP, jakie dane mogą przechowywać obiekty jej rodzaju oraz jakie operacje mogą wykonywać.
- Obiekt jest fizycznym, namacalnym bytem, przechowującym konkretne informacje i mogącym wykonywać na nich różne operacje. Każdy obiekt pochodzi od jakiejś klasy, która te operacje dokładnie definiuje.
- Elementami do przechowywania danych w obiektach są pola.
- Metody, lub inaczej funkcje wewnętrzne, opisują możliwe operacje do wykonania na polach obiektu.
- Obiekt konkretnej klasy tworzymy poleceniem new konkretnaKlasa;, np. $obiekt = new konkretnaKlasa;
- Do pól oraz metod obiektu odwołujemy się "operatorem" ->, np. $obiekt -> pole, $obiekt -> metoda().
- W PHP5 obiekt nie jest równoznaczny ze zmienną. Zmienna obiektowa przechowuje jedynie odnośnik do konkretnego obiektu, stąd wyrażenie $obiekt2 = $obiekt1; nie utworzy nam kopii obiektu!
- W PHP4 obiekt był jednocześnie zmienną obiektową.
[edytuj] Tworzenie klas
Do tej pory nastawieni byliśmy na korzystanie z gotowych obiektów, natomiast tworzenie własnych było potraktowane powierzchownie. Teraz skupimy się właśnie na tym elemencie.
Klasę deklarujemy słowem kluczowym class, po którym podajemy jej unikalną nazwę. W nawiasach klamrowych umieszczamy informacje o dozwolonych polach klasy oraz metodach, jakie będzie ona posiadać.
<?php
class osoba
{
public $imie;
public $nazwisko;
public function ustawPersonalia($imie, $nazwisko)
{
$this -> imie = $imie;
$this -> nazwisko = $nazwisko;
} // end ustawPersonalia();
public function personalia()
{
return $this -> imie.' '.$this -> nazwisko;
} // end personalia();
}
$osoba = new osoba; // 4
$osoba -> ustawPersonalia('Adam', 'Kowalski');
echo $osoba -> personalia();
?>
Możemy teraz stworzyć różne obiekty klasy osoba. Metodą ustawPersonalia() ustawiamy im imiona i nazwiska, natomiast personalia() pozwala pobrać sformatowane personalia przypisane do danego obiektu. Oczywiście w tym wypadku istnieje możliwość odwołania się np. do imienia bezpośrednio, np. $osoba -> imie. Zauważmy, że tworzenie metod nie różni się szczególnie od pisania własnych funkcji. Różnice to słowo kluczowe public na początku oraz specjalny wskaźnik $this pozwalający na odwołanie się do obiektu, na którym uruchomiliśmy metodę.
[edytuj] Kontrola dostępu (Hermetyzacja)
Zajmiemy się teraz wyjaśnieniem, co to tajemnicze słowo public oznacza. Jedną z zalet programowania obiektowego jest tzw. hermetyzacja, czyli ukrywanie przed programistą implementacji klas. Wiedza o tym, w jaki sposób obiekty działają, nie jest mu do niczego potrzebna i nawet nie jest wskazane, by ingerował on w wewnętrzne dane obiektu. Dzięki kontroli dostępu można zablokować mu dostęp do niektórych pól lub metod tak, że będą mogły zmieniać ich wartość jedynie inne metody.
Jeśli pole/metodę poprzedzimy słowem kluczowym public, PHP zapewni do niej swobodny dostęp, tj. będą się mogły odwoływać doń zarówno metody danej klasy, jak i programista korzystający z danego obiektu (np. $obiekt -> metoda()). Możemy także użyć słowa kluczowego private, które ograniczy dostęp wyłącznie do innych metod klasy (czyli poprzez $this->metoda(), napisanie $obiekt->metoda() będzie niemożliwe). Przepiszmy powyższy przykład tak, aby programista nie widział pól imie oraz nazwisko.
<?php
class osoba
{
private $imie;
private $nazwisko;
public function ustawPersonalia($imie, $nazwisko)
{
$this -> imie = $imie;
$this -> nazwisko = $nazwisko;
} // end ustawPersonalia();
public function personalia()
{
return $this -> imie.' '.$this -> nazwisko;
} // end personalia();
}
$osoba = new osoba; // 4
$osoba -> ustawPersonalia('Adam', 'Kowalski');
echo $osoba -> personalia();
?>
Działa on dalej, ponieważ nie odwołujemy się do prywatnych pól spoza obiektu. Zobaczmy, co się stanie przy próbie nielegalnego dostępu:
<?php
class osoba
{
private $imie;
private $nazwisko;
public function ustawPersonalia($imie, $nazwisko)
{
$this -> imie = $imie;
$this -> nazwisko = $nazwisko;
} // end ustawPersonalia();
public function personalia()
{
return $this -> imie.' '.$this -> nazwisko;
} // end personalia();
}
$osoba = new osoba; // 4
$osoba -> ustawPersonalia('Adam', 'Kowalski');
echo $osoba -> imie;
?>
Ten skrypt już się nie wykona. Na ekranie ujrzymy komunikat:
Fatal error: Cannot access private property osoba::$imie
Jako prywatne możemy także oznaczyć niektóre metody, zaznaczając w ten sposób, że służą one wyłącznie do prywatnego użytku danej klasy i programista nie powinien ich samodzielnie wywoływać.
[edytuj] Praktyczne zastosowanie OOP
Do tej pory operowaliśmy głównie na różnych klasach typu osoba, które nie mają zbyt wiele wspólnego z pisaniem prawdziwych aplikacji. Dlatego pragniemy już teraz wyjaśnić, w jaki sposób można wykorzystać OOP w praktyce. Z kilkoma takimi przykładami mieliśmy już do czynienia. Konkretniej mamy na myśli bibliotekę do komunikacji z bazą danych - PHP Data Objects, a także systemy szablonów Smarty i Open Power Template. W przypadku PDO każdy obiekt reprezentował tam pojedyncze połączenie z bazą danych z określonym na nim zestawem operacji. Mamy też obiekty zbioru wyników zwracane przez metody PDO::prepare() oraz PDO::query() reprezentujące wyniki danego zapytania. W obu systemach szablonów utrzymywaliśmy obiekty $tpl, dzięki którym mogliśmy manipulować szablonami. Zauważmy, że zebranie wszystkich danych oraz metod parsera w klasę ułatwia przepływ danych oraz ukrywanie tych właściwości, których programista nie powinien widzieć.
Ogólna zasada jest taka, że klasami czynimy poszczególne elementy funkcjonalne składające się na aplikację WWW:
- Połączenie z bazą danych
- System szablonów
- System zarządzania konfiguracją
- Poszczególne akcje, jakie można wykonać na danej podstronie.
- Sesje
- I wiele innych...
Rodzi się tu kolejne pytanie: na co tworzyć całą klasę, by później utworzyć jeden jej obiekt? Przecież identyczny efekt osiągniemy, wykorzystując tablice i funkcje. Odpowiedź leży w tym, że poznaliśmy dopiero ułamek możliwości programowania obiektowego. Między klasami można utworzyć znacznie bardziej złożone interakcje, a dzięki temu, że taki model programowania jest bardziej zbliżony do naturalnego sposobu myślenia, prościej projektuje się też kompletną aplikację. Popatrz na to w ten sposób: czemu mamy ograniczać się do tworzenia systemu konfiguracji dostosowanego do jednej konkretnej sytuacji? A jeśli w jakimś miejscu potrzebny by nam był podsystem, który czerpie dane z innego źródła? Zamiast tworzyć dwie osobne klasy robiące to samo, zaprojektujmy bardziej ogólną, która za parametr pobierać będzie obiekt reprezentujący jakieś źródło danych. Oto przykład:
<?php
class config
{
private $dataSources = array();
private $data = array();
private $modified = false;
public function setDatasource($ds)
{
$name = get_class_name($ds);
$this -> dataSources[$name] = $ds;
$this -> data[$name] = $ds -> getData();
} // end setDatasource();
public function get($id)
{
foreach($this -> data as &$values)
{
if(isset($values[$id]))
{
return $values[$id];
}
}
return NULL;
} // end get();
public function set($id, $value)
{
foreach($this -> data as &$values)
{
if(isset($values[$id]))
{
$values[$id] = $value;
$this -> modified = true;
}
}
} // end set();
public function save()
{
if($this -> modified)
{
foreach($this -> dataSources as $name => $ds)
{
$ds -> saveData($this -> data[$name]);
}
}
} // end save();
}
class fileResource
{
private $fn;
public function setFilename($fn)
{
$this -> fn = $fn;
} // end setFilename();
public function getData()
{
return parse_ini_file($this -> fn);
} // end save();
public function saveData($data)
{
$code = '';
foreach($data as $id => $value)
{
$code .= $id.' = "'.$value."\"\r\n";
}
file_put_contents($this -> fn, $code);
} // end saveData();
}
class databaseResource
{
public function getData()
{
global $sql;
$result = array();
$stmt = $sql -> query('SELECT name, value FROM `config` ORDER BY `name`');
while($row = $stmt -> fetch())
{
$result[$row['name']] = $row['value'];
}
$stmt -> closeCursor();
return $result;
} // end save();
public function saveData($data)
{
global $sql;
$stmt = $sql -> prepare('UPDATE `config` SET `value` = :value WHERE `name` = :name');
foreach($data as $name => $value)
{
$stmt -> bindValue(':name', $name, PDO::PARAM_STR);
$stmt -> bindValue(':value', $value, PDO::PARAM_STR);
$stmt -> execute;
}
} // end saveData();
}
// uzycie
$pdo = new PDO('mysql:host=localhost;dbname=strona', 'root', 'root');
// tworzymy pusta klase konfiguracyjna
$cfg = new config;
$file = new fileResource;
$file -> setFilename('../config/konfiguracja.php');
// teraz mamy dostepna konfiguracje wczytana z pliku
$cfg -> setDatasource($file);
echo $cfg -> get('page_title').'<br/>';
// teraz mamy takze dostepna konfiguracje wczytana z bazy!
$cfg -> setDatasource(new databaseResource);
echo $cfg -> get('page_timezone').'<br/>';
$cfg -> save(); // jesli zaszly zmiany w konfiguracji, mozemy je zapisac
?>
Do uruchomienia przykładu potrzebny jest nam jeszcze plik konfiguracyjny:
; <?php die(); ?> na wypadek, gdyby ktos z przegladarki sprobowal go zalaczyc page_title = "Tytuł strony" page_address = "http://localhost/"
A także tabelę w bazie danych:
CREATE TABLE `config` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(40) NOT NULL,
`value` VARCHAR(255),
PRIMARY KEY(`id`),
UNIQUE(`name`)
);
INSERT INTO `config` (`name`, `value`) VALUES('page_timezone', 'ETC/GMT+2');
Uruchom przykład i sprawdź, czy działa, jak należy. Powinny pokazać się wartości dwóch dyrektyw konfiguracyjnych, jednej wczytanej z pliku, a drugiej z bazy.
Przyjrzyjmy się teraz dokładniej sposobowi funkcjonowania naszego mechanizmu konfiguracyjnego. Mamy klasę config, poprzez którą możemy odczytywać i modyfikować dyrektywy konfiguracyjne. Jednak sama w sobie jest ona bezużyteczna, ponieważ nie potrafi znikąd pobrać stosownych danych, ani ich z powrotem zapisać. Jej funkcjonalność określamy, dodając do niej jeden lub więcej obiektów klas fileResource, databaseResource czy jeszcze innych, jakie sami napiszemy. Obiekt jest reprezentowany przez zmienną, dlatego w bardzo prosty sposób można go przekazywać jako parametr funkcji, składować w tablicach i później przepuszczać pętlą foreach. Jeżeli do klasy config dodamy 10 różnych obiektów źródeł danych konfiguracyjnych, wtedy klasa główna będzie skanować 10 źródeł w poszukiwaniu stosownej dyrektywy, jakiej żądamy. Jeśli dodamy pięć - wtedy pięciu. Co więcej, nie jesteśmy w żaden sposób ograniczeni przez projektanta systemu. Jeśli mielibyśmy jakieś nietypowe życzenie (np. udostępnienie jakichś ważnych danych z innego obiektu poprzez system konfiguracji), musimy jedynie stworzyć sobie nową klasę posiadającą metody getData() oraz saveData(), a jej obiekt dodać do konfiguracji. Zauważ, jak bardzo jest to zgodne z intuicją. Jeśli jakiś obiekt ma mieć większe możliwości, dołączamy do niego, jak klocki, mniejsze obiekty, które pozwalają mu na wykonanie dodatkowych czynności. Kod jest niezwykle przenośny i można bez trudu zaadaptować go do nowych zastosowań.
[edytuj] Zakończenie
Mamy już solidne podstawy programowania obiektowego, a także pokazaliśmy, w jaki sposób wykorzystuje się jego własności podczas tworzenia oskryptowania stron internetowych, pisząc modularny i łatwy w rozbudowie system konfiguracji. W następnym rozdziale pokażemy, jak sterować tworzeniem i niszczeniem obiektów.