Object Collections in PHP

Saturday, September 17, 2011

When I first started learning object-oriented PHP and I needed to access objects in an array, I used to store them in...well...arrays. This was back in the days of PHP 4, when object support was finally starting to get somewhere, and well before the addition of the ArrayIterator class. All was fine for awhile, but eventually I grew tired of doing things like this to find a particular object:

  1. <?php
  2. foreach($arrayOfUsers as $user) {
  3. if ($user->getUsername() == 'spammy') {
  4. // do stuff here
  5. reset($arrayOfUsers);
  6. break;
  7. }
  8. }

I began running into situations where I needed to remove one of the objects from the array, but I wanted to somehow mark the object as deleted so that I could, for example, pass it to a saveArray() method of a DataMapper object, and let that handle the object deletion. Or perhaps I needed to sort an array of Users alphabetically by the users' first names when it wasn't convenient (or possible) to do that with the DBMS. Also, what happens when you serialize() an array of objects? The array's internal pointer is lost. On the rare occasions I needed to store an array in mid-loop with its internal pointer intact, I was just out of luck.

While looking for a solution to these problems, I kept running into the term "Collection" here and there, but it seemed to mean different things depending on which programming language was being utilized. All of the PHP solutions I found were at least a little "icky" smelling. Finally I came up with this Collection class which I still use it to this day. I feel the syntax is easier/clearer than PHP's ArrayIterator class, and unless I were to write an ArrayIterator wrapper class I would lose many of the methods I have come to rely on.

The Collection Class

  1. <?php
  2. class Collection
  3. {
  4. protected $objects; // array
  5. protected $deletedObjects; // array
  6. protected $resetFlag;
  7. protected $numObjects;
  8. protected $iterateNum;
  9. public function __construct()
  10. {
  11. $this->resetIterator();
  12. $this->numObjects = 0;
  13. $this->objects = array();
  14. $this->deletedObjects = array();
  15. }
  16. public function add($obj)
  17. {
  18. $this->objects[] = $obj;
  19. $this->numObjects++;
  20. }
  21. public function next()
  22. {
  23. $num = ($this->currentObjIsLast()) ? 0 : $this->iterateNum + 1;
  24. $this->iterateNum = $num;
  25. }
  26. public function isOdd()
  27. {
  28. return $this->iterateNum%2==1;
  29. }
  30. public function isEven()
  31. {
  32. return $this->iterateNum%2==0;
  33. }
  34. /*
  35. get an obj based on one of it's properties.
  36. i.e. a User obj with the property 'username' and a value of 'someUser'
  37. can be retrieved by Collection::getByProperty('username', 'someUser')
  38. -- assumes that the obj has a getter method
  39. with the same spelling as the property, i.e. getUsername()
  40. */
  41. public function getByProperty($propertyName, $property)
  42. {
  43. $methodName = "get".ucwords($propertyName);
  44. foreach ($this->objects as $key => $obj) {
  45. if ($obj->{$methodName}() == $property) {
  46. return $this->objects[$key];
  47. }
  48. }
  49. return false;
  50. }
  51. /*
  52. alias for getByProperty()
  53. */
  54. public function findByProperty($propertyName, $property)
  55. {
  56. return $this->getByProperty($propertyName, $property);
  57. }
  58. /*
  59. get an objects number based on one of it's properties.
  60. i.e. a User obj with the property 'username' and a value of 'someUser'
  61. can be retrieved by Collection::getByProperty('username', 'someUser')
  62. -- assumes that the obj has a getter method
  63. with the same spelling as the property, i.e. getUsername()
  64. */
  65. public function getObjNumByProperty($propertyName, $property)
  66. {
  67. $methodName = "get".ucwords($propertyName);
  68. foreach ($this->objects as $key => $obj) {
  69. if ($obj->{$methodName}() == $property) {
  70. return $key;
  71. }
  72. }
  73. return false;
  74. }
  75. /*
  76. get the number of objects that have a property
  77. with a value matches the given value
  78. i.e. if there are objs with a property of 'verified' set to 1
  79. the number of these objects can be retrieved by:
  80. Collection::getNumObjectsWithProperty('verified', 1)
  81. -- assumes that the obj has a getter method
  82. with the same spelling as the property, i.e. getUsername()
  83. */
  84. public function getNumObjectsWithProperty($propertyName, $value)
  85. {
  86. $methodName = "get".ucwords($propertyName);
  87. $count = 0;
  88. foreach ($this->objects as $key => $obj) {
  89. if ($obj->{$methodName}() == $value) {
  90. $count++;
  91. }
  92. }
  93. return $count;;
  94. }
  95. /*
  96. remove an obj based on one of it's properties.
  97. i.e. a User obj with the property 'username' and a value of 'someUser'
  98. can be removed by Collection::removeByProperty('username', 'someUser')
  99. -- assumes that the obj has a getter method
  100. with the same spelling as the property, i.e. getUsername()
  101. */
  102. public function removeByProperty($propertyName, $property)
  103. {
  104. $methodName = "get".ucwords($propertyName);
  105. foreach ($this->objects as $key => $obj) {
  106. if ($obj->{$methodName}() == $property) {
  107. $this->deletedObjects[] = $this->objects[$key];
  108. unset($this->objects[$key]);
  109. // reindex array & subtract 1 from numObjects
  110. $this->objects = array_values($this->objects);
  111. $this->numObjects--;
  112. $this->iterateNum = ($this->iterateNum >= 0) ? $this->iterateNum - 1 : 0;
  113. return true;
  114. }
  115. }
  116. return false;
  117. }
  118. public function currentObjIsFirst()
  119. {
  120. return ($this->iterateNum == 0);
  121. }
  122. public function currentObjIsLast()
  123. {
  124. return (($this->numObjects-1) == $this->iterateNum);
  125. }
  126. public function getObjNum($num)
  127. {
  128. return (isset($this->objects[$num])) ? $this->objects[$num] : false;
  129. }
  130. public function getLast()
  131. {
  132. return $this->objects[$this->numObjects-1];
  133. }
  134. public function removeCurrent()
  135. {
  136. $this->deletedObjects[] = $this->objects[$this->iterateNum];
  137. unset($this->objects[$this->iterateNum]);
  138. // reindex array & subtract 1 from iterator
  139. $this->objects = array_values($this->objects);
  140. if ($this->iterateNum == 0) { // if deleting 1st object
  141. $this->resetFlag = true;
  142. } elseif ($this->iterateNum > 0) {
  143. $this->iterateNum--;
  144. } else {
  145. $this->iterateNum = 0;
  146. }
  147. $this->numObjects--;
  148. }
  149. public function removeLast()
  150. {
  151. $this->deletedObjects[] = $this->objects[$this->numObjects-1];
  152. unset($this->objects[$this->numObjects-1]);
  153. $this->objects = array_values($this->objects);
  154. // if iterate num is set to last object
  155. if ($this->iterateNum == $this->numObjects-1) {
  156. $this->resetIterator();
  157. }
  158. $this->numObjects--;
  159. }
  160. public function removeAll()
  161. {
  162. $this->deletedObjects = array_merge($this->deletedObjects, $this->objects);
  163. $this->objects = array();
  164. $this->numObjects = 0;
  165. }
  166. /*
  167. sort the objects by the value of each objects property
  168. $type:
  169. r regular, ascending
  170. rr regular, descending'
  171. n numeric, ascending
  172. nr numeric, descending
  173. s string, ascending
  174. sr string, descending
  175. */
  176. public function sortByProperty($propName, $type='r')
  177. {
  178. $tempArray = array();
  179. $newObjects = array();
  180. while ($obj = $this->iterate()) {
  181. $tempArray[] = call_user_func(array($obj, 'get'.ucwords($propName)));
  182. }
  183. switch($type)
  184. {
  185. case 'r':
  186. asort($tempArray);
  187. break;
  188. case 'rr':
  189. arsort($tempArray);
  190. break;
  191. case 'n':
  192. asort($tempArray, SORT_NUMERIC);
  193. break;
  194. case 'nr':
  195. arsort($tempArray, SORT_NUMERIC);
  196. break;
  197. case 's':
  198. asort($tempArray, SORT_STRING);
  199. break;
  200. case 'sr':
  201. arsort($tempArray, SORT_STRING);
  202. break;
  203. default:
  204. throw new General_Exception(
  205. 'Collection->sortByProperty():
  206. illegal sort type "'.$type.'"'
  207. );
  208. }
  209. foreach ($tempArray as $key => $val) {
  210. $newObjects[] = $this->objects[$key];
  211. }
  212. $this->objects = $newObjects;
  213. }
  214. public function isEmpty()
  215. {
  216. return ($this->numObjects == 0);
  217. }
  218. public function getCurrent()
  219. {
  220. return $this->objects[$this->iterateNum];
  221. }
  222. public function setCurrent($obj)
  223. {
  224. $this->objects[$this->iterateNum] = $obj;
  225. }
  226. public function getObjectByIterateNum($iterateNum)
  227. {
  228. return (
  229. isset($this->objects[$iterateNum])
  230. ? $this->objects[$iterateNum]
  231. : false
  232. );
  233. }
  234. public function iterate()
  235. {
  236. if ($this->iterateNum < 0) {
  237. $this->iterateNum = 0;
  238. }
  239. if ($this->resetFlag) {
  240. $this->resetFlag = false;
  241. } else {
  242. $this->iterateNum++;
  243. }
  244. if ( $this->iterateNum == $this->numObjects
  245. || !isset($this->objects[$this->iterateNum])
  246. ) {
  247. $this->resetIterator();
  248. return false;
  249. }
  250. return $this->getCurrent();
  251. }
  252. public function resetIterator()
  253. {
  254. $this->iterateNum = 0;
  255. $this->resetFlag = true;
  256. }
  257. public function __toString()
  258. {
  259. $str = '';
  260. foreach ($this->objects as $obj) {
  261. $str .= '--------------------------<br />'.$obj.'<br />';
  262. }
  263. return $str;
  264. }
  265. #################### GETTERS
  266. public function getDeletedObjects()
  267. {
  268. return $this->deletedObjects;
  269. }
  270. public function getIterateNum()
  271. {
  272. return $this->iterateNum;
  273. }
  274. public function getNumObjects()
  275. {
  276. return $this->numObjects;
  277. }
  278. #################### SETTERS
  279. public function setDeletedObjects($key, $val)
  280. {
  281. $this->deletedObjects[$key] = $val;
  282. }
  283. public function resetDeletedObjects()
  284. {
  285. $this->deletedObjects = array();
  286. }
  287. }

An important note before we look at implementation: I use getters/setters to access the properties of all of my objects (properties are set as protected or private). So if I have a Foo class with a property named bar, I would access it through $foo->getBar();. Getters and setters are not everyone's cup of tea; without getting into that debate you can feel free to modify the methods above that call object properties through getters.

Alright, so what can we do with this thing? First let's create an example object, and populate a Collection with 10 of those suckers:

  1. <?php
  2. class Example
  3. {
  4. protected $id;
  5. public function setId($id) { $this->id = $id; }
  6. public function getId() { return $this->id; }
  7. }
  8. // create a new Collection class
  9. $collection = new Collection;
  10. // populate it with 10 "Example" objects
  11. for($i=1; $i <= 10; $i++) {
  12. $example = new Example;
  13. $example->setId($i);
  14. // add the object to the collection
  15. $collection->add($example);
  16. }

Some neat things we can do now:
NOTE: Several of the following examples will use my wtf() function to show the output.

Get the First Object in the Collection

  1. <?php
  2. $first = $collection->getCurrent();
  3. wtf($first);

Output:

Example Object
(
    [id:protected] => 1
)

Advance the Iterator and Get the Second Object in the Collection

  1. <?php
  2. $collection->next();
  3. $second = $collection->getCurrent();
  4. wtf($second);

Output:

Example Object
(
    [id:protected] => 2
)

Find An Object with an id of 5

  1. <?php
  2. if ($example5 = $collection->findByProperty('id', 5)) {
  3. wtf($example5);
  4. }

Output:

Example Object
(
    [id:protected] => 5
)

Reset the Iterator and Print a Table of Ids With Row Striping

  1. <?php
  2. $collection->resetIterator();
  3. echo '<table><tr><th>ID:</th></tr>';
  4. while($example = $collection->iterate()) {
  5. $style = $collection->isOdd() ? 'background-color: #eee;' : '';
  6. echo '<tr><td style="'.$style.'">'.$example->getId().'</td></tr>';
  7. }
  8. echo '</table>';

Output:

ID:
1
2
3
4
5
6
7
8
9
10

Delete an Object with an id of 2

  1. <?php
  2. $collection->removeByProperty('id', 2);
  3. wtf($collection);

Output:

Collection Object
(
    [objects:protected] => Array
        (
            [0] => Example Object
                (
                    [id:protected] => 1
                )

            [1] => Example Object
                (
                    [id:protected] => 3
                )

            [2] => Example Object
                (
                    [id:protected] => 4
                )

            [3] => Example Object
                (
                    [id:protected] => 5
                )

            [4] => Example Object
                (
                    [id:protected] => 6
                )

            [5] => Example Object
                (
                    [id:protected] => 7
                )

            [6] => Example Object
                (
                    [id:protected] => 8
                )

            [7] => Example Object
                (
                    [id:protected] => 9
                )

            [8] => Example Object
                (
                    [id:protected] => 10
                )

        )

    [deletedObjects:protected] => Array
        (
            [0] => Example Object
                (
                    [id:protected] => 2
                )

        )

    [resetFlag:protected] => 1
    [numObjects:protected] => 9
    [iterateNum:protected] => -1
)

Display the # of objects, or an Error Message

  1. <?php
  2. if ($collection->isEmpty()) {
  3. echo 'sorry, no objects';
  4. } else {
  5. echo $collection->getNumObjects().' objects!!!!!';
  6. }

Output:

9 objects!!!!!

Find How Many Objects Have an id of "7"

  1. <?php
  2. wtf($collection->getNumObjectsWithProperty('id', 7));

Output:

1

Display a Pipe-Delimited List of Ids

  1. <?php
  2. while($example = $collection->iterate()) {
  3. echo $example->getId();
  4. echo !$collection->currentObjIsLast() ? ' | ' : '';
  5. }

Output:

1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10

Order the Collection in Descending Order by the Objects' "id" Property

  1. <?php
  2. $collection->sortByProperty('id', 'rr');
  3. wtf($collection);

Output:

Collection Object
(
    [objects:protected] => Array
        (
            [0] => Example Object
                (
                    [id:protected] => 10
                )

            [1] => Example Object
                (
                    [id:protected] => 9
                )

            [2] => Example Object
                (
                    [id:protected] => 8
                )

            [3] => Example Object
                (
                    [id:protected] => 7
                )

            [4] => Example Object
                (
                    [id:protected] => 6
                )

            [5] => Example Object
                (
                    [id:protected] => 5
                )

            [6] => Example Object
                (
                    [id:protected] => 4
                )

            [7] => Example Object
                (
                    [id:protected] => 3
                )

            [8] => Example Object
                (
                    [id:protected] => 1
                )

        )

    [deletedObjects:protected] => Array
        (
            [0] => Example Object
                (
                    [id:protected] => 2
                )

        )

    [resetFlag:protected] => 1
    [numObjects:protected] => 9
    [iterateNum:protected] => 0
)

Delete Everything

  1. <?php
  2. $collection->removeAll();
  3. wtf($collection);

Output:

Collection Object
(
    [objects:protected] => Array
        (
        )

    [deletedObjects:protected] => Array
        (
            [0] => Example Object
                (
                    [id:protected] => 2
                )

            [1] => Example Object
                (
                    [id:protected] => 10
                )

            [2] => Example Object
                (
                    [id:protected] => 9
                )

            [3] => Example Object
                (
                    [id:protected] => 8
                )

            [4] => Example Object
                (
                    [id:protected] => 7
                )

            [5] => Example Object
                (
                    [id:protected] => 6
                )

            [6] => Example Object
                (
                    [id:protected] => 5
                )

            [7] => Example Object
                (
                    [id:protected] => 4
                )

            [8] => Example Object
                (
                    [id:protected] => 3
                )

            [9] => Example Object
                (
                    [id:protected] => 1
                )

        )

    [resetFlag:protected] => 1
    [numObjects:protected] => 0
    [iterateNum:protected] => 0
)

Posted by Aaron Fisher at 5:45pm

3 Comments RSS

# 1 By timswiley on September 22, 2012 - 4:06pm

timswiley

Let me just... coming  c/vb/java i really love the php language but have fallen far short from using  oop within because of no concept of a "collection" which to me is paramount to really enjoy real world scaleable benefits of php.  I've seen many hack methods that get close but like you found i thought they were all nasty and cumbersome...

This code here will help me tremendously... i don't program enough in php to sit and build something abstract like this but i do design web pages and have strong  oop/database interaction skills. 

I truely appreciate the effort you went thru to create this code, and share it with everyone.  You would think someday that PHP would include a natural collection class in its core library but i've been waiting  for 4 years at least and nothing  yet...

thank you again,

Tim Wiley


# 2 By Martijn on October 22, 2014 - 8:25am

Martijn

Today (since PHP5.3) you could use SplObjectStorage, see php.net/SplObjectStorage


# 3 By Bhavesh on December 15, 2014 - 7:15am

Bhavesh

Hello,

I'm fetching data from database and after manipulation i store in this collection object and pass it to view to represent in Table.

Now, I want to put paging for this table to traverse from starting page to end with page size of 10.
But i a'm unable to do it.

Please give me some suggestion.

Thanks
Bhavesh


Login To Post A Comment

Don't have an account yet? Sign Up