- PVSM.RU - https://www.pvsm.ru -

Привет.
Так сложилось, что недавно мне в руки попала замечательная книжка Pro PHP [1], в которой целый раздел посвящен итераторам. Да, я знаю что на Хабре эта тема уже поднималась (и наверняка не раз), но все же позволю себе дописать данную статью, т.к. бОльшая часть примеров в вышеупомянутых статьях достаточно оторваны от реальности. И так — если Вам интересно какую же реальную задачу мы собираемся решать с помощью итераторов — добро пожаловать под кат.
По сути, итератор — это некоторый объект, который позволяет упростить специфический обход дочерних элементов. В php существует интерфейс Iterator [2], реализуя который можно добиться необходимого эффекта. В SPL (Standart PHP Library [3]) так же включены несколько классов реализующих наиболее распространенные и востребованные итераторы. Их список можно посмотреть здесь [4].
В php как-то исторически сложилось что перечисление объектов или данных просто «складывают» в массив, элементы которого потом можно перебирать. Представьте себе ситуацию, в которой у Вас есть данные некоторого поля элементов, представленного квадратом, разделенным на 9 равных частей (например карту). И Вам надо обойти все квадраты по часовой стрелке, а в массиве они сложены случайным образом. Не очень удобно, правда?
Так вот — именно в этом случае нам помогут итераторы. Вместо добавления элементов в массив, добавим их в итератор и потом сможем удобно их перебирать. Пример кода, реализующего перебор соседних элементов карты можно найти ниже:
/**
* @link https://bitbucket.org/t1gor/strategy/src/242e58cdcd60c61d02ae26d420da9d415117cb0d/application/model/map/MapTileNeighboursIterator.php?at=default
*/
class TileIterator implements Iterator
{
private $_side = 'north_west';
private $_neighbours = array();
private $_isValid = true;
public function __construct($neighboursArray)
{
$this->_side = 'north_west';
$this->_neighbours = $neighboursArray;
}
/**
* @return void
*/
function rewind() {
$this->_side = 'north_west';
}
/**
* @return MapTile
*/
function current() {
return $this->_neighbours[$this->_side];
}
/**
* @return string
*/
function key() {
return $this->_side;
}
/**
* Loop through neighbours clock-wise
*
* @return void
*/
function next()
{
switch ($this->_side)
{
case 'north_west':
$this->_side = 'north';
break;
case 'north':
$this->_side = 'north_east';
break;
case 'north_east':
$this->_side = 'east';
break;
case 'east':
$this->_side = 'south_east';
break;
case 'south_east':
$this->_side = 'south';
break;
case 'south':
$this->_side = 'south_west';
break;
case 'south_west':
$this->_side = 'west';
break;
// this is the end of a circle
case 'west':
$this->_isValid = false;
break;
}
}
function valid() {
return $this->_isValid;
}
}
А теперь собственно вызов:
// запрос не рассматриваем, т.к. это всего лишь пример
$tilesStmt = PDO::prepare("SELECT * FROM tiles ... LIMIT 9");
$tilesStmt->execute();
$tiles = new TileIterator($tilesStmt->fetchAll());
Ну а дальше — привычный всем перебор, только уже в правильном порядке:
foreach ($tiles as $tile) {
...
}
Так как тема достаточно обширна, рассмотрю только свои любимые примеры:
LimitIterator [5] очень удобно использовать при отладке или тестировании кода. В частности, при работе с PHPExcel [6], в переборе строк, библиотека использует класс RowIterator [7], имя которого подразумевает что это Iterator. Чтобы при разборе документа не «таскать» каждый раз все строки, можно обернуть RowIterator [7] в LimitIterator [5] и работать только с десятком строк:
// возьмем документ ...
$inputFileType = PHPExcel_IOFactory::identify('example.xlsx');
$objReader = PHPExcel_IOFactory::createReader($inputFileType);
$document = $objReader->load($inputFile);
$sheet = $document->getSheet(0);
// ... и получим только первые 10 строк
$dataForDebug = new LimitIterator($sheet->getRowIterator(), 0, 10);
Класс FilterIterator [8] позволяет легко фильтровать данные на лету. В каком-то роде это похоже на WHERE часть SQL запроса. Предположим, Вы работаете со сторонним API, например BaseCamp Classic API [9], SDK которого возвращает Вам объекты пользователей. И Вам нужно уведомить некоторых из них по emial об изменениях в проекте. А исключать Вам нужно будет по 3-м параметрам: email, ID и имя. Сделать это просто и поддерживаемо позволяет вышеупомянутый класс:
/**
* @link http://ua2.php.net/FilterIterator
*/
class NotificationFilter extends FilterIterator
{
/**
* Массив для хранения параметров фильтра
*/
private $_skip;
/**
* Build filter
*
* @param Iterator $iterator
* @param array $filter - массив данных о пользователях, которых надо исключить
* @throws InvalidArgumentException
*/
public function __construct(Iterator $iterator, $filter)
{
if (!is_array($filter)) {
throw new InvalidArgumentException("Filter should be an array. ".gettype($filter)." given.");
}
parent::__construct($iterator);
$this->_skip = $filter;
}
/**
* Check user data and make sure we can notify him/her
*
* Filtering by 2 params:
* - Does the user belong to your company (avoid spamming clients)?
* - Should we skipp the user based on the user ID
* - Should we skipp the user based on the user email
*
* @link http://php.net/manual/filteriterator.accept.php
* @link https://github.com/sirprize/basecamp/blob/master/example/basecamp/person/get-by-id
*
* @return bool
*/
public function accept()
{
// get current user from the Iterator
$bcUser = $this->getInnerIterator()->current();
// check if skipped by ID
$skippedById = in_array($bcUser->getId(), $this->_skip['byID']);
// or by email
$skippedByEmail = in_array($bcUser->getEmailAddress(), $this->_skip['byEmail']);
// check that he/she belongs to your company
$belongsToCompany = $yourCompanyBaseCampID === (int) $bcUser->getCompanyId()->__toString();
// notify only if belongs to your company and shouldn't be skipped
return $belongsToCompany && !$skippedById && !$skippedByEmail;
}
}
Таким образом в методе NotificationFilter::accept() мы работаем только с одним пользователем.
А еще можно легко приводить многомерные массивы к одномерным [10] с помощью RecursiveIteratorIterator [11], удобно получать файловые листинги директорий [12] с помощью RecursiveDirectoryIterator [13] и еще очень много всего.
Да, чуть не забыл. Пока я «игрался» с итераторами, пытаясь для себя понять как же их использовать, у меня возникла следующая идея — как бы мне на Хабре читать только посты, которые находятся и в хабе GameDev [14] и в Веб-разработка [15]? В ленте можно читать посты из обоих хабов, но не пересечение постов, если вы понимаете о чем я. В итоге у меня получился небольшой проектик с использованием итераторов.
Весь код проекта можно найти в репозитории на BitBucket [16], а здесь же я опубликую только самую интереснюу часть. Код ниже:
/**
* Basic post class
*/
class HabraPost {
public $name = '';
public $url = '';
public $hubs = null;
public static $baseUrl = 'http://habrahabr.ru/hub/';
/**
* Some hubs links
*/
protected static $fullHubList = array(
'infosecurity' => 'Информационная безопасность',
'webdev' => 'Веб-разработка',
'gdev' => 'Game Development',
'DIY' => 'DIY или Сделай сам',
'pm' => 'Управление проектами',
'programming' => 'Программирование',
'space' => 'Космонавтика',
'hardware' => 'Железо',
'algorithms' => 'Алгоритмы',
'image_processing' => 'Обработка изображений',
);
public function __construct($name, $url, $hubs = array())
{
$this->name = $name;
$this->url = $url;
$this->hubs = $hubs;
}
public static function getFullHubsList()
{
$list = self::$fullHubList;
asort($list);
return $list;
}
}
/**
* Post storage object
*
* @link http://php.net/manual/class.splobjectstorage.php
*/
class PostsStorage
{
private $_iterator;
public function __construct()
{
$this->_iterator = new SplObjectStorage();
}
/**
* Add new post
*
* @param HabraPost $post
* @return void
*/
public function save(HabraPost $post)
{
// reduce duplicates
if (!$this->_iterator->contains($post)) {
$this->_iterator->attach($post);
}
}
/**
* Get internal iterator
*
* @return SplObjectStorage
*/
public function getIterator()
{
return $this->_iterator;
}
}
/**
* Posts filtering class
*
* @link http://php.net/manual/class.filteriterator.php
*/
class HabraPostFilter extends FilterIterator
{
/**
* Hubs to filter by
*/
private $_filterByHubs = array();
public function __construct(Iterator $iterator, $filteringHubs)
{
parent::__construct($iterator);
$this->_filterByHubs = $filteringHubs;
}
/**
* Accept
*
* @link http://php.net/manual/filteriterator.accept.php
* @return bool
*/
public function accept()
{
$object = $this->getInnerIterator()->current();
$aggregate = true;
foreach ($this->_filterByHubs as $filterHub) {
$aggregate = $aggregate && in_array($filterHub, $object->hubs);
}
return $aggregate;
}
}
Итак — идея очень проста:
PostsStorage,HabraPostFilterВ итоге получаем нечто подобное скриншоту:

Буду рад выложить проект в свободный доступ если кто-нибудь любезно предоставит , способный выдержать Хабра-эффект.
Всем спасибо за внимание.
P.S. С удовольствием приму правки/замечания в комментариях к посту или в личной переписке.
UPD. Спасибо Nikita_Rogatnev [18] за помощь в исправлении опечаток.
Автор: t1gor
Источник [19]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/56510
Ссылки в тексте:
[1] Pro PHP: http://www.amazon.com/Pro-PHP-Patterns-Frameworks-Testing/dp/1590598199
[2] Iterator: http://www.php.net/manual/ru/class.iterator.php
[3] Standart PHP Library: http://php.net/spl
[4] здесь: http://www.php.net/manual/ru/spl.iterators.php
[5] LimitIterator: http://www.php.net/manual/ru/class.limititerator.php
[6] PHPExcel: http://phpexcel.codeplex.com/
[7] RowIterator: https://github.com/markushausammann/PHPExcel/blob/master/PHPExcel/Worksheet/RowIterator.php
[8] FilterIterator: http://www.php.net/manual/ru/class.filteriterator.php
[9] BaseCamp Classic API: https://github.com/sirprize/basecamp
[10] приводить многомерные массивы к одномерным: http://stackoverflow.com/a/1320259/1847769
[11] RecursiveIteratorIterator: http://www.php.net/manual/ru/class.recursiveiteratoriterator.php
[12] файловые листинги директорий: http://www.php.net/manual/en/class.recursivedirectoryiterator.php#97228
[13] RecursiveDirectoryIterator: http://www.php.net/manual/en/class.recursivedirectoryiterator.php
[14] GameDev: http://habrahabr.ru/hub/gdev/
[15] Веб-разработка: http://habrahabr.ru/hub/webdev/
[16] репозитории на BitBucket: https://bitbucket.org/t1gor/habraintersector
[17] хостинг: https://www.reg.ru/?rlink=reflink-717
[18] Nikita_Rogatnev: http://habrahabr.ru/users/nikita_rogatnev/
[19] Источник: http://habrahabr.ru/post/214833/
Нажмите здесь для печати.