refactor(tenant): introduce TenantInformationService for loading tenant data and resolving website extras
This commit is contained in:
@@ -5,26 +5,30 @@ namespace App\Domains\Tenant\Controllers;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Tenant\Requests\BootstrapTenantRequest;
|
||||
use App\Domains\Tenant\Resources\TenantResource;
|
||||
use App\Domains\Tenant\Services\TenantInformationService;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class BootstrapTenantController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected TenantInformationService $tenantInformationService
|
||||
) {}
|
||||
|
||||
public function __invoke(BootstrapTenantRequest $request): TenantResource
|
||||
{
|
||||
/** @var string $dominio */
|
||||
$dominio = $request->validated('dominio');
|
||||
|
||||
return TenantResource::make(
|
||||
Tenant::query()
|
||||
->with([
|
||||
'headerLogo',
|
||||
'footerLogo',
|
||||
$this->tenantInformationService->load(
|
||||
Tenant::query()
|
||||
->where('dominio', $dominio)
|
||||
->firstOrFail(),
|
||||
[
|
||||
'menues',
|
||||
'socialMedia',
|
||||
'categories' => fn ($query) => $query->orderBy('nombre'),
|
||||
])
|
||||
->where('dominio', $dominio)
|
||||
->firstOrFail()
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Tenant\Requests\StoreTenantRequest;
|
||||
use App\Domains\Tenant\Requests\UpdateTenantRequest;
|
||||
use App\Domains\Tenant\Resources\TenantResource;
|
||||
use App\Domains\Tenant\Services\TenantInformationService;
|
||||
use App\Domains\Tenant\Services\TenantService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -13,16 +14,20 @@ use Illuminate\Http\Response;
|
||||
|
||||
class TenantController extends Controller
|
||||
{
|
||||
public function __construct(protected TenantService $tenantService) {}
|
||||
public function __construct(
|
||||
protected TenantService $tenantService,
|
||||
protected TenantInformationService $tenantInformationService,
|
||||
) {}
|
||||
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
return TenantResource::collection(
|
||||
Tenant::query()
|
||||
->with(['headerLogo', 'footerLogo', 'socialMedia'])
|
||||
->latest()
|
||||
->paginateFromRequest()
|
||||
)->response();
|
||||
$tenants = Tenant::query()
|
||||
->latest()
|
||||
->paginateFromRequest();
|
||||
|
||||
$this->tenantInformationService->loadMany($tenants->getCollection());
|
||||
|
||||
return TenantResource::collection($tenants)->response();
|
||||
}
|
||||
|
||||
public function store(StoreTenantRequest $request): JsonResponse
|
||||
@@ -30,20 +35,14 @@ class TenantController extends Controller
|
||||
$tenant = $this->tenantService->create($request->validated());
|
||||
|
||||
return TenantResource::make(
|
||||
$tenant->loadMissing([
|
||||
'headerLogo',
|
||||
'footerLogo',
|
||||
'socialMedia',
|
||||
'websiteType',
|
||||
'websiteExtras.websiteTypeExtra',
|
||||
])
|
||||
$this->tenantInformationService->load($tenant)
|
||||
)->response()->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function show(Tenant $tenant): TenantResource
|
||||
{
|
||||
return TenantResource::make(
|
||||
$tenant->loadMissing(['headerLogo', 'footerLogo', 'socialMedia'])
|
||||
$this->tenantInformationService->load($tenant)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,7 +51,7 @@ class TenantController extends Controller
|
||||
$tenant = $this->tenantService->update($tenant, $request->validated());
|
||||
|
||||
return TenantResource::make(
|
||||
$tenant->loadMissing(['headerLogo', 'footerLogo', 'socialMedia'])
|
||||
$this->tenantInformationService->load($tenant)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ class WebsiteExtra extends Model
|
||||
|
||||
protected $table = 'websites_extras';
|
||||
|
||||
private mixed $resolvedConfig = null;
|
||||
|
||||
private bool $hasResolvedConfig = false;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
@@ -44,4 +48,17 @@ class WebsiteExtra extends Model
|
||||
{
|
||||
return $this->belongsTo(WebsiteTypeExtra::class, 'website_type_extra_id');
|
||||
}
|
||||
|
||||
public function setResolvedConfig(mixed $config): self
|
||||
{
|
||||
$this->resolvedConfig = $config;
|
||||
$this->hasResolvedConfig = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function resolvedConfig(): mixed
|
||||
{
|
||||
return $this->hasResolvedConfig ? $this->resolvedConfig : $this->config;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Domains\Tenant\Resources;
|
||||
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\Catalog\Models\Category;
|
||||
use App\Domains\Menu\Models\Menu;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
@@ -40,10 +41,11 @@ class TenantResource extends JsonResource
|
||||
),
|
||||
'extras' => $this->whenLoaded(
|
||||
'websiteExtras',
|
||||
fn () => $this->websiteExtras
|
||||
->mapWithKeys(fn ($extra) => [
|
||||
$extra->websiteTypeExtra->nombre => $extra->config,
|
||||
])
|
||||
fn () => $this->websiteExtras->mapWithKeys(fn ($extra) => [
|
||||
$extra->websiteTypeExtra->nombre => $this->formatExtraConfig(
|
||||
$extra->resolvedConfig()
|
||||
),
|
||||
])
|
||||
),
|
||||
// 1 day
|
||||
'header_logo' => $this->headerLogo?->getTemporaryUrl(1440),
|
||||
@@ -73,6 +75,23 @@ class TenantResource extends JsonResource
|
||||
];
|
||||
}
|
||||
|
||||
private function formatExtraConfig(mixed $value): mixed
|
||||
{
|
||||
if ($value instanceof Attachment) {
|
||||
return $value->getTemporaryUrl(1440);
|
||||
}
|
||||
|
||||
if (! is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
foreach ($value as $key => $item) {
|
||||
$value[$key] = $this->formatExtraConfig($item);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, Category> $categories
|
||||
* @return Collection<int, array<string, mixed>>
|
||||
|
||||
177
app/Domains/Tenant/Services/TenantInformationService.php
Normal file
177
app/Domains/Tenant/Services/TenantInformationService.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Tenant\Services;
|
||||
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Tenant\Models\WebsiteExtra;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class TenantInformationService
|
||||
{
|
||||
private const DEFAULT_RELATIONS = [
|
||||
'headerLogo',
|
||||
'footerLogo',
|
||||
'socialMedia',
|
||||
'websiteType',
|
||||
'websiteExtras.websiteTypeExtra',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $relations
|
||||
*/
|
||||
public function load(Tenant $tenant, array $relations = []): Tenant
|
||||
{
|
||||
$this->loadMany(new EloquentCollection([$tenant]), $relations);
|
||||
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EloquentCollection<int, Tenant> $tenants
|
||||
* @param array<int|string, mixed> $relations
|
||||
* @return EloquentCollection<int, Tenant>
|
||||
*/
|
||||
public function loadMany(
|
||||
EloquentCollection $tenants,
|
||||
array $relations = []
|
||||
): EloquentCollection {
|
||||
$tenants->loadMissing([
|
||||
...self::DEFAULT_RELATIONS,
|
||||
...$relations,
|
||||
]);
|
||||
|
||||
$websiteExtras = $tenants
|
||||
->flatMap(fn (Tenant $tenant): Collection => $tenant->websiteExtras);
|
||||
|
||||
$this->resolveWebsiteExtraAttachments($websiteExtras);
|
||||
|
||||
return $tenants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace attachment IDs with their models in each extra's resolved config.
|
||||
*
|
||||
* @param Collection<int, WebsiteExtra> $websiteExtras
|
||||
*/
|
||||
private function resolveWebsiteExtraAttachments(Collection $websiteExtras): void
|
||||
{
|
||||
$attachmentIds = $websiteExtras
|
||||
->flatMap(function (WebsiteExtra $extra): array {
|
||||
$ids = [];
|
||||
|
||||
foreach ($this->attachmentTransformPaths($extra) as $path) {
|
||||
$ids = [
|
||||
...$ids,
|
||||
...$this->valuesAtPath($extra->config, $this->pathSegments($path)),
|
||||
];
|
||||
}
|
||||
|
||||
return $ids;
|
||||
})
|
||||
->filter(fn (mixed $id): bool => is_int($id) || ctype_digit((string) $id))
|
||||
->map(fn (mixed $id): int => (int) $id)
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
$attachments = Attachment::query()
|
||||
->whereIn('id', $attachmentIds)
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
foreach ($websiteExtras as $extra) {
|
||||
$config = $extra->config;
|
||||
|
||||
foreach ($this->attachmentTransformPaths($extra) as $path) {
|
||||
$config = $this->replaceAtPath(
|
||||
$config,
|
||||
$this->pathSegments($path),
|
||||
fn (mixed $attachmentId): ?Attachment => $attachments->get((int) $attachmentId)
|
||||
);
|
||||
}
|
||||
|
||||
$extra->setResolvedConfig($config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function attachmentTransformPaths(WebsiteExtra $extra): array
|
||||
{
|
||||
return collect($extra->websiteTypeExtra->config_schema['transforms'] ?? [])
|
||||
->filter(
|
||||
fn (mixed $transform): bool => is_array($transform)
|
||||
&& ($transform['handler'] ?? null) === 'attachment'
|
||||
)
|
||||
->keys()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function pathSegments(string $path): array
|
||||
{
|
||||
$path = ltrim($path, '$');
|
||||
$path = ltrim($path, '.');
|
||||
|
||||
return $path === '' ? [] : explode('.', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
private function valuesAtPath(mixed $value, array $segments): array
|
||||
{
|
||||
if ($segments === []) {
|
||||
return [$value];
|
||||
}
|
||||
|
||||
if (! is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$segment = array_shift($segments);
|
||||
|
||||
if ($segment === '*') {
|
||||
return collect($value)
|
||||
->flatMap(fn (mixed $item): array => $this->valuesAtPath($item, $segments))
|
||||
->all();
|
||||
}
|
||||
|
||||
if (! array_key_exists($segment, $value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->valuesAtPath($value[$segment], $segments);
|
||||
}
|
||||
|
||||
private function replaceAtPath(mixed $value, array $segments, callable $replace): mixed
|
||||
{
|
||||
if ($segments === []) {
|
||||
return $value === null ? null : $replace($value);
|
||||
}
|
||||
|
||||
if (! is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$segment = array_shift($segments);
|
||||
|
||||
if ($segment === '*') {
|
||||
foreach ($value as $key => $item) {
|
||||
$value[$key] = $this->replaceAtPath($item, $segments, $replace);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (array_key_exists($segment, $value)) {
|
||||
$value[$segment] = $this->replaceAtPath($value[$segment], $segments, $replace);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user