Skip to content

Support Laravel 12.x and PHP 8.4 #56

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ jobs:
strategy:
matrix:
include:
- php: 8.4
illuminate: ^12.0
- php: 8.3
illuminate: ^11.0
- php: 8.2
Expand Down
56 changes: 56 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

### Testing
- Run all tests: `./vendor/bin/phpunit`
- Run a single test: `vendor/bin/phpunit tests/Path/To/TestFile.php`
- Run a single test method: `vendor/bin/phpunit --filter testMethodName`

### Development
- Install dependencies: `composer install`
- Update dependencies: `composer update`

## Architecture Overview

This is a Laravel package that provides DynamoDB integration by adapting Laravel's database layer to work with AWS DynamoDB.

### Key Design Patterns

1. **Adapter Pattern**: The package adapts Laravel's database abstractions to DynamoDB
- `Connection` extends Laravel's base connection class
- `Model` extends Eloquent with DynamoDB-specific behavior
- Query results are processed through `Processor` to match Laravel's expectations

2. **Builder Pattern**: DynamoDB queries are constructed using a fluent interface
- `Query\Builder` provides chainable methods
- Separate query objects for different DynamoDB operations (filter, condition, keyCondition)
- `ExpressionAttributes` manages placeholder generation for expressions

3. **Grammar Translation**: `Query\Grammar` translates Laravel-style queries to DynamoDB API format
- Uses AWS Marshaler for type conversions
- Compiles expressions using DynamoDB syntax
- Handles reserved words and attribute name conflicts

### Important Architectural Decisions

- **No Eloquent Relationships**: Models intentionally don't support relationships as DynamoDB is NoSQL
- **Primary Keys**: Models require `primaryKey` and optionally `sortKey` properties
- **Authentication**: Custom `AuthUserProvider` supports both primary key and API token authentication using DynamoDB indexes
- **Batch Operations**: Native support for DynamoDB batch operations (batchGetItem, batchPutItem, etc.)
- **Testing**: Use `dryRun()` method to inspect generated DynamoDB parameters without making API calls

### Testing Approach

Tests use Mockery to mock AWS SDK calls. When writing tests:
- Mock the DynamoDB client for unit tests
- Use `dryRun()` to test query building without API calls
- Follow existing test patterns in the `tests/` directory

### Version Compatibility

- PHP: 7.3, 7.4, 8.0, 8.1, 8.2, 8.3, 8.4
- Laravel: 6.x through 12.x
- AWS SDK: ^3.0
27 changes: 17 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Install the package via Composer:
$ composer require kitar/laravel-dynamodb
```

### Laravel (6.x, 7.x, 8.x, 9.x, 10.x, 11.x)
### Laravel (6.x, 7.x, 8.x, 9.x, 10.x, 11.x, 12.x)

Add dynamodb configs to `config/database.php`:

Expand Down Expand Up @@ -110,6 +110,14 @@ Update the `DB_CONNECTION` variable in your `.env` file:
DB_CONNECTION=dynamodb
```

> **Note for Laravel 11+**: Laravel 11 and later versions default to `database` driver for session, cache, and queue, which are not compatible with this DynamoDB package. You'll need to configure these services to use alternative drivers. For instance:
>
> ```
> SESSION_DRIVER=file
> CACHE_STORE=file
> QUEUE_CONNECTION=sync
> ```

### Non-Laravel projects

For usage outside Laravel, you can create the connection manually and start querying with [Query Builder](#query-builder).
Expand Down Expand Up @@ -397,7 +405,7 @@ Then specify driver and model name for authentication in `config/auth.php`.

### Registration Controller

You might need to modify the registration controller. For example, if we use Laravel Breeze, the modification looks like below.
You might need to modify the registration controller. For example, if we use Laravel Starter Kits, the modification looks like below.

```php
class RegisteredUserController extends Controller
Expand All @@ -408,31 +416,30 @@ class RegisteredUserController extends Controller
{
$request->validate([
'name' => 'required|string|max:255',
'email' => ['required', 'string', 'email', 'max:255', function ($attribute, $value, $fail) {
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', function ($attribute, $value, $fail) {
if (User::find($value)) {
$fail('The '.$attribute.' has already been taken.');
}
}],
'password' => 'required|string|confirmed|min:8',
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);

$user = new User([
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
$user->save();

Auth::login($user);

event(new Registered($user));

return redirect(RouteServiceProvider::HOME);
Auth::login($user);

return to_route('dashboard');
}
}
```

There are two modifications. The first one is adding the closure validator for `email` instead of `unique` validator. The second one is using the `save()` method to create user instead of the `create()` method.
The change is in the email validation rules. Instead of using the `unique` rule, we pass a closure to perform the duplicate check directly.

## Query Builder

Expand Down
12 changes: 6 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@
"ci-test": "vendor/bin/phpunit --coverage-clover coverage.xml"
},
"require": {
"php": "^7.3|^7.4|^8.0|^8.1|^8.2|^8.3",
"illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
"illuminate/container": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
"illuminate/database": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
"illuminate/hashing": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
"php": "^7.3|^7.4|^8.0|^8.1|^8.2|^8.3|^8.4",
"illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/container": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/database": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/hashing": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"aws/aws-sdk-php": "^3.0"
},
"require-dev": {
"illuminate/auth": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
"illuminate/auth": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"symfony/var-dumper": "^5.0|^6.0|^7.0",
"vlucas/phpdotenv": "^4.1|^5.0",
"mockery/mockery": "^1.3",
Expand Down
2 changes: 1 addition & 1 deletion src/Kitar/Dynamodb/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ protected function getDefaultPostProcessor()
*/
protected function getDefaultQueryGrammar()
{
return $this->withTablePrefix(new Query\Grammar());
return new Query\Grammar();
}

/**
Expand Down
8 changes: 5 additions & 3 deletions src/Kitar/Dynamodb/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,8 @@ public function newQuery()
*/
protected function process($query_method, $processor_method)
{
$table_name = $this->connection->getTablePrefix() . $this->from;

// Compile columns and wheres attributes.
// These attributes needs to interact with ExpressionAttributes during compile,
// so it need to run before compileExpressionAttributes.
Expand All @@ -590,13 +592,13 @@ protected function process($query_method, $processor_method)
// Compile rest of attributes.
$params = array_merge(
$params,
$this->grammar->compileTableName($this->from),
$this->grammar->compileTableName($table_name),
$this->grammar->compileIndexName($this->index),
$this->grammar->compileKey($this->key),
$this->grammar->compileItem($this->item),
$this->grammar->compileUpdates($this->updates),
$this->grammar->compileBatchGetRequestItems($this->from, $this->batch_get_keys),
$this->grammar->compileBatchWriteRequestItems($this->from, $this->batch_write_request_items),
$this->grammar->compileBatchGetRequestItems($table_name, $this->batch_get_keys),
$this->grammar->compileBatchWriteRequestItems($table_name, $this->batch_write_request_items),
$this->grammar->compileDynamodbLimit($this->limit),
$this->grammar->compileScanIndexForward($this->scan_index_forward),
$this->grammar->compileExclusiveStartKey($this->exclusive_start_key),
Expand Down
6 changes: 1 addition & 5 deletions src/Kitar/Dynamodb/Query/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public function __construct()
public function compileTableName($table_name)
{
return [
'TableName' => $this->tablePrefix . $table_name
'TableName' => $table_name
];
}

Expand Down Expand Up @@ -146,8 +146,6 @@ public function compileBatchGetRequestItems($table_name, $keys)
return $marshaler->marshalItem($key);
})->toArray();

$table_name = $this->tablePrefix . $table_name;

return [
'RequestItems' => [
$table_name => [
Expand All @@ -174,8 +172,6 @@ public function compileBatchWriteRequestItems($table_name, $request_items)
});
})->toArray();

$table_name = $this->tablePrefix . $table_name;

return [
'RequestItems' => [
$table_name => $marshaled_items,
Expand Down
2 changes: 2 additions & 0 deletions tests/Query/BuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,7 @@ public function it_can_process_scan_with_columns_specified()
public function it_can_process_process()
{
$connection = m::mock(Connection::class);
$connection->shouldReceive('getTablePrefix');
$connection->shouldReceive('scan')
->with(['TableName' => 'Forum'])
->andReturn(new Result(['Items' => []]))
Expand All @@ -1179,6 +1180,7 @@ public function it_can_process_process()
public function it_can_process_process_with_no_processor()
{
$connection = m::mock(Connection::class);
$connection->shouldReceive('getTablePrefix');
$connection->shouldReceive('putItem')
->with([
'TableName' => 'Thread',
Expand Down