Passed
Push — master ( ece84a...599088 )
by Andreas
24:50
created

midcom_helper__dbfactory::_load_guid()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

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