Skip to content

Instantly share code, notes, and snippets.

@rodrigopedra
Last active March 30, 2024 16:30
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save rodrigopedra/a4a91948bd41617a9b1a to your computer and use it in GitHub Desktop.
Save rodrigopedra/a4a91948bd41617a9b1a to your computer and use it in GitHub Desktop.
Laravel 5 Middleware for Database Transactions
<?php namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Response;
class DBTransaction
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
* @throws \Exception
*/
public function handle($request, Closure $next)
{
\DB::beginTransaction();
try {
$response = $next($request);
} catch (\Exception $e) {
\DB::rollBack();
throw $e;
}
if ($response instanceof Response && $response->getStatusCode() > 399) {
\DB::rollBack();
} else {
\DB::commit();
}
return $response;
}
}
@rodrigopedra
Copy link
Author

Register it in App\Http\Kernel.php (docs: http://laravel.com/docs/5.0/middleware#registering-middleware), and give it an alias (I call mine dbtransaction).

After that register it as an route middleware (same docs link above), or add this to your Controller's constructor:

$this->middleware( 'dbtransaction', [ 'only' => 'postEdit' ] );

docs: http://laravel.com/docs/5.0/controllers#controller-middleware

@hengsoheak
Copy link

Is it aviable for version 5.3?

@juancarlosestradanieto
Copy link

juancarlosestradanieto commented Jul 3, 2018

Hey I have used this in Laravel 5.6, I put the middleware in App\Http\Controllers\Controller->__construct(), the aim of this is to protect all transactions in the appllication, but i had to add a couple of things, because I don't want to bother returning codes in any controller method. I checked if the response had an exception based in the information given in the framework api

`<?php
namespace App\Http\Middleware;
use Closure;

class DBTransaction
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{

    \DB::beginTransaction();
    try
    {
        $response = $next($request);

        if($response->exception)
        {
          //if you don't throw the exception it wont get into the catch
          throw $response->exception;
        }
    }
    catch (\Exception $e)//it could be QueryException, but it can be limiting because can occur another excepcion type
    {
        //if you only care about exceptions, you could roll back here,
        //but if there's no exception and the response code is greater than 399
        //it is no needed to rollback twice, you can wait and roll back once later
        // \DB::rollBack();
    }

    if($response->exception || ($response instanceof Response && $response->getStatusCode() > 399))
    {
        \DB::rollBack();
    }
    else
    {
        \DB::commit();
    }

    return $response;

}

}`

@sanathks
Copy link

sanathks commented Sep 4, 2018

This will handle it

public function handle($request, Closure $next)
    {
        return DB::transaction(function () use ($request, $next) {
            return $next($request);
        });
    }

@miklcct
Copy link

miklcct commented Aug 26, 2019

This will handle it

public function handle($request, Closure $next)
    {
        return DB::transaction(function () use ($request, $next) {
            return $next($request);
        });
    }

This won't work because the exception was caught in the exception handler inside, which didn't thrown to the middleware

@SjorsO
Copy link

SjorsO commented Oct 21, 2019

I had trouble getting this middleware to work on an api route. Turns out the problem was that Illuminate\Http\JsonResponse is not an instance of Illuminate\Http\Response in Laravel. Changing the import to use the base Symfony response fixes this problem:

use Symfony\Component\HttpFoundation\Response;

<...>

if ($response instanceof Response && $response->getStatusCode() > 399) {

@JonMaia
Copy link

JonMaia commented Jun 26, 2021

I had trouble getting this middleware to work on an api route. Turns out the problem was that Illuminate\Http\JsonResponse is not an instance of Illuminate\Http\Response in Laravel. Changing the import to use the base Symfony response fixes this problem:

use Symfony\Component\HttpFoundation\Response;

<...>

if ($response instanceof Response && $response->getStatusCode() > 399) {

This resolved my problem. Thank you 👍

@jomisacu
Copy link

Thanks for the gist. I used it previously.

But i was thinking...

If i wish send an error code and keep the database sentences? e.g. If i have a permissions system i could send a 403 (Forbidden) status code to a user that attempts to go to restricted area, and more, at this moment i could wish log these attempts to the database and take further actions later.

Take a look to this https://github.com/cannonsir/laravel-transaction-middleware/blob/master/src/Middleware.php

This is a composer installable package created by @cannonsir

@rodrigopedra
Copy link
Author

rodrigopedra commented Sep 22, 2022

@jomisacu

I like the implementation of the linked package, but I don´t see the difference in behavior you describe.

In this gist's implementation the transaction is rolled back earlier right when an exception happens. It won't prevent this exception to be handled by the exception handler which could potentially log the exception and build a new exception in response.

If i have a permissions system i could send a 403 (Forbidden) status code to a user that attempts to go to restricted area, and more, at this moment I could wish log these attempts to the database and take further actions later.

None of the implementation would automatically allow reporting the executed queries if no database exceptions were thrown when you "unauthorize" a user.

And just by having the UnauthorizedException attached to the response wouldn't log the successful queries either.

For logging database queries you can use DB@listen to listen for the QueryExecuted event.

@jomisacu
Copy link

@rodrigopedra thank you for the response.

Excuse me, i tried to say, log the attempts to the restricted area, not log executed queries. But, this is an arbitrary example and i refer to that i could send 403 status code without associate exception, only setting the status code in the response object.

Here the question is: must a response status code revert the transaction?

@rodrigopedra
Copy link
Author

Here the question is: must a response status code revert the transaction?

Well I believe so, as any status code from 400 on are considered either an user error (400-499) or a server error (500-599).

I would not expect an unauthorized request to perform a database operation. And nothing is preventing, on either middleware, an exception to be reported.

If you want your exceptions to be saved to a database, you should use a different database connection for the logger, this would allow logging anything regardless of any open transactions, and is actually a better approach as from an application perspective that a connection responsible for transactional data (application logic) to be different than your infrastructure connection(s) (logging, session, cache, etc...)

Imagine rolling back a session data change because it happened inside a transaction you manually opened and rolled back, regardless of using a middleware for automatic transaction handling.

When using different logging, session, cache and other drivers, rolling back or committing a transaction already does not affect any approaches.

If you believe this middleware should not consider just the status code as a criteria, you can change its code above. Or use the package you linked.

Both of them just offers different takes based on different opinions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment