Sometimes, I get ideas. Are they good ideas? Lets find out!
Whilst building out the controllers for a new application I found myself repeating a pattern of creating an invokable controller that try-catches an action and returns a route with a flash message to indicate whether the action was successful or not. As these were CRUD controllers with actions touching the database, the actions are wrapped in a transaction like so:
final class StoreController extends Controller
{
public function __invoke(StoreRequest $request, StoreUserAction $storeUserAction): RedirectResponse
{
$data = StoreUserData::fromRequest($request);
try {
$success = DB::transaction(fn () => $storeUserAction->handle($data));
} catch (Throwable $e) {
$success = false;
Log::error($e);
}
$flash = $success
? get_success_flash('User created successfully')
: get_error_flash();
return to_route('admin.users.index')->with($flash);
}
}
This can be particularly useful when the action performs multiple changes to the database but also might fail at some point and throw an exception. In these cases a transaction will rollback any changes made and prevent the failure from making a mess of our application state. As always, Laravel provides excellent documentation on transactions.
As I was writing this pattern out a lot and not all actions need to be wrapped in a transaction I started to wonder if it was possible to write less in the controller, not have to keep repeating myself and have the action wrap itself in transaction itself if it needed it. So the controller would become:
final class StoreController extends Controller
{
public function __invoke(StoreRequest $request, StoreUserAction $storeUserAction): RedirectResponse
{
$data = StoreUserData::fromRequest($request);
$success = $storeUserAction->handle($data);
$flash = $success
? get_success_flash('User created successfully')
: get_error_flash();
return to_route('admin.users.index')->with($flash);
}
}
And the try catch would go, well, somewhere else on the action? To achieve this we need the action class to know that it might need to wrap the handle method in a try catch with a transaction. This is where we could reach for PHP Attributes.
Handle with care
For those that don’t know Attributes were added to PHP 8.0 and as per the PHP docs:
Attributes [can] be thought of as a configuration language embedded directly into code
For our use case we define an empty class decorated with the #[Attribute] Attribute(!) and specify that it can only be used on class methods using TARGET_METHOD:
namespace \App\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class Transactionable {}
We can now flex our meta-programming skills and use the Reflection API to check for a handle method on the class to see if our Attribute is or present not and call handle wrapped in the transaction closure or directly respectively:
public function execute(): bool
{
$reflection = new ReflectionClass($this);
foreach($reflection->getMethods() as $method) {
if ($method->name === 'handle') {
if (! empty($method->getAttributes(Transactionable::class))) {
try {
return DB::transaction(fn () => $this->handle($data));
} catch (Throwable $e) {
Log::error($e);
return false;
}
} else {
return $this->handle();
}
}
}
// consider throwing here as should have returned already
}
Note that we only want the execute method to call the handle method so we will make handle private on the action and update the controller code to call execute:
$success = $storeUserAction->execute($data);
Running into limitations
We want to make the execute method reusable to use on any action and as we prefer composition over inheritance we can move the method into a trait:
trait TransactionableTrait
{
public function execute(): bool
$reflection = new ReflectionClass($this);
foreach($reflection->getMethods() as $method) {
if ($method->name === 'handle') {
if (! empty($method->getAttributes(Transactionable::class))) {
try {
return DB::transaction(fn () => $this->handle($data));
} catch (Throwable $e) {
Log::error($e);
return false;
}
} else {
return $this->handle();
}
}
}
}
private function handle() {}
}
class UpdateAction
{
use TransactionableTrait;
...
}
Using a a trait here means we can stub out a private handle method that we expect to be overridden by the parent class. This is quite nice as we can’t enforce this with an interface or abstract class in PHP as they don’t support private methods. Furthermore PHP 8.3 introduced the #[\Override] Attribute which we can add to the trait method to ensure it is overridden:
trait TransactionableTrait
{
...
#[\Override]
private function handle() {}
}
However, this is where the idea started to feel like it wasn’t quite right. Though this approach works, the Transactionable Attribute is supposed to be optional so we don’t really want to call in the trait on an action that doesn’t use it. We can solve this by creating a more general Action class that our actions extend and call the trait there instead:
class Action {
use TransactionableTrait;
}
We can then extend our action and optionally decorate our handle method with our #[Transactionable] Attribute:
class UpdateAction extends Action
{
#[Transactionable]
private function handle(): bool
{
...
}
}
Unfortunately we can’t use the Override Attribute in this case as it’s parent is now the Action class and we don’t want the override there. Hmm, this was starting to feel like a lot of work just to save a few lines in each controller!
I decided not explore this particular refactor any further as I don’t think I would implement this in my application code. Sometimes the urge to keep our code DRY and apply the rule of three has us reaching for an abstraction that we don’t necessarily need. In particular I felt more uncomfortable about hiding away a try catch and logging inside the execute method and hiding this away on the Action class than I did repeating myself – the principle of “explicit is better than implicit” wins here.
However it was fun to explore Attributes, see how simple they are to implement and code dive into packages such as Laravel Lift, Spatie’s Laravel Data and Livewire to see how they leverage them to add a myriad of configuration options directly into your code. How about you? Do you create your own Attributes or use the ones provided by packages in your projects?