Production guarantee. Public API stable, SemVer commitment. Same code as 0.1.0 — same Astroway constructor, 100+ namespace services, 12-class error hierarchy, DTOs, helpers, PSR-16 cache, Guzzle promises concurrency, mock client, PSR-3 logger + metrics. Major bump signals the contract, not surface change.
^8.1 → ^8.2.
astroway/sdk:^0.1.0 (will receive critical security patches).Astroway surface requires 2.0.0 with deprecation period.Astroway::VERSION + DEFAULT_BASE_URL. Constructor $options shape (apiKey, baseUrl, authScheme, timeout, retry, defaultHeaders, httpClient, requestFactory, streamFactory, idempotency, cache, cacheTtlSeconds, logger, metrics). request/get/post/put/delete/concurrent + 100+ namespace accessors. 9-subclass error tree. ApiError public properties (status/errorCode/requestId/creditsRemaining/retryAfterSeconds/body). MockAstroway IS-A Astroway. phpstan level 6 clean.
composer require astroway/sdk:^1.0 is a drop-in upgrade if you're on PHP 8.2+.
128 PHPUnit tests pass. phpstan analyse clean.
First release candidate. PSR-3 logger integration + observability hooks. Production users want Astroway to participate in their existing logging stack (Monolog, Symfony's logger, Laravel's Log facade) and metrics pipeline (Prometheus, Datadog, StatsD) without re-implementing request tracing client-side.
logger constructor option — pass any Psr\Log\LoggerInterface. The SDK emits a debug record on every outgoing request, then a level-by-status record on every response (debug for 2xx/3xx, warning for 4xx, error for 5xx) and error on PSR-18 exceptions. Each record's context carries:
astroway_trace_id — a per-request UUID4 hex correlator (8 bytes).method, path, idempotency_key.status, latency_ms, request_id (server-side x-request-id), credits_remaining.exception object (PSR-3 standard) + latency_ms.use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('astroway');
$log->pushHandler(new StreamHandler('php://stderr', Logger::DEBUG));
$aw = new Astroway(['apiKey' => $key, 'logger' => $log]);
metrics constructor option — callable(array $event): void invoked alongside log records. $event['event'] is request | response | error and the rest of the fields mirror the log context. Useful for incrementing Prometheus counters / StatsD timers without re-parsing logs:
$aw = new Astroway([
'apiKey' => $key,
'logger' => $log,
'metrics' => fn(array $e) => $statsd->timing(
"astroway.{$e['event']}",
$e['latency_ms'] ?? 0,
['status' => $e['status'] ?? 'error'],
),
]);
Metrics handler errors are swallowed — observability must never break the request path.X-Astroway-Trace-Id header automatically attached to every request. If the caller already supplies one (typical when integrating with an existing Datadog / Sentry / OpenTelemetry trace), the SDK respects it instead of minting a fresh id, so trace correlation crosses the SDK boundary cleanly.Astroway\Internal\LoggingClient — PSR-18 client decorator implementing the above. Wraps the existing RetryClient so retries surface as separate log entries with the same trace id.psr/log is now a runtime dependency (^1.1 || ^2.0 || ^3.0). Existing users who don't pass logger are unaffected — the option defaults to null (no decoration).No breaking changes. New options default to disabled. Logging kicks in only when you opt in.
tests/LoggingTest.php).LoggerInterface and the eight log levels.request_id/trace_id correlation.Mock client for PHPUnit. Drop-in replacement for Astroway that records calls and returns scripted fixtures with zero HTTP traffic. Mirrors [@astroway](https://github.com/astroway)/sdk/testing (TS) and astroway.testing (Python).
Astroway\Testing\MockAstroway — extends Astroway, so the full namespace surface ($mock->chart()->compute(...), all 100+ services) works unchanged with the same type checks. Override is on the public request() method:
use Astroway\Testing\MockAstroway;
$mock = new MockAstroway();
$mock->respond('POST', '/chart', ['angles' => ['asc' => 'Aries']]);
$r = $mock->chart()->compute(['date' => '1990-01-01']);
$this->assertSame('Aries', $r['angles']['asc']);
$this->assertCount(1, $mock->calls);
MockAstroway::respond(method, path, fixture) — register a fixture as a plain value, a \Throwable (thrown when the route is hit), or a callable(array $ctx): mixed where $ctx = ['method', 'path', 'body', 'callIndex']. Multiple fixtures for the same route serve in order; the last one repeats once exhausted.MockAstroway::$calls — public property: ordered list of ['method', 'path', 'body', 'headers', 'resolved'].MockAstroway::callsFor(path, method?), callCount(), reset() — assertion helpers.Astroway\Testing\MockApiError — factory for classified ApiError subclasses, so retry / error-handling code paths see the right concrete subclass:
$mock->respond('POST', '/chart',
MockApiError::make(status: 401, code: 'INVALID_API_KEY') // → AuthenticationError
);
$mock->respond('POST', '/chart',
MockApiError::make(status: 429, retryAfterSeconds: 17) // → RateLimitError
);
$mock->respond('POST', '/chart',
MockApiError::make(status: 402, code: 'OUT_OF_CREDITS') // → QuotaExceededError
);
ApiError("MockAstroway: no fixture for POST /chart. Call \$mock->respond('POST', '/chart', \$value) before invoking this endpoint.").Astroway is non-final since beta.4. Production users should still treat the class as effectively final — the [@api](https://github.com/api) surface lives on the public methods, not on inheritance. The change is required so MockAstroway can extend it without re-declaring the 100+ namespace service shims.No breaking changes for existing code. The final removal is a relaxation, not a tightening.
tests/Testing/MockAstrowayTest.php).MockAnthropic pattern.Concurrent batch dispatch. PHP doesn't have native async, but bounded-concurrency batching is critical for "calculate natal charts for 1000 users" workloads. Mirror of [@astroway](https://github.com/astroway)/sdk rc.2 plans (TS) — TS rolls connection pooling into the same release.
$aw->concurrent(int $maxConcurrency = 10) — returns Astroway\Concurrent:
$charts = $aw->concurrent(5)->all([
fn() => $aw->charts()->natal($r1),
fn() => $aw->charts()->natal($r2),
fn() => $aw->charts()->natal($r3),
]);
Concurrent::all(array $tasks): array — runs all callables, returns positional results. Failures land at their index as Throwable entries (no early abort) so partial successes are inspectable.Concurrent::allOrFail(array $tasks): array — sequential try/throw semantics. First failure aborts and rethrows the original ApiError subclass.Concurrent::map(array $tasks): array — preserves your input keys (string or int):
$charts = $aw->concurrent()->map([
'alice' => fn() => $aw->charts()->natal($alice),
'bob' => fn() => $aw->charts()->natal($bob),
]);
maxConcurrency validation — throws InvalidArgumentException if < 1.Closures capture exactly the call you'd write sequentially — fn() => $aw->charts()->natal($req). Keeps the typed namespace surface and lets DTOs flow through unchanged.
The batch loop runs tasks of $maxConcurrency chunks back-to-back through the same PSR-18 client. True HTTP parallelism requires Guzzle promises and async transports — exposed via $concurrent->httpClient() for users who want to drive the pool directly. The portable contract (sequential within a chunk, bounded chunks) holds across all PSR-18 clients including Symfony's Psr18Client.
No breaking changes. $aw->concurrent(...) is purely additive.
Astroway\Tests\ConcurrentTest).phpstan --level=6 clean.allOrFail first-failure abort, map key preservation, maxConcurrency validation, public maxConcurrency field, empty tasks return empty, instance returned from Astroway, ApiError subclass preserved through all().PSR-16 SimpleCache for deterministic endpoints. Charts are pure functions of (date, time, lat, lon, tz) — caching them saves credits and makes dev loops instant. Mirror of [@astroway](https://github.com/astroway)/sdk v0.1.0-beta.3 / astroway (Python) b3 plans.
cache constructor option accepting any PSR-16 CacheInterface:
use Astroway\Astroway;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;
$cache = new Psr16Cache(new FilesystemAdapter('astroway', 0, '/tmp/astroway-cache'));
$aw = new Astroway(['apiKey' => '...', 'cache' => $cache]);
// Two identical calls — only one HTTP round-trip
$aw->charts()->natal($req);
$aw->charts()->natal($req);
cacheTtlSeconds constructor option — global default TTL (24h by default; pure-function endpoints don't actually expire, but the TTL bounds disk usage).cache override in request() and post() (true to force, false to skip):
$aw->post('/transits', $body, [], cache: true); // force-cache
$aw->request('POST', '/chart', ['json' => $b, 'cache' => false]); // force-skip
cacheTtlSeconds override the same way.Astroway\Internal\CachePolicy::DETERMINISTIC_PREFIXES:
/chart, /synastry, /composite, /midpoints, /aspects, /houses, /planets/vedic/*, /numerology/*, /tarot/*, /hd/*, /human-design/*, /dasha/*CachePolicy::NON_DETERMINISTIC_PREFIXES:
/transits, /horoscope, /interpret, /ai/*, /mcp/*, /stream/*, /now, /todayAstroway\Internal\CacheKey::build() — content-addressed key from canonical JSON. SHA-256 of (method, path, sorted body). Two requests with the same logical body but different field order produce the same key, so caching is order-insensitive (lists keep positional order, intentionally).astroway_v1_<hash> — bumping the v1 prefix in a future release auto-invalidates stale entries; multi-SDK Redis backends never collide.psr/simple-cache: ^1.0 || ^2.0 || ^3.0 is now a hard requirement (~3 KB, no transitive deps). Without a cache option in the constructor, behaviour is identical to beta.1.symfony/cache is a suggest (and require-dev) for filesystem/Redis/Memcached adapters; users can plug any other PSR-16 implementation.No breaking changes. Existing code keeps working without a cache. Adding 'cache' => $psr16Cache to your constructor opts is the only thing you need to change.
Astroway\Tests\CacheTest).phpstan --level=6 clean.cache: true, force-skip via cache: false, no-cache backend behaves like beta.1).First beta. Birth-moment helper that mirrors [@astroway](https://github.com/astroway)/sdk v0.1.0-alpha.6 / astroway (Python) v0.1.0a6. Less boilerplate around \DateTimeImmutable + lat/lon/tz for every astrology call.
Astroway\Helpers\BirthDateTime — final readonly class wrapping the (date, time, lat, lon, tz) tuple every calc endpoint expects. Three factories:
BirthDateTime::fromCoordinates(date: '1990-07-14', time: '14:30', latitude: 50.45, longitude: 30.52, timezoneOffset: 3)BirthDateTime::fromDateTimeImmutable($dt, latitude: 50.45, longitude: 30.52) — derives timezoneOffset from the \DateTimeImmutable offset by default; pass timezoneOffset: to override (e.g. UTC instance with original birth tz).BirthDateTime::parse('1990-07-14T14:30:00+03:00', latitude: 50.45, longitude: 30.52) — ISO-8601 with offset auto-resolved; naive ISO requires explicit timezoneOffset:.->toArray() serialises to the wire shape used by /v1/chart, /v1/synastry, /v1/transits, all /v1/vedic/*, etc.->toDateTimeImmutable() rebuilds a PHP datetime including fractional offsets (+05:30, +05:45).[-90, 90], longitude [-180, 180], timezone [-14, 14]. Throws \InvalidArgumentException.BirthDateTime::fromCity('Kyiv, UA', '1990-07-14', '14:30') — needs /v1/geo/search in api-calc. Until then, geocode externally and pass coordinates to fromCoordinates().Astroway\Tests\Helpers\BirthDateTimeTest).phpstan --level=6 clean.fromCoordinates() → toDateTimeImmutable() → ATOM preserves date/time/offset.Auto-attached Idempotency-Key (UUIDv4) on every credit-costing POST. Mirror of [@astroway](https://github.com/astroway)/sdk v0.1.0-alpha.4 / astroway (Python) v0.1.0a4. A network-blip retry never double-bills now.
Idempotency-Key header on POST by default. UUIDv4 per request via random_bytes(16) + RFC 4122 v4/variant fixups (no extra dependency). GET/HEAD untouched. User-supplied keys win.idempotency constructor option: 'auto' (default), 'off', or a callable(): string (custom generator: deterministic test keys, ULIDs, ...).idempotencyKey per-call option on every service method:
$aw->synastry()->aspectGrid([...], ['idempotencyKey' => 'replay-abc']);
idempotencyKey on $aw->request() for manual control over arbitrary methods.Astroway\Internal\Idempotency::generateKey() exposed for users who want the generator standalone.The header fails open. Older backend versions and self-hosted deployments without idempotency support simply ignore it — no breakage. As api-calc rolls out idempotency caching, existing SDK users get retry-safe POSTs automatically.
src/Internal/Idempotency.php with generateKey, shouldAttach, resolveGenerator static helpers.Astroway::request() walks $options['headers'] case-insensitively to detect existing Idempotency-Key.$options shape to include idempotencyKey?: string.No breaking changes. Auto-attachment is additive on POSTs; servers that don't recognise the header ignore it. To suppress globally: new Astroway(['apiKey' => …, 'idempotency' => 'off']).
Refined error hierarchy + uniform creditsRemaining / retryAfterSeconds on every ApiError. Mirror of TS alpha.5 / Python a5.
QuotaExceededError — distinguishes "you ran out of credits" from "you got rate-limited" (the latter resolves with backoff; the former needs a top-up). Triggered by HTTP 402 or errorCode: OUT_OF_CREDITS / QUOTA_EXCEEDED / CREDIT_LIMIT_REACHED.CalculationError — for server-side calculation failures (Swiss Ephemeris boundaries, missing datasets, unsupported house systems for high latitudes). Triggered by errorCode: CALCULATION_ERROR / EPHEMERIS_ERROR.creditsRemaining field uniform across all ApiError subclasses, surfaced from X-Credits-Remaining response header.retryAfterSeconds moved from RateLimitError to base ApiError — useful on quota-exceeded responses too, not just 429.RateLimitError constructor signature unchanged for callers (positional + named args still work); the retryAfterSeconds property now lives on the base ApiError.No breaking source changes. Existing code that catches RateLimitError and reads $e->retryAfterSeconds keeps working — the field just lives on the base ApiError now (also reachable as ($e instanceof ApiError ? $e->retryAfterSeconds : null)).
use Astroway\Errors\{RateLimitError, QuotaExceededError, CalculationError};
try {
$aw->chart()->compute([...]);
} catch (RateLimitError $e) {
sleep($e->retryAfterSeconds ?? 60);
} catch (QuotaExceededError $e) {
// $e->creditsRemaining is often 0 here — top up
notifyBilling($e->creditsRemaining);
} catch (CalculationError $e) {
// ephemeris boundary — try a different date or house system
skipDate($e->body);
}
Astroway::raiseForResponse() now reads X-Credits-Remaining and threads creditsRemaining into every classified error.Classify::fromStatus() does code-first dispatch for app-level errors that may ride on multiple HTTP statuses.DTO request classes for the top-4 endpoint categories. PHP 8.1+ readonly classes with constructor-promotion + format validation at construction time. IDE autocomplete, fewer typos, and request bodies that fail fast before the network round-trip.
Astroway\Dto namespace with hand-curated readonly classes:
BirthData — base for natal-style endpoints (date, time, timezoneOffset, latitude, longitude, houseSystem, name, city, zodiacType, ayanamsaId, cosmogram).SynastryRequest — chart1: BirthData, chart2: BirthData, orbFactor.TransitsRequest — flat birth fields + targetDate / targetTime / target* overrides.VedicDashaRequest — birth + ayanamsaId + startDate / endDate window.request() calls toArray() automatically when given any object exposing the method.date (YYYY-MM-DD) and time (HH:MM:SS) patterns enforced in constructors, throws InvalidArgumentException on bad input.readonly + constructor promotion).$aw->post(path, body, query) widened to accept array|object|null for body — pass a DTO directly.$aw->request, $aw->post).No breaking changes. DTOs are additive — existing array calls keep working unchanged.
// Both work identically:
$aw->chart()->compute(['date' => '1990-07-14', 'time' => '14:30:00', 'timezoneOffset' => 3]);
$aw->chart()->compute(new \Astroway\Dto\BirthData(date: '1990-07-14', time: '14:30:00', timezoneOffset: 3));
?array $body to array|object|null $body so DTOs type-check at the call site.Typed service classes — $aw->synastry()->aspectGrid([...]) instead of $aw->post('/synastry/aspect-grid', body: [...]). Same typing, friendlier surface, automatic envelope unwrap.
openapi.json. Naming mirrors the TS / Python SDKs: _ is the namespace separator, - becomes camelCase per segment. Single-segment opIds get compute().
$aw->transits()->compute([...]) — POST /transits$aw->synastry()->aspectGrid([...]) — POST /synastry/aspect-grid$aw->bazi()->dayMaster([...]) — POST /bazi/day-master$aw->vedic()->dashasVimshottariMaha([...]) — POST /vedic/dashas/vimshottari/maha$aw->tarot()->riderWaiteDaily([...]) — POST /tarot/rider-waite/daily$aw->humanDesign()->compute([...]) — POST /human-design$aw->namespace() returns the same service instance for the lifetime of the Astroway client.['headers' => […], 'query' => […]] second argument on every service method.scripts/generate-namespaces.php wired into composer generate:namespaces.$aw->request($method, $path, $opts) and $aw->post(...) / $aw->get(...) escape hatches still work — needed for path-template endpoints (/webhooks/{id}/test) and anything not yet covered by services.Astroway\HasServices trait holds the 103 accessor methods (auto-generated). Astroway class uses the trait.--memory-limit=1G for the larger generated tree).No breaking changes. Service accessors are additive on the Astroway instance via trait. Replace $aw->post('/x/y', body: [...]) with $aw->x()->y([...]) at your own pace — both still work.
Bring Your Own HTTP Client (PSR-18/17). Guzzle is no longer a hard dependency — the SDK runs on any PSR-18 client (Guzzle, Symfony HTTP, Buzz, …) via php-http/discovery auto-detection or explicit injection.
guzzlehttp/guzzle dependency dropped. Now only psr/http-client, psr/http-message, psr/http-factory, php-http/discovery in require. Install Guzzle (or any PSR-18 client) once: composer require guzzlehttp/guzzle nyholm/psr7.RetryClient PSR-18 decorator replaces the Guzzle-specific RetryMiddleware. Same retry semantics (408/409/429/5xx + network errors, exp backoff + jitter, Retry-After honored), now portable across HTTP clients.httpClient, requestFactory, streamFactory for explicit BYOC injection. The handlerStack Guzzle-specific option was removed — alpha stage, no BC promise. Pass a configured PSR-18 ClientInterface to httpClient instead.MockHttpClient (PSR-18) plus nyholm/psr7 for response building. No more GuzzleHttp\Handler\MockHandler / HandlerStack / Middleware::history coupling.httpClient injection (29 tests total, was 26).If you used the default Astroway constructor without overrides, no code changes required — auto-discovery picks up Guzzle if you already have it installed.
If you passed 'handlerStack' => $stack (Guzzle-specific): rebuild your customised stack into a configured GuzzleHttp\Client and pass it as 'httpClient' => $client instead.
Initial alpha release. Public API may shift before 0.1.0 proper based on integrator feedback.
Astroway client class built on Guzzle 7 (PSR-18 compatible). Sync only — PHP doesn't have a unified async story to mirror our TS / Python SDKs.X-Api-Key (default, matches curl/Postman) or Authorization: Bearer (matches Stripe/OpenAI/Anthropic convention) via 'authScheme' => 'bearer'.ApiError → BadRequestError / AuthenticationError / PermissionDeniedError / NotFoundError / UnprocessableEntityError / RateLimitError / InternalServerError / APIConnectionError (→ APITimeoutError).'retry' => ['maxRetries' => 0] to disable. Honors Retry-After (seconds or HTTP-date) on 429.timeout + connect_timeout options, default 30s.User-Agent: astroway-sdk-php/<version> (PHP/<php-version>; <os>) and X-Astroway-Channel: sdk-php. No telemetry, no phone-home.{ ok, data, error } envelope — methods return the data payload directly so user code reads naturally.errorCode property (not code) on ApiError to avoid clash with \Exception::$code.guzzlehttp/guzzle ^7.8 — HTTP client (PSR-18 compatible).psr/http-client, psr/http-message — interface contracts.Astroway\ → src/).[@astroway](https://github.com/astroway)/sdk and astroway (Python).Stable surface commitment. Public API frozen — every export shipped across alphas / betas / RC is now part of the 0.1.x contract. No code changes vs 0.1.0-rc.1 — same Astroway constructor, 100+ namespace services, 12-class error hierarchy, DTOs, helpers, PSR-16 cache, Guzzle promises concurrency, mock client, PSR-3 logger + metrics surface. Ready to be depended on.
Astroway — __construct, request, get, post, put, delete, concurrent, plus all 100+ namespace accessors (chart(), synastry(), ai(), tarot(), numerology(), ...). Removing or renaming requires 1.0.0.$options shape — apiKey, baseUrl, authScheme, timeout, retry, defaultHeaders, httpClient, requestFactory, streamFactory, idempotency, cache, cacheTtlSeconds, logger, metrics. Documented as a phpstan array shape on __construct.Astroway::VERSION and Astroway::DEFAULT_BASE_URL constants — public, documented, locked.*Error subtypes (BadRequest, Authentication, PermissionDenied, NotFound, UnprocessableEntity, RateLimit, QuotaExceeded, Calculation, InternalServer) all extend ApiError. User code catches them — collapsing breaks production silently.ApiError public properties — status, errorCode, requestId, creditsRemaining, retryAfterSeconds, body. Renaming any breaks support-ticket flows.Astroway\Testing\MockAstroway stays a subclass of Astroway so it remains drop-in for the namespace surface.tests/SurfaceLockTest.php) uses Reflection to enforce the above. PRs that drift fail CI before Packagist.phpstan analyse runs on every push — clean at level 6 (auto-generated Namespaces/*.php excluded for now; tightening is a 1.0.0 task).composer require astroway/sdk:^0.1.0 is a drop-in upgrade from any 0.1.0-rc.x. README has a migration table covering each pre-release stage.
118 PHPUnit tests pass (117 from rc.1 baseline + 11 new in tests/SurfaceLockTest.php). phpstan analyse clean.
How can I help you explore Laravel packages today?