*/ namespace Phacil\Framework; use Phacil\Framework\Controller; use Phacil\Framework\Request; use Phacil\Framework\ReflectionMethod; /** * Create a simple and faster REST API controller. * * @package Phacil\Framework * @since 2.0.0 * @abstract * @api */ abstract class RESTful extends Controller { /** * The output content type * * @var string */ public $contentType = 'application/json'; /** * * @var string */ public $acceptType = 'application/json'; /** * * @var int */ public $error_code = 404; /** * * @var string */ public $error_msg = 'This service not have implemented the %s method.'; /** * * @var string[] */ public $HTTPMETHODS = [ 'GET', 'CONNECT', 'HEAD', 'PUT', 'DELETE', 'TRACE', 'POST', 'OPTIONS', 'PATCH']; /** * * @return void */ function __construct() { parent::__construct(); } /** * * @return void * @throws \Phacil\Framework\Exception */ function index() { $method = (Request::METHOD()); if (in_array($method, $this->HTTPMETHODS) && is_callable(array($this, $method))) { $r = new ReflectionMethod($this, $method); $params = []; $comment_string = $r->getDocCommentParse(); $phpDocParams = ($comment_string) ? $comment_string->getParams() : false; foreach($r->getParameters() as $key => $value) { switch (strtoupper( $method)) { case 'GET': $data = Request::GET($value->getName()); break; case 'HEAD': $data = Request::HEADER($value->getName()); break; case 'POST': default: try { $data = (Request::POST($value->getName())) ?: Request::INPUT($value->getName()); } catch (\Exception $th) { return $this->__callInterrupt($th->getMessage()); } break; } /** * check if have a sufficiente data for the request */ if($data === null) { if(!$value->isOptional()){ return $this->__callInterrupt($value->getName(). " is required."); } } if($data !== null && $phpDocParams && isset($phpDocParams['param']) && is_array($phpDocParams['param'])){ $type = (isset($phpDocParams['param']['$'.$value->getName()])) ? $phpDocParams['param']['$' . $value->getName()]['type']: false; if($type){ if((is_array($type) && !in_array(gettype($data), $type)) || (gettype($data) != $type)){ $invalidDataType = true; if(is_array($type)){ foreach ($type as $avalType) { if(self::__testType( $avalType, $data)) { $invalidDataType = false; break; } } } else { if(self::__testType($type, $data)) { $invalidDataType = false; } } if($invalidDataType){ return $this->__callInterrupt($value->getName() . " need to be: ".(is_array($type) ? implode(', ', $type) : $type).". You give: ".gettype($data)."."); } } } } if($data){ $params[$value->getName()] = $data; }; }; try { //code... call_user_func_array(array($this, $method), $params); } catch (\Phacil\Framework\Exception\WebApiException $re) { $this->data = is_string($re->getMessage()) ? ['error' => $re->getMessage()] : $re->getMessage(); $this->response->code($re->getCode()); $this->out(); }catch (\Phacil\Framework\Exception\Throwable $th) { if(get_class($th) == 'TypeError'){ new Exception($th->getMessage(), $th->getCode()); return $this->__callInterrupt($th->getMessage()); } else { throw $th; } } catch (\TypeError $e) { throw new \Phacil\Framework\Exception\TypeError($e->getMessage(), $e->getCode(), $e); } catch (\Exception $e) { throw new Exception($e->getMessage(), $e->getCode()); } } else { $this->__callNotFound($method); } } /** * Return true or false for data type * * @param string $type Type to test * @param string $data Data to test * @return bool */ static function __testType($type, $data){ switch ($type) { case 'mixed': return true; break; case 'integer': case 'int': //return ctype_digit($data); return filter_var($data, FILTER_VALIDATE_INT) !== false; break; case 'double': case 'float': return filter_var($data, FILTER_VALIDATE_FLOAT) !== false; break; case 'string': case 'array': case 'bool': case 'long': case 'null': case 'numeric': case 'scalar': case 'real': return call_user_func("is_" . $type, $data); break; default: return false; break; } } /** * Not found default method * * @param string $method * @param mixed $args * @return void * @throws \Phacil\Framework\Exception */ protected function __callNotFound($method, $args = null) { $this->response->code($this->error_code); $this->data['error'] = sprintf($this->error_msg, $method); $this->out(); } /** * Interrupt method with a exit. * * @param string $msg * @param int $code * @return void * @throws \Phacil\Framework\Exception */ protected function __callInterrupt($msg, $code = 400) { $this->error_msg = $msg; $this->error_code = 400; $method = 'JSONERROR'; $this->__callNotFound($method); return; } /** * * @return void * @throws \Phacil\Framework\Exception */ protected function JSONERROR() { $this->out(); } /** * Deafult method to OPTIONS HTTP call. * * This method list in HTTP header a "Allow" tag with methods implemented and accesibles. * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS * * @return void * @throws \Phacil\Framework\Exception */ protected function OPTIONS(){ $methods = []; foreach ($this->HTTPMETHODS as $method) { if (is_callable(array($this, $method))) $methods[] = $method; } $this->response->addHeader('Allow', implode(", ", $methods)); $this->data['allow'] = $methods; $this->out(); } /** * The default and automated output method. All itens in the $this->data are rendered in JSON format. * * @param bool $commonChildren (optional) * @return \Phacil\Framework\Response * @throws Exception */ protected function out($commonChildren = true) { $this->response->addHeader('Content-Type', $this->contentType); if($this->acceptType) $this->response->addHeader('Accept', $this->acceptType); return $this->response->setOutput(JSON::encode($this->data)); } }