diff --git a/.gitignore b/.gitignore index 57e5dcd..de93b26 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,11 @@ doc/public doc/data/versions.json .generated .phpunit.result.cache +.idea/.gitignore +.idea/modules.xml +.idea/php-test-framework.xml +.idea/php.xml +.idea/phpunit.xml +.idea/plates.iml +.idea/vcs.xml +composer.phar diff --git a/src/Template/Template.php b/src/Template/Template.php index db11a9f..21c66f0 100644 --- a/src/Template/Template.php +++ b/src/Template/Template.php @@ -2,7 +2,6 @@ namespace League\Plates\Template; -use Exception; use League\Plates\Engine; use League\Plates\Exception\TemplateNotFound; use LogicException; @@ -43,9 +42,10 @@ class Template /** * An array of section content. - * @var array + * + * @var TemplateSectionCollection */ - protected $sections = array(); + protected $sections; /** * The name of the section currently being rendered. @@ -81,6 +81,7 @@ public function __construct(Engine $engine, $name) { $this->engine = $engine; $this->name = new Name($engine, $name); + $this->sections = new TemplateSectionCollection(); $this->data($this->engine->getData($name)); } @@ -173,7 +174,9 @@ public function render(array $data = array()) if (isset($this->layoutName)) { $layout = $this->engine->make($this->layoutName); - $layout->sections = array_merge($this->sections, array('content' => $content)); + $layout->sections->merge($this->sections); + $contentSectionName = 'content'; + $layout->sections[$contentSectionName] = $content; $content = $layout->render($this->layoutData); } @@ -251,31 +254,31 @@ public function unshift($name) */ public function stop() { - if (is_null($this->sectionName)) { + $sectionName = $this->sectionName; + + if (is_null($sectionName)) { throw new LogicException( 'You must start a section before you can stop it.' ); } - if (!isset($this->sections[$this->sectionName])) { - $this->sections[$this->sectionName] = ''; + if (!$this->sections->has($sectionName)) { + $this->sections[$sectionName] = ''; } - switch ($this->sectionMode) { + $sectionContent = ob_get_clean(); - case self::SECTION_MODE_REWRITE: - $this->sections[$this->sectionName] = ob_get_clean(); - break; - - case self::SECTION_MODE_APPEND: - $this->sections[$this->sectionName] .= ob_get_clean(); - break; + // if ob_clean failed for some reason let's just ignore the result + if ($sectionContent === false) { + return; + } - case self::SECTION_MODE_PREPEND: - $this->sections[$this->sectionName] = ob_get_clean().$this->sections[$this->sectionName]; - break; + $this->sections->add( + $sectionName, + $sectionContent, + $this->sectionMode + ); - } $this->sectionName = null; $this->sectionMode = self::SECTION_MODE_REWRITE; $this->appendSection = false; /* for backward compatibility */ @@ -307,13 +310,22 @@ public function section($name, $default = null) /** * Fetch a rendered template. + * * @param string $name * @param array $data * @return string */ public function fetch($name, array $data = array()) { - return $this->engine->render($name, $data); + $template = $this->engine->make($name); + $content = $template->render($data); + + // some info like 'sections' are only filled during + // the render processing, so here we have a window to + // fetch them and join to this template. + $this->sections->merge($template->sections); + + return $content; } /** @@ -324,7 +336,7 @@ public function fetch($name, array $data = array()) */ public function insert($name, array $data = array()) { - echo $this->engine->render($name, $data); + echo $this->fetch($name, $data); } /** diff --git a/src/Template/TemplateSection.php b/src/Template/TemplateSection.php new file mode 100644 index 0000000..9050da3 --- /dev/null +++ b/src/Template/TemplateSection.php @@ -0,0 +1,98 @@ +name = $name; + $this->content = $content; + $this->mode = $mode; + } + + /** + * @param string $content + * @param int $mode + * @return void + */ + public function add(string $content, int $mode) + { + $this->mode = $mode; + + // if this template doesn't have that section, so we just add it. + if (empty($this->content)) { + $this->content = $content; + return; + } + + // otherwise we need to consider the incoming section mode + if ($mode === Template::SECTION_MODE_REWRITE) { + $this->content = $content; + return; + } + + if ($mode === Template::SECTION_MODE_APPEND) { + $this->content = $this->content . $content; + return; + } + + if ($mode === Template::SECTION_MODE_PREPEND) { + $this->content = $content . $this->content; + } + } + + /** + * @return string|null + */ + public function getContent(): string + { + return $this->content; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return int + */ + public function getMode(): int + { + return $this->mode; + } +} diff --git a/src/Template/TemplateSectionCollection.php b/src/Template/TemplateSectionCollection.php new file mode 100644 index 0000000..98c174d --- /dev/null +++ b/src/Template/TemplateSectionCollection.php @@ -0,0 +1,148 @@ + + */ + private $sections = array(); + + /** + * @param string $offset + * + * @return bool + */ + public function has(string $offset): bool + { + return array_key_exists($offset, $this->sections); + } + + /** + * @param string $offset + * + * @return TemplateSection + */ + public function get(string $offset) + { + return $this->sections[$offset]; + } + + /** + * @param string $offset + * @param TemplateSection|null $value + * + * @return void + */ + public function set(string $offset, $value) + { + if (!is_string($offset)) { + throw new InvalidArgumentException('The desired section offset must be a string.'); + } + if (!($value instanceof TemplateSection) && $value != null) { + throw new InvalidArgumentException('The section set must be of type TemplateSection.'); + } + $this->sections[$offset] = $value; + } + + /** + * @param string $offset + * + * @return bool + */ + public function offsetExists($offset): bool + { + if (!is_string($offset)) { + throw new InvalidArgumentException('The desired section offset must be a string.'); + } + return $this->has($offset); + } + + /** + * @param string $offset + * + * @return string + */ + public function offsetGet($offset) + { + if (!is_string($offset)) { + throw new InvalidArgumentException('The desired section offset must be a string.'); + } + return $this->get($offset)->getContent(); + } + + /** + * @param string $offset + * @param string $value + * + * @return void + */ + public function offsetSet($offset, $value): void + { + if (!is_string($offset)) { + throw new InvalidArgumentException('The desired section offset must be a string.'); + } + $this->add($offset, $value, Template::SECTION_MODE_REWRITE); + } + + /** + * @param string $offset + * + * @return void + */ + public function offsetUnset($offset) + { + if (!is_string($offset)) { + throw new InvalidArgumentException('The desired section offset must be a string.'); + } + $this->sections[$offset] = null; + unset($this->sections[$offset]); + } + + /** + * Pushes the given section content to the sections array. + * You can use the $mode to merge the content to an existing + * content if that section content already exists. + * + * @param string $name + * @param string $content + * @param ?int $mode + * + * @return void + */ + public function add(string $name, string $content, $mode = Template::SECTION_MODE_APPEND) + { + if ($this->has($name)) { + $this->sections[$name]->add($content, $mode); + return; + } + $this->sections[$name] = new TemplateSection($name, $content, $mode); + } + + /** + * Merges another Template Sections collection into + * this one taking in consideration the template + * section's modes. + * + * @return void + */ + public function merge(TemplateSectionCollection $templateSections) + { + foreach ($templateSections->sections as $section) { + $this->add( + $section->getName(), + $section->getContent(), + $section->getMode() + ); + } + } +} diff --git a/tests/Template/TemplateTest.php b/tests/Template/TemplateTest.php index 788acd0..54b1be2 100644 --- a/tests/Template/TemplateTest.php +++ b/tests/Template/TemplateTest.php @@ -11,6 +11,9 @@ class TemplateTest extends TestCase { + /** + * @var Template + */ private $template; protected function setUp(): void @@ -183,6 +186,69 @@ public function testReplaceSection() $this->assertSame('See this instead!', $this->template->render()); } + public function testInheritedSection() + { + vfsStream::create( + array( + 'template-child.php' => 'push("textSection") ?>See this! stop() ?>', + 'template.php' => 'layout("layout")?>fetch("template-child")?>push("textSection") ?>See this too!stop() ?>', + 'layout.php' => 'section("textSection") ?>', + ) + ); + + $this->assertSame('See this! See this too!', $this->template->render()); + } + + public function testInheritedSectionUnshift() + { + vfsStream::create( + array( + 'template-child.php' => 'push("textSection") ?>See this!stop() ?>', + 'template.php' => 'layout("layout")?>fetch("template-child")?>unshift("textSection") ?>See this too! stop() ?>', + 'layout.php' => 'section("textSection") ?>', + ) + ); + + $this->assertSame('See this too! See this!', $this->template->render()); + } + + public function testInheritedSectionUnshiftsParent() + { + vfsStream::create( + array( + 'template-child.php' => 'unshift("textSection") ?>See this! stop() ?>', + 'template.php' => 'layout("layout")?>unshift("textSection") ?>See this too!stop() ?>fetch("template-child")?>', + 'layout.php' => 'section("textSection") ?>', + ) + ); + + $this->assertSame('See this! See this too!', $this->template->render()); + } + + public function testInheritedSectionReplacement() + { + vfsStream::create(array( + 'template-child.php' => 'push("textSection") ?>See this!stop() ?>', + 'template.php' => 'layout("layout")?>fetch("template-child")?>start("textSection") ?>See this too!stop() ?>', + 'layout.php' => 'section("textSection") ?>', + )); + + $this->assertSame('See this too!', $this->template->render()); + } + + public function testInheritedSectionReplacesParent() + { + vfsStream::create( + array( + 'template-child.php' => 'start("textSection") ?>See this replacement!stop() ?>', + 'template.php' => 'layout("layout")?>start("textSection") ?> See this too!stop() ?>fetch("template-child")?>', + 'layout.php' => 'section("textSection") ?>', + ) + ); + + $this->assertSame('See this replacement!', $this->template->render()); + } + public function testStartSectionWithInvalidName() { // The section name "content" is reserved.