diff --git a/.gitattributes b/.gitattributes index 641e8a3b..a4e1b5c5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,6 @@ src/Resources/*.php linguist-generated src/Routes/*.php linguist-generated -src/SeamClient.php linguist-generated +src/Seam.php linguist-generated # Keep development files out of the published package. # GitHub builds the archives Composer downloads as dist with git archive, diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 95ca05d1..749b0e39 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -20,7 +20,10 @@ jobs: os: - ubuntu-latest php: - - '8.0' + - '8.1' + - '8.2' + - '8.3' + - '8.4' - '8.5' include: - os: ubuntu-latest @@ -44,7 +47,7 @@ jobs: fail-fast: false matrix: php: - - '8.0' + - '8.1' - '8.5' steps: - name: Checkout @@ -73,7 +76,10 @@ jobs: os: - ubuntu-latest php: - - '8.0' + - '8.1' + - '8.2' + - '8.3' + - '8.4' - '8.5' include: - os: ubuntu-latest diff --git a/.gitignore b/.gitignore index 78aaf436..b75b485f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ vendor tmp # PHPUnit result cache -.phpunit.result.cache +.phpunit.cache/ # Build directories package diff --git a/README.md b/README.md index 567a864c..9ba25b03 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,167 @@ # Seam PHP SDK -Control locks, lights and other internet of things devices with Seam's simple API. +[![Packagist](https://img.shields.io/packagist/v/seamapi/seam.svg)](https://packagist.org/packages/seamapi/seam) +[![Seam LTS Version](https://img.shields.io/badge/Seam_LTS-1.0.0-blue)](https://docs.seam.co/lts) +[![GitHub Actions](https://github.com/seamapi/php/actions/workflows/check.yml/badge.svg)](https://github.com/seamapi/php/actions/workflows/check.yml) -Check out [the documentation](https://docs.seam.co) or the usage below. +PHP SDK for the Seam API. + +## Description + +[Seam] makes it easy to integrate IoT devices with your applications. +This is an official SDK for the Seam API. +Please refer to the official [Seam Docs] to get started. + +Parts of this SDK are generated from always up-to-date type information +provided by [@seamapi/types]. +This ensures all API methods, request shapes, and response shapes are +accurate and fully typed. + +The underlying HTTP client is [Guzzle]. + +[Seam]: https://www.seam.co/ +[Seam Docs]: https://docs.seam.co/latest/ +[@seamapi/types]: https://github.com/seamapi/types/ +[Guzzle]: https://docs.guzzlephp.org/ + +## Installation + +Add this as a dependency to your project using [Composer] with + +``` +$ composer require seamapi/seam +``` + +[Composer]: https://getcomposer.org/ ## Usage +> [!NOTE] +> These examples assume `SEAM_API_KEY` is set in your environment. + +### Examples + +#### List devices + ```php -$seam = new Seam\SeamClient("YOUR_API_KEY"); +$seam = new Seam\Seam(); -# Create a Connect Webview to login to a provider -$connect_webview = $seam->connect_webviews->create( - accepted_providers: ["august"] -); +$devices = $seam->devices->list(); +``` -print "Please Login at this url: " . $connect_webview->url; +#### Unlock a door -# Poll until connect webview is completed -while (true) { - $connect_webview = $seam->connect_webviews->get( - $connect_webview->connect_webview_id - ); - if ($connect_webview->status == "authorized") { - break; - } else { - sleep(1); - } -} +```php +$seam = new Seam\Seam(); -$connected_account = $seam->connected_accounts->get( - $connect_webview->connected_account_id -); +$lock = $seam->locks->get(name: "Front Door"); +$seam->locks->unlock_door($lock->device_id); +``` + +### Authentication Method + +The SDK supports two authentication mechanisms. +Configure either by passing the corresponding options to the `Seam` +constructor, or with the more ergonomic static factory methods. -print "Looks like you connected with " . - json_encode($connected_account->user_identifier); +#### API Key + +An API key is scoped to a single workspace and should only be used on the +server. Obtain one from the Seam Console. + +```php +// Set the SEAM_API_KEY environment variable +$seam = new Seam\Seam(); -$devices = $seam->devices->list( - connected_account_id: $connected_account->connected_account_id +// Pass as an option to the constructor +$seam = new Seam\Seam(api_key: "your-api-key"); + +// Use the factory method +$seam = Seam\Seam::from_api_key("your-api-key"); +``` + +#### Personal Access Token + +A Personal Access Token is scoped to a Seam Console user. +It must be used with a workspace id. + +```php +// Pass as options to the constructor +$seam = new Seam\Seam( + personal_access_token: "your-personal-access-token", + workspace_id: "your-workspace-id" ); -print "You have " . count($devices) . " devices"; +// Use the factory method +$seam = Seam\Seam::from_personal_access_token( + "your-personal-access-token", + "your-workspace-id" +); +``` + +### Action Attempts + +Some operations tell a device to do something, and the device may take time +to report back. Those endpoints return an [action attempt]. + +By default the SDK waits for the action attempt to finish: + +- It polls up to a timeout at a polling interval. +- It returns a fresh copy of the successful action attempt. +- It throws `Seam\ActionAttemptFailedError` if the action failed. +- It throws `Seam\ActionAttemptTimeoutError` if the timeout + elapses first. + +Both errors extend `Seam\ActionAttemptError` and expose the action +attempt with `getActionAttempt()`. -$device_id = $devices[0]->device_id; +[action attempt]: https://docs.seam.co/latest/core-concepts/action-attempts -# Lock a Door -$seam->locks->lock_door($device_id); +```php +use Seam\ActionAttemptFailedError; +use Seam\ActionAttemptTimeoutError; + +try { + $seam->locks->unlock_door($device_id); +} catch (ActionAttemptFailedError $error) { + print "Could not unlock the door: " . $error->getMessage(); + print "Error code: " . $error->getErrorCode(); +} catch (ActionAttemptTimeoutError $error) { + print "The door did not unlock in time"; + print "Action attempt: " . $error->getActionAttempt()->action_attempt_id; +} +``` + +Waiting may be disabled for the whole client: -$updated_device = $seam->devices->get($device_id); -$updated_device->properties->locked; // true +```php +$seam = new Seam\Seam(wait_for_action_attempt: false); + +$action_attempt = $seam->locks->unlock_door($device_id); +$action_attempt->status; // "pending" +``` -# Unlock a Door -$seam->locks->unlock_door($device_id); -$updated_device->properties->locked; // false +or for a single request: -# Create an access code on a device -$access_code = $seam->access_codes->create( - device_id: $device_id, - code: "1234", - name: "Test Code" +```php +$action_attempt = $seam->locks->unlock_door( + $device_id, + wait_for_action_attempt: false ); +``` + +The timeout and polling interval, both in seconds, may be configured either +on the client or per request: -# Check the status of an access code -$access_code->status; // 'setting' (it will go to 'set' when active on the device) +```php +$seam = new Seam\Seam( + wait_for_action_attempt: ["timeout" => 30.0, "polling_interval" => 2.0] +); -$seam->access_codes->delete($access_code->access_code_id); +$seam->locks->unlock_door( + $device_id, + wait_for_action_attempt: ["timeout" => 5.0] +); ``` ### Pagination @@ -153,21 +251,172 @@ $pages = $seam->createPaginator( $connectedAccounts = $pages->flattenToArray(); ``` -## Installation +### Interacting with Multiple Workspaces + +Some endpoints are not scoped to a workspace. Use `SeamMultiWorkspace` with a +personal access token to reach them. + +```php +$seam = Seam\SeamMultiWorkspace::from_personal_access_token( + "your-personal-access-token" +); + +// List workspaces authorized for this Personal Access Token +$workspaces = $seam->workspaces->list(); + +$workspace = $seam->workspaces->create( + name: "New Workspace", + connect_partner_name: "Your Company" +); +``` + +### Webhooks + +Seam delivers webhooks with [Svix]. Verify and parse an incoming request with +`SeamWebhook`, which returns the typed event. + +[Svix]: https://www.svix.com/ + +```php +$webhook = new Seam\SeamWebhook($webhook_secret); + +try { + $event = $webhook->verify($request_body, $request_headers); + print $event->event_type; +} catch (Svix\Exception\WebhookVerificationException $error) { + http_response_code(400); +} +``` + +### Advanced Usage + +#### Setting the endpoint + +The endpoint may be set with the `SEAM_ENDPOINT` environment variable, or +passed directly. + +```php +$seam = new Seam\Seam(endpoint: "https://example.com"); +``` + +#### Configuring the Guzzle client + +Pass any [Guzzle request option] with `guzzle_options`. They are merged into +the client the SDK builds, so the authorization and SDK headers are kept. + +[Guzzle request option]: https://docs.guzzlephp.org/en/stable/request-options.html -To install the latest version of the automatically generated SDK, run: +```php +$seam = new Seam\Seam( + guzzle_options: [ + "timeout" => 30, + "headers" => ["X-Custom-Header" => "value"], + "proxy" => "http://localhost:8125", + ] +); +``` + +> [!NOTE] +> Unlike the other Seam SDKs, this one sets a default request timeout of 60 +> seconds so a hung connection eventually fails. Pass `timeout` to change it, +> or `0` to disable it. + +#### Retries + +Failed requests are retried twice by default, with exponential backoff. + +A request that never reached the server, e.g. a connection failure, is always +retried. A request that did reach the server is only retried on a retryable +status when the HTTP method is idempotent. Every Seam endpoint is a `POST`, so +retrying one that the server may already have processed could duplicate a +write. The other Seam SDKs make the same trade. + +```php +// Retry more times +$seam = new Seam\Seam(retries: 5); + +// Turn retries off +$seam = new Seam\Seam(retries: 0); +``` + +#### Overriding the client + +Pass an already configured Guzzle client. It carries its own endpoint and +authorization, so no other authentication option may be given alongside it. -`composer require seamapi/seam` +```php +$client = new GuzzleHttp\Client([ + "base_uri" => "https://connect.getseam.com", + "headers" => ["authorization" => "Bearer " . $api_key], +]); + +$seam = Seam\Seam::from_client($client); +``` + +#### Errors -If you want to install our previous handwritten version, run: +Every exception the SDK raises implements `Seam\SeamException`. -`composer require seamapi/seam:1.1` +| Error | Raised when | +| --------------------------- | -------------------------------------------------- | +| `HttpApiError` | The API returned an error response. | +| `HttpUnauthorizedError` | The credentials were rejected. | +| `HttpInvalidInputError` | The request parameters were rejected. | +| `InvalidOptionsError` | The client options are incomplete or incompatible. | +| `InvalidTokenError` | The token is of the wrong kind or format. | +| `ActionAttemptFailedError` | An action attempt finished in the error state. | +| `ActionAttemptTimeoutError` | An action attempt did not finish in time. | + +An error response that is not shaped like a Seam error, e.g. a gateway +returning HTML, surfaces as the underlying `GuzzleHttp\Exception\ +BadResponseException` instead. Transport failures surface as the +corresponding Guzzle exception. + +```php +use Seam\HttpApiError; +use Seam\HttpInvalidInputError; + +try { + $seam->devices->get($device_id); +} catch (HttpInvalidInputError $error) { + print_r($error->getValidationErrorMessages("device_id")); +} catch (HttpApiError $error) { + print $error->getErrorCode(); + print $error->getStatusCode(); + print $error->getRequestId(); +} +``` + +## Upgrading from 3.x + +Version 4 brings this SDK in line with the Seam SDKs for other languages. + +- PHP 8.1 or later is now required. +- The client class is `Seam\Seam`. `Seam\SeamClient` still works as a + deprecated alias. +- The constructor takes named options. `$endpoint` is no longer the second + positional argument, and `$throw_http_errors` is gone; API errors always + raise a Seam exception. +- The exception classes keep their `Seam\` namespace. They now all implement + a shared `Seam\SeamException` interface, and `Seam\InvalidOptionsError` and + `Seam\InvalidTokenError` are new. +- `$seam->action_attempts->poll_until_ready()` was removed. Use + `wait_for_action_attempt`, which now also accepts a `timeout` and + `polling_interval`. The defaults changed from 20s/0.4s to 10s/1s. +- `$seam->client` is a `Seam\Http\SeamHttpClient`. The Guzzle client is + available with `$seam->client->get_client()`. +- The `$seam->api_key` property and the global `LTS_VERSION` constant were + removed. Use `Seam\Seam::LTS_VERSION`. +- Responses in the 3xx range are no longer treated as successful. +- Requests are now retried; see [Retries](#retries). +- Pagination metadata is a `Seam\Pagination` object rather than a + `stdClass`. ## Development and Testing ### Quickstart -Install [PHP](https://www.php.net/) 8.0 or later, +Install [PHP](https://www.php.net/) 8.1 or later, [Composer](https://getcomposer.org/) and [Node.js](https://nodejs.org/), then run @@ -186,13 +435,13 @@ View them with $ composer run-script --list ``` -| Task | Command | -| ----------------- | ------------------ | -| Run the tests | `composer test` | -| Lint | `composer lint` | -| Format | `npm run format` | -| Build the package | `composer build` | -| Generate the SDK | `npm run generate` | +| Task | Command | +| ---------------------- | ------------------ | +| Run the tests | `composer test` | +| Lint and analyze types | `composer lint` | +| Format | `npm run format` | +| Build the package | `composer build` | +| Generate the SDK | `npm run generate` | Formatting is handled by [Prettier](https://prettier.io/) via [@prettier/plugin-php](https://github.com/prettier/plugin-php), @@ -215,10 +464,20 @@ $ composer test -- tests/MyTest.php PHPUnit is configured in `phpunit.xml.dist`. +Static analysis is handled by [Psalm](https://psalm.dev/), configured in +`psalm.xml` and run as part of `composer lint`. The generated sources under +`src/Resources` and `src/Routes` are excluded, since analyzing them would only +create pressure to change the generator. + ### Requirements -This package supports PHP 8.0 and later. -Continuous integration exercises both ends of that range, PHP 8.0 and 8.5. +This package supports PHP 8.1 and later. +Continuous integration exercises every supported version, PHP 8.1 through 8.5. + +The test suite runs against [@seamapi/fake-seam-connect], which is started +automatically for each test, so `npm install` must have been run first. + +[@seamapi/fake-seam-connect]: https://github.com/seamapi/fake-seam-connect ### Publishing diff --git a/codegen/layouts/partials/client-class.hbs b/codegen/layouts/partials/client-class.hbs index a205fc3f..79d6dd3d 100644 --- a/codegen/layouts/partials/client-class.hbs +++ b/codegen/layouts/partials/client-class.hbs @@ -1,6 +1,11 @@ class {{clientName}}Client { - private SeamClient $seam; + private SeamHttpClient $client; + + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; {{#if hasChildClients}} {{#each childClients}} public {{clientName}}Client ${{namespace}}; @@ -8,18 +13,19 @@ class {{clientName}}Client {{else}} {{/if}} - public function __construct(SeamClient $seam) + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; {{#each childClients}} - $this->{{namespace}} = new {{clientName}}Client($seam); + $this->{{namespace}} = new {{clientName}}Client($client, $defaults); {{/each}} } {{#each methods}} {{> route-method}} {{/each}} -{{#if isActionAttempts}} -{{> poll-until-ready}} -{{/if}} } diff --git a/codegen/layouts/partials/poll-until-ready.hbs b/codegen/layouts/partials/poll-until-ready.hbs deleted file mode 100644 index 87ce726d..00000000 --- a/codegen/layouts/partials/poll-until-ready.hbs +++ /dev/null @@ -1,24 +0,0 @@ - public function poll_until_ready(string $action_attempt_id, float $timeout = 20.0): ActionAttempt - { - $seam = $this->seam; - $time_waiting = 0.0; - $polling_interval = 0.4; - $action_attempt = $seam->action_attempts->get($action_attempt_id); - - while ($action_attempt->status == 'pending') { - $action_attempt = $seam->action_attempts->get( - $action_attempt->action_attempt_id - ); - if ($time_waiting > $timeout) { - throw new ActionAttemptTimeoutError($action_attempt, $timeout); - } - $time_waiting += $polling_interval; - usleep($polling_interval * 1000000); - } - - if ($action_attempt->status == 'error') { - throw new ActionAttemptFailedError($action_attempt); - } - - return $action_attempt; - } diff --git a/codegen/layouts/partials/route-method.hbs b/codegen/layouts/partials/route-method.hbs index 9c73dfd9..bf344f74 100644 --- a/codegen/layouts/partials/route-method.hbs +++ b/codegen/layouts/partials/route-method.hbs @@ -3,14 +3,18 @@ {{#if hasParams}} $request_payload = []; -{{#each paramNames}} - if (${{this}} !== null) { - $request_payload["{{this}}"] = ${{this}}; +{{#each parameters}} +{{#if required}} + $request_payload["{{name}}"] = ${{name}}; +{{else}} + if (${{name}} !== null) { + $request_payload["{{name}}"] = ${{name}}; } +{{/if}} {{/each}} {{/if}} - {{#unless returnsVoid}}$res = {{/unless}}$this->seam->request( + {{#unless returnsVoid}}$res = {{/unless}}$this->client->request( "POST", "{{path}}", {{#if hasParams}} @@ -19,13 +23,11 @@ ); {{#if usesActionAttempt}} - if (!$wait_for_action_attempt) { - return {{returnResource}}::from_json($res->{{returnPath}}); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready($res->action_attempt->action_attempt_id); - - return $action_attempt; + return ResolveActionAttempt::resolve_action_attempt( + {{returnResource}}::from_json($res->{{returnPath}}), + $this->client, + $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"] + ); {{else}} {{#if usesOnResponse}} diff --git a/codegen/layouts/seam-client.hbs b/codegen/layouts/seam-client.hbs index d39a7c01..c452d625 100644 --- a/codegen/layouts/seam-client.hbs +++ b/codegen/layouts/seam-client.hbs @@ -5,96 +5,158 @@ namespace Seam; {{#each useStatements}} use {{this}}; {{/each}} -use Seam\Utils\PackageVersion; -use GuzzleHttp\Client as HTTPClient; -use \Exception as Exception; -use Seam\HttpApiError; -use Seam\HttpUnauthorizedError; -use Seam\HttpInvalidInputError; - -define('LTS_VERSION', '1.0.0'); - -class SeamClient +use GuzzleHttp\ClientInterface; +use Seam\Http\SeamHttpClient; + +/** + * Client for the Seam API. + * + * Authenticate with an API key, which is scoped to a single workspace, or with + * a personal access token together with the id of the workspace to act on. + * When neither is given, the SEAM_API_KEY environment variable is used. + * + * @see https://docs.seam.co/ + */ +class Seam { {{#each parentClients}} public {{clientName}}Client ${{namespace}}; {{/each}} - public string $api_key; - public HTTPClient $client; - public string $ltsVersion = LTS_VERSION; - + /** + * The long term support version of the Seam API this SDK targets. + */ + public const LTS_VERSION = SeamHttpClient::LTS_VERSION; + + public SeamHttpClient $client; + + /** + * Default request options applied to every call, currently just + * wait_for_action_attempt. + * + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + public array $defaults; + + /** + * @param bool|array{timeout?: float, polling_interval?: float}|null $wait_for_action_attempt Whether to wait for action attempts to finish, optionally with timeout and polling_interval in seconds. Defaults to true. + * @param array $guzzle_options Options merged into the underlying Guzzle client, e.g. timeout or headers. + * @param int|null $retries How many times to retry a failed request. Defaults to 2; pass 0 to disable. + * @param ClientInterface|null $client A preconfigured Guzzle client. It carries its own endpoint and authorization, so no other authentication option may be given with it. + */ public function __construct( - $api_key = null, - $endpoint = "https://connect.getseam.com", - $throw_http_errors = false + ?string $api_key = null, + ?string $personal_access_token = null, + ?string $workspace_id = null, + ?string $endpoint = null, + bool|array|null $wait_for_action_attempt = null, + array $guzzle_options = [], + ?int $retries = null, + ?ClientInterface $client = null ) { - $this->api_key = $api_key ?: (getenv('SEAM_API_KEY') ?: null); - $seam_sdk_version = PackageVersion::get(); - $this->client = new HTTPClient([ - "base_uri" => $endpoint, - "timeout" => 60.0, - "headers" => [ - "Authorization" => "Bearer " . $this->api_key, - "User-Agent" => "Seam PHP Client ". $seam_sdk_version, - "seam-sdk-name" => "seamapi/php", - "seam-sdk-version" => $seam_sdk_version, - "seam-lts-version" => $this->ltsVersion - ], - "http_errors" => $throw_http_errors, - ]); + $this->defaults = [ + "wait_for_action_attempt" => $wait_for_action_attempt ?? true, + ]; + + // A client carries its own endpoint and authorization, so the + // authentication options are only read when one has to be built. + $this->client = $client !== null + ? SeamHttpClient::from_client($client) + : new SeamHttpClient( + Options::get_endpoint($endpoint), + Auth::get_auth_headers($api_key, $personal_access_token, $workspace_id), + $guzzle_options, + $retries + ); + {{#each parentClients}} - $this->{{namespace}} = new {{clientName}}Client($this); + $this->{{namespace}} = new {{clientName}}Client($this->client, $this->defaults); {{/each}} } + /** + * Creates a client authorized with an API key. + */ + public static function from_api_key( + string $api_key, + ?string $endpoint = null, + bool|array|null $wait_for_action_attempt = null, + array $guzzle_options = [], + ?int $retries = null + ): static { + return new static( + api_key: $api_key, + endpoint: $endpoint, + wait_for_action_attempt: $wait_for_action_attempt, + guzzle_options: $guzzle_options, + retries: $retries + ); + } + + /** + * Creates a client authorized with a personal access token, scoped to the + * given workspace. + */ + public static function from_personal_access_token( + string $personal_access_token, + string $workspace_id, + ?string $endpoint = null, + bool|array|null $wait_for_action_attempt = null, + array $guzzle_options = [], + ?int $retries = null + ): static { + return new static( + personal_access_token: $personal_access_token, + workspace_id: $workspace_id, + endpoint: $endpoint, + wait_for_action_attempt: $wait_for_action_attempt, + retries: $retries, + guzzle_options: $guzzle_options + ); + } + + /** + * Creates a client from a preconfigured Guzzle client. + */ + public static function from_client( + ClientInterface $client, + bool|array|null $wait_for_action_attempt = null + ): static { + return new static( + client: $client, + wait_for_action_attempt: $wait_for_action_attempt + ); + } + + public function lts_version(): string + { + return self::LTS_VERSION; + } + + /** + * Makes a request against the Seam API with this client's authorization. + * + * @param array|object|null $json + * @param array|null $query + */ public function request( - $method, - $path, - $json = null, - $query = null, - ) { - $options = [ - "json" => $json, - "query" => $query, - ]; - $options = array_filter($options, fn($option) => $option !== null); - - $response = $this->client->request($method, $path, $options); - $status_code = $response->getStatusCode(); - $request_id = $response->getHeaderLine("seam-request-id"); - - $res_json = null; - try { - $res_json = json_decode($response->getBody()); - } catch (Exception $ignoreError) { - } - - if ($status_code >= 400) { - if ($status_code === 401) { - throw new HttpUnauthorizedError($request_id); - } - - if (($res_json->error ?? null) != null) { - if ($res_json->error->type === 'invalid_input') { - throw new HttpInvalidInputError($res_json->error, $status_code, $request_id); - } - - throw new HttpApiError($res_json->error, $status_code, $request_id); - } - - throw \GuzzleHttp\Exception\RequestException::create( - new \GuzzleHttp\Psr7\Request($method, $path), - $response - ); - } - - return $res_json; + string $method, + string $path, + mixed $json = null, + ?array $query = null + ): mixed { + return $this->client->request($method, $path, $json, $query); } - public function createPaginator($request, $params = []) + /** + * Creates a paginator for a list endpoint. + * + * @param callable $request Invokes the list method with a params array, e.g. fn($params) => $seam->devices->list(...$params) + * @param array $params + */ + public function createPaginator(callable $request, array $params = []): Paginator { - return new Paginator($request, $params); + return new Paginator($request, $params); } } diff --git a/codegen/lib/handlebars-helpers.ts b/codegen/lib/handlebars-helpers.ts index 263be9ce..62b6574a 100644 --- a/codegen/lib/handlebars-helpers.ts +++ b/codegen/lib/handlebars-helpers.ts @@ -9,7 +9,13 @@ export interface DeprecatedPhpDocContext { export interface MethodPhpDocContext extends DeprecatedPhpDocContext { returnType: string responseDescription: string - parameters: Array<{ name: string; type: string; description: string }> + // The endpoint parameters plus the SDK level ones, e.g. + // wait_for_action_attempt, so editors surface all of them. + documentedParameters: Array<{ + name: string + type: string + description: string + }> } export const resourcePhpDoc = (context: DeprecatedPhpDocContext): string => @@ -25,7 +31,7 @@ export const methodPhpDoc = (context: MethodPhpDocContext): string => createPhpDoc( context.description, [ - ...context.parameters.map( + ...context.documentedParameters.map( (parameter) => `@param ${parameter.type} $${parameter.name}${parameter.description === '' ? '' : ` ${parameter.description}`}`, ), diff --git a/codegen/lib/layouts/route.ts b/codegen/lib/layouts/route.ts index eb32944a..0cc1ac3e 100644 --- a/codegen/lib/layouts/route.ts +++ b/codegen/lib/layouts/route.ts @@ -8,12 +8,9 @@ import { sortPhpClientMethodParameters, } from '../class-model.js' -const seamClientClass = 'Seam\\SeamClient' +const seamHttpClientClass = 'Seam\\Http\\SeamHttpClient' +const resolveActionAttemptClass = 'Seam\\Http\\ResolveActionAttempt' const resourcesNamespace = 'Seam\\Resources' -const actionAttemptErrorClasses = [ - 'Seam\\ActionAttemptFailedError', - 'Seam\\ActionAttemptTimeoutError', -] export interface MethodLayoutContext { methodName: string @@ -21,12 +18,21 @@ export interface MethodLayoutContext { responseDescription: string isDeprecated: boolean deprecationMessage: string - parameters: Array<{ name: string; type: string; description: string }> + parameters: Array<{ + name: string + type: string + description: string + required: boolean + }> + documentedParameters: Array<{ + name: string + type: string + description: string + }> path: string returnType: string hasParams: boolean signatureParams: string - paramNames: string[] usesActionAttempt: boolean usesOnResponse: boolean returnsVoid: boolean @@ -40,21 +46,34 @@ export interface ClientLayoutContext { hasChildClients: boolean childClients: Array<{ clientName: string; namespace: string }> methods: MethodLayoutContext[] - isActionAttempts: boolean } export interface RouteLayoutContext extends ClientLayoutContext { useStatements: string[] } +const waitForActionAttemptParameter = { + name: 'wait_for_action_attempt', + type: 'bool|array|null', + description: + 'Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client.', + required: false, +} + +const onResponseParameter = { + name: 'on_response', + type: 'callable|null', + description: + 'Called with the raw response envelope, used by the paginator to read the pagination metadata.', + required: false, +} + const getMethodLayoutContext = ( method: PhpClientMethod, - clientName: string, ): MethodLayoutContext => { const { methodName, path, parameters, returnResource, returnPath } = method - const usesActionAttempt = - returnResource === 'ActionAttempt' && clientName !== 'ActionAttempts' + const usesActionAttempt = returnResource === 'ActionAttempt' const usesOnResponse = parameters.some((p) => p.name === 'page_cursor') && methodName === 'list' const returnsVoid = returnResource === '' @@ -71,26 +90,42 @@ const getMethodLayoutContext = ( (p) => `${!(p.required ?? false) && p.type !== 'mixed' ? '?' : ''}${p.type} $${p.name}${(p.required ?? false) ? '' : ' = null'}`, ) - .concat(usesActionAttempt ? ['bool $wait_for_action_attempt = true'] : []) + .concat( + usesActionAttempt + ? ['bool|array|null $wait_for_action_attempt = null'] + : [], + ) .concat(usesOnResponse ? ['?callable $on_response = null'] : []) .join(', ') + const documentedEndpointParameters = sortedParameters.map( + ({ name, type, description, required }) => ({ + name, + type, + description, + required: required ?? false, + }), + ) + return { methodName, description: method.description, responseDescription: method.responseDescription, isDeprecated: method.isDeprecated, deprecationMessage: method.deprecationMessage, - parameters: sortedParameters.map(({ name, type, description }) => ({ - name, - type, - description, - })), + // The request payload is built from the endpoint parameters alone. + parameters: documentedEndpointParameters, + // The SDK level parameters are documented alongside them so editors + // surface all of them, but they never reach the payload. + documentedParameters: [ + ...documentedEndpointParameters, + ...(usesActionAttempt ? [waitForActionAttemptParameter] : []), + ...(usesOnResponse ? [onResponseParameter] : []), + ], path, returnType, hasParams: parameters.length > 0, signatureParams, - paramNames: sortedParameters.map((p) => p.name), usesActionAttempt, usesOnResponse, returnsVoid, @@ -100,44 +135,34 @@ const getMethodLayoutContext = ( } } -// Child clients live in the same namespace as their parent, so only the -// SeamClient, the resource classes returned by the methods, and the action -// attempt errors thrown by poll_until_ready need importing. -const getUseStatements = ( - client: PhpClient, - isActionAttempts: boolean, -): string[] => { +// Child clients live in the same namespace as their parent, so only the HTTP +// client, the action attempt resolver, and the resource classes returned by +// the methods need importing. +const getUseStatements = (client: PhpClient): string[] => { const resourceNames = new Set( client.methods .map((m) => m.returnResource) .filter((resourceName) => resourceName !== ''), ) - if (isActionAttempts) resourceNames.add('ActionAttempt') + const usesActionAttempt = resourceNames.has('ActionAttempt') return [ - seamClientClass, + seamHttpClientClass, + ...(usesActionAttempt ? [resolveActionAttemptClass] : []), ...[...resourceNames].map((name) => `${resourcesNamespace}\\${name}`), - ...(isActionAttempts ? actionAttemptErrorClasses : []), ].sort((a, b) => a.localeCompare(b)) } export const setRouteLayoutContext = ( client: PhpClient, -): RouteLayoutContext => { - const isActionAttempts = client.clientName === 'ActionAttempts' - - return { - useStatements: getUseStatements(client, isActionAttempts), - clientName: client.clientName, - hasChildClients: client.childClientIdentifiers.length > 0, - childClients: client.childClientIdentifiers.map((i) => ({ - clientName: i.clientName, - namespace: i.namespace, - })), - methods: client.methods.map((m) => - getMethodLayoutContext(m, client.clientName), - ), - isActionAttempts, - } -} +): RouteLayoutContext => ({ + useStatements: getUseStatements(client), + clientName: client.clientName, + hasChildClients: client.childClientIdentifiers.length > 0, + childClients: client.childClientIdentifiers.map((i) => ({ + clientName: i.clientName, + namespace: i.namespace, + })), + methods: client.methods.map(getMethodLayoutContext), +}) diff --git a/codegen/lib/routes.ts b/codegen/lib/routes.ts index f18f155a..e49bb63b 100644 --- a/codegen/lib/routes.ts +++ b/codegen/lib/routes.ts @@ -21,7 +21,7 @@ interface Metadata { const resourcesPath = 'src/Resources' const routesPath = 'src/Routes' -const seamClientPath = 'src/SeamClient.php' +const seamClientPath = 'src/Seam.php' export const routes = ( files: Metalsmith.Files, diff --git a/codegen/smith.ts b/codegen/smith.ts index 744efd43..ba1384fa 100644 --- a/codegen/smith.ts +++ b/codegen/smith.ts @@ -12,7 +12,7 @@ import { helpers, routes } from './lib/index.js' const rootDir = dirname(fileURLToPath(import.meta.url)) await Promise.all([ - deleteAsync(['./src/Resources', './src/Routes', './src/SeamClient.php']), + deleteAsync(['./src/Resources', './src/Routes', './src/Seam.php']), ]) const partials = await getHandlebarsPartials(`${rootDir}/layouts/partials`) diff --git a/composer.json b/composer.json index 6827b5ce..486452eb 100644 --- a/composer.json +++ b/composer.json @@ -25,19 +25,18 @@ "source": "https://github.com/seamapi/php" }, "require": { - "php": "^8.0", - "guzzlehttp/guzzle": "^7.5" + "php": "^8.1", + "caseyamcl/guzzle_retry_middleware": "^2.13", + "guzzlehttp/guzzle": "^7.5", + "svix/svix": "^1.40" }, "require-dev": { - "phpunit/phpunit": "^9.5", - "squizlabs/php_codesniffer": "^3.7" + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^6.0" }, "autoload": { "psr-4": { - "Seam\\": [ - "src/", - "src/Exceptions/" - ] + "Seam\\": "src/" }, "classmap": [ "src/Resources/" @@ -58,7 +57,7 @@ "/.env.example", "/.github", "/.npmrc", - "/.phpunit.result.cache", + "/.phpunit.cache", "/.prettierignore", "/.prettierrc.json", "/.releaserc.json", @@ -69,6 +68,7 @@ "/package.json", "/phpunit.xml.dist", "/pkg", + "/psalm.xml", "/tests", "/tmp", "/tsconfig.json", @@ -81,16 +81,19 @@ "test": "phpunit", "lint": [ "@lint:composer", - "@lint:syntax" + "@lint:syntax", + "@lint:types" ], "lint:composer": "@composer validate --strict", - "lint:syntax": "! find src tests -name '*.php' -exec php -l {} \\; | grep -v '^No syntax errors detected'" + "lint:syntax": "! find src tests -name '*.php' -exec php -l {} \\; | grep -v '^No syntax errors detected'", + "lint:types": "psalm --no-cache" }, "scripts-descriptions": { "build": "Build a distributable archive of this package into pkg/.", "test": "Run the test suite.", "lint": "Run all lint checks.", "lint:composer": "Validate composer.json and composer.lock.", - "lint:syntax": "Check every PHP source file for syntax errors." + "lint:syntax": "Check every PHP source file for syntax errors.", + "lint:types": "Run static analysis with Psalm." } } diff --git a/composer.lock b/composer.lock index bc6794ae..a604302c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,38 +4,114 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "47b28c106be0ff4f75680c1fe23e931a", + "content-hash": "25d67117228a0425a94bd9f40c53f692", "packages": [ + { + "name": "caseyamcl/guzzle_retry_middleware", + "version": "v2.13.0", + "source": { + "type": "git", + "url": "https://github.com/caseyamcl/guzzle_retry_middleware.git", + "reference": "17c9299cde438b00bbeb099c6480319a81636a60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/caseyamcl/guzzle_retry_middleware/zipball/17c9299cde438b00bbeb099c6480319a81636a60", + "reference": "17c9299cde438b00bbeb099c6480319a81636a60", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.3|^7.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "jaschilz/php-coverage-badger": "^2.0", + "nesbot/carbon": "^2.0|^3.0", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.0", + "phpunit/phpunit": "^7.5|^8.0|^9.0", + "squizlabs/php_codesniffer": "^3.5", + "symfony/var-dumper": "^5.0|^6.0|^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleRetry\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Casey McLaughlin", + "email": "caseyamcl@gmail.com", + "homepage": "https://caseymclaughlin.com", + "role": "Developer" + } + ], + "description": "Guzzle v6+ retry middleware that handles 429/503 status codes and connection timeouts", + "homepage": "https://github.com/caseyamcl/guzzle_retry_middleware", + "keywords": [ + "Guzzle", + "back-off", + "caseyamcl", + "guzzle_retry_middleware", + "middleware", + "retry", + "retry-after" + ], + "support": { + "issues": "https://github.com/caseyamcl/guzzle_retry_middleware/issues", + "source": "https://github.com/caseyamcl/guzzle_retry_middleware/tree/v2.13.0" + }, + "funding": [ + { + "url": "https://github.com/caseyamcl", + "type": "github" + } + ], + "time": "2025-07-11T12:33:22+00:00" + }, { "name": "guzzlehttp/guzzle", - "version": "7.5.0", + "version": "7.15.3", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba" + "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b50a2a1251152e43f6a37f0fa053e730a67d25ba", - "reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc", + "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5", - "guzzlehttp/psr7": "^1.9 || ^2.4", + "guzzlehttp/promises": "^2.5.2", + "guzzlehttp/psr7": "^2.13", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-client-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", + "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "php-http/client-integration-tests": "^3.0", - "phpunit/phpunit": "^8.5.29 || ^9.5.23", + "guzzle/client-integration-tests": "3.0.3", + "guzzlehttp/test-server": "^0.7", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -48,9 +124,6 @@ "bamarni-bin": { "bin-links": true, "forward-command": false - }, - "branch-alias": { - "dev-master": "7.5-dev" } }, "autoload": { @@ -116,7 +189,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.5.0" + "source": "https://github.com/guzzle/guzzle/tree/7.15.3" }, "funding": [ { @@ -132,38 +205,38 @@ "type": "tidelift" } ], - "time": "2022-08-28T15:39:27+00:00" + "time": "2026-08-05T19:48:21+00:00" }, { "name": "guzzlehttp/promises", - "version": "1.5.2", + "version": "2.5.2", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "b94b2807d85443f9719887892882d0329d1e2598" + "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/b94b2807d85443f9719887892882d0329d1e2598", - "reference": "b94b2807d85443f9719887892882d0329d1e2598", + "url": "https://api.github.com/repos/guzzle/promises/zipball/2823687acff28b2dbe67b2508a6b300e2c3fa4ce", + "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce", "shasum": "" }, "require": { - "php": ">=5.5" + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4 || ^5.1" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.5-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { - "files": [ - "src/functions_include.php" - ], "psr-4": { "GuzzleHttp\\Promise\\": "src/" } @@ -200,7 +273,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.5.2" + "source": "https://github.com/guzzle/promises/tree/2.5.2" }, "funding": [ { @@ -216,36 +289,39 @@ "type": "tidelift" } ], - "time": "2022-08-28T14:55:35+00:00" + "time": "2026-08-05T19:30:54+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.4.1", + "version": "2.13.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "69568e4293f4fa993f3b0e51c9723e1e17c41379" + "reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/69568e4293f4fa993f3b0e51c9723e1e17c41379", - "reference": "69568e4293f4fa993f3b0e51c9723e1e17c41379", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/dad89620b7a6edb60c15858442eb2e408b45d8f4", + "reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", - "ralouphie/getallheaders": "^3.0" + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-factory-implementation": "1.0", "psr/http-message-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.29 || ^9.5.23" + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -255,9 +331,6 @@ "bamarni-bin": { "bin-links": true, "forward-command": false - }, - "branch-alias": { - "dev-master": "2.4-dev" } }, "autoload": { @@ -319,7 +392,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.4.1" + "source": "https://github.com/guzzle/psr7/tree/2.13.0" }, "funding": [ { @@ -335,25 +408,25 @@ "type": "tidelift" } ], - "time": "2022-08-28T14:45:39+00:00" + "time": "2026-07-16T22:23:49+00:00" }, { "name": "psr/http-client", - "version": "1.0.1", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/php-fig/http-client.git", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", "shasum": "" }, "require": { "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -373,7 +446,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP clients", @@ -385,27 +458,27 @@ "psr-18" ], "support": { - "source": "https://github.com/php-fig/http-client/tree/master" + "source": "https://github.com/php-fig/http-client" }, - "time": "2020-06-29T06:28:15+00:00" + "time": "2023-09-23T14:17:50+00:00" }, { "name": "psr/http-factory", - "version": "1.0.1", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.0.0", - "psr/http-message": "^1.0" + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -425,10 +498,10 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -440,31 +513,31 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/master" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2019-04-30T12:38:16+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { "name": "psr/http-message", - "version": "1.0.1", + "version": "2.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -479,7 +552,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -493,9 +566,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/master" + "source": "https://github.com/php-fig/http-message/tree/2.0" }, - "time": "2016-08-06T14:39:51+00:00" + "time": "2023-04-04T09:54:51+00:00" }, { "name": "ralouphie/getallheaders", @@ -541,31 +614,84 @@ }, "time": "2019-03-08T08:55:37+00:00" }, + { + "name": "svix/svix", + "version": "v1.99.1", + "source": { + "type": "git", + "url": "https://github.com/svix/svix-webhooks.git", + "reference": "6cd12c1d6d19c6d222bab889d08315f6469702c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/svix/svix-webhooks/zipball/6cd12c1d6d19c6d222bab889d08315f6469702c8", + "reference": "6cd12c1d6d19c6d222bab889d08315f6469702c8", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^7.0", + "php": ">=8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": ">=10.0 <=12.9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Svix\\": "php/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Svix", + "email": "support@svix.com", + "homepage": "http://www.svix.com" + } + ], + "description": "Svix PHP Library", + "homepage": "https://www.svix.com", + "keywords": [ + "api", + "webhooks" + ], + "support": { + "issues": "https://github.com/svix/svix-webhooks/issues", + "source": "https://github.com/svix/svix-webhooks/tree/v1.99.1" + }, + "time": "2026-07-23T16:05:36+00:00" + }, { "name": "symfony/deprecation-contracts", - "version": "v3.0.2", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/26954b3d62a6c5fd0ea8a2a00c0353a14978d05c", - "reference": "26954b3d62a6c5fd0ea8a2a00c0353a14978d05c", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { - "php": ">=8.0.2" + "php": ">=8.1" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" } }, "autoload": { @@ -590,7 +716,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.2" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -601,47 +727,51 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2022-01-02T09:55:41+00:00" - } - ], - "packages-dev": [ + "time": "2026-06-05T06:23:12+00:00" + }, { - "name": "doctrine/instantiator", - "version": "1.4.1", + "name": "symfony/polyfill-php80", + "version": "v1.37.0", "source": { "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.22" + "php": ">=7.2" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -649,1330 +779,1819 @@ ], "authors": [ { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", "keywords": [ - "constructor", - "instantiate" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" }, "funding": [ { - "url": "https://www.doctrine-project.org/sponsorship.html", + "url": "https://symfony.com/sponsor", "type": "custom" }, { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2022-03-03T08:28:38+00:00" - }, + "time": "2026-04-10T16:19:22+00:00" + } + ], + "packages-dev": [ { - "name": "myclabs/deep-copy", - "version": "1.11.0", + "name": "amphp/amp", + "version": "v3.1.3", "source": { "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "url": "https://github.com/amphp/amp.git", + "reference": "73c38b323ff8d790abf0f76c56fcc892bda4111a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "url": "https://api.github.com/repos/amphp/amp/zipball/73c38b323ff8d790abf0f76c56fcc892bda4111a", + "reference": "73c38b323ff8d790abf0f76c56fcc892bda4111a", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { "files": [ - "src/DeepCopy/deep_copy.php" + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" ], "psr-4": { - "DeepCopy\\": "src/DeepCopy/" + "Amp\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Create deep copies (clones) of your objects", + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" ], "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v3.1.3" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" + "url": "https://github.com/amphp", + "type": "github" } ], - "time": "2022-03-03T13:19:32+00:00" + "time": "2026-07-19T17:59:20+00:00" }, { - "name": "nikic/php-parser", - "version": "v4.15.1", + "name": "amphp/byte-stream", + "version": "v2.1.2", "source": { "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900" + "url": "https://github.com/amphp/byte-stream.git", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", - "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", "shasum": "" }, "require": { - "ext-tokenizer": "*", - "php": ">=7.0" + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" }, "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" }, - "bin": [ - "bin/php-parse" - ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.9-dev" - } - }, "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], "psr-4": { - "PhpParser\\": "lib/PhpParser" + "Amp\\ByteStream\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Nikita Popov" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "A PHP parser written in PHP", + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", "keywords": [ - "parser", - "php" + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" ], "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1" + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" }, - "time": "2022-09-04T07:30:47+00:00" + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T17:10:27+00:00" }, { - "name": "phar-io/manifest", - "version": "2.0.3", + "name": "amphp/cache", + "version": "v2.0.1", "source": { "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "url": "https://github.com/amphp/cache.git", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" + "amphp/amp": "^3", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" }, + "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Amp\\Cache\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" + "name": "Niklas Keller", + "email": "me@kelunik.com" }, { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" } ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "description": "A fiber-aware cache API based on Amp and Revolt.", + "homepage": "https://amphp.org/cache", "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v2.0.1" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:38:06+00:00" }, { - "name": "phar-io/version", - "version": "3.2.1", + "name": "amphp/dns", + "version": "v2.4.0", "source": { "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + "url": "https://github.com/amphp/dns.git", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "url": "https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/process": "^2", + "daverandom/libdns": "^2.0.2", + "ext-filter": "*", + "ext-json": "*", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" }, "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Dns\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" + "name": "Chris Wright", + "email": "addr@daverandom.com" }, { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" } ], - "description": "Library for handling version information and constraints", + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", + "keywords": [ + "amp", + "amphp", + "async", + "client", + "dns", + "resolve" + ], "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.2.1" + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v2.4.0" }, - "time": "2022-02-21T01:04:05+00:00" + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-01-19T15:43:40+00:00" }, { - "name": "phpunit/php-code-coverage", - "version": "9.2.17", + "name": "amphp/parallel", + "version": "v2.4.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8" + "url": "https://github.com/amphp/parallel.git", + "reference": "37f5b2754fadc229c00f9416bd68fb8d04529a81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa94dc41e8661fe90c7316849907cba3007b10d8", - "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8", + "url": "https://api.github.com/repos/amphp/parallel/zipball/37f5b2754fadc229c00f9416bd68fb8d04529a81", + "reference": "37f5b2754fadc229c00f9416bd68fb8d04529a81", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-xmlwriter": "*", - "nikic/php-parser": "^4.14", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/pipeline": "^1", + "amphp/process": "^2", + "amphp/serialization": "^1", + "amphp/socket": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1" }, "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "9.2-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "src/Context/functions.php", + "src/Context/Internal/functions.php", + "src/Ipc/functions.php", + "src/Worker/functions.php" + ], + "psr-4": { + "Amp\\Parallel\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" } ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "description": "Parallel processing component for Amp.", + "homepage": "https://github.com/amphp/parallel", "keywords": [ - "coverage", - "testing", - "xunit" + "async", + "asynchronous", + "concurrent", + "multi-processing", + "multi-threading" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.17" + "issues": "https://github.com/amphp/parallel/issues", + "source": "https://github.com/amphp/parallel/tree/v2.4.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/amphp", "type": "github" } ], - "time": "2022-08-30T12:24:04+00:00" + "time": "2026-05-16T16:54:01+00:00" }, { - "name": "phpunit/php-file-iterator", - "version": "3.0.6", + "name": "amphp/parser", + "version": "v1.1.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.4" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Amp\\Parser\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", "keywords": [ - "filesystem", - "iterator" + "async", + "non-blocking", + "parser", + "stream" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/amphp", "type": "github" } ], - "time": "2021-12-02T12:48:52+00:00" + "time": "2024-03-21T19:16:53+00:00" }, { - "name": "phpunit/php-invoker", - "version": "3.1.1", + "name": "amphp/pipeline", + "version": "v1.2.7", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "url": "https://github.com/amphp/pipeline.git", + "reference": "cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17", + "reference": "cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17", "shasum": "" }, "require": { - "php": ">=7.3" + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" }, "require-dev": { - "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-pcntl": "*" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Amp\\Pipeline\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "Invoke callables with a timeout", - "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", "keywords": [ - "process" + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.7" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/amphp", "type": "github" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2026-07-26T14:50:43+00:00" }, { - "name": "phpunit/php-text-template", - "version": "2.0.4", + "name": "amphp/process", + "version": "v2.1.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "url": "https://github.com/amphp/process.git", + "reference": "583959df17d00304ad7b0b32285373f985935643" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/amphp/process/zipball/583959df17d00304ad7b0b32285373f985935643", + "reference": "583959df17d00304ad7b0b32285373f985935643", "shasum": "" }, "require": { - "php": ">=7.3" + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Process\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], + "description": "A fiber-aware process manager based on Amp and Revolt.", + "homepage": "https://amphp.org/process", "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v2.1.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/amphp", "type": "github" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2026-05-31T15:11:55+00:00" }, { - "name": "phpunit/php-timer", - "version": "5.0.3", + "name": "amphp/serialization", + "version": "v1.1.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "url": "https://github.com/amphp/serialization.git", + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/amphp/serialization/zipball/fdf2834d78cebb0205fb2672676c1b1eb84371f0", + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.4" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "amphp/php-cs-fixer-config": "^2", + "ext-json": "*", + "ext-zlib": "*", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", "keywords": [ - "timer" + "async", + "asynchronous", + "serialization", + "serialize" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/v1.1.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/amphp", "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2026-04-05T15:59:53+00:00" }, { - "name": "phpunit/phpunit", - "version": "9.5.24", + "name": "amphp/socket", + "version": "v2.4.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "d0aa6097bef9fd42458a9b3c49da32c6ce6129c5" + "url": "https://github.com/amphp/socket.git", + "reference": "dadb63c5d3179fd83803e29dfeac27350e619314" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d0aa6097bef9fd42458a9b3c49da32c6ce6129c5", - "reference": "d0aa6097bef9fd42458a9b3c49da32c6ce6129c5", + "url": "https://api.github.com/repos/amphp/socket/zipball/dadb63c5d3179fd83803e29dfeac27350e619314", + "reference": "dadb63c5d3179fd83803e29dfeac27350e619314", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", - "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.13", - "phpunit/php-file-iterator": "^3.0.5", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.1", - "sebastian/version": "^3.0.2" + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/dns": "^2", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri": "^7", + "league/uri-interfaces": "^7", + "php": ">=8.1", + "revolt/event-loop": "^1" }, - "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/process": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" }, - "bin": [ - "phpunit" - ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "9.5-dev" - } - }, "autoload": { "files": [ - "src/Framework/Assert/Functions.php" + "src/functions.php", + "src/Internal/functions.php", + "src/SocketAddress/functions.php" ], - "classmap": [ - "src/" - ] + "psr-4": { + "Amp\\Socket\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", + "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", + "homepage": "https://github.com/amphp/socket", "keywords": [ - "phpunit", - "testing", - "xunit" + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" ], "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.24" + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v2.4.0" }, "funding": [ { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/amphp", "type": "github" } ], - "time": "2022-08-30T07:42:16+00:00" + "time": "2026-04-19T15:09:56+00:00" }, { - "name": "sebastian/cli-parser", - "version": "1.0.1", + "name": "amphp/sync", + "version": "v2.3.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "url": "https://github.com/amphp/sync.git", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", "shasum": "" }, "require": { - "php": ">=7.3" + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" } ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.3.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/amphp", "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-08-03T19:31:26+00:00" }, { - "name": "sebastian/code-unit", - "version": "1.0.8", + "name": "composer/pcre", + "version": "3.4.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + "url": "https://github.com/composer/pcre.git", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", "shasum": "" }, "require": { - "php": ">=7.3" + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<2.2.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" }, "type": "library", "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "3.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Composer\\Pcre\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" } ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.4.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } ], - "time": "2020-10-26T13:08:54+00:00" + "time": "2026-06-07T11:47:49+00:00" }, { - "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", + "name": "composer/semver", + "version": "3.4.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", "shasum": "" }, "require": { - "php": ">=7.3" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Composer\\Semver\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" } ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", "type": "github" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2025-08-20T19:15:30+00:00" }, { - "name": "sebastian/comparator", - "version": "4.0.8", + "name": "composer/xdebug-handler", + "version": "3.0.5", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" } ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", + "description": "Restarts a process without Xdebug.", "keywords": [ - "comparator", - "compare", - "equality" + "Xdebug", + "performance" ], "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { - "name": "sebastian/complexity", - "version": "2.0.2", + "name": "danog/advanced-json-rpc", + "version": "v3.2.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "url": "https://github.com/danog/php-advanced-json-rpc.git", + "reference": "ae703ea7b4811797a10590b6078de05b3b33dd91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/danog/php-advanced-json-rpc/zipball/ae703ea7b4811797a10590b6078de05b3b33dd91", + "reference": "ae703ea7b4811797a10590b6078de05b3b33dd91", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", - "php": ">=7.3" + "netresearch/jsonmapper": "^5", + "php": ">=8.1", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0 || ^6" + }, + "replace": { + "felixfbecker/php-advanced-json-rpc": "^3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "ISC" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Felix Becker", + "email": "felix.b@outlook.com" + }, + { + "name": "Daniil Gentili", + "email": "daniil@daniil.it" } ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", + "description": "A more advanced JSONRPC implementation", "support": { - "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "issues": "https://github.com/danog/php-advanced-json-rpc/issues", + "source": "https://github.com/danog/php-advanced-json-rpc/tree/v3.2.3" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2026-01-12T21:07:10+00:00" }, { - "name": "sebastian/diff", - "version": "4.0.4", + "name": "daverandom/libdns", + "version": "v2.1.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", "shasum": "" }, "require": { - "php": ">=7.3" + "ext-ctype": "*", + "php": ">=7.1" }, - "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" + "suggest": { + "ext-intl": "Required for IDN support" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "src/functions.php" + ], + "psr-4": { + "LibDNS\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - } + "MIT" ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", + "description": "DNS protocol implementation written in pure PHP", "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" + "dns" ], "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "time": "2024-04-12T12:12:48+00:00" + }, + { + "name": "dnoegel/php-xdg-base-dir", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/dnoegel/php-xdg-base-dir.git", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "XdgBaseDir\\": "src/" } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" ], - "time": "2020-10-26T13:10:38+00:00" + "description": "implementation of xdg base directory specification for php", + "support": { + "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", + "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" + }, + "time": "2019-12-04T15:06:13+00:00" }, { - "name": "sebastian/environment", - "version": "5.1.4", + "name": "doctrine/deprecations", + "version": "1.1.6", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", "shasum": "" }, "require": { - "php": ">=7.3" + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" }, "suggest": { - "ext-posix": "*" + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "felixfbecker/language-server-protocol", + "version": "v1.5.3", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-language-server-protocol.git", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/phpstan": "*", + "squizlabs/php_codesniffer": "^3.1", + "vimeo/psalm": "^4.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "1.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "LanguageServerProtocol\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "ISC" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Felix Becker", + "email": "felix.b@outlook.com" } ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", + "description": "PHP classes for the Language Server Protocol", "keywords": [ - "Xdebug", - "environment", - "hhvm" + "language", + "microsoft", + "php", + "server" ], "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" + "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2022-04-03T09:37:03+00:00" + "time": "2024-04-30T00:40:11+00:00" }, { - "name": "sebastian/exporter", - "version": "4.0.5", + "name": "fidry/cpu-core-counter", + "version": "1.3.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" } ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "https://www.github.com/sebastianbergmann/exporter", + "description": "Tiny utility to get the number of CPU cores.", "keywords": [ - "export", - "exporter" + "CPU", + "core" ], "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/theofidry", "type": "github" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2025-08-14T07:29:31+00:00" }, { - "name": "sebastian/global-state", - "version": "5.0.5", + "name": "kelunik/certificate", + "version": "v1.1.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "ext-openssl": "*", + "php": ">=7.0" }, "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^9.3" + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" }, - "suggest": { - "ext-uopz": "*" + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Kelunik\\Certificate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", + "keywords": [ + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" + ], + "support": { + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + }, + "time": "2023-02-03T21:26:53+00:00" + }, + { + "name": "league/uri", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "7.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "League\\Uri\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" } ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", "keywords": [ - "global state" + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" ], "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.1" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/sponsors/nyamsprod", "type": "github" } ], - "time": "2022-02-14T08:28:10+00:00" + "time": "2026-03-15T20:22:25+00:00" }, { - "name": "sebastian/lines-of-code", - "version": "1.0.3", + "name": "league/uri-interfaces", + "version": "7.8.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", - "php": ">=7.3" + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" }, - "require-dev": { - "phpunit/phpunit": "^9.3" + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "7.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "League\\Uri\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" } ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/sponsors/nyamsprod", "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2026-03-08T20:05:35+00:00" }, { - "name": "sebastian/object-enumerator", - "version": "4.0.4", + "name": "myclabs/deep-copy", + "version": "1.13.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "netresearch/jsonmapper", + "version": "v5.0.1", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "980674efdda65913492d29a8fd51c82270dd37bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/980674efdda65913492d29a8fd51c82270dd37bb", + "reference": "980674efdda65913492d29a8fd51c82270dd37bb", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v5.0.1" + }, + "time": "2026-02-22T16:28:03+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.8.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" + }, + "time": "2026-07-04T14:30:18+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -1985,277 +2604,2783 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "Developer" } ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/theseer", "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", + "webmozart/assert": "^1.9.1 || ^2" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" + }, + "time": "2026-03-18T20:49:53+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" + }, + "time": "2026-01-06T21:53:42+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3" + }, + "time": "2026-07-08T07:01:06+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.64", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "0e8c1d19cea35ad97d4887f363d07c78e30fbf06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0e8c1d19cea35ad97d4887f363d07c78e30fbf06", + "reference": "0e8c1d19cea35ad97d4887f363d07c78e30fbf06", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.5", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.64" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-07-06T14:50:35+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "revolt/event-loop", + "version": "v1.0.9", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "44061cf513e53c6200372fc935ac42271566295d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/44061cf513e53c6200372fc935ac42271566295d", + "reference": "44061cf513e53c6200372fc935ac42271566295d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.9" + }, + "time": "2026-05-16T17:55:38+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:25:16+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "0735b90f4da94969541dac1da743446e276defa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:09:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:50:56+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "spatie/array-to-xml", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/spatie/array-to-xml.git", + "reference": "88b2f3852a922dd73177a68938f8eb2ec70c7224" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/88b2f3852a922dd73177a68938f8eb2ec70c7224", + "reference": "88b2f3852a922dd73177a68938f8eb2ec70c7224", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "pestphp/pest": "^1.21", + "spatie/pest-plugin-snapshots": "^1.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spatie\\ArrayToXml\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://freek.dev", + "role": "Developer" + } + ], + "description": "Convert an array to xml", + "homepage": "https://github.com/spatie/array-to-xml", + "keywords": [ + "array", + "convert", + "xml" + ], + "support": { + "source": "https://github.com/spatie/array-to-xml/tree/3.4.4" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-12-15T09:00:41+00:00" + }, + { + "name": "symfony/console", + "version": "v8.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "535e18a1b8925f6c01a55b171d157ab66c2ace15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/535e18a1b8925f6c01a55b171d157ab66c2ace15", + "reference": "535e18a1b8925f6c01a55b171d157ab66c2ace15", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php85": "^1.32", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.4.6|^8.0.6" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/event-dispatcher": "<8.1" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/event-dispatcher": "^8.1", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v8.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-27T13:58:19+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v8.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "17856b7a222664a26a5ea1cb06ee0721c2438217" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/17856b7a222664a26a5ea1cb06ee0721c2438217", + "reference": "17856b7a222664a26a5ea1cb06ee0721c2438217", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v8.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-22T15:42:13+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.41.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-28T08:25:59+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.38.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:48:31+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.38.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-27T06:59:30+00:00" }, { - "name": "sebastian/object-reflector", - "version": "2.0.4", + "name": "symfony/polyfill-php84", + "version": "v1.38.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.0-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, "classmap": [ - "src/" + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { - "name": "sebastian/recursion-context", - "version": "4.0.4", + "name": "symfony/polyfill-php85", + "version": "v1.41.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "4.0-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, "classmap": [ - "src/" + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Adam Harvey", - "email": "aharvey@php.net" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2020-10-26T13:17:30+00:00" + "time": "2026-07-01T12:47:55+00:00" }, { - "name": "sebastian/resource-operations", - "version": "3.0.3", + "name": "symfony/service-contracts", + "version": "v3.7.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "url": "https://github.com/symfony/service-contracts.git", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, - "require-dev": { - "phpunit/phpunit": "^9.0" + "conflict": { + "ext-psr": "<1.1|>=2" }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "3.7-dev" } }, "autoload": { - "classmap": [ - "src/" + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2026-06-16T09:55:08+00:00" }, { - "name": "sebastian/type", - "version": "3.2.0", + "name": "symfony/string", + "version": "v8.1.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e" + "url": "https://github.com/symfony/string.git", + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", - "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", + "url": "https://api.github.com/repos/symfony/string/zipball/286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.2-dev" - } - }, "autoload": { - "classmap": [ - "src/" + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.0" + "source": "https://github.com/symfony/string/tree/v8.1.2" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2022-09-12T14:47:03+00:00" + "time": "2026-07-28T07:35:25+00:00" }, { - "name": "sebastian/version", - "version": "3.0.2", + "name": "theseer/tokenizer", + "version": "1.3.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { - "php": ">=7.3" + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, "autoload": { "classmap": [ "src/" @@ -2267,130 +5392,207 @@ ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" } ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/theseer", "type": "github" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2025-11-17T20:03:58+00:00" }, { - "name": "squizlabs/php_codesniffer", - "version": "3.7.1", + "name": "vimeo/psalm", + "version": "6.16.1", "source": { "type": "git", - "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619" + "url": "https://github.com/vimeo/psalm.git", + "reference": "f1f5de594dc76faf8784e02d3dc4716c91c6f6ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619", - "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/f1f5de594dc76faf8784e02d3dc4716c91c6f6ac", + "reference": "f1f5de594dc76faf8784e02d3dc4716c91c6f6ac", "shasum": "" }, "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/parallel": "^2.3", + "composer-runtime-api": "^2", + "composer/semver": "^1.4 || ^2.0 || ^3.0", + "composer/xdebug-handler": "^2.0 || ^3.0", + "danog/advanced-json-rpc": "^3.1", + "dnoegel/php-xdg-base-dir": "^0.1.1", + "ext-ctype": "*", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", "ext-simplexml": "*", "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" + "felixfbecker/language-server-protocol": "^1.5.3", + "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", + "netresearch/jsonmapper": "^5.0", + "nikic/php-parser": "^5.0.0", + "php": "~8.1.31 || ~8.2.27 || ~8.3.16 || ~8.4.3 || ~8.5.0", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0", + "spatie/array-to-xml": "^2.17.0 || ^3.0", + "symfony/console": "^6.0 || ^7.0 || ^8.0", + "symfony/filesystem": "~6.3.12 || ~6.4.3 || ^7.0.3 || ^8.0", + "symfony/polyfill-php84": "^1.31.0" + }, + "provide": { + "psalm/psalm": "self.version" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "amphp/phpunit-util": "^3", + "bamarni/composer-bin-plugin": "^1.4", + "brianium/paratest": "^6.9", + "danog/class-finder": "^0.4.8", + "dg/bypass-finals": "^1.5", + "ext-curl": "*", + "mockery/mockery": "^1.5", + "nunomaduro/mock-final-classes": "^1.1", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpdoc-parser": "^1.6", + "phpunit/phpunit": "^9.6", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.19", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.6", + "symfony/process": "^6.0 || ^7.0 || ^8.0" + }, + "suggest": { + "ext-curl": "In order to send data to shepherd", + "ext-igbinary": "^2.0.5 is required, used to serialize caching data" }, "bin": [ - "bin/phpcs", - "bin/phpcbf" + "psalm", + "psalm-language-server", + "psalm-plugin", + "psalm-refactor", + "psalm-review", + "psalter" ], - "type": "library", + "type": "project", "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev", + "dev-3.x": "3.x-dev", + "dev-4.x": "4.x-dev", + "dev-5.x": "5.x-dev", + "dev-6.x": "6.x-dev", + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psalm\\": "src/Psalm/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Greg Sherwood", - "role": "lead" + "name": "Matthew Brown" + }, + { + "name": "Daniil Gentili", + "email": "daniil@daniil.it" } ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "description": "A static analysis tool for finding errors in PHP applications", "keywords": [ - "phpcs", - "standards" + "code", + "inspection", + "php", + "static analysis" ], "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + "docs": "https://psalm.dev/docs", + "issues": "https://github.com/vimeo/psalm/issues", + "source": "https://github.com/vimeo/psalm" }, - "time": "2022-06-18T07:21:10+00:00" + "time": "2026-03-19T10:56:09+00:00" }, { - "name": "theseer/tokenizer", - "version": "1.2.1", + "name": "webmozart/assert", + "version": "2.4.1", "source": { "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "url": "https://github.com/webmozarts/assert.git", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" }, "type": "library", + "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, + "branch-alias": { + "dev-master": "2.0-dev", + "dev-feature/2-0": "2.0-dev" + } + }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Webmozart\\Assert\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" } ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.4.1" }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2026-06-15T15:31:57+00:00" } ], "aliases": [], @@ -2399,7 +5601,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^8.0" + "php": "^8.1" }, "platform-dev": {}, "plugin-api-version": "2.6.0" diff --git a/package-lock.json b/package-lock.json index dc179e80..35c7c127 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "devDependencies": { "@prettier/plugin-php": "^0.25.0", "@seamapi/blueprint": "^1.2.0", + "@seamapi/fake-seam-connect": "1.86.0", "@seamapi/smith": "^1.1.0", "@seamapi/types": "1.985.0", "change-case": "^5.4.4", @@ -819,6 +820,42 @@ "npm": ">=10.0.0" } }, + "node_modules/@seamapi/fake-devicedb": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@seamapi/fake-devicedb/-/fake-devicedb-1.6.1.tgz", + "integrity": "sha512-w4Ar/s2kPnE5ExJSlpD3sKL8lkF+rLHRROArIRxtR2reHfnDSVwnDt9TzBYkHqgMP/x4o7LUzlRRpobn2xn24A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18.12.0", + "npm": ">= 9.0.0" + }, + "optionalDependencies": { + "zod": "^3.21.4", + "zustand": "^4.3.7", + "zustand-hoist": "^2.0.0" + } + }, + "node_modules/@seamapi/fake-seam-connect": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/@seamapi/fake-seam-connect/-/fake-seam-connect-1.86.0.tgz", + "integrity": "sha512-iO5fwtSIPhzmIiLxrFDtCYF/7HTb+ywcGmc3WzWt8Sr0bLQlmwCyTJ8YYZy++Hx0FsnNpa5AmlJuJGul9Y5gZA==", + "dev": true, + "license": "MIT", + "bin": { + "fake-seam-connect": "dist/server.js" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">= 9.0.0" + }, + "optionalDependencies": { + "@seamapi/fake-devicedb": ">=1.0.0-rc.0", + "zustand": "^4.3.7", + "zustand-hoist": "^2.0.0" + } + }, "node_modules/@seamapi/smith": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@seamapi/smith/-/smith-1.1.0.tgz", @@ -4881,6 +4918,18 @@ "license": "MIT", "peer": true }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -5866,6 +5915,17 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "dev": true, + "license": "MIT", + "optional": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/ware": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ware/-/ware-1.3.0.tgz", @@ -6065,6 +6125,51 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/zustand-hoist": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/zustand-hoist/-/zustand-hoist-2.0.1.tgz", + "integrity": "sha512-Lhvv3RlLQx1NSUtuhk8jegXe1Wyav9RAOnLd4CRs1SbB5qcFoarAGQTE43vIxXizrm1UQJl1q5uRbOZuXGXGpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18.12.0", + "npm": ">= 9.0.0" + }, + "peerDependencies": { + "zustand": ">=4.0.0" + } } } } diff --git a/package.json b/package.json index da30a5a8..c1f2a9ed 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "lint": "eslint .", "postlint": "prettier --check --ignore-path .gitignore --ignore-path .prettierignore .", "format": "prettier --write --ignore-path .gitignore --ignore-path .prettierignore .", - "preformat": "eslint --fix ." + "preformat": "eslint --fix .", + "start": "fake-seam-connect --seed" }, "engines": { "node": ">=22.11.0", @@ -32,6 +33,7 @@ "devDependencies": { "@prettier/plugin-php": "^0.25.0", "@seamapi/blueprint": "^1.2.0", + "@seamapi/fake-seam-connect": "1.86.0", "@seamapi/smith": "^1.1.0", "@seamapi/types": "1.985.0", "change-case": "^5.4.4", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e953736f..f29c67a2 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,9 +1,9 @@ tests - + src - + + + src/Resources + src/Routes + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 00000000..f3cb128b --- /dev/null +++ b/psalm.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/src/Exceptions/ActionAttemptError.php b/src/ActionAttemptError.php similarity index 71% rename from src/Exceptions/ActionAttemptError.php rename to src/ActionAttemptError.php index 26530d06..776ee6fe 100644 --- a/src/Exceptions/ActionAttemptError.php +++ b/src/ActionAttemptError.php @@ -4,14 +4,16 @@ use Seam\Resources\ActionAttempt; -class ActionAttemptError extends \Exception +/** + * Base class for the errors raised while resolving an action attempt. + */ +class ActionAttemptError extends \RuntimeException implements SeamException { private ActionAttempt $actionAttempt; public function __construct(string $message, ActionAttempt $actionAttempt) { parent::__construct($message); - $this->name = get_class($this); $this->actionAttempt = $actionAttempt; } diff --git a/src/ActionAttemptFailedError.php b/src/ActionAttemptFailedError.php new file mode 100644 index 00000000..af5c5d16 --- /dev/null +++ b/src/ActionAttemptFailedError.php @@ -0,0 +1,34 @@ +error->message ?? "Action attempt failed", + $actionAttempt, + ); + $this->errorCode = $actionAttempt->error->type ?? "unknown_error"; + } + + /** + * The action attempt error type. + * + * Named `getErrorCode` rather than `getCode` because `Exception::getCode` + * is final and returns an int. This is the equivalent of the `code` + * property on the same error in the other Seam SDKs. + */ + public function getErrorCode(): string + { + return $this->errorCode; + } +} diff --git a/src/Exceptions/ActionAttemptTimeoutError.php b/src/ActionAttemptTimeoutError.php similarity index 67% rename from src/Exceptions/ActionAttemptTimeoutError.php rename to src/ActionAttemptTimeoutError.php index d0b6a343..5d2f3d4d 100644 --- a/src/Exceptions/ActionAttemptTimeoutError.php +++ b/src/ActionAttemptTimeoutError.php @@ -4,6 +4,12 @@ use Seam\Resources\ActionAttempt; +/** + * Raised when an action attempt does not finish within the timeout. + * + * The action attempt it carries is the last one observed, which is still + * pending. + */ class ActionAttemptTimeoutError extends ActionAttemptError { public function __construct(ActionAttempt $actionAttempt, float $timeout) @@ -12,6 +18,5 @@ public function __construct(ActionAttempt $actionAttempt, float $timeout) "Timed out waiting for action attempt after {$timeout}s", $actionAttempt, ); - $this->name = get_class($this); } } diff --git a/src/Auth.php b/src/Auth.php new file mode 100644 index 00000000..f35f5b68 --- /dev/null +++ b/src/Auth.php @@ -0,0 +1,146 @@ + + */ + public static function get_auth_headers( + ?string $api_key = null, + ?string $personal_access_token = null, + ?string $workspace_id = null, + ): array { + // The environment is only consulted when no credential was passed at + // all, so an explicit personal access token is not second guessed by + // a stray SEAM_API_KEY. + if ($api_key === null && $personal_access_token === null) { + $api_key = Options::get_env("SEAM_API_KEY"); + } + + if ( + Options::is_seam_options_with_api_key( + $api_key, + $personal_access_token, + ) + ) { + return self::get_auth_headers_for_api_key($api_key); + } + + if ( + Options::is_seam_options_with_personal_access_token( + $personal_access_token, + $api_key, + $workspace_id, + ) + ) { + return self::get_auth_headers_for_personal_access_token( + $personal_access_token, + $workspace_id, + ); + } + + throw new InvalidOptionsError( + "Must specify an api_key or personal_access_token. " . + "Attempted reading configuration from the environment, " . + "but the environment variable SEAM_API_KEY is not set.", + ); + } + + /** + * @return array + */ + public static function get_auth_headers_for_api_key(string $api_key): array + { + if (Token::is_client_session_token($api_key)) { + throw new InvalidTokenError( + "A Client Session Token cannot be used as an api_key", + ); + } + + if (Token::is_jwt($api_key)) { + throw new InvalidTokenError("A JWT cannot be used as an api_key"); + } + + if (Token::is_access_token($api_key)) { + throw new InvalidTokenError( + "An Access Token cannot be used as an api_key", + ); + } + + if (Token::is_publishable_key($api_key)) { + throw new InvalidTokenError( + "A Publishable Key cannot be used as an api_key", + ); + } + + if (!Token::is_seam_token($api_key)) { + throw new InvalidTokenError("Unknown or invalid api_key format"); + } + + return ["authorization" => "Bearer " . $api_key]; + } + + /** + * @return array + */ + public static function get_auth_headers_for_personal_access_token( + string $personal_access_token, + string $workspace_id, + ): array { + self::assert_personal_access_token($personal_access_token); + + return [ + "authorization" => "Bearer " . $personal_access_token, + "seam-workspace" => $workspace_id, + ]; + } + + /** + * Headers for a personal access token that is not scoped to a workspace. + * + * @return array + */ + public static function get_auth_headers_for_multi_workspace_personal_access_token( + string $personal_access_token, + ): array { + self::assert_personal_access_token($personal_access_token); + + return ["authorization" => "Bearer " . $personal_access_token]; + } + + private static function assert_personal_access_token(string $token): void + { + if (Token::is_client_session_token($token)) { + throw new InvalidTokenError( + "A Client Session Token cannot be used as a personal_access_token", + ); + } + + if (Token::is_jwt($token)) { + throw new InvalidTokenError( + "A JWT cannot be used as a personal_access_token", + ); + } + + if (Token::is_publishable_key($token)) { + throw new InvalidTokenError( + "A Publishable Key cannot be used as a personal_access_token", + ); + } + + if (!Token::is_access_token($token)) { + throw new InvalidTokenError( + "Unknown or invalid personal_access_token format", + ); + } + } +} diff --git a/src/Exceptions/ActionAttemptFailedError.php b/src/Exceptions/ActionAttemptFailedError.php deleted file mode 100644 index 59e307fe..00000000 --- a/src/Exceptions/ActionAttemptFailedError.php +++ /dev/null @@ -1,22 +0,0 @@ -error->message, $actionAttempt); - $this->name = get_class($this); - $this->errorCode = $actionAttempt->error->type; - } - - public function getErrorCode(): string - { - return $this->errorCode; - } -} diff --git a/src/Exceptions/HttpUnauthorizedError.php b/src/Exceptions/HttpUnauthorizedError.php deleted file mode 100644 index 921ee2eb..00000000 --- a/src/Exceptions/HttpUnauthorizedError.php +++ /dev/null @@ -1,15 +0,0 @@ - "unauthorized", - "message" => "Unauthorized", - ]; - parent::__construct($error, 401, $requestId); - } -} diff --git a/src/Http/ResolveActionAttempt.php b/src/Http/ResolveActionAttempt.php new file mode 100644 index 00000000..29ba92b9 --- /dev/null +++ b/src/Http/ResolveActionAttempt.php @@ -0,0 +1,103 @@ +status === "success") { + return $action_attempt; + } + + if ($action_attempt->status === "error") { + throw new ActionAttemptFailedError($action_attempt); + } + + if (self::now() + $polling_interval > $deadline) { + throw new ActionAttemptTimeoutError($action_attempt, $timeout); + } + + usleep((int) ($polling_interval * 1000000.0)); + + $action_attempt = self::get_action_attempt( + $client, + $action_attempt->action_attempt_id, + ); + } + } + + private static function get_action_attempt( + SeamHttpClient $client, + string $action_attempt_id, + ): ActionAttempt { + $res = $client->request( + "POST", + "/action_attempts/get", + json: (object) ["action_attempt_id" => $action_attempt_id], + ); + + $action_attempt = ActionAttempt::from_json( + $res->action_attempt ?? null, + ); + + if ($action_attempt === null) { + throw new \UnexpectedValueException( + "Seam returned no action attempt for {$action_attempt_id}", + ); + } + + return $action_attempt; + } + + private static function now(): float + { + return microtime(true); + } +} diff --git a/src/Http/SeamHttpClient.php b/src/Http/SeamHttpClient.php new file mode 100644 index 00000000..2ad04030 --- /dev/null +++ b/src/Http/SeamHttpClient.php @@ -0,0 +1,287 @@ + $auth_headers + * @param array $guzzle_options + */ + public function __construct( + string $endpoint, + array $auth_headers, + array $guzzle_options = [], + ?int $retries = null, + ?ClientInterface $client = null, + ) { + $this->client = + $client ?? + new GuzzleClient( + self::create_config( + $endpoint, + $auth_headers, + $guzzle_options, + $retries ?? self::DEFAULT_RETRIES, + ), + ); + } + + /** + * Wraps an already configured Guzzle client. The client carries its own + * endpoint and authorization, so none are resolved here. + */ + public static function from_client(ClientInterface $client): self + { + return new self("", [], [], null, $client); + } + + public function get_client(): ClientInterface + { + return $this->client; + } + + /** + * @param array $auth_headers + * @param array $guzzle_options + * @return array + */ + private static function create_config( + string $endpoint, + array $auth_headers, + array $guzzle_options, + int $retries, + ): array { + // The middleware has to be on the handler stack the client is built + // with: a stack pushed onto after construction does not apply. + $handler = $guzzle_options["handler"] ?? HandlerStack::create(); + + if ($handler instanceof HandlerStack) { + $handler->push( + GuzzleRetryMiddleware::factory([ + "retry_enabled" => $retries > 0, + "max_retry_attempts" => $retries, + // A connection failure never reached the server, so it is + // safe to retry whatever the method. Status codes are + // opted into per request, see request(). + "retry_on_timeout" => true, + "retry_on_status" => [], + ]), + "seam_retry", + ); + } + + $headers = array_merge( + $auth_headers, + $guzzle_options["headers"] ?? [], + self::sdk_headers(), + ); + + return array_merge( + [ + "base_uri" => $endpoint, + "timeout" => self::DEFAULT_TIMEOUT, + ], + $guzzle_options, + [ + "handler" => $handler, + "headers" => $headers, + // Error mapping happens in request(); letting Guzzle throw + // first would bypass it entirely. + "http_errors" => false, + ], + ); + } + + /** + * @return array + */ + private static function sdk_headers(): array + { + $version = PackageVersion::get(); + + return [ + "User-Agent" => "seam-php/" . $version, + "seam-sdk-name" => "seamapi/php", + "seam-sdk-version" => $version, + "seam-lts-version" => self::LTS_VERSION, + ]; + } + + /** + * @param array|object|null $json + * @param array|null $query + */ + public function request( + string $method, + string $path, + mixed $json = null, + ?array $query = null, + ): mixed { + $options = array_filter( + [ + "json" => $json, + "query" => $query, + ], + fn($option) => $option !== null, + ); + + if (in_array(strtoupper($method), self::IDEMPOTENT_METHODS, true)) { + $options["retry_on_status"] = self::RETRYABLE_STATUS_CODES; + } + + $response = $this->client->request($method, $path, $options); + $status_code = $response->getStatusCode(); + + if ($status_code < 200 || $status_code >= 300) { + $this->handle_error_response( + new Request($method, $path), + $response, + ); + } + + return self::decode_body($response); + } + + private function handle_error_response( + RequestInterface $request, + ResponseInterface $response, + ): void { + $status_code = $response->getStatusCode(); + $request_id = self::get_request_id($response); + + if ($status_code === 401) { + throw new HttpUnauthorizedError($request_id); + } + + if (!self::is_api_error_response($response)) { + // Not a Seam error response, so there is nothing to map onto a + // Seam exception. The other Seam SDKs surface the underlying + // transport error here too. + throw BadResponseException::create($request, $response); + } + + $error = self::decode_body($response)->error; + + if (($error->type ?? null) === "invalid_input") { + throw new HttpInvalidInputError($error, $status_code, $request_id); + } + + throw new HttpApiError($error, $status_code, $request_id); + } + + /** + * True when the response body is a Seam error envelope, i.e. JSON holding + * an `error` object with a string `type` and `message`. + */ + private static function is_api_error_response( + ResponseInterface $response, + ): bool { + if ( + !str_starts_with( + $response->getHeaderLine("content-type"), + "application/json", + ) + ) { + return false; + } + + $body = self::decode_body($response); + + if (!is_object($body)) { + return false; + } + + $error = $body->error ?? null; + + if (!is_object($error)) { + return false; + } + + return is_string($error->type ?? null) && + is_string($error->message ?? null); + } + + private static function decode_body(ResponseInterface $response): mixed + { + $body = $response->getBody(); + $body->rewind(); + $contents = $body->getContents(); + + if ($contents === "") { + return null; + } + + try { + return Utils::jsonDecode($contents); + } catch (\InvalidArgumentException) { + return null; + } + } + + private static function get_request_id(ResponseInterface $response): ?string + { + return $response->hasHeader("seam-request-id") + ? $response->getHeaderLine("seam-request-id") + : null; + } +} diff --git a/src/Exceptions/HttpApiError.php b/src/HttpApiError.php similarity index 50% rename from src/Exceptions/HttpApiError.php rename to src/HttpApiError.php index a79a8dd5..23634d6b 100644 --- a/src/Exceptions/HttpApiError.php +++ b/src/HttpApiError.php @@ -2,26 +2,31 @@ namespace Seam; -class HttpApiError extends \Exception +/** + * Raised when the Seam API returns an error response. + */ +class HttpApiError extends \RuntimeException implements SeamException { - private string $errorCode; + protected string $errorCode; private int $statusCode; - private string $requestId; - private ?object $data = null; + private ?string $requestId; + private mixed $data; public function __construct( object $error, int $statusCode, - string $requestId, + ?string $requestId, ) { - $message = $error->message ?? "Unknown error"; - parent::__construct($message); - $this->errorCode = $error->type ?? "unknown"; + parent::__construct($error->message ?? "Unknown error"); + $this->errorCode = $error->type ?? "unknown_error"; $this->statusCode = $statusCode; $this->requestId = $requestId; $this->data = $error->data ?? null; } + /** + * The Seam error type, e.g. `device_not_found`. + */ public function getErrorCode(): string { return $this->errorCode; @@ -32,7 +37,10 @@ public function getStatusCode(): int return $this->statusCode; } - public function getRequestId(): string + /** + * The `seam-request-id` response header, or null when absent. + */ + public function getRequestId(): ?string { return $this->requestId; } diff --git a/src/Exceptions/HttpInvalidInputError.php b/src/HttpInvalidInputError.php similarity index 68% rename from src/Exceptions/HttpInvalidInputError.php rename to src/HttpInvalidInputError.php index 38cd8d74..1ff84ac6 100644 --- a/src/Exceptions/HttpInvalidInputError.php +++ b/src/HttpInvalidInputError.php @@ -2,6 +2,9 @@ namespace Seam; +/** + * Raised when the Seam API rejects the request parameters. + */ class HttpInvalidInputError extends HttpApiError { private object $validationErrors; @@ -9,13 +12,19 @@ class HttpInvalidInputError extends HttpApiError public function __construct( object $error, int $statusCode, - string $requestId, + ?string $requestId, ) { parent::__construct($error, $statusCode, $requestId); $this->errorCode = "invalid_input"; $this->validationErrors = $error->validation_errors ?? (object) []; } + /** + * The validation messages for a request parameter, or an empty array when + * that parameter has none. + * + * @return string[] + */ public function getValidationErrorMessages(string $paramName): array { return $this->validationErrors->{$paramName}->_errors ?? []; diff --git a/src/HttpUnauthorizedError.php b/src/HttpUnauthorizedError.php new file mode 100644 index 00000000..386cba38 --- /dev/null +++ b/src/HttpUnauthorizedError.php @@ -0,0 +1,21 @@ + "unauthorized", + "message" => "Unauthorized", + ], + 401, + $requestId, + ); + } +} diff --git a/src/InvalidOptionsError.php b/src/InvalidOptionsError.php new file mode 100644 index 00000000..d3fd1d84 --- /dev/null +++ b/src/InvalidOptionsError.php @@ -0,0 +1,16 @@ +has_next_page ?? false), + next_page_cursor: $json->next_page_cursor ?? null, + next_page_url: $json->next_page_url ?? null, + ); + } +} diff --git a/src/Paginator.php b/src/Paginator.php index a913cf80..5b86e6db 100644 --- a/src/Paginator.php +++ b/src/Paginator.php @@ -2,11 +2,17 @@ namespace Seam; +/** + * Fetches and walks the pages of a list endpoint. + * + * Create one with `Seam::createPaginator`, passing a callable that invokes the + * list method with a params array. + */ class Paginator { private $request; - private $params; - private $pagination_cache = []; + private array $params; + private array $pagination_cache = []; private const FIRST_PAGE = "FIRST_PAGE"; public function __construct(callable $request, array $params = []) @@ -15,46 +21,55 @@ public function __construct(callable $request, array $params = []) $this->params = $params; } + /** + * @return array{0: array, 1: Pagination} + */ public function firstPage(): array { - $request = $this->request; - $params = $this->params; - - $params["on_response"] = fn($response) => $this->cachePagination( - $response, - self::FIRST_PAGE, - ); - - $data = $request($params); - - return [$data, $this->pagination_cache[self::FIRST_PAGE]]; + return $this->fetchPage(self::FIRST_PAGE); } - public function nextPage(string $next_page_cursor): array + /** + * @return array{0: array, 1: Pagination} + */ + public function nextPage(?string $next_page_cursor): array { - if ($next_page_cursor === null) { + if ($next_page_cursor === null || $next_page_cursor === "") { throw new \InvalidArgumentException( - "Cannot get the next page with a null next_page_cursor", + "Cannot get the next page without a next_page_cursor", ); } + return $this->fetchPage($next_page_cursor); + } + + /** + * @return array{0: array, 1: Pagination} + */ + private function fetchPage(string $cursor): array + { $request = $this->request; $params = $this->params; - $params["page_cursor"] = $next_page_cursor; + if ($cursor !== self::FIRST_PAGE) { + $params["page_cursor"] = $cursor; + } + $params["on_response"] = fn($response) => $this->cachePagination( $response, - $next_page_cursor, + $cursor, ); $data = $request($params); - return [$data, $this->pagination_cache[$next_page_cursor]]; + return [$data, $this->pagination_cache[$cursor] ?? new Pagination()]; } - private function cachePagination($response, $next_page_cursor) + private function cachePagination($response, string $cursor): void { - $this->pagination_cache[$next_page_cursor] = $response->pagination; + $this->pagination_cache[$cursor] = Pagination::from_json( + $response->pagination ?? null, + ); } public function flattenToArray(): array diff --git a/src/Routes/AccessCodesClient.php b/src/Routes/AccessCodesClient.php index 1c53b718..a3465a8d 100644 --- a/src/Routes/AccessCodesClient.php +++ b/src/Routes/AccessCodesClient.php @@ -2,19 +2,28 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\AccessCode; -use Seam\SeamClient; class AccessCodesClient { - private SeamClient $seam; + private SeamHttpClient $client; + + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; public AccessCodesSimulateClient $simulate; public AccessCodesUnmanagedClient $unmanaged; - public function __construct(SeamClient $seam) + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; - $this->simulate = new AccessCodesSimulateClient($seam); - $this->unmanaged = new AccessCodesUnmanagedClient($seam); + $this->client = $client; + $this->defaults = $defaults; + $this->simulate = new AccessCodesSimulateClient($client, $defaults); + $this->unmanaged = new AccessCodesUnmanagedClient($client, $defaults); } /** @@ -64,9 +73,7 @@ public function create( ): AccessCode { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; if ($allow_external_modification !== null) { $request_payload[ "allow_external_modification" @@ -127,7 +134,7 @@ public function create( ] = $use_offline_access_code; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_codes/create", json: (object) $request_payload, @@ -185,9 +192,7 @@ public function create_multiple( ): array { $request_payload = []; - if ($device_ids !== null) { - $request_payload["device_ids"] = $device_ids; - } + $request_payload["device_ids"] = $device_ids; if ($allow_external_modification !== null) { $request_payload[ "allow_external_modification" @@ -234,7 +239,7 @@ public function create_multiple( ] = $use_backup_access_code_pool; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_codes/create_multiple", json: (object) $request_payload, @@ -259,14 +264,12 @@ public function delete( ): void { $request_payload = []; - if ($access_code_id !== null) { - $request_payload["access_code_id"] = $access_code_id; - } + $request_payload["access_code_id"] = $access_code_id; if ($device_id !== null) { $request_payload["device_id"] = $device_id; } - $this->seam->request( + $this->client->request( "POST", "/access_codes/delete", json: (object) $request_payload, @@ -283,11 +286,9 @@ public function generate_code(string $device_id): AccessCode { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_codes/generate_code", json: (object) $request_payload, @@ -323,7 +324,7 @@ public function get( $request_payload["device_id"] = $device_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_codes/get", json: (object) $request_payload, @@ -347,6 +348,7 @@ public function get( * @param string $page_cursor Identifies the specific page of results to return, obtained from the previous page's `next_page_cursor`. * @param string $search String for which to search. Filters returned access codes to include all records that satisfy a partial match using `name`, `code` or `access_code_id`. * @param string $user_identifier_key Your user ID for the user by which to filter access codes. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -395,7 +397,7 @@ public function list( $request_payload["user_identifier_key"] = $user_identifier_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_codes/list", json: (object) $request_payload, @@ -429,11 +431,9 @@ public function pull_backup_access_code(string $access_code_id): AccessCode { $request_payload = []; - if ($access_code_id !== null) { - $request_payload["access_code_id"] = $access_code_id; - } + $request_payload["access_code_id"] = $access_code_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_codes/pull_backup_access_code", json: (object) $request_payload, @@ -461,9 +461,7 @@ public function report_device_constraints( ): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; if ($max_code_length !== null) { $request_payload["max_code_length"] = $max_code_length; } @@ -476,7 +474,7 @@ public function report_device_constraints( ] = $supported_code_lengths; } - $this->seam->request( + $this->client->request( "POST", "/access_codes/report_device_constraints", json: (object) $request_payload, @@ -536,9 +534,7 @@ public function update( ): void { $request_payload = []; - if ($access_code_id !== null) { - $request_payload["access_code_id"] = $access_code_id; - } + $request_payload["access_code_id"] = $access_code_id; if ($allow_external_modification !== null) { $request_payload[ "allow_external_modification" @@ -605,7 +601,7 @@ public function update( ] = $use_offline_access_code; } - $this->seam->request( + $this->client->request( "POST", "/access_codes/update", json: (object) $request_payload, @@ -639,9 +635,7 @@ public function update_multiple( ): void { $request_payload = []; - if ($common_code_key !== null) { - $request_payload["common_code_key"] = $common_code_key; - } + $request_payload["common_code_key"] = $common_code_key; if ($ends_at !== null) { $request_payload["ends_at"] = $ends_at; } @@ -652,7 +646,7 @@ public function update_multiple( $request_payload["starts_at"] = $starts_at; } - $this->seam->request( + $this->client->request( "POST", "/access_codes/update_multiple", json: (object) $request_payload, diff --git a/src/Routes/AccessCodesSimulateClient.php b/src/Routes/AccessCodesSimulateClient.php index 7f1ded04..0750e822 100644 --- a/src/Routes/AccessCodesSimulateClient.php +++ b/src/Routes/AccessCodesSimulateClient.php @@ -2,16 +2,25 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\UnmanagedAccessCode; -use Seam\SeamClient; class AccessCodesSimulateClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -29,17 +38,11 @@ public function create_unmanaged_access_code( ): UnmanagedAccessCode { $request_payload = []; - if ($code !== null) { - $request_payload["code"] = $code; - } - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } - if ($name !== null) { - $request_payload["name"] = $name; - } - - $res = $this->seam->request( + $request_payload["code"] = $code; + $request_payload["device_id"] = $device_id; + $request_payload["name"] = $name; + + $res = $this->client->request( "POST", "/access_codes/simulate/create_unmanaged_access_code", json: (object) $request_payload, diff --git a/src/Routes/AccessCodesUnmanagedClient.php b/src/Routes/AccessCodesUnmanagedClient.php index 93ec0001..e18b0ab3 100644 --- a/src/Routes/AccessCodesUnmanagedClient.php +++ b/src/Routes/AccessCodesUnmanagedClient.php @@ -2,16 +2,25 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\UnmanagedAccessCode; -use Seam\SeamClient; class AccessCodesUnmanagedClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -35,9 +44,7 @@ public function convert_to_managed( ): void { $request_payload = []; - if ($access_code_id !== null) { - $request_payload["access_code_id"] = $access_code_id; - } + $request_payload["access_code_id"] = $access_code_id; if ($allow_external_modification !== null) { $request_payload[ "allow_external_modification" @@ -52,7 +59,7 @@ public function convert_to_managed( ] = $is_external_modification_allowed; } - $this->seam->request( + $this->client->request( "POST", "/access_codes/unmanaged/convert_to_managed", json: (object) $request_payload, @@ -69,11 +76,9 @@ public function delete(string $access_code_id): void { $request_payload = []; - if ($access_code_id !== null) { - $request_payload["access_code_id"] = $access_code_id; - } + $request_payload["access_code_id"] = $access_code_id; - $this->seam->request( + $this->client->request( "POST", "/access_codes/unmanaged/delete", json: (object) $request_payload, @@ -107,7 +112,7 @@ public function get( $request_payload["device_id"] = $device_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_codes/unmanaged/get", json: (object) $request_payload, @@ -124,6 +129,7 @@ public function get( * @param string $page_cursor Identifies the specific page of results to return, obtained from the previous page's `next_page_cursor`. * @param string $search String for which to search. Filters returned access codes to include all records that satisfy a partial match using `name`, `code` or `access_code_id`. * @param string $user_identifier_key Your user ID for the user by which to filter unmanaged access codes. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -136,9 +142,7 @@ public function list( ): array { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; if ($limit !== null) { $request_payload["limit"] = $limit; } @@ -152,7 +156,7 @@ public function list( $request_payload["user_identifier_key"] = $user_identifier_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_codes/unmanaged/list", json: (object) $request_payload, @@ -187,12 +191,8 @@ public function update( ): void { $request_payload = []; - if ($access_code_id !== null) { - $request_payload["access_code_id"] = $access_code_id; - } - if ($is_managed !== null) { - $request_payload["is_managed"] = $is_managed; - } + $request_payload["access_code_id"] = $access_code_id; + $request_payload["is_managed"] = $is_managed; if ($allow_external_modification !== null) { $request_payload[ "allow_external_modification" @@ -207,7 +207,7 @@ public function update( ] = $is_external_modification_allowed; } - $this->seam->request( + $this->client->request( "POST", "/access_codes/unmanaged/update", json: (object) $request_payload, diff --git a/src/Routes/AccessGrantsClient.php b/src/Routes/AccessGrantsClient.php index 42c99bd3..188412e5 100644 --- a/src/Routes/AccessGrantsClient.php +++ b/src/Routes/AccessGrantsClient.php @@ -2,18 +2,27 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\AccessGrant; use Seam\Resources\Batch; -use Seam\SeamClient; class AccessGrantsClient { - private SeamClient $seam; + private SeamHttpClient $client; + + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; public AccessGrantsUnmanagedClient $unmanaged; - public function __construct(SeamClient $seam) + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; - $this->unmanaged = new AccessGrantsUnmanagedClient($seam); + $this->client = $client; + $this->defaults = $defaults; + $this->unmanaged = new AccessGrantsUnmanagedClient($client, $defaults); } /** @@ -55,11 +64,9 @@ public function create( ): AccessGrant { $request_payload = []; - if ($requested_access_methods !== null) { - $request_payload[ - "requested_access_methods" - ] = $requested_access_methods; - } + $request_payload[ + "requested_access_methods" + ] = $requested_access_methods; if ($user_identity_id !== null) { $request_payload["user_identity_id"] = $user_identity_id; } @@ -105,7 +112,7 @@ public function create( $request_payload["starts_at"] = $starts_at; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_grants/create", json: (object) $request_payload, @@ -124,11 +131,9 @@ public function delete(string $access_grant_id): void { $request_payload = []; - if ($access_grant_id !== null) { - $request_payload["access_grant_id"] = $access_grant_id; - } + $request_payload["access_grant_id"] = $access_grant_id; - $this->seam->request( + $this->client->request( "POST", "/access_grants/delete", json: (object) $request_payload, @@ -155,7 +160,7 @@ public function get( $request_payload["access_grant_key"] = $access_grant_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_grants/get", json: (object) $request_payload, @@ -194,7 +199,7 @@ public function get_related( $request_payload["include"] = $include; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_grants/get_related", json: (object) $request_payload, @@ -219,6 +224,7 @@ public function get_related( * @param string $reservation_key Filter Access Grants by reservation_key. * @param string $space_id ID of the space by which you want to filter the list of Access Grants. * @param string $user_identity_id ID of user identity by which you want to filter the list of Access Grants. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -279,7 +285,7 @@ public function list( $request_payload["user_identity_id"] = $user_identity_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_grants/list", json: (object) $request_payload, @@ -308,16 +314,12 @@ public function request_access_methods( ): AccessGrant { $request_payload = []; - if ($access_grant_id !== null) { - $request_payload["access_grant_id"] = $access_grant_id; - } - if ($requested_access_methods !== null) { - $request_payload[ - "requested_access_methods" - ] = $requested_access_methods; - } + $request_payload["access_grant_id"] = $access_grant_id; + $request_payload[ + "requested_access_methods" + ] = $requested_access_methods; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_grants/request_access_methods", json: (object) $request_payload, @@ -361,7 +363,7 @@ public function update( $request_payload["starts_at"] = $starts_at; } - $this->seam->request( + $this->client->request( "POST", "/access_grants/update", json: (object) $request_payload, diff --git a/src/Routes/AccessGrantsUnmanagedClient.php b/src/Routes/AccessGrantsUnmanagedClient.php index 84c4c449..72955f56 100644 --- a/src/Routes/AccessGrantsUnmanagedClient.php +++ b/src/Routes/AccessGrantsUnmanagedClient.php @@ -2,16 +2,25 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\UnmanagedAccessGrant; -use Seam\SeamClient; class AccessGrantsUnmanagedClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -24,11 +33,9 @@ public function get(string $access_grant_id): UnmanagedAccessGrant { $request_payload = []; - if ($access_grant_id !== null) { - $request_payload["access_grant_id"] = $access_grant_id; - } + $request_payload["access_grant_id"] = $access_grant_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_grants/unmanaged/get", json: (object) $request_payload, @@ -46,6 +53,7 @@ public function get(string $access_grant_id): UnmanagedAccessGrant * @param string $page_cursor Identifies the specific page of results to return, obtained from the previous page's `next_page_cursor`. * @param string $reservation_key Filter unmanaged Access Grants by reservation_key. * @param string $user_identity_id ID of user identity by which you want to filter the list of unmanaged Access Grants. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -78,7 +86,7 @@ public function list( $request_payload["user_identity_id"] = $user_identity_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_grants/unmanaged/list", json: (object) $request_payload, @@ -113,17 +121,13 @@ public function update( ): void { $request_payload = []; - if ($access_grant_id !== null) { - $request_payload["access_grant_id"] = $access_grant_id; - } - if ($is_managed !== null) { - $request_payload["is_managed"] = $is_managed; - } + $request_payload["access_grant_id"] = $access_grant_id; + $request_payload["is_managed"] = $is_managed; if ($access_grant_key !== null) { $request_payload["access_grant_key"] = $access_grant_key; } - $this->seam->request( + $this->client->request( "POST", "/access_grants/unmanaged/update", json: (object) $request_payload, diff --git a/src/Routes/AccessMethodsClient.php b/src/Routes/AccessMethodsClient.php index 6e9d975d..09421e8e 100644 --- a/src/Routes/AccessMethodsClient.php +++ b/src/Routes/AccessMethodsClient.php @@ -2,19 +2,29 @@ namespace Seam\Routes; +use Seam\Http\ResolveActionAttempt; +use Seam\Http\SeamHttpClient; use Seam\Resources\AccessMethod; use Seam\Resources\ActionAttempt; use Seam\Resources\Batch; -use Seam\SeamClient; class AccessMethodsClient { - private SeamClient $seam; + private SeamHttpClient $client; + + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; public AccessMethodsUnmanagedClient $unmanaged; - public function __construct(SeamClient $seam) + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; - $this->unmanaged = new AccessMethodsUnmanagedClient($seam); + $this->client = $client; + $this->defaults = $defaults; + $this->unmanaged = new AccessMethodsUnmanagedClient($client, $defaults); } /** @@ -22,37 +32,31 @@ public function __construct(SeamClient $seam) * * @param string $access_method_id ID of the `access_method` to assign the credential to. * @param string $card_number Card number of the credential to assign. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function assign_card( string $access_method_id, string $card_number, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($access_method_id !== null) { - $request_payload["access_method_id"] = $access_method_id; - } - if ($card_number !== null) { - $request_payload["card_number"] = $card_number; - } + $request_payload["access_method_id"] = $access_method_id; + $request_payload["card_number"] = $card_number; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_methods/assign_card", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } /** @@ -80,7 +84,7 @@ public function delete( $request_payload["reservation_key"] = $reservation_key; } - $this->seam->request( + $this->client->request( "POST", "/access_methods/delete", json: (object) $request_payload, @@ -92,37 +96,31 @@ public function delete( * * @param string $access_method_id ID of the `access_method` to encode onto a card. * @param string $acs_encoder_id ID of the `acs_encoder` to use to encode the `access_method`. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function encode( string $access_method_id, string $acs_encoder_id, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($access_method_id !== null) { - $request_payload["access_method_id"] = $access_method_id; - } - if ($acs_encoder_id !== null) { - $request_payload["acs_encoder_id"] = $acs_encoder_id; - } + $request_payload["access_method_id"] = $access_method_id; + $request_payload["acs_encoder_id"] = $acs_encoder_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_methods/encode", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } /** @@ -135,11 +133,9 @@ public function get(string $access_method_id): AccessMethod { $request_payload = []; - if ($access_method_id !== null) { - $request_payload["access_method_id"] = $access_method_id; - } + $request_payload["access_method_id"] = $access_method_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_methods/get", json: (object) $request_payload, @@ -163,9 +159,7 @@ public function get_related( ): Batch { $request_payload = []; - if ($access_method_ids !== null) { - $request_payload["access_method_ids"] = $access_method_ids; - } + $request_payload["access_method_ids"] = $access_method_ids; if ($exclude !== null) { $request_payload["exclude"] = $exclude; } @@ -173,7 +167,7 @@ public function get_related( $request_payload["include"] = $include; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_methods/get_related", json: (object) $request_payload, @@ -193,6 +187,7 @@ public function get_related( * @param int $limit Maximum number of records to return per page. * @param string $page_cursor Identifies the specific page of results to return, obtained from the previous page's `next_page_cursor`. * @param string $space_id ID of the space by which to filter the returned access methods. Must be combined with `access_grant_id`, `access_grant_key`, or `acs_entrance_id`. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -233,7 +228,7 @@ public function list( $request_payload["space_id"] = $space_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_methods/list", json: (object) $request_payload, @@ -254,36 +249,30 @@ public function list( * * @param string $access_method_id ID of the cloud_key `access_method` to use for the unlock operation. * @param string $acs_entrance_id ID of the entrance to unlock. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function unlock_door( string $access_method_id, string $acs_entrance_id, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($access_method_id !== null) { - $request_payload["access_method_id"] = $access_method_id; - } - if ($acs_entrance_id !== null) { - $request_payload["acs_entrance_id"] = $acs_entrance_id; - } + $request_payload["access_method_id"] = $access_method_id; + $request_payload["acs_entrance_id"] = $acs_entrance_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_methods/unlock_door", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } } diff --git a/src/Routes/AccessMethodsUnmanagedClient.php b/src/Routes/AccessMethodsUnmanagedClient.php index 94f9eb15..4599a2b4 100644 --- a/src/Routes/AccessMethodsUnmanagedClient.php +++ b/src/Routes/AccessMethodsUnmanagedClient.php @@ -2,16 +2,25 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\UnmanagedAccessMethod; -use Seam\SeamClient; class AccessMethodsUnmanagedClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -24,11 +33,9 @@ public function get(string $access_method_id): UnmanagedAccessMethod { $request_payload = []; - if ($access_method_id !== null) { - $request_payload["access_method_id"] = $access_method_id; - } + $request_payload["access_method_id"] = $access_method_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_methods/unmanaged/get", json: (object) $request_payload, @@ -54,9 +61,7 @@ public function list( ): array { $request_payload = []; - if ($access_grant_id !== null) { - $request_payload["access_grant_id"] = $access_grant_id; - } + $request_payload["access_grant_id"] = $access_grant_id; if ($acs_entrance_id !== null) { $request_payload["acs_entrance_id"] = $acs_entrance_id; } @@ -67,7 +72,7 @@ public function list( $request_payload["space_id"] = $space_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/access_methods/unmanaged/list", json: (object) $request_payload, diff --git a/src/Routes/AcsAccessGroupsClient.php b/src/Routes/AcsAccessGroupsClient.php index f4878d23..8fea3af4 100644 --- a/src/Routes/AcsAccessGroupsClient.php +++ b/src/Routes/AcsAccessGroupsClient.php @@ -2,18 +2,27 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\AcsAccessGroup; use Seam\Resources\AcsEntrance; use Seam\Resources\AcsUser; -use Seam\SeamClient; class AcsAccessGroupsClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -31,9 +40,7 @@ public function add_user( ): void { $request_payload = []; - if ($acs_access_group_id !== null) { - $request_payload["acs_access_group_id"] = $acs_access_group_id; - } + $request_payload["acs_access_group_id"] = $acs_access_group_id; if ($acs_user_id !== null) { $request_payload["acs_user_id"] = $acs_user_id; } @@ -41,7 +48,7 @@ public function add_user( $request_payload["user_identity_id"] = $user_identity_id; } - $this->seam->request( + $this->client->request( "POST", "/acs/access_groups/add_user", json: (object) $request_payload, @@ -58,11 +65,9 @@ public function delete(string $acs_access_group_id): void { $request_payload = []; - if ($acs_access_group_id !== null) { - $request_payload["acs_access_group_id"] = $acs_access_group_id; - } + $request_payload["acs_access_group_id"] = $acs_access_group_id; - $this->seam->request( + $this->client->request( "POST", "/acs/access_groups/delete", json: (object) $request_payload, @@ -79,11 +84,9 @@ public function get(string $acs_access_group_id): AcsAccessGroup { $request_payload = []; - if ($acs_access_group_id !== null) { - $request_payload["acs_access_group_id"] = $acs_access_group_id; - } + $request_payload["acs_access_group_id"] = $acs_access_group_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/access_groups/get", json: (object) $request_payload, @@ -122,7 +125,7 @@ public function list( $request_payload["user_identity_id"] = $user_identity_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/access_groups/list", json: (object) $request_payload, @@ -145,11 +148,9 @@ public function list_accessible_entrances( ): array { $request_payload = []; - if ($acs_access_group_id !== null) { - $request_payload["acs_access_group_id"] = $acs_access_group_id; - } + $request_payload["acs_access_group_id"] = $acs_access_group_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/access_groups/list_accessible_entrances", json: (object) $request_payload, @@ -171,11 +172,9 @@ public function list_users(string $acs_access_group_id): array { $request_payload = []; - if ($acs_access_group_id !== null) { - $request_payload["acs_access_group_id"] = $acs_access_group_id; - } + $request_payload["acs_access_group_id"] = $acs_access_group_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/access_groups/list_users", json: (object) $request_payload, @@ -199,9 +198,7 @@ public function remove_user( ): void { $request_payload = []; - if ($acs_access_group_id !== null) { - $request_payload["acs_access_group_id"] = $acs_access_group_id; - } + $request_payload["acs_access_group_id"] = $acs_access_group_id; if ($acs_user_id !== null) { $request_payload["acs_user_id"] = $acs_user_id; } @@ -209,7 +206,7 @@ public function remove_user( $request_payload["user_identity_id"] = $user_identity_id; } - $this->seam->request( + $this->client->request( "POST", "/acs/access_groups/remove_user", json: (object) $request_payload, diff --git a/src/Routes/AcsClient.php b/src/Routes/AcsClient.php index e6454811..1d5f11e0 100644 --- a/src/Routes/AcsClient.php +++ b/src/Routes/AcsClient.php @@ -2,25 +2,34 @@ namespace Seam\Routes; -use Seam\SeamClient; +use Seam\Http\SeamHttpClient; class AcsClient { - private SeamClient $seam; + private SeamHttpClient $client; + + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; public AcsAccessGroupsClient $access_groups; public AcsCredentialsClient $credentials; public AcsEncodersClient $encoders; public AcsEntrancesClient $entrances; public AcsSystemsClient $systems; public AcsUsersClient $users; - public function __construct(SeamClient $seam) + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; - $this->access_groups = new AcsAccessGroupsClient($seam); - $this->credentials = new AcsCredentialsClient($seam); - $this->encoders = new AcsEncodersClient($seam); - $this->entrances = new AcsEntrancesClient($seam); - $this->systems = new AcsSystemsClient($seam); - $this->users = new AcsUsersClient($seam); + $this->client = $client; + $this->defaults = $defaults; + $this->access_groups = new AcsAccessGroupsClient($client, $defaults); + $this->credentials = new AcsCredentialsClient($client, $defaults); + $this->encoders = new AcsEncodersClient($client, $defaults); + $this->entrances = new AcsEntrancesClient($client, $defaults); + $this->systems = new AcsSystemsClient($client, $defaults); + $this->users = new AcsUsersClient($client, $defaults); } } diff --git a/src/Routes/AcsCredentialsClient.php b/src/Routes/AcsCredentialsClient.php index b4151455..43b41e38 100644 --- a/src/Routes/AcsCredentialsClient.php +++ b/src/Routes/AcsCredentialsClient.php @@ -2,17 +2,26 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\AcsCredential; use Seam\Resources\AcsEntrance; -use Seam\SeamClient; class AcsCredentialsClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -30,9 +39,7 @@ public function assign( ): void { $request_payload = []; - if ($acs_credential_id !== null) { - $request_payload["acs_credential_id"] = $acs_credential_id; - } + $request_payload["acs_credential_id"] = $acs_credential_id; if ($acs_user_id !== null) { $request_payload["acs_user_id"] = $acs_user_id; } @@ -40,7 +47,7 @@ public function assign( $request_payload["user_identity_id"] = $user_identity_id; } - $this->seam->request( + $this->client->request( "POST", "/acs/credentials/assign", json: (object) $request_payload, @@ -82,9 +89,7 @@ public function create( ): AcsCredential { $request_payload = []; - if ($access_method !== null) { - $request_payload["access_method"] = $access_method; - } + $request_payload["access_method"] = $access_method; if ($acs_system_id !== null) { $request_payload["acs_system_id"] = $acs_system_id; } @@ -130,7 +135,7 @@ public function create( $request_payload["visionline_metadata"] = $visionline_metadata; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/credentials/create", json: (object) $request_payload, @@ -149,11 +154,9 @@ public function delete(string $acs_credential_id): void { $request_payload = []; - if ($acs_credential_id !== null) { - $request_payload["acs_credential_id"] = $acs_credential_id; - } + $request_payload["acs_credential_id"] = $acs_credential_id; - $this->seam->request( + $this->client->request( "POST", "/acs/credentials/delete", json: (object) $request_payload, @@ -170,11 +173,9 @@ public function get(string $acs_credential_id): AcsCredential { $request_payload = []; - if ($acs_credential_id !== null) { - $request_payload["acs_credential_id"] = $acs_credential_id; - } + $request_payload["acs_credential_id"] = $acs_credential_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/credentials/get", json: (object) $request_payload, @@ -194,6 +195,7 @@ public function get(string $acs_credential_id): AcsCredential * @param float $limit Number of credentials to return. * @param string $page_cursor Identifies the specific page of results to return, obtained from the previous page's `next_page_cursor`. * @param string $search String for which to search. Filters returned credentials to include all records that satisfy a partial match using `display_name`, `code`, `card_number`, `acs_user_id` or `acs_credential_id`. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -236,7 +238,7 @@ public function list( $request_payload["search"] = $search; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/credentials/list", json: (object) $request_payload, @@ -262,11 +264,9 @@ public function list_accessible_entrances(string $acs_credential_id): array { $request_payload = []; - if ($acs_credential_id !== null) { - $request_payload["acs_credential_id"] = $acs_credential_id; - } + $request_payload["acs_credential_id"] = $acs_credential_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/credentials/list_accessible_entrances", json: (object) $request_payload, @@ -293,9 +293,7 @@ public function unassign( ): void { $request_payload = []; - if ($acs_credential_id !== null) { - $request_payload["acs_credential_id"] = $acs_credential_id; - } + $request_payload["acs_credential_id"] = $acs_credential_id; if ($acs_user_id !== null) { $request_payload["acs_user_id"] = $acs_user_id; } @@ -303,7 +301,7 @@ public function unassign( $request_payload["user_identity_id"] = $user_identity_id; } - $this->seam->request( + $this->client->request( "POST", "/acs/credentials/unassign", json: (object) $request_payload, @@ -325,9 +323,7 @@ public function update( ): void { $request_payload = []; - if ($acs_credential_id !== null) { - $request_payload["acs_credential_id"] = $acs_credential_id; - } + $request_payload["acs_credential_id"] = $acs_credential_id; if ($code !== null) { $request_payload["code"] = $code; } @@ -335,7 +331,7 @@ public function update( $request_payload["ends_at"] = $ends_at; } - $this->seam->request( + $this->client->request( "POST", "/acs/credentials/update", json: (object) $request_payload, diff --git a/src/Routes/AcsEncodersClient.php b/src/Routes/AcsEncodersClient.php index d277fdb9..dd12962a 100644 --- a/src/Routes/AcsEncodersClient.php +++ b/src/Routes/AcsEncodersClient.php @@ -2,18 +2,28 @@ namespace Seam\Routes; +use Seam\Http\ResolveActionAttempt; +use Seam\Http\SeamHttpClient; use Seam\Resources\AcsEncoder; use Seam\Resources\ActionAttempt; -use Seam\SeamClient; class AcsEncodersClient { - private SeamClient $seam; + private SeamHttpClient $client; + + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; public AcsEncodersSimulateClient $simulate; - public function __construct(SeamClient $seam) + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; - $this->simulate = new AcsEncodersSimulateClient($seam); + $this->client = $client; + $this->defaults = $defaults; + $this->simulate = new AcsEncodersSimulateClient($client, $defaults); } /** @@ -22,19 +32,18 @@ public function __construct(SeamClient $seam) * @param string $acs_encoder_id ID of the `acs_encoder` to use to encode the `acs_credential`. * @param string $access_method_id ID of the `access_method` to encode onto a card. * @param string $acs_credential_id ID of the `acs_credential` to encode onto a card. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function encode_credential( string $acs_encoder_id, ?string $access_method_id = null, ?string $acs_credential_id = null, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($acs_encoder_id !== null) { - $request_payload["acs_encoder_id"] = $acs_encoder_id; - } + $request_payload["acs_encoder_id"] = $acs_encoder_id; if ($access_method_id !== null) { $request_payload["access_method_id"] = $access_method_id; } @@ -42,21 +51,18 @@ public function encode_credential( $request_payload["acs_credential_id"] = $acs_credential_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/encoders/encode_credential", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } /** @@ -69,11 +75,9 @@ public function get(string $acs_encoder_id): AcsEncoder { $request_payload = []; - if ($acs_encoder_id !== null) { - $request_payload["acs_encoder_id"] = $acs_encoder_id; - } + $request_payload["acs_encoder_id"] = $acs_encoder_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/encoders/get", json: (object) $request_payload, @@ -90,6 +94,7 @@ public function get(string $acs_encoder_id): AcsEncoder * @param array $acs_encoder_ids IDs of the encoders that you want to retrieve. * @param float $limit Number of encoders to return. * @param string $page_cursor Identifies the specific page of results to return, obtained from the previous page's `next_page_cursor`. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -118,7 +123,7 @@ public function list( $request_payload["page_cursor"] = $page_cursor; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/encoders/list", json: (object) $request_payload, @@ -139,37 +144,33 @@ public function list( * * @param string $acs_encoder_id ID of the encoder to use for the scan. * @param mixed $salto_ks_metadata Salto KS-specific metadata for the scan action. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function scan_credential( string $acs_encoder_id, mixed $salto_ks_metadata = null, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($acs_encoder_id !== null) { - $request_payload["acs_encoder_id"] = $acs_encoder_id; - } + $request_payload["acs_encoder_id"] = $acs_encoder_id; if ($salto_ks_metadata !== null) { $request_payload["salto_ks_metadata"] = $salto_ks_metadata; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/encoders/scan_credential", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } /** @@ -179,6 +180,7 @@ public function scan_credential( * @param string $acs_user_id ID of the `acs_user` to assign the scanned credential to. * @param mixed $salto_ks_metadata Salto KS-specific metadata for the scan action. * @param string $user_identity_id ID of the `user_identity` to assign the scanned credential to. If the ACS system contains an ACS user linked to this user identity, it is used. Otherwise, one is created. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function scan_to_assign_credential( @@ -186,13 +188,11 @@ public function scan_to_assign_credential( ?string $acs_user_id = null, mixed $salto_ks_metadata = null, ?string $user_identity_id = null, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($acs_encoder_id !== null) { - $request_payload["acs_encoder_id"] = $acs_encoder_id; - } + $request_payload["acs_encoder_id"] = $acs_encoder_id; if ($acs_user_id !== null) { $request_payload["acs_user_id"] = $acs_user_id; } @@ -203,20 +203,17 @@ public function scan_to_assign_credential( $request_payload["user_identity_id"] = $user_identity_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/encoders/scan_to_assign_credential", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } } diff --git a/src/Routes/AcsEncodersSimulateClient.php b/src/Routes/AcsEncodersSimulateClient.php index 7fb9c2b4..54ce9aaf 100644 --- a/src/Routes/AcsEncodersSimulateClient.php +++ b/src/Routes/AcsEncodersSimulateClient.php @@ -2,15 +2,24 @@ namespace Seam\Routes; -use Seam\SeamClient; +use Seam\Http\SeamHttpClient; class AcsEncodersSimulateClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -28,9 +37,7 @@ public function next_credential_encode_will_fail( ): void { $request_payload = []; - if ($acs_encoder_id !== null) { - $request_payload["acs_encoder_id"] = $acs_encoder_id; - } + $request_payload["acs_encoder_id"] = $acs_encoder_id; if ($error_code !== null) { $request_payload["error_code"] = $error_code; } @@ -38,7 +45,7 @@ public function next_credential_encode_will_fail( $request_payload["acs_credential_id"] = $acs_credential_id; } - $this->seam->request( + $this->client->request( "POST", "/acs/encoders/simulate/next_credential_encode_will_fail", json: (object) $request_payload, @@ -58,14 +65,12 @@ public function next_credential_encode_will_succeed( ): void { $request_payload = []; - if ($acs_encoder_id !== null) { - $request_payload["acs_encoder_id"] = $acs_encoder_id; - } + $request_payload["acs_encoder_id"] = $acs_encoder_id; if ($scenario !== null) { $request_payload["scenario"] = $scenario; } - $this->seam->request( + $this->client->request( "POST", "/acs/encoders/simulate/next_credential_encode_will_succeed", json: (object) $request_payload, @@ -87,9 +92,7 @@ public function next_credential_scan_will_fail( ): void { $request_payload = []; - if ($acs_encoder_id !== null) { - $request_payload["acs_encoder_id"] = $acs_encoder_id; - } + $request_payload["acs_encoder_id"] = $acs_encoder_id; if ($error_code !== null) { $request_payload["error_code"] = $error_code; } @@ -99,7 +102,7 @@ public function next_credential_scan_will_fail( ] = $acs_credential_id_on_seam; } - $this->seam->request( + $this->client->request( "POST", "/acs/encoders/simulate/next_credential_scan_will_fail", json: (object) $request_payload, @@ -121,9 +124,7 @@ public function next_credential_scan_will_succeed( ): void { $request_payload = []; - if ($acs_encoder_id !== null) { - $request_payload["acs_encoder_id"] = $acs_encoder_id; - } + $request_payload["acs_encoder_id"] = $acs_encoder_id; if ($acs_credential_id_on_seam !== null) { $request_payload[ "acs_credential_id_on_seam" @@ -133,7 +134,7 @@ public function next_credential_scan_will_succeed( $request_payload["scenario"] = $scenario; } - $this->seam->request( + $this->client->request( "POST", "/acs/encoders/simulate/next_credential_scan_will_succeed", json: (object) $request_payload, diff --git a/src/Routes/AcsEntrancesClient.php b/src/Routes/AcsEntrancesClient.php index 583d6e5e..270cfb24 100644 --- a/src/Routes/AcsEntrancesClient.php +++ b/src/Routes/AcsEntrancesClient.php @@ -2,18 +2,28 @@ namespace Seam\Routes; +use Seam\Http\ResolveActionAttempt; +use Seam\Http\SeamHttpClient; use Seam\Resources\AcsCredential; use Seam\Resources\AcsEntrance; use Seam\Resources\ActionAttempt; -use Seam\SeamClient; class AcsEntrancesClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -26,11 +36,9 @@ public function get(string $acs_entrance_id): AcsEntrance { $request_payload = []; - if ($acs_entrance_id !== null) { - $request_payload["acs_entrance_id"] = $acs_entrance_id; - } + $request_payload["acs_entrance_id"] = $acs_entrance_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/entrances/get", json: (object) $request_payload, @@ -54,9 +62,7 @@ public function grant_access( ): void { $request_payload = []; - if ($acs_entrance_id !== null) { - $request_payload["acs_entrance_id"] = $acs_entrance_id; - } + $request_payload["acs_entrance_id"] = $acs_entrance_id; if ($acs_user_id !== null) { $request_payload["acs_user_id"] = $acs_user_id; } @@ -64,7 +70,7 @@ public function grant_access( $request_payload["user_identity_id"] = $user_identity_id; } - $this->seam->request( + $this->client->request( "POST", "/acs/entrances/grant_access", json: (object) $request_payload, @@ -85,6 +91,7 @@ public function grant_access( * @param string $page_cursor Identifies the specific page of results to return, obtained from the previous page's `next_page_cursor`. * @param string $search String for which to search. Filters returned entrances to include all records that satisfy a partial match using `display_name`. * @param string $space_id ID of the space for which you want to list entrances. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -137,7 +144,7 @@ public function list( $request_payload["space_id"] = $space_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/entrances/list", json: (object) $request_payload, @@ -166,14 +173,12 @@ public function list_credentials_with_access( ): array { $request_payload = []; - if ($acs_entrance_id !== null) { - $request_payload["acs_entrance_id"] = $acs_entrance_id; - } + $request_payload["acs_entrance_id"] = $acs_entrance_id; if ($include_if !== null) { $request_payload["include_if"] = $include_if; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/entrances/list_credentials_with_access", json: (object) $request_payload, @@ -190,36 +195,30 @@ public function list_credentials_with_access( * * @param string $acs_credential_id ID of the cloud_key credential to use for the unlock operation. * @param string $acs_entrance_id ID of the entrance to unlock. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function unlock( string $acs_credential_id, string $acs_entrance_id, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($acs_credential_id !== null) { - $request_payload["acs_credential_id"] = $acs_credential_id; - } - if ($acs_entrance_id !== null) { - $request_payload["acs_entrance_id"] = $acs_entrance_id; - } + $request_payload["acs_credential_id"] = $acs_credential_id; + $request_payload["acs_entrance_id"] = $acs_entrance_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/entrances/unlock", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } } diff --git a/src/Routes/AcsSystemsClient.php b/src/Routes/AcsSystemsClient.php index 9278fd9f..cc519bf5 100644 --- a/src/Routes/AcsSystemsClient.php +++ b/src/Routes/AcsSystemsClient.php @@ -2,16 +2,25 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\AcsSystem; -use Seam\SeamClient; class AcsSystemsClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -24,11 +33,9 @@ public function get(string $acs_system_id): AcsSystem { $request_payload = []; - if ($acs_system_id !== null) { - $request_payload["acs_system_id"] = $acs_system_id; - } + $request_payload["acs_system_id"] = $acs_system_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/systems/get", json: (object) $request_payload, @@ -64,7 +71,7 @@ public function list( $request_payload["search"] = $search; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/systems/list", json: (object) $request_payload, @@ -86,11 +93,9 @@ public function list_compatible_credential_manager_acs_systems( ): array { $request_payload = []; - if ($acs_system_id !== null) { - $request_payload["acs_system_id"] = $acs_system_id; - } + $request_payload["acs_system_id"] = $acs_system_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/systems/list_compatible_credential_manager_acs_systems", json: (object) $request_payload, @@ -114,9 +119,7 @@ public function report_devices( ): void { $request_payload = []; - if ($acs_system_id !== null) { - $request_payload["acs_system_id"] = $acs_system_id; - } + $request_payload["acs_system_id"] = $acs_system_id; if ($acs_encoders !== null) { $request_payload["acs_encoders"] = $acs_encoders; } @@ -124,7 +127,7 @@ public function report_devices( $request_payload["acs_entrances"] = $acs_entrances; } - $this->seam->request( + $this->client->request( "POST", "/acs/systems/report_devices", json: (object) $request_payload, diff --git a/src/Routes/AcsUsersClient.php b/src/Routes/AcsUsersClient.php index 729709e1..422a0502 100644 --- a/src/Routes/AcsUsersClient.php +++ b/src/Routes/AcsUsersClient.php @@ -2,17 +2,26 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\AcsEntrance; use Seam\Resources\AcsUser; -use Seam\SeamClient; class AcsUsersClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -28,14 +37,10 @@ public function add_to_access_group( ): void { $request_payload = []; - if ($acs_access_group_id !== null) { - $request_payload["acs_access_group_id"] = $acs_access_group_id; - } - if ($acs_user_id !== null) { - $request_payload["acs_user_id"] = $acs_user_id; - } + $request_payload["acs_access_group_id"] = $acs_access_group_id; + $request_payload["acs_user_id"] = $acs_user_id; - $this->seam->request( + $this->client->request( "POST", "/acs/users/add_to_access_group", json: (object) $request_payload, @@ -67,12 +72,8 @@ public function create( ): AcsUser { $request_payload = []; - if ($acs_system_id !== null) { - $request_payload["acs_system_id"] = $acs_system_id; - } - if ($full_name !== null) { - $request_payload["full_name"] = $full_name; - } + $request_payload["acs_system_id"] = $acs_system_id; + $request_payload["full_name"] = $full_name; if ($access_schedule !== null) { $request_payload["access_schedule"] = $access_schedule; } @@ -92,7 +93,7 @@ public function create( $request_payload["user_identity_id"] = $user_identity_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/users/create", json: (object) $request_payload, @@ -126,7 +127,7 @@ public function delete( $request_payload["user_identity_id"] = $user_identity_id; } - $this->seam->request( + $this->client->request( "POST", "/acs/users/delete", json: (object) $request_payload, @@ -158,7 +159,7 @@ public function get( $request_payload["user_identity_id"] = $user_identity_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/users/get", json: (object) $request_payload, @@ -178,6 +179,7 @@ public function get( * @param string $user_identity_email_address Email address of the user identity for which you want to retrieve all access system users. * @param string $user_identity_id ID of the user identity for which you want to retrieve all access system users. * @param string $user_identity_phone_number Phone number of the user identity for which you want to retrieve all access system users, in [E.164 format](https://www.itu.int/rec/T-REC-E.164/en) (for example, `+15555550100`). + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -222,7 +224,7 @@ public function list( ] = $user_identity_phone_number; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/users/list", json: (object) $request_payload, @@ -260,7 +262,7 @@ public function list_accessible_entrances( $request_payload["user_identity_id"] = $user_identity_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/acs/users/list_accessible_entrances", json: (object) $request_payload, @@ -287,9 +289,7 @@ public function remove_from_access_group( ): void { $request_payload = []; - if ($acs_access_group_id !== null) { - $request_payload["acs_access_group_id"] = $acs_access_group_id; - } + $request_payload["acs_access_group_id"] = $acs_access_group_id; if ($acs_user_id !== null) { $request_payload["acs_user_id"] = $acs_user_id; } @@ -297,7 +297,7 @@ public function remove_from_access_group( $request_payload["user_identity_id"] = $user_identity_id; } - $this->seam->request( + $this->client->request( "POST", "/acs/users/remove_from_access_group", json: (object) $request_payload, @@ -329,7 +329,7 @@ public function revoke_access_to_all_entrances( $request_payload["user_identity_id"] = $user_identity_id; } - $this->seam->request( + $this->client->request( "POST", "/acs/users/revoke_access_to_all_entrances", json: (object) $request_payload, @@ -361,7 +361,7 @@ public function suspend( $request_payload["user_identity_id"] = $user_identity_id; } - $this->seam->request( + $this->client->request( "POST", "/acs/users/suspend", json: (object) $request_payload, @@ -393,7 +393,7 @@ public function unsuspend( $request_payload["user_identity_id"] = $user_identity_id; } - $this->seam->request( + $this->client->request( "POST", "/acs/users/unsuspend", json: (object) $request_payload, @@ -455,7 +455,7 @@ public function update( $request_payload["user_identity_id"] = $user_identity_id; } - $this->seam->request( + $this->client->request( "POST", "/acs/users/update", json: (object) $request_payload, diff --git a/src/Routes/ActionAttemptsClient.php b/src/Routes/ActionAttemptsClient.php index becd80db..971a286e 100644 --- a/src/Routes/ActionAttemptsClient.php +++ b/src/Routes/ActionAttemptsClient.php @@ -2,41 +2,55 @@ namespace Seam\Routes; -use Seam\ActionAttemptFailedError; -use Seam\ActionAttemptTimeoutError; +use Seam\Http\ResolveActionAttempt; +use Seam\Http\SeamHttpClient; use Seam\Resources\ActionAttempt; -use Seam\SeamClient; class ActionAttemptsClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** * Returns a specified [action attempt](https://docs.seam.co/core-concepts/action-attempts). * * @param string $action_attempt_id ID of the action attempt that you want to get. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ - public function get(string $action_attempt_id): ActionAttempt - { + public function get( + string $action_attempt_id, + bool|array|null $wait_for_action_attempt = null, + ): ActionAttempt { $request_payload = []; - if ($action_attempt_id !== null) { - $request_payload["action_attempt_id"] = $action_attempt_id; - } + $request_payload["action_attempt_id"] = $action_attempt_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/action_attempts/get", json: (object) $request_payload, ); - return ActionAttempt::from_json($res->action_attempt); + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], + ); } /** @@ -46,6 +60,8 @@ public function get(string $action_attempt_id): ActionAttempt * @param string $device_id ID of the device to filter action attempts by. * @param int $limit Maximum number of records to return per page. * @param string $page_cursor Identifies the specific page of results to return, obtained from the previous page's `next_page_cursor`. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -53,6 +69,7 @@ public function list( ?string $device_id = null, ?int $limit = null, ?string $page_cursor = null, + bool|array|null $wait_for_action_attempt = null, ?callable $on_response = null, ): array { $request_payload = []; @@ -70,45 +87,17 @@ public function list( $request_payload["page_cursor"] = $page_cursor; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/action_attempts/list", json: (object) $request_payload, ); - if ($on_response !== null) { - $on_response($res); - } - - return array_map( - fn($r) => ActionAttempt::from_json($r), - $res->action_attempts, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempts), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); } - public function poll_until_ready( - string $action_attempt_id, - float $timeout = 20.0, - ): ActionAttempt { - $seam = $this->seam; - $time_waiting = 0.0; - $polling_interval = 0.4; - $action_attempt = $seam->action_attempts->get($action_attempt_id); - - while ($action_attempt->status == "pending") { - $action_attempt = $seam->action_attempts->get( - $action_attempt->action_attempt_id, - ); - if ($time_waiting > $timeout) { - throw new ActionAttemptTimeoutError($action_attempt, $timeout); - } - $time_waiting += $polling_interval; - usleep($polling_interval * 1000000); - } - - if ($action_attempt->status == "error") { - throw new ActionAttemptFailedError($action_attempt); - } - - return $action_attempt; - } } diff --git a/src/Routes/ClientSessionsClient.php b/src/Routes/ClientSessionsClient.php index ebb651de..93b81d8c 100644 --- a/src/Routes/ClientSessionsClient.php +++ b/src/Routes/ClientSessionsClient.php @@ -2,16 +2,25 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\ClientSession; -use Seam\SeamClient; class ClientSessionsClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -64,7 +73,7 @@ public function create( $request_payload["user_identity_ids"] = $user_identity_ids; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/client_sessions/create", json: (object) $request_payload, @@ -83,11 +92,9 @@ public function delete(string $client_session_id): void { $request_payload = []; - if ($client_session_id !== null) { - $request_payload["client_session_id"] = $client_session_id; - } + $request_payload["client_session_id"] = $client_session_id; - $this->seam->request( + $this->client->request( "POST", "/client_sessions/delete", json: (object) $request_payload, @@ -114,7 +121,7 @@ public function get( $request_payload["user_identifier_key"] = $user_identifier_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/client_sessions/get", json: (object) $request_payload, @@ -163,7 +170,7 @@ public function get_or_create( $request_payload["user_identity_ids"] = $user_identity_ids; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/client_sessions/get_or_create", json: (object) $request_payload, @@ -212,7 +219,7 @@ public function grant_access( $request_payload["user_identity_ids"] = $user_identity_ids; } - $this->seam->request( + $this->client->request( "POST", "/client_sessions/grant_access", json: (object) $request_payload, @@ -256,7 +263,7 @@ public function list( ] = $without_user_identifier_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/client_sessions/list", json: (object) $request_payload, @@ -280,11 +287,9 @@ public function revoke(string $client_session_id): void { $request_payload = []; - if ($client_session_id !== null) { - $request_payload["client_session_id"] = $client_session_id; - } + $request_payload["client_session_id"] = $client_session_id; - $this->seam->request( + $this->client->request( "POST", "/client_sessions/revoke", json: (object) $request_payload, diff --git a/src/Routes/ConnectWebviewsClient.php b/src/Routes/ConnectWebviewsClient.php index 4bd32a3e..760d8edb 100644 --- a/src/Routes/ConnectWebviewsClient.php +++ b/src/Routes/ConnectWebviewsClient.php @@ -2,16 +2,25 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\ConnectWebview; -use Seam\SeamClient; class ConnectWebviewsClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -86,7 +95,7 @@ public function create( ] = $wait_for_device_creation; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/connect_webviews/create", json: (object) $request_payload, @@ -107,11 +116,9 @@ public function delete(string $connect_webview_id): void { $request_payload = []; - if ($connect_webview_id !== null) { - $request_payload["connect_webview_id"] = $connect_webview_id; - } + $request_payload["connect_webview_id"] = $connect_webview_id; - $this->seam->request( + $this->client->request( "POST", "/connect_webviews/delete", json: (object) $request_payload, @@ -130,11 +137,9 @@ public function get(string $connect_webview_id): ConnectWebview { $request_payload = []; - if ($connect_webview_id !== null) { - $request_payload["connect_webview_id"] = $connect_webview_id; - } + $request_payload["connect_webview_id"] = $connect_webview_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/connect_webviews/get", json: (object) $request_payload, @@ -152,6 +157,7 @@ public function get(string $connect_webview_id): ConnectWebview * @param string $page_cursor Identifies the specific page of results to return, obtained from the previous page's `next_page_cursor`. * @param string $search String for which to search. Filters returned Connect Webviews to include all records that satisfy a partial match using `connect_webview_id`, `accepted_providers`, `custom_metadata`, or `customer_key`. * @param string $user_identifier_key Your user ID for the user by which you want to filter Connect Webviews. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -184,7 +190,7 @@ public function list( $request_payload["user_identifier_key"] = $user_identifier_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/connect_webviews/list", json: (object) $request_payload, diff --git a/src/Routes/ConnectedAccountsClient.php b/src/Routes/ConnectedAccountsClient.php index d3873aa0..9725597a 100644 --- a/src/Routes/ConnectedAccountsClient.php +++ b/src/Routes/ConnectedAccountsClient.php @@ -2,17 +2,29 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\ConnectedAccount; -use Seam\SeamClient; class ConnectedAccountsClient { - private SeamClient $seam; + private SeamHttpClient $client; + + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; public ConnectedAccountsSimulateClient $simulate; - public function __construct(SeamClient $seam) + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; - $this->simulate = new ConnectedAccountsSimulateClient($seam); + $this->client = $client; + $this->defaults = $defaults; + $this->simulate = new ConnectedAccountsSimulateClient( + $client, + $defaults, + ); } /** @@ -29,11 +41,9 @@ public function delete(string $connected_account_id): void { $request_payload = []; - if ($connected_account_id !== null) { - $request_payload["connected_account_id"] = $connected_account_id; - } + $request_payload["connected_account_id"] = $connected_account_id; - $this->seam->request( + $this->client->request( "POST", "/connected_accounts/delete", json: (object) $request_payload, @@ -60,7 +70,7 @@ public function get( $request_payload["email"] = $email; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/connected_accounts/get", json: (object) $request_payload, @@ -79,6 +89,7 @@ public function get( * @param string $search String for which to search. Filters returned connected accounts to include all records that satisfy a partial match using `connected_account_id`, `account_type`, `customer_key`, `custom_metadata`, `user_identifier.username`, `user_identifier.email` or `user_identifier.phone`. * @param string $space_id ID of the space by which you want to filter connected accounts. * @param string $user_identifier_key Your user ID for the user by which you want to filter connected accounts. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -115,7 +126,7 @@ public function list( $request_payload["user_identifier_key"] = $user_identifier_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/connected_accounts/list", json: (object) $request_payload, @@ -141,11 +152,9 @@ public function sync(string $connected_account_id): void { $request_payload = []; - if ($connected_account_id !== null) { - $request_payload["connected_account_id"] = $connected_account_id; - } + $request_payload["connected_account_id"] = $connected_account_id; - $this->seam->request( + $this->client->request( "POST", "/connected_accounts/sync", json: (object) $request_payload, @@ -173,9 +182,7 @@ public function update( ): void { $request_payload = []; - if ($connected_account_id !== null) { - $request_payload["connected_account_id"] = $connected_account_id; - } + $request_payload["connected_account_id"] = $connected_account_id; if ($accepted_capabilities !== null) { $request_payload["accepted_capabilities"] = $accepted_capabilities; } @@ -194,7 +201,7 @@ public function update( $request_payload["display_name"] = $display_name; } - $this->seam->request( + $this->client->request( "POST", "/connected_accounts/update", json: (object) $request_payload, diff --git a/src/Routes/ConnectedAccountsSimulateClient.php b/src/Routes/ConnectedAccountsSimulateClient.php index d3a913df..4eef7cb8 100644 --- a/src/Routes/ConnectedAccountsSimulateClient.php +++ b/src/Routes/ConnectedAccountsSimulateClient.php @@ -2,15 +2,24 @@ namespace Seam\Routes; -use Seam\SeamClient; +use Seam\Http\SeamHttpClient; class ConnectedAccountsSimulateClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -23,11 +32,9 @@ public function disconnect(string $connected_account_id): void { $request_payload = []; - if ($connected_account_id !== null) { - $request_payload["connected_account_id"] = $connected_account_id; - } + $request_payload["connected_account_id"] = $connected_account_id; - $this->seam->request( + $this->client->request( "POST", "/connected_accounts/simulate/disconnect", json: (object) $request_payload, diff --git a/src/Routes/CustomersClient.php b/src/Routes/CustomersClient.php index f8c97a1f..3fe55788 100644 --- a/src/Routes/CustomersClient.php +++ b/src/Routes/CustomersClient.php @@ -2,16 +2,25 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\CustomerPortal; -use Seam\SeamClient; class CustomersClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -83,7 +92,7 @@ public function create_portal( $request_payload["customer_data"] = $customer_data; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/customers/create_portal", json: (object) $request_payload, @@ -198,7 +207,7 @@ public function delete_data( $request_payload["user_keys"] = $user_keys; } - $this->seam->request( + $this->client->request( "POST", "/customers/delete_data", json: (object) $request_payload, @@ -254,9 +263,7 @@ public function push_data( ): void { $request_payload = []; - if ($customer_key !== null) { - $request_payload["customer_key"] = $customer_key; - } + $request_payload["customer_key"] = $customer_key; if ($access_grants !== null) { $request_payload["access_grants"] = $access_grants; } @@ -315,7 +322,7 @@ public function push_data( $request_payload["users"] = $users; } - $this->seam->request( + $this->client->request( "POST", "/customers/push_data", json: (object) $request_payload, diff --git a/src/Routes/DevicesClient.php b/src/Routes/DevicesClient.php index 9eb2f20c..61ed5303 100644 --- a/src/Routes/DevicesClient.php +++ b/src/Routes/DevicesClient.php @@ -2,20 +2,29 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\Device; use Seam\Resources\DeviceProvider; -use Seam\SeamClient; class DevicesClient { - private SeamClient $seam; + private SeamHttpClient $client; + + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; public DevicesSimulateClient $simulate; public DevicesUnmanagedClient $unmanaged; - public function __construct(SeamClient $seam) + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; - $this->simulate = new DevicesSimulateClient($seam); - $this->unmanaged = new DevicesUnmanagedClient($seam); + $this->client = $client; + $this->defaults = $defaults; + $this->simulate = new DevicesSimulateClient($client, $defaults); + $this->unmanaged = new DevicesUnmanagedClient($client, $defaults); } /** @@ -38,7 +47,7 @@ public function get(?string $device_id = null, ?string $name = null): Device $request_payload["name"] = $name; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/devices/get", json: (object) $request_payload, @@ -66,6 +75,7 @@ public function get(?string $device_id = null, ?string $name = null): Device * @param string $space_id ID of the space for which you want to list devices. * @param string $unstable_location_id * @param string $user_identifier_key Your own internal user ID for the user for which you want to list devices. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -138,7 +148,7 @@ public function list( $request_payload["user_identifier_key"] = $user_identifier_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/devices/list", json: (object) $request_payload, @@ -170,7 +180,7 @@ public function list_device_providers( $request_payload["provider_category"] = $provider_category; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/devices/list_device_providers", json: (object) $request_payload, @@ -192,11 +202,9 @@ public function report_provider_metadata(array $devices): void { $request_payload = []; - if ($devices !== null) { - $request_payload["devices"] = $devices; - } + $request_payload["devices"] = $devices; - $this->seam->request( + $this->client->request( "POST", "/devices/report_provider_metadata", json: (object) $request_payload, @@ -226,9 +234,7 @@ public function update( ): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; if ($backup_access_code_pool_enabled !== null) { $request_payload[ "backup_access_code_pool_enabled" @@ -247,7 +253,7 @@ public function update( $request_payload["properties"] = $properties; } - $this->seam->request( + $this->client->request( "POST", "/devices/update", json: (object) $request_payload, diff --git a/src/Routes/DevicesSimulateClient.php b/src/Routes/DevicesSimulateClient.php index 9fb351b3..2a14d57d 100644 --- a/src/Routes/DevicesSimulateClient.php +++ b/src/Routes/DevicesSimulateClient.php @@ -2,15 +2,24 @@ namespace Seam\Routes; -use Seam\SeamClient; +use Seam\Http\SeamHttpClient; class DevicesSimulateClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -23,11 +32,9 @@ public function connect(string $device_id): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; - $this->seam->request( + $this->client->request( "POST", "/devices/simulate/connect", json: (object) $request_payload, @@ -47,11 +54,9 @@ public function connect_to_hub(string $device_id): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; - $this->seam->request( + $this->client->request( "POST", "/devices/simulate/connect_to_hub", json: (object) $request_payload, @@ -68,11 +73,9 @@ public function disconnect(string $device_id): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; - $this->seam->request( + $this->client->request( "POST", "/devices/simulate/disconnect", json: (object) $request_payload, @@ -93,11 +96,9 @@ public function disconnect_from_hub(string $device_id): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; - $this->seam->request( + $this->client->request( "POST", "/devices/simulate/disconnect_from_hub", json: (object) $request_payload, @@ -117,14 +118,10 @@ public function paid_subscription(string $device_id, bool $is_expired): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } - if ($is_expired !== null) { - $request_payload["is_expired"] = $is_expired; - } + $request_payload["device_id"] = $device_id; + $request_payload["is_expired"] = $is_expired; - $this->seam->request( + $this->client->request( "POST", "/devices/simulate/paid_subscription", json: (object) $request_payload, @@ -141,11 +138,9 @@ public function remove(string $device_id): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; - $this->seam->request( + $this->client->request( "POST", "/devices/simulate/remove", json: (object) $request_payload, diff --git a/src/Routes/DevicesUnmanagedClient.php b/src/Routes/DevicesUnmanagedClient.php index 171090df..541be58e 100644 --- a/src/Routes/DevicesUnmanagedClient.php +++ b/src/Routes/DevicesUnmanagedClient.php @@ -2,16 +2,25 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\UnmanagedDevice; -use Seam\SeamClient; class DevicesUnmanagedClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -38,7 +47,7 @@ public function get( $request_payload["name"] = $name; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/devices/unmanaged/get", json: (object) $request_payload, @@ -68,6 +77,7 @@ public function get( * @param string $space_id ID of the space for which you want to list devices. * @param string $unstable_location_id * @param string $user_identifier_key Your own internal user ID for the user for which you want to list devices. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -140,7 +150,7 @@ public function list( $request_payload["user_identifier_key"] = $user_identifier_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/devices/unmanaged/list", json: (object) $request_payload, @@ -173,9 +183,7 @@ public function update( ): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; if ($custom_metadata !== null) { $request_payload["custom_metadata"] = $custom_metadata; } @@ -183,7 +191,7 @@ public function update( $request_payload["is_managed"] = $is_managed; } - $this->seam->request( + $this->client->request( "POST", "/devices/unmanaged/update", json: (object) $request_payload, diff --git a/src/Routes/EventsClient.php b/src/Routes/EventsClient.php index a1c37ba1..f2f1f65c 100644 --- a/src/Routes/EventsClient.php +++ b/src/Routes/EventsClient.php @@ -2,16 +2,25 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\Event; -use Seam\SeamClient; class EventsClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -39,7 +48,7 @@ public function get( $request_payload["event_type"] = $event_type; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/events/get", json: (object) $request_payload, @@ -198,7 +207,7 @@ public function list( $request_payload["user_identity_id"] = $user_identity_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/events/list", json: (object) $request_payload, diff --git a/src/Routes/InstantKeysClient.php b/src/Routes/InstantKeysClient.php index 34815729..e91f4f30 100644 --- a/src/Routes/InstantKeysClient.php +++ b/src/Routes/InstantKeysClient.php @@ -2,16 +2,25 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\InstantKey; -use Seam\SeamClient; class InstantKeysClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -24,11 +33,9 @@ public function delete(string $instant_key_id): void { $request_payload = []; - if ($instant_key_id !== null) { - $request_payload["instant_key_id"] = $instant_key_id; - } + $request_payload["instant_key_id"] = $instant_key_id; - $this->seam->request( + $this->client->request( "POST", "/instant_keys/delete", json: (object) $request_payload, @@ -55,7 +62,7 @@ public function get( $request_payload["instant_key_url"] = $instant_key_url; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/instant_keys/get", json: (object) $request_payload, @@ -78,7 +85,7 @@ public function list(?string $user_identity_id = null): array $request_payload["user_identity_id"] = $user_identity_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/instant_keys/list", json: (object) $request_payload, diff --git a/src/Routes/LocksClient.php b/src/Routes/LocksClient.php index 00878fd9..d54afadf 100644 --- a/src/Routes/LocksClient.php +++ b/src/Routes/LocksClient.php @@ -2,18 +2,28 @@ namespace Seam\Routes; +use Seam\Http\ResolveActionAttempt; +use Seam\Http\SeamHttpClient; use Seam\Resources\ActionAttempt; use Seam\Resources\Device; -use Seam\SeamClient; class LocksClient { - private SeamClient $seam; + private SeamHttpClient $client; + + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; public LocksSimulateClient $simulate; - public function __construct(SeamClient $seam) + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; - $this->simulate = new LocksSimulateClient($seam); + $this->client = $client; + $this->defaults = $defaults; + $this->simulate = new LocksSimulateClient($client, $defaults); } /** @@ -22,43 +32,37 @@ public function __construct(SeamClient $seam) * @param bool $auto_lock_enabled Whether to enable or disable auto-lock. * @param string $device_id ID of the lock for which you want to configure the auto-lock. * @param float $auto_lock_delay_seconds Delay in seconds before the lock automatically locks. Required when enabling auto-lock. Must be between 1 and 60. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function configure_auto_lock( bool $auto_lock_enabled, string $device_id, ?float $auto_lock_delay_seconds = null, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($auto_lock_enabled !== null) { - $request_payload["auto_lock_enabled"] = $auto_lock_enabled; - } - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["auto_lock_enabled"] = $auto_lock_enabled; + $request_payload["device_id"] = $device_id; if ($auto_lock_delay_seconds !== null) { $request_payload[ "auto_lock_delay_seconds" ] = $auto_lock_delay_seconds; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/locks/configure_auto_lock", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } /** @@ -80,7 +84,7 @@ public function get(?string $device_id = null, ?string $name = null): Device $request_payload["name"] = $name; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/locks/get", json: (object) $request_payload, @@ -108,6 +112,7 @@ public function get(?string $device_id = null, ?string $name = null): Device * @param string $space_id ID of the space for which you want to list devices. * @param string $unstable_location_id * @param string $user_identifier_key Your own internal user ID for the user for which you want to list devices. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -180,7 +185,7 @@ public function list( $request_payload["user_identifier_key"] = $user_identifier_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/locks/list", json: (object) $request_payload, @@ -197,65 +202,57 @@ public function list( * Locks a [lock](https://docs.seam.co/low-level-apis/smart-locks). See also [Locking and Unlocking Smart Locks](https://docs.seam.co/low-level-apis/smart-locks/lock-and-unlock). * * @param string $device_id ID of the lock that you want to lock. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function lock_door( string $device_id, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/locks/lock_door", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } /** * Unlocks a [lock](https://docs.seam.co/low-level-apis/smart-locks). See also [Locking and Unlocking Smart Locks](https://docs.seam.co/low-level-apis/smart-locks/lock-and-unlock). * * @param string $device_id ID of the lock that you want to unlock. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function unlock_door( string $device_id, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/locks/unlock_door", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } } diff --git a/src/Routes/LocksSimulateClient.php b/src/Routes/LocksSimulateClient.php index 500448d8..ca91968c 100644 --- a/src/Routes/LocksSimulateClient.php +++ b/src/Routes/LocksSimulateClient.php @@ -2,16 +2,26 @@ namespace Seam\Routes; +use Seam\Http\ResolveActionAttempt; +use Seam\Http\SeamHttpClient; use Seam\Resources\ActionAttempt; -use Seam\SeamClient; class LocksSimulateClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -19,69 +29,59 @@ public function __construct(SeamClient $seam) * * @param string $code Code that you want to simulate entering on a keypad. * @param string $device_id ID of the device for which you want to simulate a keypad code entry. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function keypad_code_entry( string $code, string $device_id, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($code !== null) { - $request_payload["code"] = $code; - } - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["code"] = $code; + $request_payload["device_id"] = $device_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/locks/simulate/keypad_code_entry", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } /** * Simulates a manual lock action using a keypad. You can only perform this action for [August](https://docs.seam.co/device-and-system-integration-guides/august-locks) devices within [sandbox workspaces](https://docs.seam.co/core-concepts/workspaces#sandbox-workspaces). * * @param string $device_id ID of the device for which you want to simulate a manual lock action using a keypad. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function manual_lock_via_keypad( string $device_id, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/locks/simulate/manual_lock_via_keypad", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } } diff --git a/src/Routes/NoiseSensorsClient.php b/src/Routes/NoiseSensorsClient.php index d34a85b9..3f199835 100644 --- a/src/Routes/NoiseSensorsClient.php +++ b/src/Routes/NoiseSensorsClient.php @@ -2,19 +2,31 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\Device; -use Seam\SeamClient; class NoiseSensorsClient { - private SeamClient $seam; + private SeamHttpClient $client; + + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; public NoiseSensorsNoiseThresholdsClient $noise_thresholds; public NoiseSensorsSimulateClient $simulate; - public function __construct(SeamClient $seam) + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; - $this->noise_thresholds = new NoiseSensorsNoiseThresholdsClient($seam); - $this->simulate = new NoiseSensorsSimulateClient($seam); + $this->client = $client; + $this->defaults = $defaults; + $this->noise_thresholds = new NoiseSensorsNoiseThresholdsClient( + $client, + $defaults, + ); + $this->simulate = new NoiseSensorsSimulateClient($client, $defaults); } /** @@ -36,6 +48,7 @@ public function __construct(SeamClient $seam) * @param string $space_id ID of the space for which you want to list devices. * @param string $unstable_location_id * @param string $user_identifier_key Your own internal user ID for the user for which you want to list devices. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -108,7 +121,7 @@ public function list( $request_payload["user_identifier_key"] = $user_identifier_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/noise_sensors/list", json: (object) $request_payload, diff --git a/src/Routes/NoiseSensorsNoiseThresholdsClient.php b/src/Routes/NoiseSensorsNoiseThresholdsClient.php index 80492126..af7ad0de 100644 --- a/src/Routes/NoiseSensorsNoiseThresholdsClient.php +++ b/src/Routes/NoiseSensorsNoiseThresholdsClient.php @@ -2,16 +2,25 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\NoiseThreshold; -use Seam\SeamClient; class NoiseSensorsNoiseThresholdsClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -35,15 +44,9 @@ public function create( ): NoiseThreshold { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } - if ($ends_daily_at !== null) { - $request_payload["ends_daily_at"] = $ends_daily_at; - } - if ($starts_daily_at !== null) { - $request_payload["starts_daily_at"] = $starts_daily_at; - } + $request_payload["device_id"] = $device_id; + $request_payload["ends_daily_at"] = $ends_daily_at; + $request_payload["starts_daily_at"] = $starts_daily_at; if ($name !== null) { $request_payload["name"] = $name; } @@ -56,7 +59,7 @@ public function create( $request_payload["noise_threshold_nrs"] = $noise_threshold_nrs; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/noise_sensors/noise_thresholds/create", json: (object) $request_payload, @@ -76,14 +79,10 @@ public function delete(string $device_id, string $noise_threshold_id): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } - if ($noise_threshold_id !== null) { - $request_payload["noise_threshold_id"] = $noise_threshold_id; - } + $request_payload["device_id"] = $device_id; + $request_payload["noise_threshold_id"] = $noise_threshold_id; - $this->seam->request( + $this->client->request( "POST", "/noise_sensors/noise_thresholds/delete", json: (object) $request_payload, @@ -100,11 +99,9 @@ public function get(string $noise_threshold_id): NoiseThreshold { $request_payload = []; - if ($noise_threshold_id !== null) { - $request_payload["noise_threshold_id"] = $noise_threshold_id; - } + $request_payload["noise_threshold_id"] = $noise_threshold_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/noise_sensors/noise_thresholds/get", json: (object) $request_payload, @@ -123,11 +120,9 @@ public function list(string $device_id): array { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/noise_sensors/noise_thresholds/list", json: (object) $request_payload, @@ -162,12 +157,8 @@ public function update( ): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } - if ($noise_threshold_id !== null) { - $request_payload["noise_threshold_id"] = $noise_threshold_id; - } + $request_payload["device_id"] = $device_id; + $request_payload["noise_threshold_id"] = $noise_threshold_id; if ($ends_daily_at !== null) { $request_payload["ends_daily_at"] = $ends_daily_at; } @@ -186,7 +177,7 @@ public function update( $request_payload["starts_daily_at"] = $starts_daily_at; } - $this->seam->request( + $this->client->request( "POST", "/noise_sensors/noise_thresholds/update", json: (object) $request_payload, diff --git a/src/Routes/NoiseSensorsSimulateClient.php b/src/Routes/NoiseSensorsSimulateClient.php index ac6c5891..7388b50b 100644 --- a/src/Routes/NoiseSensorsSimulateClient.php +++ b/src/Routes/NoiseSensorsSimulateClient.php @@ -2,15 +2,24 @@ namespace Seam\Routes; -use Seam\SeamClient; +use Seam\Http\SeamHttpClient; class NoiseSensorsSimulateClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -23,11 +32,9 @@ public function trigger_noise_threshold(string $device_id): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; - $this->seam->request( + $this->client->request( "POST", "/noise_sensors/simulate/trigger_noise_threshold", json: (object) $request_payload, diff --git a/src/Routes/PhonesClient.php b/src/Routes/PhonesClient.php index 922b3f64..b97d5243 100644 --- a/src/Routes/PhonesClient.php +++ b/src/Routes/PhonesClient.php @@ -2,17 +2,26 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\Phone; -use Seam\SeamClient; class PhonesClient { - private SeamClient $seam; + private SeamHttpClient $client; + + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; public PhonesSimulateClient $simulate; - public function __construct(SeamClient $seam) + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; - $this->simulate = new PhonesSimulateClient($seam); + $this->client = $client; + $this->defaults = $defaults; + $this->simulate = new PhonesSimulateClient($client, $defaults); } /** @@ -25,11 +34,9 @@ public function deactivate(string $device_id): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; - $this->seam->request( + $this->client->request( "POST", "/phones/deactivate", json: (object) $request_payload, @@ -46,11 +53,9 @@ public function get(string $device_id): Phone { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/phones/get", json: (object) $request_payload, @@ -81,7 +86,7 @@ public function list( ] = $owner_user_identity_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/phones/list", json: (object) $request_payload, diff --git a/src/Routes/PhonesSimulateClient.php b/src/Routes/PhonesSimulateClient.php index e33ee226..85c5e1ff 100644 --- a/src/Routes/PhonesSimulateClient.php +++ b/src/Routes/PhonesSimulateClient.php @@ -2,16 +2,25 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\Phone; -use Seam\SeamClient; class PhonesSimulateClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -31,9 +40,7 @@ public function create_sandbox_phone( ): Phone { $request_payload = []; - if ($user_identity_id !== null) { - $request_payload["user_identity_id"] = $user_identity_id; - } + $request_payload["user_identity_id"] = $user_identity_id; if ($assa_abloy_metadata !== null) { $request_payload["assa_abloy_metadata"] = $assa_abloy_metadata; } @@ -46,7 +53,7 @@ public function create_sandbox_phone( $request_payload["phone_metadata"] = $phone_metadata; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/phones/simulate/create_sandbox_phone", json: (object) $request_payload, diff --git a/src/Routes/SpacesClient.php b/src/Routes/SpacesClient.php index b90f93b9..46db212d 100644 --- a/src/Routes/SpacesClient.php +++ b/src/Routes/SpacesClient.php @@ -2,17 +2,26 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\Batch; use Seam\Resources\Space; -use Seam\SeamClient; class SpacesClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -28,14 +37,10 @@ public function add_acs_entrances( ): void { $request_payload = []; - if ($acs_entrance_ids !== null) { - $request_payload["acs_entrance_ids"] = $acs_entrance_ids; - } - if ($space_id !== null) { - $request_payload["space_id"] = $space_id; - } + $request_payload["acs_entrance_ids"] = $acs_entrance_ids; + $request_payload["space_id"] = $space_id; - $this->seam->request( + $this->client->request( "POST", "/spaces/add_acs_entrances", json: (object) $request_payload, @@ -55,14 +60,10 @@ public function add_connected_account( ): void { $request_payload = []; - if ($connected_account_id !== null) { - $request_payload["connected_account_id"] = $connected_account_id; - } - if ($space_id !== null) { - $request_payload["space_id"] = $space_id; - } + $request_payload["connected_account_id"] = $connected_account_id; + $request_payload["space_id"] = $space_id; - $this->seam->request( + $this->client->request( "POST", "/spaces/add_connected_account", json: (object) $request_payload, @@ -80,14 +81,10 @@ public function add_devices(array $device_ids, string $space_id): void { $request_payload = []; - if ($device_ids !== null) { - $request_payload["device_ids"] = $device_ids; - } - if ($space_id !== null) { - $request_payload["space_id"] = $space_id; - } + $request_payload["device_ids"] = $device_ids; + $request_payload["space_id"] = $space_id; - $this->seam->request( + $this->client->request( "POST", "/spaces/add_devices", json: (object) $request_payload, @@ -117,9 +114,7 @@ public function create( ): Space { $request_payload = []; - if ($name !== null) { - $request_payload["name"] = $name; - } + $request_payload["name"] = $name; if ($acs_entrance_ids !== null) { $request_payload["acs_entrance_ids"] = $acs_entrance_ids; } @@ -139,7 +134,7 @@ public function create( $request_payload["space_key"] = $space_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/spaces/create", json: (object) $request_payload, @@ -158,11 +153,9 @@ public function delete(string $space_id): void { $request_payload = []; - if ($space_id !== null) { - $request_payload["space_id"] = $space_id; - } + $request_payload["space_id"] = $space_id; - $this->seam->request( + $this->client->request( "POST", "/spaces/delete", json: (object) $request_payload, @@ -189,7 +182,7 @@ public function get( $request_payload["space_key"] = $space_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/spaces/get", json: (object) $request_payload, @@ -228,7 +221,7 @@ public function get_related( $request_payload["space_keys"] = $space_keys; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/spaces/get_related", json: (object) $request_payload, @@ -245,6 +238,7 @@ public function get_related( * @param string $page_cursor Identifies the specific page of results to return, obtained from the previous page's `next_page_cursor`. * @param string $search String for which to search. Filters returned spaces to include all records that satisfy a partial match using `name`, `space_key`, or `customer_key`. * @param string $space_key Filter spaces by space_key. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -273,7 +267,7 @@ public function list( $request_payload["space_key"] = $space_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/spaces/list", json: (object) $request_payload, @@ -299,14 +293,10 @@ public function remove_acs_entrances( ): void { $request_payload = []; - if ($acs_entrance_ids !== null) { - $request_payload["acs_entrance_ids"] = $acs_entrance_ids; - } - if ($space_id !== null) { - $request_payload["space_id"] = $space_id; - } + $request_payload["acs_entrance_ids"] = $acs_entrance_ids; + $request_payload["space_id"] = $space_id; - $this->seam->request( + $this->client->request( "POST", "/spaces/remove_acs_entrances", json: (object) $request_payload, @@ -326,14 +316,10 @@ public function remove_connected_account( ): void { $request_payload = []; - if ($connected_account_id !== null) { - $request_payload["connected_account_id"] = $connected_account_id; - } - if ($space_id !== null) { - $request_payload["space_id"] = $space_id; - } + $request_payload["connected_account_id"] = $connected_account_id; + $request_payload["space_id"] = $space_id; - $this->seam->request( + $this->client->request( "POST", "/spaces/remove_connected_account", json: (object) $request_payload, @@ -351,14 +337,10 @@ public function remove_devices(array $device_ids, string $space_id): void { $request_payload = []; - if ($device_ids !== null) { - $request_payload["device_ids"] = $device_ids; - } - if ($space_id !== null) { - $request_payload["space_id"] = $space_id; - } + $request_payload["device_ids"] = $device_ids; + $request_payload["space_id"] = $space_id; - $this->seam->request( + $this->client->request( "POST", "/spaces/remove_devices", json: (object) $request_payload, @@ -405,7 +387,7 @@ public function update( $request_payload["space_key"] = $space_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/spaces/update", json: (object) $request_payload, diff --git a/src/Routes/ThermostatsClient.php b/src/Routes/ThermostatsClient.php index 650aa7d6..7d7c4b4d 100644 --- a/src/Routes/ThermostatsClient.php +++ b/src/Routes/ThermostatsClient.php @@ -2,22 +2,35 @@ namespace Seam\Routes; +use Seam\Http\ResolveActionAttempt; +use Seam\Http\SeamHttpClient; use Seam\Resources\ActionAttempt; use Seam\Resources\Device; -use Seam\SeamClient; class ThermostatsClient { - private SeamClient $seam; + private SeamHttpClient $client; + + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; public ThermostatsDailyProgramsClient $daily_programs; public ThermostatsSchedulesClient $schedules; public ThermostatsSimulateClient $simulate; - public function __construct(SeamClient $seam) + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; - $this->daily_programs = new ThermostatsDailyProgramsClient($seam); - $this->schedules = new ThermostatsSchedulesClient($seam); - $this->simulate = new ThermostatsSimulateClient($seam); + $this->client = $client; + $this->defaults = $defaults; + $this->daily_programs = new ThermostatsDailyProgramsClient( + $client, + $defaults, + ); + $this->schedules = new ThermostatsSchedulesClient($client, $defaults); + $this->simulate = new ThermostatsSimulateClient($client, $defaults); } /** @@ -25,37 +38,31 @@ public function __construct(SeamClient $seam) * * @param string $climate_preset_key Climate preset key of the climate preset that you want to activate. * @param string $device_id ID of the thermostat device for which you want to activate a climate preset. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function activate_climate_preset( string $climate_preset_key, string $device_id, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($climate_preset_key !== null) { - $request_payload["climate_preset_key"] = $climate_preset_key; - } - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["climate_preset_key"] = $climate_preset_key; + $request_payload["device_id"] = $device_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/thermostats/activate_climate_preset", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } /** @@ -64,19 +71,18 @@ public function activate_climate_preset( * @param string $device_id ID of the thermostat device that you want to set to cool mode. * @param float $cooling_set_point_celsius [Cooling set point](https://docs.seam.co/capability-guides/thermostats/understanding-thermostat-concepts/set-points) in °C that you want to set for the thermostat. You must set one of the `cooling_set_point` parameters. * @param float $cooling_set_point_fahrenheit [Cooling set point](https://docs.seam.co/capability-guides/thermostats/understanding-thermostat-concepts/set-points) in °F that you want to set for the thermostat. You must set one of the `cooling_set_point` parameters. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function cool( string $device_id, ?float $cooling_set_point_celsius = null, ?float $cooling_set_point_fahrenheit = null, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; if ($cooling_set_point_celsius !== null) { $request_payload[ "cooling_set_point_celsius" @@ -88,21 +94,18 @@ public function cool( ] = $cooling_set_point_fahrenheit; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/thermostats/cool", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } /** @@ -138,12 +141,8 @@ public function create_climate_preset( ): void { $request_payload = []; - if ($climate_preset_key !== null) { - $request_payload["climate_preset_key"] = $climate_preset_key; - } - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["climate_preset_key"] = $climate_preset_key; + $request_payload["device_id"] = $device_id; if ($climate_preset_mode !== null) { $request_payload["climate_preset_mode"] = $climate_preset_mode; } @@ -185,7 +184,7 @@ public function create_climate_preset( $request_payload["name"] = $name; } - $this->seam->request( + $this->client->request( "POST", "/thermostats/create_climate_preset", json: (object) $request_payload, @@ -205,14 +204,10 @@ public function delete_climate_preset( ): void { $request_payload = []; - if ($climate_preset_key !== null) { - $request_payload["climate_preset_key"] = $climate_preset_key; - } - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["climate_preset_key"] = $climate_preset_key; + $request_payload["device_id"] = $device_id; - $this->seam->request( + $this->client->request( "POST", "/thermostats/delete_climate_preset", json: (object) $request_payload, @@ -225,19 +220,18 @@ public function delete_climate_preset( * @param string $device_id ID of the thermostat device that you want to set to heat mode. * @param float $heating_set_point_celsius [Heating set point](https://docs.seam.co/capability-guides/thermostats/understanding-thermostat-concepts/set-points) in °C that you want to set for the thermostat. You must set one of the `heating_set_point` parameters. * @param float $heating_set_point_fahrenheit [Heating set point](https://docs.seam.co/capability-guides/thermostats/understanding-thermostat-concepts/set-points) in °F that you want to set for the thermostat. You must set one of the `heating_set_point` parameters. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function heat( string $device_id, ?float $heating_set_point_celsius = null, ?float $heating_set_point_fahrenheit = null, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; if ($heating_set_point_celsius !== null) { $request_payload[ "heating_set_point_celsius" @@ -249,21 +243,18 @@ public function heat( ] = $heating_set_point_fahrenheit; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/thermostats/heat", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } /** @@ -274,6 +265,7 @@ public function heat( * @param float $cooling_set_point_fahrenheit [Cooling set point](https://docs.seam.co/capability-guides/thermostats/understanding-thermostat-concepts/set-points) in °F that you want to set for the thermostat. You must set one of the `cooling_set_point` parameters. * @param float $heating_set_point_celsius [Heating set point](https://docs.seam.co/capability-guides/thermostats/understanding-thermostat-concepts/set-points) in °C that you want to set for the thermostat. You must set one of the `heating_set_point` parameters. * @param float $heating_set_point_fahrenheit [Heating set point](https://docs.seam.co/capability-guides/thermostats/understanding-thermostat-concepts/set-points) in °F that you want to set for the thermostat. You must set one of the `heating_set_point` parameters. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function heat_cool( @@ -282,13 +274,11 @@ public function heat_cool( ?float $cooling_set_point_fahrenheit = null, ?float $heating_set_point_celsius = null, ?float $heating_set_point_fahrenheit = null, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; if ($cooling_set_point_celsius !== null) { $request_payload[ "cooling_set_point_celsius" @@ -310,21 +300,18 @@ public function heat_cool( ] = $heating_set_point_fahrenheit; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/thermostats/heat_cool", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } /** @@ -346,6 +333,7 @@ public function heat_cool( * @param string $space_id ID of the space for which you want to list devices. * @param string $unstable_location_id * @param string $user_identifier_key Your own internal user ID for the user for which you want to list devices. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -418,7 +406,7 @@ public function list( $request_payload["user_identifier_key"] = $user_identifier_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/thermostats/list", json: (object) $request_payload, @@ -435,33 +423,29 @@ public function list( * Sets a specified [thermostat](https://docs.seam.co/capability-guides/thermostats) to ["off" mode](https://docs.seam.co/capability-guides/thermostats/configure-current-climate-settings). * * @param string $device_id ID of the thermostat device that you want to set to off mode. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function off( string $device_id, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/thermostats/off", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } /** @@ -477,14 +461,10 @@ public function set_fallback_climate_preset( ): void { $request_payload = []; - if ($climate_preset_key !== null) { - $request_payload["climate_preset_key"] = $climate_preset_key; - } - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["climate_preset_key"] = $climate_preset_key; + $request_payload["device_id"] = $device_id; - $this->seam->request( + $this->client->request( "POST", "/thermostats/set_fallback_climate_preset", json: (object) $request_payload, @@ -497,19 +477,18 @@ public function set_fallback_climate_preset( * @param string $device_id ID of the thermostat device for which you want to set the fan mode. * @param string $fan_mode Fan mode setting for the thermostat, such as `auto`, `on`, or `circulate`. * @param string $fan_mode_setting [Fan mode setting](https://docs.seam.co/capability-guides/thermostats/configure-current-climate-settings#fan-mode-settings) that you want to set for the thermostat. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function set_fan_mode( string $device_id, ?string $fan_mode = null, ?string $fan_mode_setting = null, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; if ($fan_mode !== null) { $request_payload["fan_mode"] = $fan_mode; } @@ -517,21 +496,18 @@ public function set_fan_mode( $request_payload["fan_mode_setting"] = $fan_mode_setting; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/thermostats/set_fan_mode", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } /** @@ -543,6 +519,7 @@ public function set_fan_mode( * @param float $cooling_set_point_fahrenheit [Cooling set point](https://docs.seam.co/capability-guides/thermostats/understanding-thermostat-concepts/set-points) in °F that you want to set for the thermostat. You must set one of the `cooling_set_point` parameters. * @param float $heating_set_point_celsius [Heating set point](https://docs.seam.co/capability-guides/thermostats/understanding-thermostat-concepts/set-points) in °C that you want to set for the thermostat. You must set one of the `heating_set_point` parameters. * @param float $heating_set_point_fahrenheit [Heating set point](https://docs.seam.co/capability-guides/thermostats/understanding-thermostat-concepts/set-points) in °F that you want to set for the thermostat. You must set one of the `heating_set_point` parameters. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function set_hvac_mode( @@ -552,16 +529,12 @@ public function set_hvac_mode( ?float $cooling_set_point_fahrenheit = null, ?float $heating_set_point_celsius = null, ?float $heating_set_point_fahrenheit = null, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } - if ($hvac_mode_setting !== null) { - $request_payload["hvac_mode_setting"] = $hvac_mode_setting; - } + $request_payload["device_id"] = $device_id; + $request_payload["hvac_mode_setting"] = $hvac_mode_setting; if ($cooling_set_point_celsius !== null) { $request_payload[ "cooling_set_point_celsius" @@ -583,21 +556,18 @@ public function set_hvac_mode( ] = $heating_set_point_fahrenheit; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/thermostats/set_hvac_mode", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } /** @@ -619,9 +589,7 @@ public function set_temperature_threshold( ): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; if ($lower_limit_celsius !== null) { $request_payload["lower_limit_celsius"] = $lower_limit_celsius; } @@ -639,7 +607,7 @@ public function set_temperature_threshold( ] = $upper_limit_fahrenheit; } - $this->seam->request( + $this->client->request( "POST", "/thermostats/set_temperature_threshold", json: (object) $request_payload, @@ -679,12 +647,8 @@ public function update_climate_preset( ): void { $request_payload = []; - if ($climate_preset_key !== null) { - $request_payload["climate_preset_key"] = $climate_preset_key; - } - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["climate_preset_key"] = $climate_preset_key; + $request_payload["device_id"] = $device_id; if ($climate_preset_mode !== null) { $request_payload["climate_preset_mode"] = $climate_preset_mode; } @@ -726,7 +690,7 @@ public function update_climate_preset( $request_payload["name"] = $name; } - $this->seam->request( + $this->client->request( "POST", "/thermostats/update_climate_preset", json: (object) $request_payload, @@ -744,6 +708,7 @@ public function update_climate_preset( * @param string $thursday_program_id ID of the thermostat daily program to run on Thursdays. * @param string $tuesday_program_id ID of the thermostat daily program to run on Tuesdays. * @param string $wednesday_program_id ID of the thermostat daily program to run on Wednesdays. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function update_weekly_program( @@ -755,13 +720,11 @@ public function update_weekly_program( ?string $thursday_program_id = null, ?string $tuesday_program_id = null, ?string $wednesday_program_id = null, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; if ($friday_program_id !== null) { $request_payload["friday_program_id"] = $friday_program_id; } @@ -784,20 +747,17 @@ public function update_weekly_program( $request_payload["wednesday_program_id"] = $wednesday_program_id; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/thermostats/update_weekly_program", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } } diff --git a/src/Routes/ThermostatsDailyProgramsClient.php b/src/Routes/ThermostatsDailyProgramsClient.php index 6d786342..19f8196f 100644 --- a/src/Routes/ThermostatsDailyProgramsClient.php +++ b/src/Routes/ThermostatsDailyProgramsClient.php @@ -2,17 +2,27 @@ namespace Seam\Routes; +use Seam\Http\ResolveActionAttempt; +use Seam\Http\SeamHttpClient; use Seam\Resources\ActionAttempt; use Seam\Resources\ThermostatDailyProgram; -use Seam\SeamClient; class ThermostatsDailyProgramsClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -30,17 +40,11 @@ public function create( ): ThermostatDailyProgram { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } - if ($name !== null) { - $request_payload["name"] = $name; - } - if ($periods !== null) { - $request_payload["periods"] = $periods; - } - - $res = $this->seam->request( + $request_payload["device_id"] = $device_id; + $request_payload["name"] = $name; + $request_payload["periods"] = $periods; + + $res = $this->client->request( "POST", "/thermostats/daily_programs/create", json: (object) $request_payload, @@ -61,13 +65,11 @@ public function delete(string $thermostat_daily_program_id): void { $request_payload = []; - if ($thermostat_daily_program_id !== null) { - $request_payload[ - "thermostat_daily_program_id" - ] = $thermostat_daily_program_id; - } + $request_payload[ + "thermostat_daily_program_id" + ] = $thermostat_daily_program_id; - $this->seam->request( + $this->client->request( "POST", "/thermostats/daily_programs/delete", json: (object) $request_payload, @@ -80,42 +82,34 @@ public function delete(string $thermostat_daily_program_id): void * @param string $name Name of the thermostat daily program that you want to update. * @param array $periods Array of thermostat daily program periods. The periods that you specify overwrite any existing periods for the daily program. * @param string $thermostat_daily_program_id ID of the thermostat daily program that you want to update. + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function update( string $name, array $periods, string $thermostat_daily_program_id, - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { $request_payload = []; - if ($name !== null) { - $request_payload["name"] = $name; - } - if ($periods !== null) { - $request_payload["periods"] = $periods; - } - if ($thermostat_daily_program_id !== null) { - $request_payload[ - "thermostat_daily_program_id" - ] = $thermostat_daily_program_id; - } - - $res = $this->seam->request( + $request_payload["name"] = $name; + $request_payload["periods"] = $periods; + $request_payload[ + "thermostat_daily_program_id" + ] = $thermostat_daily_program_id; + + $res = $this->client->request( "POST", "/thermostats/daily_programs/update", json: (object) $request_payload, ); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } } diff --git a/src/Routes/ThermostatsSchedulesClient.php b/src/Routes/ThermostatsSchedulesClient.php index fbc504b3..51864c69 100644 --- a/src/Routes/ThermostatsSchedulesClient.php +++ b/src/Routes/ThermostatsSchedulesClient.php @@ -2,16 +2,25 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\ThermostatSchedule; -use Seam\SeamClient; class ThermostatsSchedulesClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -37,18 +46,10 @@ public function create( ): ThermostatSchedule { $request_payload = []; - if ($climate_preset_key !== null) { - $request_payload["climate_preset_key"] = $climate_preset_key; - } - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } - if ($ends_at !== null) { - $request_payload["ends_at"] = $ends_at; - } - if ($starts_at !== null) { - $request_payload["starts_at"] = $starts_at; - } + $request_payload["climate_preset_key"] = $climate_preset_key; + $request_payload["device_id"] = $device_id; + $request_payload["ends_at"] = $ends_at; + $request_payload["starts_at"] = $starts_at; if ($is_override_allowed !== null) { $request_payload["is_override_allowed"] = $is_override_allowed; } @@ -61,7 +62,7 @@ public function create( $request_payload["name"] = $name; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/thermostats/schedules/create", json: (object) $request_payload, @@ -80,13 +81,9 @@ public function delete(string $thermostat_schedule_id): void { $request_payload = []; - if ($thermostat_schedule_id !== null) { - $request_payload[ - "thermostat_schedule_id" - ] = $thermostat_schedule_id; - } + $request_payload["thermostat_schedule_id"] = $thermostat_schedule_id; - $this->seam->request( + $this->client->request( "POST", "/thermostats/schedules/delete", json: (object) $request_payload, @@ -103,13 +100,9 @@ public function get(string $thermostat_schedule_id): ThermostatSchedule { $request_payload = []; - if ($thermostat_schedule_id !== null) { - $request_payload[ - "thermostat_schedule_id" - ] = $thermostat_schedule_id; - } + $request_payload["thermostat_schedule_id"] = $thermostat_schedule_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/thermostats/schedules/get", json: (object) $request_payload, @@ -131,14 +124,12 @@ public function list( ): array { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; if ($user_identifier_key !== null) { $request_payload["user_identifier_key"] = $user_identifier_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/thermostats/schedules/list", json: (object) $request_payload, @@ -173,11 +164,7 @@ public function update( ): void { $request_payload = []; - if ($thermostat_schedule_id !== null) { - $request_payload[ - "thermostat_schedule_id" - ] = $thermostat_schedule_id; - } + $request_payload["thermostat_schedule_id"] = $thermostat_schedule_id; if ($climate_preset_key !== null) { $request_payload["climate_preset_key"] = $climate_preset_key; } @@ -199,7 +186,7 @@ public function update( $request_payload["starts_at"] = $starts_at; } - $this->seam->request( + $this->client->request( "POST", "/thermostats/schedules/update", json: (object) $request_payload, diff --git a/src/Routes/ThermostatsSimulateClient.php b/src/Routes/ThermostatsSimulateClient.php index e3cf413f..9b00648f 100644 --- a/src/Routes/ThermostatsSimulateClient.php +++ b/src/Routes/ThermostatsSimulateClient.php @@ -2,15 +2,24 @@ namespace Seam\Routes; -use Seam\SeamClient; +use Seam\Http\SeamHttpClient; class ThermostatsSimulateClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -34,12 +43,8 @@ public function hvac_mode_adjusted( ): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } - if ($hvac_mode !== null) { - $request_payload["hvac_mode"] = $hvac_mode; - } + $request_payload["device_id"] = $device_id; + $request_payload["hvac_mode"] = $hvac_mode; if ($cooling_set_point_celsius !== null) { $request_payload[ "cooling_set_point_celsius" @@ -61,7 +66,7 @@ public function hvac_mode_adjusted( ] = $heating_set_point_fahrenheit; } - $this->seam->request( + $this->client->request( "POST", "/thermostats/simulate/hvac_mode_adjusted", json: (object) $request_payload, @@ -83,9 +88,7 @@ public function temperature_reached( ): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } + $request_payload["device_id"] = $device_id; if ($temperature_celsius !== null) { $request_payload["temperature_celsius"] = $temperature_celsius; } @@ -95,7 +98,7 @@ public function temperature_reached( ] = $temperature_fahrenheit; } - $this->seam->request( + $this->client->request( "POST", "/thermostats/simulate/temperature_reached", json: (object) $request_payload, diff --git a/src/Routes/UserIdentitiesClient.php b/src/Routes/UserIdentitiesClient.php index 4dca58e2..e70ad961 100644 --- a/src/Routes/UserIdentitiesClient.php +++ b/src/Routes/UserIdentitiesClient.php @@ -2,22 +2,34 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\AcsEntrance; use Seam\Resources\AcsSystem; use Seam\Resources\AcsUser; use Seam\Resources\Device; use Seam\Resources\InstantKey; use Seam\Resources\UserIdentity; -use Seam\SeamClient; class UserIdentitiesClient { - private SeamClient $seam; + private SeamHttpClient $client; + + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; public UserIdentitiesUnmanagedClient $unmanaged; - public function __construct(SeamClient $seam) + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; - $this->unmanaged = new UserIdentitiesUnmanagedClient($seam); + $this->client = $client; + $this->defaults = $defaults; + $this->unmanaged = new UserIdentitiesUnmanagedClient( + $client, + $defaults, + ); } /** @@ -39,9 +51,7 @@ public function add_acs_user( ): void { $request_payload = []; - if ($acs_user_id !== null) { - $request_payload["acs_user_id"] = $acs_user_id; - } + $request_payload["acs_user_id"] = $acs_user_id; if ($user_identity_id !== null) { $request_payload["user_identity_id"] = $user_identity_id; } @@ -49,7 +59,7 @@ public function add_acs_user( $request_payload["user_identity_key"] = $user_identity_key; } - $this->seam->request( + $this->client->request( "POST", "/user_identities/add_acs_user", json: (object) $request_payload, @@ -91,7 +101,7 @@ public function create( $request_payload["user_identity_key"] = $user_identity_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/user_identities/create", json: (object) $request_payload, @@ -110,11 +120,9 @@ public function delete(string $user_identity_id): void { $request_payload = []; - if ($user_identity_id !== null) { - $request_payload["user_identity_id"] = $user_identity_id; - } + $request_payload["user_identity_id"] = $user_identity_id; - $this->seam->request( + $this->client->request( "POST", "/user_identities/delete", json: (object) $request_payload, @@ -136,9 +144,7 @@ public function generate_instant_key( ): InstantKey { $request_payload = []; - if ($user_identity_id !== null) { - $request_payload["user_identity_id"] = $user_identity_id; - } + $request_payload["user_identity_id"] = $user_identity_id; if ($customization_profile_id !== null) { $request_payload[ "customization_profile_id" @@ -148,7 +154,7 @@ public function generate_instant_key( $request_payload["max_use_count"] = $max_use_count; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/user_identities/generate_instant_key", json: (object) $request_payload, @@ -177,7 +183,7 @@ public function get( $request_payload["user_identity_key"] = $user_identity_key; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/user_identities/get", json: (object) $request_payload, @@ -199,14 +205,10 @@ public function grant_access_to_device( ): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } - if ($user_identity_id !== null) { - $request_payload["user_identity_id"] = $user_identity_id; - } + $request_payload["device_id"] = $device_id; + $request_payload["user_identity_id"] = $user_identity_id; - $this->seam->request( + $this->client->request( "POST", "/user_identities/grant_access_to_device", json: (object) $request_payload, @@ -222,6 +224,7 @@ public function grant_access_to_device( * @param string $page_cursor Identifies the specific page of results to return, obtained from the previous page's `next_page_cursor`. * @param string $search String for which to search. Filters returned user identities to include all records that satisfy a partial match using `full_name`, `phone_number`, `email_address` or `user_identity_id`. * @param array $user_identity_ids Array of user identity IDs by which to filter the list of user identities. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -256,7 +259,7 @@ public function list( $request_payload["user_identity_ids"] = $user_identity_ids; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/user_identities/list", json: (object) $request_payload, @@ -282,11 +285,9 @@ public function list_accessible_devices(string $user_identity_id): array { $request_payload = []; - if ($user_identity_id !== null) { - $request_payload["user_identity_id"] = $user_identity_id; - } + $request_payload["user_identity_id"] = $user_identity_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/user_identities/list_accessible_devices", json: (object) $request_payload, @@ -305,11 +306,9 @@ public function list_accessible_entrances(string $user_identity_id): array { $request_payload = []; - if ($user_identity_id !== null) { - $request_payload["user_identity_id"] = $user_identity_id; - } + $request_payload["user_identity_id"] = $user_identity_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/user_identities/list_accessible_entrances", json: (object) $request_payload, @@ -331,11 +330,9 @@ public function list_acs_systems(string $user_identity_id): array { $request_payload = []; - if ($user_identity_id !== null) { - $request_payload["user_identity_id"] = $user_identity_id; - } + $request_payload["user_identity_id"] = $user_identity_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/user_identities/list_acs_systems", json: (object) $request_payload, @@ -354,11 +351,9 @@ public function list_acs_users(string $user_identity_id): array { $request_payload = []; - if ($user_identity_id !== null) { - $request_payload["user_identity_id"] = $user_identity_id; - } + $request_payload["user_identity_id"] = $user_identity_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/user_identities/list_acs_users", json: (object) $request_payload, @@ -380,14 +375,10 @@ public function remove_acs_user( ): void { $request_payload = []; - if ($acs_user_id !== null) { - $request_payload["acs_user_id"] = $acs_user_id; - } - if ($user_identity_id !== null) { - $request_payload["user_identity_id"] = $user_identity_id; - } + $request_payload["acs_user_id"] = $acs_user_id; + $request_payload["user_identity_id"] = $user_identity_id; - $this->seam->request( + $this->client->request( "POST", "/user_identities/remove_acs_user", json: (object) $request_payload, @@ -407,14 +398,10 @@ public function revoke_access_to_device( ): void { $request_payload = []; - if ($device_id !== null) { - $request_payload["device_id"] = $device_id; - } - if ($user_identity_id !== null) { - $request_payload["user_identity_id"] = $user_identity_id; - } + $request_payload["device_id"] = $device_id; + $request_payload["user_identity_id"] = $user_identity_id; - $this->seam->request( + $this->client->request( "POST", "/user_identities/revoke_access_to_device", json: (object) $request_payload, @@ -440,9 +427,7 @@ public function update( ): void { $request_payload = []; - if ($user_identity_id !== null) { - $request_payload["user_identity_id"] = $user_identity_id; - } + $request_payload["user_identity_id"] = $user_identity_id; if ($email_address !== null) { $request_payload["email_address"] = $email_address; } @@ -456,7 +441,7 @@ public function update( $request_payload["user_identity_key"] = $user_identity_key; } - $this->seam->request( + $this->client->request( "POST", "/user_identities/update", json: (object) $request_payload, diff --git a/src/Routes/UserIdentitiesUnmanagedClient.php b/src/Routes/UserIdentitiesUnmanagedClient.php index e8f40773..6683b8a2 100644 --- a/src/Routes/UserIdentitiesUnmanagedClient.php +++ b/src/Routes/UserIdentitiesUnmanagedClient.php @@ -2,16 +2,25 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\UnmanagedUserIdentity; -use Seam\SeamClient; class UserIdentitiesUnmanagedClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -24,11 +33,9 @@ public function get(string $user_identity_id): UnmanagedUserIdentity { $request_payload = []; - if ($user_identity_id !== null) { - $request_payload["user_identity_id"] = $user_identity_id; - } + $request_payload["user_identity_id"] = $user_identity_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/user_identities/unmanaged/get", json: (object) $request_payload, @@ -44,6 +51,7 @@ public function get(string $user_identity_id): UnmanagedUserIdentity * @param int $limit Maximum number of records to return per page. * @param string $page_cursor Identifies the specific page of results to return, obtained from the previous page's `next_page_cursor`. * @param string $search String for which to search. Filters returned unmanaged user identities to include all records that satisfy a partial match using `full_name`, `phone_number`, `email_address`, `user_identity_id` or `acs_system_id`. + * @param callable|null $on_response Called with the raw response envelope, used by the paginator to read the pagination metadata. * @return array OK */ public function list( @@ -68,7 +76,7 @@ public function list( $request_payload["search"] = $search; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/user_identities/unmanaged/list", json: (object) $request_payload, @@ -101,17 +109,13 @@ public function update( ): void { $request_payload = []; - if ($is_managed !== null) { - $request_payload["is_managed"] = $is_managed; - } - if ($user_identity_id !== null) { - $request_payload["user_identity_id"] = $user_identity_id; - } + $request_payload["is_managed"] = $is_managed; + $request_payload["user_identity_id"] = $user_identity_id; if ($user_identity_key !== null) { $request_payload["user_identity_key"] = $user_identity_key; } - $this->seam->request( + $this->client->request( "POST", "/user_identities/unmanaged/update", json: (object) $request_payload, diff --git a/src/Routes/WebhooksClient.php b/src/Routes/WebhooksClient.php index 67fd844c..4d95a1b7 100644 --- a/src/Routes/WebhooksClient.php +++ b/src/Routes/WebhooksClient.php @@ -2,16 +2,25 @@ namespace Seam\Routes; +use Seam\Http\SeamHttpClient; use Seam\Resources\Webhook; -use Seam\SeamClient; class WebhooksClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -25,14 +34,12 @@ public function create(string $url, ?array $event_types = null): Webhook { $request_payload = []; - if ($url !== null) { - $request_payload["url"] = $url; - } + $request_payload["url"] = $url; if ($event_types !== null) { $request_payload["event_types"] = $event_types; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/webhooks/create", json: (object) $request_payload, @@ -51,11 +58,9 @@ public function delete(string $webhook_id): void { $request_payload = []; - if ($webhook_id !== null) { - $request_payload["webhook_id"] = $webhook_id; - } + $request_payload["webhook_id"] = $webhook_id; - $this->seam->request( + $this->client->request( "POST", "/webhooks/delete", json: (object) $request_payload, @@ -72,11 +77,9 @@ public function get(string $webhook_id): Webhook { $request_payload = []; - if ($webhook_id !== null) { - $request_payload["webhook_id"] = $webhook_id; - } + $request_payload["webhook_id"] = $webhook_id; - $res = $this->seam->request( + $res = $this->client->request( "POST", "/webhooks/get", json: (object) $request_payload, @@ -92,7 +95,7 @@ public function get(string $webhook_id): Webhook */ public function list(): array { - $res = $this->seam->request("POST", "/webhooks/list"); + $res = $this->client->request("POST", "/webhooks/list"); return array_map(fn($r) => Webhook::from_json($r), $res->webhooks); } @@ -108,14 +111,10 @@ public function update(array $event_types, string $webhook_id): void { $request_payload = []; - if ($event_types !== null) { - $request_payload["event_types"] = $event_types; - } - if ($webhook_id !== null) { - $request_payload["webhook_id"] = $webhook_id; - } + $request_payload["event_types"] = $event_types; + $request_payload["webhook_id"] = $webhook_id; - $this->seam->request( + $this->client->request( "POST", "/webhooks/update", json: (object) $request_payload, diff --git a/src/Routes/WorkspacesClient.php b/src/Routes/WorkspacesClient.php index 630f4755..42a52ec5 100644 --- a/src/Routes/WorkspacesClient.php +++ b/src/Routes/WorkspacesClient.php @@ -2,17 +2,27 @@ namespace Seam\Routes; +use Seam\Http\ResolveActionAttempt; +use Seam\Http\SeamHttpClient; use Seam\Resources\ActionAttempt; use Seam\Resources\Workspace; -use Seam\SeamClient; class WorkspacesClient { - private SeamClient $seam; + private SeamHttpClient $client; - public function __construct(SeamClient $seam) + /** + * @var array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} + */ + private array $defaults; + + /** + * @param array{wait_for_action_attempt: bool|array{timeout?: float, polling_interval?: float}} $defaults + */ + public function __construct(SeamHttpClient $client, array $defaults) { - $this->seam = $seam; + $this->client = $client; + $this->defaults = $defaults; } /** @@ -44,9 +54,7 @@ public function create( ): Workspace { $request_payload = []; - if ($name !== null) { - $request_payload["name"] = $name; - } + $request_payload["name"] = $name; if ($company_name !== null) { $request_payload["company_name"] = $company_name; } @@ -83,7 +91,7 @@ public function create( ] = $webview_success_message; } - $res = $this->seam->request( + $res = $this->client->request( "POST", "/workspaces/create", json: (object) $request_payload, @@ -99,7 +107,7 @@ public function create( */ public function get(): Workspace { - $res = $this->seam->request("POST", "/workspaces/get"); + $res = $this->client->request("POST", "/workspaces/get"); return Workspace::from_json($res->workspace); } @@ -111,7 +119,7 @@ public function get(): Workspace */ public function list(): array { - $res = $this->seam->request("POST", "/workspaces/list"); + $res = $this->client->request("POST", "/workspaces/list"); return array_map(fn($r) => Workspace::from_json($r), $res->workspaces); } @@ -119,22 +127,20 @@ public function list(): array /** * Resets the [sandbox workspace](https://docs.seam.co/core-concepts/workspaces#sandbox-workspaces) associated with the authentication value. Note that this endpoint is only available for sandbox workspaces. * + * @param bool|array|null $wait_for_action_attempt Whether to wait for the action attempt to finish, optionally with timeout and polling_interval in seconds. Defaults to the value set on the client. * @return ActionAttempt OK */ public function reset_sandbox( - bool $wait_for_action_attempt = true, + bool|array|null $wait_for_action_attempt = null, ): ActionAttempt { - $res = $this->seam->request("POST", "/workspaces/reset_sandbox"); + $res = $this->client->request("POST", "/workspaces/reset_sandbox"); - if (!$wait_for_action_attempt) { - return ActionAttempt::from_json($res->action_attempt); - } - - $action_attempt = $this->seam->action_attempts->poll_until_ready( - $res->action_attempt->action_attempt_id, + return ResolveActionAttempt::resolve_action_attempt( + ActionAttempt::from_json($res->action_attempt), + $this->client, + $wait_for_action_attempt ?? + $this->defaults["wait_for_action_attempt"], ); - - return $action_attempt; } /** @@ -181,7 +187,7 @@ public function update( $request_payload["organization_id"] = $organization_id; } - $this->seam->request( + $this->client->request( "POST", "/workspaces/update", json: (object) $request_payload, diff --git a/src/Seam.php b/src/Seam.php new file mode 100644 index 00000000..588c7617 --- /dev/null +++ b/src/Seam.php @@ -0,0 +1,256 @@ + $guzzle_options Options merged into the underlying Guzzle client, e.g. timeout or headers. + * @param int|null $retries How many times to retry a failed request. Defaults to 2; pass 0 to disable. + * @param ClientInterface|null $client A preconfigured Guzzle client. It carries its own endpoint and authorization, so no other authentication option may be given with it. + */ + public function __construct( + ?string $api_key = null, + ?string $personal_access_token = null, + ?string $workspace_id = null, + ?string $endpoint = null, + bool|array|null $wait_for_action_attempt = null, + array $guzzle_options = [], + ?int $retries = null, + ?ClientInterface $client = null, + ) { + $this->defaults = [ + "wait_for_action_attempt" => $wait_for_action_attempt ?? true, + ]; + + // A client carries its own endpoint and authorization, so the + // authentication options are only read when one has to be built. + $this->client = + $client !== null + ? SeamHttpClient::from_client($client) + : new SeamHttpClient( + Options::get_endpoint($endpoint), + Auth::get_auth_headers( + $api_key, + $personal_access_token, + $workspace_id, + ), + $guzzle_options, + $retries, + ); + + $this->access_codes = new AccessCodesClient( + $this->client, + $this->defaults, + ); + $this->access_grants = new AccessGrantsClient( + $this->client, + $this->defaults, + ); + $this->access_methods = new AccessMethodsClient( + $this->client, + $this->defaults, + ); + $this->acs = new AcsClient($this->client, $this->defaults); + $this->action_attempts = new ActionAttemptsClient( + $this->client, + $this->defaults, + ); + $this->client_sessions = new ClientSessionsClient( + $this->client, + $this->defaults, + ); + $this->connect_webviews = new ConnectWebviewsClient( + $this->client, + $this->defaults, + ); + $this->connected_accounts = new ConnectedAccountsClient( + $this->client, + $this->defaults, + ); + $this->customers = new CustomersClient($this->client, $this->defaults); + $this->devices = new DevicesClient($this->client, $this->defaults); + $this->events = new EventsClient($this->client, $this->defaults); + $this->instant_keys = new InstantKeysClient( + $this->client, + $this->defaults, + ); + $this->locks = new LocksClient($this->client, $this->defaults); + $this->noise_sensors = new NoiseSensorsClient( + $this->client, + $this->defaults, + ); + $this->phones = new PhonesClient($this->client, $this->defaults); + $this->spaces = new SpacesClient($this->client, $this->defaults); + $this->thermostats = new ThermostatsClient( + $this->client, + $this->defaults, + ); + $this->user_identities = new UserIdentitiesClient( + $this->client, + $this->defaults, + ); + $this->webhooks = new WebhooksClient($this->client, $this->defaults); + $this->workspaces = new WorkspacesClient( + $this->client, + $this->defaults, + ); + } + + /** + * Creates a client authorized with an API key. + */ + public static function from_api_key( + string $api_key, + ?string $endpoint = null, + bool|array|null $wait_for_action_attempt = null, + array $guzzle_options = [], + ?int $retries = null, + ): static { + return new static( + api_key: $api_key, + endpoint: $endpoint, + wait_for_action_attempt: $wait_for_action_attempt, + guzzle_options: $guzzle_options, + retries: $retries, + ); + } + + /** + * Creates a client authorized with a personal access token, scoped to the + * given workspace. + */ + public static function from_personal_access_token( + string $personal_access_token, + string $workspace_id, + ?string $endpoint = null, + bool|array|null $wait_for_action_attempt = null, + array $guzzle_options = [], + ?int $retries = null, + ): static { + return new static( + personal_access_token: $personal_access_token, + workspace_id: $workspace_id, + endpoint: $endpoint, + wait_for_action_attempt: $wait_for_action_attempt, + retries: $retries, + guzzle_options: $guzzle_options, + ); + } + + /** + * Creates a client from a preconfigured Guzzle client. + */ + public static function from_client( + ClientInterface $client, + bool|array|null $wait_for_action_attempt = null, + ): static { + return new static( + client: $client, + wait_for_action_attempt: $wait_for_action_attempt, + ); + } + + public function lts_version(): string + { + return self::LTS_VERSION; + } + + /** + * Makes a request against the Seam API with this client's authorization. + * + * @param array|object|null $json + * @param array|null $query + */ + public function request( + string $method, + string $path, + mixed $json = null, + ?array $query = null, + ): mixed { + return $this->client->request($method, $path, $json, $query); + } + + /** + * Creates a paginator for a list endpoint. + * + * @param callable $request Invokes the list method with a params array, e.g. fn($params) => $seam->devices->list(...$params) + * @param array $params + */ + public function createPaginator( + callable $request, + array $params = [], + ): Paginator { + return new Paginator($request, $params); + } +} diff --git a/src/SeamClient.php b/src/SeamClient.php index f2384ab2..c43d7e1b 100644 --- a/src/SeamClient.php +++ b/src/SeamClient.php @@ -2,154 +2,8 @@ namespace Seam; -use Seam\Routes\AccessCodesClient; -use Seam\Routes\AccessGrantsClient; -use Seam\Routes\AccessMethodsClient; -use Seam\Routes\AcsClient; -use Seam\Routes\ActionAttemptsClient; -use Seam\Routes\ClientSessionsClient; -use Seam\Routes\ConnectedAccountsClient; -use Seam\Routes\ConnectWebviewsClient; -use Seam\Routes\CustomersClient; -use Seam\Routes\DevicesClient; -use Seam\Routes\EventsClient; -use Seam\Routes\InstantKeysClient; -use Seam\Routes\LocksClient; -use Seam\Routes\NoiseSensorsClient; -use Seam\Routes\PhonesClient; -use Seam\Routes\SpacesClient; -use Seam\Routes\ThermostatsClient; -use Seam\Routes\UserIdentitiesClient; -use Seam\Routes\WebhooksClient; -use Seam\Routes\WorkspacesClient; -use Seam\Utils\PackageVersion; - -use GuzzleHttp\Client as HTTPClient; -use \Exception as Exception; -use Seam\HttpApiError; -use Seam\HttpUnauthorizedError; -use Seam\HttpInvalidInputError; - -define("LTS_VERSION", "1.0.0"); - -class SeamClient -{ - public AccessCodesClient $access_codes; - public AccessGrantsClient $access_grants; - public AccessMethodsClient $access_methods; - public AcsClient $acs; - public ActionAttemptsClient $action_attempts; - public ClientSessionsClient $client_sessions; - public ConnectWebviewsClient $connect_webviews; - public ConnectedAccountsClient $connected_accounts; - public CustomersClient $customers; - public DevicesClient $devices; - public EventsClient $events; - public InstantKeysClient $instant_keys; - public LocksClient $locks; - public NoiseSensorsClient $noise_sensors; - public PhonesClient $phones; - public SpacesClient $spaces; - public ThermostatsClient $thermostats; - public UserIdentitiesClient $user_identities; - public WebhooksClient $webhooks; - public WorkspacesClient $workspaces; - - public string $api_key; - public HTTPClient $client; - public string $ltsVersion = LTS_VERSION; - - public function __construct( - $api_key = null, - $endpoint = "https://connect.getseam.com", - $throw_http_errors = false, - ) { - $this->api_key = $api_key ?: (getenv("SEAM_API_KEY") ?: null); - $seam_sdk_version = PackageVersion::get(); - $this->client = new HTTPClient([ - "base_uri" => $endpoint, - "timeout" => 60.0, - "headers" => [ - "Authorization" => "Bearer " . $this->api_key, - "User-Agent" => "Seam PHP Client " . $seam_sdk_version, - "seam-sdk-name" => "seamapi/php", - "seam-sdk-version" => $seam_sdk_version, - "seam-lts-version" => $this->ltsVersion, - ], - "http_errors" => $throw_http_errors, - ]); - $this->access_codes = new AccessCodesClient($this); - $this->access_grants = new AccessGrantsClient($this); - $this->access_methods = new AccessMethodsClient($this); - $this->acs = new AcsClient($this); - $this->action_attempts = new ActionAttemptsClient($this); - $this->client_sessions = new ClientSessionsClient($this); - $this->connect_webviews = new ConnectWebviewsClient($this); - $this->connected_accounts = new ConnectedAccountsClient($this); - $this->customers = new CustomersClient($this); - $this->devices = new DevicesClient($this); - $this->events = new EventsClient($this); - $this->instant_keys = new InstantKeysClient($this); - $this->locks = new LocksClient($this); - $this->noise_sensors = new NoiseSensorsClient($this); - $this->phones = new PhonesClient($this); - $this->spaces = new SpacesClient($this); - $this->thermostats = new ThermostatsClient($this); - $this->user_identities = new UserIdentitiesClient($this); - $this->webhooks = new WebhooksClient($this); - $this->workspaces = new WorkspacesClient($this); - } - - public function request($method, $path, $json = null, $query = null) - { - $options = [ - "json" => $json, - "query" => $query, - ]; - $options = array_filter($options, fn($option) => $option !== null); - - $response = $this->client->request($method, $path, $options); - $status_code = $response->getStatusCode(); - $request_id = $response->getHeaderLine("seam-request-id"); - - $res_json = null; - try { - $res_json = json_decode($response->getBody()); - } catch (Exception $ignoreError) { - } - - if ($status_code >= 400) { - if ($status_code === 401) { - throw new HttpUnauthorizedError($request_id); - } - - if (($res_json->error ?? null) != null) { - if ($res_json->error->type === "invalid_input") { - throw new HttpInvalidInputError( - $res_json->error, - $status_code, - $request_id, - ); - } - - throw new HttpApiError( - $res_json->error, - $status_code, - $request_id, - ); - } - - throw \GuzzleHttp\Exception\RequestException::create( - new \GuzzleHttp\Psr7\Request($method, $path), - $response, - ); - } - - return $res_json; - } - - public function createPaginator($request, $params = []) - { - return new Paginator($request, $params); - } -} +/** + * @deprecated Use Seam\Seam instead. This alias will be removed in a future + * major version. + */ +class SeamClient extends Seam {} diff --git a/src/SeamException.php b/src/SeamException.php new file mode 100644 index 00000000..fc73fdcd --- /dev/null +++ b/src/SeamException.php @@ -0,0 +1,9 @@ + $guzzle_options + */ + public function __construct( + ?string $personal_access_token = null, + ?string $endpoint = null, + array $guzzle_options = [], + ?int $retries = null, + ?ClientInterface $client = null, + ) { + // A client carries its own endpoint and authorization, so the + // authentication options are only read when one has to be built. + if ($client !== null) { + $this->client = SeamHttpClient::from_client($client); + } else { + if ($personal_access_token === null) { + throw new InvalidOptionsError( + "Must specify a personal_access_token", + ); + } + + $this->client = new SeamHttpClient( + Options::get_endpoint($endpoint), + Auth::get_auth_headers_for_multi_workspace_personal_access_token( + $personal_access_token, + ), + $guzzle_options, + $retries, + ); + } + + $this->workspaces = new WorkspacesProxy( + new WorkspacesClient($this->client, [ + "wait_for_action_attempt" => false, + ]), + ); + } + + /** + * Creates a client authorized with a personal access token, not scoped to + * any workspace. + */ + public static function from_personal_access_token( + string $personal_access_token, + ?string $endpoint = null, + array $guzzle_options = [], + ?int $retries = null, + ): static { + return new static( + personal_access_token: $personal_access_token, + endpoint: $endpoint, + guzzle_options: $guzzle_options, + retries: $retries, + ); + } + + /** + * Creates a client from a preconfigured Guzzle client. + */ + public static function from_client(ClientInterface $client): static + { + return new static(client: $client); + } + + public function lts_version(): string + { + return self::LTS_VERSION; + } +} diff --git a/src/SeamWebhook.php b/src/SeamWebhook.php new file mode 100644 index 00000000..9ab920af --- /dev/null +++ b/src/SeamWebhook.php @@ -0,0 +1,53 @@ +webhook = new Webhook($secret); + } + + /** + * Verifies an incoming webhook request and returns the event it carries. + * + * @param string $payload The raw HTTP request body. + * @param array $headers The HTTP request headers. + * + * @throws \Svix\Exception\WebhookVerificationException When the signature does not match. + */ + public function verify(string $payload, array $headers): Event + { + $normalized_headers = []; + foreach ($headers as $name => $value) { + $normalized_headers[strtolower($name)] = $value; + } + + $this->webhook->verify($payload, $normalized_headers); + + $event = Event::from_json(json_decode($payload)); + + if ($event === null) { + throw new WebhookVerificationException( + "The verified webhook payload did not contain an event", + ); + } + + return $event; + } +} diff --git a/src/Token.php b/src/Token.php new file mode 100644 index 00000000..90349502 --- /dev/null +++ b/src/Token.php @@ -0,0 +1,67 @@ +workspaces = $workspaces; + } + + /** + * @return Workspace[] + */ + public function list(): array + { + return $this->workspaces->list(); + } + + public function create( + string $name, + ?string $company_name = null, + ?string $connect_partner_name = null, + mixed $connect_webview_customization = null, + ?bool $is_sandbox = null, + ?string $organization_id = null, + ?string $webview_logo_shape = null, + ?string $webview_primary_button_color = null, + ?string $webview_primary_button_text_color = null, + ?string $webview_success_message = null, + ): Workspace { + return $this->workspaces->create( + $name, + $company_name, + $connect_partner_name, + $connect_webview_customization, + $is_sandbox, + $organization_id, + $webview_logo_shape, + $webview_primary_button_color, + $webview_primary_button_text_color, + $webview_success_message, + ); + } +} diff --git a/tests/ApiKeyTest.php b/tests/ApiKeyTest.php new file mode 100644 index 00000000..838fa24a --- /dev/null +++ b/tests/ApiKeyTest.php @@ -0,0 +1,89 @@ +seed["seam_apikey1_token"], + endpoint: $this->endpoint, + ); + + $device = $seam->devices->get($this->seed["august_device_1"]); + + $this->assertSame($this->seed["august_device_1"], $device->device_id); + $this->assertSame( + $this->seed["seed_workspace_1"], + $device->workspace_id, + ); + } + + public function testConstructorReturnsAnAuthorizedClient(): void + { + $seam = new Seam( + api_key: $this->seed["seam_apikey1_token"], + endpoint: $this->endpoint, + ); + + $device = $seam->devices->get($this->seed["august_device_1"]); + + $this->assertSame($this->seed["august_device_1"], $device->device_id); + } + + public function testInvalidApiKeyIsRejectedByTheServer(): void + { + $seam = new Seam( + api_key: "seam_invalid_api_key", + endpoint: $this->endpoint, + ); + + $this->expectException(HttpUnauthorizedError::class); + + $seam->devices->list(); + } + + /** + * @dataProvider unusableTokens + */ + public function testApiKeyFormatIsChecked( + string $token, + string $expected_message, + ): void { + $this->expectException(InvalidTokenError::class); + $this->expectExceptionMessage($expected_message); + + new Seam(api_key: $token, endpoint: $this->endpoint); + } + + public static function unusableTokens(): array + { + return [ + "client session token" => [ + "seam_cst_1234", + "A Client Session Token cannot be used as an api_key", + ], + "jwt" => ["ey_some_jwt", "A JWT cannot be used as an api_key"], + "access token" => [ + "seam_at_1234", + "An Access Token cannot be used as an api_key", + ], + "publishable key" => [ + "seam_pk_1234", + "A Publishable Key cannot be used as an api_key", + ], + "unknown format" => [ + "some-random-token", + "Unknown or invalid api_key format", + ], + ]; + } +} diff --git a/tests/ClientTest.php b/tests/ClientTest.php new file mode 100644 index 00000000..739c263b --- /dev/null +++ b/tests/ClientTest.php @@ -0,0 +1,168 @@ +seam(); + + $res = $seam->client->request( + "POST", + "/devices/get", + (object) [ + "device_id" => $this->seed["august_device_1"], + ], + ); + + $this->assertSame( + $this->seed["august_device_1"], + $res->device->device_id, + ); + $this->assertSame( + $this->seed["seed_workspace_1"], + $res->device->workspace_id, + ); + } + + public function testRequestDelegatesToTheClient(): void + { + $seam = $this->seam(); + + $res = $seam->request( + "POST", + "/devices/get", + (object) [ + "device_id" => $this->seed["august_device_1"], + ], + ); + + $this->assertSame( + $this->seed["august_device_1"], + $res->device->device_id, + ); + } + + /** + * A client carries its own endpoint and authorization, so it has to work + * on its own without any authentication option beside it. + */ + public function testFromClientNeedsNoCredentials(): void + { + $authorized = $this->seam(); + + $seam = Seam::from_client($authorized->client->get_client()); + + $device = $seam->devices->get($this->seed["august_device_1"]); + + $this->assertSame($this->seed["august_device_1"], $device->device_id); + $this->assertSame( + $this->seed["seed_workspace_1"], + $device->workspace_id, + ); + } + + public function testClientOptionReusesAnotherInstancesClient(): void + { + $seam = new Seam(client: $this->seam()->client->get_client()); + + $device = $seam->devices->get($this->seed["august_device_1"]); + + $this->assertSame($this->seed["august_device_1"], $device->device_id); + } + + public function testGuzzleOptionsAreMergedIntoTheClient(): void + { + $seam = $this->seam( + guzzle_options: [ + "headers" => ["Custom-Header" => "Test-Value"], + "timeout" => 30, + ], + ); + + $config = $seam->client->get_client()->getConfig(); + + $this->assertSame(30, $config["timeout"]); + $this->assertSame("Test-Value", $config["headers"]["Custom-Header"]); + // The custom headers must not displace the authorization or the SDK + // headers. + $this->assertSame( + "Bearer " . $this->seed["seam_apikey1_token"], + $config["headers"]["authorization"], + ); + $this->assertSame("seamapi/php", $config["headers"]["seam-sdk-name"]); + } + + public function testGuzzleOptionsStillAuthorizeRequests(): void + { + $seam = $this->seam( + guzzle_options: ["headers" => ["Custom-Header" => "Test-Value"]], + ); + + $device = $seam->devices->get($this->seed["august_device_1"]); + + $this->assertSame($this->seed["august_device_1"], $device->device_id); + } + + public function testTimeoutDefaultsToSixtySeconds(): void + { + $config = $this->seam()->client->get_client()->getConfig(); + + $this->assertSame(SeamHttpClient::DEFAULT_TIMEOUT, $config["timeout"]); + } + + /** + * Every constructor and factory has to agree on the default, otherwise + * the same call waits or does not depending on how the client was built. + */ + public function testWaitForActionAttemptDefaultsToTrueEverywhere(): void + { + $api_key = $this->seed["seam_apikey1_token"]; + + $clients = [ + "constructor" => new Seam( + api_key: $api_key, + endpoint: $this->endpoint, + ), + "from_api_key" => Seam::from_api_key( + $api_key, + endpoint: $this->endpoint, + ), + "from_personal_access_token" => Seam::from_personal_access_token( + $this->seed["seam_at1_token"], + $this->seed["seed_workspace_1"], + endpoint: $this->endpoint, + ), + "from_client" => Seam::from_client( + $this->seam()->client->get_client(), + ), + ]; + + foreach ($clients as $name => $seam) { + $this->assertTrue( + $seam->defaults["wait_for_action_attempt"], + "{$name} should wait for action attempts by default", + ); + } + } + + public function testWaitForActionAttemptDefaultCanBeDisabled(): void + { + $seam = $this->seam(wait_for_action_attempt: false); + + $this->assertFalse($seam->defaults["wait_for_action_attempt"]); + } + + public function testLtsVersionIsExposed(): void + { + $this->assertSame("1.0.0", Seam::LTS_VERSION); + $this->assertSame("1.0.0", $this->seam()->lts_version()); + } +} diff --git a/tests/EnvTest.php b/tests/EnvTest.php new file mode 100644 index 00000000..3c0f92a7 --- /dev/null +++ b/tests/EnvTest.php @@ -0,0 +1,143 @@ + */ + private array $saved_env = []; + + protected function setUp(): void + { + parent::setUp(); + + foreach (self::VARIABLES as $name) { + $this->saved_env[$name] = getenv($name); + putenv($name); + } + } + + protected function tearDown(): void + { + foreach ($this->saved_env as $name => $value) { + if ($value === false) { + putenv($name); + } else { + putenv("{$name}={$value}"); + } + } + + parent::tearDown(); + } + + public function testReadsTheApiKeyFromTheEnvironment(): void + { + putenv("SEAM_API_KEY=" . $this->seed["seam_apikey1_token"]); + + $seam = new Seam(endpoint: $this->endpoint); + + $device = $seam->devices->get($this->seed["august_device_1"]); + + $this->assertSame($this->seed["august_device_1"], $device->device_id); + } + + public function testReadsTheEndpointFromTheEnvironment(): void + { + putenv("SEAM_ENDPOINT=" . $this->endpoint); + + $seam = new Seam(api_key: $this->seed["seam_apikey1_token"]); + + $device = $seam->devices->get($this->seed["august_device_1"]); + + $this->assertSame($this->seed["august_device_1"], $device->device_id); + } + + public function testFallsBackToTheDefaultEndpoint(): void + { + $this->assertSame(Options::DEFAULT_ENDPOINT, Options::get_endpoint()); + } + + public function testEndpointOptionWinsOverTheEnvironment(): void + { + putenv("SEAM_ENDPOINT=https://from-the-environment.example.com"); + + $this->assertSame( + "https://from-the-option.example.com", + Options::get_endpoint("https://from-the-option.example.com"), + ); + } + + public function testSeamEndpointWinsOverTheDeprecatedSeamApiUrl(): void + { + putenv("SEAM_ENDPOINT=https://endpoint.example.com"); + putenv("SEAM_API_URL=https://api-url.example.com"); + + // Both the deprecation and the precedence notice are raised. + $endpoint = @Options::get_endpoint(); + + $this->assertSame("https://endpoint.example.com", $endpoint); + } + + public function testDeprecatedSeamApiUrlIsStillHonored(): void + { + putenv("SEAM_API_URL=https://api-url.example.com"); + + $this->assertSame( + "https://api-url.example.com", + @Options::get_endpoint(), + ); + } + + public function testDeprecatedSeamApiUrlWarns(): void + { + putenv("SEAM_API_URL=https://api-url.example.com"); + + set_error_handler( + static fn( + int $severity, + string $message, + ) => throw new \ErrorException($message), + E_USER_WARNING, + ); + + try { + $this->expectException(\ErrorException::class); + $this->expectExceptionMessage("SEAM_API_URL"); + Options::get_endpoint(); + } finally { + restore_error_handler(); + } + } + + public function testFailsWhenNoCredentialsAreAvailable(): void + { + $this->expectException(InvalidOptionsError::class); + $this->expectExceptionMessage("SEAM_API_KEY is not set"); + + new Seam(endpoint: $this->endpoint); + } + + public function testApiKeyEnvironmentVariableIsIgnoredForAPersonalAccessToken(): void + { + putenv("SEAM_API_KEY=" . $this->seed["seam_apikey1_token"]); + + $seam = new Seam( + personal_access_token: $this->seed["seam_at1_token"], + workspace_id: $this->seed["seed_workspace_1"], + endpoint: $this->endpoint, + ); + + $device = $seam->devices->get($this->seed["august_device_1"]); + + $this->assertSame($this->seed["august_device_1"], $device->device_id); + } +} diff --git a/tests/HeadersTest.php b/tests/HeadersTest.php new file mode 100644 index 00000000..3f8cf4c7 --- /dev/null +++ b/tests/HeadersTest.php @@ -0,0 +1,141 @@ + ["device_id" => "d1"]]), + ]); + + $seam = Seam::from_api_key( + "seam_apikey_token", + endpoint: "https://example.com", + guzzle_options: $recorder->guzzle_options(), + ); + + $device = $seam->devices->get("d1"); + + $this->assertSame("d1", $device->device_id); + + $request = $recorder->request(); + + $this->assertSame("/devices/get", $request->getUri()->getPath()); + $this->assertEquals((object) ["device_id" => "d1"], $recorder->body()); + + $this->assertSame( + "Bearer seam_apikey_token", + $request->getHeaderLine("authorization"), + ); + $this->assertSame( + "seamapi/php", + $request->getHeaderLine("seam-sdk-name"), + ); + $this->assertSame( + PackageVersion::get(), + $request->getHeaderLine("seam-sdk-version"), + ); + $this->assertSame( + SeamHttpClient::LTS_VERSION, + $request->getHeaderLine("seam-lts-version"), + ); + $this->assertSame( + "seam-php/" . PackageVersion::get(), + $request->getHeaderLine("User-Agent"), + ); + } + + public function testSendsWorkspaceHeaderWithAPersonalAccessToken(): void + { + $recorder = new RecordingClient([ + RecordingClient::json(200, ["device" => ["device_id" => "d1"]]), + ]); + + $seam = Seam::from_personal_access_token( + "seam_at_token", + "workspace-1", + endpoint: "https://example.com", + guzzle_options: $recorder->guzzle_options(), + ); + + $seam->devices->get("d1"); + + $request = $recorder->request(); + + $this->assertSame( + "Bearer seam_at_token", + $request->getHeaderLine("authorization"), + ); + $this->assertSame( + "workspace-1", + $request->getHeaderLine("seam-workspace"), + ); + } + + public function testCustomHeadersAreSentAlongsideTheSdkHeaders(): void + { + $recorder = new RecordingClient([ + RecordingClient::json(200, ["device" => ["device_id" => "d1"]]), + ]); + + $seam = Seam::from_api_key( + "seam_apikey_token", + endpoint: "https://example.com", + guzzle_options: array_merge($recorder->guzzle_options(), [ + "headers" => ["Custom-Header" => "Test-Value"], + ]), + ); + + $seam->devices->get("d1"); + + $request = $recorder->request(); + + $this->assertSame( + "Test-Value", + $request->getHeaderLine("Custom-Header"), + ); + $this->assertSame( + "seamapi/php", + $request->getHeaderLine("seam-sdk-name"), + ); + } + + /** + * The SDK headers identify the SDK, so a caller cannot displace them. + */ + public function testSdkHeadersCannotBeOverridden(): void + { + $recorder = new RecordingClient([ + RecordingClient::json(200, ["device" => ["device_id" => "d1"]]), + ]); + + $seam = Seam::from_api_key( + "seam_apikey_token", + endpoint: "https://example.com", + guzzle_options: array_merge($recorder->guzzle_options(), [ + "headers" => ["seam-sdk-name" => "not-the-sdk"], + ]), + ); + + $seam->devices->get("d1"); + + $this->assertSame( + "seamapi/php", + $recorder->request()->getHeaderLine("seam-sdk-name"), + ); + } +} diff --git a/tests/HttpErrorTest.php b/tests/HttpErrorTest.php index 648eb882..d445defc 100644 --- a/tests/HttpErrorTest.php +++ b/tests/HttpErrorTest.php @@ -2,29 +2,118 @@ declare(strict_types=1); -use PHPUnit\Framework\TestCase; +namespace Tests; -final class HttpErrorTest extends TestCase +use Seam\HttpApiError; +use Seam\HttpInvalidInputError; +use Seam\HttpUnauthorizedError; +use Seam\Seam; +use Tests\Support\FakeSeamConnectTestCase; + +final class HttpErrorTest extends FakeSeamConnectTestCase { - public function testNonSeamError(): void + public function testThrowsUnauthorizedError(): void { - $seam = new \Seam\SeamClient( - "seam_apikey1_token", - "https://nonexistent.example.com", + $seam = new Seam( + api_key: "seam_invalid_api_key", + endpoint: $this->endpoint, ); try { $seam->devices->list(); - $this->fail("Expected GuzzleHttp ConnectException"); - } catch (\GuzzleHttp\Exception\ConnectException $e) { - $this->assertInstanceOf( - \GuzzleHttp\Exception\ConnectException::class, - $e, + $this->fail("Expected HttpUnauthorizedError"); + } catch (HttpUnauthorizedError $error) { + $this->assertSame(401, $error->getStatusCode()); + $this->assertSame("unauthorized", $error->getErrorCode()); + $this->assertStringStartsWith( + "request", + (string) $error->getRequestId(), + ); + } + } + + public function testThrowsApiErrorOnStandardErrorResponse(): void + { + try { + $this->seam()->devices->get("unknown-device"); + $this->fail("Expected HttpApiError"); + } catch (HttpApiError $error) { + $this->assertSame(404, $error->getStatusCode()); + $this->assertSame("device_not_found", $error->getErrorCode()); + $this->assertStringStartsWith( + "request", + (string) $error->getRequestId(), + ); + } + } + + public function testThrowsInvalidInputErrorWithValidationMessages(): void + { + try { + $this->seam()->client->request( + "POST", + "/devices/list", + (object) [ + "device_ids" => 4242, + ], + ); + $this->fail("Expected HttpInvalidInputError"); + } catch (HttpInvalidInputError $error) { + $this->assertSame(400, $error->getStatusCode()); + $this->assertSame("invalid_input", $error->getErrorCode()); + $this->assertStringStartsWith( + "request", + (string) $error->getRequestId(), + ); + $this->assertSame( + ["Expected array, received number"], + $error->getValidationErrorMessages("device_ids"), ); - $this->assertStringContainsString( - "Could not resolve host", - $e->getMessage(), + } + } + + public function testValidationMessagesAreEmptyForAnUnknownParam(): void + { + try { + $this->seam()->client->request( + "POST", + "/devices/list", + (object) [ + "device_ids" => 4242, + ], + ); + $this->fail("Expected HttpInvalidInputError"); + } catch (HttpInvalidInputError $error) { + $this->assertSame( + [], + $error->getValidationErrorMessages("non_existent_param"), ); } } + + /** + * A workspace outage answers with a 503 that is not a Seam error + * envelope, so it surfaces as the underlying transport error rather than + * a Seam exception, which is what the other Seam SDKs do too. + */ + public function testWorkspaceOutageSurfacesTheTransportError(): void + { + $seam = $this->seam(retries: 0); + + $seam->client->request( + "POST", + "/_fake/simulate_workspace_outage", + (object) [ + "workspace_id" => $this->seed["seed_workspace_1"], + "routes" => ["/devices/list"], + ], + ); + + try { + $seam->devices->list(); + $this->fail("Expected a Guzzle BadResponseException"); + } catch (\GuzzleHttp\Exception\BadResponseException $error) { + $this->assertSame(503, $error->getResponse()->getStatusCode()); + } + } } diff --git a/tests/MalformedResponseTest.php b/tests/MalformedResponseTest.php new file mode 100644 index 00000000..e132c10a --- /dev/null +++ b/tests/MalformedResponseTest.php @@ -0,0 +1,112 @@ +guzzle_options(), + retries: 0, + ); + } + + /** + * @dataProvider nonSeamErrorResponses + */ + public function testNonSeamErrorResponsesSurfaceTheTransportError( + Response $response, + ): void { + $seam = $this->seam(new RecordingClient([$response])); + + try { + $seam->devices->list(); + $this->fail("Expected a Guzzle BadResponseException"); + } catch (HttpApiError $error) { + $this->fail( + "Expected a transport error, got " . + $error::class . + ": " . + $error->getMessage(), + ); + } catch (BadResponseException $error) { + $this->assertSame(500, $error->getResponse()->getStatusCode()); + } + } + + public static function nonSeamErrorResponses(): array + { + return [ + "plain text body" => [ + RecordingClient::raw(500, "Internal Server Error"), + ], + "html body" => [ + RecordingClient::raw( + 500, + "Gateway", + "text/html", + ), + ], + "malformed json" => [ + RecordingClient::raw(500, "{invalid json", "application/json"), + ], + "json without an error object" => [ + RecordingClient::json(500, ["message" => "Some error"]), + ], + "error without a type and message" => [ + RecordingClient::json(500, ["error" => ["code" => 500]]), + ], + "json that is not an object" => [ + RecordingClient::json(500, [1, 2]), + ], + "error that is not an object" => [ + RecordingClient::json(500, ["error" => "boom"]), + ], + "empty body" => [RecordingClient::raw(500, "", "application/json")], + ]; + } + + /** + * A redirect is not a success, so it must not be handed back to the + * caller as though it were a resource. + */ + public function testRedirectIsNotTreatedAsSuccess(): void + { + $recorder = new RecordingClient([ + new Response(302, ["location" => "https://example.com/elsewhere"]), + ]); + + $seam = Seam::from_api_key( + "seam_apikey_token", + endpoint: "https://example.com", + guzzle_options: array_merge($recorder->guzzle_options(), [ + "allow_redirects" => false, + ]), + retries: 0, + ); + + // A 3xx is not a Seam error envelope either, so it surfaces as the + // transport error rather than being handed back as a resource. + $this->expectException(RequestException::class); + + $seam->devices->list(); + } +} diff --git a/tests/PackageVersionTest.php b/tests/PackageVersionTest.php index d45e970d..7b1d7f9a 100644 --- a/tests/PackageVersionTest.php +++ b/tests/PackageVersionTest.php @@ -28,8 +28,8 @@ public function testVersionMatchesPackageJson(): void public function testVersionIsUsedAsTheSdkVersionHeader(): void { - $seam = new \Seam\SeamClient("seam_apikey1_token"); - $headers = $seam->client->getConfig("headers"); + $seam = new \Seam\Seam("seam_apikey1_token"); + $headers = $seam->client->get_client()->getConfig("headers"); $this->assertSame(PackageVersion::get(), $headers["seam-sdk-version"]); } diff --git a/tests/PaginatorTest.php b/tests/PaginatorTest.php new file mode 100644 index 00000000..8442318a --- /dev/null +++ b/tests/PaginatorTest.php @@ -0,0 +1,123 @@ + 2]): Paginator + { + $seam = $this->seam(); + + return $seam->createPaginator( + fn($p) => $seam->connected_accounts->list(...$p), + $params, + ); + } + + public function testCreatePaginatorReturnsAPaginator(): void + { + $this->assertInstanceOf(Paginator::class, $this->paginator()); + } + + public function testFirstPageReturnsTheFirstPage(): void + { + [$accounts, $pagination] = $this->paginator()->firstPage(); + + $this->assertCount(2, $accounts); + $this->assertInstanceOf(Pagination::class, $pagination); + $this->assertTrue($pagination->has_next_page); + $this->assertNotNull($pagination->next_page_cursor); + } + + public function testNextPageReturnsTheNextPage(): void + { + $pages = $this->paginator(); + + [$first, $pagination] = $pages->firstPage(); + [$second] = $pages->nextPage($pagination->next_page_cursor); + + $this->assertNotEmpty($second); + + $first_ids = array_map( + fn($account) => $account->connected_account_id, + $first, + ); + $second_ids = array_map( + fn($account) => $account->connected_account_id, + $second, + ); + + $this->assertEmpty(array_intersect($first_ids, $second_ids)); + } + + public function testNextPageRequiresACursor(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("next_page_cursor"); + + $this->paginator()->nextPage(null); + } + + public function testNextPageRejectsAnEmptyCursor(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->paginator()->nextPage(""); + } + + public function testLastPageHasNoNextPage(): void + { + $pages = $this->paginator(["limit" => 100]); + + [, $pagination] = $pages->firstPage(); + + $this->assertFalse($pagination->has_next_page); + $this->assertNull($pagination->next_page_cursor); + } + + public function testFlattenToArrayReturnsEveryResource(): void + { + $all = $this->paginator()->flattenToArray(); + $expected = $this->seam()->connected_accounts->list(); + + $this->assertCount(count($expected), $all); + } + + public function testFlattenIteratesEveryResource(): void + { + $ids = []; + + foreach ($this->paginator()->flatten() as $account) { + $ids[] = $account->connected_account_id; + } + + $expected = $this->seam()->connected_accounts->list(); + + $this->assertCount(count($expected), $ids); + $this->assertSame(array_unique($ids), $ids); + } + + /** + * Not every list endpoint returns pagination metadata, and asking for a + * page of one that does not should not fail. + */ + public function testEndpointWithoutPaginationYieldsAnEmptyPagination(): void + { + $seam = $this->seam(); + + $pages = $seam->createPaginator(fn($p) => $seam->workspaces->list()); + + [$workspaces, $pagination] = $pages->firstPage(); + + $this->assertNotEmpty($workspaces); + $this->assertInstanceOf(Pagination::class, $pagination); + $this->assertFalse($pagination->has_next_page); + $this->assertNull($pagination->next_page_cursor); + } +} diff --git a/tests/PersonalAccessTokenTest.php b/tests/PersonalAccessTokenTest.php new file mode 100644 index 00000000..bce47735 --- /dev/null +++ b/tests/PersonalAccessTokenTest.php @@ -0,0 +1,140 @@ +seed["seam_at1_token"], + $this->seed["seed_workspace_1"], + endpoint: $this->endpoint, + ); + + $device = $seam->devices->get($this->seed["august_device_1"]); + + $this->assertSame($this->seed["august_device_1"], $device->device_id); + $this->assertSame( + $this->seed["seed_workspace_1"], + $device->workspace_id, + ); + } + + public function testConstructorReturnsAnAuthorizedClient(): void + { + $seam = new Seam( + personal_access_token: $this->seed["seam_at1_token"], + workspace_id: $this->seed["seed_workspace_1"], + endpoint: $this->endpoint, + ); + + $device = $seam->devices->get($this->seed["august_device_1"]); + + $this->assertSame($this->seed["august_device_1"], $device->device_id); + } + + public function testWorkspaceIdIsRequired(): void + { + $this->expectException(InvalidOptionsError::class); + $this->expectExceptionMessage( + "Must pass a workspace_id when using a personal_access_token", + ); + + new Seam( + personal_access_token: $this->seed["seam_at1_token"], + endpoint: $this->endpoint, + ); + } + + public function testApiKeyCannotBeCombinedWithAPersonalAccessToken(): void + { + $this->expectException(InvalidOptionsError::class); + + new Seam( + api_key: $this->seed["seam_apikey1_token"], + personal_access_token: $this->seed["seam_at1_token"], + workspace_id: $this->seed["seed_workspace_1"], + endpoint: $this->endpoint, + ); + } + + public function testPersonalAccessTokenFormatIsChecked(): void + { + $this->expectException(InvalidTokenError::class); + + new Seam( + personal_access_token: "seam_cst_1234", + workspace_id: $this->seed["seed_workspace_1"], + endpoint: $this->endpoint, + ); + } + + public function testMultiWorkspaceClientListsWorkspaces(): void + { + $seam = SeamMultiWorkspace::from_personal_access_token( + $this->seed["seam_at1_token"], + endpoint: $this->endpoint, + ); + + $workspaces = $seam->workspaces->list(); + + $workspace_ids = array_map( + fn($workspace) => $workspace->workspace_id, + $workspaces, + ); + + $this->assertContains($this->seed["seed_workspace_1"], $workspace_ids); + } + + public function testMultiWorkspaceConstructorListsWorkspaces(): void + { + $seam = new SeamMultiWorkspace( + personal_access_token: $this->seed["seam_at1_token"], + endpoint: $this->endpoint, + ); + + $this->assertNotEmpty($seam->workspaces->list()); + } + + public function testMultiWorkspaceClientCreatesAWorkspace(): void + { + $seam = SeamMultiWorkspace::from_personal_access_token( + $this->seed["seam_at1_token"], + endpoint: $this->endpoint, + ); + + $workspace = $seam->workspaces->create( + name: "Test Workspace", + connect_partner_name: "Test Partner", + is_sandbox: true, + ); + + $this->assertSame("Test Workspace", $workspace->name); + } + + public function testMultiWorkspaceClientRequiresAToken(): void + { + $this->expectException(InvalidOptionsError::class); + + new SeamMultiWorkspace(endpoint: $this->endpoint); + } + + public function testMultiWorkspaceClientChecksTheTokenFormat(): void + { + $this->expectException(InvalidTokenError::class); + + SeamMultiWorkspace::from_personal_access_token( + $this->seed["seam_apikey1_token"], + endpoint: $this->endpoint, + ); + } +} diff --git a/tests/RetryTest.php b/tests/RetryTest.php new file mode 100644 index 00000000..322697e7 --- /dev/null +++ b/tests/RetryTest.php @@ -0,0 +1,139 @@ + ["type" => "service_unavailable", "message" => "Down"], + ]); + } + + private static function devices(): \GuzzleHttp\Psr7\Response + { + return RecordingClient::json(200, ["devices" => []]); + } + + private function seam(RecordingClient $recorder, ?int $retries = null): Seam + { + return new Seam( + api_key: self::API_KEY, + endpoint: "https://example.com", + guzzle_options: $recorder->guzzle_options(), + retries: $retries, + ); + } + + /** + * Every Seam endpoint is a POST. Retrying one the server may already have + * processed could duplicate a write, so a status code never triggers a + * retry. The Ruby SDK asserts the same thing. + */ + public function testDoesNotRetryPostOnServiceUnavailable(): void + { + $recorder = RecordingClient::repeating(self::service_unavailable()); + + try { + $this->seam($recorder)->devices->list(); + $this->fail("Expected the 503 to surface"); + } catch (\Throwable) { + // The error mapping is covered in HttpErrorTest. + } + + $this->assertSame(1, $recorder->attempt_count()); + } + + /** + * A connection failure never reached the server, so retrying it cannot + * duplicate anything. + */ + public function testRetriesPostOnConnectionFailure(): void + { + $connect_error = new ConnectException( + "Could not resolve host", + new Request("POST", "/devices/list"), + ); + + $recorder = new RecordingClient([ + $connect_error, + $connect_error, + self::devices(), + ]); + + $devices = $this->seam($recorder)->devices->list(); + + $this->assertSame([], $devices); + $this->assertSame(3, $recorder->attempt_count()); + } + + public function testStopsRetryingOnceRetriesAreExhausted(): void + { + $connect_error = new ConnectException( + "Could not resolve host", + new Request("POST", "/devices/list"), + ); + + $recorder = RecordingClient::repeating_throwable($connect_error); + + $this->expectException(ConnectException::class); + + try { + $this->seam($recorder, retries: 1)->devices->list(); + } finally { + $this->assertSame(2, $recorder->attempt_count()); + } + } + + public function testDoesNotRetryWhenRetriesAreDisabled(): void + { + $connect_error = new ConnectException( + "Could not resolve host", + new Request("POST", "/devices/list"), + ); + + $recorder = RecordingClient::repeating_throwable($connect_error); + + try { + $this->seam($recorder, retries: 0)->devices->list(); + $this->fail("Expected the connection failure to surface"); + } catch (ConnectException) { + // Expected. + } + + $this->assertSame(1, $recorder->attempt_count()); + } + + /** + * The SDK itself only issues POSTs, but a caller reaching for the client + * directly with an idempotent method does get status based retries. + */ + public function testRetriesIdempotentRequestsOnServiceUnavailable(): void + { + $recorder = new RecordingClient([ + self::service_unavailable(), + self::service_unavailable(), + self::devices(), + ]); + + $res = $this->seam($recorder)->client->request("GET", "/devices/list"); + + $this->assertSame([], $res->devices); + $this->assertSame(3, $recorder->attempt_count()); + } +} diff --git a/tests/SeamWebhookTest.php b/tests/SeamWebhookTest.php new file mode 100644 index 00000000..be871513 --- /dev/null +++ b/tests/SeamWebhookTest.php @@ -0,0 +1,103 @@ + "8d7e0b26-5e6c-4a1f-9b3d-1b0f0e5a9c11", + "event_type" => "device.connected", + "workspace_id" => "398d80b7-3f96-47c2-b85a-6f8ba21d07be", + "device_id" => "054765c8-a2fc-4599-b486-14c19f462c45", + "created_at" => "2024-01-01T00:00:00.000Z", + "occurred_at" => "2024-01-01T00:00:00.000Z", + ]); + } + + /** + * @return array + */ + private function signed_headers(string $payload): array + { + $id = "msg_test"; + $timestamp = (string) time(); + + $signature = (new Webhook(self::SECRET))->sign( + $id, + $timestamp, + $payload, + ); + + return [ + "svix-id" => $id, + "svix-timestamp" => $timestamp, + "svix-signature" => $signature, + ]; + } + + public function testVerifyReturnsTheEvent(): void + { + $payload = $this->payload(); + + $event = (new SeamWebhook(self::SECRET))->verify( + $payload, + $this->signed_headers($payload), + ); + + $this->assertSame("device.connected", $event->event_type); + $this->assertSame( + "8d7e0b26-5e6c-4a1f-9b3d-1b0f0e5a9c11", + $event->event_id, + ); + } + + public function testVerifyAcceptsHeadersInAnyCase(): void + { + $payload = $this->payload(); + + $headers = []; + foreach ($this->signed_headers($payload) as $name => $value) { + $headers[strtoupper($name)] = $value; + } + + $event = (new SeamWebhook(self::SECRET))->verify($payload, $headers); + + $this->assertSame("device.connected", $event->event_type); + } + + public function testVerifyRejectsATamperedPayload(): void + { + $payload = $this->payload(); + $headers = $this->signed_headers($payload); + + $this->expectException(WebhookVerificationException::class); + + (new SeamWebhook(self::SECRET))->verify( + str_replace("device.connected", "device.disconnected", $payload), + $headers, + ); + } + + public function testVerifyRejectsTheWrongSecret(): void + { + $payload = $this->payload(); + $headers = $this->signed_headers($payload); + + $this->expectException(WebhookVerificationException::class); + + (new SeamWebhook( + "whsec_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + ))->verify($payload, $headers); + } +} diff --git a/tests/SerializationTest.php b/tests/SerializationTest.php new file mode 100644 index 00000000..83310710 --- /dev/null +++ b/tests/SerializationTest.php @@ -0,0 +1,55 @@ +seam()->devices->list(); + + $this->assertNotEmpty($devices); + } + + public function testNullArrayParameterIsNotSent(): void + { + $devices = $this->seam()->devices->list(device_ids: null); + + $this->assertCount(count($this->seam()->devices->list()), $devices); + } + + public function testEmptyArrayParameterIsSent(): void + { + $devices = $this->seam()->devices->list(device_ids: []); + + $this->assertCount(0, $devices); + } + + public function testPopulatedArrayParameterFiltersTheResults(): void + { + $device_ids = [ + $this->seed["august_device_1"], + $this->seed["ecobee_device_1"], + ]; + + $devices = $this->seam()->devices->list(device_ids: $device_ids); + + $this->assertCount(2, $devices); + + $returned_ids = array_map(fn($device) => $device->device_id, $devices); + + sort($returned_ids); + sort($device_ids); + + $this->assertSame($device_ids, $returned_ids); + } +} diff --git a/tests/Support/FakeSeamConnect.php b/tests/Support/FakeSeamConnect.php new file mode 100644 index 00000000..93976532 --- /dev/null +++ b/tests/Support/FakeSeamConnect.php @@ -0,0 +1,171 @@ + */ + private array $pipes = []; + + public static function start(): self + { + $fake = new self(); + $fake->run(); + + return $fake; + } + + public function endpoint(): string + { + return $this->endpoint; + } + + /** + * The ids and tokens of the seeded records. + */ + public function seed(): array + { + return $this->seed; + } + + private function run(): void + { + $binary = dirname(__DIR__, 2) . "/node_modules/.bin/fake-seam-connect"; + + if (!is_executable($binary)) { + throw new \RuntimeException( + "Could not find {$binary}, run npm install before the tests.", + ); + } + + $port = self::unused_port(); + $this->endpoint = "http://127.0.0.1:{$port}"; + + // The binary is spawned directly rather than through npm so the + // process handle is the server itself and stopping it does not leave + // an orphan behind. PORT goes to the child only, leaving the parent + // environment alone for the tests that read it. + $this->process = proc_open( + [$binary, "--seed"], + [ + 0 => ["file", "/dev/null", "r"], + 1 => ["file", "/dev/null", "w"], + 2 => ["file", "/dev/null", "w"], + ], + $this->pipes, + dirname(__DIR__, 2), + ["PORT" => (string) $port] + getenv(), + ); + + if (!is_resource($this->process)) { + throw new \RuntimeException("Could not start Fake Seam Connect."); + } + + $this->wait_for_health(); + $this->seed = $this->fetch_seed(); + } + + public function stop(): void + { + if (!is_resource($this->process)) { + return; + } + + proc_terminate($this->process, SIGTERM); + + $deadline = microtime(true) + self::SHUTDOWN_TIMEOUT; + while (microtime(true) < $deadline) { + if (!proc_get_status($this->process)["running"]) { + break; + } + usleep(self::POLL_INTERVAL); + } + + if (proc_get_status($this->process)["running"]) { + proc_terminate($this->process, SIGKILL); + } + + proc_close($this->process); + $this->process = null; + } + + private function wait_for_health(): void + { + $deadline = microtime(true) + self::STARTUP_TIMEOUT; + + while (microtime(true) < $deadline) { + if (!proc_get_status($this->process)["running"]) { + throw new \RuntimeException( + "Fake Seam Connect exited before becoming healthy.", + ); + } + + if ($this->get("/health") !== null) { + return; + } + + usleep(self::POLL_INTERVAL); + } + + throw new \RuntimeException( + "Fake Seam Connect did not become healthy within " . + self::STARTUP_TIMEOUT . + "s.", + ); + } + + private function fetch_seed(): array + { + $body = $this->get("/_fake/default_seed"); + + if ($body === null) { + throw new \RuntimeException( + "Could not read the seed from Fake Seam Connect.", + ); + } + + return json_decode($body, true); + } + + private function get(string $path): ?string + { + $context = stream_context_create([ + "http" => ["timeout" => 5, "ignore_errors" => true], + ]); + + $body = @file_get_contents($this->endpoint . $path, false, $context); + + return $body === false ? null : $body; + } + + private static function unused_port(): int + { + $socket = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr); + + if ($socket === false) { + throw new \RuntimeException( + "Could not find an unused port: {$errstr}", + ); + } + + $name = stream_socket_get_name($socket, false); + fclose($socket); + + return (int) substr($name, strrpos($name, ":") + 1); + } +} diff --git a/tests/Support/FakeSeamConnectTestCase.php b/tests/Support/FakeSeamConnectTestCase.php new file mode 100644 index 00000000..81f91400 --- /dev/null +++ b/tests/Support/FakeSeamConnectTestCase.php @@ -0,0 +1,45 @@ +fake = FakeSeamConnect::start(); + $this->endpoint = $this->fake->endpoint(); + $this->seed = $this->fake->seed(); + } + + protected function tearDown(): void + { + $this->fake->stop(); + } + + /** + * A client authorized against the fake with the seeded API key. + */ + protected function seam( + bool|array|null $wait_for_action_attempt = null, + array $guzzle_options = [], + ?int $retries = null, + ): Seam { + return new Seam( + api_key: $this->seed["seam_apikey1_token"], + endpoint: $this->endpoint, + wait_for_action_attempt: $wait_for_action_attempt, + guzzle_options: $guzzle_options, + retries: $retries, + ); + } +} diff --git a/tests/Support/RecordingClient.php b/tests/Support/RecordingClient.php new file mode 100644 index 00000000..d2494332 --- /dev/null +++ b/tests/Support/RecordingClient.php @@ -0,0 +1,107 @@ + */ + public array $transactions = []; + + private MockHandler $mock; + + /** + * @param array $responses Served in order; the last one repeats. + */ + public function __construct(private array $responses) + { + $this->mock = new MockHandler($responses); + } + + /** + * Builds the Guzzle options to hand to a Seam client so its requests land + * here instead of on the network. + * + * @return array + */ + public function guzzle_options(): array + { + $stack = HandlerStack::create($this->mock); + $stack->push(Middleware::history($this->transactions)); + + return ["handler" => $stack]; + } + + /** + * How many logical calls the SDK made. Retries are invisible here because + * the SDK pushes its retry middleware inside this recorder; use + * attempt_count() to count those. + */ + public function request_count(): int + { + return count($this->transactions); + } + + /** + * How many requests actually reached the handler, retries included. + */ + public function attempt_count(): int + { + return count($this->responses) - $this->mock->count(); + } + + public function request(int $index = 0): RequestInterface + { + return $this->transactions[$index]["request"]; + } + + public function body(int $index = 0): mixed + { + return json_decode((string) $this->request($index)->getBody()); + } + + /** + * A response whose body repeats for every request, so retries keep + * failing the same way. + */ + public static function repeating(Response $response, int $times = 20): self + { + return new self(array_fill(0, $times, $response)); + } + + public static function repeating_throwable( + \Throwable $error, + int $times = 20, + ): self { + return new self(array_fill(0, $times, $error)); + } + + public static function json(int $status, mixed $body): Response + { + return new Response( + $status, + ["content-type" => "application/json"], + json_encode($body), + ); + } + + public static function raw( + int $status, + string $body, + string $content_type = "text/plain", + ): Response { + return new Response($status, ["content-type" => $content_type], $body); + } +} diff --git a/tests/WaitForActionAttemptTest.php b/tests/WaitForActionAttemptTest.php new file mode 100644 index 00000000..32dae081 --- /dev/null +++ b/tests/WaitForActionAttemptTest.php @@ -0,0 +1,283 @@ +locks->unlock_door( + $this->seed["august_device_1"], + ); + + $this->assertSame("pending", $action_attempt->status); + + $this->set_status($seam, $action_attempt, "pending"); + + return $action_attempt; + } + + private function set_status( + Seam $seam, + ActionAttempt $action_attempt, + string $status, + ?array $error = null, + ): void { + $seam->client->request( + "POST", + "/_fake/update_action_attempt", + (object) array_filter([ + "action_attempt_id" => $action_attempt->action_attempt_id, + "status" => $status, + "error" => $error, + ]), + ); + } + + public function testWaitsByDefault(): void + { + $action_attempt = $this->seam()->locks->unlock_door( + $this->seed["august_device_1"], + ); + + $this->assertSame("success", $action_attempt->status); + } + + public function testClientDefaultCanDisableWaiting(): void + { + $seam = $this->seam(wait_for_action_attempt: false); + + $action_attempt = $seam->locks->unlock_door( + $this->seed["august_device_1"], + ); + + $this->assertSame("pending", $action_attempt->status); + } + + /** + * The options form of the client default has to wait just like `true` + * does; treating it as "no waiting" would hand back a pending attempt + * with no indication anything was skipped. + */ + public function testClientDefaultCanBeAnOptionsArray(): void + { + $seam = $this->seam( + wait_for_action_attempt: [ + "timeout" => 5.0, + "polling_interval" => 0.05, + ], + ); + + $action_attempt = $seam->locks->unlock_door( + $this->seed["august_device_1"], + ); + + $this->assertSame("success", $action_attempt->status); + } + + public function testPerCallOptionCanDisableWaiting(): void + { + $action_attempt = $this->seam()->locks->unlock_door( + $this->seed["august_device_1"], + wait_for_action_attempt: false, + ); + + $this->assertSame("pending", $action_attempt->status); + } + + public function testPerCallOptionCanEnableWaiting(): void + { + $seam = $this->seam(wait_for_action_attempt: false); + + $action_attempt = $seam->locks->unlock_door( + $this->seed["august_device_1"], + wait_for_action_attempt: true, + ); + + $this->assertSame("success", $action_attempt->status); + } + + public function testReturnsAnAlreadySuccessfulActionAttempt(): void + { + $seam = $this->seam(wait_for_action_attempt: false); + + $action_attempt = $seam->locks->unlock_door( + $this->seed["august_device_1"], + ); + $this->set_status($seam, $action_attempt, "success"); + + $resolved = $seam->action_attempts->get( + $action_attempt->action_attempt_id, + wait_for_action_attempt: true, + ); + + $this->assertSame("success", $resolved->status); + $this->assertSame( + $action_attempt->action_attempt_id, + $resolved->action_attempt_id, + ); + } + + /** + * Proves the resolver really re-reads the action attempt: it starts out + * pending and is moved to success by something outside this process, the + * way the JavaScript and Ruby suites do it. + */ + public function testWaitsForAnActionAttemptResolvedOutOfBand(): void + { + $seam = $this->seam(wait_for_action_attempt: false); + $action_attempt = $this->pending_action_attempt($seam); + + $resolver = $this->resolve_after( + $action_attempt->action_attempt_id, + 0.5, + ); + + try { + $resolved = $seam->action_attempts->get( + $action_attempt->action_attempt_id, + wait_for_action_attempt: [ + "timeout" => 15.0, + "polling_interval" => 0.1, + ], + ); + + $this->assertSame("success", $resolved->status); + } finally { + proc_close($resolver); + } + } + + /** + * @return resource + */ + private function resolve_after(string $action_attempt_id, float $delay) + { + $payload = json_encode([ + "action_attempt_id" => $action_attempt_id, + "status" => "success", + ]); + + $script = sprintf( + "usleep(%d); file_get_contents(%s, false, stream_context_create(%s));", + (int) ($delay * 1000000.0), + var_export($this->endpoint . "/_fake/update_action_attempt", true), + var_export( + [ + "http" => [ + "method" => "POST", + "header" => "Content-Type: application/json", + "content" => $payload, + "ignore_errors" => true, + ], + ], + true, + ), + ); + + $process = proc_open( + [PHP_BINARY, "-r", $script], + [ + 1 => ["file", "/dev/null", "w"], + 2 => ["file", "/dev/null", "w"], + ], + $pipes, + ); + + if (!is_resource($process)) { + $this->fail("Could not start the out of band resolver"); + } + + return $process; + } + + public function testThrowsWhenTheActionAttemptFails(): void + { + $seam = $this->seam(wait_for_action_attempt: false); + + $action_attempt = $seam->locks->unlock_door( + $this->seed["august_device_1"], + ); + $this->set_status($seam, $action_attempt, "error", [ + "type" => "foo", + "message" => "Failed", + ]); + + try { + $seam->action_attempts->get( + $action_attempt->action_attempt_id, + wait_for_action_attempt: true, + ); + $this->fail("Expected ActionAttemptFailedError"); + } catch (ActionAttemptFailedError $error) { + $this->assertSame("Failed", $error->getMessage()); + $this->assertSame("foo", $error->getErrorCode()); + $this->assertSame("error", $error->getActionAttempt()->status); + $this->assertSame( + $action_attempt->action_attempt_id, + $error->getActionAttempt()->action_attempt_id, + ); + $this->assertInstanceOf(ActionAttemptError::class, $error); + } + } + + public function testTimesOutWhileTheActionAttemptIsPending(): void + { + $seam = $this->seam(wait_for_action_attempt: false); + $action_attempt = $this->pending_action_attempt($seam); + + try { + $seam->action_attempts->get( + $action_attempt->action_attempt_id, + wait_for_action_attempt: [ + "timeout" => 0.2, + "polling_interval" => 5.0, + ], + ); + $this->fail("Expected ActionAttemptTimeoutError"); + } catch (ActionAttemptTimeoutError $error) { + $this->assertSame( + $action_attempt->action_attempt_id, + $error->getActionAttempt()->action_attempt_id, + ); + $this->assertStringContainsString( + "Timed out waiting for action attempt", + $error->getMessage(), + ); + } + } + + /** + * Resolving fetches the action attempt through the HTTP client rather + * than the route client, so enabling the option on the route that reads + * action attempts cannot recurse. + */ + public function testActionAttemptsGetDoesNotRecurse(): void + { + $seam = $this->seam(wait_for_action_attempt: false); + + $action_attempt = $seam->locks->unlock_door( + $this->seed["august_device_1"], + ); + $this->set_status($seam, $action_attempt, "success"); + + $resolved = $seam->action_attempts->get( + $action_attempt->action_attempt_id, + wait_for_action_attempt: [ + "timeout" => 1.0, + "polling_interval" => 0.05, + ], + ); + + $this->assertSame("success", $resolved->status); + } +}