Completed
Pull Request — master (#26)
by Phil
02:18
created

AbstractSqlRepository::getRelationshipBinds()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 14
ccs 0
cts 8
cp 0
rs 9.4285
cc 3
eloc 7
nc 3
nop 3
crap 12
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
        foreach ($this->getRelationshipMap() as $key => $map) {
168
            if (is_array($include) && ! in_array($key, $include)) {
169
                continue;
170
            }
171
172
            $binds = $this->getRelationshipBinds($collection, $key, $map['defined_in']['entity']);
173
174
            if (empty($binds)) {
175
                continue;
176
            }
177
178
            $query = sprintf(
179
                'SELECT * FROM %s LEFT JOIN %s ON %s.%s = %s.%s WHERE %s.%s IN (%s)',
180
                $map['defined_in']['table'],
181
                $map['target']['table'],
182
                $map['target']['table'],
183
                $map['target']['primary'],
184
                $map['defined_in']['table'],
185
                $map['target']['relationship'],
186
                $map['defined_in']['table'],
187
                $map['defined_in']['primary'],
188
                implode(',', $binds)
189
            );
190
191
            // @todo - extend query with filters
192
193
            $result = $this->dbal->fetchAll($query, []);
194
195
            $this->attachRelationshipsToCollection($collection, $key, $result);
196
        }
197
    }
198
199
    /**
200
     * Iterate a result set and attach the relationship to it's correct entity
201
     * within a collection.
202
     *
203
     * @param \Percy\Entity\Collection $collection
204
     * @param string                   $relationship
205
     * @param array                    $data
206
     *
207
     * @return void
208
     */
209
    protected function attachRelationshipsToCollection(Collection $collection, $relationship, array $data)
210
    {
211
        $map           = $this->getRelationshipMap($relationship);
212
        $relationships = array_column($data, $map['defined_in']['primary']);
213
214
        $remove = [$map['defined_in']['primary'], $map['target']['relationship']];
215
216
        foreach ($data as &$resource) {
217
            $resource = array_filter($resource, function ($key) use ($remove) {
218
                return (! in_array($key, $remove));
219
            }, ARRAY_FILTER_USE_KEY);
220
        }
221
222
        foreach ($collection->getIterator() as $entity) {
223
            $entityRels = $entity->getRelationshipMap();
224
225
            if (! array_key_exists($relationship, $entityRels)) {
226
                continue;
227
            }
228
229
            $keys = array_keys(preg_grep("/{$entity[$map['defined_in']['entity']]}/", $relationships));
230
            $rels = array_filter($data, function ($key) use ($keys) {
231
                return in_array($key, $keys);
232
            }, ARRAY_FILTER_USE_KEY);
233
234
            $rels = $this->buildCollection($rels, $entityRels[$relationship])->setTotal(count($rels));
235
            $this->decorate($rels, StoreInterface::ON_READ);
236
237
            $entity->addRelationship($relationship, $rels);
238
        }
239
    }
240
241
    /**
242
     * Return relationship bind conditional.
243
     *
244
     * @param \Percy\Entity\Collection $collection
245
     * @param string                   $relationship
246
     * @param string                   $key
247
     *
248
     * @return string
249
     */
250
    protected function getRelationshipBinds(Collection $collection, $relationship, $key)
251
    {
252
        $primaries = [];
253
254
        foreach ($collection->getIterator() as $entity) {
255
            if (! array_key_exists($relationship, $entity->getRelationshipMap())) {
256
                continue;
257
            }
258
259
            $primaries[] = "'{$entity[$key]}'";
260
        }
261
262
        return $primaries;
263
    }
264
265
    /**
266
     * Get possible relationships and the properties attached to them.
267
     *
268
     * @param string $relationship
269
     *
270
     * @throws \InvalidArgumentException when requested relationship is not defined
271
     * @throws \RuntimeException when map structure is defined incorrectly
272
     *
273
     * @return array
274
     */
275
    public function getRelationshipMap($relationship = null)
276
    {
277
        if (is_null($relationship)) {
278
            return $this->relationships;
279
        }
280
281
        if (! array_key_exists($relationship, $this->relationships)) {
282
            throw new InvalidArgumentException(
283
                sprintf('(%s) is not defined in the relationship map on (%s)', $relationship, get_class($this))
284
            );
285
        }
286
287
        $map = $this->relationships[$relationship];
288
289
        foreach ([
290
            'defined_in' => ['table', 'primary', 'entity'],
291
            'target'     => ['table', 'primary', 'relationship']
292
        ] as $key => $value) {
293
            if (! array_key_exists($key, $map) || ! is_array($map[$key])) {
294
                throw new RuntimeException(
295
                    sprintf(
296
                        'Relationship (%s) should contain the (%s) key and should be of type array on (%s)',
297
                        $relationship, $key, get_class($this)
298
                    )
299
                );
300
            }
301
302
            if (! empty(array_diff($value, array_keys($map[$key])))) {
303
                throw new RuntimeException(
304
                    sprintf(
305
                        '(%s) for relationship (%s) should contain keys (%s) on (%s)',
306
                        $key, $relationship, implode(', ', $value), get_class($this)
307
                    )
308
                );
309
            }
310
        }
311
312
        return $map;
313
    }
314
315
    /**
316
     * Returns table that repository is reading from.
317
     *
318
     * @return string
319
     */
320
    abstract protected function getTable();
321
}
322