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