DEV Community

Cover image for The Dangers of Dynamic Method Calls in PHP
Ash Allen
Ash Allen

Posted on • Originally published at ashallendesign.co.uk

The Dangers of Dynamic Method Calls in PHP

Introduction

In your PHP application, you may sometimes see dynamic method calls being used. This is typically where a method name is constructed at runtime and then called on an object. For example, $this->{'methodName'}() could be used to call a method named methodName.

Dynamic method calls can be useful, but they also come with some risks that you should be aware of.

In this article, we'll explore the dangers of using dynamic method calls in PHP and provide some alternatives to consider.

What are Dynamic Method Calls?

Dynamic method calls in PHP allow you to call a method on an object using a variable or expression to determine the method name at runtime.

Typically, you'd call a method using something like $this->methodName(). But with dynamic method calls, you can do something like $this->{'methodName'}() or $this->{$variable}() where $variable contains the name of the method you want to call.

As you can imagine, this can be quite powerful, as it allows for flexible code. This is especially true in scenarios where the method to be called is not known until runtime (such as if you're building a framework or library). And I think this is where they can be useful. However, in most application code, I think there are usually better, safer alternative approaches.

To help us understand the dangers, let's take a look at an example. We'll imagine we're building a class which handles a webhook we receive from an external service. The webhook payload contains an event field which tells us what type of event it is (e.g. "success", "failure", etc.). We want to call a different method based on the value of this event field. For instance, if the event is "success", we want to call a handleSuccessWebhook method. If it's "failure", we want to call a handleFailureWebhook method. The payload might look something like this:

$payload = [
    'event' => 'success',
    'details' => [
        // ...
    ],
];
Enter fullscreen mode Exit fullscreen mode

And the webhook handler class might look something like this:

final readonly class WebhookHandler
{
    public function handleWebhook(array $payload): void
    {
        $this->{'handle' . ucfirst($payload['event']) . 'Webhook'}($payload);
    }

    private function handleSuccessWebhook(array $payload): void
    {
        // Handle a "success" webhook here...
    }

    private function handleFailureWebhook(array $payload): void
    {
        // Handle a "failure" webhook here...
    }

    // Other webhook handling methods here...
}
Enter fullscreen mode Exit fullscreen mode

We can see here that we have a public handleWebhook method which takes the payload as an argument. It extracts the event field from the payload and then constructs the method name to call based on that event. It uses ucfirst to ensure the first letter of the event is capitalised, and then appends "Webhook" to the end of it. Finally, it calls the constructed method name on $this.

The Dangers of Dynamic Method Calls

Now let's explore some of the dangers of using this approach.

Difficulty with IDEs

One of the biggest issues I've faced with dynamic method calls is that integrated development environments (IDEs), such as PhpStorm, struggle to understand their usage. Since the method name is being constructed at runtime, it's difficult for the IDE to detect whether the handleSuccessWebhook and handleFailureWebhook methods are actually being used. This can lead to the IDE marking them as unused, which can be misleading.

In the past, I've been tempted to delete methods that my IDE has marked as unused, only to later discover that they were indeed being used via a dynamic method call. This can lead to bugs and unexpected behaviour in your application. Thankfully, I caught them in time before deploying to production. But it was a close call.

Another downside to PhpStorm not being able to understand the dynamic method calls is that you can't make full use of the IDE's refactoring tools. For example, if you want to rename a method in PhpStorm, it won't be able to find all the references (since they're dynamic), and it won't rename them for you. This can lead to broken code if you're not careful.

Harder to Find

Another issue I've found with dynamic method calls is that you can't easily search for their usages in your codebase.

Let's say you want to find anywhere the handleSuccessWebhook method is being called. So you hit CMD+SHIFT+F in PhpStorm to open the global search window and search for "handleSuccessWebhook". You won't find any results (apart from the method definition itself), because the method name is never explicitly mentioned anywhere else in the code.

At this point, you've got to ask yourself: "Is this method being used anywhere? Or is it safe to delete it?". This question becomes much easier to answer if you have a comprehensive test suite that covers that particular method and confirms that it is being used. But if your test suite doesn't cover this particular feature, then you've got to manually inspect the code to see if it's being used anywhere. This can be time-consuming and error-prone, especially in larger codebases.

Harder to Read

I personally find that dynamic method calls can make the code harder to read at first glance. Which of these looks clearer to you at first glance?

// Dynamic method call:
$this->{'handle' . ucfirst($payload['event']) . 'Webhook'}($payload);

// Or, traditional method call:
$this->handleSuccessWebhook($payload);
Enter fullscreen mode Exit fullscreen mode

I'm going to make a guess that most people would find the traditional method call clearer. With the dynamic method call, you've got to mentally parse the string concatenation to figure out what method is being called. This can be especially challenging if the string concatenation is more complex, or if you don't know what the possible values of $event are.

An Alternative Approach

For the reasons above, I generally avoid using dynamic method calls in my code. Instead, I prefer to use a more explicit approach.

This is purely my personal preference, though, and it's not to say that dynamic method calls are inherently bad. So if you're reading this and use them in your own code, please don't think I'm insulting your code. If it works for your use case, is fully covered by tests, and makes you productive, then I'm all for it.

I just prefer the extra confidence and safety I get from using a more explicit approach. And I also think it makes the code easier to read, especially for developers who are new to the codebase.

If I were to refactor the WebhookHandler class above, I might use a match expression instead:

final readonly class WebhookHandler
{
    public function handleWebhook(array $payload): void
    {        
        match ($payload['event']) {
            'success' => $this->handleSuccessWebhook($payload),
            'failure' => $this->handleFailureWebhook($payload),
            default => throw new Exception('No handler for the event: ' . $event)
        };
    }

    private function handleSuccessWebhook(array $payload): void
    {
        // Handle a "success" webhook here...
    }

    private function handleFailureWebhook(array $payload): void
    {
        // Handle a "failure" webhook here...
    }

    // Other webhook handling methods here...
}
Enter fullscreen mode Exit fullscreen mode

In the approach above, we've used a "match expression" which reads the payload's event field and then calls the appropriate method based on its value. If the event is not recognised, it throws an exception.

By using this approach, we've been able to explicitly write the method names in our code. As a result, it means our IDE can understand their usage, so we can make full use of its features (such as refactoring tools, and knowing which types will be returned). It also means we can easily search for their usages in our codebase. I'd also argue that it makes the code easier to read at a first glance, too.

Conclusion

Hopefully, this article has given you some food for thought about the dangers of using dynamic method calls in PHP. While they can be useful in certain scenarios, they also come with some risks that you should be aware of.

If you enjoyed reading this post, you might be interested in checking out my 220+ page ebook "Battle Ready Laravel" which covers similar topics in more depth.

Or, you might want to check out my other 440+ page ebook "Consuming APIs in Laravel" which teaches you how to use Laravel to consume APIs from other services.

If you're interested in getting updated each time I publish a new post, feel free to sign up for my newsletter.

Keep on building awesome stuff! 🚀

Top comments (9)

Collapse
 
xwero profile image
david duymelinck

I agree method/function/class name building should only be done if there is no other solution. For example when the code calls a class that is added by people that use your library/framework.
I would use the method_exists function to be sure the code can be executed.

I don't like the use of the match function in the example. You are not returning in those handlers because they have a void return type. This is the case where you should use switch.

Collapse
 
ashallendesign profile image
Ash Allen

Yeah, I completely agree, a method_exists function call is super helpful if you have to use the dynamic method calls.

However, I think using a match expression is completely valid here. Although the match expression can return a value, it doesn't necessarily need to 😄

Collapse
 
xwero profile image
david duymelinck

The idea behind the match function is that it is a more convenient way to return a value when there are multiple cases. Not returning a value goes against the expectations. It is like going through a red light, because you feel that is not for you.

Thread Thread
 
ashallendesign profile image
Ash Allen • Edited

I guess we'll have to agree to disagree on that one. Just because it can return a value, it doesn't mean it must return a value (as mentioned in the PHP docs: php.net/manual/en/control-structur...) 🙂

Thread Thread
 
xwero profile image
david duymelinck • Edited

From the documentation page

Note: The result of a match expression does not need to be used.

This doesn't mean it shouldn't return a value. It is up to the application code how to use the result. But why use match if there are other control structures that have a looser definition.

Thread Thread
 
ashallendesign profile image
Ash Allen

As I say, we'll just have to agree to disagree. There are many ways to approach code, and a lot of it is down to personal preference and team conventions. We both use different (completely valid) conventions, and there's nothing wrong with that 🙂

Thread Thread
 
xwero profile image
david duymelinck

Conventions are based on agreement. There is nothing in the documentation that states a return is not required, it is exactly the opposite if you look at all the examples.

The only reason you want to use match over switch or if is because it saves you some typing. But that is not enough of a reason to create your own "convention".

If it would be used in a clever way, that is debatable. But using it as you do is as bad a using a do while loop as an if. You are changing the meaning of the code.

Collapse
 
peymansheybani profile image
peyman sheybani

For the problems you mentioned, you can put phpDoc in the class where you are using the dynamic method and define the methods so that the IDE can identify them.

Collapse
 
ashallendesign profile image
Ash Allen

That's definitely one approach that you can take (and sometimes might be the easiest depending on the use case). However, I try to avoid using docblocks for defining that a method, property, etc, like this exists wherever I can.

Docblocks can become outdated if they're forgotten about. In fact, I've run into this exact issue before with one of my Laravel packages (github.com/ash-jc-allen/short-url). I had a docblock written to define some magic methods. I then updated the methods and forgot to update the docblocks, so they were out of sync.

But if you can keep on top of the docblocks and remember to keep them updated, they can be super useful 😄