diff --git a/core/composer.json b/core/composer.json index 4fee977c23..cf2d130c57 100644 --- a/core/composer.json +++ b/core/composer.json @@ -22,6 +22,7 @@ "symfony/console": "~3.4.0", "symfony/dependency-injection": "~3.4.26", "symfony/event-dispatcher": "~3.4.0", + "symfony/finder": "~3.4.0", "symfony/http-foundation": "~3.4.27", "symfony/http-kernel": "~3.4.14", "symfony/routing": "~3.4.0", @@ -219,7 +220,8 @@ "psr-4": { "Drupal\\Core\\": "lib/Drupal/Core", "Drupal\\Component\\": "lib/Drupal/Component", - "Drupal\\Driver\\": "../drivers/lib/Drupal/Driver" + "Drupal\\Driver\\": "../drivers/lib/Drupal/Driver", + "Drupal\\DrupalConsole\\": "lib/Drupal/DrupalConsole" }, "classmap": [ "lib/Drupal.php", diff --git a/core/core.services.yml b/core/core.services.yml index 04f75f64bc..b9171d576e 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1724,3 +1724,22 @@ services: arguments: ['@keyvalue.expirable', '@lock', '@request_stack', '%tempstore.expire%'] tags: - { name: backend_overridable } + console.app: + class: Drupal\Core\Console\Application + console.bootstrapper: + class: Drupal\Core\Console\Bootstrap + arguments: ['@class_loader'] + console.install.command: + class: Drupal\Core\Command\InstallCommand + arguments: ['@class_loader'] + tags: + - { name: console.command } + console.quickstart.command: + class: Drupal\Core\Command\QuickStartCommand + tags: + - { name: console.command } + console.server.command: + class: Drupal\Core\Command\ServerCommand + arguments: ['@class_loader'] + tags: + - { name: console.command } diff --git a/core/lib/Drupal/Core/Console/Application.php b/core/lib/Drupal/Core/Console/Application.php new file mode 100644 index 0000000000..24dcc53ffc --- /dev/null +++ b/core/lib/Drupal/Core/Console/Application.php @@ -0,0 +1,19 @@ +classLoader = $class_loader; + } + + /** + * {@inheritdoc} + */ + public function bootstrap(Command $command, InputInterface $input, OutputInterface $output, Request $request = NULL) { + $env = $input->getOption('environment'); + if (!$request) { + // Create a meaningful request object. We shouldn't really need this but + // Drupal complains if it's not present. + $request = Request::createFromGlobals(); + } + try { + $kernel = DrupalKernel::createFromRequest($request, $this->classLoader, $env); + $kernel->boot(); + + $container = $kernel->getContainer(); + $container->set('request', $request); + $container->get('request_stack')->push($request); + + // @todo: Change this when we're not using includes any more. + require_once dirname(dirname(dirname(dirname(__DIR__)))) . '/includes/common.inc'; + return $kernel; + } + catch (\Exception $e) { + /** @var \Symfony\Component\Console\Helper\FormatterHelper $formatter */ + $formatter = $command->getHelperSet()->get('formatter'); + $error_messages = [ + 'Insufficient Drupal to proceed.', + 'This command must be run within an installed Drupal.', + $e->getMessage(), + ]; + $formatted_block = $formatter->formatBlock($error_messages, 'error', TRUE); + $output->writeln($formatted_block); + } + return FALSE; + } + +} diff --git a/core/lib/Drupal/Core/Console/BootstrapInterface.php b/core/lib/Drupal/Core/Console/BootstrapInterface.php new file mode 100644 index 0000000000..a9de5f8dd4 --- /dev/null +++ b/core/lib/Drupal/Core/Console/BootstrapInterface.php @@ -0,0 +1,36 @@ +bootstrap = $bootstrap; + } + + /** + * {@inheritdoc} + */ + protected function configure() { + parent::configure(); + $this->addOption( + 'environment', 'e', InputOption::VALUE_REQUIRED, 'Kernel environment.', 'prod' + ); + } + +} diff --git a/core/lib/Drupal/Core/Console/Command/RunCron.php b/core/lib/Drupal/Core/Console/Command/RunCron.php new file mode 100644 index 0000000000..2261afee10 --- /dev/null +++ b/core/lib/Drupal/Core/Console/Command/RunCron.php @@ -0,0 +1,38 @@ +setName('drupal:cron') + ->setAliases(['cron']) + ->setDescription('Performs a cron run.'); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) { + $kernel = $this->bootstrap->bootstrap($this, $input, $output); + if ($kernel) { + $output->writeln('Running cron...'); + /** @var \Drupal\Core\CronInterface $cron */ + $cron = $kernel->getContainer()->get('cron'); + $cron->run(); + $output->writeln('Done.'); + } + } + +} diff --git a/core/lib/Drupal/Core/Console/ConsoleServiceProvider.php b/core/lib/Drupal/Core/Console/ConsoleServiceProvider.php new file mode 100644 index 0000000000..2154020c7d --- /dev/null +++ b/core/lib/Drupal/Core/Console/ConsoleServiceProvider.php @@ -0,0 +1,89 @@ +classLoader = $class_loader; + } + + public function register(DrupalContainerBuilder $container) { + $container->addCompilerPass($this); + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) { + // Add tagged commands to console.app. + $app = $container->getDefinition('console.app'); + foreach (array_keys($container->findTaggedServiceIds('console.command')) as $id) { + $app->addMethodCall( + 'add', [new Reference($id)] + ); + } + + // Discover commands in namespaces. + $all_prefixes = array_keys(array_merge( + $this->classLoader->getPrefixes(), + $this->classLoader->getPrefixesPsr4(), + $this->classLoader->getClassMap() + )); + // Remove duplicate first elements. + $prefixes = []; + foreach ($all_prefixes as $prefix) { + $exploded = explode('\\', $prefix); + $prefixes[$exploded[0]] = $exploded[0]; + } + // Find service definition classes within the namespaces. + $discovered_ids = []; + foreach ($prefixes as $prefix) { + $exploded = explode('\\', $prefix); + // @todo Determine a better magic naming scheme. + $class = $exploded[0] . '\\DrupalConsole\\CommandServiceDefinition'; + // @todo Check that our class implements the interface. + if (class_exists($class)) { + $command_service_definiton = new $class; + $definitions = $command_service_definiton->getDefinitions(); + // Add the definitions to the container. + $container->addDefinitions($definitions); + $discovered_ids = array_merge(array_keys($definitions), $discovered_ids); + } + } + // Register the commands with the CLI application. + foreach ($discovered_ids as $id) { + $app->addMethodCall( + 'add', [new Reference($id)] + ); + } + } + +} diff --git a/core/lib/Drupal/Core/Console/DrupalConsoleServiceDefinitionProvider.php b/core/lib/Drupal/Core/Console/DrupalConsoleServiceDefinitionProvider.php new file mode 100644 index 0000000000..b73f15a084 --- /dev/null +++ b/core/lib/Drupal/Core/Console/DrupalConsoleServiceDefinitionProvider.php @@ -0,0 +1,20 @@ +serviceYamls['app']['core'] = 'core/core.services.yml'; $this->serviceProviderClasses['app']['core'] = 'Drupal\Core\CoreServiceProvider'; + if ($this->environment === 'console') { + $this->serviceProviderClasses['app']['console'] = new ConsoleServiceProvider($this->classLoader); + } + // Retrieve enabled modules and register their namespaces. if (!isset($this->moduleList)) { $extensions = $this->getConfigStorage()->read('core.extension'); @@ -900,7 +905,8 @@ protected function initializeContainer() { // If the module list hasn't already been set in updateModules and we are // not forcing a rebuild, then try and load the container from the cache. - if (empty($this->moduleList) && !$this->containerNeedsRebuild) { + // @todo Make the cached definition available for the console environment. + if (empty($this->moduleList) && !$this->containerNeedsRebuild && $this->environment !== 'console') { $container_definition = $this->getCachedContainerDefinition(); } @@ -949,6 +955,9 @@ protected function initializeContainer() { $this->container->get('current_user')->setInitialAccountId($current_user_id); } + // Some services need the class loader. + $this->container->set('class_loader', $this->classLoader); + \Drupal::setContainer($this->container); // Allow other parts of the codebase to react on container initialization in diff --git a/core/lib/Drupal/DrupalConsole/CommandServiceDefinition.php b/core/lib/Drupal/DrupalConsole/CommandServiceDefinition.php new file mode 100644 index 0000000000..8f11593fbd --- /dev/null +++ b/core/lib/Drupal/DrupalConsole/CommandServiceDefinition.php @@ -0,0 +1,26 @@ + + new Definition(RunCron::class, [ + new Reference('console.bootstrapper'), + ]), + ]; + } + +} diff --git a/core/modules/system/src/Command/ClearCache.php b/core/modules/system/src/Command/ClearCache.php new file mode 100644 index 0000000000..82aef678e2 --- /dev/null +++ b/core/modules/system/src/Command/ClearCache.php @@ -0,0 +1,53 @@ +setName('drupal:cache-clear') + ->setAliases(['cc']) + ->setDescription('Clears all caches.'); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) { + /* @var $kernel \Drupal\Core\DrupalKernelInterface */ + $kernel = $this->bootstrap->bootstrap($this, $input, $output); + if ($kernel) { + $this->doRebuild($kernel->getContainer()); + $output->writeln('Caches cleared.'); + } + else { + // Attempts to bootstrap should fail before we see this error, but let's + // emit one anyway. + $output->write('Unable to clear caches.'); + } + } + + protected function doRebuild(ContainerInterface $container) { + // The drupal_rebuild() function isn't autoloaded, so we have to do things + // the old-fashioned way. + // @todo Change this once + // https://www.drupal.org/project/drupal/issues/3014752 is in. + include_once $container->get('app.root') . '/core/includes/utility.inc'; + drupal_rebuild($container->get('class_loader'), $container->get('request')); + } + +} diff --git a/core/modules/system/system.services.yml b/core/modules/system/system.services.yml index 0728e0da86..702f084ce9 100644 --- a/core/modules/system/system.services.yml +++ b/core/modules/system/system.services.yml @@ -43,3 +43,8 @@ services: arguments: ['@theme_handler', '@cache_tags.invalidator'] tags: - { name: event_subscriber } + console.clear_cache.command: + class: Drupal\system\Command\ClearCache + arguments: ['@console.bootstrapper'] + tags: + - { name: console.command } diff --git a/core/scripts/console b/core/scripts/console new file mode 100755 index 0000000000..b9f48b029d --- /dev/null +++ b/core/scripts/console @@ -0,0 +1,26 @@ +#!/usr/bin/env php +boot(); + +$container = $kernel->getContainer(); + +/* @var $app \Symfony\Component\Console\Application */ +$app = $container->get('console.app'); +$app->run(); diff --git a/core/tests/Drupal/Tests/Core/Console/ApplicationTest.php b/core/tests/Drupal/Tests/Core/Console/ApplicationTest.php new file mode 100644 index 0000000000..437f81b824 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Console/ApplicationTest.php @@ -0,0 +1,23 @@ +assertEquals('Drupal', $app->getName()); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Console/Command/ClearCacheTest.php b/core/tests/Drupal/Tests/Core/Console/Command/ClearCacheTest.php new file mode 100644 index 0000000000..837bd9090f --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Console/Command/ClearCacheTest.php @@ -0,0 +1,67 @@ +getMockBuilder(BootstrapInterface::class) + ->setMethods(['bootstrap']) + ->getMockForAbstractClass(); + + $kernel = $this->getMockBuilder(DrupalKernelInterface::class) + ->setMethods(['getContainer']) + ->getMockForAbstractClass(); + + $bootstrap->expects($this->once()) + ->method('bootstrap') + ->will($this->returnValue($kernel)); + + $container = $this->getMockBuilder(ContainerInterface::class) + ->getMockForAbstractClass(); + + $kernel->expects($this->once()) + ->method('getContainer') + ->willReturn($container); + + $command = $this->getMockBuilder(ClearCache::class) + ->setMethods(['doRebuild']) + ->setConstructorArgs([$bootstrap]) + ->getMock(); + $command->expects($this->once()) + ->method('doRebuild'); + + $input = $this->getMockForAbstractClass(InputInterface::class); + + $output = $this->getMockBuilder(OutputInterface::class) + ->setMethods(['writeln']) + ->getMockForAbstractClass(); + $output->expects($this->atLeastOnce()) + ->method('writeln'); + + $method = new \ReflectionMethod($command, 'execute'); + $method->setAccessible(TRUE); + $method->invokeArgs($command, [$input, $output]); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Console/Command/RunCronTest.php b/core/tests/Drupal/Tests/Core/Console/Command/RunCronTest.php new file mode 100644 index 0000000000..926f9fe87f --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Console/Command/RunCronTest.php @@ -0,0 +1,91 @@ +bootstrap = $this->getMockBuilder(BootstrapInterface::class) + ->getMockForAbstractClass(); + + $this->command = new RunCron($this->bootstrap); + } + + /** + * @covers ::execute + */ + public function testExecute() { + $input = $this->getMockBuilder(InputInterface::class) + ->getMockForAbstractClass(); + + $output = $this->getMockBuilder(OutputInterface::class) + ->getMockForAbstractClass(); + $output->expects($this->atLeastOnce()) + ->method('writeln'); + + $cron = $this->getMockBuilder(CronInterface::class) + ->getMockForAbstractClass(); + $cron->expects($this->once()) + ->method('run'); + + $container = $this->getMockBuilder(ContainerInterface::class) + ->getMockForAbstractClass(); + $container->expects($this->once()) + ->method('get') + ->with('cron') + ->will($this->returnValue($cron)); + + $kernel = $this->getMockBuilder(DrupalKernelInterface::class) + ->getMockForAbstractClass(); + $kernel->expects($this->atLeastOnce()) + ->method('getContainer') + ->will($this->returnValue($container)); + + $this->bootstrap->expects($this->once()) + ->method('bootstrap') + ->with($this->command, $input, $output) + ->will($this->returnValue($kernel)); + + $method = new \ReflectionMethod($this->command, 'execute'); + $method->setAccessible(TRUE); + $method->invoke($this->command, $input, $output); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Console/ConsoleServiceProviderTest.php b/core/tests/Drupal/Tests/Core/Console/ConsoleServiceProviderTest.php new file mode 100644 index 0000000000..9d0bcb871f --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Console/ConsoleServiceProviderTest.php @@ -0,0 +1,61 @@ +randomMachineName(), $this->randomMachineName()]; + $references[] = new Reference($service_ids[0]); + $references[] = new Reference($service_ids[1]); + + $definition = $this->getMockBuilder(Definition::class) + ->disableOriginalConstructor() + ->getMock(); + $definition->expects($this->at(0)) + ->method('addMethodCall') + ->with('add', [$references[0]]); + $definition->expects($this->at(1)) + ->method('addMethodCall') + ->with('add', [$references[1]]); + + $container_builder = $this->getMockBuilder(ContainerBuilder::class) + ->setMethods([ + 'get', + 'findTaggedServiceIds', + 'getDefinition', + ]) + ->disableOriginalConstructor() + ->getMock(); + $container_builder->expects($this->once()) + ->method('findTaggedServiceIds') + ->with('console.command') + ->will($this->returnValue(array_fill_keys($service_ids, []))); + $container_builder->expects($this->once()) + ->method('getDefinition') + ->with('console.app') + ->will($this->returnValue($definition)); + + $command = new ConsoleServiceProvider(); + $command->process($container_builder); + } + +}