Repository::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Simply\Database;
4
5
use Simply\Database\Connection\Connection;
6
use Simply\Database\Exception\MissingRecordException;
7
8
/**
9
 * Provides functionality to perform basic database operations on models.
10
 * @author Riikka Kalliomäki <[email protected]>
11
 * @copyright Copyright (c) 2018 Riikka Kalliomäki
12
 * @license http://opensource.org/licenses/mit-license.php MIT License
13
 */
14
abstract class Repository
15
{
16
    /** @var Connection The connection to the database */
17
    protected $connection;
18
19
    /**
20
     * Repository constructor.
21
     * @param Connection $connection The connection to the database
22
     */
23 18
    public function __construct(Connection $connection)
24
    {
25 18
        $this->connection = $connection;
26 18
    }
27
28
    /**
29
     * Returns a list of models from the database based on given conditions.
30
     * @param Schema $schema The schema of the model records
31
     * @param array $conditions Conditions for the select query
32
     * @param array $order Associative list of fields and sorting directions
33
     * @param int|null $limit Maximum number of results to return for ordered query
34
     * @return Model[] List of models returned by the database
35
     */
36 10
    protected function find(Schema $schema, array $conditions, array $order = [], int $limit = null): array
37
    {
38 10
        $result = $this->connection->select($schema->getFields(), $schema->getTable(), $conditions, $order, $limit);
39 10
        $result->setFetchMode(\PDO::FETCH_ASSOC);
40
41 10
        $models = [];
42
43 10
        foreach ($result as $row) {
44 9
            $models[] = $schema->createModelFromRow($row);
45
        }
46
47 10
        return $models;
48
    }
49
50
    /**
51
     * Returns a single model based on the given conditions.
52
     * @param Schema $schema The schema of the record to select
53
     * @param array $conditions Conditions for the select query
54
     * @param array $order The order for the select query
55
     * @return Model|null The found model or null if none was found
56
     */
57 1
    protected function findOne(Schema $schema, array $conditions, array $order = []): ?Model
58
    {
59 1
        if ($order === []) {
60 1
            $keys = $schema->getPrimaryKey();
61 1
            $order = array_fill_keys($keys, Connection::ORDER_ASCENDING);
62
        }
63
64 1
        $result = $this->connection->select($schema->getFields(), $schema->getTable(), $conditions, $order, 1);
65 1
        $row = $result->fetch(\PDO::FETCH_ASSOC);
66
67 1
        return $row ? $schema->createModelFromRow($row) : null;
68
    }
69
70
    /**
71
     * Finds a single model based on the primary key.
72
     * @param Schema $schema The schema for the selected record
73
     * @param mixed $values Value for single primary key field or list of values for composite primary key
74
     * @return Model|null The found model or null if none was found
75
     */
76 4
    protected function findByPrimaryKey(Schema $schema, $values): ?Model
77
    {
78 4
        $conditions = $this->getPrimaryKeyCondition($schema, $values);
79
80 2
        $result = $this->connection->select($schema->getFields(), $schema->getTable(), $conditions);
81 2
        $rows = $result->fetchAll(\PDO::FETCH_ASSOC);
82
83 2
        if (\count($rows) > 1) {
84 1
            throw new \UnexpectedValueException('Unexpected number of results returned by primary key');
85
        }
86
87 1
        return $rows ? $schema->createModelFromRow(reset($rows)) : null;
88
    }
89
90
    /**
91
     * Formats the primary key selection condition for the given schema.
92
     * @param Schema $schema The schema for the record
93
     * @param mixed $values Value or list of values for the primary key
94
     * @return array The primary key condition based on the given schema and values
95
     */
96 4
    private function getPrimaryKeyCondition(Schema $schema, $values): array
97
    {
98 4
        $keys = $schema->getPrimaryKey();
99 4
        $condition = [];
100
101 4
        if (!\is_array($values)) {
102 2
            $values = [$values];
103
        }
104
105 4
        if (array_keys($values) === range(0, \count($keys) - 1)) {
106 3
            $values = array_combine($keys, $values);
107
        }
108
109 4
        foreach ($keys as $key) {
110 4
            if (!isset($values[$key])) {
111 1
                throw new \InvalidArgumentException('Missing value for a primary key');
112
            }
113
114 3
            if (\is_array($values[$key])) {
115 1
                throw new \InvalidArgumentException('Invalid value provided for primary key');
116
            }
117
118 3
            $condition[$key] = $values[$key];
119
        }
120
121 2
        return $condition;
122
    }
123
124
    /**
125
     * Inserts or updates the given model based on its state.
126
     * @param Model $model The model to save
127
     */
128 15
    protected function save(Model $model): void
129
    {
130 15
        $record = $model->getDatabaseRecord();
131
132 15
        if ($record->isDeleted()) {
133 1
            throw new \RuntimeException('Tried to save a record that has already been deleted');
134
        }
135
136 15
        if ($record->isNew()) {
137 15
            $this->insert($model);
138 15
            return;
139
        }
140
141 7
        $this->update($model);
142 6
    }
143
144
    /**
145
     * Inserts the given model to the database.
146
     * @param Model $model The model to insert
147
     */
148 15
    protected function insert(Model $model): void
149
    {
150 15
        $record = $model->getDatabaseRecord();
151 15
        $schema = $record->getSchema();
152 15
        $values = array_intersect_key($record->getDatabaseValues(), array_flip($record->getChangedFields()));
153
154 15
        $primaryKeys = $schema->getPrimaryKey();
155
156 15
        if (\count($primaryKeys) === 1) {
157 15
            $primary = reset($primaryKeys);
158
159 15
            if (!isset($values[$primary])) {
160 15
                unset($values[$primary]);
161
162 15
                $this->connection->insert($schema->getTable(), $values, $primary);
163 15
                $record[reset($primaryKeys)] = $primary;
164 15
                $record->updateState(Record::STATE_INSERT);
165 15
                return;
166
            }
167
        }
168
169 3
        $this->connection->insert($schema->getTable(), $values);
170 3
        $record->updateState(Record::STATE_INSERT);
171 3
    }
172
173
    /**
174
     * Updates the field values for the model from the database.
175
     * @param Model $model The model that should be updated
176
     */
177 2
    protected function refresh(Model $model): void
178
    {
179 2
        $record = $model->getDatabaseRecord();
180 2
        $schema = $record->getSchema();
181
182 2
        $result = $this->connection->select($schema->getFields(), $schema->getTable(), $record->getPrimaryKey());
183 2
        $rows = $result->fetchAll(\PDO::FETCH_ASSOC);
184
185 2
        if (\count($rows) !== 1) {
186 1
            throw new MissingRecordException('Tried to refresh a record that does not exist in the database');
187
        }
188
189 1
        $record->setDatabaseValues(reset($rows));
190 1
    }
191
192
    /**
193
     * Updates the model in the database.
194
     * @param Model $model The model to update
195
     */
196 7
    protected function update(Model $model): void
197
    {
198 7
        $record = $model->getDatabaseRecord();
199 7
        $schema = $record->getSchema();
200 7
        $values = array_intersect_key($record->getDatabaseValues(), array_flip($record->getChangedFields()));
201
202 7
        $result = $this->connection->update($schema->getTable(), $values, $record->getPrimaryKey());
203
204 7
        if ($result->rowCount() !== 1) {
205 1
            throw new MissingRecordException('Tried to update a record that does not exist in the database');
206
        }
207
208 6
        $record->updateState(Record::STATE_UPDATE);
209 6
    }
210
211
    /**
212
     * Deletes the model from the database.
213
     * @param Model $model The model to delete
214
     */
215 5
    protected function delete(Model $model): void
216
    {
217 5
        $record = $model->getDatabaseRecord();
218 5
        $schema = $record->getSchema();
219
220 5
        $result = $this->connection->delete($schema->getTable(), $record->getPrimaryKey());
221
222 5
        if ($result->rowCount() !== 1) {
223 1
            throw new MissingRecordException('Tried to delete a record that does not exist in the database');
224
        }
225
226 5
        $record->updateState(Record::STATE_DELETE);
227 5
    }
228
229
    /**
230
     * Fills the given relationships for the given models.
231
     * @param Model[] $models The models to fill with relationships
232
     * @param string[] $relationships List of relationships to fill for the models
233
     */
234 1
    protected function fillRelationships(array $models, array $relationships): void
235
    {
236
        $records = array_map(function (Model $model): Record {
237 1
            return $model->getDatabaseRecord();
238 1
        }, array_values($models));
239
240 1
        $filler = new RelationshipFiller($this->connection);
241 1
        $filler->fill($records, $relationships);
242 1
    }
243
244
    /**
245
     * Returns a custom query object for the given SQL statement.
246
     * @param string $sql The sql to use to initialize the query object
247
     * @return Query The query object initialized with the connection and the given SQL statement
248
     */
249 3
    protected function query(string $sql): Query
250
    {
251 3
        return new Query($this->connection, $sql);
252
    }
253
}
254