I'm wondering if an object belonging to a collection class, whilst being iterated on can know it's being iterated and know about the collection class it belongs to? e.g.
<?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.
I've done some Google'ing and couldn't find anything, I'm guessing it's not possible because it breaks an OOP principal somewhere but thought I'd ask anyway!
I think we can split this into the following problems:
Collection
that is iterableThe Collection
should
a. have the names of prohibited methods hard-coded (bad) or
b. be able to fetch the names of prohibited methods from the elements of the collection
when iterating over the collection, it should yield proxies to the original object, intercepting calls to methods which should not be allowed to be called when iterating over the collection
This is easy, just make it implement the Iterator
interface:
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;
}
}
Rather than hard-coding the prohibited methods into the collection, I would suggest to create an interface, and have that be implemented by the elements of the collection, if need be, for example:
<?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();
}
This also has the advantage that the collection would work with all kinds of elements, as long as it is able to fetch that information from the element.
Then have your elements, if need be, implement the interface:
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',
];
}
}
As suggested in a different answer by @akond, you could use ocramius/proxymanager
, and specifically, an Access Interceptor Value Holder Proxy.
Run
$ composer require ocramius/proxymanager
to add it to your project.
Adjust the collection as follows:
<?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();
}
Note This can still be optimized, for example, you could store references to the created proxies in the Collection
, and instead of creating new proxies every time, current()
could return previously created proxies, if need be.
For reference, see:
I should create two different classes in order to comply with single responsibility principle. One would be a Collection
, and the other would be an Object
itself. Object
s get returned only as members of Collection
. Every time when Collection
gets iterated, it "loads" corresponding Object
s.
If it seems appropriate, you might want to create a Lazy loading ghost object proxy for each of those Object
s.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With