PHPackages                             zenmanage/zenmanage-php - 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. zenmanage/zenmanage-php

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

zenmanage/zenmanage-php
=======================

Official PHP SDK for Zenmanage feature flags with local evaluation

4.0.3(1mo ago)01.7k↓45.6%1MITPHPPHP &gt;=8.0CI passing

Since Jan 31Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/zenmanage/zenmanage-php)[ Packagist](https://packagist.org/packages/zenmanage/zenmanage-php)[ RSS](/packages/zenmanage-zenmanage-php/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (10)Dependencies (12)Versions (10)Used By (1)

Zenmanage PHP SDK
=================

[](#zenmanage-php-sdk)

[![Build Status](https://github.com/zenmanage/zenmanage-php/actions/workflows/ci.yml/badge.svg)](https://github.com/zenmanage/zenmanage-php) [![Quality Gate Status](https://camo.githubusercontent.com/0277537680f742ecdecab07f1e35881733d55dd03564676d450507145f93cd3e/68747470733a2f2f736f6e6172636c6f75642e696f2f6170692f70726f6a6563745f6261646765732f6d6561737572653f70726f6a6563743d7a656e6d616e6167655f7a656e6d616e6167652d706870266d65747269633d616c6572745f737461747573)](https://sonarcloud.io/summary/new_code?id=zenmanage_zenmanage-php)

Add feature flags to your PHP application in minutes. Control feature rollouts, A/B test, and manage configurations without deploying code.

Why Zenmanage?
--------------

[](#why-zenmanage)

- 🚀 **Fast**: Rules cached locally - ~1ms evaluation time
- 🎯 **Targeted**: Roll out features to specific users, organizations, or segments
- 🛡️ **Safe**: Graceful fallbacks and error handling built-in
- 📊 **Insightful**: Automatic usage tracking (optional)
- 🧪 **Testable**: Easy to mock in tests

Installation
------------

[](#installation)

```
composer require zenmanage/zenmanage-php
```

Upgrading
---------

[](#upgrading)

See [UPGRADING.md](UPGRADING.md) for details on breaking changes and migration steps.

**Requirements**: PHP 8.0+

Key Compatibility
-----------------

[](#key-compatibility)

- Supported: case-sensitive server keys prefixed with `srv_`
- Not supported in PHP SDK: client keys (`cli_`) and mobile keys (`mob_`) (initialization fails fast)

Get Started in 60 Seconds
-------------------------

[](#get-started-in-60-seconds)

1. Get your server key (`srv_...`) from [zenmanage.com](https://zenmanage.com)
2. Initialize the SDK:

```
use Zenmanage\Config\ConfigBuilder;
use Zenmanage\Zenmanage;

$zenmanage = new Zenmanage(
    ConfigBuilder::create()
    ->withEnvironmentToken('srv_your_server_key_here')
        ->build()
);
```

3. Check a feature flag:

```
if ($zenmanage->flags()->single('new-dashboard')->isEnabled()) {
    // Show new dashboard
    return view('dashboard-v2');
}

// Show old dashboard
return view('dashboard');
```

That's it! 🎉

Common Use Cases
----------------

[](#common-use-cases)

### Roll Out a New Feature Gradually

[](#roll-out-a-new-feature-gradually)

```
// Check if user has access to beta features
$context = Context::single('user', $user->id, $user->name);

$betaAccess = $zenmanage->flags()
    ->withContext($context)
    ->single('beta-program')
    ->isEnabled();

if ($betaAccess) {
    // User is in beta program
    $features = $this->getBetaFeatures();
}
```

**Note:** Call `withContext()` on the flag manager to ensure context is sent to the API when loading rules.

### A/B Testing

[](#ab-testing)

```
$context = Context::fromArray([
    'type' => 'user',
    'identifier' => $user->id,
    'name' => $user->name,
    'attributes' => [
        ['key' => 'country', 'values' => [['value' => $user->country]]],
    ],
]);

$variant = $zenmanage->flags()
    ->withContext($context)
    ->single('checkout-flow')
    ->asString();

if ($variant === 'one-page') {
    return view('checkout.onepage');
} else {
    return view('checkout.multipage');
}
```

### Percentage Rollouts

[](#percentage-rollouts)

Gradually roll out features to a percentage of your users. The SDK handles bucketing automatically using a deterministic CRC32B hash — no manual bucket logic needed.

```
use Zenmanage\Flags\Context\Context;

// Just provide a context with an identifier — the SDK does the rest
$context = Context::single('user', $user->id);

$flag = $zenmanage->flags()
    ->withContext($context)
    ->single('new-checkout-flow');

if ($flag->isEnabled()) {
    // This user is in the rollout percentage
    return view('checkout.new');
}

// This user is outside the rollout
return view('checkout.classic');
```

**How it works:**

- Configure the rollout percentage (0–100%) and a unique salt in the Zenmanage dashboard
- The SDK hashes `salt:contextIdentifier` to deterministically assign each user to a bucket (0–99)
- Users whose bucket is below the percentage get the rollout value; others get the fallback
- The same user always gets the same result (deterministic), and increasing the percentage never removes previously included users
- Rollout rules can further refine targeting within the rollout group (e.g., only US users in the rollout)

> **Note:** A context `identifier` is required for bucketing. Without one, the user always receives the fallback value.

### Feature Toggles by Organization

[](#feature-toggles-by-organization)

```
$context = Context::fromArray([
    'type' => 'organization',
    'identifier' => $user->organization->id,
    'name' => $user->organization->name,
    'attributes' => [
        ['key' => 'plan', 'values' => [['value' => $user->organization->plan]]],
    ],
]);

$advancedReports = $zenmanage->flags()
    ->withContext($context)
    ->single('advanced-reports')
    ->isEnabled();

if ($advancedReports) {
    return $this->getAdvancedReports();
}
```

### Configuration Values

[](#configuration-values)

```
// Get configuration values from flags
$apiTimeout = $zenmanage->flags()
    ->single('api-timeout', 5000)  // Default 5000ms
    ->asNumber();

$maxUploadSize = $zenmanage->flags()
    ->single('max-upload-mb', 10)
    ->asNumber();

$welcomeMessage = $zenmanage->flags()
    ->single('welcome-text', 'Welcome!')
    ->asString();
```

### Kill Switch for Problem Features

[](#kill-switch-for-problem-features)

```
// Quickly disable a problematic feature via dashboard
if ($zenmanage->flags()->single('new-payment-processor', false)->isEnabled()) {
    return $this->processWithNewSystem($payment);
} else {
    return $this->processWithLegacySystem($payment);
}
```

Setup for Your Application
--------------------------

[](#setup-for-your-application)

### Laravel Integration

[](#laravel-integration)

Tip

There is an official Laravel integration on GitHub: [zenmanage/zenmanage-laravel](https://github.com/zenmanage/zenmanage-laravel). Use it to plug Zenmanage directly into your Laravel app with minimal setup.

Create a service provider to make Zenmanage available throughout your app:

```
// app/Providers/ZenmanageServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Zenmanage\Config\ConfigBuilder;
use Zenmanage\Zenmanage;

class ZenmanageServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(Zenmanage::class, function ($app) {
            return new Zenmanage(
                ConfigBuilder::create()
                    ->withEnvironmentToken(config('services.zenmanage.token'))
                    ->withCacheBackend('filesystem')
                    ->withCacheDirectory(storage_path('framework/cache/zenmanage'))
                    ->withCacheTtl(3600)
                    ->build()
            );
        });
    }
}
```

```
// config/services.php
return [
    // ...
    'zenmanage' => [
        'token' => env('ZENMANAGE_TOKEN'),
    ],
];
```

Then use dependency injection anywhere:

```
class DashboardController extends Controller
{
    public function __construct(private Zenmanage $zenmanage) {}

    public function index(Request $request)
    {
        $context = Context::single('user', $request->user()->email);

        $useNewDashboard = $this->zenmanage->flags()
            ->withContext($context)
            ->single('new-dashboard', false)
            ->isEnabled();

        return $useNewDashboard
            ? view('dashboard-v2')
            : view('dashboard');
    }
}
```

### Symfony Integration

[](#symfony-integration)

```
# config/services.yaml
services:
    Zenmanage\Zenmanage:
        factory: ['App\Factory\ZenmanageFactory', 'create']
        arguments:
            $token: '%env(ZENMANAGE_TOKEN)%'
            $cacheDir: '%kernel.cache_dir%/zenmanage'
```

```
// src/Factory/ZenmanageFactory.php
namespace App\Factory;

use Zenmanage\Config\ConfigBuilder;
use Zenmanage\Zenmanage;

class ZenmanageFactory
{
    public static function create(string $token, string $cacheDir): Zenmanage
    {
        return new Zenmanage(
            ConfigBuilder::create()
                ->withEnvironmentToken($token)
                ->withCacheBackend('filesystem')
                ->withCacheDirectory($cacheDir)
                ->build()
        );
    }
}
```

### Standalone PHP Application

[](#standalone-php-application)

```
// bootstrap.php or similar
$zenmanage = new Zenmanage(
    ConfigBuilder::create()
        ->withEnvironmentToken($_ENV['ZENMANAGE_TOKEN'])
        ->withCacheBackend('filesystem')
        ->withCacheDirectory(__DIR__ . '/cache/zenmanage')
        ->build()
);

// Make available globally (if needed)
$GLOBALS['zenmanage'] = $zenmanage;
// Or use a registry pattern, DI container, etc.
```

Working with Contexts
---------------------

[](#working-with-contexts)

Contexts let you target flags to specific users, organizations, or any custom attributes. This is how you do gradual rollouts, A/B tests, and targeted features.

### Simple Context (One Attribute)

[](#simple-context-one-attribute)

```
use Zenmanage\Flags\Context\Context;

// Target by user ID with name
$context = Context::single('user', $user->id, $user->name);

// Target by organization
$context = Context::single('organization', $company->id, $company->name);

// Target by user with just ID
$context = Context::single('user', $user->id);
```

### Rich Context (Multiple Attributes)

[](#rich-context-multiple-attributes)

```
$context = Context::fromArray([
    'type' => 'user',
    'identifier' => $user->id,
    'name' => $user->name,
    'attributes' => [
        ['key' => 'organization', 'values' => [['value' => $user->company->slug]]],
        ['key' => 'plan', 'values' => [['value' => $user->subscription->plan]]],
        ['key' => 'role', 'values' => [['value' => $user->role]]],
        ['key' => 'country', 'values' => [['value' => $user->country]]],
    ],
]);

$premiumFeatures = $zenmanage->flags()
    ->withContext($context)
    ->single('premium-dashboard')
    ->isEnabled();
```

**What you get:**

- `type`: Context type (user, organization, etc.)
- `identifier`: Unique identifier for targeting
- `name`: Human-readable display name
- `attributes`: Array of additional attributes for advanced targeting (plan, role, country, etc.)

**When to use contexts:**

- Rolling out to specific users (beta testers)
- Organization-based features (enterprise vs. free)
- Regional features (different countries)
- Role-based access (admins, moderators)
- Plan-based features (pro vs. basic)

### How Rule Evaluation Works

[](#how-rule-evaluation-works)

The SDK supports three types of rule selectors for targeting:

#### 1. Segment Selector

[](#1-segment-selector)

Matches against a list of specific context identifiers:

If a rule value omits `type` (or sets it to `null`), only the `identifier` is compared.

```
// Rule: "Block these specific IPs"
// Selector: "segment", Values: [{"type": "user", "identifier": "140.248.31.37"}]

$context = Context::single('user', '140.248.31.37', 'Blocked User');
$result = $zenmanage->flags()
    ->withContext($context)
    ->single('allow-feature', true)
    ->isEnabled(); // Returns: false (matched segment)
```

#### 2. Context Selector

[](#2-context-selector)

Same as segment - matches against context type and identifier:

If a rule value omits `type` (or sets it to `null`), only the `identifier` is compared.

```
// Rule: "Enable for specific users"
// Selector: "context", Values: [{"type": "user", "identifier": "john-doe"}]

$context = Context::single('user', 'john-doe', 'John Doe');
$result = $zenmanage->flags()
    ->withContext($context)
    ->single('beta-feature', false)
    ->isEnabled(); // Returns: true (matched context)
```

#### 3. Attribute Selector

[](#3-attribute-selector)

Matches against additional context attributes (plan, country, role, etc.):

```
// Rule: "Enable for enterprise plans"
// Selector: "attribute", Subtype: "plan", Values: ["enterprise"]

$context = Context::fromArray([
    'type' => 'user',
    'identifier' => 'user-123',
    'name' => 'Jane Doe',
    'attributes' => [
        ['key' => 'plan', 'values' => [['value' => 'enterprise']]],
        ['key' => 'country', 'values' => [['value' => 'US']]],
    ],
]);

$result = $zenmanage->flags()
    ->withContext($context)
    ->single('premium-features', false)
    ->isEnabled(); // Returns: true (plan matched)
```

**Supported operators for all selectors:**

- `equal` - Exact match (most common)
- `contains` - Value contains the comparison string
- `starts_with` - Value starts with the comparison string
- `ends_with` - Value ends with the comparison string
- `regex` - Value matches regex pattern
- `greater_than`, `less_than`, `in`, etc. (see OperatorEvaluator for full list)

Safe Defaults - Never Break Your App
------------------------------------

[](#safe-defaults---never-break-your-app)

Always provide defaults for critical features. The SDK will use them if:

- Flag doesn't exist yet
- API is unreachable
- Network issues occur

### Inline Defaults (Recommended)

[](#inline-defaults-recommended)

```
// If 'new-checkout' doesn't exist, returns true
$enabled = $zenmanage->flags()
    ->single('new-checkout', true)
    ->isEnabled();

// Configuration value with fallback
$timeout = $zenmanage->flags()
    ->single('api-timeout', 5000)
    ->asNumber();
```

### Default Collections (For Multiple Flags)

[](#default-collections-for-multiple-flags)

```
use Zenmanage\Flags\DefaultsCollection;

$defaults = DefaultsCollection::fromArray([
    'new-ui' => true,
    'max-upload-size' => 10,
    'welcome-message' => 'Welcome to our app!',
    'feature-x' => false,
]);

$flags = $zenmanage->flags()->withDefaults($defaults);

// All these will use defaults if flags don't exist
$newUI = $flags->single('new-ui')->isEnabled();
$maxSize = $flags->single('max-upload-size')->asNumber();
$message = $flags->single('welcome-message')->asString();
```

### Priority Order

[](#priority-order)

When retrieving a flag, the SDK checks in this order:

1. **API Value** - If flag exists in Zenmanage
2. **Inline Default** - Value passed to `single('flag', default)`
3. **Collection Default** - From `DefaultsCollection`
4. **Exception** - If none of the above

```
$defaults = DefaultsCollection::fromArray(['timeout' => 3000]);

// Uses API value if exists, otherwise inline (5000), then collection (3000)
$timeout = $zenmanage->flags()
    ->withDefaults($defaults)
    ->single('timeout', 5000)
    ->asNumber();
```

Performance - Caching Rules
---------------------------

[](#performance---caching-rules)

The SDK caches flag rules to minimize API calls. Rules are fetched once, then served from cache.

### Default Setup (In-Memory)

[](#default-setup-in-memory)

Out of the box, flags are cached in memory for the request duration. Zero configuration needed:

```
// First call fetches from API
$newUI = $zenmanage->flags()->single('new-ui')->isEnabled();

// Subsequent calls use memory cache (same request)
$newUI2 = $zenmanage->flags()->single('new-ui')->isEnabled(); // Instant
```

**Good for:** Most web applications, simple scripts

### File System Cache (Persist Between Requests)

[](#file-system-cache-persist-between-requests)

Cache rules to disk for faster performance across multiple requests:

```
$config = ConfigBuilder::create()
    ->withEnvironmentToken('srv_your_server_key')
    ->withCacheBackend('filesystem')
    ->withCacheDirectory('/var/cache/zenmanage')
    ->withCacheTtl(300) // 5 minutes
    ->build();
```

**Good for:** High-traffic sites, long-running processes, CLI applications

**When to use:**

- Production websites (cache between page loads)
- Background jobs (avoid repeated API calls)
- CLI tools (faster execution)

### Cache Duration

[](#cache-duration)

Control how long rules are cached:

```
// 5 minutes (good for rapid development)
->withCacheTtl(300)

// 1 hour (good for production)
->withCacheTtl(3600)

// 1 day (good for stable flags)
->withCacheTtl(86400)
```

**Recommendation:** Start with 5-10 minutes in production. Increase once flags are stable.

### Disable Cache (Testing Only)

[](#disable-cache-testing-only)

```
// Always fetch fresh from API
$config = ConfigBuilder::create()
    ->withCacheBackend('null')
    ->build();
```

### Manually Refresh Rules

[](#manually-refresh-rules)

Force a fresh fetch from the API:

```
$zenmanage->flags()->refreshRules();
```

Logging &amp; Debugging
-----------------------

[](#logging--debugging)

Get visibility into what the SDK is doing by providing a PSR-3 logger:

```
use Psr\Log\LoggerInterface;

$config = ConfigBuilder::create()
    ->withEnvironmentToken('srv_your_server_key')
    ->withLogger($yourLogger)
    ->build();
```

**What gets logged:**

- API requests and responses
- Cache hits and misses
- Rule evaluation results
- Errors and exceptions

**Example with Monolog:**

```
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$logger = new Logger('zenmanage');
$logger->pushHandler(new StreamHandler('path/to/zenmanage.log', Logger::DEBUG));

$config = ConfigBuilder::create()
    ->withLogger($logger)
    ->build();
```

Error Handling in Production
----------------------------

[](#error-handling-in-production)

The SDK is designed for graceful degradation. Your app should never break because of feature flags.

### Always Use Defaults for Critical Features

[](#always-use-defaults-for-critical-features)

```
// Bad - will throw exception if flag doesn't exist
$showNewUI = $zenmanage->flags()->single('new-ui')->isEnabled();

// Good - falls back to false if anything goes wrong
$showNewUI = $zenmanage->flags()->single('new-ui', false)->isEnabled();
```

### Handle API Failures

[](#handle-api-failures)

If the API is unreachable, the SDK will:

1. Use cached rules (if available)
2. Fall back to default values (if provided)
3. Throw exception (if no defaults)

**Recommended pattern:**

```
try {
    $premiumEnabled = $zenmanage->flags()
        ->withContext($context)
        ->single('premium-features', false) // Default to false
        ->isEnabled();

    if ($premiumEnabled) {
        return $this->showPremiumDashboard();
    }
} catch (ZenmanageException $e) {
    // Log error but continue with default behavior
    $this->logger->warning('Flag check failed', ['error' => $e->getMessage()]);
    $premiumEnabled = false;
}

return $this->showStandardDashboard();
```

### Retry Logic

[](#retry-logic)

The SDK automatically retries failed API calls (3 attempts with exponential backoff). You don't need to handle this.

Testing Your Feature Flags
--------------------------

[](#testing-your-feature-flags)

Test your feature-flagged code without hitting the Zenmanage API.

### Use Defaults in Tests

[](#use-defaults-in-tests)

```
public function test_premium_users_see_dashboard()
{
    $zenmanage = new Zenmanage(
        ConfigBuilder::create()
            ->withEnvironmentToken('test-token')
            ->withCacheBackend('null') // No caching in tests
            ->build()
    );

    $defaults = DefaultsCollection::fromArray([
        'premium-dashboard' => true,
    ]);

    $enabled = $zenmanage->flags()
        ->withDefaults($defaults)
        ->single('premium-dashboard')
        ->isEnabled();

    $this->assertTrue($enabled);
}
```

### Mock the Flag Manager

[](#mock-the-flag-manager)

```
use PHPUnit\Framework\TestCase;
use Zenmanage\Flags\FlagManagerInterface;
use Zenmanage\Flags\Flag;

class CheckoutTest extends TestCase
{
    public function test_new_checkout_flow()
    {
        $flagManager = $this->createMock(FlagManagerInterface::class);
        $flagManager->method('single')
            ->willReturn(new Flag('new-checkout', 'New Checkout', true));

        $checkout = new CheckoutService($flagManager);

        $result = $checkout->processPayment($order);

        $this->assertTrue($result->usedNewFlow());
    }
}
```

### Test Different Flag States

[](#test-different-flag-states)

```
public function test_feature_disabled_shows_old_ui()
{
    $defaults = DefaultsCollection::fromArray(['new-ui' => false]);

    $flag = $this->zenmanage->flags()
        ->withDefaults($defaults)
        ->single('new-ui');

    $this->assertFalse($flag->isEnabled());
}

public function test_feature_enabled_shows_new_ui()
{
    $defaults = DefaultsCollection::fromArray(['new-ui' => true]);

    $flag = $this->zenmanage->flags()
        ->withDefaults($defaults)
        ->single('new-ui');

    $this->assertTrue($flag->isEnabled());
}
```

Requirements
------------

[](#requirements)

- PHP 8.0 or higher
- Composer
- Guzzle HTTP client (automatically installed)

Installation
------------

[](#installation-1)

```
composer require zenmanage/zenmanage-php
```

Development
-----------

[](#development)

Run tests:

```
composer test
```

Run static analysis:

```
composer phpstan
```

License
-------

[](#license)

MIT

Support
-------

[](#support)

- Documentation:
- Issues:
- Email:

###  Health Score

45

—

FairBetter than 93% of packages

Maintenance89

Actively maintained with recent releases

Popularity21

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 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 ~52 days

Recently: every ~10 days

Total

9

Last Release

56d ago

Major Versions

1.0.1 → 2.0.02025-08-22

2.0.0 → 3.0.02026-01-19

3.3.0 → 4.0.22026-03-23

PHP version history (2 changes)1.0.0PHP &gt;=7.4

2.0.0PHP &gt;=8.0

### Community

Maintainers

![](https://www.gravatar.com/avatar/7fe43dc70dad96a50299ea26ec25b0a1c35a5a6b48ac4b71f843b45c25b7eb77?d=identicon)[zenmanage-brk](/maintainers/zenmanage-brk)

---

Top Contributors

[![zenmanage-brk](https://avatars.githubusercontent.com/u/219455051?v=4)](https://github.com/zenmanage-brk "zenmanage-brk (34 commits)")

---

Tags

phpfeature-flagsfeature-toggleszenmanage

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/zenmanage-zenmanage-php/health.svg)

```
[![Health](https://phpackages.com/badges/zenmanage-zenmanage-php/health.svg)](https://phpackages.com/packages/zenmanage-zenmanage-php)
```

###  Alternatives

[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

245.2k](/packages/aedart-athenaeum)

PHPackages © 2026

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