apimatic/core
Core library for APIMatic-generated PHP SDKs. It supplies shared request construction, query/header/form/body parameter handling, and common utilities so generated clients stay consistent. Supports PHP 7.2 through 8.4; install with composer require apimatic/core.
This is not a package you normally write code against. apimatic/core is the runtime substrate that APIMatic's code generator emits into your SDKs. In practice you meet it as a transitive dependency of a generated client (e.g. composer show apimatic/core reveals it pulled in by a *-sdk package). Treat direct composer require "apimatic/core" as the exception, not the rule — if a generated SDK pins a version range, adding your own constraint is how you get composer conflicts.
Minimal steps:
# Usually this, not the core lib directly
composer require vendor/some-api-sdk
# Confirm the runtime is present and which version the SDK pinned
composer show apimatic/core
There is no service provider, no config/apimatic.php to publish, no facade. Everything is constructor injection and plain objects. First thing to read is src/Request/RequestBuilder.php and src/Response/ResponseHandler.php — those two files explain 80% of the call path. Then src/Logger/ApiLogger.php and src/TestCase/CoreTestCase.php.
First use case: wrap the generated SDK in a Laravel service class, bind it as a singleton with credentials from config(), and route the SDK's logging through your application log channel:
// app/Providers/ApiServiceProvider.php
$this->app->singleton(SomeApiClient::class, fn () => new SomeApiClient(
config('services.someapi.token')
));
Then, once something breaks in staging, the second use case is turning on request/response logging (see below) — that's the fastest path from "SDK returns null" to "here's the 400 body".
Isolate the SDK behind your own boundary. Generated clients expose hundreds of methods with generated DTOs. Don't let those DTOs leak into controllers or Blade views. Wrap in a repository-ish class, map to your own arrays/models, and let the SDK be swappable:
final class PaymentGateway
{
public function __construct(private SomeApiClient $client) {}
public function charge(Order $order): array
{
$resp = $this->client->getPaymentsApi()->createPayment($order->total_cents);
return ['id' => $resp->getId(), 'status' => $resp->getStatus()];
}
}
Inject cross-cutting concerns as additional params. AdditionalHeaderParams, AdditionalQueryParams, and AdditionalFormParams exist precisely for the things your app knows and the API definition doesn't — correlation IDs, tenant IDs, feature flags. Build them from the current request context rather than hardcoding:
new AdditionalHeaderParams([
'X-Request-Id' => (string) Str::uuid(),
'X-Tenant' => tenant()->id,
]);
Logging is opt-in and defaults to silence. NullApiLogger is the default; nothing is recorded until you pass a LoggingConfiguration. Bridge it to Laravel's logger with a small custom ApiLogger implementation that calls Log::channel('api'), then supply RequestConfiguration / ResponseConfiguration with sensitive headers excluded (see LoggerConstants::NON_SENSITIVE_HEADERS — note the name; anything not in that allowlist must be treated as secret). Long-running queue workers should log at debug level only, or you'll flood the channel.
Error handling via ResponseError + ErrorType gives you typed exceptions per HTTP status range. Catch the SDK's specific exception classes, not \Throwable, and translate them into your app's domain exceptions at the boundary. Since 0.3.7 the library throws the actual exceptions rather than wrappers, so instanceof checks now behave the way you'd expect — but that also means any code written against the older wrapper types needs review.
Testing with CoreTestCase is where this package earns its keep in a Laravel codebase. The matchers let you assert on the raw wire exchange without standing up a real server:
$testCase = new CoreTestCase();
$testCase->assertResponse(
$response,
new StatusCodeMatcher(200),
new HeadersMatcher(['Content-Type' => 'application/json']),
new KeysAndValuesBodyMatcher(['id' => 'abc', 'status' => 'ok'])
);
Use KeysBodyMatcher when you only care about schema shape, RawBodyMatcher only when byte-exact output matters (signing, hashing). For integration tests inside Laravel, prefer Http::fake() against the SDK's underlying client where possible, and reserve CoreTestCase for contract tests against recorded fixtures.
Retries and queues: the core lib has no retry policy. Put retries in your job layer with Laravel's retry()/backoff(), and only retry idempotent operations — the SDK will happily re-send a POST.
0.3.7 exception unwrapping, 0.3.5 simple-date deserialization, 0.3.4 negative RFC3339 offsets, 0.3.2 unquoted multipart booleans). Pin with ^0.3.x at minimum and read release notes before bumping — a date-parsing fix can silently change your persisted timestamps.ConsoleLogger writes to stdout. In an artisan command that's fine; in a web request it corrupts JSON responses and in Octane it leaks into the worker output. Always swap in a Log-backed implementation.Authorization, cookies, and request bodies containing PII are the usual leaks. Test by logging a request with a dummy bearer token and grepping the output.null unexpectedly, dump the Context object (available on ResponseHandler) — it holds the raw request, raw response, and intermediate details, which is far quicker than adding dd() around the SDK."false" as truthy). If an older generated SDK drags in an older core, this is a real production bug.ApiLogger, custom body matchers extending the provided ones, and custom type handlers alongside DeserializableType for enums/dates. Prefer wrapping over subclassing where you can, since generated code is regenerated and your subclasses must survive that.How can I help you explore Laravel packages today?