pull/710/merge
fadrian06 3 weeks ago committed by GitHub
commit ebc928ec8a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -11,5 +11,5 @@ insert_final_newline = true
[*.md] [*.md]
indent_size = 2 indent_size = 2
[tests/views/*.php] [tests/views/**/*]
insert_final_newline = false insert_final_newline = false

@ -7,7 +7,7 @@
{ {
"name": "Mike Cao", "name": "Mike Cao",
"email": "mike@mikecao.com", "email": "mike@mikecao.com",
"homepage": "http://www.mikecao.com/", "homepage": "https://mikecao.com",
"role": "Original Developer" "role": "Original Developer"
}, },
{ {

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace flight\template;
abstract class Component
{
abstract public function html(): string;
public function css(): string
{
return '';
}
public function js(): string
{
return '';
}
}

@ -4,47 +4,59 @@ declare(strict_types=1);
namespace flight\template; namespace flight\template;
use Closure;
use Exception;
use ReflectionFunction;
/** /**
* The View class represents output to be displayed. It provides * The View class represents output to be displayed. It provides
* methods for managing view data and inserts the data into * methods for managing view data and inserts the data into
* view templates upon rendering. * view templates upon rendering.
* *
* @license MIT, http://flightphp.com/license * @license [MIT](https://docs.flightphp.com/license)
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com> * @copyright 2011 [Mike Cao](https://mikecao.com)
*/ */
class View class View
{ {
/** Location of view templates. */ /** Location of view templates */
public string $path; public string $path;
/** File extension. */ /** File extension */
public string $extension = '.php'; public string $extension = '.php';
public bool $preserveVars = true; public bool $preserveVars = true;
/** /** @var array<string, mixed> View variables */
* View variables.
*
* @var array<string, mixed> $vars
*/
protected array $vars = []; protected array $vars = [];
/** Template file. */ private string $componentPrefix;
private string $template; private string $componentsPath;
private int $fetchDepth = 0;
/** @var array<string, bool> */
private array $styles = [];
/** @var array<string, bool> */
private array $scripts = [];
/** /**
* Constructor.
*
* @param string $path Path to templates directory * @param string $path Path to templates directory
* @param string $componentPrefix Prefix for component tags.
* For example, if the prefix is `f`, then a component tag would look like `<f-component-name />`
* @param string $componentsPath Path to components directory.
* If is a relative path, it will be relative to the `$path` property.
* **We recomment that you always use absolute paths for explicitness**.
*/ */
public function __construct(string $path = '.') public function __construct(
{ string $path = ".",
string $componentPrefix = 'f',
string $componentsPath = 'components'
) {
$this->path = $path; $this->path = $path;
$this->componentPrefix = $componentPrefix;
$this->componentsPath = $componentsPath;
} }
/** /**
* Gets a template variable. * Gets a template variable
*
* @return mixed Variable value or `null` if doesn't exists * @return mixed Variable value or `null` if doesn't exists
*/ */
public function get(string $key) public function get(string $key)
@ -53,16 +65,14 @@ class View
} }
/** /**
* Sets a template variable. * Sets a template variable
* * @param string|array<string, mixed> $key
* @param string|iterable<string, mixed> $key
* @param mixed $value Value * @param mixed $value Value
* * @return $this
* @return self
*/ */
public function set($key, $value = null): self public function set($key, $value = null)
{ {
if (\is_iterable($key)) { if (is_array($key)) {
foreach ($key as $k => $v) { foreach ($key as $k => $v) {
$this->vars[$k] = $v; $this->vars[$k] = $v;
} }
@ -74,9 +84,8 @@ class View
} }
/** /**
* Checks if a template variable is set. * Checks if a template variable is set
* * @return bool If key exists and is not `null`
* @return bool If key exists
*/ */
public function has(string $key): bool public function has(string $key): bool
{ {
@ -84,11 +93,10 @@ class View
} }
/** /**
* Unsets a template variable. If no key is passed in, clear all variables. * Unsets a template variable. If no key is passed in, clear all variables
*
* @return $this * @return $this
*/ */
public function clear(?string $key = null): self public function clear(?string $key = null)
{ {
if ($key === null) { if ($key === null) {
$this->vars = []; $this->vars = [];
@ -100,104 +108,242 @@ class View
} }
/** /**
* Renders a template. * Renders a template
*
* @param string $file Template file * @param string $file Template file
* @param ?array<string, mixed> $templateData Template data * @param ?array<string, mixed> $templateData Template data
* * @throws Exception If template not found
* @throws \Exception If template not found
*/ */
public function render(string $file, ?array $templateData = null): void public function render(string $file, ?array $templateData = null): void
{ {
$this->template = $this->getTemplate($file); echo $this->fetch($file, $templateData);
if (!\file_exists($this->template)) {
$normalized_path = self::normalizePath($this->template);
throw new \Exception("Template file not found: {$normalized_path}.");
}
\extract($this->vars);
if (\is_array($templateData) === true) {
\extract($templateData);
if ($this->preserveVars === true) {
$this->vars = \array_merge($this->vars, $templateData);
}
}
include $this->template;
} }
/** /**
* Gets the output of a template. * Gets the output of a template
*
* @param string $file Template file * @param string $file Template file
* @param ?array<string, mixed> $data Template data * @param ?array<string, mixed> $data Template data
*
* @return string Output of template * @return string Output of template
*/ */
public function fetch(string $file, ?array $data = null): string public function fetch(string $file, ?array $data = null): string
{ {
\ob_start(); if ($this->fetchDepth === 0) {
$this->styles = [];
$this->scripts = [];
}
$this->fetchDepth++;
try {
$template = $this->getTemplate($file);
if (!$this->exists($file)) {
throw new Exception("Template file not found: $template.");
}
extract($this->vars);
if (is_array($data)) {
extract($data);
if ($this->preserveVars) {
$this->vars += $data;
}
}
ob_start();
$component = include $template;
switch (true) {
case is_callable($component):
$arguments = $this->getCallableArguments($component, $data);
$view = $component(...$arguments);
ob_end_clean();
break;
case $component instanceof Component:
$view = $component->html();
$css = $component->css();
$js = $component->js();
if ($css && !array_key_exists($template, $this->styles)) {
$view .= $this->renderComponentStyle($css);
$this->styles[$template] = true;
}
if ($js && !array_key_exists($template, $this->scripts)) {
$view .= $this->renderComponentScript($js);
$this->scripts[$template] = true;
}
ob_end_clean();
break;
default:
$view = ob_get_clean();
}
preg_match_all(
"/<$this->componentPrefix-(?<component>[a-z-]+)\s*(?<props>([a-z]+=\"[^\"]+\"\s*)*)?\s*\/>/",
$view,
$tagsMatches,
);
$tagsMatches = array_filter($tagsMatches);
$this->render($file, $data); foreach ($tagsMatches[0] ?? [] as $tagIndex => $match) {
$tag = $match;
$component = $tagsMatches['component'][$tagIndex];
$props = $tagsMatches['props'][$tagIndex] ?? '';
return \ob_get_clean(); preg_match_all(
'/(?<name>[a-z]+)="(?<value>[^"]+)"/',
$props,
$propsMatches,
);
$propsMatches = array_filter($propsMatches);
if ($propsMatches) {
$props = [];
foreach (array_keys($propsMatches[0]) as $propIndex) {
$name = $propsMatches['name'][$propIndex];
$value = $propsMatches['value'][$propIndex];
$props[$name] = $value;
}
} else {
$props = [];
}
$component = $this->fetch("$this->componentsPath/$component", $props);
$tagPosition = strpos($view, $tag);
if ($tagPosition === false) {
continue;
}
$view = substr_replace($view, $component, $tagPosition, strlen($tag));
}
return $view;
} finally {
$this->fetchDepth--;
}
} }
/** /**
* Checks if a template file exists. * Checks if a template file exists
*
* @param string $file Template file * @param string $file Template file
*
* @return bool Template file exists * @return bool Template file exists
*/ */
public function exists(string $file): bool public function exists(string $file): bool
{ {
return \file_exists($this->getTemplate($file)); return file_exists($this->getTemplate($file));
} }
/** /**
* Gets the full path to a template file. * Gets the full path to a template file
*
* @param string $file Template file * @param string $file Template file
*
* @return string Template file location * @return string Template file location
*/ */
public function getTemplate(string $file): string public function getTemplate(string $file): string
{ {
$ext = $this->extension; $fileDoesNotHaveExtension = substr($file, -strlen($this->extension)) !== $this->extension;
if (!empty($ext) && (\substr($file, -1 * \strlen($ext)) != $ext)) { if ($fileDoesNotHaveExtension) {
$file .= $ext; $file .= $this->extension;
} }
$is_windows = \strtoupper(\substr(PHP_OS, 0, 3)) === 'WIN'; $isLinuxAbsolutePath = $file[0] === '/';
$isWindowsAbsolutePath = PHP_OS === 'WINNT' && $file[1] === ':';
if ((\substr($file, 0, 1) === '/') || ($is_windows && \substr($file, 1, 1) === ':')) { if ($isLinuxAbsolutePath || $isWindowsAbsolutePath) {
return $file; return $this::normalizePath($file);
} }
return $this->path . DIRECTORY_SEPARATOR . $file; return $this::normalizePath("$this->path/$file");
} }
/** /**
* Displays escaped output. * Displays escaped output
*
* @param string $str String to escape * @param string $str String to escape
*
* @return string Escaped string * @return string Escaped string
*/ */
public function e(string $str): string public function e(string $str): string
{ {
$value = \htmlentities($str); $value = htmlentities($str);
echo $value; echo $value;
return $value; return $value;
} }
protected static function normalizePath(string $path, string $separator = DIRECTORY_SEPARATOR): string protected static function normalizePath(
string $path,
string $separator = DIRECTORY_SEPARATOR
): string {
return str_replace(['\\', '/'], $separator, $path);
}
/**
* @param callable $component
* @param ?array<string, mixed> $data
* @return array<int, mixed>
*/
private function getCallableArguments(callable $component, ?array $data): array
{
if (!is_array($data) || !$data) {
return [];
}
$arguments = [];
$remainingData = $data;
$reflection = new ReflectionFunction(Closure::fromCallable($component));
foreach ($reflection->getParameters() as $parameter) {
if ($parameter->isVariadic()) {
foreach ($remainingData as $value) {
$arguments[] = $value;
}
break;
}
$name = $parameter->getName();
if (array_key_exists($name, $remainingData)) {
$arguments[] = $remainingData[$name];
unset($remainingData[$name]);
}
}
return $arguments;
}
private function renderComponentStyle(string $css): string
{
if (preg_match('/^\s*<style\b[^>]*>.*<\/style>\s*$/is', $css) === 1) {
return $css;
}
return <<<html
<style>
$css
</style>
html;
}
private function renderComponentScript(string $js): string
{ {
return \str_replace(['\\', '/'], $separator, $path); if (preg_match('/^\s*<script\b[^>]*>.*<\/script>\s*$/is', $js) === 1) {
return $js;
}
return <<<html
<script>
$js
</script>
html;
} }
} }

@ -14,101 +14,83 @@ class ViewTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
$this->view = new View(); $this->view = new View(__DIR__ . '/views');
$this->view->path = __DIR__ . '/views';
} }
// Set template variables
public function testVariables(): void public function testVariables(): void
{ {
$this->view->set('test', 123); $this->view->set('test', 123);
$this->assertEquals(123, $this->view->get('test')); self::assertSame(123, $this->view->get('test'));
self::assertTrue($this->view->has('test'));
$this->assertTrue($this->view->has('test')); self::assertFalse($this->view->has('unknown'));
$this->assertTrue(!$this->view->has('unknown'));
$this->view->clear('test'); $this->view->clear('test');
$this->assertNull($this->view->get('test')); self::assertNull($this->view->get('test'));
} }
public function testMultipleVariables(): void public function testMultipleVariables(): void
{ {
$this->view->set([ $this->view->set(['test' => 123, 'foo' => 'bar']);
'test' => 123,
'foo' => 'bar'
]);
$this->assertEquals(123, $this->view->get('test')); self::assertSame(123, $this->view->get('test'));
$this->assertEquals('bar', $this->view->get('foo')); self::assertSame('bar', $this->view->get('foo'));
$this->view->clear(); $this->view->clear();
$this->assertNull($this->view->get('test')); self::assertNull($this->view->get('test'));
$this->assertNull($this->view->get('foo')); self::assertNull($this->view->get('foo'));
} }
// Check if template files exist
public function testTemplateExists(): void public function testTemplateExists(): void
{ {
$this->assertTrue($this->view->exists('hello.php')); self::assertTrue($this->view->exists('hello.php'));
$this->assertTrue(!$this->view->exists('unknown.php')); self::assertFalse($this->view->exists('unknown.php'));
} }
// Render a template
public function testRender(): void public function testRender(): void
{ {
$this->view->render('hello', ['name' => 'Bob']); $this->view->render('hello', ['name' => 'Bob']);
$this->expectOutputString('Hello, Bob!'); self::expectOutputString('Hello, Bob!');
} }
public function testRenderBadFilePath(): void public function testRenderBadFilePath(): void
{ {
$this->expectException(Exception::class);
$exception_message = sprintf( $exception_message = sprintf(
'Template file not found: %s%sviews%sbadfile.php', 'Template file not found: %s%sviews%sbadfile.php',
__DIR__, __DIR__,
DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR,
DIRECTORY_SEPARATOR DIRECTORY_SEPARATOR,
); );
$this->expectExceptionMessage($exception_message);
self::expectException(Exception::class);
self::expectExceptionMessage($exception_message);
$this->view->render('badfile'); $this->view->render('badfile');
} }
// Fetch template output
public function testFetch(): void public function testFetch(): void
{ {
$output = $this->view->fetch('hello', ['name' => 'Bob']); $output = $this->view->fetch('hello', ['name' => 'Bob']);
$this->assertEquals('Hello, Bob!', $output); self::assertSame('Hello, Bob!', $output);
} }
// Default extension
public function testTemplateWithExtension(): void public function testTemplateWithExtension(): void
{ {
$this->view->set('name', 'Bob'); $this->view->render('hello.php', ['name' => 'Bob']);
$this->view->render('hello.php'); self::expectOutputString('Hello, Bob!');
$this->expectOutputString('Hello, Bob!');
} }
// Custom extension
public function testTemplateWithCustomExtension(): void public function testTemplateWithCustomExtension(): void
{ {
$this->view->set('name', 'Bob'); self::expectOutputString('Hello world, Bob!');
$this->view->extension = '.html';
ob_start();
$this->view->render('world');
$html = ob_get_clean();
$html = str_replace(["\r\n", "\n"], '', $html);
echo $html;
$this->expectOutputString("Hello world, Bob!"); $this->view->extension = '.html';
$this->view->render('world', ['name' => 'Bob']);
} }
public function testGetTemplateAbsolutePath(): void public function testGetTemplateAbsolutePath(): void
@ -116,48 +98,64 @@ class ViewTest extends TestCase
$tmpfile = tmpfile(); $tmpfile = tmpfile();
$this->view->extension = ''; $this->view->extension = '';
$file_path = stream_get_meta_data($tmpfile)['uri']; $file_path = stream_get_meta_data($tmpfile)['uri'];
$this->assertEquals($file_path, $this->view->getTemplate($file_path));
self::assertSame($file_path, $this->view->getTemplate($file_path));
} }
public function testE(): void public function testE(): void
{ {
$this->expectOutputString('&lt;script&gt;'); $expectedString = '&lt;script&gt;';
self::expectOutputString($expectedString);
$result = $this->view->e('<script>'); $result = $this->view->e('<script>');
$this->assertEquals('&lt;script&gt;', $result);
self::assertSame($expectedString, $result);
} }
public function testeNoNeedToEscape(): void public function testeNoNeedToEscape(): void
{ {
$this->expectOutputString('script'); $expectedString = 'script';
$result = $this->view->e('script');
$this->assertEquals('script', $result); self::expectOutputString($expectedString);
$result = $this->view->e($expectedString);
self::assertSame($expectedString, $result);
} }
public function testNormalizePath(): void public function testNormalizePath(): void
{ {
$viewMock = new class extends View $viewMock = new class extends View
{ {
public static function normalizePath(string $path, string $separator = DIRECTORY_SEPARATOR): string public static function normalizePath(
{ string $path,
string $separator = DIRECTORY_SEPARATOR
): string {
return parent::normalizePath($path, $separator); return parent::normalizePath($path, $separator);
} }
}; };
$this->assertSame( self::assertSame(
'C:/xampp/htdocs/libs/Flight/core/index.php', 'C:/xampp/htdocs/libs/Flight/core/index.php',
$viewMock::normalizePath('C:\xampp\htdocs\libs\Flight/core/index.php', '/') $viewMock::normalizePath('C:\xampp\htdocs\libs\Flight/core/index.php', '/')
); );
$this->assertSame(
self::assertSame(
'C:\xampp\htdocs\libs\Flight\core\index.php', 'C:\xampp\htdocs\libs\Flight\core\index.php',
$viewMock::normalizePath('C:/xampp/htdocs/libs/Flight\core\index.php', '\\') $viewMock::normalizePath('C:/xampp/htdocs/libs/Flight\core\index.php', '\\')
); );
$this->assertSame(
self::assertSame(
'C:°xampp°htdocs°libs°Flight°core°index.php', 'C:°xampp°htdocs°libs°Flight°core°index.php',
$viewMock::normalizePath('C:/xampp/htdocs/libs/Flight\core\index.php', '°') $viewMock::normalizePath('C:/xampp/htdocs/libs/Flight\core\index.php', '°')
); );
} }
/** @dataProvider renderDataProvider */ /**
* @dataProvider renderDataProvider
* @param array{string, array<string, mixed>} $renderParams
*/
public function testDoesNotPreserveVarsWhenFlagIsDisabled( public function testDoesNotPreserveVarsWhenFlagIsDisabled(
string $output, string $output,
array $renderParams, array $renderParams,
@ -165,11 +163,11 @@ class ViewTest extends TestCase
): void { ): void {
$this->view->preserveVars = false; $this->view->preserveVars = false;
$this->expectOutputString($output); self::expectOutputString($output);
$this->view->render(...$renderParams); $this->view->render(...$renderParams);
set_error_handler(function (int $code, string $message) use ($regexp): void { set_error_handler(static function (int $code, string $message) use ($regexp): void {
$this->assertMatchesRegularExpression($regexp, $message); self::assertMatchesRegularExpression($regexp, $message);
}); });
$this->view->render($renderParams[0]); $this->view->render($renderParams[0]);
@ -179,17 +177,14 @@ class ViewTest extends TestCase
public function testKeepThePreviousStateOfOneViewComponentByDefault(): void public function testKeepThePreviousStateOfOneViewComponentByDefault(): void
{ {
$html = <<<'html' $html = self::removeLineEndings(<<<'html'
<div>Hi</div> <div>Hi</div>
<div>Hi</div> <div>Hi</div>
<input type="number" /> <input type="number" />
<input type="number" /> <input type="number" />
html; // phpcs:ignore html);
// if windows replace \n with \r\n
$html = str_replace(["\n", "\r"], '', $html);
$this->expectOutputString($html); self::expectOutputString($html);
$this->view->render('myComponent', ['prop' => 'Hi']); $this->view->render('myComponent', ['prop' => 'Hi']);
$this->view->render('myComponent'); $this->view->render('myComponent');
@ -200,17 +195,14 @@ class ViewTest extends TestCase
public function testKeepThePreviousStateOfDataSettedBySetMethod(): void public function testKeepThePreviousStateOfDataSettedBySetMethod(): void
{ {
$this->view->preserveVars = false; $this->view->preserveVars = false;
$this->view->set('prop', 'bar'); $this->view->set('prop', 'bar');
$html = <<<'html' $html = self::removeLineEndings(<<<'html'
<div>qux</div> <div>qux</div>
<div>bar</div> <div>bar</div>
html; // phpcs:ignore html);
$html = str_replace(["\n", "\r"], '', $html); self::expectOutputString($html);
$this->expectOutputString($html);
$this->view->render('myComponent', ['prop' => 'qux']); $this->view->render('myComponent', ['prop' => 'qux']);
$this->view->render('myComponent'); $this->view->render('myComponent');
@ -218,18 +210,15 @@ class ViewTest extends TestCase
public static function renderDataProvider(): array public static function renderDataProvider(): array
{ {
$html1 = <<<'html' $html1 = self::removeLineEndings(<<<'html'
<div>Hi</div> <div>Hi</div>
<div></div> <div></div>
html; // phpcs:ignore html);
$html2 = <<<'html' $html2 = self::removeLineEndings(<<<'html'
<input type="number" /> <input type="number" />
<input type="text" /> <input type="text" />
html; // phpcs:ignore html);
$html1 = str_replace(["\n", "\r"], '', $html1);
$html2 = str_replace(["\n", "\r"], '', $html2);
return [ return [
[ [
@ -244,4 +233,250 @@ class ViewTest extends TestCase
], ],
]; ];
} }
/** @dataProvider pagesDataProvider */
public function testItRendersComponent(
string $page,
string $expected,
array $props = []
): void {
$view = new View(__DIR__ . '/views');
$view->preserveVars = false;
$actual = $view->fetch("pages/$page", $props);
self::assertSame(
self::removeIndentation(self::removeLineEndings($expected)),
self::removeIndentation(self::removeLineEndings($actual)),
);
}
public function testItRendersComponentFromAnotherPath(): void
{
$view = new View(__DIR__ . '/views', 'f', __DIR__ . '/components');
$view->preserveVars = false;
$actual = $view->fetch('pages/page-with-component-from-another-path');
$expected = 'my-component-from-another-path';
self::assertSame(
self::removeIndentation(self::removeLineEndings($expected)),
self::removeIndentation(self::removeLineEndings($actual)),
);
}
public static function pagesDataProvider(): array
{
return [
[
'page-with-component-with-old-syntax',
<<<'html'
my-component
html,
],
[
'page-with-component-with-new-syntax',
<<<'html'
my-component
html,
],
[
'page-with-component-with-subcomponent',
<<<'html'
<div>
my-component-with-subcomponent
subcomponent
</div>
html,
],
[
'page-with-multiple-components',
<<<'html'
<ul>
<li>my-component</li>
<li>my-component</li>
</ul>
html,
],
[
'page-with-functional-component',
<<<'html'
my-functional-component
html,
],
[
'page-with-class-component',
<<<'html'
my-class-component
html,
],
[
'page-with-functional-component-with-props',
<<<'html'
functional-component-with-props: Astronaut Victoria
html,
['name' => 'Victoria', 'occupation' => 'Astronaut'],
],
[
'page-with-functional-component-with-individual-props',
<<<'html'
functional-component-with-individual-props: Astronaut Victoria
html,
['name' => 'Victoria', 'occupation' => 'Astronaut'],
],
[
'page-with-class-component-with-props',
<<<'html'
class-component-with-props: Astronaut Victoria
html,
['name' => 'Victoria', 'occupation' => 'Astronaut'],
],
[
'page-with-class-component-with-styles',
<<<'html'
<span class="my-class-component-with-styles">
my-class-component-with-styles
</span>
<style>
.my-class-component-with-styles {
color: red;
}
</style>
html,
],
[
'page-with-class-component-with-custom-style-tag',
<<<'html'
<span class="my-class-component-with-custom-style-tag">
my-class-component-with-custom-style-tag
</span>
<style media="print" data-component="my-class-component-with-custom-style-tag">
.my-class-component-with-custom-style-tag {
color: purple;
}
</style>
html,
],
[
'page-with-class-component-with-scripts',
<<<'html'
my-class-component-with-scripts
<script>console.log('my-class-component-with-scripts')</script>
html,
],
[
'page-with-class-component-with-custom-script-tag',
<<<'html'
my-class-component-with-custom-script-tag
<script type="module" data-component="my-class-component-with-custom-script-tag">
console.log('my-class-component-with-custom-script-tag')
</script>
html,
],
[
'page-with-class-component-that-extends-another-class-component',
<<<'html'
another-class-component extended by my-class-component-that-extends-another-class-component
html,
],
[
'page-with-component-with-one-prop',
<<<'html'
<html>
<body>
<h1>Hello, James</h1>
</body>
</html>
html,
['name' => 'James'],
],
[
'page-with-component-with-one-prop',
<<<'html'
<html>
<body>
<h1>Hello, Victoria</h1>
</body>
</html>
html,
['name' => 'Victoria'],
],
[
'page-with-component-with-two-props',
<<<'html'
<html>
<body>
<h1>Hello, Astronaut Victoria</h1>
</body>
</html>
html,
['name' => 'Victoria', 'occupation' => 'Astronaut'],
],
[
'page-with-three-different-components',
<<<'html'
my-component
my-functional-component
my-class-component
html,
],
[
'page-with-component-with-prop-which-value-contains-spaces-and-numbers',
<<<'html'
<h1>Hello, Constantine 1st the Great</h1>
html,
['name' => 'Constantine 1st the Great'],
],
[
'page-two-same-components-with-one-style-and-script-tag',
<<<'html'
<span class="my-class-component-with-styles">
my-class-component-with-styles
</span>
<style>
.my-class-component-with-styles {
color: red;
}
</style>
<span class="my-class-component-with-styles">
my-class-component-with-styles
</span>
my-class-component-with-scripts
<script>console.log('my-class-component-with-scripts')</script>
my-class-component-with-scripts
html,
],
];
}
/** @dataProvider prefixesDataProvider */
public function testRenderComponentsWithDifferentPrefixes(string $prefix): void
{
$view = new View(__DIR__ . '/views', $prefix);
$view->preserveVars = false;
$actual = $view->fetch('pages/page-with-component-with-custom-prefix', compact('prefix'));
$expected = 'my-component';
self::assertSame(
self::removeIndentation(self::removeLineEndings($expected)),
self::removeIndentation(self::removeLineEndings($actual)),
);
}
public static function prefixesDataProvider(): array
{
return [
['x'],
];
}
private static function removeLineEndings(string $subject): string
{
return str_replace(["\r", "\n"], '', $subject);
}
private static function removeIndentation(string $subject): string
{
return preg_replace('/\s{2,}/', '', $subject);
}
} }

@ -0,0 +1 @@
my-component-from-another-path

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace tests\views\components;
use flight\template\Component;
use Override;
if (!class_exists('AnotherClassComponent')) {
class AnotherClassComponent extends Component
{
#[Override]
public function html(): string
{
return 'another-class-component';
}
}
}

@ -0,0 +1 @@
<h1>Hello, <?= $occupation ?> <?= $name ?></h1>

@ -0,0 +1 @@
<h1>Hello, <?= $name ?></h1>

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
use tests\views\components\AnotherClassComponent;
require_once __DIR__ . '/another-class-component.php';
return new class extends AnotherClassComponent
{
#[Override]
public function html(): string
{
return parent::html() . ' extended by my-class-component-that-extends-another-class-component';
}
};

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
use flight\template\Component;
return new class extends Component
{
public function html(): string
{
return 'my-class-component-with-custom-script-tag';
}
public function js(): string
{
return <<<'js'
<script type="module" data-component="my-class-component-with-custom-script-tag">
console.log('my-class-component-with-custom-script-tag')
</script>
js;
}
};

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
use flight\template\Component;
return new class extends Component
{
public function html(): string
{
return <<<'html'
<span class="my-class-component-with-custom-style-tag">
my-class-component-with-custom-style-tag
</span>
html;
}
public function css(): string
{
return <<<'css'
<style media="print" data-component="my-class-component-with-custom-style-tag">
.my-class-component-with-custom-style-tag {
color: purple;
}
</style>
css;
}
};

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
use flight\template\Component;
return new class($name, $occupation) extends Component
{
private string $name;
private string $occupation;
public function __construct(string $name, string $occupation)
{
$this->name = $name;
$this->occupation = $occupation;
}
public function html(): string
{
return "class-component-with-props: $this->occupation $this->name";
}
};

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
use flight\template\Component;
return new class extends Component
{
#[Override]
public function html(): string
{
return 'my-class-component-with-scripts';
}
#[Override]
public function js(): string
{
return <<<'js'
console.log('my-class-component-with-scripts')
js;
}
};

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
use flight\template\Component;
return new class extends Component
{
#[Override]
public function html(): string
{
return <<<'html'
<span class="my-class-component-with-styles">
my-class-component-with-styles
</span>
html;
}
#[Override]
public function css(): string
{
return <<<'css'
.my-class-component-with-styles {
color: red;
}
css;
}
};

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
use flight\template\Component;
return new class extends Component
{
#[Override]
public function html(): string
{
return 'my-class-component';
}
};

@ -0,0 +1,4 @@
<div>
my-component-with-subcomponent
<f-subcomponent />
</div>

@ -0,0 +1,6 @@
<?php
declare(strict_types=1);
return fn (string $name, string $occupation): string =>
"functional-component-with-individual-props: $occupation $name";

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
return fn (...$props): string => sprintf(
'functional-component-with-props: %s %s',
$props[1] ?? '',
$props[0] ?? ''
);

@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
return fn (): string => 'my-functional-component';

@ -1 +1 @@
Hello, <?php echo $name; ?>! Hello, <?= $name ?>!

@ -0,0 +1,5 @@
<f-my-class-component-with-styles />
<f-my-class-component-with-styles />
<f-my-class-component-with-scripts />
<f-my-class-component-with-scripts />

@ -0,0 +1 @@
<f-my-class-component-that-extends-another-class-component />

@ -0,0 +1 @@
<f-my-class-component-with-custom-script-tag />

@ -0,0 +1 @@
<f-my-class-component-with-custom-style-tag />

@ -0,0 +1 @@
<f-my-class-component-with-props name="<?= $name ?>" occupation="<?= $occupation ?>" />

@ -0,0 +1 @@
<f-my-class-component-with-scripts />

@ -0,0 +1 @@
<f-my-class-component-with-styles />

@ -0,0 +1 @@
<f-my-component-from-another-path />

@ -0,0 +1 @@
<?php $this->render('components/my-component') ?>

@ -0,0 +1,5 @@
<html>
<body>
<f-greeting name="<?= $name ?>" />
</body>
</html>

@ -0,0 +1 @@
<f-my-component-with-subcomponent />

@ -0,0 +1,5 @@
<html>
<body>
<f-greeting-with-two-props name="<?= $name ?>" occupation="<?= $occupation ?>" />
</body>
</html>

@ -0,0 +1 @@
<f-my-functional-component-with-individual-props occupation="<?= $occupation ?>" name="<?= $name ?>" />

@ -0,0 +1 @@
<f-my-functional-component-with-props name="<?= $name ?>" occupation="<?= $occupation ?>" />

@ -0,0 +1,7 @@
<ul>
<?php for ($i = 1; $i <= 2; ++$i) : ?>
<li>
<f-my-component />
</li>
<?php endfor ?>
</ul>

@ -0,0 +1,3 @@
<f-my-component />
<f-my-functional-component />
<f-my-class-component />

@ -1 +1 @@
Hello world, <?php echo $name; ?>! Hello world, <?= $name ?>!
Loading…
Cancel
Save