diff --git a/.gitignore b/.gitignore index dfcfd56..8adfde4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.user *.userosscache *.sln.docstates +/vendor/ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs @@ -348,3 +349,5 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + +/vendor/ diff --git a/Http/Http.php b/Http/Http.php new file mode 100644 index 0000000..adc81e5 --- /dev/null +++ b/Http/Http.php @@ -0,0 +1,101 @@ +getResponse(); + } + + + /** + * Secure data in array + * Accept alphanumerics characters only + * @return array the input array whitout + * @todo Move method outside this class + */ + static public function secure(array $_data = []): array + { + return array_filter(array_map(function ($v) { + $v = basename($v); + if(!preg_match("/^[A-Za-z0-9\.\-]*$/", $v)) { + self::badRequest('Invalid data'); + } + return $v; + // return \preg_replace("/[^a-zA-Z0-9]/", "", \basename(\strip_tags($v), '.php')); + }, $_data)); + } + + static public function end(int $_code = 500, string $data = 'Internal Error'): void + { + http_response_code($_code); + exit($data); + } + + static public function response(ResponseInterface $response): void + { + header('Content-Type: ' . $response->getContentType()); + self::end($response->getCode(), $response->getBody()); + } + + static public function ok(string $data): void + { + self::end(200, $data); + } + + static public function accepted(string $data): void + { + self::end(202, $data); + } + + static public function badRequest(string $msg = 'Invalid message received'): void + { + self::end(400, $msg); + } + + static public function unauthorized(string $msg = 'Invalid Token'): void + { + self::end(401, $msg); + } + + static public function forbidden(string $msg = 'You are not allowed to access this resource'): void + { + self::end(403, $msg); + } + + static public function notFound(string $msg = 'Not found'): void + { + self::end(404, ('No route for '.$_SERVER['REQUEST_URI']. ' ('.$msg.')')); + } + + static public function notAllowed(): void + { + self::end(405, ('No route for '.$_SERVER['REQUEST_METHOD'].' '.$_SERVER['REQUEST_URI'])); + } + + static public function notImplemented(): void + { + self::end(501, 'Not implemented'); + } +} diff --git a/Http/HttpFactory.php b/Http/HttpFactory.php new file mode 100644 index 0000000..561bdbf --- /dev/null +++ b/Http/HttpFactory.php @@ -0,0 +1,17 @@ +withContentType($_contentType)->withBody($_body); + } + + /** + * Get the current message content-type + * @return string The message content-type + */ + public function getContentType(): string + { + return $this->contentType; + } + + /** + * Set the current message content-type + * @param string $_contentType The content-type to apply + * @return self + */ + public function withContentType(string $_contentType): MessageInterface + { + $this->contentType = mb_convert_case($_contentType, MB_CASE_LOWER); + return $this; + } + + /** + * Get the current message body + * @return string The message body + */ + public function getBody(): string + { + return $this->body; + } + + + /** + * Set the current message body + * @param string $_body The message body + * @return self + */ + public function withBody(string $_body): MessageInterface + { + $this->body = trim($_body); + return $this; + } + + + /*public function getData(): array + { + return [$this->body]; + } + public function withData(array $_data): MessageInterface + { + if($this->contentType === Message::JSON) { + return $this->withBody(json_encode($_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + } + + $s = []; + foreach($_data as $k => $v) { + $s[] = ($k.'='.$v); + } + + return $this->withBody(implode(";", $s)); + }*/ +} diff --git a/Http/MessageInterface.php b/Http/MessageInterface.php new file mode 100644 index 0000000..55e10f5 --- /dev/null +++ b/Http/MessageInterface.php @@ -0,0 +1,50 @@ +setMethod($_SERVER['REQUEST_METHOD'] ?? 'GET'); + $this->uri = $_uri ?? new Uri($_SERVER['REQUEST_URI'] ?? '/'); + //$this->route = $this->uri->getParts(); + } + + public function setMethod(string $_method): RequestInterface + { + $this->method = $_method; + return $this; + } + + public function getMethod(): string + { + return $this->method; + } + + public function getUri(): UriInterface + { + return $this->uri; + } + + /*public function getRoute(int $_pos): ?string + { + return $this->route[$_pos] ?? null; + }*/ + + public function getData(): array + { + switch($this->method) + { + case 'GET': + case 'DELETE': + return $this->uri->getQueryArray(); + break; + case 'POST': + $a = Uri::secure($_POST ?? []); + if(!empty($_FILES)) { + $a['_files'] = $_FILES; + } + return $a; + break; + case 'PATCH': + case 'PUT': + $a = []; + parse_str(file_get_contents('php://input'), $a); + return Uri::secure($a); + break; + default: + return []; + break; + } + } +} diff --git a/Http/RequestInterface.php b/Http/RequestInterface.php new file mode 100644 index 0000000..09227d0 --- /dev/null +++ b/Http/RequestInterface.php @@ -0,0 +1,36 @@ +code = 200; + } + + public function getCode(): int + { + return $this->code; + } + + public function withCode(int $_code): ResponseInterface + { + $this->code = $_code; + return $this; + } +} diff --git a/Http/ResponseInterface.php b/Http/ResponseInterface.php new file mode 100644 index 0000000..781c3dd --- /dev/null +++ b/Http/ResponseInterface.php @@ -0,0 +1,23 @@ +method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; + $this->contentType = explode(',', $_SERVER['HTTP_ACCEPT'] ?? '*/*')[0] ?? '*/*'; + $this->uri = new Uri($_SERVER['REQUEST_URI']); + } + + public function getMethod(): string + { + return $this->method; + } + + public function getContentType(): string + { + return $this->contentType; + } + + public function getUri(): UriInterface + { + return $this->uri; + } + + public function getData(): array + { + switch($this->method) + { + case 'GET': + case 'DELETE': + return $this->uri->getQueryArray(); + break; + case 'POST': + $a = Uri::secure($_POST ?? []); + if(!empty($_FILES)) { + $a['_files'] = $_FILES; + } + return $a; + break; + case 'PATCH': + case 'PUT': + $a = []; + parse_str(file_get_contents('php://input'), $a); + return Uri::secure($a); + break; + default: + return []; + break; + } + } +} diff --git a/Http/ServerRequestInterface.php b/Http/ServerRequestInterface.php new file mode 100644 index 0000000..17ee48a --- /dev/null +++ b/Http/ServerRequestInterface.php @@ -0,0 +1,20 @@ +withHost($u['host'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost') + ->withPort($u['port'] ?? $_SERVER['SERVER_PORT'] ?? 0) + ->withPath($u['path'] ?? '') + ->withQuery($u['query'] ?? ''); + } + + public function __toString(): string + { + return ($this->scheme . '://' . $this->host . + (!in_array($this->port, [0, 80, 443]) ? (':'. $this->port) : '') . '/' . + $this->path . (!empty($this->query) ? '?'.$this->query : '')); + } + + public function withHost(string $_host): UriInterface + { + $this->host = $_host; + return $this; + } + + public function withPort(int $_port): UriInterface + { + $this->port = (filter_var( + $_port, + FILTER_VALIDATE_INT, + ['options' => ['min_range' => 0, 'max_range' => 65535]] + ) !== false ? $_port : 0); + $this->scheme = ($this->port !== 80 ? 'https' : 'http'); + return $this; + } + + public function withPath(string $_path): UriInterface + { + $this->path = mb_convert_case(preg_replace(['#\.+/#','#/+#'], '/', trim($_path ?? '', '/')), MB_CASE_LOWER); + return $this; + } + + public function withQuery(string $_query): UriInterface + { + $this->query = $_query; + return $this; + } + + public function withQueryArray(array $_query): UriInterface + { + return $this->withQuery(http_build_query($_query)); + } + + public function getHost(): string + { + return $this->host; + } + + public function getPort(): int + { + return $this->host; + } + + public function getPath(): string + { + return $this->path; + } + + public function getParts(): array + { + return self::secure(explode('/', $this->path));; + } + + public function getQuery(): string + { + return $this->query; + } + + public function getQueryArray(): array + { + $q = []; + parse_str($this->query ?? '', $q); + return self::secure($q); + } +} diff --git a/Http/UriInterface.php b/Http/UriInterface.php new file mode 100644 index 0000000..0e97c2d --- /dev/null +++ b/Http/UriInterface.php @@ -0,0 +1,20 @@ +__toString(), $t, $t->__toString()]); + + /* $web = new LocalPath($_path); + $root = new LocalPath(dirname($_path)); + $app = new Module($root->getPath($_namespace)); + + $uri = new Uri($_SERVER['REQUEST_URI']); + $request = new Request($uri);*/ + + // $router = new Router(new Route(new Request(), $_options)); + + //$serverRequest = new ServerRequest(); + + //Debug::e([]); + + $app = new App(new Route($_options), new Response()); + + $app->handleRequest(); + } + + private RouterInterface $router; + + public function __construct(RouterInterface $_router, ResponseInterface $_response) + { + $this->router = $_router; + + //Debug::e($this); + + if(null !== ($controller = $this->router->createController())) { + Debug::e($this); + } + + Http::notFound(); + } + + public function getPath(string $_subPath = ''): string + { + return ($this->path . $_subPath); + } + + /** + * Override to Load customs components + */ + protected function initComponents() : void {} + + /** + * Override to cutomize Databases components + */ + protected function initDatabases(): void + { + $conf = is_file($this->getPath('var/db.conf.php')) ? (require $this->router->getPath('var/db.conf.php')) : []; + + foreach($conf as $context => $params) { + Db::register($context, $params); + } + } + + /** + * Run MVC App using IRouter + */ + public function handleRequest(): void + { + Debug::e($this); + $this->initDatabases(); + $this->initComponents(); + $response = $this->controller->handleRequest(); + + if($response->getContentType() === Message::HTML) { + $layout = new View($this->router->getPath('Views/')); + $layout->setFile('_layout'); + View::setVar('page', $response->getBody()); + $response->setBody($layout->fetch()); + } + + Http::response($response); + } +} diff --git a/Mvc/AppInterface.php b/Mvc/AppInterface.php new file mode 100644 index 0000000..57b4d48 --- /dev/null +++ b/Mvc/AppInterface.php @@ -0,0 +1,42 @@ +repo = null; + $this->data = []; + $this->onLoad(); + } + + /** + * Default Controller Action + */ + // abstract public function indexAction(): void; + + /** + * Load custom components when loading the controller + */ + protected function onLoad() {} + + /** + * Load custom components before processing the request + */ + protected function beforeRequest() {} + + /** + * Execute actions after processing the request + */ + protected function afterRequest() {} + + public function getResponse(): ResponseInterface + { + return $this->response; + } + + /** + * Handle current request + */ + public function handleRequest(RouteInterface $_route, ResponseInterface $_response): ResponseInterface + { + $this->route = $_route; + $this->response = $_response; + + $a = ($this->route->getAction()); + + if(!method_exists($this, $a)) { + return $this->response->withCode(404)->withBody('Invalid Action'); + } + + $this->beforeRequest(); + $this->{$a}(); + $this->afterRequest(); + + if($this->response->getContentType() === ResponseInterface::JSON) { + return $this->response->setBody(json_encode($this->data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + } + + $view = new View($this->request->getLocalPath('Views/')); + return $this->response->setBody($view->setFile($this->request->getView())->fetch($this->data)); + } + + /** + * Set Generic Repository from table name and primary key name + * A generic repo use Db::getContext('default') + */ + public function setRepository(string $_table, string $_pk) + { + $this->repo = new Repository($_table, $_pk); + } + +} diff --git a/Mvc/ControllerInterface.php b/Mvc/ControllerInterface.php new file mode 100644 index 0000000..4914acb --- /dev/null +++ b/Mvc/ControllerInterface.php @@ -0,0 +1,17 @@ +router = $_router; + $this->request = $_router->getRequest(); + $this->response = new Response(); + $this->repo = null; + $this->view = false; + $this->init(); + } + + protected function init() + { + + } + + public function handleRequest(): IResponse + { + $a = $this->request->getAction(); + + if(!method_exists($this, $a)) { + return $this->response->setCode(404)->addData('error', 'Invalid Action'); + } + + $this->{$a}(); + + if($this->view === false) { + return $this->response; + } + + $layout = new View($this->router->getViewsPath()); + $layout->setFile('_layout'); + $layout->setChild('page', $this->request->getView()); + $this->response->setView($layout); + return $this->response; + } + + /** + * Set Generic Repository from table name and primary key name + * Remember to open the connection with DbContext before use any repo + */ + public function setRepository(string $_table, string $_pk) + { + $this->repo = new Repository($_table, $_pk); + } + + abstract public function indexAction(): void; +} \ No newline at end of file diff --git a/Mvc/Route.php b/Mvc/Route.php new file mode 100644 index 0000000..0aa8480 --- /dev/null +++ b/Mvc/Route.php @@ -0,0 +1,111 @@ +path = (dirname($_options['path']) . DIRECTORY_SEPARATOR); + $this->realPath = $this->path; + $this->namespace = '\\'.$_options['namespace']; + $this->instance = basename($_options['path']); + $this->request = ServerRequest::get(); + $this->route = $this->request->getUri()->getParts(); + + $this->route[0] = mb_convert_case($this->route[0] ?? 'Home', MB_CASE_TITLE); + + if($this->route[0] === 'Api') { + $this->namespace .= '\\Api'; + $this->realPath .= 'Api' . DIRECTORY_SEPARATOR; + } + elseif(is_dir($this->path . 'App' . DIRECTORY_SEPARATOR . $this->route[0])) { + $this->namespace .= '\\App\\'.$this->route[0]; + $this->realPath .= 'App' . DIRECTORY_SEPARATOR . $this->route[0] . DIRECTORY_SEPARATOR; + } + + if($this->path !== $this->realPath) { + array_shift($this->route); + $this->route[0] = mb_convert_case($this->route[0] ?? 'Home', MB_CASE_TITLE); + } + + $this->route[1] = $this->route[1] ?? 'index'; + $this->route[2] = $this->route[2] ?? null; + } + + public function getPath(string $_sub = ''): string + { + return ($this->path . $_sub); + } + + public function getRealPath(string $_sub = ''): string + { + return ($this->realPath . $_sub); + } + + public function getWebPath(string $_sub = ''): string + { + return ($this->path . $this->instance . DIRECTORY_SEPARATOR . $_sub); + } + + public function getRequest(): ServerRequestInterface + { + return $this->request; + } + + public function getController(): string + { + return ($this->route[0] . 'Controller'); + } + + public function getAction(): string + { + return ($this->route[1] . 'Action'); + } + + public function getId(): ?string + { + return $this->route[2]; + } + + public function getRoute(): RouteInterface + { + return $this; + } + + public function createController(): ?ControllerInterface + { + if(is_file($this->getRealPath('Controllers' . DIRECTORY_SEPARATOR . $this->getController() .'.php'))) { + $c = ($this->namespace . 'Controllers\\' . $this->getController()); + return new $c(); + } + + return null; + } +} diff --git a/Mvc/RouteInterface.php b/Mvc/RouteInterface.php new file mode 100644 index 0000000..2c55edf --- /dev/null +++ b/Mvc/RouteInterface.php @@ -0,0 +1,22 @@ +path = $_path; + $this->file = 'index'; + $this->childs = []; + } + + public function setFile(string $_filename): ViewInterface + { + $this->file = ($this->path.$_filename.'.php'); + + if(!\is_file($this->file)) { + Http::notFound('invalid view ('.$_filename.')'); + } + + return $this; + } + + public function fetch(array $_vars = []) : string + { + foreach($this->childs as $k => $v) { + $_vars[$k] = $v->fetch($_vars); + } + + \ob_start(); + \extract($_vars); + \extract(self::$vars, EXTR_SKIP); + require ($this->file); + return \ob_get_clean(); + } + + public function setChild(string $_key, string $_filename): ViewInterface + { + $this->childs[$_key] = (new self($this->path))->setFile($_filename); + return $this->childs[$_key]; + } +} diff --git a/Mvc/ViewInterface.php b/Mvc/ViewInterface.php new file mode 100644 index 0000000..7bdaf4b --- /dev/null +++ b/Mvc/ViewInterface.php @@ -0,0 +1,12 @@ +=7.4", + "ext-mbstring": "*", + "mdevoldere/edu-php-db": "dev-mdev" + }, + "autoload": { + "psr-4": { + "Md\\": "" + } + } +}