diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 3bdbbd4..76f91e2 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -10,6 +10,8 @@ jobs: strategy: matrix: include: + - php: 8.4 + illuminate: ^12.0 - php: 8.3 illuminate: ^11.0 - php: 8.2 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..02e27f5 --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/README.md b/README.md index aa5069d..5279054 100644 --- a/README.md +++ b/README.md @@ -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`: @@ -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). @@ -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 @@ -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 diff --git a/composer.json b/composer.json index 54c4720..6cc114a 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/src/Kitar/Dynamodb/Connection.php b/src/Kitar/Dynamodb/Connection.php index e0d40be..427eeac 100644 --- a/src/Kitar/Dynamodb/Connection.php +++ b/src/Kitar/Dynamodb/Connection.php @@ -124,7 +124,7 @@ protected function getDefaultPostProcessor() */ protected function getDefaultQueryGrammar() { - return $this->withTablePrefix(new Query\Grammar()); + return new Query\Grammar(); } /** diff --git a/src/Kitar/Dynamodb/Query/Builder.php b/src/Kitar/Dynamodb/Query/Builder.php index a1b51ed..76c5c5a 100644 --- a/src/Kitar/Dynamodb/Query/Builder.php +++ b/src/Kitar/Dynamodb/Query/Builder.php @@ -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. @@ -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), diff --git a/src/Kitar/Dynamodb/Query/Grammar.php b/src/Kitar/Dynamodb/Query/Grammar.php index 16413f3..acae761 100644 --- a/src/Kitar/Dynamodb/Query/Grammar.php +++ b/src/Kitar/Dynamodb/Query/Grammar.php @@ -55,7 +55,7 @@ public function __construct() public function compileTableName($table_name) { return [ - 'TableName' => $this->tablePrefix . $table_name + 'TableName' => $table_name ]; } @@ -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 => [ @@ -174,8 +172,6 @@ public function compileBatchWriteRequestItems($table_name, $request_items) }); })->toArray(); - $table_name = $this->tablePrefix . $table_name; - return [ 'RequestItems' => [ $table_name => $marshaled_items, diff --git a/tests/Query/BuilderTest.php b/tests/Query/BuilderTest.php index 9dd786c..aba62a3 100644 --- a/tests/Query/BuilderTest.php +++ b/tests/Query/BuilderTest.php @@ -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' => []])) @@ -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',