Skip to content
This repository has been archived by the owner on Oct 10, 2023. It is now read-only.

Commit

Permalink
Fix #44: Add PHP Attributes support (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
rustamwin committed Aug 11, 2022
1 parent 762564c commit 7484407
Show file tree
Hide file tree
Showing 24 changed files with 500 additions and 61 deletions.
49 changes: 39 additions & 10 deletions README.md
Expand Up @@ -23,7 +23,7 @@ validate it when needed.
The package could be installed with composer:

```
composer require yiisoft/request-model
composer require yiisoft/request-model --prefer-dist
```

## General usage
Expand Down Expand Up @@ -112,20 +112,20 @@ final class ViewPostRequest extends RequestModel

Inside the request model class, data is available using the following keys:

| key | source |
| ---------- | ---------------------------- |
| query | $request->getQueryParams() |
| body | $request->getParsedBody() |
| attributes | $request->getAttributes() |
| headers | $request->getHeaders() |
| files | $request->getUploadedFiles() |
| cookie | $request->getCookieParams() |
| key | source |
|------------|-------------------------------|
| query | $request->getQueryParams() |
| body | $request->getParsedBody() |
| attributes | $request->getAttributes() |
| headers | $request->getHeaders() |
| files | $request->getUploadedFiles() |
| cookie | $request->getCookieParams() |
| router | $currentRoute->getArguments() |

This data can be obtained as follows

```php
$this->requestData['router.id'];
$this->requestData['router']['id'];
```

or through the methods
Expand All @@ -135,7 +135,36 @@ $this->hasAttribute('body.user_id');
$this->getAttributeValue('body.user_id');
```

#### Attributes

You can use attributes in an action handler to get data from a request:

```php
use Psr\Http\Message\ResponseInterface;
use Yiisoft\RequestModel\Attribute\Request;
use Yiisoft\RequestModel\Attribute\Route;

final class SimpleController
{
public function action(#[Route('id')] int $id, #[Request('foo')] $attribute,): ResponseInterface
{
echo $id;
//...
}
}
```

Attributes are also supported in closure actions.

There are several attributes out of the box:

| Name | Source |
|---------------|---------------------------|
| Body | Parsed body of request |
| Query | Query parameter of URI |
| Request | Attribute of request |
| Route | Argument of current route |
| UploadedFiles | Uploaded files of request |

### Unit testing

Expand Down
31 changes: 17 additions & 14 deletions src/ActionWrapper.php
Expand Up @@ -14,27 +14,30 @@

final class ActionWrapper implements MiddlewareInterface
{
private string $class;
private string $method;
private ContainerInterface $container;
private RequestModelFactory $factory;

public function __construct(ContainerInterface $container, RequestModelFactory $factory, string $class, string $method)
{
$this->container = $container;
$this->factory = $factory;
$this->class = $class;
$this->method = $method;
public function __construct(
private ContainerInterface $container,
private HandlerParametersResolver $parametersResolver,
private string $class,
private string $method
) {
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$controller = $this->container->get($this->class);
$params = array_merge([$request, $handler], $this->factory->createInstances($request, $this->getHandlerParams()));
return (new Injector($this->container))->invoke([$controller, $this->method], $params);
$parameters = array_merge(
[$request, $handler],
$this->parametersResolver->resolve($this->getHandlerParameters(), $request),
);
return (new Injector($this->container))->invoke([$controller, $this->method], $parameters);
}

private function getHandlerParams(): array
/**
* @throws \ReflectionException
*
* @return \ReflectionParameter[]
*/
private function getHandlerParameters(): array
{
return (new ReflectionClass($this->class))
->getMethod($this->method)
Expand Down
21 changes: 21 additions & 0 deletions src/Attribute/Body.php
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Yiisoft\RequestModel\Attribute;

use Attribute;

#[Attribute(flags: Attribute::TARGET_PARAMETER)]
final class Body implements HandlerParameterAttributeInterface
{
public function getType(): string
{
return self::REQUEST_BODY;
}

public function getName(): ?string
{
return null;
}
}
21 changes: 21 additions & 0 deletions src/Attribute/HandlerParameterAttributeInterface.php
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Yiisoft\RequestModel\Attribute;

/**
* Represents action handler parameter [attribute](https://www.php.net/manual/en/language.attributes.php).
*/
interface HandlerParameterAttributeInterface
{
public const ROUTE_PARAM = 'route_param';
public const REQUEST_BODY = 'request_body';
public const REQUEST_ATTRIBUTE = 'request_attribute';
public const UPLOADED_FILES = 'uploaded_files';
public const QUERY_PARAM = 'query_param';

public function getName(): ?string;

public function getType(): string;
}
25 changes: 25 additions & 0 deletions src/Attribute/Query.php
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Yiisoft\RequestModel\Attribute;

use Attribute;

#[Attribute(flags: Attribute::TARGET_PARAMETER | Attribute::IS_REPEATABLE)]
final class Query implements HandlerParameterAttributeInterface
{
public function __construct(private string $name)
{
}

public function getName(): string
{
return $this->name;
}

public function getType(): string
{
return self::QUERY_PARAM;
}
}
25 changes: 25 additions & 0 deletions src/Attribute/Request.php
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Yiisoft\RequestModel\Attribute;

use Attribute;

#[Attribute(flags: Attribute::TARGET_PARAMETER | Attribute::IS_REPEATABLE)]
final class Request implements HandlerParameterAttributeInterface
{
public function __construct(private string $name)
{
}

public function getType(): string
{
return self::REQUEST_ATTRIBUTE;
}

public function getName(): ?string
{
return $this->name;
}
}
25 changes: 25 additions & 0 deletions src/Attribute/Route.php
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Yiisoft\RequestModel\Attribute;

use Attribute;

#[Attribute(flags: Attribute::TARGET_PARAMETER | Attribute::IS_REPEATABLE)]
final class Route implements HandlerParameterAttributeInterface
{
public function __construct(private string $name)
{
}

public function getName(): string
{
return $this->name;
}

public function getType(): string
{
return self::ROUTE_PARAM;
}
}
21 changes: 21 additions & 0 deletions src/Attribute/UploadedFiles.php
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Yiisoft\RequestModel\Attribute;

use Attribute;

#[Attribute(flags: Attribute::TARGET_PARAMETER)]
final class UploadedFiles implements HandlerParameterAttributeInterface
{
public function getType(): string
{
return self::UPLOADED_FILES;
}

public function getName(): ?string
{
return null;
}
}
16 changes: 7 additions & 9 deletions src/CallableWrapper.php
Expand Up @@ -16,32 +16,30 @@

final class CallableWrapper implements MiddlewareInterface
{
private ContainerInterface $container;
private RequestModelFactory $factory;

/**
* @var callable
*/
private $callback;

public function __construct(ContainerInterface $container, RequestModelFactory $factory, callable $callback)
{
$this->container = $container;
$this->factory = $factory;
public function __construct(
private ContainerInterface $container,
private HandlerParametersResolver $parametersResolver,
callable $callback
) {
$this->callback = $callback;
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$params = array_merge(
[$request, $handler],
$this->factory->createInstances($request, $this->getHandlerParams())
$this->parametersResolver->resolve($this->getHandlerParameters(), $request)
);
$response = (new Injector($this->container))->invoke($this->callback, $params);
return $response instanceof MiddlewareInterface ? $response->process($request, $handler) : $response;
}

private function getHandlerParams(): array
private function getHandlerParameters(): array
{
return $this
->getReflector()
Expand Down
65 changes: 65 additions & 0 deletions src/HandlerParametersResolver.php
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace Yiisoft\RequestModel;

use Psr\Http\Message\ServerRequestInterface;
use Yiisoft\RequestModel\Attribute\HandlerParameterAttributeInterface;
use Yiisoft\Router\CurrentRoute;

/**
* @internal
*/
class HandlerParametersResolver
{
public function __construct(private RequestModelFactory $factory, private CurrentRoute $currentRoute)
{
}

/**
* @throws \ReflectionException
*/
public function resolve(array $parameters, ServerRequestInterface $request): array
{
return array_merge(
$this->getAttributeParams($parameters, $request),
$this->factory->createInstances($request, $parameters)
);
}

/**
* @param \ReflectionParameter[] $parameters
* @param ServerRequestInterface $request
*
* @return array
*/
private function getAttributeParams(array $parameters, ServerRequestInterface $request): array
{
$actionParameters = [];
foreach ($parameters as $parameter) {
$attributes = $parameter->getAttributes(
HandlerParameterAttributeInterface::class,
\ReflectionAttribute::IS_INSTANCEOF
);
foreach ($attributes as $attribute) {
/** @var HandlerParameterAttributeInterface $attributeInstance */
$attributeInstance = $attribute->newInstance();

$actionParameters[$parameter->getName()] = match ($attributeInstance->getType()) {
HandlerParameterAttributeInterface::ROUTE_PARAM => $this
->currentRoute
->getArgument($attributeInstance->getName()),
HandlerParameterAttributeInterface::REQUEST_BODY => $request->getParsedBody(),
HandlerParameterAttributeInterface::REQUEST_ATTRIBUTE => $request->getAttribute(
$attributeInstance->getName()
),
HandlerParameterAttributeInterface::QUERY_PARAM => $request
->getQueryParams()[$attributeInstance->getName()] ?? null,
HandlerParameterAttributeInterface::UPLOADED_FILES => $request->getUploadedFiles()
};
}
}
return $actionParameters;
}
}

0 comments on commit 7484407

Please sign in to comment.