- The Cart Class?s Needs
- Defining the Cart Class
- Making the Cart Iterable and Countable
- The Item Class
- Using the Code
- Conclusion
Making the Cart Iterable and Countable
A problem with the cart as written is there’s no way to get items from the cart, such as to show them in a form, list, or table. That code with the corresponding HTML shouldn’t be defined in the class as it would make the class less useful (or requires class edits to change the output). A better solution is to make the class iterable so a class object can be used in a foreach loop. This is accomplished by having the class implement the Iterable interface. While I’m at it, I’ll implement Countable, allowing code to invoke the count() method on shopping cart objects.
To do both of these things, start by changing the class definition:
class ShoppingCart implements Iterator, Countable {
Interfaces dictate what methods must be defined within a class. For Countable, that’s just a count() method:
public function count() { return count($this->items); }
That’s all there is to that! If you’re paying close attention, though, you’ll know that this method returns the number of unique items in the cart, not the total number of items (e.g., two of item A and one of item B counts as two items). If you’d like to change that representation, create a loop that adds up all the quantities, and have the method return that value instead.
The Iterator interface requires these methods:
- current(), which returns the current value in the list
- key(), which returns the current position in the list
- next(), which increments the position
- rewind(), which resets the position
- valid(), which returns a Boolean indicating if a value exists at the current position in the list
These methods make more sense when you envision how they’ll be used. If an iterable object is provided to a foreach loop, the rewind() method is first called to start the looping from the beginning of the list. Then the valid() method is called to see if a value exists at that initial position (normally 0). If so, then current() is called to get the current value (key() could also be called if the loop fetches the index as well).
With every subsequent iteration of the loop, the next() method is called to increment the position index. Then valid(), current(), and possibly key() are invoked again, assuming that valid() returns true. This process continues until valid() returns false.
Defining these methods is not too hard, save for two complications. First, the items array in the class does not use numeric indexes, and so you cannot use a numeric position value on it. Second, if an item is removed from the cart, this could create “holes” in the array. Thus, you can’t reliably just increment the position value from 0 to 1 to n to access every item. My solution to both of these problems is to create a second internal array that stores the indexes in use: the item IDs. This array will be numerically indexed and kept free of “holes.” Internally, the Iterator methods will make use of the new array, but the value of the current element will still come from the primary items array.
To pull that off, two attributes are added to the class:
protected $position = 0; protected $ids = array();
Then, within the addItem() method, when the item is added to the items array, the item’s ID is also added to the $ids array:
$this->ids[] = $id;
Now the class is separately tracking the product IDs from the items themselves.
Nothing needs to be added to the updateItem() method, but deleteItem() has more to do. First, after deleting the item from the items array, the ID value must be removed from $ids. This is a two-step process: find the corresponding index for that ID value, then unset that element:
$index = array_search($id, $this->ids); unset($this->ids[$index]);
This act may leave a hole in the array, which would cause problems during iterations, as already mentioned. The solution is to recreate the array using the array’s remaining values:
$this->ids = array_values($this->ids);
Now $ids is a hole-free array that can be incrementally iterated through without problem.
That’s it for the existing methods. The new iterator methods then use this new array or the $position attribute, accordingly:
public function key() { return $this->position; } public function next() { $this->position++; } public function rewind() { $this->position = 0; } public function valid() { return (isset($this->ids[$this->position])); }
The only method that uses the items array is current():
public function current() { $index = $this->ids[$this->position]; return $this->items[$index]; } // End of current() method.
The method first gets the current index (the item ID) using the position and the $ids array. Next, the method returns the value in $items indexed at that point.