php-commerce/Services/ContentOverrideService.php
Snider a774f4e285 refactor: migrate namespace from Core\Commerce to Core\Mod\Commerce
Align commerce module with the monorepo module structure by updating
all namespaces to use the Core\Mod\Commerce convention. This change
supports the recent monorepo separation and ensures consistency with
other modules.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 16:23:12 +00:00

271 lines
8.8 KiB
PHP

<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Services;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Core\Mod\Commerce\Models\ContentOverride;
use Core\Mod\Commerce\Models\Entity;
/**
* Content Override Service - Sparse override resolution for white-label commerce.
*
* Resolution chain: entity → parent → parent → M1 (original)
* Only stores what's different. Returns merged view at runtime.
*/
class ContentOverrideService
{
/**
* Get a single field value, resolved through the entity hierarchy.
*
* Checks from the entity up to root, returns first override found.
* If no override, returns the original model value.
*/
public function get(Entity $entity, Model $model, string $field): mixed
{
$hierarchy = $this->getHierarchyBottomUp($entity);
$morphType = $model->getMorphClass();
$modelId = $model->getKey();
// Check from this entity up to root
foreach ($hierarchy as $ancestor) {
$override = ContentOverride::where('entity_id', $ancestor->id)
->where('overrideable_type', $morphType)
->where('overrideable_id', $modelId)
->where('field', $field)
->first();
if ($override) {
return $override->getCastedValue();
}
}
// No override found - return original model value
return $model->getAttribute($field);
}
/**
* Set an override for an entity.
*/
public function set(Entity $entity, Model $model, string $field, mixed $value): ContentOverride
{
return ContentOverride::setOverride($entity, $model, $field, $value);
}
/**
* Clear (remove) an override for an entity.
*
* After clearing, the entity will inherit from parent or original.
*/
public function clear(Entity $entity, Model $model, string $field): bool
{
return ContentOverride::clearOverride($entity, $model, $field);
}
/**
* Get all resolved fields for a model within an entity context.
*
* Returns the model's attributes with all applicable overrides applied.
*/
public function getEffective(Entity $entity, Model $model, ?array $fields = null): array
{
// Start with original model data
$resolved = $model->toArray();
// If specific fields requested, filter to just those
if ($fields !== null) {
$resolved = array_intersect_key($resolved, array_flip($fields));
}
// Get hierarchy from M1 down to this entity
$hierarchy = $this->getHierarchyTopDown($entity);
$morphType = $model->getMorphClass();
$modelId = $model->getKey();
// Apply overrides in order (M1 first, then M2, then M3, etc.)
// Later overrides win, so entity's own overrides take precedence
foreach ($hierarchy as $ancestor) {
$overrides = ContentOverride::where('entity_id', $ancestor->id)
->where('overrideable_type', $morphType)
->where('overrideable_id', $modelId)
->when($fields !== null, fn ($q) => $q->whereIn('field', $fields))
->get();
foreach ($overrides as $override) {
$resolved[$override->field] = $override->getCastedValue();
}
}
return $resolved;
}
/**
* Get override status for all fields of a model.
*
* Returns information about what's overridden vs inherited.
*/
public function getOverrideStatus(Entity $entity, Model $model, array $fields): array
{
$morphType = $model->getMorphClass();
$modelId = $model->getKey();
$hierarchy = $this->getHierarchyBottomUp($entity);
$hierarchyIds = $hierarchy->pluck('id')->toArray();
$status = [];
foreach ($fields as $field) {
// Find the override for this field (if any)
$override = ContentOverride::where('overrideable_type', $morphType)
->where('overrideable_id', $modelId)
->where('field', $field)
->whereIn('entity_id', $hierarchyIds)
->orderByRaw('FIELD(entity_id, '.implode(',', $hierarchyIds).')')
->with('entity')
->first();
$resolvedValue = $override
? $override->getCastedValue()
: $model->getAttribute($field);
$status[$field] = [
'value' => $resolvedValue,
'original' => $model->getAttribute($field),
'source' => $override ? $override->entity->name : 'original',
'source_type' => $override ? $override->entity->type : null,
'is_overridden' => $override && $override->entity_id === $entity->id,
'inherited_from' => $override && $override->entity_id !== $entity->id
? $override->entity->name
: null,
'can_override' => true, // Could add permission check here
];
}
return $status;
}
/**
* Get all overrides for an entity (for admin UI).
*/
public function getEntityOverrides(Entity $entity): Collection
{
return ContentOverride::where('entity_id', $entity->id)
->orderBy('overrideable_type')
->orderBy('overrideable_id')
->orderBy('field')
->get();
}
/**
* Get overrides grouped by model (for admin UI).
*/
public function getEntityOverridesGrouped(Entity $entity): Collection
{
return $this->getEntityOverrides($entity)
->groupBy(['overrideable_type', 'overrideable_id']);
}
/**
* Bulk set overrides for a model.
*/
public function setBulk(Entity $entity, Model $model, array $overrides): array
{
$results = [];
foreach ($overrides as $field => $value) {
$results[$field] = $this->set($entity, $model, $field, $value);
}
return $results;
}
/**
* Clear all overrides for a model within an entity.
*/
public function clearAll(Entity $entity, Model $model): int
{
return ContentOverride::where('entity_id', $entity->id)
->where('overrideable_type', $model->getMorphClass())
->where('overrideable_id', $model->getKey())
->delete();
}
/**
* Copy overrides from one entity to another.
*
* Useful when creating child entities that should start with parent's customisations.
*/
public function copyOverrides(Entity $source, Entity $target, ?Model $model = null): int
{
$query = ContentOverride::where('entity_id', $source->id);
if ($model) {
$query->where('overrideable_type', $model->getMorphClass())
->where('overrideable_id', $model->getKey());
}
$overrides = $query->get();
$count = 0;
foreach ($overrides as $override) {
ContentOverride::updateOrCreate(
[
'entity_id' => $target->id,
'overrideable_type' => $override->overrideable_type,
'overrideable_id' => $override->overrideable_id,
'field' => $override->field,
],
[
'value' => $override->value,
'value_type' => $override->value_type,
'created_by' => auth()->id(),
]
);
$count++;
}
return $count;
}
/**
* Check if an entity has any overrides for a model.
*/
public function hasOverrides(Entity $entity, Model $model): bool
{
return ContentOverride::where('entity_id', $entity->id)
->where('overrideable_type', $model->getMorphClass())
->where('overrideable_id', $model->getKey())
->exists();
}
/**
* Get which fields are overridden by an entity.
*/
public function getOverriddenFields(Entity $entity, Model $model): array
{
return ContentOverride::where('entity_id', $entity->id)
->where('overrideable_type', $model->getMorphClass())
->where('overrideable_id', $model->getKey())
->pluck('field')
->toArray();
}
/**
* Get hierarchy from this entity up to root (including self).
*/
protected function getHierarchyBottomUp(Entity $entity): Collection
{
$hierarchy = $entity->getHierarchy(); // Includes self
return $hierarchy->reverse()->values();
}
/**
* Get hierarchy from root down to this entity (including self).
*/
protected function getHierarchyTopDown(Entity $entity): Collection
{
return $entity->getHierarchy(); // Already ordered root to self
}
}