Skip to content
Merged
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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ The format is based on Keep a Changelog.
- Added adapter-based Lang provider support with `DeepL` and `Google Translate` adapters plus shared remote request/caching infrastructure (#533)

### Changed
- **BREAKING:** The console adapter constructor is now only the boot pipeline, like the web adapter (#576):
- The Symfony `Application`, the input and the output are provided by the new `InitConsoleStage` through the container, and the new `SetupConsoleApplicationStage` names the application from `app.name` and `app.version` once the whole boot has succeeded (it stays `UNKNOWN` after a failed boot). `ConsoleContext` is built from the `AppContext` and the boot failure, and reads those objects from the container. Removed `ConsoleAppAdapter::createApplication()` (get the application with `Di::get(Application::class)`) and `ConsoleContext::getAppContext()`.
- The adapter no longer makes the environment mutable for every command. `core:key` and the toolkit installer already do it themselves; a command outside the framework that calls `environment()->updateRow()` must call `setMutable(true)` first, or it gets `EnvException::environmentImmutable()`.
- The console boot no longer decides by the `core:env` command name (#575):
- `ConsoleAppAdapter` always runs the full boot and, when it fails, keeps the failure instead of stopping. `RunCommandStage` re-throws that original failure only for a command that requires the environment, so `core:env`, `list`, and `help` still run and `core:env --yes` can repair a missing, empty, or malformed `.env`. The reduced boot for `core:env` is gone, and the Symfony `Application` is named `UNKNOWN` until a successful boot names it from the app config.
- Added `CliCommand::$requiresEnvironment` (default `true`) with `CliCommand::requiresEnvironment()`; a command that works without the environment, like `EnvCommand`, sets it to `false`. `ConsoleContext` takes an optional boot failure and exposes it with `getBootFailure()`.
- **BREAKING:** Refactored the web and console runtime into staged pipelines (#537):
- `WebAppAdapter::start()` now runs a `RequestPipeline` of the preflight, route resolution, not found, prepare, dispatch, and send stages with a `RequestContext`, and `ConsoleAppAdapter::start()` runs a `ConsolePipeline` of the register core commands, register app commands, and run stages with a `ConsoleContext`; the run stage rejects a missing or unknown command before running anything, and registration goes through the new `Quantum\Console\CommandRegistrar`. Behavior is unchanged, and runtime stage events are not dispatched yet.
- Moved the web adapter, context, and pipeline to `Quantum\App\Adapters\Web`, the console ones to `Quantum\App\Adapters\Console`, and the boot stages to `Quantum\App\Stages\Boot`. Code that goes through `AppFactory` is unaffected; code that references these classes directly must update its imports.
- Removed the `AppAdapter` base class and the `WebAppTrait` and `ConsoleAppTrait` traits. `WebAppAdapter` and `ConsoleAppAdapter` now implement `AppInterface` directly, and `ConsoleAppAdapter::createApplication()` stays public.
- Removed the `AppAdapter` base class and the `WebAppTrait` and `ConsoleAppTrait` traits. `WebAppAdapter` and `ConsoleAppAdapter` now implement `AppInterface` directly.
- **BREAKING:** Replaced the `Quantum\Hook` package with `Quantum\Event` (#536):
- `hook()` is now `event()`; `on()` and `fire()` are now `listen()` and `dispatch()`, and no aliases remain. Listeners receive one named payload array, which is empty when none is given.
- Event names no longer need to be registered, so `shared/config/hooks.php` is no longer read; dispatching a name without listeners does nothing and an empty name throws `EventException`.
Expand Down
34 changes: 6 additions & 28 deletions src/App/Adapters/Console/ConsoleAppAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,15 @@

namespace Quantum\App\Adapters\Console;

use Quantum\App\Stages\Boot\SetupConsoleApplicationStage;
use Quantum\App\Stages\Console\RegisterCoreCommandsStage;
use Quantum\App\Stages\Console\RegisterAppCommandsStage;
use Symfony\Component\Console\Output\ConsoleOutput;
use Quantum\App\Stages\Boot\SetupErrorHandlerStage;
use Quantum\App\Stages\Boot\LoadEnvironmentStage;
use Quantum\App\Stages\Console\RunCommandStage;
use Symfony\Component\Console\Input\ArgvInput;
use Quantum\App\Stages\Boot\LoadAppConfigStage;
use Quantum\App\Stages\Boot\InitConsoleStage;
use Quantum\App\Stages\Boot\LoadHelpersStage;
use Symfony\Component\Console\Application;
use Quantum\App\Contracts\AppInterface;
use Quantum\App\Enums\ExitCode;
use Quantum\App\BootPipeline;
Expand All @@ -37,64 +36,43 @@
*/
class ConsoleAppAdapter implements AppInterface
{
private const UNKNOWN = 'UNKNOWN';

protected AppContext $context;

protected ArgvInput $input;

protected ConsoleOutput $output;

protected Application $application;

protected ?Throwable $bootFailure = null;

public function __construct(AppContext $context)
{
$this->context = $context;

$this->input = new ArgvInput();
$this->output = new ConsoleOutput();

$this->application = $this->createApplication(self::UNKNOWN, self::UNKNOWN);

try {
$pipeline = new BootPipeline([
new InitConsoleStage(),
new LoadHelpersStage(),
new LoadEnvironmentStage(),
new LoadAppConfigStage(),
new SetupErrorHandlerStage(),
new SetupConsoleApplicationStage(),
]);

$pipeline->run($this->context);

environment()->setMutable(true);

$this->application->setName(config()->get('app.name') ?? self::UNKNOWN);
$this->application->setVersion(config()->get('app.version') ?? self::UNKNOWN);
} catch (Throwable $exception) {
$this->bootFailure = $exception;
}
}

public function createApplication(string $name, string $version): Application
{
return new Application($name, $version);
}

/**
* @throws Exception|Throwable
*/
public function start(): ?int
{
$context = new ConsoleContext($this->context, $this->application, $this->input, $this->output, $this->bootFailure);

$pipeline = new ConsolePipeline([
new RegisterCoreCommandsStage(),
new RegisterAppCommandsStage(),
new RunCommandStage(),
]);

$context = new ConsoleContext($this->context, $this->bootFailure);

try {
$pipeline->run($context);
} catch (Throwable $exception) {
Expand Down
30 changes: 6 additions & 24 deletions src/App/Adapters/Console/ConsoleContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,55 +22,37 @@
*
* Carries the state of a single console run through the console stages.
* Unlike AppContext, which is immutable, the exit code is set by the stages.
* The Symfony application, input and output are read from the container of the AppContext.
*
* @package Quantum\App
*/
class ConsoleContext
{
private AppContext $appContext;

private Application $application;

private InputInterface $input;

private OutputInterface $output;

private ?Throwable $bootFailure;

private int $exitCode = ExitCode::SUCCESS;

public function __construct(
AppContext $appContext,
Application $application,
InputInterface $input,
OutputInterface $output,
?Throwable $bootFailure = null
) {
public function __construct(AppContext $appContext, ?Throwable $bootFailure = null)
{
$this->appContext = $appContext;
$this->application = $application;
$this->input = $input;
$this->output = $output;
$this->bootFailure = $bootFailure;
}

public function getAppContext(): AppContext
{
return $this->appContext;
}

public function getApplication(): Application
{
return $this->application;
return $this->appContext->getContainer()->get(Application::class);
}

public function getInput(): InputInterface
{
return $this->input;
return $this->appContext->getContainer()->get(InputInterface::class);
}

public function getOutput(): OutputInterface
{
return $this->output;
return $this->appContext->getContainer()->get(OutputInterface::class);
}

/**
Expand Down
47 changes: 47 additions & 0 deletions src/App/Stages/Boot/InitConsoleStage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

/**
* Quantum PHP Framework
* An open-source software development framework for PHP
* @link https://quantumphp.io
*/

namespace Quantum\App\Stages\Boot;

use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\ArgvInput;
use Quantum\App\Contracts\BootStageInterface;
use Symfony\Component\Console\Application;
use Quantum\App\AppContext;
use Quantum\Di\Di;

/**
* Class InitConsoleStage
*
* Runs before the helpers are loaded, so that the console application, input and output exist
* even when a helper file fails; for the same reason it declares no boot events.
*
* @package Quantum\App
*/
class InitConsoleStage implements BootStageInterface
{
public function process(AppContext $context): void
{
if (!Di::isRegistered(Application::class)) {
Di::register(Application::class);
}

if (!Di::isRegistered(OutputInterface::class)) {
Di::register(ConsoleOutput::class, OutputInterface::class);
}

// An instance, not a registration: the container would autowire the ?array $argv parameter as an empty array
if (!Di::isRegistered(InputInterface::class)) {
Di::set(InputInterface::class, new ArgvInput());
}
}
}
31 changes: 31 additions & 0 deletions src/App/Stages/Boot/SetupConsoleApplicationStage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

/**
* Quantum PHP Framework
* An open-source software development framework for PHP
* @link https://quantumphp.io
*/

namespace Quantum\App\Stages\Boot;

use Quantum\App\Contracts\BootStageInterface;
use Symfony\Component\Console\Application;
use Quantum\App\AppContext;
use Quantum\Di\Di;

/**
* Class SetupConsoleApplicationStage
* @package Quantum\App
*/
class SetupConsoleApplicationStage implements BootStageInterface
{
public function process(AppContext $context): void
{
$application = Di::get(Application::class);

$application->setName(config()->get('app.name') ?? 'UNKNOWN');
$application->setVersion(config()->get('app.version') ?? 'UNKNOWN');
}
}
20 changes: 20 additions & 0 deletions tests/Helpers/NonExitingApplication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Quantum\Tests\Helpers;

use Symfony\Component\Console\Application;

/**
* A Symfony Application for tests: its run() returns the exit code instead of calling exit(),
* and exceptions of a command reach the caller instead of being printed.
*/
class NonExitingApplication extends Application
{
public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
{
parent::__construct($name, $version);

$this->setAutoExit(false);
$this->setCatchExceptions(false);
}
}
Loading
Loading