diff --git a/composer.json b/composer.json
index f58b5af..83879ff 100644
--- a/composer.json
+++ b/composer.json
@@ -23,12 +23,16 @@
"symfony-cmf/routing": "1.1.*@alpha",
"easyrdf/easyrdf": "0.8.*",
"phpunit/phpunit": "3.7.*",
- "zendframework/zend-feed": "2.2.*"
+ "zendframework/zend-feed": "~2.2",
+ "symfony/console": "2.4.*",
+ "symfony/finder": "~2.4"
},
+ "prefer-stable": true,
"autoload": {
"psr-4": {
"Drupal\\Core\\": "core/lib/Drupal/Core",
"Drupal\\Component\\": "core/lib/Drupal/Component",
+ "Drupal\\Console\\": "core/lib/Drupal/Console",
"Drupal\\Driver\\": "drivers/lib/Drupal/Driver"
},
"files": [
diff --git a/core/console b/core/console
new file mode 100755
index 0000000..900b40e
--- /dev/null
+++ b/core/console
@@ -0,0 +1,13 @@
+#!/usr/bin/env php
+run();
diff --git a/core/lib/Drupal/Component/Discovery/FileClassResolver.php b/core/lib/Drupal/Component/Discovery/FileClassResolver.php
new file mode 100644
index 0000000..12cbd95
--- /dev/null
+++ b/core/lib/Drupal/Component/Discovery/FileClassResolver.php
@@ -0,0 +1,83 @@
+files[$path])) {
+ return $this->files[$path];
+ }
+ $fp = fopen($path, 'r');
+ $class = $namespace = $buffer = '';
+ $i = 0;
+ while (!$class) {
+ if (feof($fp)) {
+ break;
+ }
+
+ $buffer .= fread($fp, 512);
+ if (strpos($buffer, '{') === FALSE) {
+ continue;
+ }
+ $tokens = @token_get_all($buffer);
+
+ for (; $i < count($tokens); $i++) {
+ if ($tokens[$i][0] === T_NAMESPACE) {
+ for ($j = $i + 1; $j < count($tokens); $j++) {
+ if ($tokens[$j][0] === T_STRING) {
+ $namespace .= '\\' . $tokens[$j][1];
+ }
+ elseif ($tokens[$j] === '{' || $tokens[$j] === ';') {
+ break;
+ }
+ }
+ }
+
+ if ($tokens[$i][0] === T_CLASS) {
+ for ($j = $i + 1; $j < count($tokens); $j++) {
+ if ($tokens[$j] === '{') {
+ $class = $tokens[$i + 2][1];
+ }
+ }
+ }
+ }
+ }
+ if ($namespace && $class) {
+ $class_name = $namespace . '\\' . $class;
+ $this->files[$path] = $class_name;
+ return $class_name;
+ }
+ return FALSE;
+ }
+
+}
diff --git a/core/lib/Drupal/Component/Discovery/IsSubclassFilterIterator.php b/core/lib/Drupal/Component/Discovery/IsSubclassFilterIterator.php
new file mode 100644
index 0000000..760fa75
--- /dev/null
+++ b/core/lib/Drupal/Component/Discovery/IsSubclassFilterIterator.php
@@ -0,0 +1,61 @@
+superClass = $super_class;
+ $this->resolver = $resolver;
+ parent::__construct($iterator);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function accept() {
+ $current = $this->current();
+ // Normalize to \SplFileInfo.
+ if (!$current instanceof \SplFileInfo) {
+ $current = new \SplFileInfo($current);
+ }
+ if (!file_exists($current->getPathname())) {
+ return FALSE;
+ }
+
+ if (!$this->resolver) {
+ $this->resolver = new FileClassResolver();
+ }
+
+ $current_class = $this->resolver->classInFile($current->getPathName());
+ if (class_exists($current_class)) {
+ $r = new \ReflectionClass($current_class);
+ return $r->isSubclassOf($this->superClass);
+ }
+ return FALSE;
+ }
+
+}
diff --git a/core/lib/Drupal/Console/Command/CommandBase.php b/core/lib/Drupal/Console/Command/CommandBase.php
new file mode 100644
index 0000000..9adf5c4
--- /dev/null
+++ b/core/lib/Drupal/Console/Command/CommandBase.php
@@ -0,0 +1,19 @@
+getOption('environment');
+ // Yes, we have to require_once.
+ require_once __DIR__ . '/../../../../includes/bootstrap.inc';
+ try {
+ // This boot strategy ripped from drupal_handle_request().
+ // This should be about four lines of code, but it's not properly
+ // refactored at the kernel level.
+ drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);
+
+ $kernel = new DrupalKernel($env, \drupal_classloader(), TRUE);
+ $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) {
+ $formatter = $this->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/Console/Command/DrupalCronCommand.php b/core/lib/Drupal/Console/Command/DrupalCronCommand.php
new file mode 100644
index 0000000..16794d3
--- /dev/null
+++ b/core/lib/Drupal/Console/Command/DrupalCronCommand.php
@@ -0,0 +1,39 @@
+setName('drupal:cron')
+ ->setDescription('Perform a cron run.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $kernel = $this->bootstrap($input, $output);
+ if ($kernel) {
+ $output->writeln('Running cron...');
+ $cron = $kernel->getContainer()->get('cron');
+ $cron->run();
+ $output->writeln('Done.');
+ return;
+ }
+ }
+
+}
diff --git a/core/lib/Drupal/Console/DrupalApplication.php b/core/lib/Drupal/Console/DrupalApplication.php
new file mode 100644
index 0000000..00c05a0
--- /dev/null
+++ b/core/lib/Drupal/Console/DrupalApplication.php
@@ -0,0 +1,72 @@
+getDefinition()->addOption($env_opt);
+ $this->discoverCommands();
+ }
+
+ /**
+ * Perform command discovery.
+ *
+ * @todo: Inject DRUPAL_ROOT, so we can use vfsStream to test.
+ */
+ private function discoverCommands() {
+ $finder = new DrupalFinder();
+
+ $finder
+ // Search in Drupal's special places.
+ ->inDrupalCore()
+ ->inContrib()
+ ->excludeVendor()
+ // Only files, no directories.
+ ->files()
+ // Don't try to look in forbidden places.
+ ->ignoreUnreadableDirs()
+ // Ignore .git directories.
+ ->ignoreVCS(TRUE);
+
+ // We want *Command.php files.
+ $finder->name('*Command.php');
+
+ // We keep a class file resolver around so we can use it in both discovery
+ // and loading, since it stores what it has looked up.
+ $resolver = new FileClassResolver();
+
+ $filter_iterator = new IsSubclassFilterIterator(
+ $finder->getIterator(),
+ 'Drupal\\Console\\Command\\CommandBase',
+ $resolver
+ );
+
+ foreach ($filter_iterator as $file) {
+ $class = $resolver->classInFile($file->getPathName());
+ $this->add(new $class());
+ }
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Discovery/DrupalFinder.php b/core/lib/Drupal/Core/Discovery/DrupalFinder.php
new file mode 100644
index 0000000..2c4364d
--- /dev/null
+++ b/core/lib/Drupal/Core/Discovery/DrupalFinder.php
@@ -0,0 +1,94 @@
+drupalRoot = $drupal_root;
+ }
+
+ /**
+ * Path to the root of the Drupal installation.
+ *
+ * @return string
+ * Path to the root of the Drupal installation.
+ */
+ public function drupalRoot() {
+ return $this->drupalRoot;
+ }
+
+ /**
+ * Path to the Drupal installation's core directory.
+ *
+ * @return string
+ * Path to the Drupal installation's core directory.
+ */
+ public function drupalCore() {
+ return $this->drupalRoot . '/core';
+ }
+
+ /**
+ * Set iterator to search the Drupal root directory.
+ *
+ * @return Drupal\Core\Discovery\DrupalFinder
+ * Fluent interface.
+ */
+ public function inDrupalRoot() {
+ return $this->in($this->drupalRoot);
+ }
+
+ /**
+ * Set iterator to search the Drupal core directory.
+ *
+ * @return Drupal\Core\Discovery\DrupalFinder
+ * Fluent interface.
+ */
+ public function inDrupalCore() {
+ return $this->in($this->drupalCore());
+ }
+
+ /**
+ * Set iterator to search within Drupal's contrib directories.
+ *
+ * @return Drupal\Core\Discovery\DrupalFinder
+ * Fluent interface.
+ */
+ public function inContrib() {
+ $this->in($this->drupalRoot . '/modules');
+ return $this->in($this->drupalRoot . '/sites');
+ }
+
+ /**
+ * Set iterator to exclude Composer's vendor directory.
+ *
+ * @return Drupal\Core\Discovery\DrupalFinder
+ * Fluent interface.
+ */
+ public function excludeVendor() {
+ return $this->exclude($this->drupalCore() . '/vendor');
+ }
+
+}