Programowanie obiektowe w PHP

Artykuł dodany: 10 września 2011. Ostatnia modyfikacja: 02 listopada 2011.

Stopień trudności (1 - dla początkujących, 5 - dla ekspertów): 2

Według słownikowej definicji programowanie obiektowe (ang. object-oriented programming) to “paradygmat programowania, w którym programy definiuje się za pomocą obiektów — elementów łączących stan (czyli dane, nazywane najczęściej polami) i zachowanie (czyli procedury, tu: metody). Obiektowy program komputerowy wyrażony jest jako zbiór takich obiektów, komunikujących się pomiędzy sobą w celu wykonywania zadań” (źródło: wikipedia). W praktyce wyobraź sobie przykładowe figury geometryczne – trójkąt, kwadrat, elipsa. Każda figura może mieć zdefiniowane pola – długość, szerokość, pole powierzchni. Na każdej figurze możemy wykonywać operacje – przeliczać pole powierzchni, wyliczać średnicę, promień. W programowaniu strukturalnym takie wyliczenie zrobiłbyś za pomocą funkcji, w programowaniu obiektowym funkcja ta nosi nazwę metody (method). Zbiór metod i pól (właściwości) grupujących elementy jednego zbioru to właśnie klasa. Obiekt jest natomiast konkretnym egzemplarzem danej klasy. Przykładowo, możesz utworzyć 5 prostokątów różniących się długością boków. Korzystasz zatem z bazowej klasy opisującej prostokąt, definiujesz im długość boków i na podstawie tej jednej klasy tworzysz pięć różnych obiektów. Nie przejmuj się jeśli brzmi skomplikowanie – za chwilę przejdziemy do kodu opisującego te twierdzenia.

Definicja klasy w PHP

Nazwa klasy może zaczynać się od podkreślenia i litery a następnie zawierać podkreślenie, litery i cyfry określone wyrażeniem regularnym [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*. Nie może natomiast zawierać zarezerwowanych słów kluczowych. Jako nazwy klasy nie możesz również wykorzystać słów: self, static oraz parent. Poniżej przedstawię Ci czytelniku pełną definicję klasy zgodnie z zasadą iż najlepiej uczyć się na trudnym kodzie.

<?php

# Ustaw powiadomienia o błędach żeby móc eksperymentować z kodem
ini_set('error_reporting', E_ALL);
ini_set('display_errors', 'on');

interface iWielokat {
  public function wyliczPole();
}

# Nie możesz utworzyć obiektu klasy abstrakcyjnej
abstract class wielokat implements iWielokat {
  # Definicja zmiennych klasowych czyli "properties"
  protected $_x, $_y;

  # Definicja szczególnej metody - konstruktora
  public function __construct($x, $y) {
    $this->_x = (int) $x;
    $this->_y = (int) $y;
    echo 'Długość boku wynosi: '.$this->_x.', szerokość: '.$y;
  }

  # Definicja kolejnej metody, wymuszona przez interface
  public function wyliczPole() {
    return $this->_x * $this->_y;
  }

}

class prostokat extends wielokat {
  public function __construct($x, $y) {
    parent::__construct($x, $y);
  }

}

# tworzmy obiekt klasy prostokat
$prostokat1 = new prostokat(5, 6);
echo '<br>Pole prostokąta pierwszego wynosi: '.$prostokat1->wyliczPole();
echo '<br>';

# kolejny obiekt klasy prostokat
$prostokat2 = new prostokat(3, 3);

Jako wynik podanego kodu otrzymasz:

Długość boku wynosi: 5, szerokość: 6
Pole prostokąta wynosi: 30
Długość boku wynosi: 3, szerokość: 3

Podstawowa definicja klasy od której zawsze zaczynamy to

class nazwa {...}

Aby lepiej zrozumieć jakie dane powinna klasa zawierać spójrz na nią trochę jak na bazę danych. Masz w niej tabele, definicje kolumn i typy danych. Klasę możesz rozumieć jako tabelę, pola jako kolumny. Ponieważ PHP jest językiem dynamicznie typowanym wartości pól mogą być stringiem, integerem, tablicą czy nawet innym obiektem. Bardzo często, przy bardziej zaawansowanych projektach, mamy zresztą do czynienia z sytuacją w której dane z tabel są bezpośrednio mapowane na poszczególne klasy w relacji 1:1. Zobacz dla przykładu tabelę “użytkownicy”:

CREATE TABLE uzytkownicy
(
  id serial NOT NULL,
  imie character varying(50) NOT NULL,
  nazwisko character varying(200) NOT NULL,
  wiek integer,
  CONSTRAINT uzytkownicy_pkey PRIMARY KEY (id )
);

Tabela (baza danych Postgresql) zawiera kolumny imię, nazwisko oraz wiek. Klasa do której mógłbyś zapisywać dane pochodzące z tej tabeli może mieć postać:

<?php
class uzytkownik {
  public $id, $imie, $nazwisko, $wiek;

  public function setId($id) {
    $this->id = (int) $id;
    return $this;
  }

  public function getId() {
    return $this->id;
  }

  public function setImie($imie) {
    $this->imie = $imie;
    return $this;
  }

  public function getImie() {
    return $this->imie;
  }

  # Podobnie dla pól nazwisko i wiek
}

Interesującą składnią którą możesz zauważyć w kodzie jest return $this. Poprzez słowo kluczowe $this otrzymujesz dostęp do metod i pól klasy, natomiast zwracając return $this zwracasz całą instancję klasy. Prowadzi to do zjawiska zwanego “method chaining”.

<?php
$uzytkownik = new uzytkownik();
$uzytkownik->setId($row->id)->setImie($row->imie)->setNazwisko($row->nazwisko)->setWiek($row->wiek);

Niestety w PHP nie możesz operować bezpośrednio na utworzonym obiekcie więc nie zadziała składnia

<?php
$uzytkownik = new uzytkownik()->setId($row->id)->setImie($row->imie);

Nową instancję klasy tworzymy poprzedzając nazwę klasy słowem kluczowym new.

Dziedziczenie

Prostokąt lub kwadrat są podzbiorem szerszego zbioru wielokątów. Jako wielokąty posiadają wiele cech wspólnych. Możemy wyliczyć ich pole oraz obwód na podstawie dostarczonych długości boków. Zdefiniowaliśmy zatem bazową klasę wielokat która uwzględnia podstawowe dane jak długość i szerokość. Prostokąt dziedziczy pola i metody z klasy bazowej wielokąt tym samym są one dostępne w każdej klasie potomnej bez ponownej deklaracji.

class prostokat extends wielokat {...}

Za dziedziczenie (inheritance) odpowiada słowo kluczowe extends.

Metoda wyliczPole() jest zadeklarowana w klasie wielokat a jednak mamy do niej dostęp właśnie z racji dziedziczenia metod.

$prostokat1->wyliczPole();

Ale zaraz. Kwadrat jest szczególnym przykładem czworokąta którego wszystkie boki są równe a pole wyliczamy podnosząc długość tylko jednego boku do kwadratu. Czy możemy to uwzględnić przepisując metodę wyliczPole() tylko dla kwadratu? Oczywiście i nawet tak powinniśmy zrobić od samego początku. Każdy z wielokątów ma inny wzór na pole powierzchni stąd iloczyn obu boków nie będzie prawidłowym działaniem.

<?php
class kwadrat extends wielokat {
  public function wyliczPole() {
    return pow($this->_x, 2);
  }
}

$kwadrat = new kwadrat(4, 4);
echo '<br>Pole kwadratu wynosi: '.$kwadrat->wyliczPole();

Nadpisaliśmy tym samym metodę $wielokat->wyliczPole(). Wynikiem wywołania teraz tej metody będzie:

Długość boku wynosi: 4, szerokość: 4
Pole kwadratu wynosi: 16

Jeśli jednak z jakiegoś powodu chcielibyśmy wywołać metodę wyliczPole() klasy wielokąt musimy skorzystać ze specjalnej konstrukcji parent::nazwaMetody().

<?php
class kwadrat extends wielokat {
  public function wyliczPole() {
    echo 'Wywołano '.__METHOD__;
    return parent::wyliczPole();
  }
}

W przykładzie użyta została również magiczna stała. Za ich pomocą możesz określać nazwę metody, klasy czy np. używanej przestrzeni nazw.

Rozszerzać możemy prawie dowolną metodę o ile nie będzie zadeklarowana jako finalna. Gdyby w kodzie klasy wielokat przed definicją metody wyliczPole() znalazło się słowo kluczowe final wtedy parser PHP poinformuje nas o błędzie:

<?php
abstract class wielokat implements iWielokat {
  final public function wyliczPole() {
    return $this->_x * $this->_y;
  }
}

Fatal error: Cannot override final method wielokat::wyliczPole()

Publiczna, prywatna czy może chroniona?

W kodzie powyżej obok nazw metod i pól występowały również dwa słowa kluczowe: public i protected. Jest jeszcze trzecie – private. Określają one sposób dostępu do pól i metod. Największą widoczność zapewnia deklaracja public która oznacza że dostęp możemy uzyskać wszędzie. Przeciwieństwem tego jest private zapewniający dostęp wyłącznie z tego samego obiektu. Natomiast protected daje nam dostęp w obiekcie bazowym i potomnych.

Dobrym nawykiem jest oznaczać pola i metody z dostępem private i protected poprzez umieszczenie podkreślenia przed nazwą. Łatwo w ten sposób szybko sprawdzić widoczność np. protected $_nazwisko.

Wróćmy zatem do naszego bazowego przykładu i spróbujmy uzyskać dostęp do pola $_x

<?php
$prostokat1 = new prostokat(5, 6);
echo $prostokat1->_x;

Fatal error: Cannot access protected property prostokat::$_x

Zmieńmy teraz widoczność na public:

<?php
abstract class wielokat implements iWielokat {
  # Zmieniamy widoczność na public
  public $x, $y;

  public function __construct($x, $y) {
    $this->x = (int) $x;
    $this->y = (int) $y;
    echo 'Długość boku wynosi: '.$this->x.', szerokość: '.$this->y;
  }
}

$prostokat1 = new prostokat(5, 6);
echo 'Odczyt pola public $x: '.$prostokat1->x;
Długość boku wynosi: 5, szerokość: 6
Odczyt pola public $x: 5

Ostatecznie spróbujmy dodać metodę wyświetlającą długość jednego z boków:

<?php
class prostokat extends wielokat {

  public function dlugoscBoku() {
    return (!isset($this->_x)) ? 'undefined' : $this->_x;
  }

}

$prostokat1 = new prostokat(5, 6);
echo 'Długość boku x: '.$prostokat1->dlugoscBoku();
Długość boku wynosi: 5, szerokość: 6
Długość boku x: 5

A jak zachowa się metoda dlugoscBoku() jeśli zmienimy widoczność pola $_x na private?

<?php
abstract class wielokat implements iWielokat {
  private $_x;
  protected $_y;

print_r($prostokat1);
Object ( [_x:wielokat:private] => 5 [_y:protected] => 6 ) 
Długość boku wynosi: 5, szerokość: 6
Długość boku x: undefined

Z racji tego iż tylko obiekt wielokat jest w stanie odczytać wartości zadeklarowane jako prywatne obiekt dziedziczący prostokat nie ma do tych danych bezpośredniego dostępu. A niebezpośredni?

<?php
abstract class wielokat implements iWielokat {
  private $_x;

  protected function _getDlugosc() {
    # Zwróć wartość pola private 
    return $this->_x;
  }
}

class prostokat extends wielokat {

  public function dlugoscBoku() {
    return $this->_getDlugosc();
  }

}

Object ( [_x:wielokat:private] => 5 [_y:protected] => 6 ) 
Długość boku wynosi: 5, szerokość: 6
Długość boku x: 5

Warto jeszcze dodać jedną uwagę. W PHP4 deklaracja pól następowała przez słowo kluczowe var.

var $imie;

Ze względu na wsteczną kompatybilność składnia ta jest jeszcze zachowana a widoczność takiej zmiennej to public. Jeśli jednak chcesz pisać poprawny kod używaj raczej składni public, private i protected.

Konstruktory i destruktory

Są to specjalne metody wywoływane odpowiednio podczas tworzenia i niszczenia obiektu. Najczęściej do konstruktora przekazywane są dyrektywy konfiguracyjne albo wartości które muszą być później tak czy inaczej wykorzystane. Niektóre języki jak Java nie posiadają destruktorów. W PHP5 był z nimi problem, kiedy twórcy języka zdecydowali się zmienić moment wykonania destruktora. Od pewnego czasu dokumentacja jest stała aczkolwiek destruktory raczej rzadko używane są w kodzie rzeczywistych aplikacji.

W naszej klasie wielokat konstruktor odpowiada za przypisanie przekazywanych zmiennych długości boku oraz wypisanie ich wartości. Jak zauważyliśmy podczas omawiania dziedziczenia, dla różnych wielokątów inaczej oblicza się pole powierzchni na podstawie różnej niż tylko $x, $y długości boków. Dlatego możemy zmienić nasz konstruktor na bardziej uniwersalny przyjmujący jako argument tablicę.

<?php
abstract class wielokat implements iWielokat {
  protected $_dlugosci = array();

  # Type Hinting
  public function __construct(array $dlugosci) {
    $this->_dlugosci = $dlugosci;
  }
}

Co zostało użyte przy deklaracji array $dlugosci nazywa się rzutowaniem typów. Już podczas samego przypisywania wartości możemy sobie zażyczyć aby wartość była tablicą lub obiektem. Jeśli typ danych nie będzie się zgadzał otrzymamy komunikat:

Catchable fatal error: Argument 1 passed to wielokat::__construct() must be an array, integer given

Klasy abstrakcyjne

W naszym przykładzie klasą abstrakcyjną jest bazowa klasa wielokat. Nie możemy utworzyć instancji klasy oznaczonej jako abstrakcyjnej. Klasa uznawana jest za abstrakcyjną jeśli choć jedna z jej metod również jest abstrakcyjna. A abstrakcyjna metoda nie może zawierać żadnego ciała – wyłącznie deklarację widoczności i przyjmowane argumenty.

Co to oznacza w praktyce?

$wielokat = new wielokat();
Fatal error: Cannot instantiate abstract class wielokat

Postępując w ten sposób zapewniliśmy sobie ogólne metody do obsługi naszych wielokątów ale poszczególne klasy dziedziczące muszą same dokonywać obliczeń i przypisań. I znowu, pole każdego wielokąta wyliczane jest na podstawie innych wartości więc najlepszym sposobem będzie tylko pokazanie – musi występować metoda wyliczPole().

<?php
# Skasujmy na razie interface iWielokat, dubluje funkcjonalność
abstract class wielokat {

  # Deklaracja metody abstrakcyjnej, nie zawiera ciała
  abstract protected function wyliczPole();
}

$prostokat1 = new prostokat(5, 6);
echo '<br>Pole prostokąta pierwszego wynosi: '.$prostokat1->wyliczPole();

PHP Fatal error: Class prostokat contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (wielokat::wyliczPole)

Klasa prostokąt nie posiada zadeklarowanej wymaganej metody wyliczPole().

<?php
class prostokat extends wielokat {

  public function wyliczPole() {
    return $this->_x * $this->_y;
  }

}

$prostokat1 = new prostokat(5, 6);
echo '<br>Pole prostokąta pierwszego wynosi: '.$prostokat1->wyliczPole();
Długość boku wynosi: 5, szerokość: 6
Pole prostokąta pierwszego wynosi: 30

Teraz kiedy stworzyliśmy ciało metody wyliczPole() otrzymujemy prawidłowy wynik. Warto również zauważyć że metoda abstrakcyjna miała widoczność protected. Klasa potomna może zadeklarować widoczność taką samą bądź mniej restrykcyjną.

Interface

Interface przydaje się przy bardziej skomplikowanym kodzie nad którym pracuje grupa ludzi. Nie zawiera żadnej definicji ciała (z wyjątkiem pól statycznych). Mówi nam tylko że konkretna klasa musi zawierać takie a nie inne metody. Nasz interface:

<?php
interface iWielokat {
  public function wyliczPole();
}

został zaimplementowany (implements) w klasie wielokat wymuszając użycie publicznej metody wyliczPole(). Gdybyśmy teraz skasowali definicje tej metody parser PHP poinformuje nas o błędzie. Również widoczność metod musi się zgadzać:

Fatal error: Access level to wielokat::wyliczPole() must be public (as in class iWielokat)

Co ważne w PHP nie ma wielodziedziczenia (dziedziczenia po wielu klasach bazowych) natomiast klasy mogą implementować wiele interface‘ów (nazwy oddzielone przecinkami).

Możemy również sprawdzić czy zmienna jest instancją danej klasy lub interface’u:

<?php
if ($prostokat1 instanceof iWielokat) {
  echo 'Prostokąt ma zaimplementowany interface.';
}

Stałe, metody i pola statyczne

Podobnie jak w programowaniu strukturalnym, w OOP możemy również definiować pola i metody statyczne. Dostęp do nich uzyskujemy bez tworzenia instancji danej klasy oraz bez operatora $this->. Przykładowo:

<?php
class Test {
  const STALA = 'wartość stałej';

  public static $nazwa = 'test';

  public static function pobierzNazwe() {
        return self::$nazwa;
    }
}

echo 'Nazwa z metody statycznej: '. Test::pobierzNazwe();
echo 'Pobieramy stałą: '. TEST::STALA;
Nazwa z metody statycznej: test
Pobieramy stałą: wartość stałej

Self oznacza tutaj tę samą klasę. Dzięki operatorowi dwukropka (:: – Paamayim Nekudotayim) uzyskujemy dostęp do stałych i pól statycznych.

Klasa specjalna stdClass

Tablicę, string albo inny typ danych również możemy przekształcić na obiekt.

<?php
$test = 'wartość';
$object = (object) $test;
print_r($object);
stdClass Object ( [scalar] => wartość )
<?php
$naszeDane = new stdClass();
$naszeDane->imie = 'Jerzy';
$naszeDane->zawod = 'informatyk';

var_dump($naszeDane);

$json = json_encode($naszeDane);

var_dump($json);

Przekształcenie takie jest bardzo użyteczne kiedy potrzebujemy, na dalszym etapie korzystania, odwoływać się również do obiektów np. wysyłamy do dalszej obróbki tablicę JSON. Warto pamiętać że istnieje taka możliwość.

Standardowa biblioteka PHP (SPL – Standard PHP Library)

W PHP5 został wprowadzony zbiór klas i interface‘ów nazwanych SPL. Zawiera bardzo popularne i przydatne biblioteki np. ArrayObject – obiektowe wyrażenie tablic, w wielu sytuacjach operacje na dużym zbiorze są dużo szybsze niż przez typowe funkcje tablicowe array(). Często może się zdarzyć że w kodzie zewnętrznych bibliotek (Zend, Symfony) spotkasz się kodem który właśnie będzie implementował SPL. Dlatego warto zapoznać się z dokumentacją PHP na ten temat.

Podsumowanie

Choć dla osób które dopiero zaczynają przygodę z programowaniem, albo dotychczas pisały kod strukturalny początki z obiektami będą trudne, to jednak warto podjąć wyzwanie. Kod oparty na obiektach jest dużo bardziej uporządkowany, samo PHP staje się również coraz mocniej obiektowe. Popularne webowe frameworki pisanie są wyłącznie w OOP. Aby zrozumieć ekspertów należy posiadać minimalną wiedzę na początek a studiowanie tematu głębiej podnosi również nasze kwalifikacje. Jako kolejny krok zapraszam zatem do lektury klasy i obiekty w PHP5.

Komentarze

Nie ma jeszcze żadnych komentarzy do wyświetlenia. Może chcesz zostać pierwszą osobą która podzieli się swoją opinią?

Dodaj komentarz

*
Nazwa zostanie wyświetlona wraz z komentarzem. Możesz też utworzyć nowe konto w serwisie, dzięki czemu uzyskasz dodatkową funkcjonalność.
*
Akceptowana jest ograniczona składnia Textile. Wszystkie tagi HTML zostaną usunięte.