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