├── test ├── products_xml.png ├── products_1_json.png ├── products_1_xml.png ├── products_json.png └── api.php ├── LICENSE ├── README.md └── WebApi.php /test/products_xml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungive/php-rest-api/HEAD/test/products_xml.png -------------------------------------------------------------------------------- /test/products_1_json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungive/php-rest-api/HEAD/test/products_1_json.png -------------------------------------------------------------------------------- /test/products_1_xml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungive/php-rest-api/HEAD/test/products_1_xml.png -------------------------------------------------------------------------------- /test/products_json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ungive/php-rest-api/HEAD/test/products_json.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jonas van den Berg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/api.php: -------------------------------------------------------------------------------- 1 | id = $id; 17 | $this->name = $name; 18 | $this->price = $price; 19 | } 20 | } 21 | 22 | class ProductsController extends ApiController 23 | { 24 | private $products = []; 25 | private $isXml = false; 26 | 27 | public function __controller() 28 | { 29 | $this->products[] = new Product(1, 'Pizza', 4); 30 | $this->products[] = new Product(2, 'Keyboard', 32.95); 31 | $this->products[] = new Product(3, 'Water', 1.09); 32 | 33 | $this->JSON_OPTIONS = JSON_PRETTY_PRINT; 34 | $this->RESPONSE_TYPE = $this->OPTIONS[0]; 35 | $this->isXml = strcasecmp($this->RESPONSE_TYPE, 'xml') === 0; 36 | } 37 | 38 | /** :GET :/{controller}.(json|xml)/ */ 39 | public function products() 40 | { 41 | return $this->isXml ? [ 42 | 'products' => $this->products 43 | ] : $this->products; 44 | } 45 | 46 | /** :GET :/{controller}/{$id}.(json|xml)/ */ 47 | public function product($id) 48 | { 49 | foreach ($this->products as $product) { 50 | if (strval($product->id) === $id) { 51 | return $this->isXml ? [ 52 | 'product' => $product 53 | ] : $product; 54 | } 55 | } 56 | 57 | return new HttpResponse(404, 'Not Found', (object)[ 58 | 'exception' => (object)[ 59 | 'type' => 'NotFoundApiException', 60 | 'message' => 'Product not found', 61 | 'code' => 404 62 | ] 63 | ]); 64 | } 65 | } 66 | 67 | $api = new Api(); 68 | $api->handle(); 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Creating a REST API with PHP 2 | 3 | This tutorial will cover all features of this library and how to use them. I.a. it has the following features: 4 | - Each method can have any number of HTTP verbs associated to it in its documentation comment. [#](#http-verbs) 5 | - Methods are distinguished by their parameters if the method name is not specified in the URI. 6 | - The URI can contain variables[#](#uri-variables) and regex-like alternations[#](#alternations) if desired. E.g. `(json|xml)`. 7 | A rewrite engine is not needed for that. 8 | - The `ApiController` base class has several attributes that you can work with inside your controller class. [#](#the-apicontroller-class) 9 | - A response (-object) is encoded by the WebApi class. The supported types are JSON and XML. 10 | - The HTTP response code can be specified by returning an instance of the `HttpResponse` class. 11 | 12 | -- 13 | 14 | The `WebApi` class is located in the `Vanen\Mvc` namespace and contains the following attributes and methods: 15 | >`$version` - The version of the api (optional). Is added directly after the file name and before the URI path: e.g. "v1": `/api.php/v1/products`. 16 | `__construct($version = null)` - Constructor with $version as parameter. 17 | `handle()` - Handles the request. 18 | `allowHeader($name)` - Allows a header. Its value will be stored in `ApiController`'s[#](#the-apicontroller-class) `$HEADERS` property. 19 | 20 | 21 | -- 22 | 23 | This is an example of a simple API controller located in `api.php`: 24 | 25 | ```php 26 | products[] = (object)['id' => 1, 'name' => 'Pizza', 'price' => 3.85]; 39 | $this->products[] = (object)['id' => 2, 'name' => 'Pencil', 'price' => 0.49]; 40 | $this->products[] = (object)['id' => 3, 'name' => 'Flashdrive', 'price' => 14.99]; 41 | } 42 | 43 | /** :GET */ 44 | public function products() 45 | { 46 | return $this->products; 47 | } 48 | 49 | /** :GET */ 50 | public function product($id) 51 | { 52 | foreach ($this->products as $product) { 53 | if (strval($product->id) === $id) { 54 | return $product; 55 | } 56 | } 57 | 58 | return null; 59 | } 60 | } 61 | 62 | $api = new Api(); 63 | $api->handle(); 64 | ``` 65 | 66 | ### Method Attributes 67 | 68 | #### HTTP Verbs 69 | 70 | This controller has two methods. They can be called like this: 71 | `/api.php/products` returns all products. 72 | `/api.php/products?id=1` returns the product with the id 1. 73 | 74 | Both methods only allow `GET` requests as specified in the documentation comment. A method's attribute is identified by a preceding colon to differ from normal comments. Hence `/** :POST :PUT */` will allow `POST` and `PUT` requests. 75 | 76 | #### URI Variables 77 | 78 | The controller's name is always put after the script's file name by default. 79 | You can change this by adding another attribute to the documentation comment like so: `/** :GET :{method} */`. 80 | `{}` indicates a path variable. An attribute is recognized as path if it contains at least one variable. 81 | `{method}` is a global variable which refers to the current method. There is also `{controller}` which is the controller's name. 82 | 83 | You can also insert a method's variables into the path by adding an anchor character in front of its name. 84 | Here is an example for the `product` method: `/** :GET :{controller}/{$id} */`. 85 | It can now be called like this: `api.php/products/1`. 86 | 87 | #### Alternations 88 | 89 | Here is an example of an alternation in the URI path: `:{controller}.(json|xml)`. 90 | You can now either call `/api.php/products.json` or `/api.php/products.xml`. 91 | The user's decision is saved in `ApiController`'s `$OPTIONS` property. 92 | 93 | -- 94 | 95 | ### The ApiController class 96 | 97 | Your controller class has to be derived from this class, otherwise it will not be recognized as ApiController. Its name also has to end with `Controller` (case-insensitive). 98 | The `ApiController` class is located in the `Vanen\Mvc` namespace and has the following attributes and methods: 99 | 100 | >`$METHOD` - Represents the HTTP method of the current request. 101 | `$OPTIONS` - Results of alternations[#](#alternations) in the URI path. 102 | `$HEADERS` - Contains the names and values of the headers that were allowed with `allowHeader()`. 103 | `$RESPONSE_TYPE` - The response's type. Either JSON or XML (case-insensitive). 104 | `$JSON_OPTIONS` - An integer value that represents the response's [JSON options](http://php.net/manual/en/json.constants.php). 105 | `__controller()` - This method is called after the controller has been instantiated and values of the `ApiController` class were set. 106 | 107 | -- 108 | 109 | ### Response Types 110 | 111 | The available response types are JSON and XML. It can be set in the controller class as follows: 112 | `$this->RESPONSE_TYPE = 'JSON'` or `$this->RESPONSE_TYPE = 'XML'`. 113 | 114 | In combination with alternations[#](#alternations) you can design the `products` method like this: 115 | ```php 116 | /** :GET :{controller}.(json|xml) */ 117 | public function products() 118 | { 119 | $this->RESPONSE_TYPE = $this->OPTIONS[0]; 120 | $isXml = $this->RESPONSE_TYPE === 'xml'; 121 | return $isXml ? [ 122 | 'products' => $this->products 123 | ] : $this->products; 124 | } 125 | ``` 126 | 127 | `api.php/products.json`: 128 | ![JSON](http://image.prntscr.com/image/05e78d47e87f42cc8ef7e71d8a92b414.png) 129 | 130 | `/api.php/products.xml`: 131 | ![XML](http://image.prntscr.com/image/996e26447c5f4243b766d2546216b1fc.png) 132 | 133 | -- 134 | 135 | ### The HttpResponse class 136 | 137 | Instead of returning a normal object, you can also return an instance of the `HttpResponse` class. 138 | With it you can specify an [HTTP status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) and a message including a custom object to descibe an error e.g. The default status code is 200. 139 | 140 | The `HttpResponse` class class is located in the `Vanen\Mvc` namespace and has the following attributes and methods: 141 | 142 | >`$object` - The response object. 143 | `$statusCode` - The [HTTP status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) of this request. 144 | `$message` - The message that is associated with the code. 145 | `__construct($statusCode = 200, $message = 'Ok', $object = null)` - Constructor. 146 | 147 | Here is an example of the `product` method using this class. Remember to `use Vanen\Net\HttpResponse;`. 148 | ```php 149 | /** :GET :{controller}/{$id} */ 150 | public function product($id) 151 | { 152 | foreach ($this->products as $product) { 153 | if (strval($product->id) === $id) { 154 | return $product; 155 | } 156 | } 157 | 158 | return new HttpResponse(404, 'Not Found', (object)[ 159 | 'exception' => (object)[ 160 | 'type' => 'NotFoundApiException', 161 | 'message' => 'Product not found', 162 | 'code' => 404 163 | ] 164 | ]); 165 | } 166 | ``` 167 | 168 | `/api.php/products/4`: 169 | ![JSON2](http://image.prntscr.com/image/ce3b2754d93d4f8abda5ea993ec1a72d.png) 170 | 171 | --- 172 | 173 | A full example wrapping all of this up can be found in the `test` folder of this repository. 174 | The style of this library is inspired by ASP.NET. 175 | 176 | ©2016 Jonas Vanen. 177 | -------------------------------------------------------------------------------- /WebApi.php: -------------------------------------------------------------------------------- 1 | statusCode = $statusCode; 15 | $this->message = "$serverProtocol $statusCode $message"; 16 | $this->object = $object; 17 | } 18 | } 19 | 20 | namespace Vanen\Mvc; 21 | 22 | abstract class ApiController 23 | { 24 | /** 25 | * The HTTP method of this request. 26 | * @var string 27 | */ 28 | public $METHOD = null; 29 | /** 30 | * The values of the options of the URI pattern. 31 | * @var array 32 | */ 33 | public $OPTIONS = []; 34 | /** 35 | * The values of the headers that were sent with this request. 36 | * @var array 37 | */ 38 | public $HEADERS = []; 39 | 40 | /** 41 | * The type of the response. Either JSON or XML. 42 | * @var string 43 | */ 44 | public $RESPONSE_TYPE = 'JSON'; 45 | /** 46 | * The options for reponse type JSON. 47 | * http://php.net/manual/en/json.constants.php 48 | * @var integer 49 | */ 50 | public $JSON_OPTIONS = 0; 51 | 52 | /** 53 | * This method is called after the controller has been instantiated and 54 | * the properties of this class were set. 55 | */ 56 | public function __controller() 57 | { 58 | } 59 | } 60 | 61 | final class Api 62 | { 63 | private $mDefaultPattern = '{controller}'; 64 | private $mHttpMethod = null; 65 | private $mController = null; 66 | private $mHeaders = []; 67 | 68 | /** 69 | * The version of the API. This string is appended after the file name. 70 | * E.g. www.example.com/api.php/v1/ where 'v1' is the version. 71 | * @var string 72 | */ 73 | public $version = null; 74 | 75 | public function __construct($version = null) 76 | { 77 | if ($version) { 78 | $this->version = trim($version, '/\\'); 79 | } 80 | $this->mHttpMethod = filter_input(INPUT_SERVER, 'REQUEST_METHOD'); 81 | } 82 | 83 | /** 84 | * Handles the request. 85 | * @return void 86 | */ 87 | public function handle() 88 | { 89 | $parsedInfo = $this->parseRequest(); 90 | 91 | $_httpResponse = "Vanen\\Net\\HttpResponse"; 92 | if ($parsedInfo instanceof $_httpResponse) { 93 | header($parsedInfo->message, true, $parsedInfo->statusCode); 94 | return; 95 | } 96 | 97 | $headers = []; 98 | $requestHeaders = array_change_key_case(getallheaders(), CASE_LOWER); 99 | 100 | // Extract the values of the allowed headers from the request headers. 101 | foreach ($this->mHeaders as $header) { 102 | $headerLower = strtolower($header); 103 | if (key_exists($headerLower, $requestHeaders)) { 104 | $headers[$header] = $requestHeaders[$headerLower]; 105 | } else { 106 | // The header is still added if it doesn't exist. 107 | $headers[$header] = null; 108 | } 109 | } 110 | 111 | if (count($headers)) { 112 | // Allow the headers that are saved in ApiController::$HEADERS 113 | header('Access-Control-Allow-Headers: ' . join(', ', array_keys($headers))); 114 | } 115 | 116 | // Instantiate the controller. 117 | $this->mController = new $parsedInfo->class(); 118 | // Update the values of ApiController base class. 119 | $this->mController->METHOD = $this->mHttpMethod; 120 | $this->mController->OPTIONS = $parsedInfo->options; 121 | $this->mController->HEADERS = $headers; 122 | 123 | // Call the __controller method that is used as second constructor. 124 | $__controller = '__controller'; 125 | if (method_exists($this->mController, $__controller)) { 126 | $this->mController->$__controller(); 127 | } 128 | 129 | // Call the function that was requested. 130 | $response = call_user_func_array([ 131 | $this->mController, 132 | $parsedInfo->method 133 | ], $parsedInfo->parameters); 134 | 135 | // Check if the response is an instance of HttpResponse. 136 | if ($response instanceof $_httpResponse) { 137 | // Update the header depending on the response type. 138 | header($response->message, true, $response->statusCode); 139 | if ($response->object !== null) { 140 | $this->respond($response->object); 141 | } 142 | } else if ($response !== null) { 143 | $this->respond($response); 144 | } 145 | } 146 | 147 | /** 148 | * Adds a header to the allowed headers. 149 | * The header's value is saved in $HEADERS of the ApiController class. 150 | * @param string $name The header's name. 151 | * @return void 152 | */ 153 | public function allowHeader($name) 154 | { 155 | $this->mHeaders[] = $name; 156 | } 157 | 158 | // 159 | // Echos the response object encoded with JSON or XML. 160 | // 161 | private function respond($response) 162 | { 163 | if (!$this->mController) { 164 | header('Content-Type: application/json'); 165 | echo json_encode($response); 166 | return; 167 | } 168 | 169 | $notXml = strcasecmp($this->mController->RESPONSE_TYPE, 'xml') !== 0; 170 | if (strcasecmp($this->mController->RESPONSE_TYPE, 'json') !== 0 && $notXml) { 171 | trigger_error('Unknown response type "' . $this->mController->RESPONSE_TYPE . 172 | '". Valid response types are JSON and XML.', E_USER_ERROR); 173 | } 174 | 175 | header('Content-Type: application/' . $this->mController->RESPONSE_TYPE); 176 | 177 | if ($notXml) { 178 | echo json_encode($response, $this->mController->JSON_OPTIONS); 179 | } else { 180 | echo $this->xml_encode($response); 181 | } 182 | } 183 | 184 | // 185 | // Parses the request and returns an object with the name of 186 | // the class (controller) that was requested, the method and 187 | // the parameters with their values. 188 | // 189 | private function parseRequest() 190 | { 191 | $methodNotAllowed = false; 192 | 193 | // Get information about all controllers. 194 | foreach ($this->getControllerInfo() ?: [] as $controller => $methods) { 195 | foreach ($methods as $method) { 196 | // Skip this method if it doesn't accept this request method. 197 | if (!in_array($this->mHttpMethod, $method->http_verbs)) { 198 | $methodNotAllowed = true; 199 | continue; 200 | } 201 | 202 | // Filter all information from the URI using the pattern of this method. 203 | $uriInfo = $this->filterUri($method->uri_pattern); 204 | if (!$uriInfo) { 205 | continue; 206 | } 207 | 208 | // Skip this controller if it was not requested. 209 | if (property_exists($uriInfo, 'controller') && $uriInfo->controller !== null && 210 | strcasecmp(substr($controller, 0, -10), $uriInfo->controller) !== 0) { 211 | break; 212 | } 213 | 214 | // Skip this method if it was not requested. 215 | if (property_exists($uriInfo, 'method') && $uriInfo->method !== null && 216 | strcasecmp($method->name, $uriInfo->method) !== 0) { 217 | continue; 218 | } 219 | 220 | // Skip this method if more parameters were passed than it takes. 221 | if (count($uriInfo->parameters) > count($method->parameters)) { 222 | continue; 223 | } 224 | 225 | $parameters = []; 226 | foreach ($method->parameters as $param) { 227 | // Copy the parameters from the URI into a new array. 228 | foreach ($uriInfo->parameters as $name => $value) { 229 | if (strcasecmp($param->name, $name) === 0) { 230 | $parameters[$param->name] = $value; 231 | } 232 | } 233 | // If the parameter was not passed take the default value. 234 | if (!key_exists($param->name, $parameters)) { 235 | if ($param->is_optional) { 236 | $parameters[$param->name] = $param->default; 237 | } else { 238 | $parameters = false; 239 | break; 240 | } 241 | } 242 | } 243 | 244 | // Skip this method if a parameter was not passed. 245 | if ($parameters === false) { 246 | continue; 247 | } 248 | 249 | return (object)[ 250 | 'class' => $controller, 251 | 'method' => $method->name, 252 | 'parameters' => $parameters, 253 | 'options' => $uriInfo->options 254 | ]; 255 | } 256 | } 257 | 258 | if ($methodNotAllowed) { 259 | // Resource was found but the method is not allowed. 260 | return new \Vanen\Net\HttpResponse(405, 'Method Not Allowed'); 261 | } 262 | return new \Vanen\Net\HttpResponse(404, 'Not Found'); 263 | 264 | } 265 | 266 | // 267 | // Gets information about the method(s) of a controller(s). 268 | // 269 | private function getControllerInfo($class = null, $method = null) 270 | { 271 | // This function is used to filter a controller's method. 272 | // It returns the class name, the method name and 273 | // all HTTP verbs and the URI pattern from its doc comment. 274 | $methodFilter = function ($method) { 275 | // Skip methods that are not public. 276 | if (!in_array('public', \Reflection::getModifierNames($method->getModifiers()))) { 277 | return null; 278 | } 279 | 280 | // Get the cleaned comment text. 281 | $comment = $this->filterCommentText($method->getDocComment()); 282 | $result = (object)[ 283 | 'name' => $method->name, 284 | 'http_verbs' => [], 285 | 'uri_pattern' => null, 286 | 'parameters' => array_map(function ($p) { 287 | $isOptional = $p->isOptional(); 288 | return (object)[ 289 | 'name' => $p->name, 290 | 'is_optional' => $isOptional, 291 | 'default' => $isOptional ? $p->getDefaultValue() : null 292 | ]; 293 | }, $method->getParameters()) 294 | ]; 295 | 296 | // Get all words from the comment that start with a colon. 297 | foreach (array_filter(preg_split('/\s+/', $comment), function ($s) { 298 | return substr($s, 0, 1) === ':'; 299 | }) as $word) { 300 | if (strpos($word, '{') !== false && strpos($word, '}') !== false) { 301 | // If it contains braces it is a URI pattern. 302 | $result->uri_pattern = ltrim($word, ':'); 303 | } else { 304 | // Add the word to the HTTP verbs array otherwise. 305 | if (!in_array($word, $result->http_verbs)) { 306 | $result->http_verbs[] = ltrim($word, ':'); 307 | } 308 | } 309 | } 310 | 311 | // Only return the result if it has HTTP verbs. 312 | // If it doesn't null is returned and it will be filtered out (array_filter). 313 | return $result->http_verbs ? $result : null; 314 | }; 315 | 316 | // If no class name was passed get all classes that end with 'controller'. 317 | $reflects = $class === null ? array_map(function ($class) { 318 | return new \ReflectionClass($class); 319 | }, array_filter(get_declared_classes(), function ($class) { 320 | return substr_compare(strtolower($class), 'controller', -10) === 0 && 321 | is_subclass_of($class, 'Vanen\Mvc\ApiController'); 322 | })) : [new \ReflectionClass($class)]; 323 | 324 | $info = []; 325 | foreach ($reflects as $reflect) { 326 | if ($method !== null && !method_exists($reflect->name, $method)) { 327 | continue; 328 | } 329 | $methods = $method === null ? $reflect->getMethods() : [$reflect->getMethod($method)]; 330 | $info[$reflect->name] = array_filter(array_map($methodFilter, $methods)); 331 | } 332 | 333 | return $info ?: false; 334 | } 335 | 336 | // 337 | // Gets the text of a documentation comment without asterisks. 338 | // 339 | private function filterCommentText($comment) 340 | { 341 | preg_match_all('/[^*\s]+|(\s*[^*]+)/', $comment, $matches); 342 | $text = trim(join('', array_slice($matches[0], 1, -1))); 343 | 344 | $cleaned = ''; 345 | for ($i = 0, $len = strlen($text), $lb = false; $i < $len; ++$i) { 346 | if ($lb && ctype_space($text[$i])) { 347 | continue; 348 | } 349 | $lb = $text[$i] === "\n"; 350 | $cleaned .= $text[$i]; 351 | } 352 | 353 | return $cleaned; 354 | } 355 | 356 | // 357 | // Filters the information from the URI that is needed to parse the request. 358 | // 359 | private function filterUri($pattern) 360 | { 361 | $pattern = ($this->version ? "$this->version/" : '') . (trim($pattern, '/') ?: $this->mDefaultPattern); 362 | 363 | // Get the part after the file name. 364 | // Also handles the case of .htaccess usage where the file name might be missing. 365 | $requestUri = filter_input(INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_STRING); 366 | $parts = explode(dirname(filter_input(INPUT_SERVER, 'SCRIPT_NAME')), $requestUri); 367 | $temp = end($parts); 368 | $last = substr($temp, strpos($temp, '/', strpos($temp, '/') + 1)); 369 | 370 | // Convert the URI pattern to a regular expression: 371 | // Match all normal words in the pattern and make it a capturing group with the variable name as key. 372 | $regex = preg_replace('/{([a-zA-Z]+)}/', '(?<$1>[^?&\/]+)', addcslashes($pattern, '$./+*?[^]=!<>:-')); 373 | // All variable names that start with '$' are PHP variables and are indexed with an integer. 374 | $regex = preg_replace('/{\\\?\$?[a-zA-Z]+}/', '([^?&\/]+)', $regex); 375 | preg_match("/^$regex(?.*)$/", trim($last, '/'), $matches); 376 | 377 | if (!$matches && !$pattern && !$this->mDefaultPattern) { 378 | return (object)[ 379 | 'parameters' => filter_input_array(INPUT_GET) ?: [], 380 | 'options' => [] 381 | ]; 382 | } 383 | 384 | // This array contains all values that were passed in the URI. 385 | $values = array_slice($matches, 1); 386 | $wasString = false; 387 | // Entries that belong to a capturing group are appearing twice (numeric and string key). 388 | // This loop removes all the entries that have a numeric key. 389 | foreach (array_slice($matches, 1) as $key => $value) { 390 | if ($wasString) { 391 | unset($values[$key]); 392 | $wasString = false; 393 | } 394 | $wasString = is_string($key); 395 | } 396 | 397 | // End here if the path contains more characters than it should. 398 | $firstChar = $values ? substr($values['rest'], 0, 1) : null; 399 | if (!$values || $values['rest'] && (strlen($values['rest']) > 1 || $values['rest'] !== '/') 400 | && $firstChar !== '?' && $firstChar !== '&') { 401 | return false; 402 | } 403 | unset($values['rest']); 404 | 405 | // Save the names of all pattern variables in an array. 406 | // E.g. {controller}/{method} => ['controller', 'method'] 407 | preg_match_all('/({|\()\\\?\$?([a-zA-Z|]+)(}|\))/', $pattern, $keys); 408 | $keys = count($keys) > 2 ? $keys[2] : []; 409 | 410 | $result = [ 411 | 'parameters' => filter_input_array(INPUT_GET) ?: [], 412 | 'options' => [] 413 | ]; 414 | 415 | // Iterate over all keys and sort them into the result array. 416 | for ($i = 0, $len = count($keys); $i < $len; ++$i) { 417 | if (key_exists($keys[$i], $values)) { 418 | // This key-value-pair is a global variable like {controller} or {method}. 419 | $result[$keys[$i]] = $values[$keys[$i]]; 420 | } else if (strpos($keys[$i], '|') !== false) { 421 | // This value is an option. E.g. (json|xml). 422 | $result['options'][] = $values[$i]; 423 | } else { 424 | // This key-value-pair is a parameter and its value. 425 | $result['parameters'][$keys[$i]] = $values[$i]; 426 | } 427 | } 428 | 429 | return (object)$result; 430 | } 431 | 432 | // 433 | // Extension for conversion to XML. 434 | // Source: https://www.darklaunch.com/2009/05/23/php-xml-encode-using-domdocument-convert-array-to-xml-json-encode 435 | // 436 | private function xml_encode($mixed, $domElement = null, $DOMDocument = null) 437 | { 438 | if (is_null($DOMDocument)) { 439 | $DOMDocument = new \DOMDocument; 440 | $DOMDocument->formatOutput = true; 441 | $this->xml_encode($mixed, $DOMDocument, $DOMDocument); 442 | return $DOMDocument->saveXML(); 443 | } else { 444 | if (is_object($mixed)) { 445 | $mixed = get_object_vars($mixed); 446 | } 447 | if (is_array($mixed)) { 448 | foreach ($mixed as $index => $mixedElement) { 449 | if (is_int($index)) { 450 | if ($index === 0) { 451 | $node = $domElement; 452 | } else { 453 | $node = $DOMDocument->createElement($domElement->tagName); 454 | $domElement->parentNode->appendChild($node); 455 | } 456 | } else { 457 | $plural = $DOMDocument->createElement($index); 458 | $domElement->appendChild($plural); 459 | $node = $plural; 460 | // Added filter for properties that end with 's': is_array($mixedElement). 461 | // Those are only converted to an array if they contain an array. 462 | if (!(rtrim($index, 's') === $index) && is_array($mixedElement)) { 463 | $singular = $DOMDocument->createElement(rtrim($index, 's')); 464 | $plural->appendChild($singular); 465 | $node = $singular; 466 | } 467 | } 468 | $this->xml_encode($mixedElement, $node, $DOMDocument); 469 | } 470 | } else { 471 | $mixed = is_bool($mixed) ? ($mixed ? 'true' : 'false') : $mixed; 472 | $domElement->appendChild($DOMDocument->createTextNode($mixed)); 473 | } 474 | } 475 | } 476 | } 477 | --------------------------------------------------------------------------------