diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcfd94c..83ad6d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,18 +51,20 @@ jobs: strategy: matrix: - php: ['8.0', 8.1, 8.2] - lib: - - { laravel: ^11.0 } - - { laravel: ^10.0 } - - { laravel: ^9.0 } + php: [8.2, 8.3, 8.4] + laravel: [^11.0, ^12.0, ^13.0.x-dev] exclude: - - { php: 8.0, lib: { laravel: ^10.0 } } - - { php: 8.0, lib: { laravel: ^11.0 } } - - { php: 8.1, lib: { laravel: ^11.0 } } + - php: 8.2 + laravel: ^13.0.x-dev include: - - { lib: { laravel: ^9.0 }, stable: 1 } - - { lib: { laravel: ^10.0 }, stable: 1 } + - php: 8.2 + php-cs-fixer: 1 + - php: 8.3 + php-cs-fixer: 1 + - laravel: ^11.0 + larastan: 1 + - laravel: ^12.0 + larastan: 1 steps: - uses: actions/checkout@v3 @@ -73,24 +75,28 @@ jobs: php-version: ${{ matrix.php }} coverage: xdebug - - name: Remove impossible dependencies - if: ${{ matrix.stable != 1 }} - run: composer remove nunomaduro/larastan friendsofphp/php-cs-fixer --dev --no-update + - name: Remove impossible dependencies (nunomaduro/larastan) + if: ${{ matrix.larastan != 1 }} + run: composer remove nunomaduro/larastan --dev --no-update + + - name: Remove impossible dependencies (friendsofphp/php-cs-fixer) + if: ${{ matrix.php-cs-fixer != 1 }} + run: composer remove friendsofphp/php-cs-fixer --dev --no-update - name: Adjust Package Versions run: | - composer require "laravel/framework:${{ matrix.lib.laravel }}" --dev --no-update + composer require "laravel/framework:${{ matrix.laravel }}" --dev --no-update composer update - name: Prepare Coverage Directory run: mkdir -p build/logs - name: PHP-CS-Fixer - if: ${{ matrix.stable == 1 }} + if: ${{ matrix.php-cs-fixer == 1 }} run: composer cs - name: PHPStan - if: ${{ matrix.stable == 1 }} + if: ${{ matrix.larastan == 1 }} run: composer phpstan - name: Test @@ -106,7 +112,7 @@ jobs: env: COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_PARALLEL: 'true' - COVERALLS_FLAG_NAME: "laravel:${{ matrix.lib.laravel }} php:${{ matrix.php }}" + COVERALLS_FLAG_NAME: "laravel:${{ matrix.laravel }} php:${{ matrix.php }}" with: timeout_minutes: 1 max_attempts: 3 diff --git a/README.md b/README.md index 40b2022..cb19713 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,14 @@ Advisory Locking Features of Postgres/MySQL/MariaDB on Laravel ## Requirements -| Package | Version | Mandatory | -|:--------|:-------------------------------------|:---------:| -| PHP | ^8.0.2 | ✅ | -| Laravel | ^9.0 || ^10.0 | ✅ | -| PHPStan | >=1.1 | | +| Package | Version | Mandatory | +|:--------|:--------------------------------------|:---------:| +| PHP | ^8.2 | ✅ | +| Laravel | ^11.0 || ^12.0 | ✅ | +| PHPStan | >=2.0 | | + +> [!NOTE] +> Older versions have outdated dependency requirements. If you cannot prepare the latest environment, please refer to past releases. | RDBMS | Version | |:---------|:--------------------------| @@ -19,7 +22,7 @@ Advisory Locking Features of Postgres/MySQL/MariaDB on Laravel ## Installing ``` -composer require mpyw/laravel-database-advisory-lock:^4.3 +composer require mpyw/laravel-database-advisory-lock:^4.4 ``` ## Basic usage @@ -171,21 +174,20 @@ END ## Caveats about Transaction Levels -### Key Principle - -Always avoid nested transactions when using advisory locks to ensure adherence to the **[S2PL (Strict 2-Phase Locking)](https://en.wikipedia.org/wiki/Two-phase_locking#Strict_two-phase_locking)** principle. - ### Recommended Approach -When transactions and advisory locks are related, either locking approach can be applied. +When transactions and advisory locks are related, either locking approach can be applied. + +> [!TIP] +> **For Postgres, always prefer Transaction-Level Locking.** > [!NOTE] > **Transaction-Level Locks:** -> Acquire the lock at the transaction nesting level 1, then rely on automatic release mechanisms. +> Ensure the current context is inside the transaction, then rely on automatic release mechanisms. > > ```php -> if (DB::transactionLevel() > 1) { -> throw new LogicException("Don't use nested transactions outside of this logic."); +> if (DB::transactionLevel() < 1) { +> throw new LogicException("Unexpectedly transaction is not active."); > } > > DB::advisoryLocker() @@ -196,11 +198,11 @@ When transactions and advisory locks are related, either locking approach can be > [!NOTE] > **Session-Level Locks:** -> Acquire the lock at the transaction nesting level 0, then proceed to call `DB::transaction()` call. +> Ensure the current context is outside the transaction, then proceed to call `DB::transaction()` call. > > ```php > if (DB::transactionLevel() > 0) { -> throw new LogicException("Don't use transactions outside of this logic."); +> throw new LogicException("Unexpectedly transaction is already active."); > } > > $result = DB::advisoryLocker() @@ -215,10 +217,6 @@ When transactions and advisory locks are related, either locking approach can be ### Considerations -> [!CAUTION] -> **Transaction-Level Locks:** -> Don't take transaction-level locks in nested transactions. They are unaware of Laravel's nested transaction emulation. - > [!CAUTION] > **Session-Level Locks:** > Don't take session-level locks in the transactions when the content to be committed by the transaction is related to the advisory locks. diff --git a/_ide_helper.php b/_ide_helper.php index 4515bff..459e092 100644 --- a/_ide_helper.php +++ b/_ide_helper.php @@ -14,9 +14,7 @@ public function advisoryLocker(): LockerFactory; class Connection implements ConnectionInterface { - public function advisoryLocker(): LockerFactory - { - } + public function advisoryLocker(): LockerFactory {} } } } @@ -28,9 +26,7 @@ public function advisoryLocker(): LockerFactory if (false) { class DB extends Facade { - public static function advisoryLocker(): LockerFactory - { - } + public static function advisoryLocker(): LockerFactory {} } } } diff --git a/composer.json b/composer.json index 5da0e34..2a4aa0f 100644 --- a/composer.json +++ b/composer.json @@ -22,21 +22,21 @@ } }, "require": { - "php": "^8.0.2", + "php": "^8.2", "ext-pdo": "*", - "illuminate/events": "^9.0 || ^10.0 || ^11.0", - "illuminate/support": "^9.0 || ^10.0 || ^11.0", - "illuminate/database": "^9.0 || ^10.0 || ^11.0", - "illuminate/contracts": "^9.0 || ^10.0 || ^11.0" + "illuminate/events": "^11.0 || ^12.0 || ^13.0", + "illuminate/support": "^11.0 || ^12.0 || ^13.0", + "illuminate/database": "^11.0 || ^12.0 || ^13.0", + "illuminate/contracts": "^11.0 || ^12.0 || ^13.0" }, "require-dev": { "orchestra/testbench": "*", - "orchestra/testbench-core": ">=7.0", - "phpunit/phpunit": ">=9.5", - "phpstan/phpstan": ">=1.1", + "orchestra/testbench-core": ">=9.0", + "phpunit/phpunit": ">=11.0", + "phpstan/phpstan": ">=2.0", "phpstan/extension-installer": ">=1.1", - "nunomaduro/larastan": ">=1.0", - "friendsofphp/php-cs-fixer": "^3.9" + "nunomaduro/larastan": ">=3.1", + "friendsofphp/php-cs-fixer": "^3.70" }, "scripts": { "test": "vendor/bin/phpunit", diff --git a/phpstan/AdvisoryLockerMethod.php b/phpstan/AdvisoryLockerMethod.php index d66901f..e4f13b9 100644 --- a/phpstan/AdvisoryLockerMethod.php +++ b/phpstan/AdvisoryLockerMethod.php @@ -15,8 +15,6 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use function is_a; - final class AdvisoryLockerMethod implements MethodReflection { private ClassReflection $class; @@ -33,7 +31,7 @@ public function getDeclaringClass(): ClassReflection public function isStatic(): bool { - return is_a($this->class->getName(), DB::class, true); + return $this->class->is(DB::class); } public function isPrivate(): bool diff --git a/phpstan/ConnectionClassExtension.php b/phpstan/ConnectionClassExtension.php index dd993c9..2f7aaf8 100644 --- a/phpstan/ConnectionClassExtension.php +++ b/phpstan/ConnectionClassExtension.php @@ -10,16 +10,14 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; -use function is_a; - final class ConnectionClassExtension implements MethodsClassReflectionExtension { public function hasMethod(ClassReflection $classReflection, string $methodName): bool { return $methodName === 'advisoryLocker' && ( - is_a($classReflection->getName(), ConnectionInterface::class, true) - || is_a($classReflection->getName(), DB::class, true) + $classReflection->is(ConnectionInterface::class) + || $classReflection->is(DB::class) ); } diff --git a/phpunit.xml b/phpunit.xml index 4a14c2d..e484804 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,15 +1,26 @@ - - + + + - src + ./src - + + + + - ./tests/ + ./tests + diff --git a/tests/PostgresTransactionErrorRecoveryTest.php b/tests/PostgresTransactionErrorRecoveryTest.php index cbf65d3..c7127a2 100644 --- a/tests/PostgresTransactionErrorRecoveryTest.php +++ b/tests/PostgresTransactionErrorRecoveryTest.php @@ -4,7 +4,6 @@ namespace Mpyw\LaravelDatabaseAdvisoryLock\Tests; -use Illuminate\Database\Connection; use Illuminate\Database\ConnectionInterface; use Illuminate\Database\QueryException; use Illuminate\Support\Facades\DB; @@ -20,7 +19,6 @@ public function testWithoutTransactions(): void $passed = false; $conn = DB::connection('pgsql'); - assert($conn instanceof Connection); $conn->enableQueryLog(); $conn @@ -64,7 +62,6 @@ public function testWithLockingRollbacksToSavepoint(): void $passed = false; $conn = DB::connection('pgsql'); - assert($conn instanceof Connection); $conn->enableQueryLog(); $conn->transaction(function (ConnectionInterface $conn) use (&$passed): void { @@ -115,7 +112,6 @@ public function testWithLockingRollbacksToSavepoint(): void public function testDestructorReleasesLocksAfterTransactionTerminated(): void { $conn = DB::connection('pgsql'); - assert($conn instanceof Connection); $conn->enableQueryLog(); try { diff --git a/tests/PostgresTransactionErrorRefreshDatabaseRecoveryTest.php b/tests/PostgresTransactionErrorRefreshDatabaseRecoveryTest.php index 841a439..f060ed1 100644 --- a/tests/PostgresTransactionErrorRefreshDatabaseRecoveryTest.php +++ b/tests/PostgresTransactionErrorRefreshDatabaseRecoveryTest.php @@ -4,7 +4,6 @@ namespace Mpyw\LaravelDatabaseAdvisoryLock\Tests; -use Illuminate\Database\Connection; use Illuminate\Database\ConnectionInterface; use Illuminate\Database\QueryException; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -23,7 +22,6 @@ class PostgresTransactionErrorRefreshDatabaseRecoveryTest extends TestCase public function testImplicitTransactionRollbacksToSavepoint(): void { $conn = DB::connection('pgsql'); - assert($conn instanceof Connection); $conn->enableQueryLog(); try { @@ -79,7 +77,6 @@ public function testWithLockingRollbacksToSavepoint(): void $passed = false; $conn = DB::connection('pgsql'); - assert($conn instanceof Connection); $conn->enableQueryLog(); $conn->transaction(function (ConnectionInterface $conn) use (&$passed): void { @@ -131,7 +128,6 @@ public function testWithLockingRollbacksToSavepoint(): void public function testDestructorReleasesLocksAfterRollingBackToSavepoint(): void { $conn = DB::connection('pgsql'); - assert($conn instanceof Connection); $conn->enableQueryLog(); try { diff --git a/tests/TestCase.php b/tests/TestCase.php index 7b6c940..3556ed2 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -22,18 +22,18 @@ protected function getEnvironmentSetUp($app): void { config(['database.connections.mariadb' => config('database.connections.mysql')]); config([ - 'database.connections.pgsql.host' => env('PG_HOST', 'postgres'), - 'database.connections.pgsql.port' => env('PG_PORT', '5432'), + 'database.connections.pgsql.host' => getenv('PG_HOST') ?: 'postgres', + 'database.connections.pgsql.port' => getenv('PG_PORT') ?: '5432', 'database.connections.pgsql.database' => 'testing', 'database.connections.pgsql.username' => 'testing', 'database.connections.pgsql.password' => 'testing', - 'database.connections.mysql.host' => env('MY_HOST', 'mysql'), - 'database.connections.mysql.port' => env('MY_PORT', '3306'), + 'database.connections.mysql.host' => getenv('MY_HOST') ?: 'mysql', + 'database.connections.mysql.port' => getenv('MY_PORT') ?: '3306', 'database.connections.mysql.database' => 'testing', 'database.connections.mysql.username' => 'testing', 'database.connections.mysql.password' => 'testing', - 'database.connections.mariadb.host' => env('MA_HOST', 'mariadb'), - 'database.connections.mariadb.port' => env('MA_PORT', '3306'), + 'database.connections.mariadb.host' => getenv('MA_HOST') ?: 'mariadb', + 'database.connections.mariadb.port' => getenv('MA_PORT') ?: '3306', 'database.connections.mariadb.database' => 'testing', 'database.connections.mariadb.username' => 'testing', 'database.connections.mariadb.password' => 'testing',