Files
shopit-back/app/Domains/Tenant/Services/WebsiteExtraService.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);
}
}
}