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

Add PHP Attributes support #48

Merged
merged 8 commits into from
Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
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,35 @@ $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\Route;
rustamwin marked this conversation as resolved.
Show resolved Hide resolved

final class SimpleController
{
public function action(#[Route('id')] int $id, #[ReqAttribute('foo')] $attribute,): ResponseInterface
rustamwin marked this conversation as resolved.
Show resolved Hide resolved
{
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading