Passed
Push — master ( 1ab329...3a819d )
by Andreas
09:51
created

midcom_helper_nav_backend::get_leaf()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 8
ccs 5
cts 5
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @package midcom.helper
4
 * @author The Midgard Project, http://www.midgard-project.org
5
 * @copyright The Midgard Project, http://www.midgard-project.org
6
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
7
 */
8
9
/**
10
 * This class is the basic building stone of the Navigation Access Point
11
 * System of MidCOM.
12
 *
13
 * It is responsible for collecting the available
14
 * information and for building the navigational tree out of it. This
15
 * class is only the internal interface to the NAP System and is used by
16
 * midcom_helper_nav as a node cache. The framework should ensure that
17
 * only one class of this type is active at one time.
18
 *
19
 * It will give you a very abstract view of the content tree, modified
20
 * by the NAP classes of the components. You can retrieve a node/leaf tree
21
 * of the content, and for each element you can retrieve a URL name and a
22
 * long name for navigation display.
23
 *
24
 * Leaves and Nodes are both indexed by integer constants which are assigned
25
 * by the framework. The framework defines two starting points in this tree:
26
 * The root node and the "current" node. The current node defined through
27
 * the topic of the component that declared to be able to handle the request.
28
 *
29
 * The class will load the necessary information on demand to minimize
30
 * database traffic.
31
 *
32
 * The interface functions should enable you to build any navigation tree you
33
 * desire. The public nav class will give you some of those high-level
34
 * functions.
35
 *
36
 * <b>Node data interchange format</b>
37
 *
38
 * Node NAP data consists of a simple key => value array with the following
39
 * keys required by the component:
40
 *
41
 * - MIDCOM_NAV_NAME => The real (= displayable) name of the element
42
 *
43
 * Other keys delivered to NAP users include:
44
 *
45
 * - MIDCOM_NAV_URL  => The URL name of the element, which is automatically
46
 *   defined by NAP.
47
 *
48
 * <b>Leaf data interchange format</b>
49
 *
50
 * Basically for each leaf the usual meta information is returned:
51
 *
52
 * - MIDCOM_NAV_URL      => URL of the leaf element
53
 * - MIDCOM_NAV_NAME     => Name of the leaf element
54
 * - MIDCOM_NAV_GUID     => Optional argument denoting the GUID of the referred element
55
 * - MIDCOM_NAV_SORTABLE => Optional argument denoting whether the element is sortable
56
 *
57
 * @package midcom.helper
58
 */
59
class midcom_helper_nav_backend
60
{
61
    /**
62
     * The ID of the MidCOM Root Content Topic
63
     */
64
    private int $_root;
65
66
    /**
67
     * The ID of the currently active Navigation Node, determined by the active
68
     * MidCOM Topic or one of its uplinks, if the subtree in question is invisible.
69
     */
70
    private int $_current;
71
72
    /**
73
     * The GUID of the currently active leaf.
74
     */
75
    private ?string $_currentleaf = null;
76
77
    /**
78
     * Leaf cache. It is an array which contains elements indexed by
79
     * their leaf ID. The data is again stored in an associative array:
80
     *
81
     * - MIDCOM_NAV_NODEID => ID of the parent node (int)
82
     * - MIDCOM_NAV_URL => URL name of the leaf (string)
83
     * - MIDCOM_NAV_NAME => Textual name of the leaf (string)
84
     *
85
     * @todo Update the data structure documentation
86
     * @var midcom_helper_nav_leaf[]
87
     */
88
    private array $_leaves = [];
89
90
    /**
91
     * Node cache. It is an array which contains elements indexed by
92
     * their node ID. The data is again stored in an associative array:
93
     *
94
     * - MIDCOM_NAV_NODEID => ID of the parent node (-1 for the root node) (int)
95
     * - MIDCOM_NAV_URL => URL name of the leaf (string)
96
     * - MIDCOM_NAV_NAME => Textual name of the leaf (string)
97
     *
98
     * @todo Update the data structure documentation
99
     * @var midcom_helper_nav_node[]
100
     */
101
    private static array $_nodes = [];
102
103
    /**
104
     * List of all topics for which the leaves have been loaded.
105
     * If the id of the node is in this array, the leaves are available, otherwise,
106
     * the leaves have to be loaded.
107
     *
108
     * @var midcom_helper_nav_leaf[]
109
     */
110
    private array $_loaded_leaves = [];
111
112
    /**
113
     * The NAP cache store
114
     */
115
    private midcom_services_cache_module_nap $_nap_cache;
116
117
    /**
118
     * This array holds the node path from the URL. First value at key 0 is
119
     * the root node ID, possible second value is the first subnode ID etc.
120
     * Contains only visible nodes (nodes which can be loaded).
121
     */
122
    private array $_node_path = [];
123
124
    /**
125
     * Will initialize Root Topic, Current Topic and all cache arrays.
126
     * The constructor retrieves all initialization data from the component context.
127
     *
128
     * @param midcom_db_topic[] $urltopics
129
     */
130 288
    public function __construct(midcom_db_topic $root, array $urltopics)
131
    {
132 288
        $this->_nap_cache = midcom::get()->cache->nap;
133
134 288
        $this->_root = $root->id;
135 288
        $this->_current = $this->_root;
136
137 288
        $this->init_topics($root, $urltopics);
138
    }
139
140
    /**
141
     * Loads all nodes between root and current node.
142
     *
143
     * If the current node is behind an invisible or undescendable node, the last
144
     * known good node will be used instead for the current node.
145
     *
146
     * @param midcom_db_topic[] $urltopics
147
     */
148 288
    private function init_topics(midcom_db_topic $root, array $urltopics)
149
    {
150 288
        $node_path_candidates = array_merge([$root], $urltopics);
151 288
        $this->_current = end($node_path_candidates)->id;
152
153 288
        $lastgood = null;
154 288
        foreach ($node_path_candidates as $topic) {
155 288
            if (!$this->load_node($topic)) {
156
                // Node is hidden behind an undescendable one
157
                $this->_current = $lastgood;
158
                return;
159
            }
160 288
            $this->_node_path[] = $topic->id;
161 288
            $lastgood = $topic->id;
162
        }
163
    }
164
165
    /**
166
     * Load the navigational information associated with the topic $param, which
167
     * can be passed as an ID or as a MidgardTopic object.
168
     *
169
     * This function is the controlling instance of the loading mechanism. It
170
     * is able to load the navigation data of any topic within MidCOM's topic
171
     * tree into memory. Any uplink nodes that are not loaded into memory will
172
     * be loaded until any other known topic is encountered.
173
     *
174
     * This method does query the topic for all information and completes it to
175
     * build up a full NAP data structure
176
     *
177
     * It determines the URL_NAME of the topic automatically using the name of the
178
     * topic in question.
179
     *
180
     * The currently active leaf is only queried if and only if the currently
181
     * processed topic is equal to the current context's content topic. This should
182
     * prevent dynamically loaded components from disrupting active leaf information,
183
     * as this can happen if dynamic_load is called before showing the navigation.
184
     *
185
     * @param mixed $topic Topic object or ID to be processed
186
     */
187 356
    private function load_node($topic) : bool
188
    {
189 356
        if ($topic instanceof midcom_db_topic) {
190 288
            $id = $topic->id;
191
        } else {
192 342
            $id = $topic;
193
        }
194 356
        if (!array_key_exists($id, self::$_nodes)) {
195 142
            $node = new midcom_helper_nav_node($topic);
196 142
            if (!$node->is_visible()) {
197 15
                return false;
198
            }
199
200 132
            if ($node->id == $this->_root) {
201 61
                $node->nodeid = -1;
202 61
                $node->relativeurl = '';
203 61
                $node->url = '';
204
            } else {
205 82
                if (!$node->nodeid || !$this->load_node($node->nodeid)) {
206 64
                    return false;
207
                }
208 19
                $node->relativeurl = self::$_nodes[$node->nodeid]->relativeurl . $node->url;
209
            }
210
            // Rewrite all host dependent URLs based on the relative URL within our topic tree.
211 78
            $node->fullurl = midcom::get()->config->get('midcom_site_url') . $node->relativeurl;
212 78
            $node->absoluteurl = midcom_connection::get_url('self') . $node->relativeurl;
213 78
            $node->permalink = midcom::get()->permalinks->create_permalink($node->guid);
214
215
            // The node is visible, add it to the list.
216 78
            self::$_nodes[$id] = $node;
217
        } else {
218 301
            $node = self::$_nodes[$id];
219
        }
220
        // Set the current leaf, this does *not* load the leaves from the DB, this is done during get_leaf.
221 301
        if ($node->id === $this->_current) {
222 297
            $currentleaf = midcom_baseclasses_components_configuration::get($node->component, 'active_leaf');
223 297
            if ($currentleaf !== false) {
224 26
                $this->_currentleaf = "{$node->id}-{$currentleaf}";
225
            }
226
        }
227
228 301
        return true;
229
    }
230
231
    /**
232
     * Return the list of leaves for a given node. This helper will construct complete leaf
233
     * data structures for each leaf found. It will first check the cache for the leaf structures,
234
     * and query the database only if the corresponding objects have not been found there.
235
     */
236 47
    private function load_leaves(midcom_helper_nav_node $node)
237
    {
238 47
        if (array_key_exists($node->id, $this->_loaded_leaves)) {
239 17
            return;
240
        }
241 43
        $this->_loaded_leaves[$node->id] = [];
242
243 43
        $fullprefix = midcom::get()->config->get('midcom_site_url');
244 43
        $absoluteprefix = midcom_connection::get_url('self');
245
246 43
        foreach ($node->get_leaves() as $id => $leaf) {
247 31
            if (!$leaf->is_visible()) {
248
                continue;
249
            }
250
251
            // Rewrite all host-dependent URLs based on the relative URL within our topic tree.
252 31
            $leaf->fullurl = $fullprefix . $leaf->relativeurl;
253 31
            $leaf->absoluteurl = $absoluteprefix . $leaf->relativeurl;
254
255 31
            if ($leaf->guid === null) {
256 6
                $leaf->permalink = $leaf->fullurl;
257
            } else {
258 25
                $leaf->permalink = midcom::get()->permalinks->create_permalink($leaf->guid);
259
            }
260
261 31
            $this->_leaves[$id] = $leaf;
262 31
            $this->_loaded_leaves[$node->id][$id] =& $this->_leaves[$id];
263
        }
264
    }
265
266
    /**
267
     * Verifies the existence of a given leaf. Call this before getting a leaf from the
268
     * $_leaves cache. It will load all necessary nodes/leaves as necessary.
269
     *
270
     * @param string $leaf_id A valid NAP leaf id ($nodeid-$leafid pattern).
271
     */
272 40
    private function load_leaf(string $leaf_id) : bool
273
    {
274 40
        if (!$leaf_id) {
275
            debug_add("Tried to load an empty leaf id.");
276
            return false;
277
        }
278
279 40
        if (array_key_exists($leaf_id, $this->_leaves)) {
280 2
            return true;
281
        }
282
283 40
        $node_id = explode('-', $leaf_id)[0];
284
285 40
        if (!$this->load_node($node_id)) {
286
            debug_add("Tried to verify the leaf id {$leaf_id}, which should belong to node {$node_id}, but this node cannot be loaded, see debug level log for details.",
287
            MIDCOM_LOG_INFO);
288
            return false;
289
        }
290 40
        $this->load_leaves(self::$_nodes[$node_id]);
291
292 40
        return array_key_exists($leaf_id, $this->_leaves);
293
    }
294
295
    /**
296
     * Lists all Sub-nodes of $parent_node. If there are no subnodes, or if there was an error
297
     * (for instance an unknown parent node ID) you will get an empty array
298
     *
299
     * @param mixed $parent_node    The ID of the node of which the subnodes are searched.
300
     * @param boolean $show_noentry Show all objects on-site which have the noentry flag set.
301
     */
302 21
    public function list_nodes($parent_node, bool $show_noentry) : array
303
    {
304 21
        static $listed = [];
305
306 21
        if (!$this->load_node($parent_node)) {
307
            debug_add("Unable to load parent node $parent_node", MIDCOM_LOG_ERROR);
308
            return [];
309
        }
310
311 21
        $cache_identifier = $parent_node . ($show_noentry ? 'noentry' : '');
312 21
        if (!isset($listed[$cache_identifier])) {
313 10
            $listed[$cache_identifier] = [];
314
315 10
            foreach (self::$_nodes[$parent_node]->get_subnodes() as $id) {
316 1
                if (!$this->load_node($id)) {
317
                    continue;
318
                }
319
320 1
                if (   !$show_noentry
321 1
                    && self::$_nodes[$id]->noentry) {
322
                    // Hide "noentry" items
323
                    continue;
324
                }
325
326 1
                $listed[$cache_identifier][] = $id;
327
            }
328
        }
329
330 21
        return $listed[$cache_identifier];
331
    }
332
333
    /**
334
     * Lists all leaves of $parent_node. If there are no leaves, or if there was an error
335
     * (for instance an unknown parent node ID) you will get an empty array,
336
     *
337
     * @param mixed $parent_node    The ID of the node of which the leaves are searched.
338
     * @param boolean $show_noentry Show all objects on-site which have the noentry flag set.
339
     */
340 18
    public function list_leaves($parent_node, bool $show_noentry) : array
341
    {
342 18
        static $listed = [];
343
344 18
        if (!$this->load_node($parent_node)) {
345
            return [];
346
        }
347 18
        $cache_key = $parent_node . '--' . $show_noentry;
348
349 18
        if (!isset($listed[$cache_key])) {
350 8
            $listed[$cache_key] = [];
351 8
            $this->load_leaves(self::$_nodes[$parent_node]);
352
353 8
            foreach ($this->_loaded_leaves[self::$_nodes[$parent_node]->id] as $id => $leaf) {
354 1
                if ($show_noentry || !$leaf->noentry) {
355 1
                    $listed[$cache_key][] = $id;
356
                }
357
            }
358
        }
359
360 18
        return $listed[$cache_key];
361
    }
362
363
    /**
364
     * This is a helper function used by midcom_helper_nav::resolve_guid(). It
365
     * checks if the object denoted by the passed GUID is already loaded into
366
     * memory and returns it, if available. This should speed up GUID lookup heavy
367
     * code.
368
     *
369
     * @return Array A NAP structure if the GUID is known, null otherwise.
370
     */
371 28
    public function get_loaded_object_by_guid(string $guid) : ?array
372
    {
373 28
        $entry = $this->_nap_cache->get_guid($guid);
374 28
        if (empty($entry)) {
375 20
            return null;
376
        }
377 8
        if ($entry[MIDCOM_NAV_TYPE] == 'leaf') {
378
            return $this->get_leaf($entry[MIDCOM_NAV_ID]);
379
        }
380 8
        return $this->get_node($entry[MIDCOM_NAV_ID]);
381
    }
382
383
    /**
384
     * This will give you a key-value pair describing the node with the ID
385
     * $node_id. The defined keys are described above in Node data interchange
386
     * format. You will get false if the node ID is invalid.
387
     *
388
     * @param mixed $node_id    The node ID to be retrieved.
389
     */
390 328
    public function get_node($node_id) : ?array
391
    {
392 328
        $node = $node_id;
393 328
        if (!empty($node->guid)) {
394
            $node_id = $node->id;
395
        }
396 328
        if (!$this->load_node($node_id)) {
397 75
            return null;
398
        }
399
400 273
        return self::$_nodes[$node_id]->get_data();
401
    }
402
403
    /**
404
     * This will give you a key-value pair describing the leaf with the ID
405
     * $node_id. The defined keys are described above in leaf data interchange
406
     * format. You will get null if the leaf ID is invalid.
407
     *
408
     * @param string $leaf_id    The leaf-id to be retrieved.
409
     */
410 40
    public function get_leaf(string $leaf_id) : ?array
411
    {
412 40
        if (!$this->load_leaf($leaf_id)) {
413 22
            debug_add("This leaf is unknown, aborting.", MIDCOM_LOG_INFO);
414 22
            return null;
415
        }
416
417 24
        return $this->_leaves[$leaf_id]->get_data();
418
    }
419
420
    /**
421
     * Retrieve the ID of the currently displayed node. Defined by the topic of
422
     * the component that declared able to handle the request.
423
     */
424 243
    public function get_current_node() : int
425
    {
426 243
        return $this->_current;
427
    }
428
429
    /**
430
     * Retrieve the ID of the currently displayed leaf. This is a leaf that is
431
     * displayed by the handling topic. If no leaf is active, this function
432
     * returns null. (Remember to make a type sensitive check, e.g.
433
     * nav::get_current_leaf() !== null to distinguish "0" and "null".)
434
     */
435 257
    public function get_current_leaf() : ?string
436
    {
437 257
        return $this->_currentleaf;
438
    }
439
440
    /**
441
     * Retrieve the ID of the upper node of the currently displayed node.
442
     */
443 2
    public function get_current_upper_node() : int
444
    {
445 2
        if (count($this->_node_path) > 1) {
446 1
            return $this->_node_path[count($this->_node_path) - 2];
447
        }
448 1
        return $this->_node_path[0];
449
    }
450
451
    /**
452
     * Retrieve the ID of the root node. Note that this ID is dependent from the
453
     * ID of the MidCOM Root topic and therefore will change as easily as the
454
     * root topic ID might. The MIDCOM_NAV_URL entry of the root node's data will
455
     * always be empty.
456
     */
457 100
    public function get_root_node() : int
458
    {
459 100
        return $this->_root;
460
    }
461
462
    /**
463
     * Retrieve the IDs of the nodes from the URL. First value at key 0 is
464
     * the root node ID, possible second value is the first subnode ID etc.
465
     * Contains only visible nodes (nodes which can be loaded).
466
     */
467 12
    public function get_node_path() : array
468
    {
469 12
        return $this->_node_path;
470
    }
471
}
472