PHP数组,只包含特定类的对象

I have a class called Rule and I'm about to create a RuleContainer class that's actually an array of Rule objects.

I wonder if there is an alternative of creating a new class. Is there any (modern) way to approach this problem? That is, something like using SPL to define an array that only allows adding objects of a specific class.

If not, which interface should I implement in my RuleContainer class?

The most suitable class for your task would be SplObjectStorage, but it doesn't allow for class typehint.

I think, you could do as follow:

class RuleContainer extends SplObjectStorage
{
    function attach(Rule $rule)
    {
         parent::attach($rule);
    }

    function detach(Rule $rule)
    {
         parent::detach($rule);
    }
}

and so on. You can read for SplObjectStorage interface on php.net and decide, what will you use and what needs overriding.

In your case, I would implement the Iterator interface in the RuleContainer, as I've done several times when I needed a sort of Collection<T> as we know it from other (typed) languages. And in the add(Rule $item) or addItem(Rule $item) method I'd make sure with the type definition of the argument (or using instanceof) that the item to be added is of type Rule.

Depending on the usage patterns for your container class, you need to implement one or more of these interfaces:

  • Iterator - to use it as foreach($container as $key => $value);
  • Countable - for count($container);
  • ArrayAccess - for $container[$key] (set it, get it, check if it isset(), unset() it);

Usage of PHP array-routines interfaces

You may achieve your goal with, for example, ArrayAccess implementation. Together with Iterator it will look like:

class ArrayStorage implements Iterator, ArrayAccess
{
    private $holder = [];

    private $instanceName;

    public function __construct($instanceName)
    {
        if (!class_exists($instanceName)) {
            throw new \Exception('Class '.$instanceName.' was not found');
        }
        $this->instanceName = $instanceName;
    }
    public function rewind() 
    {
        reset($this->holder);
    }

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

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

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

    public function valid() 
    {
        return false !== $this->current();
    }

    public function offsetSet($offset, $value) 
    {
        if (!($value instanceof $this->instanceName)) {
            throw new \Exception('Storage allows only '.$this->instanceName.' instances');
        }
        if (is_null($offset)) {
            $this->holder[] = $value;
        } else {
            $this->holder[$offset] = $value;
        }
    }

    public function offsetExists($offset) 
    {
        return isset($this->holder[$offset]);
    }

    public function offsetUnset($offset) 
    {
        unset($this->holder[$offset]);
    }

    public function offsetGet($offset) 
    {
        return isset($this->holder[$offset]) ? $this->holder[$offset] : null;
    }
}

Procs

So - yes, you are doing instanceof check explicitly, but end user of your class doesn't know about that. It will only be possible to operate on valid instances in context of this storage (you can check this fiddle for usage sample). Concept is like:

$storage = new ArrayStorage('Foo'); //define what we will accept
$storage[] = new Foo; //fine, [] array-writing
$storage['baz'] = new Foo; //fine, key set
foreach ($storage as $key => $value) {
    echo($key. ' => '.PHP_EOL.var_export($value, 1).PHP_EOL);
}
//invalid, will not pass. Either throw exception or just ignore:
$storage['bee'] = new Bar; 

End fail-check behavior is up to you, but, my opinion, throwing exception is the best choice here as they are catchable, thus, end user may decide what to do in this case. Further option may be to add Countable to the storage, but it won't change generic idea.

And cons

Downside - no, you will not be able to "typehint" it somehow. While it is useful, in doc blocks you still will need to show what kind of entity are you accepting. In terms of general language features, there is arrayof RFC, by Joe Watkins, which was proposed for PHP version 5.6, but, unfortunately, failed. May be it will be reconsidered in next versions releases.

You can make RuleContainer yourself (as you say) and do all sorts of cleverness to manually enforce it but you live in the real world I live in the real world and you simply don't need a container object for this, just use an array.

If your problem is simply one of enforcement of the subject object type a lá List<className>() you can't do this in PHP and to be honest it's of debatable use in the languages where it is found (I know I will get down voted for saying this, but I will still be right) //excepting it helps further clarify the purpose of the list// In all honesty my 20+ years of programming across almost all the languages there is (except machine code perl and fortran), I can tell you such constructs and simply not worth the human overhead and themselves can include indirect unintended burdens way over their actual worth:

An easy compromise is: no laziness, start naming the array more than something like tmpList and if you are absolultey determined implment a simple test to http://php.net/manual/en/function.get-class.php at the start of the forloops you surely eventually use