diff --git a/.gitignore b/.gitignore index 57e5dcd..5aa800f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.idea + build composer.lock vendor diff --git a/src/Engine.php b/src/Engine.php index 3786734..d99e56b 100644 --- a/src/Engine.php +++ b/src/Engine.php @@ -137,7 +137,9 @@ public function getFileExtension() public function addFolder($name, $directory, $fallback = false) { $this->folders->add($name, $directory, $fallback); - + if (method_exists($this->getResolveTemplatePath(), 'addPath')) { + $this->getResolveTemplatePath()->addPath($directory, $name); + } return $this; } @@ -258,26 +260,26 @@ public function loadExtensions(array $extensions = array()) /** * Get a template path. - * @param string $name + * @param string|Name $name * @return string */ public function path($name) { - $name = new Name($this, $name); + $name = $name instanceof Name ? $name : new Name($this, $name); - return $name->getPath(); + return $this->getResolveTemplatePath()->resolvePath($name); } /** * Check if a template exists. - * @param string $name + * @param string|Name $name * @return boolean */ public function exists($name) { - $name = new Name($this, $name); + $name = $name instanceof Name ? $name : new Name($this, $name); - return $name->doesPathExist(); + return $this->getResolveTemplatePath()->exists($name); } /** diff --git a/src/Template/Name.php b/src/Template/Name.php index a866923..6e713af 100644 --- a/src/Template/Name.php +++ b/src/Template/Name.php @@ -10,6 +10,15 @@ */ class Name { + const NAMESPACE_DELIMITER = '::'; + + /** + * Hint path delimiter value. + * + * @var string + */ + const HINT_PATH_DELIMITER = '::'; + /** * Instance of the template engine. * @var Engine @@ -23,16 +32,17 @@ class Name protected $name; /** - * The parsed template folder. - * @var Folder + * The parsed namespace */ + protected $namespace; + protected $folder; /** - * The parsed template filename. + * The parsed template path. * @var string */ - protected $file; + protected $path; /** * Create a new Name instance. @@ -75,13 +85,13 @@ public function setName($name) { $this->name = $name; - $parts = explode('::', $this->name); + $parts = explode(static::NAMESPACE_DELIMITER, $this->name); if (count($parts) === 1) { - $this->setFile($parts[0]); + $this->setPath($parts[0]); } elseif (count($parts) === 2) { - $this->setFolder($parts[0]); - $this->setFile($parts[1]); + $this->setNamespace($parts[0]); + $this->setPath($parts[1]); } else { throw new LogicException( 'The template name "' . $this->name . '" is not valid. ' . @@ -103,12 +113,12 @@ public function getName() /** * Set the parsed template folder. - * @param string $folder + * @param string $namespace * @return Name */ - public function setFolder($folder) + public function setNamespace($namespace) { - $this->folder = $this->engine->getFolders()->get($folder); + $this->namespace = $namespace; return $this; } @@ -117,64 +127,61 @@ public function setFolder($folder) * Get the parsed template folder. * @return string */ + public function getNamespace() + { + return $this->namespace; + } + + /** + * @deprecated + */ public function getFolder() { - return $this->folder; + if ($this->getNamespace()) { + return $this->getEngine()->getFolders()->get($this->getNamespace()); + } + return null; } /** * Set the parsed template file. - * @param string $file + * @param string $path * @return Name */ - public function setFile($file) + public function setPath($path) { - if ($file === '') { + if ($path === '') { throw new LogicException( 'The template name "' . $this->name . '" is not valid. ' . 'The template name cannot be empty.' ); } - $this->file = $file; - - if (!is_null($this->engine->getFileExtension())) { - $this->file .= '.' . $this->engine->getFileExtension(); - } + $this->path = $path; return $this; } - /** - * Get the parsed template file. - * @return string - */ public function getFile() { - return $this->file; + $file = $this->path; + if (!is_null($this->getEngine()->getFileExtension())) { + $file .= '.'.$this->getEngine()->getFileExtension(); + } + return $file; } /** - * Resolve template path. + * Resolve template path or + * Get the parsed template file. * @return string */ - public function getPath() + public function getPath($resolve = true ) { - if (is_null($this->folder)) { - return "{$this->getDefaultDirectory()}/{$this->file}"; - } - - $path = "{$this->folder->getPath()}/{$this->file}"; - - if ( - !is_file($path) - && $this->folder->getFallback() - && is_file("{$this->getDefaultDirectory()}/{$this->file}") - ) { - $path = "{$this->getDefaultDirectory()}/{$this->file}"; + if ($resolve) { + return $this->engine->path($this); } - - return $path; + return $this->path; } /** @@ -183,24 +190,6 @@ public function getPath() */ public function doesPathExist() { - return is_file($this->getPath()); - } - - /** - * Get the default templates directory. - * @return string - */ - protected function getDefaultDirectory() - { - $directory = $this->engine->getDirectory(); - - if (is_null($directory)) { - throw new LogicException( - 'The template name "' . $this->name . '" is not valid. '. - 'The default directory has not been defined.' - ); - } - - return $directory; + return $this->engine->exists($this); } } diff --git a/src/Template/ResolveTemplatePath.php b/src/Template/ResolveTemplatePath.php index 84d62b4..5d0e59c 100644 --- a/src/Template/ResolveTemplatePath.php +++ b/src/Template/ResolveTemplatePath.php @@ -10,4 +10,7 @@ interface ResolveTemplatePath * @throws TemplateNotFound if the template could not be properly resolved to a file path */ public function __invoke(Name $name): string; + + + public function exists(Name $name): bool; } diff --git a/src/Template/ResolveTemplatePath/NameAndFolderResolveTemplatePath.php b/src/Template/ResolveTemplatePath/NameAndFolderResolveTemplatePath.php index 6885081..8a36f2b 100644 --- a/src/Template/ResolveTemplatePath/NameAndFolderResolveTemplatePath.php +++ b/src/Template/ResolveTemplatePath/NameAndFolderResolveTemplatePath.php @@ -5,16 +5,162 @@ use League\Plates\Exception\TemplateNotFound; use League\Plates\Template\Name; use League\Plates\Template\ResolveTemplatePath; +use LogicException; /** Resolves the path from the logic in the Name class which resolves via folder lookup, and then the default directory */ final class NameAndFolderResolveTemplatePath implements ResolveTemplatePath { - public function __invoke(Name $name): string { - $path = $name->getPath(); + /** + * Identifier of the main namespace. + * + * @var string + */ + const MAIN_NAMESPACE = '__main__'; + + /** + * Collection of template folders. + */ + protected $paths = []; + + public function __invoke(Name $name): string + { + $path = $this->resolvePath($name); if (is_file($path)) { return $path; } - throw new TemplateNotFound($name->getName(), [$name->getPath()], 'The template "' . $name->getName() . '" could not be found at "' . $name->getPath() . '".'); + throw new TemplateNotFound( + $name->getName(), + [$path], + 'The template "'.$name->getName().'" could not be found at "'.$path.'".' + ); + } + + public function exists(Name $name): bool + { + $path = $this->resolvePath($name); + if (is_file($path)) { + return true; + } + + return false; + } + + public function resolvePath(Name $name) + { + $namespace = $this->normalizeNamespace($name->getNamespace()); + $shortPath = $name->getPath(false); + $shortPath = $this->normalizePath($shortPath, $name); + + $fullPath = null; + $paths = $this->getPaths($namespace); + foreach ($paths as $path) { + $fullPath = $path.'/'.$shortPath; + if (is_file($fullPath)) { + return $fullPath; + } + } + + $defaultDirectory = $this->getDefaultDirectory($name); + + return "{$defaultDirectory}/{$shortPath}"; + } + + /** + * Returns the paths to the templates. + */ + public function getPaths(string $namespace = self::MAIN_NAMESPACE): array + { + $namespace = $this->normalizeNamespace($namespace); + + return $this->paths[$namespace] ?? []; + } + + /** + * Returns the path namespaces. + * + * The main namespace is always defined. + */ + public function getNamespaces(): array + { + return array_keys($this->paths); + } + + /** + * @param string|array $paths A path or an array of paths where to look for templates + */ + public function setPaths($paths, string $namespace = self::MAIN_NAMESPACE): void + { + if (!\is_array($paths)) { + $paths = [$paths]; + } + + $this->paths[$namespace] = []; + foreach ($paths as $path) { + $this->addPath($path, $namespace); + } + } + + /** + * @throws \Exception + */ + public function addPath(string $path, string $namespace = self::MAIN_NAMESPACE) + { + if (!is_dir($path)) { + throw new \Exception(sprintf('The "%s" directory does not exist.', $path)); + } + + $this->paths[$namespace][] = rtrim($path, '/\\'); + } + + public function prependPath(string $path, string $namespace = self::MAIN_NAMESPACE): void + { + if (!is_dir($path)) { + throw new \Exception(sprintf('The "%s" directory does not exist.', $path)); + } + + $path = rtrim($path, '/\\'); + + if (!isset($this->paths[$namespace])) { + $this->paths[$namespace][] = $path; + } else { + array_unshift($this->paths[$namespace], $path); + } + } + + protected function normalizePath($path, Name $name): string + { + if (!is_null($name->getEngine()->getFileExtension())) { + $path .= '.'.$name->getEngine()->getFileExtension(); + } + + return $path; + } + + /** + * Get the default templates directory. + * @return string + */ + protected function getDefaultDirectory(Name $name) + { + $directory = $name->getEngine()->getDirectory(); + + if (is_null($directory)) { + throw new LogicException( + 'The template name "'.$name->getName().'" is not valid. '. + 'The default directory has not been defined.' + ); + } + + return $directory; + } + + protected function normalizeNamespace($namespace = self::MAIN_NAMESPACE): string + { + if (empty($namespace)) { + return self::MAIN_NAMESPACE; + } + + return $namespace; } } diff --git a/src/Template/ResolveTemplatePath/ThemeResolveTemplatePath.php b/src/Template/ResolveTemplatePath/ThemeResolveTemplatePath.php index f4c2786..277827e 100644 --- a/src/Template/ResolveTemplatePath/ThemeResolveTemplatePath.php +++ b/src/Template/ResolveTemplatePath/ThemeResolveTemplatePath.php @@ -1,8 +1,8 @@ given_a_directory_structure_is_setup_like('templates', [ + 'parent' => [ + 'main.php' => 'layout("layout") ?>parent', + 'layout.php' => 'parent: section("content")?>', + ], + 'child' => [ + 'layout.php' => 'child: section("content")?>', + ], + ]); + + $this->engine->getResolveTemplatePath()->addPath($this->vfsPath('templates/parent')); + $this->engine->getResolveTemplatePath()->prependPath($this->vfsPath('templates/child')); + + $this->when_the_engine_renders('main'); + $this->then_the_rendered_template_matches('child: parent'); + } + + protected function setUp(): void + { + vfsStream::setup('templates'); + + $this->engine = new Engine(vfsStream::url('templates')); + } + + private function given_a_directory_structure_is_setup_like(string $rootDir, array $directoryStructure) + { + vfsStream::setup($rootDir); + vfsStream::create($directoryStructure); + } + + private function vfsPath(string $path): string { + return vfsStream::url($path); + } + + private function when_the_engine_renders(string $templateName, array $data = []) + { + try { + $this->result = $this->engine->render($templateName, $data); + } catch (\Throwable $e) { + $this->exception = $e; + } + } + + private function then_the_rendered_template_matches(string $expected) + { + if ($this->exception) { + throw $this->exception; + } + + $this->assertEquals($expected, $this->result); + } +}