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);
+ }
+
+}