306 lines
9.3 KiB
PHP
306 lines
9.3 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\WebsiteType;
|
|
use App\Domains\Tenant\Models\WebsiteTypeExtra;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Validator;
|
|
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);
|
|
$allowedNames = $definitions->pluck('nombre')->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 ($allowedNames): void {
|
|
if (! is_array($value)) {
|
|
return;
|
|
}
|
|
|
|
$unknownNames = array_diff(array_keys($value), $allowedNames);
|
|
|
|
if ($unknownNames !== []) {
|
|
$fail(
|
|
'The '.$attribute.' field contains extras not supported by the website type: '
|
|
.implode(', ', $unknownNames).'.'
|
|
);
|
|
}
|
|
},
|
|
],
|
|
];
|
|
|
|
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->nombre}"] = $rootRules;
|
|
|
|
foreach ($schemaRules as $path => $pathRules) {
|
|
if ($path === '$') {
|
|
continue;
|
|
}
|
|
|
|
$rules[$this->requestAttribute($definition->nombre, $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('nombre');
|
|
|
|
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."],
|
|
]);
|
|
}
|
|
|
|
$transformedConfig = $this->applyTransforms($tenant, $definition, $config);
|
|
$this->validateDatabaseConfig($definition, $transformedConfig);
|
|
|
|
$tenant->websiteExtras()->create([
|
|
'website_type_extra_id' => $definition->id,
|
|
'config' => $transformedConfig,
|
|
]);
|
|
}
|
|
|
|
$tenant->unsetRelation('websiteExtras');
|
|
}
|
|
|
|
/**
|
|
* @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 $extraName, string $path): string
|
|
{
|
|
if ($path === '$') {
|
|
return "extras.{$extraName}";
|
|
}
|
|
|
|
if (str_starts_with($path, '$.')) {
|
|
$path = substr($path, 2);
|
|
}
|
|
|
|
return "extras.{$extraName}.{$path}";
|
|
}
|
|
|
|
private function applyTransforms(
|
|
Tenant $tenant,
|
|
WebsiteTypeExtra $definition,
|
|
mixed $config
|
|
): 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
|
|
)
|
|
);
|
|
}
|
|
|
|
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
|
|
): mixed {
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
|
|
if (($transform['handler'] ?? null) !== 'attachment') {
|
|
throw new InvalidArgumentException(
|
|
"Unsupported transform handler for {$definition->nombre}: ".($transform['handler'] ?? 'null')
|
|
);
|
|
}
|
|
|
|
if (is_string($value) && Str::isUuid($value)) {
|
|
$attachment = Attachment::query()->where('key', $value)->first();
|
|
|
|
if (! $attachment) {
|
|
throw ValidationException::withMessages([
|
|
$this->requestAttribute($definition->nombre, $path) => [
|
|
'The selected attachment does not exist.',
|
|
],
|
|
]);
|
|
}
|
|
} else {
|
|
$attachment = $this->attachmentService->store(
|
|
$value,
|
|
"tenants/{$tenant->codigo}/extras/{$definition->nombre}"
|
|
);
|
|
}
|
|
|
|
$expectedType = $transform['attachment_type'] ?? null;
|
|
|
|
if (
|
|
is_string($expectedType)
|
|
&& $attachment->type instanceof AttachmentType
|
|
&& $attachment->type->value !== $expectedType
|
|
) {
|
|
throw ValidationException::withMessages([
|
|
$this->requestAttribute($definition->nombre, $path) => [
|
|
"The attachment must be of type {$expectedType}.",
|
|
],
|
|
]);
|
|
}
|
|
|
|
return $attachment->id;
|
|
}
|
|
|
|
private function validateDatabaseConfig(
|
|
WebsiteTypeExtra $definition,
|
|
mixed $config
|
|
): void {
|
|
$schemaRules = $definition->config_schema['database_rules'] ?? [];
|
|
$rules = [];
|
|
|
|
foreach ($schemaRules as $path => $pathRules) {
|
|
$attribute = $path === '$'
|
|
? 'config'
|
|
: 'config.'.ltrim(str_starts_with($path, '$.') ? substr($path, 2) : $path, '.');
|
|
$rules[$attribute] = $this->compileRules($pathRules);
|
|
}
|
|
|
|
$validator = Validator::make(['config' => $config], $rules);
|
|
|
|
if ($validator->fails()) {
|
|
$messages = [];
|
|
|
|
foreach ($validator->errors()->toArray() as $attribute => $errors) {
|
|
$suffix = $attribute === 'config' ? '' : substr($attribute, strlen('config'));
|
|
$messages["extras.{$definition->nombre}{$suffix}"] = $errors;
|
|
}
|
|
|
|
throw ValidationException::withMessages($messages);
|
|
}
|
|
}
|
|
}
|