View a printer-optimized version of this page

Managing 0bject Collections in PHP 5

Posted at April 16, 2007 by Vagharshak Tozalakyan

Manipulating the lists of objects is one of the common tasks in many PHP applications. Developers may use regular arrays to keep the track of object collections, but it may cause a number of problems regarding effectivity and object-oriented usability.

In this article I'm going to demonstrate the possibility to create an effective object-oriented array wrapper using PHP 5 features.

Basically, the Collection class should be able to perform some simple array manipulation tasks like adding and removing items, getting an element by its index. Besides, it would be preferable to have possiblity to retrieve the total number of elements in collection, retireve the array of keys and check if an element with specified key exists in collection. Having all that in mind, here is the skeleton of our new class:

<?php

class Collection
{

    protected 
$_items = array();

    public function 
addItem($item$key null)
    {
        
// adding a new item with specified key...
    
}

    public function 
removeItem($key)
    {
        
// removing the item with specified key...
    
}

    public function 
getItem($key)
    {
        
// getting the item with specified key...
    
}

    public function 
getLength()
    {
        
// getting the total number of existed item...
    
}

    public function 
getKeys()
    {
        
// getting the list of existed keys...
    
}

    public function 
isExist($key)
    {
        
// checking if an item with specified key exists...
    
}

}

?>

Before starting to fill the skeleton with code, it will be not bad to have a class-level PHP's Exception descendant, which can be used to handle exceptional situations that may occur during operation. This may be done with the following line of the code:

<?php

class CollectionException extends Exception { }

.....

?>

A new item may be added to collection by its key (unordered collections) or by its current numerical index (ordered collections). Also, we need to check if an item with specified key or infex already exist, and, if so, raise an exception. Here is the code of our addItem() method:

<?php

.....

public function 
addItem($item$key null)
{
    if (!
is_null($key)) {
        if (!isset(
$this->_items[$key])) {
            
$this->_items[$key] = $item;
        } else {
            throw new 
CollectionException('The key already exists.');
        }
    } else {
        
$this->_items[] = $item;
    }
}

.....

?>

We also need to check the existence of specified key (index) while trying to remove or retrieve an item:

<?php

.....

public function 
removeItem($key)
{
    if (isset(
$this->_items[$key])) {
        unset(
$this->_items[$key]);
    } else {
        throw new 
CollectionException('Incorrect key.');
    }
}

public function 
getItem($key)
{
    if (isset(
$this->_items[$key])) {
        return 
$this->_items[$key];
    } else {
        throw new 
CollectionException('Incorrect key.');
    }
}

.....

?>

Finally, getLength(), getKeys() and isExist() methods are quite simple as they only refer to the appropriate array functions from PHP library:

<?php

....

public function 
getLength()
{
    return 
sizeof($this->_items);
}

public function 
getKeys()
{
    return 
array_keys($this->_items);
}

public function 
isExist($key)
{
    return isset(
$this->_items[$key]);
}

....

?>

In order to test the above code let's assume, that we need to create a Book class, which among the other properties will contain a collection of Author objects. The Author class may consist of the following fields: first name, last name and the number of books the author wrote:

<?php

class Author
{

    protected 
$firstName;
    protected 
$lastName;
    protected 
$numBooks;

    public function 
__construct($firstName$lastName$numBooks 1)
    {
        
$this->firstName $firstName;
        
$this->lastName $lastName;
        
$this->numBooks $numBooks;
    }

    public function 
__toString()
    {
        return 
sprintf('%s %s (%d) <br />',
            
$this->firstName$this->lastName$this->numBooks);
    }

}

?>

In addition, we can create a Collection class descendant named AuthorsCollection. Having such class will allow to pass only Author objects to the addItem() method. It is one of many advantages of PHP version 5.

<?php

require_once 'Collection.php';
require_once 
'Author.php';

class 
AuthorsCollection extends Collection
{
    public function 
addItem(Author $item$key null)
    {
        
parent::addItem($item$key);
    }
}

?>

Now we can write the Book class, which will contain a collection of authors as one of its properties:

<?php

require_once 'AuthorsCollection.php';

class 
Book
{

    protected 
$title;
    protected 
$isbn;
    protected 
$price;

    public 
$authors;

    public function 
__construct($title$isbn$price)
    {
        
$this->title $title;
        
$this->isbn $isbn;
        
$this->price $price;
        
$this->authors = new AuthorsCollection();
    }

    public function 
__toString()
    {
        return 
sprintf('<b>Title:</b> %s,
            <b>ISDN:</b> %s,
            <b>Price:</b> %.2f <br />'
,
            
$this->title$this->isbn$this->price);
    }

}

?>

We can refer to the methods of AuthorCollection by the authors property of the Book instance. Here is an example:

<?php

require_once 'Book.php';

$book = new Book('PHP 5 Definitive Guide''1-56592-681-1'49.95);

$book->authors->addItem(new Author('Alex''Smith'5));
$book->authors->addItem(new Author('David''Malevich'2));

echo 
$book;
echo 
$book->authors->getItem(0);
echo 
$book->authors->getItem(1);

?>

Probably you'll not believe, but the foreach() operator in PHP 5 allows to work with objects just as with arrays. It can be done with the help of two predefined interfaces - Iterator and IteratorAggregate.

The buil-in Iterator interface consists of five abstract methods that should be overrided in object-level iterator class. These methods are: valid(), rewind(), key(), current() and next().

Here is the implementation of Iterator interface for our Collection class:

<?php

require_once 'Collection.php';

class 
CollectionIterator implements Iterator
{

    protected 
$_collection;
    protected 
$_index;
    protected 
$_keys;

    public function 
__construct(Collection $col)
    {
        
$this->_collection $col;
        
$this->_index 0;
        
$this->_keys $col->getKeys();
    }

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

    public function 
valid()
    {
        return 
$this->_index $this->_collection->getLength();
    }

    public function 
key()
    {
        return 
$this->_keys[$this->_index];
    }

    public function 
current()
    {
        return 
$this->_collection->getItem(
            
$this->_keys[$this->_index]);
    }

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

}

?>

In order to use foreach() operator on Collection object we also need to implement the IteratorAggregate interface in our Collection class. That will require to override the getIterator() method, which should return the instance of Iterator object:

<?php

require_once 'CollectionIterator.php';

class 
CollectionException extends Exception { }

class 
Collection implements IteratorAggregate
{

    protected 
$_items = array();

    public function 
getIterator()
    {
        return new 
CollectionIterator($this);
    }

    ....

}

?>

And now, after all, we may use foreach() on Collection instances like in the following example:

<?php

require_once 'Book.php';

$book = new Book('PHP 5 Definitive Guide''1-56592-681-1'49.95);

$book->authors->addItem(new Author('Alex''Smith'5));
$book->authors->addItem(new Author('David''Malevich'2));

echo 
$book;
foreach (
$book->authors as $author) {
    echo 
$author;
}

?>

The last thing we may want to add to our Collection class is the ability of so-called lazy instantation.

Suppose, that the information about authors is stored in database and the Book object is quering database to load the collection. But if we want only to see the book title then there is no need to query database at all. So we need a mechanism that will allow to load collection only when it really needed. The lazy instantation may be implemented with the help of callback method passed to collection object.

<?php

require_once 'CollectionIterator.php';

class 
CollectionException extends Exception {}

class 
Collection implements IteratorAggregate
{

    protected 
$_items = array();

    protected 
$_onLoad;
    protected 
$_isLoaded false;

    public function 
getIterator()
    {
        return new 
CollectionIterator($this);
    }

    public function 
setCallback($functionName$owner null)
    {
        if (!
is_null($owner)) {
            
$callback = array($owner$functionName);
        } else {
            
$callback $functionName;
        }
        if (!
is_callable($callbackfalse$callableName)) {
            throw new 
CollectionException('Invalid callback.');
        }
        
$this->_onLoad $callback;
    }

    protected function 
_checkCallback()
    {
        if (isset(
$this->_onLoad) && !$this->_isLoaded) {
            
$this->_isLoaded true;
            
call_user_func($this->_onLoad$this);
        }
    }

    public function 
addItem($item$key null)
    {
        
$this->_checkCallback();
        .....
    }

    public function 
removeItem($key)
    {
        
$this->_checkCallback();
        .....
    }

    public function 
getItem($key)
    {
        
$this->_checkCallback();
        .....
    }

    public function 
getLength()
    {
        
$this->_checkCallback();
        .....
    }

    public function 
getKeys()
    {
        
$this->_checkCallback();
        .....
    }

    public function 
isExist($key)
    {
        
$this->_checkCallback();
        .....
    }

}

?>

Now it is possible to define a new method of Book class that will query databse. The method can be passed as a callback for Collection instance and will be called only when we refer to one of the collection methods.

<?php

require_once 'AuthorsCollection.php';

class 
Book
{

    protected 
$title;
    protected 
$isbn;
    protected 
$price;

    public 
$authors;

    public function 
__construct($title$isbn$price)
    {
        
$this->title $title;
        
$this->isbn $isbn;
        
$this->price $price;
        
$this->authors = new AuthorsCollection();
        
$this->authors->setCallback('loadAuthors'$this);
    }

    public function 
__toString()
    {
        return 
sprintf('<b>Title:</b> %s,
            <b>ISDN:</b> %s,
            <b>Price:</b> %.2f <br />'
,
            
$this->title$this->isbn$this->price);
    }

    public function 
loadAuthors($col)
    {
        
$query sprintf('SELECT fname, lname, num_books
            FROM authors WHERE "%s" IN isbns'
$this->isbn);
        
$result mysql_query($query) or die(mysql_error());
        while (
$row mysql_fetch_assoc($result)) {
            
$col->addItem(new Author($row['fname'], $row['lname'],
                
$row['num_books']));
        }
    }

}

?>

if we don't write the foreach() cycle in the following example, then no database query will be made.

<?php

require_once 'Book.php';

$book = new Book('PHP 5 Definitive Guide''1-56592-681-1'49.95);

echo 
$book;

foreach (
$book->authors as $author) {
    echo 
$author;
}

?>

Example code for this article: collection.zip

Comments

View all comments (2)

Comments are moderated. If your comment does not appear immediately, there is no need to submit it again. Please treat others with respect. Comments containing hate speech, obscenity, and personal attacks will not be approved.






Validation code