Completed
Push — master ( 23ff15...beb2ae )
by Phil
05:50
created

AbstractSqlRepository::getRelationshipsFor()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 8

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 15
ccs 0
cts 10
cp 0
rs 9.4286
cc 3
eloc 8
nc 4
nop 2
crap 12
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)
44
    {
45 1
        $rules = $this->parseQueryString($request->getUri()->getQuery());
46 1
        list($query, $params) = $this->buildQueryFromRules($rules, 'SELECT COUNT(*) as total FROM ');
47
48 1
        return (int) $this->dbal->fetchOne($query, $params)['total'];
49
    }
50
51
    /**
52
     * {@inheritdoc}
53
     */
54 1
    public function getFromRequest(ServerRequestInterface $request)
55
    {
56 1
        $rules = $this->parseQueryString($request->getUri()->getQuery());
57
58 1
        list($query, $params) = $this->buildQueryFromRules($rules);
59
60 1
        if (array_key_exists('sort', $rules)) {
61 1
            $query .= sprintf(' ORDER BY %s ', $rules['sort']);
62 1
            $query .= (array_key_exists('sort_direction', $rules)) ? $rules['sort_direction'] : 'ASC';
63 1
        }
64
65 1
        if (array_key_exists('limit', $rules)) {
66 1
            $query .= ' LIMIT ';
67 1
            $query .= (array_key_exists('offset', $rules)) ? sprintf('%d,', $rules['offset']) : '';
68 1
            $query .= $rules['limit'];
69 1
        }
70
71 1
        return $this->buildCollection($this->dbal->fetchAll($query, $params))
72 1
                    ->setTotal($this->countFromRequest($request));
73
    }
74
75
    /**
76
     * Build a base query without sorting and limits from filter rules.
77
     *
78
     * @param array  $rules
79
     * @param string $start
80
     *
81
     * @return array
82
     */
83 1
    protected function buildQueryFromRules(array $rules, $start = 'SELECT * FROM ')
84
    {
85 1
        $query = $start . $this->getTable();
86
87 1
        $params = [];
88
89 1
        if (array_key_exists('filter', $rules)) {
90 1
            foreach ($rules['filter'] as $key => $where) {
91 1
                $keyword = ($key === 0) ? ' WHERE' : ' AND';
92 1
                $query  .= sprintf('%s %s %s :%s', $keyword, $where['field'], $where['delimiter'], $where['field']);
93
94 1
                $params[$where['field']] = $where['value'];
95 1
            }
96 1
        }
97
98 1
        return [$query, $params];
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104 1
    public function countByField($field, $value)
105
    {
106 1
        $query = sprintf('SELECT COUNT(*) as total FROM %s WHERE %s IN (:%s)', $this->getTable(), $field, $field);
107
108
        $params = [
109 1
            $field => implode(',', (array) $value)
110 1
        ];
111
112 1
        return (int) $this->dbal->fetchOne($query, $params)['total'];
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118 1
    public function getByField($field, $value)
119
    {
120 1
        $query = sprintf('SELECT * FROM %s WHERE %s IN (:%s)', $this->getTable(), $field, $field);
121
122
        $params = [
123 1
            $field => implode(',', (array) $value)
124 1
        ];
125
126 1
        return $this->buildCollection($this->dbal->fetchAll($query, $params))
127 1
                    ->setTotal($this->countByField($field, $value));
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133
    public function getRelationshipsFor(Collection $collection, array $relationships = [])
134
    {
135
        $relationsips = new Collection;
0 ignored issues
show
Unused Code introduced by
$relationsips is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
136
        $entities     = [];
137
138
        foreach ($collection->getIterator() as $entity) {
139
            array_walk($entity->getRelationships(), [$this, 'getEntityRelationships'], $entity, $entities);
0 ignored issues
show
Bug introduced by
$entity->getRelationships() cannot be passed to array_walk() as the parameter $array expects a reference.
Loading history...
140
        }
141
142
        foreach ($entities as $entity) {
143
            $relationships->addEntity($entity);
0 ignored issues
show
Bug introduced by
The method addEntity cannot be called on $relationships (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
144
        }
145
146
        return $relationships;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $relationships; (array) is incompatible with the return type declared by the interface Percy\Repository\Reposit...ce::getRelationshipsFor of type Percy\Entity\Collection.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
147
    }
148
149
    /**
150
     * Attach relationships to a specific entity.
151
     *
152
     * @param string                        $entityType
153
     * @param string                        $relationship
154
     * @param \Percy\Entity\EntityInterface $entity
155
     * @param array                         $entities
156
     *
157
     * @return void
158
     */
159
    protected function getEntityRelationships($entityType, $relationship, EntityInterface $entity, array &$entities)
160
    {
161
        $map = $this->getRelationshipMap($relationship);
162
163
        $query = sprintf(
164
            'SELECT * FROM %s LEFT JOIN %s ON %s.%s = %s.%s WHERE %s = :%s',
165
            $map['defined_in']['table'],
166
            $map['target']['table'],
167
            $map['target']['table'],
168
            $map['target']['primary'],
169
            $map['defined_in']['table'],
170
            $map['target']['relationship'],
171
            $map['defined_in']['primary'],
172
            $map['defined_in']['entity']
173
        );
174
175
        $result = $this->dbal->fetchAll($query, [
176
            $map['defined_in']['entity'] => $entity[$map['defined_in']['entity']]
177
        ]);
178
179
        $remove = [$map['defined_in']['primary'], $map['target']['relationship']];
180
181
        foreach ($result as $resource) {
182
            $resource = array_filter($resource, function ($key) use ($remove) {
183
                return (! in_array($key, $remove));
184
            }, ARRAY_FILTER_USE_KEY);
185
186
            $entities[] = (new $entityType)->hydrate($resource);
187
        }
188
    }
189
190
    /**
191
     * Get possible relationships and the properties attached to them.
192
     *
193
     * @param string $relationship
194
     *
195
     * @throws \InvalidArgumentException when requested relationship is not defined
196
     * @throws \RuntimeException when map structure is defined incorrectly
197
     *
198
     * @return array
199
     */
200
    protected function getRelationshipMap($relationship)
201
    {
202
        if (! array_key_exists($relationship, $this->relationships)) {
203
            throw new InvalidArgumentException(
204
                sprintf('(%s) is not defined in the relationship map on (%s)', $relationship, get_class($this))
205
            );
206
        }
207
208
        $map = $this->relationships[$relationship];
209
210
        foreach ([
211
            'defined_in' => ['table', 'primary', 'entity'],
212
            'target'     => ['table', 'primary', 'relationship']
213
        ] as $key => $value) {
214
            if (! array_key_exists($key, $map) || ! is_array($map[$key])) {
215
                throw new RuntimeException(
216
                    sprintf(
217
                        'Relationship (%s) should contain the (%s) key and should be of type array on (%s)',
218
                        $relationship, $key, get_class($this)
219
                    )
220
                );
221
            }
222
223
            if (! empty(array_diff($value, array_keys($map[$key])))) {
224
                throw new RuntimeException(
225
                    sprintf(
226
                        '(%s) for relationship (%s) should contain keys (%s) on (%s)',
227
                        $key, $relationship, implode(', ', $value), get_class($this)
228
                    )
229
                );
230
            }
231
        }
232
233
        return $map;
234
    }
235
236
    /**
237
     * Returns table that repository is reading from.
238
     *
239
     * @return string
240
     */
241
    abstract protected function getTable();
242
}
243