diff --git a/CHANGELOG.md b/CHANGELOG.md index 0030cbdc..7cbd0b2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +* Add support for configuration flags in `Context` * Allow to specify directory where to output repacked phar * Do not ask confirmation when explicitly using `castor init` command * Allow to pass a callback when using the `run_php` function (similar to the `run` function) diff --git a/doc/getting-started/context.md b/doc/getting-started/context.md index 342c86a5..387d65d6 100644 --- a/doc/getting-started/context.md +++ b/doc/getting-started/context.md @@ -358,6 +358,117 @@ if you run castor [in verbose mode](../going-further/interacting-with-castor/log Additionally, if this command fails when Castor is not in verbose mode, it will ask you if you want to retry the command with the verbose arguments. +### Configuration flags + +Castor allows you to configure optional features through configuration flags. +These flags control behavior changes that may be introduced in future versions +of Castor, allowing you to opt-in or opt-out of specific features. + +You can enable or disable flags when creating a context by using the `config` +parameter: + +```php +use Castor\Attribute\AsContext; +use Castor\Config; +use Castor\ConfigFlag; +use Castor\Context; + +#[AsContext(name: 'my_context')] +function create_context(): Context +{ + return new Context( + config: (new Config()) + ->withEnabled(ConfigFlag::ContextAwareFilesystem) + ); +} +``` + +You can also explicitly disable flags: + +```php +use Castor\Attribute\AsContext; +use Castor\Config; +use Castor\ConfigFlag; +use Castor\Context; + +#[AsContext(name: 'my_context')] +function create_context(): Context +{ + return new Context( + config: (new Config()) + ->withDisabled(ConfigFlag::ContextAwareFilesystem) + ); +} +``` + +Multiple flags can be configured at once: + +```php +use Castor\Config; +use Castor\ConfigFlag; + +$config = (new Config()) + ->withEnabled(ConfigFlag::ContextAwareFilesystem) + ->withDisabled(ConfigFlag::SomeOtherFlag); +``` + +#### Checking flag status + +You can check whether a flag is enabled in your tasks: + +```php +use Castor\Attribute\AsTask; + +use function Castor\context; +use function Castor\io; + +#[AsTask()] +function check_flags(): void +{ + $context = context(); + + if ($context->config->isEnabled(ConfigFlag::ContextAwareFilesystem)) { + io()->writeln('ContextAwareFilesystem is enabled.'); + } +} +``` + +#### Available flags + +- `ConfigFlag::ContextAwareFilesystem`: When enabled, filesystem operations + will be automatically aware of the context's working directory. + +#### Deprecation warnings + +Configuration flags support a deprecation system to help you prepare for future +versions of Castor. When a flag is not explicitly configured, or when it's +configured to a value that will change in a future version, Castor will emit +a deprecation warning. + +These warnings help you understand: + +- Which flags are not configured in your project +- What the current default value is +- What the future default value will be +- In which version the default will change + +To avoid these warnings, explicitly configure the flags you use: + +```php +use Castor\Config; +use Castor\ConfigFlag; +use Castor\Context; + +return new Context( + config: (new Config()) + ->withEnabled(ConfigFlag::ContextAwareFilesystem) +); +``` + +> [!TIP] +> Check the [configuration flags example](https://github.com/jolicode/castor/blob/main/examples/basic/context/config.php) +> for a complete working example. + ## Advanced usage See [this documentation](../going-further/interacting-with-castor/advanced-context.md) for more usage about diff --git a/examples/basic/context/config.php b/examples/basic/context/config.php new file mode 100644 index 00000000..3c231a1e --- /dev/null +++ b/examples/basic/context/config.php @@ -0,0 +1,52 @@ +withEnabled(ConfigFlag::ContextAwareFilesystem) + // You can also disable flags explicitly + // ->withDisabled(ConfigFlag::ContextAwareFilesystem) + ); +} + +#[AsTask] +function use_context_with_config_flags(): void +{ + $context = context('context_with_config_flags'); + + // You can get all flags and their values + $flags = $context->config->getFlags(); + foreach ($flags as $flagName => $flagValue) { + io()->writeln("Flag: {$flagName}, Value: " . var_export($flagValue, true)); + } + + // Or check specific flag if necessary for your logic + if ($context->config->isEnabled(ConfigFlag::ContextAwareFilesystem)) { + io()->writeln('ContextAwareFilesystem is enabled.'); + } else { + io()->writeln('ContextAwareFilesystem is disabled.'); + } + + io()->newLine(); + io()->writeln('Disabling ContextAwareFilesystem flag...'); + $context->config->withDisabled(ConfigFlag::ContextAwareFilesystem); + + if ($context->config->isEnabled(ConfigFlag::ContextAwareFilesystem)) { + io()->writeln('ContextAwareFilesystem is enabled.'); + } else { + io()->writeln('ContextAwareFilesystem is disabled.'); + } +} diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 00000000..a0ec0506 --- /dev/null +++ b/src/Config.php @@ -0,0 +1,109 @@ + */ + private array $flags = []; + + /** @var array */ + private static array $warnedFlags = []; + + public function isEnabled(ConfigFlag $flag): bool + { + $value = $this->get($flag); + + $futureDefault = !$flag->defaultValueWhenNull(); + + if (null === $value || $value !== $futureDefault) { + $this->triggerDeprecationWarning($flag, $value); + } + + if (\is_bool($value)) { + return $value; + } + + return $flag->defaultValueWhenNull(); + } + + public function withEnabled(ConfigFlag ...$flags): self + { + foreach ($flags as $flag) { + $this->set($flag, true); + } + + return $this; + } + + public function withDisabled(ConfigFlag ...$flags): self + { + foreach ($flags as $flag) { + $this->set($flag, false); + } + + return $this; + } + + /** + * @return array + */ + public function getFlags(): array + { + return $this->flags; + } + + private function get(ConfigFlag $flag): ?bool + { + return $this->flags[$flag->name] ?? null; + } + + private function set(ConfigFlag $flag, ?bool $value): void + { + $this->flags[$flag->name] = $value; + } + + private function triggerDeprecationWarning(ConfigFlag $flag, ?bool $value): void + { + // Only warn once per flag per execution + if (isset(self::$warnedFlags[$flag->name])) { + return; + } + + self::$warnedFlags[$flag->name] = true; + + $currentDefault = $flag->defaultValueWhenNull() ? 'true' : 'false'; + $futureDefault = !$flag->defaultValueWhenNull() ? 'true' : 'false'; + $futureVersion = $flag->willBeDefaultInVersion(); + + if (null === $value) { + $message = \sprintf( + 'Configuration flag "%s" is not set and defaults to "%s". It will default to "%s" in version %s. ', + $flag->name, + $currentDefault, + $futureDefault, + $futureVersion, + ); + } else { + $currentValue = $value ? 'true' : 'false'; + $message = \sprintf( + 'Configuration flag "%s" is explicitly set to "%s", but will default to "%s" in version %s. ', + $flag->name, + $currentValue, + $futureDefault, + $futureVersion, + ); + } + + trigger_deprecation('castor/castor', $futureVersion, $message); + } +} diff --git a/src/ConfigFlag.php b/src/ConfigFlag.php new file mode 100644 index 00000000..70da8531 --- /dev/null +++ b/src/ConfigFlag.php @@ -0,0 +1,29 @@ + 'Context-aware filesystem with automatic path resolution', + }; + } + + public function willBeDefaultInVersion(): string + { + return match ($this) { + self::ContextAwareFilesystem => '2.0', + }; + } + + public function defaultValueWhenNull(): bool + { + return match ($this) { + self::ContextAwareFilesystem => false, + }; + } +} diff --git a/src/Context.php b/src/Context.php index c618a8ba..efac6a2e 100644 --- a/src/Context.php +++ b/src/Context.php @@ -35,6 +35,7 @@ public function __construct( public readonly string $name = '', public readonly string $notificationTitle = '', public readonly array $verboseArguments = [], + public readonly Config $config = new Config(), ) { $this->workingDirectory = $workingDirectory ?? PathHelper::getRoot(false); } @@ -55,6 +56,11 @@ public function getDebugInfo(): array 'notify' => $this->notify, 'verbosityLevel' => $this->verbosityLevel, 'notificationTitle' => $this->notificationTitle, + 'config' => array_map(fn (ConfigFlag $flag) => [ + 'name' => $flag->name, + 'default' => $flag->defaultValueWhenNull(), + 'is_enabled' => $this->config->isEnabled($flag), + ], ConfigFlag::cases()), ]; } @@ -91,6 +97,7 @@ public function withData(array $data, bool $keepExisting = true, bool $recursive $this->name, $this->notificationTitle, $this->verboseArguments, + $this->config, ); } @@ -111,6 +118,7 @@ public function withEnvironment(array $environment, bool $keepExisting = true): $this->name, $this->notificationTitle, $this->verboseArguments, + $this->config, ); } @@ -130,6 +138,7 @@ public function withWorkingDirectory(string $workingDirectory): self $this->name, $this->notificationTitle, $this->verboseArguments, + $this->config, ); } @@ -149,6 +158,7 @@ public function withTty(bool $tty = true): self $this->name, $this->notificationTitle, $this->verboseArguments, + $this->config, ); } @@ -168,6 +178,7 @@ public function withPty(bool $pty = true): self $this->name, $this->notificationTitle, $this->verboseArguments, + $this->config, ); } @@ -187,6 +198,7 @@ public function withTimeout(?float $timeout): self $this->name, $this->notificationTitle, $this->verboseArguments, + $this->config, ); } @@ -206,6 +218,7 @@ public function withQuiet(bool $quiet = true): self $this->name, $this->notificationTitle, $this->verboseArguments, + $this->config, ); } @@ -225,6 +238,7 @@ public function withAllowFailure(bool $allowFailure = true): self $this->name, $this->notificationTitle, $this->verboseArguments, + $this->config, ); } @@ -244,6 +258,7 @@ public function withNotify(?bool $notify = true): self $this->name, $this->notificationTitle, $this->verboseArguments, + $this->config, ); } @@ -263,6 +278,7 @@ public function withVerbosityLevel(VerbosityLevel $verbosityLevel): self $this->name, $this->notificationTitle, $this->verboseArguments, + $this->config, ); } @@ -286,6 +302,7 @@ public function withName(string $name): self $name, $this->notificationTitle, $this->verboseArguments, + $this->config, ); } @@ -305,6 +322,7 @@ public function withNotificationTitle(string $notificationTitle): self $this->name, $notificationTitle, $this->verboseArguments, + $this->config, ); } @@ -325,6 +343,27 @@ public function withVerboseArguments(array $arguments = []): self $this->name, $this->notificationTitle, $arguments, + $this->config, + ); + } + + public function withConfig(Config $config): self + { + return new self( + $this->data, + $this->environment, + $this->workingDirectory, + $this->tty, + $this->pty, + $this->timeout, + $this->quiet, + $this->allowFailure, + $this->notify, + $this->verbosityLevel, + $this->name, + $this->notificationTitle, + $this->verboseArguments, + $config, ); } diff --git a/tests/Generated/ContextUseContextWithConfigFlagsTest.php b/tests/Generated/ContextUseContextWithConfigFlagsTest.php new file mode 100644 index 00000000..bd72c1be --- /dev/null +++ b/tests/Generated/ContextUseContextWithConfigFlagsTest.php @@ -0,0 +1,22 @@ +runTask(['context:use-context-with-config-flags']); + + if (0 !== $process->getExitCode()) { + throw new ProcessFailedException($process); + } + + $this->assertStringEqualsFile(__FILE__ . '.output.txt', $process->getOutput()); + $this->assertSame('', $process->getErrorOutput()); + } +} diff --git a/tests/Generated/ContextUseContextWithConfigFlagsTest.php.output.txt b/tests/Generated/ContextUseContextWithConfigFlagsTest.php.output.txt new file mode 100644 index 00000000..4fe70dc1 --- /dev/null +++ b/tests/Generated/ContextUseContextWithConfigFlagsTest.php.output.txt @@ -0,0 +1,6 @@ +Flag: ContextAwareFilesystem, Value: true +ContextAwareFilesystem is enabled. + +Disabling ContextAwareFilesystem flag... +hh:mm:ss WARNING [castor] User Deprecated: Since castor/castor 2.0: Configuration flag "ContextAwareFilesystem" is explicitly set to "false", but will default to "true" in version 2.0. ["exception" => ErrorException { …}] +ContextAwareFilesystem is disabled. diff --git a/tests/Generated/ListTest.php.output.txt b/tests/Generated/ListTest.php.output.txt index 25ff75d7..e53565ab 100644 --- a/tests/Generated/ListTest.php.output.txt +++ b/tests/Generated/ListTest.php.output.txt @@ -45,6 +45,7 @@ configuration:renamed Task that was renamed context:context Displays information about the context context:display-context-info-with Displays information about the context, using a specific context context:display-dynamic-context-info Displays information about the context +context:use-context-with-config-flags crypto:decrypt Decrypt content with a password crypto:decrypt-file Decrypt file with a password crypto:encrypt Encrypt content with a password