Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased](https://github.com/Pollora/framework/compare/v13.34.0-beta...develop)

### Added
- `pollora:doctor`: checks a project for the failures that stay silent — the site renders, the command exits 0 — and prints, under each, the command that fixes it. Each check comes from a failure met in practice: WordPress core not patched or `__()` not Pollora's; `patches.lock.json` missing or older than the framework's patches; `.env` names Pollora does not read (`DB_NAME`, `WP_HOME`…) or MySQL settings on a sqlite connection; configuration or routes cached outside production; classes missing from the discovery cache; for the theme, every Pollora plugin and every enabled module: a build missing, written to another folder than Pollora reads, or a hot file pointing at a dev server that is stopped or not exposed, a directory symlinked under another name, `%theme_*%`/`%plugin_*%` placeholders or `.stub` files left from a copied template, blocks still in the legacy `resources/blocks`; pattern files WordPress never registers or has not cached; `Route::wp()` routes answering in place of a block theme's templates. `--json` for scripts; exits 1 on an error
- The same checks in WordPress's **Site Health** (Tools › Site Health), with a "Pollora" badge, plus one that only a web request can make: every block of the theme, the Pollora plugins and the modules is registered. That is the boot a visitor gets — blocks were once registered under WP-CLI but not over HTTP, which a console check would have passed

### Fixed
- While Vite ran hot, its client was enqueued with WordPress's version appended (`@vite/client?ver=7.1.2`). The modules Vite serves import `/@vite/client` by its bare URL, so the browser loaded the client twice, as two modules with two HMR connections. It is enqueued with no version, like the entries (regression from v13.34.0-beta)
- On a branch install (`dev-develop`, `13.x-dev`), Site Health and the admin notice announced "Pollora 13.4.4 is available": `version_compare()` ranks a branch name below every release. A development build is no longer compared with releases — Site Health says it is one, the notice stays silent — and the dashboard, the admin menu badge and `pollora:status` share that one rule, which they each duplicated and missed `13.x-dev`. Site Health's info tab now labels the compared version "Latest stable version", since pre-releases are not counted
Expand Down
40 changes: 40 additions & 0 deletions src/Discovery/Infrastructure/Services/DiscoveryCacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,46 @@ public function clearCache(iterable $locations): void
}
}

/**
* Classes on disk that the cached entry for a location does not list.
*
* They stay invisible to discovery — no hook, no post type, no route — until
* the cache is cleared, and nothing says so. Null when the location has no
* cached entry, or the cache is off, since then discovery reads the disk.
*
* @return list<string>|null
*/
public function classesMissingFromCache(DiscoveryLocationInterface $location): ?array
{
if (! $this->shouldUseCache()) {
return null;
}

$cacheId = $this->generateCacheId($location);

if (! $this->cacheDriver->has($cacheId)) {
return null;
}

$names = static fn (array $structures): array => array_map(
static fn (mixed $structure): string => is_object($structure) && method_exists($structure, 'getFcqn') ? $structure->getFcqn() : (string) $structure,
$structures,
);

return array_values(array_diff(
$names(Discover::in($location->getPath())->full()->get()),
$names($this->cacheDriver->get($cacheId)),
));
}

/**
* Whether discovery reads through a persistent cache (it does not in debug mode).
*/
public function isCacheEnabled(): bool
{
return $this->shouldUseCache();
}

/**
* Get the cache driver instance.
*/
Expand Down
63 changes: 63 additions & 0 deletions src/Doctor/Application/Services/Doctor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace Pollora\Doctor\Application\Services;

use Pollora\Doctor\Domain\Contracts\CheckInterface;
use Pollora\Doctor\Domain\Enums\RunContext;
use Pollora\Doctor\Domain\Models\CheckResult;
use Throwable;

/**
* Runs the checks that belong to a context.
*
* A check that throws is reported as an error of that check, with the exception's
* message: one broken check must not hide the verdict of all the others.
*/
final readonly class Doctor
{
/**
* @param iterable<CheckInterface> $checks
*/
public function __construct(private iterable $checks) {}

/**
* @return list<CheckInterface>
*/
public function checksFor(RunContext $context): array
{
$checks = [];

foreach ($this->checks as $check) {
if (in_array($context, $check->runsIn(), true)) {
$checks[] = $check;
}
}

return $checks;
}

public function runCheck(CheckInterface $check, RunContext $context): CheckResult
{
try {
return $check->run($context);
} catch (Throwable $throwable) {
return CheckResult::error(
'The check itself failed: '.$throwable->getMessage(),
[$throwable::class.' in '.$throwable->getFile().':'.$throwable->getLine()],
);
}
}

/**
* @return list<array{check: CheckInterface, result: CheckResult}>
*/
public function run(RunContext $context): array
{
return array_map(
fn (CheckInterface $check): array => ['check' => $check, 'result' => $this->runCheck($check, $context)],
$this->checksFor($context),
);
}
}
33 changes: 33 additions & 0 deletions src/Doctor/Domain/Contracts/CheckInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Pollora\Doctor\Domain\Contracts;

use Pollora\Doctor\Domain\Enums\RunContext;
use Pollora\Doctor\Domain\Models\CheckResult;

/**
* One thing `pollora:doctor` and Site Health look at.
*
* A check earns its place by naming a failure that has happened and stays silent
* — the site renders, the command exits 0 — and by saying how to fix it. It
* reads; it never writes, and never requests the site over HTTP.
*/
interface CheckInterface
{
/** Stable identifier, kebab-case: the key of the JSON output and of the Site Health test. */
public function id(): string;

/** What is checked, for a person. */
public function label(): string;

/**
* Where the check can reach a verdict it can stand behind.
*
* @return list<RunContext>
*/
public function runsIn(): array;

public function run(RunContext $context): CheckResult;
}
19 changes: 19 additions & 0 deletions src/Doctor/Domain/Enums/CheckStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Pollora\Doctor\Domain\Enums;

/**
* The verdict of one doctor check.
*
* Skipped is a verdict of its own, never folded into Ok: a check that could not
* run says so, rather than passing a site it never looked at.
*/
enum CheckStatus: string
{
case Ok = 'ok';
case Warning = 'warning';
case Error = 'error';
case Skipped = 'skipped';
}
18 changes: 18 additions & 0 deletions src/Doctor/Domain/Enums/RunContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Pollora\Doctor\Domain\Enums;

/**
* Where a check runs.
*
* Some failures only exist in a web request — blocks registered under WP-CLI but
* not over HTTP, once — so those checks run in WordPress's Site Health, never in
* the console, where they would pass a broken site.
*/
enum RunContext: string
{
case Console = 'console';
case Http = 'http';
}
66 changes: 66 additions & 0 deletions src/Doctor/Domain/Models/CheckResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Pollora\Doctor\Domain\Models;

use Pollora\Doctor\Domain\Enums\CheckStatus;

/**
* What a check found: a one-line summary, the details that prove it, and, when
* something is wrong, what to do about it.
*/
final readonly class CheckResult
{
/**
* @param list<string> $details
*/
private function __construct(
public CheckStatus $status,
public string $summary,
public array $details = [],
public ?string $fix = null,
) {}

/**
* @param list<string> $details
*/
public static function ok(string $summary, array $details = []): self
{
return new self(CheckStatus::Ok, $summary, $details);
}

/**
* @param list<string> $details
*/
public static function warning(string $summary, array $details = [], ?string $fix = null): self
{
return new self(CheckStatus::Warning, $summary, $details, $fix);
}

/**
* @param list<string> $details
*/
public static function error(string $summary, array $details = [], ?string $fix = null): self
{
return new self(CheckStatus::Error, $summary, $details, $fix);
}

public static function skipped(string $reason): self
{
return new self(CheckStatus::Skipped, $reason);
}

/**
* @return array{status: string, summary: string, details: list<string>, fix: ?string}
*/
public function toArray(): array
{
return [
'status' => $this->status->value,
'summary' => $this->summary,
'details' => $this->details,
'fix' => $this->fix,
];
}
}
Loading
Loading