What does gamedev have in common with astronautics or work with iterators in PHP



Hi, Habr.

It so happened that recently I 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 nevertheless I will allow myself to add this article, because Most of the examples in the above articles are quite divorced from reality. And so - if you are interested in what real problem we are going to solve with the help of iterators - welcome to cat.

What kind of animal is iterator?


In fact, an iterator is an object that allows you to simplify the specific traversal of child elements. There is an Iterator interface in php , implementing which you can achieve the desired effect. 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 need iterators then - can we just use arrays?


In php, it has somehow historically happened that an enumeration of objects or data is simply “added” to an array, the elements of which can then be sorted out later. Imagine a situation in which you have data from a certain field of elements represented by a square divided into 9 equal parts (for example, a map). And you need to go around all the squares in a clockwise direction, and in the array they are folded randomly. Not very convenient, right?

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 iterate over them. An example of code that implements 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 - busting familiar to everyone, only in the correct order:
foreach ($tiles as $tile) {
    ...
}

Yes, really not bad. What else can you do with an iterator?


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

LimitIterator is very convenient to use when debugging or testing code. In particular, when working with PHPExcel , in iterating over strings, the library uses the RowIterator class , whose name implies that it is an Iterator. To avoid “dragging” all the lines every time you parse a document, you can wrap RowIterator in 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 a way, this is similar to the WHERE part of an SQL query. Suppose you are working with a third-party API, for example, the BaseCamp Classic API , whose SDK returns user objects to you. 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. The aforementioned class allows this to be done simply and supported:

/**
 * @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 cast multidimensional arrays to one-dimensional ones using RecursiveIteratorIterator , it’s convenient to get file listings of directories using RecursiveDirectoryIterator, and much more.

And where does the space program?


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 would I read only posts on the Habr that are both in the GameDev hub and in Web development ? In the feed, you can read posts from both hubs, but not the intersection of posts, if you understand what I mean. As a result, I got a small project using iterators.

All project code can be found in the repository on BitBucket , but 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 pages of Habr and collect links to content,
  3. We drive it all in PostsStorage,
  4. And filter with HabraPostFilter

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

GameDev + Web Development


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

Thank you all for your attention.

PS With pleasure I will accept corrections / comments in the comments to the post or in personal correspondence.

UPD . Thanks to Nikita_Rogatnev for helping me correct the typos.
UPD . Thanks to hell0w0rd for the demo .

Also popular now: