-
Notifications
You must be signed in to change notification settings - Fork 1
Read only model with lazy objects
Sometimes you need to pass a model to a function call and you don’t want any changes to be made to that model so, it should be treated as a read-only model.
You can accomplish that by using BaseModelFrozenAttributes class which is a DTO without setters.
Note that 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.
Given this model:
<?php
namespace App\Models;
use App\Models\Attributes\OperationAttributes;
use App\Models\Attributes\OperationFrozenAttributes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use MacropaySolutions\LaravelCrudWizard\Models\BaseModel;
/**
* @property OperationAttributes $a
* @mixin OperationAttributes
*/
class Operation extends BaseModel
{
public const RESOURCE_NAME = 'operations'; // optional but recommended
public const WITH_RELATIONS = [
'client',
'products',
];
protected array $ignoreUpdateFor = [
'client_id',
'currency',
'value',
'created_at',
];
protected $table = 'operations'; // optional but recommended
protected $fillable = [
'parent_id',
'client_id',
'currency',
'value',
'created_at',
'updated_at',
];
public function client(): BelongsTo
{
return $this->belongsTo(Client::class, 'client_id', 'id', __FUNCTION__);
}
public function products(): HasManyThrough
{
return $this->hasManyThrough(
Product::class,
OperationProductPivot::class,
'operation_id',
'id',
'id',
'product_id'
);
}
/**
* Optional for its return type to be used for autocomplete
* can be replaced with a @var OperationFrozenAttributes $dto
* see below latest update of this article about lazy object in PHP >= 8.4
*/
public function getFrozen(): OperationFrozenAttributes
{
// parent will include also the loaded relations into the attributes
// same as new OperationFrozenAttributes((clone $this)->forceFill($this->toArray()));
return parent::getFrozen();
// just attributes without loaded relations
// return new OperationFrozenAttributes((clone $this)->forceFill($this->attributesToArray()));
}
}
With Attributes class:
<?php
namespace App\Models\Attributes;
use MacropaySolutions\LaravelCrudWizard\Models\Attributes\BaseModelAttributes;
/**
* @mixin OperationFrozenAttributes
*/
class OperationAttributes extends BaseModelAttributes
{
}
And with Frozen Attributes class:
<?php
namespace App\Models\Attributes;
use MacropaySolutions\LaravelCrudWizard\Models\Attributes\BaseModelFrozenAttributes;
/**
* @property int $id
* @property ?int $parent_id
* @property int client_id
* @property string currency
* @property string value
* @property ?string created_at
* @property ?string updated_at
*/
class OperationFrozenAttributes extends BaseModelFrozenAttributes
{
}
You can use it like this:
<?php
namespace App\Services;
use App\Models\Attributes\ClientFrozenAttributes;
use App\Models\Attributes\ProductFrozenAttributes;
use App\Models\Operation;
use MacropaySolutions\LaravelCrudWizard\Services\BaseResourceService;
class OperationsService extends BaseResourceService
{
/**
* @inheritDoc
*/
protected function setBaseModel(): void
{
$this->model = new Operation();
}
public function demoOperationFrozenAttributes(): void
{
/** @var Operation $model */
$model = Operation::query()->with(['client', 'products'])->firstOrFail();
$dto = $model->getFrozen();
echo $dto->client_id; // has autocomplete - will print for example 1
$dto->client_id = 4; // Exception: Dynamic properties are forbidden.
if (isset($dto->client)) {
/** @var ClientFrozenAttributes $client */
// $client will be an stdClass that has autocomplete like a ClientFrozenAttributes
$client = $dto->client;
echo $client->name; // has autocomplete - will print for example 'name'
$client->name = 'text'; // NO Exception
echo $client->name; // will print 'text'
// $client changes can happen, but they will not be persisted in the $dto ($client is a stdClass clone)
echo $dto->client->name; // will print 'name'
echo $dto->client->name = 'text'; // will print 'text'
echo $dto->client->name; // will print 'name'
}
foreach (($dto->products ?? []) as $k => $product) {
/** @var ProductFrozenAttributes $product */
// $product will be an stdClass that has autocompletes like a ProductFrozenAttributes
echo $product->value; // has autocomplete - will print for example 1
$product->value = 2; // NO Exception
echo $product->value; // will print 2
// $product changes can happen, but they will not be persisted in the $dto ($product is a stdClass clone)
echo $dto->products[$k]->value; // will print 1
echo $dto->products[$k]->value = 2; // will print 2
echo $dto->products[$k]->value; // will print 1
}
}
public function demoOperationAttributes(): void
{
$this->model->incrementing = 0; // sets the public declared property to 0 because it is not type hinted
$this->model->setAttribute('incrementing', 0); // sets the model attribute
$this->model->a->incrementing = 0; // sets the model attribute
$this->model->exists = 0; // sets the public declared property to 0 because it is not type hinted
$this->model->setAttribute('exists', 0); // sets the model attribute
$this->model->a->exists = 0; // sets the model attribute
echo $this->model-a->value; // has autocomplete - will print for example 1
echo $this->model-a->value = 10; // has autocomplete - will print 10
echo $this->model->value; // has autocomplete - will print 10
}
}
Good news is that you can use laravel-crud-wizard-generator to generate almost all the needed classes for this.
The bad news is that the FrozenAttributes Class is not auto-generated but it can be easily created from the AttributesClass.
More details can be found in the lib’s Readme file.
Route::get('/', fn (): View => \view('welcome', [
/** Illuminate\Support\Collection<int, OperationFrozenAttributes> */
'operations' => Operation::query()->limit(15)->get()
->map(fn (Operation $o): OperationFrozenAttributes => $o->getFrozen())
]));
Using Lazy Object in PHP ≥ 8.4
public function getFrozen(): OperationFrozenAttributes
{
$reflector = new ReflectionClass(OperationFrozenAttributes::class);
$t = $this;
return $reflector->newLazyGhost(function (OperationFrozenAttributes $object) use ($t): void {
$object->__construct((clone $t)->forceFill($t->toArray()));
});
}
Version 4.0.0 introduced a breaking change that will eliminate (also for the future) the clashes between the object property and the attribute/column and the relation if used like:
// access php property
$model->exists;
// access column
$model->a->exists;
// access cached/future cached relation
$model->r->exists;
// get relation object
$model->exists();
$model->r->exists();
$model->a->exists(); // a and r forward calls to the model via __call
Also, if you want autocomplete and you don’t want to create child classes from BaseModelAttributes and BaseModelRelations you can just put in your models:
/**
* @property static $a
* @property static $r
*/