Index: core/install/cache/class_structure.php
===================================================================
--- core/install/cache/class_structure.php
+++ core/install/cache/class_structure.php
@@ -87,11 +87,13 @@
'InPortal\\Core\\kernel\\Console\\Command\\IConsoleCommand' => '/core/kernel/Console/Command/IConsoleCommand.php',
'InPortal\\Core\\kernel\\Console\\Command\\ResetCacheCommand' => '/core/kernel/Console/Command/ResetCacheCommand.php',
'InPortal\\Core\\kernel\\Console\\Command\\RunEventCommand' => '/core/kernel/Console/Command/RunEventCommand.php',
+ 'InPortal\\Core\\kernel\\Console\\Command\\RunPhingCommand' => '/core/kernel/Console/Command/RunPhingCommand.php',
'InPortal\\Core\\kernel\\Console\\Command\\RunScheduledTaskCommand' => '/core/kernel/Console/Command/RunScheduledTaskCommand.php',
'InPortal\\Core\\kernel\\Console\\ConsoleApplication' => '/core/kernel/Console/ConsoleApplication.php',
'InPortal\\Core\\kernel\\Console\\ConsoleCommandProvider' => '/core/kernel/Console/ConsoleCommandProvider.php',
'InPortal\\Core\\kernel\\Console\\ConsoleIO' => '/core/kernel/Console/ConsoleIO.php',
'InPortal\\Core\\kernel\\Console\\IConsoleCommandProvider' => '/core/kernel/Console/IConsoleCommandProvider.php',
+ 'InPortal\\Core\\kernel\\Console\\PhingCache' => '/core/kernel/Console/PhingCache.php',
'InPortal\\Core\\kernel\\utility\\ClassDiscovery\\ClassDetector' => '/core/kernel/utility/ClassDiscovery/ClassDetector.php',
'InPortal\\Core\\kernel\\utility\\ClassDiscovery\\ClassMapBuilder' => '/core/kernel/utility/ClassDiscovery/ClassMapBuilder.php',
'InPortal\\Core\\kernel\\utility\\ClassDiscovery\\CodeFolderFilterIterator' => '/core/kernel/utility/ClassDiscovery/CodeFolderFilterIterator.php',
@@ -882,6 +884,13 @@
0 => 'InPortal\\Core\\kernel\\Console\\Command\\AbstractCommand',
),
),
+ 'InPortal\\Core\\kernel\\Console\\Command\\RunPhingCommand' => array(
+ 'type' => 1,
+ 'modifiers' => 0,
+ 'extends' => array(
+ 0 => 'InPortal\\Core\\kernel\\Console\\Command\\AbstractCommand',
+ ),
+ ),
'InPortal\\Core\\kernel\\Console\\Command\\RunScheduledTaskCommand' => array(
'type' => 1,
'modifiers' => 0,
@@ -911,6 +920,13 @@
'InPortal\\Core\\kernel\\Console\\IConsoleCommandProvider' => array(
'type' => 2,
),
+ 'InPortal\\Core\\kernel\\Console\\PhingCache' => array(
+ 'type' => 1,
+ 'modifiers' => 0,
+ 'extends' => array(
+ 0 => 'kBase',
+ ),
+ ),
'InPortal\\Core\\kernel\\utility\\ClassDiscovery\\ClassDetector' => array(
'type' => 1,
'modifiers' => 0,
Index: core/kernel/Console/Command/CompletionCommand.php
===================================================================
--- core/kernel/Console/Command/CompletionCommand.php
+++ core/kernel/Console/Command/CompletionCommand.php
@@ -17,6 +17,7 @@
use InPortal\Core\kernel\Console\ConsoleApplication;
use Stecman\Component\Symfony\Console\BashCompletion\Completion;
+use Stecman\Component\Symfony\Console\BashCompletion\Completion\ShellPathCompletion;
use Stecman\Component\Symfony\Console\BashCompletion\CompletionCommand as BashCompletionCommand;
use Stecman\Component\Symfony\Console\BashCompletion\CompletionHandler;
@@ -66,6 +67,14 @@
*/
protected function configureCompletion(CompletionHandler $handler)
{
+ $handler->addHandler(
+ new ShellPathCompletion(
+ 'phing:run',
+ 'path',
+ Completion::TYPE_ARGUMENT
+ )
+ );
+
// This can be removed once https://github.com/stecman/symfony-console-completion v0.5.2 will be released.
$handler->addHandler(
new Completion(
Index: core/kernel/Console/Command/RunPhingCommand.php
===================================================================
--- /dev/null
+++ core/kernel/Console/Command/RunPhingCommand.php
@@ -0,0 +1,347 @@
+setName('phing:run')
+ ->setDescription('Executes/lists Phing targets')
+ ->addArgument(
+ 'target',
+ InputArgument::OPTIONAL,
+ 'Build target (e.g. "build")'
+ )
+ ->addArgument(
+ 'path',
+ InputArgument::OPTIONAL,
+ 'Path to limit target activity with'
+ );
+ }
+
+ /**
+ * Initializes dependencies.
+ *
+ * @return void
+ */
+ protected function initDependencies()
+ {
+ parent::initDependencies();
+
+ $this->phingCache = $this->Application->makeClass('InPortal\\Core\\kernel\\Console\\PhingCache');
+ $this->phing = $this->createPhing();
+ }
+
+ /**
+ * Executes the current command.
+ *
+ * @param InputInterface $input An InputInterface instance.
+ * @param OutputInterface $output An OutputInterface instance.
+ *
+ * @return null|integer
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $target = $this->io->getArgument('target');
+
+ if ( $target !== null ) {
+ return $this->executePhingTarget($target);
+ }
+
+ return $this->listPhingTargets();
+ }
+
+ /**
+ * Executes specific Phing target.
+ *
+ * @param string $target Target.
+ *
+ * @return integer
+ */
+ protected function executePhingTarget($target)
+ {
+ if ( !array_key_exists($target, $this->getTargets()) ) {
+ throw new \RuntimeException('The "' . $target . '" target is unknown.');
+ }
+
+ $path = $this->io->getArgument('path');
+ $phing = $this->getPhing()->add($target);
+
+ if ( $path ) {
+ $absolute_path = realpath($path);
+ $relative_path = $this->getRelativePath($absolute_path);
+
+ if ( $relative_path === $absolute_path ) {
+ throw new \RuntimeException('The "path" argument must be relative to root In-Portal directory.');
+ }
+
+ $phing->add('-Dscan.dir=${base.dir}' . $relative_path);
+ }
+
+ $io = $this->io;
+ $process = $phing->build();
+ $process->run(function ($type, $data) use ($io) {
+ $io->write($data);
+ });
+
+ return $process->getExitCode();
+ }
+
+ /**
+ * Lists Phing targets.
+ *
+ * @return integer
+ */
+ protected function listPhingTargets()
+ {
+ $build_file = $this->getRelativePath($this->getBuildFile());
+ $this->io->writeln(array('Phing targets (build file: ' . $build_file . '):', ''));
+
+ $table = new Table($this->io->getOutput());
+ $table->setHeaders(array('Name', 'Description'));
+
+ foreach ( $this->getTargets() as $name => $description ) {
+ $table->addRow(array($name, $description));
+ }
+
+ $table->render();
+
+ return 0;
+ }
+
+ /**
+ * Return possible values for the named argument.
+ *
+ * @param string $argumentName Argument name.
+ * @param CompletionContext $context Completion context.
+ *
+ * @return array
+ */
+ public function completeArgumentValues($argumentName, CompletionContext $context)
+ {
+ $suggestions = parent::completeArgumentValues($argumentName, $context);
+
+ if ( $argumentName === 'target' ) {
+ return array_keys($this->getTargets());
+ }
+
+ return $suggestions;
+ }
+
+ /**
+ * Create process builder for executing Phing commands.
+ *
+ * @return \ProcessBuilder
+ * @throws \RuntimeException When "phing" executable not found in PATH.
+ */
+ protected function createPhing()
+ {
+ if ( !$this->isExecutableInPath('phing') ) {
+ throw new \RuntimeException('Unable to locate "phing" executable in PATH.');
+ }
+
+ /** @var \ProcessBuilder $phing */
+ $phing = $this->Application->makeClass('ProcessBuilder');
+ $phing->setExecutable('phing');
+
+ // The "$this->io" isn't set during auto-complete.
+ if ( isset($this->io) && $this->io->isDecorated() ) {
+ $phing->add('-logger')->add('phing.listener.AnsiColorLogger');
+ }
+
+ $build_file = $this->getBuildFile();
+
+ if ( $build_file ) {
+ $phing->add('-f')->add($build_file);
+ }
+
+ return $phing;
+ }
+
+ /**
+ * Forks Phing process builder.
+ *
+ * @return \ProcessBuilder
+ */
+ protected function getPhing()
+ {
+ return clone $this->phing;
+ }
+
+ /**
+ * Returns possible task names.
+ *
+ * @return array
+ */
+ protected function getTargets()
+ {
+ $targets = $this->phingCache->getTargets();
+
+ if ( !$targets ) {
+ $targets = $this->parseTargets();
+ $this->phingCache->setTargets($targets);
+ }
+
+ return $targets;
+ }
+
+ /**
+ * Returns possible task names.
+ *
+ * @return array
+ */
+ protected function parseTargets()
+ {
+ $process = $this->getPhing()->add('-l')->build();
+ $process->mustRun();
+
+ $lines = explode(PHP_EOL, $process->getOutput());
+
+ $headings = array(
+ 'Default target:',
+ 'Main targets:',
+ 'Subtargets:',
+ );
+
+ $targets = array();
+ $current_heading = '';
+
+ foreach ( $lines as $line ) {
+ if ( $line === '' ) {
+ $current_heading = '';
+ continue;
+ }
+ elseif ( $line === '-------------------------------------------------------------------------------' ) {
+ continue;
+ }
+
+ if ( in_array($line, $headings) ) {
+ $current_heading = $line;
+ $targets[$current_heading] = array();
+
+ continue;
+ }
+
+ if ( $current_heading ) {
+ list ($target, $description) = explode(' ', $line, 2);
+
+ // Only add targets from topmost project.
+ if ( strpos($target, '.') === false ) {
+ $targets[$current_heading][trim($target)] = trim($description);
+ }
+ }
+ }
+
+ return $targets['Main targets:'];
+ }
+
+ /**
+ * Checks if executable is in PATH.
+ *
+ * @param string $executable Executable.
+ *
+ * @return boolean
+ */
+ protected function isExecutableInPath($executable)
+ {
+ /** @var \ProcessBuilder $process_builder */
+ $process_builder = $this->Application->makeClass('ProcessBuilder');
+
+ $process = $process_builder->setExecutable('which')->add($executable)->build();
+ $process->run();
+
+ return $process->isSuccessful();
+ }
+
+ /**
+ * Allows user to choose build file.
+ *
+ * @return string
+ * @throws \LogicException When no suitable build files found.
+ */
+ protected function getBuildFile()
+ {
+ $build_file = $this->phingCache->getBuildFile();
+
+ // The "$this->io" isn't set during auto-complete.
+ if ( !$build_file && isset($this->io) ) {
+ $build_file_folder = FULL_PATH . '/tools/build';
+ $build_files = glob($build_file_folder . '/*.xml');
+
+ if ( !$build_files ) {
+ throw new \LogicException('No build files found in "' . $build_file_folder . '" directory.');
+ }
+
+ foreach ( $build_files as $index => $build_file ) {
+ $build_files[$index] = $this->getRelativePath($build_file);
+ }
+
+ $build_file = $this->io->choose('Please choose build file:', $build_files, null, 'Incorrect build file');
+
+ $this->phingCache->setBuildFile($build_file);
+ }
+
+ if ( $build_file ) {
+ return FULL_PATH . $build_file;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns relative path to In-Portal root folder.
+ *
+ * @param string $absolute_path Absolute path.
+ *
+ * @return string
+ */
+ protected function getRelativePath($absolute_path)
+ {
+ return preg_replace('/^' . preg_quote(FULL_PATH, '/') . '/', '', $absolute_path, 1);
+ }
+
+}
Index: core/kernel/Console/PhingCache.php
===================================================================
--- /dev/null
+++ core/kernel/Console/PhingCache.php
@@ -0,0 +1,133 @@
+cacheFile = WRITEABLE . '/phing_settings.json';
+ $this->cacheData = $this->read();
+ }
+
+ /**
+ * Returns path to build file.
+ *
+ * @return string
+ */
+ public function getBuildFile()
+ {
+ $build_file = isset($this->cacheData['build_file']) ? $this->cacheData['build_file'] : '';
+
+ if ( $build_file && file_exists(FULL_PATH . $build_file) ) {
+ return $build_file;
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets build file.
+ *
+ * @param string $build_file Build file.
+ *
+ * @return void
+ */
+ public function setBuildFile($build_file)
+ {
+ $this->cacheData['build_file'] = $build_file;
+
+ // Build file change > force target re-parsing.
+ $this->setTargets(array());
+ $this->store();
+ }
+
+ /**
+ * Returns supported targets.
+ *
+ * @return array
+ */
+ public function getTargets()
+ {
+ return isset($this->cacheData['targets']) ? $this->cacheData['targets'] : array();
+ }
+
+ /**
+ * Sets targets.
+ *
+ * @param array $targets Targets.
+ *
+ * @return void
+ */
+ public function setTargets(array $targets)
+ {
+ $this->cacheData['targets'] = $targets;
+ $this->store();
+ }
+
+ /**
+ * Returns build file from cache.
+ *
+ * @return string
+ * @throws \LogicException When cache file format can't be recognized.
+ */
+ protected function read()
+ {
+ if ( !file_exists($this->cacheFile) ) {
+ return array();
+ }
+
+ $content = json_decode(file_get_contents($this->cacheFile), true);
+
+ if ( $content === null ) {
+ throw new \LogicException('The "' . $this->cacheFile . '" file format can\'t be recognized.');
+ }
+
+ return $content;
+ }
+
+ /**
+ * Stores new cache content.
+ *
+ * @return void
+ */
+ protected function store()
+ {
+ $json_encode_options = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0;
+ file_put_contents($this->cacheFile, json_encode($this->cacheData, $json_encode_options));
+ }
+
+}