PHP/Klasy i obiekty

Z Wikibooks, biblioteki wolnych podręczników.

< PHP

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:

  1. Klasa jest elementem opisowym. Informuje PHP, jakie dane mogą przechowywać obiekty jej rodzaju oraz jakie operacje mogą wykonywać.
  2. 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.
  3. Elementami do przechowywania danych w obiektach są pola.
  4. Metody, lub inaczej funkcje wewnętrzne, opisują możliwe operacje do wykonania na polach obiektu.
  5. Obiekt konkretnej klasy tworzymy poleceniem new konkretnaKlasa;, np. $obiekt = new konkretnaKlasa;
  6. Do pól oraz metod obiektu odwołujemy się "operatorem" ->, np. $obiekt -> pole, $obiekt -> metoda().
  7. 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!
  8. 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:

  1. Połączenie z bazą danych
  2. System szablonów
  3. System zarządzania konfiguracją
  4. Poszczególne akcje, jakie można wykonać na danej podstronie.
  5. Sesje
  6. 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.