Skip to content

Commit

Permalink
Route API Endpoint by class name with searching by interface
Browse files Browse the repository at this point in the history
  • Loading branch information
janbarasek committed Apr 17, 2020
1 parent 6380a46 commit 7086574
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 86 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ class TestEndpoint extends BaseEndpoint
'data' => $data,
]);
}

}
```

Expand Down
16 changes: 9 additions & 7 deletions common.neon
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
parameters:
api:
namespaceConvention:
- '\Baraja\StructuredApi\*Endpoint'

services:
apiManager: Baraja\StructuredApi\ApiManager(%api.namespaceConvention%)
apiManager: Baraja\StructuredApi\ApiManager

extensions:
structuredApi: Baraja\StructuredApi\ApiExtension
structuredApi: Baraja\StructuredApi\ApiExtension

search:
structuredApi:
in: %appDir%/..
files: ['*Endpoint.php']
implements: Baraja\StructuredApi\Endpoint
tags: ['structured-api-endpoint']
42 changes: 30 additions & 12 deletions src/ApiExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,51 @@

use Nette\Application\Application;
use Nette\DI\CompilerExtension;
use Nette\DI\Definitions\ServiceDefinition;
use Nette\PhpGenerator\ClassType;
use Tracy\Debugger;

final class ApiExtension extends CompilerExtension
{
public function beforeCompile(): void
{
/** @var ServiceDefinition $apiManager */
$apiManager = $this->getContainerBuilder()->getDefinitionByType(ApiManager::class);
$apiManager->addSetup('?->setEndpoints(array_keys($this->findByTag(?)))', ['@self', 'structured-api-endpoint']);
}


/**
* @param ClassType $class
*/
public function afterCompile(ClassType $class): void
{
/** @var ServiceDefinition $application */
$application = $this->getContainerBuilder()->getDefinitionByType(Application::class);

/** @var ServiceDefinition $apiManager */
$apiManager = $this->getContainerBuilder()->getDefinitionByType(ApiManager::class);

$skipError = (bool) ($this->config['skipError'] ?? false);
$body = '$this->getByType(\'' . ApiManager::class . '\')->run($structuredApi__basePath);';
$body = '$this->getService(\'' . $apiManager->getName() . '\')->run($basePath);';

$class->getMethod('initialize')->addBody(
'if (strncmp($structuredApi__basePath = ' . Helpers::class . '::processPath($this->getService(\'http.request\')), \'api/\', 4) === 0) {'
. "\n\t" . '$this->getByType(' . Application::class . '::class)->onStartup[] = function(' . Application::class . ' $a) use ($structuredApi__basePath) {'
. "\n\t\t" . ($skipError === true
? 'try {'
. "\n\t\t\t" . $body
. "\n\t\t" . '} catch (' . StructuredApiException::class . ' $e) {'
. "\n\t\t\t" . (class_exists(Debugger::class) ? Debugger::class . '::log($e);' : '/* log error */')
. "\n\t\t" . '}'
: $body
'// Structured API.' . "\n"
. '(function () {' . "\n"
. "\t" . 'if (strncmp($basePath = ' . Helpers::class . '::processPath($this->getService(\'http.request\')), \'api/\', 4) === 0) {' . "\n"
. "\t\t" . '$this->getService(?)->onStartup[] = function(' . Application::class . ' $a) use ($basePath): void {' . "\n"
. "\t\t\t" . ($skipError === true
? "\t" . 'try {' . "\n"
. "\t\t\t" . $body . "\n"
. "\t\t\t" . '} catch (' . StructuredApiException::class . ' $e) {' . "\n"
. "\t\t\t\t" . (class_exists(Debugger::class) ? Debugger::class . '::log($e);' : '/* log error */') . "\n"
. "\t\t\t" . '}' . "\n"
: $body . "\n"
)
. "\n\t" . '};'
. "\n" . '}'
. "\t\t" . '};' . "\n"
. "\t" . '}' . "\n"
. '})();' . "\n",
[$application->getName()]
);
}
}
85 changes: 59 additions & 26 deletions src/ApiManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace Baraja\StructuredApi;


use Nette\Caching\Cache;
use Nette\Caching\IStorage;
use Nette\DI\Container;
use Nette\Utils\Strings;
use Tracy\Debugger;
Expand All @@ -21,21 +23,24 @@ final class ApiManager
'null' => null,
];

/** @var string[] */
private $namespaceConventions;

/** @var Container */
private $container;

/** @var Cache */
private $cache;

/** @var string[] (endpointPath => endpointType) */
private $endpoints = [];


/**
* @param string[] $namespaceConventions
* @param Container $container
* @param IStorage $storage
*/
public function __construct(array $namespaceConventions, Container $container)
public function __construct(Container $container, IStorage $storage)
{
$this->namespaceConventions = $namespaceConventions;
$this->container = $container;
$this->cache = new Cache($storage, 'structured-api');
}


Expand Down Expand Up @@ -121,6 +126,38 @@ public function get(string $path, ?array $params = [], ?string $method = null):
}


/**
* @internal for DIC
* @param string[] $endpointServices
*/
public function setEndpoints(array $endpointServices): void
{
$hash = implode('|', $endpointServices);
if (($cache = $this->cache->load('endpoints')) === null || ($cache['hash'] ?? '') !== $hash) {
$endpoints = [];
foreach ($endpointServices as $endpointService) {
$type = \get_class($this->container->getService($endpointService));
$className = (string) preg_replace('/^.*?([^\\\\]+)Endpoint$/', '$1', $type);
$endpointPath = Helpers::formatToApiName($className);
if (isset($endpoints[$endpointPath]) === true) {
throw new \RuntimeException(
'Api Manager: Endpoint "' . $endpointPath . '" already exist, '
. 'because this endpoint implements service "' . $type . '" and "' . $endpoints[$endpointPath] . '".'
);
}
$endpoints[$endpointPath] = $type;
}
$this->cache->save('endpoints', [
'hash' => $hash,
'endpoints' => $endpoints,
]);
} else {
$endpoints = $cache['endpoints'] ?? [];
}
$this->endpoints = $endpoints;
}


/**
* Route user query to class and action.
*
Expand All @@ -135,18 +172,10 @@ private function route(string $route, array $params): array
$action = null;

if (strpos($route = trim($route, '/'), '/') === false) { // 1. Simple match
foreach ($this->namespaceConventions as $classFormat) {
if (\class_exists($class = str_replace('*', Helpers::formatApiName($route), $classFormat)) === true) {
break;
}
}
$class = $this->endpoints[$route] ?? null;
$action = 'default';
} elseif (preg_match('/^([^\/]+)\/([^\/]+)$/', $route, $routeParser)) { // 2. <endpoint>/<action>
foreach ($this->namespaceConventions as $classFormat) {
if (\class_exists($class = str_replace('*', Helpers::formatApiName($routeParser[1]), $classFormat)) === true) {
break;
}
}
$class = $this->endpoints[$routeParser[1]] ?? null;
$action = Helpers::formatApiName($routeParser[2]);
}

Expand All @@ -155,7 +184,7 @@ private function route(string $route, array $params): array
}

if (\class_exists($class) === false) {
StructuredApiException::routeClassDoesNotExist((string) $class);
StructuredApiException::routeClassDoesNotExist($class);
}

return [
Expand All @@ -170,25 +199,29 @@ private function route(string $route, array $params): array
*
* @param string $class
* @param mixed[] $params
* @return BaseEndpoint
* @return Endpoint
*/
private function createInstance(string $class, array $params): BaseEndpoint
private function createInstance(string $class, array $params): Endpoint
{
return new $class($this->container, $params);
/** @var Endpoint $endpoint */
$endpoint = new $class($this->container);
$endpoint->setData($params);

return $endpoint;
}


/**
* Call all endpoint methods in regular order and return response state.
*
* @param BaseEndpoint $endpoint
* @param Endpoint $endpoint
* @param string $action
* @param string $method
* @param mixed[] $params
* @return BaseResponse|null
* @throws RuntimeStructuredApiException
*/
private function callActionMethods(BaseEndpoint $endpoint, string $action, string $method, array $params): ?BaseResponse
private function callActionMethods(Endpoint $endpoint, string $action, string $method, array $params): ?BaseResponse
{
$endpoint->startup();
$endpoint->startupCheck();
Expand Down Expand Up @@ -247,13 +280,13 @@ private function callActionMethods(BaseEndpoint $endpoint, string $action, strin


/**
* @param BaseEndpoint $endpoint
* @param Endpoint $endpoint
* @param string $parameter
* @param \ReflectionType $type
* @return mixed|null
* @throws RuntimeStructuredApiException
*/
private function returnEmptyValue(BaseEndpoint $endpoint, string $parameter, \ReflectionType $type)
private function returnEmptyValue(Endpoint $endpoint, string $parameter, \ReflectionType $type)
{
if ($type->allowsNull() === true) {
return null;
Expand Down Expand Up @@ -355,12 +388,12 @@ private function fixType($haystack, ?\ReflectionType $type)


/**
* @param BaseEndpoint $endpoint
* @param Endpoint $endpoint
* @param string $method
* @param string $action
* @return string|null
*/
private function getActionMethodName(BaseEndpoint $endpoint, string $method, string $action): ?string
private function getActionMethodName(Endpoint $endpoint, string $method, string $action): ?string
{
$tryMethods = [];
$tryMethods[] = ($method === 'GET' ? 'action' : strtolower($method)) . Strings::firstUpper($action);
Expand Down
18 changes: 13 additions & 5 deletions src/Endpoint/BaseEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
use Nette\Security\User;
use Nette\SmartObject;

abstract class BaseEndpoint
abstract class BaseEndpoint implements Endpoint
{
use SmartObject;

Expand All @@ -27,20 +27,18 @@ abstract class BaseEndpoint
protected $container;

/** @var mixed[] */
protected $data;
protected $data = [];

/** @var bool */
private $startupCheck = false;


/**
* @param Container $container
* @param mixed[] $data
*/
final public function __construct(Container $container, array $data)
final public function __construct(Container $container)
{
$this->container = $container;
$this->data = $data;

foreach (InjectExtension::getInjectProperties(\get_class($this)) as $property => $service) {
$this->{$property} = $container->getByType($service);
Expand Down Expand Up @@ -83,6 +81,16 @@ public function getData(): array
}


/**
* @internal
* @param mixed[] $data
*/
final public function setData(array $data): void
{
$this->data = $data;
}


/**
* @param mixed[] $haystack
*/
Expand Down
26 changes: 26 additions & 0 deletions src/Endpoint/Endpoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Baraja\StructuredApi;


use Nette\DI\Container;

interface Endpoint
{
public function __construct(Container $container);

/**
* @param mixed[] $data
*/
public function setData(array $data): void;

public function startup(): void;

public function startupCheck(): void;

public function saveState(): void;

public function __toString(): string;
}
31 changes: 6 additions & 25 deletions src/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@


use Nette\Http\Request;
use Nette\Utils\Strings;

final class Helpers
{
Expand Down Expand Up @@ -83,34 +84,14 @@ public static function formatApiName(string $name): string
{
return (string) preg_replace_callback('/-([a-z])/', function (array $match): string {
return strtoupper($match[1]);
}, self::firstUpper($name));
}, Strings::firstUpper($name));
}


/**
* Converts first character to lower case.
*
* @param string $s
* @return string
*/
public static function firstUpper(string $s): string
{
return strtoupper($s[0] ?? '') . (function_exists('mb_substr')
? mb_substr($s, 1, null, 'UTF-8')
: iconv_substr($s, 1, self::length($s), 'UTF-8')
);
}


/**
* Returns number of characters (not bytes) in UTF-8 string.
* That is the number of Unicode code points which may differ from the number of graphemes.
*
* @param string $s
* @return int
*/
public static function length(string $s): int
public static function formatToApiName(string $type): string
{
return function_exists('mb_strlen') ? mb_strlen($s, 'UTF-8') : strlen(utf8_decode($s));
return (string) preg_replace_callback('/([A-Z])/', function (array $match): string {
return '-' . strtolower($match[1]);
}, Strings::firstLower($type));
}
}
Loading

0 comments on commit 7086574

Please sign in to comment.