meius / laravel-transaction-orchestrator
Attribute-driven transactions and row-level locks for Laravel: retries, backoff, HTTP-aware rollbacks, and FOR UPDATE/SHARE on route model binding.
Fund package maintenance!
Buy Me A Coffee
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/meius/laravel-transaction-orchestrator
Requires
- php: ^8.2
- ext-pdo: *
- illuminate/database: ^11.0 || ^12.0
- illuminate/routing: ^11.0 || ^12.0
- illuminate/support: ^11.0 || ^12.0
Requires (Dev)
- orchestra/testbench: ^9 || ^10
- phpstan/phpstan: ^2
- phpunit/phpunit: ^10
- squizlabs/php_codesniffer: 4.0.x-dev
This package is auto-updated.
Last update: 2025-10-04 12:05:39 UTC
README
Declarative transactions and row-level locks for Laravel 11+ via PHP attributes.
Annotate controller methods with #[Transactional] — get transactions with retries/backoff and HTTP-aware rollback.
Annotate parameters with #[LockForUpdate] / #[SharedLock] — get row-locks directly during route model binding. Zero boilerplate.
Internally it uses the standard Laravel/Eloquent API:
lockForUpdate()andsharedLock(). Support = whatever Laravel and your DB driver support.
Table of Contents
- Features
- Requirements & DB Support
- Installation
- Quick Start
- Attribute Reference
- Retries & Backoff
- HTTP-Aware Rollback
- Multiple Connections
- Nested Transactions
- Limitations
- License
Features
#[Transactional]— wrap controller actions in a transaction, optionally with retries and backoff.- Rollback policy based on HTTP response (4xx/5xx/specific codes).
- Exceptions that do not trigger rollback (
noRollbackOn). #[LockForUpdate]/#[SharedLock]on action parameters — row-lock during route model binding.- Zero config: service provider and router decoration auto-registered.
Requirements & DB Support
- PHP 8.2+
- Laravel 11+
Row lock support is fully delegated to Laravel/Eloquent:
- MySQL/MariaDB —
FOR UPDATE/FOR SHARE(orLOCK IN SHARE MODEon older versions). - PostgreSQL —
FOR UPDATE/FOR SHARE(orFOR KEY SHAREdepending on context). - SQL Server — via hints (
UPDLOCK,ROWLOCK), same as Laravel does. - SQLite — no row-level lock for
SELECT(effectively no-op).
If your driver/version doesn’t support the mode, behavior matches Laravel.
Installation
-
Composer Installation:
Install the package using Composer:composer require meius/laravel-transaction-orchestrator
-
Register the Service Provider:
Manually register the service provider by adding it to yourbootstrap/providers.phpfile:
return [ // Other service providers... Meius\LaravelTransactionOrchestrator\Providers\TransactionOrchestratorServiceProvider::class, ];
Quick Start
use App\Models\Order; use App\Repositories\OrderRepository; use Meius\LaravelTransactionOrchestrator\Attributes\Locks\LockForUpdate; use Meius\LaravelTransactionOrchestrator\Attributes\Transactional; use Meius\LaravelTransactionOrchestrator\Enums\HttpRollbackPolicy; use Symfony\Component\HttpFoundation\Response; class OrderController extends Controller { public function __construct( private readonly OrderRepository $orderRepository, ) { // } #[Transactional( connection: 'mysql', retries: 3, backoff: [50, 100, 200], // milliseconds noRollbackOn: [QueryException::class], rollbackOnHttpError: HttpRollbackPolicy::ROLLBACK_ON_5XX, )] public function destroy(#[LockForUpdate] Order $order): Response { try { $this->orderRepository->delete($order); } catch (\Throwable) { return response()->json([ 'message' => 'Unable to delete the order.', ], Response::HTTP_INTERNAL_SERVER_ERROR); // 5xx → rollback (per policy) } return response()->noContent(); // 204 → commit } }
Attribute Reference
Transactional
Purpose: run the controller method inside transaction(s).
Parameters & behavior:
connection:null|string|string[].null→ default fromconfig/database.php.- Normalized to array at runtime →
$connections.
retries: how many times to retry on transient DB errors (deadlock, lock timeout, disconnect, etc.).backoff: delay in ms before retry.- Single number = constant delay.
- Array = per-attempt delay, last value repeats.
noRollbackOn: list of exception FQCNs that do not trigger rollback.rollbackOnHttpError: rollback policy based on HTTP response:ROLLBACK_NONE,ROLLBACK_ON_4XX,ROLLBACK_ON_5XX,ROLLBACK_ON_4XX_5XX(default), or list of codes ([409, 422]).
Validation (constructor enforces):
backoffas array → must belist<int>.rollbackOnHttpErroras array → must belist<int>.noRollbackOn→ must beThrowablesubclasses.
Transaction outcome:
- Exceptions are not swallowed. Any unhandled exception → rollback (unless in
noRollbackOn). - If no exception: commit/rollback decided by the Response and policy.
LockForUpdate and SharedLock
Purpose: apply row-lock to action parameter during route model binding.
How it works:
- Before resolving the parameter, the router checks the attribute and applies the standard Eloquent lock (
lockForUpdate()orsharedLock()) just once. - Lock does not leak into other queries inside the method.
- SQL and semantics depend on your DB driver/version (see Requirements).
Example:
use App\Exceptions\Products\CannotRemoveProductException; use App\Exceptions\Products\ProductNotInOrderException; use App\Http\Resources\OrderResource; use App\Models\Order; use App\Models\Product; use App\Services\OrderService; use Meius\LaravelTransactionOrchestrator\Attributes\Locks\LockForUpdate; use Meius\LaravelTransactionOrchestrator\Attributes\Locks\SharedLock; use Meius\LaravelTransactionOrchestrator\Attributes\Transactional; use Symfony\Component\HttpFoundation\Response; class OrderProductController extends Controller { public function __construct( private readonly OrderService $orderService, ) { // } /** * Removes a product from the order. */ #[Transactional] public function destroy( #[LockForUpdate] Order $order, #[SharedLock] Product $product ): Response { try { $order = $this->orderService->recalculate($order, $product); } catch (ProductNotInOrderException|CannotRemoveProductException $exception) { return response()->json([ 'error' => $exception->getMessage(), ], Response::HTTP_UNPROCESSABLE_ENTITY); } catch (\Throwable) { return response()->json([ 'error' => 'Unable to remove product from order.', ], Response::HTTP_INTERNAL_SERVER_ERROR); } return OrderResource::make($order)->response(); } }
Retries & Backoff
- Enabled when
retries > 0. - Transient errors include common concurrency/connection issues.
backoffin ms. Example[10, 30, 70]→ values applied per attempt, last repeated.
With retries enabled, make operations idempotent (or dedupe-safe). The package does not enforce idempotency.
HTTP-Aware Rollback
Rollback can be triggered by response status without exceptions:
- Validation →
422→ rollback. - Resource conflict →
409→ rollback. - Any
5xx→ rollback (default).
Customize via policy or code list:
#[Transactional(rollbackOnHttpError: [409, 422])]
Decision is made after action returns a Response, before sending body.
Multiple Connections
connection accepts an array:
#[Transactional(connection: ['mysql', 'pgsql'])]
- Opens a transaction for each connection.
- On error/rollback condition — all are rolled back.
- This is not 2PC (no cross-DB atomicity).
Nested Transactions
- If you call
DB::transaction()inside, Laravel uses savepoints (if supported). - Outer
#[Transactional]decides final commit/rollback. - Do not mix manual
commit()/rollBack()with orchestrator — useDB::transaction().
Limitations
- Lock behavior depends on DB driver/version; package follows Laravel exactly.
- Locks apply only during route model binding. Queries inside method are unaffected.
- No 2PC across DBs.
- Retries ≠ idempotency: duplicate side effects are your responsibility.
License
This package is open-sourced software licensed under the MIT license.