1

I have a class that looks a bit like this:

<?php

namespace App\Http\Controllers;

use Exception;
use Illuminate\Http\Request;

class FormatAddressController extends Controller
{
    public function __construct()
    {
        $this->middleware(['api-auth']);
        $this->middleware(['api-after']);
    }

    public function format(Request $request) {
        // TODO first, get customer settings to see what functions to run, and how to run them
        // but, assume that the settings come as an array where the key is the function name
        // and the value is one of NULL, false, or settings to pass through to the function
        $settings = ['isoAndCountry' => true, 'testFunc' => ['testSetting' => 'test setting value']];

        $address = $request->input('address');

        $errors = [];

        foreach ($settings as $funcName => $funcSettings) {    
            try {
                $address = $this->$funcName($funcSettings, $address); // every function has to return the modified address
            } catch(Exception $e) {
                $errors[$funcName] = $e;
            }

        }

        return response()->json([
            'address' => $address,
            'errors' => $errors
        ]);
    }

    public function isoAndCountry($settings, $address) {
        // TODO

        return $address;
    }
}

Now, when I call this function, isoAndCountry, through that settings loop I defined above, it works! It works just fine.

However I tried following this thread and checking is_callable and... it errors:

            if (is_callable($this->$funcName)) {
                try {
                    $address = $this->$funcName($funcSettings, $address); // every function has to return the modified address
                } catch(Exception $e) {
                    $errors[$funcName] = $e;
                }
            }

How can I check if it's callable? Why doesn't this work?

TKoL
  • 13,158
  • 3
  • 39
  • 73
  • 2
    what the error you got here ? – Yassine CHABLI Jun 25 '19 at 16:41
  • Undefined property: App\Http\Controllers\FormatAddressController::$isoAndCountry – TKoL Jun 25 '19 at 16:45
  • Try doing `call_user_func([ $this, $funcName ])` to avoid the complexities of operator precedence and evaluation order – apokryfos Jun 25 '19 at 16:55
  • @apokryfos is there any documentation to describe the complexities you're talking about? I'm relatively new to PHP and would like to learn what pitfalls to avoid and why. – TKoL Jun 25 '19 at 16:58

4 Answers4

2

May be this can also solve the problem :

 if(method_exists($this,$funcName)){ ... }
Yassine CHABLI
  • 3,459
  • 2
  • 23
  • 43
  • Yes, me too. Still, is_callable should have worked as well (https://3v4l.org/TNN70), so I'm wondering if something else is going on that we can't see. – Don't Panic Jun 25 '19 at 17:08
  • the **testFunc** is missing ? isn't that ? – Yassine CHABLI Jun 25 '19 at 17:17
  • I thought that was the point. It's still in the `$settings` array in that example link, and is_callable verifies that it doesn't exist in the object so it doesn't get called. – Don't Panic Jun 25 '19 at 17:19
2

You have to use method_exists here to check if the method really exists in the class.

foreach ($settings as $funcName => $funcSettings) {
    if (method_exists($this, $funcName)) {
        $this->$funcName($funcSettings, $address);
    }
}

The reason why is_callable will not work in your scenario is because Laravel controllers has a __call magic method which will handle undefined methods, so running is_callable on any non existing methods would return true.

Take the following class as an example:

class A
{
    public function __construct()
    {
        var_dump(is_callable([$this, 'testFunc']));
    }
}

The output of new A would be false. However, if you add the following into the class:

public function __call($name, $arguments)
{
    //
}

Now the output of the var_dump would return true.

You can read more about the __call scenario I've mentioned right here: https://www.php.net/manual/en/function.is-callable.php#118623

For more information about __call: https://www.php.net/manual/en/language.oop5.overloading.php#object.call

Chin Leung
  • 14,621
  • 3
  • 34
  • 58
1

You can use

if (is_callable([$this, $funcName])) { ...

instead.

The way you have it written with is_callable($this->$funcName), it's going to look for a property called $funcName on $this, (which probably doesn't exist) and check if that's callable. If you use that array syntax it will evaluate the named method instead.


It may be simpler in this case to use

if (method_exists($this, $funcName)) {

since you're using it in another method of the same object, if the method exists it should be callable.

Don't Panic
  • 41,125
  • 10
  • 61
  • 80
  • OK so I've done it the way you said, and when $funcName is isoAndCountry, it returns true, but... strangely, when $funcName iterates to the next item, testFunc, that ALSO returns true, even though I defined no testFunc function into the class. It returns true, tries to run the function, and then errors with `Method App\Http\Controllers\FormatAddressController::testFunc does not exist.` – TKoL Jun 25 '19 at 16:48
  • Uh oh, I didn't think that would happen, sorry – Don't Panic Jun 25 '19 at 16:50
  • Pretty strange though, huh? I didn't expect it either. – TKoL Jun 25 '19 at 16:52
  • It returns false? – TKoL Jun 25 '19 at 16:57
  • It's possible that it's because I'm in Laravel maybe? – TKoL Jun 25 '19 at 16:57
  • I made this, based on your example (as closely as possible without the framework stuff) https://3v4l.org/TNN70 – Don't Panic Jun 25 '19 at 16:58
  • Maybe it's getting called directly somewhere else you aren't expecting? – Don't Panic Jun 25 '19 at 16:59
  • You can have a look at my answer. I explain why `is_callable` will not work in his scenario. :) – Chin Leung Jun 25 '19 at 17:49
0

You need to differentiate between class properties and methods. For example in this class:

class A {
    private $foo = null;

    public function getFoo() {
        return $this->foo;
    }
}
  • the private $foo is property and can be checked by property_exists()
  • the public function getFoo() is method and can be checked by method_exists()

https://www.php.net/manual/en/function.property-exists.php
https://www.php.net/manual/en/function.method-exists.php

I think that $this->$funcName works only for properties.

Paolo42
  • 182
  • 1
  • 8