Completed
Pull Request — master (#28)
by James
05:20
created

AbstractSqlRepository::getRelationshipsFor()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 4
Bugs 2 Features 0
Metric Value
cc 2
eloc 9
c 4
b 2
f 0
nc 2
nop 2
dl 0
loc 15
ccs 0
cts 10
cp 0
crap 6
rs 9.4285
1
<?php
2
3
namespace Percy\Repository;
4
5
use Aura\Sql\ExtendedPdoInterface;
6
use InvalidArgumentException;
7
use Percy\Decorator\DecoratorTrait;
8
use Percy\Entity\Collection;
9
use Percy\Entity\CollectionBuilderTrait;
10
use Percy\Entity\EntityInterface;
11
use Percy\Http\QueryStringParserTrait;
12
use Percy\Store\StoreInterface;
13
use Psr\Http\Message\ServerRequestInterface;
14
use RuntimeException;
15
16
abstract class AbstractSqlRepository implements RepositoryInterface
17
{
18
    use CollectionBuilderTrait;
19
    use DecoratorTrait;
20
    use QueryStringParserTrait;
21
22
    /**
23
     * @var \Aura\Sql\ExtendedPdoInterface
24
     */
25
    protected $dbal;
26
27
    /**
28
     *
29
     * @var mixed
30
     */
31
    protected $relationships = [];
32
33
    /**
34
     * Construct.
35
     *
36
     * @param \Aura\Sql\ExtendedPdoInterface $dbal
37
     */
38 2
    public function __construct(ExtendedPdoInterface $dbal)
39
    {
40 2
        $this->dbal = $dbal;
41 2
    }
42
43
    /**
44
     * {@inheritdoc}
45
     */
46 1
    public function countFromRequest(ServerRequestInterface $request)
47
    {
48 1
        $rules = $this->parseQueryString($request->getUri()->getQuery());
49 1
        list($query, $params) = $this->buildQueryFromRules($rules, 'SELECT COUNT(*) as total FROM ');
50
51 1
        return (int) $this->dbal->fetchOne($query, $params)['total'];
52
    }
53
54
    /**
55
     * {@inheritdoc}
56
     */
57 1
    public function getFromRequest(ServerRequestInterface $request)
58
    {
59 1
        $rules = $this->parseQueryString($request->getUri()->getQuery());
60
61 1
        list($query, $params) = $this->buildQueryFromRules($rules);
62
63 1
        if (array_key_exists('sort', $rules)) {
64 1
            $query .= sprintf(' ORDER BY %s ', $rules['sort']);
65 1
            $query .= (array_key_exists('sort_direction', $rules)) ? $rules['sort_direction'] : 'ASC';
66 1
        }
67
68 1
        if (array_key_exists('limit', $rules)) {
69 1
            $query .= ' LIMIT ';
70 1
            $query .= (array_key_exists('offset', $rules)) ? sprintf('%d,', $rules['offset']) : '';
71 1
            $query .= $rules['limit'];
72 1
        }
73
74 1
        $query = trim(preg_replace('!\s+!', ' ', $query));
75
76 1
        $collection = $this->buildCollection($this->dbal->fetchAll($query, $params))
77 1
                           ->setTotal($this->countFromRequest($request));
78
79 1
        $this->decorate($collection, StoreInterface::ON_READ);
80
81 1
        return $collection;
82
    }
83
84
    /**
85
     * Build a base query without sorting and limits from filter rules.
86
     *
87
     * @param array  $rules
88
     * @param string $start
89
     *
90
     * @return array
91
     */
92 1
    protected function buildQueryFromRules(array $rules, $start = 'SELECT * FROM ')
93
    {
94 1
        $query = $start . $this->getTable();
95
96 1
        $params = [];
97
98 1
        if (array_key_exists('filter', $rules)) {
99 1
            foreach ($rules['filter'] as $key => $where) {
100 1
                $keyword   = ($key === 0) ? ' WHERE' : ' AND';
101 1
                $delimiter = strtoupper($where['delimiter']);
102 1
                $binding   = (in_array($delimiter, ['IN', 'NOT IN'])) ? sprintf('(:%s)', $where['binding']) : ':' . $where['binding'];
103 1
                $query    .= sprintf('%s %s %s %s', $keyword, $where['field'], $delimiter, $binding);
104
105 1
                $params[$where['binding']] = $where['value'];
106 1
            }
107 1
        }
108
109 1
        return [$query, $params];
110
    }
111
112
    /**
113
     * {@inheritdoc}
114
     */
115 1
    public function countByField($field, $value, ServerRequestInterface $request = null)
116
    {
117 1
        $query = sprintf(
118 1
            "SELECT COUNT(*) as total FROM %s WHERE %s.%s IN (:%s)",
119 1
            $this->getTable(),
120 1
            $this->getTable(),
121 1
            $field,
122
            $field
123 1
        );
124
125
        $params = [
126 1
            $field => implode(',', (array) $value)
127 1
        ];
128
129 1
        return (int) $this->dbal->fetchOne($query, $params)['total'];
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135 1
    public function getByField($field, $value, ServerRequestInterface $request = null)
136
    {
137 1
        $query = sprintf(
138 1
            'SELECT * FROM %s WHERE %s.%s IN (:%s)',
139 1
            $this->getTable(),
140 1
            $this->getTable(),
141 1
            $field,
142
            $field
143 1
        );
144
145
        // @todo - allow extra filtering from request
146
147
        $params = [
148 1
            $field => implode(',', (array) $value)
149 1
        ];
150
151 1
        $collection = $this->buildCollection($this->dbal->fetchAll($query, $params))
152 1
                           ->setTotal($this->countByField($field, $value));
153
154 1
        $this->decorate($collection, StoreInterface::ON_READ);
155
156 1
        return $collection;
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162
    public function attachRelationships(
163
        Collection $collection,
164
        $include                        = null,
165
        ServerRequestInterface $request = null
166
    ) {
167
        if (is_null($include)) {
168
            return;
169
        }
170
171
        foreach ($this->getRelationshipMap() as $key => $map) {
172
            if (is_array($include) && ! in_array($key, $include)) {
173
                continue;
174
            }
175
176
            $binds = $this->getRelationshipBinds($collection, $key, $map['defined_in']['entity']);
177
178
            if (empty($binds)) {
179
                continue;
180
            }
181
182
            $query = sprintf(
183
                'SELECT * FROM %s LEFT JOIN %s ON %s.%s = %s.%s WHERE %s.%s IN (%s)',
184
                $map['defined_in']['table'],
185
                $map['target']['table'],
186
                $map['target']['table'],
187
                $map['target']['primary'],
188
                $map['defined_in']['table'],
189
                $map['target']['relationship'],
190
                $map['defined_in']['table'],
191
                $map['defined_in']['primary'],
192
                implode(',', $binds)
193
            );
194
195
            // @todo - extend query with filters
196
197
            $result = $this->dbal->fetchAll($query, []);
198
199
            $this->attachRelationshipsToCollection($collection, $key, $result);
200
        }
201
    }
202
203
    /**
204
     * Iterate a result set and attach the relationship to it's correct entity
205
     * within a collection.
206
     *
207
     * @param \Percy\Entity\Collection $collection
208
     * @param string                   $relationship
209
     * @param array                    $data
210
     *
211
     * @return void
212
     */
213
    protected function attachRelationshipsToCollection(Collection $collection, $relationship, array $data)
214
    {
215
        $map           = $this->getRelationshipMap($relationship);
216
        $relationships = array_column($data, $map['defined_in']['primary']);
217
218
        $remove = [$map['defined_in']['primary'], $map['target']['relationship']];
219
220
        foreach ($data as &$resource) {
221
            $resource = array_filter($resource, function ($key) use ($remove) {
222
                return (! in_array($key, $remove));
223
            }, ARRAY_FILTER_USE_KEY);
224
        }
225
226
        foreach ($collection->getIterator() as $entity) {
227
            $entityRels = $entity->getRelationshipMap();
228
229
            if (! array_key_exists($relationship, $entityRels)) {
230
                continue;
231
            }
232
233
            $keys = array_keys(preg_grep("/{$entity[$map['defined_in']['entity']]}/", $relationships));
234
            $rels = array_filter($data, function ($key) use ($keys) {
235
                return in_array($key, $keys);
236
            }, ARRAY_FILTER_USE_KEY);
237
238
            $rels = $this->buildCollection($rels, $entityRels[$relationship])->setTotal(count($rels));
239
            $this->decorate($rels, StoreInterface::ON_READ);
240
241
            $entity->addRelationship($relationship, $rels);
242
        }
243
    }
244
245
    /**
246
     * Return relationship bind conditional.
247
     *
248
     * @param \Percy\Entity\Collection $collection
249
     * @param string                   $relationship
250
     * @param string                   $key
251
     *
252
     * @return string
253
     */
254
    protected function getRelationshipBinds(Collection $collection, $relationship, $key)
255
    {
256
        $primaries = [];
257
258
        foreach ($collection->getIterator() as $entity) {
259
            if (! array_key_exists($relationship, $entity->getRelationshipMap())) {
260
                continue;
261
            }
262
263
            $primaries[] = "'{$entity[$key]}'";
264
        }
265
266
        return $primaries;
267
    }
268
269
    /**
270
     * Get possible relationships and the properties attached to them.
271
     *
272
     * @param string $relationship
273
     *
274
     * @throws \InvalidArgumentException when requested relationship is not defined
275
     * @throws \RuntimeException when map structure is defined incorrectly
276
     *
277
     * @return array
278
     */
279
    public function getRelationshipMap($relationship = null)
280
    {
281
        if (is_null($relationship)) {
282
            return $this->relationships;
283
        }
284
285
        if (! array_key_exists($relationship, $this->relationships)) {
286
            throw new InvalidArgumentException(
287
                sprintf('(%s) is not defined in the relationship map on (%s)', $relationship, get_class($this))
288
            );
289
        }
290
291
        $map = $this->relationships[$relationship];
292
293
        foreach ([
294
            'defined_in' => ['table', 'primary', 'entity'],
295
            'target'     => ['table', 'primary', 'relationship']
296
        ] as $key => $value) {
297
            if (! array_key_exists($key, $map) || ! is_array($map[$key])) {
298
                throw new RuntimeException(
299
                    sprintf(
300
                        'Relationship (%s) should contain the (%s) key and should be of type array on (%s)',
301
                        $relationship, $key, get_class($this)
302
                    )
303
                );
304
            }
305
306
            if (! empty(array_diff($value, array_keys($map[$key])))) {
307
                throw new RuntimeException(
308
                    sprintf(
309
                        '(%s) for relationship (%s) should contain keys (%s) on (%s)',
310
                        $key, $relationship, implode(', ', $value), get_class($this)
311
                    )
312
                );
313
            }
314
        }
315
316
        return $map;
317
    }
318
319
    /**
320
     * Returns table that repository is reading from.
321
     *
322
     * @return string
323
     */
324
    abstract protected function getTable();
325
}
326