From 2837bf8f0753083e031ca66b16c49d04976d8103 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Tue, 26 Aug 2025 09:58:37 -0700 Subject: [PATCH 1/7] Implement local evaluation of flag dependencies --- .gitignore | 3 +- README.md | 15 +- lib/Client.php | 32 +- lib/FeatureFlag.php | 179 ++++- test/FeatureFlagLocalEvaluationTest.php | 71 +- test/FlagDependencyIntegrationTest.php | 130 ++++ test/FlagDependencyTest.php | 824 ++++++++++++++++++++++++ test/MultivariateIntegrationTest.php | 189 ++++++ 8 files changed, 1366 insertions(+), 77 deletions(-) create mode 100644 test/FlagDependencyIntegrationTest.php create mode 100644 test/FlagDependencyTest.php create mode 100644 test/MultivariateIntegrationTest.php diff --git a/.gitignore b/.gitignore index 5d6f66d..59169f1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ test/posthog.log .phpunit.result.cache clover.xml xdebug.log -.DS_Store \ No newline at end of file +.DS_Store +.env diff --git a/README.md b/README.md index 817d3d9..8804475 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,24 @@ Please see the main [PostHog docs](https://posthog.com/docs). Specifically, the [PHP integration](https://posthog.com/docs/integrations/php-integration) details. +## Features + +- ✅ Event capture and user identification +- ✅ Feature flag local evaluation +- ✅ **Feature flag dependencies** (new!) - Create conditional flags based on other flags +- ✅ Multivariate flags and payloads +- ✅ Group analytics +- ✅ Comprehensive test coverage + +## Quick Start + +1. Copy `.env.example` to `.env` and add your PostHog credentials +2. Run `php example.php` to see interactive examples of all features + ## Questions? ### [Join our Slack community.](https://join.slack.com/t/posthogusers/shared_invite/enQtOTY0MzU5NjAwMDY3LTc2MWQ0OTZlNjhkODk3ZDI3NDVjMDE1YjgxY2I4ZjI4MzJhZmVmNjJkN2NmMGJmMzc2N2U3Yjc3ZjI5NGFlZDQ) - ## Contributing 1. [Download PHP](https://www.php.net/manual/en/install.php) and [Composer](https://getcomposer.org/download/) diff --git a/lib/Client.php b/lib/Client.php index 147859d..9e6fe89 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -62,6 +62,10 @@ class Client */ public $cohorts; + /** + * @var array + */ + public $featureFlagsByKey; /** * @var SizeLimitedHash @@ -100,6 +104,7 @@ public function __construct( $this->featureFlags = []; $this->groupTypeMapping = []; $this->cohorts = []; + $this->featureFlagsByKey = []; $this->distinctIdsFeatureFlagsReported = new SizeLimitedHash(SIZE_LIMIT); // Populate featureflags and grouptypemapping if possible @@ -412,6 +417,9 @@ private function computeFlagLocally( array $personProperties = array(), array $groupProperties = array() ): bool | string { + // Create evaluation cache for flag dependencies + $evaluationCache = []; + if ($featureFlag["ensure_experience_continuity"] ?? false) { throw new InconclusiveMatchException("Flag has experience continuity enabled"); } @@ -435,9 +443,23 @@ private function computeFlagLocally( } $focusedGroupProperties = $groupProperties[$groupName]; - return FeatureFlag::matchFeatureFlagProperties($featureFlag, $groups[$groupName], $focusedGroupProperties); + return FeatureFlag::matchFeatureFlagProperties( + $featureFlag, + $groups[$groupName], + $focusedGroupProperties, + $this->cohorts, + $this->featureFlagsByKey, + $evaluationCache + ); } else { - return FeatureFlag::matchFeatureFlagProperties($featureFlag, $distinctId, $personProperties, $this->cohorts); + return FeatureFlag::matchFeatureFlagProperties( + $featureFlag, + $distinctId, + $personProperties, + $this->cohorts, + $this->featureFlagsByKey, + $evaluationCache + ); } } @@ -491,6 +513,12 @@ public function loadFlags() $this->featureFlags = $payload['flags'] ?? []; $this->groupTypeMapping = $payload['group_type_mapping'] ?? []; $this->cohorts = $payload['cohorts'] ?? []; + + // Build flags by key dictionary for dependency resolution + $this->featureFlagsByKey = []; + foreach ($this->featureFlags as $flag) { + $this->featureFlagsByKey[$flag['key']] = $flag; + } } diff --git a/lib/FeatureFlag.php b/lib/FeatureFlag.php index 7ae9b43..5d9cbf0 100644 --- a/lib/FeatureFlag.php +++ b/lib/FeatureFlag.php @@ -99,7 +99,7 @@ public static function matchProperty($property, $propertyValues) return false; } - public static function matchCohort($property, $propertyValues, $cohortProperties) + public static function matchCohort($property, $propertyValues, $cohortProperties, $flagsByKey = null, $evaluationCache = null, $distinctId = null) { $cohortId = strval($property["value"]); if (!array_key_exists($cohortId, $cohortProperties)) { @@ -107,10 +107,10 @@ public static function matchCohort($property, $propertyValues, $cohortProperties } $propertyGroup = $cohortProperties[$cohortId]; - return FeatureFlag::matchPropertyGroup($propertyGroup, $propertyValues, $cohortProperties); + return FeatureFlag::matchPropertyGroup($propertyGroup, $propertyValues, $cohortProperties, $flagsByKey, $evaluationCache, $distinctId); } - public static function matchPropertyGroup($propertyGroup, $propertyValues, $cohortProperties) + public static function matchPropertyGroup($propertyGroup, $propertyValues, $cohortProperties, $flagsByKey = null, $evaluationCache = null, $distinctId = null) { if (!$propertyGroup) { return true; @@ -130,7 +130,7 @@ public static function matchPropertyGroup($propertyGroup, $propertyValues, $coho // a nested property group foreach ($properties as $prop) { try { - $matches = FeatureFlag::matchPropertyGroup($prop, $propertyValues, $cohortProperties); + $matches = FeatureFlag::matchPropertyGroup($prop, $propertyValues, $cohortProperties, $flagsByKey, $evaluationCache, $distinctId); if ($propertyGroupType === 'AND') { if (!$matches) { return false; @@ -157,14 +157,9 @@ public static function matchPropertyGroup($propertyGroup, $propertyValues, $coho $matches = false; $propType = $prop["type"] ?? null; if ($propType === 'cohort') { - $matches = FeatureFlag::matchCohort($prop, $propertyValues, $cohortProperties); + $matches = FeatureFlag::matchCohort($prop, $propertyValues, $cohortProperties, $flagsByKey, $evaluationCache, $distinctId); } elseif ($propType === 'flag') { - error_log(sprintf( - "PostHog: Flag dependency filters are not supported in local evaluation. " . - "Skipping condition with dependency on flag '%s'", - $prop["key"] ?? "unknown" - )); - continue; + $matches = FeatureFlag::evaluateFlagDependency($prop, $flagsByKey, $evaluationCache, $distinctId, $propertyValues, $cohortProperties); } else { $matches = FeatureFlag::matchProperty($prop, $propertyValues); } @@ -189,6 +184,10 @@ public static function matchPropertyGroup($propertyGroup, $propertyValues, $coho } } } catch (InconclusiveMatchException $err) { + // If this is a flag dependency error, preserve the original message + if ($propType === 'flag') { + throw $err; + } $errorMatchingLocally = true; } } @@ -354,7 +353,7 @@ private static function compareFlagConditions($conditionA, $conditionB) } } - public static function matchFeatureFlagProperties($flag, $distinctId, $properties, $cohorts = []) + public static function matchFeatureFlagProperties($flag, $distinctId, $properties, $cohorts = [], $flagsByKey = null, $evaluationCache = null) { $flagConditions = ($flag["filters"] ?? [])["groups"] ?? []; $isInconclusive = false; @@ -390,7 +389,7 @@ function ($conditionA, $conditionB) { foreach ($flagConditionsWithIndexes as $conditionWithIndex) { $condition = $conditionWithIndex[0]; try { - if (FeatureFlag::isConditionMatch($flag, $distinctId, $condition, $properties, $cohorts)) { + if (FeatureFlag::isConditionMatch($flag, $distinctId, $condition, $properties, $cohorts, $flagsByKey, $evaluationCache)) { $variantOverride = $condition["variant"] ?? null; $flagVariants = (($flag["filters"] ?? [])["multivariate"] ?? [])["variants"] ?? []; $variantKeys = array_map(function ($variant) { @@ -404,6 +403,13 @@ function ($conditionA, $conditionB) { } } } catch (InconclusiveMatchException $e) { + // If this is a flag dependency error, preserve the original message + if ( + strpos($e->getMessage(), "Cannot evaluate flag dependency") !== false || + strpos($e->getMessage(), "Circular dependency detected") !== false + ) { + throw $e; + } $isInconclusive = true; } } @@ -415,7 +421,7 @@ function ($conditionA, $conditionB) { return false; } - private static function isConditionMatch($featureFlag, $distinctId, $condition, $properties, $cohorts) + private static function isConditionMatch($featureFlag, $distinctId, $condition, $properties, $cohorts, $flagsByKey = null, $evaluationCache = null) { $rolloutPercentage = array_key_exists("rollout_percentage", $condition) ? $condition["rollout_percentage"] : null; @@ -424,15 +430,9 @@ private static function isConditionMatch($featureFlag, $distinctId, $condition, $matches = false; $propertyType = $property['type'] ?? null; if ($propertyType == 'cohort') { - $matches = FeatureFlag::matchCohort($property, $properties, $cohorts); + $matches = FeatureFlag::matchCohort($property, $properties, $cohorts, $flagsByKey, $evaluationCache, $distinctId); } elseif ($propertyType == 'flag') { - error_log(sprintf( - "PostHog: Flag dependency filters are not supported in local evaluation. " . - "Skipping condition for flag '%s' with dependency on flag '%s'", - $featureFlag["key"] ?? "unknown", - $property["key"] ?? "unknown" - )); - continue; + $matches = FeatureFlag::evaluateFlagDependency($property, $flagsByKey, $evaluationCache, $distinctId, $properties, $cohorts); } else { $matches = FeatureFlag::matchProperty($property, $properties); } @@ -485,4 +485,139 @@ private static function prepareValueForRegex($value) return $regex; } + + public static function evaluateFlagDependency($property, $flagsByKey, $evaluationCache, $distinctId, $properties, $cohortProperties) + { + if ($flagsByKey === null || $evaluationCache === null) { + throw new InconclusiveMatchException(sprintf( + "Cannot evaluate flag dependency on '%s' without flags_by_key and evaluation_cache", + $property["key"] ?? "unknown" + )); + } + + // Check if dependency_chain is present - it should always be provided for flag dependencies + if (!array_key_exists("dependency_chain", $property)) { + // If no dependency_chain is provided, this is likely an old-style flag property + // that was meant to be skipped in the old implementation + throw new InconclusiveMatchException(sprintf( + "Cannot evaluate flag dependency on '%s' without flags_by_key and evaluation_cache", + $property["key"] ?? "unknown" + )); + } + + $dependencyChain = $property["dependency_chain"]; + + // Handle circular dependency (empty chain means circular) + if (count($dependencyChain) === 0) { + error_log(sprintf("Circular dependency detected for flag: %s", $property["key"] ?? "unknown")); + throw new InconclusiveMatchException(sprintf( + "Circular dependency detected for flag '%s'", + $property["key"] ?? "unknown" + )); + } + + // The flag key to evaluate is in the "key" field + $depFlagKey = $property["key"] ?? null; + if (!$depFlagKey) { + throw new InconclusiveMatchException(sprintf( + "Flag dependency missing 'key' field: %s", + json_encode($property) + )); + } + + // Check if we've already evaluated this flag + if (!array_key_exists($depFlagKey, $evaluationCache)) { + // Need to evaluate this dependency first + $depFlag = $flagsByKey[$depFlagKey] ?? null; + if (!$depFlag) { + // Missing flag dependency - cannot evaluate locally + $evaluationCache[$depFlagKey] = null; + throw new InconclusiveMatchException(sprintf( + "Cannot evaluate flag dependency '%s' - flag not found in local flags", + $depFlagKey + )); + } else { + // Check if the flag is active (same check as in Client::computeFlagLocally) + if (!($depFlag["active"] ?? false)) { + $evaluationCache[$depFlagKey] = false; + } else { + // Recursively evaluate the dependency + try { + $depResult = FeatureFlag::matchFeatureFlagProperties( + $depFlag, + $distinctId, + $properties, + $cohortProperties, + $flagsByKey, + $evaluationCache + ); + $evaluationCache[$depFlagKey] = $depResult; + } catch (InconclusiveMatchException $e) { + // If we can't evaluate a dependency, store null and propagate the error + $evaluationCache[$depFlagKey] = null; + throw new InconclusiveMatchException(sprintf( + "Cannot evaluate flag dependency '%s': %s", + $depFlagKey, + $e->getMessage() + )); + } + } + } + } + + // Get the evaluated flag value + $flagValue = $evaluationCache[$depFlagKey]; + if ($flagValue === null) { + // Previously inconclusive - raise error again + throw new InconclusiveMatchException(sprintf( + "Flag dependency '%s' was previously inconclusive", + $depFlagKey + )); + } + + // Now check if the flag value matches the expected value in the property + $expectedValue = $property["value"] ?? null; + $operator = $property["operator"] ?? "exact"; + + if ($expectedValue !== null) { + // For flag dependencies, we need to compare the actual flag result with expected value + // using the flag_evaluates_to operator logic + if ($operator === "flag_evaluates_to") { + return FeatureFlag::matchesDependencyValue($expectedValue, $flagValue); + } else { + // This should never happen, but just to be defensive + throw new InconclusiveMatchException(sprintf( + "Flag dependency property for '%s' has invalid operator '%s'", + $depFlagKey, + $operator + )); + } + } + + // If no value check needed, return true (all dependencies passed) + return true; + } + + public static function matchesDependencyValue($expectedValue, $actualValue) + { + // String variant case - check for exact match or boolean true + if (is_string($actualValue) && strlen($actualValue) > 0) { + if (is_bool($expectedValue)) { + // Any variant matches boolean true + return $expectedValue; + } elseif (is_string($expectedValue)) { + // variants are case-sensitive, hence our comparison is too + return $actualValue === $expectedValue; + } else { + return false; + } + } + // Boolean case - must match expected boolean value + elseif (is_bool($actualValue) && is_bool($expectedValue)) { + return $actualValue === $expectedValue; + } + + // Default case + return false; + } } diff --git a/test/FeatureFlagLocalEvaluationTest.php b/test/FeatureFlagLocalEvaluationTest.php index eb02121..8fafa0f 100644 --- a/test/FeatureFlagLocalEvaluationTest.php +++ b/test/FeatureFlagLocalEvaluationTest.php @@ -3791,9 +3791,7 @@ public function testMultivariateFlagConsistency() public function testFeatureFlagsWithFlagDependencies(): void { - global $errorMessages; - - // Test flag dependency in matchPropertyGroup + // Test flag dependency evaluation without required context throws exception $propertyGroup = [ "type" => "AND", "values" => [ @@ -3803,20 +3801,18 @@ public function testFeatureFlagsWithFlagDependencies(): void ]; $properties = ["email" => "test@example.com"]; - $result = FeatureFlag::matchPropertyGroup($propertyGroup, $properties, []); - - // Should return true because AND group continues evaluation when flag dependencies are skipped - $this->assertTrue($result); - // Check that a warning was logged - $this->assertCount(1, $errorMessages); - $this->assertStringContainsString("Flag dependency filters are not supported in local evaluation", $errorMessages[0]); - $this->assertStringContainsString("parent-flag", $errorMessages[0]); - - // Reset error messages - $errorMessages = []; + // Should throw InconclusiveMatchException because flag dependencies cannot be evaluated without flags_by_key + $threwException = false; + try { + FeatureFlag::matchPropertyGroup($propertyGroup, $properties, []); + } catch (InconclusiveMatchException $e) { + $this->assertStringContainsString("Cannot evaluate flag dependency on 'parent-flag' without flags_by_key and evaluation_cache", $e->getMessage()); + $threwException = true; + } + $this->assertTrue($threwException, "Expected InconclusiveMatchException was not thrown"); - // Test flag dependency in isConditionMatch via matchFeatureFlagProperties + // Test flag dependency via matchFeatureFlagProperties $flag = [ "key" => "test-flag", "filters" => [ @@ -3833,42 +3829,15 @@ public function testFeatureFlagsWithFlagDependencies(): void ]; $properties = ["name" => "test"]; - $result = FeatureFlag::matchFeatureFlagProperties($flag, "test-user", $properties); - - // Should return true because the other condition matches - $this->assertTrue($result); - - // Check that a warning was logged - $this->assertCount(1, $errorMessages); - $this->assertStringContainsString("Flag dependency filters are not supported in local evaluation", $errorMessages[0]); - $this->assertStringContainsString("test-flag", $errorMessages[0]); - $this->assertStringContainsString("dependency-flag", $errorMessages[0]); - - // Reset error messages - $errorMessages = []; - - // Test that evaluation continues when only flag dependencies exist - $flagOnlyDependency = [ - "key" => "only-flag-dep", - "filters" => [ - "groups" => [ - [ - "properties" => [ - ["type" => "flag", "key" => "parent-flag-only", "value" => true] - ], - "rollout_percentage" => 100 - ] - ] - ] - ]; - $result = FeatureFlag::matchFeatureFlagProperties($flagOnlyDependency, "test-user", []); - - // Should return true due to rollout percentage - $this->assertTrue($result); - - // Check that a warning was logged - $this->assertCount(1, $errorMessages); - $this->assertStringContainsString("parent-flag-only", $errorMessages[0]); + // Should also throw InconclusiveMatchException because flag dependencies need context + $threwException = false; + try { + FeatureFlag::matchFeatureFlagProperties($flag, "test-user", $properties); + } catch (InconclusiveMatchException $e) { + $this->assertStringContainsString("Cannot evaluate flag dependency", $e->getMessage()); + $threwException = true; + } + $this->assertTrue($threwException, "Expected InconclusiveMatchException was not thrown"); } } diff --git a/test/FlagDependencyIntegrationTest.php b/test/FlagDependencyIntegrationTest.php new file mode 100644 index 0000000..1290edd --- /dev/null +++ b/test/FlagDependencyIntegrationTest.php @@ -0,0 +1,130 @@ +featureFlags = [ + [ + "id" => 1, + "name" => "Base Flag", + "key" => "base-flag", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + [ + "key" => "email", + "operator" => "icontains", + "value" => "@company.com", + "type" => "person" + ] + ], + "rollout_percentage" => 100, + ] + ] + ] + ], + [ + "id" => 2, + "name" => "Dependent Flag", + "key" => "dependent-flag", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + [ + "key" => "base-flag", + "operator" => "flag_evaluates_to", + "value" => true, + "type" => "flag", + "dependency_chain" => ["base-flag"], + ], + [ + "key" => "role", + "operator" => "exact", + "value" => "admin", + "type" => "person" + ] + ], + "rollout_percentage" => 100, + ] + ] + ] + ] + ]; + + // Build flags by key dictionary + $client->featureFlagsByKey = []; + foreach ($client->featureFlags as $flag) { + $client->featureFlagsByKey[$flag['key']] = $flag; + } + + // Test 1: When both base flag and dependent conditions are satisfied + $result = $client->getFeatureFlag( + "dependent-flag", + "test-user", + [], + ["email" => "admin@company.com", "role" => "admin"], + [], + true // only_evaluate_locally + ); + $this->assertTrue($result); + + // Test 2: When base flag condition is satisfied but dependent condition is not + $result = $client->getFeatureFlag( + "dependent-flag", + "test-user-2", + [], + ["email" => "user@company.com", "role" => "user"], // role is not admin + [], + true // only_evaluate_locally + ); + $this->assertFalse($result); + + // Test 3: When base flag condition is not satisfied + $result = $client->getFeatureFlag( + "dependent-flag", + "test-user-3", + [], + ["email" => "user@external.com", "role" => "admin"], // email domain doesn't match + [], + true // only_evaluate_locally + ); + $this->assertFalse($result); + + // Test 4: Test getAllFlags with dependencies + $allFlags = $client->getAllFlags( + "test-user", + [], + ["email" => "admin@company.com", "role" => "admin"], + [], + true // only_evaluate_locally + ); + + $this->assertTrue($allFlags["base-flag"]); + $this->assertTrue($allFlags["dependent-flag"]); + + // Test 5: Test getAllFlags when dependency fails + $allFlags = $client->getAllFlags( + "test-user-external", + [], + ["email" => "admin@external.com", "role" => "admin"], // email domain doesn't match base flag + [], + true // only_evaluate_locally + ); + + $this->assertFalse($allFlags["base-flag"]); + $this->assertFalse($allFlags["dependent-flag"]); + } +} diff --git a/test/FlagDependencyTest.php b/test/FlagDependencyTest.php new file mode 100644 index 0000000..538d3d8 --- /dev/null +++ b/test/FlagDependencyTest.php @@ -0,0 +1,824 @@ + 1, + "name" => "Flag A", + "key" => "flag-a", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + ["key" => "email", "operator" => "icontains", "value" => "@example.com", "type" => "person"] + ], + "rollout_percentage" => 100, + ] + ] + ] + ]; + + $flagB = [ + "id" => 2, + "name" => "Flag B", + "key" => "flag-b", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + [ + "key" => "flag-a", + "operator" => "flag_evaluates_to", + "value" => true, + "type" => "flag", + "dependency_chain" => ["flag-a"], + ] + ], + "rollout_percentage" => 100, + ] + ] + ] + ]; + + $flagsByKey = [ + "flag-a" => $flagA, + "flag-b" => $flagB + ]; + + $evaluationCache = []; + + // Test when dependency is satisfied + $result = FeatureFlag::matchFeatureFlagProperties( + $flagB, + "test-user", + ["email" => "test@example.com"], + [], + $flagsByKey, + $evaluationCache + ); + $this->assertTrue($result); + + // Test when dependency is not satisfied + $evaluationCache = []; // Reset cache + $result = FeatureFlag::matchFeatureFlagProperties( + $flagB, + "test-user-2", + ["email" => "test@other.com"], + [], + $flagsByKey, + $evaluationCache + ); + $this->assertFalse($result); + } + + public function testFlagDependencyCircularDependency(): void + { + // Test circular dependency handling: flag-a depends on flag-b, flag-b depends on flag-a + $flagA = [ + "id" => 1, + "name" => "Flag A", + "key" => "flag-a", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + [ + "key" => "flag-b", + "operator" => "flag_evaluates_to", + "value" => true, + "type" => "flag", + "dependency_chain" => [], // Empty chain indicates circular dependency + ] + ], + "rollout_percentage" => 100, + ] + ] + ] + ]; + + $flagB = [ + "id" => 2, + "name" => "Flag B", + "key" => "flag-b", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + [ + "key" => "flag-a", + "operator" => "flag_evaluates_to", + "value" => true, + "type" => "flag", + "dependency_chain" => [], // Empty chain indicates circular dependency + ] + ], + "rollout_percentage" => 100, + ] + ] + ] + ]; + + $flagsByKey = [ + "flag-a" => $flagA, + "flag-b" => $flagB + ]; + + $evaluationCache = []; + + // Both flags should raise InconclusiveMatchException due to circular dependency + $this->expectException(InconclusiveMatchException::class); + $this->expectExceptionMessage("Circular dependency detected for flag 'flag-b'"); + FeatureFlag::matchFeatureFlagProperties($flagA, "test-user", [], [], $flagsByKey, $evaluationCache); + } + + public function testFlagDependencyMissingFlag(): void + { + // Test handling of missing flag dependency + $flag = [ + "id" => 1, + "name" => "Flag A", + "key" => "flag-a", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + [ + "key" => "non-existent-flag", + "operator" => "flag_evaluates_to", + "value" => true, + "type" => "flag", + "dependency_chain" => ["non-existent-flag"], + ] + ], + "rollout_percentage" => 100, + ] + ] + ] + ]; + + $flagsByKey = [ + "flag-a" => $flag + ]; + + $evaluationCache = []; + + // Should raise InconclusiveMatchException because dependency doesn't exist + $this->expectException(InconclusiveMatchException::class); + $this->expectExceptionMessage( + "Cannot evaluate flag dependency 'non-existent-flag' - flag not found in local flags" + ); + FeatureFlag::matchFeatureFlagProperties($flag, "test-user", [], [], $flagsByKey, $evaluationCache); + } + + public function testFlagDependencyComplexChain(): void + { + // Test complex dependency chain: flag-d -> flag-c -> [flag-a, flag-b] + $flagA = [ + "id" => 1, + "name" => "Flag A", + "key" => "flag-a", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + ["key" => "email", "operator" => "icontains", "value" => "@example.com", "type" => "person"] + ], + "rollout_percentage" => 100, + ] + ] + ] + ]; + + $flagB = [ + "id" => 2, + "name" => "Flag B", + "key" => "flag-b", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + ["key" => "name", "operator" => "exact", "value" => "test", "type" => "person"] + ], + "rollout_percentage" => 100, + ] + ] + ] + ]; + + $flagC = [ + "id" => 3, + "name" => "Flag C", + "key" => "flag-c", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + [ + "key" => "flag-a", + "operator" => "flag_evaluates_to", + "value" => true, + "type" => "flag", + "dependency_chain" => ["flag-a"], + ], + [ + "key" => "flag-b", + "operator" => "flag_evaluates_to", + "value" => true, + "type" => "flag", + "dependency_chain" => ["flag-b"], + ], + ], + "rollout_percentage" => 100, + ] + ] + ] + ]; + + $flagD = [ + "id" => 4, + "name" => "Flag D", + "key" => "flag-d", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + [ + "key" => "flag-c", + "operator" => "flag_evaluates_to", + "value" => true, + "type" => "flag", + "dependency_chain" => ["flag-a", "flag-b", "flag-c"], + ] + ], + "rollout_percentage" => 100, + ] + ] + ] + ]; + + $flagsByKey = [ + "flag-a" => $flagA, + "flag-b" => $flagB, + "flag-c" => $flagC, + "flag-d" => $flagD + ]; + + $evaluationCache = []; + + // Test when all dependencies are satisfied + $result = FeatureFlag::matchFeatureFlagProperties( + $flagD, + "test-user", + ["email" => "test@example.com", "name" => "test"], + [], + $flagsByKey, + $evaluationCache + ); + $this->assertTrue($result); + + // Test when one dependency fails + $evaluationCache = []; // Reset cache + $result = FeatureFlag::matchFeatureFlagProperties( + $flagD, + "test-user-2", + ["email" => "test@other.com", "name" => "test"], // email doesn't match flag-a + [], + $flagsByKey, + $evaluationCache + ); + $this->assertFalse($result); + } + + public function testFlagDependencyMixedConditions(): void + { + // Test flag dependency mixed with other property conditions + $baseFlag = [ + "id" => 1, + "name" => "Base Flag", + "key" => "base-flag", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + ["key" => "region", "operator" => "exact", "value" => "us", "type" => "person"] + ], + "rollout_percentage" => 100, + ] + ] + ] + ]; + + $mixedFlag = [ + "id" => 2, + "name" => "Mixed Flag", + "key" => "mixed-flag", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + [ + "key" => "base-flag", + "operator" => "flag_evaluates_to", + "value" => true, + "type" => "flag", + "dependency_chain" => ["base-flag"], + ], + ["key" => "email", "operator" => "icontains", "value" => "@example.com", "type" => "person"] + ], + "rollout_percentage" => 100, + ] + ] + ] + ]; + + $flagsByKey = [ + "base-flag" => $baseFlag, + "mixed-flag" => $mixedFlag + ]; + + $evaluationCache = []; + + // Both flag dependency and email condition satisfied + $result = FeatureFlag::matchFeatureFlagProperties( + $mixedFlag, + "test-user", + ["email" => "test@example.com", "region" => "us"], + [], + $flagsByKey, + $evaluationCache + ); + $this->assertTrue($result); + + // Flag dependency satisfied but email condition not satisfied + $evaluationCache = []; // Reset cache + $result = FeatureFlag::matchFeatureFlagProperties( + $mixedFlag, + "test-user-2", + ["email" => "test@other.com", "region" => "us"], + [], + $flagsByKey, + $evaluationCache + ); + $this->assertFalse($result); + + // Email condition satisfied but flag dependency not satisfied + $evaluationCache = []; // Reset cache + $result = FeatureFlag::matchFeatureFlagProperties( + $mixedFlag, + "test-user-3", + ["email" => "test@example.com", "region" => "eu"], // region doesn't match base-flag + [], + $flagsByKey, + $evaluationCache + ); + $this->assertFalse($result); + } + + public function testMatchesDependencyValue(): void + { + // Test the matches_dependency_value function logic + + // String variant matches string exactly (case-sensitive) + $this->assertTrue(FeatureFlag::matchesDependencyValue("control", "control")); + $this->assertTrue(FeatureFlag::matchesDependencyValue("Control", "Control")); + $this->assertFalse(FeatureFlag::matchesDependencyValue("control", "Control")); + $this->assertFalse(FeatureFlag::matchesDependencyValue("Control", "CONTROL")); + $this->assertFalse(FeatureFlag::matchesDependencyValue("control", "test")); + + // String variant matches boolean true (any variant is truthy) + $this->assertTrue(FeatureFlag::matchesDependencyValue(true, "control")); + $this->assertTrue(FeatureFlag::matchesDependencyValue(true, "test")); + $this->assertFalse(FeatureFlag::matchesDependencyValue(false, "control")); + + // Boolean matches boolean exactly + $this->assertTrue(FeatureFlag::matchesDependencyValue(true, true)); + $this->assertTrue(FeatureFlag::matchesDependencyValue(false, false)); + $this->assertFalse(FeatureFlag::matchesDependencyValue(false, true)); + $this->assertFalse(FeatureFlag::matchesDependencyValue(true, false)); + + // Empty string doesn't match + $this->assertFalse(FeatureFlag::matchesDependencyValue(true, "")); + $this->assertFalse(FeatureFlag::matchesDependencyValue("control", "")); + + // Type mismatches + $this->assertFalse(FeatureFlag::matchesDependencyValue(123, "control")); + $this->assertFalse(FeatureFlag::matchesDependencyValue("control", 123)); + } + + public function testProductionStyleMultivariateDependencyChain(): void + { + // Test production-style multivariate dependency chain: + // multivariate-root-flag -> multivariate-intermediate-flag -> multivariate-leaf-flag + $client = new Client("fake-api-key", [], null, "fake-personal-api-key", false); + $client->featureFlags = [ + // Leaf flag: multivariate with fruit variants + [ + "id" => 451, + "name" => "Multivariate Leaf Flag (Base)", + "key" => "multivariate-leaf-flag", + "active" => true, + "rollout_percentage" => 100, + "filters" => [ + "groups" => [ + [ + "properties" => [ + [ + "key" => "email", + "type" => "person", + "value" => ["pineapple@example.com"], + "operator" => "exact", + ] + ], + "rollout_percentage" => 100, + "variant" => "pineapple", + ], + [ + "properties" => [ + [ + "key" => "email", + "type" => "person", + "value" => ["mango@example.com"], + "operator" => "exact", + ] + ], + "rollout_percentage" => 100, + "variant" => "mango", + ], + [ + "properties" => [ + [ + "key" => "email", + "type" => "person", + "value" => ["papaya@example.com"], + "operator" => "exact", + ] + ], + "rollout_percentage" => 100, + "variant" => "papaya", + ], + [ + "properties" => [ + [ + "key" => "email", + "type" => "person", + "value" => ["kiwi@example.com"], + "operator" => "exact", + ] + ], + "rollout_percentage" => 100, + "variant" => "kiwi", + ], + [ + "properties" => [], + "rollout_percentage" => 0, // Force default to false for unknown emails + ], + ], + "multivariate" => [ + "variants" => [ + ["key" => "pineapple", "rollout_percentage" => 25], + ["key" => "mango", "rollout_percentage" => 25], + ["key" => "papaya", "rollout_percentage" => 25], + ["key" => "kiwi", "rollout_percentage" => 25], + ] + ], + ], + ], + // Intermediate flag: multivariate with color variants, depends on fruit + [ + "id" => 467, + "name" => "Multivariate Intermediate Flag (Depends on fruit)", + "key" => "multivariate-intermediate-flag", + "active" => true, + "rollout_percentage" => 100, + "filters" => [ + "groups" => [ + [ + "properties" => [ + [ + "key" => "multivariate-leaf-flag", + "type" => "flag", + "value" => "pineapple", + "operator" => "flag_evaluates_to", + "dependency_chain" => ["multivariate-leaf-flag"], + ] + ], + "rollout_percentage" => 100, + "variant" => "blue", + ], + [ + "properties" => [ + [ + "key" => "multivariate-leaf-flag", + "type" => "flag", + "value" => "mango", + "operator" => "flag_evaluates_to", + "dependency_chain" => ["multivariate-leaf-flag"], + ] + ], + "rollout_percentage" => 100, + "variant" => "red", + ], + ], + "multivariate" => [ + "variants" => [ + ["key" => "blue", "rollout_percentage" => 100], + ["key" => "red", "rollout_percentage" => 0], + ["key" => "green", "rollout_percentage" => 0], + ["key" => "black", "rollout_percentage" => 0], + ] + ], + ], + ], + // Root flag: multivariate with show variants, depends on color + [ + "id" => 468, + "name" => "Multivariate Root Flag (Depends on color)", + "key" => "multivariate-root-flag", + "active" => true, + "rollout_percentage" => 100, + "filters" => [ + "groups" => [ + [ + "properties" => [ + [ + "key" => "multivariate-intermediate-flag", + "type" => "flag", + "value" => "blue", + "operator" => "flag_evaluates_to", + "dependency_chain" => [ + "multivariate-leaf-flag", + "multivariate-intermediate-flag", + ], + ] + ], + "rollout_percentage" => 100, + "variant" => "breaking-bad", + ], + [ + "properties" => [ + [ + "key" => "multivariate-intermediate-flag", + "type" => "flag", + "value" => "red", + "operator" => "flag_evaluates_to", + "dependency_chain" => [ + "multivariate-leaf-flag", + "multivariate-intermediate-flag", + ], + ] + ], + "rollout_percentage" => 100, + "variant" => "the-wire", + ], + ], + "multivariate" => [ + "variants" => [ + ["key" => "breaking-bad", "rollout_percentage" => 100], + ["key" => "the-wire", "rollout_percentage" => 0], + ["key" => "game-of-thrones", "rollout_percentage" => 0], + ["key" => "the-expanse", "rollout_percentage" => 0], + ] + ], + ], + ], + ]; + $client->featureFlagsByKey = []; + foreach ($client->featureFlags as $flag) { + $client->featureFlagsByKey[$flag['key']] = $flag; + } + + // Test successful pineapple -> blue -> breaking-bad chain + $leafResult = $client->getFeatureFlag( + "multivariate-leaf-flag", + "test-user", + [], + ["email" => "pineapple@example.com"], + [], + true + ); + $intermediateResult = $client->getFeatureFlag( + "multivariate-intermediate-flag", + "test-user", + [], + ["email" => "pineapple@example.com"], + [], + true + ); + $rootResult = $client->getFeatureFlag( + "multivariate-root-flag", + "test-user", + [], + ["email" => "pineapple@example.com"], + [], + true + ); + $this->assertEquals("pineapple", $leafResult); + $this->assertEquals("blue", $intermediateResult); + $this->assertEquals("breaking-bad", $rootResult); + + // Test successful mango -> red -> the-wire chain + $mangoLeafResult = $client->getFeatureFlag( + "multivariate-leaf-flag", + "test-user", + [], + ["email" => "mango@example.com"], + [], + true + ); + $mangoIntermediateResult = $client->getFeatureFlag( + "multivariate-intermediate-flag", + "test-user", + [], + ["email" => "mango@example.com"], + [], + true + ); + $mangoRootResult = $client->getFeatureFlag( + "multivariate-root-flag", + "test-user", + [], + ["email" => "mango@example.com"], + [], + true + ); + $this->assertEquals("mango", $mangoLeafResult); + $this->assertEquals("red", $mangoIntermediateResult); + $this->assertEquals("the-wire", $mangoRootResult); + + // Test broken chain - user without matching email gets default/false results + $unknownLeafResult = $client->getFeatureFlag( + "multivariate-leaf-flag", + "test-user", + [], + ["email" => "unknown@example.com"], + [], + true + ); + $unknownIntermediateResult = $client->getFeatureFlag( + "multivariate-intermediate-flag", + "test-user", + [], + ["email" => "unknown@example.com"], + [], + true + ); + $unknownRootResult = $client->getFeatureFlag( + "multivariate-root-flag", + "test-user", + [], + ["email" => "unknown@example.com"], + [], + true + ); + $this->assertEquals(false, $unknownLeafResult); // No matching email -> null variant -> false + $this->assertEquals(false, $unknownIntermediateResult); // Dependency not satisfied + $this->assertEquals(false, $unknownRootResult); // Chain broken + } + + public function testMultiLevelMultivariateDependencyChain(): void + { + // Test multi-level multivariate dependency chain: dependent-flag -> intermediate-flag -> leaf-flag + + // Leaf flag: multivariate with "control" and "test" variants using person property overrides + $leafFlag = [ + "id" => 1, + "name" => "Leaf Flag", + "key" => "leaf-flag", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + ["key" => "email", "operator" => "icontains", "value" => "@example.com", "type" => "person"] + ], + "rollout_percentage" => 100, + "variant" => "test", + ], + [ + "properties" => [], + "rollout_percentage" => 100, + "variant" => "control", + ] + ], + "multivariate" => [ + "variants" => [ + ["key" => "control", "name" => "Control", "rollout_percentage" => 50], + ["key" => "test", "name" => "Test", "rollout_percentage" => 50] + ] + ] + ] + ]; + + // Intermediate flag: depends on leaf flag being "control" variant + $intermediateFlag = [ + "id" => 2, + "name" => "Intermediate Flag", + "key" => "intermediate-flag", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + [ + "key" => "leaf-flag", + "operator" => "flag_evaluates_to", + "value" => "control", + "type" => "flag", + "dependency_chain" => ["leaf-flag"], + ], + ["key" => "variant_type", "operator" => "exact", "value" => "blue", "type" => "person"] + ], + "rollout_percentage" => 100, + "variant" => "blue", + ], + [ + "properties" => [ + [ + "key" => "leaf-flag", + "operator" => "flag_evaluates_to", + "value" => "control", + "type" => "flag", + "dependency_chain" => ["leaf-flag"], + ], + ["key" => "variant_type", "operator" => "exact", "value" => "green", "type" => "person"] + ], + "rollout_percentage" => 100, + "variant" => "green", + ] + ], + "multivariate" => [ + "variants" => [ + ["key" => "blue", "name" => "Blue", "rollout_percentage" => 50], + ["key" => "green", "name" => "Green", "rollout_percentage" => 50] + ] + ] + ] + ]; + + $flagsByKey = [ + "leaf-flag" => $leafFlag, + "intermediate-flag" => $intermediateFlag + ]; + + $evaluationCache = []; + + // Test 1: Leaf flag should evaluate to "control" when email condition is not satisfied + $leafResult = FeatureFlag::matchFeatureFlagProperties( + $leafFlag, + "user-with-control-variant", + ["email" => "test@other.com"], // This won't match @example.com condition + [], + $flagsByKey, + $evaluationCache + ); + // Since email doesn't match, it should fall back to the second condition which has variant "control" + $this->assertEquals("control", $leafResult); + + // Test 2: Intermediate flag should evaluate to "blue" when dependency is satisfied and variant_type is "blue" + $evaluationCache = []; // Reset cache + $intermediateResult = FeatureFlag::matchFeatureFlagProperties( + $intermediateFlag, + "user-with-control-variant", + // email doesn't match, so leaf-flag="control", variant_type="blue" + ["email" => "test@other.com", "variant_type" => "blue"], + [], + $flagsByKey, + $evaluationCache + ); + $this->assertEquals("blue", $intermediateResult); + + // Test 3: Intermediate flag should evaluate to false when leaf dependency fails + $evaluationCache = []; // Reset cache + $intermediateResult = FeatureFlag::matchFeatureFlagProperties( + $intermediateFlag, + "user-with-test-variant", + // This makes leaf-flag="test", breaking dependency + ["email" => "test@example.com", "variant_type" => "blue"], + [], + $flagsByKey, + $evaluationCache + ); + $this->assertFalse($intermediateResult); + } +} diff --git a/test/MultivariateIntegrationTest.php b/test/MultivariateIntegrationTest.php new file mode 100644 index 0000000..c667ddf --- /dev/null +++ b/test/MultivariateIntegrationTest.php @@ -0,0 +1,189 @@ + 1, + "name" => "Leaf Multivariate Flag", + "key" => "leaf-mv-flag", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [], + "rollout_percentage" => 100, + ] + ], + "multivariate" => [ + "variants" => [ + ["key" => "variant-a", "name" => "Variant A", "rollout_percentage" => 33], + ["key" => "variant-b", "name" => "Variant B", "rollout_percentage" => 33], + ["key" => "variant-c", "name" => "Variant C", "rollout_percentage" => 34] + ] + ] + ] + ]; + + // Dependent flag: depends on specific variant of leaf flag + $dependentFlag = [ + "id" => 2, + "name" => "Multivariate Dependent Flag", + "key" => "dependent-mv-flag", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + [ + "key" => "leaf-mv-flag", + "operator" => "flag_evaluates_to", + "value" => "variant-a", // Only true when leaf flag returns variant-a + "type" => "flag", + "dependency_chain" => ["leaf-mv-flag"], + ] + ], + "rollout_percentage" => 100, + "variant" => "special-variant", + ] + ], + "multivariate" => [ + "variants" => [ + ["key" => "special-variant", "name" => "Special Variant", "rollout_percentage" => 100] + ] + ] + ] + ]; + + $client->featureFlags = [$leafFlag, $dependentFlag]; + $client->featureFlagsByKey = [ + "leaf-mv-flag" => $leafFlag, + "dependent-mv-flag" => $dependentFlag + ]; + + // Test with different user IDs to get different variants + // We'll test a few different user IDs until we find one that gets variant-a + $foundVariantA = false; + $foundOtherVariant = false; + + for ($i = 0; $i < 100; $i++) { + $userId = "test-user-{$i}"; + + $leafResult = $client->getFeatureFlag( + "leaf-mv-flag", + $userId, + [], + [], + [], + true // only_evaluate_locally + ); + + $dependentResult = $client->getFeatureFlag( + "dependent-mv-flag", + $userId, + [], + [], + [], + true // only_evaluate_locally + ); + + if ($leafResult === "variant-a") { + // When leaf flag is variant-a, dependent should be "special-variant" + $this->assertEquals("special-variant", $dependentResult); + $foundVariantA = true; + } else { + // When leaf flag is NOT variant-a, dependent should be false + $this->assertFalse($dependentResult); + $foundOtherVariant = true; + } + + if ($foundVariantA && $foundOtherVariant) { + break; // We've tested both cases + } + } + + // Make sure we tested both scenarios + $this->assertTrue($foundVariantA, "Should have found at least one user that gets variant-a"); + $this->assertTrue($foundOtherVariant, "Should have found at least one user that gets other variants"); + } + + public function testBooleanFlagDependencyOnMultivariate(): void + { + // Test a boolean flag that depends on any variant of a multivariate flag + $client = new Client("fake-api-key", [], null, null, false); + + $multivariateFlag = [ + "id" => 1, + "name" => "Multivariate Base", + "key" => "mv-base", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [], + "rollout_percentage" => 100, + ] + ], + "multivariate" => [ + "variants" => [ + ["key" => "control", "name" => "Control", "rollout_percentage" => 50], + ["key" => "test", "name" => "Test", "rollout_percentage" => 50] + ] + ] + ] + ]; + + $booleanFlag = [ + "id" => 2, + "name" => "Boolean Dependent", + "key" => "boolean-dependent", + "active" => true, + "filters" => [ + "groups" => [ + [ + "properties" => [ + [ + "key" => "mv-base", + "operator" => "flag_evaluates_to", + "value" => true, // Any variant should satisfy this + "type" => "flag", + "dependency_chain" => ["mv-base"], + ] + ], + "rollout_percentage" => 100, + ] + ] + ] + ]; + + $client->featureFlags = [$multivariateFlag, $booleanFlag]; + $client->featureFlagsByKey = [ + "mv-base" => $multivariateFlag, + "boolean-dependent" => $booleanFlag + ]; + + // Test several users - all should have boolean flag true since multivariate always returns a variant + for ($i = 0; $i < 10; $i++) { + $userId = "user-{$i}"; + + $mvResult = $client->getFeatureFlag("mv-base", $userId, [], [], [], true); + $booleanResult = $client->getFeatureFlag("boolean-dependent", $userId, [], [], [], true); + + // Multivariate should always return either "control" or "test" + $this->assertContains($mvResult, ["control", "test"]); + + // Boolean dependent should always be true because any variant satisfies boolean true + $this->assertTrue($booleanResult); + } + } +} From 730365c3f9e60cb04d8e0c3de09765915116b4bb Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Tue, 26 Aug 2025 09:58:56 -0700 Subject: [PATCH 2/7] Add support for .env to example.php And add flag dependencies examples. --- .env.example | 11 ++ example.php | 480 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 454 insertions(+), 37 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..15ad669 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# PostHog API Configuration +# Copy this file to .env and update with your actual values + +# Your project API key (found on the /setup page in PostHog) +POSTHOG_PROJECT_API_KEY=phc_your_project_api_key_here + +# Your personal API key (for local evaluation and other advanced features) +POSTHOG_PERSONAL_API_KEY=phx_your_personal_api_key_here + +# PostHog host URL (remove this line if using posthog.com) +POSTHOG_HOST=https://app.posthog.com \ No newline at end of file diff --git a/example.php b/example.php index 457c97f..e4ed686 100644 --- a/example.php +++ b/example.php @@ -1,54 +1,460 @@ $host, + 'debug' => false, + 'ssl' => !str_starts_with($host, 'http://') // Use SSL unless explicitly http:// + ], + null, + $personalApiKey + ); + + // Test by attempting to get feature flags (this validates both keys) + $testFlags = PostHog::getAllFlags("test_user", [], [], [], true); -PostHog::init( - PROJECT_API_KEY, - array('host' => 'https://app.posthog.com', 'debug' => true), - null, - PERSONAL_API_KEY -); + // If we get here without exception, credentials work + echo "✅ Authentication successful!\n"; + echo " Project API Key: " . substr($projectKey, 0, 9) . "...\n"; + echo " Personal API Key: [REDACTED]\n"; + echo " Host: $host\n\n\n"; +} catch (Exception $e) { + echo "❌ Authentication failed!\n"; + echo " Error: " . $e->getMessage() . "\n"; + echo "\n Please check your credentials:\n"; + echo " - POSTHOG_PROJECT_API_KEY: Project API key from PostHog settings\n"; + echo " - POSTHOG_PERSONAL_API_KEY: Personal API key (required for local evaluation)\n"; + echo " - POSTHOG_HOST: Your PostHog instance URL\n"; + exit(1); +} +// Display menu and get user choice +echo "🚀 PostHog PHP SDK Demo - Choose an example to run:\n\n"; +echo "1. Identify and capture examples\n"; +echo "2. Feature flag local evaluation examples\n"; +echo "3. Feature flag dependencies examples\n"; +echo "4. Context management and tagging examples\n"; +echo "5. Run all examples\n"; +echo "6. Exit\n"; +$choice = trim(readline("\nEnter your choice (1-6): ")); -# Capture an event -PostHog::capture( - [ +function identifyAndCaptureExamples() { + echo "\n" . str_repeat("=", 60) . "\n"; + echo "IDENTIFY AND CAPTURE EXAMPLES\n"; + echo str_repeat("=", 60) . "\n"; + + // Enable debug for this section + PostHog::init( + $_ENV['POSTHOG_PROJECT_API_KEY'], + [ + 'host' => $_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', + 'debug' => true, + 'ssl' => !str_starts_with($_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'http://') + ], + null, + $_ENV['POSTHOG_PERSONAL_API_KEY'] + ); + + // Capture an event + echo "📊 Capturing events...\n"; + PostHog::capture([ 'distinctId' => 'distinct_id', 'event' => 'event', 'properties' => [ 'property1' => 'value', 'property2' => 'value', ], - // 'groups' => [ - // 'org' => 123 - // ], - // 'sendFeatureFlags' => true - // 'sendFeatureFlags' => true 'send_feature_flags' => true - ] -); - -// PostHog::capture( -// [ -// 'distinctId' => 'distinct_id', -// 'event' => 'event2', -// 'properties' => [ -// 'property1' => 'value', -// 'property2' => 'value', -// ], -// // 'groups' => [ -// // 'org' => 123 -// // ], -// 'sendFeatureFlags' => false -// ] -// ); - -$enabled = PostHog::getFeatureFlag("first", "user_2311144"); - -echo $enabled; \ No newline at end of file + ]); + + // Alias a previous distinct id with a new one + echo "🔗 Creating alias...\n"; + PostHog::alias([ + 'distinctId' => 'distinct_id', + 'alias' => 'new_distinct_id' + ]); + + PostHog::capture([ + 'distinctId' => 'new_distinct_id', + 'event' => 'event2', + 'properties' => [ + 'property1' => 'value', + 'property2' => 'value', + ] + ]); + + PostHog::capture([ + 'distinctId' => 'new_distinct_id', + 'event' => 'event-with-groups', + 'properties' => [ + 'property1' => 'value', + 'property2' => 'value', + ], + 'groups' => ['company' => 'id:5'] + ]); + + // Add properties to the person + echo "👤 Identifying user...\n"; + PostHog::identify([ + 'distinctId' => 'new_distinct_id', + 'properties' => ['email' => 'something@something.com'] + ]); + + echo "✅ Identify and capture examples completed!\n"; +} + +function featureFlagExamples() { + echo "\n" . str_repeat("=", 60) . "\n"; + echo "FEATURE FLAG LOCAL EVALUATION EXAMPLES\n"; + echo str_repeat("=", 60) . "\n"; + + // Disable debug for cleaner output + PostHog::init( + $_ENV['POSTHOG_PROJECT_API_KEY'], + [ + 'host' => $_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', + 'debug' => false, + 'ssl' => !str_starts_with($_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'http://') + ], + null, + $_ENV['POSTHOG_PERSONAL_API_KEY'] + ); + + echo "🚩 Getting individual feature flags...\n"; + + // Test different users to see different results + $users = ['user_1', 'user_2', 'user_3']; + + foreach ($users as $user) { + $flags = PostHog::getAllFlags($user, [], [], [], true); + echo "User $user flags: " . json_encode($flags, JSON_PRETTY_PRINT) . "\n"; + + // Get a specific flag + if (!empty($flags)) { + $firstFlag = array_key_first($flags); + $flagValue = PostHog::getFeatureFlag($firstFlag, $user, [], [], [], true); + echo "Flag '$firstFlag' for $user: " . ($flagValue ? json_encode($flagValue) : 'false') . "\n"; + } + echo "\n"; + } + + echo "✅ Feature flag examples completed!\n"; +} + +function flagDependencyExamples() { + echo "\n" . str_repeat("=", 60) . "\n"; + echo "FLAG DEPENDENCIES EXAMPLES\n"; + echo str_repeat("=", 60) . "\n"; + echo "🔗 Testing flag dependencies with local evaluation...\n"; + echo " Flag structure: 'test-flag-dependency' depends on 'beta-feature' being enabled\n"; + echo "\n"; + echo "📋 Required setup (if 'test-flag-dependency' doesn't exist):\n"; + echo " 1. Create feature flag 'beta-feature':\n"; + echo " - Condition: email contains '@example.com'\n"; + echo " - Rollout: 100%\n"; + echo " 2. Create feature flag 'test-flag-dependency':\n"; + echo " - Condition: flag 'beta-feature' is enabled\n"; + echo " - Rollout: 100%\n"; + echo "\n"; + + // Enable debug for this section + PostHog::init( + $_ENV['POSTHOG_PROJECT_API_KEY'], + [ + 'host' => $_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', + 'debug' => true, + 'ssl' => !str_starts_with($_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'http://') + ], + null, + $_ENV['POSTHOG_PERSONAL_API_KEY'] + ); + + // Test @example.com user (should satisfy dependency if flags exist) + $result1 = PostHog::getFeatureFlag( + "test-flag-dependency", + "example_user", + [], + ["email" => "user@example.com"], + [], + true // only_evaluate_locally + ); + echo "✅ @example.com user (test-flag-dependency): " . json_encode($result1) . "\n"; + + // Test non-example.com user (dependency should not be satisfied) + $result2 = PostHog::getFeatureFlag( + "test-flag-dependency", + "regular_user", + [], + ["email" => "user@other.com"], + [], + true + ); + echo "❌ Regular user (test-flag-dependency): " . json_encode($result2) . "\n"; + + // Test beta-feature directly for comparison + $beta1 = PostHog::getFeatureFlag( + "beta-feature", + "example_user", + [], + ["email" => "user@example.com"], + [], + true + ); + $beta2 = PostHog::getFeatureFlag( + "beta-feature", + "regular_user", + [], + ["email" => "user@other.com"], + [], + true + ); + echo "📊 Beta feature comparison - @example.com: " . json_encode($beta1) . ", regular: " . json_encode($beta2) . "\n"; + + echo "\n🎯 Results Summary:\n"; + echo " - Flag dependencies evaluated locally: " . ($result1 != $result2 ? "✅ YES" : "❌ NO") . "\n"; + echo " - Zero API calls needed: ✅ YES (all evaluated locally)\n"; + echo " - PHP SDK supports flag dependencies: ✅ YES\n"; + + echo "\n" . str_repeat("-", 60) . "\n"; + echo "PRODUCTION-STYLE MULTIVARIATE DEPENDENCY CHAIN\n"; + echo str_repeat("-", 60) . "\n"; + echo "🔗 Testing complex multivariate flag dependencies...\n"; + echo " Structure: multivariate-root-flag -> multivariate-intermediate-flag -> multivariate-leaf-flag\n"; + echo "\n"; + echo "📋 Required setup (if flags don't exist):\n"; + echo " 1. Create 'multivariate-leaf-flag' with fruit variants (pineapple, mango, papaya, kiwi)\n"; + echo " - pineapple: email = 'pineapple@example.com'\n"; + echo " - mango: email = 'mango@example.com'\n"; + echo " 2. Create 'multivariate-intermediate-flag' with color variants (blue, red)\n"; + echo " - blue: depends on multivariate-leaf-flag = 'pineapple'\n"; + echo " - red: depends on multivariate-leaf-flag = 'mango'\n"; + echo " 3. Create 'multivariate-root-flag' with show variants (breaking-bad, the-wire)\n"; + echo " - breaking-bad: depends on multivariate-intermediate-flag = 'blue'\n"; + echo " - the-wire: depends on multivariate-intermediate-flag = 'red'\n"; + echo "\n"; + + // Test pineapple -> blue -> breaking-bad chain + $dependentResult3 = PostHog::getFeatureFlag( + "multivariate-root-flag", + "regular_user", + [], + ["email" => "pineapple@example.com"], + [], + true + ); + if ($dependentResult3 !== "breaking-bad") { + echo " ❌ Something went wrong evaluating 'multivariate-root-flag' with pineapple@example.com. Expected 'breaking-bad', got '" . json_encode($dependentResult3) . "'\n"; + } else { + echo "✅ 'multivariate-root-flag' with email pineapple@example.com succeeded\n"; + } + + // Test mango -> red -> the-wire chain + $dependentResult4 = PostHog::getFeatureFlag( + "multivariate-root-flag", + "regular_user", + [], + ["email" => "mango@example.com"], + [], + true + ); + if ($dependentResult4 !== "the-wire") { + echo " ❌ Something went wrong evaluating multivariate-root-flag with mango@example.com. Expected 'the-wire', got '" . json_encode($dependentResult4) . "'\n"; + } else { + echo "✅ 'multivariate-root-flag' with email mango@example.com succeeded\n"; + } + + // Show the complete chain evaluation + echo "\n🔍 Complete dependency chain evaluation:\n"; + $scenarios = [ + ["email" => "pineapple@example.com", "expected" => ["pineapple", "blue", "breaking-bad"]], + ["email" => "mango@example.com", "expected" => ["mango", "red", "the-wire"]] + ]; + + foreach ($scenarios as $scenario) { + $email = $scenario["email"]; + $expectedChain = $scenario["expected"]; + + $leaf = PostHog::getFeatureFlag( + "multivariate-leaf-flag", + "regular_user", + [], + ["email" => $email], + [], + true + ); + $intermediate = PostHog::getFeatureFlag( + "multivariate-intermediate-flag", + "regular_user", + [], + ["email" => $email], + [], + true + ); + $root = PostHog::getFeatureFlag( + "multivariate-root-flag", + "regular_user", + [], + ["email" => $email], + [], + true + ); + + $actualChain = [$leaf, $intermediate, $root]; + $chainSuccess = $actualChain === $expectedChain; + + echo " 📧 $email:\n"; + echo " Expected: " . implode(" -> ", $expectedChain) . "\n"; + echo " Actual: " . implode(" -> ", array_map('strval', $actualChain)) . "\n"; + echo " Status: " . ($chainSuccess ? "✅ SUCCESS" : "❌ FAILED") . "\n"; + } + + echo "\n🎯 Multivariate Chain Summary:\n"; + echo " - Complex dependency chains: ✅ SUPPORTED\n"; + echo " - Multivariate flag dependencies: ✅ SUPPORTED\n"; + echo " - Local evaluation of chains: ✅ WORKING\n"; +} + +function contextManagementExamples() { + echo "\n" . str_repeat("=", 60) . "\n"; + echo "CONTEXT MANAGEMENT AND TAGGING EXAMPLES\n"; + echo str_repeat("=", 60) . "\n"; + + // Enable debug for this section + PostHog::init( + $_ENV['POSTHOG_PROJECT_API_KEY'], + [ + 'host' => $_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', + 'debug' => true, + 'ssl' => !str_starts_with($_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'http://') + ], + null, + $_ENV['POSTHOG_PERSONAL_API_KEY'] + ); + + echo "🏷️ Testing groups and properties...\n"; + + // Capture event with groups + PostHog::capture([ + 'distinctId' => 'group_user_1', + 'event' => 'group_event', + 'properties' => [ + 'plan' => 'enterprise', + 'feature_used' => 'advanced_analytics' + ], + 'groups' => [ + 'company' => 'acme_corp', + 'team' => 'engineering' + ] + ]); + + // Test feature flags with group properties + echo "🚩 Testing flags with group context...\n"; + $flagValue = PostHog::getFeatureFlag( + "enterprise_features", + "group_user_1", + ['company' => 'acme_corp'], + ['plan' => 'enterprise'], + ['company' => ['name' => 'Acme Corp', 'employees' => 100]] + ); + + echo "Enterprise features flag: " . ($flagValue ? json_encode($flagValue) : 'false') . "\n"; + + echo "✅ Context management examples completed!\n"; +} + +function runAllExamples() { + identifyAndCaptureExamples(); + echo "\n" . str_repeat("-", 60) . "\n"; + + featureFlagExamples(); + echo "\n" . str_repeat("-", 60) . "\n"; + + flagDependencyExamples(); + echo "\n" . str_repeat("-", 60) . "\n"; + + contextManagementExamples(); + + echo "\n🎉 All examples completed!\n"; +} + +// Handle user choice +switch ($choice) { + case '1': + identifyAndCaptureExamples(); + break; + case '2': + featureFlagExamples(); + break; + case '3': + flagDependencyExamples(); + break; + case '4': + contextManagementExamples(); + break; + case '5': + runAllExamples(); + break; + case '6': + echo "👋 Goodbye!\n"; + exit(0); + default: + echo "❌ Invalid choice. Please run the script again and choose 1-6.\n"; + exit(1); +} + +echo "\n💡 Tip: Check your PostHog dashboard to see the captured events and user data!\n"; +echo "📖 For more examples and documentation, visit: https://posthog.com/docs/integrations/php-integration\n"; \ No newline at end of file From 6677aedaa0a16206675d984d4ada5e4793f549da Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Tue, 26 Aug 2025 10:38:05 -0700 Subject: [PATCH 3/7] Update .env.example --- .env.example | 2 +- example.php | 57 ++++++++++++++++++++++++++++------------------------ 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/.env.example b/.env.example index 15ad669..01d4fc6 100644 --- a/.env.example +++ b/.env.example @@ -8,4 +8,4 @@ POSTHOG_PROJECT_API_KEY=phc_your_project_api_key_here POSTHOG_PERSONAL_API_KEY=phx_your_personal_api_key_here # PostHog host URL (remove this line if using posthog.com) -POSTHOG_HOST=https://app.posthog.com \ No newline at end of file +POSTHOG_HOST=https://app.posthog.com diff --git a/example.php b/example.php index e4ed686..0b8b96d 100644 --- a/example.php +++ b/example.php @@ -16,7 +16,8 @@ use PostHog\PostHog; -function loadEnvFile() { +function loadEnvFile() +{ $envPath = __DIR__ . '/.env'; if (file_exists($envPath)) { $lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); @@ -55,10 +56,10 @@ function loadEnvFile() { PostHog::init( $projectKey, [ - 'host' => $host, + 'host' => $host, 'debug' => false, 'ssl' => !str_starts_with($host, 'http://') // Use SSL unless explicitly http:// - ], + ], null, $personalApiKey ); @@ -71,7 +72,6 @@ function loadEnvFile() { echo " Project API Key: " . substr($projectKey, 0, 9) . "...\n"; echo " Personal API Key: [REDACTED]\n"; echo " Host: $host\n\n\n"; - } catch (Exception $e) { echo "❌ Authentication failed!\n"; echo " Error: " . $e->getMessage() . "\n"; @@ -92,7 +92,8 @@ function loadEnvFile() { echo "6. Exit\n"; $choice = trim(readline("\nEnter your choice (1-6): ")); -function identifyAndCaptureExamples() { +function identifyAndCaptureExamples() +{ echo "\n" . str_repeat("=", 60) . "\n"; echo "IDENTIFY AND CAPTURE EXAMPLES\n"; echo str_repeat("=", 60) . "\n"; @@ -101,7 +102,7 @@ function identifyAndCaptureExamples() { PostHog::init( $_ENV['POSTHOG_PROJECT_API_KEY'], [ - 'host' => $_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', + 'host' => $_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'debug' => true, 'ssl' => !str_starts_with($_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'http://') ], @@ -157,7 +158,8 @@ function identifyAndCaptureExamples() { echo "✅ Identify and capture examples completed!\n"; } -function featureFlagExamples() { +function featureFlagExamples() +{ echo "\n" . str_repeat("=", 60) . "\n"; echo "FEATURE FLAG LOCAL EVALUATION EXAMPLES\n"; echo str_repeat("=", 60) . "\n"; @@ -166,7 +168,7 @@ function featureFlagExamples() { PostHog::init( $_ENV['POSTHOG_PROJECT_API_KEY'], [ - 'host' => $_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', + 'host' => $_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'debug' => false, 'ssl' => !str_starts_with($_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'http://') ], @@ -175,14 +177,14 @@ function featureFlagExamples() { ); echo "🚩 Getting individual feature flags...\n"; - + // Test different users to see different results $users = ['user_1', 'user_2', 'user_3']; - + foreach ($users as $user) { $flags = PostHog::getAllFlags($user, [], [], [], true); echo "User $user flags: " . json_encode($flags, JSON_PRETTY_PRINT) . "\n"; - + // Get a specific flag if (!empty($flags)) { $firstFlag = array_key_first($flags); @@ -195,7 +197,8 @@ function featureFlagExamples() { echo "✅ Feature flag examples completed!\n"; } -function flagDependencyExamples() { +function flagDependencyExamples() +{ echo "\n" . str_repeat("=", 60) . "\n"; echo "FLAG DEPENDENCIES EXAMPLES\n"; echo str_repeat("=", 60) . "\n"; @@ -215,7 +218,7 @@ function flagDependencyExamples() { PostHog::init( $_ENV['POSTHOG_PROJECT_API_KEY'], [ - 'host' => $_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', + 'host' => $_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'debug' => true, 'ssl' => !str_starts_with($_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'http://') ], @@ -234,7 +237,7 @@ function flagDependencyExamples() { ); echo "✅ @example.com user (test-flag-dependency): " . json_encode($result1) . "\n"; - // Test non-example.com user (dependency should not be satisfied) + // Test non-example.com user (dependency should not be satisfied) $result2 = PostHog::getFeatureFlag( "test-flag-dependency", "regular_user", @@ -256,7 +259,7 @@ function flagDependencyExamples() { ); $beta2 = PostHog::getFeatureFlag( "beta-feature", - "regular_user", + "regular_user", [], ["email" => "user@other.com"], [], @@ -327,7 +330,7 @@ function flagDependencyExamples() { foreach ($scenarios as $scenario) { $email = $scenario["email"]; $expectedChain = $scenario["expected"]; - + $leaf = PostHog::getFeatureFlag( "multivariate-leaf-flag", "regular_user", @@ -355,7 +358,7 @@ function flagDependencyExamples() { $actualChain = [$leaf, $intermediate, $root]; $chainSuccess = $actualChain === $expectedChain; - + echo " 📧 $email:\n"; echo " Expected: " . implode(" -> ", $expectedChain) . "\n"; echo " Actual: " . implode(" -> ", array_map('strval', $actualChain)) . "\n"; @@ -368,7 +371,8 @@ function flagDependencyExamples() { echo " - Local evaluation of chains: ✅ WORKING\n"; } -function contextManagementExamples() { +function contextManagementExamples() +{ echo "\n" . str_repeat("=", 60) . "\n"; echo "CONTEXT MANAGEMENT AND TAGGING EXAMPLES\n"; echo str_repeat("=", 60) . "\n"; @@ -377,7 +381,7 @@ function contextManagementExamples() { PostHog::init( $_ENV['POSTHOG_PROJECT_API_KEY'], [ - 'host' => $_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', + 'host' => $_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'debug' => true, 'ssl' => !str_starts_with($_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'http://') ], @@ -404,7 +408,7 @@ function contextManagementExamples() { // Test feature flags with group properties echo "🚩 Testing flags with group context...\n"; $flagValue = PostHog::getFeatureFlag( - "enterprise_features", + "enterprise_features", "group_user_1", ['company' => 'acme_corp'], ['plan' => 'enterprise'], @@ -416,18 +420,19 @@ function contextManagementExamples() { echo "✅ Context management examples completed!\n"; } -function runAllExamples() { +function runAllExamples() +{ identifyAndCaptureExamples(); echo "\n" . str_repeat("-", 60) . "\n"; - + featureFlagExamples(); echo "\n" . str_repeat("-", 60) . "\n"; - + flagDependencyExamples(); echo "\n" . str_repeat("-", 60) . "\n"; - + contextManagementExamples(); - + echo "\n🎉 All examples completed!\n"; } @@ -457,4 +462,4 @@ function runAllExamples() { } echo "\n💡 Tip: Check your PostHog dashboard to see the captured events and user data!\n"; -echo "📖 For more examples and documentation, visit: https://posthog.com/docs/integrations/php-integration\n"; \ No newline at end of file +echo "📖 For more examples and documentation, visit: https://posthog.com/docs/integrations/php-integration\n"; From 9d9d69c1d3709164aa848f9c067cffe24656ea40 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Tue, 26 Aug 2025 10:38:41 -0700 Subject: [PATCH 4/7] Add a bin/fmt file --- bin/fmt | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100755 bin/fmt diff --git a/bin/fmt b/bin/fmt new file mode 100755 index 0000000..adae55c --- /dev/null +++ b/bin/fmt @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -e + +# Source helper functions +source "$(dirname "$0")/helpers/_utils.sh" +set_source_and_root_dir + +# Format PHP files using PHPCBF (PHP Code Beautifier and Fixer) +echo "Formatting PHP files..." + +# Check if vendor/bin/phpcbf exists +if [ ! -f "./vendor/bin/phpcbf" ]; then + fatal "PHPCBF not found. Please run 'composer install' first." +fi + +# Run PHPCBF on all PHP files in lib/ and test/ directories +echo "Running PHPCBF on lib/ and test/ directories..." +./vendor/bin/phpcbf --standard=phpcs.xml lib/ test/ || { + # PHPCBF returns exit code 1 when it fixes files, which is expected behavior + # Only fail if it's a different error (exit code 2 or higher) + exit_code=$? + if [ $exit_code -gt 1 ]; then + fatal "PHPCBF failed with exit code $exit_code" + fi + echo "PHPCBF finished fixing files (exit code $exit_code is expected when fixes are made)" +} + +# Also format the example.php file +echo "Running PHPCBF on example.php..." +./vendor/bin/phpcbf --standard=phpcs.xml example.php || { + exit_code=$? + if [ $exit_code -gt 1 ]; then + fatal "PHPCBF failed on example.php with exit code $exit_code" + fi +} + +echo "PHP formatting complete!" \ No newline at end of file From 285f5b84f2e39e4b4b4f9e4aaeb883c44ecd11e6 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Tue, 26 Aug 2025 10:39:34 -0700 Subject: [PATCH 5/7] Fix error message Update FeatureFlag.php Co-Authored-By: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/FeatureFlag.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/FeatureFlag.php b/lib/FeatureFlag.php index 5d9cbf0..88d2ce6 100644 --- a/lib/FeatureFlag.php +++ b/lib/FeatureFlag.php @@ -497,10 +497,8 @@ public static function evaluateFlagDependency($property, $flagsByKey, $evaluatio // Check if dependency_chain is present - it should always be provided for flag dependencies if (!array_key_exists("dependency_chain", $property)) { - // If no dependency_chain is provided, this is likely an old-style flag property - // that was meant to be skipped in the old implementation throw new InconclusiveMatchException(sprintf( - "Cannot evaluate flag dependency on '%s' without flags_by_key and evaluation_cache", + "Cannot evaluate flag dependency on '%s' without dependency_chain", $property["key"] ?? "unknown" )); } From a451a2b32b2a8966296fe4f473aff53539799bc0 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Tue, 26 Aug 2025 10:40:45 -0700 Subject: [PATCH 6/7] Ensure compatibility with older PHP Co-Authored-By: Copilot <175728472+Copilot@users.noreply.github.com> --- example.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example.php b/example.php index 0b8b96d..31fad79 100644 --- a/example.php +++ b/example.php @@ -23,7 +23,7 @@ function loadEnvFile() $lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { $line = trim($line); - if ($line && !str_starts_with($line, '#') && strpos($line, '=') !== false) { + if ($line && ($line[0] !== '#') && strpos($line, '=') !== false) { list($key, $value) = explode('=', $line, 2); $_ENV[trim($key)] = trim($value); putenv(trim($key) . '=' . trim($value)); @@ -58,7 +58,7 @@ function loadEnvFile() [ 'host' => $host, 'debug' => false, - 'ssl' => !str_starts_with($host, 'http://') // Use SSL unless explicitly http:// + 'ssl' => !(substr($host, 0, 7) === 'http://') // Use SSL unless explicitly http:// ], null, $personalApiKey From 42821f4bc5dc69de3a19319f39501bafdb519117 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Tue, 26 Aug 2025 10:41:08 -0700 Subject: [PATCH 7/7] Remove unnecessary logging --- lib/FeatureFlag.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/FeatureFlag.php b/lib/FeatureFlag.php index 88d2ce6..17e95ec 100644 --- a/lib/FeatureFlag.php +++ b/lib/FeatureFlag.php @@ -507,7 +507,6 @@ public static function evaluateFlagDependency($property, $flagsByKey, $evaluatio // Handle circular dependency (empty chain means circular) if (count($dependencyChain) === 0) { - error_log(sprintf("Circular dependency detected for flag: %s", $property["key"] ?? "unknown")); throw new InconclusiveMatchException(sprintf( "Circular dependency detected for flag '%s'", $property["key"] ?? "unknown"