Completed
Push — master ( 0709b5...59a2ac )
by Phil
02:09
created

AbstractSqlRepository::buildQueryFromRules()   C

Complexity

Conditions 7
Paths 4

Size

Total Lines 28
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 7

Importance

Changes 7
Bugs 1 Features 1
Metric Value
c 7
b 1
f 1
dl 0
loc 28
ccs 21
cts 21
cp 1
rs 6.7272
cc 7
eloc 18
nc 4
nop 6
crap 7
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, $joins = '', $conditionals = '', $end = '')
47
    {
48 1
        $rules = $this->parseQueryString($request->getUri()->getQuery());
49
50 1
        list($query, $params) = $this->buildQueryFromRules(
51 1
            $rules,
52 1
            'SELECT COUNT(*) as total FROM',
53 1
            $joins,
54 1
            $conditionals,
55
            $end
56 1
        );
57
58 1
        return (int) $this->dbal->fetchOne($query, $params)['total'];
59
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64 1
    public function getFromRequest(
65
        ServerRequestInterface $request,
66
        $start        = 'SELECT * FROM',
67
        $joins        = '',
68
        $conditionals = '',
69
        $end          = ''
70
    ) {
71 1
        $rules = $this->parseQueryString($request->getUri()->getQuery());
72
73 1
        list($query, $params) = $this->buildQueryFromRules($rules, $start, $joins, $conditionals, $end);
74
75 1
        if (array_key_exists('sort', $rules)) {
76 1
            $query .= sprintf(' ORDER BY %s ', $rules['sort']);
77 1
            $query .= (array_key_exists('sort_direction', $rules)) ? $rules['sort_direction'] : 'ASC';
78 1
        }
79
80 1
        if (array_key_exists('limit', $rules)) {
81 1
            $query .= ' LIMIT ';
82 1
            $query .= (array_key_exists('offset', $rules)) ? sprintf('%d,', $rules['offset']) : '';
83 1
            $query .= $rules['limit'];
84 1
        }
85
86 1
        $collection = $this->buildCollection($this->dbal->fetchAll($query, $params))
87 1
                           ->setTotal($this->countFromRequest($request, $joins, $conditionals, $end));
88
89 1
        $this->decorate($collection, StoreInterface::ON_READ);
90
91 1
        return $collection;
92
    }
93
94
    /**
95
     * Build a base query without sorting and limits from filter rules.
96
     *
97
     * @param array       $rules
98
     * @param string      $start
99
     * @param string      $joins
100
     * @param string      $conditionals
101
     * @param string      $end
102
     * @param string|null $table
103
     *
104
     * @return array
105
     */
106 1
    protected function buildQueryFromRules(array $rules, $start, $joins, $conditionals, $end, $table = null)
107
    {
108 1
        $table = (is_null($table)) ? $this->getTable() : $table;
109 1
        $query = sprintf(
110 1
            '%s %s %s %s',
111 1
            trim($start),
112 1
            trim($table),
113 1
            trim($joins),
114 1
            trim($conditionals)
115 1
        );
116
117 1
        $params = [];
118
119 1
        if (array_key_exists('filter', $rules)) {
120 1
            foreach ($rules['filter'] as $key => $where) {
121 1
                $keyword   = ($key === 0 || $conditionals !== '') ? ' WHERE' : ' AND';
122 1
                $delimiter = strtoupper($where['delimiter']);
123 1
                $binding   = (in_array($delimiter, ['IN', 'NOT IN'])) ? sprintf('(:%s)', $where['binding']) : ':' . $where['binding'];
124 1
                $query    .= sprintf('%s %s %s %s', $keyword, $where['field'], $delimiter, $binding);
125
126 1
                $params[$where['binding']] = $where['value'];
127 1
            }
128 1
        }
129
130 1
        $query .= " {$end}";
131
132 1
        return [$query, $params];
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138 1
    public function countByField($field, $value)
139
    {
140 1
        $query = sprintf(
141 1
            'SELECT COUNT(*) as total FROM %s WHERE %s.%s IN (:%s)',
142 1
            $this->getTable(),
143 1
            $this->getTable(),
144 1
            $field,
145
            $field
146 1
        );
147
148
        $params = [
149 1
            $field => implode(',', (array) $value)
150 1
        ];
151
152 1
        return (int) $this->dbal->fetchOne($query, $params)['total'];
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158 1
    public function getByField($field, $value)
159
    {
160 1
        $query = sprintf(
161 1
            'SELECT * FROM %s WHERE %s.%s IN (:%s)',
162 1
            $this->getTable(),
163 1
            $this->getTable(),
164 1
            $field,
165
            $field
166 1
        );
167
168
        $params = [
169 1
            $field => implode(',', (array) $value)
170 1
        ];
171
172 1
        $collection = $this->buildCollection($this->dbal->fetchAll($query, $params))
173 1
                           ->setTotal($this->countByField($field, $value));
174
175 1
        $this->decorate($collection, StoreInterface::ON_READ);
176
177 1
        return $collection;
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183
    public function getRelationshipsFor(Collection $collection, array $relationships = [])
184
    {
185
        $relCollection = new Collection;
186
187
        foreach ($collection->getIterator() as $entity) {
188
            $rels = $entity->getRelationships();
189
            array_walk($rels, [$this, 'getEntityRelationships'], [
190
                'entity'     => $entity,
191
                'collection' => $relCollection,
192
                'include'    => $relationships
193
            ]);
194
        }
195
196
        return $relCollection;
197
    }
198
199
    /**
200
     * Attach relationships to a specific entity.
201
     *
202
     * @param string $entityType
203
     * @param string $relationship
204
     * @param array  $userData
205
     *
206
     * @return void
207
     */
208
    protected function getEntityRelationships($entityType, $relationship, array $userData)
209
    {
210
        $collection = $userData['collection'];
211
        $include    = $userData['include'];
212
        $entity     = $userData['entity'];
213
        $map        = $this->getRelationshipMap($relationship);
214
215
        if (! in_array($relationship, $include)) {
216
            return false;
217
        }
218
219
        $query = sprintf(
220
            'SELECT * FROM %s LEFT JOIN %s ON %s.%s = %s.%s WHERE %s = :%s',
221
            $map['defined_in']['table'],
222
            $map['target']['table'],
223
            $map['target']['table'],
224
            $map['target']['primary'],
225
            $map['defined_in']['table'],
226
            $map['target']['relationship'],
227
            $map['defined_in']['primary'],
228
            $map['defined_in']['entity']
229
        );
230
231
        $result = $this->dbal->fetchAll($query, [
232
            $map['defined_in']['entity'] => $entity[$map['defined_in']['entity']]
233
        ]);
234
235
        $remove = [$map['defined_in']['primary'], $map['target']['relationship']];
236
237
        foreach ($result as $resource) {
238
            $resource = array_filter($resource, function ($key) use ($remove) {
239
                return (! in_array($key, $remove));
240
            }, ARRAY_FILTER_USE_KEY);
241
242
            $collection->addEntity((new $entityType)->hydrate($resource));
243
        }
244
    }
245
246
    /**
247
     * Get possible relationships and the properties attached to them.
248
     *
249
     * @param string $relationship
250
     *
251
     * @throws \InvalidArgumentException when requested relationship is not defined
252
     * @throws \RuntimeException when map structure is defined incorrectly
253
     *
254
     * @return array
255
     */
256
    public function getRelationshipMap($relationship)
257
    {
258
        if (! array_key_exists($relationship, $this->relationships)) {
259
            throw new InvalidArgumentException(
260
                sprintf('(%s) is not defined in the relationship map on (%s)', $relationship, get_class($this))
261
            );
262
        }
263
264
        $map = $this->relationships[$relationship];
265
266
        foreach ([
267
            'defined_in' => ['table', 'primary', 'entity'],
268
            'target'     => ['table', 'primary', 'relationship']
269
        ] as $key => $value) {
270
            if (! array_key_exists($key, $map) || ! is_array($map[$key])) {
271
                throw new RuntimeException(
272
                    sprintf(
273
                        'Relationship (%s) should contain the (%s) key and should be of type array on (%s)',
274
                        $relationship, $key, get_class($this)
275
                    )
276
                );
277
            }
278
279
            if (! empty(array_diff($value, array_keys($map[$key])))) {
280
                throw new RuntimeException(
281
                    sprintf(
282
                        '(%s) for relationship (%s) should contain keys (%s) on (%s)',
283
                        $key, $relationship, implode(', ', $value), get_class($this)
284
                    )
285
                );
286
            }
287
        }
288
289
        return $map;
290
    }
291
292
    /**
293
     * Returns table that repository is reading from.
294
     *
295
     * @return string
296
     */
297
    abstract protected function getTable();
298
}
299