Completed
Push — master ( a11ce5...0e663f )
by Andreas
27:27
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.0625

Importance

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