Completed
Pull Request — master (#20)
by Phil
18:27 queued 16:04
created

AbstractSqlRepository   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 267
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 49.21%

Importance

Changes 26
Bugs 4 Features 2
Metric Value
wmc 26
c 26
b 4
f 2
lcom 1
cbo 6
dl 0
loc 267
ccs 62
cts 126
cp 0.4921
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A countByField() 0 16 1
A getByField() 0 17 1
A getRelationshipsFor() 0 15 2
B getEntityRelationships() 0 37 3
B getRelationshipMap() 0 35 6
getTable() 0 1 ?
A countFromRequest() 0 14 1
B buildQueryFromRules() 0 22 6
B getFromRequest() 0 25 5
1
<?php
2
3
namespace Percy\Repository;
4
5
use Aura\Sql\ExtendedPdoInterface;
6
use InvalidArgumentException;
7
use Percy\Entity\Collection;
8
use Percy\Entity\CollectionBuilderTrait;
9
use Percy\Entity\EntityInterface;
10
use Percy\Http\QueryStringParserTrait;
11
use Psr\Http\Message\ServerRequestInterface;
12
use RuntimeException;
13
14
abstract class AbstractSqlRepository implements RepositoryInterface
15
{
16
    use CollectionBuilderTrait;
17
    use QueryStringParserTrait;
18
19
    /**
20
     * @var \Aura\Sql\ExtendedPdoInterface
21
     */
22
    protected $dbal;
23
24
    /**
25
     *
26
     * @var mixed
27
     */
28
    protected $relationships = [];
29
30
    /**
31
     * Construct.
32
     *
33
     * @param \Aura\Sql\ExtendedPdoInterface $dbal
34
     */
35 2
    public function __construct(ExtendedPdoInterface $dbal)
36
    {
37 2
        $this->dbal = $dbal;
38 2
    }
39
40
    /**
41
     * {@inheritdoc}
42
     */
43 1
    public function countFromRequest(ServerRequestInterface $request, $joins = '', $conditionals = '', $end = '')
44
    {
45 1
        $rules = $this->parseQueryString($request->getUri()->getQuery());
46
47 1
        list($query, $params) = $this->buildQueryFromRules(
48 1
            $rules,
49 1
            'SELECT COUNT(*) as total FROM ',
50 1
            $joins,
51 1
            $conditionals,
52
            $end
53 1
        );
54
55 1
        return (int) $this->dbal->fetchOne($query, $params)['total'];
56
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61 1
    public function getFromRequest(
62
        ServerRequestInterface $request,
63
        $start        = 'SELECT * FROM ',
64
        $joins        = '',
65
        $conditionals = '',
66
        $end          = ''
67
    ) {
68 1
        $rules = $this->parseQueryString($request->getUri()->getQuery());
69
70 1
        list($query, $params) = $this->buildQueryFromRules($rules, $start, $joins, $conditionals, $end);
71
72 1
        if (array_key_exists('sort', $rules)) {
73 1
            $query .= sprintf(' ORDER BY %s ', $rules['sort']);
74 1
            $query .= (array_key_exists('sort_direction', $rules)) ? $rules['sort_direction'] : 'ASC';
75 1
        }
76
77 1
        if (array_key_exists('limit', $rules)) {
78 1
            $query .= ' LIMIT ';
79 1
            $query .= (array_key_exists('offset', $rules)) ? sprintf('%d,', $rules['offset']) : '';
80 1
            $query .= $rules['limit'];
81 1
        }
82
83 1
        return $this->buildCollection($this->dbal->fetchAll($query, $params))
84 1
                    ->setTotal($this->countFromRequest($request, $joins, $conditionals, $end));
85
    }
86
87
    /**
88
     * Build a base query without sorting and limits from filter rules.
89
     *
90
     * @param array  $rules
91
     * @param string $start
92
     * @param string $joins
93
     * @param string $conditionals
94
     * @param string $end
95
     *
96
     * @return array
97
     */
98 1
    protected function buildQueryFromRules(array $rules, $start, $joins, $conditionals, $end)
99
    {
100 1
        $start = $start . $this->getTable();
101 1
        $query = sprintf('%s %s %s', $start, $joins, $conditionals);
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 || $conditionals !== '') ? ' 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
        $query .= " {$end}";
117
118 1
        return [$query, $params];
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124 1
    public function countByField($field, $value)
125
    {
126 1
        $query = sprintf(
127 1
            'SELECT COUNT(*) as total FROM %s WHERE %s.%s IN (:%s)',
128 1
            $this->getTable(),
129 1
            $this->getTable(),
130 1
            $field,
131
            $field
132 1
        );
133
134
        $params = [
135 1
            $field => implode(',', (array) $value)
136 1
        ];
137
138 1
        return (int) $this->dbal->fetchOne($query, $params)['total'];
139
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144 1
    public function getByField($field, $value)
145
    {
146 1
        $query = sprintf(
147 1
            'SELECT * FROM %s WHERE %s.%s IN (:%s)',
148 1
            $this->getTable(),
149 1
            $this->getTable(),
150 1
            $field,
151
            $field
152 1
        );
153
154
        $params = [
155 1
            $field => implode(',', (array) $value)
156 1
        ];
157
158 1
        return $this->buildCollection($this->dbal->fetchAll($query, $params))
159 1
                    ->setTotal($this->countByField($field, $value));
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165
    public function getRelationshipsFor(Collection $collection, array $relationships = [])
166
    {
167
        $relCollection = new Collection;
168
169
        foreach ($collection->getIterator() as $entity) {
170
            $rels = $entity->getRelationships();
171
            array_walk($rels, [$this, 'getEntityRelationships'], [
172
                'entity'     => $entity,
173
                'collection' => $relCollection,
174
                'include'    => $relationships
175
            ]);
176
        }
177
178
        return $relCollection;
179
    }
180
181
    /**
182
     * Attach relationships to a specific entity.
183
     *
184
     * @param string $entityType
185
     * @param string $relationship
186
     * @param array  $userData
187
     *
188
     * @return void
189
     */
190
    protected function getEntityRelationships($entityType, $relationship, array $userData)
191
    {
192
        $collection = $userData['collection'];
193
        $include    = $userData['include'];
194
        $entity     = $userData['entity'];
195
        $map        = $this->getRelationshipMap($relationship);
196
197
        if (! in_array($relationship, $include)) {
198
            return false;
199
        }
200
201
        $query = sprintf(
202
            'SELECT * FROM %s LEFT JOIN %s ON %s.%s = %s.%s WHERE %s = :%s',
203
            $map['defined_in']['table'],
204
            $map['target']['table'],
205
            $map['target']['table'],
206
            $map['target']['primary'],
207
            $map['defined_in']['table'],
208
            $map['target']['relationship'],
209
            $map['defined_in']['primary'],
210
            $map['defined_in']['entity']
211
        );
212
213
        $result = $this->dbal->fetchAll($query, [
214
            $map['defined_in']['entity'] => $entity[$map['defined_in']['entity']]
215
        ]);
216
217
        $remove = [$map['defined_in']['primary'], $map['target']['relationship']];
218
219
        foreach ($result as $resource) {
220
            $resource = array_filter($resource, function ($key) use ($remove) {
221
                return (! in_array($key, $remove));
222
            }, ARRAY_FILTER_USE_KEY);
223
224
            $collection->addEntity((new $entityType)->hydrate($resource));
225
        }
226
    }
227
228
    /**
229
     * Get possible relationships and the properties attached to them.
230
     *
231
     * @param string $relationship
232
     *
233
     * @throws \InvalidArgumentException when requested relationship is not defined
234
     * @throws \RuntimeException when map structure is defined incorrectly
235
     *
236
     * @return array
237
     */
238
    public function getRelationshipMap($relationship)
239
    {
240
        if (! array_key_exists($relationship, $this->relationships)) {
241
            throw new InvalidArgumentException(
242
                sprintf('(%s) is not defined in the relationship map on (%s)', $relationship, get_class($this))
243
            );
244
        }
245
246
        $map = $this->relationships[$relationship];
247
248
        foreach ([
249
            'defined_in' => ['table', 'primary', 'entity'],
250
            'target'     => ['table', 'primary', 'relationship']
251
        ] as $key => $value) {
252
            if (! array_key_exists($key, $map) || ! is_array($map[$key])) {
253
                throw new RuntimeException(
254
                    sprintf(
255
                        'Relationship (%s) should contain the (%s) key and should be of type array on (%s)',
256
                        $relationship, $key, get_class($this)
257
                    )
258
                );
259
            }
260
261
            if (! empty(array_diff($value, array_keys($map[$key])))) {
262
                throw new RuntimeException(
263
                    sprintf(
264
                        '(%s) for relationship (%s) should contain keys (%s) on (%s)',
265
                        $key, $relationship, implode(', ', $value), get_class($this)
266
                    )
267
                );
268
            }
269
        }
270
271
        return $map;
272
    }
273
274
    /**
275
     * Returns table that repository is reading from.
276
     *
277
     * @return string
278
     */
279
    abstract protected function getTable();
280
}
281