PHPackages                             ambrion/feature-flags-core - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. ambrion/feature-flags-core

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

ambrion/feature-flags-core
==========================

Framework-agnostic Feature Flags library following DDD and Clean Architecture principles

v1.4.0-alpha(3w ago)01131MITPHPPHP ^8.3

Since May 6Pushed 3w agoCompare

[ Source](https://github.com/Ambrion/feature-flags-core)[ Packagist](https://packagist.org/packages/ambrion/feature-flags-core)[ RSS](/packages/ambrion-feature-flags-core/feed)WikiDiscussions master Synced 1w ago

READMEChangelogDependencies (3)Versions (5)Used By (1)

🚩 Feature Flags Core
====================

[](#-feature-flags-core)

Framework-agnostic Feature Flags engine built with **DDD** and **Clean Architecture** principles.
Designed to be portable, testable, and easily integrated into any PHP project.

[![PHP Version](https://camo.githubusercontent.com/5c8ce4571ddf4b6b8ca847e0c4c079de98fc6460eb7eae9c81ca63319c21f546/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253345253344382e332d626c75652e737667)](https://php.net)[![License](https://camo.githubusercontent.com/8bb50fd2278f18fc326bf71f6e88ca8f884f72f179d3e555e20ed30157190d0d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e2e737667)](LICENSE)![Tests](https://camo.githubusercontent.com/25cf0bd5b2772f58b363912f33b0656b757c6cd70faebe19d7a4b2b664aad131/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f636f7665726167652d37342532352d79656c6c6f77677265656e2e737667)[![Pest](https://camo.githubusercontent.com/37491669625d60541c710ad4ac89d11aba01557457633ec16bcc6195cd486a5d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7465737465645f776974682d506573742d6363333336362e737667)](https://pestphp.com)

✨ Features
----------

[](#-features)

- ✅ **Framework-agnostic**: No dependencies on CMS, framework, or global state.
- ✅ **Domain-Driven Design**: Clean separation of Entities, Value Objects, Specifications, and Services.
- ✅ **Unified Evaluation API**: Single `evaluate()` method returns `EvaluationResult` with `enabled`, `variant`, `weight`, `matchedRule`.
- ✅ **Rule-Based Evaluation**: `category=`, `user_role=`, `target_id IN (...)`, `current_date BETWEEN`, `PERCENTAGE N`.
- ✅ **Logical Operators**: Support for `AND`, `OR`, `NOT`, `!=` via `CompositeSpecification` (#JAM-7731).
- ✅ **A/B/C Testing**: Deterministic variant selection with statistical weight tracking.
- ✅ **Extensible Logging**: Contract-based via `logEvaluation(EvaluationResult)` — no duplicate records.
- ✅ **TDD-Verified**: 74%+ code coverage with Pest/PHPUnit, safe for production.

📦 Installation
--------------

[](#-installation)

```
composer require ambrion/feature-flags-core:^1.4@alpha
```

🚀 Quick Start
-------------

[](#-quick-start)

### 1. Wire Dependencies

[](#1-wire-dependencies)

```
use FeatureFlags\Core\Application\Service\FeatureFlagService;
use FeatureFlags\Core\Domain\Logger\NullFlagUsageLogger;
use FeatureFlags\Core\Domain\Repository\FlagRepositoryInterface;

// Implement your own repository (DB, config file, etc.)
$repository = new MyFlagRepository();
$logger = new NullFlagUsageLogger(); // or your implementation

$flagService = new FeatureFlagService($repository, $logger);
```

### 2. Evaluate Flags — Unified API

[](#2-evaluate-flags--unified-api)

```
// Single call returns all evaluation data
$result = $flagService->evaluate('new_product_template', [
    'user_role' => 'manager',
    'category' => 'electronics',
    'target_id' => 42,
]);

// Access what you need:
if ($result->enabled) {
    // Show new feature
}

// For A/B tests:
$variant = $result->variant; // 'A', 'B', 'C', or null
$weight = $result->weight;   // 0.34 for PERCENTAGE rules, null otherwise
$rule = $result->matchedRule; // 'category=electronics' — for debugging
```

### 3. Convenience Wrappers (Optional)

[](#3-convenience-wrappers-optional)

```
// Boolean check — same as evaluate()->enabled
$isEnabled = $flagService->isEnabled('show_banner', $context);

// A/B variant — same as evaluate()->variant
$variant = $flagService->getVariant('checkout_test', $context);

// Statistical weight — same as evaluate()->weight
$weight = $flagService->getVariantWeight('checkout_test', $context);
```

> 💡 All wrappers delegate to `evaluate()` internally — no duplicate evaluations or log entries.

🧩 How Rules Work
----------------

[](#-how-rules-work)

Flags are evaluated **in order** (short-circuit). First matching rule wins.

### Condition Syntax

[](#condition-syntax)

Condition SyntaxDescriptionExample`user_role=VALUE`Exact role match`user_role=admin``category IN (a,b)`List match`category IN (electronics,phones)``target_id=VALUE`Exact entity ID`target_id=101``current_date BETWEEN MM-DD AND MM-DD`Seasonal window`current_date BETWEEN 12-01 AND 12-31``user_hash PERCENTAGE N`Deterministic rollout`user_hash PERCENTAGE 25`### 🔗 Logical Operators (CompositeSpecification)

[](#-logical-operators-compositespecification)

Combine conditions with `AND`, `OR`, `NOT`, `!=`:

```
// AND: both conditions must match
['condition' => 'category=electronics AND user_tier=premium', 'value' => true]

// OR: at least one condition matches
['condition' => 'user_role=admin OR user_role=manager', 'value' => true]

// NOT / !=: negation
['condition' => 'category!=clothing', 'value' => true]
['condition' => 'NOT environment=production', 'value' => true]

// Precedence: NOT > AND > OR (like SQL/PHP)
// "A OR B AND C" is evaluated as "A OR (B AND C)"
```

> ⚠️ **Limitation**: Parentheses for explicit grouping are not supported in v1. Use De Morgan's laws: `NOT (A AND B)` → `NOT A OR NOT B`.

### Default Value Behavior

[](#default-value-behavior)

Flag Type`default` Value`evaluate()->enabled``evaluate()->variant`Boolean toggle`true` / `false``bool``null`A/B test`'A'` / `'B'` / `'C'``(bool)'A'` → `true``'A'` (string)Hybrid`null``false``null`### Example: A/B Test Configuration

[](#example-ab-test-configuration)

```
'checkout_flow_test' => [
    'default' => 'A',  // Polymorphic default: string for variants
    'rules' => [
        // 34% of users get variant A (weight = 0.34)
        ['condition' => 'user_hash PERCENTAGE 34', 'value' => 'A'],
        // Next 33% (buckets 34-66) get variant B (weight = 0.67)
        ['condition' => 'user_hash PERCENTAGE 67', 'value' => 'B'],
        // Remaining 33% (buckets 67-99) get variant C (weight = 1.0)
        ['condition' => 'user_hash PERCENTAGE 100', 'value' => 'C'],
    ]
]
```

> 💡 **Platform Integration**: Map your platform-specific keys (e.g., `document_id`) to `target_id` in your adapter. The core stays neutral.

🏗️ Architecture Overview
------------------------

[](#️-architecture-overview)

```
Application/
  └── Service/FeatureFlagService.php  ← Orchestration layer
Domain/
  ├── Entity/FeatureFlag.php          ← Business rules & evaluation
  ├── ValueObject/
  │   ├── EvaluationResult.php        ← Unified result: enabled/variant/weight/matchedRule
  │   ├── EvaluationContext.php       ← Type-safe context access
  │   └── FlagName.php                ← Strongly-typed flag identifier
  ├── Specification/                  ← Condition strategies
  │   ├── CategorySpecification.php
  │   ├── UserRoleSpecification.php
  │   ├── PercentageSpecification.php
  │   ├── DateBetweenSpecification.php
  │   ├── TargetIdSpecification.php
  │   └── CompositeSpecification.php  ← AND/OR/NOT/!= support
  ├── Logger/
  │   └── FlagUsageLoggerInterface.php ← Contract: logEvaluation(EvaluationResult)
  └── Repository/
      └── FlagRepositoryInterface.php  ← Contract for flag storage

```

- **No framework ties**: Core doesn't know about Laravel, Symfony, or DB drivers.
- **Ports &amp; Adapters**: Implement interfaces for your stack.
- **Polymorphic defaults**: `FeatureFlag::$default` is `mixed` — supports `bool|string|null`.

📊 Logging &amp; Analytics
-------------------------

[](#-logging--analytics)

### Unified Logging Contract

[](#unified-logging-contract)

Implement `FlagUsageLoggerInterface` to track flag usage:

```
public function logEvaluation(string $flagName, EvaluationResult $result, array $context = []): void;
```

The `EvaluationResult` contains all data for analytics:

```
$result->enabled;      // bool — for feature toggles
$result->variant;      // ?string — for A/B/C tests
$result->weight;       // ?float — normalized weight (0.0-1.0) for PERCENTAGE rules
$result->matchedRule;  // ?string — condition string of the matched rule (debugging)
```

### Example: Custom Logger Implementation

[](#example-custom-logger-implementation)

```
class DatabaseFlagUsageLogger implements FlagUsageLoggerInterface
{
    public function logEvaluation(string $flagName, EvaluationResult $result, array $context = []): void
    {
        // Save to your analytics store
        $this->analytics->record([
            'flag' => $flagName,
            'enabled' => $result->enabled,
            'variant' => $result->variant,
            'weight' => $result->weight,
            'matched_rule' => $result->matchedRule,
            'context_hash' => md5(json_encode($context)),
            'evaluated_at' => now(),
        ]);
    }
}
```

> 💡 **No duplicate logs**: Since `evaluate()` calls `logEvaluation()` once, you get exactly one record per flag evaluation — even when accessing `variant` and `weight`.

### 📈 Statistical Analysis Example

[](#-statistical-analysis-example)

```
$result = $flagService->evaluate('checkout_test', $context);

if ($result->weight !== null) {
    // Normalize metrics for fair A/B comparison
    $normalizedConversion = $rawConversion / $result->weight;

    // Log to external analytics
    $analytics->track('checkout_variant', [
        'variant' => $result->variant,
        'weight' => $result->weight,
        'conversion' => $normalizedConversion,
    ]);
}
```

---

🧪 Development &amp; Testing
---------------------------

[](#-development--testing)

```
composer install
composer test          # Run unit tests
composer test:coverage # Run with coverage report
composer stan          # Static analysis with PHPStan
composer format        # Auto-format with Pint
composer check         # Run all checks: lint + test + stan
```

Built with **Pest**. Follow TDD: Red → Green → Refactor.

### Testing Polymorphic Defaults

[](#testing-polymorphic-defaults)

```
// Test: getVariant returns string default when no rules match
it('returns string default for A/B test', function() {
    $flag = new FeatureFlag(
        name: new FlagName('ab_test'),
        default: 'A',  // string default
        rules: [],
    );

    expect($flag->getVariant(new EvaluationContext([])))->toBe('A');
});

// Test: evaluate always returns bool for enabled (backward compatibility)
it('evaluate returns bool even with string default', function() {
    $flag = new FeatureFlag(
        name: new FlagName('hybrid'),
        default: 'A',  // string
        rules: [],
    );

    $result = $flag->evaluate(new EvaluationContext([]));
    expect($result->enabled)->toBeBool();      // ✅ always bool
    expect($result->enabled)->toBeTrue();      // (bool)'A' === true
});
```

🔙 Backward Compatibility
------------------------

[](#-backward-compatibility)

Existing code continues to work without changes:

Old CodeBehaviorNew Behavior`default: false``evaluate()->enabled` → `false`✅ Same`default: true``getVariant()` → `null`✅ Same`rules: [['value' => true]]``evaluate()->enabled` → `true`✅ Same`rules: [['value' => 'B']]``getVariant()` → `'B'`✅ Same> ⚠️ **Migration Note**: If storing flags in a database, change `default_value` column from `BOOLEAN` to `JSON` to support polymorphic values. Laravel's `'json'` cast handles serialization automatically.

🔄 Migration: v1.3 → v1.4
------------------------

[](#-migration-v13--v14)

### Logging Interface Change

[](#logging-interface-change)

```
// Before (v1.3):
public function log(string $flagName, bool $result, array $context = []): void;
public function logVariant(string $flagName, ?string $variant, array $context = []): void;

// After (v1.4):
public function logEvaluation(string $flagName, EvaluationResult $result, array $context = []): void;
```

Update your logger implementation:

```
public function logEvaluation(string $flagName, EvaluationResult $result, array $context = []): void
{
    // Access all data from EvaluationResult
    $this->save([
        'flag' => $flagName,
        'enabled' => $result->enabled,
        'variant' => $result->variant,
        'weight' => $result->weight,
        'matched_rule' => $result->matchedRule,
        'context' => $context,
    ]);
}
```

### Service API: Prefer `evaluate()`

[](#service-api-prefer-evaluate)

```
// Before:
$variant = $service->getVariant('flag', $ctx);
$weight = $service->getVariantWeight('flag', $ctx); // Two calls, two logs

// After:
$result = $service->evaluate('flag', $ctx); // One call, one log
$variant = $result->variant;
$weight = $result->weight;
```

📄 License
---------

[](#-license)

MIT © Ambrion. See [LICENSE](LICENSE) for details.

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance94

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity37

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

4

Last Release

27d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/ef01d6aa82668faa03d544a80ed0e79f99c83a885e4705536801012f078719e7?d=identicon)[Ambrion](/maintainers/Ambrion)

---

Top Contributors

[![Ambrion](https://avatars.githubusercontent.com/u/31336547?v=4)](https://github.com/Ambrion "Ambrion (36 commits)")

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/ambrion-feature-flags-core/health.svg)

```
[![Health](https://phpackages.com/badges/ambrion-feature-flags-core/health.svg)](https://phpackages.com/packages/ambrion-feature-flags-core)
```

###  Alternatives

[nonsapiens/realaddressfactory

Creates real-world street addresses from Google Maps, to use in database seeding, unit tests, or anything else. Supports Laravel 11+, and Faker

5011.3k8](/packages/nonsapiens-realaddressfactory)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
