Snapshot of main at 9f39a6e1e (2026-11-02)
This commit is contained in:
commit
6ccf1792d7
2580 files changed
+1040202
No files matched your search
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Translation\FileLoader;
|
||||
|
||||
/**
|
||||
* What the platform folder (platform_path(), see PlatformServiceProvider) overrides, compared with the code.
|
||||
* Overrides go stale without anyone noticing: a translation key is the English source sentence, so a reworded
|
||||
* sentence leaves its override unused, and a copied template misses later fixes to its original. check() reports
|
||||
* that; platform:override copies a template with a fingerprint of its original, so a later change shows.
|
||||
*/
|
||||
class PlatformOverrides
|
||||
{
|
||||
/** First line of a template copied by platform:override: the original's path and its sha1 at the time. */
|
||||
public const FINGERPRINT = '/^\{\{-- platform:override of (\S+) sha1:([0-9a-f]{40}) --\}\}/';
|
||||
|
||||
public function __construct(private Filesystem $files)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Findings, each ['level' => 'ok'|'info'|'warn'|'fail', 'message' => string].
|
||||
*
|
||||
* @return array<int, array{level: string, message: string}>
|
||||
*/
|
||||
public function check(): array
|
||||
{
|
||||
$root = platform_path();
|
||||
if (!is_dir($root)) {
|
||||
return [['level' => 'ok', 'message' => "No platform folder ({$root}): this installation runs on config/ and the files in the code."]];
|
||||
}
|
||||
|
||||
return [
|
||||
...$this->checkSources(),
|
||||
...$this->checkPublicLink(),
|
||||
...$this->checkTranslations(),
|
||||
...$this->checkTemplates(),
|
||||
];
|
||||
}
|
||||
|
||||
private function checkSources(): array
|
||||
{
|
||||
$found = [];
|
||||
$name = (string) config('app.timebank_config');
|
||||
|
||||
if (is_file(platform_path('config.php'))) {
|
||||
$found[] = is_file(config_path("{$name}.php"))
|
||||
? ['level' => 'warn', 'message' => "Both platform/config.php and config/{$name}.php exist; the platform folder wins. Remove config/{$name}.php once the site runs well."]
|
||||
: ['level' => 'ok', 'message' => 'Platform config: platform/config.php'];
|
||||
} else {
|
||||
$found[] = ['level' => 'info', 'message' => "No platform/config.php: the platform config is config/{$name}.php."];
|
||||
}
|
||||
|
||||
if (is_file(platform_path('theme.php'))) {
|
||||
$theme = require platform_path('theme.php');
|
||||
$found[] = isset($theme['id'])
|
||||
? ['level' => 'ok', 'message' => "Platform theme: platform/theme.php (id {$theme['id']})"]
|
||||
: ['level' => 'warn', 'message' => "platform/theme.php has no 'id': it runs as theme 'platform'. Give it an id."];
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
private function checkPublicLink(): array
|
||||
{
|
||||
$target = platform_path('public');
|
||||
if (!is_dir($target)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$link = public_path('platform');
|
||||
|
||||
return is_link($link) && readlink($link) === $target
|
||||
? [['level' => 'ok', 'message' => 'Platform public files linked at /platform.']]
|
||||
: [['level' => 'warn', 'message' => 'platform/public is not served: run php artisan platform:link.']];
|
||||
}
|
||||
|
||||
/** The translations as the code alone defines them: every path the translator has, except the platform's. */
|
||||
private function baseLoader(): FileLoader
|
||||
{
|
||||
$loader = app('translator')->getLoader();
|
||||
$platformLang = platform_path('lang');
|
||||
$base = new FileLoader($this->files, array_values(array_filter($loader->paths(), fn ($path) => $path !== $platformLang)));
|
||||
|
||||
foreach ($loader->namespaces() as $namespace => $hint) {
|
||||
$base->addNamespace($namespace, $hint);
|
||||
}
|
||||
foreach ($loader->jsonPaths() as $path) {
|
||||
$base->addJsonPath($path);
|
||||
}
|
||||
|
||||
return $base;
|
||||
}
|
||||
|
||||
private function checkTranslations(): array
|
||||
{
|
||||
$lang = platform_path('lang');
|
||||
if (!is_dir($lang)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$base = $this->baseLoader();
|
||||
$platformViews = $this->platformTemplateSource();
|
||||
$found = [];
|
||||
|
||||
foreach ($this->files->allFiles($lang) as $file) {
|
||||
$relative = str_replace('\\', '/', $file->getRelativePathname());
|
||||
|
||||
if (preg_match('#^([A-Za-z_-]+)\.json$#', $relative, $m)) {
|
||||
$overrides = json_decode($this->files->get($file->getPathname()), true);
|
||||
if (!is_array($overrides)) {
|
||||
$found[] = ['level' => 'fail', 'message' => "lang/{$relative} is not valid JSON: none of its texts are used."];
|
||||
continue;
|
||||
}
|
||||
// A key the platform's own templates use is the platform's own text, not a stale override
|
||||
$baseTexts = $base->load($m[1], '*', '*');
|
||||
$unused = array_values(array_filter(array_keys($overrides), fn ($key) => !array_key_exists($key, $baseTexts)
|
||||
&& !str_contains($platformViews, $key)));
|
||||
} elseif (preg_match('#^vendor/([^/]+)/([^/]+)/(.+)\.php$#', $relative, $m)) {
|
||||
$unused = $this->unusedKeys($file->getPathname(), $base->load($m[2], $m[3], $m[1]));
|
||||
} elseif (preg_match('#^([^/]+)/(.+)\.php$#', $relative, $m)) {
|
||||
$unused = $this->unusedKeys($file->getPathname(), $base->load($m[1], $m[2]));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($unused) {
|
||||
$found[] = ['level' => 'warn', 'message' => "lang/{$relative}: " . count($unused) . ' key(s) not in the code (reworded or removed there): '
|
||||
. implode(' | ', array_map(fn ($key) => mb_strimwidth($key, 0, 60, '...'), array_slice($unused, 0, 5)))
|
||||
. (count($unused) > 5 ? ' ...' : '')];
|
||||
}
|
||||
}
|
||||
|
||||
return $found ?: [['level' => 'ok', 'message' => 'Translation overrides: none stale.']];
|
||||
}
|
||||
|
||||
private function unusedKeys(string $file, array $base): array
|
||||
{
|
||||
$overrides = require $file;
|
||||
|
||||
return is_array($overrides) ? array_keys(array_diff_key(Arr::dot($overrides), Arr::dot($base))) : [];
|
||||
}
|
||||
|
||||
private function platformTemplateSource(): string
|
||||
{
|
||||
$views = platform_path('views');
|
||||
|
||||
return is_dir($views)
|
||||
? implode("\n", array_map(fn ($file) => $this->files->get($file->getPathname()), $this->files->allFiles($views)))
|
||||
: '';
|
||||
}
|
||||
|
||||
private function checkTemplates(): array
|
||||
{
|
||||
$views = platform_path('views');
|
||||
if (!is_dir($views)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$found = [];
|
||||
foreach ($this->files->allFiles($views) as $file) {
|
||||
$relative = str_replace('\\', '/', $file->getRelativePathname());
|
||||
$original = $this->originalOf($relative);
|
||||
|
||||
if ($original === null) {
|
||||
$found[] = ['level' => 'info', 'message' => "views/{$relative}: the platform's own template (no original in the code)."];
|
||||
continue;
|
||||
}
|
||||
|
||||
$firstLine = strtok($this->files->get($file->getPathname()), "\n");
|
||||
if (!preg_match(self::FINGERPRINT, (string) $firstLine, $m)) {
|
||||
$found[] = ['level' => 'info', 'message' => "views/{$relative}: copied without a fingerprint; cannot tell whether the original changed since (copy with php artisan platform:override)."];
|
||||
} elseif (sha1_file($original) !== $m[2]) {
|
||||
$found[] = ['level' => 'warn', 'message' => "views/{$relative}: the original changed since it was copied. Compare with " . $this->shown($original) . ', then copy the changes over and update the fingerprint.'];
|
||||
}
|
||||
}
|
||||
|
||||
return $found ?: [['level' => 'ok', 'message' => 'Template overrides: none stale.']];
|
||||
}
|
||||
|
||||
/**
|
||||
* The code's file that a platform template overrides, or null. $relative is the path inside platform/views:
|
||||
* mail/<file> for the mail components, vendor/<package>/<file> for package views, else an app view.
|
||||
*/
|
||||
public function originalOf(string $relative): ?string
|
||||
{
|
||||
$platformViews = platform_path('views');
|
||||
|
||||
if (str_starts_with($relative, 'mail/')) {
|
||||
$candidates = [
|
||||
...array_filter(config('mail.markdown.paths', []), fn ($path) => $path !== "{$platformViews}/mail"),
|
||||
base_path('vendor/laravel/framework/src/Illuminate/Mail/resources/views'),
|
||||
];
|
||||
$file = substr($relative, strlen('mail/'));
|
||||
} elseif (preg_match('#^vendor/([^/]+)/(.+)$#', $relative, $m)) {
|
||||
$candidates = array_filter(View::getFinder()->getHints()[$m[1]] ?? [], fn ($path) => !str_starts_with($path, $platformViews));
|
||||
$file = $m[2];
|
||||
} else {
|
||||
$candidates = array_filter(View::getFinder()->getPaths(), fn ($path) => $path !== $platformViews);
|
||||
$file = $relative;
|
||||
}
|
||||
|
||||
foreach ($candidates as $path) {
|
||||
if (is_file("{$path}/{$file}")) {
|
||||
return "{$path}/{$file}";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function shown(string $path): string
|
||||
{
|
||||
return str_starts_with($path, base_path() . '/') ? substr($path, strlen(base_path()) + 1) : $path;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user