1 <?php
2
3 /*
4 * This file is part of the Icybee package.
5 *
6 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Icybee\Modules\Pages;
13
14 use ICanBoogie\ActiveRecord\Query;
15 use ICanBoogie\PropertyNotDefined;
16
17 /**
18 * A simplified data structure representing the relashionship between pages.
19 *
20 * @property-read array[]Page $ordered_records Records ordered according to their position and
21 * relation. See: {@link get_ordered_records()}.
22 *
23 * @see BlueprintNode
24 */
25 class Blueprint
26 {
27 /**
28 * Creates a {@link Blueprint} instance from an {@link ActiveRecord\Query}.
29 *
30 * @param Query $query
31 *
32 * @return Blueprint
33 */
34 static public function from(Query $query)
35 {
36 $query->mode(\PDO::FETCH_CLASS, __NAMESPACE__ . '\BluePrintNode');
37
38 $relation = array();
39 $children = array();
40 $index = array();
41
42 foreach ($query as $row)
43 {
44 $row->parent = null;
45 $row->depth = null;
46 $row->children = array();
47
48 $nid = $row->nid;
49 $parent_id = $row->parentid;
50 $index[$nid] = $row;
51 $relation[$nid] = $parent_id;
52 $children[$parent_id][$nid] = $nid;
53 }
54
55 $tree = array();
56
57 foreach ($index as $nid => $page)
58 {
59 if (!$page->parentid || empty($index[$page->parentid]))
60 {
61 $tree[$nid] = $page;
62
63 continue;
64 }
65
66 $page->parent = $index[$page->parentid];
67 $page->parent->children[$nid] = $page;
68 }
69
70 self::set_depth($tree);
71
72 return new static($query->model, $relation, $children, $index, $tree);
73 }
74
75 /**
76 * Set the depth of the nodes of the specified branch.
77 *
78 * @param array $branch
79 * @param number $depth Starting depth.
80 */
81 static private function set_depth(array $branch, $depth=0)
82 {
83 foreach ($branch as $node)
84 {
85 $node->depth = $depth;
86
87 if (!$node->children)
88 {
89 continue;
90 }
91
92 self::set_depth($node->children, $depth + 1);
93 }
94 }
95
96 /**
97 * The child/parent relation.
98 *
99 * An array where each key/value is the identifier of a node and the idenfier of its parent,
100 * or zero if the node has no parent.
101 *
102 * @var array[int]int
103 */
104 public $relation;
105
106 /**
107 * The parent/children relation.
108 *
109 * An array where each key/value is the identifier of a parent and an array made of the
110 * identifiers of its children. Each key/value pair of the children value is made of the
111 * child identifier.
112 *
113 * @var array[int]array
114 */
115 public $children;
116
117 /**
118 * Index of the blueprint nodes.
119 *
120 * Blueprint nodes are instances of the {@link BlueprintNode} class. The key of the index is
121 * the identifier of the node, while the value is the node instance.
122 *
123 * @var array[int]BlueprintNode
124 */
125 public $index;
126
127 /**
128 * Pages nested as a tree.
129 *
130 * @var array[int]BlueprintNode
131 */
132 public $tree;
133
134 /**
135 * Model associated with the blueprint.
136 *
137 * @var \ICanBoogie\ActiveRecord\Model
138 */
139 public $model;
140
141 /**
142 * The blueprint is usualy constructed by the {@link Model::blueprint()} method or the
143 * {@link subset()} method.
144 *
145 * @param Model $model
146 * @param array $relation The child/parent relations.
147 * @param array $children The parent/children relations.
148 * @param array $index Pages index.
149 * @param array $tree Pages nested as a tree.
150 */
151 protected function __construct(Model $model, array $relation, array $children, array $index, array $tree)
152 {
153 $this->relation = $relation;
154 $this->children = $children;
155 $this->index = $index;
156 $this->tree = $tree;
157 $this->model = $model;
158 }
159
160 /**
161 * Support for the {@link $ordered_records} property.
162 *
163 * @param string $property
164 *
165 * @throws PropertyNotDefined in attempt to get a property that is not defined or supported.
166 *
167 * @return mixed
168 */
169 public function __get($property)
170 {
171 switch ($property)
172 {
173 case 'ordered_nodes';
174
175 return $this->get_ordered_nodes();
176
177 case 'ordered_records':
178
179 return $this->get_ordered_records();
180 }
181
182 throw new PropertyNotDefined(array($property, $this));
183 }
184
185 protected function get_ordered_nodes()
186 {
187 $nodes = array();
188
189 $ordering = function(array $branch) use(&$ordering, &$nodes) {
190
191 foreach ($branch as $node)
192 {
193 $nodes[$node->nid] = $node;
194
195 if ($node->children)
196 {
197 $ordering($node->children);
198 }
199 }
200 };
201
202 $ordering($this->tree);
203
204 return $nodes;
205 }
206
207 /**
208 * Returns the records of the blueprint ordered according to their position and relation.
209 *
210 * Note: The blueprint is populated with records if needed.
211 *
212 * @return array[int]ActiveRecord
213 */
214 protected function get_ordered_records()
215 {
216 $records = array();
217
218 $ordering = function(array $branch) use(&$ordering, &$records) {
219
220 foreach ($branch as $node)
221 {
222 $records[$node->nid] = $node->record;
223
224 if ($node->children)
225 {
226 $ordering($node->children);
227 }
228 }
229 };
230
231 $node = current($this->index);
232
233 if (empty($node->record))
234 {
235 $this->populate();
236 }
237
238 $ordering($this->tree);
239
240 return $records;
241 }
242
243 /**
244 * Checks if a branch has children.
245 *
246 * @param int $nid Identifier of the parent record.
247 *
248 * @return boolean
249 */
250 public function has_children($nid)
251 {
252 return !empty($this->children[$nid]);
253 }
254
255 /**
256 * Returns the number of children of a branch.
257 *
258 * @param int $nid The identifier of the parent record.
259 *
260 * @return int
261 */
262 public function children_count($nid)
263 {
264 return $this->has_children($nid) ? count($this->children[$nid]) : 0;
265 }
266
267 /**
268 * Create a subset of the blueprint.
269 *
270 * A filter can be specified to filter out the nodes of the subset. The function returns `true`
271 * to discart a node. The callback function have the following signature:
272 *
273 * function(BlueprintNode $node)
274 *
275 * The following example demonstrate how offline nodes cen be filtered out.
276 *
277 * <pre>
278 * <?php
279 *
280 * use Icybee\Modules\Pages\BlueprintNode;
281 *
282 * $subset = $core->models['pages']
283 * ->blueprint($site_id = 1)
284 * ->subset(null, null, function(BlueprintNode $node) {
285 *
286 * return !$node->is_online;
287 *
288 * });
289 * </pre>
290 *
291 * @param int $nid Identifier of the starting branch.
292 * @param int $depth Maximum depth of the subset.
293 * @param callable $filter The filter callback.
294 *
295 * @return Blueprint
296 */
297 public function subset($nid=null, $depth=null, $filter=null)
298 {
299 $relation = array();
300 $children = array();
301 $index = array();
302
303 $iterator = function(array $branch) use(&$iterator, &$filter, &$depth, &$relation, &$children, &$index)
304 {
305 $pages = array();
306
307 foreach ($branch as $nid => $node)
308 {
309 $node_children = $node->children;
310 $node = clone $node;
311 $node->children = array();
312
313 if ($node_children && ($depth === null || $node->depth < $depth))
314 {
315 $node->children = $iterator($node_children);
316 }
317
318 if ($filter && $filter($node))
319 {
320 continue;
321 }
322
323 $parentid = $node->parentid;
324
325 $relation[$nid] = $parentid;
326 $children[$parentid][] = $nid;
327 $pages[$nid] = $node;
328 $index[$nid] = $node;
329 }
330
331 return $pages;
332 };
333
334 $tree = $iterator($nid ? $this->index[$nid]->children : $this->tree);
335
336 return new static($this->model, $relation, $children, $index, $tree);
337 }
338
339 /**
340 * Populates the blueprint by loading the associated records.
341 *
342 * The method adds the `record` property to the blueprint nodes.
343 *
344 * @return array[int]\Icybee\Modules\Pages\Page
345 */
346 public function populate()
347 {
348 if (!$this->index)
349 {
350 return array();
351 }
352
353 $records = $this->model->find(array_keys($this->index));
354
355 foreach ($records as $nid => $record)
356 {
357 $this->index[$nid]->record = $record;
358 }
359
360 return $records;
361 }
362 }
363
364 /**
365 * A node of the blueprint.
366 *
367 * @property-read int $children_count The number of children.
368 * @property-read array[int]ActiveRecord $descendents The descendents ordered according to their
369 * position and relation.
370 * @property-read int $descendents_count The number of descendents.
371 *
372 * @see Blueprint
373 */
374 class BlueprintNode
375 {
376 /**
377 * The identifier of the page.
378 *
379 * @var int
380 */
381 public $nid;
382
383 /**
384 * Depth of the node is the tree.
385 *
386 * @var int
387 */
388 public $depth;
389
390 /**
391 * The identifier of the parent of the page.
392 *
393 * @var int
394 */
395 public $parentid;
396
397 /**
398 * Blueprint node of the parent of the page.
399 *
400 * @var BlueprintNode
401 */
402 public $parent;
403
404 /**
405 * The children of the node.
406 *
407 * @var array[int]BlueprintNode
408 */
409 public $children;
410
411 /**
412 * Inaccessible properties are obtained from the record.
413 *
414 * @param string $property
415 */
416 public function __get($property)
417 {
418 switch ($property)
419 {
420 case 'children_count': return count($this->children);
421 case 'descendents': return $this->get_descendents();
422 case 'descendents_count': return $this->get_descendents_count();
423 }
424
425 return $this->record->$property;
426 }
427
428 /**
429 * Return the descendent nodes of the node.
430 *
431 * @return int
432 */
433 protected function get_descendents()
434 {
435 $descendents = array();
436
437 foreach ($this->children as $nid => $child)
438 {
439 $descendents[$nid] = $child;
440 $descendents += $child->descendents;
441 }
442
443 return $descendents;
444 }
445
446 /**
447 * Return the number of descendents.
448 *
449 * @return int
450 */
451 protected function get_descendents_count()
452 {
453 $n = 0;
454
455 foreach ($this->children as $child)
456 {
457 $n += 1 + $child->descendents_count;
458 }
459
460 return $n;
461 }
462
463 /**
464 * Forwards calls to the record.
465 *
466 * @param string $method
467 * @param array $arguments
468 *
469 * @return mixed
470 */
471 public function __call($method, $arguments)
472 {
473 return call_user_func_array($this->record, $method, $arguments);
474 }
475 }