📜 ⬆️ ⬇️

What do gamedevs have in common with astronautics or work with PHP iterators?



Hi, Habr.

It so happened that I recently got into the hands of a wonderful book Pro PHP , in which a whole section is devoted to iterators. Yes, I know that this topic has already been raised on Habré (and probably more than once), but I will allow myself to finish this article, since Most of the examples in the above-mentioned articles are quite divorced from reality. And so - if you are interested in what a real problem we are going to solve with the help of iterators - welcome under cat.

What kind of animal is this - an iterator?


Essentially, an iterator is an object that allows you to simplify a specific traversal of child elements. In php there is an interface Iterator , implementing which you can achieve the desired effect. The SPL ( Standart PHP Library ) also includes several classes that implement the most common and popular iterators. Their list can be found here .
')

So why do we then need iterators - can we just use arrays?


In php, it has somehow historically developed that the enumeration of objects or data simply “adds” to an array, the elements of which can then be searched. Imagine a situation in which you have data of some field of elements represented by a square, divided into 9 equal parts (for example, a map). And you need to bypass all the squares in a clockwise direction, and in the array they are folded randomly. Not very comfortable, right?

And so - in this case iterators will help us. Instead of adding elements to the array, add them to the iterator and then we can conveniently sort through them. An example of code that implements the enumeration of neighboring map elements can be found below:

/** * @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; } } 

And now the actual call:
 //   , ..     $tilesStmt = PDO::prepare("SELECT * FROM tiles ... LIMIT 9"); $tilesStmt->execute(); $tiles = new TileIterator($tilesStmt->fetchAll()); 

Well, then - the usual bust of all, but already in the correct order:
 foreach ($tiles as $tile) { ... } 

Yes, really not bad. And what else can you do with the iterator?


Since the topic is quite extensive, I will consider only my favorite examples:

LimitIterator is very useful when debugging or testing code. In particular, when working with PHPExcel , in the search for rows, the library uses the RowIterator class, whose name implies that it is an Iterator. To avoid “dragging” all the lines every time you parse the document, you can wrap the RowIterator in the LimitIterator and work with only a dozen lines:

 //   ... $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); 

The FilterIterator class makes it easy to filter data on the fly. In some way, this is similar to the WHERE part of the SQL query. Suppose you are working with a third-party API, for example, the BaseCamp Classic API , whose SDK returns to you user objects. And you need to notify some of them by emial about changes in the project. And you will need to exclude according to 3 parameters: email, ID and name. To make it simple and supported, the above class allows:

 /** * @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; } } 

Thus, in the NotificationFilter :: accept () method, we work with only one user.

And, you can easily convert multidimensional arrays to one-dimensional using RecursiveIteratorIterator , conveniently get directory listings using RecursiveDirectoryIterator, and much more.

And where does astronautics?


Yes, I almost forgot. While I was “playing” with iterators, trying to understand for myself how to use them, I had the following idea - how could I read only posts on Habré that are in the GameDev hub and in Web development ? In the tape, you can read posts from both hubs, but not the intersection of posts, if you know what I mean. As a result, I got a small project using iterators.

The entire project code can be found in the repository on BitBucket , and here I will publish only the most interesting part. Code below:
 /** * 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; } } 

So - the idea is very simple:
  1. The user selects one or more hubs,
  2. We iterate over the available Habr pages and collect links to content,
  3. We drive all this into the PostsStorage ,
  4. And filter using HabraPostFilter

As a result, we get something similar to the screenshot:

GameDev + Web Development


I would be happy to put the project in free access if someone kindly provides hosting that can withstand the Habra effect.

Thank you all for your attention.

PS I am pleased to accept the edits / comments in the comments to the post or in personal correspondence.

UPD . Thank you Nikita_Rogatnev for help in correcting typos.
UPD . Thank you hell0w0rd for the demo .

Source: https://habr.com/ru/post/214833/


All Articles