Итерирование объектов в php. Встроенные классы-итераторы. Объект как массив

Класс часто представляет собой контейнер для хранения целого набора каких-то однотипных данных, в том числе и экземпляров других классов. В этом случае неплохо было бы, чтобы такой класс-контейнер предоставлял удобный интерфейс для доступа к этому набору данных. Это чем-то похоже на обычный массив, только в нашем случае механизм доступа к объектам, хранящемся в нашем контейнере, мы реализуем сами. Так же желательно, чтобы методы для доступа к содержимому классов-контейнеров были стандартизированы, то есть имели одинаковое имя для многих классов-контейнеров. Такого можно добиться при помощи интерфейсов, которые обязывают реализовывать описываемые ими методы в классах, которые имплементируют эти интерфейсы. Так же прекрасно было бы, если механизм доступа к содержимому был интегрирован в язык, то есть доступ к элементам внутреннего набора, их перебор и прочие действия осуществлялись бы так же, как в случае с массивом.

Простой перебор свойств объекта

В php существует возможность перебрать доступные свойства объекта. Для этого достаточно просто передать ссылку на объект в цикл foreach. При этом будут перебираться только те свойства, которые доступны в данной области видимости: внутри класса будет возможно перебрать абсолютно все свойства, в потомках класса — public и protected свойства, в остальных ситуациях — только публичные свойства. Для демонстрации этой возможности давайте создадим простой класс и его наследника.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Листинг 1.1 : Классы со свойствами для перебора
// Файл ItemsClasses.php
 
class ItemsParent {
  public $item1 = 'public item1';
  public $item2 = 'public item2';
 
  protected $item3 = 'protected item3';
 
  private $item4 = 'private item4';
 
  // Метод для перебора свойств внутри класса
  public function iterateItems() {
    foreach ($this as $key => $item) {
      echo "{$key} => {$item}<br />";
    }
  }
}
 
class ItemsChild extends ItemsParent {
  // Метод для перебора свойств наследника класса
  public function iterateItems() {
    foreach ($this as $key => $item) {
      echo "{$key} => {$item}<br />";
    }
  }
}

Теперь переберём свойства экземпляров этих классов и посмотрим, какие из них будут доступны.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Листинг 1.2 : Простой перебор свойств объекта
// Файл simpleObjectIteration.php
 
$itemsParent = new ItemsParent();
 
  ?><h4>Все свойства: </h4><?
$itemsParent->iterateItems();
 
$itemsChild = new ItemsChild();
 
  ?><h4>Все protected и public свойства: </h4><?
$itemsChild->iterateItems();
 
  ?><h4>Только public свойства: </h4> <? 
foreach ($itemsParent as $key => $item) {
  echo "{$key} => {$item}<br />";
}

Результат выполнения этого кода:

Все свойства:

item1 => public item1
item2 => public item2
item3 => protected item3
item4 => private item4

Все protected и public свойства:

item1 => public item1
item2 => public item2
item3 => protected item3

Только public свойства:

item1 => public item1
item2 => public item2

Такой подход плох тем, что при нём нет возможности чётко контролировать механизм переборки свойств объекта. Кроме того, в этом случае, каждый элемент набора данных, хранимых классом, приходится содержать в отдельном свойстве, что затруднительно, если класс призван содержать достаточно большие наборы однотипных данных. Можно, конечно, хранить их в поле-массиве, но тогда придётся писать два цикла foreach. Устранить эти сложности можно, если реализовать какой-нибудь интерфейс из потомков встроенного интерфейса Traversable.

Интерфейс Traversable

Это пустой интерфейс, который напрямую реализуют только встроенные классы, содержимое экземпляров которых можно перебирать при помощи foreach и each. Пользовательский класс не может непосредственно реализовывать этот интерфейс, но может реализовать его потомков, таких как Iterator, IteratorAggregate и других, при этом проверка на принадлежность интерфейсу Traversable при помощи оператора instanceof будет успешной, так же экземпляр этого класса можно будет использовать в качестве параметра метода с уточнённым типом Traversable. Таким образом, этот интерфейс может служить своего рода маркером, который говорит, что содержимое объекта можно перебирать. Механизм перебора же зависит от применённого интерфейса и его реализации.

Интерфейс Iterator

Содержимое реализующих этот интерфейс объектов может быть перебрано при помощи foreach или each. Интерфейс Iterator содержит методы:

  • сurrent – возвращает текущий элемент
  • key – возвращает позицию текущего элемента
  • next – сдвигает позицию итератора к следующему элементу
  • rewind – устанавливает итератор на первый элемент
  • valid – проверяет корректность текущей позиции

Давайте создадим класс MessageList реализующий интерфейс Iterator и описывающий набор сообщений, представленных экземплярами класса Message.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Листинг 2.1 : Класс, реализующий интерфейс Iterator.
// Файл MessageList.php 
 
require_once 'Message.php';
 
class MessageList implements Iterator {
  private $position = 0;
  private $messages;
 
  public function __construct(array $messages) {    
    $this->messages = $messages;
  }
 
  public function current() {
    return $this->messages[$this->position];
  }
 
  public function next() {
    ++$this->position;
  }
 
  public function valid() {
    return isset($this->messages[$this->position]);
  }
 
  public function key() {
    return $this->position;
  }
 
  public function rewind() {
    $this->position = 0;
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Листинг 2.2 : Класс, описывающий сообщение.
// Файл Message.php
 
class Message {
  private $date;
  private $author;
  private $text;
 
  public function __construct(DateTime $date, $author, $text) {
    $this->date = $date;
    $this->author = $author;
    $this->text = $text;
  }
 
  public function getAuthor() {
    return $this->author;
  }
 
  public function getText() {
    return $this->text;
  }
 
  public function getFormatDate($format) {
    return $this->date->format($format);
  }
 
  public function __toString() {
    return "[{$this->getFormatDate('H:i:s')}] ".
      "{$this->getAuthor()}: {$this->getText()}";
  }
}

Теперь список сообщений, хранящийся в классе MessageList, можно перебрать при помощи foreach.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Листинг 2.3 : Итерирование экземпляра класса MessageList
// Файл messageListIteration.php
 
require_once 'MessageList.php';
 
$now = new DateTime();
 
$tenSecondsLater = new DateTime();
$tenSecondsLater->modify("+10 seconds");
 
$twelveSecondsLater = new DateTime();
$twelveSecondsLater->modify("+12 seconds");
 
$thirtenSecondsLater = new DateTime();
$thirtenSecondsLater->modify("+13 seconds");
 
$messages = array (
  new Message($now, "Bob", "Hello!"),
  new Message($tenSecondsLater, "Mary", "hi!"),
  new Message($twelveSecondsLater, "Bob", "How are you?"),
  new Message($thirtenSecondsLater, "Mary", "good thx")
);
 
$messageList = new MessageList($messages);
 
foreach ($messageList  as $message) {
  echo "{$message}<br />";
}

Результат выполнения этого будет что-то вроде этого:

[11:20:48] Bob: Hello!
[11:20:58] Mary: hi!
[11:21:00] Bob: How are you?
[11:21:01] Mary: good thx

Порядок вызова методов интерфейса Iterator при использовании оператора foreach

При переборе содержимого класса оператором foreach происходит автоматический вызов реализованных методов интерфейса Iterator. Знание порядка их вызова поможет вам в дальнейшем оптимизировать ваш код и понять причину возможных ошибок.

Перед началом перебора вызывается метод rewind, устанавливающий итератор на начальную позицию. Затем, для проверки корректности положения итератора, происходит вызов метода valid. Затем, при помощи вызова метода current, берётся текущий элемент. После этого происходит выполнение тела цикла и сдвиг позиции итератора на единицу вперёд при помощи вызова метода next. На следующей итерации цикла порядок вызова повторяется: valid, current, выполнение тела цикла, next. Так происходит до тех пор, пока метод valid не вернёт false. Для иллюстрации всего этого напишем небольшой пример.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Листинг 3.1 : Класс, для демонстрации порядка вызова методов интерфейса Iterator
// Файл IterableClass.php
 
class IterableClass implements Iterator {
  private $position;
  private $items = array (
    'one', 'two', 'three'
  );
 
  public function rewind() {
    echo "Вызов rewind: устанавливаем итератор на начальную позицию<br />";
    $this->position = 0;
  }
 
  public function current() {
    echo "Вызов current: получаем текущий элемент<br />";
    return $this->items[$this->position];
  }
 
  public function next() {
    echo "Вызов next: сдвигаем итератор на один элемент вперёд<br />";
    ++$this->position;
  }
 
  public function key() {
    echo "Вызов key: получаем текущую позицию<br />";
    return $this->position;
  }
 
  public function valid() {
    $isValid = isset($this->items[$this->position]);
    echo "Вызов valid: проверка корректность текущей позиции - (" .  
      ($isValid ? 'оk' : 'fail') . ") <br />";
 
    return $isValid;
  }
}

1
2
3
4
5
6
7
8
9
10
// Листинг 3.2 : Порядок вызова методов интерфейса Iterator
// Файл callOrder.php
 
require_once 'IterableClass.php';
 
$iterableInstance = new IterableClass();
 
foreach ($iterableInstance as $item) {
  echo "Выполняем тело цикла. Текущий элемент: \"{$item}\"<br /><br />";
}

Результатом выполнения этого кода будет:

Вызов rewind: устанавливаем итератор на начальную позицию
Вызов valid: проверка корректность текущей позиции — (оk)
Вызов current: получаем текущий элемент
Выполняем тело цикла. Текущий элемент: «one»

Вызов next: сдвигаем итератор на один элемент вперёд
Вызов valid: проверка корректность текущей позиции — (оk)
Вызов current: получаем текущий элемент
Выполняем тело цикла. Текущий элемент: «two»

Вызов next: сдвигаем итератор на один элемент вперёд
Вызов valid: проверка корректность текущей позиции — (оk)
Вызов current: получаем текущий элемент
Выполняем тело цикла. Текущий элемент: «three»

Вызов next: сдвигаем итератор на один элемент вперёд
Вызов valid: проверка корректность текущей позиции — (fail)

SeekableIterator

В этом интерфейсе есть единственный метод seek, который должен возвращать элемент по позиции. Давайте реализуем этот интерфейс в нашем классе MessageList. Так как SeekableIterator является наследником интерфейса Iterator, то все классы, реализующие SeekableIterator, должны реализовать не только метод seek, но и методы интерфейса Iterator. В нашем случае достаточно просто заменить имя имплементируемого классом MessageList интерфейса на SeekableIterator и реализовать его метод seek.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Листинг 4.1 : Класс, реализующий интерфейс SeekableIterator.
// Файл SeekableMessageList.php 
 
require_once 'Message.php';
 
class MessageList implements SeekableIterator {
  private $position = 0;
  private $messages;
 
  public function __construct(array $messages) {
    $this->messages = $messages;
  }
 
  /**
   * Возвращает элемент по его позиции.
   *
   * @param int позиция элемента
   * @return Message
   */
 
  public function seek($position) {
    return $this->messages[$position];
  }
 
  public function current() {
    return $this->messages[$this->position];
  }
 
  public function next() {
    ++$this->position;
  }
 
  public function valid() {
    return isset($this->messages[$this->position]);
  }
 
  public function key() {
    return $this->position;
  }
 
  public function rewind() {
    $this->position = 0;
  }
}

Теперь можно получать сообщение по его порядковому номеру.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Листинг 4.2 : Получения элемента внутреннего набора по индексу
// Файл getMessageByIndex.php
 
require_once 'SeekableMessageList.php';
 
$now = new DateTime();
 
$tenSecondsLater = new DateTime();
$tenSecondsLater->modify("+10 seconds");
 
$twelveSecondsLater = new DateTime();
$twelveSecondsLater->modify("+12 seconds");
 
$thirtenSecondsLater = new DateTime();
$thirtenSecondsLater->modify("+13 seconds");
 
$messages = array (
    new Message($now, "Bob", "Hello!"),
    new Message($tenSecondsLater, "Mary", "hi!"),
    new Message($twelveSecondsLater, "Bob", "How are you?"),
    new Message($thirtenSecondsLater, "Mary", "good thx")
);
 
$messageList = new MessageList($messages);
 
echo "#1 {$message=>seek(0)}<br />#2 {$message->seek(1)}";

Примеры встроенных классов-итераторов

Встроенных классов-итераторов с уже реализованным функционалом для итерирования в php существует превеликое множество. Мы поверхностно рассмотрим лишь некоторые самые интересные и полезные, на мой взгляд.

AppendIterator

Этот итератор позволяет выполнять сразу несколько итераторов. Это позволяет перебрать содержимое нескольких экземпляров в одном цикле foreach. Для добавления итерируемых экземпляров используется метод append этого класса. Класс добавляемого элемента должен реализовывать интерфейсc Iterator.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Листинг 5 : Пример использования класса AppendIterator
// Файл useAppendIterator.php
 
require_once 'MessageList.php';
 
$now = new DateTime();
 
$tenSecondsLater = new DateTime();
$tenSecondsLater->modify("+10 seconds");
 
$twelveSecondsLater = new DateTime();
$twelveSecondsLater->modify("+12 seconds");
 
$thirtenSecondsLater = new DateTime();
$thirtenSecondsLater->modify("+13 seconds");
 
$oneMessagesArray = array (
    new Message($now, "Bob", "Hello!"),
    new Message($tenSecondsLater, "Mary", "hi!")
);
 
$twoMessagesArray = array (
    new Message($twelveSecondsLater, "Bob", "How are you?"),
    new Message($thirtenSecondsLater, "Mary", "good thx")
);
 
$appendIterator = new AppendIterator();
$appendIterator->append(new MessageList($oneMessagesArray));
$appendIterator->append(new MessageList($twoMessagesArray));
 
foreach ($appendIterator as $message) {
    echo "{$message}<br />";        
}

Результат выполнения этого кода полностью аналогичен результату выполнения кода из листинга 3.

CallbackFilterIterator

Этот класс-итератор появился в php 5.4. Он является наследником абстрактного класса FilterIterator и позволяет отфильтровывать перебираемые данные. В качестве параметра конструктор класса CallbackFilterIterator принимает итерируемый экземпляр, класс которого должен реализовывать интерфейс Iterator, и функцию-фильтр. Функция-фильтр принимает в качестве параметров текущий элемент из итерируемого набора, его позицию и итерируемый объект.

В качестве демонстрации работы этого класса давайте отфильтруем вначале сообщения Боба, а затем сообщения Мари.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php
// Листинг 6 : Пример использования класса CallbackFilterIterator
// Файл useCallbackFilterIterator.php
// Требуется php >= 5.4!!!
 
require_once 'MessageList.php';
 
/* function isBobMessage(Message $message) {
  return $message->getAuthor() == 'Bob';
}
 
function isMaryMessage(Message $message) {
  return $message->getAuthor() == 'Mary';
} */
 
$now = new DateTime();
 
$tenSecondsLater = new DateTime();
$tenSecondsLater->modify("+10 seconds");
 
$twelveSecondsLater = new DateTime();
$twelveSecondsLater->modify("+12 seconds");
 
$thirtenSecondsLater = new DateTime();
$thirtenSecondsLater->modify("+13 seconds");
 
$messages = array (
  new Message($now, "Bob", "Hello!"),
  new Message($tenSecondsLater, "Mary", "hi!"),
  new Message($twelveSecondsLater, "Bob", "How are you?"),
  new Message($thirtenSecondsLater, "Mary", "good thx")
);
 
$messageList = new MessageList($messages);
 
/* $bobMessageIterator = new CallbackFilterIterator($messageList, 'isBobMessage');
$maryMessageIterator = new CallbackFilterIterator($messageList, 'isMaryMessage'); */
 
$bobMessageIterator = new CallbackFilterIterator($messageList, function ($message) {
  return $message->getAuthor() == 'Bob';
});
 
$maryMessageIterator = new CallbackFilterIterator($messageList, function ($message) {
  return $message->getAuthor() == 'Mary';
});
 
?><h4>Сообщения Боба</h4><?
foreach ($bobMessageIterator as $bobMessage) {
  echo "{$bobMessage}<br />";
}
 
?><h4>Сообщения Мари</h4><?
foreach ($maryMessageIterator as $maryMessage) {
  echo "{$maryMessage}<br />";
}
?>

Если вы не знаете про анонимные функции или вам сложно воспринять то, что функция передаётся методу в качестве параметра, то обратите внимания на закомментированный код – он делает тоже, что происходит при передаче методу функции в качестве параметра.

DirectoryIterator

Эти класс призван выполнять более узкую задачу – перебирать каталоги и файлы. В качестве параметра конструкторы этих классов принимают строку с путём к каталогу, содержание которого мы хотим итерировать. Класс DirectoryIterator унаследовал от класса SplFileInfo множество методов, которые позволяют узнавать свойства и типы файлов и каталогов.

В качестве демонстрации работы этого класса давайте переберём файлы каталога, в котором запустится скрипт.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
// Листинг 6 : Пример использования класса DirectoryIterator
// Файл useDirectoryIterator.php
 
$directoryIterator = new DirectoryIterator('.');
  ?><table>
    <tr>
      <th>Имя файла</th>
      <th>Размер файла</th>
    </tr><?
foreach ($directoryIterator as $fileInfo) {
  if ($fileInfo->isFile()) {
    ?><tr>
        <td><?=$fileInfo->getFileName(); ?></td>
        <td><?=$fileInfo->getSize(); ?></td>
      </tr><?
  }
}
?>
</table>

Для удобного рекурсивного перебора вложенных каталогов существует класс RecursiveDirectoryIterator.

IteratorAggregate

Если вы хотите использовать аналогичный механизм для итерации экземпляров нескольких классов, то имеет смысл вынести его реализацию в отдельный класс-итератор, чтобы не дублировать её в каждом итерабельном классе. В таком случае экземпляр класса-итератора нужно получать или хранить в классе, содержимое объектов которого необходимо будет итерировать. Интерфейс IteratorAggreagte как раз и описывает метод для получения экземпляра класса-итератора. Класс-итератор вы можете реализовать сами, а можете воспользоваться уже имеющимися в php реализациями. Некоторые их них мы уже рассмотрели выше.

Для примера давайте реализуем интерфейс IteratorAggregate, а в качестве класса-итератора возьмём самый обычный итератор по массиву ArrayIterator. Его бывает удобно использовать, когда сам итератор необходимо использовать отдельно от массива.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Листинг 7 : Класс, реализующий интерфейс IteratorAggregateIterator.
// Файл IteratorAggregateMessageList.php 
 
require_once 'Message.php';
 
class MessageList implements IteratorAggregate {
  private $messages;
 
  public function __construct(array $messages) {    
    $this->messages = $messages;
  }
 
  public function getIterator() {
    return new ArrayIterator($this->messages);
  }
}

Пример использование этого класса такой же, как в листинге 2.3.

Этот пример так же показывает, что незачем реализовывать простые итераторы самому – для этого уже существует достаточно встроенных классов. Предыдущие примеры, в большей части, показывали собственный способы реализация для того, чтобы вы поняли, как реализовать нужный вам механизм итерации, если встроенные классы вам по какой-то причине не подойдут.

ArrayAccess

Чтобы полностью обеспечить работу с экземпляром класса, как с массивом можно использовать интерфейс ArrayAccess, содержащий методы для получения, изменения, удаления и проверки существования элемента по его индексу. При этом синтаксис для осуществления доступа ничем не будет отличаться от синтаксиса для работы с массивом.

Этот интерфейс содержит следующие методы:

  • offsetExists – принимает в качестве параметра индекс элемента и возвращает булево значение, показывающее, существует ли элемент с таким индексом
  • offsetUnset – удаляет элемент по его индексу
  • offsetGet – возвращает элемент с индексом, указанным в качестве параметра
  • offsetSet – изменяет значение элемента. Принимает в качестве параметра индекс изменяемого элемента и его новое значение

В качестве примера давайте реализуем этот интерфейс в классе MessageList (листинг 2.1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Листинг 8.1 : Класс MessageList реализующий интерфейс ArrayAccess
// Файл ArrayAccessMessageList.php
 
require_once 'Message.php';
 
class MessageList implements ArrayAccess {
  private $messages;
 
  public function __construct(array $messages) {    
    $this->messages = $messages;
  }
 
  public function offsetExists($i) {
    return isset($this->messages[$i]);
  }
 
  public function offsetUnset($i) {
    unset($this->messages[$i]);
  }
 
  public function offsetGet($i) {
    return $this->messages[$i];
  }
 
  public function offsetSet($i, $message) {
    $this->messages[$i] = $message;
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Листинг 8.2 : Получения сообщения по индексу с использованием синтаксиса массива
// Файл useArrayAccessMessageList.php
 
require_once 'ArrayAccessMessageList.php';
 
$now = new DateTime();
 
$tenSecondsLater = new DateTime();
$tenSecondsLater->modify("+10 seconds");
 
$messages = array (
  new Message($now, "Bob", "Hello!"),
  new Message($tenSecondsLater, "Mary", "hi!")
);
 
$messageList = new MessageList($messages);
 
echo "Первое сообщение: {$messageList[0]}<br />";
$messageList[0] = new Message($now, "Bob", "hi");
echo "Изменённое первое сообщение: {$messageList[0]}<br />";

Обратите внимание, что этот интерфейс не является наследником Traversable и не даёт возможности перебирать содержимое экземпляра класса, который его реализовывает, при помощи foreach.

Countable и Serializable

После того, как в классе был реализован интерфейс ArrayAccess, он всё ещё не ведёт себя полностью как массив. В частности функция count возвращает для экземпляра этого класса единицу. Для решения этой проблемы существует интерфейс countable. Этот интерфейс содержит единственный метод сount, возвращаемый результат которого будет расцениваться как результат работы нативной функции count. Для того чтобы заставить наш экземпляры нашего класса ещё и серилиазаваться как массив, можно использовать интерфейс serializable c методами serialize и unserialize.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Листинг 9.1 : Класс, реализующий интерфейсы countable и serializable
// Файл CountableSerializableMessageList.php
 
require_once 'Message.php';
 
class MessageList implements ArrayAccess, Countable, Serializable {
  private $messages;
 
  public function __construct(array $messages) {    
    $this->messages = $messages;
  }
 
  public function offsetExists($i) {
    return isset($this->messages[$i]);
  }
 
  public function offsetUnset($i) {
    unset($this->messages[$i]);
  }
 
  public function offsetGet($i) {
    return $this->messages[$i];
  }
 
  public function offsetSet($i, $message) {
    $this->messages[$i] = $message;
  }
 
  public function count() {
    return count($this->messages);
  }
 
  public function serialize() {
    return serialize($this->messages);
  }
 
  public function unserialize($data) {
    $this->messages = unserialize($data);
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Листинг 9.2 : Вызов функции count c экземпляром класса, 
// реализующего countable в качестве параметра.
// сериализация и десириализация
// Файл useCountableSerializbale.php
 
require_once 'CountableSerializableMessageList.php';
 
$now = new DateTime();
 
$tenSecondsLater = new DateTime();
$tenSecondsLater->modify("+10 seconds");
 
$twelveSecondsLater = new DateTime();
$twelveSecondsLater->modify("+12 seconds");
 
$thirtenSecondsLater = new DateTime();
$thirtenSecondsLater->modify("+13 seconds");
 
$messages = array (
  new Message($now, "Bob", "Hello!"),
  new Message($tenSecondsLater, "Mary", "hi!"),
  new Message($twelveSecondsLater, "Bob", "How are you?"),
  new Message($thirtenSecondsLater, "Mary", "good thx")
);
 
$messageList = new MessageList($messages);
 
echo "Количество сообщений: " . count($messageList) . "<br />"; // 4
// сериализуем
$data  = serialize($messageList);
// десериализуем
$messageList = unserialize($data);
// объект десериализован и может использоваться как и ранее
echo $messageList[2]; // [12:18:22] Bob: How are you?

Зачем это нужно

Возможно, вам покажется странным и ненужным создание подобной обёртки – ведь с таким же успехом сообщения можно было бы хранить просто в массиве, но класс, в отличие от массива, можно дополнить дополнительным функционалом, который сделает работу с набором сообщений более удобной и безопасной. К примеру, можно сделать так, что этот класс гарантировано будет хранить только сообщения, а не данные какого-либо другого типа. Для этого давайте создадим простой класс-исключение, которое будет генерироваться, если в массиве найдётся элемент, который не будет являться экземпляром класса Message.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Листинг 10.1 : Класс-исключение, которое будет генерироваться 
// если аргументы метода (функции) некорректны
// файл IllegalArgumentException.php
 
class IllegalArgumentException extends Exception {
  public function __contsruct($message) {
    if (is_string($message)) {
      $this->message = $message;
    } else {
      $this->message = "Illegal argument";
    }
  }
}

Теперь немного модифицируем конструктор класса Message. Не забудьте только подключить файл IllegalArgumentException.php.

// Листинг 10.2 : Модифицированный конструктор класса MessageList (см листинг 2.1)
 
public function __construct(array $messages) {
  foreach ($messages as $message) {
    if (!($message instanceof Message)) {
      throw new IllegalArgumentException(
              "Array of messages must be contains only onstance of class Message");
    }
  }
  $this->messages = $messages;
}

Таким образом, теперь можно быть уверенным, что перебирая содержимое экземпляра класса MessageList, мы перебираем только сообщения, а не что-то другое. В случае с массивом этого нельзя было сказать точно.

Второй причиной использования подхода является то, что экземпляры итерабельных классов и классов-массивов можно будет использовать там, где происходила работа с обычными массивами и дополнительного кода для обработки экземпляров классов-контейнеров писать будет не нужно. При этом, код будет более безопасным, ведь доступ к внутренним наборам итерабельных классов мы можем контролировать.

На это всё. Успехов вам!

Скачать исходники

  • bairon

    ПХП вообще серьёзная штука, пока не начнёшь работать, не поймёшь никогда!
    http://web-rubik.ru создание базы данных MySQL, таблиц и управление б-д