Skip to content

Commit abb97bd

Browse files
author
Admin
committed
Add Active Record Segregation Properties
1 parent bf99dd1 commit abb97bd

File tree

5 files changed

+175
-63
lines changed

5 files changed

+175
-63
lines changed

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ Optionally if you don't want to expose some models as resources but, you want to
124124
];
125125
````
126126

127-
For model properties autocomplete:
127+
**Active Record Segregation of Properties:** for model attributes(columns) autocomplete and to avoid clashes with the model properties or relations:
128128

129129
- extend BaseModelAttributes following the same FQN structure as the parent's:
130130

@@ -133,7 +133,18 @@ For model properties autocomplete:
133133

134134
- add in its class dock block using **@property** all the models properties/attributes/columns
135135
- add in the model's class dock block **@property ChildBaseModelAttributes $a** and **@mixin ChildBaseModelAttributes**
136-
- use **$model->a->** instead of **$model->**
136+
- use `$model->a->` instead of `$model->` _(this will work without autocomplete even if you don't do the above)_
137+
138+
**Active Record Segregation of Properties:** for model relation autocomplete and to avoid clashes with the model properties and attributes(columns):
139+
140+
- extend BaseModelRelations following the same FQN structure as the parent's:
141+
142+
\App\Models\ChildBaseModel paired with \App\Models\Attributes\ChildBaseModelRelations
143+
\App\Models\Folder\ChildBaseModel paired with \App\Models\Folder\Attributes\ChildBaseModelRelations
144+
145+
- add in its class dock block using **@property-read** all the models relations
146+
- add in the model's class dock block **@property ChildBaseModelRelations $r** and **@mixin ChildBaseModelRelations**
147+
- use `$model->r->` instead of `$model->` _(this will work without autocomplete even if you don't do the above)_
137148
- BaseModelFrozenAttributes can be also extended on the same logic and used for model read only situations - DTO without setters (Reflection or Closure binding usage will retrieve/set protected stdClass not Model - but the model can be retrieved from DB by its primary key that is readable in this frozen model):
138149
```php
139150
#OperationModel example for BaseModelFrozenAttributes
@@ -279,7 +290,7 @@ for example for lumen:
279290

280291
OBS
281292

282-
Set `$returnNullOnInvalidColumnAttributeAccess = false;` in model if you want exception instead of null on accessing invalid model attributes (It also needs error_reporting = E_ALL in php ini file).
293+
Set `$returnNullOnInvalidColumnAttributeAccess = false;` in model if you want exception instead of null on accessing invalid model attributes or invalid relations (It also needs error_reporting = E_ALL in php ini file).
283294

284295
Set LIVE_MODE=false in your .env file for non prod environments.
285296

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace MacropaySolutions\LaravelCrudWizard\Models\Attributes;
4+
5+
use MacropaySolutions\LaravelCrudWizard\Models\BaseModel;
6+
7+
abstract class AbstractBaseModelAttributes
8+
{
9+
public function __construct(
10+
protected BaseModel $ownerBaseModel
11+
) {
12+
}
13+
14+
/**
15+
* @throws \Throwable
16+
*/
17+
public function __call(string $method, array $parameters): mixed
18+
{
19+
try {
20+
return $this->ownerBaseModel->{$method}(...$parameters);
21+
} catch (\Error | \BadMethodCallException $e) {
22+
$pattern = '~^Call to undefined method (?P<class>[^:]+)::(?P<method>[^\(]+)\(\)$~';
23+
24+
if (
25+
0 === (int)\preg_match($pattern, $e->getMessage(), $matches)
26+
|| $matches['class'] !== \get_class($this->ownerBaseModel)
27+
|| $matches['method'] !== $method
28+
) {
29+
throw $e;
30+
}
31+
32+
throw new \BadMethodCallException(
33+
\sprintf('Call to undefined method %s::%s()', static::class, $method)
34+
);
35+
}
36+
}
37+
}

src/Models/Attributes/BaseModelAttributes.php

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,15 @@
77
/**
88
* For properties autocompletion declare in the children classes (with @ property) all the model's parameters (columns)
99
*/
10-
class BaseModelAttributes
10+
class BaseModelAttributes extends AbstractBaseModelAttributes
1111
{
12-
public function __construct(
13-
private BaseModel $ownerBaseModel
14-
) {
15-
}
16-
12+
/**
13+
* @throws \Exception
14+
* @see BaseModel::$returnNullOnInvalidColumnAttributeAccess
15+
*/
1716
public function __get(string $key): mixed
1817
{
19-
return $this->ownerBaseModel->getAttribute($key);
18+
return $this->ownerBaseModel->getAttributeValue($key);
2019
}
2120

2221
public function __set(string $key, mixed $value): void
@@ -26,35 +25,11 @@ public function __set(string $key, mixed $value): void
2625

2726
public function __isset(string $key): bool
2827
{
29-
return $this->ownerBaseModel->__isset($key);
28+
return $this->ownerBaseModel->attributeOffsetExists($key);
3029
}
3130

3231
public function __unset(string $key): void
3332
{
34-
$this->ownerBaseModel->__unset($key);
35-
}
36-
37-
/**
38-
* @throws \Throwable
39-
*/
40-
public function __call(string $method, array $parameters): mixed
41-
{
42-
try {
43-
return $this->ownerBaseModel->{$method}(...$parameters);
44-
} catch (\Error | \BadMethodCallException $e) {
45-
$pattern = '~^Call to undefined method (?P<class>[^:]+)::(?P<method>[^\(]+)\(\)$~';
46-
47-
if (
48-
0 === (int)\preg_match($pattern, $e->getMessage(), $matches)
49-
|| $matches['class'] !== \get_class($this->ownerBaseModel)
50-
|| $matches['method'] !== $method
51-
) {
52-
throw $e;
53-
}
54-
55-
throw new \BadMethodCallException(
56-
\sprintf('Call to undefined method %s::%s()', static::class, $method)
57-
);
58-
}
33+
$this->ownerBaseModel->attributeOffsetUnset($key);
5934
}
6035
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace MacropaySolutions\LaravelCrudWizard\Models\Attributes;
4+
5+
use MacropaySolutions\LaravelCrudWizard\Models\BaseModel;
6+
7+
/**
8+
* For properties autocompletion declare in the children classes (with @ property-read) all the model's relations
9+
*/
10+
class BaseModelRelations extends AbstractBaseModelAttributes
11+
{
12+
/**
13+
* @throws \Exception
14+
* @see BaseModel::$returnNullOnInvalidColumnAttributeAccess
15+
*/
16+
public function __get(string $key): mixed
17+
{
18+
return $this->ownerBaseModel->getRelationValue($key);
19+
}
20+
21+
public function __set(string $key, mixed $value): void
22+
{
23+
$this->ownerBaseModel->setRelation($key, $value);
24+
}
25+
26+
public function __isset(string $key): bool
27+
{
28+
return $this->ownerBaseModel->relationLoaded($key);
29+
}
30+
31+
public function __unset(string $key): void
32+
{
33+
$this->ownerBaseModel->unsetRelation($key);
34+
}
35+
}

src/Models/BaseModel.php

Lines changed: 81 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use MacropaySolutions\LaravelCrudWizard\Helpers\GeneralHelper;
1515
use MacropaySolutions\LaravelCrudWizard\Models\Attributes\BaseModelAttributes;
1616
use MacropaySolutions\LaravelCrudWizard\Models\Attributes\BaseModelFrozenAttributes;
17+
use MacropaySolutions\LaravelCrudWizard\Models\Attributes\BaseModelRelations;
1718

1819
/**
1920
* For properties autocompletion declare in the children classes (with @ property) ChildBaseModelAttributes $a
@@ -30,6 +31,7 @@ abstract class BaseModel extends Model
3031
public $timestamps = false;
3132
public static $snakeAttributes = false;
3233
public BaseModelAttributes $a;
34+
public BaseModelRelations $r;
3335
protected bool $returnNullOnInvalidColumnAttributeAccess = true;
3436
protected array $ignoreUpdateFor = [];
3537
protected array $ignoreExternalCreateFor = [];
@@ -47,12 +49,7 @@ public function __construct(array $attributes = [])
4749
{
4850
parent::__construct($attributes);
4951

50-
$attributes = \substr($class = static::class, 0, $l = (-1 * (\strlen($class) - \strrpos($class, '\\') - 1))) .
51-
'Attributes\\' . \substr($class, $l) . 'Attributes';
52-
53-
if (\class_exists($attributes)) {
54-
$this->a = new $attributes($this);
55-
}
52+
$this->initializeActiveRecordSegregationProperties();
5653

5754
$this->append('primary_key_identifier');
5855
}
@@ -306,27 +303,6 @@ protected function setKeysForSaveQuery($query)
306303
return $query->where($this->getPrimaryKeyFilter());
307304
}
308305

309-
/**
310-
* @inheritDoc
311-
*/
312-
public function getAttribute($key)
313-
{
314-
if ($this->exists && isset($this->incrementsToRefresh[$key])) {
315-
$this->attributes = \array_merge(
316-
$this->attributes,
317-
(array)($this->setKeysForSelectQuery($this->newQueryWithoutScopes())
318-
->useWritePdo()
319-
->select($attributes = \array_keys($this->incrementsToRefresh))
320-
->first()
321-
?->toArray())
322-
);
323-
$this->syncOriginalAttributes($attributes);
324-
$this->incrementsToRefresh = [];
325-
}
326-
327-
return parent::getAttribute($key);
328-
}
329-
330306
/**
331307
* @throws \Exception
332308
*/
@@ -355,6 +331,19 @@ public function shouldReturnNullOnInvalidColumnAttributeAccess(): bool
355331
*/
356332
public function getAttributeValue($key): mixed
357333
{
334+
if ($this->exists && isset($this->incrementsToRefresh[$key])) {
335+
$this->attributes = \array_merge(
336+
$this->attributes,
337+
(array)($this->setKeysForSelectQuery($this->newQueryWithoutScopes())
338+
->useWritePdo()
339+
->select($attributes = \array_keys($this->incrementsToRefresh))
340+
->first()
341+
?->toArray())
342+
);
343+
$this->syncOriginalAttributes($attributes);
344+
$this->incrementsToRefresh = [];
345+
}
346+
358347
$return = parent::getAttributeValue($key);
359348

360349
if (
@@ -382,6 +371,48 @@ public function shouldEscapeWhenCastingToString(): bool
382371
return $this->escapeWhenCastingToString;
383372
}
384373

374+
public function attributeOffsetUnset(string $offset): void
375+
{
376+
unset($this->attributes[$offset]);
377+
}
378+
379+
public function attributeOffsetExists(string $offset): bool
380+
{
381+
return isset($this->attributes[$offset]);
382+
}
383+
384+
/**
385+
* @inheritDoc
386+
* @throws \Exception
387+
* @see static::returnNullOnInvalidColumnAttributeAccess
388+
*/
389+
public function getRelationValue($key): mixed
390+
{
391+
// If the key already exists in the relationships array, it just means the
392+
// relationship has already been loaded, so we'll just return it out of
393+
// here because there is no need to query within the relations twice.
394+
if ($this->relationLoaded($key)) {
395+
return $this->relations[$key];
396+
}
397+
398+
if (!$this->isRelation($key)) {
399+
if (!$this->returnNullOnInvalidColumnAttributeAccess) {
400+
throw new \Exception('Undefined relation: ' . $key . ' in model: ' . static::class);
401+
}
402+
403+
return null;
404+
}
405+
406+
if ($this->preventsLazyLoading) {
407+
$this->handleLazyLoadingViolation($key);
408+
}
409+
410+
// If the "attribute" exists as a method on the model, we will just assume
411+
// it is a relationship and will load and return results from the query
412+
// and hydrate the relationship's value on the "relationships" array.
413+
return $this->getRelationshipFromMethod($key);
414+
}
415+
385416
/**
386417
* This will mass update the whole table if the model does not exist!
387418
* @inheritDoc
@@ -397,4 +428,27 @@ protected function incrementOrDecrement($column, $amount, $extra, $method): int
397428

398429
return $return;
399430
}
431+
432+
/**
433+
* @see static::a
434+
* @see static::p
435+
*/
436+
protected function initializeActiveRecordSegregationProperties(): void
437+
{
438+
$prefix = \substr($class = static::class, 0, $l = (-1 * (\strlen($class) - \strrpos($class, '\\') - 1))) .
439+
'Attributes\\' . \substr($class, $l);
440+
441+
foreach (
442+
[
443+
'a' => ['p' => 'Attributes', 'c' => BaseModelAttributes::class],
444+
'r' => ['p' => 'Relations', 'c' => BaseModelRelations::class]
445+
] as $p => $propertyMap
446+
) {
447+
try {
448+
$this->$p = new ($prefix . $this->$propertyMap['p'])($this);
449+
} catch (\Throwable) {
450+
$this->$p = new ($this->$propertyMap['c'])($this);
451+
}
452+
}
453+
}
400454
}

0 commit comments

Comments
 (0)