src/Api.php
<?php declare(strict_types=1); namespace Atk4\Api; use Atk4\Data\Model;use Laminas\Diactoros\Request;use Laminas\Diactoros\Response\JsonResponse;use Laminas\Diactoros\ServerRequestFactory;use Laminas\HttpHandlerRunner\Emitter\EmitterInterface;use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; /** * Main API class. */class Api{ /** @var Request Request object */ public $request; /** @var string */ protected $request_data; /** @var array */ protected $_vars = []; /** @var string Request path */ public $path; /** @var JsonResponse Response object */ public $response; /** @var int Response code */ public $response_code = 200; /** @var EmitterInterface Emitter object */ public $emitter; /** @var array Response header */ protected $response_headers = []; /** @var int Response options */ protected $response_options = JSON_PRETTY_PRINT | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT; /** * Reads everything off globals. * * @param Request $request */ public function __construct(Request $request = null) { if ($request !== null) { $request->getBody()->rewind(); // reset pointer of request. } $this->request = $request ?: ServerRequestFactory::fromGlobals(); $this->path = $this->request->getUri()->getPath(); if (isset($_SERVER['SCRIPT_NAME'], $_SERVER['REQUEST_URI'])) { // both script name and request uri are supplied, possibly // we would want to extract path relative from script location $script = $_SERVER['SCRIPT_NAME']; $path = $_SERVER['REQUEST_URI']; $regex = '|^' . preg_quote(dirname($script)) . '(/' . preg_quote(basename($script)) . ')?|i'; $this->path = preg_replace($regex, '', $path, 1); } if ($this->request->getHeader('Content-Type')[0] ?? null === 'application/json') { $this->request_data = json_decode($this->request->getBody()->getContents(), true); } else { $this->request_data = $this->request->getParsedBody(); } // This is how we will send responses $this->emitter = new SapiEmitter(); } Function `match` has a Cognitive Complexity of 26 (exceeds 5 allowed). Consider refactoring.
Method `match` has 33 lines of code (exceeds 25 allowed). Consider refactoring. public function match($pattern) { $path = explode('/', rtrim($this->path, '/')); $pattern = explode('/', rtrim($pattern, '/')); $this->_vars = []; while ($path || $pattern) { $p = array_shift($path); $r = array_shift($pattern); // if path ends and there is nothing in pattern (used //) then continue if ($p === null && $r === '') { continue; } // if both match, then continue if ($p === $r) { continue; } // pattern '*' accepts anything if ($r === '*' && is_string($p) && strlen($p) > 0) { continue; } // if pattern ends, but there is still something in path, then don't match if ($r === null || $r === '') { return false; } // parameters always start with ':', save in $vars and continue if ($r[0] === ':' && is_string($p) && strlen($p) > 0) { // if value contains : then treat it as fieldname:value pair // if value contains : and there is no fieldname (:ABC for example), // then it will use model->title_field as fieldname // otherwise it will be treated as id value if (strpos($p, ':') !== false) { $parts = explode(':', $p, 2); $this->_vars[] = [urldecode($parts[0]), urldecode($parts[1])]; } else { $this->_vars[] = urldecode($p); } continue; } // pattern '**' = good until the end if ($r === '**') { break; } return false; } return true; } /** * Call callable and emit response. * * @param callable $callable * @param array $vars */ public function exec($callable, $vars = []) { // try to call callable function $ret = $this->call($callable, $vars); // if callable function returns agile data model, then export it // this is important for REST API implementation if ($ret instanceof Model) { $ret = $this->exportModel($ret); } // no response, just step out if ($ret === null) { return; } // emit successful response $this->successResponse($ret); } /** * Call callable and return response. * * @param callable $callable * @param array $vars * * @return mixed */ protected function call($callable, $vars = []) { // try to call callable function try { $ret = call_user_func_array($callable, $vars); } catch (\Exception $e) { $this->caughtException($e); } return $ret; } /** * Exports data model. * * Extend this method to implement your own field restrictions. * * @return array */ protected function exportModel(Model $m) { return $m->export($this->getAllowedFields($m, 'read')); } /** * Load model by value. * * Value could be: * - array[fieldname,value]: * - if fieldname is empty, then use model->title_field * - if fieldname is not empty, then use it * - string|integer : will be treated as ID value * * @param mixed $value * * @return Model */ protected function loadModelByValue(Model $m, $value) { // value is not ID if (is_array($value)) { $field = $value[0] ?? $m->title_field; return $m->loadBy($field, $value[1]); } // value is ID return $m->load($value); } /** * Returns list of model field names which allow particular action - read or modify. * Also takes model->only_fields into account if that's defined. * * It uses custom model property apiFields[$action] which should contain array of * allowed field names or null to allow all model fields. * * @param string $action read|modify * * @return array|null of field names */ protected function getAllowedFields(Model $m, $action = 'read') { // take model only_fields into account $fields = is_array($m->only_fields) ? $m->only_fields : []; // limit by apiFields if (isset($m->apiFields, $m->apiFields[$action])) { $allowed = $m->apiFields[$action]; $fields = $fields ? array_intersect($fields, $allowed) : $allowed; } return $fields; } /** * Filters data array by only allowed fields. * * Extend this method to implement your own field restrictions. * * @param Model $m * @param array $data * * @return array */ /* not used and maybe will not be needed too protected function filterData(\Atk4\Data\Model $m, array $data) { $allowed = $this->getAllowedFields($m, 'modify'); if ($allowed) { $data = array_intersect_key($data, array_flip($allowed)); } return $data; } */ /** * Emit successful response. * * @param mixed $response */ protected function successResponse($response) { // create response object if (!$this->response) { $this->response = new JsonResponse( $response, $this->response_code, $this->response_headers, $this->response_options ); } // if there is emitter, then emit response and exit // for testing purposes there can be situations when emitter is disabled. then do nothing. if ($this->emitter) { $this->emitter->emit($this->response); exit; } // @todo Should we also stop script execution if no emitter is defined or just ignore that? //exit; } /** * Do GET pattern matching. * * @param string $pattern * @param callable $callable */ public function get($pattern, $callable = null) { if ($this->request->getMethod() === 'GET' && $this->match($pattern)) { $this->exec($callable, $this->_vars); } } /** * Do POST pattern matching. * * @param string $pattern * @param callable $callable */ public function post($pattern, $callable = null) { if ($this->request->getMethod() === 'POST' && $this->match($pattern)) { $this->exec($callable, $this->_vars); } } /** * Do PATCH pattern matching. * * @param string $pattern * @param callable $callable */ public function patch($pattern, $callable = null) { if ($this->request->getMethod() === 'PATCH' && $this->match($pattern)) { $this->exec($callable, $this->_vars); } } /** * Do PUT pattern matching. * * @param string $pattern * @param callable $callable */ public function put($pattern, $callable = null) { if ($this->request->getMethod() === 'PUT' && $this->match($pattern)) { $this->exec($callable, $this->_vars); } } /** * Do DELETE pattern matching. * * @param string $pattern * @param callable $callable */ public function delete($pattern, $callable = null) { if ($this->request->getMethod() === 'DELETE' && $this->match($pattern)) { $this->exec($callable, $this->_vars); } } /** * Implement REST pattern matching. * * @param string $pattern * @param Model|callable $model * @param array $methods Allowed methods (read|modify|delete). By default all are allowed */Method `rest` has 60 lines of code (exceeds 25 allowed). Consider refactoring.
Function `rest` has a Cognitive Complexity of 15 (exceeds 5 allowed). Consider refactoring. public function rest($pattern, $model = null, $methods = ['read', 'modify', 'delete']) { $methods = array_map('strtolower', $methods); // GET all records if (in_array('read', $methods, true)) { $f = function (...$params) use ($model) { if (is_callable($model)) { $model = $this->call($model, $params); } return $model; }; $this->get($pattern, $f); } // GET :id - one record if (in_array('read', $methods, true)) { $f = function (...$params) use ($model) { $id = array_pop($params); // pop last element of args array, it's :id if (is_callable($model)) { $model = $this->call($model, $params); } // limit fields $model->onlyFields($this->getAllowedFields($model, 'read')); // load model and get field values return $this->loadModelByValue($model, $id)->get(); }; $this->get($pattern . '/:id', $f); } // POST :id - update one record // PATCH :id - update one record (same as POST :id) // PUT :id - update one record (same as POST :id) if (in_array('modify', $methods, true)) { $f = function (...$params) use ($model) { $id = array_pop($params); // pop last element of args array, it's :id if (is_callable($model)) { $model = $this->call($model, $params); } // limit fields $model->onlyFields($this->getAllowedFields($model, 'modify')); $this->loadModelByValue($model, $id)->save($this->request_data); $model->onlyFields($this->getAllowedFields($model, 'read')); return $model->get(); }; $this->patch($pattern . '/:id', $f); $this->post($pattern . '/:id', $f); $this->put($pattern . '/:id', $f); } // POST - insert new record if (in_array('modify', $methods, true)) { $f = function (...$params) use ($model) { if (is_callable($model)) { $model = $this->call($model, $params); } // limit fields $model->onlyFields($this->getAllowedFields($model, 'modify')); $model->unload()->save($this->request_data); $model->onlyFields($this->getAllowedFields($model, 'read')); $this->response_code = 201; // http code for created return $model->get(); }; $this->post($pattern, $f); } // DELETE :id - delete one record if (in_array('delete', $methods, true)) { $f = function (...$params) use ($model) { $id = array_pop($params); // pop last element of args array, it's :id if (is_callable($model)) { $model = $this->call($model, $params); } // limit fields (not necessary, but will limit field list for performance) $model->onlyFields($this->getAllowedFields($model, 'read')); Avoid too many `return` statements within this method. return !$model->delete($id)->loaded(); }; $this->delete($pattern . '/:id', $f); } } /** * Our own exception handling. */ public function caughtException(\Exception $e) { $params = []; if ($e instanceof \Atk4\Core\Exception) { foreach ($e->getParams() as $key => $val) { $params[$key] = $e->toString($val); } } $this->response = new JsonResponse( [ 'error' => [ 'code' => $e->getCode(), 'message' => $e->getMessage(), 'args' => $params, ], ], (int) $e->getCode() > 0 ? $e->getCode() : 500, $this->response_headers, $this->response_options ); //var_dump($this->response, $e->getMessage()); (new SapiEmitter())->emit($this->response); exit; }}