zaghyr/eloquent-dynamodb
Bring Eloquent-style models to Amazon DynamoDB in Laravel. Define DynamoDB-backed models with familiar syntax, query and persist data without raw AWS SDK calls, and keep application code consistent with the rest of your Eloquent stack.
v0.3.0 is a dependency upgrade of laravel-dynamodb, not a feature release — there's nothing new to adopt, but you should treat it as a "re-verify" release rather than a no-op.
composer update baopham/laravel-dynamodb --with-dependencies, then diff composer.lock. The upgrade pulls a transitive aws/aws-sdk-php; a minor/major jump there is the actual risk, not the Laravel wrapper.BaoPham\DynamoDb\DynamoDbServiceProvider to config/app.php. The facade is BaoPham\DynamoDb\Facades\DynamoDb if you prefer the container-free style.php artisan vendor:publish --provider="BaoPham\DynamoDb\DynamoDbServiceProvider" gives you config/dynamodb.php with key, secret, region, local_endpoint, and debug.docker run -p 8000:8000 amazon/dynamodb-local, set DYNAMODB_LOCAL_ENDPOINT=http://localhost:8000, and create the table with aws dynamodb create-table --endpoint-url http://localhost:8000 ....class UserPreference extends DynamoDbModel
{
protected $table = 'user_preferences';
protected $fillable = ['user_id', 'theme', 'locale'];
protected $primaryKey = 'user_id';
}
UserPreference::create(['user_id' => 1, 'theme' => 'dark']);
UserPreference::find(1);
Where to look first: the DynamoDbModel and DynamoDbQueryBuilder source in vendor/baopham/laravel-dynamodb/src, and the published config file. Those three files are the entire mental model — treat the README as a summary, not the spec.
Model per table, with composite keys where the access pattern demands it. Use $primaryKey for a hash-only table and $compositeKey = ['pk', 'sk'] when you have a range key. find() takes a scalar or an array for composite keys. Don't try to fight the key design later — DynamoDB key schema is immutable after creation (you'd have to build a new table and migrate).
Design access patterns before models. Write down every read your app performs. Then choose pk/sk so each read is a Query, not a Scan. Single-table design works, but this package's Eloquent veneer actively hides the item-collection semantics — for most teams, one table per entity plus a couple of GSIs is the lower-friction path.
Wrap it in a repository. DynamoDB's constraints (no joins, no lazy loading, cursor pagination, eventually consistent index reads) leak badly into controllers. A thin repository/service per table lets you swap to raw DynamoDbClient calls later without touching controllers, which you will inevitably need.
Be explicit about Query vs Scan. The builder falls back to a Scan when your where clauses don't cover the primary key. That fallback is silent and expensive. Use ->query() / ->scan() / ->onScan() to make intent visible in code review, and log the generated request via ->getQuery() or ->dd() during development.
Cursor pagination, not offset. DynamoDB returns LastEvaluatedKey; there is no page=2. Implement cursor pagination yourself with ExclusiveStartKey, expose the token as an opaque base64 string in an API Resource, and never let the frontend assume stable offsets.
Push batch and conditional operations to the raw client. The model layer is one-item-at-a-time. For bulk seeding, BatchWriteItem (25 items max per call); for idempotency or optimistic locking, UpdateItem with a ConditionExpression such as attribute_not_exists(pk) or version = :expected. updateOrCreate / firstOrCreate are not concurrency-safe here — they read-then-write.
Handle throttling explicitly. Catch Aws\Exception\AwsException, check for ProvisionedThroughputExceededException / RequestLimitExceeded, and retry with jittered exponential backoff. On Lambda or ECS, let the IAM role supply credentials and leave key/secret null.
Test against DynamoDB Local, not a mock. Bind a test DynamoDbClientInterface in the container, or point local_endpoint at the local instance from phpunit.xml. Create and drop tables in setUpBeforeClass / tearDownAfterClass, with a per-run table-name prefix so parallel test runs don't collide.
Use $casts for map/list attributes. DynamoDB natively stores maps and lists, so protected $casts = ['settings' => 'array'] is genuinely useful. Dates are less graceful — store them as ISO-8601 strings or epoch integers explicitly rather than trusting Eloquent's $dates marshalling.
Illuminate\Database\Eloquent\Model. You must extend BaoPham\DynamoDb\DynamoDbModel. Relationships, with(), save() cascades, and many model events simply don't exist. If your domain needs joins, DynamoDB is the wrong tool.where on a non-key attribute is a filter, not a narrowing of the read. You pay for every item read before filtering. Same for Scan filters. Check ConsumedCapacity in CloudWatch after a feature ships.limit applies before filtering. A filtered query can legitimately return zero items even though matching items exist further along. Always loop on LastEvaluatedKey until exhausted or the limit is reached.getItem and base-table Query support ConsistentRead; GSI reads and Scan do not. Read-after-write against an index is a classic source of flaky tests and "missing record" bugs.status, size, data, timestamp need ExpressionAttributeNames escaping. The builder may not do it for you — name your attributes defensively or drop to the raw client.BatchWriteItem, 100 per BatchGetItem. Design for them rather than discovering them at scale.config:cache will bake in local_endpoint and any hard-coded credentials. Guard the config file with env() defaults that are null in production, and verify local_endpoint is absent after config:cache in your deploy pipeline.debug => true logs full request payloads. Never enable it in production — request bodies routinely contain PII.DynamoDbModel/DynamoDbQueryBuilder, check the transitive AWS SDK version delta, run your DynamoDB Local integration suite, and smoke-test one query and one conditional write against a staging table before promoting.How can I help you explore Laravel packages today?