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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ 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
- 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.
Expand All @@ -34,6 +37,9 @@ The format is based on Keep a Changelog.
- **BREAKING:** Reshaped Lang configuration so `lang.default` now selects the adapter, locale fallback moved to `lang.default_locale`, and the unused `lang.enabled` toggle was removed (#533)
- **BREAKING:** Removed `Lang::isEnabled()` from the public Lang API because it no longer affected runtime behavior (#533)

### Fixed
- `core:env` now looks for the existing `.env` in the application base directory, so running it from another working directory asks for the confirmation instead of silently overwriting the file (#575)

## [3.0.3] - 2026-07-10

### Changed
Expand Down
53 changes: 29 additions & 24 deletions src/App/Adapters/Console/ConsoleAppAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@
use Quantum\App\Stages\Console\RegisterCoreCommandsStage;
use Quantum\App\Stages\Console\RegisterAppCommandsStage;
use Symfony\Component\Console\Output\ConsoleOutput;
use Quantum\App\Stages\Console\RunCommandStage;
use Symfony\Component\Console\Input\ArgvInput;
use Quantum\App\Stages\Boot\SetupErrorHandlerStage;
use Quantum\App\Stages\Boot\LoadEnvironmentStage;
use Symfony\Component\Console\Application;
use Quantum\App\Stages\Console\RunCommandStage;
use Symfony\Component\Console\Input\ArgvInput;
use Quantum\App\Stages\Boot\LoadAppConfigStage;
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;
use Quantum\App\AppContext;
use Throwable;
use Exception;

if (!defined('DS')) {
Expand All @@ -36,6 +37,8 @@
*/
class ConsoleAppAdapter implements AppInterface
{
private const UNKNOWN = 'UNKNOWN';

protected AppContext $context;

protected ArgvInput $input;
Expand All @@ -44,36 +47,34 @@ class ConsoleAppAdapter implements AppInterface

protected Application $application;

protected ?Throwable $bootFailure = null;

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

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

$commandName = $this->input->getFirstArgument();

$stages = [
new LoadHelpersStage(),
];
$this->application = $this->createApplication(self::UNKNOWN, self::UNKNOWN);
Comment thread
armanist marked this conversation as resolved.

if ($commandName !== 'core:env') {
$stages[] = new LoadEnvironmentStage();
$stages[] = new LoadAppConfigStage();
$stages[] = new SetupErrorHandlerStage();
}
try {
$pipeline = new BootPipeline([
new LoadHelpersStage(),
new LoadEnvironmentStage(),
new LoadAppConfigStage(),
new SetupErrorHandlerStage(),
]);

$pipeline = new BootPipeline($stages);
$pipeline->run($this->context);
$pipeline->run($this->context);

if ($commandName !== 'core:env') {
environment()->setMutable(true);
}

$this->application = $this->createApplication(
config()->get('app.name', 'UNKNOWN'),
config()->get('app.version', 'UNKNOWN')
);
$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
Expand All @@ -82,19 +83,23 @@ public function createApplication(string $name, string $version): Application
}

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

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

$pipeline->run($context);
try {
$pipeline->run($context);
} catch (Throwable $exception) {
throw $this->bootFailure ?? $exception;
}

return $context->getExitCode() ?: ExitCode::SUCCESS;
}
Expand Down
15 changes: 14 additions & 1 deletion src/App/Adapters/Console/ConsoleContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Symfony\Component\Console\Application;
use Quantum\App\Enums\ExitCode;
use Quantum\App\AppContext;
use Throwable;

/**
* Class ConsoleContext
Expand All @@ -34,18 +35,22 @@ class ConsoleContext

private OutputInterface $output;

private ?Throwable $bootFailure;

private int $exitCode = ExitCode::SUCCESS;

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

public function getAppContext(): AppContext
Expand All @@ -68,6 +73,14 @@ public function getOutput(): OutputInterface
return $this->output;
}

/**
* The failure of the boot that the console run continued after, if there was one
*/
public function getBootFailure(): ?Throwable
{
return $this->bootFailure;
}

public function getExitCode(): int
{
return $this->exitCode;
Expand Down
13 changes: 11 additions & 2 deletions src/App/Stages/Console/RunCommandStage.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,22 @@

use Quantum\App\Contracts\ConsoleStageInterface;
use Quantum\App\Adapters\Console\ConsoleContext;
use Quantum\Console\CliCommand;
use Exception;
use Throwable;

/**
* Class RunCommandStage
*
* Runs the requested command; a missing or unknown command is rejected before anything runs.
* Runs the requested command; a missing or unknown command is rejected before anything runs,
* and so is a command that needs the environment after the boot failed.
*
* @package Quantum\App
*/
class RunCommandStage implements ConsoleStageInterface
{
/**
* @throws Exception
* @throws Exception|Throwable
*/
public function process(ConsoleContext $context): void
{
Expand All @@ -34,6 +37,12 @@ public function process(ConsoleContext $context): void
throw new Exception("Command `$commandName` is not defined");
}

$command = $context->getApplication()->get($commandName);

if ($context->getBootFailure() !== null && $command instanceof CliCommand && $command->requiresEnvironment()) {
throw $context->getBootFailure();
}

$context->setExitCode(
$context->getApplication()->run($context->getInput(), $context->getOutput())
);
Expand Down
13 changes: 13 additions & 0 deletions src/Console/CliCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ abstract class CliCommand extends Command implements CommandInterface
*/
protected array $options = [];

/**
* Whether the command needs the environment file to run
*/
protected bool $requiresEnvironment = true;

protected ?InputInterface $input = null;

protected ?OutputInterface $output = null;
Expand All @@ -74,6 +79,14 @@ public function __construct()
}
}

/**
* Tells whether the command needs the environment file to run
*/
public function requiresEnvironment(): bool
{
return $this->requiresEnvironment;
}

/**
* Returns the argument value for a given argument name.
* @return mixed|string
Expand Down
7 changes: 6 additions & 1 deletion src/Console/Commands/EnvCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ class EnvCommand extends CliCommand
['yes', 'y', 'none', 'Acceptance of the confirmation'],
];

/**
* The command creates the environment file, so it must run when there is none
*/
protected bool $requiresEnvironment = false;

/**
* Executes the command and creates new .env file
* @throws BaseException
Expand All @@ -61,7 +66,7 @@ public function exec(): void
$this->error('.env.example file not found');
}

if ($fs->exists('.env') && !$this->getOption('yes') && !$this->confirm('The operation will overwrite values of the existing .env and will create new one from .env.example. Continue?')) {
if ($fs->exists(App::getBaseDir() . DS . '.env') && !$this->getOption('yes') && !$this->confirm('The operation will overwrite values of the existing .env and will create new one from .env.example. Continue?')) {
$this->info('Operation was canceled!');
return;
}
Expand Down
Loading
Loading