Skip to content

[WIP] [Console] Make creating single command app easier #9609

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions src/Symfony/Component/Console/SingleCommandApplication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Console;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;

/**
* Application providing access to just one command.
*
* When a console application only consists of one
* command, having to specify this command's name as first
* argument is superfluous.
* This class simplifies creating and using this
* kind of applications.
*
* Usage:
*
* $cmd = new SimpleCommand();
* $app = new SingleCommandApplication($cmd, '1.2');
* $app->run();
*
* @author Stefaan Lippens <soxofaan@gmail.com>
*/
class SingleCommandApplication extends Application
{
/**
* Name of the single accessible command of this application
* @var string
*/
private $commandName;

/**
* Constructor to build a "single command" application, given a command.
*
* The application will adopt the same name as the command.
*
* @param Command $command The single (accessible) command for this application
* @param string $version The version of the application
*/
public function __construct(Command $command, $version = 'UNKNOWN')
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dropped the $name argument from my previous version here, in favour of reusing the command's name, for the sake of simplicity.

{
parent::__construct($command->getName(), $version);

// Add the given command as single (accessible) command.
$this->add($command);
$this->commandName = $command->getName();

// Override the Application's definition so that it does not
// require a command name as first argument.
$this->getDefinition()->setArguments();
}

/**
* {@inheritdoc}
*/
protected function getCommandName(InputInterface $input)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this method and why its dependence on $input?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it overwrites a method of the parent class, so it cannot change its signature.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see it now, thanks @stof

{
return $this->commandName;
}

/**
* Adds a command object.
*
* This function overrides (public) Application::add()
* but should should only be used internally.
* Will raise \LogicException when called
* after the single accessible command is set up
* (from the constructor).
*
* @param Command $command A Command object
*
* @return Command The registered command
*
* @throws \LogicException
*/
public function add(Command $command)
{
// Fail if we already set up the single accessible command.
if ($this->commandName) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to check, as the constructor requires setting a command

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but the constructor calls add()
Also the constructor calls parent::__construct, which also calls add() with the default commands (list and help),

So we have to allow three add() calls and throw exception for subsequent add() calls

throw new \LogicException("You should not add additional commands to a SingleCommandApplication instance.");
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this exception based on what @stof said at #9564 (comment)

}

return parent::add($command);
}
}
38 changes: 38 additions & 0 deletions src/Symfony/Component/Console/Tests/Fixtures/FooScaCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface;

class FooScaCommand extends Command
{
protected function configure()
{
$this
->setName('foosca')
->setDescription('The foosca command');
$this->addArgument(
'items',
InputArgument::IS_ARRAY,
'Items to process'
);
$this->addOption(
'bar',
'b',
InputOption::VALUE_NONE,
'Enable barring'
);
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$bar = $input->getOption('bar');
$output->writeln('<info>FooSca</info>' . ($bar ? ' (barred)': ' (basic)'));

foreach ($input->getArgument('items') as $item) {
$output->writeln('Item: ' . $item);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Usage:
foosca [-b|--bar] [items1] ... [itemsN]

Arguments:
items Items to process

Options:
--bar (-b) Enable barring
--help (-h) Display this help message.
--quiet (-q) Do not output any message.
--verbose (-v|vv|vvv) Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
--version (-V) Display this application version.
--ansi Force ANSI output.
--no-ansi Disable ANSI output.
--no-interaction (-n) Do not ask any interactive question.

138 changes: 138 additions & 0 deletions src/Symfony/Component/Console/Tests/SingleCommandApplicationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Console\Tests;

use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\StreamOutput;
use Symfony\Component\Console\SingleCommandApplication;

class SingleCommandApplicationTest extends \PHPUnit_Framework_TestCase
{
protected static $fixturesPath;

public static function setUpBeforeClass()
{
self::$fixturesPath = realpath(__DIR__ . '/Fixtures/');
require_once self::$fixturesPath.'/FooCommand.php';
require_once self::$fixturesPath . '/FooScaCommand.php';
}

public function testConstructor()
{
$application = new SingleCommandApplication(new \FooScaCommand(), 'v2.3');
$this->assertEquals(
'foosca',
$application->getName(),
'__construct() takes the application name as its first argument'
);
$this->assertEquals(
'v2.3',
$application->getVersion(),
'__construct() takes the application version as its second argument'
);
$this->assertEquals(
array('help', 'list', 'foosca'),
array_keys($application->all()),
'__construct() registered the help and list commands by default'
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do not wrap long lines in symfony core afaik

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen this in several places in Symfony.

But actually the auto formatting feature of my editor (PhpStorm) chose to do it this way (set up for PSR-2). I don't disagree.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use it in the docs, not in the core code

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some examples:

Symfony\Component\Console\Application::getHelp()
Symfony\Component\Console\Command\Command::getProcessedHelp()
Symfony\Component\Console\Descriptor\JsonDescriptor::describeApplication()
and various cases of multi-line arrays along the way

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wouterj could you please show counter examples? would be good to compare

}

/**
* @dataProvider provideRunData
*/
public function testRun(InputInterface $input, $expectedOutput, $expectedStatusCode=0)
{
// Set up application.
$application = new SingleCommandApplication(new \FooScaCommand(), '1.234');
$application->setAutoExit(false);
$application->setCatchExceptions(false);

// Set up output for application to render to.
$stream = fopen('php://memory', 'w', false);
$output = new StreamOutput($stream);

// Run application with given input.
$statusCode = $application->run($input, $output);

// Get generated output (and normalize newlines)
rewind($stream);
$display = stream_get_contents($stream);
$display = str_replace(PHP_EOL, "\n", $display);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: I dropped usage of Symfony\Component\Console\Tester\ApplicationTester and the removal of the array type hint in ApplicationTester::run(array $input, $options)
(which apparently broke BC) and replaced it with the low fat roll-your-own alternative above


$this->assertEquals($expectedStatusCode, $statusCode);
$this->assertEquals($expectedOutput, $display);
}

public function provideRunData()
{
$data = array();
$data[] = array(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird, why not just building an array of arrays?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is more readable.
I have worked on big @dataProvider unit tests where the arguments were multidimensional arrays and building the data array piece by piece ($data[] = ...) greatly helped readability, debugability and maintainability.
But I agree that in this case it it might be less of an issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes it is less of an issue, let's move to multidimensional array

new ArgvInput(array('cli.php')),
"FooSca (basic)\n",
);

$data[] = array(
new ArgvInput(array('cli.php', 'qwe')),
"FooSca (basic)\nItem: qwe\n",
);

$data[] = array(
new ArgvInput(array('cli.php', '--bar', 'qwe')),
"FooSca (barred)\nItem: qwe\n",
);

$data[] = array(
new ArgvInput(array('cli.php', '--bar', 'qwe', 'rty')),
"FooSca (barred)\nItem: qwe\nItem: rty\n",
);

$data[] = array(
new ArgvInput(array('cli.php', 'list')),
"FooSca (basic)\nItem: list\n",
);

$data[] = array(
new ArgvInput(array('cli.php', 'help')),
"FooSca (basic)\nItem: help\n",
);

$data[] = array(
new ArgvInput(array('cli.php', '--help')),
file_get_contents(__DIR__ . '/Fixtures/' . '/application_run_foosca_help.txt'),
);

$data[] = array(
new ArgvInput(array('cli.php', '-h')),
file_get_contents(__DIR__ . '/Fixtures/' . '/application_run_foosca_help.txt'),
);

$data[] = array(
new ArgvInput(array('cli.php', '--version')),
"foosca version 1.234\n"
);

$data[] = array(
new ArgvInput(array('cli.php', '-V')),
"foosca version 1.234\n"
);

return $data;
}

public function testAddingMoreCommands()
{
$app = new SingleCommandApplication(new \FooScaCommand());
$this->setExpectedException('LogicException');
$app->add(new \FooCommand());
}
}