Могут ли методы объектов быть перехвачены при переборе их как части коллекции?

Мне интересно, может ли объект, принадлежащий классу коллекции, итерируемый, знать, что он повторяется, и знать, к какому классу коллекции он принадлежит? например

<?php
class ExampleObject
{
    public function myMethod()
    {
        if( functionForIterationCheck() ) {
           throw new Exception('Please do not call myMethod during iteration in ' . functionToGetIteratorClass());
        }
    }
}

$collection = new CollectionClass([
    new ExampleObject,
    new ExampleObject,
    new ExampleObject
]);

foreach($collection as $item) {
    $item->myMethod(); //Exception should be thrown.
}

(new ExampleObject)->myMethod(); //No Exception thrown.

Я немного поработал с Google и ничего не смог найти, наверное, это невозможно, потому что это где-то нарушает принцип ООП, но я все равно решил спросить!

2 ответа

Решение

Я думаю, что мы можем разделить это на следующие проблемы:

  1. Нам нужно создать Collection это итеративно
  2. Collection должен

    а. иметь имена запрещенных методов жестко (плохо) или

    б. быть в состоянии извлечь имена запрещенных методов из элементов коллекции

  3. при выполнении итерации по коллекции он должен давать прокси к исходному объекту, перехватывая вызовы методов, которые не должны вызываться при итерации по коллекции

1) Коллекция должна быть повторяемой

Это легко, просто заставьте его реализовать Iterator интерфейс:

class Collection implements \Iterator
{
    /**
     * @var array
     */
    private $elements;

    /**
     * @var int
     */
    private $key;

    public function __construct(array $elements)
    {
        // use array_values() here to normalize keys 
        $this->elements = array_values($elements);
        $this->key = 0;
    }

    public function current()
    {
        return $this->elements[$this->key];
    }

    public function next()
    {
        ++$this->key;
    }

    public function key()
    {
        return $this->key;
    }

    public function valid()
    {
        return array_key_exists(
            $this->key,
            $this->elements
        );
    }

    public function rewind()
    {
        $this->key = 0;
    }
}

2) Коллекция должна иметь возможность извлекать методы из элементов

Вместо того, чтобы жестко кодировать запрещенные методы в коллекции, я бы предложил создать интерфейс, который должен реализовываться элементами коллекции, если это необходимо, например:

<?php

interface HasProhibitedMethods
{
    /**
     * Returns an array of method names which are prohibited 
     * to be called when implementing class is element of a collection.
     *
     * @return string[]
     */
    public function prohibitedMethods();
}

Это также имеет то преимущество, что коллекция будет работать со всеми видами элементов, если она способна извлекать эту информацию из элемента.

Затем, при необходимости, создайте элементы:

class Element implements HasProhibitedMethods
{ 
    public function foo()
    {
        return 'foo';
    }

    public function bar()
    {
        return 'bar';
    }

    public function baz()
    {
        return 'baz';
    }

    public function prohibitedMethods()
    {
        return [
            'foo',
            'bar',
        ];
    }
}

3) При итерации выведите прокси

Как предложено в другом ответе @akond, вы можете использовать ocramius/proxymanager и, в частности, прокси-сервер держателя значения перехватчика доступа.

Бежать

$ composer require ocramius/proxymanager

добавить его в свой проект.

Отрегулируйте коллекцию следующим образом:

<?php

use ProxyManager\Factory\AccessInterceptorValueHolderFactory;

class Collection implements \Iterator
{
    /**
     * @var array
     */
    private $elements;

    /**
     * @var int
     */
    private $key;

    /**
     * @var AccessInterceptorValueHolderFactory
     */
    private $proxyFactory;

    public function __construct(array $elements)
    {
        $this->elements = array_values($elements);
        $this->key = 0;
        $this->proxyFactory = new AccessInterceptorValueHolderFactory();
    }

    public function current()
    {
        $element = $this->elements[$key];

        // if the element is not an object that implements the desired interface
        // just return it
        if (!$element instanceof HasProhibitedMethods) {
            return $element;
        }

        // fetch methods which are prohibited and should be intercepted
        $prohibitedMethods = $element->prohibitedMethods();

        // prepare the configuration for the factory, a map of method names 
        // and closures that should be invoked before the actual method will be called
        $configuration = array_combine(
            $prohibitedMethods,
            array_map(function ($prohibitedMethod) {
                // return a closure which, when invoked, throws an exception
                return function () use ($prohibitedMethod) {
                    throw new \RuntimeException(sprintf(
                        'Method "%s" can not be called during iteration',
                        $prohibitedMethod
                    ));
                };
            }, $prohibitedMethods)
        );

        return $this->proxyFactory->createProxy(
            $element,
            $configuration
        );
    }

    public function next()
    {
        ++$this->key;
    }

    public function key()
    {
        return $this->key;
    }

    public function valid()
    {
        return array_key_exists(
            $this->key,
            $this->elements
        );
    }

    public function rewind()
    {
        $this->key = 0;
    }
}

пример

<?php

require_once __DIR__ .'/vendor/autoload.php';

$elements = [
    new Element(),
    new Element(),
    new Element(),
];

$collection = new Collection($elements);

foreach ($collection as $element) {
    $element->foo();
}

Примечание. Это все еще можно оптимизировать, например, вы можете хранить ссылки на созданные прокси в Collection и вместо того, чтобы создавать новые прокси каждый раз, current() может вернуть ранее созданные прокси, если это необходимо.

Для справки смотрите:

Я должен создать два разных класса, чтобы соответствовать принципу единой ответственности. Один был бы Collection, а другой будет Object сам. Objectвозвращаются только как члены Collection, Каждый раз когда Collection повторяется, он "загружает" соответствующий Objects.

Если это кажется уместным, вы можете создать прокси-объект Lazy для загрузки призраков для каждого из них. Objects.

Другие вопросы по тегам