$visiblePackages
+ */
+ public function render(array $packages, array $visiblePackages): void
+ {
+ ?>
+
+
+
+
+
+
+ renderNotice(); ?>
+ renderViews($packages); ?>
+
+
+
+ $packages */
+ private function renderTableNav(array $packages, string $which): void
+ {
+ ?>
+
+ renderBulkActions($which); ?>
+
+
+ itemCountLabel(count($packages))); ?>
+
+
+
+
+
+
+
+
+
+
+ $packages */
+ private function renderTable(array $packages): void
+ {
+ ?>
+
+
+ renderTableHeader(); ?>
+
+
+
+
+ |
+
+ |
+
+
+
+
+ renderRow($package); ?>
+
+
+
+ renderTableHeader('cb-select-all-2'); ?>
+
+
+
+
+ |
+ renderSelectAllCheckbox($checkboxId); ?>
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ active() ? 'active' : 'inactive';
+
+ ?>
+
+ |
+ renderRowCheckbox($package); ?>
+ |
+
+ name()); ?>
+ renderRowActions($package); ?>
+
+ |
+
+
+ description($package)); ?>
+
+
+ metaLine($package)); ?>
+
+ |
+
+ typeLabel()); ?>
+ |
+
+ statusLabel()); ?>
+ |
+
+
+
+
+ hasBulkAction($package)) {
+ echo ' ';
+
+ return;
+ }
+
+ $id = sprintf('package-%s', sanitize_html_class($package->package()));
+
+ ?>
+
+
+ rowActions($package);
+
+ if ($actions === []) {
+ return;
+ }
+
+ echo '';
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ echo implode(' | ', $actions);
+ echo '
';
+ }
+
+ /** @return list */
+ private function rowActions(PackageMetadata $package): array
+ {
+ if ($package->isMustUsePlugin()) {
+ return [
+ sprintf(
+ '%s',
+ esc_html__('Must-use', 'sympress-kernel'),
+ ),
+ ];
+ }
+
+ $actions = [];
+
+ if ($this->isActionAvailable('deactivate', $package)) {
+ $actions[] = $this->actionLink('deactivate', $package, __('Deactivate'));
+ }
+
+ if ($this->isActionAvailable('activate', $package)) {
+ $actions[] = $this->actionLink('activate', $package, __('Activate'));
+ }
+
+ if ($this->isActionAvailable('delete', $package)) {
+ $actions[] = $this->actionLink('delete', $package, __('Delete'), 'delete');
+ }
+
+ return array_values(array_filter($actions));
+ }
+
+ private function actionLink(
+ string $action,
+ PackageMetadata $package,
+ string $label,
+ string $class = '',
+ ): string {
+
+ if (!$this->canRun($action, $package)) {
+ return '';
+ }
+
+ $attributes = [
+ 'href' => esc_url($this->actionUrl($action, $package)),
+ ];
+
+ if ($class !== '') {
+ $attributes['class'] = $class;
+ }
+
+ if ($action === 'delete') {
+ $confirm = wp_json_encode(
+ __('Are you sure you want to delete this package?', 'sympress-kernel'),
+ );
+ $attributes['onclick'] = sprintf(
+ 'return confirm(%s);',
+ is_string($confirm) ? $confirm : '""',
+ );
+ }
+
+ return sprintf(
+ '%s',
+ esc_attr($action),
+ $this->htmlAttributes($attributes),
+ esc_html($label),
+ );
+ }
+
+ /** @param list $packages */
+ private function renderViews(array $packages): void
+ {
+ $counts = $this->counts($packages);
+ $views = [
+ 'all' => __('All'),
+ 'active' => __('Active'),
+ 'inactive' => __('Inactive'),
+ 'mustuse' => __('Must-Use', 'sympress-kernel'),
+ 'plugins' => __('Plugins'),
+ 'themes' => __('Themes'),
+ ];
+ $items = [];
+
+ foreach ($views as $view => $label) {
+ if (($counts[$view] ?? 0) < 1 && $view !== 'all') {
+ continue;
+ }
+
+ $items[] = sprintf(
+ '%4$s '
+ . '(%5$s)',
+ esc_attr($view),
+ esc_url($this->viewUrl($view)),
+ $this->currentView === $view ? ' class="current" aria-current="page"' : '',
+ esc_html($label),
+ esc_html((string) ($counts[$view] ?? 0)),
+ );
+ }
+
+ echo '';
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ echo implode(' | ', $items);
+ echo '
';
+ }
+
+ private function renderNotice(): void
+ {
+ if ($this->notice === '') {
+ return;
+ }
+
+ $class = $this->notice === 'error' ? 'notice notice-error' : 'notice notice-success';
+
+ printf(
+ '',
+ esc_attr($class),
+ esc_html($this->noticeMessage),
+ );
+ }
+
+ private function hasBulkAction(PackageMetadata $package): bool
+ {
+ foreach (['activate', 'deactivate', 'delete'] as $action) {
+ if ($this->isActionAvailable($action, $package) && $this->canRun($action, $package)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private function isActionAvailable(string $action, PackageMetadata $package): bool
+ {
+ return ($this->isActionAvailable)($action, $package);
+ }
+
+ private function canRun(string $action, PackageMetadata $package): bool
+ {
+ return ($this->canRun)($action, $package);
+ }
+
+ private function actionUrl(string $action, PackageMetadata $package): string
+ {
+ return ($this->actionUrl)($action, $package);
+ }
+
+ private function viewUrl(string $view): string
+ {
+ return ($this->viewUrl)($view);
+ }
+
+ private function description(PackageMetadata $package): string
+ {
+ if ($package->description() !== '') {
+ return $package->description();
+ }
+
+ return __('No description available.', 'sympress-kernel');
+ }
+
+ private function metaLine(PackageMetadata $package): string
+ {
+ $items = [
+ sprintf(
+ /* translators: %s: Package type. */
+ __('Type: %s', 'sympress-kernel'),
+ $package->typeLabel(),
+ ),
+ sprintf(
+ /* translators: %s: Composer package name. */
+ __('Package: %s', 'sympress-kernel'),
+ $package->package(),
+ ),
+ sprintf(
+ /* translators: %s: Package entry file or theme stylesheet. */
+ __('Entry: %s', 'sympress-kernel'),
+ $package->entry(),
+ ),
+ ];
+
+ if ($package->version() !== '') {
+ array_unshift(
+ $items,
+ sprintf(
+ /* translators: %s: Package version. */
+ __('Version %s'),
+ $package->version(),
+ ),
+ );
+ }
+
+ return implode(' | ', $items);
+ }
+
+ /**
+ * @param list $packages
+ * @return array
+ */
+ private function counts(array $packages): array
+ {
+ $counts = [
+ 'all' => count($packages),
+ 'active' => 0,
+ 'inactive' => 0,
+ 'mustuse' => 0,
+ 'plugins' => 0,
+ 'themes' => 0,
+ ];
+
+ foreach ($packages as $package) {
+ ++$counts[$package->active() ? 'active' : 'inactive'];
+
+ if ($package->isMustUsePlugin()) {
+ ++$counts['mustuse'];
+ }
+
+ if ($package->isPlugin()) {
+ ++$counts['plugins'];
+ }
+
+ if (!$package->isTheme()) {
+ continue;
+ }
+
+ ++$counts['themes'];
+ }
+
+ return $counts;
+ }
+
+ /** @param array $attributes */
+ private function htmlAttributes(array $attributes): string
+ {
+ $parts = [];
+
+ foreach ($attributes as $name => $value) {
+ $parts[] = sprintf('%s="%s"', esc_attr($name), esc_attr($value));
+ }
+
+ return implode(' ', $parts);
+ }
+}
diff --git a/src/App.php b/src/App.php
index dbb8bc8..4dd6e0f 100644
--- a/src/App.php
+++ b/src/App.php
@@ -4,7 +4,6 @@
namespace SymPress\Kernel;
-use SymPress\Kernel\Bundle\BundleRegistry;
use SymPress\Kernel\Hook\HookLoader;
use SymPress\Kernel\Kernel\KernelInterface;
use SymPress\Kernel\Kernel\SiteKernel;
@@ -24,9 +23,9 @@ final class App
private static ?self $app = null;
private ?Container $container = null;
- private ?BundleRegistry $bundles = null;
private bool $booted = false;
private bool $booting = false;
+ private ?bool $debugEnabled = null;
private function __construct(
private readonly KernelInterface $kernel,
@@ -96,11 +95,17 @@ public static function handleThrowable(\Throwable $throwable): void
public function enableDebug(): self
{
+ $this->debugEnabled = true;
+ $this->applyDebugState();
+
return $this;
}
public function disableDebug(): self
{
+ $this->debugEnabled = false;
+ $this->applyDebugState();
+
return $this;
}
@@ -122,43 +127,43 @@ public function boot(): void
$this->booting = true;
self::dispatchAction(self::ACTION_BOOTING, $this->kernel);
- $this->initializeContainer();
- $this->bundles = $this->kernel->discoverBundles();
+ $container = $this->initializeContainer();
+ $bundles = $this->kernel->discoverBundles();
- if (!$this->kernel->tryUseRuntimeContainer($this->container, $this->bundles)) {
- self::dispatchAction(self::ACTION_BEFORE_CONTAINER_BUILD, $this->container, $this->bundles);
+ if (!$this->kernel->tryUseRuntimeContainer($container, $bundles)) {
+ self::dispatchAction(self::ACTION_BEFORE_CONTAINER_BUILD, $container, $bundles);
self::dispatchAction(self::LEGACY_ACTION_BEFORE_CONTAINER_BUILD);
$loadedConfigFiles = $this->kernel->configureContainer(
- $this->container->builder(),
- $this->container,
- $this->bundles,
+ $container->builder(),
+ $container,
+ $bundles,
);
self::dispatchAction(
self::ACTION_CONTAINER_CONFIGURED,
- $this->container,
- $this->bundles,
+ $container,
+ $bundles,
$loadedConfigFiles,
);
$this->kernel->createRuntimeContainer(
- $this->container,
- $this->bundles,
+ $container,
+ $bundles,
$loadedConfigFiles,
);
}
- self::dispatchAction(self::ACTION_CONTAINER_READY, $this->container, $this->bundles);
- self::dispatchAction(self::LEGACY_ACTION_CONTAINER_READY, $this->container);
+ self::dispatchAction(self::ACTION_CONTAINER_READY, $container, $bundles);
+ self::dispatchAction(self::LEGACY_ACTION_CONTAINER_READY, $container);
+ $this->applyDebugState();
$this->registerHooks();
- $this->kernel->boot($this->container, $this->bundles);
+ $this->kernel->boot($container, $bundles);
$this->booted = true;
$this->booting = false;
- self::dispatchAction(self::ACTION_BOOTED, $this->container, $this->bundles);
- self::dispatchAction(self::LEGACY_ACTION_CONTAINER_LOADED, $this->container);
+ self::dispatchAction(self::ACTION_BOOTED, $container, $bundles);
+ self::dispatchAction(self::LEGACY_ACTION_CONTAINER_LOADED, $container);
} catch (\Throwable $throwable) {
$this->booted = false;
$this->booting = false;
$this->container = null;
- $this->bundles = null;
self::handleThrowable($throwable);
throw $throwable;
}
@@ -173,9 +178,9 @@ public function resolve(string $id, mixed $default = null): mixed
throw new \DomainException("Can't resolve from an uninitialised application.");
}
- $this->initializeContainer();
+ $container = $this->initializeContainer();
- if (!$this->container->has($id)) {
+ if (!$container->has($id)) {
if (function_exists('do_action')) {
do_action(
self::ACTION_ERROR,
@@ -186,7 +191,7 @@ public function resolve(string $id, mixed $default = null): mixed
return $default;
}
- $value = $this->container->get($id);
+ $value = $container->get($id);
} catch (\Throwable $throwable) {
self::handleThrowable($throwable);
}
@@ -194,14 +199,17 @@ public function resolve(string $id, mixed $default = null): mixed
return $value;
}
- private function initializeContainer(): void
+ private function initializeContainer(): Container
{
if ($this->container instanceof Container) {
- return;
+ return $this->container;
}
- $this->container = $this->kernel->createContainer();
- $this->container->setApp($this);
+ $container = $this->kernel->createContainer();
+ $container->setApp($this);
+ $this->container = $container;
+
+ return $container;
}
private function registerHooks(): void
@@ -219,6 +227,26 @@ private function registerHooks(): void
$hookLoader->register();
}
+ private function applyDebugState(): void
+ {
+ if (
+ !$this->container instanceof Container
+ || $this->debugEnabled === null
+ || !$this->container->has('profiler')
+ ) {
+ return;
+ }
+
+ $profiler = $this->container->get('profiler');
+ $method = $this->debugEnabled ? 'enable' : 'disable';
+
+ if (!is_object($profiler) || !method_exists($profiler, $method)) {
+ return;
+ }
+
+ $profiler->{$method}();
+ }
+
private static function defaultProjectDir(): string
{
$directory = __DIR__;
@@ -257,7 +285,7 @@ private static function isProjectComposerFile(string $composerFile): bool
private static function dispatchAction(string $action, mixed ...$arguments): void
{
- if (!function_exists('do_action')) {
+ if ($action === '' || !function_exists('do_action')) {
return;
}
diff --git a/src/Bundle/AbstractBundle.php b/src/Bundle/AbstractBundle.php
index 84c138c..a7db024 100644
--- a/src/Bundle/AbstractBundle.php
+++ b/src/Bundle/AbstractBundle.php
@@ -191,7 +191,6 @@ public function getContainerExtension(): ?ExtensionInterface
);
}
- // @phpstan-ignore new.internalClass, method.internalClass
$this->extension = new BundleExtension($this, $this->extensionAlias);
return $this->extension;
diff --git a/src/Bundle/BundleInterface.php b/src/Bundle/BundleInterface.php
index d204484..32e9ac3 100644
--- a/src/Bundle/BundleInterface.php
+++ b/src/Bundle/BundleInterface.php
@@ -5,7 +5,7 @@
namespace SymPress\Kernel\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\HttpKernel\Bundle\BundleInterface as SymfonyBundleInterface;
+use Symfony\Component\DependencyInjection\Kernel\BundleInterface as SymfonyBundleInterface;
interface BundleInterface extends SymfonyBundleInterface
{
diff --git a/src/Console/ContainerDumpCommand.php b/src/Console/ContainerDumpCommand.php
index 93172af..3586d5c 100644
--- a/src/Console/ContainerDumpCommand.php
+++ b/src/Console/ContainerDumpCommand.php
@@ -33,7 +33,8 @@ protected function configure(): void
protected function execute(InputInterface $input, OutputInterface $output): int
{
- $format = strtolower((string) $input->getOption('format'));
+ $format = $input->getOption('format');
+ $format = is_string($format) ? strtolower($format) : 'yaml';
$builder = $this->container->builder();
if ($format === 'yaml') {
diff --git a/src/Console/DebugContainerCommand.php b/src/Console/DebugContainerCommand.php
index 1b56e74..8d620da 100644
--- a/src/Console/DebugContainerCommand.php
+++ b/src/Console/DebugContainerCommand.php
@@ -40,7 +40,8 @@ protected function configure(): void
protected function execute(InputInterface $input, OutputInterface $output): int
{
- $search = strtolower((string) $input->getArgument('search'));
+ $searchArgument = $input->getArgument('search');
+ $search = is_string($searchArgument) ? strtolower($searchArgument) : '';
$items = $this->items($input);
foreach ($items as $item) {
@@ -91,7 +92,7 @@ private function serviceIds(): array
$runtimeContainer instanceof SymfonyContainerInterface
&& method_exists($runtimeContainer, 'getServiceIds')
) {
- $ids = $runtimeContainer->getServiceIds();
+ $ids = $this->stringList($runtimeContainer->getServiceIds());
sort($ids);
return $ids;
@@ -120,6 +121,21 @@ private function taggedServices(string $tag, bool $types, bool $showArguments):
);
}
+ /** @return list */
+ private function stringList(mixed $values): array
+ {
+ if (!is_array($values)) {
+ return [];
+ }
+
+ return array_values(
+ array_filter(
+ $values,
+ static fn (mixed $value): bool => is_string($value),
+ ),
+ );
+ }
+
/** @return list */
private function describedServices(bool $showArguments): array
{
diff --git a/src/Container.php b/src/Container.php
index 86d49fa..a9655e0 100644
--- a/src/Container.php
+++ b/src/Container.php
@@ -24,6 +24,8 @@ final class Container implements SymfonyContainerInterface
private ?PsrContainerInterface $runtimeContainer = null;
private ?App $app = null;
private ?KernelInterface $kernel = null;
+ private SiteConfig $config;
+ private WpContext $context;
/** @var array */
private array $containers;
@@ -38,13 +40,10 @@ public function __construct(
$this->config = $config ?? new EnvConfig();
$this->context = $context ?? WpContext::new()->force(WpContext::CORE);
$this->builder = $builder ?? new ContainerBuilder();
- $this->containers = $containers;
+ $this->containers = array_values($containers);
$this->bootstrapBuilder();
}
- private SiteConfig $config;
- private WpContext $context;
-
public function __clone(): void
{
$this->runtimeContainer = null;
diff --git a/src/Discovery/BundleDiscovery.php b/src/Discovery/BundleDiscovery.php
index 68c6db7..0f0c664 100644
--- a/src/Discovery/BundleDiscovery.php
+++ b/src/Discovery/BundleDiscovery.php
@@ -127,10 +127,10 @@ private function composerBundle(string $packageName): ?BundleMetadata
}
$metadata = $this->composerMetadata($composerFile);
- $kernel = $metadata['extra']['kernel'] ?? null;
- $bundleClass = is_array($kernel) ? (string) ($kernel['bundle'] ?? '') : '';
- $entry = is_array($kernel) ? (string) ($kernel['entry'] ?? '') : '';
- $type = (string) ($metadata['type'] ?? '');
+ $kernel = $this->kernelMetadata($metadata);
+ $bundleClass = $this->stringValue($kernel['bundle'] ?? null);
+ $entry = $this->stringValue($kernel['entry'] ?? null);
+ $type = $this->stringValue($metadata['type'] ?? null);
if ($bundleClass === '' || $entry === '' || $type === '') {
return null;
@@ -211,7 +211,7 @@ private function configuredBundle(mixed $key, mixed $value, string $source): ?Bu
}
$bundleClass = is_string($value) ? $value : (is_string($key) ? $key : '');
- $environments = is_array($value) ? $value : ['all' => true];
+ $environments = is_array($value) ? $this->stringKeyMap($value) : ['all' => true];
if ($bundleClass === '' || !$this->shouldLoadConfiguredBundle($environments)) {
return null;
@@ -458,9 +458,9 @@ private function requirementsActive(array $requirements): bool
}
$metadata = $this->composerMetadata($composerFile);
- $kernel = $metadata['extra']['kernel'] ?? null;
- $entry = is_array($kernel) ? (string) ($kernel['entry'] ?? '') : '';
- $type = (string) ($metadata['type'] ?? '');
+ $kernel = $this->kernelMetadata($metadata);
+ $entry = $this->stringValue($kernel['entry'] ?? null);
+ $type = $this->stringValue($metadata['type'] ?? null);
if ($entry === '' || $type === '') {
return false;
@@ -516,7 +516,7 @@ private function composerMetadata(string $composerFile): array
$decoded = json_decode($contents, true);
- $this->metadata[$composerFile] = is_array($decoded) ? $decoded : [];
+ $this->metadata[$composerFile] = is_array($decoded) ? $this->stringKeyMap($decoded) : [];
return $this->metadata[$composerFile];
}
@@ -576,7 +576,7 @@ private function hasKernelMetadata(string $package): bool
$metadata = $this->composerMetadata($composerFile);
- return is_array($metadata['extra']['kernel'] ?? null);
+ return $this->kernelMetadata($metadata) !== [];
}
private function isKernelPackage(string $package): bool
@@ -614,4 +614,49 @@ private function normalizePackagePrefixes(array $packagePrefixes): array
return array_values(array_unique($normalized));
}
+
+ /**
+ * @param array $metadata
+ * @return array
+ */
+ private function kernelMetadata(array $metadata): array
+ {
+ $extra = $metadata['extra'] ?? null;
+
+ if (!is_array($extra)) {
+ return [];
+ }
+
+ $kernel = $extra['kernel'] ?? null;
+
+ return is_array($kernel) ? $this->stringKeyMap($kernel) : [];
+ }
+
+ private function stringValue(mixed $value): string
+ {
+ if (is_scalar($value) || $value instanceof \Stringable) {
+ return (string) $value;
+ }
+
+ return '';
+ }
+
+ /**
+ * @param array $values
+ * @return array
+ */
+ private function stringKeyMap(array $values): array
+ {
+ $map = [];
+
+ foreach ($values as $key => $value) {
+ if (!is_string($key)) {
+ continue;
+ }
+
+ $map[$key] = $value;
+ }
+
+ return $map;
+ }
}
diff --git a/src/EnvConfig.php b/src/EnvConfig.php
index 66ef898..62441a8 100644
--- a/src/EnvConfig.php
+++ b/src/EnvConfig.php
@@ -144,12 +144,16 @@ public function env(): string
?? $this->readEnvVarOrConstant('VIP_GO_APP_ENVIRONMENT')
?? $this->readEnvVarOrConstant('VIP_GO_ENV');
- if ($env !== null) {
+ if (is_scalar($env) || $env instanceof \Stringable) {
return $this->normalizeEnv((string) $env, false);
}
if (function_exists('is_wpe')) {
- $env = (int) is_wpe() > 0 ? self::PRODUCTION : self::STAGING;
+ $isWpe = is_wpe();
+ $env = (is_bool($isWpe) && $isWpe)
+ || (is_numeric($isWpe) && (int) $isWpe > 0)
+ ? self::PRODUCTION
+ : self::STAGING;
$this->env = $this->normalizeEnv($env, true);
return $this->env;
diff --git a/src/Hook/HookCompilerPass.php b/src/Hook/HookCompilerPass.php
index bb69db2..d899e31 100644
--- a/src/Hook/HookCompilerPass.php
+++ b/src/Hook/HookCompilerPass.php
@@ -29,6 +29,11 @@ public function process(ContainerBuilder $container): void
$serviceMap[$id] = new Reference($id);
foreach ($definition->getTag(HookLoader::TAG) as $attributes) {
+ if (!is_array($attributes)) {
+ continue;
+ }
+
+ $attributes = $this->stringKeyMap($attributes);
$method = $this->optionalStringAttribute($attributes, 'method', '__invoke');
$this->validateHookMethod($container, $id, $definition->getClass(), $method);
@@ -174,6 +179,25 @@ private function validateHookMethod(
);
}
+ /**
+ * @param array $values
+ * @return array
+ */
+ private function stringKeyMap(array $values): array
+ {
+ $map = [];
+
+ foreach ($values as $key => $value) {
+ if (!is_string($key)) {
+ continue;
+ }
+
+ $map[$key] = $value;
+ }
+
+ return $map;
+ }
+
private function reflectHookMethod(
ContainerBuilder $container,
string $serviceId,
diff --git a/src/Kernel/AbstractKernel.php b/src/Kernel/AbstractKernel.php
index 62571e1..301d5b5 100644
--- a/src/Kernel/AbstractKernel.php
+++ b/src/Kernel/AbstractKernel.php
@@ -4,76 +4,32 @@
namespace SymPress\Kernel\Kernel;
-use Psr\Container\ContainerInterface as PsrContainerInterface;
-use SymPress\Kernel\App;
-use SymPress\Kernel\Attribute\AsHook;
-use SymPress\Kernel\Attribute\Route;
use SymPress\Kernel\Bundle\BundleInterface;
use SymPress\Kernel\Bundle\BundleRegistry;
-use SymPress\Kernel\Console\ConsoleApplicationFactory;
-use SymPress\Kernel\Console\WpCliConsoleBridge;
use SymPress\Kernel\Container;
use SymPress\Kernel\DependencyInjection\EnvironmentParameterLoader;
use SymPress\Kernel\Discovery\BundleDiscovery;
use SymPress\Kernel\EnvConfig;
-use SymPress\Kernel\Hook\HookCompilerPass;
-use SymPress\Kernel\Hook\HookLoader;
use SymPress\Kernel\Resolver\ActivePackageResolver;
-use SymPress\Kernel\Routing\RouteCompilerPass;
use SymPress\Kernel\Routing\RouteLoader;
use SymPress\Kernel\SiteConfig;
-use SymPress\Kernel\Translation\TranslationLoader;
use SymPress\Kernel\WpContext;
-use Symfony\Component\Config\Resource\FileExistenceResource;
use Symfony\Component\Config\Resource\FileResource;
-use Symfony\Component\Config\Resource\SelfCheckingResourceChecker;
-use Symfony\Component\Config\ResourceCheckerConfigCacheFactory;
-use Symfony\Component\Config\ResourceCheckerInterface;
use Symfony\Component\Config\Loader\DelegatingLoader;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Loader\LoaderResolver;
-use Symfony\Component\DependencyInjection\Config\ContainerParametersResourceChecker;
-use Symfony\Component\Console\Application;
-use Symfony\Component\Console\Attribute\AsCommand;
-use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
-use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
-use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
-use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
-use Symfony\Component\DependencyInjection\ChildDefinition;
-use Symfony\Component\DependencyInjection\Compiler\AddBehaviorDescribingTagsPass;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyDiContainerInterface;
-use Symfony\Component\DependencyInjection\Definition;
-use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
-use Symfony\Component\DependencyInjection\Compiler\MergeExtensionConfigurationPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
-use Symfony\Component\DependencyInjection\Compiler\ResettableServicePass;
-use Symfony\Component\DependencyInjection\EnvVarLoaderInterface;
-use Symfony\Component\DependencyInjection\EnvVarProcessor;
-use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
-use Symfony\Component\DependencyInjection\Kernel\KernelInterface as DependencyInjectionKernelInterface;
use Symfony\Component\DependencyInjection\Loader\ClosureLoader;
use Symfony\Component\DependencyInjection\Loader\DirectoryLoader;
use Symfony\Component\DependencyInjection\Loader\GlobFileLoader;
use Symfony\Component\DependencyInjection\Loader\IniFileLoader;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
-use Symfony\Component\DependencyInjection\ParameterBag\ContainerBag;
-use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
-use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
-use Symfony\Component\DependencyInjection\Reference;
-use Symfony\Component\DependencyInjection\ReverseContainer;
-use Symfony\Component\DependencyInjection\ServiceLocator;
-use Symfony\Component\DependencyInjection\ServicesResetter;
-use Symfony\Component\DependencyInjection\ServicesResetterInterface;
-use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
-use Symfony\Component\HttpKernel\KernelInterface as SymfonyKernelInterface;
-use Symfony\Contracts\Service\ResetInterface;
-use Symfony\Contracts\Service\ServiceSubscriberInterface;
abstract class AbstractKernel implements KernelInterface
{
@@ -87,8 +43,7 @@ abstract class AbstractKernel implements KernelInterface
/** @var array */
protected array $bundles = [];
- /** @var array */
- private array $preparedBuilders = [];
+ private ?CoreServiceRegistrar $coreServiceRegistrar = null;
public function __construct(
protected readonly string $projectDir,
@@ -120,7 +75,7 @@ public function __clone()
$this->container = null;
$this->bundleRegistry = null;
$this->bundles = [];
- $this->preparedBuilders = [];
+ $this->coreServiceRegistrar = null;
}
/** @return array{project_dir: string, environment: string, debug: bool} */
@@ -169,54 +124,22 @@ public function getCharset(): string
public function getCacheDir(): string
{
- $dir = $this->serverString('APP_CACHE_DIR');
-
- if ($dir !== null) {
- return sprintf('%s/kernel', $this->environmentDirectory($dir));
- }
-
- return sprintf('%s/var/cache/%s/kernel', $this->projectDir, $this->environment);
+ return $this->configuration()->cacheDir();
}
public function getBuildDir(): string
{
- $dir = $this->serverString('APP_BUILD_DIR');
-
- if ($dir !== null) {
- return sprintf('%s/kernel', $this->environmentDirectory($dir));
- }
-
- return $this->getCacheDir();
+ return $this->configuration()->buildDir();
}
public function getShareDir(): ?string
{
- $dir = $this->serverNullableDirectory('APP_SHARE_DIR');
-
- if ($dir !== null) {
- return sprintf('%s/kernel', $this->environmentDirectory($dir));
- }
-
- if ($this->serverValueIsFalse('APP_SHARE_DIR')) {
- return null;
- }
-
- return $this->getCacheDir();
+ return $this->configuration()->shareDir();
}
public function getLogDir(): ?string
{
- $dir = $this->serverNullableDirectory('APP_LOG_DIR');
-
- if ($dir !== null) {
- return $this->environmentDirectory($dir);
- }
-
- if ($this->serverValueIsFalse('APP_LOG_DIR')) {
- return null;
- }
-
- return sprintf('%s/var/log', $this->projectDir);
+ return $this->configuration()->logDir();
}
public function getStartTime(): float
@@ -339,7 +262,7 @@ public function discoverBundles(): BundleRegistry
{
$this->bundleRegistry = (new BundleDiscovery(
new ActivePackageResolver(),
- $this->packagePrefixes(),
+ $this->configuration()->packagePrefixes(),
$this->projectDir,
$this->environment,
))->discover();
@@ -376,13 +299,13 @@ public function configureContainer(
$builder->setParameter('kernel.build_dir', $this->getBuildDir());
$builder->setParameter('kernel.share_dir', $this->getShareDir());
$builder->setParameter('kernel.logs_dir', $this->getLogDir());
- $builder->setParameter('kernel.package_prefixes', $this->packagePrefixes());
+ $builder->setParameter('kernel.package_prefixes', $this->configuration()->packagePrefixes());
$builder->setParameter('kernel.translation_paths', $bundles->translationDirectories());
$builder->setParameter('kernel.bundles', $this->bundleClasses($bundles));
$builder->setParameter('kernel.bundles_metadata', $this->bundleMetadata($bundles));
$builder->setParameter('kernel.container_class', 'KernelContainer');
$builder->setParameter('.kernel.config_dir', sprintf('%s/config', $this->projectDir));
- $builder->setParameter('.container.known_envs', $this->knownEnvironments());
+ $builder->setParameter('.container.known_envs', $this->configuration()->knownEnvironments());
$this->loadEnvironmentVariablesAsParameters($builder);
foreach ($bundles->all() as $bundle) {
@@ -401,8 +324,8 @@ public function configureContainer(
$loaded = [];
- foreach ($this->configDirectories($bundles) as $configDir) {
- foreach ($this->resolveConfigFiles($configDir) as $file) {
+ foreach ($this->configuration()->configDirectories($bundles) as $configDir) {
+ foreach ($this->configuration()->resolveConfigFiles($configDir) as $file) {
$this->loadConfigFile($builder, $file);
$loaded[] = $file;
}
@@ -419,28 +342,10 @@ public function build(ContainerBuilder $builder): void
public function tryUseRuntimeContainer(Container $container, BundleRegistry $bundles): bool
{
- if ($this->tracksSourceChanges()) {
- return false;
- }
-
- $cacheDir = $this->getCacheDir();
- $metaFile = sprintf('%s/meta.php', $cacheDir);
-
- if (!is_file($metaFile)) {
- return false;
- }
-
- $metadata = require $metaFile;
-
- if (!is_array($metadata)) {
- return false;
- }
-
- return $this->useCachedRuntimeContainer(
+ return $this->containerCacheManager()->tryUseRuntimeContainer(
$container,
- $cacheDir,
- $metadata,
- $this->fingerprint($bundles, $this->runtimeConfigFiles($bundles)),
+ $bundles,
+ $this->configuration()->runtimeConfigFiles($bundles),
);
}
@@ -449,85 +354,25 @@ public function createRuntimeContainer(
BundleRegistry $bundles,
array $configFiles,
): void {
+ $this->containerCacheManager()->createRuntimeContainer($container, $bundles, $configFiles);
+ }
- $cacheDir = $this->getCacheDir();
- $filesystem = new Filesystem();
- $filesystem->mkdir($cacheDir);
- $metaFile = sprintf('%s/meta.php', $cacheDir);
- $lockFile = sprintf('%s/container.lock', $cacheDir);
- $fingerprint = $this->fingerprint($bundles, $configFiles);
- $lock = fopen($lockFile, 'c+');
-
- if (!is_resource($lock)) {
- throw new \RuntimeException(sprintf('Unable to create cache lock "%s".', $lockFile));
- }
-
- try {
- if (!flock($lock, LOCK_EX)) {
- throw new \RuntimeException(sprintf('Unable to lock cache file "%s".', $lockFile));
- }
-
- clearstatcache(true, $metaFile);
-
- $metadata = is_file($metaFile) ? require $metaFile : null;
-
- if (
- is_array($metadata)
- && $this->useCachedRuntimeContainer($container, $cacheDir, $metadata, $fingerprint)
- ) {
- return;
- }
-
- $sourceResources = $this->sourceResourceManifest($bundles);
- $sourceFingerprint = $this->sourceResourceFingerprint($sourceResources);
- $cacheKey = substr(hash('sha256', "{$fingerprint}|{$sourceFingerprint}"), 0, 16);
- $containerFile = sprintf('%s/container_%s.php', $cacheDir, $cacheKey);
- clearstatcache(true, $containerFile);
-
- $class = sprintf('KernelContainer_%s', $cacheKey);
- $runtime = $this->createRuntimeBuilder($container, $class);
- $runtime->compile(true);
- $configResources = $this->configResourceManifest($runtime, $configFiles);
- $dumper = new PhpDumper($runtime);
- $filesystem->dumpFile(
- $containerFile,
- $dumper->dump(
- [
- 'class' => $class,
- 'debug' => $this->debug,
- 'file' => $containerFile,
- 'build_time' => $this->containerBuildTime($runtime),
- 'inline_class_loader' => $this->debug,
- ],
- ),
- );
- $filesystem->dumpFile(
- $metaFile,
- sprintf(
- " $fingerprint,
- 'config_resources' => $configResources,
- 'source_fingerprint' => $sourceFingerprint,
- 'source_resources' => $sourceResources,
- 'class' => $class,
- 'file' => basename($containerFile),
- ],
- true,
- ),
- ),
- );
-
- if (!class_exists($class, false)) {
- require $containerFile;
- }
+ private function containerCacheManager(): ContainerCacheManager
+ {
+ return new ContainerCacheManager(
+ $this->getCacheDir(),
+ $this->debug,
+ new ContainerResourceFingerprinter($this->projectDir, $this->environment, $this->debug),
+ );
+ }
- $container->useRuntimeContainer($this->newRuntimeContainer($class));
- } finally {
- flock($lock, LOCK_UN);
- fclose($lock);
- }
+ private function configuration(): KernelConfigurationResolver
+ {
+ return new KernelConfigurationResolver(
+ $this->projectDir,
+ $this->environment,
+ $this->config,
+ );
}
public function boot(?Container $container = null, ?BundleRegistry $bundles = null): void
@@ -635,249 +480,6 @@ private function bundleMetadata(BundleRegistry $bundles): array
return $metadataByName;
}
- /** @return list */
- private function knownEnvironments(): array
- {
- $known = [
- $this->environment,
- 'all',
- 'dev',
- 'development',
- 'local',
- 'prod',
- 'production',
- 'stage',
- 'staging',
- 'test',
- ];
- $bundlesDefinition = sprintf('%s/config/bundles.php', $this->projectDir);
-
- if (!is_file($bundlesDefinition)) {
- return $this->normalizeKnownEnvironments($known);
- }
-
- $configuration = require $bundlesDefinition;
-
- if (!is_array($configuration)) {
- return $this->normalizeKnownEnvironments($known);
- }
-
- foreach ($configuration as $envs) {
- if (!is_array($envs)) {
- continue;
- }
-
- foreach (array_keys($envs) as $environment) {
- if (!is_string($environment)) {
- continue;
- }
-
- $known[] = $environment;
- }
- }
-
- return $this->normalizeKnownEnvironments($known);
- }
-
- /**
- * @param list $environments
- * @return list
- */
- private function normalizeKnownEnvironments(array $environments): array
- {
- return array_values(
- array_unique(
- array_filter($environments, static fn (string $env): bool => $env !== ''),
- ),
- );
- }
-
- /** @return array */
- private function configDirectories(BundleRegistry $bundles): array
- {
- $directories = [];
- $libraryDir = dirname(__DIR__, 2) . '/config';
- $siteDir = sprintf('%s/config', $this->projectDir);
-
- if (is_dir($libraryDir)) {
- $directories[] = $libraryDir;
- }
-
- foreach ($bundles->configDirectories() as $configDir) {
- $directories[] = $configDir;
- }
-
- if (is_dir($siteDir)) {
- $directories[] = $siteDir;
- }
-
- return array_values(array_unique($directories));
- }
-
- private function serverString(string $name): ?string
- {
- $value = $_SERVER[$name] ?? $_ENV[$name] ?? null;
-
- if (!is_scalar($value) && !$value instanceof \Stringable) {
- return null;
- }
-
- $value = trim((string) $value);
-
- return $value === '' ? null : $value;
- }
-
- private function serverNullableDirectory(string $name): ?string
- {
- if ($this->serverValueIsFalse($name)) {
- return null;
- }
-
- return $this->serverString($name);
- }
-
- private function serverValueIsFalse(string $name): bool
- {
- $value = $_SERVER[$name] ?? $_ENV[$name] ?? null;
-
- if ($value === null) {
- return false;
- }
-
- return filter_var($value, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE) === false;
- }
-
- private function environmentDirectory(string $directory): string
- {
- if ($directory !== '' && in_array($directory[0], ['/', '\\'], true)) {
- return sprintf('%s/%s', rtrim($directory, '/'), $this->environment);
- }
-
- if (
- DIRECTORY_SEPARATOR === '\\'
- && isset($directory[1])
- && $directory[1] === ':'
- && preg_match('/^[A-Za-z]:/', $directory) === 1
- ) {
- return sprintf('%s/%s', rtrim($directory, '/'), $this->environment);
- }
-
- return sprintf('%s/%s/%s', $this->projectDir, trim($directory, '/'), $this->environment);
- }
-
- /** @return list */
- private function packagePrefixes(): array
- {
- $configured = $this->config->get('KERNEL_PACKAGE_PREFIXES', null);
-
- if ($configured === null) {
- $configured = $this->composerKernelPackagePrefixes();
- }
-
- return $this->normalizePackagePrefixes($configured);
- }
-
- private function composerKernelPackagePrefixes(): mixed
- {
- $composerFile = sprintf('%s/composer.json', $this->projectDir);
-
- if (!is_file($composerFile)) {
- return null;
- }
-
- $contents = file_get_contents($composerFile);
-
- if (!is_string($contents) || $contents === '') {
- return null;
- }
-
- $metadata = json_decode($contents, true);
-
- if (!is_array($metadata)) {
- return null;
- }
-
- $kernel = $metadata['extra']['kernel'] ?? null;
-
- if (!is_array($kernel)) {
- return null;
- }
-
- return $kernel['package_prefixes'] ?? $kernel['packagePrefixes'] ?? null;
- }
-
- /** @return list */
- private function normalizePackagePrefixes(mixed $packagePrefixes): array
- {
- if (is_string($packagePrefixes)) {
- $packagePrefixes = preg_split('/[,\s]+/', $packagePrefixes) ?: [];
- }
-
- if (!is_array($packagePrefixes)) {
- return [];
- }
-
- $normalized = [];
-
- foreach ($packagePrefixes as $prefix) {
- if (!is_scalar($prefix) && !$prefix instanceof \Stringable) {
- continue;
- }
-
- $prefix = trim((string) $prefix);
-
- if ($prefix === '') {
- continue;
- }
-
- $normalized[] = str_ends_with($prefix, '/') ? $prefix : "{$prefix}/";
- }
-
- return array_values(array_unique($normalized));
- }
-
- /** @return array */
- private function resolveConfigFiles(string $configDir): array
- {
- $files = [];
-
- foreach ($this->patterns($configDir) as $pattern) {
- $matches = glob($pattern, GLOB_BRACE) ?: [];
- sort($matches);
- $files = [...$files, ...$matches];
- }
-
- return $files;
- }
-
- /** @return array */
- private function runtimeConfigFiles(BundleRegistry $bundles): array
- {
- $files = [];
-
- foreach ($this->configDirectories($bundles) as $configDir) {
- $files = [...$files, ...$this->resolveConfigFiles($configDir)];
- }
-
- return array_values(array_unique($files));
- }
-
- /** @return array */
- private function patterns(string $configDir): array
- {
- $env = $this->environment;
- $extensions = '{php,yaml,yml,ini}';
-
- return [
- sprintf('%s/packages/*.%s', $configDir, $extensions),
- sprintf('%s/packages/%s/*.%s', $configDir, $env, $extensions),
- sprintf('%s/services.%s', $configDir, $extensions),
- sprintf('%s/services_%s.%s', $configDir, $env, $extensions),
- sprintf('%s/wordpress.%s', $configDir, $extensions),
- sprintf('%s/wordpress_%s.%s', $configDir, $env, $extensions),
- ];
- }
-
private function loadConfigFile(ContainerBuilder $builder, string $file): void
{
$basename = basename($file);
@@ -912,977 +514,21 @@ private function loadEnvironmentVariablesAsParameters(ContainerBuilder $builder)
{
(new EnvironmentParameterLoader())->load(
$builder,
- array_merge($_SERVER, $_ENV),
- $this->config->get('KERNEL_ENV_PARAMETERS', null),
- );
- }
-
- /** @param array $configFiles */
- private function fingerprint(BundleRegistry $bundles, array $configFiles): string
- {
- sort($configFiles);
-
- $parts = [
- $this->projectDir,
- $this->environment,
- (string) (int) $this->debug,
- $this->deploymentFingerprint(),
- $this->kernelFingerprint(),
- ...$bundles->identityFingerprintParts(),
- ];
-
- foreach ($configFiles as $file) {
- $parts[] = sprintf(
- '%s:%s',
- $file,
- is_file($file) ? $this->fileFingerprint($file) : 'missing',
- );
- }
-
- return hash('sha256', implode('|', $parts));
- }
-
- private function createRuntimeBuilder(Container $container, string $class): ContainerBuilder
- {
- $runtime = new ContainerBuilder();
- $this->copyExtensions($container->builder(), $runtime);
- $runtime->merge($container->builder());
- $this->copyCompilerPasses($container->builder(), $runtime);
- $runtime->setParameter('kernel.container_class', $class);
- $runtime->getCompilerPassConfig()->setMergePass(
- new MergeExtensionConfigurationPass($this->registeredExtensionAliases($runtime)),
- );
- $runtime->addCompilerPass(new HookCompilerPass());
- $runtime->addCompilerPass(new RouteCompilerPass());
- $runtime->addCompilerPass(new AddConsoleCommandPass());
- $this->ensureSynthetic($runtime, Container::CONTAINER_ID, Container::class);
- $this->ensureSynthetic($runtime, Container::CONFIG_ID, SiteConfig::class);
- $this->ensureSynthetic($runtime, Container::CONTEXT_ID, WpContext::class);
- $this->ensureSynthetic($runtime, Container::KERNEL_ID, KernelInterface::class);
- $this->ensureSynthetic($runtime, Container::APP_ID, App::class);
-
- $runtime->setAlias(Container::class, Container::CONTAINER_ID)->setPublic(true);
- $runtime->setAlias(PsrContainerInterface::class, Container::CONTAINER_ID)->setPublic(true);
- $runtime->setAlias(SiteConfig::class, Container::CONFIG_ID)->setPublic(true);
- $runtime->setAlias(WpContext::class, Container::CONTEXT_ID)->setPublic(true);
- $runtime->setAlias(KernelInterface::class, Container::KERNEL_ID)->setPublic(true);
- $runtime->setAlias(App::class, Container::APP_ID)->setPublic(true);
-
- return $runtime;
- }
-
- /** @return list */
- private function registeredExtensionAliases(ContainerBuilder $builder): array
- {
- $aliases = [];
-
- foreach ($builder->getExtensions() as $extension) {
- $aliases[] = $extension->getAlias();
- }
-
- return array_values(array_unique($aliases));
- }
-
- private function copyExtensions(ContainerBuilder $source, ContainerBuilder $target): void
- {
- foreach ($source->getExtensions() as $extension) {
- $target->registerExtension($extension);
- }
- }
-
- private function copyCompilerPasses(ContainerBuilder $source, ContainerBuilder $target): void
- {
- $sourcePasses = $source->getCompilerPassConfig();
- $targetPasses = $target->getCompilerPassConfig();
-
- $targetPasses->setBeforeOptimizationPasses($sourcePasses->getBeforeOptimizationPasses());
- $targetPasses->setOptimizationPasses($sourcePasses->getOptimizationPasses());
- $targetPasses->setBeforeRemovingPasses($sourcePasses->getBeforeRemovingPasses());
- $targetPasses->setRemovingPasses($sourcePasses->getRemovingPasses());
- $targetPasses->setAfterRemovingPasses($sourcePasses->getAfterRemovingPasses());
- }
-
- /** @param array $metadata */
- private function useCachedRuntimeContainer(
- Container $container,
- string $cacheDir,
- array $metadata,
- string $fingerprint,
- ): bool {
-
- if (
- ($metadata['fingerprint'] ?? null) !== $fingerprint
- || !is_string($metadata['class'] ?? null)
- || !is_string($metadata['file'] ?? null)
- ) {
- return false;
- }
-
- if (!$this->configResourcesAreFresh($metadata['config_resources'] ?? null)) {
- return false;
- }
-
- if (
- $this->shouldValidateCachedSourceResources()
- && !$this->sourceResourcesAreFresh($metadata['source_resources'] ?? null)
- ) {
- return false;
- }
-
- $cachedContainerFile = sprintf('%s/%s', $cacheDir, basename($metadata['file']));
-
- if (!is_file($cachedContainerFile)) {
- return false;
- }
-
- require_once $cachedContainerFile;
- $class = $metadata['class'];
-
- if (!class_exists($class, false)) {
- return false;
- }
-
- $container->useRuntimeContainer($this->newRuntimeContainer($class));
-
- return true;
- }
-
- private function kernelFingerprint(): string
- {
- $packageDir = dirname(__DIR__, 2);
- $composerFile = sprintf('%s/composer.json', $packageDir);
-
- return hash(
- 'sha256',
- implode(
- '|',
- [
- $packageDir,
- sprintf('%s:%s', $composerFile, $this->fileFingerprint($composerFile)),
- ],
+ array_merge(
+ $this->configuration()->stringKeyMap($_SERVER),
+ $this->configuration()->stringKeyMap($_ENV),
),
+ $this->config->get('KERNEL_ENV_PARAMETERS', null),
);
}
- private function tracksSourceChanges(): bool
- {
- return $this->debug && !$this->resourceTrackingDisabled();
- }
-
- private function resourceTrackingDisabled(): bool
- {
- $value = $_SERVER['SYMFONY_DISABLE_RESOURCE_TRACKING']
- ?? $_ENV['SYMFONY_DISABLE_RESOURCE_TRACKING']
- ?? null;
-
- if ($value === null) {
- return false;
- }
-
- if (is_array($value)) {
- return $value !== [];
- }
-
- return filter_var($value, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE) ?? ((string) $value !== '');
- }
-
- private function fileFingerprint(string $file): string
- {
- if (!is_file($file)) {
- return 'missing';
- }
-
- if ($this->tracksSourceChanges()) {
- return sha1_file($file);
- }
-
- return $this->sourceFileMtime($file);
- }
-
- private function deploymentFingerprint(): string
- {
- $buildId = defined('SYMPRESS_KERNEL_BUILD_ID')
- ? (string) constant('SYMPRESS_KERNEL_BUILD_ID')
- : getenv('SYMPRESS_KERNEL_BUILD_ID');
-
- if (is_string($buildId) && $buildId !== '') {
- return 'build:' . $buildId;
- }
-
- $composerLock = sprintf('%s/composer.lock', $this->projectDir);
-
- if (is_file($composerLock)) {
- return 'composer-lock:' . $this->fileFingerprint($composerLock);
- }
-
- return 'build:implicit';
- }
-
- private function containerBuildTime(ContainerBuilder $builder): int
+ private function prepareBuilder(ContainerBuilder $builder): void
{
- if ($builder->hasParameter('kernel.container_build_time')) {
- $buildTime = $builder->getParameter('kernel.container_build_time');
-
- if (is_int($buildTime)) {
- return $buildTime;
- }
-
- if (is_string($buildTime) && ctype_digit($buildTime)) {
- return (int) $buildTime;
- }
- }
-
- $sourceDateEpoch = $_SERVER['SOURCE_DATE_EPOCH'] ?? $_ENV['SOURCE_DATE_EPOCH'] ?? null;
-
- if (is_scalar($sourceDateEpoch) && filter_var($sourceDateEpoch, \FILTER_VALIDATE_INT) !== false) {
- return (int) $sourceDateEpoch;
- }
-
- return time();
+ $this->coreServiceRegistrar()->prepare($builder, $this->build(...));
}
- /** @return array */
- private function sourceResourceManifest(BundleRegistry $bundles): array
+ private function coreServiceRegistrar(): CoreServiceRegistrar
{
- $resources = [];
- $this->collectSourceResources(dirname(__DIR__, 2) . '/src', $resources);
-
- foreach ($bundles->all() as $metadata) {
- $this->collectSourceResources(sprintf('%s/src', rtrim($metadata->path(), '/')), $resources, 'php');
-
- $bundleFile = (new \ReflectionObject($metadata->bundle()))->getFileName();
-
- if (!is_string($bundleFile)) {
- continue;
- }
-
- $resources[$bundleFile] = $this->sourceFileMtime($bundleFile);
- }
-
- ksort($resources);
-
- return $resources;
- }
-
- /**
- * @param array $resources
- */
- private function collectSourceResources(
- string $directory,
- array &$resources,
- ?string $fileExtension = null,
- ): void {
-
- if (!is_dir($directory)) {
- return;
- }
-
- $resources[$directory] = $this->sourceDirectoryMtime($directory);
- $iterator = new \RecursiveIteratorIterator(
- new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
- \RecursiveIteratorIterator::SELF_FIRST,
- );
-
- foreach ($iterator as $resource) {
- if (!$resource instanceof \SplFileInfo) {
- continue;
- }
-
- $pathname = $resource->getPathname();
-
- if ($resource->isDir()) {
- $resources[$pathname] = $this->sourceDirectoryMtime($pathname);
-
- continue;
- }
-
- if (!$resource->isFile()) {
- continue;
- }
-
- if ($fileExtension !== null && $resource->getExtension() !== $fileExtension) {
- continue;
- }
-
- $resources[$pathname] = $this->sourceFileMtime($pathname);
- }
- }
-
- private function sourceResourcesAreFresh(mixed $resources): bool
- {
- if (!is_array($resources) || $resources === []) {
- return false;
- }
-
- foreach ($resources as $path => $expected) {
- if (!is_string($path) || !is_string($expected)) {
- return false;
- }
-
- $actual = str_starts_with($expected, 'dir:')
- ? $this->sourceDirectoryMtime($path)
- : $this->sourceFileMtime($path);
-
- if ($actual !== $expected) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * @param array $configFiles
- * @return array
- */
- private function configResourceManifest(ContainerBuilder $builder, array $configFiles): array
- {
- $resources = [];
-
- foreach ($configFiles as $file) {
- if ($file === '') {
- continue;
- }
-
- $resources[$file] = $this->fileFingerprint($file);
- }
-
- foreach ($builder->getResources() as $resource) {
- if ($resource instanceof FileResource) {
- $file = $resource->getResource();
- $resources[$file] = $this->fileFingerprint($file);
-
- continue;
- }
-
- if (!$resource instanceof FileExistenceResource) {
- continue;
- }
-
- $file = $resource->getResource();
- $resources[sprintf('exists:%s', $file)] = file_exists($file) ? 'exists:1' : 'exists:0';
- }
-
- ksort($resources);
-
- return $resources;
- }
-
- private function configResourcesAreFresh(mixed $resources): bool
- {
- if (!is_array($resources) || $resources === []) {
- return false;
- }
-
- foreach ($resources as $path => $expected) {
- if (!is_string($path) || !is_string($expected)) {
- return false;
- }
-
- if (str_starts_with($path, 'exists:')) {
- $actual = file_exists(substr($path, 7)) ? 'exists:1' : 'exists:0';
-
- if ($actual !== $expected) {
- return false;
- }
-
- continue;
- }
-
- if ($this->fileFingerprint($path) !== $expected) {
- return false;
- }
- }
-
- return true;
- }
-
- private function shouldValidateCachedSourceResources(): bool
- {
- $value = $_SERVER['SYMPRESS_KERNEL_VALIDATE_SOURCE_RESOURCES']
- ?? $_ENV['SYMPRESS_KERNEL_VALIDATE_SOURCE_RESOURCES']
- ?? null;
-
- if ($value === null) {
- return false;
- }
-
- return filter_var($value, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE) === true;
- }
-
- /** @param array $resources */
- private function sourceResourceFingerprint(array $resources): string
- {
- ksort($resources);
- $parts = [];
-
- foreach ($resources as $path => $fingerprint) {
- $parts[] = sprintf('%s:%s', $path, $fingerprint);
- }
-
- return hash('sha256', implode('|', $parts));
- }
-
- private function sourceDirectoryMtime(string $directory): string
- {
- if (!is_dir($directory)) {
- return 'missing';
- }
-
- return sprintf('dir:%s', (string) filemtime($directory));
- }
-
- private function sourceFileMtime(string $file): string
- {
- if (!is_file($file)) {
- return 'missing';
- }
-
- return sprintf('file:%s:%s', (string) filemtime($file), (string) filesize($file));
- }
-
- private function prepareBuilder(ContainerBuilder $builder): void
- {
- $builderId = spl_object_id($builder);
-
- if (($this->preparedBuilders[$builderId] ?? false) === true) {
- return;
- }
-
- $this->preparedBuilders[$builderId] = true;
- $this->registerCoreContainerServices($builder);
- $this->registerTranslationLoader($builder);
- $this->registerHookLoader($builder);
- $this->registerRouteLoader($builder);
- $this->registerConsoleApplication($builder);
- $this->registerConsoleAttributes($builder);
- $builder->registerAttributeForAutoconfiguration(
- AsHook::class,
- static function (ChildDefinition $definition, AsHook $attribute, \Reflector $reflector): void {
- if (!$reflector instanceof \ReflectionClass && !$reflector instanceof \ReflectionMethod) {
- return;
- }
-
- $tag = $attribute->toTag();
-
- if ($reflector instanceof \ReflectionMethod && $attribute->method === '__invoke') {
- $tag['method'] = $reflector->getName();
- }
-
- $definition->addTag(HookLoader::TAG, $tag);
- },
- );
- $this->registerRouteAttributes($builder);
- $builder->addCompilerPass(new HookCompilerPass());
- $builder->addCompilerPass(new RouteCompilerPass());
- $this->build($builder);
- }
-
- private function registerCoreContainerServices(ContainerBuilder $builder): void
- {
- if (!$builder->has('kernel')) {
- $builder->setAlias('kernel', Container::KERNEL_ID)->setPublic(true);
- }
-
- $builder->setAlias(SymfonyKernelInterface::class, Container::KERNEL_ID)
- ->setPublic(true);
- $builder->setAlias(DependencyInjectionKernelInterface::class, Container::KERNEL_ID)
- ->setPublic(true);
-
- $this->registerFilesystemService($builder);
- $this->registerEventDispatcherServices($builder);
- $this->registerClockService($builder);
- $this->registerExpressionLanguageService($builder);
-
- if (!$builder->hasDefinition('parameter_bag')) {
- $builder->setDefinition(
- 'parameter_bag',
- (new Definition(ContainerBag::class))
- ->setArguments([new Reference('service_container')]),
- );
- }
-
- $builder->setAlias(ContainerBagInterface::class, 'parameter_bag')->setPublic(false);
- $builder->setAlias(ParameterBagInterface::class, 'parameter_bag')->setPublic(false);
-
- if (!$builder->hasDefinition('file_locator')) {
- $builder->setDefinition(
- 'file_locator',
- (new Definition(FileLocator::class))
- ->setArguments([new Reference(Container::KERNEL_ID)]),
- );
- }
-
- $builder->setAlias(FileLocator::class, 'file_locator')->setPublic(false);
-
- if (!$builder->hasDefinition('reverse_container')) {
- $builder->setDefinition(
- 'reverse_container',
- (new Definition(ReverseContainer::class))
- ->setArguments([
- new Reference('service_container'),
- new ServiceLocatorArgument([]),
- ]),
- );
- }
-
- $builder->setAlias(ReverseContainer::class, 'reverse_container')->setPublic(false);
-
- if (!$builder->hasDefinition('config_cache_factory')) {
- $builder->setDefinition(
- 'config_cache_factory',
- (new Definition(ResourceCheckerConfigCacheFactory::class))
- ->setArguments([new TaggedIteratorArgument('config_cache.resource_checker')]),
- );
- }
-
- if (!$builder->hasDefinition('dependency_injection.config.container_parameters_resource_checker')) {
- $builder->setDefinition(
- 'dependency_injection.config.container_parameters_resource_checker',
- (new Definition(ContainerParametersResourceChecker::class))
- ->setArguments([new Reference('service_container')])
- ->addTag('config_cache.resource_checker', ['priority' => -980]),
- );
- }
-
- if (!$builder->hasDefinition('config.resource.self_checking_resource_checker')) {
- $builder->setDefinition(
- 'config.resource.self_checking_resource_checker',
- (new Definition(SelfCheckingResourceChecker::class))
- ->addTag('config_cache.resource_checker', ['priority' => -990]),
- );
- }
-
- if (!$builder->hasDefinition('services_resetter')) {
- $builder->setDefinition(
- 'services_resetter',
- (new Definition(ServicesResetter::class))
- ->setPublic(true)
- ->setArguments([new IteratorArgument([]), []]),
- );
- }
-
- $builder->setAlias(ServicesResetterInterface::class, 'services_resetter')->setPublic(true);
-
- if (!$builder->hasDefinition('container.env_var_processor')) {
- $builder->setDefinition(
- 'container.env_var_processor',
- (new Definition(EnvVarProcessor::class))
- ->setArguments([
- new Reference('service_container'),
- new TaggedIteratorArgument('container.env_var_loader'),
- ])
- ->addTag('container.env_var_processor')
- ->addTag('kernel.reset', ['method' => 'reset']),
- );
- }
-
- $builder->registerForAutoconfiguration(EnvVarLoaderInterface::class)
- ->addTag('container.env_var_loader');
- $builder->registerForAutoconfiguration(EnvVarProcessorInterface::class)
- ->addTag('container.env_var_processor');
- $builder->registerForAutoconfiguration(ResourceCheckerInterface::class)
- ->addTag('config_cache.resource_checker');
- $builder->registerForAutoconfiguration(ServiceLocator::class)
- ->addTag('container.service_locator');
- $builder->registerForAutoconfiguration(ResetInterface::class)
- ->addTag('kernel.reset', ['method' => 'reset']);
- $builder->registerForAutoconfiguration(ServiceSubscriberInterface::class)
- ->addTag('container.service_subscriber');
- $builder->registerForAutoconfiguration(CompilerPassInterface::class)
- ->addTag('container.excluded', ['source' => 'because it is a compiler pass']);
- $builder->registerForAutoconfiguration(\UnitEnum::class)
- ->addTag('container.excluded', ['source' => 'because it is an enum']);
- $builder->registerAttributeForAutoconfiguration(
- \Attribute::class,
- static function (ChildDefinition $definition): void {
- $definition->addTag('container.excluded', ['source' => 'because it is a PHP attribute']);
- },
- );
- $this->registerOptionalAutoconfiguration($builder);
-
- $builder->addCompilerPass(
- new AddBehaviorDescribingTagsPass(
- [
- 'container.do_not_inline',
- 'container.excluded',
- 'container.hot_path',
- 'container.service_locator',
- 'container.service_subscriber',
- 'event_dispatcher.dispatcher',
- 'kernel.event_listener',
- 'kernel.event_subscriber',
- 'kernel.reset',
- ],
- ),
- PassConfig::TYPE_BEFORE_OPTIMIZATION,
- 200,
- );
- $builder->addCompilerPass(new ResettableServicePass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
- }
-
- private function registerFilesystemService(ContainerBuilder $builder): void
- {
- if (!$builder->hasDefinition('filesystem')) {
- $builder->setDefinition('filesystem', new Definition(Filesystem::class));
- }
-
- $this->setAliasIfMissing($builder, Filesystem::class, 'filesystem');
- }
-
- private function registerEventDispatcherServices(ContainerBuilder $builder): void
- {
- $eventDispatcherClass = 'Symfony\Component\EventDispatcher\EventDispatcher';
-
- if (class_exists($eventDispatcherClass) && !$builder->hasDefinition('event_dispatcher')) {
- $builder->setDefinition(
- 'event_dispatcher',
- (new Definition($eventDispatcherClass))
- ->setPublic(true)
- ->addTag('container.hot_path')
- ->addTag('event_dispatcher.dispatcher', ['name' => 'event_dispatcher']),
- );
- }
-
- $this->registerEventDispatcherAliases($builder);
-
- $registerListenersPass = 'Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass';
-
- if (class_exists($registerListenersPass)) {
- $builder->addCompilerPass(new $registerListenersPass(), PassConfig::TYPE_BEFORE_REMOVING);
- }
-
- foreach (
- [
- 'Symfony\Component\EventDispatcher\EventDispatcherInterface' => 'event_dispatcher.dispatcher',
- 'Symfony\Component\EventDispatcher\EventSubscriberInterface' => 'kernel.event_subscriber',
- ] as $interface => $tag
- ) {
- if (!interface_exists($interface)) {
- continue;
- }
-
- $builder->registerForAutoconfiguration($interface)->addTag($tag);
- }
-
- $asEventListenerClass = 'Symfony\Component\EventDispatcher\Attribute\AsEventListener';
-
- if (!class_exists($asEventListenerClass)) {
- return;
- }
-
- $builder->registerAttributeForAutoconfiguration(
- $asEventListenerClass,
- static function (ChildDefinition $definition, object $attribute, \Reflector $reflector): void {
- if (!$reflector instanceof \ReflectionClass && !$reflector instanceof \ReflectionMethod) {
- return;
- }
-
- $tagAttributes = array_filter(
- get_object_vars($attribute),
- static fn (mixed $value): bool => $value !== null,
- );
-
- if ($reflector instanceof \ReflectionMethod) {
- if (isset($tagAttributes['method'])) {
- throw new \LogicException(
- sprintf(
- 'AsEventListener attribute cannot declare a method on "%s::%s()".',
- $reflector->class,
- $reflector->name,
- ),
- );
- }
-
- $tagAttributes['method'] = $reflector->getName();
- }
-
- $definition->addTag('kernel.event_listener', $tagAttributes);
- },
- );
- }
-
- private function registerEventDispatcherAliases(ContainerBuilder $builder): void
- {
- if (!$builder->hasDefinition('event_dispatcher') && !$builder->hasAlias('event_dispatcher')) {
- return;
- }
-
- foreach (
- [
- 'Symfony\Component\EventDispatcher\EventDispatcherInterface',
- 'Symfony\Contracts\EventDispatcher\EventDispatcherInterface',
- 'Psr\EventDispatcher\EventDispatcherInterface',
- ] as $eventDispatcherInterface
- ) {
- if (!interface_exists($eventDispatcherInterface)) {
- continue;
- }
-
- $this->setAliasIfMissing($builder, $eventDispatcherInterface, 'event_dispatcher', true);
- }
- }
-
- private function registerClockService(ContainerBuilder $builder): void
- {
- $clockClass = 'Symfony\Component\Clock\Clock';
-
- if (!class_exists($clockClass)) {
- return;
- }
-
- if (!$builder->hasDefinition('clock')) {
- $builder->setDefinition('clock', new Definition($clockClass));
- }
-
- foreach (['Symfony\Component\Clock\ClockInterface', 'Psr\Clock\ClockInterface'] as $clockInterface) {
- if (!interface_exists($clockInterface)) {
- continue;
- }
-
- $this->setAliasIfMissing($builder, $clockInterface, 'clock');
- }
- }
-
- private function registerExpressionLanguageService(ContainerBuilder $builder): void
- {
- $expressionLanguageClass = 'Symfony\Component\DependencyInjection\ExpressionLanguage';
-
- if (!class_exists($expressionLanguageClass) || $builder->hasDefinition('container.expression_language')) {
- return;
- }
-
- $builder->setDefinition('container.expression_language', new Definition($expressionLanguageClass));
- }
-
- private function registerOptionalAutoconfiguration(ContainerBuilder $builder): void
- {
- $this->registerLoggerAwareAutoconfiguration($builder);
- $this->registerTestCaseExclusion($builder);
- $this->registerLoaderInterfaceExclusion($builder);
- }
-
- private function registerLoggerAwareAutoconfiguration(ContainerBuilder $builder): void
- {
- $loggerAwareInterface = 'Psr\Log\LoggerAwareInterface';
-
- if (!interface_exists($loggerAwareInterface)) {
- return;
- }
-
- $builder->registerForAutoconfiguration($loggerAwareInterface)
- ->addMethodCall(
- 'setLogger',
- [new Reference('logger', SymfonyDiContainerInterface::IGNORE_ON_INVALID_REFERENCE)],
- );
- }
-
- private function registerTestCaseExclusion(ContainerBuilder $builder): void
- {
- $testCaseClass = 'PHPUnit\Framework\TestCase';
-
- if (!class_exists($testCaseClass)) {
- return;
- }
-
- $builder->registerForAutoconfiguration($testCaseClass)
- ->addTag('container.excluded', ['source' => 'because it is a test case']);
- }
-
- private function registerLoaderInterfaceExclusion(ContainerBuilder $builder): void
- {
- if ($builder->hasDefinition(LoaderInterface::class)) {
- return;
- }
-
- $builder->setDefinition(
- LoaderInterface::class,
- (new Definition())
- ->setAbstract(true)
- ->addTag('container.excluded', ['source' => 'because it is a loader interface']),
- );
- }
-
- private function setAliasIfMissing(
- ContainerBuilder $builder,
- string $alias,
- string $target,
- bool $public = false,
- ): void {
- if ($builder->hasAlias($alias) || $builder->hasDefinition($alias)) {
- return;
- }
-
- $builder->setAlias($alias, $target)->setPublic($public);
- }
-
- private function registerTranslationLoader(ContainerBuilder $builder): void
- {
- if ($builder->hasDefinition(TranslationLoader::class)) {
- return;
- }
-
- $builder->setDefinition(
- TranslationLoader::class,
- (new Definition(TranslationLoader::class))
- ->setPublic(true)
- ->setArguments(['%kernel.translation_paths%']),
- );
- }
-
- private function registerConsoleApplication(ContainerBuilder $builder): void
- {
- if (!$builder->hasDefinition(ConsoleApplicationFactory::class)) {
- $builder->setDefinition(
- ConsoleApplicationFactory::class,
- (new Definition(ConsoleApplicationFactory::class))
- ->setPublic(true)
- ->setArguments([
- new Reference(Container::KERNEL_ID),
- new Reference('console.command_loader'),
- ]),
- );
- }
-
- if (!$builder->hasDefinition(Application::class)) {
- $builder->setDefinition(
- Application::class,
- (new Definition(Application::class))
- ->setPublic(true)
- ->setFactory([
- new Reference(ConsoleApplicationFactory::class),
- 'create',
- ]),
- );
- }
-
- if ($builder->hasDefinition(WpCliConsoleBridge::class)) {
- return;
- }
-
- $builder->setDefinition(
- WpCliConsoleBridge::class,
- (new Definition(WpCliConsoleBridge::class))
- ->setArguments([new Reference(Application::class)])
- ->addTag(
- HookLoader::TAG,
- [
- 'hook' => 'muplugins_loaded',
- 'method' => 'register',
- 'priority' => 1,
- ],
- ),
- );
- }
-
- private function registerConsoleAttributes(ContainerBuilder $builder): void
- {
- $builder->registerAttributeForAutoconfiguration(
- AsCommand::class,
- static function (ChildDefinition $definition): void {
- $definition->addTag('console.command');
- },
- );
- }
-
- private function registerHookLoader(ContainerBuilder $builder): void
- {
- if ($builder->hasDefinition(HookLoader::class)) {
- return;
- }
-
- $builder->setDefinition(
- HookLoader::class,
- (new Definition(HookLoader::class))
- ->setPublic(true)
- ->setArguments([null, []]),
- );
- }
-
- private function registerRouteLoader(ContainerBuilder $builder): void
- {
- if ($builder->hasDefinition(RouteLoader::class)) {
- return;
- }
-
- $builder->setDefinition(
- RouteLoader::class,
- (new Definition(RouteLoader::class))
- ->setPublic(true)
- ->setArguments([null, [], []])
- ->addTag(
- HookLoader::TAG,
- [
- 'hook' => 'template_redirect',
- 'method' => 'dispatchFrontendRequest',
- 'priority' => 0,
- ],
- )
- ->addTag(
- HookLoader::TAG,
- [
- 'hook' => 'rest_api_init',
- 'method' => 'registerRestRoutes',
- ],
- ),
- );
- }
-
- private function registerRouteAttributes(ContainerBuilder $builder): void
- {
- foreach (
- [
- Route::class,
- 'Symfony\Component\Routing\Attribute\Route',
- ] as $attributeClass
- ) {
- if (!class_exists($attributeClass)) {
- continue;
- }
-
- $builder->registerAttributeForAutoconfiguration(
- $attributeClass,
- static function (ChildDefinition $definition): void {
- $definition->addTag(RouteLoader::TAG);
- },
- );
- }
- }
-
- private function ensureSynthetic(ContainerBuilder $builder, string $id, string $class): void
- {
- if ($builder->hasDefinition($id)) {
- return;
- }
-
- $builder->setDefinition(
- $id,
- (new Definition($class))
- ->setSynthetic(true)
- ->setPublic(true),
- );
- }
-
- private function newRuntimeContainer(string $class): PsrContainerInterface
- {
- if (!class_exists($class, false)) {
- throw new \RuntimeException(
- sprintf('Compiled container class "%s" was not loaded.', $class),
- );
- }
-
- $container = new $class();
-
- if (!$container instanceof PsrContainerInterface) {
- throw new \RuntimeException(
- sprintf(
- 'Compiled container "%s" must implement %s.',
- $class,
- PsrContainerInterface::class,
- ),
- );
- }
-
- return $container;
+ return $this->coreServiceRegistrar ??= new CoreServiceRegistrar();
}
}
diff --git a/src/Kernel/ContainerCacheManager.php b/src/Kernel/ContainerCacheManager.php
new file mode 100644
index 0000000..36523a3
--- /dev/null
+++ b/src/Kernel/ContainerCacheManager.php
@@ -0,0 +1,304 @@
+ $runtimeConfigFiles */
+ public function tryUseRuntimeContainer(
+ Container $container,
+ BundleRegistry $bundles,
+ array $runtimeConfigFiles,
+ ): bool {
+
+ if ($this->fingerprints->tracksSourceChanges()) {
+ return false;
+ }
+
+ $metaFile = sprintf('%s/meta.php', $this->cacheDir);
+
+ if (!is_file($metaFile)) {
+ return false;
+ }
+
+ $metadata = require $metaFile;
+
+ if (!is_array($metadata)) {
+ return false;
+ }
+
+ return $this->useCachedRuntimeContainer(
+ $container,
+ $this->fingerprints->stringKeyMap($metadata),
+ $this->fingerprints->fingerprint($bundles, $runtimeConfigFiles),
+ );
+ }
+
+ /** @param array $configFiles */
+ public function createRuntimeContainer(
+ Container $container,
+ BundleRegistry $bundles,
+ array $configFiles,
+ ): void {
+
+ $filesystem = new Filesystem();
+ $filesystem->mkdir($this->cacheDir);
+ $metaFile = sprintf('%s/meta.php', $this->cacheDir);
+ $lockFile = sprintf('%s/container.lock', $this->cacheDir);
+ $fingerprint = $this->fingerprints->fingerprint($bundles, $configFiles);
+ $lock = fopen($lockFile, 'c+');
+
+ if (!is_resource($lock)) {
+ throw new \RuntimeException(sprintf('Unable to create cache lock "%s".', $lockFile));
+ }
+
+ try {
+ if (!flock($lock, LOCK_EX)) {
+ throw new \RuntimeException(sprintf('Unable to lock cache file "%s".', $lockFile));
+ }
+
+ clearstatcache(true, $metaFile);
+
+ $metadata = is_file($metaFile) ? require $metaFile : null;
+
+ if (
+ is_array($metadata)
+ && $this->useCachedRuntimeContainer(
+ $container,
+ $this->fingerprints->stringKeyMap($metadata),
+ $fingerprint,
+ )
+ ) {
+ return;
+ }
+
+ $sourceResources = $this->fingerprints->sourceResourceManifest($bundles);
+ $sourceFingerprint = $this->fingerprints->sourceResourceFingerprint($sourceResources);
+ $cacheKey = substr(hash('sha256', "{$fingerprint}|{$sourceFingerprint}"), 0, 16);
+ $containerFile = sprintf('%s/container_%s.php', $this->cacheDir, $cacheKey);
+ clearstatcache(true, $containerFile);
+
+ $class = sprintf('KernelContainer_%s', $cacheKey);
+ $runtime = $this->createRuntimeBuilder($container, $class);
+ $runtime->compile(true);
+ $configResources = $this->fingerprints->configResourceManifest($runtime, $configFiles);
+ $dump = (new PhpDumper($runtime))->dump(
+ [
+ 'class' => $class,
+ 'debug' => $this->debug,
+ 'file' => $containerFile,
+ 'build_time' => $this->containerBuildTime($runtime),
+ 'inline_class_loader' => $this->debug,
+ ],
+ );
+
+ if (!is_string($dump)) {
+ throw new \RuntimeException('The runtime container dumper did not return PHP code.');
+ }
+
+ $filesystem->dumpFile($containerFile, $dump);
+ $filesystem->dumpFile(
+ $metaFile,
+ sprintf(
+ " $fingerprint,
+ 'config_resources' => $configResources,
+ 'source_fingerprint' => $sourceFingerprint,
+ 'source_resources' => $sourceResources,
+ 'class' => $class,
+ 'file' => basename($containerFile),
+ ],
+ true,
+ ),
+ ),
+ );
+
+ if (!class_exists($class, false)) {
+ require $containerFile;
+ }
+
+ $container->useRuntimeContainer($this->newRuntimeContainer($class));
+ } finally {
+ flock($lock, LOCK_UN);
+ fclose($lock);
+ }
+ }
+
+ private function createRuntimeBuilder(Container $container, string $class): ContainerBuilder
+ {
+ $runtime = new ContainerBuilder();
+ $this->copyExtensions($container->builder(), $runtime);
+ $runtime->merge($container->builder());
+ $this->copyCompilerPasses($container->builder(), $runtime);
+ $runtime->setParameter('kernel.container_class', $class);
+ $runtime->getCompilerPassConfig()->setMergePass(
+ new MergeExtensionConfigurationPass($this->registeredExtensionAliases($runtime)),
+ );
+ $runtime->addCompilerPass(new HookCompilerPass());
+ $runtime->addCompilerPass(new RouteCompilerPass());
+ $runtime->addCompilerPass(new AddConsoleCommandPass());
+ $this->ensureSynthetic($runtime, Container::CONTAINER_ID, Container::class);
+ $this->ensureSynthetic($runtime, Container::CONFIG_ID, SiteConfig::class);
+ $this->ensureSynthetic($runtime, Container::CONTEXT_ID, WpContext::class);
+ $this->ensureSynthetic($runtime, Container::KERNEL_ID, KernelInterface::class);
+ $this->ensureSynthetic($runtime, Container::APP_ID, App::class);
+
+ $runtime->setAlias(Container::class, Container::CONTAINER_ID)->setPublic(true);
+ $runtime->setAlias(PsrContainerInterface::class, Container::CONTAINER_ID)->setPublic(true);
+ $runtime->setAlias(SiteConfig::class, Container::CONFIG_ID)->setPublic(true);
+ $runtime->setAlias(WpContext::class, Container::CONTEXT_ID)->setPublic(true);
+ $runtime->setAlias(KernelInterface::class, Container::KERNEL_ID)->setPublic(true);
+ $runtime->setAlias(App::class, Container::APP_ID)->setPublic(true);
+
+ return $runtime;
+ }
+
+ /** @return list */
+ private function registeredExtensionAliases(ContainerBuilder $builder): array
+ {
+ $aliases = [];
+
+ foreach ($builder->getExtensions() as $extension) {
+ $aliases[] = $extension->getAlias();
+ }
+
+ return array_values(array_unique($aliases));
+ }
+
+ private function copyExtensions(ContainerBuilder $source, ContainerBuilder $target): void
+ {
+ foreach ($source->getExtensions() as $extension) {
+ $target->registerExtension($extension);
+ }
+ }
+
+ private function copyCompilerPasses(ContainerBuilder $source, ContainerBuilder $target): void
+ {
+ $sourcePasses = $source->getCompilerPassConfig();
+ $targetPasses = $target->getCompilerPassConfig();
+
+ $targetPasses->setBeforeOptimizationPasses($sourcePasses->getBeforeOptimizationPasses());
+ $targetPasses->setOptimizationPasses($sourcePasses->getOptimizationPasses());
+ $targetPasses->setBeforeRemovingPasses($sourcePasses->getBeforeRemovingPasses());
+ $targetPasses->setRemovingPasses($sourcePasses->getRemovingPasses());
+ $targetPasses->setAfterRemovingPasses($sourcePasses->getAfterRemovingPasses());
+ }
+
+ /** @param array $metadata */
+ private function useCachedRuntimeContainer(
+ Container $container,
+ array $metadata,
+ string $fingerprint,
+ ): bool {
+
+ if (
+ ($metadata['fingerprint'] ?? null) !== $fingerprint
+ || !is_string($metadata['class'] ?? null)
+ || !is_string($metadata['file'] ?? null)
+ ) {
+ return false;
+ }
+
+ if (!$this->fingerprints->configResourcesAreFresh($metadata['config_resources'] ?? null)) {
+ return false;
+ }
+
+ if (
+ $this->fingerprints->shouldValidateCachedSourceResources()
+ && !$this->fingerprints->sourceResourcesAreFresh($metadata['source_resources'] ?? null)
+ ) {
+ return false;
+ }
+
+ $cachedContainerFile = sprintf('%s/%s', $this->cacheDir, basename($metadata['file']));
+
+ if (!is_file($cachedContainerFile)) {
+ return false;
+ }
+
+ require_once $cachedContainerFile;
+ $class = $metadata['class'];
+
+ if (!class_exists($class, false)) {
+ return false;
+ }
+
+ $container->useRuntimeContainer($this->newRuntimeContainer($class));
+
+ return true;
+ }
+
+ private function containerBuildTime(ContainerBuilder $builder): int
+ {
+ if ($builder->hasParameter('kernel.container_build_time')) {
+ $buildTime = $builder->getParameter('kernel.container_build_time');
+
+ if (is_int($buildTime)) {
+ return $buildTime;
+ }
+
+ if (is_string($buildTime) && ctype_digit($buildTime)) {
+ return (int) $buildTime;
+ }
+ }
+
+ $sourceDateEpoch = $_SERVER['SOURCE_DATE_EPOCH'] ?? $_ENV['SOURCE_DATE_EPOCH'] ?? null;
+
+ if (is_scalar($sourceDateEpoch) && filter_var($sourceDateEpoch, \FILTER_VALIDATE_INT) !== false) {
+ return (int) $sourceDateEpoch;
+ }
+
+ return time();
+ }
+
+ private function ensureSynthetic(ContainerBuilder $builder, string $id, string $class): void
+ {
+ if ($builder->hasDefinition($id)) {
+ return;
+ }
+
+ $builder->setDefinition(
+ $id,
+ (new Definition($class))
+ ->setSynthetic(true)
+ ->setPublic(true),
+ );
+ }
+
+ private function newRuntimeContainer(string $class): PsrContainerInterface
+ {
+ $instance = new $class();
+
+ if (!$instance instanceof PsrContainerInterface) {
+ throw new \RuntimeException(sprintf('Runtime container "%s" is invalid.', $class));
+ }
+
+ return $instance;
+ }
+}
diff --git a/src/Kernel/ContainerResourceFingerprinter.php b/src/Kernel/ContainerResourceFingerprinter.php
new file mode 100644
index 0000000..48d1194
--- /dev/null
+++ b/src/Kernel/ContainerResourceFingerprinter.php
@@ -0,0 +1,343 @@
+ $configFiles */
+ public function fingerprint(BundleRegistry $bundles, array $configFiles): string
+ {
+ sort($configFiles);
+
+ $parts = [
+ $this->projectDir,
+ $this->environment,
+ (string) (int) $this->debug,
+ $this->deploymentFingerprint(),
+ $this->kernelFingerprint(),
+ ...$bundles->identityFingerprintParts(),
+ ];
+
+ foreach ($configFiles as $file) {
+ $parts[] = sprintf(
+ '%s:%s',
+ $file,
+ is_file($file) ? $this->fileFingerprint($file) : 'missing',
+ );
+ }
+
+ return hash('sha256', implode('|', $parts));
+ }
+
+ public function tracksSourceChanges(): bool
+ {
+ return $this->debug && !$this->resourceTrackingDisabled();
+ }
+
+ /**
+ * @param array $values
+ * @return array
+ */
+ public function stringKeyMap(array $values): array
+ {
+ $map = [];
+
+ foreach ($values as $key => $value) {
+ if (!is_string($key)) {
+ continue;
+ }
+
+ $map[$key] = $value;
+ }
+
+ return $map;
+ }
+
+ /** @return array */
+ public function sourceResourceManifest(BundleRegistry $bundles): array
+ {
+ $resources = [];
+ $this->collectSourceResources(dirname(__DIR__, 2) . '/src', $resources);
+
+ foreach ($bundles->all() as $metadata) {
+ $this->collectSourceResources(sprintf('%s/src', rtrim($metadata->path(), '/')), $resources, 'php');
+
+ $bundleFile = (new \ReflectionObject($metadata->bundle()))->getFileName();
+
+ if (!is_string($bundleFile)) {
+ continue;
+ }
+
+ $resources[$bundleFile] = $this->sourceFileMtime($bundleFile);
+ }
+
+ ksort($resources);
+
+ return $resources;
+ }
+
+ /** @param array $resources */
+ public function sourceResourceFingerprint(array $resources): string
+ {
+ ksort($resources);
+ $parts = [];
+
+ foreach ($resources as $path => $fingerprint) {
+ $parts[] = sprintf('%s:%s', $path, $fingerprint);
+ }
+
+ return hash('sha256', implode('|', $parts));
+ }
+
+ public function sourceResourcesAreFresh(mixed $resources): bool
+ {
+ if (!is_array($resources) || $resources === []) {
+ return false;
+ }
+
+ foreach ($resources as $path => $expected) {
+ if (!is_string($path) || !is_string($expected)) {
+ return false;
+ }
+
+ $actual = str_starts_with($expected, 'dir:')
+ ? $this->sourceDirectoryMtime($path)
+ : $this->sourceFileMtime($path);
+
+ if ($actual !== $expected) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @param array $configFiles
+ * @return array
+ */
+ public function configResourceManifest(ContainerBuilder $builder, array $configFiles): array
+ {
+ $resources = [];
+
+ foreach ($configFiles as $file) {
+ if ($file === '') {
+ continue;
+ }
+
+ $resources[$file] = $this->fileFingerprint($file);
+ }
+
+ foreach ($builder->getResources() as $resource) {
+ if ($resource instanceof FileResource) {
+ $file = $resource->getResource();
+ $resources[$file] = $this->fileFingerprint($file);
+
+ continue;
+ }
+
+ if (!$resource instanceof FileExistenceResource) {
+ continue;
+ }
+
+ $file = $resource->getResource();
+ $resources[sprintf('exists:%s', $file)] = file_exists($file) ? 'exists:1' : 'exists:0';
+ }
+
+ ksort($resources);
+
+ return $resources;
+ }
+
+ public function configResourcesAreFresh(mixed $resources): bool
+ {
+ if (!is_array($resources) || $resources === []) {
+ return false;
+ }
+
+ foreach ($resources as $path => $expected) {
+ if (!is_string($path) || !is_string($expected)) {
+ return false;
+ }
+
+ if (str_starts_with($path, 'exists:')) {
+ $actual = file_exists(substr($path, 7)) ? 'exists:1' : 'exists:0';
+
+ if ($actual !== $expected) {
+ return false;
+ }
+
+ continue;
+ }
+
+ if ($this->fileFingerprint($path) !== $expected) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public function shouldValidateCachedSourceResources(): bool
+ {
+ $value = $_SERVER['SYMPRESS_KERNEL_VALIDATE_SOURCE_RESOURCES']
+ ?? $_ENV['SYMPRESS_KERNEL_VALIDATE_SOURCE_RESOURCES']
+ ?? null;
+
+ if ($value === null) {
+ return false;
+ }
+
+ return filter_var($value, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE) === true;
+ }
+
+ private function resourceTrackingDisabled(): bool
+ {
+ $value = $_SERVER['SYMFONY_DISABLE_RESOURCE_TRACKING']
+ ?? $_ENV['SYMFONY_DISABLE_RESOURCE_TRACKING']
+ ?? null;
+
+ if ($value === null) {
+ return false;
+ }
+
+ if (is_array($value)) {
+ return $value !== [];
+ }
+
+ if (!is_scalar($value) && !$value instanceof \Stringable) {
+ return true;
+ }
+
+ $value = (string) $value;
+
+ return filter_var($value, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE) ?? ($value !== '');
+ }
+
+ private function fileFingerprint(string $file): string
+ {
+ if (!is_file($file)) {
+ return 'missing';
+ }
+
+ if ($this->tracksSourceChanges()) {
+ $hash = sha1_file($file);
+
+ return is_string($hash) ? $hash : 'unreadable';
+ }
+
+ return $this->sourceFileMtime($file);
+ }
+
+ private function deploymentFingerprint(): string
+ {
+ $buildId = defined('SYMPRESS_KERNEL_BUILD_ID')
+ ? constant('SYMPRESS_KERNEL_BUILD_ID')
+ : getenv('SYMPRESS_KERNEL_BUILD_ID');
+
+ if ((is_scalar($buildId) || $buildId instanceof \Stringable) && (string) $buildId !== '') {
+ return 'build:' . (string) $buildId;
+ }
+
+ $composerLock = sprintf('%s/composer.lock', $this->projectDir);
+
+ if (is_file($composerLock)) {
+ return 'composer-lock:' . $this->fileFingerprint($composerLock);
+ }
+
+ return 'build:implicit';
+ }
+
+ private function kernelFingerprint(): string
+ {
+ $packageDir = dirname(__DIR__, 2);
+ $composerFile = sprintf('%s/composer.json', $packageDir);
+
+ return hash(
+ 'sha256',
+ implode(
+ '|',
+ [
+ $packageDir,
+ sprintf('%s:%s', $composerFile, $this->fileFingerprint($composerFile)),
+ ],
+ ),
+ );
+ }
+
+ /**
+ * @param array $resources
+ */
+ private function collectSourceResources(
+ string $directory,
+ array &$resources,
+ ?string $fileExtension = null,
+ ): void {
+
+ if (!is_dir($directory)) {
+ return;
+ }
+
+ $resources[$directory] = $this->sourceDirectoryMtime($directory);
+ $iterator = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
+ \RecursiveIteratorIterator::SELF_FIRST,
+ );
+
+ foreach ($iterator as $resource) {
+ if (!$resource instanceof \SplFileInfo) {
+ continue;
+ }
+
+ $pathname = $resource->getPathname();
+
+ if ($resource->isDir()) {
+ $resources[$pathname] = $this->sourceDirectoryMtime($pathname);
+
+ continue;
+ }
+
+ if (!$resource->isFile()) {
+ continue;
+ }
+
+ if ($fileExtension !== null && $resource->getExtension() !== $fileExtension) {
+ continue;
+ }
+
+ $resources[$pathname] = $this->sourceFileMtime($pathname);
+ }
+ }
+
+ private function sourceDirectoryMtime(string $directory): string
+ {
+ if (!is_dir($directory)) {
+ return 'missing';
+ }
+
+ return sprintf('dir:%s', (string) filemtime($directory));
+ }
+
+ private function sourceFileMtime(string $file): string
+ {
+ if (!is_file($file)) {
+ return 'missing';
+ }
+
+ return sprintf('file:%s:%s', (string) filemtime($file), (string) filesize($file));
+ }
+}
diff --git a/src/Kernel/CoreServiceRegistrar.php b/src/Kernel/CoreServiceRegistrar.php
new file mode 100644
index 0000000..e02dfb2
--- /dev/null
+++ b/src/Kernel/CoreServiceRegistrar.php
@@ -0,0 +1,565 @@
+ */
+ private array $preparedBuilders = [];
+
+ public function prepare(ContainerBuilder $builder, \Closure $build): void
+ {
+ $builderId = spl_object_id($builder);
+
+ if (($this->preparedBuilders[$builderId] ?? false) === true) {
+ return;
+ }
+
+ $this->preparedBuilders[$builderId] = true;
+ $this->registerCoreContainerServices($builder);
+ $this->registerTranslationLoader($builder);
+ $this->registerHookLoader($builder);
+ $this->registerRouteLoader($builder);
+ $this->registerConsoleApplication($builder);
+ $this->registerConsoleAttributes($builder);
+ $builder->registerAttributeForAutoconfiguration(
+ AsHook::class,
+ static function (ChildDefinition $definition, AsHook $attribute, \Reflector $reflector): void {
+ if (!$reflector instanceof \ReflectionClass && !$reflector instanceof \ReflectionMethod) {
+ return;
+ }
+
+ $tag = $attribute->toTag();
+
+ if ($reflector instanceof \ReflectionMethod && $attribute->method === '__invoke') {
+ $tag['method'] = $reflector->getName();
+ }
+
+ $definition->addTag(HookLoader::TAG, $tag);
+ },
+ );
+ $this->registerRouteAttributes($builder);
+ $builder->addCompilerPass(new HookCompilerPass());
+ $builder->addCompilerPass(new RouteCompilerPass());
+ $build($builder);
+ }
+
+ private function registerCoreContainerServices(ContainerBuilder $builder): void
+ {
+ if (!$builder->has('kernel')) {
+ $builder->setAlias('kernel', Container::KERNEL_ID)->setPublic(true);
+ }
+
+ $builder->setAlias(HttpKernelInterface::class, Container::KERNEL_ID)
+ ->setPublic(true);
+ $builder->setAlias(DependencyInjectionKernelInterface::class, Container::KERNEL_ID)
+ ->setPublic(true);
+
+ $this->registerFilesystemService($builder);
+ $this->registerEventDispatcherServices($builder);
+ $this->registerClockService($builder);
+ $this->registerExpressionLanguageService($builder);
+
+ if (!$builder->hasDefinition('parameter_bag')) {
+ $builder->setDefinition(
+ 'parameter_bag',
+ (new Definition(ContainerBag::class))
+ ->setArguments([new Reference('service_container')]),
+ );
+ }
+
+ $builder->setAlias(ContainerBagInterface::class, 'parameter_bag')->setPublic(false);
+ $builder->setAlias(ParameterBagInterface::class, 'parameter_bag')->setPublic(false);
+
+ if (!$builder->hasDefinition('file_locator')) {
+ $builder->setDefinition(
+ 'file_locator',
+ (new Definition(FileLocator::class))
+ ->setArguments([new Reference(Container::KERNEL_ID)]),
+ );
+ }
+
+ $builder->setAlias(FileLocator::class, 'file_locator')->setPublic(false);
+
+ if (!$builder->hasDefinition('reverse_container')) {
+ $builder->setDefinition(
+ 'reverse_container',
+ (new Definition(ReverseContainer::class))
+ ->setArguments([
+ new Reference('service_container'),
+ new ServiceLocatorArgument([]),
+ ]),
+ );
+ }
+
+ $builder->setAlias(ReverseContainer::class, 'reverse_container')->setPublic(false);
+
+ if (!$builder->hasDefinition('config_cache_factory')) {
+ $builder->setDefinition(
+ 'config_cache_factory',
+ (new Definition(ResourceCheckerConfigCacheFactory::class))
+ ->setArguments([new TaggedIteratorArgument('config_cache.resource_checker')]),
+ );
+ }
+
+ if (!$builder->hasDefinition('dependency_injection.config.container_parameters_resource_checker')) {
+ $builder->setDefinition(
+ 'dependency_injection.config.container_parameters_resource_checker',
+ (new Definition(ContainerParametersResourceChecker::class))
+ ->setArguments([new Reference('service_container')])
+ ->addTag('config_cache.resource_checker', ['priority' => -980]),
+ );
+ }
+
+ if (!$builder->hasDefinition('config.resource.self_checking_resource_checker')) {
+ $builder->setDefinition(
+ 'config.resource.self_checking_resource_checker',
+ (new Definition(SelfCheckingResourceChecker::class))
+ ->addTag('config_cache.resource_checker', ['priority' => -990]),
+ );
+ }
+
+ if (!$builder->hasDefinition('services_resetter')) {
+ $builder->setDefinition(
+ 'services_resetter',
+ (new Definition(ServicesResetter::class))
+ ->setPublic(true)
+ ->setArguments([new IteratorArgument([]), []]),
+ );
+ }
+
+ $builder->setAlias(ServicesResetterInterface::class, 'services_resetter')->setPublic(true);
+
+ if (!$builder->hasDefinition('container.env_var_processor')) {
+ $builder->setDefinition(
+ 'container.env_var_processor',
+ (new Definition(EnvVarProcessor::class))
+ ->setArguments([
+ new Reference('service_container'),
+ new TaggedIteratorArgument('container.env_var_loader'),
+ ])
+ ->addTag('container.env_var_processor')
+ ->addTag('kernel.reset', ['method' => 'reset']),
+ );
+ }
+
+ $builder->registerForAutoconfiguration(EnvVarLoaderInterface::class)
+ ->addTag('container.env_var_loader');
+ $builder->registerForAutoconfiguration(EnvVarProcessorInterface::class)
+ ->addTag('container.env_var_processor');
+ $builder->registerForAutoconfiguration(ResourceCheckerInterface::class)
+ ->addTag('config_cache.resource_checker');
+ $builder->registerForAutoconfiguration(ServiceLocator::class)
+ ->addTag('container.service_locator');
+ $builder->registerForAutoconfiguration(ResetInterface::class)
+ ->addTag('kernel.reset', ['method' => 'reset']);
+ $builder->registerForAutoconfiguration(ServiceSubscriberInterface::class)
+ ->addTag('container.service_subscriber');
+ $builder->registerForAutoconfiguration(CompilerPassInterface::class)
+ ->addTag('container.excluded', ['source' => 'because it is a compiler pass']);
+ $builder->registerForAutoconfiguration(\UnitEnum::class)
+ ->addTag('container.excluded', ['source' => 'because it is an enum']);
+ $builder->registerAttributeForAutoconfiguration(
+ \Attribute::class,
+ static function (ChildDefinition $definition): void {
+ $definition->addTag('container.excluded', ['source' => 'because it is a PHP attribute']);
+ },
+ );
+ $this->registerOptionalAutoconfiguration($builder);
+
+ $builder->addCompilerPass(
+ new AddBehaviorDescribingTagsPass(
+ [
+ 'container.do_not_inline',
+ 'container.excluded',
+ 'container.hot_path',
+ 'container.service_locator',
+ 'container.service_subscriber',
+ 'event_dispatcher.dispatcher',
+ 'kernel.event_listener',
+ 'kernel.event_subscriber',
+ 'kernel.reset',
+ ],
+ ),
+ PassConfig::TYPE_BEFORE_OPTIMIZATION,
+ 200,
+ );
+ $builder->addCompilerPass(new ResettableServicePass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
+ }
+
+ private function registerFilesystemService(ContainerBuilder $builder): void
+ {
+ if (!$builder->hasDefinition('filesystem')) {
+ $builder->setDefinition('filesystem', new Definition(Filesystem::class));
+ }
+
+ $this->setAliasIfMissing($builder, Filesystem::class, 'filesystem');
+ }
+
+ private function registerEventDispatcherServices(ContainerBuilder $builder): void
+ {
+ $eventDispatcherClass = 'Symfony\Component\EventDispatcher\EventDispatcher';
+
+ if (class_exists($eventDispatcherClass) && !$builder->hasDefinition('event_dispatcher')) {
+ $builder->setDefinition(
+ 'event_dispatcher',
+ (new Definition($eventDispatcherClass))
+ ->setPublic(true)
+ ->addTag('container.hot_path')
+ ->addTag('event_dispatcher.dispatcher', ['name' => 'event_dispatcher']),
+ );
+ }
+
+ $this->registerEventDispatcherAliases($builder);
+
+ $registerListenersPass = 'Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass';
+
+ if (class_exists($registerListenersPass)) {
+ $builder->addCompilerPass(new $registerListenersPass(), PassConfig::TYPE_BEFORE_REMOVING);
+ }
+
+ foreach (
+ [
+ 'Symfony\Component\EventDispatcher\EventDispatcherInterface' => 'event_dispatcher.dispatcher',
+ 'Symfony\Component\EventDispatcher\EventSubscriberInterface' => 'kernel.event_subscriber',
+ ] as $interface => $tag
+ ) {
+ if (!interface_exists($interface)) {
+ continue;
+ }
+
+ $builder->registerForAutoconfiguration($interface)->addTag($tag);
+ }
+
+ $asEventListenerClass = 'Symfony\Component\EventDispatcher\Attribute\AsEventListener';
+
+ if (!class_exists($asEventListenerClass)) {
+ return;
+ }
+
+ $builder->registerAttributeForAutoconfiguration(
+ $asEventListenerClass,
+ static function (ChildDefinition $definition, object $attribute, \Reflector $reflector): void {
+ if (!$reflector instanceof \ReflectionClass && !$reflector instanceof \ReflectionMethod) {
+ return;
+ }
+
+ $tagAttributes = array_filter(
+ get_object_vars($attribute),
+ static fn (mixed $value): bool => $value !== null,
+ );
+
+ if ($reflector instanceof \ReflectionMethod) {
+ if (isset($tagAttributes['method'])) {
+ throw new \LogicException(
+ sprintf(
+ 'AsEventListener attribute cannot declare a method on "%s::%s()".',
+ $reflector->class,
+ $reflector->name,
+ ),
+ );
+ }
+
+ $tagAttributes['method'] = $reflector->getName();
+ }
+
+ $definition->addTag('kernel.event_listener', $tagAttributes);
+ },
+ );
+ }
+
+ private function registerEventDispatcherAliases(ContainerBuilder $builder): void
+ {
+ if (!$builder->hasDefinition('event_dispatcher') && !$builder->hasAlias('event_dispatcher')) {
+ return;
+ }
+
+ foreach (
+ [
+ 'Symfony\Component\EventDispatcher\EventDispatcherInterface',
+ 'Symfony\Contracts\EventDispatcher\EventDispatcherInterface',
+ 'Psr\EventDispatcher\EventDispatcherInterface',
+ ] as $eventDispatcherInterface
+ ) {
+ if (!interface_exists($eventDispatcherInterface)) {
+ continue;
+ }
+
+ $this->setAliasIfMissing($builder, $eventDispatcherInterface, 'event_dispatcher', true);
+ }
+ }
+
+ private function registerClockService(ContainerBuilder $builder): void
+ {
+ $clockClass = 'Symfony\Component\Clock\Clock';
+
+ if (!class_exists($clockClass)) {
+ return;
+ }
+
+ if (!$builder->hasDefinition('clock')) {
+ $builder->setDefinition('clock', new Definition($clockClass));
+ }
+
+ foreach (['Symfony\Component\Clock\ClockInterface', 'Psr\Clock\ClockInterface'] as $clockInterface) {
+ if (!interface_exists($clockInterface)) {
+ continue;
+ }
+
+ $this->setAliasIfMissing($builder, $clockInterface, 'clock');
+ }
+ }
+
+ private function registerExpressionLanguageService(ContainerBuilder $builder): void
+ {
+ $expressionLanguageClass = 'Symfony\Component\DependencyInjection\ExpressionLanguage';
+
+ if (!class_exists($expressionLanguageClass) || $builder->hasDefinition('container.expression_language')) {
+ return;
+ }
+
+ $builder->setDefinition('container.expression_language', new Definition($expressionLanguageClass));
+ }
+
+ private function registerOptionalAutoconfiguration(ContainerBuilder $builder): void
+ {
+ $this->registerLoggerAwareAutoconfiguration($builder);
+ $this->registerTestCaseExclusion($builder);
+ $this->registerLoaderInterfaceExclusion($builder);
+ }
+
+ private function registerLoggerAwareAutoconfiguration(ContainerBuilder $builder): void
+ {
+ $loggerAwareInterface = 'Psr\Log\LoggerAwareInterface';
+
+ if (!interface_exists($loggerAwareInterface)) {
+ return;
+ }
+
+ $builder->registerForAutoconfiguration($loggerAwareInterface)
+ ->addMethodCall(
+ 'setLogger',
+ [new Reference('logger', SymfonyDiContainerInterface::IGNORE_ON_INVALID_REFERENCE)],
+ );
+ }
+
+ private function registerTestCaseExclusion(ContainerBuilder $builder): void
+ {
+ $testCaseClass = 'PHPUnit\Framework\TestCase';
+
+ if (!class_exists($testCaseClass)) {
+ return;
+ }
+
+ $builder->registerForAutoconfiguration($testCaseClass)
+ ->addTag('container.excluded', ['source' => 'because it is a test case']);
+ }
+
+ private function registerLoaderInterfaceExclusion(ContainerBuilder $builder): void
+ {
+ if ($builder->hasDefinition(LoaderInterface::class)) {
+ return;
+ }
+
+ $builder->setDefinition(
+ LoaderInterface::class,
+ (new Definition())
+ ->setAbstract(true)
+ ->addTag('container.excluded', ['source' => 'because it is a loader interface']),
+ );
+ }
+
+ private function setAliasIfMissing(
+ ContainerBuilder $builder,
+ string $alias,
+ string $target,
+ bool $public = false,
+ ): void {
+ if ($builder->hasAlias($alias) || $builder->hasDefinition($alias)) {
+ return;
+ }
+
+ $builder->setAlias($alias, $target)->setPublic($public);
+ }
+
+ private function registerTranslationLoader(ContainerBuilder $builder): void
+ {
+ if ($builder->hasDefinition(TranslationLoader::class)) {
+ return;
+ }
+
+ $builder->setDefinition(
+ TranslationLoader::class,
+ (new Definition(TranslationLoader::class))
+ ->setPublic(true)
+ ->setArguments(['%kernel.translation_paths%']),
+ );
+ }
+
+ private function registerConsoleApplication(ContainerBuilder $builder): void
+ {
+ if (!$builder->hasDefinition(ConsoleApplicationFactory::class)) {
+ $builder->setDefinition(
+ ConsoleApplicationFactory::class,
+ (new Definition(ConsoleApplicationFactory::class))
+ ->setPublic(true)
+ ->setArguments([
+ new Reference(Container::KERNEL_ID),
+ new Reference('console.command_loader'),
+ ]),
+ );
+ }
+
+ if (!$builder->hasDefinition(Application::class)) {
+ $builder->setDefinition(
+ Application::class,
+ (new Definition(Application::class))
+ ->setPublic(true)
+ ->setFactory([
+ new Reference(ConsoleApplicationFactory::class),
+ 'create',
+ ]),
+ );
+ }
+
+ if ($builder->hasDefinition(WpCliConsoleBridge::class)) {
+ return;
+ }
+
+ $builder->setDefinition(
+ WpCliConsoleBridge::class,
+ (new Definition(WpCliConsoleBridge::class))
+ ->setArguments([new Reference(Application::class)])
+ ->addTag(
+ HookLoader::TAG,
+ [
+ 'hook' => 'muplugins_loaded',
+ 'method' => 'register',
+ 'priority' => 1,
+ ],
+ ),
+ );
+ }
+
+ private function registerConsoleAttributes(ContainerBuilder $builder): void
+ {
+ $builder->registerAttributeForAutoconfiguration(
+ AsCommand::class,
+ static function (ChildDefinition $definition): void {
+ $definition->addTag('console.command');
+ },
+ );
+ }
+
+ private function registerHookLoader(ContainerBuilder $builder): void
+ {
+ if ($builder->hasDefinition(HookLoader::class)) {
+ return;
+ }
+
+ $builder->setDefinition(
+ HookLoader::class,
+ (new Definition(HookLoader::class))
+ ->setPublic(true)
+ ->setArguments([null, []]),
+ );
+ }
+
+ private function registerRouteLoader(ContainerBuilder $builder): void
+ {
+ if ($builder->hasDefinition(RouteLoader::class)) {
+ return;
+ }
+
+ $builder->setDefinition(
+ RouteLoader::class,
+ (new Definition(RouteLoader::class))
+ ->setPublic(true)
+ ->setArguments([null, [], []])
+ ->addTag(
+ HookLoader::TAG,
+ [
+ 'hook' => 'template_redirect',
+ 'method' => 'dispatchFrontendRequest',
+ 'priority' => 0,
+ ],
+ )
+ ->addTag(
+ HookLoader::TAG,
+ [
+ 'hook' => 'rest_api_init',
+ 'method' => 'registerRestRoutes',
+ ],
+ ),
+ );
+ }
+
+ private function registerRouteAttributes(ContainerBuilder $builder): void
+ {
+ foreach (
+ [
+ Route::class,
+ 'Symfony\Component\Routing\Attribute\Route',
+ ] as $attributeClass
+ ) {
+ if (!class_exists($attributeClass)) {
+ continue;
+ }
+
+ $builder->registerAttributeForAutoconfiguration(
+ $attributeClass,
+ static function (ChildDefinition $definition): void {
+ $definition->addTag(RouteLoader::TAG);
+ },
+ );
+ }
+ }
+}
diff --git a/src/Kernel/KernelConfigurationResolver.php b/src/Kernel/KernelConfigurationResolver.php
new file mode 100644
index 0000000..9e60aef
--- /dev/null
+++ b/src/Kernel/KernelConfigurationResolver.php
@@ -0,0 +1,334 @@
+serverString('APP_CACHE_DIR');
+
+ if ($dir !== null) {
+ return sprintf('%s/kernel', $this->environmentDirectory($dir));
+ }
+
+ return sprintf('%s/var/cache/%s/kernel', $this->projectDir, $this->environment);
+ }
+
+ public function buildDir(): string
+ {
+ $dir = $this->serverString('APP_BUILD_DIR');
+
+ if ($dir !== null) {
+ return sprintf('%s/kernel', $this->environmentDirectory($dir));
+ }
+
+ return $this->cacheDir();
+ }
+
+ public function shareDir(): ?string
+ {
+ $dir = $this->serverNullableDirectory('APP_SHARE_DIR');
+
+ if ($dir !== null) {
+ return sprintf('%s/kernel', $this->environmentDirectory($dir));
+ }
+
+ if ($this->serverValueIsFalse('APP_SHARE_DIR')) {
+ return null;
+ }
+
+ return $this->cacheDir();
+ }
+
+ public function logDir(): ?string
+ {
+ $dir = $this->serverNullableDirectory('APP_LOG_DIR');
+
+ if ($dir !== null) {
+ return $this->environmentDirectory($dir);
+ }
+
+ if ($this->serverValueIsFalse('APP_LOG_DIR')) {
+ return null;
+ }
+
+ return sprintf('%s/var/log', $this->projectDir);
+ }
+
+ /** @return list */
+ public function packagePrefixes(): array
+ {
+ $configured = $this->config->get('KERNEL_PACKAGE_PREFIXES', null);
+
+ if ($configured === null) {
+ $configured = $this->composerKernelPackagePrefixes();
+ }
+
+ return $this->normalizePackagePrefixes($configured);
+ }
+
+ /** @return list */
+ public function knownEnvironments(): array
+ {
+ $known = [
+ $this->environment,
+ 'all',
+ 'dev',
+ 'development',
+ 'local',
+ 'prod',
+ 'production',
+ 'stage',
+ 'staging',
+ 'test',
+ ];
+ $bundlesDefinition = sprintf('%s/config/bundles.php', $this->projectDir);
+
+ if (!is_file($bundlesDefinition)) {
+ return $this->normalizeKnownEnvironments($known);
+ }
+
+ $configuration = require $bundlesDefinition;
+
+ if (!is_array($configuration)) {
+ return $this->normalizeKnownEnvironments($known);
+ }
+
+ foreach ($configuration as $envs) {
+ if (!is_array($envs)) {
+ continue;
+ }
+
+ foreach (array_keys($envs) as $environment) {
+ if (!is_string($environment)) {
+ continue;
+ }
+
+ $known[] = $environment;
+ }
+ }
+
+ return $this->normalizeKnownEnvironments($known);
+ }
+
+ /** @return array */
+ public function configDirectories(BundleRegistry $bundles): array
+ {
+ $directories = [];
+ $libraryDir = dirname(__DIR__, 2) . '/config';
+ $siteDir = sprintf('%s/config', $this->projectDir);
+
+ if (is_dir($libraryDir)) {
+ $directories[] = $libraryDir;
+ }
+
+ foreach ($bundles->configDirectories() as $configDir) {
+ $directories[] = $configDir;
+ }
+
+ if (is_dir($siteDir)) {
+ $directories[] = $siteDir;
+ }
+
+ return array_values(array_unique($directories));
+ }
+
+ /** @return array */
+ public function resolveConfigFiles(string $configDir): array
+ {
+ $files = [];
+
+ foreach ($this->patterns($configDir) as $pattern) {
+ $matches = glob($pattern, GLOB_BRACE) ?: [];
+ sort($matches);
+ $files = [...$files, ...$matches];
+ }
+
+ return $files;
+ }
+
+ /** @return array */
+ public function runtimeConfigFiles(BundleRegistry $bundles): array
+ {
+ $files = [];
+
+ foreach ($this->configDirectories($bundles) as $configDir) {
+ $files = [...$files, ...$this->resolveConfigFiles($configDir)];
+ }
+
+ return array_values(array_unique($files));
+ }
+
+ /**
+ * @param array $values
+ * @return array
+ */
+ public function stringKeyMap(array $values): array
+ {
+ $map = [];
+
+ foreach ($values as $key => $value) {
+ if (!is_string($key)) {
+ continue;
+ }
+
+ $map[$key] = $value;
+ }
+
+ return $map;
+ }
+
+ private function composerKernelPackagePrefixes(): mixed
+ {
+ $composerFile = sprintf('%s/composer.json', $this->projectDir);
+
+ if (!is_file($composerFile)) {
+ return null;
+ }
+
+ $contents = file_get_contents($composerFile);
+
+ if (!is_string($contents) || $contents === '') {
+ return null;
+ }
+
+ $metadata = json_decode($contents, true);
+
+ if (!is_array($metadata)) {
+ return null;
+ }
+
+ $metadata = $this->stringKeyMap($metadata);
+ $extra = $metadata['extra'] ?? null;
+ $kernel = is_array($extra) ? ($extra['kernel'] ?? null) : null;
+
+ if (!is_array($kernel)) {
+ return null;
+ }
+
+ return $kernel['package_prefixes'] ?? $kernel['packagePrefixes'] ?? null;
+ }
+
+ /** @return list */
+ private function normalizePackagePrefixes(mixed $packagePrefixes): array
+ {
+ if (is_string($packagePrefixes)) {
+ $packagePrefixes = preg_split('/[,\s]+/', $packagePrefixes) ?: [];
+ }
+
+ if (!is_array($packagePrefixes)) {
+ return [];
+ }
+
+ $normalized = [];
+
+ foreach ($packagePrefixes as $prefix) {
+ if (!is_scalar($prefix) && !$prefix instanceof \Stringable) {
+ continue;
+ }
+
+ $prefix = trim((string) $prefix);
+
+ if ($prefix === '') {
+ continue;
+ }
+
+ $normalized[] = str_ends_with($prefix, '/') ? $prefix : "{$prefix}/";
+ }
+
+ return array_values(array_unique($normalized));
+ }
+
+ /**
+ * @param list $environments
+ * @return list
+ */
+ private function normalizeKnownEnvironments(array $environments): array
+ {
+ return array_values(
+ array_unique(
+ array_filter($environments, static fn (string $env): bool => $env !== ''),
+ ),
+ );
+ }
+
+ private function serverString(string $name): ?string
+ {
+ $value = $_SERVER[$name] ?? $_ENV[$name] ?? null;
+
+ if (!is_scalar($value) && !$value instanceof \Stringable) {
+ return null;
+ }
+
+ $value = trim((string) $value);
+
+ return $value === '' ? null : $value;
+ }
+
+ private function serverNullableDirectory(string $name): ?string
+ {
+ if ($this->serverValueIsFalse($name)) {
+ return null;
+ }
+
+ return $this->serverString($name);
+ }
+
+ private function serverValueIsFalse(string $name): bool
+ {
+ $value = $_SERVER[$name] ?? $_ENV[$name] ?? null;
+
+ if ($value === null) {
+ return false;
+ }
+
+ return filter_var($value, \FILTER_VALIDATE_BOOL, \FILTER_NULL_ON_FAILURE) === false;
+ }
+
+ private function environmentDirectory(string $directory): string
+ {
+ if ($directory !== '' && in_array($directory[0], ['/', '\\'], true)) {
+ return sprintf('%s/%s', rtrim($directory, '/'), $this->environment);
+ }
+
+ if (
+ DIRECTORY_SEPARATOR === '\\'
+ && isset($directory[1])
+ && $directory[1] === ':'
+ && preg_match('/^[A-Za-z]:/', $directory) === 1
+ ) {
+ return sprintf('%s/%s', rtrim($directory, '/'), $this->environment);
+ }
+
+ return sprintf('%s/%s/%s', $this->projectDir, trim($directory, '/'), $this->environment);
+ }
+
+ /** @return array */
+ private function patterns(string $configDir): array
+ {
+ $env = $this->environment;
+ $extensions = '{php,yaml,yml,ini}';
+
+ return [
+ sprintf('%s/packages/*.%s', $configDir, $extensions),
+ sprintf('%s/packages/%s/*.%s', $configDir, $env, $extensions),
+ sprintf('%s/services.%s', $configDir, $extensions),
+ sprintf('%s/services_%s.%s', $configDir, $env, $extensions),
+ sprintf('%s/wordpress.%s', $configDir, $extensions),
+ sprintf('%s/wordpress_%s.%s', $configDir, $env, $extensions),
+ ];
+ }
+}
diff --git a/src/Kernel/KernelInterface.php b/src/Kernel/KernelInterface.php
index ac7684d..b693953 100644
--- a/src/Kernel/KernelInterface.php
+++ b/src/Kernel/KernelInterface.php
@@ -7,10 +7,12 @@
use SymPress\Kernel\Bundle\BundleInterface;
use SymPress\Kernel\Bundle\BundleRegistry;
use SymPress\Kernel\Container;
+use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\HttpKernel\KernelInterface as SymfonyKernelInterface;
+use Symfony\Component\DependencyInjection\Kernel\KernelInterface as SymfonyKernelInterface;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
-interface KernelInterface extends SymfonyKernelInterface
+interface KernelInterface extends HttpKernelInterface, SymfonyKernelInterface
{
public function getProjectDir(): string;
@@ -37,6 +39,11 @@ public function getBundles(): array;
public function getBundle(string $name): BundleInterface;
+ /** @return iterable */
+ public function registerBundles(): iterable;
+
+ public function registerContainerConfiguration(LoaderInterface $loader): void;
+
public function locateResource(string $name): string;
public function createContainer(): Container;
diff --git a/src/Location/LocationResolver.php b/src/Location/LocationResolver.php
index a7a6ca9..482dd2a 100644
--- a/src/Location/LocationResolver.php
+++ b/src/Location/LocationResolver.php
@@ -107,11 +107,14 @@ private function discoverVendorPath(): ?string
private function resolve(string $location, string $dirOrUrl, ?string $subDir = null): ?string
{
- $envBase = (string) $this->config->get('WP_APP_' . strtoupper(sprintf('%s_%s', $location, $dirOrUrl)));
+ $configuredBase = $this->config->get('WP_APP_' . strtoupper(sprintf('%s_%s', $location, $dirOrUrl)));
+ $envBase = is_scalar($configuredBase) || $configuredBase instanceof \Stringable
+ ? (string) $configuredBase
+ : '';
$base = $envBase !== ''
? ($dirOrUrl === self::DIR ? wp_normalize_path($envBase) : $envBase)
- : ($this->locations[$dirOrUrl][$location] ?? null);
+ : $this->locations[$dirOrUrl][$location] ?? null;
if ($base === null && array_key_exists($location, self::CONTENT_LOCATIONS)) {
$contentBase = $this->resolve(Locations::CONTENT, $dirOrUrl);
@@ -147,7 +150,7 @@ private function locationsByConfig(EnvConfig $config): array
return [];
}
- return $this->parseExtendedDefaults($locations);
+ return $this->parseExtendedDefaults($this->stringKeyMap($locations));
}
/**
@@ -178,4 +181,23 @@ private function parseExtendedDefaults(array $locations): array
return array_filter($custom);
}
+
+ /**
+ * @param array $values
+ * @return array
+ */
+ private function stringKeyMap(array $values): array
+ {
+ $map = [];
+
+ foreach ($values as $key => $value) {
+ if (!is_string($key)) {
+ continue;
+ }
+
+ $map[$key] = $value;
+ }
+
+ return $map;
+ }
}
diff --git a/src/Location/VipLocations.php b/src/Location/VipLocations.php
index 83bb256..a95e607 100644
--- a/src/Location/VipLocations.php
+++ b/src/Location/VipLocations.php
@@ -23,28 +23,32 @@ public static function createFromConfig(EnvConfig $config): Locations
private function __construct(EnvConfig $config)
{
$baseResolver = new LocationResolver($config);
- $contentUrl = $baseResolver->resolveUrl(self::CONTENT);
- $contentDir = $baseResolver->resolveDir(self::CONTENT);
+ $contentUrl = $baseResolver->resolveUrl(self::CONTENT) ?? '';
+ $contentDir = $baseResolver->resolveDir(self::CONTENT) ?? '';
$privateDir = defined('WPCOM_VIP_PRIVATE_DIR')
- ? trailingslashit(wp_normalize_path((string) WPCOM_VIP_PRIVATE_DIR))
+ ? trailingslashit(wp_normalize_path($this->constantString('WPCOM_VIP_PRIVATE_DIR')))
: null;
$clientMuDir = defined('WPCOM_VIP_CLIENT_MU_PLUGIN_DIR')
- ? trailingslashit(wp_normalize_path((string) WPCOM_VIP_CLIENT_MU_PLUGIN_DIR))
+ ? trailingslashit(wp_normalize_path($this->constantString('WPCOM_VIP_CLIENT_MU_PLUGIN_DIR')))
: sprintf('%sclient-mu-plugins/', $contentDir);
$clientMuUrl = sprintf('%sclient-mu-plugins/', $contentUrl);
- $abspath = trailingslashit(wp_normalize_path((string) ABSPATH));
+ $abspath = trailingslashit(wp_normalize_path($this->constantString('ABSPATH')));
+ $directories = [
+ self::IMAGES => sprintf('%simages/', $contentDir),
+ self::CLIENT_MU_PLUGINS => $clientMuDir,
+ self::VENDOR => sprintf('%svendor/', $clientMuDir),
+ self::VIP_CONFIG => sprintf('%svip-config/', $abspath),
+ ];
+
+ if ($privateDir !== null) {
+ $directories[self::PRIVATE] = $privateDir;
+ }
$this->injectResolver(
new LocationResolver(
$config,
[
- LocationResolver::DIR => [
- self::IMAGES => sprintf('%simages/', $contentDir),
- self::CLIENT_MU_PLUGINS => $clientMuDir,
- self::VENDOR => sprintf('%svendor/', $clientMuDir),
- self::PRIVATE => $privateDir,
- self::VIP_CONFIG => sprintf('%svip-config/', $abspath),
- ],
+ LocationResolver::DIR => $directories,
LocationResolver::URL => [
self::IMAGES => sprintf('%simages', $contentUrl),
self::CLIENT_MU_PLUGINS => $clientMuUrl,
@@ -55,6 +59,21 @@ private function __construct(EnvConfig $config)
);
}
+ private function constantString(string $name): string
+ {
+ if (!defined($name)) {
+ return '';
+ }
+
+ $value = constant($name);
+
+ if (is_scalar($value) || $value instanceof \Stringable) {
+ return (string) $value;
+ }
+
+ return '';
+ }
+
public function clientMuPluginsDir(string $path = '/'): ?string
{
return $this->resolveDir(self::CLIENT_MU_PLUGINS, $path);
diff --git a/src/Package/PackageDiscovery.php b/src/Package/PackageDiscovery.php
index 5b7266c..7a0f29f 100644
--- a/src/Package/PackageDiscovery.php
+++ b/src/Package/PackageDiscovery.php
@@ -80,10 +80,10 @@ private function package(string $packageName): ?PackageMetadata
}
$metadata = $this->composerMetadata($composerFile);
- $kernel = $metadata['extra']['kernel'] ?? null;
- $bundleClass = is_array($kernel) ? (string) ($kernel['bundle'] ?? '') : '';
- $entry = is_array($kernel) ? (string) ($kernel['entry'] ?? '') : '';
- $type = (string) ($metadata['type'] ?? '');
+ $kernel = $this->kernelMetadata($metadata);
+ $bundleClass = $this->stringValue($kernel['bundle'] ?? null);
+ $entry = $this->stringValue($kernel['entry'] ?? null);
+ $type = $this->stringValue($metadata['type'] ?? null);
if ($bundleClass === '' || $entry === '' || $type === '') {
return null;
@@ -127,9 +127,9 @@ private function displayData(
): array {
$data = [
- 'name' => $this->humanName((string) ($metadata['name'] ?? '')),
- 'description' => (string) ($metadata['description'] ?? ''),
- 'version' => (string) ($metadata['version'] ?? ''),
+ 'name' => $this->humanName($this->stringValue($metadata['name'] ?? null)),
+ 'description' => $this->stringValue($metadata['description'] ?? null),
+ 'version' => $this->stringValue($metadata['version'] ?? null),
];
if ($type === 'wordpress-theme') {
@@ -231,7 +231,7 @@ private function composerMetadata(string $composerFile): array
$decoded = json_decode($contents, true);
- $this->metadata[$composerFile] = is_array($decoded) ? $decoded : [];
+ $this->metadata[$composerFile] = is_array($decoded) ? $this->stringKeyMap($decoded) : [];
return $this->metadata[$composerFile];
}
@@ -357,9 +357,9 @@ private function requirementsActive(array $requirements): bool
}
$metadata = $this->composerMetadata($composerFile);
- $kernel = $metadata['extra']['kernel'] ?? null;
- $entry = is_array($kernel) ? (string) ($kernel['entry'] ?? '') : '';
- $type = (string) ($metadata['type'] ?? '');
+ $kernel = $this->kernelMetadata($metadata);
+ $entry = $this->stringValue($kernel['entry'] ?? null);
+ $type = $this->stringValue($metadata['type'] ?? null);
if ($entry === '' || $type === '') {
return false;
@@ -421,4 +421,49 @@ private function loadPluginAdminFunctions(): void
require_once $file;
}
+
+ /**
+ * @param array $metadata
+ * @return array
+ */
+ private function kernelMetadata(array $metadata): array
+ {
+ $extra = $metadata['extra'] ?? null;
+
+ if (!is_array($extra)) {
+ return [];
+ }
+
+ $kernel = $extra['kernel'] ?? null;
+
+ return is_array($kernel) ? $this->stringKeyMap($kernel) : [];
+ }
+
+ private function stringValue(mixed $value): string
+ {
+ if (is_scalar($value) || $value instanceof \Stringable) {
+ return (string) $value;
+ }
+
+ return '';
+ }
+
+ /**
+ * @param array $values
+ * @return array
+ */
+ private function stringKeyMap(array $values): array
+ {
+ $map = [];
+
+ foreach ($values as $key => $value) {
+ if (!is_string($key)) {
+ continue;
+ }
+
+ $map[$key] = $value;
+ }
+
+ return $map;
+ }
}
diff --git a/src/Routing/RouteCompilerPass.php b/src/Routing/RouteCompilerPass.php
index a89cb53..60612d7 100644
--- a/src/Routing/RouteCompilerPass.php
+++ b/src/Routing/RouteCompilerPass.php
@@ -21,8 +21,11 @@ public function process(ContainerBuilder $container): void
return;
}
- $environment = $container->hasParameter('kernel.environment')
- ? (string) $container->getParameter('kernel.environment')
+ $environmentParameter = $container->hasParameter('kernel.environment')
+ ? $container->getParameter('kernel.environment')
+ : null;
+ $environment = is_scalar($environmentParameter) || $environmentParameter instanceof \Stringable
+ ? (string) $environmentParameter
: null;
$frontendRoutes = [];
$restRoutes = [];
@@ -60,7 +63,49 @@ public function process(ContainerBuilder $container): void
$definition->setArgument(2, $restRoutes);
}
- /** @return array{list>, list>} */
+ /**
+ * @return array{
+ * list,
+ * schemes: list,
+ * host: string,
+ * defaults: array,
+ * requirements: array,
+ * options: array,
+ * condition: string,
+ * priority: int,
+ * service: string,
+ * class: string,
+ * method: string
+ * }>,
+ * list,
+ * schemes: list,
+ * host: string,
+ * defaults: array,
+ * requirements: array,
+ * options: array,
+ * condition: string,
+ * priority: int,
+ * service: string,
+ * class: string,
+ * method: string,
+ * rest: array{
+ * namespace: string,
+ * path: ?string,
+ * args: array>,
+ * permission_callback: mixed,
+ * public: bool,
+ * show_in_index: ?bool,
+ * override: bool
+ * }
+ * }>
+ * }
+ */
private function compiledRoutesForController(string $class, string $serviceId, ?string $environment): array
{
$frontendRoutes = [];
@@ -153,12 +198,12 @@ private function definitionFromRoute(RouteCollection $collection, string $name,
return [
'name' => $name,
'path' => $route->getPath(),
- 'methods' => array_values($route->getMethods()),
- 'schemes' => array_values($route->getSchemes()),
+ 'methods' => $this->stringList($route->getMethods()),
+ 'schemes' => $this->stringList($route->getSchemes()),
'host' => $route->getHost(),
- 'defaults' => $route->getDefaults(),
- 'requirements' => $route->getRequirements(),
- 'options' => $route->getOptions(),
+ 'defaults' => $this->stringKeyMap($route->getDefaults()),
+ 'requirements' => $this->stringMap($route->getRequirements()),
+ 'options' => $this->stringKeyMap($route->getOptions()),
'condition' => $route->getCondition(),
'priority' => $collection->getPriority($name) ?? 0,
'service' => $service,
@@ -196,11 +241,7 @@ private function restDefinition(string $name, Route $route): array
);
}
- $args = $options['args'] ?? [];
-
- if (!is_array($args)) {
- $args = [];
- }
+ $args = $this->restArgs($options['args'] ?? []);
$permissionCallback = $options['permission_callback'] ?? null;
$public = ($options['public'] ?? false) === true;
@@ -224,4 +265,81 @@ private function restDefinition(string $name, Route $route): array
'override' => (bool) ($options['override'] ?? false),
];
}
+
+ /**
+ * @param array $values
+ * @return array
+ */
+ private function stringKeyMap(array $values): array
+ {
+ $map = [];
+
+ foreach ($values as $key => $value) {
+ if (!is_string($key)) {
+ continue;
+ }
+
+ $map[$key] = $value;
+ }
+
+ return $map;
+ }
+
+ /**
+ * @param array $values
+ * @return array
+ */
+ private function stringMap(array $values): array
+ {
+ $map = [];
+
+ foreach ($values as $key => $value) {
+ if (!is_string($key)) {
+ continue;
+ }
+
+ if (!is_scalar($value) && !$value instanceof \Stringable) {
+ continue;
+ }
+
+ $map[$key] = (string) $value;
+ }
+
+ return $map;
+ }
+
+ /** @return list */
+ private function stringList(mixed $values): array
+ {
+ if (!is_array($values)) {
+ return [];
+ }
+
+ return array_values(
+ array_filter(
+ $values,
+ static fn (mixed $value): bool => is_string($value),
+ ),
+ );
+ }
+
+ /** @return array> */
+ private function restArgs(mixed $args): array
+ {
+ if (!is_array($args)) {
+ return [];
+ }
+
+ $normalized = [];
+
+ foreach ($args as $name => $definition) {
+ if (!is_string($name) || !is_array($definition)) {
+ continue;
+ }
+
+ $normalized[$name] = $this->stringKeyMap($definition);
+ }
+
+ return $normalized;
+ }
}
diff --git a/src/Routing/RouteLoader.php b/src/Routing/RouteLoader.php
index 30fb3bf..545f96e 100644
--- a/src/Routing/RouteLoader.php
+++ b/src/Routing/RouteLoader.php
@@ -15,13 +15,55 @@
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
+/**
+ * @phpstan-type RestRouteDefinition array{
+ * namespace: string,
+ * path: ?string,
+ * args: array>,
+ * permission_callback: mixed,
+ * public: bool,
+ * show_in_index: ?bool,
+ * override: bool
+ * }
+ * @phpstan-type CompiledRoute array{
+ * name: string,
+ * path: string,
+ * methods: list,
+ * schemes: list,
+ * host: string,
+ * defaults: array,
+ * requirements: array,
+ * options: array,
+ * condition: string,
+ * priority: int,
+ * service: string,
+ * class: string,
+ * method: string
+ * }
+ * @phpstan-type CompiledRestRoute array{
+ * name: string,
+ * path: string,
+ * methods: list,
+ * schemes: list,
+ * host: string,
+ * defaults: array,
+ * requirements: array,
+ * options: array,
+ * condition: string,
+ * priority: int,
+ * service: string,
+ * class: string,
+ * method: string,
+ * rest: RestRouteDefinition
+ * }
+ */
final class RouteLoader
{
public const string TAG = 'kernel.route_controller';
/**
- * @param list> $routes
- * @param list> $restRoutes
+ * @param list $routes
+ * @param list $restRoutes
*/
public function __construct(
private readonly ContainerInterface $controllers,
@@ -56,7 +98,7 @@ public function handle(Request $request): ?Response
);
try {
- $attributes = $matcher->matchRequest($request);
+ $attributes = $this->stringKeyMap($matcher->matchRequest($request));
} catch (ResourceNotFoundException) {
return null;
} catch (MethodNotAllowedException $exception) {
@@ -82,6 +124,9 @@ public function registerRestRoutes(): void
foreach ($this->restRoutes as $route) {
$rest = $route['rest'];
+ $namespace = $this->nonFalsyString($rest['namespace'], $route['name'], 'namespace');
+ $path = $this->nonFalsyString($this->restPath($route), $route['name'], 'path');
+
$args = [
'methods' => $route['methods'] === [] ? 'GET' : $route['methods'],
'callback' => $this->restCallback($route),
@@ -94,8 +139,8 @@ public function registerRestRoutes(): void
}
register_rest_route(
- $rest['namespace'],
- $this->restPath($route),
+ $namespace,
+ $path,
$args,
(bool) ($rest['override'] ?? false),
);
@@ -123,7 +168,7 @@ public function routeCollection(): RouteCollection
return $collection;
}
- /** @return list> */
+ /** @return list */
public function restRoutes(): array
{
return $this->restRoutes;
@@ -141,27 +186,38 @@ private function invokeHttpController(array $attributes, Request $request): mixe
$service = $this->controllers->get($serviceId);
+ if (!is_object($service)) {
+ $routeName = $attributes['_route'] ?? $serviceId;
+ $routeName = is_scalar($routeName) || $routeName instanceof \Stringable ? (string) $routeName : $serviceId;
+
+ throw new \RuntimeException(sprintf('Route "%s" controller service is not an object.', $routeName));
+ }
+
return $this->invokeController($service, $method, $request, $attributes);
}
- /** @param array $route */
+ /** @param CompiledRestRoute $route */
private function restCallback(array $route): \Closure
{
return function (mixed $request = null) use ($route): mixed {
$service = $this->controllers->get($route['service']);
+ if (!is_object($service)) {
+ throw new \RuntimeException(sprintf('REST route "%s" controller service is not an object.', $route['name']));
+ }
+
return $this->invokeController($service, $route['method'], $request, $this->restRequestParameters($request));
};
}
- /** @param array $route */
+ /** @param CompiledRestRoute $route */
private function restPermissionCallback(array $route): callable
{
return function (mixed $request = null) use ($route): mixed {
- $permissionCallback = $route['rest']['permission_callback'] ?? null;
+ $permissionCallback = $route['rest']['permission_callback'];
if ($permissionCallback === null) {
- if (($route['rest']['public'] ?? false) === true) {
+ if ($route['rest']['public']) {
return true;
}
@@ -177,7 +233,7 @@ private function restPermissionCallback(array $route): callable
if (is_string($permissionCallback)) {
$service = $this->controllers->get($route['service']);
- if (method_exists($service, $permissionCallback)) {
+ if (is_object($service) && method_exists($service, $permissionCallback)) {
return $this->invokeController(
$service,
$permissionCallback,
@@ -235,12 +291,15 @@ private function controllerArgument(\ReflectionParameter $parameter, mixed $requ
return $attributes[$parameter->getName()];
}
- if (is_object($request) && method_exists($request, 'has_param') && $request->has_param($parameter->getName())) {
- return $request->get_param($parameter->getName());
+ if (
+ is_object($request)
+ && $this->callObjectMethod($request, 'has_param', $parameter->getName()) === true
+ ) {
+ return $this->callObjectMethod($request, 'get_param', $parameter->getName());
}
- if (is_object($request) && method_exists($request, 'get_param')) {
- $value = $request->get_param($parameter->getName());
+ if (is_object($request)) {
+ $value = $this->callObjectMethod($request, 'get_param', $parameter->getName());
if ($value !== null) {
return $value;
@@ -285,6 +344,17 @@ private function parameterAcceptsObject(\ReflectionParameter $parameter, object
return false;
}
+ private function callObjectMethod(object $object, string $method, mixed ...$arguments): mixed
+ {
+ $callback = [$object, $method];
+
+ if (!is_callable($callback)) {
+ return null;
+ }
+
+ return $callback(...$arguments);
+ }
+
private function normalizeResponse(mixed $value): Response
{
if ($value instanceof Response) {
@@ -302,25 +372,25 @@ private function normalizeResponse(mixed $value): Response
return new JsonResponse($value);
}
- /** @param array $definition */
+ /** @param CompiledRoute|CompiledRestRoute $definition */
private function symfonyRoute(array $definition): Route
{
return new Route(
$definition['path'],
- $definition['defaults'] ?? [],
- $definition['requirements'] ?? [],
- $definition['options'] ?? [],
- $definition['host'] ?? '',
- $definition['schemes'] ?? [],
- $definition['methods'] ?? [],
- $definition['condition'] ?? '',
+ $definition['defaults'],
+ $definition['requirements'],
+ $definition['options'],
+ $definition['host'],
+ $definition['schemes'],
+ $definition['methods'],
+ $definition['condition'],
);
}
- /** @param array $route */
+ /** @param CompiledRestRoute $route */
private function restPath(array $route): string
{
- $explicitPath = $route['rest']['path'] ?? null;
+ $explicitPath = $route['rest']['path'];
if (is_string($explicitPath) && $explicitPath !== '') {
return str_starts_with($explicitPath, '/') ? $explicitPath : '/' . $explicitPath;
@@ -360,10 +430,41 @@ private function restRequestParameters(mixed $request): array
$parameters = $request->get_url_params();
if (is_array($parameters)) {
- return $parameters;
+ return $this->stringKeyMap($parameters);
}
}
return [];
}
+
+ /**
+ * @param array $values
+ * @return array
+ */
+ private function stringKeyMap(array $values): array
+ {
+ $map = [];
+
+ foreach ($values as $key => $value) {
+ if (!is_string($key)) {
+ continue;
+ }
+
+ $map[$key] = $value;
+ }
+
+ return $map;
+ }
+
+ /** @return non-falsy-string */
+ private function nonFalsyString(string $value, string $routeName, string $field): string
+ {
+ if ($value === '' || $value === '0') {
+ throw new \RuntimeException(
+ sprintf('REST route "%s" must define a non-falsy %s.', $routeName, $field),
+ );
+ }
+
+ return $value;
+ }
}
diff --git a/src/WpContext.php b/src/WpContext.php
index 55741e8..35936ac 100644
--- a/src/WpContext.php
+++ b/src/WpContext.php
@@ -237,8 +237,9 @@ private static function isLoginRequest(): bool
}
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
+ $scriptName = is_scalar($scriptName) || $scriptName instanceof \Stringable ? (string) $scriptName : '';
- return stripos(wp_login_url(), (string) $scriptName) !== false;
+ return $scriptName !== '' && stripos(wp_login_url(), $scriptName) !== false;
}
private static function isWpActivateRequest(): bool
@@ -252,7 +253,8 @@ private static function isWpActivateRequest(): bool
private static function isPageNow(string $page, string $url): bool
{
- $pageNow = (string) ($GLOBALS['pagenow'] ?? '');
+ $pageNow = $GLOBALS['pagenow'] ?? '';
+ $pageNow = is_scalar($pageNow) || $pageNow instanceof \Stringable ? (string) $pageNow : '';
if ($pageNow !== '' && basename($pageNow) === $page) {
return true;
diff --git a/tests/Admin/PackageManagerPageTest.php b/tests/Admin/PackageManagerPageTest.php
index 5f2ba2d..a1afb85 100644
--- a/tests/Admin/PackageManagerPageTest.php
+++ b/tests/Admin/PackageManagerPageTest.php
@@ -137,6 +137,7 @@ function wp_clean_plugins_cache(bool $clearUpdateCache = true): void
namespace SymPress\Kernel\Tests\Admin {
use PHPUnit\Framework\TestCase;
+ use SymPress\Kernel\Admin\PackageManagerActions;
use SymPress\Kernel\Admin\PackageManagerPage;
use SymPress\Kernel\Package\PackageDiscovery;
use SymPress\Kernel\Package\PackageMetadata;
@@ -214,7 +215,7 @@ public function testUnmanagedSymlinkDeleteIsRejected(): void
rmdir($link);
symlink($target, $link);
- $result = $this->invokePageMethod('deleteSymlinkPackage', $this->pluginPackage($link));
+ $result = $this->invokeActionMethod('deleteSymlinkPackage', $this->pluginPackage($link));
self::assertInstanceOf(\WP_Error::class, $result);
self::assertSame('kernel_package_unmanaged_symlink', $result->get_error_code());
@@ -235,7 +236,7 @@ public function testManagedSymlinkDeleteRemovesExpectedWordPressPackageLink(): v
$this->paths[] = $contentDir;
symlink($target, $link);
- $result = $this->invokePageMethod('deleteSymlinkPackage', $this->pluginPackage($link));
+ $result = $this->invokeActionMethod('deleteSymlinkPackage', $this->pluginPackage($link));
self::assertNull($result);
self::assertFalse(is_link($link));
@@ -273,6 +274,13 @@ private function invokePageMethod(string $method, mixed ...$arguments): mixed
return $reflection->invoke($this->page(true), ...$arguments);
}
+ private function invokeActionMethod(string $method, mixed ...$arguments): mixed
+ {
+ $reflection = new \ReflectionMethod(PackageManagerActions::class, $method);
+
+ return $reflection->invoke(new PackageManagerActions(), ...$arguments);
+ }
+
private function tmpPath(string $prefix): string
{
$path = sprintf('%s/%s-%s', sys_get_temp_dir(), $prefix, uniqid('', true));
diff --git a/tests/Kernel/BundleCompatibilityTest.php b/tests/Kernel/BundleCompatibilityTest.php
index b5634d7..0f41401 100644
--- a/tests/Kernel/BundleCompatibilityTest.php
+++ b/tests/Kernel/BundleCompatibilityTest.php
@@ -169,6 +169,37 @@ public function testKernelLifecycleActionsAreDispatched(): void
);
}
+ public function testDebugMethodsBridgeToOptionalProfilerService(): void
+ {
+ $profiler = new class {
+ public bool $enabled = false;
+ public bool $disabled = false;
+
+ public function enable(): void
+ {
+ $this->enabled = true;
+ $this->disabled = false;
+ }
+
+ public function disable(): void
+ {
+ $this->disabled = true;
+ $this->enabled = false;
+ }
+ };
+
+ $app = App::new(new LifecycleKernel($this->tmpPath('debug-project'), $profiler));
+ $app->enableDebug()->boot();
+
+ self::assertTrue($profiler->enabled);
+ self::assertFalse($profiler->disabled);
+
+ $app->disableDebug();
+
+ self::assertFalse($profiler->enabled);
+ self::assertTrue($profiler->disabled);
+ }
+
public function testKernelBootRethrowsErrorsAfterDispatchingErrorAction(): void
{
$kernel = new class (
diff --git a/tests/Kernel/LifecycleKernel.php b/tests/Kernel/LifecycleKernel.php
index a0c8501..60f1310 100644
--- a/tests/Kernel/LifecycleKernel.php
+++ b/tests/Kernel/LifecycleKernel.php
@@ -22,6 +22,7 @@ final class LifecycleKernel implements KernelInterface
public function __construct(
private readonly string $projectDir,
+ private readonly ?object $profiler = null,
) {
}
@@ -136,6 +137,9 @@ public function configureContainer(
Container $container,
BundleRegistry $bundles,
): array {
+ if ($this->profiler !== null) {
+ $builder->set('profiler', $this->profiler);
+ }
return [];
}
diff --git a/tests/Support/TestEnvironment.php b/tests/Support/TestEnvironment.php
new file mode 100644
index 0000000..9e97fce
--- /dev/null
+++ b/tests/Support/TestEnvironment.php
@@ -0,0 +1,28 @@
+ $args
+ */
+ public static function add_command(string $name, callable|object|string $callable, array $args = []): void
+ {
+ }
+
+ public static function error(string $message): never
+ {
+ exit(1);
+ }
+
+ public static function halt(int $status): never
+ {
+ exit($status);
+ }
+ }
+}