• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

letsdrink / ouzo / 7531968351

15 Jan 2024 05:00PM UTC coverage: 86.005% (+0.002%) from 86.003%
7531968351

push

github

bbankowski
Reverted StreamStub changes. Fixed the problem by not invoking `stream_get_contents` when it is not needed.

9 of 10 new or added lines in 1 file covered. (90.0%)

86 existing lines in 9 files now uncovered.

4984 of 5795 relevant lines covered (86.01%)

101.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.01
/src/Ouzo/Core/Db/ModelQueryBuilder.php
1
<?php
2
/*
3
 * Copyright (c) Ouzo contributors, https://github.com/letsdrink/ouzo
4
 * This file is made available under the MIT License (view the LICENSE file for more information).
5
 */
6

7
namespace Ouzo\Db;
8

9
use Iterator;
10
use Ouzo\Db;
11
use Ouzo\Db\WhereClause\WhereClause;
12
use Ouzo\DbException;
13
use Ouzo\Model;
14
use Ouzo\Utilities\Arrays;
15
use Ouzo\Utilities\Iterator\BatchingIterator;
16
use Ouzo\Utilities\Iterator\TransformingIterator;
17
use Ouzo\Utilities\Iterator\UnbatchingIterator;
18
use PDO;
19

20
class ModelQueryBuilder
21
{
22
    const MODEL_QUERY_MARKER_COMMENT = 'orm:model';
23

24
    private Db $db;
25
    /** @var ModelJoin[] */
26
    private array $joinedModels = [];
27
    /** @var RelationToFetch[] */
28
    private array $relationsToFetch = [];
29
    private Query $query;
30
    private bool $selectModel = true;
31

32
    public function __construct(private Model $model, ?Db $db = null, ?string $alias = null)
33
    {
34
        $this->db = $db ?: Db::getInstance();
436✔
35

36
        $this->query = new Query();
436✔
37
        $this->query->table = $model->getTableName();
436✔
38
        $this->query->aliasTable = $alias;
436✔
39
        $this->query->selectType = PDO::FETCH_NUM;
436✔
40
        $this->query->selectColumns = [];
436✔
41
        $this->selectModelColumns($model, $this->getModelAliasOrTable());
436✔
42
    }
43

44
    private function getModelAliasOrTable(): string
45
    {
46
        return $this->query->aliasTable ?: $this->model->getTableName();
436✔
47
    }
48

49
    private function selectModelColumns(Model $metaInstance, string $alias): void
50
    {
51
        if ($this->selectModel) {
436✔
52
            $this->query->selectColumns = array_merge($this->query->selectColumns, ColumnAliasHandler::createSelectColumnsWithAliases($metaInstance->_getFields(), $alias));
436✔
53
        }
54
    }
55

56
    public function distinctOn(array $columns): static
57
    {
UNCOV
58
        $this->query->distinctOn($columns);
1✔
UNCOV
59
        return $this;
1✔
60
    }
61

62
    public function where(string|array|WhereClause $where = '', mixed $values = []): static
63
    {
64
        $this->query->where($where, $values);
377✔
65
        return $this;
374✔
66
    }
67

68
    public function order(array|string|null $columns): static
69
    {
70
        $this->query->order = $columns;
106✔
71
        return $this;
106✔
72
    }
73

74
    public function offset(?int $offset): static
75
    {
76
        $this->query->offset = $offset;
3✔
77
        return $this;
3✔
78
    }
79

80
    public function limit(?int $limit): static
81
    {
82
        $this->query->limit = $limit;
3✔
83
        return $this;
3✔
84
    }
85

86
    public function lockForUpdate(): static
87
    {
88
        $this->query->lockForUpdate = true;
2✔
89
        return $this;
2✔
90
    }
91

92
    public function count(): int
93
    {
94
        $this->query->type = QueryType::$COUNT;
15✔
95
        $value = (array)QueryExecutor::prepare($this->db, $this->query)->fetch();
15✔
96
        return intval(Arrays::firstOrNull(Arrays::toArray($value)));
15✔
97
    }
98

99
    private function beforeSelect(): void
100
    {
101
        if ($this->selectModel) {
408✔
102
            $this->query->comment(ModelQueryBuilder::MODEL_QUERY_MARKER_COMMENT);
381✔
103
        }
104
    }
105

106
    public function fetch(): Model|array|null
107
    {
108
        $this->beforeSelect();
204✔
109
        $result = QueryExecutor::prepare($this->db, $this->query)->fetch();
204✔
110
        if (!$result) {
204✔
111
            return null;
12✔
112
        }
113
        return !$this->selectModel ? $result : Arrays::firstOrNull($this->processResults([$result]));
192✔
114
    }
115

116
    /** @return Model[] */
117
    public function fetchAll(): array
118
    {
119
        $this->beforeSelect();
234✔
120
        $result = QueryExecutor::prepare($this->db, $this->query)->fetchAll();
234✔
121
        return !$this->selectModel ? $result : $this->processResults($result);
230✔
122
    }
123

124
    public function fetchIterator(int $batchSize = 500): Iterator
125
    {
126
        $this->beforeSelect();
6✔
127
        $iterator = QueryExecutor::prepare($this->db, $this->query)->fetchIterator();
6✔
128
        $iterator->rewind();
6✔
129
        return !$this->selectModel ? $iterator : new UnbatchingIterator(new TransformingIterator(new BatchingIterator($iterator, $batchSize), fn($result) => $this->processResults($result)));
6✔
130
    }
131

132
    /** @return Model[] */
133
    public function processResults(array $results): array
134
    {
135
        $resultSetConverter = new ModelResultSetConverter($this->model, $this->getModelAliasOrTable(), $this->joinedModels, $this->relationsToFetch);
368✔
136
        $converted = $resultSetConverter->convert($results);
368✔
137
        BatchLoadingSession::attach($converted);
368✔
138
        return $converted;
368✔
139
    }
140

141
    /**
142
     * Issues "delete from ... where ..." sql command.
143
     * Note that overridden Model::delete is not called.
144
     * @return int
145
     */
146
    public function deleteAll(): int
147
    {
148
        $this->query->type = QueryType::$DELETE;
11✔
149
        return QueryExecutor::prepare($this->db, $this->query)->execute();
11✔
150
    }
151

152
    /**
153
     * Calls Model::delete method for each matching object
154
     * @return bool[]
155
     */
156
    public function deleteEach(): array
157
    {
158
        $objectIterator = $this->fetchIterator();
3✔
159
        $result = [];
3✔
160
        /** @var Model $object */
161
        foreach ($objectIterator as $object) {
3✔
162
            $result[] = !$object->delete();
3✔
163
        }
164
        return $result;
3✔
165
    }
166

167
    /**
168
     * Runs an update query against a set of models
169
     */
170
    public function update(array $attributes): int
171
    {
172
        $this->query->type = QueryType::$UPDATE;
12✔
173
        $this->query->updateAttributes = $attributes;
12✔
174
        return QueryExecutor::prepare($this->db, $this->query)->execute();
12✔
175
    }
176

177
    public function join(Relation|string $relationSelector, array|string|null $aliases = null, string $type = 'LEFT', array|string|WhereClause $on = []): static
178
    {
179
        $modelJoins = $this->createModelJoins($relationSelector, $aliases, $type, $on);
84✔
180
        foreach ($modelJoins as $modelJoin) {
84✔
181
            $this->addJoin($modelJoin);
84✔
182
        }
183
        return $this;
84✔
184
    }
185

186
    public function innerJoin(Relation|string $relationSelector, array|string|null $aliases = null, array|string|WhereClause $on = []): static
187
    {
188
        return $this->join($relationSelector, $aliases, 'INNER', $on);
18✔
189
    }
190

191
    public function rightJoin(Relation|string $relationSelector, array|string|null $aliases = null, array|string|WhereClause $on = []): static
192
    {
193
        return $this->join($relationSelector, $aliases, 'RIGHT', $on);
3✔
194
    }
195

196
    public function leftJoin(Relation|string $relationSelector, array|string|null $aliases = null, array|string|WhereClause $on = []): static
197
    {
198
        return $this->join($relationSelector, $aliases, 'LEFT', $on);
3✔
199
    }
200

201
    public function using(Relation|string $relationSelector, array|string|null $aliases): static
202
    {
203
        $modelJoins = $this->createModelJoins($relationSelector, $aliases, 'USING', []);
2✔
204
        foreach ($modelJoins as $modelJoin) {
2✔
205
            $this->query->addUsing($modelJoin->asJoinClause());
2✔
206
        }
207
        return $this;
2✔
208
    }
209

210
    private function addJoin(ModelJoin $modelJoin): void
211
    {
212
        if (!$this->isAlreadyJoined($modelJoin)) {
84✔
213
            $this->query->addJoin($modelJoin->asJoinClause());
84✔
214
            $this->joinedModels[] = $modelJoin;
84✔
215
            if ($modelJoin->storeField()) {
84✔
216
                $this->selectModelColumns($modelJoin->getModelObject(), $modelJoin->alias());
69✔
217
            }
218
        }
219
    }
220

221
    private function isAlreadyJoined(ModelJoin $modelJoin): bool
222
    {
223
        return Arrays::any($this->joinedModels, ModelJoin::equalsPredicate($modelJoin));
84✔
224
    }
225

226
    public function with(Relation|string $relationSelector): static
227
    {
228
        if (!BatchLoadingSession::isAllocated()) {
66✔
229
            $relations = ModelQueryBuilderHelper::extractRelations($this->model, $relationSelector, $this->joinedModels);
60✔
230
            $field = '';
60✔
231

232
            foreach ($relations as $relation) {
60✔
233
                $nestedField = $field ? "{$field}->{$relation->getName()}" : $relation->getName();
60✔
234
                $this->addRelationToFetch(new RelationToFetch($field, $relation, $nestedField));
60✔
235
                $field = $nestedField;
60✔
236
            }
237
        }
238
        return $this;
66✔
239
    }
240

241
    private function addRelationToFetch(RelationToFetch $relationToFetch)
242
    {
243
        if (!$this->isAlreadyAddedToFetch($relationToFetch)) {
60✔
244
            $this->relationsToFetch[] = $relationToFetch;
60✔
245
        }
246
    }
247

248
    private function isAlreadyAddedToFetch(RelationToFetch $relationToFetch): bool
249
    {
250
        return Arrays::any($this->relationsToFetch, RelationToFetch::equalsPredicate($relationToFetch));
60✔
251
    }
252

253
    public function select(array|string $columns, int $type = PDO::FETCH_NUM): static
254
    {
255
        $this->selectModel = false;
27✔
256
        $this->query->selectColumns = Arrays::toArray($columns);
27✔
257
        $this->query->selectType = $type;
27✔
258
        return $this;
27✔
259
    }
260

261
    public function selectDistinct(array|string $columns, int $type = PDO::FETCH_NUM): static
262
    {
263
        $this->query->distinct = true;
3✔
264
        return $this->select($columns, $type);
3✔
265
    }
266

267
    public function __clone(): void
268
    {
269
        $this->query = clone $this->query;
3✔
270
    }
271

272
    public function copy(): static
273
    {
274
        return clone $this;
3✔
275
    }
276

277
    public function options(array $options): static
278
    {
279
        $this->query->options = $options;
3✔
280
        return $this;
3✔
281
    }
282

283
    public function groupBy(string|array $groupBy): static
284
    {
285
        if ($this->selectModel) {
9✔
286
            throw new DbException("Cannot use group by without specifying columns.\n"
3✔
287
                . "e.g. Model::select('column, count(*)')->groupBy('column')->fetchAll();");
3✔
288
        }
289

290
        $this->query->groupBy = $groupBy;
6✔
291
        return $this;
6✔
292
    }
293

294
    public function getQuery(): Query
295
    {
296
        return $this->query;
9✔
297
    }
298

299
    /** @return ModelJoin[] */
300
    private function createModelJoins(Relation|string $relationSelector, array|string|null $aliases, string $type, array|string|WhereClause $on): array
301
    {
302
        $relations = ModelQueryBuilderHelper::extractRelations($this->model, $relationSelector, $this->joinedModels);
86✔
303
        $relationWithAliases = ModelQueryBuilderHelper::associateRelationsWithAliases($relations, $aliases);
86✔
304
        return ModelQueryBuilderHelper::createModelJoins($this->getModelAliasOrTable(), $relationWithAliases, $type, $on);
86✔
305
    }
306

307
    public function getModel(): Model
308
    {
309
        return $this->model;
×
310
    }
311
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc