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
68 changes: 62 additions & 6 deletions lib/Api/DevCycleClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
use DevCycle\Model\ErrorResponse;
use DevCycle\Model\InlineResponse201;
use DevCycle\Model\Variable;
use DevCycle\Model\EvalHookRunner;
use DevCycle\Model\HookContext;
use DevCycle\Model\BeforeHookError;
use DevCycle\Model\AfterHookError;
use DevCycle\ObjectSerializer;
use DevCycle\OpenFeature\DevCycleProvider;
use Exception;
Expand Down Expand Up @@ -59,6 +63,8 @@ class DevCycleClient

private bool $usingOpenFeature;

protected EvalHookRunner $hookRunner;

/**
* @param string $sdkKey
* @param DevCycleOptions $dvcOptions
Expand All @@ -84,6 +90,7 @@ public function __construct(
$this->dvcOptions = $dvcOptions;
$this->openFeatureProvider = new DevCycleProvider($this);
$this->usingOpenFeature = false;
$this->hookRunner = new EvalHookRunner($dvcOptions->getHooks());
}

public function getOpenFeatureProvider(): DevCycleProvider
Expand Down Expand Up @@ -285,15 +292,64 @@ public function variable(DevCycleUser $user, string $key, mixed $default): Varia
{
$this->validateUserData($user);

// Create hook context
$context = new HookContext($user, $key, $default);
$hooks = $this->hookRunner->getHooks();
$reversedHooks = array_reverse($hooks);
$beforeHookError = null;
$afterHookError = null;
$evaluationError = null;
$result = null;

try {
list($response) = $this->variableWithHttpInfo($user, $key);
return $this->reformatVariable($key, $response, $default);
} catch (GuzzleException|ApiException $e) {
if ($e->getCode() != 404) {
// Run before hooks
if (!empty($hooks)) {
try {
$this->hookRunner->runBeforeHooks($hooks, $context);
} catch (BeforeHookError $e) {
$beforeHookError = $e;
error_log("Before hook error: " . $e->getMessage());
}
}

try {
list($response) = $this->variableWithHttpInfo($user, $key);
$result = $this->reformatVariable($key, $response, $default);
$context->setVariableDetails($result);
} catch (GuzzleException|ApiException $e) {
$evaluationError = $e;
if ($e->getCode() != 404) {
error_log("Failed to get variable value for key $key, " . $e->getMessage());
}
$result = new Variable(array("key" => $key, "value" => $default, "type" => gettype($default), "isDefaulted" => true));
$context->setVariableDetails($result);
}

error_log("Failed to get variable value for key $key, " . $e->getMessage());
// Run after hooks if no before hook error
if (!empty($hooks)) {
try {
$this->hookRunner->runAfterHooks($reversedHooks, $context);
} catch (AfterHookError $e) {
$afterHookError = $e;
error_log("After hook error: " . $e->getMessage());
}
}

return $result ?? new Variable(array("key" => $key, "value" => $default, "type" => gettype($default), "isDefaulted" => true));

} finally {
// Always run onFinally hooks
if (!empty($hooks)) {
$this->hookRunner->runOnFinallyHooks($reversedHooks, $context);
}

// Run error hooks if any error occurred
if (!empty($hooks) && ($beforeHookError !== null || $afterHookError !== null || $evaluationError !== null)) {
$error = $beforeHookError ?? $afterHookError ?? $evaluationError;
if ($error !== null) {
$this->hookRunner->runErrorHooks($reversedHooks, $context, $error);
}
}
return new Variable(array("key" => $key, "value" => $default, "type" => gettype($default), "isDefaulted" => true));
}
}

Expand Down
28 changes: 28 additions & 0 deletions lib/Model/AfterHookError.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace DevCycle\Model;

use Exception;

/**
* AfterHookError Class
*
* Exception thrown when after hooks fail during variable evaluation.
*
* @category Class
* @package DevCycle
*/
class AfterHookError extends Exception
{
/**
* Constructor
*
* @param string $message The error message
* @param int $code The error code
* @param Exception|null $previous The previous exception
*/
public function __construct(string $message = "", int $code = 0, ?Exception $previous = null)
{
parent::__construct($message, $code, $previous);
}
}
28 changes: 28 additions & 0 deletions lib/Model/BeforeHookError.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace DevCycle\Model;

use Exception;

/**
* BeforeHookError Class
*
* Exception thrown when before hooks fail during variable evaluation.
*
* @category Class
* @package DevCycle
*/
class BeforeHookError extends Exception
{
/**
* Constructor
*
* @param string $message The error message
* @param int $code The error code
* @param Exception|null $previous The previous exception
*/
public function __construct(string $message = "", int $code = 0, ?Exception $previous = null)
{
parent::__construct($message, $code, $previous);
}
}
17 changes: 16 additions & 1 deletion lib/Model/DevCycleOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,26 @@ class DevCycleOptions

protected array $httpOptions = [];

protected array $hooks = [];

/**
* Constructor
*
* @param boolean $enableEdgeDB flag to enable EdgeDB user data storage
* @param string|null $bucketingApiHostname
* @param string|null $unixSocketPath
* @param array $httpOptions
* @param array $hooks Array of evaluation hooks
*/
public function __construct(bool $enableEdgeDB = false, ?string $bucketingApiHostname = null, ?string $unixSocketPath = null, array $httpOptions = [])
public function __construct(bool $enableEdgeDB = false, ?string $bucketingApiHostname = null, ?string $unixSocketPath = null, array $httpOptions = [], array $hooks = [])
{
$this->enableEdgeDB = $enableEdgeDB;
if ($bucketingApiHostname !== null) {
$this->bucketingApiHostname = $bucketingApiHostname;
}
$this->unixSocketPath = $unixSocketPath;
$this->httpOptions = $httpOptions;
$this->hooks = $hooks;
}

/**
Expand Down Expand Up @@ -67,4 +72,14 @@ public function getHttpOptions(): array
{
return $this->httpOptions;
}

/**
* Gets the evaluation hooks
*
* @return array Array of evaluation hooks
*/
public function getHooks(): array
{
return $this->hooks;
}
}
95 changes: 95 additions & 0 deletions lib/Model/EvalHook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace DevCycle\Model;

/**
* EvalHook Class
*
* Represents an evaluation hook with callback functions for different stages
* of variable evaluation.
*
* @category Class
* @package DevCycle
*/
class EvalHook
{
/**
* @var callable|null
*/
private $before;

/**
* @var callable|null
*/
private $after;

/**
* @var callable|null
*/
private $onFinally;

/**
* @var callable|null
*/
private $error;

/**
* Constructor
*
* @param callable|null $before Callback function called before variable evaluation
* @param callable|null $after Callback function called after variable evaluation
* @param callable|null $onFinally Callback function called in finally block
* @param callable|null $error Callback function called when an error occurs
*/
public function __construct(
?callable $before = null,
?callable $after = null,
?callable $onFinally = null,
?callable $error = null
) {
$this->before = $before;
$this->after = $after;
$this->onFinally = $onFinally;
$this->error = $error;
}

/**
* Get the before callback
*
* @return callable|null
*/
public function getBefore(): ?callable
{
return $this->before;
}

/**
* Get the after callback
*
* @return callable|null
*/
public function getAfter(): ?callable
{
return $this->after;
}

/**
* Get the onFinally callback
*
* @return callable|null
*/
public function getOnFinally(): ?callable
{
return $this->onFinally;
}

/**
* Get the error callback
*
* @return callable|null
*/
public function getError(): ?callable
{
return $this->error;
}
}
Loading