Challenged by this discussion which proposes PHP attributes for the relation definition to avoid the method definition issues that arise for identifying if a method is relation or not in an active record , after this PR you can segregate the relation definition from the modelโs methods without using Reflection :
// model
protected function segregatedRelationsDefinitionMap(): array
{
return [
'relName' => fn(): HasOne => $this->hasOne(Model::class, 'model_id', 'id'),
// Reuse the segregatedrelation inside another segregated relation:
'relNameScoped' => fn(): HasOne => $this->relName()->where('col', '=', 'text'),
'relNameScoped2' => fn(): HasOne => $this->callSegregatedRelation('relName')->where('col', '=', 'text'),
// Reuse the method relation:
'relNameAsMethod' => $this->relNameAsMethod(...),
'relNameAsMethod' => fn(): HasOne => $this->relNameAsMethod(),
// AVOID THESE:
'relNameAsMethod' => [$this, 'relNameAsMethod'],
'relNameAsMethod' => fn(): HasOne => [$this, 'relNameAsMethod'](),
// DO NOT USE IT LIKE THIS!:
'relNameAsMethod' => fn(): HasOne => $this->relNameAsMethod(...)(), // executes the relation inside the map.
];
}
The old way of defining the relations in the model as methods will still work and will be auto-promoted into this new segregated logic on touch.
If all the method relations are used in a request cycle before calling:
$model->segregatedRelationList(); // no reflection involved
it will return a list with all.
To promote beforehand all method relations to this new segregation logic, you can call:
$model->segregatedRelationList(discoverMethods: true); // reflection involved
Note that this will discover ONLY methods that DEFINE a return type instance of Relation!
If you want to use reflection on the method it self, the \ReflectionFunction can be retrieved via:
$model->getSegregatedRelationReflectionFunction('methodName') // returns null|\ReflectionFunction
This will also auto-promote the method to this new logic. PHP attributes can be used on the method/callback and read this way.
The reflection usage is OPT IN! If the developer does not call the above methods, no reflection is involved.
This PR is a follow up for the laravel-crud-wizard-free / maravel-rest-wizard active record property segregation (BaseModel::initializeActiveRecordSegregationProperties).
#OperationService example for BaseModelRelations
public function someFunction(): void
{
// BaseModelRelations
$this->model-r->relName; // has autocomplete - will retrieve the relation result
$this->model-relName; // has autocomplete - will retrieve the relation result
$this->model-r->relName(); // has autocomplete - will retrieve the Relation instance
$this->model->relName(); // has autocomplete - will retrieve the Relation instance
}
The ->r skips the isRelation check and calls directly getRelationValue method.
In the above mentioned libs, a constant (array as list) is used to manually define the API exposed relations:
public const WITH_RELATIONS = [];
This is used by them to check if a relation exists or not.
The segregatedRelationList(true) would contain more or equal methods than the above constant.
The segregatedRelationList(false) could contain more, equal or less methods than the above constant, depending on:
- if ALL relations are defined inside segregatedRelationsDefinitionMap, then segregatedRelationList(true) === segregatedRelationList(false).
- if NONE of the relations are defined inside segregatedRelationsDefinitionMap, then the result is dynamic based on the above.
- if SOME relations are defined inside segregatedRelationsDefinitionMap, then the result will always contain the ones included and for the rest it will be dynamic based on the above.
The promotion happens ONLY ONCE per Model class, and is bound to each Model instance when used.
A small amout of memory will be needed for the 3 new cache properties introduced:
trait HasRelationships
{
/**
* The static cache of relationship blueprints across all model instances.
* Stored statically so X instances share the same number of closures per model.
* Structured as: [ModelFqn => [RelationName => Closure]]
*/
private static array $segregatedRelationsGlobalMap = [];
/**
* The static cache for relation ReflectionFunction
* Structured as: [ModelFqn => [RelationName => false|\ReflectionFunction]]
*/
private static array $segregatedRelationsReflectionFunctionsMap = [];
trait HasAttributes
{
/**
* The static cache for isRelation repeated calls
* Structured as: [ModelFqn => [RelationName => bool]
*/
private static array $isRelationCacheMap = [];

Top comments (0)