# Source for file NestedSet.php

Documentation is available at NestedSet.php

1. <?php
2. /*
3. * $Id: NestedSet.php 2230 2007-08-14 16:37:48Z Jonathan.Wage $
4. *
5. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
6. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
7. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
8. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
9. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
10. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
11. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
12. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
13. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
14. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
15. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
16. *
17. * This software consists of voluntary contributions made by many individuals
18. * and is licensed under the LGPL. For more information, see
19. * <http://www.phpdoctrine.com>.
20. */
21. /**
22. * Doctrine_Tree_NestedSet
23. *
24. * @package Doctrine
25. * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
26. * @category Object Relational Mapping
27. * @link www.phpdoctrine.com
28. * @since 1.0
29. * @version $Revision: 2230 $
30. * @author Joe Simms <joe.simms@websites4.com>
31. */
32. class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Interface
33. {
34. private $_baseQuery;
35. private $_baseAlias = "base";
37. /**
38. * constructor, creates tree with reference to table and sets default root options
39. *
40. * @param object $table instance of Doctrine_Table
41. * @param array $options options
42. */
43. public function __construct(Doctrine_Table $table, $options)
44. {
45. // set default many root attributes
46. $options['hasManyRoots'] = isset($options['hasManyRoots']) ? $options['hasManyRoots'] : false;
47. if($options['hasManyRoots'])
48. $options['rootColumnName'] = isset($options['rootColumnName']) ? $options['rootColumnName'] : 'root_id';
50. parent::__construct($table, $options);
51. }
53. /**
54. * used to define table attributes required for the NestetSet implementation
55. * adds lft and rgt columns for corresponding left and right values
56. *
57. */
58. public function setTableDefinition()
59. {
60. if ($root = $this->getAttribute('rootColumnName')) {
61. $this->table->setColumn($root, 'integer', 4);
62. }
64. $this->table->setColumn('lft', 'integer', 4);
65. $this->table->setColumn('rgt', 'integer', 4);
66. $this->table->setColumn('level', 'integer', 2);
67. }
69. /**
70. * creates root node from given record or from a new record
71. *
72. * @param object $record instance of Doctrine_Record
73. */
74. public function createRoot(Doctrine_Record $record = null)
75. {
76. if ( ! $record) {
77. $record = $this->table->create();
78. }
80. // if tree is many roots, and no root id has been set, then get next root id
81. if ($root = $this->getAttribute('hasManyRoots') && $record->getNode()->getRootValue() <= 0) {
82. $record->getNode()->setRootValue($this->getNextRootId());
83. }
85. $record->set('lft', '1');
86. $record->set('rgt', '2');
87. $record->set('level', 0);
89. $record->save();
91. return $record;
92. }
94. /**
95. * returns root node
96. *
97. * @return object $record instance of Doctrine_Record
98. * @deprecated Use fetchRoot()
99. */
100. public function findRoot($rootId = 1)
101. {
102. return $this->fetchRoot($rootId);
103. }
105. /**
106. * Fetches a/the root node.
107. *
108. * @param integer $rootId
109. */
110. public function fetchRoot($rootId = 1)
111. {
112. $q = $this->getBaseQuery();
113. $q = $q->addWhere($this->_baseAlias . '.lft = ?', 1);
115. // if tree has many roots, then specify root id
116. $q = $this->returnQueryWithRootId($q, $rootId);
117. $data = $q->execute();
119. if (count($data) <= 0) {
120. return false;
121. }
123. if ($data instanceof Doctrine_Collection) {
124. $root = $data->getFirst();
125. $root['level'] = 0;
126. } else if (is_array($data)) {
127. $root = array_shift($data);
128. $root['level'] = 0;
129. } else {
130. throw new Doctrine_Tree_Exception("Unexpected data structure returned.");
131. }
133. return $root;
134. }
136. /**
137. * Fetches a tree.
138. *
139. * @param array $options Options
140. * @return mixed The tree or FALSE if the tree could not be found.
141. */
142. public function fetchTree($options = array())
143. {
144. // fetch tree
145. $q = $this->getBaseQuery();
147. $q = $q->addWhere($this->_baseAlias . ".lft >= ?", 1);
149. // if tree has many roots, then specify root id
150. $rootId = isset($options['root_id']) ? $options['root_id'] : '1';
151. if (is_array($rootId)) {
152. $q->addOrderBy($this->_baseAlias . "." . $this->getAttribute('rootColumnName') .
153. ", " . $this->_baseAlias . ".lft ASC");
154. } else {
155. $q->addOrderBy($this->_baseAlias . ".lft ASC");
156. }
158. $q = $this->returnQueryWithRootId($q, $rootId);
159. $tree = $q->execute();
161. if (count($tree) <= 0) {
162. return false;
163. }
165. return $tree;
166. }
168. /**
169. * Fetches a branch of a tree.
170. *
171. * @param mixed $pk primary key as used by table::find() to locate node to traverse tree from
172. * @param array $options Options.
173. * @return mixed The branch or FALSE if the branch could not be found.
174. * @todo Only fetch the lft and rgt values of the initial record. more is not needed.
175. */
176. public function fetchBranch($pk, $options = array())
177. {
178. $record = $this->table->find($pk);
179. if ( ! ($record instanceof Doctrine_Record) || !$record->exists()) {
180. // TODO: if record doesn't exist, throw exception or similar?
181. return false;
182. }
183. //$depth = isset($options['depth']) ? $options['depth'] : null;
185. $q = $this->getBaseQuery();
186. $params = array($record->get('lft'), $record->get('rgt'));
187. $q->addWhere($this->_baseAlias . ".lft >= ? AND " . $this->_baseAlias . ".rgt <= ?", $params)
188. ->addOrderBy($this->_baseAlias . ".lft asc");
189. $q = $this->returnQueryWithRootId($q, $record->getNode()->getRootValue());
190. return $q->execute();
191. }
193. /**
194. * Fetches all root nodes. If the tree has only one root this is the same as
195. * fetchRoot().
196. *
197. * @return mixed The root nodes.
198. */
199. public function fetchRoots()
200. {
201. $q = $this->getBaseQuery();
202. $q = $q->addWhere($this->_baseAlias . '.lft = ?', 1);
203. return $q->execute();
204. }
206. /**
207. * calculates the next available root id
208. *
209. * @return integer
210. */
211. public function getNextRootId()
212. {
213. return $this->getMaxRootId() + 1;
214. }
216. /**
217. * calculates the current max root id
218. *
219. * @return integer
220. */
221. public function getMaxRootId()
222. {
223. $component = $this->table->getComponentName();
224. $column = $this->getAttribute('rootColumnName');
226. // cannot get this dql to work, cannot retrieve result using $coll[0]->max
227. //$dql = "SELECT MAX(c.$column) FROM $component c";
229. $dql = 'SELECT c.' . $column . ' FROM ' . $component . ' c ORDER BY c.' . $column . ' DESC LIMIT 1';
231. $coll = $this->table->getConnection()->query($dql);
233. $max = $coll[0]->get($column);
235. $max = !is_null($max) ? $max : 0;
237. return $max;
238. }
240. /**
241. * returns parsed query with root id where clause added if applicable
242. *
243. * @param object $query Doctrine_Query
244. * @param integer $root_id id of destination root
245. * @return object Doctrine_Query
246. */
247. public function returnQueryWithRootId($query, $rootId = 1)
248. {
249. if ($root = $this->getAttribute('rootColumnName')) {
250. if (is_array($rootId)) {
251. $query->addWhere($root . ' IN (' . implode(',', array_fill(0, count($rootId), '?')) . ')',
252. $rootId);
253. } else {
254. $query->addWhere($root . ' = ?', $rootId);
255. }
256. }
258. return $query;
259. }
261. /**
262. * Enter description here...
263. *
264. * @param array $options
265. * @return unknown
266. */
267. public function getBaseQuery()
268. {
269. if (!isset($this->_baseQuery)) {
270. $this->_baseQuery = $this->_createBaseQuery();
271. }
272. return $this->_baseQuery->copy();
273. }
275. /**
276. * Enter description here...
277. *
278. */
279. public function getBaseAlias()
280. {
281. return $this->_baseAlias;
282. }
284. /**
285. * Enter description here...
286. *
287. */
288. private function _createBaseQuery()
289. {
290. $this->_baseAlias = "base";
291. $q = new Doctrine_Query();
292. $q->select($this->_baseAlias . ".*")->from($this->getBaseComponent() . " " . $this->_baseAlias);
293. return $q;
294. }
296. /**
297. * Enter description here...
298. *
299. * @param Doctrine_Query $query
300. */
301. public function setBaseQuery(Doctrine_Query $query)
302. {
303. $this->_baseAlias = $query->getRootAlias();
304. $query->addSelect($this->_baseAlias . ".lft, " . $this->_baseAlias . ".rgt, ". $this->_baseAlias . ".level");
305. if ($this->getAttribute('rootColumnName')) {
306. $query->addSelect($this->_baseAlias . "." . $this->getAttribute('rootColumnName'));
307. }
308. $this->_baseQuery = $query;
309. }
311. /**
312. * Enter description here...
313. *
314. */
315. public function resetBaseQuery()
316. {
317. $this->_baseQuery = null;
318. }
320. /**
321. * Enter description here...
322. *
323. * @param unknown_type $graph
324. */
325. /*
326. public function computeLevels($tree)
327. {
328. $right = array();
329. $isArray = is_array($tree);
330. $rootColumnName = $this->getAttribute('rootColumnName');
332. for ($i = 0, $count = count($tree); $i < $count; $i++) {
333. if ($rootColumnName && $i > 0 && $tree[$i][$rootColumnName] != $tree[$i-1][$rootColumnName]) {
334. $right = array();
335. }
337. if (count($right) > 0) {
338. while (count($right) > 0 && $right[count($right)-1] < $tree[$i]['rgt']) {
339. //echo count($right);
340. array_pop($right);
341. }
342. }
344. if ($isArray) {
345. $tree[$i]['level'] = count($right);
346. } else {
347. $tree[$i]->getNode()->setLevel(count($right));
348. }
350. $right[] = $tree[$i]['rgt'];
351. }
352. return $tree;
353. }
354. */
355. }