Completed
Pull Request — master (#30)
by Phil
03:31 queued 01:23
created

AbstractSqlRepository::getFromRequest()   C

Complexity

Conditions 8
Paths 18

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 8.064

Importance

Changes 10
Bugs 0 Features 2
Metric Value
c 10
b 0
f 2
dl 0
loc 31
ccs 18
cts 20
cp 0.9
rs 5.3846
cc 8
eloc 17
nc 18
nop 1
crap 8.064
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, true);
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) && ! array_key_exists('search', $rules)) {
64 1
            $query .= sprintf(' ORDER BY %s ', (in_array($rules['sort'], ['rand', 'random'])) ? 'RAND()' : $rules['sort']);
65 1
            $query .= (array_key_exists('sort_direction', $rules)) ? $rules['sort_direction'] : 'ASC';
66 1
        }
67
68 1
        if (array_key_exists('search', $rules)) {
69
            $query .= sprintf(' ORDER BY MATCH (%s) AGAINST (:match_bind) > :score_bind', $rules['search']['fields']);
70
        }
71
72 1
        if (array_key_exists('limit', $rules)) {
73 1
            $query .= ' LIMIT ';
74 1
            $query .= (array_key_exists('offset', $rules)) ? sprintf('%d,', $rules['offset']) : '';
75 1
            $query .= $rules['limit'];
76 1
        }
77
78 1
        $query = trim(preg_replace('!\s+!', ' ', $query));
79
80 1
        $data = $this->dbal->fetchAll($query, $params);
81
82 1
        $collection = $this->buildCollection($data)->setTotal($this->countFromRequest($request));
83
84 1
        $this->decorate($collection, StoreInterface::ON_READ);
85
86 1
        return $collection;
87
    }
88
89
    /**
90
     * Build a base query without sorting and limits from filter rules.
91
     *
92
     * @param array  $rules
93
     * @param string $start
0 ignored issues
show
Bug introduced by
There is no parameter named $start. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
94
     *
95
     * @return array
96
     */
97 1
    protected function buildQueryFromRules(array $rules, $count = false)
98
    {
99 1
        $start = ($count === false) ? 'SELECT * FROM ' : 'SELECT *, COUNT(*) as total FROM ';
100
101 1
        $query = $start . $this->getTable();
102
103 1
        $params = [];
104
105 1
        if (array_key_exists('filter', $rules)) {
106 1
            foreach ($rules['filter'] as $key => $where) {
107 1
                $keyword   = ($key === 0) ? ' WHERE' : ' AND';
108 1
                $delimiter = strtoupper($where['delimiter']);
109 1
                $binding   = (in_array($delimiter, ['IN', 'NOT IN'])) ? sprintf('(:%s)', $where['binding']) : ':' . $where['binding'];
110 1
                $query    .= sprintf('%s %s %s %s', $keyword, $where['field'], $delimiter, $binding);
111
112 1
                $params[$where['binding']] = $where['value'];
113 1
            }
114 1
        }
115
116 1
        if (array_key_exists('search', $rules)) {
117
            $keyword = (array_key_exists('filter', $rules)) ? ' AND' : ' WHERE';
118
            $query  .= sprintf('%s MATCH (%s) AGAINST (:match_bind IN BOOLEAN MODE)', $keyword, $rules['search']['columns']);
119
            $query  .= sprintf(' HAVING MATCH (%s) AGAINST (:match_bind) > :score_bind', $rules['search']['columns']);
120
121
            $params['match_bind'] = $rules['search']['term'];
122
            $params['score_bind'] = (array_key_exists('minscore', $rules)) ? $rules['minscore'] : 0;
123
        }
124
125 1
        return [$query, $params];
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131 1
    public function countByField($field, $value, ServerRequestInterface $request = null)
132
    {
133 1
        $query = sprintf(
134 1
            "SELECT COUNT(*) as total FROM %s WHERE %s.%s IN (:%s)",
135 1
            $this->getTable(),
136 1
            $this->getTable(),
137 1
            $field,
138
            $field
139 1
        );
140
141
        $params = [
142 1
            $field => implode(',', (array) $value)
143 1
        ];
144
145 1
        return (int) $this->dbal->fetchOne($query, $params)['total'];
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151 1
    public function getByField($field, $value, ServerRequestInterface $request = null)
152
    {
153 1
        $query = sprintf(
154 1
            'SELECT * FROM %s WHERE %s.%s IN (:%s)',
155 1
            $this->getTable(),
156 1
            $this->getTable(),
157 1
            $field,
158
            $field
159 1
        );
160
161
        // @todo - allow extra filtering from request
162
163
        $params = [
164 1
            $field => implode(',', (array) $value)
165 1
        ];
166
167 1
        $collection = $this->buildCollection($this->dbal->fetchAll($query, $params))
168 1
                           ->setTotal($this->countByField($field, $value));
169
170 1
        $this->decorate($collection, StoreInterface::ON_READ);
171
172 1
        return $collection;
173
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178
    public function attachRelationships(
179
        Collection $collection,
180
        $include                        = null,
181
        ServerRequestInterface $request = null
182
    ) {
183
        if (is_null($include)) {
184
            return;
185
        }
186
187
        foreach ($this->getRelationshipMap() as $key => $map) {
188
            if (is_array($include) && ! in_array($key, $include)) {
189
                continue;
190
            }
191
192
            $binds = $this->getRelationshipBinds($collection, $key, $map['defined_in']['entity']);
193
194
            if (empty($binds)) {
195
                continue;
196
            }
197
198
            $query = sprintf(
199
                'SELECT * FROM %s LEFT JOIN %s ON %s.%s = %s.%s WHERE %s.%s IN (%s)',
200
                $map['defined_in']['table'],
201
                $map['target']['table'],
202
                $map['target']['table'],
203
                $map['target']['primary'],
204
                $map['defined_in']['table'],
205
                $map['target']['relationship'],
206
                $map['defined_in']['table'],
207
                $map['defined_in']['primary'],
208
                implode(',', $binds)
209
            );
210
211
            // @todo - extend query with filters
212
213
            $result = $this->dbal->fetchAll($query, []);
214
215
            $this->attachRelationshipsToCollection($collection, $key, $result);
216
        }
217
    }
218
219
    /**
220
     * Iterate a result set and attach the relationship to it's correct entity
221
     * within a collection.
222
     *
223
     * @param \Percy\Entity\Collection $collection
224
     * @param string                   $relationship
225
     * @param array                    $data
226
     *
227
     * @return void
228
     */
229
    protected function attachRelationshipsToCollection(Collection $collection, $relationship, array $data)
230
    {
231
        $map           = $this->getRelationshipMap($relationship);
232
        $relationships = array_column($data, $map['defined_in']['primary']);
233
234
        $remove = [$map['defined_in']['primary'], $map['target']['relationship']];
235
236
        foreach ($data as &$resource) {
237
            $resource = array_filter($resource, function ($key) use ($remove) {
238
                return (! in_array($key, $remove));
239
            }, ARRAY_FILTER_USE_KEY);
240
        }
241
242
        foreach ($collection->getIterator() as $entity) {
243
            $entityRels = $entity->getRelationshipMap();
244
245
            if (! array_key_exists($relationship, $entityRels)) {
246
                continue;
247
            }
248
249
            $keys = array_keys(preg_grep("/{$entity[$map['defined_in']['entity']]}/", $relationships));
250
            $rels = array_filter($data, function ($key) use ($keys) {
251
                return in_array($key, $keys);
252
            }, ARRAY_FILTER_USE_KEY);
253
254
            $rels = $this->buildCollection($rels, $entityRels[$relationship])->setTotal(count($rels));
255
            $this->decorate($rels, StoreInterface::ON_READ);
256
257
            $entity->addRelationship($relationship, $rels);
258
        }
259
    }
260
261
    /**
262
     * Return relationship bind conditional.
263
     *
264
     * @param \Percy\Entity\Collection $collection
265
     * @param string                   $relationship
266
     * @param string                   $key
267
     *
268
     * @return string
269
     */
270
    protected function getRelationshipBinds(Collection $collection, $relationship, $key)
271
    {
272
        $primaries = [];
273
274
        foreach ($collection->getIterator() as $entity) {
275
            if (! array_key_exists($relationship, $entity->getRelationshipMap())) {
276
                continue;
277
            }
278
279
            $primaries[] = "'{$entity[$key]}'";
280
        }
281
282
        return $primaries;
283
    }
284
285
    /**
286
     * Get possible relationships and the properties attached to them.
287
     *
288
     * @param string $relationship
289
     *
290
     * @throws \InvalidArgumentException when requested relationship is not defined
291
     * @throws \RuntimeException when map structure is defined incorrectly
292
     *
293
     * @return array
294
     */
295
    public function getRelationshipMap($relationship = null)
296
    {
297
        if (is_null($relationship)) {
298
            return $this->relationships;
299
        }
300
301
        if (! array_key_exists($relationship, $this->relationships)) {
302
            throw new InvalidArgumentException(
303
                sprintf('(%s) is not defined in the relationship map on (%s)', $relationship, get_class($this))
304
            );
305
        }
306
307
        $map = $this->relationships[$relationship];
308
309
        foreach ([
310
            'defined_in' => ['table', 'primary', 'entity'],
311
            'target'     => ['table', 'primary', 'relationship']
312
        ] as $key => $value) {
313
            if (! array_key_exists($key, $map) || ! is_array($map[$key])) {
314
                throw new RuntimeException(
315
                    sprintf(
316
                        'Relationship (%s) should contain the (%s) key and should be of type array on (%s)',
317
                        $relationship, $key, get_class($this)
318
                    )
319
                );
320
            }
321
322
            if (! empty(array_diff($value, array_keys($map[$key])))) {
323
                throw new RuntimeException(
324
                    sprintf(
325
                        '(%s) for relationship (%s) should contain keys (%s) on (%s)',
326
                        $key, $relationship, implode(', ', $value), get_class($this)
327
                    )
328
                );
329
            }
330
        }
331
332
        return $map;
333
    }
334
335
    /**
336
     * Returns table that repository is reading from.
337
     *
338
     * @return string
339
     */
340
    abstract protected function getTable();
341
}
342