Route controller callback as array


#1

Hello to everyone,

I’m reading this article on Glenn Eggleton’s blog and got a doubt about controller callbacks:

He explains it is possible to provide a php array callback to a route as follows:

//Option A
$app->get('/hello', /*Route Action*/ [MyAction::class, 'myAction']); 
//MyAction defines myAction($req, $args)

However when I do so, I’m getting the following error:

Using $this when not in object context in....

The route works ok if I use the ‘classname:method’ style.

Is it really possible to do it that way? Wouldn’t the first field of the array callback need to be an object instead of a class name to be considered a valid php callback?

Thank you!


#2

This is working for me…

index.php

<?php

require __DIR__ . '/../vendor/autoload.php';

$config = ['settings' => [
    'addContentLengthHeader' => false,
]];
$app = new \Slim\App($config);

$app->get('/hello/{name}', [Src\MyAction::class, 'hello']);

$app->run();

MyAction.php

<?php

namespace Src;

class MyAction
{
    public function hello($request, $response, $args)
    {
        return $response->write("Hello " . $args['name']);
    }
}

My request is http://localhost:8080/hello/Slim and the response is Hello Slim.


#3

Hello Tim, your example works for me too.

The problem seems to occur when using some service from the container, it fails
using the array callback, and works okay with the class:method style.

I change a little your example to reproduce the problem (add one service, and one controller to the container) :

<?php

require __DIR__ . '/../vendor/autoload.php';

$config = ['settings' => [
	    'addContentLengthHeader' => false,
		]];

$app = new \Slim\App($config);
$container = $app->getContainer();

class Greeter
{
	public function sayHello()
	{
		return 'Hello ';
	}
}

$container['greeter'] = function($c) {
	return new Greeter;
};

class Controller
{
	public function __construct($greeter) 
	{
		$this->greeter = $greeter;
	}

	public function hello($request, $response, $args) {
		return $response->write($this->greeter->sayHello().$args['name']);
	}
}

$container['Controller'] = function($c) {
	return new Controller($c['greeter']);
};

$app->get('/hello/{name}', [Controller::class,'hello']); // This fails
//$app->get('/hello/{name}', Controller::class.':hello'); # This works
$app->run();

#4

@tflight I think this syntax is deprecated since PHP 7.2:

[MyAction::class, 'hello']

I get this error message in this line:

$app->get('/hello/{name}', [MyAction::class, 'hello']);
Deprecated: Non-static method MyAction::hello() should not be called statically

I use this syntax:

$app->get('/', \App\Action\HomeIndexAction::class);
<?php

namespace App\Action;

use Psr\Http\Message\ResponseInterface;
use Slim\Http\Request;
use Slim\Http\Response;

class HomeIndexAction
{
    public function __invoke(Request $request, Response $response): ResponseInterface
    {
        return $response->write("Hello world");
    }
}

#5

Maybe the problem is that the [‘className’, ‘method’] it is a valid callable but it is not a instance of Closure, therefore it can not be bound to the scope of the container, and $this won’t be usable. It will work for static methods or methods that not refer to $this though.

But, that would mean that the syntax in Glenn Eggleton’s blog is not valid in Slim. In Slim docs doesn’t seem to be documented that style either. I am confused.


#6

My suggested route is document in the Slim docs here:

Note that the second parameter is a callback. You could specify a Class (which need a __invoke() implementation) instead of a Closure. You can then do the mapping somewhere else:

$app->any('/user', 'MyRestfulController');

This is the same like writing this:

$app->any('/user', \App\Controller\MyRestfulController::class);

The positive effect with the ::class syntax is, your IDE (and phpstan) can type hint this class and you can refactor this class very easily. Last but not least, your Controller (or Action) class is responsible for only one thing (SRP). :slight_smile:


#7

Hi @odan, thank you for your help! I like the idea of Action classes to meet the SRP, however…

What would be the best way to deal with the Action classes, should I put each of them in their own file so they can be loaded with psr-4 autoloader? What if there are a lot of actions? Isn’t it difficult to deal with so many files?

Also, where will you set up the services needed by the Action class? In the constructor, or adding the Action Class to the container?

I think that setting up the services for the controllers, either in the constructor (and receiving the entire container), or adding the entire class to the container itself it’s pretty much the same task over and over. Is this ok? I didn’t use a framework before, so I am a little confused with these things.

Thank you!