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:
//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.