Completed
Push — master ( 920166...6999ec )
by Andreas
17:15
created

midcom_helper__dbfactory::get_parent_data()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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 3
        } catch (mgd_exception $e) {
46 3
            debug_add('Loading object by GUID ' . $guid . ' failed, reason: ' . $e->getMessage(), MIDCOM_LOG_INFO);
47
48 3
            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 261
    public function &get_cached(string $classname, $src) : midcom_core_dbaobject
65
    {
66 261
        static $cache = [];
67
68 261
        if (empty($src)) {
69 12
            throw new midcom_error('invalid source identifier');
70
        }
71
72 255
        if (!isset($cache[$classname])) {
73 17
            $cache[$classname] = [];
74
        }
75
76 255
        if (isset($cache[$classname][$src])) {
77 236
            return $cache[$classname][$src];
78
        }
79 176
        $object = new $classname($src);
80 169
        $cache[$classname][$object->guid] = $object;
81 169
        $cache[$classname][$object->id] =& $cache[$classname][$object->guid];
82 169
        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 122
    public function new_collector($classname, $domain, $value) : midcom_core_collector
95
    {
96 122
        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 449
    public function new_query_builder($classname) : midcom_core_querybuilder
107
    {
108 449
        return new midcom_core_querybuilder($classname);
109
    }
110
111
    /**
112
     * Convert MgdSchema object into a MidCOM DBA object.
113
     */
114 165
    public function convert_midgard_to_midcom(mgdobject $object) : midcom_core_dbaobject
115
    {
116 165
        $classname = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($object);
117
118 165
        if (!class_exists($classname)) {
119
            throw new midcom_error("Got non-existing DBA class {$classname} for object of type " . get_class($object) . ", cannot convert.");
120
        }
121
122 165
        return new $classname($object);
123
    }
124
125
    /**
126
     * This is a helper for emulating is_a() functionality with MidCOM DBA objects that are decorators.
127
     * This method can be used to check whether an object is of a MidCOM DBA or MgdSchema type
128
     *
129
     * @param mixed $object MgdSchema or MidCOM DBA object
130
     * @param string $class Class to check the instance against
131
     */
132 94
    public function is_a($object, $class, bool $allow_string = false) : bool
133
    {
134 94
        if (is_a($object, $class, $allow_string)) {
135
            // Direct match
136 10
            return true;
137
        }
138
139 88
        if (   isset($object->__object)
140 88
            && is_object($object->__object)
141 88
            && $object->__object instanceof $class) {
142
            // Decorator whose MgdSchema object matches
143 21
            return true;
144
        }
145
146 79
        if (   isset($object->__mgdschema_class_name__)
147 79
            && $object->__mgdschema_class_name__ == $class) {
148
            // Decorator without object instantiated, check class match
149
            return true;
150
        }
151
152 79
        return false;
153
    }
154
155
    /**
156
     * Returns the parent object. Tries to utilize the Memcache
157
     * data, loading the actual information only if it is not cached.
158
     *
159
     * @see get_parent_data()
160
     * @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
161
     */
162 303
    public function get_parent(midcom_core_dbaobject $object) : ?midcom_core_dbaobject
163
    {
164 303
        [$classname, $parent_guid] = $this->_get_parent_guid_cached($object->guid, $object);
165
166 303
        if (   empty($parent_guid)
167 303
            || $parent_guid === $object->guid) {
168 297
            return null;
169
        }
170
171 178
        if (empty($classname)) {
172
            //This must be a GUID link (or wrongly configured schema)
173
            try {
174 29
                $parent = $this->get_object_by_guid($parent_guid);
175 29
                $parent_data = [$parent->__midcom_class_name__, $parent_guid];
176 3
            } catch (midcom_error $e) {
177 3
                $parent_data = ['', null];
178 3
                $parent = null;
179
            }
180
            // Cache the classname so that we can avoid get_object_by_guid calls the next time
181 29
            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...
182
183 29
            return $parent;
184
        }
185
186
        try {
187 156
            return $this->get_cached($classname, $parent_guid);
188
        } catch (midcom_error $e) {
189
            return null;
190
        }
191
    }
192
193
    /**
194
     * Determines the parent GUID for an existing GUID according to the MidCOM content tree rules.
195
     *
196
     * It tries to look up the GUID in the memory cache, only if this fails, the regular
197
     * content getters are invoked.
198
     *
199
     * @param string $guid A GUID string.
200
     * @param string $class class name of object if known (so we can use get_parent_guid_uncached_static and avoid instantiating full object)
201
     * @return array The parent GUID and class (value might be null, if this is a top level object).
202
     */
203 77
    public function get_parent_data(string $guid, string $class = null) : array
204
    {
205 77
        return $this->_get_parent_guid_cached($guid, null, $class);
206
    }
207
208 318
    private function _get_parent_guid_cached(string $object_guid, $the_object, $class = null) : array
209
    {
210 318
        static $cached_parent_data = [];
211
212 318
        $parent_data = false;
213 318
        if (mgd_is_guid($object_guid)) {
214 317
            if (array_key_exists($object_guid, $cached_parent_data)) {
215
                // We already got this either via query or memcache
216 310
                return $cached_parent_data[$object_guid];
217
            }
218
219 285
            $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...
220 285
        } elseif ($the_object === null) {
221
            throw new midcom_error('Tried to resolve an invalid GUID without an object being present. This cannot be done.');
222
        }
223
224 287
        if (!is_array($parent_data)) {
225
            // No cache hit, retrieve guid and update the cache
226 287
            if ($class) {
227
                // Class defined, we can use the static method for fetching parent and avoiding full object instantiate
228 2
                $parent_data = $this->_get_parent_guid_uncached_static($object_guid, $class);
229
            } else {
230
                // class not defined, retrieve the full object by guid
231 286
                if ($the_object === null) {
232
                    try {
233
                        $the_object = $this->get_object_by_guid($object_guid);
234
                    } catch (midcom_error $e) {
235
                        return ['' => null];
236
                    }
237
                }
238
239 286
                $parent_data = $this->_get_parent_guid_uncached($the_object);
240
            }
241
242 287
            $classname = $parent_data[0];
243 287
            $parent_guid = $parent_data[1];
244 287
            if (!empty($classname)) {
245 148
                $classname = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($classname);
246
            }
247 287
            if (!mgd_is_guid($parent_guid)) {
248 208
                $parent_guid = null;
249
            }
250 287
            $parent_data = [$classname, $parent_guid];
251
252 287
            if (mgd_is_guid($object_guid)) {
253 285
                midcom::get()->cache->memcache->update_parent_data($object_guid, $parent_data);
254
            }
255
        }
256
257
        // Remember this so we don't need to get it again
258 287
        $cached_parent_data[$object_guid] = $parent_data;
259
260 287
        return $parent_data;
261
    }
262
263 286
    private function _get_parent_guid_uncached(midcom_core_dbaobject $object) : array
264
    {
265 286
        if (method_exists($object, 'get_parent_guid_uncached')) {
266 29
            return ['', $object->get_parent_guid_uncached()];
267
        }
268
269 272
        $candidates = $this->_get_parent_candidates($object->__mgdschema_class_name__);
270
271 272
        foreach ($candidates as $data) {
272 198
            $parent_guid = $this->_load_guid($data['target_class'], $data['target_property'], $object->{$data['source_property']});
273 198
            if (null !== $parent_guid) {
274 147
                return [$data['target_class'], $parent_guid];
275
            }
276
        }
277 206
        return ['', null];
278
    }
279
280
    /**
281
     * Get the GUID of the object's parent. This is done by reading up or parent
282
     * property values, which will give us the parent's ID
283
     */
284 2
    private function _get_parent_guid_uncached_static(string $object_guid, string $class_name) : array
285
    {
286 2
        if (method_exists($class_name, 'get_parent_guid_uncached_static')) {
287
            return ['', $class_name::get_parent_guid_uncached_static($object_guid)];
288
        }
289
290 2
        $class_name = midcom::get()->dbclassloader->get_mgdschema_class_name_for_midcom_class($class_name);
291 2
        $candidates = $this->_get_parent_candidates($class_name);
292
293 2
        foreach ($candidates as $data) {
294 2
            $mc = new midgard_collector($class_name, 'guid', $object_guid);
295 2
            $mc->set_key_property($data['source_property']);
296 2
            $mc->execute();
297 2
            $link_values = $mc->list_keys();
298
299 2
            if (empty($link_values)) {
300
                continue;
301
            }
302 2
            $link_value = key($link_values);
303 2
            $parent_guid = $this->_load_guid($data['target_class'], $data['target_property'], $link_value);
304 2
            if (null !== $parent_guid) {
305 2
                return [$data['target_class'], $parent_guid];
306
            }
307
        }
308
        return ['', null];
309
    }
310
311 199
    private function _load_guid(string $target_class, string $target_property, $link_value) : ?string
312
    {
313 199
        if (empty($link_value)) {
314 125
            return null;
315
        }
316 148
        if (!array_key_exists($target_class, $this->_parent_mapping)) {
317 8
            $this->_parent_mapping[$target_class] = [];
318
        }
319 148
        if (array_key_exists($link_value, $this->_parent_mapping[$target_class])) {
320 147
            return $this->_parent_mapping[$target_class][$link_value];
321
        }
322 90
        $this->_parent_mapping[$target_class][$link_value] = null;
323
324 90
        $mc2 = new midgard_collector($target_class, $target_property, $link_value);
325 90
        $mc2->set_key_property('guid');
326 90
        $mc2->execute();
327 90
        $guids = $mc2->list_keys();
328 90
        if (!empty($guids)) {
329 90
            $this->_parent_mapping[$target_class][$link_value] = key($guids);
330
        }
331 90
        return $this->_parent_mapping[$target_class][$link_value];
332
    }
333
334 273
    private function _get_parent_candidates(string $classname) : array
335
    {
336 273
        if (!isset($this->_parent_candidates[$classname])) {
337 23
            $this->_parent_candidates[$classname] = [];
338 23
            $reflector = new midgard_reflection_property($classname);
339 23
            $up_property = midgard_object_class::get_property_up($classname);
340 23
            $parent_property = midgard_object_class::get_property_parent($classname);
341
342 23
            if ($up_property) {
343 8
                $this->_parent_candidates[$classname][] = [
344 8
                    'source_property' => $up_property,
345 8
                    'target_property' => $reflector->get_link_target($up_property),
346 8
                    'target_class' => $reflector->get_link_name($up_property),
347
                ];
348
            }
349
350 23
            if (   $parent_property
351 23
                && $reflector->get_link_target($parent_property)) {
352 14
                $target_class = $reflector->get_link_name($parent_property);
353 14
                if ($target_class == 'midgard_person') {
354
                    $person_class = midcom::get()->config->get('person_class');
355
                    if ($person_class != 'midgard_person') {
356
                        $target_class = $person_class;
357
                    }
358
                }
359 14
                $this->_parent_candidates[$classname][] = [
360 14
                    'source_property' => $parent_property,
361 14
                    'target_property' => $reflector->get_link_target($parent_property),
362 14
                    'target_class' => $target_class,
363
                ];
364
            }
365
            // FIXME: Handle GUID linking
366
        }
367 273
        return $this->_parent_candidates[$classname];
368
    }
369
}
370