Files
fotospiel-app/app/Services/Help/HelpSyncService.php

142 lines
4.8 KiB
PHP

<?php
namespace App\Services\Help;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\MarkdownConverter;
use RuntimeException;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\Yaml\Yaml;
class HelpSyncService
{
private MarkdownConverter $converter;
public function __construct(private readonly Filesystem $files)
{
$environment = new Environment;
$environment->addExtension(new CommonMarkCoreExtension);
$environment->addExtension(new TableExtension);
$this->converter = new MarkdownConverter($environment);
}
/**
* @return array<string, array<string, int>>
*/
public function sync(): array
{
$sourcePath = base_path(config('help.source_path'));
if (! $this->files->exists($sourcePath)) {
throw new RuntimeException('Help source directory not found: '.$sourcePath);
}
$articles = collect();
$audiences = config('help.audiences', []);
$localeDirectories = $this->files->directories($sourcePath);
$useLocaleDirectories = ! empty($localeDirectories);
$audienceRoots = $useLocaleDirectories
? array_map(static fn (string $path) => $path.DIRECTORY_SEPARATOR, $localeDirectories)
: [$sourcePath.DIRECTORY_SEPARATOR];
foreach ($audienceRoots as $root) {
foreach ($audiences as $audience) {
$audiencePath = $root.$audience;
if (! $this->files->isDirectory($audiencePath)) {
continue;
}
$files = $this->files->allFiles($audiencePath);
/** @var SplFileInfo $file */
foreach ($files as $file) {
if ($file->getExtension() !== 'md') {
continue;
}
$parsed = $this->parseFile($file);
$articles->push($parsed);
}
}
}
$disk = config('help.disk');
$compiledPath = trim(config('help.compiled_path'), '/');
$written = [];
foreach ($articles->groupBy(fn ($article) => $article['audience'].'::'.$article['locale']) as $key => $group) {
[$audience, $locale] = explode('::', $key);
$path = sprintf('%s/%s/%s/articles.json', $compiledPath, $audience, $locale);
Storage::disk($disk)->put($path, $group->values()->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
Cache::forget($this->cacheKey($audience, $locale));
$written[$audience][$locale] = $group->count();
}
return $written;
}
private function parseFile(SplFileInfo $file): array
{
$contents = $this->files->get($file->getPathname());
if (! Str::startsWith($contents, "---\n")) {
throw new RuntimeException('Missing front matter in '.$file->getPathname());
}
$pattern = '/^---\s*\n(?P<yaml>.*?)-{3}\s*\n(?P<body>.*)$/s';
if (! preg_match($pattern, $contents, $matches)) {
throw new RuntimeException('Unable to parse front matter for '.$file->getPathname());
}
$frontMatter = Yaml::parse(trim($matches['yaml'] ?? '')) ?? [];
$frontMatter = array_map(static fn ($value) => $value ?? null, $frontMatter);
$this->validateFrontMatter($frontMatter, $file->getPathname());
$body = trim($matches['body'] ?? '');
$html = trim($this->converter->convert($body)->getContent());
$updatedAt = now()->setTimestamp($file->getMTime())->toISOString();
return array_merge($frontMatter, [
'body_markdown' => $body,
'body_html' => $html,
'source_path' => $file->getRelativePathname(),
'updated_at' => $updatedAt,
]);
}
private function validateFrontMatter(array $frontMatter, string $path): void
{
$required = config('help.required_front_matter', []);
foreach ($required as $key) {
if (! Arr::exists($frontMatter, $key)) {
throw new RuntimeException(sprintf('Missing front matter key "%s" in %s', $key, $path));
}
}
$audiences = config('help.audiences', []);
$audience = $frontMatter['audience'];
if (! in_array($audience, $audiences, true)) {
throw new RuntimeException(sprintf('Invalid audience "%s" in %s', $audience, $path));
}
}
private function cacheKey(string $audience, string $locale): string
{
return sprintf('help.%s.%s', $audience, $locale);
}
}