Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 29 additions & 1 deletion psalm-baseline.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="6.15.1@28dc127af1b5aecd52314f6f645bafc10d0e11f9">
<files psalm-version="6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac">
<file src="src/LaunchDarkly/EvaluationDetail.php">
<ClassMustBeFinal>
<code><![CDATA[EvaluationDetail]]></code>
Expand Down Expand Up @@ -45,6 +45,33 @@
<code><![CDATA[$prerequisites]]></code>
</RiskyTruthyFalsyComparison>
</file>
<file src="src/LaunchDarkly/Hooks/EvaluationSeriesContext.php">
<PossiblyUnusedProperty>
<code><![CDATA[$context]]></code>
<code><![CDATA[$defaultValue]]></code>
<code><![CDATA[$method]]></code>
</PossiblyUnusedProperty>
</file>
<file src="src/LaunchDarkly/Hooks/Hook.php">
<PossiblyUnusedParam>
<code><![CDATA[$detail]]></code>
<code><![CDATA[$seriesContext]]></code>
<code><![CDATA[$seriesContext]]></code>
<code><![CDATA[$seriesContext]]></code>
</PossiblyUnusedParam>
</file>
<file src="src/LaunchDarkly/Hooks/Metadata.php">
<PossiblyUnusedMethod>
<code><![CDATA[__construct]]></code>
</PossiblyUnusedMethod>
</file>
<file src="src/LaunchDarkly/Hooks/TrackSeriesContext.php">
<PossiblyUnusedProperty>
<code><![CDATA[$context]]></code>
<code><![CDATA[$data]]></code>
<code><![CDATA[$metricValue]]></code>
</PossiblyUnusedProperty>
</file>
<file src="src/LaunchDarkly/Impl/BigSegments/MembershipResult.php">
<ClassMustBeFinal>
<code><![CDATA[MembershipResult]]></code>
Expand Down Expand Up @@ -451,6 +478,7 @@
</ClassMustBeFinal>
<PossiblyUnusedMethod>
<code><![CDATA[__construct]]></code>
<code><![CDATA[addHook]]></code>
<code><![CDATA[allFlagsState]]></code>
<code><![CDATA[flush]]></code>
<code><![CDATA[getBigSegmentStatusProvider]]></code>
Expand Down
24 changes: 24 additions & 0 deletions src/LaunchDarkly/Hooks/EvaluationSeriesContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace LaunchDarkly\Hooks;

use LaunchDarkly\LDContext;

/**
* Contextual information provided to stages of the evaluation series.
*
* An instance is created once per variation call and passed, unchanged, to every
* stage of every registered hook for that call.
*/
final class EvaluationSeriesContext
{
public function __construct(
public readonly string $flagKey,
public readonly LDContext $context,
public readonly mixed $defaultValue,
public readonly string $method,
) {
}
}
67 changes: 67 additions & 0 deletions src/LaunchDarkly/Hooks/Hook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace LaunchDarkly\Hooks;

use LaunchDarkly\EvaluationDetail;

/**
* Base class for extending SDK functionality via hooks.
*
* Hook implementations MUST inherit from this class. Default no-op
* implementations are provided for every stage so the SDK can add new stages
* without breaking existing implementations.
*
* Only `getMetadata` is abstract; subclasses override just the stages they need.
*/
abstract class Hook
{
/**
* Get metadata about the hook implementation.
*/
abstract public function getMetadata(): Metadata;

/**
* Stage executed before the flag value has been determined.
*
* Called synchronously on the thread performing the variation call.
*
* @param array<string, mixed> $data Data returned by the previous stage of this hook.
* For beforeEvaluation this will be an empty array.
* @return array<string, mixed> Data to be passed to the next stage of this hook.
* Implementations should return the input data (optionally augmented) unchanged
* if they do not need to pass additional information forward.
*/
public function beforeEvaluation(EvaluationSeriesContext $seriesContext, array $data): array
{
return $data;
}

/**
* Stage executed after the flag value has been determined.
*
* Called synchronously on the thread performing the variation call.
*
* @param array<string, mixed> $data Data returned by the beforeEvaluation stage of this hook.
* @param EvaluationDetail $detail The result of the evaluation. This value should not be modified.
* @return array<string, mixed> Data to be passed to the next stage of this hook.
* The return is currently unused but future stages may consume it.
*/
public function afterEvaluation(
EvaluationSeriesContext $seriesContext,
array $data,
EvaluationDetail $detail,
): array {
return $data;
}

/**
* Handler executed after a custom event has been enqueued by a call to `track`.
*
* Not invoked if the track call could not enqueue an event (e.g. invalid context).
*/
public function afterTrack(TrackSeriesContext $seriesContext): void
{
}
}
16 changes: 16 additions & 0 deletions src/LaunchDarkly/Hooks/Metadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace LaunchDarkly\Hooks;

/**
* Metadata about a hook implementation.
*/
final class Metadata
{
public function __construct(
public readonly string $name,
) {
}
}
21 changes: 21 additions & 0 deletions src/LaunchDarkly/Hooks/TrackSeriesContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace LaunchDarkly\Hooks;

use LaunchDarkly\LDContext;

/**
* Contextual information provided to the afterTrack stage of the track series.
*/
final class TrackSeriesContext
{
public function __construct(
public readonly LDContext $context,
public readonly string $key,
public readonly int|float|null $metricValue,
public readonly mixed $data,
) {
}
}
133 changes: 133 additions & 0 deletions src/LaunchDarkly/Impl/Hooks/HookRunner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

declare(strict_types=1);

namespace LaunchDarkly\Impl\Hooks;

use LaunchDarkly\EvaluationDetail;
use LaunchDarkly\Hooks\EvaluationSeriesContext;
use LaunchDarkly\Hooks\Hook;
use LaunchDarkly\Hooks\TrackSeriesContext;
use Psr\Log\LoggerInterface;
use Throwable;

/**
* @ignore
* @internal
*
* Runs hook stages with the required ordering and error isolation behavior.
*/
final class HookRunner
{
/** @var list<Hook> */
private array $_hooks;

/**
* @param list<Hook> $hooks
*/
public function __construct(
private readonly LoggerInterface $_logger,
array $hooks = [],
) {
$this->_hooks = $hooks;
}

public function addHook(Hook $hook): void
{
$this->_hooks[] = $hook;
}

public function hasHooks(): bool
{
return count($this->_hooks) > 0;
}

/**
* Executes the beforeEvaluation stage of every registered hook in registration order.
*
* @return list<array<string, mixed>> The data returned by each hook, in the same order as
* the registered hooks. On error, the slot for that hook contains an empty array.
*/
public function beforeEvaluation(EvaluationSeriesContext $seriesContext): array
{
$result = [];
foreach ($this->_hooks as $hook) {
$result[] = $this->safeInvoke(
'beforeEvaluation',
$seriesContext->flagKey,
$hook,
fn () => $hook->beforeEvaluation($seriesContext, []),
[],
);
}
return $result;
}

/**
* Executes the afterEvaluation stage of every registered hook in reverse registration order.
*
* @param list<array<string, mixed>> $beforeData The per-hook data returned from beforeEvaluation.
*/
public function afterEvaluation(
EvaluationSeriesContext $seriesContext,
array $beforeData,
EvaluationDetail $detail,
): void {
for ($i = count($this->_hooks) - 1; $i >= 0; $i--) {
$hook = $this->_hooks[$i];
$data = $beforeData[$i] ?? [];
$this->safeInvoke(
'afterEvaluation',
$seriesContext->flagKey,
$hook,
fn () => $hook->afterEvaluation($seriesContext, $data, $detail),
$data,
);
}
}

/**
* Executes the afterTrack handler of every registered hook in registration order.
*/
public function afterTrack(TrackSeriesContext $seriesContext): void
{
foreach ($this->_hooks as $hook) {
try {
$hook->afterTrack($seriesContext);
} catch (Throwable $e) {
$name = $this->hookName($hook);
$this->_logger->error(
"During tracking of event \"{$seriesContext->key}\", stage \"afterTrack\" of hook \"{$name}\" reported error: {$e->getMessage()}"
);
}
}
}

/**
* @template T
* @param callable(): T $fn
* @param T $fallback
* @return T
*/
private function safeInvoke(string $stage, string $flagKey, Hook $hook, callable $fn, mixed $fallback): mixed
{
try {
return $fn();
} catch (Throwable $e) {
$name = $this->hookName($hook);
$this->_logger->error(
"During evaluation of flag \"{$flagKey}\", stage \"{$stage}\" of hook \"{$name}\" reported error: {$e->getMessage()}"
);
return $fallback;
}
}

private function hookName(Hook $hook): string
{
try {
return $hook->getMetadata()->name;
} catch (Throwable) {
return 'unknown hook';
}
}
}
Loading
Loading