dotsplatform / reports-api-sdk
Library for the Dots Platform reports service client
Requires
- php: ^8.3
- ext-json: *
- dotsplatform/utils: ^2.3
- guzzlehttp/guzzle: ^7.8
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- eduarguz/shift-php-cs: ^3.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^11.5|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-02 22:29:45 UTC
README
PHP SDK for the Dots reports service, REST API v1 (task §11.2 / §11.3).
Install
composer require dotsplatform/reports-api-sdk php artisan vendor:publish --tag=reports-api-sdk-config
config/reports-server.php:
| Key | Env | Meaning |
|---|---|---|
host |
REPORTS_SERVER_HOST |
Base URL of the reports service, e.g. https://reports.dotsplatform.com |
token |
INTERNAL_GATEWAY_TOKEN |
The shared secret of every Dots service |
connect_timeout |
– | Seconds to connect, 2 by default |
timeout |
– | Seconds for a whole JSON request, 8 by default |
pdf_timeout |
– | Seconds for a PDF download, 30 by default |
Every request sends X-Internal-Gateway-Token: <token>. The reports service answers 401 without it. See
livesite/ai/2026-09/DEV-18603-microservices-auth-middleware/internal-request-authorization.md.
Write calls take an optional $actorId, the livesite user who acts. It is sent as X-Actor-Id, and the service
stores it as created_by for the audit log.
Usage
use Dotsplatform\ReportsApi\ReportsClient; $reportsClient = app(ReportsClient::class);
The client is split into groups:
| Group | Methods |
|---|---|
metrics() |
listMetrics, listDimensions, listScopes (cursor), queryMetrics, getSnapshot |
reportDefinitions() |
list, create, get, update, delete, duplicate, resetToTemplate, getData, listBlocks |
reportRuns() |
list, listForAccount (every definition's runs of an account, newest first, cursor pages, with the definition and the period label), create, get, downloadPdf (bytes), createShareLink (always a new run), ensureShareLink (a link on an existing run: its live one, or a new token on it), regenerate (remakes the stored page and PDF; the link stays) |
A run's page and PDF are generated and stored when the run is made: a share link shows the data as of generated_at, not live data. regenerate makes the files again with current data; the link stays.
| settings() | get, update |
| subscriptions() | list, create, get, update, delete, sendNow, listDeliveries (latest 100, newest first) |
| apiClients() | list, create (the plain token is returned once), revoke |
| operations() | listTemplates, queueBackfill, queueReaggregation |
Query metrics
use Dotsplatform\ReportsApi\DTO\Common\ScopeDTO; use Dotsplatform\ReportsApi\DTO\Metrics\Requests\QueryMetricsRequestDTO; use Dotsplatform\ReportsApi\DTO\Metrics\Requests\SeriesRequestDTO; use Dotsplatform\ReportsApi\Enums\Grain; use Dotsplatform\ReportsApi\Enums\ScopeType; $result = $reportsClient->metrics()->queryMetrics($accountId, QueryMetricsRequestDTO::fromArray([ 'metricKeys' => ['orders.count', 'orders.revenue', 'orders.avg_check'], 'scope' => ScopeDTO::create(ScopeType::ACCOUNT, $accountId), 'series' => SeriesRequestDTO::last(6, Grain::MONTH), ])); foreach ($result->getPeriods() as $period) { $revenue = $period->getMetric('orders.revenue')?->getMoney(); // MoneyDTO: getAmountCents(), getCurrency() $averageCheck = $period->getMetric('orders.avg_check')?->getRatio(); // RatioDTO: numerator, denominator, value }
A period is chosen in exactly one way: period (one PeriodRequestDTO), periods (a list) or series.
Values
- Ids are uuid strings.
- Money is a
MoneyDTO(amountCents,currency). Amounts are never floats. - Ratios and means are a
RatioDTO(numerator,denominator,value). - Periods are a
PeriodDTO(grain,key, andfrom/to/labelwhere the response has them). - Durations are seconds; times are unix timestamps.
Compatibility: enums in responses are tolerant
The service may add enum values (a new subscription format, delivery status, scope type, …) before every consumer has upgraded the SDK. An unknown value must never break a whole response, so:
- every enum-backed field of a response DTO keeps the raw string:
getStatusValue(): string; - its typed getter returns the enum or null for a value this SDK version does not know:
getStatus(): ?DeliveryStatus; - code that branches on a value handles null (treat it as "unknown", show the raw value, or skip);
- helpers such as
ReportRunDTO::isPdfReady()answer false for unknown values.
Request DTOs stay strict: they take enum cases, so the SDK never sends a value the service rejects. A field that
is not an enum but has an unexpected type (a string where an integer is documented) still throws
ReportsUnexpectedResponseException.
Report definitions
use Dotsplatform\ReportsApi\DTO\Reports\LayoutBlockDTO; use Dotsplatform\ReportsApi\DTO\Reports\Requests\ReportDefinitionFormDTO; $definition = $reportsClient->reportDefinitions()->create($accountId, ReportDefinitionFormDTO::fromArray([ 'templateKey' => 'weekly', 'name' => 'Weekly for owners', ]), $actorId); $reportsClient->reportDefinitions()->update($accountId, $definition->getId(), ReportDefinitionFormDTO::fromArray([ 'blocks' => [LayoutBlockDTO::create('header'), LayoutBlockDTO::create('kpi_group', ['group' => 'general'])], ]), $actorId);
Unset form fields are not sent, so update changes only what is set.
Subscription channels
A subscription is delivered by email (the default) or Telegram. The channel is chosen on create and never changes:
update() does not send it. Every recipient must match the channel.
use Dotsplatform\ReportsApi\DTO\Subscriptions\Requests\RecipientRequestDTO; use Dotsplatform\ReportsApi\DTO\Subscriptions\Requests\SubscriptionFormDTO; use Dotsplatform\ReportsApi\Enums\SubscriptionChannel; $subscription = $reportsClient->subscriptions()->create($accountId, SubscriptionFormDTO::fromArray([ 'definitionId' => $definitionId, 'channel' => SubscriptionChannel::TELEGRAM, 'recipients' => [ RecipientRequestDTO::telegramUser($livesiteUserToken, 'Owner'), // a livesite user (by token) with Telegram connected RecipientRequestDTO::telegramChat($telegramChatId), // a chat connected to the notifier bot ], ]), $actorId);
Email recipients use RecipientRequestDTO::email($email, $name). In responses, a recipient has getKind()
(RecipientKind, tolerant like every response enum), a nullable getEmail(), getTelegramChatId() and
getTelegramUserId().
Clearing a field in an update
A null field in a form means "not given", so it is not sent. To remove a value, clear it explicitly. The
cleared fields are sent as JSON null:
use Dotsplatform\ReportsApi\Enums\SubscriptionClearableField; $form = SubscriptionFormDTO::fromArray(['sendTime' => '08:00']) ->clear(SubscriptionClearableField::LOCALE, SubscriptionClearableField::SEND_WEEKDAY); // PATCH body: {"send_time": "08:00", "locale": null, "send_weekday": null}
- Subscriptions can clear
LOCALE,SCOPE_ID,SEND_WEEKDAYandSEND_MONTH_DAY. - Report definitions can clear
SCOPE_ID(ReportDefinitionClearableField). clear()returns a copy. A cleared field is sent as null even when a value was set on the same form.
Errors
Every call may throw a subclass of Dotsplatform\ReportsApi\Exceptions\ReportsApiException:
| Exception | When |
|---|---|
ReportsNotFoundException |
404: unknown id, or an id of another account |
ReportsValidationException |
422: getErrors() / getErrorsFor('field'), Laravel style |
ReportsUnauthorizedException |
401: wrong or missing token |
ReportsForbiddenException |
403: an account token without the ability |
ReportsRateLimitException |
429: getRetryAfterSeconds() |
ReportsServerException |
any other error status |
ReportsConnectionException |
no response (DNS, timeout, refused) |
ReportsUnexpectedResponseException |
the body does not have the documented shape |
ReportsConfigurationException |
reports-server.host or reports-server.token is empty |
Tests of consumers
use Dotsplatform\ReportsApi\Clients\FailReportsStubClient; use Dotsplatform\ReportsApi\Clients\SuccessReportsStubClient; use Dotsplatform\ReportsApi\ReportsClient; $this->app->bind(ReportsClient::class, SuccessReportsStubClient::class); // sample data, no HTTP $this->app->bind(ReportsClient::class, FailReportsStubClient::class); // every call throws ReportsServerException (503)
The sample data is in Clients\Stubs\StubResponses.
Development
composer install composer init-pre-commit # fixer + phpstan + phpunit before each commit composer test # PHPUnit, HTTP mocked with Guzzle MockHandler composer analyse # PHPStan, level max composer fix # PHP CS Fixer (Laravel Shift style + strict rules)