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

varhall / dbino / 16010703990

01 Jul 2025 09:37PM UTC coverage: 91.069% (+0.2%) from 90.822%
16010703990

push

github

feropeterko
array access

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

9 existing lines in 1 file now uncovered.

673 of 739 relevant lines covered (91.07%)

0.91 hits per line

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

91.16
/src/Model.php
1
<?php
2

3
namespace Varhall\Dbino;
4

5
use Nette\Database\Table\ActiveRow;
6
use Varhall\Dbino\Casts\AttributeCast;
7
use Varhall\Dbino\Collections\Collection;
8
use Varhall\Dbino\Collections\GroupedCollection;
9
use Varhall\Dbino\Collections\ManyToManySelection;
10
use Varhall\Dbino\Events\DeleteArgs;
11
use Varhall\Dbino\Events\InsertArgs;
12
use Varhall\Dbino\Events\UpdateArgs;
13
use Varhall\Dbino\Scopes\Scope;
14
use Varhall\Dbino\Traits\Events;
15
use Varhall\Dbino\Traits\SoftDeletes;
16
use Varhall\Dbino\Traits\Timestamps;
17
use Varhall\Utilino\Collections\ICollection;
18
use Varhall\Utilino\ISerializable;
19
use Varhall\Utilino\Utils\Reflection;
20

21
/**
22
 * Base database model class
23
 *
24
 * @method static Collection all()
25
 * @method static Collection where($condition, ...$parameters)
26
 * @method static $this find($id)
27
 * @method static $this findOrDefault($id, array $data = [])
28
 * @method static $this findOrFail($id)
29
 * @method static instance(array $data = [])
30
 * @method static create(array $data = [])
31
 * @method static array columns()
32
 * @method static Collection withTrashed()
33
 * @method static Collection onlyTrashed()
34
 */
35
abstract class Model implements ISerializable, \ArrayAccess
36
{
37
    use Events {
38
        Events::raise as private raise_Events;
39
    }
40

41
    private Dbino $dbino;
42

43
    private ?ActiveRow $row;
44

45
    protected array $attributes     = [];
46

47
    protected $casts                = [];
48

49
    protected $scopes               = [];
50

51

52
    ////////////////////////////////////////////////////////////////////////////
53
    /// MAGIC METHODS                                                        ///
54
    ////////////////////////////////////////////////////////////////////////////
55

56
    public function __construct(Dbino $dbino, ?ActiveRow $row = null)
1✔
57
    {
58
        $this->dbino = $dbino;
1✔
59
        $this->row = $row;
1✔
60

61
        $this->on('creating', function($args) { $this->raise('saving', $args); });
1✔
62
        $this->on('updating', function($args) { $this->raise('saving', $args); });
1✔
63

64
        $this->on('created', function($args) { $this->raise('saved', $args); });
1✔
65
        $this->on('updated', function($args) { $this->raise('saved', $args); });
1✔
66

67
        $this->initializeTraits();
1✔
68
        $this->setup();
1✔
69
    }
1✔
70

71
    public function __isset(string $name): bool
1✔
72
    {
73
        if (isset($this->attributes[$name])) {
1✔
74
            return true;
1✔
75
        }
76

77
        if (!$this->isSaved()) {
1✔
78
            return false;
1✔
79
        }
80

81
        return isset($this->row[$name]);
1✔
82
    }
83

84
    public function &__get(string $key): mixed
1✔
85
    {
86
        // call method with attribute name if exists
87
        if (method_exists($this, $key)) {
1✔
88
            $value = call_user_func([$this, $key]);
1✔
89
            return $value;
1✔
90
        }
91

92
        $camelKey = $this->toCamelCase($key);
1✔
93

94
        // call camel case method with attribute name if exists
95
        if (method_exists($this, $camelKey)) {
1✔
96
            $value = call_user_func([$this, $camelKey]);
×
97
            return $value;
×
98
        }
99

100
        $value = $this->getAttribute($key);
1✔
101

102
        // call mutator getter if exists
103
        $mutator = 'get' . ucfirst($key) . 'Attribute';
1✔
104
        if (method_exists($this, $mutator)) {
1✔
105
            $value = call_user_func([$this, $mutator], $value);
×
106
            return $value;
×
107
        }
108

109
        return $value;
1✔
110
    }
111

112
    public function __set(string $name, mixed $value): void
1✔
113
    {
114
        $camelName = $this->toCamelCase($name);
1✔
115

116
        // call mutator getter if exists
117
        $mutator = 'set' . ucfirst($camelName) . 'Attribute';
1✔
118
        if (method_exists($this, $mutator)) {
1✔
119
            call_user_func([$this, $mutator], $value);
×
120

121
            // set value
122
        } else {
123
            $this->setAttribute($name, $value);
1✔
124
        }
125
    }
1✔
126

127
    public static function __callStatic(string $name, array $arguments): mixed
1✔
128
    {
129
        $repository = static::configuration()->getRepository();
1✔
130

131
        if (!method_exists($repository, $name)) {
1✔
132
            throw new \Nette\MemberAccessException("Method {$name} not found in class " . get_class($repository));
1✔
133
        }
134

135
        return call_user_func_array([ $repository, $name ], $arguments);
1✔
136
    }
137

138

139
    public function offsetGet(mixed $offset): mixed
1✔
140
    {
141
        return $this->__get($offset);
1✔
142
    }
143

144
    public function offsetSet(mixed $offset, mixed $value): void
1✔
145
    {
146
        $this->__set($offset, $value);
1✔
147
    }
1✔
148

149
    public function offsetExists(mixed $offset): bool
1✔
150
    {
151
        return $this->__isset($offset);
1✔
152
    }
153

154
    public function offsetUnset(mixed $offset): void
1✔
155
    {
156
        throw new \Nette\NotSupportedException('Unsetting attributes is not supported');
1✔
157
    }
158

159

160
    ////////////////////////////////////////////////////////////////////////////
161
    /// STATIC METHODS                                                       ///
162
    ////////////////////////////////////////////////////////////////////////////
163
    
164
    public static function configuration(): Configuration
165
    {
166
        return (new static(Dbino::instance()))->getConfiguration();
1✔
167
    }
168

169

170
    ////////////////////////////////////////////////////////////////////////////
171
    /// CONFIG METHODS                                                       ///
172
    ////////////////////////////////////////////////////////////////////////////
173

174
    protected abstract function table();
175

176
    protected function repository(): string
177
    {
178
        return Repository::class;
1✔
179
    }
180

181
    protected function setup()
182
    {
183

184
    }
1✔
185

186
    protected function defaults()
187
    {
188
        return [];
1✔
189
    }
190

191
    protected function hiddenAttributes()
192
    {
193
        return [];
1✔
194
    }
195

196
    protected function serializationDateFormat()
197
    {
198
        return 'c';
1✔
199
    }
200

201
    public function addScope(Scope $scope, ?string $name = null): void
1✔
202
    {
203
        $this->scopes[$name ?? get_class($scope)] = $scope;
1✔
204
    }
1✔
205

206
    public function removeScope(string $name): void
207
    {
208
        unset($this->scopes[$name]);
×
209
    }
210

211

212
    ////////////////////////////////////////////////////////////////////////////
213
    /// PUBLIC INSTANCE METHODS                                              ///
214
    ////////////////////////////////////////////////////////////////////////////
215

216
    public function fill(array $data): static
1✔
217
    {
218
        $this->setAttributes($data);
1✔
219

220
        return $this;
1✔
221
    }
222

223
    public function isSaved(): bool
224
    {
225
        return !$this->isNew() && empty($this->attributes);
1✔
226
    }
227

228
    public function isNew(): bool
229
    {
230
        return !$this->row;
1✔
231
    }
232

233
    public function save(): static
234
    {
235
        if (empty($this->attributes)) {
1✔
236
            return $this;
1✔
237
        }
238

239
        if ($this->isNew()) {
1✔
240
            $this->insertInstance();
1✔
241

242
        } else {
243
            $this->updateInstance();
1✔
244
        }
245

246
        $this->attributes = [];
1✔
247

248
        return $this;
1✔
249
    }
250

251
    public function update(iterable $data): bool
1✔
252
    {
253
        $this->setAttributes($data);
1✔
254

255
        return $this->updateInstance();
1✔
256
    }
257

258
    public function delete(): int
259
    {
260
        if ($this->isNew()) {
1✔
261
            throw new \Nette\InvalidStateException('Cannot delete unsaved model');
1✔
262
        }
263

264
        $args = new DeleteArgs([
1✔
265
            'id'        => $this->row->getPrimary(),
1✔
266
            'instance'  => $this,
1✔
267
            'soft'      => false
268
        ]);
269

270
        $this->raise('deleting', $args);
1✔
271
        $result = $this->row->delete();
1✔
272
        $this->raise('deleted', $args);
1✔
273

274
        return $result;
1✔
275
    }
276

277
    public function duplicate(array $values = [], array $except = []): static
1✔
278
    {
279
        if ($this->isNew()) {
1✔
280
            throw new \Nette\InvalidStateException('Cannot duplicate unsaved model');
1✔
281
        }
282

283
        $except = array_merge(
1✔
284
            Reflection::hasTrait($this, Timestamps::class) ? array_values($this->timestampsColumns()) : [],
1✔
285
            Reflection::hasTrait($this, SoftDeletes::class) ? (array) $this->softDeleteColumn : [],
1✔
286
            (array) $this->row->getTable()->getPrimary(false),
1✔
287
            $except
288
        );
289

290

291
        $data = $this->toNativeArray();
1✔
292

293
        foreach ($except as $property) {
1✔
294
            if (isset($data[$property])) {
1✔
295
                unset($data[$property]);
1✔
296
            }
297
        }
298

299
        return static::create(array_merge($data, $values));
1✔
300
    }
301

302
    public function getPrimary(): mixed
303
    {
304
        return $this->row ? $this->row->getPrimary() : null;
1✔
305
    }
306

307
    public function toArray(): array
308
    {
309
        $values = $this->toNativeArray();
1✔
310

311
        foreach ($values as $key => $value) {
1✔
312
            $values[$key] = $this->readField($key, $value);
1✔
313

314
            if ($values[$key] instanceof \Varhall\Utilino\ISerializable) {
1✔
315
                $values[$key] = $values[$key]->toArray();
1✔
316
            }
317

318
            if ($values[$key] instanceof \DateTime) {
1✔
319
                $values[$key] = $values[$key]->format($this->serializationDateFormat());
1✔
320
            }
321

322
            if (in_array($key, $this->hiddenAttributes())) {
1✔
323
                unset($values[$key]);
1✔
324
            }
325
        }
326

327
        return $values;
1✔
328
    }
329

330
    public function toJson()
331
    {
332
        return \Nette\Utils\Json::encode($this->toArray());
1✔
333
    }
334

335

336
    ////////////////////////////////////////////////////////////////////////////
337
    /// PRIVATE & PROTECTED METHODS                                          ///
338
    ////////////////////////////////////////////////////////////////////////////
339

340
    protected function insertInstance(): void
341
    {
342
        // raise events
343
        $this->raise('creating', new InsertArgs([
1✔
344
            'data' => $this->attributes,
1✔
345
            'instance' => $this
1✔
346
        ]));
347

348
        // insert
349
        $model = $this->getConfiguration()->getRepository()->all()->insert($this->prepareDbData($this->attributes));
1✔
350

351
        if ($model instanceof Model) {
1✔
352
            $this->row = $model->row;
1✔
353
        }
354

355
        // raise events
356
        $this->raise('created', new InsertArgs([
1✔
357
            'data'      => $this->attributes,
1✔
358
            'instance'  => $this,
1✔
359
            'id'        => $this->row ? $this->row->getPrimary() : null
1✔
360
        ]));
361
    }
1✔
362

363
    protected function updateInstance()
364
    {
365
        // prcompute data
366
        $originals = $this->row->toArray();
1✔
367
        $diff = array_filter($this->attributes, function($value, $key) use ($originals) {
1✔
368
            return $value != $originals[$key];
1✔
369
        }, ARRAY_FILTER_USE_BOTH);
1✔
370

371
        // raise events
372
        $args = new UpdateArgs([
1✔
373
            'data'      => $this->toNativeArray(),
1✔
374
            'instance'  => $this,
1✔
375
            'id'        => $this->row->getPrimary(),
1✔
376
            'diff'      => $diff
1✔
377
        ]);
378

379
        $this->raise('updating', $args);
1✔
380

381
        // update
382
        $result = $this->row->update($this->prepareDbData($this->attributes));
1✔
383

384
        // raise events
385
        $this->raise('updated', $args);
1✔
386

387
        return $result;
1✔
388
    }
389

390
    protected function hasMany(string $class, ?string $throughColumn = null): ICollection
1✔
391
    {
392
        $configuration = $class::configuration();
1✔
393
        $related = $this->row->related($configuration->table, $throughColumn);
1✔
394

395
        return new GroupedCollection($related, $configuration);
1✔
396
    }
397

398
    protected function hasOne(string $class, ?string $throughColumn = null): Model
399
    {
UNCOV
400
        return $this->hasMany($class, $throughColumn)->first();
×
401
    }
402

403
    protected function belongsTo(string $class, ?string $throughColumn = null): ?Model
1✔
404
    {
405
        if ($this->isNew()) {
1✔
406
            return $this->dbino->repository($class)->all()->get($this->$throughColumn);
1✔
407
        }
408

409
        $table = $class::configuration()->table;
1✔
410
        $ref = $this->row->ref($table, $throughColumn);
1✔
411

412
        if (!$ref) {
1✔
UNCOV
413
            return null;
×
414
        }
415

416
        return new $class($this->dbino, $ref);
1✔
417
    }
418

419
    /**
420
     * Maps many to many relationship. Example reference from Student (current) to Course (related):
421
     *
422
     * @param string $class Target class Course::class
423
     * @param string $intermediateTable Intermediate table student_courses
424
     * @param string $foreignColumn Column in intermediate table pointing to current class (student_id)
425
     * @param string $referenceColumn Column in intermediate table pointing to related class (course_id)
426
     */
427
    protected function belongsToMany(string $class, string $intermediateTable, string $foreignColumn, string $referenceColumn): ICollection
1✔
428
    {
429
        $configuration = $class::configuration();
1✔
430

431
        $intermediate = $this->row->related($intermediateTable, $foreignColumn);
1✔
432
        $result = new ManyToManySelection($intermediate, $intermediateTable, $configuration->table, $foreignColumn, $referenceColumn, $this->row->getPrimary());
1✔
433

434
        return new GroupedCollection($result, $configuration);
1✔
435
    }
436

437
    protected function addCast(string $field, AttributeCast $cast): void
438
    {
UNCOV
439
        $this->casts[$field] = $cast;
×
440
    }
441

442
    protected function initializeTraits(): void
443
    {
444
        $class = new \ReflectionClass($this);
1✔
445

446
        foreach ($class->getTraits() as $trait) {
1✔
447
            $method = 'initialize' . $trait->getShortName();
1✔
448

449
            if (method_exists($this, $method)) {
1✔
450
                call_user_func([ $this, $method ]);
1✔
451
            }
452
        }
453
    }
1✔
454

455
    protected function getAttribute(string $key): mixed
1✔
456
    {
457
        // get unsaved value if set
458
        if (array_key_exists($key, $this->attributes)) {
1✔
459
            return $this->readField($key, $this->attributes[$key]);
1✔
460
        }
461

462
        // call parent getter
463
        $value = null;
1✔
464

465
        try {
466
            if (!$this->isNew()) {
1✔
467
                $value = $this->row->$key;
1✔
468
            }
469

470
        } catch (\Nette\MemberAccessException $ex) {
×
UNCOV
471
            $this->row->accessColumn(null);
×
UNCOV
472
            $value = $this->row->$key;
×
473
        }
474

475
        return $this->readField($key, $value);
1✔
476
    }
477

478
    protected function setAttribute(string $name, mixed $value): void
1✔
479
    {
480
        $this->attributes[$name] = $value;
1✔
481
    }
1✔
482

483
    protected function setAttributes(iterable $data): void
1✔
484
    {
485
        foreach ($data as $key => $value) {
1✔
486
            $this->__set($key, $value);
1✔
487
        }
488
    }
1✔
489

490
    protected function toNativeArray(): array
491
    {
492
        $values = array_merge(!$this->isNew() ? $this->row->toArray() : [], $this->attributes);
1✔
493

494
        if (!$values || !is_array($values)) {
1✔
UNCOV
495
            throw new \Nette\InvalidStateException('Unable convert row to array');
×
496
        }
497

498
        return $values;
1✔
499
    }
500

501
    protected function readField(string $key, mixed $value): mixed
1✔
502
    {
503
        $cast = $this->getCast($key);
1✔
504

505
        $defaults = array_key_exists($key, $this->defaults()) ? $this->defaults()[$key] : null;
1✔
506
        $value = $value ?? $defaults;
1✔
507

508
        return $cast ? $cast->get($this, $key, $value, [ 'defaults' => $defaults ]) : $value;
1✔
509
    }
510

511
    protected function toCamelCase(string $input): string
1✔
512
    {
513
        $case = implode('',
1✔
514
            array_map(
1✔
515
                function($item) { return ucfirst(strtolower($item)); },
1✔
516
                explode('_', $input)
1✔
517
            )
518
        );
519

520
        return lcfirst($case);
1✔
521
    }
522

523
    protected function fromCamelCase(string $input): string
524
    {
525
        $input = ucfirst($input);
×
526

527
        preg_match_all('/([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)/', $input, $matches);
×
528

529
        $ret = $matches[0];
×
UNCOV
530
        foreach ($ret as &$match) {
×
UNCOV
531
            $match = $match == strtoupper($match) ? strtolower($match) : lcfirst($match);
×
532
        }
533

UNCOV
534
        return strtolower(implode('_', $ret));
×
535
    }
536

537
    protected function raise(string $event, ...$args): void
1✔
538
    {
539
        $this->raise_Events($event, ...$args);
1✔
540
        Reflection::callPrivateMethod($this->getConfiguration()->getRepository(), 'raise', [ $event, ...$args ]);
1✔
541
    }
1✔
542

543
    protected function getCast(string $field): ?AttributeCast
1✔
544
    {
545
        return isset($this->casts[$field]) ? $this->dbino->cast($this->casts[$field]) : null;
1✔
546
    }
547

548
    protected function prepareDbData(array $data): array
1✔
549
    {
550
        $result = [];
1✔
551

552
        foreach ($data as $key => $value) {
1✔
553
            $cast = $this->getCast($key);
1✔
554
            $result[$key] = $cast ? $cast->set($this, $key, $value) : $value;
1✔
555
        }
556

557
        return $result;
1✔
558
    }
559
    
560
    protected function getConfiguration(): Configuration
561
    {
562
        return new Configuration($this->dbino, [
1✔
563
            'model'         => static::class,
1✔
564
            'table'         => $this->table(),
1✔
565
            'repository'    => $this->repository(),
1✔
566
            'casts'         => $this->casts,
1✔
567
            'scopes'        => $this->scopes,
1✔
568
            'events'        => $this->events,
1✔
569
        ]);
570
    }
571
}
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