Что общего у gamedev-а с космонавтикой?

от автора

Привет, Хабр.

Так сложилось, что недавно мне в руки попала замечательная книжка 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;     } } 

Итак — идея очень проста:

  1. Пользователь выбирает один или несколько хабов,
  2. Мы перебираем доступные страницы Хабра и собираем ссылки на контент,
  3. Загоняем все это в PostsStorage,
  4. И фильтруем с помощью HabraPostFilter

В итоге получаем нечто подобное скриншоту:

GameDev + Веб разработка

Буду рад выложить проект в свободный доступ если кто-нибудь любезно предоставит хостинг, способный выдержать Хабра-эффект.

Всем спасибо за внимание.

P.S. С удовольствием приму правки/замечания в комментариях к посту или в личной переписке.

ссылка на оригинал статьи http://habrahabr.ru/post/214833/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *