diff --git a/composer.json b/composer.json index 65f58d1..81a5d27 100644 --- a/composer.json +++ b/composer.json @@ -24,12 +24,17 @@ "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", + "mikey179/vfsStream": "~1.2" }, + "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..9ee2d76 --- /dev/null +++ b/core/console @@ -0,0 +1,30 @@ +#!/usr/bin/env php +consoleServiceYamlFiles(); +foreach ($service_files as $file) { + $loader->load($file->getPathname()); +} + +$pass = new ConsoleCompilerPass(); +$container->addCompilerPass($pass); +$container->compile(); + +$app = $container->get('drupal_console_app'); + +$app->run(); diff --git a/core/core.console.services.yml b/core/core.console.services.yml new file mode 100644 index 0000000..d379f47 --- /dev/null +++ b/core/core.console.services.yml @@ -0,0 +1,15 @@ +parameters: + # Make the class a property so we can change it for testing. + drupal_console_app.class: Drupal\Console\DrupalConsoleApp + +services: + drupal_console_app: + class: "%drupal_console_app.class%" + tags: + - { name: drupal_console } + + drupal_console.drupal_cron: + class: Drupal\Console\Command\DrupalCronCommand + tags: + - { name: drupal_console.command } + - { name: drupal_console } 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 @@ +addOption( + 'environment', 'e', InputOption::VALUE_REQUIRED, 'Kernel environment.', 'prod' + ); + parent::configure(); + } + + /** + * Bootstrap Drupal enough so that we can run commands which require it. + * + * @param InputInterface $input + * Command input object. + * @param OutputInterface $output + * Command output object + * + * @returns DrupalKernel|FALSE + * Booted kernel, or FALSE. + */ + protected function bootstrap(InputInterface $input, OutputInterface $output, Request $request = \NULL) { + // This defaults to 'prod.' + $env = $input->getOption('environment'); + try { + // This boot strategy ripped from drupal_handle_request(). + require_once __DIR__ . '/../../../../includes/bootstrap.inc'; + 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); + + // Don't ask me why we need this voodo. It's in drupal_handle_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..58470e6 --- /dev/null +++ b/core/lib/Drupal/Console/Command/DrupalCronCommand.php @@ -0,0 +1,40 @@ +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/ConsoleCompilerPass.php b/core/lib/Drupal/Console/ConsoleCompilerPass.php new file mode 100644 index 0000000..dccbc80 --- /dev/null +++ b/core/lib/Drupal/Console/ConsoleCompilerPass.php @@ -0,0 +1,56 @@ +findTaggedServiceIds('drupal_console.command'); + // Remove all services which aren't console-related. We don't want to + // instantiate any classes which have any dependencies other than those for + // console. +/* $services = $container->getServiceIds(); + foreach ($services as $service) { + if (!in_array($service, array('service_container', 'drupal_console_app'))) { + if (!in_array($service, $tagged_services)) { + $container->removeDefinition($service); + } + } + } +*/ + // Tell the container to add these services to the console app. + $definition = $container->getDefinition('drupal_console_app'); + foreach ($tagged_services as $id => $attributes) { + $definition->addMethodCall( + 'add', array(new Reference($id)) + ); + } + } + +} diff --git a/core/lib/Drupal/Console/Discovery/DrupalConsoleServicesFinder.php b/core/lib/Drupal/Console/Discovery/DrupalConsoleServicesFinder.php new file mode 100644 index 0000000..189d1cf --- /dev/null +++ b/core/lib/Drupal/Console/Discovery/DrupalConsoleServicesFinder.php @@ -0,0 +1,29 @@ +inDrupalCore() + ->inContrib() + ->excludeVendor() + // Only files, no directories. + ->files() + // Don't try to look in forbidden places. + ->ignoreUnreadableDirs() + // Ignore .git directories. + ->ignoreVCS(TRUE); + + // We want console service YAML files. + $this->name('*.console.services.yml'); + + return $this->getIterator(); + } + + +} \ No newline at end of file diff --git a/core/lib/Drupal/Console/DrupalConsoleApp.php b/core/lib/Drupal/Console/DrupalConsoleApp.php new file mode 100644 index 0000000..2f2f301 --- /dev/null +++ b/core/lib/Drupal/Console/DrupalConsoleApp.php @@ -0,0 +1,73 @@ +inDrupalCore() + ->inContrib() + ->excludeVendor() + // Only files, no directories. + ->files() + // Don't try to look in forbidden places. + ->ignoreUnreadableDirs() + // Ignore .git directories. + ->ignoreVCS(TRUE); + + // We want console service YAML files. + $finder->name('*.console.services.yml'); + + // 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(); + + // Use IsSubclassFilterIterator to find classes which are subclasses of + // CommandBase. + $filter_iterator = new IsSubclassFilterIterator( + $finder->getIterator(), + 'Drupal\\Console\\Command\\CommandBase', + $resolver + ); + + // Add the commands we found to the app. + foreach ($filter_iterator as $file) { + $class = $resolver->classInFile($file->getPathName()); + $this->add(new $class()); + } + + return $this; + } + +} diff --git a/core/lib/Drupal/Core/Discovery/DrupalFinder.php b/core/lib/Drupal/Core/Discovery/DrupalFinder.php new file mode 100644 index 0000000..016b0e5 --- /dev/null +++ b/core/lib/Drupal/Core/Discovery/DrupalFinder.php @@ -0,0 +1,96 @@ +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'); + } + +}