Skip to content

Commit

Permalink
response types as value objects
Browse files Browse the repository at this point in the history
  • Loading branch information
juliangut committed Mar 3, 2018
1 parent 40a8283 commit 4462d2e
Show file tree
Hide file tree
Showing 35 changed files with 482 additions and 476 deletions.
107 changes: 61 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

# slim-routing

A replacement for Slim's router that adds annotation and configuration based routing as well as expands the possibilities of your route callbacks by handling return types
A replacement for Slim's router that adds annotation and configuration based routing as well as expands the possibilities of your route callbacks by handling different response types

Thanks to this library, instead of configuring routes by hand one by one and including them into Slim's router you can create mapping files that define and structure your routes and let them be included into the router.

Expand All @@ -23,7 +23,7 @@ If you're familiar with how Doctrine defines entities mappings you'll feel at ho

> Routing gathering and compilation can be quite a heavy load process depending on how many classes/files and routes are defined, specially for annotations. For this reason it's advised to always use [Slim's router cache](https://www.slimframework.com/docs/objects/application.html#slim-default-settings) on production applications and invalidate cache on deployment
Route callbacks can now return `\Jgut\Slim\Routing\Response\ResponseTypeInterface` responses that will be later transformed into the mandatory `Psr\Message\ResponseInterface` in a way that lets you decouple view from controller
Route callbacks can now return `\Jgut\Slim\Routing\Response\ResponseType` responses that will be later transformed into the mandatory `Psr\Message\ResponseInterface` in a way that lets you decouple view from controller

## Installation

Expand All @@ -33,12 +33,30 @@ Route callbacks can now return `\Jgut\Slim\Routing\Response\ResponseTypeInterfac
composer require juliangut/slim-routing
```

symfony/yaml to parse yaml files
doctrine/annotations to parse routing annotations

```
composer require doctrine/annotations
```

symfony/yaml to parse yaml routing files

```
composer require symfony/yaml
```

spatie/array-to-xml to return XML responses

```
composer require spatie/array-to-xml
```

slim/twig-view to return Twig rendered responses

```
composer require slim/twig-view
```

## Usage

Require composer autoload file
Expand All @@ -49,7 +67,7 @@ require './vendor/autoload.php';

```php
use Jgut\Slim\Routing\Configuration;
use Jgut\Slim\Routing\Response\PayloadResponseType;
use Jgut\Slim\Routing\Response\PayloadResponse;
use Jgut\Slim\Routing\Response\Handler\JsonResponseHandler;
use Jgut\Slim\Routing\Router;
use Slim\App;
Expand All @@ -62,7 +80,7 @@ $container['router'] = function ($container) {
$configuration = new Configuration([
'sources' => ['/path/to/routing/files'],
'responseHandlers' => [
PayloadResponseType::class => new JsonResponseHandler(),
PayloadResponse::class => new JsonResponseHandler(),
],
]);
$router = new Router($configuration);
Expand All @@ -78,7 +96,7 @@ $container['router'] = function ($container) {
};

$app->get('/', function(ServerRequestInterface $request, ResponseInterface $response) {
return (new PayloadResponseType())->setResponse($response)->setPayload(['param' => 'value']);
return (new PayloadResponse())->setResponse($response)->setPayload(['param' => 'value']);
});

$app->run();
Expand All @@ -96,7 +114,7 @@ $app->run();
* alnum => `[a-zA-Z0-9]+`
* any => `[^}]+`
* `namingStrategy`, instance of \Jgut\Slim\Routing\Naming\NamingInterface (\Jgut\Slim\Routing\Naming\SnakeCase by default)
* `responseHandlers` array of \Jgut\Slim\Routing\Response\ResponseTypeInterface::class => \Jgut\Slim\Routing\Response\Handler\ResponseHandlerInterface or container entry
* `responseHandlers` array of \Jgut\Slim\Routing\Response\ResponseType::class => \Jgut\Slim\Routing\Response\Handler\ResponseHandlerInterface or container entry

## Response handling

Expand All @@ -110,54 +128,53 @@ $app->get('/hello/{name}', function ($request, $response, $args) {
})->setName('profile');
```

Route callbacks normally respond with a `Psr\Message\ResponseInterface` object, but thanks to slim-routing they can now respond with a more intent expressive ResponseTypeInterface object that will be handled afterwards
Route callbacks normally respond with a `Psr\Message\ResponseInterface` object, but thanks to slim-routing they can now respond with a more intent expressive ResponseType object that will be handled afterwards

Of course normal ResponseInterface responses from route callback will be treated as usual

### Response type

Response types are DTO objects with the needed data to later create a ResponseInterface object. This leaves the presentation logic out of router and allows for cleaner routes and easy presentation logic reuse
Response types are Value Objects with the needed data to later produce a ResponseInterface object. This leaves the presentation logic out of routes allowing for cleaner routes and easy presentation logic reuse

```
$app->get('/hello/{name}', function ($request, $response, $args) {
return ViewResponseType()
->setResponse($response)
->setTemplate('profile.html')
->setParameters(['name' => $args['name']]);
return ViewResponse('profile.html', ['name' => $args['name']], $request, $response);
})->setName('profile');
```

If route returns an instance of `\Jgut\Slim\Routing\Response\ResponseTypeInterface` it will be passed to the corresponding handler according to routing configuration
If a route returns an instance of `\Jgut\Slim\Routing\Response\ResponseType` it will be passed to the corresponding handler according to configuration

Provided response types:

* `PayloadResponseType` stores simple payload to be transformed for example to JSON
* `ViewResponseType` keeps agnostic template payload so it can be rendered in a handler
* `PayloadResponse` stores simple payload data to be later transformed for example into JSON or XML
* `ViewResponse` keeps agnostic template parameters so it can be rendered in a handler

### Response type handler

Mapped on configuration's "responseHandlers" key, a response handler will be responsible of returning a `Psr\Message\ResponseInterface` from the received `\Jgut\Slim\Routing\Response\ResponseTypeInterface`
Mapped on configuration's "responseHandlers" key, a response handler will be responsible of returning a `Psr\Message\ResponseInterface` from the received `\Jgut\Slim\Routing\Response\ResponseType`

Typically they will agglutinate presentation logic: how to represent the data contained in the response type, such as transform it into json, XML, etc, or render it with a template engine such as Twig or Plates
Typically they will agglutinate presentation logic: how to represent the data contained in the response type, such as transform it into JSON, XML, etc, or render it with a template engine such as Twig or Plates

Provided response types:
Provided response types handlers:

* `JsonResponseTypeHandler` receives a PayloadResponseType and returns a JSON response
* `XmlResponseTypeHandler` receives a PayloadResponseType and returns a XML response (requires [spatie/array-to-xml](https://github.com/spatie/array-to-xml))
* `TwigViewResponseTypeHandler` receives a generic ViewResponseType and returns a template rendered thanks to Twig and [Slim's Twig-View](https://github.com/slimphp/Twig-View)
* `JsonResponseHandler` receives a PayloadResponse and returns a JSON response
* `XmlResponseHandler` receives a PayloadResponse and returns a XML response (requires [spatie/array-to-xml](https://github.com/spatie/array-to-xml))
* `TwigViewResponseHandler` receives a generic ViewResponse and returns a template rendered thanks to [slim/twig-view](https://github.com/slimphp/Twig-View)

You can create you're own response handlers to compose specifically formatted JSON (JSON-API, ...) or use another template engines (Plates, ...)

### Routes

Routes can be defined in two basic ways: by setting them in definition files of various types or directly defined in annotations on controller classes
Routes can be defined in two basic ways: by setting them in definition files of various formats or directly defined in annotations on controller classes

#### Annotations

##### Router (Class level)

Just a mark to identify classes defining routes. Its presence is mandatory on each routing class
Just to identify classes defining routes. Its presence is mandatory on each routing class either way the rest of the annotations won't be read

```php
use Jgut\Slim\Routing\Mapping\Annotation as JSR
use Jgut\Slim\Routing\Mapping\Annotation as JSR;

/**
* @JSR\Router
Expand All @@ -172,7 +189,7 @@ class Home
Defines a group in which routes may reside. It is not mandatory but useful when you want to do route grouping or apply middleware to several routes at the same time

```php
use Jgut\Slim\Routing\Mapping\Annotation as JSR
use Jgut\Slim\Routing\Mapping\Annotation as JSR;

/**
* @JSR\Router
Expand All @@ -190,17 +207,17 @@ class Section
```

* `prefix`, optional, prefix to be prepended to route names
* `parent`, optional, references a parent class name
* `pattern`, optional, path pattern
* `parent`, optional, references a parent group name
* `pattern`, optional, path pattern, prepended to route patterns
* `placeholders`, optional, array of regex/alias for path placeholders,
* `middleware`, optional, array of middleware to be added to all group routes

##### Route (Method level)

Defines the final routes added to the router
Defines the final routes added to Slim

```php
use Jgut\Slim\Routing\Mapping\Annotation as JSR
use Jgut\Slim\Routing\Mapping\Annotation as JSR;

/**
* @JSR\Router
Expand All @@ -215,7 +232,7 @@ class Section
* pattern="do/{action}",
* placeholders={"action": "[a-z0-9]+"},
* middleware={"routeMiddlewareName"},
* priority=-10,
* priority=-10
* )
*/
public function doSomething()
Expand Down Expand Up @@ -269,7 +286,7 @@ return [
],
],
// Routes/groups ...
]
];
```

####### JSON
Expand Down Expand Up @@ -385,11 +402,11 @@ return [

##### Group

Defines a group in which routes may reside.
Defines a group in which routes may reside

* `routes`, array of routes and/or subgroups (this key identifies a group)
* `prefix`, optional, prefix to be prepended to route names
* `pattern`, optional, path pattern
* `pattern`, optional, path pattern, prepended to route patterns
* `placeholders`, optional, array of regex/alias for path placeholders,
* `middleware`, optional, array of middleware to be added to all group routes

Expand All @@ -408,13 +425,17 @@ Defines a route added to Slim

### Route composition

Using grouping with juliangut/slim-routing is a little different to how default Slim's router works

Groups are never really added to the router (in the sense you can add them in Slim with `$app->group(...)`) but routes are a composition of definitions that makes the final route

#### Name

Final route name is composed of the concatenation of group prefixes followed by route name according to configured route naming strategy
Final route name is composed of the concatenation of group prefixes followed by route name according to configured route _naming strategy_

#### Pattern

Resulting route pattern is composed of the concatenation of group patterns and finally route pattern
Resulting route pattern is composed of the concatenation of group patterns if any and finally route pattern

#### Placeholders

Expand All @@ -424,17 +445,11 @@ It is important to pay attention not to duplicate placeholder names in the resul

#### Middleware

Resulting middleware applied to a route will be the result of combining group middleware and route middleware

There is a drawback on defining middleware in any other format but PHP definition files. You cannot use a Closure, only strings, so that middleware must be a reference to a container entry

## Considerations

Important to note is the order in which the middleware is assigned to each route:
Resulting middleware added to a route will be the result of combining group middleware and route middleware and are applied to the route in the following order, so that final middleware execution order will be the same as expected in any Slim app:

* Firstly route middleware will be applied in the order they are defined
* Then group (if any) middleware are to be applied in the same order they are defined
* If route group (if any) has a parent then parent's middleware are applied in the order they are defined, and this goes up until no group parent is defined
* Firstly route middleware will be set to the route **in the order they are defined**
* Then route group (if any) middleware are to be set into the route **in the same order they are defined**
* If group has a parent then parent's middleware are set **in the order they are defined**, and this goes up until no parent group is left

## Contributing

Expand Down
8 changes: 4 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,13 @@
"prefer-stable": true,
"require": {
"php": "^7.0",
"doctrine/annotations": "^1.4",
"juliangut/mapping": "~0.3",
"psr/http-server-middleware": "^1.0",
"slim/slim": "^3.8",
"symfony/polyfill-php71": "^1.0"
"slim/slim": "^3.8"
},
"require-dev": {
"brainmaestro/composer-git-hooks": "^2.1",
"doctrine/annotations": "^1.4",
"friendsofphp/php-cs-fixer": "^2.0",
"infection/infection": "^0.7.0",
"mikey179/vfsstream": "^1.6",
Expand All @@ -47,8 +46,9 @@
"squizlabs/php_codesniffer": "^2.0"
},
"suggest": {
"doctrine/annotations": "In order to load routing from annotations",
"symfony/yaml": "In order to load routing from YAML files",
"spatie/array-to-xml": "If using XmlResponseHandler"
"spatie/array-to-xml": "In order to return XML responses"
},
"autoload": {
"psr-4": {
Expand Down
24 changes: 12 additions & 12 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
use Jgut\Mapping\Driver\DriverInterface;
use Jgut\Mapping\Metadata\MetadataResolver;
use Jgut\Slim\Routing\Mapping\Driver\DriverFactory;
use Jgut\Slim\Routing\Naming\NamingInterface;
use Jgut\Slim\Routing\Naming\SnakeCase;
use Jgut\Slim\Routing\Response\Handler\ResponseTypeHandlerInterface;
use Jgut\Slim\Routing\Naming\Strategy;
use Jgut\Slim\Routing\Response\Handler\ResponseTypeHandler;
use Jgut\Slim\Routing\Route\Resolver;

/**
Expand Down Expand Up @@ -62,14 +62,14 @@ class Configuration
/**
* Naming strategy.
*
* @var NamingInterface
* @var Strategy
*/
protected $namingStrategy;

/**
* Response handlers.
*
* @var ResponseTypeHandlerInterface[]|string[]
* @var ResponseTypeHandler[]|string[]
*/
protected $responseHandlers = [];

Expand All @@ -82,7 +82,7 @@ class Configuration
*/
public function __construct($configurations = [])
{
if (!\is_iterable($configurations)) {
if (!\is_array($configurations) && !$configurations instanceof \Traversable) {
throw new \InvalidArgumentException('Configurations must be an iterable');
}

Expand Down Expand Up @@ -272,9 +272,9 @@ public function setRouteResolver(Resolver $routeResolver): self
/**
* Get naming strategy.
*
* @return NamingInterface
* @return Strategy
*/
public function getNamingStrategy(): NamingInterface
public function getNamingStrategy(): Strategy
{
if ($this->namingStrategy === null) {
$this->namingStrategy = new SnakeCase();
Expand All @@ -286,11 +286,11 @@ public function getNamingStrategy(): NamingInterface
/**
* Set naming strategy.
*
* @param NamingInterface $namingStrategy
* @param Strategy $namingStrategy
*
* @return static
*/
public function setNamingStrategy(NamingInterface $namingStrategy): self
public function setNamingStrategy(Strategy $namingStrategy): self
{
$this->namingStrategy = $namingStrategy;

Expand All @@ -300,7 +300,7 @@ public function setNamingStrategy(NamingInterface $namingStrategy): self
/**
* Get response handlers.
*
* @return ResponseTypeHandlerInterface[]|string[]
* @return ResponseTypeHandler[]|string[]
*/
public function getResponseHandlers(): array
{
Expand Down Expand Up @@ -328,8 +328,8 @@ public function setResponseHandlers(array $handlers): self
/**
* Add response handler.
*
* @param string $response
* @param ResponseTypeHandlerInterface|string $handler
* @param string $response
* @param ResponseTypeHandler|string $handler
*
* @throws \InvalidArgumentException
*
Expand Down
16 changes: 7 additions & 9 deletions src/Mapping/Metadata/RouteMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,15 @@ public function getGroupChain(): array
if ($this->groupChain === null) {
$groupChain = [];

if ($this->group instanceof GroupMetadata) {
$parent = $this->group;
while ($parent instanceof GroupMetadata) {
if (\in_array($parent, $groupChain, true)) {
throw new MetadataException('Circular group reference detected');
}
$parent = $this->group;
while ($parent instanceof GroupMetadata) {
if (\in_array($parent, $groupChain, true)) {
throw new MetadataException('Circular group reference detected');
}

\array_unshift($groupChain, $parent);
\array_unshift($groupChain, $parent);

$parent = $parent->getParent();
}
$parent = $parent->getParent();
}

$this->groupChain = $groupChain;
Expand Down
Loading

0 comments on commit 4462d2e

Please sign in to comment.