Laravel middleware: WithTransaction

Search for a command to run...

No comments yet. Be the first to comment.
Hey guys, An update after I posted the “Laravel Excel: How to append rows to an existing Excel file“ a while back. This post will give you another solution and probably a better way to export that fits every case. Problems Previously, when trying to ...

Hey guys, An easy topic for today! Make something a default (e.g., category, product, campaign, etc). Let’s go with the categories table for the examples. Let’s say we only want one category to be the default one. And find out which is the optimal wa...
Hey guys, Probably a chapter 2 of this https://sethphat.dev/why-is-wordpress-still-an-awesome-cms blog post, haha. Here was the story: I’ve been spending like 2 days figuring out which CMS I want to use for my new blog (for my new hobby lol) Let’s se...

Hey guys, Well damn, it has been a while since my last post lol, lazy & busy. But here I am, back to write useful tips for y’all 🥹 Let us do some exporting tasks for today, using Laravel Excel to export XLSX (yeah CSV is so 2010 lol). But, append mo...

Hey guys, Recently, I just bought Herd PRO (a yearly subscription). I’ve been using the basic Herd since the first release. So, my latest work required interaction with S3 storage, there are several ways to achieve that in the local env: Use Cloud (...

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.
Likely in the Laravel Controller, we usually use the 2 options, let's check them out below:
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']);
}
);
}
}
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']);
});
}
}
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 😎.
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']);
<?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.
<?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
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.
I hope that this Middleware can bring some joy to your Controller 🥰 and get rid of the tedious code.
Happy friyay!