This is a library and an Object Document Mapper to use with AWS DynamoDB in a more convenient way.
Set up native client:
$dynamoDbClient = new DynamoDbClient(array_merge(
[
'region' => 'eu-west-2',
'version' => 'latest',
]
));
Set up the main operations lib client:
$client = new DynamodbOperationsClient($dynamoDbClient);
Set up the marshaller. Native AWS marshaller may be taken:
$marshaler = new Marshaler();
Set up the Query builder:
$queryBuilder = new QueryBuilder($marshaler, new ExpressionFactory($marshaler));
Set up annotation reader and annotation manager:
// annotation reader
$annotationReader = new AnnotationReader();
// annotation manager
$annotationManager = new AnnotationManager($annotationReader);
The hydrators for the models:
$newModelHydrator = new Hydrator(NewModel::class, $annotationManager);
$sortKeyModelHydrator = new Hydrator(SortKeyModel::class, $annotationManager);
Serializer for inserting records into DB:
// serializer for
$serializer = new Serializer($annotationManager);
The full example is in here.
The lib operates with models. Each model may have various supported field types. Here is a list of types which correlate with PHP and Dynamodb types:
BooleanType
: boolean for DynamoDb and for phpCollectionType
: list for DynamoDb, in php it's an array list of items of specific modelDateType
: string for DynamoDb, DateTime for phpEnumType
: string for DynamoDb, enum for phpFloatType
: number for DynamoDb, float for phpHashMapType
: map for DynamoDb, in php it's associative array of items of specific modelIntegerType
: number for DynamoDb, int for phpModelType
: map for DynamoDb, instance of model for phpMoney
: map for DynamoDb, special MoneyObject for PHP. Money value as a conceptNumberType
: number for DynamoDb. An abstract type, not a handy one. My be used occasionallyScalarCollectionType
: map for DynamoDb. in php it's associative array of any dynamodb compatible types excludingCollectionType
,HashMapType
orModelType
StringType
: string for DynamoDb and for php
Here is a model example:
class ExampleDemoModel extends Model
{
protected const TABLE_NAME = 'test-table';
// Primary means that this is a partition key for the DynamoDb table
#[StringType, Primary] protected string $id;
#[StringType] protected string $name;
#[FloatType] protected float $price;
#[Money] protected Money $priceNet;
#[FloatType] protected float $percent;
#[IntegerType] protected int $itemsAmount;
#[DateType] protected DateTime $createdAt;
#[BooleanType] protected bool $isDeleted;
#[BooleanType] protected bool $isPhoneNumber;
#[ModelType([ModelType::MODEL_CLASS_NAME => RelatedModel::class])]
protected RelatedModel $buyer;
#[CollectionType([CollectionType::MODEL_CLASS_NAME => RelatedModel::class])]
protected array $buyers;
#[ModelType([Asset::MODEL_CLASS_NAME => Asset::class])]
protected Asset $asset;
#[HashMapType([HashMapType::MODEL_CLASS_NAME => RelatedModel::class])]
protected array $buyersMap;
// getter and setters should be here
}
Model full example is here: model.php
Enumerations are also supported. Here is an example of the model with enumeration fields:
class ModelWithEnumeration extends Model
{
#[Primary, StringType]
protected string $id;
#[EnumType]
protected OrderStatus $orderStatus;
// union types
#[EnumType]
protected OrderStatus|ApplicationStatus $unionStatus;
// union types with null
#[EnumType]
protected OrderStatus|ApplicationStatus|null $unionNullableStatus;
// isStrict means the value will be null in case wrong value comes from the DB
#[EnumType(isStrict: false)]
protected ?OrderStatus $orderStatusAdditional = null;
#[EnumType]
protected CustomerType $customerType;
}
Certain types custom encryption is supported. In case there are some fields which needs to be encrypted.
First of all we need to create a custom encryptor:
MyEncryptor implements EncryptorInterface {
protected const ENCRYPTION_KEY = 'def000008053addc0f94b14c0e480a10631a0a970b3565e5a7a2aeaeeb51a39e2d139a8977bc02be0195f0036a29aefff9df6d2ddb81432d14b4dce82b83b3a95c6d0205';
public function decrypt(string|array $encryptedData, array $options = []): string|array
{
// any decryption way may be implemented
if (is_array($encryptedData)) {
// ...specific property decryption operations...
return $encryptedData;
}
return Crypto::decrypt(
$encryptedData,
Key::loadFromAsciiSafeString(static::ENCRYPTION_KEY)
);
}
}
Then the decryptor should be passed into the hydrator:
$newModelHydrator = new Hydrator(
EncryptionDemoModel::class,
$annotationManager,
new MyEncryptor(),
);
And the model may be the following:
class EncryptionDemoModel extends Model
{
#[Key\Primary, Types\StringType]
protected string $id;
// ability to encrypt a specific property in a scalar associative array
#[Types\ScalarCollectionType, Encrypted(["encryptedProperty" => "secretProperty"])]
protected array $encryptedArray;
#[Types\StringType, Encrypted]
protected string $encryptedName;
}
The best way to operate with records is to create a repository. There is a built-in already:
$newModelDynamoDbRepository = new DynamoDBRepository(
NewModel::class,
$client,
$queryBuilder,
$newModelHydrator,
$annotationManager,
$marshaler,
$serializer
);
There are built-in operation in the default repository.
$foundModel = $newModelDynamoDbRepository->get($id);
$foundModel = $newModelDynamoDbRepository->get($id, $sortKey);
$foundModel = $newModelDynamoDbRepository->get($id, $sortKey, false);
$foundModel = $newModelDynamoDbRepository->getOneById($id, $sortKey, false);
$newModelDynamoDbRepository->save($model);
$newModelDynamoDbRepository->delete($model);
Sometimes we need to fetch not the whole model, but just a part of it. For this purpose there is such called DocumentRepository
. The part of the document may be technically fetched using native DynamoDb projection expressions.
Setting DocumentRepository
up:
$documentRepository = new DocumentRepository(
NewModelNested::class,
$client,
$this->queryBuilder,
$this->newModelHydrator,
$annotationManager,
$marshaler,
$serializer
);
$projectionExpression = "property.subPropertyModel";
$model = $documentRepository->getDocument()
->setConsistentRead(true)
->withAttrPath($projectionExpression)
->withPrKey($keyValue)
->execute()
;
$projectionExpression = "property.subPropertyModel.name";
$name = $this->documentRepository->getDocumentProperty()
->setConsistentRead(true)
->withAttrPath($projection)
->withPrKey($keyValue)
->execute()
;
Document repository supports specific property get/create/update/delete operations:
createDocument()
updateDocument()
removeDocument()
getDocumentCollection()
updateDocumentCollection()
createDocumentCollection()
Another powerful feature is query builders. This adds flexibility to fetch items by specific criteria which is supported by DynamoDB.
This is a way to work with the Dynamodb using raw queries and results
Fetch items:
$getItemQuery = $queryBuilder
->getItem(self::DB_TABLE)
->itemKey([$itemKey => $keyValue])
->getQuery();
$item = $this->dynamoDbClient
->getItem($getItemQuery)->get('Item');
Ability to update specific attributes.
$attributesForUpdate = [
"numberProp" => 2,
"stringProp" => "updated string value",
"hashMapProp.map-id-1.type" => "updated map-type-1",
"hashMapProp.map-id-1.mapProp" => "updated mapProp",
"listProp" => [
"updated listProp 1",
"updated listProp 2"
]
];
$getItemQuery = $queryBuilder
->updateItem(self::DB_TABLE)
->itemKey([$itemKey => $keyValue])
->attributes($attributesForUpdate)
->getQuery();
$dynamoDbClient->updateItem($getItemQuery);
- In order to build a dev image, please, run:
docker-compose build
- Then run to install dependencies:
docker-compose run --no-deps dynamodb-odm composer install
This package uses phpspec for running unit tests.
Run them using the following way:
docker-compose run --no-deps dynamodb-odm vendor/bin/phpspec run
One can use environment variables in the .env.local
file to be able to debug the library. For this just Copy file .env.local.sample into .env.local and set up the variable according to your OS.
And then run the tests with:
docker-compose --env-file ./.env.local run --no-deps dynamodb-odm vendor/bin/phpspec run
This package uses behat for running functional tests.
Then just run the tests:
docker-compose run dynamodb-odm vendor/bin/behat -c behat.yml --stop-on-failure
You need to check if the code style is OK by running:
docker-compose run --no-deps dynamodb-odm vendor/bin/phpcs --standard=/application/phpcs.xml