Passed
Push — master ( 83cee0...2131d7 )
by Andreas
19:23
created

midcom_helper__dbfactory::_get_parent_candidates()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 7
nc 5
nop 1
dl 0
loc 14
ccs 8
cts 8
cp 1
crap 4
rs 10
c 2
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 149
    public function get_object_by_guid(string $guid) : midcom_core_dbaobject
43
    {
44
        try {
45 149
            $tmp = midgard_object_class::get_object_by_guid($guid);
46
        } catch (mgd_exception $e) {
47
            debug_add('Loading object by GUID ' . $guid . ' failed, reason: ' . $e->getMessage(), MIDCOM_LOG_INFO);
48
49
            throw new midcom_error_midgard($e, $guid);
50
        }
51 149
        $person_class = midcom::get()->config->get('person_class');
52 149
        if (   get_class($tmp) == 'midgard_person'
53 149
            && $person_class != 'midgard_person') {
54 114
            $tmp = new $person_class($guid);
55
        }
56 149
        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 247
    public function &get_cached(string $classname, $src) : midcom_core_dbaobject
66
    {
67 247
        static $cache = [];
68
69 247
        if (empty($src)) {
70 12
            throw new midcom_error('invalid source identifier');
71
        }
72
73 241
        if (!isset($cache[$classname])) {
74 16
            $cache[$classname] = [];
75
        }
76
77 241
        if (isset($cache[$classname][$src])) {
78 222
            return $cache[$classname][$src];
79
        }
80 155
        $object = new $classname($src);
81 146
        $cache[$classname][$object->guid] = $object;
82 146
        $cache[$classname][$object->id] =& $cache[$classname][$object->guid];
83 146
        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.
89
     *
90
     * @param mixed $value Value match for the collector instance
91
     * @see midcom_core_collector
92
     */
93 122
    public function new_collector(string $classname, ?string $domain, $value) : midcom_core_collector
94
    {
95 122
        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.
101
     */
102 452
    public function new_query_builder(string $classname) : midcom_core_querybuilder
103
    {
104 452
        return new midcom_core_querybuilder($classname);
105
    }
106
107
    /**
108
     * Convert MgdSchema object into a MidCOM DBA object.
109
     */
110 151
    public function convert_midgard_to_midcom(mgdobject $object) : midcom_core_dbaobject
111
    {
112 151
        $classname = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($object);
113 151
        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 88
    public function is_a($object, string $class, bool $allow_string = false) : bool
124
    {
125 88
        if (is_a($object, $class, $allow_string)) {
126
            // Direct match
127 6
            return true;
128
        }
129
130 85
        if (   isset($object->__object)
131 85
            && is_object($object->__object)
132 85
            && $object->__object instanceof $class) {
133
            // Decorator whose MgdSchema object matches
134 21
            return true;
135
        }
136
137 76
        if (   isset($object->__mgdschema_class_name__)
138 76
            && $object->__mgdschema_class_name__ == $class) {
139
            // Decorator without object instantiated, check class match
140
            return true;
141
        }
142
143 76
        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 306
    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 289
            return $this->get_parent_data_uncached($object);
157 306
        });
158
159 306
        if (   empty($parent_guid)
160 306
            || $parent_guid === $object->guid) {
161 300
            return null;
162
        }
163
164
        try {
165 178
            return $this->get_cached($classname, $parent_guid);
166
        } catch (midcom_error $e) {
167
            return null;
168
        }
169
    }
170
171
    /**
172
     * Determines the parent GUID for an existing GUID according to the MidCOM content tree rules.
173
     *
174
     * It tries to look up the GUID in the memory cache, only if this fails, the regular
175
     * content getters are invoked.
176
     *
177
     * @param string $guid A GUID string.
178
     * @param string $class class name of object
179
     * @return array The parent GUID and class (value might be null, if this is a top level object).
180
     */
181 79
    public function get_parent_data(string $guid, string $class) : array
182
    {
183 79
        if (!mgd_is_guid($guid)) {
184
            throw new midcom_error('Tried to resolve an invalid GUID.');
185
        }
186
        return $this->get_parent_data_cached($guid, function() use ($guid, $class) {
187 3
            return $this->get_parent_data_uncached_static($guid, $class);
188 79
        });
189
    }
190
191 322
    private function get_parent_data_cached(string $guid, callable $callback) : array
192
    {
193 322
        static $cached_parent_data = [];
194
195 322
        if (mgd_is_guid($guid)) {
196 321
            if (array_key_exists($guid, $cached_parent_data)) {
197
                // We already got this either via query or memcache
198 312
                return $cached_parent_data[$guid];
199
            }
200 289
            $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...
201
        }
202
203 291
        if (empty($parent_data)) {
204
            // No cache hit, retrieve guid and update the cache
205 291
            $parent_data = $callback();
206
207 291
            if (!empty($parent_data[0])) {
208 172
                $parent_data[0] = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($parent_data[0]);
209
            }
210
211 291
            if (mgd_is_guid($guid)) {
212 289
                midcom::get()->cache->memcache->update_parent_data($guid, $parent_data);
213
            }
214
        }
215
216
        // Remember this so we don't need to get it again
217 291
        $cached_parent_data[$guid] = $parent_data;
218
219 291
        return $parent_data;
220
    }
221
222 289
    private function get_parent_data_uncached(midcom_core_dbaobject $object) : array
223
    {
224 289
        $candidates = $this->_get_parent_candidates($object->__mgdschema_class_name__);
225 289
        foreach ($candidates as $data) {
226 217
            if ($data['target_property'] === 'guid') {
227 23
                return $this->get_data_from_guid($object->{$data['source_property']});
228
            }
229 204
            $parent_guid = $this->_load_guid($data['target_class'], $data['target_property'], $object->{$data['source_property']});
230 204
            if ($parent_guid) {
231 152
                return [$data['target_class'], $parent_guid];
232
            }
233
        }
234 213
        return ['', null];
235
    }
236
237
    /**
238
     * Get the GUID of the object's parent. This is done by reading up or parent
239
     * property values, which will give us the parent's ID
240
     */
241 3
    private function get_parent_data_uncached_static(string $object_guid, string $class_name) : array
242
    {
243 3
        $class_name = midcom::get()->dbclassloader->get_mgdschema_class_name_for_midcom_class($class_name);
244 3
        $candidates = $this->_get_parent_candidates($class_name);
245
246 3
        foreach ($candidates as $data) {
247 3
            $mc = new midgard_collector($class_name, 'guid', $object_guid);
248 3
            $mc->set_key_property($data['source_property']);
249 3
            $mc->execute();
250
251 3
            if ($link_values = $mc->list_keys()) {
252 3
                if ($data['target_property'] === 'guid') {
253 1
                    return $this->get_data_from_guid(key($link_values));
254
                }
255
256 3
                $parent_guid = $this->_load_guid($data['target_class'], $data['target_property'], key($link_values));
257 3
                if ($parent_guid) {
258 2
                    return [$data['target_class'], $parent_guid];
259
                }
260
            }
261
        }
262 1
        return ['', null];
263
    }
264
265 24
    private function get_data_from_guid(string $guid) : array
266
    {
267 24
        if (!mgd_is_guid($guid)) {
268 1
            return ['', null];
269
        }
270 24
        $class_name = connection::get_em()
271 24
            ->createQuery('SELECT r.typename from midgard:midgard_repligard r WHERE r.guid = ?1')
272 24
            ->setParameter(1, $guid)
273 24
            ->getSingleScalarResult();
274
275 24
        return [$class_name, $guid];
276
    }
277
278 206
    private function _load_guid(string $target_class, string $target_property, $link_value) : ?string
279
    {
280 206
        if (empty($link_value)) {
281 134
            return null;
282
        }
283 153
        if (!array_key_exists($target_class, $this->_parent_mapping)) {
284 8
            $this->_parent_mapping[$target_class] = [];
285
        }
286 153
        if (!array_key_exists($link_value, $this->_parent_mapping[$target_class])) {
287 88
            $mc = new midgard_collector($target_class, $target_property, $link_value);
288 88
            $mc->set_key_property('guid');
289 88
            $mc->execute();
290 88
            $this->_parent_mapping[$target_class][$link_value] = key($mc->list_keys());
291
        }
292
293 153
        return $this->_parent_mapping[$target_class][$link_value];
294
    }
295
296 291
    private function _get_parent_candidates(string $classname) : array
297
    {
298 291
        if (!isset($this->_parent_candidates[$classname])) {
299 24
            $this->_parent_candidates[$classname] = [];
300
301 24
            if ($up = midgard_object_class::get_property_up($classname)) {
302 8
                $this->add_candidate($classname, $up);
303
            }
304
305 24
            if ($parent = midgard_object_class::get_property_parent($classname)) {
306 15
                $this->add_candidate($classname, $parent);
307
            }
308
        }
309 291
        return $this->_parent_candidates[$classname];
310
    }
311
312 20
    private function add_candidate(string $classname, string $property)
313
    {
314 20
        $mrp = new midgard_reflection_property($classname);
315
316
        $data = [
317 20
            'source_property' => $property,
318 20
            'target_property' => $mrp->get_link_target($property) ?? 'guid',
319 20
            'target_class' => $mrp->get_link_name($property)
320
        ];
321
322 20
        if ($data['target_class'] == 'midgard_person') {
323
            $data['target_class'] = midcom::get()->config->get('person_class');
324
        }
325
326 20
        $this->_parent_candidates[$classname][] = $data;
327 20
    }
328
}
329