A clean way to safely call your potentially destructive methods in PHP
Tags: php, laravel, beginners, webdev
Published: 2021-08-14 22:35:23

I’ve been building an application with [Laravel 8.0](https://laravel.com/docs/8.x) recently that utilizes a lot of [console commands](https://laravel.com/docs/8.x/artisan#writing-commands) for various housekeeping reasons. For most of these methods, I want to utilize a `dry-run` test to deny changes by default.

The first most obvious approach is to use an option that you can check in each of your function calls. That would look like:

```php
public function myMethod() {
    if ($this->dryrun) {
        return;
    }
 
    // Execute destructive code...
}
```

This puts a little too much pressure on each function to know about what a `dry-run` even is, how to check against it, and it couples logic too directly.

The approach I’ve chosen to use instead utilizes PHP’s [magic](https://www.php.net/manual/en/language.oop5.magic.php) `__call()` to help make better determinations; much of Laravel uses this as well.

--

For my commands, I have a `BaseCommand.php` file that will ask for the dryrun option, store the value, and provide the magic methods required. Here’s the magic method that I wrote for it:

```php
<?php

// Rest of class...

/**
 * Magical CALL method that checks for dynamic method
 * names. Allows us to write "safelyMyFunction" but
 * reference "myFunction"
 *
 * @param string $method
 * @param mixed $arguments
 * @return void
 */
public function __call($method, $arguments)
{
    preg_match('/(safely|test)(.*)/', $method, $matches);

    // Check if we have any matches
    if (isset($matches)) {
        $protectedMethod = $matches[1] . 'Call';
        $classMethod = lcfirst($matches[2]);

        // Check if modified method exists on this class
        // e.g. "safelyCall(...)" or "testCall(...)"
        if (method_exists($this, $protectedMethod) && method_exists($this, $classMethod)) {
            return $this->$protectedMethod($classMethod, $arguments);
        }
    }
}
```

The signature `__call($method, $arguments)` is provided by PHP.

You can see we’re using a regular expression to check for the words `safely` and `test`. This gives us the flexibility to prefix more types of calls in the future where we could write: `safelyExecuteMethod` or `testExecuteMethod`, or potentially `queueExecuteMethod` , etc.

We call a method that matches such a pattern, then it continues on to parse it. It generates two things:

1. The internal call wrapper, e.g. `safelyCall`, `testCall`, or `queueCall`
2. A camel-case method name, e.g. `myMethod` rather than `MyMethod`

We check to make sure that `safelyCall` and `myMethod` exist on the instance, and if it does, we call it.

Where does `safelyCall` come from? It’s a custom method that we write based on the prefixes we want to support. Here’s mine:

```php
<?php

// Rest of class...

/**
 * Special call using prefix "safely"
 *
 * @param string $classMethod
 * @param mixed $arguments
 * @return void
 */
protected function safelyCall(string $classMethod, $arguments)
{
    // Check if it's a dry run
    if ($this->dryrun) {
        $this->log("🟡 Not executing `$classMethod` on a dry-run.\n");
        return false;
    }

    return call_user_func_array(array($this, $classMethod), $arguments);
}
```

This is the function that would be called when you prefix `safely` and you can see it accepts the `$classMethod` you’re trying to call and additional arguments.

It’s important to have this intermediate layer. You can see how we check for the `dryrun` here as opposed to in every single method that might want one. It’s a nice little bit of middleware at the class level.

If we put it all together, here’s what we end up with:

```php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;

abstract class BaseCommand extends Command
{
    /**
     * If we are expecting a dry run here
     *
     * For destructive calls, use --dryrun=false
     *
     * We are intentionally using "false" explicitly because
     * we don't want accidental values like "0" causing
     * damaging effects.
     *
     * @var boolean
     */
    protected bool $dryrun = true;

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $dryrun = $this->option('dryrun') === 'false';

        // Set options
        $this->dryrun = $dryrun ? !$dryrun : $this->dryrun;
    }

    /**
     * Magical CALL method that checks for dynamic method
     * names. Allows us to write "safelyMyFunction" but
     * reference "myFunction"
     *
     * @param string $method
     * @param mixed $arguments
     * @return void
     */
    public function __call($method, $arguments)
    {
        preg_match('/(safely|test)(.*)/', $method, $matches);

        // Check if we have any matches
        if (isset($matches)) {
            $protectedMethod = $matches[1] . 'Call';
            $classMethod = lcfirst($matches[2]);

            // Check if modified method exists on this class
            // e.g. "safelyCall(...)" or "testCall(...)"
            if (method_exists($this, $protectedMethod) && method_exists($this, $classMethod)) {
                return $this->$protectedMethod($classMethod, $arguments);
            }
        }
    }

    /**
     * Special call using prefix "safely"
     *
     * @return void
     */
    protected function safelyCall(string $classMethod, $arguments)
    {
        // Check if it's a dry run
        if ($this->dryrun) {
            $this->log("🟡 Not executing `$classMethod` on a dry-run.\n");
            return false;
        }

        return call_user_func_array(array($this, $classMethod), $arguments);
    }

    // ...

}
```

As you might have noticed, we’re missing a `testCall` method above, but you can determine if you want to add that. As mentioned, there’s the regular expression of `/(safely|test|whatever|you|want)(.*)/` which would allow you to define signatures for `safelyCall`, `testCall`, `whateverCall`, `youCall`, and `wantCall`. If the method doesn’t exist, it will just be skipped.
How to use it?

### How to use it?
Let’s say we want to run some potentially destructive SQL queries that in order to clean abandoned content. *Please note that the following design is for demonstrative purposes.*

We can have a function that searches for the content we want to remove and another that will actually remove it. Separating the [search and destroy](https://www.youtube.com/watch?v=NUAdgt5Glk0) methods provides more flexibility and allows a structure like this to work.

```php
<?php

// Rest of class...

/**
 * Search and destroy unused content
 * 
 * @return void
 */
protected function cleanContentTable()
{
    $sql = "
        SELECT `content`.*
          FROM `content`
     LEFT JOIN `user` ON `user`.`content_id` = `content`.`id`
         WHERE (`user`.`id` IS NULL AND `content`.`type` = 1234)
    ";

    // Determine rows found
    $ids = array_column(DB::select($sql), 'id');

    // Checks to see if it's a dry run or not first
    $this->safelyRemoveContentByIds($ids);

    // Does NOT test for dry run, just removes content
    // $this->removeContentByIds($ids);
}

/**
 * Remove content
 * 
 * @param array $ids
 * @return void
 */
protected function removeContentByIds(array $ids = []) 
{
    // Delete content...
}
```

You can see that our `cleanContentTable` is the primary call that will search for the content, find a list of IDs, then ask to destroy them.

The function that actually destroys them is `removeContentByIds` but you’ll notice we’re literally calling something else; we’re actually calling `safelyRemoveContentByIds` which doesn’t technically exist in the class.

This is where the [**magic**](https://www.php.net/manual/en/language.oop5.magic.php) happens. Since the method doesn’t actually exist, it’s forwarded to `__call()` provided by PHP. In our earlier code, we wrote some logic to parse out the method name and delegate further responsibilities.

By prefixing our destructive call with a self-commenting `safely` prefix, we’re able to call methods without writing unnecessarily repetitive logic. From this point, you can write whatever type of prefix, suffix, or anything you want to intelligently wrap your method calls with self-documenting code.

### But you must remember…

[Magic](https://www.php.net/manual/en/language.oop5.magic.php) can be very dangerous and is often **not** the right choice!

Some people use magic to hide poorly written code. It’s akin to asking a child to clean his room and then finding out he just shoved all of his toys in the closet; that’s not clean kiddo. *(It’s what I used to do when I was young.)*

If you use magic in that way, you’re abusing the system.

I would argue the use of magic in this example is valuable because it’s designed to reduce repetitive code in a language that doesn’t support decorators while extending value to the developers.

There are some frameworks that allow you to decorate functions using comments, but personally, *I think that’s an awful idea*. Comments should never be allowed to execute code.