Call a function by name

An ability to call a function/method by name does not exist in compiled languages. But it exists in plain PHP and is essential in various routing/ORMs and so on.

KPHP does not support calling by name, just as any other compiled language. How to achieve the same result?

How is it done in plain PHP?

In PHP you just call any function by name / any method by name / create a class by name:

$f_name = "...";
$f_name();
$f_name(1, "with", ["arguments"]);    
// and methods
$method_name = "...";
A::$method_name();
call_user_func(['A', $method_name]);
// and classes
$class_name = "...";
new $class_name("constructor", "parameters");  

This is prohibited in KPHP. All calls, all property access — everything must be statically resolved.

How to achieve this in KPHP?

Think of KPHP as of any other compiled language. Nothing better than switch-case is generally unavailable.

And this is OK, this is not a drawback. Static call graph makes your code predictable and understandable, you always see all available methods reached out from here.

$func_name = "...";
switch ($func_name) {
case "authenticate":
  authenticate();
  break;
case "logout":
  logout();
  break; 
}

If so, how do I perform request routing?

Let's say you want your-site.com?page=profile&action=edit to call \UrlHandler\Profile::edit(). You have various classes in \UrlHandler namespace for &page get parameter and every class has a method for &action.

First of all, even in plain PHP, it's a bad idea to do like this:

// bad: primitive url mapping
$class_name = "\\UrlHandler\\" . ucfirst($_GET['page']);
$method_name = $_GET['action'];
$class_name::$method_name();

Why is it a bad pattern? Take these points under consideration:

  • you don't have a full list of all available URLs
  • if an action has been renamed, to make old links work you should handle it somewhere
  • when an URL requires the user to be logged, you check this in each handler
  • when an URL is available only for admins/VPN, you check this in each handler
  • when an URL distinguishes GET/POST/etc, you handle this in each handler

“You handle this in each handler” using PHP code is not a good practice — not just because it leads to code duplication — but because you spread the area of responsibility between routing and business logic.

Declarative descriptions are much acceptable than imperative handling.

Having all this, you probably already use a declarative manner:

  • Either you write something like this:
Route::get('/profile/edit', ['logged' => true], 'Profile::edit');  
  • Or you use annotations, something like this:
class Profile {
  /**
   * @url-handler logged:true method:get
   */
  static public function edit() { /* ... */  }
}
  • Or you have a schema, JSON/PHP/doesn't matter:
[
  { "url": "/profile_edit", "logged": true, "method": "get", "handler": "Profile::edit" }
]

All these approaches are similar for the following reasons:

  1. you use declarative manner, not imperative logic for common handling parts (logged, etc)
  2. you have a full list of what can be called
  3. calling by name is OK here — its safeness comes from your source code, it happens only on correct input

Because of (2), you can write a script that generates a switch-case code — and use it instead of (3).

This article tried to address the following:

  • either you don’t need calling by name at all,
  • or you have 2-3 cases and write switch-case manually,
  • or you should have a declarative source, from which you can codegenerate switch-case; if you don’t, you are doing something wrong anyway