Stanąłem dziś przed takim dylematem: chciałem utworzyć listę obiektów (każdy obiekt to konkretny wpis w bazie danych). No OK, samo utworzenie listy to niewielki problem. Problemem jest to, że w całej aplikacji używam dość zaawansowanych “ułatwiaczy”: przeładowywanie __isset(), __unset(), __set() i __get(). Np. mam obiekt $p, instancję klasy Post. Ma on różne własności, np tytuł (Post::$title), treść (Post::$body). Dostęp do nich jest za pomocą przeładowanych metod __set() i __get(), co pozwala mi na zautomatyzowanie kilku rzeczy - jak na przykład niewidoczną dla programisty zamianę znaków nowej linii na HTMLowe znaczniki <br />. Ale ja nie o tym miałem…

Chciałem zachować naturalność rozwiązań także w klasie agregującej obiekty Post. Czyli potraktować de facto obiektu agregatu (który ma kilka zadań, jak np utworzenie tej listy obiektów) jako tablicy. Tworząc instancję klasy, chcę mu zapodać: utwórz listę wpisów z kategorii o ID = 2, obiektów ma być 30. No i z tego korzystać. Mogę oczywiście dać sobie dostęp do jednej z własności tegoż obiektu-agregatu, która będzie tablicą i zawierać w sobie będzie wszystkie pobrane wpisy. Ale czy wspominałem coś o naturalności rozwiązań? :)

PHP 5 dostarcza nam coś, co nazywa się SPL, czyli Standard PHP Library. Jedną z klas (a konkretnie intefejsów) modułu SPL jest ArrayAccess.

Aby tego użyć, należy zadeklarować klasę jako implementującą z tego interfejsu:

class Aggregate implements ArrayAccess {}

A w niej trzeba zaimplementować 4 metody: offsetExists($offset), offsetGet($offset), offsetSet($offset, $value), offsetUnset($offset).

Przykład tego, jak powinna wyglądać cała klasa:

class Aggregate implements ArrayAccess
{
    private $data = array();

    public function __construct($array=null)
    {
        if (!is_null($array)) {
            $this->data = $array;
        }
    }

    public function offsetExists($offset)
    {
        return array_key_exists($offset, $this->data);
    }

    public function offsetGet($offset)
    {
        return $this->data[$offset];
    }

    public function offsetSet($offset, $value)
    {
        $this->data[$offset] = $value;
    }

    public function offsetUnset($offset)
    {
        unset($this->data[$offset]);
    }
}

Do tego przydałby się prosty sposób na sprawdzenie liczby obiektów które są na liście (kazaliśmy pobrać 30, ale to jest ilość maksymalna - w bazie danych może być ich mniej). Wykonamy to poprzez interfejs Countable i metodę count(). Nagłówek naszej klasy zmieniamy na taki:

class Aggregate implements ArrayAccess, Countable

I dodajemy metodę count():

    public function count()
    {
        return count($this->data);
    }

Żeby poczuć się jakbyśmy pracowali z prawdziwą tablicą, brakuje nam jeszcze możliwości iteracji po poszczególnych elementach tablicy. Do tego trzeba zaimplementować interfejs Iterator. Do listy implementowanych interfejsów dodajemy Iterator:

class Aggregate implements ArrayAccess, Countable, Iterator

I implementujemy jego metody:

    public function current()
    {
        return current($this->data);
    }

    public function key()
    {
        return key($this->data);
    }

    public function next()
    {
        return next($this->data);
    }

    public function rewind()
    {
        return reset($this->data);
    }

    public function valid()
    {
        return current($this->data);
    }

Od tego momentu możemy każdą instancję klasy Aggregate traktować jak tablicę. Pobieramy dowolny element tak jak z normalnej tablicy: $aggregate[2], tak samo zmieniać jego zawartosć: $aggregate[2] = ’something’;, iterować za pomocą foreach() {}, usuwać elementy za pomocą unset() etc. Jedyne czego nie osiągnęłem, ale też specjalnie nie próbowałem, to dodawać elementów za pomocą notacji []. Ale myślę że w większości zastosowań nie jest to potrzebne.

Cała, działająca klasa wraz z przykładem użycia: urzenia.net/wp-content/class_aggregate.php.

UPDATE:
Udoskonalona wersja klasy, odporna na błąd z metody valid (), a także pozwalająca na dodawanie kolejnych elementów tablicy PHPowym idiomem $aggregate[] = ‘wartosc’ znajduje się pod adresem: urzenia.net/wp-content/class_aggregate_new.php.


Liczba komentarzy: 19

  1. 1 Był Poniedziałek, 19 czerwiec 2006 roku gdy o godzinie 16:15 przyszedł Fipaj i stwierdził:

    Genialne, choć mało przydatne. Muszę więcej doczytać o SPL, iteratory to zdaje się wierzchołek góry lodowej…

  2. 2 Była Środa, 21 czerwiec 2006 roku gdy o godzinie 00:09 przyszedł MySZ i stwierdził:

    Czy mało przydatne… kwestia gustu. Ja w Core zastosowanie znalazłem ;) Mam po prostu inteligentny (sam ładuje swoją zawartość etc) agregat na obiekty, do których to obiektów mam przyjemny dostęp korzystając z operatorów tablicy.

    Staram się robić jak najbardziej przyjazne dla programiasty interfejsy, tak żeby używanie obiektów było jak najbardziej naturalne. W tym kontekście podejście zaprezentowane powyżej jest jak najbardziej przydatne :)

  3. 3 Była Środa, 21 czerwiec 2006 roku gdy o godzinie 15:32 przyszedł Fipaj i stwierdził:

    No dobra, od czasu do czasu się przydaje :)

    > Jedyne czego nie osiągnęłem, ale też specjalnie nie próbowałem, to dodawać elementów za pomocą notacji [].

    ?
    Z tego, co wiem, to jest to możliwe.
    (Może bug PHP?)

  4. 4 Była Środa, 21 czerwiec 2006 roku gdy o godzinie 16:43 przyszedł MySZ i stwierdził:

    Do zwykłej tablicy - nie ma problemu. Gorzej do tej tworzonej w sposób z artykułu :)

  5. 5 Była Środa, 21 czerwiec 2006 roku gdy o godzinie 17:07 przyszedł Fipaj i stwierdził:

    No więc właśnie mi chodzi o tą “tablicę” implementującą ArrayAccess.

    Sprawdziłem w książce Sebastiana Bergmanna - na 100% można.

  6. 6 Była Środa, 21 czerwiec 2006 roku gdy o godzinie 19:04 przyszedł Fipaj i stwierdził:

    OK, sorry że tak na dwa razy, ale już wiem, gdzie masz błąd :)

    CHYBA powinno być tak:

    public function valid()
    {
    return current($this->data) + 1;
    }

  7. 7 Była Środa, 21 czerwiec 2006 roku gdy o godzinie 19:12 przyszedł Fipaj i stwierdził:

    Kurde, cofam to, co napisałem, ale jednak w tej raczej jest błąd, bo powinna ona sprawdzać, czy jest następny element tablicy.

    (mój przykład tworzy pętlę nieskończoną ^^)

  8. 8 Była Środa, 21 czerwiec 2006 roku gdy o godzinie 19:20 przyszedł Fipaj i stwierdził:

    “Iterator::valid ( ) [abstract]

    Check if there is a current element after calls to rewind() or next(). ” - SPL Reference

    “valid() - sprawdza, czy w zbiorze znajduje się kolejny element” - Sebastian Bergmann (bądź tłumacz).

    Reference ma zawsze rację, wobec czego ja też się myliłem. Sorry za flood, już więcej się nie odzywam.

  9. 9 Był Piątek, 23 czerwiec 2006 roku gdy o godzinie 18:42 przyszedł MySZ i stwierdził:

    Dobry flood nie jest zły - przynajmniej Ty się czegoś dowiedziałeś, i ktoś kto to później będzie czytał - też :) A mało brakowało żebym i ja… ;)

  10. 10 Był Czwartek, 07 wrzesień 2006 roku gdy o godzinie 11:03 przyszedł Sija i stwierdził:

    Zawsze jeszcze można zrobić tak:

    class Aggregate extends ArrayObject { }
    :)

  11. 11 Była Niedziela, 24 czerwiec 2007 roku gdy o godzinie 12:23 przyszedł myGeeBlog » Blog Archive » Settery, gettery i inna magia i stwierdził:

    [...] PS. Kilka przykładów użycia magicznych właściwości znajduje się też w innym moim wpisie: Obiekt jako tablica (wprowadzenie do SPL). [...]

  12. 12 Był Poniedziałek, 30 lipiec 2007 roku gdy o godzinie 16:57 przyszedł Eluś i stwierdził:

    Sory, że odkopuję tak stary wpis, ale akurat przypadkiem na niego trafiłem.

    Przed chwilą właśnie potrzebowałem zaimplementować interfejs Iteratora w jednej ze swoich klas. I chciałem podzielić się swoimi spostrzeżeniami :)
    Jeśli klasa “opakowuje” tablicę (jak w Twoim przykładzie) to można skorzystać z interfejsu IteratorAggregate i dodać metodę:


    public function getIterator() {
    $tmp = new ArrayObject($this->data);
    return $tmp->getIterator();
    }

    O wiele mniej pisania niż w przypadku samodzielnej implementacji wszystkich 5 metod interfejsu Iterator :)

  13. 13 Była Środa, 01 sierpień 2007 roku gdy o godzinie 13:10 przyszedł MySZ i stwierdził:

    @Eluś:
    Różnica jest taka, że własne metody mogą być “leniwe”, mogą pobierać dane z bazy po jednym rekordzie, mogą robić przy okazji wiele innych rzeczy. W przypadku ArrayObject() musisz podać wszystkie elementy, czyli np wybrać z DBMS 4 mln rekordów… :)
    Tablica była przykładem na którym najprościej było mi coś pokazać :)

  14. 14 Była Środa, 04 czerwiec 2008 roku gdy o godzinie 23:54 przyszedł eyescream i stwierdził:

    Witaj! Trochę głupio mi odkopywać wpis sprzed dwóch lat, ale po przeczytaniu go wiedziałem za czym “googlać” to wypada podziękować. I się dopytać o drobny błąd.

    To było już wspomniane w komentarzach, ale czy masz poprawniejszą wersję metody valid() do implementacji Iteratora? Foreach się na mnie wypiął i wcale nie chciał się wykonywać, okazało się że pierwszy element tablicy (pobrany z bazy danych) ma wartość 0, jak to current() zwróci to koniec.

    Jutro będę się zastanawiał jak to obejść (na razie w akcie desperacji wstawiłem licznik kroków do next() i rewind(), ale to bzdura straszna). if(!isset(key($this->data)))? Hmm…

    I jakie są szanse na podobnie przejrzysty tutorial do RecursiveIterator(Iterator)? :)

  15. 15 Był Czwartek, 05 czerwiec 2008 roku gdy o godzinie 21:22 przyszedł MySZ i stwierdził:

    Żeby valid() działało chociaż w miarę sensownie, to trzeba zrobić podobnie jak to napisałeś, czyli coś a’la ‘licznik kroków’, tylko inaczej zaimplementować: dodaj do klasy właściwość $_pointer, i pracuj z nią zamiast z wbudowanymi funkcjami PHP. Metoda current () niech zwraca element będący na pozycji $this->data[$this->_pointer], rewind () zeruje $_pointer, a next () inkrementuje. W valid () dajesz wtedy sprawdzanie czy istnieje w $data klucz $_pointer.

    Co do tutoriala - to przecież jest prawie taki sam jak Iterator, tylko ma dwie dodatkowe metody, w sumie dość proste do implemetacji jak mnie się zdaje… :)

  16. 16 Był Czwartek, 05 czerwiec 2008 roku gdy o godzinie 21:28 przyszedł MySZ i stwierdził:

    Dodałem małe uaktualnienie na końcu artykułu.

  17. 17 Był Poniedziałek, 09 czerwiec 2008 roku gdy o godzinie 14:49 przyszedł eyescream i stwierdził:

    W sumie to się wczytałem i opitoliłem problem tak:
    public function valid()
    {
    return $this->current() !== false;
    }

    Mimo wszystko dzięki za szybką odpowiedź :)

  18. 18 Był Poniedziałek, 09 czerwiec 2008 roku gdy o godzinie 19:00 przyszedł MySZ i stwierdził:

    Nie załatwia Ci to przypadku gdy w tablicy masz wartość false.

  19. 19 Była Sobota, 20 grudzień 2008 roku gdy o godzinie 16:35 przyszedł s7h i stwierdził:


    public function valid () {
    return isset($this->aData[key($this->aData)]);
    }

A Ty? Co o tym myślisz?

Możesz używać w komentarzach następujących znaczników:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Sblam! Antyspam