-
Hey there! I’ve set up a Model and a GlobalFields table. The GlobalFields table includes a model_type field, which contains the morph class name—but without the actual model id, since these fields are intended to be shared across different model types. What I’d like to do is load all related GlobalFields records by calling $model->globalFields, with support for eager loading as well. Is there a way to achieve this using a relation, maybe with a workaround? I’ve tried something along these lines, but it didn’t work: /**
* @return HasMany<GlobalField>
*/
public function globalFields(): HasMany
{
$hasMany = new HasMany(GlobalField::query(), $this, null, null);
return $hasMany->where('model_type', $this->getMorphClass());
} Best - Alex |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 4 replies
-
Hi Alex 👋 interesting use case! You're basically trying to emulate a partial morph relation, where only ✅ What you're trying to do is technically not a HasMany(because it needs Here’s a clean solution: 🛠 Option 1: Use custom accessor (no eager loading)public function getGlobalFieldsAttribute()
{
return GlobalField::query()
->where('model_type', $this->getMorphClass())
->get();
} Usage: $model->global_fields; 🛠 Option 2: Use custom query method and eager load manuallyIn your model: public function globalFieldsCustom()
{
return GlobalField::query()
->where('model_type', $this->getMorphClass());
} Then eager load manually: $models = YourModel::all();
$models->each(function ($model) {
$model->setRelation('globalFields', $model->globalFieldsCustom()->get());
}); Now you can use: $model->globalFields; 🧠 Bonus idea: use a trait for reusetrait HasGlobalFields
{
public function globalFields()
{
return GlobalField::query()
->where('model_type', $this->getMorphClass());
}
} Then in your model: use HasGlobalFields; Let me know if you want a custom |
Beta Was this translation helpful? Give feedback.
-
I came to another solution, which might not be perfect in general but works for our use cases: <?php
declare(strict_types=1);
namespace App\Freispace\CustomFields\Traits;
use App\Freispace\CustomFields\Models\GlobalField;
use App\Freispace\CustomFields\Models\GlobalFieldResponse;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;
/**
* Trait LoadsGlobalFields
*
* This trait provides functionality to load global fields for models.
* It supports loading fields for individual models and collections.
*
* @property-read Collection $global_fields
*/
trait LoadsGlobalFields
{
/**
* Load global fields for the given targets.
*
* @param array $targets
*
* @throws InvalidArgumentException
*
* @return void
*/
public function loadGlobalFields(array $targets): void
{
if (empty($targets)) {
return;
}
$modelsByType = [];
foreach ($targets as $target) {
if (!is_string($target)) {
throw new InvalidArgumentException('Target must be a string');
}
if ($target === 'self') {
$modelsByType[static::class][] = $this;
} else {
$relations = explode('.', $target);
$this->traverseAndCollect($this, $relations, $modelsByType);
}
}
if (empty($modelsByType)) {
return;
}
$allFields = GlobalField::whereIn('model_type', array_keys($modelsByType))
->orderBy('order')
->get()
->groupBy('model_type');
$allIds = collect($modelsByType)->flatten()->pluck('id');
$allResponses = GlobalFieldResponse::whereIn('model_type', array_keys($modelsByType))
->whereIn('model_id', $allIds)
->with('field')
->get()
->groupBy(fn ($r) => $r->model_type . ':' . $r->model_id);
foreach ($modelsByType as $modelType => $models) {
$fields = $allFields->get($modelType, collect());
foreach ($models as $model) {
$key = $modelType . ':' . $model->id;
$responses = $allResponses->get($key, collect());
$fieldsWithResponses = $fields->map(function ($field) use ($responses) {
// Clone the field to ensure each model gets its own independent copy
$field = clone $field;
$fieldResponse = $responses->firstWhere('field_id', $field->id);
if ($fieldResponse) {
$fieldResponse->setRelation('field', $field);
$field->setRelation('responses', collect([$fieldResponse]));
$field->value = $fieldResponse->value;
} else {
$field->setRelation('responses', collect());
$field->value = null;
}
return $field;
});
$model->setRelation('global_fields', $fieldsWithResponses);
}
}
}
/**
* Traverse the model and its relations to collect all models of a specific type.
*
* @param Model $model
* @param array<string> $relations
* @param array<string, array<Model>> $modelsByType
*
* @throws InvalidArgumentException
*
* @return void
*/
protected function traverseAndCollect(Model $model, array $relations, array &$modelsByType): void
{
if (empty($relations)) {
return;
}
$relation = array_shift($relations);
if (!method_exists($model, $relation)) {
throw new InvalidArgumentException("Relation '{$relation}' does not exist on model " . $model::class);
}
$related = $model->$relation;
if ($related instanceof Collection) {
foreach ($related as $item) {
if (!$item instanceof Model) {
continue;
}
$modelsByType[$item::class][] = $item;
$this->traverseAndCollect($item, $relations, $modelsByType);
}
} elseif ($related instanceof Model) {
$modelsByType[$related::class][] = $related;
$this->traverseAndCollect($related, $relations, $modelsByType);
}
}
} Now I can laod all the global fields like this: $client->loadGlobalFields([
'self',
'addresses',
'contacts',
]); |
Beta Was this translation helpful? Give feedback.
I came to another solution, which might not be perfect in general but works for our use cases: