Lädt...

🔧 Digging into Laravel's Conditionable trait


Nachrichtenbereich: 🔧 Programmierung
🔗 Quelle: dev.to

Conditionable is a trait that adds two methods to any class: when and unless. They are essentially syntactic sugar. They allow an unbroken chain of method calls that keep the reader from having to continually evaluate whether the next if-block they are looking at is doing anything other than continuing to work with the original object. Its value is most obvious when used by builder classes. A prime example is Eloquent.

A standard implementation of a query that has optional filter parameters would look like this:

<?php

class ContactController
{
    public function index(Request $request)
    {
        $query = Contact::query();
        $search = $request->query('search');

        if ($search) {
            $query->where('first_name', 'like', '%' . $search . '%')
                ->orWhere('last_name', 'like', '%' . $search . '%');
        }

        $contacts = $query->get();

        return view('contacts', ['contacts' => $contacts]);
    }
}

There's nothing wrong with the above solution, to be sure. However, I've seen some mighty complex queries in my time — when there are many conditionals happening, it becomes very confusing to follow along to a conclusive end result.

This is where the Conditionable trait shines! It provides the benefit of keeping all the context of the query in a single chain of events. You see the single block of code and know that it all is related.

Two or three argument variant

There are multiple "variants" of the when method. The method works differently based on the number of arguments, sort of like a poor man's method overloading.

The two to three argument variant is the most commonly seen variant. The first argument is the predicate — the condition being evaluated for true/false. The second argument is the consequent — the callback that's executed when the condition is truthy. The third and optional argument is the alternative — the callback that's executed when the condition is falsy.

$conditionable->when(
    true,
    fn ($cond) => $cond->consequent(),
    fn ($cond) => $cond->alternative(),
);

Here is the previous example re-written using the two argument variant of when afforded by Conditionable:

<?php

class ContactController
{
    public function index(Request $request)
    {
        $contacts = Contact::query()
            ->when($request->query('search'), fn (Builder $query, $search)
                => $query->where('first_name', 'like', '%' . $search . '%')
                         ->orWhere('last_name', 'like', '%' . $search . '%'))
            ->get();

        return view('contacts', ['contacts' => $contacts]);
    }
}

All the logic for building our query is now in a single chain. You'll also notice that the predicate of our condition ($request->query('search')) is provided back to us in the callback ($search), so we don't have to declare an intermediate variable or re-write the expression.

One argument variant

We are going to talk about how this variant and the zero argument variant work in a bit, but first let me explain what they look like.

Here's the syntax:

$conditionable->when(true)->consequent();

The one argument variant moves the consequent from the second argument position of the when method chained directly after when. Take a look at this example to see what I mean.

$email = $request->query('email');

$query->when($email)->where('email', $email);

->where('email', $email) is only called if ->when($email) evaluates to true. This variant makes sense if you are calling a single method if the predicate is true.

Zero argument variant

This one is even more niche than the last variant. It looks like this:

$conditionable->when()->predicate()->consequent();

And using a real example, like this:

now()->when()->isWeekend()->nextWeekDay();

We call when(), and we evaluate isWeekend() for truthiness. If truthy, we call nextWeekDay(), otherwise skip it.

Like I said, very niche, but neat if you recognize an opportunity.

Higher-order message

So what black magic is powering this overloaded little trait?

The answer is higher order messages (or proxies).

A higher-order proxy is a shortcut that allows you to chain directly to the method name when doing a basic function.

You may have been using Laravel for years without being aware of their existence, but Laravel uses them everywhere.

  • HigherOrderWhenProxy — used in Conditionable like we just covered.
  • HigherOrderCollectionProxy — used by collections, e.g. $collection->map->name.
  • HigherOrderTapProxy — used by the tap helper and therefor by the Tappable trait.

How does it work?

Well, they work thanks to ✨magic✨. No, seriously — PHP magic methods to be exact. First let's look at Conditionable's when method, and then we can dive into the proxy class.

public function when($value = null, callable $callback = null, callable $default = null)
{
    $value = $value instanceof Closure ? $value($this) : $value;
    if (func_num_args() === 0) { // [!code highlight:7]
        return new HigherOrderWhenProxy($this);
    }

    if (func_num_args() === 1) {
        return (new HigherOrderWhenProxy($this))->condition($value);
    }

    if ($value) {
        return $callback($this, $value) ?? $this;
    } elseif ($default) {
        return $default($this, $value) ?? $this;
    }

    return $this;
}

Focusing on the highlighted area, we can see the one and zero argument variants are handled here. And those are the ones that use the HigherOrderWhenProxy.

As you can see, the one argument variant uses the value passed to when as the condition, while the zero argument variant does not set a condition because we didn't provide one!

Now let's look at HigherOrderWhenProxy and see how it handles it (once again, comments removed for brevity).

<?php

namespace Illuminate\Support;

class HigherOrderWhenProxy
{
    protected $target;

    protected $condition;

    protected $hasCondition = false;

    protected $negateConditionOnCapture;

    public function __construct($target)
    {
        $this->target = $target;
    }

    public function condition($condition)
    {
        [$this->condition, $this->hasCondition] = [$condition, true];

        return $this;
    }

    public function negateConditionOnCapture()
    {
        $this->negateConditionOnCapture = true;

        return $this;
    }

    public function __get($key)
    {
        if (! $this->hasCondition) {
            $condition = $this->target->{$key};

            return $this->condition($this->negateConditionOnCapture ? ! $condition : $condition);
        }

        return $this->condition
            ? $this->target->{$key}
            : $this->target;
    }

    public function __call($method, $parameters)
    {
        if (! $this->hasCondition) {
            $condition = $this->target->{$method}(...$parameters);

            return $this->condition($this->negateConditionOnCapture ? ! $condition : $condition);
        }

        return $this->condition
            ? $this->target->{$method}(...$parameters)
            : $this->target;
    }
}

Fix your eyes on the __call method here.

__call, as I said earlier, is a poor man's method overloading. __call is triggered whenever we attempt to invoke a method that does not exist on the object that implements it. It takes the method name being invoked and its arguments and you can do whatever you want with it!

The __call method diverges into two distinct branches here.

The first branch is the if-block, ! $this->hasCondition. The zero argument variant does not have a condition. It calls the $method on the $target (in our original example, the target was now() and the method was isWeekend()).

Here's something clever: they call their condition method with the result of the method call and return it. condition() returns $this and acts as a form of recursion because then the following method will only be called (nextWeekDay) if that previous method (isWeekend) was true.

The second branch is the final return statement which, as you can see, simply calls the method if the condition is true. The condition was provided in the when method when it was called.

And that is how the when proxy works! This blew my mind when I first dug into it. So much power in a couple miniscule files. This is what makes Laravel great in my opinion — truly embodying the artisan ethos.

How to implement yourself

The awesome thing about many of these internal patterns, including Conditionable, is that you don't have to do almost any leg-work. To use the power of the Conditionable trait, add it to a class that you think it would benefit.

The caveat is to not get carried away with it since it's so fun to use. Personally, I would reserve its use to Builder pattern classes, or classes that have a large API surface area where many method calls are expected behavior. Other examples where Laravel uses Conditionable are in collections, Carbon, and pending batches.

Conclusion

Some developers operating from a different set of ideals do not like Laravel because of its use of magic like we've seen in this article. Whether you should like it or not is for you to decide. Either way, Laravel has some interesting patterns under the hood, and I encourage you to take the time to look below the surface and understand them. You'll learn a lot, and it'll make you a better developer.

Resources

...

🔧 Digging into Laravel's Conditionable trait


📈 94.06 Punkte
🔧 Programmierung

🕵️ Medium CVE-2021-29929: Endian trait project Endian trait


📈 47.48 Punkte
🕵️ Sicherheitslücken

🔧 Enhancing Laravel Models with a Case-Insensitive Searchable Trait


📈 33.1 Punkte
🔧 Programmierung

🔧 Using the `InteractsWithUuid` Trait to Manage UUIDs in Laravel


📈 33.1 Punkte
🔧 Programmierung

🎥 Upgrade Laravel 11 to Laravel 12 #laravel #thecodeholic


📈 28.06 Punkte
🎥 Video | Youtube

🎥 Using Laravel Breeze Starter Kits in Laravel 12 #laravel #thecodeholic #laravel12


📈 28.06 Punkte
🎥 Video | Youtube

🎥 Print SQL Queries in Laravel #laravel #thecodeholic #laravelcourse #php #laravel


📈 28.06 Punkte
🎥 Video | Youtube

🎥 Create First Laravel Project using Laravel Herd #laravel #laravelproject #laravelphp


📈 28.06 Punkte
🎥 Video | Youtube

🔧 [Laravel v11 x Docker] Efficiently Set Up a Laravel App Dev Environment with Laravel Sail


📈 28.06 Punkte
🔧 Programmierung

🍏 AppStories, Episode 369 – Digging into the DMA


📈 24.93 Punkte
🍏 iOS / Mac OS

⚠️ Digging deeper into JAR packages and Java bytecode


📈 24.93 Punkte
⚠️ Malware / Trojaner / Viren

🔧 Off we go! Digging into the game engine of War Thunder and interviewing its devs


📈 24.93 Punkte
🔧 Programmierung

🎥 Digging into the Zoomit64 Executable with Mark Russinovich and Scott Hanselman at Microsoft Ignite


📈 24.93 Punkte
🎥 Video | Youtube

📰 SEC still digging into SolarWinds fallout, nudges undeclared victims


📈 24.93 Punkte
📰 IT Security Nachrichten

📰 Digging into AppBoundDomains in iOS


📈 24.93 Punkte
📰 IT Security Nachrichten

🔧 Digging Deeper into Code with Tree-Sitter: How to Query Your Syntax Tree


📈 24.93 Punkte
🔧 Programmierung

🕵️ CVE-2021-22909- Digging into a Ubiquiti Firmware Update bug


📈 24.93 Punkte
🕵️ Hacking

🔧 Digging Deep into the Core of Frontend Development


📈 24.93 Punkte
🔧 Programmierung

📰 Digging Into the Third Zero-Day Chrome Flaw of 2021


📈 24.93 Punkte
📰 IT Security Nachrichten

🔧 Mastering MySQL: Digging into Variables


📈 24.93 Punkte
🔧 Programmierung

📰 Digging into the Dark Web: How Security Researchers Learn to Think Like the Bad Guys


📈 24.93 Punkte
📰 IT Security Nachrichten

🔧 Mastering MySQL: Digging into Variables


📈 24.93 Punkte
🔧 Programmierung

🐧 Initcalls, part 2: Digging into implementation


📈 24.93 Punkte
🐧 Linux Tipps

📰 Digging into voice AI platform Deepgram


📈 24.93 Punkte
📰 IT Security Nachrichten

🕵️ New Cyber Operation Targets Italy: Digging Into the Netwire Attack Chain


📈 24.93 Punkte
🕵️ Hacking

📰 Digging into voice AI platform Deepgram


📈 24.93 Punkte
📰 IT Security Nachrichten

🕵️ Reversing Windows Internals (Part 1) - Digging Into Handles, Callbacks and ObjectTypes


📈 24.93 Punkte
🕵️ Reverse Engineering

🔧 Digging Into React Source Code: Why Not Use a Single Function called commitPhase?


📈 24.93 Punkte
🔧 Programmierung

🐧 Digging into the new features in OpenZFS post-Linux migration


📈 24.93 Punkte
🐧 Linux Tipps

🔧 The last source code: digging into bugs in projects after indie game studio shuts down


📈 24.93 Punkte
🔧 Programmierung

📰 Digging Deeper into Vulnerable Windows Services


📈 24.93 Punkte
📰 IT Security Nachrichten

🔧 Digging Deep Into Docker; A Step-by-Step Guide For Begginers


📈 24.93 Punkte
🔧 Programmierung

⚠️ Digging into the SAS 2024 agenda


📈 24.93 Punkte
⚠️ Malware / Trojaner / Viren