Skip to content

Commit 8411cc0

Browse files
committed
DecoratorChain: Support class paths in createDecorator
Allows to use a specific decorator, bypassing a loader that would load another decorator when only using the name.
1 parent 9f698b9 commit 8411cc0

2 files changed

Lines changed: 61 additions & 31 deletions

File tree

‎src/FormDecoration/DecoratorChain.php‎

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
* @template TDecorator of object
1818
* @implements IteratorAggregate<int, TDecorator>
1919
*
20+
* @phpstan-type Ident string|class-string
2021
* @phpstan-type decoratorOptionsFormat array<string, mixed>
21-
* @phpstan-type _decoratorsFormat1 array<string, decoratorOptionsFormat>
22-
* @phpstan-type _decoratorsFormat2 array<int, string|TDecorator|array{name: string, options?: decoratorOptionsFormat}>
22+
* @phpstan-type _decoratorsFormat1 array<Ident, decoratorOptionsFormat>
23+
* @phpstan-type _decoratorsFormat2 array<int, Ident|TDecorator|array{name: Ident, options?: decoratorOptionsFormat}>
2324
* @phpstan-type decoratorsFormat _decoratorsFormat1 | _decoratorsFormat2
2425
*/
2526
class DecoratorChain implements IteratorAggregate
@@ -62,7 +63,7 @@ public function addDecoratorLoader(string $namespace, string $suffix = ''): stat
6263
/**
6364
* Add a decorator to the chain.
6465
*
65-
* @param TDecorator|string $decorator
66+
* @param TDecorator|Ident $decorator
6667
* @param decoratorOptionsFormat $options Only allowed if parameter 1 is a string
6768
*
6869
* @return $this
@@ -100,28 +101,34 @@ public function addDecorator(object|string $decorator, array $options = []): sta
100101
* ```
101102
* // When no options are required or defaults are sufficient
102103
* $decorators = [
103-
* 'HtmlTag',
104-
* 'Label'
104+
* 'HtmlTag',
105+
* 'Label'
105106
* ];
106107
*
107108
* // Override default options by defining the option key and value
108109
*
109110
* // key: decorator name, value: options
110111
* $decorators = [
111-
* 'HtmlTag' => ['tag' => 'span', 'placement' => 'append'],
112-
* 'Label' => ['class' => 'element-label']
112+
* 'HtmlTag' => ['tag' => 'span', 'placement' => 'append'],
113+
* 'Label' => ['class' => 'element-label']
113114
* ];
114115
*
115116
* // or define the `name` and `options` key
116117
* $decorators = [
117-
* ['name' => 'HtmlTag', 'options' => ['tag' => 'span', 'placement' => 'append']],
118-
* ['name' => 'Label', 'options' => ['class' => 'element-label']]
118+
* ['name' => 'HtmlTag', 'options' => ['tag' => 'span', 'placement' => 'append']],
119+
* ['name' => 'Label', 'options' => ['class' => 'element-label']]
119120
* ];
120121
*
121122
* // or add Decorator instances
122123
* $decorators = [
123-
* (new HtmlTagDecorator())->getAttributes()->add(['tag' => 'span', 'placement' => 'append']),
124-
* (new LabelDecorator())->getAttributes()->add(['class' => 'element-label'])
124+
* (new HtmlTagDecorator())->getAttributes()->add(['tag' => 'span', 'placement' => 'append']),
125+
* (new LabelDecorator())->getAttributes()->add(['class' => 'element-label'])
126+
* ];
127+
*
128+
* // Class paths are also supported
129+
* $decorators = [
130+
* LabelDecorator::class,
131+
* ['name' => HtmlTagDecorator::class, ['tag' => 'span', 'placement' => 'append']]
125132
* ];
126133
* ```
127134
*
@@ -214,34 +221,44 @@ public function clearDecorators(): static
214221
/**
215222
* Create a decorator from the given name and options
216223
*
217-
* @param string $name
224+
* @param Ident $name
218225
* @param decoratorOptionsFormat $options
219226
*
220227
* @return TDecorator
221228
*
222-
* @throws InvalidArgumentException If the given decorator is unknown
229+
* @throws InvalidArgumentException If the given decorator is unknown or not an instance of the expected type
230+
* @throws UnexpectedValueException If the loaded decorator is not an instance of the expected type
223231
*/
224232
protected function createDecorator(string $name, array $options = []): object
225233
{
226-
$class = $this->loadPlugin('decorator', $name);
227-
228-
if (! $class) {
229-
throw new InvalidArgumentException(sprintf(
230-
"Can't load decorator '%s'. decorator unknown",
231-
$name
232-
));
233-
}
234-
235-
$decorator = new $class();
234+
if (class_exists($name)) {
235+
$decorator = new $name();
236+
if (! $decorator instanceof $this->decoratorType) {
237+
throw new InvalidArgumentException(sprintf(
238+
"Invalid decorator class '%s'. decorator must be an instance of %s",
239+
$name,
240+
$this->decoratorType,
241+
));
242+
}
243+
} else {
244+
$class = $this->loadPlugin('decorator', $name);
245+
if (! $class) {
246+
throw new InvalidArgumentException(sprintf(
247+
"Can't load decorator '%s'. decorator unknown",
248+
$name
249+
));
250+
}
236251

237-
if (! $decorator instanceof $this->decoratorType) {
238-
throw new UnexpectedValueException(sprintf(
239-
"%s expects loader to return an instance of %s for decorator '%s', got %s instead",
240-
__METHOD__,
241-
$this->decoratorType,
242-
$name,
243-
get_php_type($decorator)
244-
));
252+
$decorator = new $class();
253+
if (! $decorator instanceof $this->decoratorType) {
254+
throw new UnexpectedValueException(sprintf(
255+
"%s expects loader to return an instance of %s for decorator '%s', got %s instead",
256+
__METHOD__,
257+
$this->decoratorType,
258+
$name,
259+
get_php_type($decorator)
260+
));
261+
}
245262
}
246263

247264
if (! empty($options)) {

‎tests/FormDecorator/DecoratorChainTest.php‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use InvalidArgumentException;
66
use ipl\Html\FormDecoration\DecoratorChain;
77
use ipl\Html\Contract\FormElementDecoration;
8+
use ipl\Html\HtmlDocument;
89
use ipl\Tests\Html\TestCase;
910

1011
class DecoratorChainTest extends TestCase
@@ -104,11 +105,23 @@ public function testMethodAddDecoratorThrowsExceptionWhenDecoratorInstanceWithOp
104105
$this->chain->addDecorator(new TestDecorator(), ['optionKey1' => 'optionValue1']);
105106
}
106107

108+
public function testMethodAddDecoratorThrowsExceptionWhenInvalidClassPathIsPassed(): void
109+
{
110+
$this->expectException(InvalidArgumentException::class);
111+
$this->expectExceptionMessage(
112+
"Invalid decorator class 'ipl\Html\HtmlDocument'."
113+
." decorator must be an instance of ipl\Html\Contract\FormElementDecoration"
114+
);
115+
116+
$this->chain->addDecorator(HtmlDocument::class);
117+
}
118+
107119
public function testMethodAddDecoratorsWithValidArrayAsParam(): void
108120
{
109121
$decoratorFormats = [
110122
'TestWithOptions',
111123
new TestWithOptionsDecorator(),
124+
TestWithOptionsDecorator::class,
112125
'TestWithOptions' => ['optionKey1' => 'optionValue1', 'options' => ['optionKey2' => 'optionValue2']],
113126
['name' => 'TestWithOptions', 'options' => ['optionKey2' => 'optionValue2']]
114127
];

0 commit comments

Comments
 (0)