4
\$\begingroup\$

I am putting together a php Rest API service that uses Controller and Command pattern to handle requests.

Firstly, in Apache config I redirect all requests to a single end point, /api/v1/index.php.

RewriteEngine On
RewriteRule ^/api/v1/([^/]*)/([^/]*)/* /api/v1/index.php/?command=$1&subcommand=$2 [PT]
RewriteRule ^/api/v1/([^/]*)/* /api/v1/index.php/?command=$1 [PT]

That maps /api/v1/param1 to /api/v1/?command=param1 and /api/v1/param1/param2 to /api/v1/?command=param1&subcommand=param2. Depending on the parameters, the correct Command object is created and a correct function of that object is called based on REQUEST_METHOD and presence of subcommand. The beauty of it is that you can add a new route as a file in commands folder and CommandResolver will automatically use it if appropriate based on param1 value. It matches param1 with a file in commands folder.

It is very similar to how Express handles routes. You declare a route path and the function is called when needed by the express:

//express routing
app.get('/ab+cd', function (req, res) {
  res.send('ab+cd')
})

Each route is declared only ones in both Express and the design I present.

This is the structure of the project: enter image description here

//index.php
<?php
require("vendor/autoload.php");

$controller = new api\Controller();
$controller->handleRequest();

// Controller.php

<?php
namespace api;
class Controller{
    public function handleRequest(){
        $commandResolver = new CommandResolver();
        $command = $commandResolver->resolveCommand();
        $command->execute();
    }
}

//CommandResolver.php

<?php
namespace api;

class CommandResolver{
    private string $base_class = "api\\v1\commands\\ACommand";
    public string $command;

    public function resolveCommand():ACommand{

        if(isset($_GET) && !empty($_GET) ){
            $this->command = $_GET["command"];

        }
        else if(isset($_SERVER['argv'])){

            $this->command = explode("=", $_SERVER['argv'][1])[1];
        }

        $this->command = ucfirst($this->command);

        if(class_exists("api\\v1\commands\\".$this->command)){
            $command_class = new \ReflectionClass("api\\v1\commands\\" . $this->command);

            if ($command_class->isSubClassOf($this->base_class)){
                return  $command_class->newInstance();
            } else {
                throw new \Exception("Command: '" . $this->command . "' is not subclass of base command.");
            }

        }else{
            throw new \Exception("Command: '" . $this->command . "' does not exist.");

        }
    }
}

//ACommand.php

<?php
namespace api\v1\commands;
abstract class ACommand{

    public function execute(){

        if(isset($_GET["subcommand"])){
            $subcommand = $_GET["subcommand"];
            switch ($_SERVER['REQUEST_METHOD']){
                case "GET": $this->GetExecuteWithData($subcommand);break;
                case "POST": $this->PostExecuteWithData($subcommand);break;
                case "PATCH": $this->PatchExecuteWithData($subcommand);break;
                default:break;
            }
        }else{
            switch ($_SERVER['REQUEST_METHOD']){
                case "GET": $this->GetExecute();break;
                case "POST": $this->PostExecute();break;
                case "PATCH": $this->PatchExecute();break;
                default:break;
            }
        }
    }

    protected function GetExecute(){}
    protected function PostExecute(){}
    protected function PatchExecute(){}

    protected function GetExecuteWithData(string $version){}
    protected function PostExecuteWithData(string $version){}
    protected function PatchExecuteWithData(string $version){}
}

Version.php // example of end point

<?php
namespace api\v1\commands;
class Version extends ACommand{
    protected function GetExecuteWithData(string $version{
        echo $version."<br>";
    }

    protected function PostExecute(){
        // handle $_POST data
    }
}

I am very satisfied with the ease of use of this approach. However, I can't think of a way to extend this approach to more than 2 url params. Currently it only handles urls of format /api/v1/param1 and /api/v1/param1/param2. I'd like to extend it to also handle /api/v1/param1/param2/param3 and /api/v1/param1/param2/param3/param4. The Express way is the following:

app.get('/users/:userId/books/:bookId', function (req, res) {
   res.send(req.params)
})

I'd like to keep the 1 route to 1 function mapping. Route /users/:userId/books/:bookId would map to different function than /users/:userId/toys/:toyId. I am looking for ideas on how to implement this in PHP and keep code repeating to minimum.

\$\endgroup\$
1
  • \$\begingroup\$ There is a lot to review. And I might add my review if I have some free time after work. Util then, you can check the slim framework and especialy their router. slimframework.com/docs/v3/objects/router.html \$\endgroup\$
    – slepic
    Commented Feb 3, 2020 at 9:36

1 Answer 1

3
\$\begingroup\$

Reinventing the wheel

You didnt make any indication that you are doing this as excercise. In that case you should try something already written and tested. Like Slim framework (http://www.slimframework.com/docs/v3/objects/router.html) which looks very similar to the "Express way".

Also check out PSR interfaces.

In case this is just an excercise for you or you simply want to have everything under your own control for some reason, let me review your code anyway (in a bit abstract way tho)

Apache

You are making it unnecesarily coupled to apache rewrite module. You can just put redirect everything (except static assets I suppose) to index.php. This will make it very easy to switch to nginx for example, someday in future. And decide on which "command" to use just from the request uri (accessible as $_SERVER['REQUEST_URI']). Unless of course it is essential for you to be able to call the api as /command-x as well as /?command=command-x. Personally I dont see a reason for this. This will also automatically allow you to encode much more things in the uri path.

Dependency Injection

You should read something about dependency injection. Ie here:

public function handleRequest(){
        $commandResolver = new CommandResolver();
        $command = $commandResolver->resolveCommand();
        $command->execute();
    }

It is not very good if one class instantiates another classes and at the same time acts on the new instance. In the case above, CommandResolver instance should be passed to the controller in contructor.

You will also find out that to inject dependencies to the individual commands, you will need something more sophisticated than match command name to a file. As those individual commands will have different dependencies and thus different constructors.

(Super)global variables

Try to avoid access to global variables as much as possible. You should only access $_GET, $_POST, etc from one place and pass the values around to those who need them. Dont access those superglobals from any class where you need it.

Abstract Command Is Too Restrictive

You should not restrict your commands to POST PATCH and GET. Some endpoints may not have all of those, some endpoints may need more. You could pass something like Psr\Http\RequestInterface to your commands and let them decide if method is acceptable. And unlike in your code returning HTTP 405 is appropriate when given http method is not supported for given endpoint.

The command should be just an interface with probably one method, if not using directly \Psr\Http\Server\RequestHandlerInterface.

The abstract class will only make things worse.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.