imper86 / dynamodb-client
Object oriented DynamoDB Client
Requires
- php: >=8.1
- composer-runtime-api: ^2.0
- php-http/client-common: ^2.3
- php-http/discovery: ^1.11
- php-http/promise: ^1.1
- phpdocumentor/reflection-docblock: ^5.2 || ^6.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.0 || ^2.0
- symfony/property-access: ^6.4 || ^7.4 || ^8.0
- symfony/property-info: ^6.4.33 || ^7.4 || ^8.0
- symfony/serializer: ^6.4 || ^7.4 || ^8.0
- webmozart/assert: ^1.10 || ^2.0
Requires (Dev)
- captainhook/captainhook-phar: ^5.29
- friendsofphp/php-cs-fixer: ^3.95
- nyholm/psr7: ^1.8
- php-http/curl-client: ^2.4
- php-http/message: ^1.16
- php-http/mock-client: ^1.6
- phpstan/phpstan: ^2.2
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpstan/phpstan-webmozart-assert: ^2.0
- phpunit/phpunit: ^10.5 || ^11.5 || ^12.5 || ^13.3
- rector/rector: ^2.6
- shipmonk/composer-dependency-analyser: ^1.8
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-28 18:58:55 UTC
README
An object-oriented PHP client for the Amazon DynamoDB API.
- It maps the AWS API one to one. Every operation takes a request object and returns a response object, and each class and property is named after the matching type and member in the AWS API reference. You can read the AWS docs and know which class to use.
- It uses typed, immutable value objects. Requests, responses and models are
finalclasses withreadonlyproperties, and every fixed set of values is a backed enum. The code passes PHPStan at level 10, so your IDE and static analyser can check your calls too. - It is built on PSR standards and has few dependencies. It works with any PSR-18 HTTP client and any PSR-17 factories. It signs requests with Signature V4 itself, so you do not need the AWS SDK.
Installation
composer require imper86/dynamodb-client
You need PHP 8.1 or newer, plus a PSR-18 HTTP client and PSR-17 factories. If your project has none yet, install one, for example:
composer require symfony/http-client nyholm/psr7
The client finds them through php-http/discovery. It works
with Symfony Serializer 6.4, 7.4 and 8.x.
Symfony bundle
If you use Symfony, install
imper86/dynamodb-client-bundle instead. It
registers the client as an autowirable service that you configure in YAML, and it sends requests
through your app's PSR-18 client when there is one, so they show up in the profiler.
composer require imper86/dynamodb-client-bundle
Creating the client
use Imper86\DynamoDBClient\DynamoDBClient; use Imper86\DynamoDBClient\Model\Credentials; // Credentials come from AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and, if set, AWS_SESSION_TOKEN. $client = new DynamoDBClient('eu-central-1'); // Or you pass them in yourself. $client = new DynamoDBClient( region: 'eu-central-1', credentials: new Credentials('AKIA...', 'secret', token: null), );
The constructor also accepts your own PSR-18 httpClient, PSR-17 requestFactory and
streamFactory, a Symfony serializer, and an endpoint.
Requests go to https://dynamodb.<region>.amazonaws.com unless you set an endpoint. The client picks
the first of these that is set, the same order the AWS SDKs use:
- the
endpointconstructor argument - the
AWS_ENDPOINT_URL_DYNAMODBenvironment variable - the
AWS_ENDPOINT_URLenvironment variable
Set AWS_IGNORE_CONFIGURED_ENDPOINT_URLS=true to make the client ignore both variables. The endpoint
must be an absolute http or https url.
Running against DynamoDB Local
DynamoDB Local runs in Docker:
docker run -p 8000:8000 amazon/dynamodb-local
Point the client at it. DynamoDB Local does not check credentials, but requests still have to be signed, so pass any key and secret:
$client = new DynamoDBClient( region: 'us-east-1', credentials: new Credentials('local', 'local'), endpoint: 'http://localhost:8000', );
You can also leave your code as it is and switch to DynamoDB Local in the environment, for example
in compose.yaml:
services: app: environment: AWS_ENDPOINT_URL_DYNAMODB: http://dynamodb:8000 AWS_ACCESS_KEY_ID: local AWS_SECRET_ACCESS_KEY: local dynamodb: image: amazon/dynamodb-local
DynamoDB Local keeps a separate database for each access key and region, unless you start it with
-sharedDb. Use the same credentials and region everywhere, or you will not see your tables.
Type your dependencies against DynamoDBClientInterface, which makes the client easy to mock in tests.
Usage
Attribute values
An item is an AttributeValueMap of AttributeValues. The named constructors cover every DynamoDB
type:
use Imper86\DynamoDBClient\Model\AttributeValue; use Imper86\DynamoDBClient\Model\AttributeValueMap; $item = new AttributeValueMap([ 'Artist' => AttributeValue::string('No One You Know'), 'SongTitle' => AttributeValue::string('Call Me Today'), 'Year' => AttributeValue::number(2015), // int, float or numeric string 'Genres' => AttributeValue::stringSet('Country', 'Pop'), 'Awards' => AttributeValue::map([ 'Grammy' => AttributeValue::bool(false), ]), ]);
The full list is blob(), bool(), blobSet(), list(), map(), number(), numberSet(),
null(), string() and stringSet(). To read a value back, use the matching property, such as
$value->string, $value->number or $value->map.
Creating a table
use Imper86\DynamoDBClient\Message\CreateTableRequest; use Imper86\DynamoDBClient\Model\AttributeDefinition; use Imper86\DynamoDBClient\Model\AttributeDefinitionList; use Imper86\DynamoDBClient\Model\BillingMode; use Imper86\DynamoDBClient\Model\KeySchemaElement; use Imper86\DynamoDBClient\Model\KeySchemaElementList; use Imper86\DynamoDBClient\Model\KeyType; use Imper86\DynamoDBClient\Model\ScalarAttributeType; $client->createTable(new CreateTableRequest( tableName: 'Music', attributeDefinitions: new AttributeDefinitionList([ new AttributeDefinition('Artist', ScalarAttributeType::STRING), new AttributeDefinition('SongTitle', ScalarAttributeType::STRING), ]), billingMode: BillingMode::PAY_PER_REQUEST, keySchema: new KeySchemaElementList([ new KeySchemaElement('Artist', KeyType::HASH), new KeySchemaElement('SongTitle', KeyType::RANGE), ]), ));
Writing and reading items
use Imper86\DynamoDBClient\Message\GetItemRequest; use Imper86\DynamoDBClient\Message\PutItemRequest; $client->putItem(new PutItemRequest( item: $item, tableName: 'Music', conditionExpression: 'attribute_not_exists(SongTitle)', )); $response = $client->getItem(new GetItemRequest( key: new AttributeValueMap([ 'Artist' => AttributeValue::string('No One You Know'), 'SongTitle' => AttributeValue::string('Call Me Today'), ]), tableName: 'Music', consistentRead: true, )); if (null === $response->item) { // DynamoDB reports "no such item" as an absent Item. } else { echo $response->item->get('Year')?->number; // "2015" }
Querying with pagination
use Imper86\DynamoDBClient\Message\QueryRequest; use Imper86\DynamoDBClient\Model\ReturnConsumedCapacity; $startKey = null; do { $page = $client->query(new QueryRequest( tableName: 'Music', exclusiveStartKey: $startKey, expressionAttributeValues: new AttributeValueMap([ ':artist' => AttributeValue::string('No One You Know'), ]), keyConditionExpression: 'Artist = :artist', returnConsumedCapacity: ReturnConsumedCapacity::TOTAL, )); foreach ($page->items as $item) { echo $item->get('SongTitle')?->string, PHP_EOL; } $startKey = $page->lastEvaluatedKey; } while (null !== $startKey);
Batch writes
use Imper86\DynamoDBClient\Message\BatchWriteItemRequest; use Imper86\DynamoDBClient\Model\WriteRequest; use Imper86\DynamoDBClient\Model\WriteRequestList; use Imper86\DynamoDBClient\Model\WriteRequestListMap; $response = $client->batchWriteItem(new BatchWriteItemRequest( requestItems: new WriteRequestListMap([ 'Music' => new WriteRequestList([ WriteRequest::put($item), WriteRequest::delete($key), ]), ]), )); // Retry anything DynamoDB did not process. $response->unprocessedItems;
Named constructors on requests
Some requests have several mutually exclusive modes. These requests have a factory for each mode, so you do not have to build the nested models yourself:
UpdateTimeToLiveRequest::enable('Music', 'ExpiresAt'); UpdateContinuousBackupsRequest::enable('Music', recoveryPeriodInDays: 7); RestoreTableToPointInTimeRequest::latest(...); // or ::at(...) ExportTableToPointInTimeRequest::full(...); // or ::incremental(...) ImportTableRequest::csv(...); // or ::dynamoDbJson(...), ::ion(...) TagResourceRequest::tags($arn, ['env' => 'prod']);
If an operation takes only optional parameters, you can call it without a request object:
$client->listTables(), $client->listBackups(), $client->describeLimits().
Error handling
Every exception the client throws implements Imper86\DynamoDBClient\Exception\ExceptionInterface:
| Exception | When |
|---|---|
BadResponseException |
DynamoDB answered with a status other than 200. Its $response property holds the PSR-7 response, whose body contains the AWS error __type and message. |
HttpClientException |
The PSR-18 client failed, for example with a network error. |
RequestSerializationException |
The request object could not be serialized. |
ResponseDeserializationException |
The response body could not be turned into the response class. |
InvalidArgumentException |
A value broke a constraint of the API. |
MissingCredentialsException |
You passed no credentials and the environment does not provide any. |
use Imper86\DynamoDBClient\Exception\BadResponseException; use Imper86\DynamoDBClient\Exception\ExceptionInterface; try { $client->deleteTable(new DeleteTableRequest('Music')); } catch (BadResponseException $e) { $error = json_decode((string) $e->response->getBody(), true); } catch (ExceptionInterface $e) { // anything else from the client }
Models and requests check the API's constraints, such as sizes, counts and mutually exclusive members,
in their constructors. When you build one yourself outside a client call, it throws
Webmozart\Assert\InvalidArgumentException.
Supported operations
52 operations of the DynamoDB API (DynamoDB_20120810) are covered:
batchExecuteStatement, batchGetItem, batchWriteItem, createBackup, createTable,
deleteBackup, deleteItem, deleteResourcePolicy, deleteTable, describeBackup,
describeContinuousBackups, describeContributorInsights, describeEndpoints, describeExport,
describeImport, describeKinesisStreamingDestination, describeLimits, describeTable,
describeTableReplicaAutoScaling, describeTimeToLive, disableKinesisStreamingDestination,
enableKinesisStreamingDestination, executeStatement, executeTransaction,
exportTableToPointInTime, getItem, getResourcePolicy, importTable, listBackups,
listContributorInsights, listExports, listImports, listTables, listTagsOfResource, putItem,
putResourcePolicy, query, restoreTableFromBackup, restoreTableToPointInTime, scan,
searchVectors, tagResource, transactGetItems, transactWriteItems, untagResource,
updateContinuousBackups, updateContributorInsights, updateItem,
updateKinesisStreamingDestination, updateTable, updateTableReplicaAutoScaling,
updateTimeToLive.
The operations of the legacy Global Tables version 2017.11.29 are not covered: CreateGlobalTable,
DescribeGlobalTable, DescribeGlobalTableSettings, ListGlobalTables, UpdateGlobalTable and
UpdateGlobalTableSettings. For current global tables, add replicas with updateTable instead.
Known limitations
- The client does not retry. Retries on throttling and
UnprocessedItems/UnprocessedKeysare up to you. ReturnConsumedCapacity::INDEXESis not supported yet:ConsumedCapacitycannot read the per-table breakdown it returns, so the response fails to deserialize. UseTOTAL.
Development
composer fix # php-cs-fixer + Rector composer analyse # code style, PHPStan (level 10), Rector, dependency analysis, PHPUnit composer unit # PHPUnit only
The integration tests run against DynamoDB Local and are not part of composer analyse:
docker compose up -d DYNAMODB_LOCAL_ENDPOINT=http://localhost:8000 composer integration
License
MIT. See LICENSE.