
Hey guys,
I've used the Transaction Wrapper middleware for my action APIs for a while. It comes in pretty handy.
For the action APIs where multiple DB write operations can happen, best to wrap them in a transaction for the data consistency.
The good'ol way
Likely in the Laravel Controller, we usually use the 2 options, let's check them out below:
The Options
Start - Try Commit - Catch Rollback
Legends know this haha 😎
use Illuminate\Support\Facades\DB;
use Illuminate\Http\JsonResponse;
class UserController
{
public function update(UpdateUserRequest $request, User $user): JsonResponse
{
DB::beginTransaction();
try {
$user->update($request->getUpdatableField());
$user->updateProfilePicture($request->getProfilePic());
DB::commit();
return new JsonResponse(['outcome' => 'SUCCESS']);
} catch (Throwable) {
DB::rollBack();
return new JsonResponse(['outcome' => 'FAILED_TO_UPDATE']);
}
}
}
Within the new Laravel version, you'd be able to use the rescue HoF:
use Illuminate\Support\Facades\DB;
use Illuminate\Http\JsonResponse;
class UserController
{
public function update(UpdateUserRequest $request, User $user): JsonResponse
{
DB::beginTransaction();
return rescue(
function () use ($request, $user) {
$user->update($request->getUpdatableField());
$user->updateProfilePicture($request->getProfilePic());
DB::commit();
return new JsonResponse(['outcome' => 'SUCCESS']);
},
function () {
DB::rollBack();
return new JsonResponse(['outcome' => 'FAILED_TO_UPDATE']);
}
);
}
}
Use DB::transaction HoF
Instead of using rescue and also do the beginTransaction , you can do something better & shorter:
use Illuminate\Support\Facades\DB;
use Illuminate\Http\JsonResponse;
class UserController
{
public function update(UpdateUserRequest $request, User $user): JsonResponse
{
return DB::transaction(function () use ($request, $user) {
$user->update($request->getUpdatableField());
$user->updateProfilePicture($request->getProfilePic());
return new JsonResponse(['outcome' => 'SUCCESS']);
});
}
}
The problem with the above options
Isn't reusable.
Your code is always inside a scope level, doesn't look so cool.
So why wouldn't we create something that is reusable, and get rid of a scoped level?
That's the reason for this article hehe 😎.
WithTransaction middleware
The goal of this middleware is to wrap a whole Controller's action inside a middleware, and rollback when there is an exception. Also reusable, we just simply register the middleware in the routes
Route::post('/users/{user}', [UserController::class, 'update'])
->middleware(['withTransaction']);
The simple Middleware
<?php
namespace App\Http\Middleware;
use App\Http\JsonResponseFactory;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
use Throwable;
class WithTransaction
{
public function handle(Request $request, Closure $next): Response
{
return DB::transaction(function () use ($request, $next) {
$result = $next($request);
if (isset($result->exception)) {
throw $result->exception;
}
return $result;
});
}
}
Laravel has its Error Response layer, after triggering the $next, we won't get any exceptions.
If there is an exception, the result will have the exception property. That's why we'll need to check & throw to commit or roll back the transaction.
This would do the job, but I don't like this way because we'll expose the internal error to the client side, therefore I came up with another better solution.
The improved version
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
use Throwable;
class WithTransaction
{
public function handle(Request $request, Closure $next): Response
{
return rescue(
fn () => DB::transaction(function () use ($request, $next) {
$result = $next($request);
if (isset($result->exception)) {
throw $result->exception;
}
return $result;
}),
function (Throwable $e) use ($request) {
// report($e); // you can also do this
Log::error('Rolled back transaction on error', [
'error' => [
'msg' => $e->getMessage(),
'traces' => $e->getTrace(),
],
'url' => $request->fullUrl(),
]);
return new JsonResponse([
'outcome' => 'ACTION_FAILED',
...(config('app.debug')
? ['msg' => $e->getMessage()]
: []),
], 400);
},
);
}
}
With this improved way, I'll have:
Internal error log - would be really useful for staging & production environments.
Standardized error response
Notes
If your endpoint is purely read, DO NOT use it, for better performance.
If your action has only a single write/update operation, you don't have to use it either.
Conclusion
I hope that this Middleware can bring some joy to your Controller 🥰 and get rid of the tedious code.
Happy friyay!
About the author
develops awesome software, contributes to OSS, writes tech tips, and loves Vietnamese milk coffee!
Comments
Join the discussionNo comments yet. Be the first to comment.



