From 9f84ba63e4617f318340eb4121ce12c27ed2f1ed Mon Sep 17 00:00:00 2001 From: Inhere Date: Mon, 2 Jan 2023 22:17:56 +0800 Subject: [PATCH] feat: new support extends, block renderer tempalte --- src/Extra/ExtendTemplate.php | 256 ++++++++++++++++++++++ src/Extra/LayoutTemplate.php | 71 ++++++ test/Extra/ExtendTemplateTest.php | 82 +++++++ test/testdata/extends/home.php | 12 - test/testdata/extends/home.tpl | 4 +- test/testdata/extends/layouts/layout.php | 20 -- test/testdata/extends/layouts/layout.tpl | 2 +- test/testdata/layout/layouts/layout01.tpl | 0 8 files changed, 412 insertions(+), 35 deletions(-) create mode 100644 src/Extra/ExtendTemplate.php create mode 100644 src/Extra/LayoutTemplate.php create mode 100644 test/Extra/ExtendTemplateTest.php delete mode 100644 test/testdata/extends/home.php delete mode 100644 test/testdata/extends/layouts/layout.php create mode 100644 test/testdata/layout/layouts/layout01.tpl diff --git a/src/Extra/ExtendTemplate.php b/src/Extra/ExtendTemplate.php new file mode 100644 index 0000000..a427eaf --- /dev/null +++ b/src/Extra/ExtendTemplate.php @@ -0,0 +1,256 @@ +extends(%s)', $body); + }; + + $compiler + ->addDirective('extend', $extendFn) + ->addDirective('extends', $extendFn) + // syntax: {{ block 'NAME' }} + ->addDirective('block', function (string $body) { + $block = trim($body, '() '); + return sprintf('$this->startBlock(%s);', $block); + }) + ->addDirective('endblock', function () { + return '$this->endBlock();'; + }); + } + + /** + * Render give template file. + * + * @param string $tplFile + * @param array $tplVars + * + * @return string + * @noinspection PhpDocMissingThrowsInspection + * @noinspection PhpUnhandledExceptionInspection + */ + public function renderFile(string $tplFile, array $tplVars = []): string + { + try { + parent::renderFile($tplFile, $tplVars); + + $this->resetContext(); + } catch (Throwable $e) { + $this->resetContext(true); + throw $e; + } + + $blockName = $this->currentBlock; + Assert::empty($blockName, "error: the block '$blockName' must end by 'endblock'"); + + // if use extends() on tpl file. + if ($this->extendEt) { + $contents = array_merge($this->extendEt->blockContents, $this->blockContents); + // reset runtime property. + $this->extendEt = null; + } else { + $contents = $this->blockContents; + } + + return implode("\n", $contents); + } + + /** + * Render give template string. + * + * @param string $tplCode + * @param array $tplVars + * + * @return string + * @noinspection PhpDocMissingThrowsInspection + * @noinspection PhpUnhandledExceptionInspection + */ + public function renderString(string $tplCode, array $tplVars = []): string + { + try { + parent::renderString($tplCode, $tplVars); + + $this->resetContext(); + } catch (Throwable $e) { + $this->resetContext(true); + throw $e; + } + + return implode("\n", $this->blockContents); + } + + /** + * render extends layout template file. + * + * @param string $tplFile + * @param array $tplVars + * + * @return void + */ + protected function extends(string $tplFile, array $tplVars = []): void + { + if ($this->extendEt) { + throw new RuntimeException("cannot use multi extends() on template file: $this->curTplFile"); + } + + $et = clone $this; + + try { + // cannot use returns, will only return contents not in any block. + $et->renderFile($tplFile, $tplVars); + } catch (Throwable $e) { + throw new RuntimeException("render the extends template '$tplFile' fail", $e->getCode(), $e); + } + + $this->extendEt = $et; + } + + /** + * Block contents + * - key is block name + * + * @var array + */ + private array $blockContents = []; + + /** + * Start new block + * + * @param string $name block name. + * @param array $tplVars TODO + * + * @return void + */ + protected function startBlock(string $name, array $tplVars = []): void + { + Assert::notBlank($name, 'block name is required'); + + $curName = $this->currentBlock; + Assert::empty($curName, "current in the block '$curName', cannot start new block '$name'"); + + // start block rendering + $this->currentBlock = trim($name); + Assert::isTrue(ob_start(), "start output buffer fail on block '$name'"); + } + + /** + * end the block + * + * @return void + */ + protected function endBlock(): void + { + $block = $this->currentBlock; + Assert::notEmpty($block, 'missing block start statement before call endblock.'); + + $this->blockContents[$block] = ob_get_clean(); + // reset current + $this->currentBlock = ''; + } + + /** + * @param bool $clearBuf + * + * @return void + */ + protected function resetContext(bool $clearBuf = false): void + { + // end and clean buffer on started. + if ($clearBuf && $this->currentBlock) { + ob_end_clean(); + } + + // reset runtime property. + $this->extendEt = null; + + $this->currentBlock = ''; + $this->blockContents = []; + } +} diff --git a/src/Extra/LayoutTemplate.php b/src/Extra/LayoutTemplate.php new file mode 100644 index 0000000..9c968f6 --- /dev/null +++ b/src/Extra/LayoutTemplate.php @@ -0,0 +1,71 @@ +addDirective('layout', function (string $tplName) { + $tplName = trim($tplName, '() '); + return sprintf('$this->renderLayout(%s)', $tplName); + }) + ->addDirective('contents', function () { + return '$this->mainContents();'; + }); + } + + protected function renderLayout(): void + { + // TODO + } +} diff --git a/test/Extra/ExtendTemplateTest.php b/test/Extra/ExtendTemplateTest.php new file mode 100644 index 0000000..df0f7f2 --- /dev/null +++ b/test/Extra/ExtendTemplateTest.php @@ -0,0 +1,82 @@ + $this->getTestdataPath('extend-caches'), + ]); + } + + public function testExtend_renderFile(): void + { + $tplFile = $this->getTestdataPath('extends/home.tpl'); + + $et = $this->newTemplate(); + $str = $et->renderFile($tplFile); + + $this->assertStringContainsString('on layout: block header;', $str); + $this->assertStringContainsString('on home: block body;', $str); + $this->assertStringContainsString('on home: block footer;', $str); + } + + public function testExtend_renderString(): void + { + $et = $this->newTemplate(); + + $tplCode = <<renderString($tplCode); + $this->assertEquals("content int first block\n", $str); + + $tplCode = <<renderString($tplCode); + $this->assertEquals("content int first block\n", $str); + } + + public function testExtend_renderString_error(): void + { + $et = $this->newTemplate(); + + // not end block + $ex = $this->runAndGetException(function () use ($et) { + $tpl1 = <<renderString($tpl1); + }); + $this->assertEquals("current in the block 'first', cannot start new block 'second'", $ex->getMessage()); + + // not start block + $ex = $this->runAndGetException(function () use ($et) { + $tpl1 = <<renderString($tpl1); + }); + $this->assertEquals("missing block start statement before call endblock.", $ex->getMessage()); + + } +} diff --git a/test/testdata/extends/home.php b/test/testdata/extends/home.php deleted file mode 100644 index 56f652a..0000000 --- a/test/testdata/extends/home.php +++ /dev/null @@ -1,12 +0,0 @@ - -extends('layouts/layout.php') ?> - -block('body') ?> -on home: block body; -endblock() ?> - -block('footer') ?> -on home: block footer; -endblock() ?> - - diff --git a/test/testdata/extends/home.tpl b/test/testdata/extends/home.tpl index f6d161b..012fc3c 100644 --- a/test/testdata/extends/home.tpl +++ b/test/testdata/extends/home.tpl @@ -1,9 +1,9 @@ - {{ extends('layouts/layout.tpl') }} +{{# will replace to layout.body block #}} {{ block 'body' }} on home: block body; -{{ endblock; }} +{{ endblock }} {{ block 'footer' }} on home: block footer; diff --git a/test/testdata/extends/layouts/layout.php b/test/testdata/extends/layouts/layout.php deleted file mode 100644 index b286cbc..0000000 --- a/test/testdata/extends/layouts/layout.php +++ /dev/null @@ -1,20 +0,0 @@ -this is an layout file. - ---- head - -block('header') ?> -on layout: block header; -endblock(); ?> - ---- body - -block('body') ?> -on layout: block body; -endblock(); ?> - ---- footer - -block('footer') ?> -on layout: block footer; -endblock(); ?> - diff --git a/test/testdata/extends/layouts/layout.tpl b/test/testdata/extends/layouts/layout.tpl index 68adbd7..5c7331e 100644 --- a/test/testdata/extends/layouts/layout.tpl +++ b/test/testdata/extends/layouts/layout.tpl @@ -1,5 +1,5 @@ {{ block 'header' }} - on layout: block header; +on layout: block header; {{ endblock }} this is an layout file. diff --git a/test/testdata/layout/layouts/layout01.tpl b/test/testdata/layout/layouts/layout01.tpl new file mode 100644 index 0000000..e69de29