Passed
Push — master ( 59f703...4ebed3 )
by Andreas
18:58
created

midcom_helper__dbfactory::_load_guid()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 14
nc 7
nop 3
dl 0
loc 21
ccs 15
cts 15
cp 1
crap 5
rs 9.4888
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 11
        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
        $parent_data = $this->_get_parent_guid_cached($object->guid, $object);
205
206 302
        $parent_guid = current($parent_data);
207 302
        if (   empty($parent_guid)
208 302
            || $parent_guid === $object->guid) {
209 296
            return null;
210
        }
211 178
        $classname = key($parent_data);
212
213 178
        if (empty($classname)) {
214
            //This must be a GUID link (or wrongly configured schema)
215
            try {
216 28
                $parent = $this->get_object_by_guid($parent_guid);
217
                $parent_data = [
218 28
                    $parent->__midcom_class_name__ => $parent_guid
219
                ];
220 2
            } catch (midcom_error $e) {
221
                $parent_data = [
222 2
                    '' => null
223
                ];
224 2
                $parent = null;
225
            }
226
            // Cache the classname so that we can avoid get_object_by_guid calls the next time
227 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...
228
229 28
            return $parent;
230
        }
231
232
        try {
233 156
            return $this->get_cached($classname, $parent_guid);
234
        } catch (midcom_error $e) {
235
            return null;
236
        }
237
    }
238
239
    /**
240
     * Determines the parent GUID for an existing GUID according to the MidCOM content tree rules.
241
     *
242
     * It tries to look up the GUID in the memory cache, only if this fails, the regular
243
     * content getters are invoked.
244
     *
245
     * @param string $guid A GUID string.
246
     * @param string $class class name of object if known (so we can use get_parent_guid_uncached_static and avoid instantiating full object)
247
     * @return array The parent GUID and class (value might be null, if this is a top level object).
248
     */
249 77
    public function get_parent_data(string $guid, string $class = null) : array
250
    {
251 77
        return $this->_get_parent_guid_cached($guid, null, $class);
252
    }
253
254 317
    private function _get_parent_guid_cached(string $object_guid, $the_object, $class = null) : array
255
    {
256 317
        static $cached_parent_data = [];
257
258 317
        $parent_data = false;
259 317
        if (mgd_is_guid($object_guid)) {
260 316
            if (array_key_exists($object_guid, $cached_parent_data)) {
261
                // We already got this either via query or memcache
262 311
                return $cached_parent_data[$object_guid];
263
            }
264
265 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...
266 284
        } elseif ($the_object === null) {
267
            throw new midcom_error('Tried to resolve an invalid GUID without an object being present. This cannot be done.');
268
        }
269
270 286
        if (!is_array($parent_data)) {
271
            // No cache hit, retrieve guid and update the cache
272 286
            if ($class) {
273
                // Class defined, we can use the static method for fetching parent and avoiding full object instantiate
274 2
                $parent_data = $this->_get_parent_guid_uncached_static($object_guid, $class);
275
            } else {
276
                // class not defined, retrieve the full object by guid
277 285
                if ($the_object === null) {
278
                    try {
279
                        $the_object = $this->get_object_by_guid($object_guid);
280
                    } catch (midcom_error $e) {
281
                        return ['' => null];
282
                    }
283
                }
284
285 285
                $parent_data = $this->_get_parent_guid_uncached($the_object);
286
            }
287
288 286
            $parent_guid = current($parent_data);
289 286
            $classname = key($parent_data);
290 286
            $parent_data = [];
291 286
            if (!empty($classname)) {
292 148
                $classname = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($classname);
293
            }
294 286
            if (!mgd_is_guid($parent_guid)) {
295 206
                $parent_guid = null;
296
            }
297 286
            $parent_data[$classname] = $parent_guid;
298
299 286
            if (mgd_is_guid($object_guid)) {
300 284
                midcom::get()->cache->memcache->update_parent_data($object_guid, $parent_data);
301
            }
302
        }
303
304
        // Remember this so we don't need to get it again
305 286
        $cached_parent_data[$object_guid] = $parent_data;
306
307 286
        return $parent_data;
308
    }
309
310 285
    private function _get_parent_guid_uncached(midcom_core_dbaobject $object) : array
311
    {
312 285
        if (method_exists($object, 'get_parent_guid_uncached')) {
313 28
            return ['' => $object->get_parent_guid_uncached()];
314
        }
315
316 271
        $candidates = $this->_get_parent_candidates($object->__mgdschema_class_name__);
317
318 271
        foreach ($candidates as $data) {
319 196
            $parent_guid = $this->_load_guid($data['target_class'], $data['target_property'], $object->{$data['source_property']});
320 196
            if (null !== $parent_guid) {
321 147
                return [$data['target_class'] => $parent_guid];
322
            }
323
        }
324 204
        return ['' => null];
325
    }
326
327
    /**
328
     * Get the GUID of the object's parent. This is done by reading up or parent
329
     * property values, which will give us the parent's ID
330
     */
331 2
    private function _get_parent_guid_uncached_static(string $object_guid, string $class_name) : array
332
    {
333 2
        if (method_exists($class_name, 'get_parent_guid_uncached_static')) {
334
            return ['', $class_name::get_parent_guid_uncached_static($object_guid)];
335
        }
336
337 2
        $class_name = midcom::get()->dbclassloader->get_mgdschema_class_name_for_midcom_class($class_name);
338 2
        $candidates = $this->_get_parent_candidates($class_name);
339
340 2
        foreach ($candidates as $data) {
341 2
            $mc = new midgard_collector($class_name, 'guid', $object_guid);
342 2
            $mc->set_key_property($data['source_property']);
343 2
            $mc->execute();
344 2
            $link_values = $mc->list_keys();
345
346 2
            if (empty($link_values)) {
347
                continue;
348
            }
349 2
            $link_value = key($link_values);
350 2
            $parent_guid = $this->_load_guid($data['target_class'], $data['target_property'], $link_value);
351 2
            if (null !== $parent_guid) {
352 2
                return [$data['target_class'] => $parent_guid];
353
            }
354
        }
355
        return ['' => null];
356
    }
357
358 197
    private function _load_guid(string $target_class, string $target_property, $link_value) : ?string
359
    {
360 197
        if (empty($link_value)) {
361 123
            return null;
362
        }
363 148
        if (!array_key_exists($target_class, $this->_parent_mapping)) {
364 9
            $this->_parent_mapping[$target_class] = [];
365
        }
366 148
        if (array_key_exists($link_value, $this->_parent_mapping[$target_class])) {
367 147
            return $this->_parent_mapping[$target_class][$link_value];
368
        }
369 90
        $this->_parent_mapping[$target_class][$link_value] = null;
370
371 90
        $mc2 = new midgard_collector($target_class, $target_property, $link_value);
372 90
        $mc2->set_key_property('guid');
373 90
        $mc2->execute();
374 90
        $guids = $mc2->list_keys();
375 90
        if (!empty($guids)) {
376 90
            $this->_parent_mapping[$target_class][$link_value] = key($guids);
377
        }
378 90
        return $this->_parent_mapping[$target_class][$link_value];
379
    }
380
381 272
    private function _get_parent_candidates(string $classname) : array
382
    {
383 272
        if (!isset($this->_parent_candidates[$classname])) {
384 23
            $this->_parent_candidates[$classname] = [];
385 23
            $reflector = new midgard_reflection_property($classname);
386 23
            $up_property = midgard_object_class::get_property_up($classname);
387 23
            $parent_property = midgard_object_class::get_property_parent($classname);
388
389 23
            if ($up_property) {
390 8
                $this->_parent_candidates[$classname][] = [
391 8
                    'source_property' => $up_property,
392 8
                    'target_property' => $reflector->get_link_target($up_property),
393 8
                    'target_class' => $reflector->get_link_name($up_property),
394
                ];
395
            }
396
397 23
            if (   $parent_property
398 23
                && $reflector->get_link_target($parent_property)) {
399 14
                $target_class = $reflector->get_link_name($parent_property);
400 14
                if ($target_class == 'midgard_person') {
401
                    $person_class = midcom::get()->config->get('person_class');
402
                    if ($person_class != 'midgard_person') {
403
                        $target_class = $person_class;
404
                    }
405
                }
406 14
                $this->_parent_candidates[$classname][] = [
407 14
                    'source_property' => $parent_property,
408 14
                    'target_property' => $reflector->get_link_target($parent_property),
409 14
                    'target_class' => $target_class,
410
                ];
411
            }
412
            // FIXME: Handle GUID linking
413
        }
414 272
        return $this->_parent_candidates[$classname];
415
    }
416
}
417