Skip to content

Commit 52c5009

Browse files
committed
TemplateGenerator WIP
1 parent 76419c6 commit 52c5009

File tree

6 files changed

+919
-2
lines changed

6 files changed

+919
-2
lines changed

src/Bridges/ApplicationDI/ApplicationExtension.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ public function getConfigSchema(): Nette\Schema\Schema
6464
'scanComposer' => Expect::bool(class_exists(ClassLoader::class)),
6565
'scanFilter' => Expect::string('*Presenter'),
6666
'silentLinks' => Expect::bool(),
67+
'generate' => Expect::structure([
68+
'templateClass' => Expect::bool(),
69+
]),
6770
]);
6871
}
6972

@@ -131,6 +134,11 @@ public function beforeCompile(): void
131134
->addSetup([self::class, 'initializeBlueScreenPanel']);
132135
}
133136

137+
if ($this->debugMode && $this->config->generate->templateClass) {
138+
$builder->getDefinition('latte.templateFactory')
139+
->setArgument('generate', true);
140+
}
141+
134142
$all = [];
135143

136144
foreach ($builder->findByType(Nette\Application\IPresenter::class) as $def) {

src/Bridges/ApplicationLatte/Template.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ final public function getFile(): ?string
117117
/**
118118
* Returns array of all parameters.
119119
*/
120-
final public function getParameters(): array
120+
public function getParameters(): array
121121
{
122122
$res = [];
123123
foreach ((new \ReflectionObject($this))->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) {

src/Bridges/ApplicationLatte/TemplateFactory.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public function __construct(
2929
private readonly ?Nette\Http\IRequest $httpRequest = null,
3030
private readonly ?Nette\Security\User $user = null,
3131
$templateClass = null,
32+
private bool $generate = false,
3233
) {
3334
if ($templateClass && (!class_exists($templateClass) || !is_a($templateClass, Template::class, true))) {
3435
throw new Nette\InvalidArgumentException("Class $templateClass does not implement " . Template::class . ' or it does not exist.');
@@ -51,7 +52,9 @@ public function createTemplate(?UI\Control $control = null, ?string $class = nul
5152
}
5253

5354
$latte = $this->latteFactory->create($control);
54-
$template = new $class($latte);
55+
$template = $this->generate && $control instanceof UI\Presenter
56+
? new TemplateGenerator($latte, $class, $control)
57+
: new $class($latte);
5558
$this->injectDefaultVariables($template, $control);
5659

5760
Nette\Utils\Arrays::invoke($this->onCreate, $template);
@@ -85,6 +88,8 @@ private function injectDefaultVariables(Template $template, ?UI\Control $control
8588
$template->$key = $value;
8689
} catch (\TypeError) {
8790
}
91+
} elseif ($template instanceof TemplateGenerator) {
92+
$template->addDefaultVariable($key, $value);
8893
}
8994
}
9095
}
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Nette Framework (https://nette.org)
5+
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Nette\Bridges\ApplicationLatte;
11+
12+
use Latte;
13+
use Nette\Application\UI;
14+
use Nette\Utils\FileSystem;
15+
use Nette\Utils\Helpers;
16+
use Nette\Utils\Type;
17+
use Nette\Utils\Validators;
18+
use function is_object;
19+
20+
21+
/**
22+
* On-the-fly template class generator.
23+
*/
24+
final class TemplateGenerator extends Template
25+
{
26+
private string $className;
27+
private ?self $parent = null;
28+
private array $params = [];
29+
30+
/** @var array<string, array{Type, ?string}> */
31+
private array $properties = [];
32+
33+
34+
public function __construct(
35+
Latte\Engine $latte,
36+
?string $className = null,
37+
?UI\Control $control = null,
38+
) {
39+
parent::__construct($latte);
40+
41+
$this->className = $className && $className !== DefaultTemplate::class
42+
? $className
43+
: preg_replace('#Control|Presenter$#', '', $control::class) . 'Template';
44+
45+
if (!class_exists($this->className)) {
46+
$this->createTemplateClass($control);
47+
$this->updateControlPhpDoc($control);
48+
}
49+
$this->loadTemplateClass();
50+
}
51+
52+
53+
public function render(?string $file = null, array $params = []): void
54+
{
55+
$this->updateTemplate($file ?? $this->getFile());
56+
$this->getLatte()->render($file ?? $this->getFile(), $params + $this->params);
57+
}
58+
59+
60+
public function addDefaultVariable(string $name, mixed $value): void
61+
{
62+
$owner = $this->findPropertyOwner($name) ?? $this;
63+
if (!isset($owner->properties[$name])) {
64+
if (is_object($value)) {
65+
if (PHP_VERSION_ID >= 80400) {
66+
$rc = new \ReflectionClass($value);
67+
$rc->initializeLazyObject($value);
68+
$value = ($rc)->newLazyProxy(fn() => $this->ensureProperty($name, $value));
69+
}
70+
71+
} else {
72+
$value = new class (fn() => $this->ensureProperty($name, $value)) implements \IteratorAggregate {
73+
public function __construct(
74+
private \Closure $cb,
75+
) {
76+
}
77+
78+
79+
public function __toString(): string
80+
{
81+
return ($this->cb)(); // basePath & baseUrl
82+
}
83+
84+
85+
public function getIterator(): \Traversable
86+
{
87+
yield from ($this->cb)(); // flashes
88+
}
89+
};
90+
}
91+
}
92+
93+
$this->params[$name] = $value;
94+
}
95+
96+
97+
private function ensureProperty(string $name, mixed $value): mixed
98+
{
99+
[$declaredType, $phpDoc] = $this->properties[$name] ?? null;
100+
$actualType = Type::fromValue($value);
101+
// TODO: support for generics
102+
if (!$declaredType) {
103+
$this->properties[$name] = [$actualType, null];
104+
$this->updateTemplateClass($name);
105+
} elseif (!$declaredType->allows($actualType)) {
106+
$this->properties[$name] = [$declaredType->with($actualType), $phpDoc];
107+
$this->updateTemplateClass($name);
108+
}
109+
return $value;
110+
}
111+
112+
113+
private function findPropertyOwner(string $name): ?self
114+
{
115+
return match (true) {
116+
isset($this->properties[$name]) => $this,
117+
$this->parent !== null => $this->parent->findPropertyOwner($name),
118+
default => null,
119+
};
120+
}
121+
122+
123+
/********************* generator ****************d*g**/
124+
125+
126+
private function createTemplateClass(UI\Control $control): void
127+
{
128+
[$namespace, $shortName] = Helpers::splitClassName($this->className);
129+
$namespaceCode = $namespace ? PHP_EOL . "namespace $namespace;" . PHP_EOL : '';
130+
$fileName = dirname((new \ReflectionClass($control))->getFileName()) . '/' . $shortName . '.php';
131+
file_put_contents($fileName, <<<XX
132+
<?php
133+
134+
declare(strict_types=1);
135+
$namespaceCode
136+
use Nette\\Bridges\\ApplicationLatte\\Template;
137+
138+
class $shortName extends Template
139+
{
140+
}
141+
XX);
142+
require $fileName;
143+
}
144+
145+
146+
private function loadTemplateClass(): void
147+
{
148+
$rc = new \ReflectionClass($this->className);
149+
foreach ($rc->getProperties() as $prop) {
150+
if ($prop->getDeclaringClass() == $rc) { // intentionally ==
151+
$this->properties[$prop->getName()] = [Type::fromReflection($prop), $prop->getDocComment() ?: null];
152+
}
153+
}
154+
155+
$parent = $rc->getParentClass()->getName();
156+
if ($parent !== Template::class) {
157+
$this->parent = new self($this->getLatte(), $parent);
158+
}
159+
}
160+
161+
162+
private function updateTemplateClass(string $name): void
163+
{
164+
$rc = new \ReflectionClass($this->className);
165+
$content = FileSystem::read($rc->getFileName());
166+
[$type, $phpDoc] = $this->properties[$name];
167+
$type = $this->relativizeType((string) $type, $rc->getNamespaceName());
168+
$declaration = ($phpDoc ? $phpDoc . PHP_EOL : '') . "\tpublic $type \$$name";
169+
170+
// replace
171+
$content = preg_replace(
172+
'/^(?>\s*\/\*\*.*?\*\/)?\s*public\s+[^$;]*\s*\$' . $name . '\b/m',
173+
$declaration,
174+
$content,
175+
count: $count,
176+
);
177+
if (!$count) {
178+
if ($pos = strrpos($content, '}')) { // or append
179+
$content = substr_replace($content, $declaration . ';' . PHP_EOL, $pos, 0);
180+
} else {
181+
throw new \RuntimeException("Cannot update class file for {$this->className}, invalid syntax.");
182+
}
183+
}
184+
file_put_contents($rc->getFileName(), $content);
185+
}
186+
187+
188+
private function relativizeType(string $type, string $namespace): string
189+
{
190+
return preg_replace_callback(
191+
'~[\w\x7f-\xff\\\]+~',
192+
function ($m) use ($namespace) {
193+
$name = $m[0];
194+
return match (true) {
195+
Validators::isBuiltinType($name) => $name,
196+
str_starts_with($name, $namespace . '\\') => substr($name, strlen($namespace) + 1),
197+
default => '\\' . $name,
198+
};
199+
},
200+
$type,
201+
);
202+
}
203+
204+
205+
private function updateControlPhpDoc(UI\Control $control): void
206+
{
207+
$rc = new \ReflectionClass($control);
208+
$content = FileSystem::read($rc->getFileName());
209+
$doc = $rc->getDocComment();
210+
$nl = PHP_EOL;
211+
$annotation = '* @property-read ' . Helpers::splitClassName($this->className)[1] . ' $template';
212+
213+
if (!$doc) {
214+
$content = preg_replace(
215+
'/^((final\s+)?class\s+' . $rc->getShortName() . ')/m',
216+
"/**$nl $annotation$nl */$nl$1",
217+
$content,
218+
);
219+
} elseif (!preg_match('/@property(-read)?\s+.*\$template/', $doc)) {
220+
$newDoc = preg_replace('~(\s*)\*/\s*$~', "$1$annotation$0", $doc, 1);
221+
$content = str_replace($doc, $newDoc, $content);
222+
} else {
223+
return;
224+
}
225+
226+
file_put_contents($rc->getFileName(), $content);
227+
}
228+
229+
230+
private function updateTemplate(string $file): void
231+
{
232+
$content = FileSystem::read($file);
233+
if (!str_contains($content, '{templateType ')) {
234+
$content = '{templateType ' . $this->className . '}' . PHP_EOL . $content;
235+
file_put_contents($file, $content);
236+
}
237+
}
238+
239+
240+
/********************* template parameters ****************d*g**/
241+
242+
243+
public function getParameters(): array
244+
{
245+
return $this->params;
246+
}
247+
248+
249+
public function __set($name, $value): void
250+
{
251+
($this->findPropertyOwner($name) ?? $this)->ensureProperty($name, $value);
252+
$this->params[$name] = $value;
253+
}
254+
255+
256+
public function &__get($name)
257+
{
258+
if (!array_key_exists($name, $this->params)) {
259+
trigger_error("The variable '$name' does not exist in template.", E_USER_WARNING);
260+
}
261+
262+
return $this->params[$name];
263+
}
264+
265+
266+
public function __isset($name)
267+
{
268+
return isset($this->params[$name]);
269+
}
270+
271+
272+
public function __unset(string $name): void
273+
{
274+
unset($this->params[$name]);
275+
}
276+
}

0 commit comments

Comments
 (0)