Created
March 13, 2019 12:02
-
-
Save funyx/09eeeccb21186c928fd054038b7070ea to your computer and use it in GitHub Desktop.
atk4/api
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace app; | |
use \atk4\core\Exception; | |
use \atk4\core\InitializerTrait; | |
use \atk4\core\HookTrait; | |
use \atk4\core\DynamicMethodTrait; | |
use \atk4\core\ContainerTrait; | |
use \atk4\core\FactoryTrait; | |
use \atk4\core\AppScopeTrait; | |
use \atk4\core\DIContainerTrait; | |
use \app\Config as Config; | |
use \atk4\data\Persistence as Persistence; | |
/** | |
* Main API class. | |
*/ | |
class API | |
{ | |
use DynamicMethodTrait; | |
use FactoryTrait; | |
use ContainerTrait; | |
use DIContainerTrait; | |
use HookTrait; | |
use AppScopeTrait; | |
use InitializerTrait { | |
init as initialize; | |
} | |
public $authSeed = '\app\API\Authentication'; | |
public $routerSeed = '\app\API\Router'; | |
public $title = 'Agile REST API - Untitled Application'; | |
public $config; | |
public $db; | |
public $router; | |
public $app; | |
public $auth; | |
// authenticated user | |
public $user; | |
/** | |
* @param $initConfig array - initialize config | |
* | |
* @throws \atk4\core\Exception | |
*/ | |
public function __construct($initConfig = []) | |
{ | |
$this->app = $this; | |
$this->initialize(); | |
$this->config = array_merge(Config::loadConfig(), $initConfig); | |
$this->router = $this->add($this->routerSeed); | |
} | |
/** | |
* @throws \atk4\data\Exception | |
*/ | |
public function addAuth() | |
{ | |
if ($this->db === null) { | |
$this->addPersistence(); | |
} | |
try { | |
$this->auth = $this->add($this->authSeed, [ | |
'request' => $this->router->request | |
]); | |
} catch (Exception $e) { | |
throw new \atk4\data\Exception('Failed to attach Authentication', 500, $e); | |
} | |
return $this->user = $this->auth->authenticate(); | |
} | |
/** | |
* $this->db \atk4\data\Persistence | |
* | |
* @throws \atk4\data\Exception | |
*/ | |
private function addPersistence() | |
{ | |
try { | |
$this->db = $this->add(Persistence::connect($this->config['db']['dsn'])); | |
} catch (Exception $e) { | |
throw new \atk4\data\Exception('Failed to attach Persistence', 500, $e); | |
} | |
} | |
/** | |
* @param $endpoint : string | |
*/ | |
public function addEndpoint($endpoint = '') | |
{ | |
$this->router->loadEndpoint($endpoint); | |
} | |
/** | |
* @param $pattern : string | |
* @param $callable : callable method | |
*/ | |
public function useRoute($pattern, $callable) | |
{ | |
$this->router->loadRoute($pattern, $callable); | |
} | |
public function start() | |
{ | |
if ($this->router->findMatchingRoute()) { | |
return $this->router->runLoadedRoute(); | |
} | |
// show 404 | |
new \app\API\Exception('Not Found', 'Not Found', 404); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace app\API; | |
use \atk4\core\DIContainerTrait; | |
use \atk4\core\AppScopeTrait; | |
use \atk4\core\InitializerTrait; | |
class Authentication | |
{ | |
use AppScopeTrait; | |
use DIContainerTrait; | |
use InitializerTrait{ | |
init as initialize; | |
} | |
public $app; | |
public $request; | |
public $user; | |
public $authenticationModel = '\app\Model\User'; | |
public $model; | |
public function init() | |
{ | |
$this->initialize(); | |
$this->model = new $this->authenticationModel($this->app->db); | |
} | |
public function authenticate() | |
{ | |
$user = false; | |
// min. requirement | |
if ($key = $this->getKey()) { | |
/// custom logic | |
$user = $this->model->load(1); | |
} | |
return $user; | |
} | |
private function getKey() | |
{ | |
$key = false; | |
$query = $this->request->getQueryParams(); | |
if (isset($query['public-key']) && $query['public-key'] != '') { | |
$key = $query['public-key']; | |
} | |
// Custom header : Authorization-Key -> authorization-key | |
if ($this->request->hasHeader('authorization-key') && !empty($this->request->getHeader('authorization-key'))) { | |
$value = $this->request->getHeader('authorization-key'); | |
$key = $value[0]; | |
} | |
return $key; | |
} | |
private function getDomain() | |
{ | |
$origin = false; | |
/* | |
* Origin: https://developer.mozilla.org | |
* Origin: <scheme> "://" <hostname> [ ":" <port> ] | |
* Origin can be the empty string: this is useful, for example, if the source is a data URL. | |
* @src : https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin | |
*/ | |
if ($this->request->hasHeader('origin') && !empty($this->request->getHeader('origin'))) { | |
$value = $this->request->getHeader('origin'); | |
$origin = $value[0]; | |
} | |
return $origin; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace app\API; | |
use \atk4\core\InitializerTrait; | |
use \atk4\core\AppScopeTrait; | |
use \atk4\core\DIContainerTrait; | |
use \atk4\core\FactoryTrait; | |
use \atk4\core\ContainerTrait; | |
class Router | |
{ | |
use InitializerTrait{ | |
init as initialize; | |
} | |
use AppScopeTrait; | |
use DIContainerTrait; | |
use FactoryTrait; | |
use ContainerTrait; | |
public $app; | |
/** | |
* @var string uri path + query string | |
*/ | |
public $path; | |
/** | |
* @var string http request method GET|POST|DELETE etc... | |
*/ | |
public $method; | |
/** | |
* @var array holds the parsed values for URI path and ($_GET) QUERY string | |
*/ | |
private $parsed; | |
/** | |
* @var array stores the variables found in the uri pattern registered | |
*/ | |
public $params; | |
/** | |
* @var array that holds the registered routes | |
*/ | |
private $routes = []; | |
private $triggeredRoute; | |
/** | |
* @var object \app\API\RESTEndpoint | |
*/ | |
private $endpoint; | |
/** | |
* @var string \app\API\RESTEndpoint ::method | |
*/ | |
private $endpointMethod; | |
// uri pattern prefix ->(/v1/)some/route | |
protected $routerPatternPrefix = 'v1'; | |
/** | |
* @var object \Zend\Diactoros\ServerRequestFactory | |
*/ | |
public $request; | |
public function init() | |
{ | |
$this->initialize(); | |
// support for body with non-POST request method or payload that PHP doesn't parse by default | |
if (empty($_POST)) { | |
if (isset($_SERVER['CONTENT_TYPE']) && $_SERVER['CONTENT_TYPE'] === 'application/json') { | |
$_POST = json_decode(file_get_contents("php://input"), true); | |
} else { | |
parse_str(file_get_contents("php://input"), $_POST); | |
} | |
} | |
$this->request = \Zend\Diactoros\ServerRequestFactory::fromGlobals(); | |
$this->method = $this->request->getMethod(); | |
$this->path = $this->request->getRequestTarget(); | |
$this->parsed = parse_url($this->path); | |
$qry = isset($this->parsed['query'])?$this->parsed['query']:false; | |
if ($qry) { | |
parse_str($qry, $this->parsed['query']); | |
} else { | |
$this->parsed['query'] = []; | |
} | |
} | |
public function loadEndpoint($ep_ns) | |
{ | |
$endpointRoutes = $ep_ns::routes(); | |
foreach ($endpointRoutes as $route) { | |
$this->registerRoute($route['pattern'], $route['callable'], $route['method']); | |
} | |
} | |
public function sendResponse($responseData) | |
{ | |
if (is_a($responseData, '\app\API\Response') || | |
is_a($responseData, '\app\API\Exception') | |
) { | |
return; | |
} | |
return new \app\API\Response($responseData); | |
} | |
public function loadRoute($var, $callable = null) | |
{ | |
if (is_array($var)) { | |
foreach ($var as $pattern => $callable) { | |
$this->registerRoute($pattern, $callable); | |
} | |
} | |
if (is_string($var)) { | |
$this->registerRoute($var, $callable); | |
} | |
} | |
public function findMatchingRoute($path = null) : bool | |
{ | |
$this->filterRoutesByHTTPVerb(); | |
// regex uri variables search pattern | |
$variableSearchPattern = '\(:[\wd-]+?\)'; // /some/url/(:someVariable-maybe)/with/(:some-other)/variables | |
// if found any variables in the uri | |
// they will be replaced with this regex | |
// which matches a-z A-Z 0-9 and - | |
$patternWildcard = '([\wd-]+?)'; // -> /some/url/*/with/*/variables | |
$uriPath = $this->parsed['path'] == '/' ? $this->parsed['path'] : ltrim($this->parsed['path'], '/'); | |
if (empty($this->routes)) { | |
return false; | |
} | |
// Loop through the route array looking for wildcards | |
foreach ($this->routes as $key => $route) { | |
$pattern = $this->routerPatternPrefix.$route['pattern']; | |
// search for variables | |
$hasUriVariables = preg_match_all('/' . $variableSearchPattern . '/', $pattern, $_uriVariablesKeys); | |
// switch the search regex with wildcard | |
$wildcardUriPattern = preg_replace('#' . $variableSearchPattern . '#', $patternWildcard, $pattern); | |
// Check if the wildcard pattern is the one requested | |
$is_requested = preg_match('#^' . $wildcardUriPattern . '\Z#i', $uriPath, $_uriVariablesValues); | |
if ($is_requested) { | |
if ($hasUriVariables) { | |
$this->params = []; | |
array_shift($_uriVariablesValues); // we use preg_match | |
$keyStrings = array_shift($_uriVariablesKeys); // we use preg_match_all | |
foreach ($keyStrings as $position => $key) { | |
$this->params[preg_replace('/\(\:(.*?)\)/i', '$1', $key)] = $_uriVariablesValues[$position]; | |
} | |
} | |
if (! is_string($route['callable']) && is_callable($route['callable'])) { | |
// we accept closures | |
$reflection = new \ReflectionFunction($route['callable']); | |
if ((bool) $reflection->isClosure()) { | |
$route['type'] = 'closure'; | |
$this->triggeredRoute = $route; | |
return true; | |
} | |
} elseif (is_string($route['callable'])) { | |
// check if its a RESTEndpoint instance | |
// callable string format should be \Endpoint::method | |
list($endpoint, $method) = explode('::', $route['callable']); | |
// check if is RESTEndpoint or a child | |
if (is_subclass_of($endpoint = new $endpoint(), '\app\API\RESTEndpoint')) { | |
$this->endpoint = $endpoint; | |
$this->endpointMethod = $method; | |
$route['type'] = 'endpoint'; | |
$this->triggeredRoute = $route; | |
return true; | |
} | |
} | |
} | |
} | |
return false; | |
} | |
/** | |
* @return \app\API\Exception | |
* @throws \atk4\core\Exception | |
* @throws \atk4\data\Exception | |
*/ | |
public function runLoadedRoute() | |
{ | |
if (empty($this->triggeredRoute)) { | |
return new \app\API\Exception('Not Found', 'Bad Request', 404); | |
} | |
if (!isset($this->triggeredRoute['type'])) { | |
throw new \atk4\core\Exception('Route not loaded Router', 500); | |
} | |
$call = null; | |
switch ($this->triggeredRoute['type']) { | |
case 'closure': | |
$call = $this->loadCallable($this->triggeredRoute['callable']); | |
break; | |
case 'endpoint': | |
// attach the endpoint to the router | |
try { | |
// check if the authentication is not attached | |
if ($this->app->auth === null) { | |
$this->app->addAuth(); | |
} | |
// check if the method requested is public (default behaviour is locked) | |
if (!in_array($this->endpointMethod, $this->endpoint->publicMethods)) { | |
// if not authenticated $this->app->user is null | |
if ($this->app->user === false) { | |
return new \app\API\Exception('You do not have access', 'Unauthorized', 401); | |
} | |
} | |
$endpoint = $this->add($this->endpoint, [ | |
'params' => $this->params, | |
'request' => $this->request | |
]); | |
$call = $endpoint->{$this->endpointMethod}($this->params); | |
} catch (\atk4\data\ValidationException $e) { | |
$error_msg = reset($e->errors); | |
$error_msg_key = key($e->errors); | |
return new \app\API\Exception($error_msg_key.':'.$error_msg, 'Bad Request', 400); | |
} catch (\atk4\dsql\Exception $e) { | |
$exc = $e->by_exception->errorInfo; | |
$txt = str_replace("'", "", $exc[2]); | |
$txt = str_replace("Column ", "", $txt); | |
$txt = str_replace("null", "empty", $txt); | |
return new \app\API\Exception($txt, 'Bad Request', 400); | |
} catch (\atk4\core\Exception $e) { | |
throw new \atk4\data\Exception('Failed to attach Endpoint', 500, $e); | |
} | |
break; | |
} | |
$this->interpreteCall($call); | |
} | |
/** | |
* @param $call callable response|return value | |
* | |
* @return \app\API\Exception|\app\API\Response|void | |
*/ | |
private function interpreteCall($call = null) | |
{ | |
if (headers_sent()) { | |
return ; | |
} | |
if ($call === null) { | |
return new \app\API\Exception('No content', 'No content', 204); | |
} | |
if (is_a($call, '\app\API\Response') || is_a($call, '\app\API\Exception')) { | |
return ; | |
} | |
return new \app\API\Response($call); | |
} | |
/** | |
* @param null $callable | |
* | |
* @throws \atk4\data\Exception | |
*/ | |
public function loadCallable($callable = null) | |
{ | |
if ($callable === null) { | |
return; | |
} | |
try { | |
$params = is_array($this->params)?$this->params:[]; | |
call_user_func_array($callable, [array_merge($this->parsed['query'], $params)]); | |
} catch (\Exception $e) { | |
throw new \atk4\data\Exception('Failed to call closure', 500, $e); | |
} | |
} | |
private function registerRoute($pattern = '/', $callable = null, $HTTPVerb = null) | |
{ | |
// check if we have a set http verb or listen for all | |
$verb = $HTTPVerb ? $HTTPVerb : '*'; | |
$this->routes[] = ['verb' => $verb, 'pattern' => $pattern, 'callable' => $callable]; | |
} | |
private function filterRoutesByHTTPVerb($verb = null) | |
{ | |
$verb = $verb ? $verb : $this->method; | |
// get all registered routes | |
$routes = $this->routes; | |
$filteredRoutes = []; | |
foreach ($routes as $route) { | |
if ($route['verb'] === $verb || $route['verb'] === '*') { | |
$filteredRoutes[] = $route; | |
} | |
} | |
$this->routes = $filteredRoutes; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment