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

letsdrink / ouzo / 4784818091

pending completion
4784818091

push

github

Bartosz Bańkowski
Fixed tests.

4833 of 5741 relevant lines covered (84.18%)

33.59 hits per line

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

86.79
/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
use Serializable;
29

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

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

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

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

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

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

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

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

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

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

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

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

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

121
    public function definedAttributes(): array
122
    {
123
        return $this->filterAttributesPreserveNull($this->attributes());
×
124
    }
125

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

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

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

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

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

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

165
        $primaryKey = $this->modelDefinition->primaryKey;
154✔
166
        $attributes = $this->filterAttributesPreserveNull($this->attributes);
154✔
167

168
        $query = $callback($attributes);
154✔
169

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

173
        if ($primaryKey) {
154✔
174
            if ($sequence) {
154✔
175
                $this->$primaryKey = $lastInsertedId;
152✔
176
            } else {
177
                $lastInsertedId = $this->$primaryKey;
4✔
178
            }
179
        }
180

181
        $this->callAfterSaveCallbacks();
154✔
182
        $this->resetModifiedFields();
154✔
183

184
        return $lastInsertedId;
154✔
185
    }
186

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

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

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

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

205
        $this->callAfterSaveCallbacks();
13✔
206
        $this->resetModifiedFields();
13✔
207
    }
208

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

511
    public function serialize(): string
512
    {
513
        return serialize($this->attributes);
2✔
514
    }
515

516
    public function unserialize($serialized): void
517
    {
518
        $result = unserialize($serialized);
2✔
519
        foreach ($result as $key => $value) {
2✔
520
            $this->$key = $value;
2✔
521
        }
522
    }
523

524
    public function jsonSerialize(): string
525
    {
526
        return json_encode($this->attributes);
1✔
527
    }
528

529
    public function getModelDefinition(): ModelDefinition
530
    {
531
        return $this->modelDefinition;
×
532
    }
533
}
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