diff --git a/bin/castor b/bin/castor index 087ff87d..958d839c 100755 --- a/bin/castor +++ b/bin/castor @@ -1,7 +1,6 @@ #!/usr/bin/env php =8.2", + "php": ">=8.4", "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^3.6", - "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0" + "symfony/var-exporter": "^8.1" }, "conflict": { - "ext-psr": "<1.1|>=2", - "symfony/config": "<6.4", - "symfony/finder": "<6.4", - "symfony/yaml": "<6.4" + "ext-psr": "<1.1|>=2" }, "provide": { "psr/container-implementation": "1.1|2.0", "symfony/service-implementation": "1.1|2.0|3.0" }, "require-dev": { - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/yaml": "^6.4|^7.0|^8.0" + "symfony/config": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -2019,7 +2010,6 @@ "/Tests/" ] }, - "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -2035,28 +2025,9 @@ ], "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.4.5" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-01-27T16:16:02+00:00" + "transport-options": { + "relative": true + } }, { "name": "symfony/deprecation-contracts", @@ -4524,27 +4495,27 @@ }, { "name": "symfony/var-exporter", - "version": "v7.4.0", + "version": "8.1.x-dev", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" + "reference": "116d995a21a7c71c81a8feab5546cd2765d076bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/116d995a21a7c71c81a8feab5546cd2765d076bb", + "reference": "116d995a21a7c71c81a8feab5546cd2765d076bb", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3" + "php": ">=8.4" }, "require-dev": { - "symfony/property-access": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" + "symfony/property-access": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, + "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -4568,11 +4539,12 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "description": "Provides tools to export, instantiate, hydrate, clone and lazy-load PHP objects", "homepage": "https://symfony.com", "keywords": [ "clone", "construct", + "deep-clone", "export", "hydrate", "instantiate", @@ -4581,7 +4553,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" + "source": "https://github.com/symfony/var-exporter/tree/8.1" }, "funding": [ { @@ -4601,7 +4573,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:15:23+00:00" + "time": "2026-03-30T15:21:58+00:00" }, { "name": "symfony/yaml", @@ -6407,7 +6379,10 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": { + "symfony/dependency-injection": 20, + "symfony/var-exporter": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -6415,7 +6390,7 @@ }, "platform-dev": {}, "platform-overrides": { - "php": "8.2.27" + "php": "8.5" }, "plugin-api-version": "2.9.0" } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5d40dc54..76234c46 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -24,12 +24,6 @@ parameters: count: 1 path: examples/basic/assertion/ensure.php - - - message: '#^Class RepackedApplication not found\.$#' - identifier: class.notFound - count: 1 - path: src/Console/ApplicationFactory.php - - message: '#^Default value of the parameter \#1 \$data \(array\{\}\) of method Castor\\Context\:\:__construct\(\) is incompatible with type array\{name\: string, production\: bool, foo\?\: string\}\.$#' identifier: parameter.defaultValue diff --git a/src/Console/Application.php b/src/Console/Application.php index 4e1bbfca..1782a7e5 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -2,7 +2,6 @@ namespace Castor\Console; -use Castor\Container; use Castor\Exception\ProblemException; use Castor\Kernel; use Castor\Runner\ProcessRunner; @@ -16,7 +15,6 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\DependencyInjection\Attribute\Autowire; -use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Process\Exception\ProcessFailedException; /** @internal */ @@ -30,7 +28,6 @@ class Application extends SymfonyApplication private Command $command; public function __construct( - private readonly ContainerBuilder $containerBuilder, private readonly Kernel $kernel, #[Autowire(lazy: true)] private readonly SymfonyStyle $io, @@ -58,13 +55,7 @@ public function doRun(InputInterface $input, OutputInterface $output): int return parent::doRun($input, $output); } - $this->containerBuilder->set(InputInterface::class, $input); - $this->containerBuilder->set(OutputInterface::class, $output); - - // @phpstan-ignore argument.type - Container::set($this->containerBuilder->get(Container::class)); - - $this->kernel->boot($input, $output); + $this->kernel->init($input, $output); try { return parent::doRun($input, $output); diff --git a/src/Console/ApplicationFactory.php b/src/Console/ApplicationFactory.php index dca3d04a..c5e0bf4f 100644 --- a/src/Console/ApplicationFactory.php +++ b/src/Console/ApplicationFactory.php @@ -2,52 +2,16 @@ namespace Castor\Console; -use Castor\Console\Command\CompileCommand; -use Castor\Console\Command\ComposerCommand; -use Castor\Console\Command\DebugCommand; -use Castor\Console\Command\ExecuteCommand; -use Castor\Console\Command\InitCommand; -use Castor\Console\Command\RepackCommand; -use Castor\Container; use Castor\Helper\PathHelper; -use Castor\Helper\PlatformHelper; -use Castor\Monolog\Processor\ProcessProcessor; -use Joli\JoliNotif\DefaultNotifier; -use Monolog\Logger; -use Psr\Cache\CacheItemPoolInterface; -use Psr\Container\ContainerInterface; -use Psr\Log\LoggerInterface; -use Symfony\Component\Cache\Adapter\FilesystemAdapter; -use Symfony\Component\Config\FileLocator; +use Castor\Kernel; use Symfony\Component\Console\Application as SymfonyApplication; -use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Input\ArgvInput; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\Console\Terminal; use Symfony\Component\DependencyInjection\Attribute\Exclude; -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; -use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\ErrorHandler\ErrorHandler; -use Symfony\Component\EventDispatcher\Attribute\AsEventListener; -use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; -use Symfony\Component\EventDispatcher\EventDispatcher; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Path; -use Symfony\Component\HttpClient\HttpClient; -use Symfony\Component\String\Slugger\AsciiSlugger; use Symfony\Component\VarDumper\Caster\StubCaster; use Symfony\Component\VarDumper\Cloner\AbstractCloner; -use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\EventDispatcher\Event; -use Symfony\Contracts\HttpClient\HttpClientInterface; - -use function Symfony\Component\DependencyInjection\Loader\Configurator\service; /** @internal */ #[Exclude] @@ -84,26 +48,10 @@ public static function create(): SymfonyApplication } } - $container = self::buildContainer($repacked, $hasCastorFile); - $container->getParameterBag()->add([ - 'root_dir' => $rootDir, - '.default_cache_dir' => PlatformHelper::getDefaultCacheDirectory(), - 'event_dispatcher.event_aliases' => ConsoleEvents::ALIASES, - 'repacked' => $repacked, - 'cache_dir' => '%env(default:.default_cache_dir:CASTOR_CACHE_DIR)%', - 'composer_no_remote' => '%env(bool:default::CASTOR_NO_REMOTE)%', - 'context' => '', - 'env(CASTOR_GENERATE_STUBS)' => 'true', - 'generate_stubs' => '%env(bool:CASTOR_GENERATE_STUBS)%', - 'test' => '%env(bool:default::CASTOR_TEST)%', - 'use_output_section' => '%env(bool:default::CASTOR_USE_SECTION)%', - 'has_castor_file' => $hasCastorFile, - 'castor_file_path' => $castorFilePath, - ]); - - $container->compile(true); + $kernel = new Kernel('dev', true, $rootDir, $hasCastorFile, $castorFilePath, $repacked); + $kernel->boot(); - $container->set(ContainerInterface::class, $container); + $container = $kernel->getContainer(); $container->set(ErrorHandler::class, $errorHandler); // @phpstan-ignore-next-line @@ -119,135 +67,4 @@ private static function configureDebug(): ErrorHandler return $errorHandler; } - - private static function buildContainer(bool $repacked, bool $hasCastorFile): ContainerBuilder - { - $container = new ContainerBuilder(); - - $container->registerForAutoconfiguration(EventSubscriberInterface::class) - ->addTag('kernel.event_subscriber') - ; - $container->addCompilerPass(new RegisterListenersPass()); - // from https://github.com/symfony/symfony/blob/6.4/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php#L676-L685 - $container->registerAttributeForAutoconfiguration(AsEventListener::class, static function (ChildDefinition $definition, AsEventListener $attribute, \Reflector $reflector): void { - $tagAttributes = get_object_vars($attribute); - 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); - }); - - $phpLoader = new PhpFileLoader($container, new FileLocator()); - $instanceof = []; - $configurator = new ContainerConfigurator($container, $phpLoader, $instanceof, __DIR__, __FILE__); - self::configureContainer($configurator, $repacked, $hasCastorFile); - - return $container; - } - - private static function configureContainer(ContainerConfigurator $c, bool $repacked, bool $hasCastorFile): void - { - $services = $c->services(); - - $services - ->defaults() - ->autowire() - ->autoconfigure() - ->bind('string $rootDir', '%root_dir%') - ->bind('string $cacheDir', '%cache_dir%') - ->bind('bool $hasCastorFile', '%has_castor_file%') - ->bind('string $castorFilePath', '%castor_file_path%') - - ->load('Castor\\', __DIR__ . '/../*') - ->exclude([ - __DIR__ . '/../functions.php', - __DIR__ . '/../functions-internal.php', - __DIR__ . '/../Descriptor/*', - __DIR__ . '/../Event/*', - __DIR__ . '/../**/Exception/*', - ]) - - ->set(CacheInterface::class, FilesystemAdapter::class) - ->args([ - '$directory' => '%cache_dir%', - ]) - ->alias(CacheItemPoolInterface::class . '&' . CacheInterface::class, CacheInterface::class) - - ->set(HttpClientInterface::class) - ->factory([HttpClient::class, 'create']) - ->args([ - '$defaultOptions' => [ - 'headers' => [ - 'User-Agent' => 'Castor/' . Application::VERSION, - ], - ], - ]) - - ->set(Logger::class) - ->args([ - '$name' => 'castor', - '$processors' => [ - service(ProcessProcessor::class), - ], - ]) - ->alias(LoggerInterface::class, Logger::class) - - ->set(EventDispatcher::class) - ->alias(EventDispatcherInterface::class, EventDispatcher::class) - ->alias('event_dispatcher', EventDispatcherInterface::class) - - ->set(Filesystem::class) - - ->set(AsciiSlugger::class) - - ->set(DefaultNotifier::class) - - ->set(Container::class) - ->public() - - ->set(ContainerInterface::class) - ->synthetic() - - ->set(OutputInterface::class) - ->synthetic() - - ->set(InputInterface::class) - ->synthetic() - - ->set(SymfonyStyle::class) - - ->set(Terminal::class) - - ->set(ErrorHandler::class) - ->synthetic() - ; - - $app = $services->set(Application::class, $repacked ? \RepackedApplication::class : null) - ->public() - ->args([ - '$containerBuilder' => service(ContainerInterface::class), - ]) - ->call('addCommand', [service(DebugCommand::class)]) - ->call('addCommand', [service(ExecuteCommand::class)]) - ->call('setDispatcher', [service(EventDispatcherInterface::class)]) - ->call('setCatchErrors', [true]) - ; - if (!$repacked && $hasCastorFile) { - $app - ->call('addCommand', [service(ComposerCommand::class)]) - ->call('addCommand', [service(RepackCommand::class)]) - ->call('addCommand', [service(CompileCommand::class)]) - ; - } - - if (!$hasCastorFile) { - $app - ->call('addCommand', [service(InitCommand::class)]) - ->call('setDefaultCommand', ['init']) - ; - } - } } diff --git a/src/Container.php b/src/Container.php index 8c32e953..9de700f5 100644 --- a/src/Container.php +++ b/src/Container.php @@ -5,6 +5,8 @@ use Castor\Console\Application; use Castor\Console\Output\SectionOutput; use Castor\Fingerprint\FingerprintHelper; +use Castor\Function\FunctionLoader; +use Castor\Function\FunctionResolver; use Castor\Helper\Notifier; use Castor\Helper\Slugger; use Castor\Helper\SymmetricCrypto; @@ -12,6 +14,7 @@ use Castor\Helper\ZipArchiver; use Castor\Http\HttpDownloader; use Castor\Import\Importer; +use Castor\Import\Remote\Composer; use Castor\Runner\ParallelRunner; use Castor\Runner\PhpRunner; use Castor\Runner\ProcessRunner; @@ -37,11 +40,13 @@ final class Container public function __construct( public readonly Application $application, public readonly CacheItemPoolInterface&CacheInterface $cache, + public readonly Composer $composer, public readonly ContextRegistry $contextRegistry, - public readonly ContextRegistry $outputInterface, public readonly EventDispatcherInterface $eventDispatcher, public readonly Filesystem $fs, public readonly FingerprintHelper $fingerprintHelper, + public readonly FunctionLoader $functionLoader, + public readonly FunctionResolver $functionResolver, public readonly HttpClientInterface $httpClient, public readonly HttpDownloader $httpDownloader, public readonly Importer $importer, diff --git a/src/Kernel.php b/src/Kernel.php index 584c0477..5d87a095 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -3,26 +3,54 @@ namespace Castor; use Castor\Console\Application; +use Castor\Console\Command\CompileCommand; +use Castor\Console\Command\ComposerCommand; +use Castor\Console\Command\DebugCommand; +use Castor\Console\Command\ExecuteCommand; +use Castor\Console\Command\InitCommand; +use Castor\Console\Command\RepackCommand; use Castor\Console\Output\VerbosityLevel; use Castor\Event\AfterBootEvent; use Castor\Event\BeforeBootEvent; use Castor\Event\FunctionsResolvedEvent; use Castor\Exception\CouldNotFindEntrypointException; -use Castor\Function\FunctionLoader; -use Castor\Function\FunctionResolver; use Castor\Helper\PlatformHelper; use Castor\Import\Importer; use Castor\Import\Mount; -use Castor\Import\Remote\Composer; +use Castor\Monolog\Processor\ProcessProcessor; +use Joli\JoliNotif\DefaultNotifier; +use Monolog\Logger; +use Psr\Cache\CacheItemPoolInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Console\Terminal; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Kernel\AbstractKernel; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\ErrorHandler\ErrorHandler; +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; +use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpClient\HttpClient; +use Symfony\Component\String\Slugger\AsciiSlugger; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +use function Symfony\Component\DependencyInjection\Loader\Configurator\service; /** @internal */ -final class Kernel +final class Kernel extends AbstractKernel { /** * @var list @@ -30,28 +58,234 @@ final class Kernel private array $mounts = []; public function __construct( - #[Autowire(lazy: true)] - private readonly Application $application, - private readonly EventDispatcherInterface $eventDispatcher, + string $environment, + bool $debug, private readonly string $rootDir, - #[Autowire(lazy: true)] - private readonly Importer $importer, - #[Autowire(lazy: true)] - private readonly Composer $composer, - private readonly FunctionResolver $functionResolver, - private readonly FunctionLoader $functionLoader, - private readonly ContextRegistry $contextRegistry, private readonly bool $hasCastorFile, private readonly ?string $castorFilePath, + private readonly bool $repacked, ) { + parent::__construct($environment, $debug); + } + + public function getProjectDir(): string + { + return $this->rootDir; + } + + public function getCacheDir(): string + { + return (string) (getenv('CASTOR_CACHE_DIR') ?: PlatformHelper::getDefaultCacheDirectory()); + } + + public function getLogDir(): ?string + { + return null; } - public function boot(InputInterface $input, OutputInterface $output): void + public function boot(): void { + // AbstractKernel::boot() unconditionally sets SHELL_VERBOSITY=3 in debug mode, + // which would make all console output verbose. Castor manages verbosity via + // its own -v flag, so we restore the original value after boot. + $shellVerbosity = getenv('SHELL_VERBOSITY'); + + parent::boot(); + + if (false === $shellVerbosity) { + putenv('SHELL_VERBOSITY'); + unset($_ENV['SHELL_VERBOSITY'], $_SERVER['SHELL_VERBOSITY']); + } + + if (!$this->container instanceof ContainerInterface) { + throw new \LogicException('Container should be initialized after boot.'); + } + + $this->container->set(self::class, $this); + $this->container->set(ContainerInterface::class, $this->container); + } + + public function init(InputInterface $input, OutputInterface $output): void + { + $this->container->set(InputInterface::class, $input); + $this->container->set(OutputInterface::class, $output); + + $castorContainer = $this->container->get(Container::class); + + if (!$castorContainer instanceof Container) { + throw new \LogicException('Castor container should be initialized after boot.'); + } + + Container::set($castorContainer); + + $this->mount($input, $output); + } + + public function addMount(Mount $mount): void + { + $this->mounts[] = $mount; + } + + public function configureContainer(ContainerConfigurator $c): void + { + $services = $c->services(); + $repacked = $this->repacked; + $hasCastorFile = $this->hasCastorFile; + + $services + ->defaults() + ->autowire() + ->autoconfigure() + ->bind('string $rootDir', '%root_dir%') + ->bind('string $cacheDir', '%cache_dir%') + ->bind('bool $hasCastorFile', '%has_castor_file%') + ->bind('string $castorFilePath', '%castor_file_path%') + + ->load('Castor\\', __DIR__ . '/*') + ->exclude([ + __DIR__ . '/functions.php', + __DIR__ . '/functions-internal.php', + __DIR__ . '/Descriptor/*', + __DIR__ . '/Event/*', + __DIR__ . '/**/Exception/*', + __DIR__ . '/Kernel.php', + ]) + + ->set(CacheInterface::class, FilesystemAdapter::class) + ->args([ + '$directory' => '%cache_dir%', + ]) + ->alias(CacheItemPoolInterface::class . '&' . CacheInterface::class, CacheInterface::class) + + ->set(HttpClientInterface::class) + ->factory([HttpClient::class, 'create']) + ->args([ + '$defaultOptions' => [ + 'headers' => [ + 'User-Agent' => 'Castor/' . Application::VERSION, + ], + ], + ]) + + ->set(Logger::class) + ->args([ + '$name' => 'castor', + '$processors' => [ + service(ProcessProcessor::class), + ], + ]) + ->alias(LoggerInterface::class, Logger::class) + + ->set(EventDispatcher::class) + ->alias(EventDispatcherInterface::class, EventDispatcher::class) + ->alias('event_dispatcher', EventDispatcherInterface::class) + + ->set(Filesystem::class) + + ->set(AsciiSlugger::class) + + ->set(DefaultNotifier::class) + + ->set(SymfonyStyle::class) + + ->set(Terminal::class) + + ->set(Container::class) + ->public() + + ->set(self::class) + ->synthetic() + ->public() + + ->set(ContainerInterface::class) + ->synthetic() + + ->set(OutputInterface::class) + ->synthetic() + + ->set(InputInterface::class) + ->synthetic() + + ->set(ErrorHandler::class) + ->synthetic() + ; + + $app = $services->set(Application::class, $repacked ? \RepackedApplication::class : null) + ->public() + ->call('addCommand', [service(DebugCommand::class)]) + ->call('addCommand', [service(ExecuteCommand::class)]) + ->call('setDispatcher', [service(EventDispatcherInterface::class)]) + ->call('setCatchErrors', [true]) + ; + if (!$repacked && $hasCastorFile) { + $app + ->call('addCommand', [service(ComposerCommand::class)]) + ->call('addCommand', [service(RepackCommand::class)]) + ->call('addCommand', [service(CompileCommand::class)]) + ; + } + + if (!$hasCastorFile) { + $app + ->call('addCommand', [service(InitCommand::class)]) + ->call('setDefaultCommand', ['init']) + ; + } + } + + protected function build(ContainerBuilder $container): void + { + $container->registerForAutoconfiguration(EventSubscriberInterface::class) + ->addTag('kernel.event_subscriber') + ; + $container->addCompilerPass(new RegisterListenersPass()); + $container->registerAttributeForAutoconfiguration(AsEventListener::class, static function (ChildDefinition $definition, AsEventListener $attribute, \Reflector $reflector): void { + $tagAttributes = get_object_vars($attribute); + 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); + }); + } + + protected function getKernelParameters(): array + { + return array_merge(parent::getKernelParameters(), [ + 'container.runtime_mode' => 'cli=1', + 'root_dir' => $this->rootDir, + '.default_cache_dir' => PlatformHelper::getDefaultCacheDirectory(), + 'event_dispatcher.event_aliases' => ConsoleEvents::ALIASES, + 'repacked' => $this->repacked, + 'cache_dir' => '%env(default:.default_cache_dir:CASTOR_CACHE_DIR)%', + 'composer_no_remote' => '%env(bool:default::CASTOR_NO_REMOTE)%', + 'context' => '', + 'env(CASTOR_GENERATE_STUBS)' => 'true', + 'generate_stubs' => '%env(bool:CASTOR_GENERATE_STUBS)%', + 'test' => '%env(bool:default::CASTOR_TEST)%', + 'use_output_section' => '%env(bool:default::CASTOR_USE_SECTION)%', + 'has_castor_file' => $this->hasCastorFile, + 'castor_file_path' => $this->castorFilePath, + ]); + } + + protected function initializeContainer(): void + { + $container = $this->buildContainer(); + $container->compile(true); + $this->container = $container; + } + + private function mount(InputInterface $input, OutputInterface $output): void + { + $c = Container::get(); + try { - $this->eventDispatcher->dispatch(new BeforeBootEvent($this->application)); + $c->eventDispatcher->dispatch(new BeforeBootEvent($c->application)); - $allowRemotePackage = $this->composer->isRemoteAllowed(); + $allowRemotePackage = $c->composer->isRemoteAllowed(); $this->addMount(new Mount($this->rootDir, allowRemotePackage: $allowRemotePackage, allowEmptyEntrypoint: !$this->hasCastorFile, file: $this->castorFilePath)); @@ -62,19 +296,14 @@ public function boot(InputInterface $input, OutputInterface $output): void $this->load($mount, $currentFunctions, $currentClasses, $input, $output); } - $this->eventDispatcher->dispatch(new AfterBootEvent($this->application)); + $c->eventDispatcher->dispatch(new AfterBootEvent($c->application)); } catch (\Throwable $e) { - $this->eventDispatcher->dispatch(new ConsoleErrorEvent($input, $output, $e), 'console.error'); + $c->eventDispatcher->dispatch(new ConsoleErrorEvent($input, $output, $e), 'console.error'); throw $e; } } - public function addMount(Mount $mount): void - { - $this->mounts[] = $mount; - } - /** * @param list $currentFunctions * @param list $currentClasses @@ -86,23 +315,25 @@ private function load( InputInterface $input, OutputInterface $output, ): void { + $c = Container::get(); + if ($mount->allowRemotePackage) { - $this->composer->install($mount->path); + $c->composer->install($mount->path); } if ($mount->path === $this->rootDir) { - $this->composer->requireAutoload(); + $c->composer->requireAutoload(); } try { - $this->requireEntrypoint($mount); + $this->requireEntrypoint($mount, $c->importer); } catch (CouldNotFindEntrypointException $e) { if (!$mount->allowEmptyEntrypoint) { throw $e; } } - $descriptorsCollection = $this->functionResolver->resolveFunctions($currentFunctions, $currentClasses); + $descriptorsCollection = $c->functionResolver->resolveFunctions($currentFunctions, $currentClasses); // Apply mounts foreach ($descriptorsCollection->taskDescriptors as $taskDescriptor) { @@ -118,34 +349,34 @@ private function load( } } - $this->functionLoader->loadListeners($descriptorsCollection->listenerDescriptors); + $c->functionLoader->loadListeners($descriptorsCollection->listenerDescriptors); // Must load contexts before tasks, because tasks can be disabled // depending on the context. And it must be before executing // listeners too, to get the context there. - $this->functionLoader->loadContexts($descriptorsCollection->contextDescriptors, $descriptorsCollection->contextGeneratorDescriptors); - $this->configureContext($input, $output); + $c->functionLoader->loadContexts($descriptorsCollection->contextDescriptors, $descriptorsCollection->contextGeneratorDescriptors); + $this->configureContext($input, $output, $c->contextRegistry); $event = new FunctionsResolvedEvent( $descriptorsCollection->taskDescriptors, $descriptorsCollection->symfonyTaskDescriptors ); - $this->eventDispatcher->dispatch($event); + $c->eventDispatcher->dispatch($event); - $this->functionLoader->loadTasks( + $c->functionLoader->loadTasks( $event->taskDescriptors, $event->symfonyTaskDescriptors ); } - private function requireEntrypoint(Mount $mount): void + private function requireEntrypoint(Mount $mount, Importer $importer): void { $path = $mount->path; // It's an import, via a remote package, with a file specified if ($mount->file) { if (file_exists($file = $mount->path . '/' . $mount->file)) { - $this->importer->importFile($file); + $importer->importFile($file); return; } @@ -154,22 +385,22 @@ private function requireEntrypoint(Mount $mount): void } if (file_exists($file = $path . '/castor.php')) { - $this->importer->importFile($file); + $importer->importFile($file); } elseif (file_exists($file = $path . '/.castor/castor.php')) { - $this->importer->importFile($file); + $importer->importFile($file); } else { throw new CouldNotFindEntrypointException(); } } - private function configureContext(InputInterface $input, OutputInterface $output): void + private function configureContext(InputInterface $input, OutputInterface $output, ContextRegistry $contextRegistry): void { - $this->contextRegistry->setDefaultIfEmpty(); + $contextRegistry->setDefaultIfEmpty(); - $contextNames = $this->contextRegistry->getNames(); + $contextNames = $contextRegistry->getNames(); if (!$contextNames || 'list' === $input->getFirstArgument()) { - $this->contextRegistry->setCurrentContext(new Context( + $contextRegistry->setCurrentContext(new Context( verbosityLevel: VerbosityLevel::fromSymfonyOutput($output) )); @@ -186,9 +417,10 @@ private function configureContext(InputInterface $input, OutputInterface $output $currentContextName = $input->getParameterOption($contextOptions) ?: PlatformHelper::getEnv('CASTOR_CONTEXT') - ?: $this->contextRegistry->getDefaultName(); + ?: $contextRegistry->getDefaultName(); - $applicationDefinition = $this->application->getDefinition(); + $application = Container::get()->application; + $applicationDefinition = $application->getDefinition(); $applicationDefinition->addOption(new InputOption( 'context', $isAutocomplete ? null : 'c', @@ -198,15 +430,12 @@ private function configureContext(InputInterface $input, OutputInterface $output $contextNames, )); - $context = $this - ->contextRegistry - ->get($currentContextName) - ; + $context = $contextRegistry->get($currentContextName); if ($context->verbosityLevel->isNotConfigured()) { $context = $context->withVerbosityLevel(VerbosityLevel::fromSymfonyOutput($output)); } - $this->contextRegistry->setCurrentContext($context->withName($currentContextName)); + $contextRegistry->setCurrentContext($context->withName($currentContextName)); } }