Files
WP-LIT-MCP/src/Integrations/Divi/Release/ReleaseGate.php
T

129 lines
4.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Release;
use Closure;
use LIT\MCP\Log;
use Throwable;
defined('ABSPATH') || exit;
/**
* The release status of this request and the lock every writing Divi path asks for the feature it needs (spec §5.5,
* E1). whileProbing() lets the admin check write its throwaway page whatever the status – only that post, only
* during the call, and only from code (there is no parameter or option for it).
*/
final class ReleaseGate
{
private static ?ReleaseStatus $status = null;
/** @var (Closure(): ReleaseStatus)|null */
private static ?Closure $resolver = null;
private static ?int $probe = null;
private const UNRESOLVED = 'Der Freigabe-Zustand ließ sich nicht ermitteln (Details im PHP-Error-Log).';
/** Spec §9: a status that cannot be determined locks every feature as incompatible (and is logged). */
public static function status(): ReleaseStatus
{
if (self::$status !== null) {
return self::$status;
}
try {
self::$status = (self::$resolver ?? static fn (): ReleaseStatus => (new ReleaseResolver())->resolve())();
} catch (Throwable $e) {
Log::failure('divi release status', $e);
self::$status = ReleaseStatus::of('', CompatibilityMatrix::allIncompatible(self::UNRESOLVED));
}
return self::$status;
}
/** @param (Closure(): ReleaseStatus)|null $resolver null = ReleaseResolver; tests set their own. */
public static function useResolver(?Closure $resolver): void
{
self::$resolver = $resolver;
self::$status = null;
self::$probe = null;
}
/** The next status() of this request computes again; the cached mirror results stay (see invalidate()). */
public static function forget(): void
{
self::$status = null;
}
/**
* The single way to make the release state current again: forgets the status of this request and, with
* $mirrors, also deletes the cached mirror results (transient ReleaseResolver::CACHE) so the next status()
* fingerprints Divi's code again. Without $mirrors: after a release or a withdrawal and after the admin check
* (only the source of the state changed). With $mirrors: after a theme update, a CLI check or a new reference
* (the code or the reference may have changed).
*/
public static function invalidate(bool $mirrors = false): void
{
if ($mirrors) {
delete_transient(ReleaseResolver::CACHE);
}
self::forget();
}
/** @throws DiviLocked When $feature is not usable – before anything is written. */
public static function assertFeature(DiviFeature $feature, ?int $postId = null): void
{
self::assertFeatures([$feature], $postId);
}
/**
* Every one of $features usable, else DiviLocked with the locked ones. The probe post passes whatever the status.
*
* @param list<DiviFeature> $features
* @throws DiviLocked
*/
public static function assertFeatures(array $features, ?int $postId = null): void
{
if (self::$probe !== null && $postId === self::$probe) {
return;
}
$lock = self::lockOf($features);
if ($lock !== null) {
throw $lock;
}
}
/**
* The lock of this request's status for the locked ones of $features, or null when every one is usable. Asked by
* DiviFeatureGate before a tool runs and by the Divi short form of lit/content-create (DiviContentComposer::
* refusal()); no probe exception – tool calls never write the probe page.
*
* @param list<DiviFeature> $features
*/
public static function lockOf(array $features): ?DiviLocked
{
$status = self::status();
$locked = $status->matrix->locked($features);
return $locked === [] ? null : new DiviLocked(
$status,
array_map(static fn (FeatureStatus $feature): DiviFeature => $feature->feature, $locked)
);
}
public static function whileProbing(int $postId, callable $write): mixed
{
$previous = self::$probe;
self::$probe = $postId;
try {
return $write();
} finally {
self::$probe = $previous;
}
}
}