mirror of
https://github.com/kevinveenbirkenbach/infinito.git
synced 2024-12-04 23:17:19 +01:00
Optimized REST API and tests
This commit is contained in:
parent
cbb1c76640
commit
1beae0cdc0
@ -1,6 +0,0 @@
|
||||
# REST API Controller
|
||||
The controllers use the [FOSRestBundle](https://symfony.com/doc/master/bundles/FOSRestBundle/index.html).
|
||||
## Workflow
|
||||
The abstract workflow of the REST API controllers for a singular entity looks like this:
|
||||
![REST API Workflow](.meta/workflow.svg)
|
||||
Special actions, e.g. lists are not shown in this diagram. This diagram also shows downstream procedures, to remember to implement them. Feel free to remove them from the diagram, as soon as they are documented somewhere else.
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Infinito\Controller\API\Meta;
|
||||
namespace Infinito\Controller\API\Rest;
|
||||
|
||||
use Infinito\Controller\API\AbstractAPIController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@ -11,7 +11,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
*
|
||||
* @todo Implement!
|
||||
*/
|
||||
class RightApiController extends AbstractAPIController
|
||||
final class HeredityController extends AbstractAPIController
|
||||
{
|
||||
public function read(Request $request, $identifier): Response
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Infinito\Controller\API\Meta;
|
||||
namespace Infinito\Controller\API\Rest;
|
||||
|
||||
use Infinito\Controller\API\AbstractAPIController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@ -11,7 +11,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
*
|
||||
* @todo Implement!
|
||||
*/
|
||||
class LawApiController extends AbstractAPIController
|
||||
final class LawController extends AbstractAPIController
|
||||
{
|
||||
public function read(Request $request, $identifier): Response
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Infinito\Controller\API\Meta;
|
||||
namespace Infinito\Controller\API\Rest;
|
||||
|
||||
use Infinito\Controller\API\AbstractAPIController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@ -11,7 +11,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
*
|
||||
* @todo Implement!
|
||||
*/
|
||||
class MemberApiController extends AbstractAPIController
|
||||
final class MemberController extends AbstractAPIController
|
||||
{
|
||||
public function read(Request $request, $identifier): Response
|
||||
{
|
24
application/symfony/src/Controller/API/Rest/README.md
Normal file
24
application/symfony/src/Controller/API/Rest/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# REST API Controller
|
||||
The controllers use the [FOSRestBundle](https://symfony.com/doc/master/bundles/FOSRestBundle/index.html).
|
||||
## Url Scheme
|
||||
The scheme for the rest api is the following:
|
||||
|
||||
| Url | Methods | Function |
|
||||
|---|---|---|
|
||||
| api/rest/{entity}.{format} | HEAD | Returns the create information for a specific entity |
|
||||
| api/rest/{entity}.{format} | POST | Creates a specific entity and returns it. |
|
||||
| api/rest/{entity}/{uri}.{format} | GET | Returns a specific entity. Including all actions |
|
||||
| api/rest/{entity}/{uri}/{action}.{format} | GET | Returns the result for an action of an specific entity. |
|
||||
| api/rest/{entity}/{uri}.{format} | PUT, PATCH | Updates a specific entity and returns it. |
|
||||
| api/rest/{entity}/{uri}.{format} | DELETE | Deletes a specific entity|
|
||||
|
||||
If an concrete entity doesn't implement an method it should redirect to the connected entity which is responsible for this method.
|
||||
|
||||
In the future it would make sense to implement [more methods](https://de.wikipedia.org/wiki/Representational_State_Transfer#Umsetzung).
|
||||
|
||||
The standard format of an entity MUST be JSON.
|
||||
|
||||
## Workflow
|
||||
The abstract workflow of the REST API controllers for a singular entity looks like this:
|
||||
![REST API Workflow](.meta/workflow.svg)
|
||||
Special actions, e.g. lists are not shown in this diagram. This diagram also shows downstream procedures, to remember to implement them. Feel free to remove them from the diagram, as soon as they are documented somewhere else.
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Infinito\Controller\API\Meta;
|
||||
namespace Infinito\Controller\API\Rest;
|
||||
|
||||
use Infinito\Controller\API\AbstractAPIController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@ -11,7 +11,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
*
|
||||
* @todo Implement!
|
||||
*/
|
||||
class HeredityApiController extends AbstractAPIController
|
||||
final class RightController extends AbstractAPIController
|
||||
{
|
||||
public function read(Request $request, $identifier): Response
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Infinito\Controller\API\Source;
|
||||
namespace Infinito\Controller\API\Rest;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@ -18,20 +18,19 @@ use Infinito\DBAL\Types\Meta\Right\LayerType;
|
||||
* @see https://symfony.com/blog/new-in-symfony-4-1-internationalized-routing
|
||||
* @Route(
|
||||
* {
|
||||
* "en":"/source/{identity}.{_format}",
|
||||
* "de":"/quelle/{identity}.{_format}",
|
||||
* "eo":"/fonto/{identity}.{_format}",
|
||||
* "es":"/fontanar/{identity}.{_format}",
|
||||
* "nl":"/bron/{identity}.{_format}"
|
||||
* "en":"/api/rest/source/{identity}.{_format}",
|
||||
* "de":"/api/rest/quelle/{identity}.{_format}",
|
||||
* "eo":"/api/rest/fonto/{identity}.{_format}",
|
||||
* "es":"/api/rest/fontanar/{identity}.{_format}",
|
||||
* "nl":"/api/rest/bron/{identity}.{_format}"
|
||||
* },
|
||||
* defaults={
|
||||
* "identity"="",
|
||||
* "_format"="json"
|
||||
* } ,
|
||||
* name="source_"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class SourceApiController extends AbstractAPIController
|
||||
final class SourceController extends AbstractAPIController
|
||||
{
|
||||
/**
|
||||
* @Route(
|
@ -12,7 +12,7 @@ use Infinito\DBAL\Types\Meta\Right\CRUDType;
|
||||
final class ActionType extends CRUDType
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @var string this action executes an entity
|
||||
*/
|
||||
const EXECUTE = 'execute';
|
||||
|
||||
|
@ -10,20 +10,22 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
*/
|
||||
final class ActionHttpMethodMap extends AbstractMap implements ActionHttpMethodMapInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
const ACTION_HTTP_METHOD_MAP = [
|
||||
ActionType::READ => [
|
||||
Request::METHOD_GET,
|
||||
],
|
||||
ActionType::CREATE => [
|
||||
Request::METHOD_POST,
|
||||
Request::METHOD_GET,
|
||||
Request::METHOD_HEAD,
|
||||
],
|
||||
ActionType::UPDATE => [
|
||||
Request::METHOD_PUT,
|
||||
Request::METHOD_GET,
|
||||
Request::METHOD_PATCH,
|
||||
],
|
||||
ActionType::DELETE => [
|
||||
Request::METHOD_GET,
|
||||
Request::METHOD_DELETE,
|
||||
],
|
||||
ActionType::EXECUTE => [
|
||||
|
@ -13,11 +13,11 @@ use Infinito\Entity\Source\AbstractSource;
|
||||
use Infinito\Exception\NotSetException;
|
||||
use Infinito\Repository\RepositoryInterface;
|
||||
use Infinito\Entity\Source\SourceInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Infinito\Attribut\ClassAttribut;
|
||||
use Infinito\Exception\AllreadyDefinedException;
|
||||
use Infinito\Domain\RequestManagement\Right\RequestedRightInterface;
|
||||
use Infinito\Domain\RepositoryManagement\LayerRepositoryFactoryService;
|
||||
use Infinito\Exception\EntityNotFoundHttpException;
|
||||
|
||||
/**
|
||||
* @author kevinfrantz
|
||||
@ -51,12 +51,12 @@ class RequestedEntity extends AbstractEntity implements RequestedEntityInterface
|
||||
/**
|
||||
* @param EntityInterface|null $entity
|
||||
*
|
||||
* @throws NotFoundHttpException
|
||||
* @throws EntityNotFoundHttpException
|
||||
*/
|
||||
private function validateLoadedEntity(?EntityInterface $entity): void
|
||||
{
|
||||
if (!$entity) {
|
||||
throw new NotFoundHttpException('Entity with {id:"'.$this->id.'",slug:"'.$this->slug.'"} not found');
|
||||
throw new EntityNotFoundHttpException('Entity with {id:"'.$this->id.'",slug:"'.$this->slug.'"} not found');
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,7 +97,7 @@ class RequestedEntity extends AbstractEntity implements RequestedEntityInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotFoundHttpException
|
||||
* @throws NotSetException
|
||||
*/
|
||||
private function validateLayerRepositoryFactoryService(): void
|
||||
{
|
||||
|
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Infinito\Exception;
|
||||
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* @author kevinfrantz
|
||||
*/
|
||||
final class EntityNotFoundHttpException extends NotFoundHttpException
|
||||
{
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
# API
|
||||
This folder contains the different API controllers.
|
||||
Right now just an REST API is implemented.
|
||||
In the future it would also make sense to implement an [GraphQL](https://graphql.org/) API.
|
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace tests\Integration\Controller\API\Rest;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Infinito\DBAL\Types\Meta\Right\LayerType;
|
||||
|
||||
/**
|
||||
* @author kevinfrantz
|
||||
*/
|
||||
class ControllerLayerIntegrationTest extends TestCase
|
||||
{
|
||||
public function testThatControllerForEachLayerExist(): void
|
||||
{
|
||||
foreach (LayerType::getChoices() as $layer) {
|
||||
$className = 'Infinito\\Controller\\API\\Rest\\'.ucfirst($layer).'Controller';
|
||||
$this->assertTrue(class_exists($className));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Integration\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Infinito\DBAL\Types\Meta\Right\LayerType;
|
||||
use Infinito\DBAL\Types\RESTResponseType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Infinito\Domain\LayerManagement\LayerActionMap;
|
||||
use Infinito\DBAL\Types\ActionType;
|
||||
use Infinito\Domain\MapManagement\ActionHttpMethodMap;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @author kevinfrantz
|
||||
*
|
||||
* @todo Implement more tests for success etc.
|
||||
*/
|
||||
class RestRoutesReachableIntegrationTest extends KernelTestCase
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see \PHPUnit\Framework\TestCase::setUp()
|
||||
*/
|
||||
public function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
}
|
||||
|
||||
public function testAllRoutePossibilities(): void
|
||||
{
|
||||
foreach ([
|
||||
'12314123',
|
||||
'testslug',
|
||||
] as $uri) {
|
||||
foreach (RESTResponseType::getChoices() as $format) {
|
||||
foreach (LayerType::getChoices() as $layer) {
|
||||
$actions = LayerActionMap::getActions($layer);
|
||||
foreach ($actions as $action) {
|
||||
foreach (ActionHttpMethodMap::getHttpMethods($action) as $method) {
|
||||
$baseUrl = "api/rest/$layer";
|
||||
switch ($action) {
|
||||
case ActionType::CREATE:
|
||||
$url = "$baseUrl.$format";
|
||||
$this->routeAssert($url, $method);
|
||||
break;
|
||||
case ActionType::EXECUTE:
|
||||
$url = "$baseUrl/$uri/action/execute.$format";
|
||||
$this->routeAssert($url, $method);
|
||||
break;
|
||||
default:
|
||||
$url = "$baseUrl/$uri.$format";
|
||||
$this->routeAssert($url, $method);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param string $method
|
||||
*/
|
||||
private function routeAssert(string $url, string $method): void
|
||||
{
|
||||
$request = new Request([], [], [], [], [], [
|
||||
'REQUEST_URI' => $url,
|
||||
]);
|
||||
$request->setMethod($method);
|
||||
$response = static::$kernel->handle($request);
|
||||
$this->assertTrue($this->isResponseValid($response), "Route $url with Method $method sends an 404 response and doesn't throw an EntityNotFoundHttpException!");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Response $response
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isResponseValid(Response $response): bool
|
||||
{
|
||||
$is404 = 404 === $response->getStatusCode();
|
||||
$isEntityNotFoundHttpException = strpos($response->getContent(), 'EntityNotFoundHttpException');
|
||||
|
||||
return !$is404 || $isEntityNotFoundHttpException;
|
||||
}
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Integration\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Infinito\DBAL\Types\LanguageType;
|
||||
use Infinito\DBAL\Types\Meta\Right\LayerType;
|
||||
use Infinito\DBAL\Types\RESTResponseType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* @author kevinfrantz
|
||||
*
|
||||
* @todo Implement more tests for success etc.
|
||||
*/
|
||||
class RoutesReachableIntegrationTest extends KernelTestCase
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see \PHPUnit\Framework\TestCase::setUp()
|
||||
*/
|
||||
public function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
}
|
||||
|
||||
public function testAllRoutePossibilities()
|
||||
{
|
||||
foreach (LayerType::getChoices() as $layer) {
|
||||
$this->controller($layer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $entity
|
||||
*/
|
||||
private function controller(string $entity): void
|
||||
{
|
||||
$this->language($entity, Request::METHOD_GET);
|
||||
$this->language($entity, Request::METHOD_POST);
|
||||
$this->language($entity.'s', Request::METHOD_GET);
|
||||
$this->slugAndId($entity, Request::METHOD_PUT);
|
||||
$this->slugAndId($entity, Request::METHOD_GET);
|
||||
$this->slugAndId($entity, Request::METHOD_DELETE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $route
|
||||
* @param string $method
|
||||
*/
|
||||
private function slugAndId(string $route, string $method): void
|
||||
{
|
||||
$this->language("$route/12345", $method);
|
||||
$this->language("$route/asdfg", $method);
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Implement routing without i18l part!
|
||||
*
|
||||
* @param string $entity
|
||||
* @param string $method
|
||||
*/
|
||||
private function language(string $entity, string $method): void
|
||||
{
|
||||
//$this->type('api/'.$entity, $method);
|
||||
foreach (LanguageType::getChoices() as $language) {
|
||||
$this->type("$language/api/$entity", $method);
|
||||
$this->type("$language/html/$entity", $method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $route
|
||||
* @param string $method
|
||||
*/
|
||||
private function type(string $route, string $method): void
|
||||
{
|
||||
$this->routeAssert($route, $method);
|
||||
foreach (RESTResponseType::getChoices() as $restResponseType => $value) {
|
||||
$this->routeAssert("$route.$restResponseType", $method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param string $method
|
||||
*/
|
||||
private function routeAssert(string $url, string $method): void
|
||||
{
|
||||
$request = new Request([], [], [], [], [], [
|
||||
'REQUEST_URI' => $url,
|
||||
]);
|
||||
$request->setMethod($method);
|
||||
$response = static::$kernel->handle($request);
|
||||
$this->assertNotEquals(404, $response->getStatusCode(), "Route $url with Method $method sends an 404 response!");
|
||||
}
|
||||
}
|
@ -10,6 +10,11 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
*/
|
||||
class ImprintFixtureSourceTest extends KernelTestCase
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @see \PHPUnit\Framework\TestCase::setUp()
|
||||
*/
|
||||
public function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
@ -18,7 +23,7 @@ class ImprintFixtureSourceTest extends KernelTestCase
|
||||
public function testImprintSourceReachable(): void
|
||||
{
|
||||
$request = new Request([], [], [], [], [], [
|
||||
'REQUEST_URI' => 'source/imprint.html',
|
||||
'REQUEST_URI' => 'api/rest/source/imprint.html',
|
||||
]);
|
||||
$request->setMethod(Request::METHOD_GET);
|
||||
$response = static::$kernel->handle($request);
|
||||
|
@ -25,7 +25,7 @@ class ActionHttpMethodTest extends TestCase
|
||||
|
||||
public function testCreateActionTrue(): void
|
||||
{
|
||||
$subset = [Request::METHOD_GET, Request::METHOD_POST];
|
||||
$subset = [Request::METHOD_POST, Request::METHOD_HEAD];
|
||||
$action = ActionType::CREATE;
|
||||
$haystack = ActionHttpMethodMap::getHttpMethods($action);
|
||||
$this->assertSubsetInArray($subset, $haystack, true);
|
||||
@ -34,7 +34,7 @@ class ActionHttpMethodTest extends TestCase
|
||||
|
||||
public function testCreateActionFalse(): void
|
||||
{
|
||||
$subset = [Request::METHOD_GET, Request::METHOD_POST];
|
||||
$subset = [Request::METHOD_POST, Request::METHOD_HEAD];
|
||||
$action = 'wrong value';
|
||||
$haystack = ActionHttpMethodMap::getHttpMethods($action);
|
||||
$this->assertSubsetInArray($subset, $haystack, false);
|
||||
|
Loading…
Reference in New Issue
Block a user