Completed
Push — master ( 6999ec...03b597 )
by Andreas
09:54
created

midcom_helper__dbfactory::get_parent_data()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 2
b 0
f 0
nc 2
nop 2
dl 0
loc 7
ccs 3
cts 3
cp 1
crap 2
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, function() use ($object) {
165
            return $this->_get_parent_guid_uncached($object);
166 303
        });
167 303
168 297
        if (   empty($parent_guid)
169
            || $parent_guid === $object->guid) {
170
            return null;
171 178
        }
172
173
        if (empty($classname)) {
174 29
            //This must be a GUID link (or wrongly configured schema)
175 29
            try {
176 3
                $parent = $this->get_object_by_guid($parent_guid);
177 3
                $parent_data = [$parent->__midcom_class_name__, $parent_guid];
178 3
            } catch (midcom_error $e) {
179
                $parent_data = ['', null];
180
                $parent = null;
181 29
            }
182
            // Cache the classname so that we can avoid get_object_by_guid calls the next time
183 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...
184
185
            return $parent;
186
        }
187 156
188
        try {
189
            return $this->get_cached($classname, $parent_guid);
190
        } catch (midcom_error $e) {
191
            return null;
192
        }
193
    }
194
195
    /**
196
     * Determines the parent GUID for an existing GUID according to the MidCOM content tree rules.
197
     *
198
     * It tries to look up the GUID in the memory cache, only if this fails, the regular
199
     * content getters are invoked.
200
     *
201
     * @param string $guid A GUID string.
202
     * @param string $class class name of object (so we can use get_parent_guid_uncached_static and avoid instantiating full object)
203 77
     * @return array The parent GUID and class (value might be null, if this is a top level object).
204
     */
205 77
    public function get_parent_data(string $guid, string $class) : array
206
    {
207
        if (!mgd_is_guid($guid)) {
208 318
            throw new midcom_error('Tried to resolve an invalid GUID.');
209
        }
210 318
        return $this->_get_parent_guid_cached($guid, function() use ($guid, $class) {
211
            return $this->_get_parent_guid_uncached_static($guid, $class);
212 318
        });
213 318
    }
214 317
215
    private function _get_parent_guid_cached(string $guid, callable $callback) : array
216 310
    {
217
        static $cached_parent_data = [];
218
219 285
        if (mgd_is_guid($guid)) {
220 285
            if (array_key_exists($guid, $cached_parent_data)) {
221
                // We already got this either via query or memcache
222
                return $cached_parent_data[$guid];
223
            }
224 287
            $parent_data = midcom::get()->cache->memcache->lookup_parent_data($guid);
0 ignored issues
show
Bug introduced by
The property memcache does not seem to exist on midcom_services_cache.
Loading history...
225
        }
226 287
227
        if (empty($parent_data)) {
228 2
            // No cache hit, retrieve guid and update the cache
229
            [$classname, $parent_guid] = $callback();
230
231 286
            if (!empty($classname)) {
232
                $classname = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($classname);
233
            }
234
            if (!mgd_is_guid($parent_guid)) {
235
                $parent_guid = null;
236
            }
237
            $parent_data = [$classname, $parent_guid];
238
239 286
            if (mgd_is_guid($guid)) {
240
                midcom::get()->cache->memcache->update_parent_data($guid, $parent_data);
241
            }
242 287
        }
243 287
244 287
        // Remember this so we don't need to get it again
245 148
        $cached_parent_data[$guid] = $parent_data;
246
247 287
        return $parent_data;
248 208
    }
249
250 287
    private function _get_parent_guid_uncached(midcom_core_dbaobject $object) : array
251
    {
252 287
        if (method_exists($object, 'get_parent_guid_uncached')) {
253 285
            return ['', $object->get_parent_guid_uncached()];
254
        }
255
256
        $candidates = $this->_get_parent_candidates($object->__mgdschema_class_name__);
257
        foreach ($candidates as $data) {
258 287
            $parent_guid = $this->_load_guid($data['target_class'], $data['target_property'], $object->{$data['source_property']});
259
            if (null !== $parent_guid) {
260 287
                return [$data['target_class'], $parent_guid];
261
            }
262
        }
263 286
        return ['', null];
264
    }
265 286
266 29
    /**
267
     * Get the GUID of the object's parent. This is done by reading up or parent
268
     * property values, which will give us the parent's ID
269 272
     */
270
    private function _get_parent_guid_uncached_static(string $object_guid, string $class_name) : array
271 272
    {
272 198
        if (method_exists($class_name, 'get_parent_guid_uncached_static')) {
273 198
            return ['', $class_name::get_parent_guid_uncached_static($object_guid)];
274 147
        }
275
276
        $class_name = midcom::get()->dbclassloader->get_mgdschema_class_name_for_midcom_class($class_name);
277 206
        $candidates = $this->_get_parent_candidates($class_name);
278
279
        foreach ($candidates as $data) {
280
            $mc = new midgard_collector($class_name, 'guid', $object_guid);
281
            $mc->set_key_property($data['source_property']);
282
            $mc->execute();
283
            $link_values = $mc->list_keys();
284 2
285
            if (empty($link_values)) {
286 2
                continue;
287
            }
288
            $link_value = key($link_values);
289
            $parent_guid = $this->_load_guid($data['target_class'], $data['target_property'], $link_value);
290 2
            if (null !== $parent_guid) {
291 2
                return [$data['target_class'], $parent_guid];
292
            }
293 2
        }
294 2
        return ['', null];
295 2
    }
296 2
297 2
    private function _load_guid(string $target_class, string $target_property, $link_value) : ?string
298
    {
299 2
        if (empty($link_value)) {
300
            return null;
301
        }
302 2
        if (!array_key_exists($target_class, $this->_parent_mapping)) {
303 2
            $this->_parent_mapping[$target_class] = [];
304 2
        }
305 2
        if (array_key_exists($link_value, $this->_parent_mapping[$target_class])) {
306
            return $this->_parent_mapping[$target_class][$link_value];
307
        }
308
        $this->_parent_mapping[$target_class][$link_value] = null;
309
310
        $mc2 = new midgard_collector($target_class, $target_property, $link_value);
311 199
        $mc2->set_key_property('guid');
312
        $mc2->execute();
313 199
        $guids = $mc2->list_keys();
314 125
        if (!empty($guids)) {
315
            $this->_parent_mapping[$target_class][$link_value] = key($guids);
316 148
        }
317 8
        return $this->_parent_mapping[$target_class][$link_value];
318
    }
319 148
320 147
    private function _get_parent_candidates(string $classname) : array
321
    {
322 90
        if (!isset($this->_parent_candidates[$classname])) {
323
            $this->_parent_candidates[$classname] = [];
324 90
            $reflector = new midgard_reflection_property($classname);
325 90
            $up_property = midgard_object_class::get_property_up($classname);
326 90
            $parent_property = midgard_object_class::get_property_parent($classname);
327 90
328 90
            if ($up_property) {
329 90
                $this->_parent_candidates[$classname][] = [
330
                    'source_property' => $up_property,
331 90
                    'target_property' => $reflector->get_link_target($up_property),
332
                    'target_class' => $reflector->get_link_name($up_property),
333
                ];
334 273
            }
335
336 273
            if (   $parent_property
337 23
                && $reflector->get_link_target($parent_property)) {
338 23
                $target_class = $reflector->get_link_name($parent_property);
339 23
                if ($target_class == 'midgard_person') {
340 23
                    $person_class = midcom::get()->config->get('person_class');
341
                    if ($person_class != 'midgard_person') {
342 23
                        $target_class = $person_class;
343 8
                    }
344 8
                }
345 8
                $this->_parent_candidates[$classname][] = [
346 8
                    'source_property' => $parent_property,
347
                    'target_property' => $reflector->get_link_target($parent_property),
348
                    'target_class' => $target_class,
349
                ];
350 23
            }
351 23
            // FIXME: Handle GUID linking
352 14
        }
353 14
        return $this->_parent_candidates[$classname];
354
    }
355
}
356