-
Notifications
You must be signed in to change notification settings - Fork 1
fix: audit operator fixes and PHP 8.4 compat #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
43c2c4c
cc89d5a
f4a2515
d88e1f2
ecabe41
9168988
ab7c04f
4b575fd
0d51ff6
e01fbbe
d82b09e
235ab4a
b8fa6f9
1f04e03
eb7aa19
9933447
e88af05
e9e6bba
1ebd865
ed9cc97
5b685c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,3 +5,8 @@ composer.lock | |
| coverage.xml | ||
| .phpunit.cache | ||
| phpunit.xml | ||
|
|
||
| .claude/ | ||
| .DS_Store | ||
| AUDIT_REPORT.md | ||
| FIXES_IMPLEMENTED.md | ||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| <?php | ||
|
|
||
| namespace ABSmartly\SDK; | ||
|
|
||
| use ABSmartly\SDK\Client\Client; | ||
| use ABSmartly\SDK\Client\ClientConfig; | ||
| use ABSmartly\SDK\Context\AsyncContextDataProvider; | ||
| use ABSmartly\SDK\Context\Context; | ||
| use ABSmartly\SDK\Context\ContextConfig; | ||
| use ABSmartly\SDK\Context\ContextData; | ||
| use ABSmartly\SDK\Context\ContextDataProvider; | ||
| use ABSmartly\SDK\Context\ContextPublisher; | ||
| use ABSmartly\SDK\Context\ContextEventLogger; | ||
| use ABSmartly\SDK\Http\HTTPClient; | ||
| use React\Promise\PromiseInterface; | ||
|
|
||
| use function React\Promise\resolve; | ||
|
|
||
| class ABsmartly { | ||
|
|
||
| private Client $client; | ||
| private ContextPublisher $handler; | ||
| private ContextDataProvider $provider; | ||
| private ?ContextEventLogger $eventLogger; | ||
|
|
||
| public function __construct(Config $config) { | ||
| $this->client = $config->getClient(); | ||
| $this->provider = $config->getContextDataProvider(); | ||
| $this->handler = $config->getContextEventHandler(); | ||
| $this->eventLogger = $config->getContextEventLogger(); | ||
| } | ||
|
|
||
| /** | ||
| * @param string $endpoint URL to your API endpoint. Most commonly "your-company.absmartly.io". | ||
| * @param string $apiKey API key which can be found on the Web Console. | ||
| * @param string $application Name of the application where the SDK is installed. Applications are created on the | ||
| * Web Console and should match the applications where your experiments will be running. | ||
| * @param string $environment Environment of the platform where the SDK is installed. Environments are created on | ||
| * the Web Console and should match the available environments in your infrastructure. | ||
| * @param int $retries The number of retries before the SDK stops trying to connect. | ||
| * @param int $timeout Amount of time, in milliseconds, before the SDK will stop trying to connect. | ||
| * @param callable|null $eventLogger A callback function which runs after SDK events. | ||
| * @return ABsmartly ABsmartly instance created using the credentials and details above. | ||
| */ | ||
| public static function createSimple( | ||
| string $endpoint, | ||
| string $apiKey, | ||
| string $application, | ||
| string $environment, | ||
| int $retries = 5, | ||
| int $timeout = 3000, | ||
| ?callable $eventLogger = null | ||
| ): ABsmartly { | ||
| $clientConfig = new ClientConfig( | ||
| $endpoint, | ||
| $apiKey, | ||
| $application, | ||
| $environment, | ||
| ); | ||
| $clientConfig->setRetries($retries); | ||
| $clientConfig->setTimeout($timeout); | ||
|
|
||
| $client = new Client($clientConfig, new HTTPClient()); | ||
| $sdkConfig = new Config($client); | ||
| if ($eventLogger !== null) { | ||
| $sdkConfig->setContextEventLogger(new \ABSmartly\SDK\Context\ContextEventLoggerCallback($eventLogger)); | ||
| } | ||
| return new ABsmartly($sdkConfig); | ||
| } | ||
|
|
||
| /** | ||
| * @deprecated Use createSimple() instead. This method has $environment and $application in the wrong order | ||
| * relative to ClientConfig::__construct(). createSimple() fixes this with the correct order: | ||
| * endpoint, apiKey, application, environment. | ||
| * | ||
| * @param string $endpoint URL to your API endpoint. Most commonly "your-company.absmartly.io". | ||
| * @param string $apiKey API key which can be found on the Web Console. | ||
| * @param string $environment Environment of the platform where the SDK is installed. Environments are created on | ||
| * the Web Console and should match the available environments in your infrastructure. | ||
| * @param string $application Name of the application where the SDK is installed. Applications are created on the | ||
| * Web Console and should match the applications where your experiments will be running. | ||
| * @param int $retries The number of retries before the SDK stops trying to connect. | ||
| * @param int $timeout Amount of time, in milliseconds, before the SDK will stop trying to connect. | ||
| * @param callable|null $eventLogger A callback function which runs after SDK events. | ||
| * @return ABsmartly ABsmartly instance created using the credentials and details above. | ||
| */ | ||
| public static function createWithDefaults( | ||
| string $endpoint, | ||
| string $apiKey, | ||
| string $environment, | ||
| string $application, | ||
| int $retries = 5, | ||
| int $timeout = 3000, | ||
| ?callable $eventLogger = null | ||
| ): ABsmartly { | ||
|
|
||
| $clientConfig = new ClientConfig( | ||
| $endpoint, | ||
| $apiKey, | ||
| $application, | ||
| $environment, | ||
| ); | ||
| $clientConfig->setRetries($retries); | ||
| $clientConfig->setTimeout($timeout); | ||
|
|
||
| $client = new Client($clientConfig, new HTTPClient()); | ||
| $sdkConfig = new Config($client); | ||
| if ($eventLogger !== null) { | ||
| $sdkConfig->setContextEventLogger(new \ABSmartly\SDK\Context\ContextEventLoggerCallback($eventLogger)); | ||
| } | ||
| return new ABsmartly($sdkConfig); | ||
| } | ||
|
|
||
| public function createContext(ContextConfig $contextConfig): Context { | ||
| $this->applyEventLogger($contextConfig); | ||
| return Context::createFromContextConfig($this, $contextConfig, $this->provider, $this->handler); | ||
| } | ||
|
|
||
| public function createContextWithData(ContextConfig $contextConfig, ContextData $contextData): Context { | ||
| $this->applyEventLogger($contextConfig); | ||
| return Context::createFromContextConfig($this, $contextConfig, $this->provider, $this->handler, $contextData); | ||
| } | ||
|
|
||
| private function applyEventLogger(ContextConfig $contextConfig): void { | ||
| if ($this->eventLogger !== null && $contextConfig->getEventLogger() === null) { | ||
| $contextConfig->setEventLogger($this->eventLogger); | ||
| } | ||
| } | ||
|
|
||
| public function createContextAsync(ContextConfig $contextConfig): PromiseInterface { | ||
| $this->applyEventLogger($contextConfig); | ||
| if (!$this->provider instanceof AsyncContextDataProvider) { | ||
| return resolve($this->createContext($contextConfig)); | ||
| } | ||
|
|
||
| return $this->provider->getContextDataAsync() | ||
| ->then(fn($data) => $this->createContextWithData($contextConfig, $data)); | ||
| } | ||
|
|
||
| public function createContextPending(ContextConfig $contextConfig): array { | ||
| $this->applyEventLogger($contextConfig); | ||
| $context = Context::createPending($this, $contextConfig, $this->provider, $this->handler); | ||
|
|
||
| if (!$this->provider instanceof AsyncContextDataProvider) { | ||
| $promise = resolve(null)->then(function() use ($context) { | ||
| $data = $this->provider->getContextData(); | ||
| $context->setContextData($data); | ||
| return $context; | ||
| }); | ||
| } else { | ||
| $promise = $this->provider->getContextDataAsync() | ||
| ->then(function($data) use ($context) { | ||
| $context->setContextData($data); | ||
| return $context; | ||
| }); | ||
| } | ||
|
|
||
| return ['context' => $context, 'promise' => $promise]; | ||
| } | ||
|
|
||
| public function close(): void { | ||
| $this->client->close(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| <?php | ||
|
|
||
| namespace ABSmartly\SDK\Client; | ||
|
|
||
| use ABSmartly\SDK\PublishEvent; | ||
| use React\Promise\PromiseInterface; | ||
|
|
||
| interface AsyncClientInterface extends ClientInterface { | ||
| public function getContextDataAsync(): PromiseInterface; | ||
| public function publishAsync(PublishEvent $publishEvent): PromiseInterface; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,29 +3,38 @@ | |
| namespace ABSmartly\SDK\Client; | ||
|
|
||
| use ABSmartly\SDK\Context\ContextData; | ||
| use ABSmartly\SDK\Http\HttpClientInterface; | ||
| use ABSmartly\SDK\Http\AsyncHttpClientInterface; | ||
| use ABSmartly\SDK\Http\HTTPClient; | ||
| use ABSmartly\SDK\PublishEvent; | ||
| use React\Promise\PromiseInterface; | ||
|
|
||
| use function json_decode; | ||
| use function json_encode; | ||
| use function React\Promise\resolve; | ||
| use function rtrim; | ||
|
|
||
| use const JSON_THROW_ON_ERROR; | ||
|
|
||
| class Client { | ||
| class Client implements AsyncClientInterface { | ||
| protected const VERSION = '1.0.3'; | ||
| private HTTPClient $httpClient; | ||
| private HttpClientInterface $httpClient; | ||
| private string $url; | ||
| private array $query; | ||
| private array $headers; | ||
|
|
||
| public function __construct(ClientConfig $clientConfig, ?HTTPClient $HTTPClient = null) { | ||
| public function __construct(ClientConfig $clientConfig, ?HttpClientInterface $HTTPClient = null) { | ||
| if (!$HTTPClient) { | ||
| $HTTPClient = new HTTPClient(); | ||
| } | ||
| $this->httpClient = $HTTPClient; | ||
| $this->httpClient->timeout = $clientConfig->getTimeout(); | ||
| $this->httpClient->retries = $clientConfig->getRetries(); | ||
|
|
||
| if (property_exists($this->httpClient, 'timeout')) { | ||
| $this->httpClient->timeout = $clientConfig->getTimeout(); | ||
| } | ||
| if (property_exists($this->httpClient, 'retries')) { | ||
| $this->httpClient->retries = $clientConfig->getRetries(); | ||
| } | ||
|
|
||
| $this->url = rtrim($clientConfig->getEndpoint(), '/') .'/context'; | ||
| $this->query = [ | ||
|
|
@@ -45,23 +54,48 @@ public function __construct(ClientConfig $clientConfig, ?HTTPClient $HTTPClient | |
|
|
||
|
|
||
| private function authRequest(): void { | ||
|
|
||
| if (empty($this->headers['X-API-Key'])) { | ||
| throw new \ABSmartly\SDK\Exception\RuntimeException( | ||
| 'API key is not configured. Please set a valid API key in ClientConfig.' | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| public function getContextData(): ContextData { | ||
| $this->authRequest(); | ||
| $response = $this->httpClient->get($this->url, $this->query, $this->headers); | ||
| $response = $this->decode($response->content); | ||
| return new ContextData($response->experiments); | ||
| $decoded = $this->decode($response->content); | ||
| return new ContextData($decoded->experiments); | ||
| } | ||
|
|
||
| public function getContextDataAsync(): PromiseInterface { | ||
| if (!$this->httpClient instanceof AsyncHttpClientInterface) { | ||
| return resolve($this->getContextData()); | ||
| } | ||
|
|
||
| $this->authRequest(); | ||
| return $this->httpClient | ||
| ->getAsync($this->url, $this->query, $this->headers) | ||
| ->then(fn($response) => new ContextData($this->decode($response->content)->experiments)); | ||
| } | ||
|
|
||
| public function publish(PublishEvent $publishEvent): void { | ||
| $data = $this->encode($publishEvent); | ||
| $this->httpClient->put($this->url, $this->query, $this->headers, $data); | ||
| } | ||
|
Comment on lines
82
to
85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate the API key in
Proposed fix public function publish(PublishEvent $publishEvent): void {
+ $this->authRequest();
$data = $this->encode($publishEvent);
$this->httpClient->put($this->url, $this->query, $this->headers, $data);
}🤖 Prompt for AI Agents |
||
|
|
||
| public function publishAsync(PublishEvent $publishEvent): PromiseInterface { | ||
| if (!$this->httpClient instanceof AsyncHttpClientInterface) { | ||
| $this->publish($publishEvent); | ||
| return resolve(null); | ||
| } | ||
|
|
||
| $data = $this->encode($publishEvent); | ||
| return $this->httpClient->putAsync($this->url, $this->query, $this->headers, $data); | ||
| } | ||
|
Comment on lines
+71
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: You get a synchronous exception, not a rejected
Only exceptions thrown inside a promise resolver/canceller (i.e., inside Sources: [1] 🏁 Script executed: cd src/Client && head -100 Client.php | cat -nRepository: absmartly/php-sdk Length of output: 3848 🏁 Script executed: rg "authRequest" --type php -B 2 -A 2Repository: absmartly/php-sdk Length of output: 852 🏁 Script executed: rg "publish|getContextData" --type php -A 3 | head -80Repository: absmartly/php-sdk Length of output: 4776 🏁 Script executed: rg "publish\(\)" tests/ --type php -B 3 -A 3 | head -100Repository: absmartly/php-sdk Length of output: 5623 🏁 Script executed: cat -n tests/Client/ClientAsyncTest.phpRepository: absmartly/php-sdk Length of output: 4835 🏁 Script executed: rg "authRequest|X-API-Key|API key|RuntimeException" tests/ --type php -B 2 -A 2Repository: absmartly/php-sdk Length of output: 5421 🏁 Script executed: rg "publish" src/ --type php -B 3 -A 3 | grep -A 5 -B 5 "authRequest"Repository: absmartly/php-sdk Length of output: 43 🏁 Script executed: rg "getContextData|publish" src/Client/Client.php -B 2 -A 8Repository: absmartly/php-sdk Length of output: 1386 🏁 Script executed: rg "API key|getApiKey" tests/ --type php -B 2 -A 2Repository: absmartly/php-sdk Length of output: 580 🏁 Script executed: rg "publish.*auth|auth.*publish" src/ tests/ --type php -i -B 2 -A 2Repository: absmartly/php-sdk Length of output: 43 🏁 Script executed: cat -n src/Client/AsyncClientInterface.phpRepository: absmartly/php-sdk Length of output: 440 🏁 Script executed: grep -r "getApiKey\|API key" src/Client/ --include="*.php" -B 2 -A 2Repository: absmartly/php-sdk Length of output: 1310 Wrap synchronous operations in try/catch to prevent promise contract violations. Both async methods execute synchronous code before returning a Additionally, Ensure both sync and async publish methods validate authentication, and wrap all sync operations in try/catch blocks that return rejected promises on failure. 🤖 Prompt for AI Agents |
||
|
|
||
| public function decode(string $jsonString): object { | ||
| return json_decode($jsonString, false, 16, JSON_THROW_ON_ERROR); | ||
| return json_decode($jsonString, false, 512, JSON_THROW_ON_ERROR); | ||
| } | ||
|
|
||
| public function encode(object $object): string { | ||
|
|
@@ -71,4 +105,8 @@ public function encode(object $object): string { | |
| public function close(): void { | ||
| $this->httpClient->close(); | ||
| } | ||
|
|
||
| public function isAsync(): bool { | ||
| return $this->httpClient instanceof AsyncHttpClientInterface; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <?php | ||
|
|
||
| namespace ABSmartly\SDK\Client; | ||
|
|
||
| use ABSmartly\SDK\Context\ContextData; | ||
| use ABSmartly\SDK\PublishEvent; | ||
|
|
||
| interface ClientInterface { | ||
| public function getContextData(): ContextData; | ||
| public function publish(PublishEvent $publishEvent): void; | ||
| public function close(): void; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: absmartly/php-sdk
Length of output: 9434
ReactPHP packages must be moved from
require-devtorequire.Multiple runtime classes in
src/have hard dependencies on ReactPHP packages:ReactHttpClientimportsReact\Http\BrowserandReact\Async\await;Client,AsyncClientInterface,ABsmartly, and async context classes all useReact\Promise\PromiseInterfacein their public method signatures. Consumers installing this library without the--devflag will encounter fatal class/interface-not-found errors when instantiating async components. Movereact/http,react/async, andreact/promiseto therequiresection, or fully hard-isolate async code so autoloaded runtime classes do not depend on these packages.🤖 Prompt for AI Agents