Skip to content
Open
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
25 changes: 25 additions & 0 deletions system/CLI/AbstractCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,31 @@ protected function call(string $command, array $arguments = [], array $options =
return $this->commands->runCommand($command, $arguments, $this->resolveChildInteractiveState($options, $noInteractionOverride));
}

/**
* Like `call()`, but suppresses the sub-command's output. Defaults to non-interactive
* since a silenced sub-command cannot meaningfully prompt.
*
* @param list<string> $arguments Parsed arguments from command line.
* @param array<string, list<string>|string|null> $options Parsed options from command line.
* @param bool|null $noInteractionOverride See `call()` for the semantics.
*/
protected function callSilently(string $command, array $arguments = [], array $options = [], ?bool $noInteractionOverride = true): int
{
$priorIo = CLI::getInputOutput();

CLI::setInputOutput(new NullInputOutput());

try {
return $this->call($command, $arguments, $options, $noInteractionOverride);
} finally {
if ($priorIo instanceof InputOutput) {
CLI::setInputOutput($priorIo);
} else {
CLI::resetInputOutput();
}
}
}
Comment on lines +515 to +530
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This makes me wonder... should we also restore the CLI::$lastWrite state? Or this isn't really important? Honestly, I can't decide.


/**
* Gets the unbound arguments that can be passed to other commands when called via the `call()` method.
*
Expand Down
12 changes: 8 additions & 4 deletions system/CLI/CLI.php
Original file line number Diff line number Diff line change
Expand Up @@ -1166,8 +1166,14 @@ public static function resetLastWrite(): void
}

/**
* Testing purpose only
*
* @internal
*/
public static function getInputOutput(): ?InputOutput
{
return static::$io;
}

/**
* @internal
*/
public static function setInputOutput(InputOutput $io): void
Expand All @@ -1176,8 +1182,6 @@ public static function setInputOutput(InputOutput $io): void
}

/**
* Testing purpose only
*
* @internal
*/
public static function resetInputOutput(): void
Expand Down
32 changes: 32 additions & 0 deletions system/CLI/NullInputOutput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\CLI;

/**
* An InputOutput sink that discards all output and never reads input.
*
* Backs `AbstractCommand::callSilently()` to silence a sub-command's
* output when the parent wants to emit its own consolidated message instead.
*/
final class NullInputOutput extends InputOutput
{
public function fwrite($handle, string $string): void
{
}

public function input(?string $prefix = null): string
{
return '';
}
}
5 changes: 5 additions & 0 deletions tests/_support/Commands/Modern/AppAboutCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ public function helpMe(): int
return $this->call('help');
}

public function helpMeSilently(): int
{
return $this->callSilently('help');
}

/**
* @param array<string, list<string|null>|string|null>|null $options
*/
Expand Down
33 changes: 33 additions & 0 deletions tests/system/CLI/AbstractCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use ReflectionClass;
use ReflectionProperty;
use Tests\Support\Commands\Modern\AppAboutCommand;
use Tests\Support\Commands\Modern\InteractFixtureCommand;
use Tests\Support\Commands\Modern\InteractiveStateProbeCommand;
Expand Down Expand Up @@ -257,6 +258,38 @@ public function testCommandCanCallAnotherCommand(): void
$this->assertStringContainsString('help [options] [--] [<command_name>]', $this->getStreamFilterBuffer());
}

public function testCallSilentlySuppressesSubCommandOutputAndReturnsExitCode(): void
{
$command = new AppAboutCommand(new Commands());

$this->assertSame(EXIT_SUCCESS, $command->helpMeSilently());
$this->assertSame('', $this->getStreamFilterBuffer());
}

public function testCallSilentlyRestoresPriorIo(): void
{
$custom = new InputOutput();
CLI::setInputOutput($custom);

$command = new AppAboutCommand(new Commands());
$command->helpMeSilently();

$this->assertSame($custom, CLI::getInputOutput());
}

public function testCallSilentlyResetsToFreshInputOutputWhenPriorWasNull(): void
{
$property = new ReflectionProperty(CLI::class, 'io');
$property->setValue(null, null);

$command = new AppAboutCommand(new Commands());
$command->helpMeSilently();

$current = CLI::getInputOutput();
$this->assertInstanceOf(InputOutput::class, $current);
$this->assertNotInstanceOf(NullInputOutput::class, $current);
}

public function testRunCommand(): void
{
command('app:about a');
Expand Down
63 changes: 63 additions & 0 deletions tests/system/CLI/NullInputOutputTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\CLI;

use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\StreamFilterTrait;
use PHPUnit\Framework\Attributes\Group;

/**
* @internal
*/
#[Group('Others')]
final class NullInputOutputTest extends CIUnitTestCase
{
use StreamFilterTrait;

public function testFwriteDiscardsOutput(): void
{
$io = new NullInputOutput();
$io->fwrite(STDOUT, 'should not appear');
$io->fwrite(STDERR, 'should not appear either');

$this->assertSame('', $this->getStreamFilterBuffer());
}

public function testInputReturnsEmptyStringWithoutEchoingPrefix(): void
{
$io = new NullInputOutput();

$this->assertSame('', $io->input());
$this->assertSame('', $io->input('any prefix > '));
$this->assertSame('', $this->getStreamFilterBuffer());
}

public function testCanBeSwappedIntoCliToSilenceWrites(): void
{
$prior = CLI::getInputOutput();
CLI::setInputOutput(new NullInputOutput());

try {
CLI::write('this should be discarded');
CLI::error('this too');
$this->assertSame('', $this->getStreamFilterBuffer());
} finally {
if ($prior instanceof InputOutput) {
CLI::setInputOutput($prior);
} else {
CLI::resetInputOutput();
}
}
}
}
3 changes: 3 additions & 0 deletions user_guide_src/source/changelogs/v4.8.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ Commands
When used with ``CLI::getOption()``, an array option will return its last value (for example, in this case, ``value2``). To retrieve all values for an array option, use ``CLI::getRawOption()``.
- Likewise, the ``command()`` function now also supports the above enhancements for command-line option parsing when using the function to run commands from code.
- Added ``make:request`` generator command to scaffold :ref:`Form Request <form-requests>` classes.
- Added ``AbstractCommand::callSilently()`` to invoke another command with its output discarded, restoring the prior IO afterwards. See :ref:`modern-commands-call-silently`.
- Added :php:class:`NullInputOutput <CodeIgniter\\CLI\\NullInputOutput>`, an :php:class:`InputOutput <CodeIgniter\\CLI\\InputOutput>` sink that discards all writes and returns an empty string from ``input()``.
- Added ``CLI::getInputOutput()`` (``@internal``) as a getter symmetric to ``CLI::setInputOutput()`` and ``CLI::resetInputOutput()``.

Testing
=======
Expand Down
25 changes: 25 additions & 0 deletions user_guide_src/source/cli/cli_modern_commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,21 @@ To forward the caller's own input through to the target command, pass

.. literalinclude:: cli_modern_commands/008.php

.. _modern-commands-call-silently:

Calling Silently
================

When a command delegates a step to another command but wants to emit its own
consolidated message instead of letting the sub-command's output leak through,
use ``$this->callSilently()``:

.. literalinclude:: cli_modern_commands/012.php

The sub-command's output is suppressed and ``$noInteractionOverride`` defaults
to ``true``, since a silenced sub-command cannot meaningfully prompt. Pass an
explicit value to override.

**************
Usage Examples
**************
Expand Down Expand Up @@ -546,6 +561,16 @@ covered in the sections above and are not listed here.
Invokes another modern command. The arguments and options go through
bind and validate on the target command, just like a user invocation.

.. php:method:: callSilently(string $command[, array $arguments = [], array $options = [], ?bool $noInteractionOverride = true]): int

:param string $command: The name of the modern command to call.
:param array $arguments: Positional arguments to forward.
:param array $options: Options to forward, keyed by long name, shortcut, or negation.
:param bool|null $noInteractionOverride: See :php:meth:`call`. Defaults to ``true``.
:returns: The exit code returned by the called command.

Like :php:meth:`call`, but suppresses the sub-command's output.

.. php:method:: getUnboundArguments(): array

Returns the raw, parsed positional arguments as passed to the
Expand Down
12 changes: 12 additions & 0 deletions user_guide_src/source/cli/cli_modern_commands/012.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

use CodeIgniter\CLI\CLI;

// Inside execute():

// Run `cache:clear` without leaking its own output; emit our own message instead.
$exitCode = $this->callSilently('cache:clear');

if ($exitCode === EXIT_SUCCESS) {
CLI::write('Cache cleared as part of deploy step.', 'green');
}
Loading