Skip to content

Commit 67d91a3

Browse files
authored
Merge pull request #3 from softius/param-converter-interface
Param converter interface
2 parents a56e5dc + 43367b8 commit 67d91a3

File tree

9 files changed

+244
-43
lines changed

9 files changed

+244
-43
lines changed

README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Param Converter
22

3-
[...]
3+
CakePHP v3.x plugin for converting request parameters to objects. These objects replace the original parameters before dispatching the controller action and hence they can be injected as controller method arguments.
4+
5+
Heavily inspired by [Symfony ParamConverter](https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html)
46

57
## Install
68

@@ -18,7 +20,29 @@ bin/cake plugin load ParamConverter
1820

1921
## Usage
2022

21-
[...]
23+
Adjustments on application level are only necessary if you need to remove or / add new param converters.
24+
25+
### Configuration
26+
27+
By default, the plugin provides and registers two converters that can be used to convert request parameters to DateTime and Entity instances.
28+
Converters can be removed / added by adjusting the following configuration:
29+
30+
``` php
31+
<?php
32+
// config/param_converter.php
33+
return [
34+
'ParamConverter' => [
35+
'converters' => [
36+
\ParamConverter\EntityParamConverter::class,
37+
\ParamConverter\DateTimeParamConverter::class,
38+
]
39+
]
40+
];
41+
```
42+
43+
### Creating a converter
44+
45+
All converters must implement the `ParamConverterInterface`.
2246

2347
## Security
2448

config/bootstrap.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<?php
22

3+
use Cake\Core\Configure;
34
use Cake\Event\EventManager;
45
use ParamConverter\DispatchListener;
56

7+
Configure::load('ParamConverter.param_converter');
8+
69
EventManager::instance()->on(new DispatchListener());

config/param_converter.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
return [
3+
'ParamConverter' => [
4+
'converters' => [
5+
\ParamConverter\EntityParamConverter::class,
6+
\ParamConverter\DateTimeParamConverter::class,
7+
]
8+
]
9+
];

src/DateTimeParamConverter.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace ParamConverter;
4+
5+
use DateTime;
6+
7+
/**
8+
* Class DateTimeParamConverter
9+
*
10+
* Param Converter for DateTime class
11+
*
12+
* @package ParamConverter
13+
*/
14+
class DateTimeParamConverter implements ParamConverterInterface
15+
{
16+
/**
17+
* @inheritDoc
18+
*/
19+
public function supports(string $class): bool
20+
{
21+
return $class === DateTime::class;
22+
}
23+
24+
/**
25+
* @inheritDoc
26+
*/
27+
public function convertTo(string $value, string $class)
28+
{
29+
return new DateTime($value);
30+
}
31+
}

src/DispatchListener.php

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,12 @@
33
namespace ParamConverter;
44

55
use Cake\Controller\Controller;
6+
use Cake\Core\Configure;
67
use Cake\Event\Event;
78
use Cake\Event\EventListenerInterface;
89
use Cake\Http\ControllerFactory;
910
use Cake\Http\Response;
1011
use Cake\Http\ServerRequest;
11-
use Cake\ORM\Entity;
12-
use Cake\ORM\TableRegistry;
13-
use Cake\Utility\Inflector;
14-
use DateTime;
15-
use ReflectionException;
16-
use ReflectionMethod;
1712

1813
/**
1914
* Listens to 'beforeDispatch' event and applies the parameter convertion
@@ -49,40 +44,15 @@ public function beforeDispatch(Event $beforeEvent, ServerRequest $request, Respo
4944
$controller = $factory->create($request, $response);
5045
}
5146

52-
$action = $request->getParam('action');
53-
try {
54-
$method = new ReflectionMethod($controller, $action);
55-
$methodParams = $method->getParameters();
56-
$requestParams = $request->getParam('pass');
57-
58-
$stopAt = min(count($methodParams), count($requestParams));
59-
for ($i = 0; $i < $stopAt; $i++) {
60-
$methodParam = $methodParams[$i];
61-
$requestParam = $requestParams[$i];
62-
63-
$methodParamClass = $methodParam->getClass();
64-
$methodParamType = $methodParam->getType();
65-
if (empty($methodParamClass) && empty($methodParamType)) {
66-
continue;
67-
}
68-
69-
if (!empty($methodParamClass) && $methodParamClass->isSubclassOf(Entity::class)) {
70-
$table = TableRegistry::getTableLocator()->get(
71-
Inflector::tableize($methodParamClass->getShortName())
72-
);
47+
$converters = [];
48+
foreach (Configure::readOrFail('ParamConverter.converters') as $converter) {
49+
$converters[] = new $converter;
50+
}
51+
$manager = new ParamConverterManager($converters);
7352

74-
$requestParams[$i] = $table->get($requestParam);
75-
} elseif (!empty($methodParamClass) && $methodParamClass->getName() === DateTime::class) {
76-
$requestParams[$i] = new DateTime($requestParam);
77-
} elseif (!empty($methodParamType)) {
78-
settype($requestParam, $methodParamType->getName());
79-
$requestParams[$i] = $requestParam;
80-
}
81-
}
53+
$action = $request->getParam('action');
54+
$request = $manager->apply($request, get_class($controller), $action);
8255

83-
$beforeEvent->setData('request', $request->withParam('pass', $requestParams));
84-
} catch (ReflectionException $e) {
85-
return;
86-
}
56+
$beforeEvent->setData('request', $request);
8757
}
8858
}

src/EntityParamConverter.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace ParamConverter;
4+
5+
use Cake\Core\App;
6+
use Cake\ORM\Entity;
7+
use Cake\ORM\TableRegistry;
8+
use Cake\Utility\Inflector;
9+
10+
/**
11+
* Class EntityParamConverter
12+
*
13+
* Param Converter for Entity classes
14+
*
15+
* @package ParamConverter
16+
*/
17+
class EntityParamConverter implements ParamConverterInterface
18+
{
19+
/**
20+
* @inheritDoc
21+
*/
22+
public function supports(string $class): bool
23+
{
24+
return !empty($class) && is_subclass_of($class, Entity::class);
25+
}
26+
27+
/**
28+
* @inheritDoc
29+
*/
30+
public function convertTo(string $value, string $class)
31+
{
32+
$table = App::shortName($class, 'Model/Entity');
33+
$table = TableRegistry::getTableLocator()->get(
34+
Inflector::tableize($table)
35+
);
36+
37+
return $table->get($value);
38+
}
39+
}

src/ParamConverterInterface.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace ParamConverter;
4+
5+
/**
6+
* Interface ParamConverterInterface
7+
*
8+
* Converts request parameters to objects so that they can be injected to controller actions
9+
*
10+
* @package ParamConverter
11+
*/
12+
interface ParamConverterInterface
13+
{
14+
/**
15+
* Returns true only and only if the specified class is supported
16+
*
17+
* @param string $class Class to be checked
18+
* @return bool
19+
*/
20+
public function supports(string $class): bool;
21+
22+
/**
23+
* @param string $value Value to be converted
24+
* @param string $class Target class
25+
* @return mixed
26+
*/
27+
public function convertTo(string $value, string $class);
28+
}

src/ParamConverterManager.php

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
namespace ParamConverter;
4+
5+
use Cake\Http\ServerRequest;
6+
use ReflectionMethod;
7+
8+
class ParamConverterManager
9+
{
10+
/**
11+
* @var \ParamConverter\ParamConverterInterface[]
12+
*/
13+
private $converters;
14+
15+
/**
16+
* ParamConverterManager constructor.
17+
* @param \ParamConverter\ParamConverterInterface[] $paramConverters List of converters
18+
*/
19+
public function __construct(array $paramConverters)
20+
{
21+
foreach ($paramConverters as $paramConverter) {
22+
$this->add($paramConverter);
23+
}
24+
}
25+
26+
/**
27+
* Add the specified converter
28+
*
29+
* @param \ParamConverter\ParamConverterInterface $paramConverter Param Converter to be add
30+
* @return void
31+
*/
32+
public function add(ParamConverterInterface $paramConverter): void
33+
{
34+
$this->converters[] = $paramConverter;
35+
}
36+
37+
/**
38+
* Returns all the registered param converters
39+
*
40+
* @return \ParamConverter\ParamConverterInterface[]
41+
*/
42+
public function all(): array
43+
{
44+
return $this->converters;
45+
}
46+
47+
/**
48+
* Applies all the registered converters to the specified request
49+
*
50+
* @param \Cake\Http\ServerRequest $request Request to be updated (replace params with objects)
51+
* @param string $controller Controller name
52+
* @param string $action action name
53+
* @return \Cake\Http\ServerRequest
54+
* @throws \ReflectionException
55+
*/
56+
public function apply(ServerRequest $request, string $controller, string $action): ServerRequest
57+
{
58+
$method = new ReflectionMethod($controller, $action);
59+
$methodParams = $method->getParameters();
60+
$requestParams = $request->getParam('pass');
61+
62+
$stopAt = min(count($methodParams), count($requestParams));
63+
for ($i = 0; $i < $stopAt; $i++) {
64+
$methodParam = $methodParams[$i];
65+
$requestParam = $requestParams[$i];
66+
67+
$methodParamClass = $methodParam->getClass();
68+
if (!empty($methodParamClass)) {
69+
$requestParams[$i] = $this->convertParam($requestParam, $methodParamClass->getName());
70+
}
71+
72+
$methodParamType = $methodParam->getType();
73+
if (!empty($methodParamType) && $methodParamType->isBuiltin()) {
74+
settype($requestParam, $methodParamType->getName());
75+
$requestParams[$i] = $requestParam;
76+
}
77+
}
78+
79+
return $request->withParam('pass', $requestParams);
80+
}
81+
82+
/**
83+
* Converts the specified string value to the specified class
84+
*
85+
* @param string $value Raw value to be converted to a class
86+
* @param string $class Target class
87+
* @return mixed
88+
*/
89+
private function convertParam(string $value, string $class)
90+
{
91+
foreach ($this->all() as $converter) {
92+
if ($converter->supports($class)) {
93+
return $converter->convertTo($value, $class);
94+
}
95+
}
96+
}
97+
}

tests/TestCase/DispatchListenerTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ public function testUndefinedAction(): void
133133
$response = new Response();
134134

135135
$listener = new DispatchListener();
136-
$listener->beforeDispatch($event, $request, $response);
137136

138-
$this->assertEmpty($event->getData('request'));
137+
$this->expectException(\ReflectionException::class);
138+
$listener->beforeDispatch($event, $request, $response);
139139
}
140140
}

0 commit comments

Comments
 (0)