Skip to content

Commit

Permalink
👔 up: enhance the array key path expression parse
Browse files Browse the repository at this point in the history
- update the simple template render logic
  • Loading branch information
inhere committed Mar 23, 2024
1 parent effc043 commit f3899cd
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 76 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.1, 8.2]
php: [8.1, 8.2, 8.3]
os: [ubuntu-latest, macOS-latest] # windows-latest,
# include: # will not testing on php 7.2
# - os: 'ubuntu-latest'
Expand Down
25 changes: 12 additions & 13 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" backupStaticAttributes="false"
bootstrap="test/bootstrap.php" colors="false" convertErrorsToExceptions="true" convertNoticesToExceptions="true"
convertWarningsToExceptions="true" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage>
<include>
<directory suffix=".php">app</directory>
</include>
</coverage>
<testsuites>
<testsuite name="Library Test Suite">
<directory>test/</directory>
</testsuite>
</testsuites>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="test/bootstrap.php" colors="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.4/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<coverage/>
<testsuites>
<testsuite name="Library Test Suite">
<directory>test/</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">app</directory>
</include>
</source>
</phpunit>
55 changes: 39 additions & 16 deletions src/Compiler/CompileUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PhpPkg\EasyTpl\Compiler;

use function defined;
use function preg_match;
use function str_contains;
use function str_starts_with;

Expand Down Expand Up @@ -53,42 +54,64 @@ public static function canAddVarPrefix(string $line): bool
*/
private static ?string $matchKeyPath = null;

private const ONLY_PATH_PATTERN = '/^\$?[a-zA-Z_][\w.-]+\w$/';

/**
* convert access array key path to php array access expression.
*
* - convert $ctx.top.sub to $ctx['top']['sub']
* - convert `$ctx.top.sub` to `$ctx['top']['sub']`
* - convert `ctx.top.sub` to `$ctx['top']['sub']`
*
* @param string $line var line. must start with $
* @param string $line var line. NOT: line must start with $
*
* @return string
*/
public static function toArrayAccessStmt(string $line): string
{
if (self::$matchKeyPath === null) {
// - convert $ctx.top.sub to $ctx['top']['sub']
$hasSpace = str_contains($line, ' ');

// only key path.
if (!$hasSpace && preg_match(self::ONLY_PATH_PATTERN, $line) === 1) {
return self::handleMatch($line);
}

// with space and key path at first node. like: ctx.top.sub ?? "fallback"
if ($hasSpace) {
[$first, $last] = explode(' ', $line, 2);
if (preg_match(self::ONLY_PATH_PATTERN, $first) === 1 && !str_contains($last, '.')) {
return self::handleMatch($first) . " $last";
}
}

// with fallback statement. like: $ctx.top.sub ?? "fallback"
if (!self::$matchKeyPath) {
self::$matchKeyPath = '~(' . implode(')|(', [
'\$[\w.-]+\w', // array key path.
]) . ')~';
}

// https://www.php.net/manual/zh/reference.pcre.pattern.modifiers.php
return preg_replace_callback(self::$matchKeyPath, static function (array $matches) {
return preg_replace_callback(self::$matchKeyPath, static function (array $matches) use ($line) {
$varName = $matches[0];
// convert $ctx.top.sub to $ctx[top][sub]
if (str_contains($varName, '.')) {
$nodes = [];
foreach (explode('.', $varName) as $key) {
if ($key[0] === '$') {
$nodes[] = $key;
} else {
$nodes[] = is_numeric($key) ? "[$key]" : "['$key']";
}
}

$varName = implode('', $nodes);
return self::handleMatch($varName);
}

return $varName;
}, $line);
}

private static function handleMatch(string $varName): string
{
$nodes = [];
foreach (explode('.', $varName) as $i => $key) {
if ($i === 0) {
$nodes[] = $key[0] === '$' ? $key : '$' . $key;
} else {
$nodes[] = is_numeric($key) ? "[$key]" : "['$key']";
}
}

return implode('', $nodes);
}
}
5 changes: 3 additions & 2 deletions src/Compiler/PregCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,9 @@ public function parseCodeBlock(string $trimmed): string

// handle quick access array key.
// - convert $ctx.top.sub to $ctx['top']['sub']
$trimmed = CompileUtil::toArrayAccessStmt($trimmed);

if (str_contains($trimmed, '.')) {
$trimmed = CompileUtil::toArrayAccessStmt($trimmed);
}
return $open . $trimmed . $close;
}

Expand Down
5 changes: 4 additions & 1 deletion src/EasyTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,10 @@ public static function newRaw(array $config = []): self
/**
* Class constructor.
*
* @param array{tmpDir: string, compiler: class-string|CompilerInterface} $config
* @param array{
* tmpDir: string,
* tplDir: string,
* compiler: class-string|CompilerInterface } $config
*/
public function __construct(array $config = [])
{
Expand Down
18 changes: 5 additions & 13 deletions src/SimpleTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,11 @@ public function renderString(string $tplCode, array $tplVars = []): string
*/
protected function renderVars(string $tplCode, array $vars): string
{
$fmtVars = Arr::flattenMap($vars, Arr\ArrConst::FLAT_DOT_JOIN_INDEX);

// convert array value to string.
foreach ($vars as $name => $val) {
if (is_array($val)) {
$fmtVars[$name] = Arr::toStringV2($val);
}
}

if (!$this->pattern) {
$this->parseFormat($this->format);
}

return preg_replace_callback($this->pattern, function (array $match) use ($fmtVars) {
return preg_replace_callback($this->pattern, function (array $match) use ($vars) {
$var = trim($match[1]);
if (!$var) {
return $match[0];
Expand All @@ -137,9 +128,10 @@ protected function renderVars(string $tplCode, array $vars): string
[$var, $filters] = Str::explode($var, '|', 2);
}

if (isset($fmtVars[$var])) {
$val = $fmtVars[$var];
return $filters ? $this->pipeFilter->applyStringRules($val, $filters) : $val;
$value = Arr::getByPath($vars, $var);
if ($value !== null) {
$value = is_array($value) ? Arr::toStringV2($value) : (string)$value;
return $filters ? $this->pipeFilter->applyStringRules($value, $filters) : $value;
}

return $match[0];
Expand Down
22 changes: 7 additions & 15 deletions src/TextTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,21 @@

namespace PhpPkg\EasyTpl;

use PhpPkg\EasyTpl\Contract\CompilerInterface;

/**
* class TextTemplate
* - will disable echo filter for default.
*
* @author inhere
* @deprecated please use {@see EasyTemplate::textTemplate() }
*/
class TextTemplate extends EasyTemplate
class TextTemplate
{
/**
* @var string[]
* @param array $config
*
* @return EasyTemplate
*/
protected array $allowExt = ['.php', '.tpl'];

/**
* @param CompilerInterface $compiler
*/
protected function initCompiler(CompilerInterface $compiler): void
public static function new(array $config = []): EasyTemplate
{
parent::initCompiler($compiler);

// use raw echo for text template
$compiler->disableEchoFilter();
return EasyTemplate::newTexted($config);
}
}
23 changes: 22 additions & 1 deletion test/Compiler/CompileUtilTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,28 @@ public function testCanAddVarPrefix(): void

public function testPathToArrayAccess(): void
{
$this->assertEquals('ctx.top.sub', CompileUtil::toArrayAccessStmt('ctx.top.sub'));
$this->assertEquals('$varName', CompileUtil::toArrayAccessStmt('varName'));
$this->assertEquals("= varName", CompileUtil::toArrayAccessStmt('= varName'));

$this->assertEquals("\$ctx['top']['sub']", CompileUtil::toArrayAccessStmt('ctx.top.sub'));
$this->assertEquals("\$ctx['top']['sub']", CompileUtil::toArrayAccessStmt('$ctx.top.sub'));

// with fallback statement
$ret = <<<EOT
\$ctx['top']['sub'] ?? "fallback"
EOT;

$this->assertEquals($ret, CompileUtil::toArrayAccessStmt('ctx.top.sub ?? "fallback"'));
$this->assertEquals($ret, CompileUtil::toArrayAccessStmt('$ctx.top.sub ?? "fallback"'));

$ret = <<<EOT
= \$ctx['top']['sub'] ?? "fallback"
EOT;
$this->assertEquals($ret, CompileUtil::toArrayAccessStmt('= $ctx.top.sub ?? "fallback"'));

$ret = <<<EOT
= \$ctx['top']['sub'] ?? "fall.back"
EOT;
$this->assertEquals($ret, CompileUtil::toArrayAccessStmt('= $ctx.top.sub ?? "fall.back"'));
}
}
53 changes: 39 additions & 14 deletions test/EasyTemplateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public function testRenderFile_use_all_token(): void
$t = $this->newTemplate();

$tplFile = $this->getTestTplFile('testdata/use_all_token.tpl');
$result = $t->renderFile($tplFile, $this->tplVars);
$result = $t->renderFile($tplFile, $this->tplVars);

$this->assertNotEmpty($result);
$this->assertStringNotContainsString('{{', $result);
Expand All @@ -102,16 +102,41 @@ public function testRenderFile_use_all_token(): void

public function testRender_array_value_use_keyPath(): void
{
$t = new EasyTemplate();
$t = $this->newTemplate();
// inline
$code = '
{{= $ctx.pkgName ?? "org.example.entity" }}
';

$tplVars = ['ctx' => ['pkgName' => 'MyPKG']];
$result = $t->renderString($code, $tplVars);
// vdump($result);
$tplVars = ['str' => 'hello', 'ctx' => ['pkgName' => 'MyPKG']];
$result = $t->renderString($code, $tplVars);
$this->assertEquals("\nMyPKG\n", $result);

$code = '{{ $ctx.pkgName }}';
$result = $t->renderString($code, $tplVars);
$this->assertStringContainsString('MyPKG', $result);

$tests = [
['tpl' => '{{= $str }}', 'result' => 'hello'],
['tpl' => '{{ str }}', 'result' => 'hello'],
['tpl' => '{{= $ctx.pkgName ?? "org.example.entity" }}', 'result' => 'MyPKG'],
['tpl' => '{{ $ctx.pkgName ?? "org.example.entity" }}', 'result' => 'MyPKG'],
// ['tpl' => '{{ ctx.pkgName ?? "org.example.entity" }}', 'result' => 'MyPKG'],
['tpl' => '{{ $ctx.pkgName }}', 'result' => 'MyPKG'],
['tpl' => '{{ ctx.pkgName }}', 'result' => 'MyPKG'],
['tpl' => '{{ $ctx.notExist ?? "fallback" }}', 'result' => 'fallback'],
['tpl' => '{{ ctx.notExist ?? "fallback" }}', 'result' => 'fallback'],
];
foreach ($tests as $test) {
$result = $t->renderString($test['tpl'], $test['vars'] ?? $tplVars);
$this->assertStringContainsString($test['result'], $result);
}

$e = $this->runAndGetException(function () use ($t, $tplVars) {
$tpl = '{{ ctx.pkgName ?? "org.example.entity" }}';
$t->renderString($tpl, $tplVars);
});
$this->assertStringContainsString('Undefined constant "ctx"', $e->getMessage());
}

public function testPhpFuncAsFilter_compile_render(): void
Expand All @@ -127,21 +152,21 @@ public function testAddFilters_compile_render(): void
{
$t = new EasyTemplate();
$t->addFilters([
'upper' => 'strtoupper',
'upper' => 'strtoupper',
'myFilter' => function (string $str) {
return substr($str, 3);
},
]);

$code = '{{ $name | upper }}';
$out = '<?= htmlspecialchars((string)strtoupper($name)) ?>';
$out = '<?= htmlspecialchars((string)strtoupper($name)) ?>';
$this->assertEquals($out, $t->compileCode($code));
$this->assertEquals('INHERE', $t->renderString($code, [
'name' => 'inhere',
]));

$code = '{{ $name | myFilter }}';
$out = <<<'CODE'
$out = <<<'CODE'
<?= htmlspecialchars((string)$this->applyFilter('myFilter', $name)) ?>
CODE;
$this->assertEquals($out, $t->compileCode($code));
Expand All @@ -150,7 +175,7 @@ public function testAddFilters_compile_render(): void
]));

$code = '{{ $name | upper | myFilter }}';
$out = <<<'CODE'
$out = <<<'CODE'
<?= htmlspecialchars((string)$this->applyFilter('myFilter', strtoupper($name))) ?>
CODE;
$this->assertEquals($out, $t->compileCode($code));
Expand All @@ -167,12 +192,12 @@ public function testAddFilter_setFilterSep(): void

// bad
$code = '{{ $name |upper }}';
$out = '<?= $name |upper ?>';
$out = '<?= $name |upper ?>';
$this->assertEquals($out, $t->compileCode($code));

// goods
$code = '{{ $name | upper }}';
$out = '<?= htmlspecialchars((string)strtoupper($name)) ?>';
$out = '<?= htmlspecialchars((string)strtoupper($name)) ?>';
$this->assertEquals($out, $t->compileCode($code));
}

Expand All @@ -186,7 +211,7 @@ public function testRender_define(): void
}}
{{ $name | upper }}
CODE;
$result = $t->renderString($tplCode);
$result = $t->renderString($tplCode);
$this->assertNotEmpty($result);
$this->assertEquals('INHERE', $result);
}
Expand Down Expand Up @@ -255,13 +280,13 @@ public function testRender_include_file(): void

public function testEasy_textTemplate(): void
{
$t = $this->newTexted();
$t = $this->newTexted();
$vs = [
'name' => 'inhere',
];

$code = '{{ $name | upper }}';
$out = '<?= strtoupper($name) ?>';
$out = '<?= strtoupper($name) ?>';
$this->assertEquals($out, $t->compileCode($code));
$this->assertEquals('INHERE', $t->renderString($code, $vs));

Expand Down

0 comments on commit f3899cd

Please sign in to comment.