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!