Skip to content

Commit

Permalink
Added request attribute interface (#2)
Browse files Browse the repository at this point in the history
* Added request attribute interface

* Fixed typos + minor issues + updated README
  • Loading branch information
ezimuel authored Sep 13, 2022
1 parent fdd2998 commit 39ed176
Show file tree
Hide file tree
Showing 11 changed files with 501 additions and 77 deletions.
7 changes: 7 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Files to exclude when creating archive
/.github export-ignore
/tests export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/phpstan.neon export-ignore
/phpunit.xml.dist export-ignore
112 changes: 112 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,118 @@ The previous PHP script is basically a front controller of an MVC application (s
In this diagram the front controller is stored in a `public/index.php` file.
The `public` folder is usually the document root of a web server.

## Using a pipeline of controllers

If you want you can specify a pipeline of controllers to be executed for a specific route.
For instance, imagine to have a route as follows:

```php
// config/container.php
use App\Controller;
use SimpleMVC\Controller\BasicAuth;

return [
'config' => [
'routing' => [
'routes' => [
[ 'GET', '/admin', [BasicAuth::class, Controller\HomePage::class ]
]
],
'authentication' => [
'username' => 'admin',
'password' => '1234567890'
]
]
];
```

The route `GET /admin` will execute the `BasicAuth` controller first and, if the
authentication will be successfull, the `HomePage` controller after.

This is a pipeline of two controllers executed in order. The `BasicAuth` is a
simple implementation of the [Basic Access Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). This controller uses the `username` and `password`
configuration in the `authentication` section.

If the authentication is not success, the `BasicAuth` emits an `HaltResponse` that
will stop the pipeline execution. `HaltResponse` is a special PSR-7 that informs
the SimpleMVC framework to halt the execution.

## Passing attributes bewtween controllers

If you need to pass an attribute (parameter) from a controller to another in a
pipeline of execution you can use the `AttributeInterface`. For instance, imagine
to pass a `foo` attribute from a controller `A` to controller `B`, using the follwing
routing pipeline:

```php
// config/container.php
use App\Controller;

return [
'config' => [
'routing' => [
'routes' => [
[ 'GET', '/', [Controller\A::class, Controller\B::class ]
]
]
]
];
```

You need to create the controller A as follows:

```php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use SimpleMVC\Controller\AttributeInterface;
use SimpleMVC\Controller\AttributeTrait;
use SimpleMVC\Controller\ControllerInterface;

class A implements ControllerInterface, AttributeInterface
{
use AttributeTrait;

public function execute(
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface
{
$this->addRequestAttribute('foo', 'bar');
return $response;
}
}
```

We can use an `AttributeTrait` that implements the `AttributeInterface` with
the `addRequestAttribute(string $name, $value)`. This function adds a [PSR-7](https://www.php-fig.org/psr/psr-7/)
attribute into the `$request` for the next controller.

In order to get the `foo` parameter in the `B` controller you can use the
PSR-7 standard function `getAttribute()` from the HTTP request, as follows:

```php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use SimpleMVC\Controller\ControllerInterface;

class B implements ControllerInterface
{
public function execute(
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface
{
$attribute = $request->getAttribute('foo');
pritnf("Attribute is: %s", $attribute);
return $response;
}
}
```

Notice that you don't need to implement the `AttributeInterface` for the `B` controller
since we only need to read from the `$request`.


## Quickstart

You can start using the framework with the [skeleton](https://github.com/simplemvc/skeleton) application.
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
},
"require-dev": {
"phpstan/phpstan": "^1.8",
"phpunit/phpunit": "^9.5"
"phpunit/phpunit": "^9.5",
"mockery/mockery": "^1.5",
"phpstan/phpstan-mockery": "^1.1"
},
"autoload": {
"psr-4": {
Expand Down
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ parameters:
- tests
ignoreErrors:
- '#Class SimpleMVC\\Response\\HaltResponse extends \@final class Nyholm\\Psr7\\Response#'
- '#Call to an undefined method Mockery\\ExpectationInterface\|Mockery\\HigherOrderMessage::with\(\)#'
74 changes: 42 additions & 32 deletions src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use SimpleMVC\Controller\AttributeInterface;
use SimpleMVC\Controller\Error404;
use SimpleMVC\Controller\Error405;
use SimpleMVC\Exception\ControllerException;
Expand All @@ -34,7 +35,6 @@ class App
const VERSION = '0.2.0';

private Dispatcher $dispatcher;
private ServerRequestInterface $request;
private ContainerInterface $container;
private LoggerInterface $logger;
private float $startTime;
Expand Down Expand Up @@ -83,24 +83,6 @@ public function __construct(ContainerInterface $container)
if (isset($this->config['bootstrap']) && !is_callable($this->config['bootstrap'])) {
throw new InvalidConfigException('The ["bootstrap"] must a callable');
}

$factory = new Psr17Factory();
$this->request = (new ServerRequestCreator($factory, $factory, $factory, $factory))
->fromGlobals();

$this->logger->info(sprintf(
"Request: %s %s",
$this->request->getMethod(),
$this->request->getUri()->getPath()
));
}

/**
* Returns the PSR-7 request
*/
public function getRequest(): ServerRequestInterface
{
return $this->request;
}

public function getContainer(): ContainerInterface
Expand Down Expand Up @@ -138,11 +120,17 @@ public function bootstrap(): void
/**
* @throws ControllerException
*/
public function dispatch(): ResponseInterface
public function dispatch(ServerRequestInterface $request): ResponseInterface
{
$this->logger->info(sprintf(
"Request: %s %s",
$request->getMethod(),
$request->getUri()->getPath()
));

$routeInfo = $this->dispatcher->dispatch(
$this->request->getMethod(),
$this->request->getUri()->getPath()
$request->getMethod(),
$request->getUri()->getPath()
);
$controllerName = null;
switch ($routeInfo[0]) {
Expand All @@ -158,36 +146,58 @@ public function dispatch(): ResponseInterface
$controllerName = $routeInfo[1];
if (isset($routeInfo[2])) {
foreach ($routeInfo[2] as $name => $value) {
$this->request = $this->request->withAttribute($name, $value);
$request = $request->withAttribute($name, $value);
}
}
break;
}
// default HTTP response
$response = new Response(200);

$controllerName = is_array($controllerName) ?: [$controllerName];
foreach ($controllerName as $controller) {
$this->logger->debug(sprintf("Executing %s", $controller));
if (!is_array($controllerName)) {
$controllerName = [$controllerName];
}
foreach ($controllerName as $name) {
$this->logger->debug(sprintf("Executing %s", $name));
try {
$response = $this->container
->get($controller)
->execute($this->request, $response);
$controller = $this->container->get($name);
$response = $controller->execute($request, $response);
if ($response instanceof HaltResponse) {
$this->logger->debug(sprintf("Found HaltResponse in %s", $controller));
$this->logger->debug(sprintf("Found HaltResponse in %s", $name));
break;
}
// Add the PSR-7 attributes to the next request
if ($controller instanceof AttributeInterface) {
foreach ($controller->getRequestAttribute() as $key => $value) {
$request = $request->withAttribute($key, $value);
}
}
} catch (NotFoundExceptionInterface $e) {
throw new ControllerException(sprintf(
'The controller name %s cannot be retrieved from the container',
$controller
$name
));
}
}


$this->logger->info(sprintf(
"Response: %d",
$response->getStatusCode()
));

$this->logger->info(sprintf("Execution time: %.3f sec", microtime(true) - $this->startTime));
$this->logger->info(sprintf("Memory usage: %d bytes", memory_get_usage(true)));

return $response;
}

/**
* Returns a PSR-7 request from globals ($_GET, $_POST, $_SERVER, etc)
*/
public static function buildRequestFromGlobals(): ServerRequestInterface
{
$factory = new Psr17Factory();
return (new ServerRequestCreator($factory, $factory, $factory, $factory))
->fromGlobals();
}
}
29 changes: 29 additions & 0 deletions src/Controller/AttributeInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
/**
* SimpleMVC
*
* @link http://github.com/simplemvc/framework
* @copyright Copyright (c) Enrico Zimuel (https://www.zimuel.it)
* @license https://opensource.org/licenses/MIT MIT License
*/
declare(strict_types=1);

namespace SimpleMVC\Controller;

interface AttributeInterface
{
/**
* Add an attribute for the next PSR-7 request in a pipeline
*
* @param mixed $value
*/
public function addRequestAttribute(string $name, $value): void;

/**
* Get a request attribute, if $name is not specified return
* all the attributes as array
*
* @return mixed
*/
public function getRequestAttribute(string $name = null);
}
36 changes: 36 additions & 0 deletions src/Controller/AttributeTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/**
* SimpleMVC
*
* @link http://github.com/simplemvc/framework
* @copyright Copyright (c) Enrico Zimuel (https://www.zimuel.it)
* @license https://opensource.org/licenses/MIT MIT License
*/
declare(strict_types=1);

namespace SimpleMVC\Controller;

trait AttributeTrait
{
/** @var mixed[] */
protected array $attributes = [];

/**
* @param mixed $value
*/
public function addRequestAttribute(string $name, $value): void
{
$this->attributes[$name] = $value;
}

/**
* @return mixed
*/
public function getRequestAttribute(string $name = null)
{
if (empty($name)) {
return $this->attributes;
}
return $this->attributes[$name] ?? null;
}
}
Loading

0 comments on commit 39ed176

Please sign in to comment.