201 lines
7.2 KiB
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;
|
|
}
|
|
} |