lthn.io/app/Core/Config/Models/ConfigResolved.php

240 lines
6.3 KiB
PHP
Raw Permalink Normal View History

<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Config\Models;
use Carbon\Carbon;
use Core\Config\ConfigResult;
use Core\Config\Enums\ConfigType;
use Core\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Materialised config resolution.
*
* This is the READ table - all config lookups hit this directly.
* No computation at read time, just indexed lookup.
*
* Prime operation populates this table by running full resolution.
*
* Note: workspace_id and channel_id use 0 for "null/system" scope
* due to MariaDB composite unique constraint limitations.
*
* @property int $workspace_id 0 = system scope
* @property int $channel_id 0 = all channels
* @property string $key_code
* @property mixed $value
* @property string $type
* @property bool $locked
* @property int|null $source_profile_id
* @property int|null $source_channel_id
* @property bool $virtual
* @property Carbon $computed_at
*/
class ConfigResolved extends Model
{
protected $table = 'config_resolved';
public $timestamps = false;
protected $fillable = [
'workspace_id',
'channel_id',
'key_code',
'value',
'type',
'locked',
'source_profile_id',
'source_channel_id',
'virtual',
'computed_at',
];
protected $casts = [
'value' => 'json',
'locked' => 'boolean',
'virtual' => 'boolean',
'computed_at' => 'datetime',
];
/**
* Workspace this resolution is for (null = system).
*
* Requires Core\Tenant module to be installed.
*/
public function workspace(): BelongsTo
{
if (class_exists(Workspace::class)) {
return $this->belongsTo(Workspace::class);
}
// Return a null relationship when Tenant module is not installed
return $this->belongsTo(self::class, 'workspace_id')->whereRaw('1 = 0');
}
/**
* Channel this resolution is for (null = all channels).
*/
public function channel(): BelongsTo
{
return $this->belongsTo(Channel::class);
}
/**
* Profile that provided this value.
*/
public function sourceProfile(): BelongsTo
{
return $this->belongsTo(ConfigProfile::class, 'source_profile_id');
}
/**
* Get the resolved value with proper type casting.
*/
public function getTypedValue(): mixed
{
$type = ConfigType::tryFrom($this->type) ?? ConfigType::STRING;
return $type->cast($this->value);
}
/**
* Convert to ConfigResult for API compatibility.
*/
public function toResult(): ConfigResult
{
$type = ConfigType::tryFrom($this->type) ?? ConfigType::STRING;
if ($this->virtual) {
return ConfigResult::virtual(
key: $this->key_code,
value: $this->value,
type: $type,
);
}
// Determine scope type from source profile
$scopeType = null;
if ($this->source_profile_id !== null) {
$scopeType = $this->sourceProfile?->scope_type;
}
return new ConfigResult(
key: $this->key_code,
value: $type->cast($this->value),
type: $type,
found: true,
locked: $this->locked,
virtual: $this->virtual,
resolvedFrom: $scopeType,
profileId: $this->source_profile_id,
channelId: $this->source_channel_id,
);
}
/**
* Look up a resolved config value.
*
* This is THE read path - single indexed lookup.
*/
public static function lookup(string $keyCode, ?int $workspaceId = null, ?int $channelId = null): ?self
{
return static::where('workspace_id', $workspaceId)
->where('channel_id', $channelId)
->where('key_code', $keyCode)
->first();
}
/**
* Get all resolved config for a scope.
*
* @return Collection<int, self>
*/
public static function forScope(?int $workspaceId = null, ?int $channelId = null): Collection
{
return static::where('workspace_id', $workspaceId)
->where('channel_id', $channelId)
->get();
}
/**
* Store a resolved config value.
*/
public static function store(
string $keyCode,
mixed $value,
ConfigType $type,
?int $workspaceId = null,
?int $channelId = null,
bool $locked = false,
?int $sourceProfileId = null,
?int $sourceChannelId = null,
bool $virtual = false,
): self {
return static::updateOrCreate(
[
'workspace_id' => $workspaceId,
'channel_id' => $channelId,
'key_code' => $keyCode,
],
[
'value' => $value,
'type' => $type->value,
'locked' => $locked,
'source_profile_id' => $sourceProfileId,
'source_channel_id' => $sourceChannelId,
'virtual' => $virtual,
'computed_at' => now(),
]
);
}
/**
* Clear resolved config for a scope.
*/
public static function clearScope(?int $workspaceId = null, ?int $channelId = null): int
{
return static::where('workspace_id', $workspaceId)
->where('channel_id', $channelId)
->delete();
}
/**
* Clear all resolved config for a workspace (all channels).
*/
public static function clearWorkspace(?int $workspaceId = null): int
{
return static::where('workspace_id', $workspaceId)->delete();
}
/**
* Clear resolved config for a specific key across all scopes.
*/
public static function clearKey(string $keyCode): int
{
return static::where('key_code', $keyCode)->delete();
}
/**
* Composite key handling for Eloquent.
*/
protected function setKeysForSaveQuery($query)
{
$query->where('workspace_id', $this->workspace_id)
->where('channel_id', $this->channel_id)
->where('key_code', $this->key_code);
return $query;
}
}