Lazy Loading and Traversables in PHP5
In my previous blog post, I demonstrated a simple implementation of lazy loading and how it can be used to load resources on demand. While the concept of lazy loading is simple, it can be used to solve many problems in real world applications.
For example, let's say you are building a simple quiz application. Each quiz can have multiple questions, and each question can have multiple answer choices. When it comes to domain modelling, you may want to access a quiz's questions directly from its model like this:
$id = 123; $quiz = new Quiz($id); $questions = $quiz->getQuestions(); // returns array of Question objects
Taking a closer look at the Quiz class, we can see what is happening when the getQuestions is called:
class Quiz
{
/* ... */
protected $_questions = null;
public function getQuestions()
{
if (null === $this->_questions) {
// Perform some magic to load the questions
}
return $this->_questions;
}
}
While this is a form of lazy loading, it may still be providing us with too much information. For example, let's say the questions are displayed to the user one at a time. There's certainly no need to load and instantiate all of those Question objects if we only need to display one.
Part of the problem is that, currently, $this->_questions is an array, and arrays are not flexible enough for lazy loading. That's where the Iterator interface comes into play.
By using the Iterator as a container for the questions and adding a lazy-loading mechanism to the current() method, we can improve the efficiency of container by only loading what we need when we need it.
class QuestionsContainer implements Iterator
{
protected $_questionIds = array();
protected $_questionInstances = array();
protected $_position = 0;
public function __construct(array $ids)
{
$this->_questionIds = array_values($ids);
}
public function rewind()
{
$this->_position = 0;
}
public function next()
{
++$this->_position;
}
public function key()
{
return $this->_position;
}
public function valid()
{
return isset($this->_questionIds[$this->_position]);
}
public function current()
{
if (!isset($this->_questionInstances[$this->_position])) {
$id = $this->_questionIds[$this->_position];
$this->_questionInstances[$this->_position] = new Question($id);
}
return $this->_questionInstances[$this->_position];
}
}
Now your collection is ready to go, so long as you provide it with an array of IDs to work with. You can use foreach() on this container and only with each iteration will a Question object be instantiated.
Room for Improvement?
Depending on your circumstances you may need to be able to count the items in your container. To do this, simply implement the Countable interface and add a count() method:
class QuestionsContainer implements Iterator, Countable
{
/* ... */
public function count()
{
return count($this->_questionIds);
}
}
Note: Be sure to count the number of IDs, not the number of instances, or else you may end up with a smaller count than expected!
One of the benefits of using a lazy-loading iterator is that you can easily paginate through thousands of results and only show 10 or 20 at time.
Any Drawbacks or Limitations?
There are a few drawbacks to using this pattern over a traditional array, but depending on your use the strengths may easily outweigh the weaknesses. The weakness I've noticed are:
- Since the container object is not a true array, it's array capabilities are limited. For example, you cannot perform an array_merge() on this container, but you can implement your own method that merges in another array (or even another container).
- This pattern is susceptible to the N+1 problem, where the number of queries is equal to the number of items plus one. However, usually the queries required for the instantiation of a Question object are minimal.
In my next post, I will show you how you can combine these ideas with the Data Mapper pattern to keep your business logic and persistence logic separated.

Comments
blog comments powered by Disqus