Skip to content

Instantly share code, notes, and snippets.

@funyx
Created March 13, 2019 12:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save funyx/09eeeccb21186c928fd054038b7070ea to your computer and use it in GitHub Desktop.
Save funyx/09eeeccb21186c928fd054038b7070ea to your computer and use it in GitHub Desktop.
atk4/api
<?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);
}
}
<?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;
}
}
<?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