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
22 changes: 17 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `EnvVarProvider` and `RedisProvider` now return a `PARSE_ERROR` (and the default value) for raw values that do not match the requested type, instead of silently casting them (`"abc"` as integer used to resolve to `0`, `"banana"` as boolean to `false`).
- `InMemoryProvider` returns a `TYPE_MISMATCH` instead of casting for integers other than `0`/`1` requested as boolean (e.g. `2` or `-1`, previously `true`), non-string values requested as string (e.g. `42`, previously `"42"`), and floats requested as integer (e.g. `1.5`, previously `1`).
- `ResolutionDetailsTrait::toBool()` is replaced by `parseBool()`, `parseInt()`, `parseFloat()`, and `parseObject()`.
- Evaluation context providers now run on the first flag evaluation of the request instead of on `kernel.request`. A request that evaluates no flag no longer reads the security token, so a `lazy` firewall stays lazy and the response stays HTTP-cacheable. `EvaluationContextContributedEvent` is dispatched at that time.
- An exception thrown by an evaluation context provider, or by a listener of `EvaluationContextContributedEvent`, is now logged instead of failing the request (the failing provider is skipped).
- Provider services (`provider`, `providers`) must now declare their class, as Symfony already requires for services built by a factory. A provider created by a factory, or inheriting its class from a `parent` under an id that is not a class name, now fails at compile time without an explicit `class` option instead of skipping validation.
- `evaluation_context.user_provider` now defaults to `false` instead of `auto`: the user identifier is only sent to the flag provider when explicitly enabled. Since `auto` always resolved to `false` in 0.3, the effective default does not change.

### Fixed

- `evaluation_context.user_provider` now works: `auto` always resolved to `false` and `true` always failed, even with SecurityBundle enabled. With SecurityBundle enabled but not configured (Symfony 6.4), the user provider now does nothing instead of breaking the container compilation.
- `feature_flag.on_disabled: auto` now picks `access_denied` only when SecurityBundle is enabled. With `symfony/security-core` installed but no SecurityBundle (e.g. pulled by `symfony/security-csrf`), a disabled feature gate returned a 500 instead of a 403. Set `on_disabled: access_denied` explicitly to keep the previous exception.
- A provider service whose class does not exist now fails with an explicit "cannot be found" message instead of "must implement Provider", and a provider class whose parent class or interface is missing reports that missing class.
- Providers now receive the application logger when MonologBundle is not installed; previously they got none. A logger already set on a provider service (e.g. a dedicated Monolog channel) is no longer overridden.
- `flags` and `providers`: keys are now kept as declared. Dashes were converted to underscores (a flag declared as `new-checkout` could only be evaluated as `new_checkout`), and an object flag holding a `name` key was renamed after that value. The undocumented list form `flags: [{name: ..., value: ...}]` is no longer supported.
- `RedisProvider` now logs Redis client failures at `error` level. Previously, an unavailable Redis silently resolved every flag to its default value, with nothing in the logs.
- The profiler panel no longer breaks on object flags or on array and date context attributes: these values are now shown with the VarDumper, like in other Symfony panels.

### Upgrade notes

- Run `composer update open-feature/sdk` if your lock file pins a version below 2.3.0.
- Code calling `OpenFeatureAPI::getInstance()` directly now gets an instance distinct from the bundle's `API` service (different provider, hooks, and evaluation context). Inject the `API` or `Client` service instead.
- **Check your raw flag values.** `EnvVarProvider` and `RedisProvider` no longer cast unparsable values: a boolean flag set to anything other than `true`/`false`/`1`/`0`/`yes`/`no`/`on`/`off`/empty (e.g. `FEATURE_X=enabled`, previously `false`) or a numeric flag with a non-numeric value (for integers, decimal or exponent notation such as `10.0` or `1e3` too; leading zeros such as `08` are accepted) now resolves to the default value with a `PARSE_ERROR`. `InMemoryProvider` flags declared with a mismatching type (e.g. `max_items: 1.5` read as integer, `label: 42` read as string) now resolve to the default value with a `TYPE_MISMATCH`, like with typed providers such as flagd. In Twig, `{{ feature_value('max_items') }}` without a default reads the flag as a string and now renders `''`: pass a typed default (`feature_value('max_items', 10)`). These errors are not logged by the SDK: check the `open_feature` profiler panel in dev (error column), or register a hook that logs `ResolutionDetails::getError()` in `after()`.
- **Provider services created by a factory, or defined through `parent` under an id that is not a class name,** must set the `class` option (e.g. `class: App\FeatureFlag\MyProvider` next to `factory:`; `OpenFeature\interfaces\provider\Provider` is accepted when the concrete class is unknown), otherwise the container fails to compile with `Class "" used for OpenFeature provider service "..." cannot be found`.
- Custom providers using `ResolutionDetailsTrait::toBool()` must switch to `parseBool($flagKey, $raw, $defaultValue)`, which returns a `ResolutionDetails` instead of a `bool`.
See [UPGRADE.md][upgrade-0.4] for details and examples.

- Update `open-feature/sdk` to 2.3 (`composer update open-feature/sdk`).
- Inject the `API` or `Client` service instead of calling `OpenFeatureAPI::getInstance()`.
- Check the raw values of `EnvVarProvider` and `RedisProvider` flags and the types of `InMemoryProvider` flags: a mismatch now resolves to the default value with an error. In Twig, pass a typed default to `feature_value()`.
- Set the `class` option on provider services created by a factory or defined through `parent`.
- `user_provider` now defaults to `false`, as it effectively was in 0.3. An explicit `auto` now takes effect with SecurityBundle, and `true` no longer fails.
- Move logic that must run on every request out of evaluation context providers, which now run on the first flag evaluation. `API::getEvaluationContext()` no longer returns a `MutableEvaluationContext`.
- Replace `ResolutionDetailsTrait::toBool()` with `parseBool()`.

## [0.3.0] - 2026-06-15

Expand Down Expand Up @@ -74,3 +84,5 @@ Initial release.
[0.2.0]: https://github.com/aubes/openfeature-bundle/compare/v0.1.1...v0.2.0
[0.1.1]: https://github.com/aubes/openfeature-bundle/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/aubes/openfeature-bundle/releases/tag/v0.1.0

[upgrade-0.4]: UPGRADE.md#upgrading-from-03-to-04
135 changes: 135 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Upgrade guide

## Upgrading from 0.3 to 0.4

0.4 is a minor release with breaking changes, as allowed for 0.x versions by [Semantic Versioning][semver-4]. This guide lists what you may have to change, grouped by how you use the bundle. The [changelog][changelog] has the complete list of changes.

### Everyone

**The bundle requires `open-feature/sdk` 2.3.** Update it if your lock file pins an older version:

```bash
composer update open-feature/sdk
```

**The `API` service is an isolated instance.** It no longer shares its provider, hooks, and evaluation context with the `OpenFeatureAPI::getInstance()` singleton. Code calling `getInstance()` directly now gets a different, unconfigured instance: inject the `API` or `Client` service instead.

```php
// Before
$client = OpenFeatureAPI::getInstance()->getClient();

// After
public function __construct(private readonly Client $client) {}
```

### If you configure the bundle

**`evaluation_context.user_provider` defaults to `false`.** The previous default, `auto`, always resolved to `false` because of a bug, so nothing changes by default. If you set `auto` or `true` explicitly, it now works: with SecurityBundle enabled, the identifier of the authenticated user becomes the targeting key, and it is sent to your flag provider. To enable it:

```yaml
open_feature:
evaluation_context:
user_provider: auto
```

If the user identifier is personal data (an email address, for example), read the note in [Evaluation context][evaluation-context] first.

**`feature_flag.on_disabled: auto` depends on SecurityBundle.** It picks `access_denied` only when SecurityBundle is enabled, and `http_exception` otherwise. Only an application with `symfony/security-core` installed but no SecurityBundle is affected: a closed `#[FeatureGate]` now returns a 403 instead of a 500. To keep throwing an `AccessDeniedException`, set it explicitly:

```yaml
open_feature:
feature_flag:
on_disabled: access_denied
```

**Keys under `flags` and `providers` are kept as declared.** Dashes were converted to underscores, so a flag declared as `new-checkout` could only be evaluated as `new_checkout`. Evaluate it with its declared key, or rename it. The same applies to provider names referenced by `strategy.fallback`.

The undocumented list form of `flags` is no longer supported. Use a map:

```yaml
# Before
open_feature:
flags:
- { name: dark_mode, value: true }

# After
open_feature:
flags:
dark_mode: true
```

**Provider services must declare their class.** A provider created by a factory, or defined through `parent` under an id that is not a class name, needs an explicit `class` option. Without it, the container fails to compile with `Class "" used for OpenFeature provider service "..." cannot be found`.

```yaml
services:
app.feature_provider:
class: App\FeatureFlag\MyProvider
factory: ['@App\FeatureFlag\ProviderFactory', 'create']
```

When the concrete class is unknown, `class: OpenFeature\interfaces\provider\Provider` is accepted.

### If you use the built-in providers

**`EnvVarProvider` and `RedisProvider` no longer cast raw values.** A value that does not match the requested type now resolves to the default value with a `PARSE_ERROR`, instead of being silently cast. Check your environment variables and Redis keys against the accepted values listed in [EnvVar provider][env-var] and [Redis provider][redis]:

```bash
# Before: resolved to false and 10
FEATURE_NEW_CHECKOUT=enabled
FEATURE_MAX_ITEMS=10.0

# After
FEATURE_NEW_CHECKOUT=true
FEATURE_MAX_ITEMS=10
```

**`InMemoryProvider` checks the type of each flag.** A flag read with a method that does not match its YAML type now resolves to the default value with a `TYPE_MISMATCH`, as with typed providers such as flagd. The only conversions left are `0`/`1` read as booleans and integers read as floats. See the table in [InMemory provider][in-memory]:

```yaml
open_feature:
flags:
max_items: 10 # was 1.5, read as an integer
label: '42' # was 42, read as a string
```

**In Twig, pass a default of the flag's type to `feature_value()`.** Without a default, the flag is read as a string, so a boolean or integer flag now renders `''`:

```twig
{# Before #}
{{ feature_value('max_items') }}

{# After #}
{{ feature_value('max_items', 10) }}
```

These errors are not logged by the SDK. In dev, the error column of the `open_feature` profiler panel shows them.

**`RedisProvider` logs client failures** at `error` level, once per flag evaluation while Redis is unavailable. See [Redis provider][redis] to limit the volume.

### If you wrote code around the bundle

**Evaluation context providers run on the first flag evaluation**, not on `kernel.request`. A request that evaluates no flag never runs them. Logic that must run at the start of every request (side effects, timing) belongs in its own `kernel.request` listener. Two related changes:

- An exception thrown by a context provider, or by a listener of `EvaluationContextContributedEvent`, is now logged instead of failing the request.
- A flag evaluated from inside a context provider gets an empty context.

**The API-level evaluation context is lazy.** `API::getEvaluationContext()` now returns an internal lazy context: an `instanceof MutableEvaluationContext` check no longer matches, and reading its targeting key or attributes runs the context providers. To add data to the context, implement `EvaluationContextProviderInterface`, or pass an invocation context to the evaluation.

**`ResolutionDetailsTrait::toBool()` is removed.** Custom providers using the trait switch to `parseBool()`, which returns the `ResolutionDetails` directly. `parseInt()`, `parseFloat()`, and `parseObject()` follow the same pattern:

```php
// Before
return $this->found($this->toBool($raw));

// After
return $this->parseBool($flagKey, $raw, $defaultValue);
```

**Provider validation messages changed.** If your tests assert them, a class that does not exist now reports `Class "..." used for OpenFeature provider service "..." cannot be found`, and a class that is not a provider reports `OpenFeature provider service "..." (class "...") must implement interface "OpenFeature\interfaces\provider\Provider"`.

[semver-4]: https://semver.org/#spec-item-4
[changelog]: CHANGELOG.md
[evaluation-context]: docs/features/evaluation-context.md
[env-var]: docs/providers/env-var.md
[redis]: docs/providers/redis.md
[in-memory]: docs/providers/in-memory.md
36 changes: 18 additions & 18 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ Full configuration tree for `open_feature`:
open_feature:

# Service ID of the OpenFeature provider
# Default: Aubes\OpenFeatureBundle\Provider\InMemoryProvider
# Default: none (the InMemoryProvider is used when neither "provider" nor "providers" is set)
# Mutually exclusive with "providers"
provider: Aubes\OpenFeatureBundle\Provider\InMemoryProvider
provider: ~

# Multiple providers combined through the SDK MultiProvider
# Keys are provider names, values are service IDs
# Evaluation follows declaration order
# Mutually exclusive with "provider"
providers:
remote: App\OpenFeature\MyProvider
local: Aubes\OpenFeatureBundle\Provider\InMemoryProvider
providers: {}
# remote: App\OpenFeature\MyProvider
# local: Aubes\OpenFeatureBundle\Provider\InMemoryProvider

# Evaluation strategy for the MultiProvider (only when using "providers")
# Shorthand: strategy: first_match
Expand All @@ -35,15 +35,15 @@ open_feature:

# EvaluationContext settings
evaluation_context:
# Populate targeting key from the authenticated Symfony user
# auto: enabled if symfony/security-core is installed
# true: always enabled (requires symfony/security-core)
# false: disabled
user_provider: auto # auto | true | false
# Populate targeting key from the authenticated Symfony user (sent to the flag provider)
# false: disabled (default)
# auto: enabled if SecurityBundle is enabled
# true: always enabled (requires SecurityBundle)
user_provider: false # false | auto | true

# Exception behavior for #[FeatureGate]
feature_flag:
# auto: AccessDeniedException if security-core is available, HttpException otherwise
# auto: AccessDeniedException if SecurityBundle is enabled, HttpException otherwise
# access_denied: always throw AccessDeniedException
# http_exception: always throw HttpException
on_disabled: auto # auto | access_denied | http_exception
Expand All @@ -52,11 +52,11 @@ open_feature:
status_code: 403

# Redis provider settings (only when using RedisProvider)
redis:
# Service implementing RedisClientInterface
client: ~
# Key prefix for flag lookup
prefix: 'feature:'
# redis:
# # Service implementing RedisClientInterface (required)
# client: App\OpenFeature\MyRedisClient
# # Key prefix for flag lookup
# prefix: 'feature:'
```

## Provider
Expand All @@ -72,7 +72,7 @@ See [Providers](providers/index.md) for available options.

## Multiple providers

Declare several providers under `providers` to combine them through the SDK `MultiProvider` (requires `open-feature/sdk` >= 2.2). Each key is a provider name, each value a service ID. Providers are evaluated in declaration order:
Declare several providers under `providers` to combine them through the SDK `MultiProvider`. Each key is a provider name, each value a service ID. Providers are evaluated in declaration order:

```yaml
open_feature:
Expand Down Expand Up @@ -126,6 +126,6 @@ The `feature_flag.on_disabled` setting controls what happens when a `#[FeatureGa

| Value | Exception type | When to use |
|---|---|---|
| `auto` (default) | `AccessDeniedException` if `symfony/security-core` is installed, `HttpException` otherwise | Most apps |
| `auto` (default) | `AccessDeniedException` if SecurityBundle is enabled, `HttpException` otherwise | Most apps |
| `access_denied` | `AccessDeniedException` | When you have a security error handler |
| `http_exception` | `HttpException` with configurable status code | APIs, custom error pages |
Loading
Loading