Files
SkinbaseNova/app/Services/Sitemaps/SitemapPublishService.php

201 lines
7.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Sitemaps;
use Illuminate\Support\Facades\Cache;
final class SitemapPublishService
{
public function __construct(
private readonly SitemapBuildService $build,
private readonly SitemapReleaseCleanupService $cleanup,
private readonly SitemapReleaseManager $releases,
private readonly SitemapReleaseValidator $validator,
) {
}
/**
* @param list<string>|null $families
* @return array<string, mixed>
*/
public function buildRelease(?array $families = null, ?string $releaseId = null): array
{
return $this->withLock(function () use ($families, $releaseId): array {
return $this->buildReleaseUnlocked($families, $releaseId);
});
}
/**
* @return array<string, mixed>
*/
public function publish(?string $releaseId = null): array
{
return $this->withLock(function () use ($releaseId): array {
$manifest = $releaseId !== null
? $this->releases->readManifest($releaseId)
: $this->buildReleaseUnlocked();
if ($manifest === null) {
throw new \RuntimeException('Sitemap release [' . $releaseId . '] does not exist.');
}
$releaseId = (string) $manifest['release_id'];
$validation = $this->validator->validate($releaseId);
if (! ($validation['ok'] ?? false)) {
$manifest['status'] = 'failed';
$manifest['validation'] = $validation;
$this->releases->writeManifest($releaseId, $manifest);
throw new \RuntimeException('Sitemap release validation failed.');
}
$manifest['status'] = 'published';
$manifest['published_at'] = now()->toAtomString();
$manifest['validation'] = $validation;
$this->releases->writeManifest($releaseId, $manifest);
$this->releases->activate($releaseId);
$deleted = $this->cleanup->cleanup();
return $manifest + ['cleanup_deleted' => $deleted];
});
}
/**
* @return array<string, mixed>
*/
public function rollback(?string $releaseId = null): array
{
return $this->withLock(function () use ($releaseId): array {
if ($releaseId === null) {
$activeReleaseId = $this->releases->activeReleaseId();
foreach ($this->releases->listReleases() as $release) {
if ((string) ($release['status'] ?? '') === 'published' && (string) ($release['release_id'] ?? '') !== $activeReleaseId) {
$releaseId = (string) $release['release_id'];
break;
}
}
}
if (! is_string($releaseId) || $releaseId === '') {
throw new \RuntimeException('No rollback release is available.');
}
$manifest = $this->releases->readManifest($releaseId);
if ($manifest === null) {
throw new \RuntimeException('Rollback release [' . $releaseId . '] not found.');
}
$this->releases->activate($releaseId);
return $manifest + ['rolled_back_at' => now()->toAtomString()];
});
}
/**
* @template TReturn
* @param callable(): TReturn $callback
* @return TReturn
*/
private function withLock(callable $callback): mixed
{
$lock = Cache::lock('sitemaps:publish-flow', max(30, (int) config('sitemaps.releases.lock_seconds', 900)));
if (! $lock->get()) {
throw new \RuntimeException('Another sitemap build or publish operation is already running.');
}
try {
return $callback();
} finally {
$lock->release();
}
}
/**
* @param list<string>|null $families
* @return array<string, mixed>
*/
private function buildReleaseUnlocked(?array $families = null, ?string $releaseId = null): array
{
$selectedFamilies = $families ?: $this->build->enabledFamilies();
$releaseId ??= $this->releases->generateReleaseId();
$familyManifest = [];
$documents = [
SitemapCacheService::INDEX_DOCUMENT => $this->releases->documentRelativePath(SitemapCacheService::INDEX_DOCUMENT),
];
$totalUrls = 0;
$rootIndex = $this->build->buildIndex(true, false, $selectedFamilies);
$this->releases->putDocument($releaseId, SitemapCacheService::INDEX_DOCUMENT, (string) $rootIndex['content']);
foreach ($selectedFamilies as $family) {
$builder = app(SitemapRegistry::class)->get($family);
if ($builder === null) {
continue;
}
$canonicalNames = $this->build->canonicalDocumentNamesForFamily($family);
$shardNames = [];
$urlCount = 0;
foreach ($canonicalNames as $documentName) {
$built = $this->build->buildNamed($documentName, true, false);
if ($built === null) {
throw new \RuntimeException('Failed to build sitemap document [' . $documentName . '].');
}
$this->releases->putDocument($releaseId, $documentName, (string) $built['content']);
$documents[$documentName] = $this->releases->documentRelativePath($documentName);
if ($built['type'] !== SitemapTarget::TYPE_INDEX) {
$urlCount += (int) $built['url_count'];
}
if (str_starts_with($documentName, $family . '-') && ! str_ends_with($documentName, '-index')) {
$shardNames[] = $documentName;
}
}
$totalUrls += $urlCount;
$familyManifest[$family] = [
'family' => $family,
'entry_name' => app(SitemapShardService::class)->rootEntryName($builder),
'documents' => $canonicalNames,
'shards' => $shardNames,
'url_count' => $urlCount,
'shard_count' => count($shardNames),
'type' => $builder->name() === (string) config('sitemaps.news.google_variant_name', 'news-google')
? SitemapTarget::TYPE_GOOGLE_NEWS
: (count($shardNames) > 0 ? SitemapTarget::TYPE_INDEX : SitemapTarget::TYPE_URLSET),
];
}
$manifest = [
'release_id' => $releaseId,
'status' => 'built',
'built_at' => now()->toAtomString(),
'published_at' => null,
'families' => $familyManifest,
'documents' => $documents,
'totals' => [
'families' => count($familyManifest),
'documents' => count($documents),
'urls' => $totalUrls,
],
];
$this->releases->writeManifest($releaseId, $manifest);
$validation = $this->validator->validate($releaseId);
$manifest['status'] = ($validation['ok'] ?? false) ? 'validated' : 'failed';
$manifest['validation'] = $validation;
$this->releases->writeManifest($releaseId, $manifest);
$this->releases->writeBuildReport($releaseId, $validation);
return $manifest;
}
}