Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API: Look for templates of namespaced classes in subfolders. #5490

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
76 changes: 64 additions & 12 deletions core/manifest/TemplateManifest.php
Expand Up @@ -178,34 +178,86 @@ public function regenerate($cache = true) {
$this->inited = true;
}

public function handleFile($basename, $pathname, $depth) {
public function handleFile($basename, $pathname, $depth)
{
$projectFile = false;
$theme = null;

if (strpos($pathname, $this->base . '/' . THEMES_DIR) === 0) {
$start = strlen($this->base . '/' . THEMES_DIR) + 1;
$theme = substr($pathname, $start);
$theme = substr($theme, 0, strpos($theme, '/'));
$theme = strtok($theme, '_');
} else if($this->project && (strpos($pathname, $this->base . '/' . $this->project .'/') === 0)) {
// Template in theme
if (preg_match(
'#'.preg_quote($this->base.'/'.THEMES_DIR).'/([^/_]+)(_[^/]+)?/(.*)$#',
$pathname,
$matches
)) {
$theme = $matches[1];
$relPath = $matches[3];

// Template in project
} elseif (preg_match(
'#'.preg_quote($this->base.'/'.$this->project).'/(.*)$#',
$pathname,
$matches
)) {
$projectFile = true;
$relPath = $matches[1];

// Template in module
} elseif (preg_match(
'#'.preg_quote($this->base).'/([^/]+)/(.*)$#',
$pathname,
$matches
)) {
$relPath = $matches[2];

} else {
throw new \LogicException("Can't determine meaning of path: $pathname");
}

$type = basename(dirname($pathname));
$name = strtolower(substr($basename, 0, -3));
// If a templates subfolder is used, ignore that
if (preg_match('#'.preg_quote(self::TEMPLATES_DIR).'/(.*)$#', $relPath, $matches)) {
$relPath = $matches[1];
}

if ($type == self::TEMPLATES_DIR) {
$type = 'main';
// Layout and Content folders have special meaning
if (preg_match('#^(.*/)?(Layout|Content|Includes)/([^/]+)$#', $relPath, $matches)) {
$type = $matches[2];
$relPath = "$matches[1]$matches[3]";
} else {
$type = "main";
}

$name = strtolower(substr($relPath, 0, -3));
$name = str_replace('/', '\\', $name);

if ($theme) {
$this->templates[$name]['themes'][$theme][$type] = $pathname;
} else if($projectFile) {
} else if ($projectFile) {
$this->templates[$name][$this->project][$type] = $pathname;
} else {
$this->templates[$name][$type] = $pathname;
}

// If we've found a template in a subdirectory, then allow its use for a non-namespaced class
// as well. This was a common SilverStripe 3 approach, where templates were placed into
// subfolders to suit the whim of the developer.
if (strpos($name, '\\') !== false) {
$name2 = substr($name, strrpos($name, '\\') + 1);
// In of these cases, the template will only be provided if it isn't already set. This
// matches SilverStripe 3 prioritisation.
if ($theme) {
if (!isset($this->templates[$name2]['themes'][$theme][$type])) {
$this->templates[$name2]['themes'][$theme][$type] = $pathname;
}
} else if ($projectFile) {
if (!isset($this->templates[$name2][$this->project][$type])) {
$this->templates[$name2][$this->project][$type] = $pathname;
}
} else {
if (!isset($this->templates[$name2][$type])) {
$this->templates[$name2][$type] = $pathname;
}
}
}
}

protected function init() {
Expand Down
28 changes: 21 additions & 7 deletions docs/en/02_Developer_Guides/01_Templates/01_Syntax.md
Expand Up @@ -3,10 +3,9 @@ summary: A look at the operations, variables and language controls you can use w

# Template Syntax

SilverStripe templates are plain text files that have `.ss` extension and located within the `templates` directory of
a module, theme, or your `mysite` folder. A template can contain any markup language (e.g HTML, CSV, JSON..) and before
being rendered to the user, they're processed through [api:SSViewer]. This process replaces placeholders such as `$Var`
with real content from your [model](../model) and allows you to define logic controls like `<% if $Var %>`.
A template can contain any markup language (e.g HTML, CSV, JSON..) and before being rendered to the user, they're
processed through [api:SSViewer]. This process replaces placeholders such as `$Var` with real content from your
[model](../model) and allows you to define logic controls like `<% if $Var %>`.

An example of a SilverStripe template is below:

Expand Down Expand Up @@ -45,6 +44,17 @@ Templates can be used for more than HTML output. You can use them to output your
text-based format.
</div>

## Template file location

SilverStripe templates are plain text files that have `.ss` extension and located within the `templates` directory of
a module, theme, or your `mysite` folder.

By default, templates will have the same name as the class they are used to render. So, your Page class will
be rendered with the `templates/Page.ss` template.

When the class has a namespace, the namespace will be interpreted as subfolder within the `templates` path. For, example, the class `SilverStripe\Control\Controller` will be rendered with the
`templates/SilverStripe/Control/Controller.ss` template.

## Variables

Variables are placeholders that will be replaced with data from the [DataModel](../model/) or the current
Expand Down Expand Up @@ -190,11 +200,15 @@ You can use inequalities like `<`, `<=`, `>`, `>=` to compare numbers.

## Includes

Within SilverStripe templates we have the ability to include other templates from the `template/Includes` directory
using the `<% include %>` tag.
Within SilverStripe templates we have the ability to include other templates using the `<% include %>` tag. The includes
will be searched for using the same filename look-up rules as a regular template, so this will include
`templates/Includes/Sidebar.ss`

:::ss
<% include SideBar %>
<% include Includes\SideBar %>

Note that in SilverStripe 3, you didn't have to specify a namespace in your `include` tag, as the template engine didn't
use namespaces. As of SilverStripe 4, the template namespaces need to match the folder structure of your template files.

The `include` tag can be particularly helpful for nested functionality and breaking large templates up. In this example,
the include only happens if the user is logged in.
Expand Down
Expand Up @@ -101,3 +101,7 @@ footer and navigation will remain the same and we don't want to replicate this w
<blink>Hi!</blink>


If your classes have in a namespace, the Layout folder will be a found inside of the appropriate namespace folder.

For example, the layout template for `SilverStripe\Control\Controller` will be
found at `templates/SilverStripe/Control/Layout/Controller.ss`.
26 changes: 25 additions & 1 deletion tests/core/manifest/TemplateManifestTest.php
Expand Up @@ -25,7 +25,7 @@ public function setUp() {
public function testGetTemplates() {
$expect = array(
'root' => array(
'module' => "{$this->base}/module/Root.ss"
'main' => "{$this->base}/module/Root.ss"
),
'page' => array(
'main' => "{$this->base}/module/templates/Page.ss",
Expand Down Expand Up @@ -54,6 +54,30 @@ public function testGetTemplates() {
'theme' => array('main' => "{$this->base}/themes/theme/templates/CustomThemePage.ss",)
)
),
'mynamespace\myclass' => array(
'main' => "{$this->base}/module/templates/MyNamespace/MyClass.ss",
'Layout' => "{$this->base}/module/templates/MyNamespace/Layout/MyClass.ss",
'themes' => array(
'theme' => array(
'main' => "{$this->base}/themes/theme/templates/MyNamespace/MyClass.ss",
)
),
),
'mynamespace\mysubnamespace\mysubclass' => array(
'main' => "{$this->base}/module/templates/MyNamespace/MySubnamespace/MySubclass.ss",
),
'myclass' => array(
'main' => "{$this->base}/module/templates/MyNamespace/MyClass.ss",
'Layout' => "{$this->base}/module/templates/MyNamespace/Layout/MyClass.ss",
'themes' => array(
'theme' => array(
'main' => "{$this->base}/themes/theme/templates/MyNamespace/MyClass.ss",
)
),
),
'mysubclass' => array(
'main' => "{$this->base}/module/templates/MyNamespace/MySubnamespace/MySubclass.ss",
),
'include' => array('themes' => array(
'theme' => array(
'Includes' => "{$this->base}/themes/theme/templates/Includes/Include.ss"
Expand Down
@@ -0,0 +1 @@
MyClass.ss
@@ -0,0 +1 @@
MyClass.ss
@@ -0,0 +1 @@
MySubclass.ss
1 change: 1 addition & 0 deletions tests/templates/Namespace/NamespaceInclude.ss
@@ -0,0 +1 @@
NamespaceInclude
48 changes: 32 additions & 16 deletions tests/view/SSViewerTest.php
Expand Up @@ -728,6 +728,28 @@ public function testIncludeWithArguments() {
$this->assertEqualIgnoringWhitespace('A B', $res, 'Objects can be passed as named arguments');
}

public function testNamespaceInclude() {
$data = new ArrayData([]);

$this->assertEquals(
"tests:( NamespaceInclude\n )",
$this->render('tests:( <% include Namespace\NamespaceInclude %> )', $data),
'Backslashes work for namespace references in includes'
);

$this->assertEquals(
"tests:( NamespaceInclude\n )",
$this->render('tests:( <% include Namespace/NamespaceInclude %> )', $data),
'Forward slashes work for namespace references in includes'
);

$this->assertEquals(
"tests:( NamespaceInclude\n )",
$this->render('tests:( <% include NamespaceInclude %> )', $data),
'Namespace can be missed for a namespaed include'
);
}


public function testRecursiveInclude() {
$view = new SSViewer(array('SSViewerTestRecursiveInclude'));
Expand Down Expand Up @@ -1124,24 +1146,24 @@ public function testGetTemplatesByClass() {
$self = $this;
$this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) {
// Test passing a string
$templates = SSViewer::get_templates_by_class('SSViewerTest_Controller', '', 'Controller');
$self->assertCount(2, $templates);
$templates = SSViewer::get_templates_by_class(
'TestNamespace\SSViewerTest_Controller',
'',
'Controller'
);
$self->assertEquals([
'TestNamespace\SSViewerTest_Controller',
'Controller',
], $templates);

// Test to ensure we're stopping at the base class.
$templates = SSViewer::get_templates_by_class('SSViewerTest_Controller', '', 'SSViewerTest_Controller');
$templates = SSViewer::get_templates_by_class('TestNamespace\SSViewerTest_Controller', '', 'TestNamespace\SSViewerTest_Controller');
$self->assertCount(1, $templates);

// Make sure we can filter our templates by suffix.
$templates = SSViewer::get_templates_by_class('SSViewerTest', '_Controller');
$self->assertCount(1, $templates);

// Test passing a valid object
$templates = SSViewer::get_templates_by_class("SSViewerTest_Controller", '', 'Controller');

// Test that templates are returned in the correct order
$self->assertEquals('SSViewerTest_Controller', array_shift($templates));
$self->assertEquals('Controller', array_shift($templates));

// Let's throw something random in there.
$self->setExpectedException('InvalidArgumentException');
$templates = SSViewer::get_templates_by_class(array());
Expand Down Expand Up @@ -1593,11 +1615,6 @@ public function methodWithTwoArguments($arg1, $arg2) {
}
}


class SSViewerTest_Controller extends Controller {

}

class SSViewerTest_Object extends DataObject implements TestOnly {

public $number = null;
Expand Down Expand Up @@ -1687,4 +1704,3 @@ public function forWith($number) {
return new self($number);
}
}

8 changes: 8 additions & 0 deletions tests/view/TestNamespace/SSViewerTest_Controller.php
@@ -0,0 +1,8 @@
<?php

namespace TestNamespace;

class SSViewerTest_Controller extends \Controller
{

}