• 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

87.9
/src/Ouzo/Core/Model.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;
8

9
use Exception;
10
use InvalidArgumentException;
11
use JsonSerializable;
12
use Ouzo\Db\BatchLoadingSession;
13
use Ouzo\Db\ModelDefinition;
14
use Ouzo\Db\ModelQueryBuilder;
15
use Ouzo\Db\Query;
16
use Ouzo\Db\QueryExecutor;
17
use Ouzo\Db\Relation;
18
use Ouzo\Db\RelationFetcher;
19
use Ouzo\Db\WhereClause\WhereClause;
20
use Ouzo\Exception\ValidationException;
21
use Ouzo\Restriction\Restriction;
22
use Ouzo\Utilities\Arrays;
23
use Ouzo\Utilities\Functions;
24
use Ouzo\Utilities\Objects;
25
use Ouzo\Utilities\Strings;
26
use PDO;
27
use ReflectionClass;
28

29
class Model extends Validatable implements JsonSerializable
30
{
31
    private ModelDefinition $modelDefinition;
32
    private array $attributes;
33
    private array $modifiedFields;
34

35
    /**
36
     * Creates a new model object.
37
     * Accepted parameters:
38
     * @param array $params {
39
     * @var string $table defaults to pluralized class name. E.g. customer_orders for CustomerOrder
40
     * @var string $primaryKey defaults to id
41
     * @var string $sequence defaults to table_primaryKey_seq
42
     * @var string $hasMany specification of a has-many relation e.g. array('name' => array('class' => 'Class', 'foreignKey' => 'foreignKey'))
43
     * @var string $hasOne specification of a has-one relation e.g. array('name' => array('class' => 'Class', 'foreignKey' => 'foreignKey'))
44
     * @var string $belongsTo specification of a belongs-to relation e.g. array('name' => array('class' => 'Class', 'foreignKey' => 'foreignKey'))
45
     * @var string $fields mapped column names
46
     * @var string $attributes array of column => value
47
     * @var string $beforeSave function to invoke before insert or update
48
     * @var string $afterSave function to invoke after insert or update
49
     * }
50
     */
51
    public function __construct(array $params)
52
    {
53
        $this->prepareParameters($params);
708✔
54

55
        $this->modelDefinition = ModelDefinition::get(get_called_class(), $params);
702✔
56
        $primaryKeyName = $this->modelDefinition->primaryKey;
702✔
57
        $attributes = $this->modelDefinition->mergeWithDefaults($params['attributes'], $params['fields']);
702✔
58

59
        if (isset($attributes[$primaryKeyName]) && Strings::isBlank($attributes[$primaryKeyName])) {
702✔
60
            unset($attributes[$primaryKeyName]);
×
61
        }
62
        $this->attributes = $this->filterAttributes($attributes);
702✔
63
        $this->modifiedFields = array_keys($this->attributes);
702✔
64
    }
65

66
    public function __set(string $name, mixed $value): void
67
    {
68
        $this->modifiedFields[] = $name;
480✔
69
        $this->attributes[$name] = $value;
480✔
70
    }
71

72
    public function __get(string $name): mixed
73
    {
74
        if (empty($name)) {
609✔
75
            throw new Exception('Illegal attribute: field name for Model cannot be empty');
×
76
        }
77
        if (array_key_exists($name, $this->attributes)) {
609✔
78
            return $this->attributes[$name];
570✔
79
        }
80

81
        if ($this->hasRelation($name)) {
486✔
82
            $this->fetchRelation($name);
43✔
83
            return $this->attributes[$name];
43✔
84
        }
85
        return null;
486✔
86
    }
87

88
    public function __isset(string $name): bool
89
    {
90
        return $this->__get($name) !== null;
218✔
91
    }
92

93
    public function __unset(string $name): void
94
    {
95
        unset($this->attributes[$name]);
3✔
96
    }
97

98
    public function assignAttributes(array $attributes): void
99
    {
100
        unset($attributes[$this->modelDefinition->primaryKey]);
27✔
101
        $this->modifiedFields = array_merge($this->modifiedFields, array_keys($attributes));
27✔
102
        $this->attributes = array_merge($this->attributes, $this->filterAttributesPreserveNull($attributes));
27✔
103
    }
104

105
    private function filterAttributesPreserveNull(array $data): array
106
    {
107
        return array_intersect_key($data, array_flip($this->modelDefinition->fields));
702✔
108
    }
109

110
    private function filterAttributes(array $data): array
111
    {
112
        return Arrays::filter($this->filterAttributesPreserveNull($data), Functions::notNull());
702✔
113
    }
114

115
    public function attributes(): array
116
    {
117
        return array_replace(array_fill_keys($this->modelDefinition->fields, null), $this->attributes);
130✔
118
    }
119

120
    public function definedAttributes(): array
121
    {
UNCOV
122
        return $this->filterAttributesPreserveNull($this->attributes());
5✔
123
    }
124

125
    private function prepareParameters(array &$params): void
126
    {
127
        if (empty($params['attributes'])) {
708✔
128
            $params['attributes'] = [];
139✔
129
        }
130
        if (empty($params['fields'])) {
708✔
131
            throw new InvalidArgumentException('Fields are required');
6✔
132
        }
133
    }
134

135
    public function getTableName(): string
136
    {
137
        return $this->modelDefinition->table;
445✔
138
    }
139

140
    public function insert(): string|int|null
141
    {
142
        return $this->doInsert(fn($attributes) => Query::insert($attributes)->into($this->modelDefinition->table));
457✔
143
    }
144

145
    public function insertOrDoNothing(): ?int
146
    {
UNCOV
147
        return $this->doInsert(fn($attributes) => Query::insertOrDoNoting($attributes)->into($this->modelDefinition->table));
1✔
148
    }
149

150
    public function upsert(array $upsertConflictColumns = []): ?int
151
    {
152
        return $this->doInsert(function ($attributes) use ($upsertConflictColumns) {
8✔
153
            if (empty($upsertConflictColumns)) {
8✔
154
                $upsertConflictColumns = [$this->getIdName()];
8✔
155
            }
156
            return Query::upsert($attributes)->onConflict($upsertConflictColumns)->table($this->modelDefinition->table);
8✔
157
        });
8✔
158
    }
159

160
    private function doInsert($callback): string|int|null
161
    {
162
        $this->callBeforeSaveCallbacks();
461✔
163

164
        $primaryKey = $this->modelDefinition->primaryKey;
461✔
165
        $attributes = $this->filterAttributesPreserveNull($this->attributes);
461✔
166

167
        $query = $callback($attributes);
461✔
168

169
        $sequence = $primaryKey && $this->$primaryKey !== null ? null : $this->modelDefinition->sequence;
461✔
170
        $lastInsertedId = QueryExecutor::prepare($this->modelDefinition->db, $query)->insert($sequence);
461✔
171

172
        if ($primaryKey) {
460✔
173
            if ($sequence) {
460✔
174
                $this->$primaryKey = $lastInsertedId;
454✔
175
            } else {
176
                $lastInsertedId = $this->$primaryKey;
10✔
177
            }
178
        }
179

180
        $this->callAfterSaveCallbacks();
460✔
181
        $this->resetModifiedFields();
460✔
182

183
        return $lastInsertedId;
460✔
184
    }
185

186
    public function callAfterSaveCallbacks(): void
187
    {
188
        $this->callCallbacks($this->modelDefinition->afterSaveCallbacks);
462✔
189
    }
190

191
    public function update(): void
192
    {
193
        $this->callBeforeSaveCallbacks();
39✔
194

195
        $attributes = $this->getAttributesForUpdate();
39✔
196
        if ($attributes) {
39✔
197
            $query = Query::update($attributes)
36✔
198
                ->table($this->modelDefinition->table)
36✔
199
                ->where([$this->modelDefinition->primaryKey => $this->getId()]);
36✔
200

201
            QueryExecutor::prepare($this->modelDefinition->db, $query)->execute();
36✔
202
        }
203

204
        $this->callAfterSaveCallbacks();
39✔
205
        $this->resetModifiedFields();
39✔
206
    }
207

208
    public function callBeforeSaveCallbacks(): void
209
    {
210
        $this->callCallbacks($this->modelDefinition->beforeSaveCallbacks);
466✔
211
    }
212

213
    private function callCallbacks($callbacks): void
214
    {
215
        foreach ($callbacks as $callback) {
466✔
216
            if (is_string($callback)) {
15✔
217
                $callback = [$this, $callback];
3✔
218
            }
219
            call_user_func($callback, $this);
15✔
220
        }
221
    }
222

223
    public function insertOrUpdate(): void
224
    {
225
        $this->isNew() ? $this->insert() : $this->update();
×
226
    }
227

228
    public function isNew(): bool
229
    {
230
        return is_null($this->getId());
×
231
    }
232

233
    public function updateAttributes(array $attributes)
234
    {
235
        if (!$this->updateAttributesIfValid($attributes)) {
12✔
236
            throw new ValidationException($this->getErrorObjects());
3✔
237
        }
238
    }
239

240
    public function updateAttributesIfValid(array $attributes): bool
241
    {
242
        $this->assignAttributes($attributes);
15✔
243
        if ($this->isValid()) {
15✔
244
            $this->update();
12✔
245
            return true;
12✔
246
        }
247
        return false;
3✔
248
    }
249

250
    public function delete(): bool
251
    {
252
        return (bool)$this->where([$this->modelDefinition->primaryKey => $this->getId()])->deleteAll();
6✔
253
    }
254

255
    public function getId(): int|string|null
256
    {
257
        $primaryKeyName = $this->modelDefinition->primaryKey;
244✔
258
        return $this->$primaryKeyName;
244✔
259
    }
260

261
    public function getIdName(): string
262
    {
263
        return $this->modelDefinition->primaryKey;
185✔
264
    }
265

266
    public function getSequenceName(): string
267
    {
268
        return $this->modelDefinition->sequence;
15✔
269
    }
270

271
    /**
272
     * Returns model object as a nicely formatted string.
273
     */
274
    public function inspect(): string
275
    {
276
        return get_called_class() . Objects::toString(Arrays::filter($this->attributes, Functions::notNull()));
126✔
277
    }
278

279
    public function getModelName(): string
280
    {
281
        $function = new ReflectionClass($this);
83✔
282
        return $function->getShortName();
83✔
283
    }
284

285
    public static function getFields(): array
286
    {
287
        return static::metaInstance()->_getFields();
380✔
288
    }
289

290
    public function _getFields(): array
291
    {
292
        return $this->modelDefinition->fields;
451✔
293
    }
294

295
    public static function getFieldsWithoutPrimaryKey(): array
296
    {
297
        return static::metaInstance()->_getFieldsWithoutPrimaryKey();
8✔
298
    }
299

300
    private function _getFieldsWithoutPrimaryKey(): array
301
    {
302
        return array_diff($this->modelDefinition->fields, [$this->modelDefinition->primaryKey]);
8✔
303
    }
304

305
    private function fetchRelation(string $name): void
306
    {
307
        $relation = $this->getRelation($name);
43✔
308
        $relationFetcher = new RelationFetcher($relation);
43✔
309
        $results = BatchLoadingSession::getBatch($this);
43✔
310
        $relationFetcher->transform($results);
43✔
311
    }
312

313
    public static function newInstance(array $attributes): static
314
    {
315
        $className = get_called_class();
460✔
316
        /** @var Model $object */
317
        $object = new $className($attributes);
460✔
318
        $object->resetModifiedFields();
460✔
319
        return $object;
460✔
320
    }
321

322
    public static function metaInstance(): static
323
    {
324
        return MetaModelCache::getMetaInstance(get_called_class());
466✔
325
    }
326

327
    /**
328
     * @return static[]
329
     */
330
    public static function all(): array
331
    {
332
        return static::queryBuilder()->fetchAll();
15✔
333
    }
334

335
    public static function select(array|string $columns, int $type = PDO::FETCH_NUM): ModelQueryBuilder
336
    {
337
        return static::queryBuilder()->select($columns, $type);
24✔
338
    }
339

340
    public static function selectDistinct(array|string $columns, int $type = PDO::FETCH_NUM): ModelQueryBuilder
341
    {
342
        return static::queryBuilder()->selectDistinct($columns, $type);
3✔
343
    }
344

345
    public static function join(Relation|string $relation, null|array|string $alias = null, string $type = 'LEFT', array|string|WhereClause $on = []): ModelQueryBuilder
346
    {
347
        return static::queryBuilder()->join($relation, $alias, $type, $on);
45✔
348
    }
349

350
    public static function innerJoin(Relation|string $relation, null|array|string $alias = null, array|string $on = []): ModelQueryBuilder
351
    {
352
        return static::queryBuilder()->innerJoin($relation, $alias, $on);
15✔
353
    }
354

355
    public static function rightJoin(Relation|string $relation, null|array|string $alias = null, array|string $on = []): ModelQueryBuilder
356
    {
357
        return static::queryBuilder()->rightJoin($relation, $alias, $on);
×
358
    }
359

360
    public static function using(string $relation, null|array|string $alias = null): ModelQueryBuilder
361
    {
362
        return static::queryBuilder()->using($relation, $alias);
2✔
363
    }
364

365
    public static function where(null|string|array|WhereClause $params = '', null|string|array|Restriction $values = []): ModelQueryBuilder
366
    {
367
        return static::queryBuilder()->where($params, $values);
327✔
368
    }
369

370
    public static function queryBuilder(?string $alias = null): ModelQueryBuilder
371
    {
372
        $obj = static::metaInstance();
430✔
373
        return new ModelQueryBuilder($obj, $obj->modelDefinition->db, $alias);
430✔
374
    }
375

376
    public static function count(string|array $where = '', ?array $bindValues = null): int
377
    {
378
        return static::metaInstance()->where($where, $bindValues)->count();
3✔
379
    }
380

381
    public static function alias(string $alias): ModelQueryBuilder
382
    {
383
        return static::queryBuilder($alias);
15✔
384
    }
385

386
    /** @return static[] */
387
    public static function find(array|string $where, array|string $whereValues, array $orderBy = [], ?int $limit = null, ?int $offset = null): array
388
    {
389
        return static::metaInstance()
×
390
            ->where($where, $whereValues)
×
391
            ->order($orderBy)
×
392
            ->limit($limit)
×
393
            ->offset($offset)
×
394
            ->fetchAll();
×
395
    }
396

397
    /**
398
     * Executes a native sql and returns an array of model objects created by passing every result row to the model constructor.
399
     * @return static[]
400
     */
401
    public static function findBySql(string $nativeSql, null|string|array $params = []): array
402
    {
403
        $meta = static::metaInstance();
9✔
404
        $results = $meta->modelDefinition->db->query($nativeSql, Arrays::toArray($params))->fetchAll();
9✔
405

406
        return Arrays::map($results, fn($row) => $meta->newInstance($row));
9✔
407
    }
408

409
    public static function findById(?int $value): static
410
    {
411
        return static::metaInstance()->internalFindById($value);
76✔
412
    }
413

414
    private function internalFindById(?int $value): static
415
    {
416
        if (!$this->modelDefinition->primaryKey) {
76✔
417
            throw new DbException("Primary key is not defined for table {$this->modelDefinition->table}");
3✔
418
        }
419
        $result = $this->internalFindByIdOrNull($value);
73✔
420
        if ($result) {
73✔
421
            return $result;
70✔
422
        }
423
        throw new DbException("{$this->modelDefinition->table} with {$this->modelDefinition->primaryKey}={$value} not found");
3✔
424
    }
425

426
    public static function findByIdOrNull(?int $value): ?static
427
    {
428
        return static::metaInstance()->internalFindByIdOrNull($value);
6✔
429
    }
430

431
    private function internalFindByIdOrNull(?int $value): ?static
432
    {
433
        return $this->where([$this->modelDefinition->primaryKey => $value])->fetch();
76✔
434
    }
435

436
    public static function create(array $attributes = []): static
437
    {
438
        $instance = static::newInstance($attributes);
433✔
439
        if ($instance->isValid()) {
433✔
440
            $instance->insert();
430✔
441
            return $instance;
430✔
442
        }
443
        throw new ValidationException($instance->getErrorObjects());
3✔
444
    }
445

446
    public static function createOrUpdate(array $attributes = [], array $upsertConflictColumns = []): static
447
    {
448
        $instance = static::newInstance($attributes);
4✔
449
        if ($instance->isValid()) {
4✔
450
            $instance->upsert($upsertConflictColumns);
4✔
451
            return $instance;
4✔
452
        }
453
        throw new ValidationException($instance->getErrorObjects());
×
454
    }
455

456
    public static function createWithoutValidation(array $attributes = []): static
457
    {
458
        $instance = static::newInstance($attributes);
×
459
        $instance->insert();
×
460
        return $instance;
×
461
    }
462

463
    public function reload(): static
464
    {
465
        $this->attributes = $this->findById($this->getId())->attributes;
20✔
466
        $this->resetModifiedFields();
20✔
467
        return $this;
20✔
468
    }
469

470
    public function nullifyIfEmpty(string...$fields): void
471
    {
472
        foreach ($fields as $field) {
×
473
            if (isset($this->$field) && !is_bool($this->$field) && Strings::isBlank($this->$field)) {
×
474
                $this->$field = null;
×
475
            }
476
        }
477
    }
478

479
    public function get(string $names, mixed $default = null): mixed
480
    {
481
        return Objects::getValueRecursively($this, $names, $default);
18✔
482
    }
483

484
    public function hasRelation(string $name): bool
485
    {
486
        return $this->modelDefinition->relations->hasRelation($name);
489✔
487
    }
488

489
    public function getRelation(string $name): Relation
490
    {
491
        return $this->modelDefinition->relations->getRelation($name);
186✔
492
    }
493

494
    public function __toString(): string
495
    {
496
        return $this->inspect();
123✔
497
    }
498

499
    public function resetModifiedFields(): void
500
    {
501
        $this->modifiedFields = [];
473✔
502
    }
503

504
    private function getAttributesForUpdate(): array
505
    {
506
        $attributes = $this->filterAttributesPreserveNull($this->attributes);
39✔
507
        return array_intersect_key($attributes, array_flip($this->modifiedFields));
39✔
508
    }
509

510
    public function __serialize(): array
511
    {
512
        return $this->attributes;
6✔
513
    }
514

515
    public function __unserialize(array $serialized): void
516
    {
517
        $this->attributes = $serialized;
6✔
518
    }
519

520
    public function jsonSerialize(): string
521
    {
522
        return json_encode($this->attributes);
3✔
523
    }
524

525
    public function getModelDefinition(): ModelDefinition
526
    {
527
        return $this->modelDefinition;
×
528
    }
529
}
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

© 2026 Coveralls, Inc