1414use LaunchDarkly \LDContext ;
1515use Psr \Log \LoggerInterface ;
1616
17+ /**
18+ * @ignore
19+ * @internal
20+ */
21+ class EvaluatorState
22+ {
23+ public ?array $ prerequisiteStack = null ;
24+
25+ public function __construct (public FeatureFlag $ originalFlag )
26+ {
27+ }
28+ }
29+
1730/**
1831 * Encapsulates the feature flag evaluation logic. The Evaluator has no direct access to the
1932 * rest of the SDK environment; if it needs to retrieve flags or segments that are referenced
@@ -46,81 +59,96 @@ public function __construct(FeatureRequester $featureRequester, ?LoggerInterface
4659 */
4760 public function evaluate (FeatureFlag $ flag , LDContext $ context , ?callable $ prereqEvalSink ): EvalResult
4861 {
62+ $ stateStack = null ;
63+ $ state = new EvaluatorState ($ flag );
4964 try {
50- return $ this ->evaluateInternal ($ flag , $ context , $ prereqEvalSink );
65+ return $ this ->evaluateInternal ($ flag , $ context , $ prereqEvalSink, $ state );
5166 } catch (EvaluationException $ e ) {
5267 return new EvalResult (new EvaluationDetail (null , null , EvaluationReason::error ($ e ->getErrorKind ())));
68+ } catch (\Throwable $ e ) {
69+ Util::logExceptionAtErrorLevel ($ this ->_logger , $ e , 'Unexpected error when evaluating flag ' . $ flag ->getKey ());
70+ return new EvalResult (new EvaluationDetail (null , null , EvaluationReason::error (EvaluationReason::EXCEPTION_ERROR )));
5371 }
5472 }
5573
5674 private function evaluateInternal (
5775 FeatureFlag $ flag ,
5876 LDContext $ context ,
59- ?callable $ prereqEvalSink
77+ ?callable $ prereqEvalSink ,
78+ EvaluatorState $ state
6079 ): EvalResult {
61- try {
62- // The reason there's an extra try block here is that if something fails during evaluation of the
63- // prerequisites, we don't want that to short-circuit evaluation of the base flag in the way that
64- // an error normally would, where the whole evaluation would return an error result with a default
65- // value. Instead we want the base flag to return its off variation, as it always does if any
66- // prerequisites have failed.
67- if (!$ flag ->isOn ()) {
68- return EvaluatorHelpers::getOffResult ($ flag , EvaluationReason::off ());
69- }
80+ if (!$ flag ->isOn ()) {
81+ return EvaluatorHelpers::getOffResult ($ flag , EvaluationReason::off ());
82+ }
7083
71- $ prereqFailureReason = $ this ->checkPrerequisites ($ flag , $ context , $ prereqEvalSink );
72- if ($ prereqFailureReason !== null ) {
73- return EvaluatorHelpers::getOffResult ($ flag , $ prereqFailureReason );
74- }
84+ $ prereqFailureReason = $ this ->checkPrerequisites ($ flag , $ context , $ prereqEvalSink, $ state );
85+ if ($ prereqFailureReason !== null ) {
86+ return EvaluatorHelpers::getOffResult ($ flag , $ prereqFailureReason );
87+ }
7588
76- // Check to see if targets match
77- $ targetResult = $ this ->checkTargets ($ flag , $ context );
78- if ($ targetResult ) {
79- return $ targetResult ;
80- }
89+ // Check to see if targets match
90+ $ targetResult = $ this ->checkTargets ($ flag , $ context );
91+ if ($ targetResult ) {
92+ return $ targetResult ;
93+ }
8194
82- // Now walk through the rules and see if any match
83- foreach ($ flag ->getRules () as $ i => $ rule ) {
84- if ($ this ->ruleMatchesContext ($ rule , $ context )) {
85- return EvaluatorHelpers::getResultForVariationOrRollout (
86- $ flag ,
87- $ rule ,
88- $ rule ->isTrackEvents (),
89- $ context ,
90- EvaluationReason::ruleMatch ($ i , $ rule ->getId ())
91- );
92- }
95+ // Now walk through the rules and see if any match
96+ foreach ($ flag ->getRules () as $ i => $ rule ) {
97+ if ($ this ->ruleMatchesContext ($ rule , $ context )) {
98+ return EvaluatorHelpers::getResultForVariationOrRollout (
99+ $ flag ,
100+ $ rule ,
101+ $ rule ->isTrackEvents (),
102+ $ context ,
103+ EvaluationReason::ruleMatch ($ i , $ rule ->getId ())
104+ );
93105 }
94- return EvaluatorHelpers::getResultForVariationOrRollout (
95- $ flag ,
96- $ flag ->getFallthrough (),
97- $ flag ->isTrackEventsFallthrough (),
98- $ context ,
99- EvaluationReason::fallthrough ()
100- );
101- } catch (EvaluationException $ e ) {
102- return new EvalResult (new EvaluationDetail (null , null , EvaluationReason::error ($ e ->getErrorKind ())));
103- } catch (\Throwable $ e ) {
104- Util::logExceptionAtErrorLevel ($ this ->_logger , $ e , 'Unexpected error when evaluating flag ' . $ flag ->getKey ());
105- return new EvalResult (new EvaluationDetail (null , null , EvaluationReason::error (EvaluationReason::EXCEPTION_ERROR )));
106106 }
107+ return EvaluatorHelpers::getResultForVariationOrRollout (
108+ $ flag ,
109+ $ flag ->getFallthrough (),
110+ $ flag ->isTrackEventsFallthrough (),
111+ $ context ,
112+ EvaluationReason::fallthrough ()
113+ );
107114 }
108115
109116 private function checkPrerequisites (
110117 FeatureFlag $ flag ,
111118 LDContext $ context ,
112- ?callable $ prereqEvalSink
119+ ?callable $ prereqEvalSink ,
120+ EvaluatorState $ state
113121 ): ?EvaluationReason {
114- foreach ($ flag ->getPrerequisites () as $ prereq ) {
115- $ prereqOk = true ;
116- try {
117- $ prereqFeatureFlag = $ this ->_featureRequester ->getFeature ($ prereq ->getKey ());
122+ // We use the state object to guard against circular references in prerequisites. To avoid
123+ // the overhead of creating the $state->prerequisiteStack array in the most common case where
124+ // there's only a single level of prerequisites, we treat $state->originalFlag as the first
125+ // element in the stack.
126+ $ flagKey = $ flag ->getKey ();
127+ if ($ flag !== $ state ->originalFlag ) {
128+ if ($ state ->prerequisiteStack === null ) {
129+ $ state ->prerequisiteStack = [];
130+ }
131+ $ state ->prerequisiteStack [] = $ flagKey ;
132+ }
133+ try {
134+ foreach ($ flag ->getPrerequisites () as $ prereq ) {
135+ $ prereqKey = $ prereq ->getKey ();
136+
137+ if ($ prereqKey === $ state ->originalFlag ->getKey () ||
138+ ($ state ->prerequisiteStack !== null && in_array ($ prereqKey , $ state ->prerequisiteStack ))) {
139+ throw new EvaluationException (
140+ "prerequisite relationship to \"$ prereqKey \" caused a circular reference; this is probably a temporary condition due to an incomplete update " ,
141+ EvaluationReason::MALFORMED_FLAG_ERROR
142+ );
143+ }
144+ $ prereqOk = true ;
145+ $ prereqFeatureFlag = $ this ->_featureRequester ->getFeature ($ prereqKey );
118146 if ($ prereqFeatureFlag === null ) {
119147 $ prereqOk = false ;
120148 } else {
121149 // Note that if the prerequisite flag is off, we don't consider it a match no matter what its
122150 // off variation was. But we still need to evaluate it in order to generate an event.
123- $ prereqEvalResult = $ this ->evaluateInternal ($ prereqFeatureFlag , $ context , $ prereqEvalSink );
151+ $ prereqEvalResult = $ this ->evaluateInternal ($ prereqFeatureFlag , $ context , $ prereqEvalSink, $ state );
124152 $ variation = $ prereq ->getVariation ();
125153 if (!$ prereqFeatureFlag ->isOn () || $ prereqEvalResult ->getDetail ()->getVariationIndex () !== $ variation ) {
126154 $ prereqOk = false ;
@@ -129,16 +157,13 @@ private function checkPrerequisites(
129157 $ prereqEvalSink (new PrerequisiteEvaluationRecord ($ prereqFeatureFlag , $ flag , $ prereqEvalResult ));
130158 }
131159 }
132- } catch (\Throwable $ e ) {
133- Util::logExceptionAtErrorLevel (
134- $ this ->_logger ,
135- $ e ,
136- 'Unexpected error when evaluating prerequisite flag ' . $ prereq ->getKey ()
137- );
138- $ prereqOk = false ;
160+ if (!$ prereqOk ) {
161+ return EvaluationReason::prerequisiteFailed ($ prereqKey );
162+ }
139163 }
140- if (!$ prereqOk ) {
141- return EvaluationReason::prerequisiteFailed ($ prereq ->getKey ());
164+ } finally {
165+ if ($ state ->prerequisiteStack !== null && count ($ state ->prerequisiteStack ) !== 0 ) {
166+ array_pop ($ state ->prerequisiteStack );
142167 }
143168 }
144169 return null ;
0 commit comments