Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f3ceae1
feat(flags): add feature flag support (local + remote evaluation)
tylerjroach Jun 22, 2026
b8dcf50
feat(flags): emit ISO start/complete timestamps in remote exposure
tylerjroach Jun 23, 2026
d8ae1f9
feat(flags): add MODE_LOCAL / MODE_REMOTE class constants
tylerjroach Jun 24, 2026
f42140c
feat(flags): add needsRefresh() / refresh() for local mode
tylerjroach Jun 24, 2026
b09af45
Revert "feat(flags): add needsRefresh() / refresh() for local mode"
tylerjroach Jun 24, 2026
539fb6d
refactor(flags): align config surface with peer server SDKs
tylerjroach Jun 24, 2026
5e5d78d
refactor(flags): drop ext-* install requirements
tylerjroach Jun 24, 2026
69df542
refactor(flags): move failure reason onto SelectedVariant
tylerjroach Jun 24, 2026
6f8a368
chore: drop CHANGELOG edits — changelog is generated at release time
tylerjroach Jun 24, 2026
764acc3
chore: drop README edits — docs live on the official docs site
tylerjroach Jun 24, 2026
94ca40e
chore: drop example file — examples live alongside the docs site
tylerjroach Jun 24, 2026
e6dc14e
refactor(flags): resolve lib_version dynamically via Composer
tylerjroach Jun 24, 2026
cebc4a7
docs(readme): document feature flags, drop dead Travis badge, fix typo
tylerjroach Jun 25, 2026
71f740f
revert(readme): drop feature flags section — keep docs on the docs site
tylerjroach Jun 25, 2026
8ab9127
ci: run PHPUnit across the supported PHP version matrix
tylerjroach Jun 25, 2026
c34b56c
ci: bump action SHAs to Node 24 runtime, drop vulnerable PHPUnit range
tylerjroach Jun 25, 2026
d4abfff
fix(flags): tag fallback_reason so OpenFeature can distinguish causes
tylerjroach Jun 29, 2026
f4fafd3
fix(flags): address review feedback (mode validation, hardening, hygi…
tylerjroach Jul 1, 2026
4af078e
refactor(flags): bump PHP min to 8.1 and strongly type the feature-fl…
tylerjroach Jul 1, 2026
8d58a16
fix(flags): detect json_encode failure + guard curl_init + document i…
tylerjroach Jul 2, 2026
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
56 changes: 56 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Tests

on:
pull_request:
push:
branches: [master]

permissions:
contents: read

concurrency:
# Cancel in-flight runs for the same PR when a new commit lands; let
# master pushes always finish so we keep a complete trunk history.
group: tests-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
phpunit:
name: "PHPUnit (PHP ${{ matrix.php }})"
runs-on: ubuntu-latest
strategy:
# Surface every failing version on every run, not just the first.
fail-fast: false
matrix:
# Floor matches the `>=8.1` declared in composer.json. The
# 8.1 entry enforces the promise; later minors cover every
# currently-supported PHP release.
php: ['8.1', '8.3', '8.4']
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v7.0.0

- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@2282b6a082fc605c8320908a4cca3a5d1ca6c6fe # 2.37.2
with:
php-version: ${{ matrix.php }}
coverage: none
tools: composer:v2

- name: Resolve Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"

- name: Cache Composer packages
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v6.0.0
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
restore-keys: |
php-${{ matrix.php }}-composer-

- name: Install dependencies
run: composer update --no-interaction --no-progress --prefer-dist

- name: Run PHPUnit
run: composer run-script unit-tests
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Mixpanel PHP Library [![Build Status](https://travis-ci.org/mixpanel/mixpanel-php.svg)](https://travis-ci.org/mixpanel/mixpanel-php)
Mixpanel PHP Library
============

##### _May 13, 2026_ - [2.11.0](https://github.com/mixpanel/mixpanel-php/releases/tag/2.11.0)
Expand Down Expand Up @@ -82,7 +82,7 @@ Documentation
* <a href="https://mixpanel.com/help/reference/php" target="_blank">Reference Docs</a>
* <a href="http://mixpanel.github.io/mixpanel-php" target="_blank">Full API Reference</a>

For further examples and options checkout out the "examples" folder
For further examples and options check out the "examples" folder.

Changelog
-------------
Expand Down
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
}
],
"require": {
"php": ">=5.0"
"php": ">=8.1",
"jwadhams/json-logic-php": "^1.5",
"symfony/polyfill-mbstring": "^1.27"
},
"require-dev": {
"phpunit/phpunit": "5.6.*",
"phpdocumentor/phpdocumentor": "2.9.*"
"phpunit/phpunit": "^8.5.52 || ^9.5"
},
"autoload": {
"files": ["lib/Mixpanel.php"]
Expand Down
139 changes: 139 additions & 0 deletions lib/FeatureFlags/MixpanelFlags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php

declare(strict_types=1);

require_once(dirname(__FILE__) . "/MixpanelLocalFlags.php");
require_once(dirname(__FILE__) . "/MixpanelRemoteFlags.php");

/**
* Public entry point reached via $mp->flags. Decides between the local
* and remote provider based on the `mode` option, owns the underlying
* provider's lifecycle, and forwards every public method to it.
*
* Usage:
*
* $mp = Mixpanel::getInstance('TOKEN', array(
* 'flags' => array('mode' => FeatureFlags_MixpanelFlags::MODE_REMOTE),
* ));
* $enabled = $mp->flags->isEnabled('my-flag', array(
* 'distinct_id' => 'user-123',
* ));
*/
class FeatureFlags_MixpanelFlags {

/**
* Evaluation mode values for the `mode` config key. These constants
* give callers an IDE-checkable, grep-able alternative to bare string
* literals. The raw strings remain valid input — these are exact aliases.
*/
const MODE_LOCAL = 'local';
const MODE_REMOTE = 'remote';

private FeatureFlags_MixpanelFlagsBase $_provider;

/** One of the MODE_* constants. */
private string $_mode;

public function __construct(string $token, string $version, callable $tracker, array $options) {
// No extension checks — FNV-1a hashing uses PHP's built-in
// ext-hash (bundled in core), case folding uses
// symfony/polyfill-mbstring (composer dep), HTTP uses the
// existing CurlConsumer which already runtime-checks ext-curl.
$flagsOpts = isset($options['flags']) && is_array($options['flags']) ? $options['flags'] : array();
if (isset($flagsOpts['mode'])) {
$requested = strtolower((string) $flagsOpts['mode']);
if ($requested !== self::MODE_LOCAL && $requested !== self::MODE_REMOTE) {
// Fail loudly on typos ('lcoal' -> silently falls through to remote is a debug trap).
throw new InvalidArgumentException(
"Invalid flags 'mode' option: " . var_export($flagsOpts['mode'], true) .
". Expected '" . self::MODE_LOCAL . "' or '" . self::MODE_REMOTE . "'."
);
}
$this->_mode = $requested;
} else {
$this->_mode = self::MODE_REMOTE;
}

if ($this->_mode === self::MODE_LOCAL) {
$this->_provider = new FeatureFlags_MixpanelLocalFlags($token, $version, $tracker, $options);
} else {
$this->_provider = new FeatureFlags_MixpanelRemoteFlags($token, $version, $tracker, $options);
}
}

public function __destruct() {
try {
$this->shutdown();
} catch (\Throwable $t) {
// Swallow: destructors run during shutdown/fatal-error paths where
// throwing could mask the original error or hit a partially-torn-down
// interpreter state.
}
}

public function getMode(): string {
return $this->_mode;
}

public function getProvider(): FeatureFlags_MixpanelFlagsBase {
return $this->_provider;
}

/**
* Fetch flag definitions from the server. Local mode only; no-op
* (returns true) in remote mode.
*/
public function loadDefinitions(): bool {
if ($this->_provider instanceof FeatureFlags_MixpanelLocalFlags) {
return $this->_provider->loadDefinitions();
}
return true;
}

public function areFlagsReady(): bool {
if ($this->_provider instanceof FeatureFlags_MixpanelLocalFlags) {
return $this->_provider->areFlagsReady();
}
return true;
}

public function lastSyncedAt(): ?int {
if ($this->_provider instanceof FeatureFlags_MixpanelLocalFlags) {
return $this->_provider->lastSyncedAt();
}
return null;
}

public function getVariant(
string $flagKey,
FeatureFlags_MixpanelSelectedVariant $fallback,
array $context,
bool $reportExposure = true
): FeatureFlags_MixpanelSelectedVariant {
return $this->_provider->getVariant($flagKey, $fallback, $context, $reportExposure);
}

public function getVariantValue(string $flagKey, mixed $fallbackValue, array $context): mixed {
return $this->_provider->getVariantValue($flagKey, $fallbackValue, $context);
}

public function isEnabled(string $flagKey, array $context): bool {
return $this->_provider->isEnabled($flagKey, $context);
}

public function getAllVariants(array $context): array {
return $this->_provider->getAllVariants($context);
}

public function trackExposure(
string $flagKey,
FeatureFlags_MixpanelSelectedVariant $variant,
array $context
): void {
$this->_provider->trackExposure($flagKey, $variant, $context);
}

public function shutdown(): void {
$this->_provider->shutdown();
}
}
Loading
Loading