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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
111 changes: 111 additions & 0 deletions doc/getting-started/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +443 to +446
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.

I don't really like this behavior. If I configured the feature A with true and castor will change it to false in next version, I don't wan't to be warned anymore.

That being said, I don't know what to think about this PR yet. The implementation is quite simple, It's cool.

But I'm not sure we need some tooling to handle a use case we met only once from the beginning of castor.

I think you should not invest too much time on this PR, we need to think / discuss about it before.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I let you discuss it internally, I keep them open, and tell me what to do after or close the PR if we do not want to implement this feature


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:
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.

According to the previous paragraph, it does not


```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
Expand Down
52 changes: 52 additions & 0 deletions examples/basic/context/config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace context;

use Castor\Attribute\AsContext;
use Castor\Attribute\AsTask;
use Castor\Config;
use Castor\ConfigFlag;
use Castor\Context;

use function Castor\context;
use function Castor\io;

#[AsContext(name: 'context_with_config_flags')]
function create_context_with_config_flags(): Context
{
return new Context(
config: (new Config())
->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.');
}
}
109 changes: 109 additions & 0 deletions src/Config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

namespace Castor;

use Symfony\Component\DependencyInjection\Attribute\Exclude;

/**
* Each flag supports three states:
* - true: explicitly enabled
* - false: explicitly disabled
* - null: not configured (will show deprecation warning and use default)
*/
#[Exclude]
class Config
{
/** @var array<string, bool|null> */
private array $flags = [];

/** @var array<string, bool> */
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<string, bool|null>
*/
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);
}
}
29 changes: 29 additions & 0 deletions src/ConfigFlag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Castor;

enum ConfigFlag
{
case ContextAwareFilesystem;

public function description(): string
{
return match ($this) {
self::ContextAwareFilesystem => '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,
};
}
}
Loading