AGENTS.md — ArrowSphere Public API Client
PHP 8.0+ library (arrowsphere/public-api-client) providing a Guzzle-based client for
ArrowSphere's public REST API. Namespace root: ArrowSphere\PublicApiClient\ → src/.
Commands
Tests
# Run the full test suite
vendor/bin/phpunit
# Run a single test file
vendor/bin/phpunit tests/General/WhoamiClientTest.php
# Run a single test method
vendor/bin/phpunit --filter testGetWhoami tests/General/WhoamiClientTest.php
# Run with code coverage (outputs to build/artifacts/coverage/)
make coverage
Static analysis and code style
PHPStan, Psalm, and php-cs-fixer run via Docker (requires Docker daemon).
# Run all static checks (phpstan + psalm + cs-fixer dry-run)
make static
# Code style: check only (dry-run)
make static-codestyle-check
# Code style: apply fixes
make static-codestyle-fix
# PHPStan only (max level, paths: src/)
make static-phpstan
# Psalm only (level 3)
make static-psalm
# Regenerate baselines after intentional suppressions
make static-phpstan-update-baseline
make static-psalm-update-baseline
Code Style
Enforced by php-cs-fixer (@PSR2 base). Key rules:
- No
declare(strict_types=1)— explicitly disabled; do not add it. - Short array syntax
[]everywhere; no trailing comma in single-line arrays. - Imports: alphabetically ordered (
ordered_imports), no leading slash, no unused imports. Use fully-qualified types (fully_qualified_strict_types); never use unqualified class names without anusestatement. - Strings: concatenation operator
.surrounded by single spaces. - No yoda conditions:
$x === null, notnull === $x. - Compact nullable typehints:
?string, not? string. - No extra blank lines; one blank line between class methods
(
class_attributes_separation). - Ternary to null-coalescing: prefer
$a ?? $bover$a !== null ? $a : $b. - Self static accessor: use
self::inside a class instead of the class name. - No useless
elseafter areturn/throw; no uselessreturnat end of void methods.
PHPDoc
PHPDoc is required on every property and every method (multi-line style enforced by
phpdoc_line_span):
/**
* @var string
*/
private string $foo;
/**
* Does something useful.
*
* @param string $bar
*
* @return string
*
* @throws PublicApiClientException
*/
public function doSomething(string $bar): string
- Every exception that can propagate out of a method must appear in a
@throwstag. nullalways last in union types:string|null, notnull|string.- Use scalar types in PHPDoc (
bool,int,float,string), notboolean/integer. - No
@packagetags (phpdoc_no_package).
Naming
- Classes / Interfaces / Traits:
PascalCase. - Methods / properties / variables:
camelCase. - Constants:
UPPER_SNAKE_CASE. Entity field/API key names are stored asCOLUMN_*constants on the entity class (e.g.COLUMN_COMPANY_NAME = 'companyName'). - Enum-style classes extend
AbstractEnum; use class constants for values. - Test classes: suffix
Test(e.g.WhoamiClientTest). Test methods are prefixedtest(no@testannotation).
Architecture Conventions
Clients
- All sub-clients extend
AbstractClient(or a domain abstract extending it). - Two-method pattern per endpoint:
getFooRaw(): string— sets$this->path, calls$this->get(), returns raw JSON string.getFoo(): Entity— callsgetFooRaw(), decodes, returns a typed entity.
- Paginated list endpoints return a
Generatorviayieldinside a page loop. - Fluent setters always return
self(orstatic). - Set
$this->pathat the start of each client method, not in the constructor.
Entities (two systems — both in use)
Legacy (src/AbstractEntity.php):
- Constructor receives
array $data. - Field name constants:
COLUMN_FOO = 'foo'on the entity class. - Getters read from
$this->data[static::COLUMN_FOO]. - Validation via
illuminate/validationis disabled by default ($enableValidation = false); it is enabled only in tests viasetUp().
Newer (src/Entities/AbstractEntity.php):
- PHP 8
#[Property]attribute on constructor parameters. - Reflection-based auto-hydration from the input array.
- Getters read from typed properties.
- ALWAYS USE NEWER STYLE FOR NEW ENTITIES; legacy style is only for existing entities that haven't been migrated yet.
Exceptions
PublicApiClientException— base; thrown on HTTP 4xx/5xx or JSON decode failure.NotFoundException— thrown specifically on HTTP 404.EntityValidationException— thrown when legacy entity validation fails.
Testing Conventions
- All test classes extend
AbstractClientTest. - Declare
protected const MOCKED_CLIENT_CLASS = YourClient::class;. GuzzleHttp\Clientis mocked via$this->createMock(Client::class); the mock is available as$this->httpClient.- Triple test pattern per endpoint (use
@dependsto chain them):testGetFooRaw— verify URL and HTTP method called.testGetFooWithInvalidResponse— assertPublicApiClientExceptionis thrown on bad JSON.testGetFoo— full test with inline JSON heredoc fixture; assert entity getters.
- Use
self::for all assertions and mock expectations (self::assertEquals,self::once(), etc.) — never$this->assert*. - JSON fixtures go as heredoc strings directly inside the test method body.
- Do not change
AbstractEntity::$enableValidation; it is set totrueinAbstractClientTest::setUp().
Changelog
The project follows Keep a Changelog and Semantic Versioning.
Every code change must be recorded under ## [Unreleased] at the top of
CHANGELOG.md, before the first versioned section. Entries use bullet points; no
sub-sections are required for minor/patch releases (most existing entries do not use them),
but the canonical sub-section labels are ### Added, ### Changed, ### Fixed,
### Deprecated, and ### Removed — use them for larger releases or when clarity
benefits from grouping.
Entry wording conventions observed in this repo:
- Added: start with
Added …— new client, method, property, entity, or behaviour. Name themethodunderclassexplicitly:Added new method \postExport()` under `CustomersClient``. - Changed: start with
Changed …orUpdated …— modifications to existing signatures, renamed symbols, updated dependencies. - Fixed: start with
Fixed …— bug fixes; include the class/method name. - Deprecated: start with
Deprecated …; state the replacement in the same sentence. - Removed: start with
Removed …; note the replacement if one exists.