If you just want to install it without reading the whole article, you can install it via Composer: rikudou/aws-sdk-phpstan.
When using PHPStan alongside AWS, you end up making a lot of type assertions, because the automatically generated AWS SDK doesn’t strictly define types, so everything defaults to mixed
. Fortunately, the SDK package includes the source data used to generate itself, so I reused that information to create a PHPStan extension. This extension provides precise type definitions, catches all type-related errors for return types, and allows you to remove those otherwise unnecessary type assertions.
How It’s Made
As mentioned earlier, if all you want is to install and use the package, you don’t really need this article. But if you want a closer look at how it works, read on.
The first step was to make the Result
class (which is returned by all API calls) generic by providing a custom stub—particularly for its get()
method:
/**
* @template T of array<string, mixed>
* @implements ResultInterface<T>
*/
class Result implements ResultInterface, MonitoringEventsInterface
{
/**
* @template TKey of (key-of<T>|string)
* @param TKey $key
* @return (TKey is key-of<T> ? T[TKey] : null)
*/
public function get(string $key): mixed {}
}
The class itself is generic, constrained to an array. The get()
method is also made generic based on the key. If the key is a known key of T
, the method returns its corresponding value; otherwise, it returns null
. Essentially, if the response type expects the property, it’s returned—if not, null
is returned.
The Individual Clients
All the client classes are generated from the type definitions in the src/data directory of the official AWS SDK for PHP. Each client’s definitions come in two files, such as:
(These map one-to-one with the PHP client methods. You’ll notice that the actual PHP client just uses __call()
for these methods.)
For example, in the JSON definition for S3Client, you might see:
{
"GetObject":{
"name":"GetObject",
"http":{
"method":"GET",
"requestUri":"/{Bucket}/{Key+}"
},
"input":{"shape":"GetObjectRequest"},
"output":{"shape":"GetObjectOutput"},
"errors":[
{"shape":"NoSuchKey"},
{"shape":"InvalidObjectState"}
],
"documentationUrl":"http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectGET.html",
"httpChecksum":{
"requestValidationModeMember":"ChecksumMode",
"responseAlgorithms":[
"CRC64NVME",
"CRC32",
"CRC32C",
"SHA256",
"SHA1"
]
}
}
}
And in PHP:
use Aws\S3\S3Client;
$client = new S3Client([]);
$object = $client->getObject([
'Bucket' => 'test',
'Key' => 'test',
]);
In reality, these methods don’t actually exist in the client class; they’re invoked through __call()
under the hood.
Going back to the JSON definitions, each operation has an input
and an output
shape. The package currently only focuses on the output
shape, although I plan to add input
shape support in the future. For the GetObjectOutput
, the relevant shape might be:
{
"GetObjectOutput":{
"type":"structure",
"members":{
"Body":{
"shape":"Body",
"streaming":true
},
"DeleteMarker":{
"shape":"DeleteMarker",
"location":"header",
"locationName":"x-amz-delete-marker"
},
"AcceptRanges":{
"shape":"AcceptRanges",
"location":"header",
"locationName":"accept-ranges"
},
"Expiration":{
"shape":"Expiration",
"location":"header",
"locationName":"x-amz-expiration"
},
"Restore":{
"shape":"Restore",
"location":"header",
"locationName":"x-amz-restore"
},
"LastModified":{
"shape":"LastModified",
"location":"header",
"locationName":"Last-Modified"
}
}
}
(Note: The actual shape is larger, but I’ve omitted some fields to keep this article shorter.)
Generating Type Extensions
PHPStan lets you add extensions that generate return types based on the method call and its input parameters. I decided to take that approach, though there are other possibilities (such as generating a stub file for each client).
For every client, a class like {ShortAwsClientClassName}TypeExtension
is generated, for example, S3ClientReturnTypeExtension. The isMethodSupported()
method just checks if the method name matches one of the operations defined in the JSON file. Then there’s a getTypeFromMethodCall()
method that uses a match
expression to call a private method of the same name.
Those private methods return PHPStan types derived from the shapes in the JSON data. The generator supports lists, maps, nested structures, binary blobs, date/time objects, enums, and simple types (strings, booleans, integers, floats), including unions, nested arrays, and more.
If you want to dive into the code:
As a final touch, the extension.neon
file (which registers extensions with PHPStan) is populated automatically with each generated class.
Performance
The performance isn’t ideal if you include every single client class by default—which is understandable considering there are around 400 classes, each containing thousands of lines. Most projects likely won’t use all 400 AWS services in one codebase. That’s why I provided a generation script as a Composer binary, along with support for specifying only the clients you need by updating your composer.json
:
{
"extra": {
"aws-sdk-phpstan": {
"only": [
"Aws\\S3\\S3Client",
"Aws\\CloudFront\\CloudFrontClient"
]
}
}
}
After that, run vendor/bin/generate-aws-phpstan
to regenerate the classes. The script deletes all existing type extensions first, then generates only for S3Client
and CloudFrontClient
. It also updates the extensions.neon
file with just those extensions. With only a few extensions active, there’s no noticeable slowdown in PHPStan.
You can also leverage Composer’s script events to run the binary automatically:
{
"extra": {
"aws-sdk-phpstan": {
"only": [
"Aws\\S3\\S3Client",
"Aws\\CloudFront\\CloudFrontClient"
]
}
},
"scripts": {
"post-install-cmd": [
"generate-aws-phpstan"
],
"post-update-cmd": [
"generate-aws-phpstan"
]
}
}
Ideally, the official SDK itself would include type definitions (it shouldn’t be too difficult, given they already generate the SDK from these JSON files). In the meantime, though, I’m pretty happy with how this little project turned out.