AbstractRelation   A
last analyzed

Complexity

Total Complexity 34

Size/Duplication

Total Lines 316
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 15
Bugs 4 Features 1
Metric Value
wmc 34
c 15
b 4
f 1
lcom 1
cbo 13
dl 0
loc 316
rs 9.2

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A name() 0 4 1
A model() 0 4 1
A query() 0 4 1
A where() 0 6 1
A order() 0 6 1
A limit() 0 7 1
A buildLocalKey() 0 6 1
A buildForeignKey() 0 4 1
A buildKey() 0 9 2
C fetch() 0 30 8
A assertInstance() 0 9 2
B cleanup() 0 21 5
A isCleanupNecessary() 0 14 3
A identifyEntity() 0 11 2
A assertArrayAccess() 0 6 3
1
<?php
2
3
/*
4
 * This file is part of the Storage package
5
 *
6
 * (c) Michal Wachowski <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Moss\Storage\Query\Relation;
13
14
use Doctrine\DBAL\Query\QueryBuilder;
15
use Moss\Storage\GetTypeTrait;
16
use Moss\Storage\Model\Definition\RelationInterface as DefinitionInterface;
17
use Moss\Storage\Model\ModelBag;
18
use Moss\Storage\Model\ModelInterface;
19
use Moss\Storage\Query\AbstractRelational;
20
use Moss\Storage\Query\Accessor\Accessor;
21
use Moss\Storage\Query\Accessor\AccessorInterface;
22
use Moss\Storage\Query\StorageInterface;
23
24
/**
25
 * Abstract class for basic relation methods
26
 *
27
 * @author  Michal Wachowski <[email protected]>
28
 * @package Moss\Storage
29
 */
30
abstract class AbstractRelation extends AbstractRelational
31
{
32
    use GetTypeTrait;
33
34
    /**
35
     * @var StorageInterface
36
     */
37
    protected $storage;
38
39
    /**
40
     * @var ModelBag
41
     */
42
    protected $models;
43
44
    /**
45
     * @var AccessorInterface
46
     */
47
    protected $accessor;
48
49
    /**
50
     * @var DefinitionInterface
51
     */
52
    protected $definition;
53
54
    protected $conditions = [];
55
    protected $orders = [];
56
    protected $limit;
57
    protected $offset;
58
59
    /**
60
     * Constructor
61
     *
62
     * @param StorageInterface         $storage
63
     * @param DefinitionInterface      $relation
64
     * @param ModelBag                 $models
65
     * @param RelationFactoryInterface $factory
66
     */
67
    public function __construct(StorageInterface $storage, DefinitionInterface $relation, ModelBag $models, RelationFactoryInterface $factory)
68
    {
69
        $this->storage = $storage;
70
        $this->definition = $relation;
71
        $this->models = $models;
72
        $this->factory = $factory;
73
        $this->accessor = new Accessor();
74
    }
75
76
    /**
77
     * Returns relation name
78
     *
79
     * @return string
80
     */
81
    public function name()
82
    {
83
        return $this->definition->name();
84
    }
85
86
    /**
87
     * Returns model
88
     *
89
     * @return ModelInterface
90
     */
91
    public function model()
92
    {
93
        return $this->models->get($this->definition->entity());
94
    }
95
96
    /**
97
     * Returns relation query instance
98
     *
99
     * @return QueryBuilder
100
     */
101
    public function query()
102
    {
103
        return $this->storage;
104
    }
105
106
    /**
107
     * Adds where condition to relation
108
     *
109
     * @param mixed  $field
110
     * @param mixed  $value
111
     * @param string $comparison
112
     * @param string $logical
113
     *
114
     * @return $this
115
     */
116
    public function where($field, $value, $comparison = '==', $logical = 'and')
117
    {
118
        $this->conditions[] = [$field, $value, $comparison, $logical];
119
120
        return $this;
121
    }
122
123
    /**
124
     * Adds sorting to relation
125
     *
126
     * @param string       $field
127
     * @param string|array $order
128
     *
129
     * @return $this
130
     */
131
    public function order($field, $order = 'desc')
132
    {
133
        $this->conditions[] = [$field, $order];
134
135
        return $this;
136
    }
137
138
    /**
139
     * Sets limits to relation
140
     *
141
     * @param int      $limit
142
     * @param null|int $offset
143
     *
144
     * @return $this
145
     */
146
    public function limit($limit, $offset = null)
147
    {
148
        $this->limit = $limit;
149
        $this->offset = $offset;
150
151
        return $this;
152
    }
153
154
    /**
155
     * Builds local key from field property pairs
156
     *
157
     * @param mixed $entity
158
     * @param array $pairs
159
     *
160
     * @return string
161
     */
162
    protected function buildLocalKey($entity, array $pairs)
163
    {
164
        $keys = array_keys($pairs);
165
166
        return $this->buildKey($entity, array_combine($keys, $keys));
167
    }
168
169
    /**
170
     * Builds foreign key from field property pairs
171
     *
172
     * @param mixed $entity
173
     * @param array $pairs
174
     *
175
     * @return string
176
     */
177
    protected function buildForeignKey($entity, array $pairs)
178
    {
179
        return $this->buildKey($entity, $pairs);
180
    }
181
182
    /**
183
     * Builds key from key-value pairs
184
     *
185
     * @param mixed $entity
186
     * @param array $pairs
187
     *
188
     * @return string
189
     */
190
    protected function buildKey($entity, array $pairs)
191
    {
192
        $key = [];
193
        foreach ($pairs as $local => $refer) {
194
            $key[] = $local . ':' . $this->accessor->getPropertyValue($entity, $refer);
195
        }
196
197
        return implode('-', $key);
198
    }
199
200
    /**
201
     * Fetches collection of entities matching set conditions
202
     * Optionally sorts it and limits it
203
     *
204
     * @param string $entityName
205
     * @param array  $conditions
206
     * @param bool   $result
207
     *
208
     * @return array
209
     */
210
    protected function fetch($entityName, array $conditions, $result = false)
211
    {
212
        $query = $this->storage->read($entityName);
213
214
        foreach ($conditions as $field => $values) {
215
            $query->where($field, $values);
216
        }
217
218
        if (!$result) {
219
            return $query->execute();
220
        }
221
222
        foreach ($this->relations as $relation) {
223
            $query->with($relation->name());
224
        }
225
226
        foreach ($this->conditions as $condition) {
227
            $query->where($condition[0], $condition[1], $condition[2], $condition[3]);
228
        }
229
230
        foreach ($this->orders as $order) {
231
            $query->order($order[0], $order[1]);
232
        }
233
234
        if ($this->limit !== null || $this->offset !== null) {
235
            $query->limit($this->limit, $this->offset);
236
        }
237
238
        return $query->execute();
239
    }
240
241
    /**
242
     * Throws exception when entity is not required instance
243
     *
244
     * @param mixed $entity
245
     *
246
     * @return bool
247
     * @throws RelationException
248
     */
249
    protected function assertInstance($entity)
250
    {
251
        $entityClass = $this->definition->entity();
252
        if (!$entity instanceof $entityClass) {
253
            throw new RelationException(sprintf('Relation entity must be instance of %s, got %s', $entityClass, $this->getType($entity)));
254
        }
255
256
        return true;
257
    }
258
259
    /**
260
     * Removes obsolete entities that match conditions but don't exist in collection
261
     *
262
     * @param string $entityName
263
     * @param array  $collection
264
     * @param array  $conditions
265
     */
266
    protected function cleanup($entityName, array $collection, array $conditions)
267
    {
268
        if (!$existing = $this->isCleanupNecessary($entityName, $conditions)) {
269
            return;
270
        }
271
272
        $identifiers = [];
273
        foreach ($collection as $instance) {
274
            $identifiers[] = $this->identifyEntity($instance, $entityName);
275
        }
276
277
        foreach ($existing as $instance) {
278
            if (in_array($this->identifyEntity($instance, $entityName), $identifiers)) {
279
                continue;
280
            }
281
282
            $this->storage->delete($instance, $entityName)->execute();
283
        }
284
285
        return;
286
    }
287
288
    /**
289
     * Returns array with entities that should be deleted or false otherwise
290
     *
291
     * @param string $entityName
292
     * @param array  $conditions
293
     *
294
     * @return array|bool
295
     */
296
    private function isCleanupNecessary($entityName, $conditions)
297
    {
298
        if (empty($conditions)) {
299
            return false;
300
        }
301
302
        $existing = $this->fetch($entityName, $conditions);
303
304
        if (empty($existing)) {
305
            return false;
306
        }
307
308
        return $existing;
309
    }
310
311
    /**
312
     * Returns entity identifier
313
     * If more than one primary keys, entity will not be identified
314
     *
315
     * @param object $instance
316
     * @param string $entityName
317
     *
318
     * @return string
319
     */
320
    protected function identifyEntity($instance, $entityName)
321
    {
322
        $fields = $this->models->get($entityName)->primaryFields();
323
324
        $id = [];
325
        foreach ($fields as $field) {
326
            $id[] = $this->accessor->getPropertyValue($instance, $field->name());
327
        }
328
329
        return implode(':', $id);
330
    }
331
332
    /**
333
     * Checks if container has array access
334
     *
335
     * @param $container
336
     *
337
     * @throws RelationException
338
     */
339
    protected function assertArrayAccess($container)
340
    {
341
        if (!$container instanceof \Traversable && !is_array($container)) {
342
            throw new RelationException(sprintf('Relation container must be array or instance of ArrayAccess, got %s', $this->getType($container)));
343
        }
344
    }
345
}
346