From acec672355656d881290bbd3b6ee2ee5d3a8b522 Mon Sep 17 00:00:00 2001 From: "Bruno O. Notario" Date: Sun, 17 Apr 2022 00:46:33 -0300 Subject: [PATCH] Added REST Api class! Also added PHPDocParser, Reflection classes extended and Request class improvements. --- system/engine/action.php | 21 +++ system/engine/controller.php | 13 +- system/engine/front.php | 36 +++- system/engine/restful.php | 242 ++++++++++++++++++++++++++ system/phpdocparser/autoload.php | 245 +++++++++++++++++++++++++++ system/reflectionclass/autoload.php | 31 ++++ system/reflectionmethod/autoload.php | 41 +++++ system/request/autoload.php | 12 +- 8 files changed, 622 insertions(+), 19 deletions(-) create mode 100644 system/engine/restful.php create mode 100644 system/phpdocparser/autoload.php create mode 100644 system/reflectionclass/autoload.php create mode 100644 system/reflectionmethod/autoload.php diff --git a/system/engine/action.php b/system/engine/action.php index 527e573..9b601f6 100644 --- a/system/engine/action.php +++ b/system/engine/action.php @@ -124,6 +124,27 @@ final class Action implements ActionInterface { $this->method = 'index'; } + if (!$this->classAlt) { + + $lastTry = explode('/', $this->route, 2); + + $class1 = $this->mountClass($lastTry[0] . "\\", str_replace("/", "\\", end($lastTry))); + $class2 = implode("\\", explode("\\", $class1, -1)); + + if(class_exists($class1)){ + $this->classAlt = [ + 'class' => $class1 + ]; + $this->method = 'index'; + } elseif(class_exists($class2)){ + $this->classAlt = [ + 'class' => $class2 + ]; + $this->method = $method; + } + + } + } diff --git a/system/engine/controller.php b/system/engine/controller.php index 89af0d1..1a282de 100644 --- a/system/engine/controller.php +++ b/system/engine/controller.php @@ -8,10 +8,6 @@ namespace Phacil\Framework; -use TypeError; -use Mustache_Exception_UnknownTemplateException; -use RuntimeException; -use SmartyException; use \Phacil\Framework\Config; /** @@ -233,11 +229,10 @@ abstract class Controller { } /** + * Render template + * * @return string * @throws TypeError - * @throws Mustache_Exception_UnknownTemplateException - * @throws RuntimeException - * @throws SmartyException * @throws Exception * @final */ @@ -316,10 +311,10 @@ abstract class Controller { } /** - * @param bool $commonChildren + * @param bool $commonChildren (optional) Whether to include the common children * @return \Phacil\Framework\Response * @throws Exception - * @final + * @since 1.1.0 */ protected function out ($commonChildren = true) { if($commonChildren === true){ diff --git a/system/engine/front.php b/system/engine/front.php index e089cf8..89c398a 100644 --- a/system/engine/front.php +++ b/system/engine/front.php @@ -8,12 +8,10 @@ namespace Phacil\Framework; -use Phacil\Framework\Interfaces\Front as frontinterface; - -//use Exception; +use Phacil\Framework\Interfaces\Front as frontInterface; /** @package Phacil\Framework */ -final class Front implements frontinterface { +final class Front implements frontInterface { /** * @@ -75,7 +73,7 @@ final class Front implements frontinterface { /** * @param object $action - * @return \Phacil\Framework\Interfaces\Action + * @return \Phacil\Framework\Interfaces\Action|\Phacil\Framework\Controller * @throws Exception */ private function execute($action) { @@ -87,7 +85,7 @@ final class Front implements frontinterface { unset($action); - if (file_exists($file)) { + if ($file && file_exists($file)) { require_once($file); foreach($classAlt as $classController){ @@ -95,6 +93,10 @@ final class Front implements frontinterface { if(class_exists($classController)){ $controller = new $classController($this->registry); + if(!is_subclass_of($controller, 'Phacil\Framework\Controller')){ + throw new Exception('PHACIL ERROR: Controller '. get_class($controller) . ' doesn\'t have Phacil\Framework\Controller implemented'); + } + break; } } catch (Exception $th) { @@ -121,7 +123,27 @@ final class Front implements frontinterface { } - } else { + } elseif(!$file && isset($classAlt['class'])) { + try { + $controller = new $classAlt['class']($this->registry); + + if (!is_subclass_of($controller, 'Phacil\Framework\Controller')) { + throw new Exception('PHACIL ERROR: Controller ' . get_class($controller) . ' doesn\'t have Phacil\Framework\Controller implemented'); + } + + if(!is_callable(array($controller, $method))) { + $action = new Action($this->error); + + $this->error = ''; + + new Exception("PHACIL ERROR: Controller class " . get_class($controller) . "->".$method."() is not a callable function"); + } else { + $action = call_user_func_array(array($controller, $method), $args); + } + } catch (Exception $th) { + throw new Exception("The controller can't be loaded: " . $th->getMessage(), $th->getCode(), $th); + } + }else { $action = new Action($this->error); $this->error = ''; diff --git a/system/engine/restful.php b/system/engine/restful.php new file mode 100644 index 0000000..b135f62 --- /dev/null +++ b/system/engine/restful.php @@ -0,0 +1,242 @@ + + */ + +namespace Phacil\Framework; + +use Phacil\Framework\Controller; + +/** + * Create a simple anda faster REST API controller. + * + * @package Phacil\Framework + * @since 2.0.0 + */ +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 (array_search($method, $this->HTTPMETHODS) && is_callable(array($this, $method))) { + $r = new ReflectionMethod($this, $method); + $params = []; + /* $requiredParamsTotal = $r->getNumberOfRequiredParameters(); + + $requiredParams = $r->getRequiredParameters(); + + $vondc = $r->getParameters(); */ + + $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; + + 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) && !array_search(gettype($data), $type)) || (gettype($data) != $type)){ + $invalidDataType = true; + + if(is_array($type)){ + foreach ($type as $avalType) { + if(call_user_func("is_" . $avalType, $data)) { + $invalidDataType = false; + break; + } + } + } else { + if(call_user_func("is_" . $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 (\Throwable $th) { + if(get_class($th) == 'TypeError'){ + new Exception($th->getMessage(), $th->getCode()); + return $this->__callInterrupt($th->getMessage()); + } else { + throw new Exception($th->getMessage(), $th->getCode()); + } + + } catch (Exception $e) { + throw new Exception($e->getMessage(), $e->getCode()); + } + //$this->$method(); + } else { + $this->__callNotFound($method); + } + } + + /** + * 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)); + } +} \ No newline at end of file diff --git a/system/phpdocparser/autoload.php b/system/phpdocparser/autoload.php new file mode 100644 index 0000000..e891e41 --- /dev/null +++ b/system/phpdocparser/autoload.php @@ -0,0 +1,245 @@ + + */ + +namespace Phacil\Framework; + +/** + * PHPDoc Parser + * + * Simple example usage: + * @example $a = new Parser($string); $a->parse(); + * + * @package Phacil\Framework + * @since 2.0.0 + */ +class PHPDocParser { + + /** + * The PHPDoc string that we want to parse + * @var string + */ + private $string; + + /** + * Storge for the short description + * @var string + */ + private $shortDesc; + + /** + * Storge for the long description + * + * @var string + */ + private $longDesc; + + /** + * Storge for all the PHPDoc parameters + * + * @var array + */ + private $params = []; + + /** + * Parse each line + * + * Takes an array containing all the lines in the string and stores + * the parsed information in the object properties + * + * @param array $lines An array of strings to be parsed + */ + private function parseLines($lines) { + foreach($lines as $line) { + $parsedLine = $this->parseLine($line); //Parse the line + + if($parsedLine === false && empty($this->shortDesc)) { + if(isset($desc) && is_array($desc)) + $this->shortDesc = implode(PHP_EOL, $desc); //Store the first line in the short description + + $desc = array(); + } elseif($parsedLine !== false) { + $desc[] = $parsedLine; //Store the line in the long description + } + } + $this->longDesc = implode(PHP_EOL, $desc); + } + + /** + * Parse the line + * + * Takes a string and parses it as a PHPDoc comment + * + * @param string $line The line to be parsed + * @return string|bool|array False if the line contains no parameters or paramaters that aren't valid otherwise, the line that was passed in. + */ + private function parseLine($line) { + + //Trim the whitespace from the line + $line = trim($line); + + if(empty($line)) return false; //Empty line + + if(strpos($line, '@') === 0) { + $param = substr($line, 1, strpos($line, ' ') - 1); //Get the parameter name + $value = substr($line, strlen($param) + 2); //Get the value + if($this->setParam($param, $value)) return false; //Parse the line and return false if the parameter is valid + } + + return $line; + } + + /** + * Setup the valid parameters + * + * @param string $type NOT USED + */ + private function setupParams($type = "") { + $params = array( + "access" => '', + "author" => '', + "copyright" => '', + "deprecated"=> '', + "example" => '', + "ignore" => '', + "internal" => '', + "link" => '', + "param" => '', + "return" => '', + "see" => '', + "since" => '', + "tutorial" => '', + "version" => '', + 'throws' => '', + 'todo' => '' + ); + + $this->params = $params; + } + + /** + * Parse a parameter or string to display in simple typecast display + * + * @param string $string The string to parse + * @return string Formatted string wiht typecast + */ + private function formatParamOrReturn($string) { + + //$pos = strpos($string, ' '); + + $parts = preg_split('/\s+/', $string, 3, PREG_SPLIT_NO_EMPTY); + + //$type = substr($string, 0, $pos); + + if(count($parts) == 3) { + $return = [ + $parts[1] => [ + 'type' => (strpos($parts[0], '|')) ? explode('|',$parts[0]) : $parts[0], + 'desc' => $parts[2] + ] + ]; + } elseif (count($parts) == 2) { + $return = [ + $parts[1] => [ + 'type' => (strpos($parts[0], '|')) ? explode('|', $parts[0]) : $parts[0] + ] + ]; + } elseif (count($parts) == 1) { + $return = (strpos($parts[0], '|')) ? explode('|', $parts[0]) : $parts[0]; + } else { $return = '';} + + //return '(' . $type . ')' . substr($string, $pos+1); + return $return; + } + + /** + * Set a parameter + * + * @param string $param The parameter name to store + * @param string $value The value to set + * @return bool True = the parameter has been set, false = the parameter was invalid + */ + private function setParam($param, $value) + { + if (!array_key_exists($param, $this->params)) $this->params[$param] = ''; + + if ($param == 'param' || $param == 'return') $value = $this->formatParamOrReturn($value); + + if (empty($this->params[$param])) { + $this->params[$param] = $value; + } elseif (is_array($this->params[$param])) { + $this->params[$param] = array_merge($this->params[$param], (is_array($value) ? $value : array($value))); + } elseif (is_string($this->params[$param])) { + $this->params[$param] = array($this->params[$param], $value); + } + return true; + } + + /** + * Setup the initial object + * + * @param string $string The string we want to parse + */ + public function __construct($string) { + $this->string = $string; + //$this->setupParams(); + } + + /** + * Parse the string + * @return void + */ + public function parse() { + //Get the comment + if(preg_match('#^/\*\*(.*)\*/#s', $this->string, $comment) === false) + die("Error"); + + $comment = trim($comment[1]); + + //Get all the lines and strip the * from the first character + if(preg_match_all('#^\s*\*(.*)#m', $comment, $lines) === false) + die('Error'); + + $this->parseLines($lines[1]); + } + + /** + * Get the short description + * + * @return string The short description + */ + public function getShortDesc() { + return $this->shortDesc; + } + + /** + * Get the long description + * + * @return string The long description + */ + public function getDesc() { + return $this->longDesc; + } + + /** + * Get the parameters + * + * @return array The parameters + */ + public function getParams() { + return $this->params; + } + + /** + * @param string $key + * @return mixed + */ + public function get($key){ + return ($this->$key) ?: null; + } +} diff --git a/system/reflectionclass/autoload.php b/system/reflectionclass/autoload.php new file mode 100644 index 0000000..7548d72 --- /dev/null +++ b/system/reflectionclass/autoload.php @@ -0,0 +1,31 @@ + + */ + + +namespace Phacil\Framework; + +/** + * @since 2.0.0 + * @package Phacil\Framework + */ +class ReflectionClass extends \ReflectionClass{ + + /** + * + * @return \Phacil\Framework\PHPDocParser + */ + public function getDocCommentParse() { + $docParse = new \Phacil\Framework\PHPDocParser($this->getDocComment()); + + $docParse->parse(); + + return $docParse; + } + +} \ No newline at end of file diff --git a/system/reflectionmethod/autoload.php b/system/reflectionmethod/autoload.php new file mode 100644 index 0000000..e6d4156 --- /dev/null +++ b/system/reflectionmethod/autoload.php @@ -0,0 +1,41 @@ + + */ + +namespace Phacil\Framework; + +/** + * @since 2.0.0 + * @package Phacil\Framework + */ +class ReflectionMethod extends \ReflectionMethod { + + /** @return \Phacil\Framework\PHPDocParser */ + public function getDocCommentParse() + { + if(!$this->getDocComment()) + return false; + + $docParse = new \Phacil\Framework\PHPDocParser($this->getDocComment()); + $docParse->parse(); + return $docParse; + } + + /** + * + * @return array + */ + public function getRequiredParameters(){ + $array = []; + foreach ($this->getParameters() as $value) { + if(!$value->isOptional()) + $array[] = $value; + } + return $array; + } +} \ No newline at end of file diff --git a/system/request/autoload.php b/system/request/autoload.php index a2f6973..0927967 100644 --- a/system/request/autoload.php +++ b/system/request/autoload.php @@ -139,7 +139,7 @@ final class Request { $data[self::clean($key)] = self::clean($value); } } else { - $data = htmlspecialchars($data, ENT_COMPAT); + $data = (gettype($data) == 'string') ? trim(htmlspecialchars($data, ENT_COMPAT)) : $data; } return $data; @@ -171,10 +171,16 @@ final class Request { */ static public function INPUT($key = null){ if (self::HEADER("Content-Type") == 'application/json'){ - $data = (JSON::decode(file_get_contents('php://input'))); + try{ + $data = self::clean(JSON::decode(file_get_contents('php://input'))); + } catch (\Exception $e){ + throw new \UnexpectedValueException($e->getMessage(), $e->getCode()); + } + } else { + $data = self::clean(file_get_contents('php://input')); } - return (self::HEADER("Content-Type") == 'application/json') ? (($key) ? $data[$key] : $data) : self::clean(file_get_contents('php://input')); + return (self::HEADER("Content-Type") == 'application/json') ? (($key) ? (isset($data[$key]) ? $data[$key] : null) : $data) : ($data ?: null); } /**