Passed
Push — master ( cc2a52...b609fd )
by Andreas
23:17
created

midcom_helper__dbfactory::_get_parent_candidates()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 34
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 7.1086

Importance

Changes 0
Metric Value
cc 7
eloc 22
nc 9
nop 1
dl 0
loc 34
ccs 20
cts 23
cp 0.8696
crap 7.1086
rs 8.6346
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
use midgard\portable\api\error\exception as mgd_exception;
10
use midgard\portable\api\mgdobject;
11
12
/**
13
 * This class contains various factory methods to retrieve objects from the database.
14
 * The only instance of this class you should ever use is available through
15
 * midcom::get()->dbfactory.
16
 *
17
 * @package midcom.helper
18
 */
19
class midcom_helper__dbfactory
20
{
21
    /**
22
     * ID => GUID cache for parents
23
     *
24
     * @var array
25
     */
26
    private $_parent_mapping = [];
27
28
    /**
29
     * Cache for possible parent configurations per mgdschema class
30
     *
31
     * @var array
32
     */
33
    private $_parent_candidates = [];
34
35
    /**
36
     * This is a replacement for the original midgard_object_class::get_object_by_guid method, which takes
37
     * the MidCOM DBA system into account.
38
     *
39
     * @param string $guid The object GUID.
40
     */
41 163
    public function get_object_by_guid($guid) : midcom_core_dbaobject
42
    {
43
        try {
44 163
            $tmp = midgard_object_class::get_object_by_guid($guid);
45 2
        } catch (mgd_exception $e) {
46 2
            debug_add('Loading object by GUID ' . $guid . ' failed, reason: ' . $e->getMessage(), MIDCOM_LOG_INFO);
47
48 2
            throw new midcom_error_midgard($e, $guid);
49
        }
50 163
        $person_class = midcom::get()->config->get('person_class');
51 163
        if (   get_class($tmp) == 'midgard_person'
52 163
            && $person_class != 'midgard_person') {
53 114
            $tmp = new $person_class($guid);
54
        }
55 163
        return $this->convert_midgard_to_midcom($tmp);
56
    }
57
58
    /**
59
     * Retrieve a reference to an object, uses in-request caching
60
     *
61
     * @param string $classname Which DBA are we dealing with
62
     * @param mixed $src GUID of object (ids work but are discouraged)
63
     */
64 255
    public function &get_cached(string $classname, $src) : midcom_core_dbaobject
65
    {
66 255
        static $cache = [];
67
68 255
        if (empty($src)) {
69 12
            throw new midcom_error('invalid source identifier');
70
        }
71
72 249
        if (!isset($cache[$classname])) {
73 17
            $cache[$classname] = [];
74
        }
75
76 249
        if (isset($cache[$classname][$src])) {
77 222
            return $cache[$classname][$src];
78
        }
79 175
        $object = new $classname($src);
80 170
        $cache[$classname][$object->guid] = $object;
81 170
        $cache[$classname][$object->id] =& $cache[$classname][$object->guid];
82 170
        return $cache[$classname][$object->guid];
83
    }
84
85
    /**
86
     * This function will determine the correct type of midgard_collector that
87
     * has to be created. It will also call the _on_prepare_new_collector event handler.
88
     *
89
     * @param string $classname The name of the class for which you want to create a collector.
90
     * @param string $domain The domain property of the collector instance
91
     * @param mixed $value Value match for the collector instance
92
     * @see midcom_core_collector
93
     */
94 124
    public function new_collector($classname, $domain, $value) : midcom_core_collector
95
    {
96 124
        return new midcom_core_collector($classname, $domain, $value);
97
    }
98
99
    /**
100
     * This function will determine the correct type of midgard_query_builder that
101
     * has to be created. It will also call the _on_prepare_new_query_builder event handler.
102
     *
103
     * @param string $classname The name of the class for which you want to create a query builder.
104
     * @see midcom_core_querybuilder
105
     */
106 448
    public function new_query_builder($classname) : midcom_core_querybuilder
107
    {
108 448
        return new midcom_core_querybuilder($classname);
109
    }
110
111
    /**
112
     * Convert MgdSchema object into a MidCOM DBA object.
113
     *
114
     * If the conversion cannot be done for some reason, the function returns null and logs
115
     * an error. We also ensure that the corresponding component has been loaded.
116
     *
117
     * @param midgard\portable\api\mgdobject $object MgdSchema Object
118
     */
119 165
    public function convert_midgard_to_midcom($object) : midcom_core_dbaobject
120
    {
121 165
        if (!is_object($object)) {
122
            debug_print_r("Object dump:", $object);
123
            throw new midcom_error("Cannot cast the object to a MidCOM DBA type, it is not an object.");
124
        }
125
126 165
        if (!midcom::get()->dbclassloader->is_mgdschema_object($object)) {
127
            debug_print_r("Object dump:", $object);
128
            throw new midcom_error("Cannot cast the object to a MidCOM DBA type, it is not a regular MgdSchema object");
129
        }
130 165
        $classname = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($object);
131
132 165
        if (!class_exists($classname)) {
133
            throw new midcom_error("Got non-existing DBA class {$classname} for object of type " . get_class($object) . ", cannot convert.");
134
        }
135
136 165
        return new $classname($object);
137
    }
138
139
    /**
140
     * Helper function, it takes a MidCOM DBA object and converts it into an MgdSchema object.
141
     *
142
     * If the conversion cannot be done for some reason, the function throws an error.
143
     *
144
     * @param midcom_core_dbaobject $object MidCOM DBA Object
145
     */
146 288
    public function convert_midcom_to_midgard($object) : mgdobject
147
    {
148 288
        if (!is_object($object)) {
149
            debug_print_r("Object dump:", $object);
150
            throw new midcom_error("Cannot cast the object to an MgdSchema type, it is not an object");
151
        }
152
153 288
        if (!midcom::get()->dbclassloader->is_midcom_db_object($object)) {
154 279
            if (midcom::get()->dbclassloader->is_mgdschema_object($object)) {
155
                // Return it directly, it is already in the format we want
156 279
                return $object;
157
            }
158
            debug_print_r("Object dump:", $object);
159
            throw new midcom_error("Cannot cast the object to an MgdSchema type, it is not a MidCOM DBA object");
160
        }
161
162 13
        return $object->__object;
163
    }
164
165
    /**
166
     * This is a helper for emulating is_a() functionality with MidCOM DBA objects that are decorators.
167
     * This method can be used to check whether an object is of a MidCOM DBA or MgdSchema type
168
     *
169
     * @param mixed $object MgdSchema or MidCOM DBA object
170
     * @param string $class Class to check the instance against
171
     */
172 94
    public function is_a($object, $class, bool $allow_string = false) : bool
173
    {
174 94
        if (is_a($object, $class, $allow_string)) {
175
            // Direct match
176 10
            return true;
177
        }
178
179 88
        if (   isset($object->__object)
180 88
            && is_object($object->__object)
181 88
            && $object->__object instanceof $class) {
182
            // Decorator whose MgdSchema object matches
183 21
            return true;
184
        }
185
186 79
        if (   isset($object->__mgdschema_class_name__)
187 79
            && $object->__mgdschema_class_name__ == $class) {
188
            // Decorator without object instantiated, check class match
189
            return true;
190
        }
191
192 79
        return false;
193
    }
194
195
    /**
196
     * Returns the parent object. Tries to utilize the Memcache
197
     * data, loading the actual information only if it is not cached.
198
     *
199
     * @see get_parent_data()
200
     * @todo rethink this, IMO we should trust midgard core's get_parent and then just do the object conversion if necessary since this can return stale objects and other nastiness
201
     */
202 302
    public function get_parent(midcom_core_dbaobject $object) : ?midcom_core_dbaobject
203
    {
204 302
        [$classname, $parent_guid] = $this->_get_parent_guid_cached($object->guid, $object);
205
206 302
        if (   empty($parent_guid)
207 302
            || $parent_guid === $object->guid) {
208 296
            return null;
209
        }
210
211 178
        if (empty($classname)) {
212
            //This must be a GUID link (or wrongly configured schema)
213
            try {
214 28
                $parent = $this->get_object_by_guid($parent_guid);
215 28
                $parent_data = [$parent->__midcom_class_name__, $parent_guid];
216 2
            } catch (midcom_error $e) {
217 2
                $parent_data = ['', null];
218 2
                $parent = null;
219
            }
220
            // Cache the classname so that we can avoid get_object_by_guid calls the next time
221 28
            midcom::get()->cache->memcache->update_parent_data($object->guid, $parent_data);
0 ignored issues
show
Bug introduced by
The property memcache does not seem to exist on midcom_services_cache.
Loading history...
222
223 28
            return $parent;
224
        }
225
226
        try {
227 156
            return $this->get_cached($classname, $parent_guid);
228
        } catch (midcom_error $e) {
229
            return null;
230
        }
231
    }
232
233
    /**
234
     * Determines the parent GUID for an existing GUID according to the MidCOM content tree rules.
235
     *
236
     * It tries to look up the GUID in the memory cache, only if this fails, the regular
237
     * content getters are invoked.
238
     *
239
     * @param string $guid A GUID string.
240
     * @param string $class class name of object if known (so we can use get_parent_guid_uncached_static and avoid instantiating full object)
241
     * @return array The parent GUID and class (value might be null, if this is a top level object).
242
     */
243 77
    public function get_parent_data(string $guid, string $class = null) : array
244
    {
245 77
        return $this->_get_parent_guid_cached($guid, null, $class);
246
    }
247
248 317
    private function _get_parent_guid_cached(string $object_guid, $the_object, $class = null) : array
249
    {
250 317
        static $cached_parent_data = [];
251
252 317
        $parent_data = false;
253 317
        if (mgd_is_guid($object_guid)) {
254 316
            if (array_key_exists($object_guid, $cached_parent_data)) {
255
                // We already got this either via query or memcache
256 311
                return $cached_parent_data[$object_guid];
257
            }
258
259 284
            $parent_data = midcom::get()->cache->memcache->lookup_parent_data($object_guid);
0 ignored issues
show
Bug introduced by
The property memcache does not seem to exist on midcom_services_cache.
Loading history...
260 284
        } elseif ($the_object === null) {
261
            throw new midcom_error('Tried to resolve an invalid GUID without an object being present. This cannot be done.');
262
        }
263
264 286
        if (!is_array($parent_data)) {
265
            // No cache hit, retrieve guid and update the cache
266 286
            if ($class) {
267
                // Class defined, we can use the static method for fetching parent and avoiding full object instantiate
268 2
                $parent_data = $this->_get_parent_guid_uncached_static($object_guid, $class);
269
            } else {
270
                // class not defined, retrieve the full object by guid
271 285
                if ($the_object === null) {
272
                    try {
273
                        $the_object = $this->get_object_by_guid($object_guid);
274
                    } catch (midcom_error $e) {
275
                        return ['' => null];
276
                    }
277
                }
278
279 285
                $parent_data = $this->_get_parent_guid_uncached($the_object);
280
            }
281
282 286
            $classname = $parent_data[0];
283 286
            $parent_guid = $parent_data[1];
284 286
            if (!empty($classname)) {
285 148
                $classname = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($classname);
286
            }
287 286
            if (!mgd_is_guid($parent_guid)) {
288 206
                $parent_guid = null;
289
            }
290 286
            $parent_data = [$classname, $parent_guid];
291
292 286
            if (mgd_is_guid($object_guid)) {
293 284
                midcom::get()->cache->memcache->update_parent_data($object_guid, $parent_data);
294
            }
295
        }
296
297
        // Remember this so we don't need to get it again
298 286
        $cached_parent_data[$object_guid] = $parent_data;
299
300 286
        return $parent_data;
301
    }
302
303 285
    private function _get_parent_guid_uncached(midcom_core_dbaobject $object) : array
304
    {
305 285
        if (method_exists($object, 'get_parent_guid_uncached')) {
306 28
            return ['', $object->get_parent_guid_uncached()];
307
        }
308
309 271
        $candidates = $this->_get_parent_candidates($object->__mgdschema_class_name__);
310
311 271
        foreach ($candidates as $data) {
312 196
            $parent_guid = $this->_load_guid($data['target_class'], $data['target_property'], $object->{$data['source_property']});
313 196
            if (null !== $parent_guid) {
314 147
                return [$data['target_class'], $parent_guid];
315
            }
316
        }
317 204
        return ['', null];
318
    }
319
320
    /**
321
     * Get the GUID of the object's parent. This is done by reading up or parent
322
     * property values, which will give us the parent's ID
323
     */
324 2
    private function _get_parent_guid_uncached_static(string $object_guid, string $class_name) : array
325
    {
326 2
        if (method_exists($class_name, 'get_parent_guid_uncached_static')) {
327
            return ['', $class_name::get_parent_guid_uncached_static($object_guid)];
328
        }
329
330 2
        $class_name = midcom::get()->dbclassloader->get_mgdschema_class_name_for_midcom_class($class_name);
331 2
        $candidates = $this->_get_parent_candidates($class_name);
332
333 2
        foreach ($candidates as $data) {
334 2
            $mc = new midgard_collector($class_name, 'guid', $object_guid);
335 2
            $mc->set_key_property($data['source_property']);
336 2
            $mc->execute();
337 2
            $link_values = $mc->list_keys();
338
339 2
            if (empty($link_values)) {
340
                continue;
341
            }
342 2
            $link_value = key($link_values);
343 2
            $parent_guid = $this->_load_guid($data['target_class'], $data['target_property'], $link_value);
344 2
            if (null !== $parent_guid) {
345 2
                return [$data['target_class'], $parent_guid];
346
            }
347
        }
348
        return ['', null];
349
    }
350
351 197
    private function _load_guid(string $target_class, string $target_property, $link_value) : ?string
352
    {
353 197
        if (empty($link_value)) {
354 123
            return null;
355
        }
356 148
        if (!array_key_exists($target_class, $this->_parent_mapping)) {
357 8
            $this->_parent_mapping[$target_class] = [];
358
        }
359 148
        if (array_key_exists($link_value, $this->_parent_mapping[$target_class])) {
360 147
            return $this->_parent_mapping[$target_class][$link_value];
361
        }
362 90
        $this->_parent_mapping[$target_class][$link_value] = null;
363
364 90
        $mc2 = new midgard_collector($target_class, $target_property, $link_value);
365 90
        $mc2->set_key_property('guid');
366 90
        $mc2->execute();
367 90
        $guids = $mc2->list_keys();
368 90
        if (!empty($guids)) {
369 90
            $this->_parent_mapping[$target_class][$link_value] = key($guids);
370
        }
371 90
        return $this->_parent_mapping[$target_class][$link_value];
372
    }
373
374 272
    private function _get_parent_candidates(string $classname) : array
375
    {
376 272
        if (!isset($this->_parent_candidates[$classname])) {
377 23
            $this->_parent_candidates[$classname] = [];
378 23
            $reflector = new midgard_reflection_property($classname);
379 23
            $up_property = midgard_object_class::get_property_up($classname);
380 23
            $parent_property = midgard_object_class::get_property_parent($classname);
381
382 23
            if ($up_property) {
383 8
                $this->_parent_candidates[$classname][] = [
384 8
                    'source_property' => $up_property,
385 8
                    'target_property' => $reflector->get_link_target($up_property),
386 8
                    'target_class' => $reflector->get_link_name($up_property),
387
                ];
388
            }
389
390 23
            if (   $parent_property
391 23
                && $reflector->get_link_target($parent_property)) {
392 14
                $target_class = $reflector->get_link_name($parent_property);
393 14
                if ($target_class == 'midgard_person') {
394
                    $person_class = midcom::get()->config->get('person_class');
395
                    if ($person_class != 'midgard_person') {
396
                        $target_class = $person_class;
397
                    }
398
                }
399 14
                $this->_parent_candidates[$classname][] = [
400 14
                    'source_property' => $parent_property,
401 14
                    'target_property' => $reflector->get_link_target($parent_property),
402 14
                    'target_class' => $target_class,
403
                ];
404
            }
405
            // FIXME: Handle GUID linking
406
        }
407 272
        return $this->_parent_candidates[$classname];
408
    }
409
}
410