Привет, Хабр.
Так сложилось, что недавно мне в руки попала замечательная книжка Pro PHP, в которой целый раздел посвящен итераторам. Да, я знаю что на Хабре эта тема уже поднималась (и наверняка не раз), но все же позволю себе дописать данную статью, т.к. бОльшая часть примеров в вышуепомянутых статьях достаточно оторваны от реальности. И так — если Вам интересно какую же рельную задачу мы собираемся решать с помощью итераторов — добро пожаловать под кат.
Что же это за зверь такой — итератор?
По сути, итератор — это некоторый объект, который позволяет упростить специфический обход дочерних элементов. В php существует интерфейс Iterator, реализуюя который можно добиться необходимого эффекта. В SPL (Standart PHP Library) так же включены несколько классов реализующих наиболее распространенные и востребованные итераторы. Их список можно посмотреть здесь.
Так зачем нам тогда итераторы — можно же просто массивы использовать?
В 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 очень удобно использовать при отладке или тестировании кода. В частности, при работе с PHPExcel, в переборе строк, библиотека использует класс RowIterator, имя которого подразумевает что это Iterator. Чтобы при разборе документа не «таскать» каждый раз все строки, можно обернуть RowIterator в LimitIterator и работать только с десятком строк:
// возьмем документ ... $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 позволяет легко фильтровать данные на лету. В каком-то роде это похоже на WHERE часть SQL запроса. Предположим, Вы работаете со сторонним API, например BaseCamp Classic API, 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() мы работаем только с одним пользователем.
А еще можно легко приводить многомерные массивы к одномерным с помощью RecursiveIteratorIterator, удобно получать файловые листинги директорий с помощью RecursiveDirectoryIterator и еще очень много всего.
А причем тут космонавтика?
Да, чуть не забыл. Пока я «игрался» с итераторами, пытаясь для себя понять как же их использовать, у меня возникла следующая идея — как бы мне на Хабре читать только посты, которые находятся и в хабе GameDev и в Веб-разработка? В ленте можно читать посты из обоих хабов, но не пересечение постов, если вы понимаете о чем я. В итоге у меня получился небольшой проектик с использованием итераторов.
Весь код проекта можно найти в репозитории на BitBucket, а здесь же я опубликую только самую интереснюу часть. Код ниже:
/** * 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. С удовольствием приму правки/замечания в комментариях к посту или в личной переписке.
ссылка на оригинал статьи http://habrahabr.ru/post/214833/
Добавить комментарий