368 lines
11 KiB
PHP
368 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Domains\Tenant\Services;
|
|
|
|
use App\Domains\Attachable\Enums\AttachmentType;
|
|
use App\Domains\Attachable\Models\Attachment;
|
|
use App\Domains\Attachable\Services\AttachmentService;
|
|
use App\Domains\Shared\Rules\ImageOrBase64Rule;
|
|
use App\Domains\Tenant\Models\Tenant;
|
|
use App\Domains\Tenant\Models\WebsiteExtra;
|
|
use App\Domains\Tenant\Models\WebsiteType;
|
|
use App\Domains\Tenant\Models\WebsiteTypeExtra;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
use InvalidArgumentException;
|
|
|
|
class WebsiteExtraService
|
|
{
|
|
public function __construct(protected AttachmentService $attachmentService) {}
|
|
|
|
/**
|
|
* Build the request rules declared by the selected website type.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function requestRules(?string $websiteTypeCode): array
|
|
{
|
|
if (! is_string($websiteTypeCode) || $websiteTypeCode === '') {
|
|
return [
|
|
'extras' => ['prohibited'],
|
|
];
|
|
}
|
|
|
|
$definitions = $this->definitionsFor($websiteTypeCode);
|
|
$allowedCodes = $definitions->pluck('codigo')->all();
|
|
$hasRequiredExtras = $definitions->contains(
|
|
fn (WebsiteTypeExtra $definition): bool => $definition->is_required
|
|
);
|
|
|
|
$rules = [
|
|
'extras' => [
|
|
$hasRequiredExtras ? 'required' : 'sometimes',
|
|
'array',
|
|
function (string $attribute, mixed $value, \Closure $fail) use ($allowedCodes): void {
|
|
if (! is_array($value)) {
|
|
return;
|
|
}
|
|
|
|
$unknownCodes = array_diff(array_keys($value), $allowedCodes);
|
|
|
|
if ($unknownCodes !== []) {
|
|
$fail(
|
|
'The '.$attribute.' field contains extras not supported by the website type: '
|
|
.implode(', ', $unknownCodes).'.'
|
|
);
|
|
}
|
|
},
|
|
],
|
|
];
|
|
|
|
foreach ($definitions as $definition) {
|
|
$schemaRules = $definition->config_schema['request_rules'] ?? [];
|
|
$rootRules = $this->compileRules($schemaRules['$'] ?? []);
|
|
$rootRules = array_values(array_filter(
|
|
$rootRules,
|
|
fn (mixed $rule): bool => ! in_array($rule, ['required', 'sometimes'], true)
|
|
));
|
|
array_unshift($rootRules, $definition->is_required ? 'required' : 'sometimes');
|
|
|
|
$rules["extras.{$definition->codigo}"] = $rootRules;
|
|
|
|
foreach ($schemaRules as $path => $pathRules) {
|
|
if ($path === '$') {
|
|
continue;
|
|
}
|
|
|
|
$rules[$this->requestAttribute($definition->codigo, $path)] = $this->compileRules($pathRules);
|
|
}
|
|
}
|
|
|
|
return $rules;
|
|
}
|
|
|
|
/**
|
|
* Transform and persist each extra selected for a tenant.
|
|
*
|
|
* @param array<string, mixed> $extras
|
|
*/
|
|
public function createForTenant(Tenant $tenant, array $extras): void
|
|
{
|
|
if ($extras === []) {
|
|
return;
|
|
}
|
|
|
|
$definitions = $this->definitionsFor((string) $tenant->website_type_code)
|
|
->keyBy('codigo');
|
|
|
|
foreach ($extras as $name => $config) {
|
|
/** @var WebsiteTypeExtra|null $definition */
|
|
$definition = $definitions->get($name);
|
|
|
|
if (! $definition) {
|
|
throw ValidationException::withMessages([
|
|
'extras' => ["The extra {$name} is not supported by the selected website type."],
|
|
]);
|
|
}
|
|
|
|
$requestRoot = "extras.{$definition->codigo}";
|
|
$transformedConfig = $this->applyTransforms(
|
|
$tenant,
|
|
$definition,
|
|
$config,
|
|
$requestRoot
|
|
);
|
|
$tenant->websiteExtras()->create([
|
|
'website_type_extra_id' => $definition->id,
|
|
'config' => $transformedConfig,
|
|
]);
|
|
}
|
|
|
|
$tenant->unsetRelation('websiteExtras');
|
|
}
|
|
|
|
/**
|
|
* Build request rules for one extra addressed by its stable code.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function requestRulesForExtra(Tenant $tenant, string $extraCode): array
|
|
{
|
|
$definition = $this->definitionForTenant($tenant, $extraCode);
|
|
$schemaRules = $definition->config_schema['request_rules'] ?? [];
|
|
$rootRules = $this->compileRules($schemaRules['$'] ?? []);
|
|
$rootRules = array_values(array_filter(
|
|
$rootRules,
|
|
fn (mixed $rule): bool => ! in_array($rule, ['required', 'sometimes'], true)
|
|
));
|
|
array_unshift($rootRules, 'required');
|
|
|
|
$rules = ['config' => $rootRules];
|
|
|
|
foreach ($schemaRules as $path => $pathRules) {
|
|
if ($path === '$') {
|
|
continue;
|
|
}
|
|
|
|
$rules[$this->configAttribute('config', $path)] = $this->compileRules($pathRules);
|
|
}
|
|
|
|
return $rules;
|
|
}
|
|
|
|
public function updateForTenant(Tenant $tenant, string $extraCode, mixed $config): WebsiteExtra
|
|
{
|
|
$definition = $this->definitionForTenant($tenant, $extraCode);
|
|
|
|
return DB::transaction(function () use ($tenant, $definition, $config): WebsiteExtra {
|
|
$transformedConfig = $this->applyTransforms($tenant, $definition, $config, 'config');
|
|
|
|
return $tenant->websiteExtras()->updateOrCreate(
|
|
['website_type_extra_id' => $definition->id],
|
|
['config' => $transformedConfig]
|
|
);
|
|
});
|
|
}
|
|
|
|
public function toggleForTenant(Tenant $tenant, string $extraCode): WebsiteExtra
|
|
{
|
|
$definition = $this->definitionForTenant($tenant, $extraCode);
|
|
|
|
return DB::transaction(function () use ($tenant, $definition): WebsiteExtra {
|
|
$websiteExtra = $tenant->websiteExtras()
|
|
->where('website_type_extra_id', $definition->id)
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if (! $websiteExtra) {
|
|
return $tenant->websiteExtras()->create([
|
|
'website_type_extra_id' => $definition->id,
|
|
'config' => [],
|
|
'is_enabled' => true,
|
|
]);
|
|
}
|
|
|
|
$websiteExtra->update([
|
|
'is_enabled' => ! $websiteExtra->is_enabled,
|
|
]);
|
|
|
|
return $websiteExtra;
|
|
});
|
|
}
|
|
|
|
public function definitionForTenant(Tenant $tenant, string $extraCode): WebsiteTypeExtra
|
|
{
|
|
return WebsiteTypeExtra::query()
|
|
->where('website_type_code', $tenant->website_type_code)
|
|
->where('codigo', $extraCode)
|
|
->firstOrFail();
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, WebsiteTypeExtra>
|
|
*/
|
|
private function definitionsFor(string $websiteTypeCode): Collection
|
|
{
|
|
$websiteType = WebsiteType::query()
|
|
->where('codigo', $websiteTypeCode)
|
|
->with('extras')
|
|
->first();
|
|
|
|
return $websiteType?->extras ?? collect();
|
|
}
|
|
|
|
/**
|
|
* @param string|array<int, mixed> $rules
|
|
* @return array<int, mixed>
|
|
*/
|
|
private function compileRules(string|array $rules): array
|
|
{
|
|
$compiled = is_string($rules) ? explode('|', $rules) : $rules;
|
|
|
|
return array_map(
|
|
fn (mixed $rule): mixed => $rule === 'image_or_base64'
|
|
? new ImageOrBase64Rule
|
|
: $rule,
|
|
$compiled
|
|
);
|
|
}
|
|
|
|
private function requestAttribute(string $extraCode, string $path): string
|
|
{
|
|
return $this->configAttribute("extras.{$extraCode}", $path);
|
|
}
|
|
|
|
private function configAttribute(string $root, string $path): string
|
|
{
|
|
if ($path === '$') {
|
|
return $root;
|
|
}
|
|
|
|
if (str_starts_with($path, '$.')) {
|
|
$path = substr($path, 2);
|
|
}
|
|
|
|
return "{$root}.{$path}";
|
|
}
|
|
|
|
private function applyTransforms(
|
|
Tenant $tenant,
|
|
WebsiteTypeExtra $definition,
|
|
mixed $config,
|
|
string $requestRoot
|
|
): mixed {
|
|
foreach ($definition->config_schema['transforms'] ?? [] as $path => $transform) {
|
|
$segments = $this->pathSegments($path);
|
|
$config = $this->transformAtPath(
|
|
$config,
|
|
$segments,
|
|
fn (mixed $value): mixed => $this->transformValue(
|
|
$tenant,
|
|
$definition,
|
|
$path,
|
|
$value,
|
|
$transform,
|
|
$requestRoot
|
|
)
|
|
);
|
|
}
|
|
|
|
return $config;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function pathSegments(string $path): array
|
|
{
|
|
$path = ltrim($path, '$');
|
|
$path = ltrim($path, '.');
|
|
|
|
return $path === '' ? [] : explode('.', $path);
|
|
}
|
|
|
|
private function transformAtPath(mixed $value, array $segments, callable $transform): mixed
|
|
{
|
|
if ($segments === []) {
|
|
return $transform($value);
|
|
}
|
|
|
|
if (! is_array($value)) {
|
|
return $value;
|
|
}
|
|
|
|
$segment = array_shift($segments);
|
|
|
|
if ($segment === '*') {
|
|
foreach ($value as $key => $item) {
|
|
$value[$key] = $this->transformAtPath($item, $segments, $transform);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
if (array_key_exists($segment, $value)) {
|
|
$value[$segment] = $this->transformAtPath($value[$segment], $segments, $transform);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $transform
|
|
*/
|
|
private function transformValue(
|
|
Tenant $tenant,
|
|
WebsiteTypeExtra $definition,
|
|
string $path,
|
|
mixed $value,
|
|
array $transform,
|
|
string $requestRoot
|
|
): mixed {
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
|
|
if (($transform['handler'] ?? null) !== 'attachment') {
|
|
throw new InvalidArgumentException(
|
|
"Unsupported transform handler for {$definition->codigo}: ".($transform['handler'] ?? 'null')
|
|
);
|
|
}
|
|
|
|
if (is_string($value) && Str::isUuid($value)) {
|
|
$attachment = Attachment::query()->where('key', $value)->first();
|
|
|
|
if (! $attachment) {
|
|
throw ValidationException::withMessages([
|
|
$this->configAttribute($requestRoot, $path) => [
|
|
'The selected attachment does not exist.',
|
|
],
|
|
]);
|
|
}
|
|
} else {
|
|
$attachment = $this->attachmentService->store(
|
|
$value,
|
|
"tenants/{$tenant->codigo}/extras/{$definition->codigo}"
|
|
);
|
|
}
|
|
|
|
$expectedType = $transform['attachment_type'] ?? null;
|
|
|
|
if (
|
|
is_string($expectedType)
|
|
&& $attachment->type instanceof AttachmentType
|
|
&& $attachment->type->value !== $expectedType
|
|
) {
|
|
throw ValidationException::withMessages([
|
|
$this->configAttribute($requestRoot, $path) => [
|
|
"The attachment must be of type {$expectedType}.",
|
|
],
|
|
]);
|
|
}
|
|
|
|
return $attachment->id;
|
|
}
|
|
}
|