1
\$\begingroup\$

Here is my attempt at a PHP class to be used as a base class for typed collections. My goals with this are:

  • to be able to access collection members with foreach
  • to be able to access collection members using indices (array-like)
  • to ensure that all collection members are objects of the same type

I have implemented the appropriate interfaces. Here is the code:

<?php

/**
 * A class to hold a collection of objects of  the same type.
 * 
 * There are tow purposes:
 * - to ensure that all elements are of the same type
 * - to be enumerable by the foreach loop
 */

abstract class TypedCollection implements Countable, Iterator, ArrayAccess
{
    /**
     * Child classes pass the collection type to this constructor
     * 
     * @param string $typeName The name of the underlying class
     * @throws InvalidArgumentException If the class can't be found by reflection
     */

    public function __construct($typeName) 
    {
        if(class_exists($typeName))
        {
            $this->_type = new ReflectionClass($typeName);
        }
        else
        {
            throw new InvalidArgumentException(
                "The class $typeName does not exist.");
        }
    }

    /* Implement Countable */

    /**
     * Returns the count of the elements in the collection
     * @return int 
     */

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

    /* Implement Iterator */

    /**
     * Returns the current element of the collection
     * @return Object 
     */

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

    /**
     * Return the key of the current element
     * @return int 
     */

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

    /**
     * Move forward to next element
     */

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

    /**
     * Rewind to its first element 
     */

    public function rewind() 
    {
        reset($this->_innerArray);
    }

    /**
     * Checks if the current position is valid
     * @return bool 
     */

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

    /* Implement ArrayAccess */

    /**
     * Whether an offset exists
     * @param int $offset
     * @return bool 
     */

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

    /**
     * Returns the value at the specified offset
     * @param int $offset
     * @return int 
     */

    public function offsetGet($offset) 
    {
        if(isset($this->_innerArray[$offset]))
        {
            return $this->_innerArray[$offset];
        }
        else
        {
            return null;
        }
    }

    /**
     * Assigns a value to the specified offset
     * @param int $offset 
     * @param mixed $object 
     * @throws InvalidArgumentException If $object is not of the underlying type
     */

    public function offsetSet($offset, $object) 
    {
        if ($object instanceof $this->_type->name)
        {
            if (is_null($offset)) 
            {
                $this->_innerArray[] = $object;
            }
            else 
            {
                $this->_innerArray[$offset] = $object;
            }
        } 
        else 
        {
            throw new InvalidArgumentException(
                "Object needs to be a $this->_type->name instance.");
        }
    }

    /**
     * Unsets an offset
     * @param int $offset 
     */

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

    /* TypedCollection functions */

    /**
     * Adds an object to the collection.
     * @param mixed $object The object to add to the collection
     */

    public function add($object)
    {
        $this->offsetSet(null, $object);
    }

    /**
     * Removes all elements from the collection 
     */

    public function clear()
    {
        $this->_innerArray = array();
    }

    /**
     * Checks if an object belongs to the collection
     * @param mixed $object
     * @return bool 
     */

    public function contains($object)
    {
        return in_array($object, $this->_innerArray, true);
    }

    /**
     * Return an object index
     * @param mixed $object
     * @return int 
     */

    public function indexOf($object)
    {
        return array_search($object);
    }

    /**
     * Inserts an object at the specified offset in the collection
     * @param int $offset
     * @param mixed $object
     * @throws InvalidArgumentException If the object is not of the underlying type
     * @throws OutOfRangeException If the offset does not exist
     */

    public function insert($offset, $object)
    {
        if(array_key_exists($offset, $this->_innerArray))
        {
            if($object instanceof $this->_type->name)
            {
                $tempArray = array($object, $this->offsetGet($offset));
                array_splice($this->_innerArray, $offset, 1, $tempArray);
                $this->_innerArray = array_values($this->_innerArray);
            }
            else
            {
                throw new InvalidArgumentException(
                    "Object needs to be a $this->_type->name instance.");
            }
        }
        else
        {
            throw new OutOfRangeException(
                "The index $offset does not exist in the collection.");
        }
    }

    /**
     * Removes the specified object from the collection
     * @param mixed $object 
     */

    public function remove($object)
    {
        if($this->contains($object))
        {
            $this->offsetUnset(array_search($object, $this->_innerArray));
            $this->_innerArray = array_values($this->_innerArray);
        }
    }    

    /**
     * Removes the object at the specified offset in the collection
     * @param int $offset 
     */

    public function removeAt($offset)
    {
        $this->offsetUnset($offset);
        $this->_innerArray = array_values($this->_innerArray);
    }

    /**
     * The array that contains collection elements
     * @var array 
     */

    protected $_innerArray = array();

    /**
     * The collection type
     * @var ReflectionClass
     */

    protected $_type;
}

?>

A child class:

<?php

include_once 'TypedCollection.php';
include_once 'Statement.php';

class StatementCollection extends TypedCollection
{
    public function __construct()
    {
        parent::__construct('Statement');
    }
}

?>

How I use it:

$for2->Statements[0]->TrueStatements->add($break);

$for2->Statements->add($echo1);

Any thoughts on the code are welcome.

\$\endgroup\$
3
\$\begingroup\$

Instead of recreating methods that perform functions that already exist ( count(), current(), etc...), have you thought of using PHP's magic __toString() method? I've not had much need to play around with it myself, but from my understanding you can use that magic method then just call functions such as count() and current() outside of the class with the class instance as its argument and it will accomplish the same thing. This will reduce your code considerably.

In my opinion offsetGet() should either return the found offset, or FALSE. Usually when I see NULL that means an error occurred that made performing that task impossible and so it was silently handled. FALSE on the other hand says clearly that "no this doesn't exist". At least to me... This may just be a preference thing, I just thought it looked extremely odd.

A better way to declare offsetSet() would be to reverse the arguments. If you expect that you aren't always going to use an argument it should be declared after those arguments that aren't ever going to be empty. That way calling that method will not begin with empty arguments.

$this->offsetSet( $object ); // Equivalent to $this->offsetSet( $object, '' );
//Compared to...
$this->offsetSet( '', $object );

When using PHP to process strings with variables, it is important to remember that PHP only expects simple variables such as $name. More complex variables, such as class properties or arrays will not work as expected. So $this->_type->name will not display what you are expecting. In fact, what PHP sees here is $this which can not be processed without var_dump() or print_r(). This is good, otherwise you'd have your entire class dumped onto the screen. Instead, what you will have to do is use PHP's escape identifiers {} (not right term, not sure what they're called) to wrap those more complex variables. Or you could abstract it by simplifying it before using it, but that means creating an unnecessary variable. So...

throw new InvalidArgumentException(
    "Object needs to be a {$this->_type->name} instance.");

I stopped here because most of what remained appeared to be things that could be fixed/removed with my first suggestion. Hopefully I didn't miss anything. I'll take another look at it if you want, just let me know. Hope this helps!

\$\endgroup\$
  • \$\begingroup\$ I implemented count() and current() (and most of the other methods) as required by the implemented interfaces. I haven't used __toString() before, I'll have to test it to see if PHP complains if I don't define the methods explicitly. \$\endgroup\$ – Anonimista Jul 4 '12 at 17:51
  • \$\begingroup\$ Same for offsetSet(), it is declared by ArrayAccess and the first argument as $offset and the second as $value (which I renamed to $object). I'll try to reverse the order to see if it works. \$\endgroup\$ – Anonimista Jul 4 '12 at 17:56
  • \$\begingroup\$ Thanks for that last tip on referencing _type->name, good explanation. I hope this site gets out of beta soon, I only stumbled upon it by chance. I like the concept, I think it could be very useful. \$\endgroup\$ – Anonimista Jul 4 '12 at 18:05
  • \$\begingroup\$ @anonimista: I didn't think about that... If it comes down to it you might be able to extend the classes rather than implement them to bypass this need. \$\endgroup\$ – mseancole Jul 5 '12 at 0:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.