diff --git a/composer.json b/composer.json
index 974dc0e..c8b6728 100644
--- a/composer.json
+++ b/composer.json
@@ -27,7 +27,9 @@
"zendframework/zend-feed": "2.2.*",
"mikey179/vfsStream": "1.*",
"stack/builder": "1.0.*",
- "egulias/email-validator": "1.2.*"
+ "egulias/email-validator": "1.2.*",
+ "symfony/console": "~2.5",
+ "symfony/finder": "~2.5"
},
"autoload": {
"psr-4": {
diff --git a/core/console b/core/console
new file mode 100755
index 0000000..50e1afa
--- /dev/null
+++ b/core/console
@@ -0,0 +1,28 @@
+#!/usr/bin/env php
+load($file->getPathname());
+}
+
+$pass = new CompilerPass();
+$container->addCompilerPass($pass);
+$container->compile();
+
+$app = $container->get('console.app');
+$app->run();
diff --git a/core/core.console.services.yml b/core/core.console.services.yml
new file mode 100644
index 0000000..6f70991
--- /dev/null
+++ b/core/core.console.services.yml
@@ -0,0 +1,22 @@
+parameters:
+ # Make the class a property so we can change it for testing.
+ console.app.class: Drupal\Core\Console\Application
+
+services:
+ console.app:
+ class: "%console.app.class%"
+
+ console.bootstrap:
+ class: Drupal\Core\Console\Bootstrap
+
+ console.command.cache_clear:
+ class: Drupal\Core\Console\Command\ClearCache
+ arguments: ['@console.bootstrap']
+ tags:
+ - { name: console.command }
+
+ console.command.run_cron:
+ class: Drupal\Core\Console\Command\RunCron
+ arguments: ['@console.bootstrap']
+ 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 0000000..aa7deed
--- /dev/null
+++ b/core/lib/Drupal/Core/Console/Application.php
@@ -0,0 +1,21 @@
+getOption('environment');
+ try {
+ require_once __DIR__ . '/../../../../includes/bootstrap.inc';
+ drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);
+
+ $kernel = new DrupalKernel($env ?: 'prod', \drupal_classloader(), TRUE, FALSE);
+ $kernel->boot();
+
+ if (!$request) {
+ // Create a meaningful request object. We shouldn't really need this but
+ // Drupal complains if it's not present.
+ $request = Request::createFromGlobals();
+ }
+ $container = $kernel->getContainer();
+ $container->set('request', $request);
+ $container->get('request_stack')->push($request);
+
+ drupal_bootstrap(DRUPAL_BOOTSTRAP_CODE);
+ return $kernel;
+ }
+ catch (\Exception $e) {
+ /** @var \Symfony\Component\Console\Helper\FormatterHelper $formatter */
+ $formatter = $command->getHelperSet()->get('formatter');
+ $error_messages = array(
+ 'Insufficient Drupal to proceed.',
+ 'This command requires a bootable Drupal installation.',
+ );
+ $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 0000000..ae7767e
--- /dev/null
+++ b/core/lib/Drupal/Core/Console/BootstrapInterface.php
@@ -0,0 +1,38 @@
+setName('drupal:cache-clear')
+ ->setAliases(array('cc'))
+ ->setDescription('Clears all caches.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $kernel = $this->bootstrap->bootstrap($this, $input, $output);
+ if ($kernel) {
+ drupal_flush_all_caches();
+ $output->writeln('Caches cleared.');
+ }
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Console/Command/CommandBootstrapBase.php b/core/lib/Drupal/Core/Console/Command/CommandBootstrapBase.php
new file mode 100644
index 0000000..f9fb61a
--- /dev/null
+++ b/core/lib/Drupal/Core/Console/Command/CommandBootstrapBase.php
@@ -0,0 +1,54 @@
+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 0000000..f47cedc
--- /dev/null
+++ b/core/lib/Drupal/Core/Console/Command/RunCron.php
@@ -0,0 +1,43 @@
+setName('drupal:cron')
+ ->setAliases(array('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/CompilerPass.php b/core/lib/Drupal/Core/Console/CompilerPass.php
new file mode 100644
index 0000000..39fda13
--- /dev/null
+++ b/core/lib/Drupal/Core/Console/CompilerPass.php
@@ -0,0 +1,42 @@
+findTaggedServiceIds('console.command');
+ $definition = $container->getDefinition('console.app');
+ foreach (array_keys($tagged_services) as $id) {
+ $definition->addMethodCall(
+ 'add', array(new Reference($id))
+ );
+ }
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Console/ServicesFinder.php b/core/lib/Drupal/Core/Console/ServicesFinder.php
new file mode 100644
index 0000000..baa32de
--- /dev/null
+++ b/core/lib/Drupal/Core/Console/ServicesFinder.php
@@ -0,0 +1,24 @@
+inDrupalCore()
+ ->inContrib()
+ ->excludeVendor()
+ ->files()
+ ->ignoreUnreadableDirs()
+ ->ignoreVCS(TRUE)
+ ->name('*.console.services.yml');
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Discovery/DrupalFinder.php b/core/lib/Drupal/Core/Discovery/DrupalFinder.php
new file mode 100644
index 0000000..9baf4a6
--- /dev/null
+++ b/core/lib/Drupal/Core/Discovery/DrupalFinder.php
@@ -0,0 +1,95 @@
+drupalRoot = $drupal_root;
+ }
+
+ /**
+ * Gets the path to the root of the Drupal installation.
+ *
+ * @return string
+ * The path to the root of the Drupal installation.
+ */
+ public function getDrupalRootPath() {
+ return $this->drupalRoot;
+ }
+
+ /**
+ * Gets the path to the Drupal installation's ./core directory.
+ *
+ * @return string
+ * The path to the Drupal installation's core directory.
+ */
+ public function getDrupalCorePath() {
+ return $this->getDrupalRootPath() . '/core';
+ }
+
+ /**
+ * Sets the iterator to search the Drupal root directory.
+ *
+ * @return $this
+ */
+ public function inDrupalRoot() {
+ return $this->in($this->getDrupalRootPath());
+ }
+
+ /**
+ * Sets the iterator to search the Drupal core directory.
+ *
+ * @return $this
+ */
+ public function inDrupalCore() {
+ return $this->in($this->getDrupalCorePath());
+ }
+
+ /**
+ * Sets the iterator to search within Drupal's contrib directories.
+ *
+ * @return $this
+ */
+ public function inContrib() {
+ $this->in($this->getDrupalRootPath() . '/modules');
+ return $this->in($this->getDrupalRootPath() . '/sites');
+ }
+
+ /**
+ * Sets the iterator to exclude Composer's vendor directory.
+ *
+ * @return $this
+ */
+ public function excludeVendor() {
+ return $this->exclude($this->getDrupalCorePath() . '/vendor');
+ }
+
+}
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 0000000..7b7a74c
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Console/ApplicationTest.php
@@ -0,0 +1,27 @@
+bootstrap = $this->getMock('\Drupal\Core\Console\BootstrapInterface');
+
+ $this->command = new ClearCache($this->bootstrap);
+ }
+
+ /**
+ * @covers ::execute
+ */
+ public function testExecute() {
+ $input = $this->getMock('\Symfony\Component\Console\Input\InputInterface');
+
+ $output = $this->getMock('\Symfony\Component\Console\Output\OutputInterface');
+ $output->expects($this->atLeastOnce())
+ ->method('writeln');
+
+ $kernel = $this->getMock('\Drupal\Core\DrupalKernelInterface');
+
+ $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);
+ }
+
+}
+
+}
+
+namespace {
+
+if (!function_exists('drupal_flush_all_caches')) {
+ function drupal_flush_all_caches() {
+ }
+}
+
+}
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 0000000..865472b
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Console/Command/RunCronTest.php
@@ -0,0 +1,84 @@
+bootstrap = $this->getMock('\Drupal\Core\Console\BootstrapInterface');
+
+ $this->command = new RunCron($this->bootstrap);
+ }
+
+ /**
+ * @covers ::execute
+ */
+ public function testExecute() {
+ $input = $this->getMock('\Symfony\Component\Console\Input\InputInterface');
+
+ $output = $this->getMock('\Symfony\Component\Console\Output\OutputInterface');
+ $output->expects($this->atLeastOnce())
+ ->method('writeln');
+
+ $cron = $this->getMock('\Drupal\Core\CronInterface');
+ $cron->expects($this->once())
+ ->method('run');
+
+ $container = $this->getMock('\Symfony\Component\DependencyInjection\ContainerInterface');
+ $container->expects($this->once())
+ ->method('get')
+ ->with('cron')
+ ->will($this->returnValue($cron));
+
+ $kernel = $this->getMock('\Drupal\Core\DrupalKernelInterface');
+ $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/CompilerPassTest.php b/core/tests/Drupal/Tests/Core/Console/CompilerPassTest.php
new file mode 100644
index 0000000..59814a7
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Console/CompilerPassTest.php
@@ -0,0 +1,84 @@
+command = new CompilerPass();
+ }
+
+ /**
+ * Test the process() method.
+ *
+ * @covers ::process
+ */
+ public function testProcess() {
+ $service_ids = array($this->randomMachineName(), $this->randomMachineName());
+ $references[] = new Reference($service_ids[0]);
+ $references[] = new Reference($service_ids[1]);
+
+ $definition = $this->getMockBuilder('\Symfony\Component\DependencyInjection\Definition')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $definition->expects($this->at(0))
+ ->method('addMethodCall')
+ ->with('add', array($references[0]));
+ $definition->expects($this->at(1))
+ ->method('addMethodCall')
+ ->with('add', array($references[1]));
+
+ $container_builder = $this->getMockBuilder('\Symfony\Component\DependencyInjection\ContainerBuilder')
+ ->setMethods(array(
+ 'get',
+ 'findTaggedServiceIds',
+ 'getDefinition',
+ ))
+ ->disableOriginalConstructor()
+ ->getMock();
+ $container_builder->expects($this->once())
+ ->method('findTaggedServiceIds')
+ ->with('console.command')
+ ->will($this->returnValue(array_fill_keys($service_ids, array())));
+ $container_builder->expects($this->once())
+ ->method('getDefinition')
+ ->with('console.app')
+ ->will($this->returnValue($definition));
+
+ $this->command->process($container_builder);
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Console/ServicesFinderTest.php b/core/tests/Drupal/Tests/Core/Console/ServicesFinderTest.php
new file mode 100644
index 0000000..fe2ac0f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Console/ServicesFinderTest.php
@@ -0,0 +1,49 @@
+ '',
+ 'name' => '\Drupal\Core\Console\ServicesFinder unit test',
+ 'group' => 'Console',
+ );
+ }
+
+ /**
+ * Basic test of constructing a new services finder.
+ *
+ * @covers ::__construct
+ */
+ public function testConstruct() {
+ $finder = new ServicesFinder();
+ $files = array();
+ foreach ($finder as $file) {
+ /** @var \Symfony\Component\Finder\SplFileInfo $file */
+ $files[] = $file->getRealPath();
+ $this->assertFileExists($file->getRealPath());
+ }
+ $this->assertNotCount(0, $files);
+ }
+
+}