Passed
Push — master ( 1b35fc...f88b0e )
by Andreas
18:21
created

midcom_helper__dbfactory::get_cached()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 5
nop 2
dl 0
loc 19
ccs 12
cts 12
cp 1
crap 4
rs 9.8666
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
use midgard\portable\storage\connection;
12
13
/**
14
 * This class contains various factory methods to retrieve objects from the database.
15
 * The only instance of this class you should ever use is available through
16
 * midcom::get()->dbfactory.
17
 *
18
 * @package midcom.helper
19
 */
20
class midcom_helper__dbfactory
21
{
22
    /**
23
     * ID => GUID cache for parents
24
     *
25
     * @var array
26
     */
27
    private $_parent_mapping = [];
28
29
    /**
30
     * Cache for possible parent configurations per mgdschema class
31
     *
32
     * @var array
33
     */
34
    private $_parent_candidates = [];
35
36
    /**
37
     * This is a replacement for the original midgard_object_class::get_object_by_guid method, which takes
38
     * the MidCOM DBA system into account.
39
     *
40
     * @param string $guid The object GUID.
41
     */
42 162
    public function get_object_by_guid(string $guid) : midcom_core_dbaobject
43
    {
44
        try {
45 162
            $tmp = midgard_object_class::get_object_by_guid($guid);
46 4
        } catch (mgd_exception $e) {
47 4
            debug_add('Loading object by GUID ' . $guid . ' failed, reason: ' . $e->getMessage(), MIDCOM_LOG_INFO);
48
49 4
            throw new midcom_error_midgard($e, $guid);
50
        }
51 162
        $person_class = midcom::get()->config->get('person_class');
52 162
        if (   get_class($tmp) == 'midgard_person'
53 162
            && $person_class != 'midgard_person') {
54 114
            $tmp = new $person_class($guid);
55
        }
56 162
        return $this->convert_midgard_to_midcom($tmp);
57
    }
58
59
    /**
60
     * Retrieve a reference to an object, uses in-request caching
61
     *
62
     * @param string $classname Which DBA are we dealing with
63
     * @param mixed $src GUID of object (ids work but are discouraged)
64
     */
65 261
    public function &get_cached(string $classname, $src) : midcom_core_dbaobject
66
    {
67 261
        static $cache = [];
68
69 261
        if (empty($src)) {
70 12
            throw new midcom_error('invalid source identifier');
71
        }
72
73 255
        if (!isset($cache[$classname])) {
74 16
            $cache[$classname] = [];
75
        }
76
77 255
        if (isset($cache[$classname][$src])) {
78 235
            return $cache[$classname][$src];
79
        }
80 176
        $object = new $classname($src);
81 168
        $cache[$classname][$object->guid] = $object;
82 168
        $cache[$classname][$object->id] =& $cache[$classname][$object->guid];
83 168
        return $cache[$classname][$object->guid];
84
    }
85
86
    /**
87
     * This function will determine the correct type of midgard_collector that
88
     * has to be created. It will also call the _on_prepare_new_collector event handler.
89
     *
90
     * @param mixed $value Value match for the collector instance
91
     * @see midcom_core_collector
92
     */
93 123
    public function new_collector(string $classname, ?string $domain, $value) : midcom_core_collector
94
    {
95 123
        return new midcom_core_collector($classname, $domain, $value);
96
    }
97
98
    /**
99
     * This function will determine the correct type of midgard_query_builder that
100
     * has to be created. It will also call the _on_prepare_new_query_builder event handler.
101
     */
102 449
    public function new_query_builder(string $classname) : midcom_core_querybuilder
103
    {
104 449
        return new midcom_core_querybuilder($classname);
105
    }
106
107
    /**
108
     * Convert MgdSchema object into a MidCOM DBA object.
109
     */
110 164
    public function convert_midgard_to_midcom(mgdobject $object) : midcom_core_dbaobject
111
    {
112 164
        $classname = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($object);
113 164
        return new $classname($object);
114
    }
115
116
    /**
117
     * This is a helper for emulating is_a() functionality with MidCOM DBA objects that are decorators.
118
     * This method can be used to check whether an object is of a MidCOM DBA or MgdSchema type
119
     *
120
     * @param mixed $object MgdSchema or MidCOM DBA object
121
     * @param string $class Class to check the instance against
122
     */
123 89
    public function is_a($object, string $class, bool $allow_string = false) : bool
124
    {
125 89
        if (is_a($object, $class, $allow_string)) {
126
            // Direct match
127 6
            return true;
128
        }
129
130 86
        if (   isset($object->__object)
131 86
            && is_object($object->__object)
132 86
            && $object->__object instanceof $class) {
133
            // Decorator whose MgdSchema object matches
134 20
            return true;
135
        }
136
137 77
        if (   isset($object->__mgdschema_class_name__)
138 77
            && $object->__mgdschema_class_name__ == $class) {
139
            // Decorator without object instantiated, check class match
140
            return true;
141
        }
142
143 77
        return false;
144
    }
145
146
    /**
147
     * Returns the parent object. Tries to utilize the Memcache
148
     * data, loading the actual information only if it is not cached.
149
     *
150
     * @see get_parent_data()
151
     * @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
152
     */
153 303
    public function get_parent(midcom_core_dbaobject $object) : ?midcom_core_dbaobject
154
    {
155
        [$classname, $parent_guid] = $this->get_parent_data_cached($object->guid, function() use ($object) {
156 286
            return $this->get_parent_data_uncached($object);
157 303
        });
158
159 303
        if (   empty($parent_guid)
160 303
            || $parent_guid === $object->guid) {
161 297
            return null;
162
        }
163
164 177
        if (empty($classname)) {
165
            //This must be a GUID link (or wrongly configured schema)
166
            try {
167 29
                $parent = $this->get_object_by_guid($parent_guid);
168 29
                $parent_data = [$parent->__midcom_class_name__, $parent_guid];
169 4
            } catch (midcom_error $e) {
170 4
                $parent_data = ['', null];
171 4
                $parent = null;
172
            }
173
            // Cache the classname so that we can avoid get_object_by_guid calls the next time
174 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...
175
176 29
            return $parent;
177
        }
178
179
        try {
180 156
            return $this->get_cached($classname, $parent_guid);
181
        } catch (midcom_error $e) {
182
            return null;
183
        }
184
    }
185
186
    /**
187
     * Determines the parent GUID for an existing GUID according to the MidCOM content tree rules.
188
     *
189
     * It tries to look up the GUID in the memory cache, only if this fails, the regular
190
     * content getters are invoked.
191
     *
192
     * @param string $guid A GUID string.
193
     * @param string $class class name of object (so we can use get_parent_guid_uncached_static and avoid instantiating full object)
194
     * @return array The parent GUID and class (value might be null, if this is a top level object).
195
     */
196 78
    public function get_parent_data(string $guid, string $class) : array
197
    {
198 78
        if (!mgd_is_guid($guid)) {
199
            throw new midcom_error('Tried to resolve an invalid GUID.');
200
        }
201
        return $this->get_parent_data_cached($guid, function() use ($guid, $class) {
202 3
            return $this->get_parent_data_uncached_static($guid, $class);
203 78
        });
204
    }
205
206 319
    private function get_parent_data_cached(string $guid, callable $callback) : array
207
    {
208 319
        static $cached_parent_data = [];
209
210 319
        if (mgd_is_guid($guid)) {
211 318
            if (array_key_exists($guid, $cached_parent_data)) {
212
                // We already got this either via query or memcache
213 309
                return $cached_parent_data[$guid];
214
            }
215 286
            $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...
216
        }
217
218 288
        if (empty($parent_data)) {
219
            // No cache hit, retrieve guid and update the cache
220 288
            [$classname, $parent_guid] = $callback();
221
222 288
            if (!empty($classname)) {
223 148
                $classname = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($classname);
224
            }
225 288
            if (!mgd_is_guid($parent_guid)) {
226 210
                $parent_guid = null;
227
            }
228 288
            $parent_data = [$classname, $parent_guid];
229
230 288
            if (mgd_is_guid($guid)) {
231 286
                midcom::get()->cache->memcache->update_parent_data($guid, $parent_data);
232
            }
233
        }
234
235
        // Remember this so we don't need to get it again
236 288
        $cached_parent_data[$guid] = $parent_data;
237
238 288
        return $parent_data;
239
    }
240
241 286
    private function get_parent_data_uncached(midcom_core_dbaobject $object) : array
242
    {
243 286
        if (method_exists($object, 'get_parent_guid_uncached')) {
244 29
            return ['', $object->get_parent_guid_uncached()];
245
        }
246
247 273
        $candidates = $this->_get_parent_candidates($object->__mgdschema_class_name__);
248 273
        foreach ($candidates as $data) {
249 199
            $parent_guid = $this->_load_guid($data['target_class'], $data['target_property'], $object->{$data['source_property']});
250 199
            if (null !== $parent_guid) {
251 147
                return [$data['target_class'], $parent_guid];
252
            }
253
        }
254 207
        return ['', null];
255
    }
256
257
    /**
258
     * Get the GUID of the object's parent. This is done by reading up or parent
259
     * property values, which will give us the parent's ID
260
     */
261 3
    private function get_parent_data_uncached_static(string $object_guid, string $class_name) : array
262
    {
263 3
        if (!$class_name) {
264 1
            $class_name = connection::get_em()
265 1
                ->createQuery('SELECT r.typename from midgard:midgard_repligard r WHERE r.guid = ?1')
266 1
                ->setParameter(1, $object_guid)
267 1
                ->getSingleScalarResult();
268 1
            $class_name = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($class_name);
269
        }
270
271 3
        if (method_exists($class_name, 'get_parent_guid_uncached_static')) {
272 1
            return ['', $class_name::get_parent_guid_uncached_static($object_guid)];
273
        }
274
275 3
        $class_name = midcom::get()->dbclassloader->get_mgdschema_class_name_for_midcom_class($class_name);
276 3
        $candidates = $this->_get_parent_candidates($class_name);
277
278 3
        foreach ($candidates as $data) {
279 3
            $mc = new midgard_collector($class_name, 'guid', $object_guid);
280 3
            $mc->set_key_property($data['source_property']);
281 3
            $mc->execute();
282 3
            $link_values = $mc->list_keys();
283
284 3
            if (!empty($link_values)) {
285 3
                $parent_guid = $this->_load_guid($data['target_class'], $data['target_property'], key($link_values));
286 3
                if (null !== $parent_guid) {
287 2
                    return [$data['target_class'], $parent_guid];
288
                }
289
            }
290
        }
291 1
        return ['', null];
292
    }
293
294 201
    private function _load_guid(string $target_class, string $target_property, $link_value) : ?string
295
    {
296 201
        if (empty($link_value)) {
297 127
            return null;
298
        }
299 148
        if (!array_key_exists($target_class, $this->_parent_mapping)) {
300 8
            $this->_parent_mapping[$target_class] = [];
301
        }
302 148
        if (!array_key_exists($link_value, $this->_parent_mapping[$target_class])) {
303 90
            $mc2 = new midgard_collector($target_class, $target_property, $link_value);
304 90
            $mc2->set_key_property('guid');
305 90
            $mc2->execute();
306 90
            $this->_parent_mapping[$target_class][$link_value] = key($mc2->list_keys());
307
        }
308
309 148
        return $this->_parent_mapping[$target_class][$link_value];
310
    }
311
312 275
    private function _get_parent_candidates(string $classname) : array
313
    {
314 275
        if (!isset($this->_parent_candidates[$classname])) {
315 21
            $this->_parent_candidates[$classname] = [];
316 21
            $reflector = new midgard_reflection_property($classname);
317 21
            $up_property = midgard_object_class::get_property_up($classname);
318 21
            $parent_property = midgard_object_class::get_property_parent($classname);
319
320 21
            if ($up_property) {
321 7
                $this->_parent_candidates[$classname][] = [
322 7
                    'source_property' => $up_property,
323 7
                    'target_property' => $reflector->get_link_target($up_property),
324 7
                    'target_class' => $reflector->get_link_name($up_property),
325
                ];
326
            }
327
328 21
            if (   $parent_property
329 21
                && $reflector->get_link_target($parent_property)) {
330 13
                $target_class = $reflector->get_link_name($parent_property);
331 13
                if ($target_class == 'midgard_person') {
332
                    $person_class = midcom::get()->config->get('person_class');
333
                    if ($person_class != 'midgard_person') {
334
                        $target_class = $person_class;
335
                    }
336
                }
337 13
                $this->_parent_candidates[$classname][] = [
338 13
                    'source_property' => $parent_property,
339 13
                    'target_property' => $reflector->get_link_target($parent_property),
340 13
                    'target_class' => $target_class,
341
                ];
342
            }
343
            // FIXME: Handle GUID linking
344
        }
345 275
        return $this->_parent_candidates[$classname];
346
    }
347
}
348