feat: ship creator journey v2 and profile updates

This commit is contained in:
2026-04-12 21:42:07 +02:00
parent a2457f4e49
commit d5cff21ea2
335 changed files with 20147 additions and 1545 deletions

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Services\ContentTypes;
use App\Models\ContentType;
class ContentTypeSlugResolution
{
public function __construct(
public readonly string $requestedSlug,
public readonly ?ContentType $contentType = null,
public readonly ?string $redirectSlug = null,
public readonly bool $isVirtual = false,
public readonly ?string $virtualType = null,
) {
}
public function found(): bool
{
return $this->contentType !== null || $this->isVirtual;
}
public function requiresRedirect(): bool
{
return $this->redirectSlug !== null && $this->redirectSlug !== '' && $this->redirectSlug !== $this->requestedSlug;
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace App\Services\ContentTypes;
use App\Models\ContentType;
use App\Models\ContentTypeSlugHistory;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
class ContentTypeSlugResolver
{
public function publicContentTypes(): Collection
{
return Cache::rememberForever($this->publicListCacheKey(), function () {
return ContentType::query()
->ordered()
->get(['id', 'name', 'slug', 'description', 'order', 'hide_from_menu']);
});
}
public function toolbarContentTypes(): Collection
{
return $this->publicContentTypes()
->reject(static fn (ContentType $contentType): bool => (bool) $contentType->hide_from_menu)
->values();
}
public function resolve(string $slug, bool $allowVirtual = false): ContentTypeSlugResolution
{
$normalizedSlug = strtolower(trim($slug));
if ($allowVirtual && $this->isVirtualSlug($normalizedSlug)) {
return new ContentTypeSlugResolution(
requestedSlug: $normalizedSlug,
isVirtual: true,
virtualType: $normalizedSlug,
);
}
$slugMap = $this->currentSlugMap();
if (isset($slugMap[$normalizedSlug])) {
return new ContentTypeSlugResolution(
requestedSlug: $normalizedSlug,
contentType: $this->publicContentTypes()->firstWhere('id', $slugMap[$normalizedSlug]),
);
}
$historyMap = $this->historySlugMap();
$redirectSlug = $historyMap[$normalizedSlug] ?? null;
if ($redirectSlug !== null) {
$contentTypeId = $slugMap[$redirectSlug] ?? null;
return new ContentTypeSlugResolution(
requestedSlug: $normalizedSlug,
contentType: $contentTypeId !== null ? $this->publicContentTypes()->firstWhere('id', $contentTypeId) : null,
redirectSlug: $redirectSlug,
);
}
return new ContentTypeSlugResolution(requestedSlug: $normalizedSlug);
}
public function reservedSlugs(): array
{
return array_values(array_unique(array_map(
static fn (string $slug): string => strtolower(trim($slug)),
(array) config('content_types.reserved_slugs', [])
)));
}
public function isReservedSlug(string $slug): bool
{
return in_array(strtolower(trim($slug)), $this->reservedSlugs(), true);
}
public function historicalSlugExists(string $slug, ?int $ignoreContentTypeId = null): bool
{
$query = ContentTypeSlugHistory::query()->where('old_slug', strtolower(trim($slug)));
if ($ignoreContentTypeId !== null) {
$query->where('content_type_id', '!=', $ignoreContentTypeId);
}
return $query->exists();
}
public function flushCaches(): void
{
Cache::forget($this->publicListCacheKey());
Cache::forget($this->slugMapCacheKey());
Cache::forget($this->historyMapCacheKey());
}
public function dynamicSitemapContentTypes(): Collection
{
return $this->publicContentTypes();
}
private function currentSlugMap(): array
{
return Cache::rememberForever($this->slugMapCacheKey(), function () {
return ContentType::query()
->ordered()
->pluck('id', 'slug')
->mapWithKeys(static fn ($id, $slug) => [strtolower((string) $slug) => (int) $id])
->all();
});
}
private function historySlugMap(): array
{
return Cache::rememberForever($this->historyMapCacheKey(), function () {
$currentSlugById = ContentType::query()
->pluck('slug', 'id')
->mapWithKeys(static fn ($slug, $id) => [(int) $id => strtolower((string) $slug)])
->all();
return ContentTypeSlugHistory::query()
->orderByDesc('id')
->get(['content_type_id', 'old_slug'])
->mapWithKeys(function (ContentTypeSlugHistory $history) use ($currentSlugById) {
$currentSlug = $currentSlugById[(int) $history->content_type_id] ?? null;
return $currentSlug !== null
? [strtolower((string) $history->old_slug) => $currentSlug]
: [];
})
->all();
});
}
private function isVirtualSlug(string $slug): bool
{
return array_key_exists($slug, (array) config('content_types.virtual_types', []));
}
private function publicListCacheKey(): string
{
return (string) config('content_types.cache.public_list_key', 'content-types.public-list');
}
private function slugMapCacheKey(): string
{
return (string) config('content_types.cache.slug_map_key', 'content-types.slug-map');
}
private function historyMapCacheKey(): string
{
return (string) config('content_types.cache.history_map_key', 'content-types.slug-history-map');
}
}