php-commerce/Models/ContentOverride.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

214 lines
5.4 KiB
PHP

<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* Content Override - Sparse override entry for white-label commerce.
*
* Stores a single field override for a specific entity + model combination.
* Only stores what's different from the parent/original.
*
* @property int $id
* @property int $entity_id
* @property string $overrideable_type
* @property int $overrideable_id
* @property string $field
* @property string|null $value
* @property string $value_type
* @property int|null $created_by
* @property int|null $updated_by
*/
class ContentOverride extends Model
{
// Value types
public const TYPE_STRING = 'string';
public const TYPE_JSON = 'json';
public const TYPE_HTML = 'html';
public const TYPE_INTEGER = 'integer';
public const TYPE_DECIMAL = 'decimal';
public const TYPE_BOOLEAN = 'boolean';
protected $table = 'commerce_content_overrides';
protected $fillable = [
'entity_id',
'overrideable_type',
'overrideable_id',
'field',
'value',
'value_type',
'created_by',
'updated_by',
];
protected $casts = [
'overrideable_id' => 'integer',
];
// Relationships
public function entity(): BelongsTo
{
return $this->belongsTo(Entity::class);
}
public function overrideable(): MorphTo
{
return $this->morphTo();
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function updater(): BelongsTo
{
return $this->belongsTo(User::class, 'updated_by');
}
// Value casting
/**
* Get the value cast to its appropriate type.
*/
public function getCastedValue(): mixed
{
if ($this->value === null) {
return null;
}
return match ($this->value_type) {
self::TYPE_JSON => json_decode($this->value, true),
self::TYPE_INTEGER => (int) $this->value,
self::TYPE_DECIMAL => (float) $this->value,
self::TYPE_BOOLEAN => filter_var($this->value, FILTER_VALIDATE_BOOLEAN),
default => $this->value, // string, html
};
}
/**
* Set the value with automatic type detection.
*/
public function setValueWithType(mixed $value): self
{
if ($value === null) {
$this->value = null;
$this->value_type = self::TYPE_STRING;
return $this;
}
if (is_bool($value)) {
$this->value = $value ? '1' : '0';
$this->value_type = self::TYPE_BOOLEAN;
} elseif (is_int($value)) {
$this->value = (string) $value;
$this->value_type = self::TYPE_INTEGER;
} elseif (is_float($value)) {
$this->value = (string) $value;
$this->value_type = self::TYPE_DECIMAL;
} elseif (is_array($value)) {
$this->value = json_encode($value);
$this->value_type = self::TYPE_JSON;
} elseif (is_string($value) && $this->looksLikeHtml($value)) {
$this->value = $value;
$this->value_type = self::TYPE_HTML;
} else {
$this->value = (string) $value;
$this->value_type = self::TYPE_STRING;
}
return $this;
}
/**
* Check if a string looks like HTML content.
*/
protected function looksLikeHtml(string $value): bool
{
return preg_match('/<[a-z][\s\S]*>/i', $value) === 1;
}
// Scopes
public function scopeForEntity($query, int $entityId)
{
return $query->where('entity_id', $entityId);
}
public function scopeForModel($query, string $type, int $id)
{
return $query->where('overrideable_type', $type)
->where('overrideable_id', $id);
}
public function scopeForField($query, string $field)
{
return $query->where('field', $field);
}
public function scopeForEntities($query, array $entityIds)
{
return $query->whereIn('entity_id', $entityIds);
}
// Factory helpers
/**
* Create or update an override.
*/
public static function setOverride(
Entity $entity,
Model $model,
string $field,
mixed $value,
?int $userId = null
): self {
$override = static::firstOrNew([
'entity_id' => $entity->id,
'overrideable_type' => $model->getMorphClass(),
'overrideable_id' => $model->getKey(),
'field' => $field,
]);
$override->setValueWithType($value);
if ($override->exists) {
$override->updated_by = $userId ?? auth()->id();
} else {
$override->created_by = $userId ?? auth()->id();
}
$override->save();
return $override;
}
/**
* Remove an override.
*/
public static function clearOverride(
Entity $entity,
Model $model,
string $field
): bool {
return static::where('entity_id', $entity->id)
->where('overrideable_type', $model->getMorphClass())
->where('overrideable_id', $model->getKey())
->where('field', $field)
->delete() > 0;
}
}