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.