diff --git a/composer.json b/composer.json index 9845103..7adee85 100644 --- a/composer.json +++ b/composer.json @@ -1,52 +1,56 @@ { - "name": "league/plates", - "description": "Plates, the native PHP template system that's fast, easy to use and easy to extend.", - "keywords": [ - "league", - "package", - "templating", - "templates", - "views" - ], - "homepage": "https://platesphp.com", - "license": "MIT", - "authors" : [ - { - "name": "Jonathan Reinink", - "email": "jonathan@reinink.ca", - "role": "Developer" - }, - { - "name": "RJ Garcia", - "email": "ragboyjr@icloud.com", - "role": "Developer" - } - ], - "require" : { - "php": "^7.0|^8.0" + "name": "league/plates", + "description": "Plates, the native PHP template system that's fast, easy to use and easy to extend.", + "keywords": [ + "league", + "package", + "templating", + "templates", + "views" + ], + "homepage": "https://platesphp.com", + "license": "MIT", + "authors": [ + { + "name": "Jonathan Reinink", + "email": "jonathan@reinink.ca", + "role": "Developer" }, - "require-dev": { - "mikey179/vfsstream": "^1.6", - "phpunit/phpunit": "^9.5", - "squizlabs/php_codesniffer": "^3.5" - }, - "autoload": { - "psr-4": { - "League\\Plates\\": "src" - } - }, - "autoload-dev": { - "psr-4": { - "League\\Plates\\Tests\\": "tests" - } - }, - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "scripts": { - "test": "phpunit --testdox --colors=always", - "docs": "hugo -s doc server" + { + "name": "RJ Garcia", + "email": "ragboyjr@icloud.com", + "role": "Developer" + } + ], + "require": { + "php": "^7.0|^8.0", + "symfony/var-dumper": "^6.3" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.5", + "rector/rector": "^0.18.6", + "phpstan/phpstan": "^1.10" + }, + "autoload": { + "psr-4": { + "League\\Plates\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "League\\Plates\\Tests\\": "tests", + "Templates\\": "exampleTemplateClass/Templates" + } + }, + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" } + }, + "scripts": { + "test": "phpunit --testdox --colors=always", + "docs": "hugo -s doc server" + } } diff --git a/exampleTemplateClass/Templates/Layout.php b/exampleTemplateClass/Templates/Layout.php new file mode 100644 index 0000000..3b34bc2 --- /dev/null +++ b/exampleTemplateClass/Templates/Layout.php @@ -0,0 +1,35 @@ + + + + <?=$tpl->e($title)?> | <?=$tpl->e($company)?> + + + +section('content')?> + +section('scripts')?> + + + + +layout(new Layout('User Profile')) ?> +layout('layout', ['title' => 'User Profile']) // this is working too and will get the example/templates/layout.php ?> + +

User Profile

+

Hello, !

+ +insert(new Sidebar()) ?> + + +push('scripts') ?> + +end() ?> + + +addData(['company' => 'The Company Name'], Layout::class); + +// Render a template +echo $templates->render(new Profile('Jonathan')); diff --git a/exampleTemplateClass/rector.php b/exampleTemplateClass/rector.php new file mode 100644 index 0000000..97882b6 --- /dev/null +++ b/exampleTemplateClass/rector.php @@ -0,0 +1,12 @@ +rule(RectorizeTemplate::class); +}; + +// vendor/bin/rector process exampleTemplateClass/Templates --config ./exampleTemplateClass/rector.php --debug +// vendor/bin/rector process exampleTemplateClass/Templates --config ./vendor/league/plates/exampleTemplateClass/rector.php diff --git a/src/Engine.php b/src/Engine.php index c889c5e..54265e8 100644 --- a/src/Engine.php +++ b/src/Engine.php @@ -12,6 +12,8 @@ use League\Plates\Template\Name; use League\Plates\Template\ResolveTemplatePath; use League\Plates\Template\Template; +use League\Plates\Template\TemplateClass; +use League\Plates\Template\TemplateClassInterface; use League\Plates\Template\Theme; /** @@ -282,20 +284,22 @@ public function exists($name) /** * Create a new template. - * @param string $name - * @param array $data + * @param string|TemplateClassInterface $name + * @param array $data * @return Template */ public function make($name, array $data = array()) { - $template = new Template($this, $name); + + $template = $name instanceof TemplateClassInterface ? new TemplateClass($this, $name) + : new Template($this, $name); $template->data($data); return $template; } /** * Create a new template and render it. - * @param string $name + * @param string|TemplateClassInterface $name * @param array $data * @return string */ diff --git a/src/RectorizeTemplate.php b/src/RectorizeTemplate.php new file mode 100644 index 0000000..04f57c2 --- /dev/null +++ b/src/RectorizeTemplate.php @@ -0,0 +1,248 @@ +> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + $implementedInterfaces = array_map(static fn (Name $interface): string => $interface->toString(), $node->implements); + if (! \in_array(TemplateClassInterface::class, $implementedInterfaces, true)) { + return null; + } + + $displayMethod = $node->getMethod('display'); + if (null === $displayMethod) { + return null; + } + + if ([] === $displayMethod->params) { + return $this->removeConstructor($node); + } + + $paramsForConstructor = []; + $docBlockForConstructor = []; + $methodDocBlock = $displayMethod?->getDocComment()?->getText() ?? ''; + foreach ($displayMethod->params as $parameter) { + if (in_array($this->getName($parameter), self::PARAMETER_NAMES_TO_NOT_ADD, true)) { + continue; + } + + $paramType = $this->paramTypeResolver->resolve($parameter); + if ($paramType instanceof FullyQualifiedObjectType && ! $this->mustAddObjectInConstructor($parameter, $paramType)) { + continue; + } + + $paramDocBlock = $this->getParameterDocblock($methodDocBlock, $this->getName($parameter)); + if (null !== $paramDocBlock) { + $docBlockForConstructor[] = $paramDocBlock; + } + + $cloneParameter = clone $parameter; + $cloneParameter->flags = 1; + + if ($parameter->default instanceof Expr + && (! $this->hasNullType($paramType) + && $parameter->default instanceof ConstFetch + && $this->hasNullValue($parameter))) { + $this->addTypeToParameter($cloneParameter, 'null'); + } + + $paramsForConstructor[] = $cloneParameter; + } + + if ([] === $paramsForConstructor) { + return $this->removeConstructor($node); + } + + $constructor = $node->getMethod(MethodName::CONSTRUCT); + $docBlockConstructor = $constructor?->getDocComment(); + $newDocBlockConstructor = new Doc("/**\n * ".trim("Autogenerated constructor.\n * \n * ".implode("\n * ", $docBlockForConstructor), "\n *")."\n */"); + if ($paramsForConstructor === $constructor?->params || $docBlockConstructor === $newDocBlockConstructor) { // TODO compare docblock + return null; + } + + $this->removeConstructor($node); + + $constructor = new ClassMethod('__construct', [ + 'flags' => Class_::MODIFIER_PUBLIC, + 'params' => $paramsForConstructor, + ]); + $constructor->setDocComment($newDocBlockConstructor); + + $node->stmts[] = $constructor; + + return $node; + } + + private function hasNullType(Type $type): bool + { + if ($type instanceof NullType) { + return true; + } + + if ($type instanceof \PHPStan\Type\UnionType) { + foreach ($type->getTypes() as $type) { + if ($this->hasNullType($type)) { + return true; + } + } + } + + return false; + } + + private function hasNullValue(Param $param): bool + { + $text = (new BetterStandardPrinter())->print($param); + + return str_ends_with($text, 'null'); + } + + private function removeConstructor(Class_ $class): null + { + foreach ($class->stmts as $key => $stmt) { + if ($stmt instanceof ClassMethod && MethodName::CONSTRUCT === $stmt->name->toString()) { + unset($class->stmts[$key]); + + break; + } + } + + return null; + } + + private function getParameterDocblock(?string $methodDocblock, string $parameterName): ?string + { + if (null === $methodDocblock) { + return null; + } + + // Regular expression to match a docblock for a specific parameter + $pattern = '/@param\s+[^$]*?\$'.$parameterName.'\b([^@]*)/'; + + $patternIsFound = preg_match($pattern, $methodDocblock, $matches); + + if ($patternIsFound) { + return trim($matches[0], '*/ '."\n"); + } + + return null; + } + + private function mustAddObjectInConstructor(Param $param, FullyQualifiedObjectType $paramType): bool + { + if (\in_array($paramType->getClassName(), self::CLASS_TO_NOT_ADD_IN_CONSTRUCTOR, true)) { + return false; + } + + foreach (self::CLASS_TO_NOT_ADD_IN_CONSTRUCTOR as $classToCheck) { + if (is_subclass_of($paramType->getClassName(), $classToCheck)) { + return false; + } + } + + return true; + } + + private function addTypeToParameter(Param $param, string $type): void + { + $existingType = $this->nodeTypeResolver->getType($param); + + if ($existingType instanceof \PHPStan\Type\UnionType) { + $existingTypes = $existingType->getTypes(); + $newTypes = [...$existingTypes, new NullType()]; + $param->type = new UnionType($newTypes); + + return; + } + + if (null === $existingType) { + $param->type = new NullType(); + + return; + } + + $param->type = null; + // The following code is not working + // $param->type = new UnionType([$existingType, new \PHPStan\Type\NullType()]); + // Get it worked using nullable_type_declaration_for_default_null_value for phpcsfixer + } +} diff --git a/src/Template/DoNotAddItInConstructorInterface.php b/src/Template/DoNotAddItInConstructorInterface.php new file mode 100644 index 0000000..d82d522 --- /dev/null +++ b/src/Template/DoNotAddItInConstructorInterface.php @@ -0,0 +1,7 @@ +data($data); - $path = ($this->engine->getResolveTemplatePath())($this->name); try { $level = ob_get_level(); ob_start(); - - (function() { - extract($this->data); - include func_get_arg(0); - })($path); - + $this->display(); $content = ob_get_clean(); if (isset($this->layoutName)) { @@ -187,9 +181,19 @@ public function render(array $data = array()) } } + + protected function display() { + $path = ($this->engine->getResolveTemplatePath())($this->name); + + (function() { + extract($this->data); + include func_get_arg(0); + })($path); + } + /** * Set the template's layout. - * @param string $name + * @param string|TemplateClassInterface $name * @param array $data * @return null */ @@ -307,7 +311,7 @@ public function section($name, $default = null) /** * Fetch a rendered template. - * @param string $name + * @param string|TemplateClassInterface $name * @param array $data * @return string */ @@ -318,7 +322,7 @@ public function fetch($name, array $data = array()) /** * Output a rendered template. - * @param string $name + * @param string|TemplateClassInterface $name * @param array $data * @return null */ diff --git a/src/Template/TemplateClass.php b/src/Template/TemplateClass.php new file mode 100644 index 0000000..39e63e7 --- /dev/null +++ b/src/Template/TemplateClass.php @@ -0,0 +1,132 @@ +engine = $engine; + $name = $templateClass::class; + + $this->data($this->engine->getData($name)); // needed for addData, too much magic, deprecate it ?! + } + + protected function display() { + + $this->mergePropertyToData(); + $this->autowireDataToTemplateClass(); + + $vars = $this->getVarToAutowireDisplayMethod(); + $this->templateClass->display(...$vars); + } + + protected function mergePropertyToData(): void + { + $properties = (new ReflectionClass($this->templateClass))->getProperties(ReflectionProperty::IS_PUBLIC); + + $dataToImport = []; + foreach ($properties as $property) { + if (!$property->isInitialized($this->templateClass)) // $property->isReadOnly() && + continue; + + $propertyValue = $property->getValue($this->templateClass); + if ($propertyValue === $property->getDefaultValue() || $propertyValue === null) + continue; + + $dataToImport[$property->getName()] = $propertyValue; + } + + if ($dataToImport !== []) { + $this->data($dataToImport); + } + + } + + protected function getVarToAutowireDisplayMethod(): array + { + $displayReflection = new ReflectionMethod($this->templateClass, 'display'); + + $parameters = $displayReflection->getParameters(); + + // Extract the parameter names + $parametersToAutowire = []; + foreach ($parameters as $parameter) { + + if ($parameter->getType() instanceof ReflectionNamedType // avoid union or intersection type + && in_array($parameter->getType()->getName(), [TemplateClass::class, Template::class], true)) { + $parametersToAutowire[$parameter->getName()] = $this; + + continue; + } + + if ($parameter->getName() === 'f') { + $parametersToAutowire['f'] = $this->fetch(...); + + continue; + } + + + if ($parameter->getName() === 'e') { + $parametersToAutowire['e'] = $this->escape(...); + + continue; + } + + $parametersToAutowire[$parameter->getName()] = $this->data()[$parameter->getName()] ?? $parameter->getDefaultValue() ?? null; + } + + return $parametersToAutowire; + + } + + protected function autowireDataToTemplateClass() + { + $properties = (new ReflectionClass($this->templateClass))->getProperties(); + + foreach ($properties as $property) { + if ($property->isInitialized($this->templateClass)) { + continue; + } + + if ($property->getName() === 'template') { + $this->templateClass->template = $this; + continue; + } + + if (isset($this->data[$property->getName()])) { + $this->templateClass->{$property->getName()} = $this->data[$property->getName()]; + } + } + } + + /** Disable useless public/protected parent method and property */ + + /** + * @var Name Useless here + */ + protected $name; + + public function exists(): bool + { + return true; + } + + public function path(): string + { + return get_class($this->templateClass); + } +} diff --git a/src/Template/TemplateClassInterface.php b/src/Template/TemplateClassInterface.php new file mode 100644 index 0000000..f516ad0 --- /dev/null +++ b/src/Template/TemplateClassInterface.php @@ -0,0 +1,8 @@ +