Skip to content

Commit f9b265f

Browse files
committed
Feature: add MagicControl for dynamic component creation
Add MagicControl that allows dynamic creation of subcomponents based on registered factories configured via Neon. - Add MagicControl class for component registry and factory management - Add MagicComponents trait for presenters/controls - Add ApplicationExtension DI extension for Neon configuration - Add nette/di as required dependency - Add comprehensive tests and documentation Usage in Neon: application: components: latestArticles: App\LatestArticlesControlFactory Usage in Latte: {control magic-latestArticles} {control magic-latestArticles, count: 10} Closes #2
1 parent 691f106 commit f9b265f

13 files changed

+591
-1
lines changed

.docs/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [Presenter](#presenter)
88
- [StructuredTemplates](#structured-templates)
99
- [Control](#control)
10+
- [MagicControl](#magiccontrol)
1011
- [Component](#component)
1112
- [Responses](#responses)
1213
- [CSVResponse](#csvresponse)
@@ -58,6 +59,67 @@ class YourPresenter extends Presenter
5859

5960
- NullControl - displays nothing
6061

62+
#### MagicControl
63+
64+
MagicControl allows dynamic creation of components based on registered factories. This is useful when you want to create reusable components that can be configured via Neon.
65+
66+
**1. Register the DI extension:**
67+
68+
```neon
69+
extensions:
70+
application: Contributte\Application\DI\ApplicationExtension
71+
72+
application:
73+
components:
74+
latestArticles: App\Components\LatestArticlesControlFactory
75+
sidebar: App\Components\SidebarControlFactory
76+
```
77+
78+
**2. Create your component factory interface:**
79+
80+
```php
81+
namespace App\Components;
82+
83+
interface LatestArticlesControlFactory
84+
{
85+
public function create(): LatestArticlesControl;
86+
}
87+
```
88+
89+
**3. Use the MagicComponents trait in your presenter:**
90+
91+
```php
92+
use Contributte\Application\UI\MagicComponents;
93+
use Contributte\Application\UI\MagicControl;
94+
use Nette\Application\UI\Presenter;
95+
use Nette\ComponentModel\IComponent;
96+
97+
class BasePresenter extends Presenter
98+
{
99+
use MagicComponents;
100+
101+
public function injectMagicComponents(MagicControl $magicControl): void
102+
{
103+
$this->setMagicComponentFactories($magicControl->getFactories());
104+
}
105+
106+
protected function createComponent(string $name): ?IComponent
107+
{
108+
return $this->tryCreateMagicComponent($name) ?? parent::createComponent($name);
109+
}
110+
}
111+
```
112+
113+
**4. Use in Latte templates:**
114+
115+
```latte
116+
{* Basic usage *}
117+
{control magic-latestArticles}
118+
119+
{* With parameters (passed to component's render method) *}
120+
{control magic-latestArticles, count: 10}
121+
```
122+
61123
### Component
62124

63125
- NullComponent - displays nothing

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
],
2020
"require": {
2121
"php": ">=8.2",
22-
"nette/application": "^3.2.6 || ^4.0.0"
22+
"nette/application": "^3.2.6 || ^4.0.0",
23+
"nette/di": "^3.2.0 || ^4.0.0"
2324
},
2425
"require-dev": {
2526
"nette/http": "^3.3.2 || ^4.0.0",

phpstan.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ parameters:
1717
- .docs
1818

1919
ignoreErrors:
20+
-
21+
identifier: trait.unused

src/DI/ApplicationExtension.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Contributte\Application\DI;
4+
5+
use Contributte\Application\UI\MagicControl;
6+
use Nette\DI\CompilerExtension;
7+
use Nette\DI\ContainerBuilder;
8+
use Nette\DI\Definitions\ServiceDefinition;
9+
use Nette\Schema\Expect;
10+
use Nette\Schema\Schema;
11+
use stdClass;
12+
13+
/**
14+
* Contributte Application DI Extension
15+
*
16+
* Example configuration:
17+
* application:
18+
* components:
19+
* latestArticles: App\LatestArticlesControlFactory
20+
* sidebar: App\SidebarControlFactory
21+
*/
22+
class ApplicationExtension extends CompilerExtension
23+
{
24+
25+
public function getConfigSchema(): Schema
26+
{
27+
return Expect::structure([
28+
'components' => Expect::arrayOf(
29+
Expect::string()->required()
30+
)->default([]),
31+
]);
32+
}
33+
34+
public function loadConfiguration(): void
35+
{
36+
$builder = $this->getContainerBuilder();
37+
/** @var stdClass $config */
38+
$config = $this->getConfig();
39+
40+
// Skip if no components configured
41+
if ($config->components === []) {
42+
return;
43+
}
44+
45+
// Register MagicControl service
46+
$magicControl = $builder->addDefinition($this->prefix('magicControl'))
47+
->setFactory(MagicControl::class)
48+
->setAutowired(true);
49+
50+
// Add factory references
51+
foreach ($config->components as $name => $factoryClass) {
52+
$this->addComponentFactory($magicControl, (string) $name, $factoryClass);
53+
}
54+
}
55+
56+
private function addComponentFactory(ServiceDefinition $magicControl, string $name, string $factoryClass): void
57+
{
58+
$builder = $this->getContainerBuilder();
59+
60+
// Get or register the factory service
61+
$serviceName = $builder->getByType($factoryClass) ?? $this->registerFactory($factoryClass);
62+
63+
// Create a factory callback that uses the DI container to get the factory service
64+
$magicControl->addSetup('addFactory', [
65+
$name,
66+
ContainerBuilder::literal('fn() => $this->getService(?)->create()', [$serviceName]),
67+
]);
68+
}
69+
70+
private function registerFactory(string $factoryClass): string
71+
{
72+
$builder = $this->getContainerBuilder();
73+
$serviceName = $this->prefix('factory.' . md5($factoryClass));
74+
75+
if (!$builder->hasDefinition($serviceName)) {
76+
$builder->addFactoryDefinition($serviceName)
77+
->setImplement($factoryClass);
78+
}
79+
80+
return $serviceName;
81+
}
82+
83+
}

src/UI/MagicComponents.php

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Contributte\Application\UI;
4+
5+
use Nette\Application\UI\Control;
6+
use Nette\Application\UI\Presenter;
7+
use Nette\ComponentModel\IComponent;
8+
9+
/**
10+
* Trait for enabling magic component creation in presenters and controls.
11+
*
12+
* Usage in presenter/control:
13+
* use MagicComponents;
14+
*
15+
* public function injectMagicComponents(MagicControl $magicControl): void
16+
* {
17+
* $this->setMagicComponentFactories($magicControl->getFactories());
18+
* }
19+
*
20+
* protected function createComponent(string $name): ?IComponent
21+
* {
22+
* return $this->tryCreateMagicComponent($name) ?? parent::createComponent($name);
23+
* }
24+
*
25+
* Usage in Latte:
26+
* {control magic-latestArticles}
27+
* {control magic-latestArticles, count: 10}
28+
*
29+
* @mixin Presenter
30+
* @mixin Control
31+
*/
32+
trait MagicComponents
33+
{
34+
35+
public const MAGIC_PREFIX = 'magic-';
36+
37+
/** @var array<string, callable> */
38+
private array $magicComponentFactories = [];
39+
40+
/**
41+
* Register a magic component factory
42+
*/
43+
public function addMagicComponentFactory(string $name, callable $factory): void
44+
{
45+
$this->magicComponentFactories[$name] = $factory;
46+
}
47+
48+
/**
49+
* Set magic component factories in bulk
50+
*
51+
* @param array<string, callable> $factories
52+
*/
53+
public function setMagicComponentFactories(array $factories): void
54+
{
55+
$this->magicComponentFactories = $factories;
56+
}
57+
58+
/**
59+
* Check if a magic component factory is registered
60+
*/
61+
public function hasMagicComponentFactory(string $name): bool
62+
{
63+
return isset($this->magicComponentFactories[$name]);
64+
}
65+
66+
/**
67+
* Get all registered magic component factory names
68+
*
69+
* @return string[]
70+
*/
71+
public function getMagicComponentFactoryNames(): array
72+
{
73+
return array_keys($this->magicComponentFactories);
74+
}
75+
76+
/**
77+
* Try to create a magic component by name.
78+
* Returns null if the name doesn't match the magic prefix or if no factory is registered.
79+
*
80+
* Call this from your createComponent() method:
81+
* return $this->tryCreateMagicComponent($name) ?? parent::createComponent($name);
82+
*/
83+
protected function tryCreateMagicComponent(string $name): ?IComponent
84+
{
85+
if (!str_starts_with($name, self::MAGIC_PREFIX)) {
86+
return null;
87+
}
88+
89+
$componentName = substr($name, strlen(self::MAGIC_PREFIX));
90+
91+
if (!isset($this->magicComponentFactories[$componentName])) {
92+
return null;
93+
}
94+
95+
$factory = $this->magicComponentFactories[$componentName];
96+
97+
return $factory();
98+
}
99+
100+
/**
101+
* Create a magic component by name with arguments
102+
*
103+
* @param mixed ...$args Arguments to pass to the factory
104+
*/
105+
protected function createMagicComponent(string $name, mixed ...$args): ?IComponent
106+
{
107+
if (!isset($this->magicComponentFactories[$name])) {
108+
return null;
109+
}
110+
111+
$factory = $this->magicComponentFactories[$name];
112+
113+
return $factory(...$args);
114+
}
115+
116+
}

0 commit comments

Comments
 (0)