LaravelPackages.net
Acme Inc.
Toggle sidebar
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.

1
5
v0.1.0
About meius/laravel-transaction-orchestrator

meius/laravel-transaction-orchestrator is a Laravel package for attribute-driven transactions and row-level locks for laravel: retries, backoff, http-aware rollbacks, and for update/share on route model binding.. It currently has 5 GitHub stars and 1 downloads on Packagist (latest version v0.1.0). Install it with composer require meius/laravel-transaction-orchestrator. Discover more Laravel packages by meius or browse all Laravel packages to compare alternatives.

Last updated

Laravel Transaction Orchestrator

Build Status License codecov Codacy Badge PHP Version Laravel Version


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() and sharedLock(). Support = whatever Laravel and your DB driver support.


Table of Contents


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 (or LOCK IN SHARE MODE on older versions).
  • PostgreSQL — FOR UPDATE / FOR SHARE (or FOR KEY SHARE depending 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

  1. Composer Installation:
    Install the package using Composer:

    composer require meius/laravel-transaction-orchestrator
    
  2. Register the Service Provider:
    Manually register the service provider by adding it to your bootstrap/providers.php file:

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 from config/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):

  • backoff as array → must be list<int>.
  • rollbackOnHttpError as array → must be list<int>.
  • noRollbackOn → must be Throwable subclasses.

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() or sharedLock()) 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.
  • backoff in 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 — use DB::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.

Comments