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

Add support for path namespaces. #1791

Closed
wants to merge 15 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
Empty file modified bin/install-wp-tests.sh 100644 → 100755
Empty file.
34 changes: 34 additions & 0 deletions docs/guides/template-locations.md
Expand Up @@ -23,6 +23,40 @@ Timber::$locations = array(
);
```

## Register your own namespaces

You can also use namespaces in your locations too, just define it as the value next to a path, for example:
gchtr marked this conversation as resolved.
Show resolved Hide resolved

```php
<?php
Timber::$locations = array(
'/Users/jared/Sandbox/templates',
'~/Sites/timber-templates/',
ABSPATH.'/wp-content/templates',
ABSPATH.'/wp-content/styleguide' => 'styleguide'
);
```

gchtr marked this conversation as resolved.
Show resolved Hide resolved
In the example above the namespace is called `styleguide`. You must prefix this with `@` when using it in templates (that's how Twig can differentiate namespaces from regular paths).
Assuming you have a template called `menu.twig` within that namespace, you would call it like so:

```twig
{% include '@styleguide/menu.twig' %}
```

You can also register multiple paths for the same namespace. Order is important as it will look top to bottom and return the first one it encounters, for example:

```php
<?php
Timber::$locations = array(
'/Users/jared/Sandbox/templates',
'~/Sites/timber-templates/',
ABSPATH.'/wp-content/templates',
ABSPATH.'/wp-content/styleguide' => 'styleguide',
'/Users/jared/Sandbox/styleguide' => 'styleguide'
);
```

You only need to do this once in your project (in `functions.php` of your theme). When you call one of the render or compile functions from a PHP file (say `single.php`), Timber will look for Twig files in these locations before it checks the child or parent theme.

## Changing the default folder for Twig files
Expand Down
25 changes: 20 additions & 5 deletions lib/Loader.php
Expand Up @@ -3,6 +3,7 @@
namespace Timber;

use Timber\Cache\Cleaner;
use Twig\Loader\FilesystemLoader;

class Loader {

Expand All @@ -16,6 +17,9 @@ class Loader {
const CACHE_SITE_TRANSIENT = 'site-transient';
const CACHE_USE_DEFAULT = 'default';

/** Identifier of the main namespace. Will likely mirror Twig\Loader\FilesystemLoader::MAIN_NAMESPACE */
const MAIN_NAMESPACE = '__main__';

public static $cache_modes = array(
self::CACHE_NONE,
self::CACHE_OBJECT,
Expand Down Expand Up @@ -130,16 +134,27 @@ protected function template_exists( $name ) {
* @return \Twig_Loader_Filesystem
*/
public function get_loader() {
$open_basedir = ini_get('open_basedir');
$paths = array_merge($this->locations, array($open_basedir ? ABSPATH : '/'));
$paths = apply_filters('timber/loader/paths', $paths);
$open_basedir = ini_get( 'open_basedir' );
$paths = array_merge_recursive(
$this->locations,
array( self::MAIN_NAMESPACE => $open_basedir ? ABSPATH : '/' )
);
$paths = apply_filters( 'timber/loader/paths', $paths );

$rootPath = '/';
if ( $open_basedir ) {
$rootPath = null;
}
$fs = new \Twig_Loader_Filesystem($paths, $rootPath);
$fs = apply_filters('timber/loader/loader', $fs);
$fs = new \Twig_Loader_Filesystem( array(), $rootPath );
foreach ( $paths as $namespace => $path_locations ) {
if ( is_array( $path_locations ) ) {
array_map( function ( $path ) use ( $fs, $namespace ) {
$fs->addPath( $path, $namespace );
}, $path_locations );
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm being overly defensive here as $path_locations will always be an array, as it affects code coverage I'll remove this if/else.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have removed the else, think the if is fine.

}
$fs = apply_filters( 'timber/loader/loader', $fs );

return $fs;
}

Expand Down
90 changes: 56 additions & 34 deletions lib/LocationManager.php
Expand Up @@ -12,17 +12,26 @@ class LocationManager {
public static function get_locations( $caller = false ) {
//prioirty: user locations, caller (but not theme), child theme, parent theme, caller
$locs = array();
$locs = array_merge($locs, self::get_locations_user());
$locs = array_merge($locs, self::get_locations_caller($caller));
$locs = array_merge_recursive( $locs, self::get_locations_user() );
$locs = array_merge_recursive( $locs, self::get_locations_caller( $caller ) );
//remove themes from caller
$locs = array_diff($locs, self::get_locations_theme());
$locs = array_merge($locs, self::get_locations_theme());
$locs = array_merge($locs, self::get_locations_caller($caller));
$locs = array_unique($locs);
$locs = array_merge_recursive( $locs, self::get_locations_theme() );
$locs = array_merge_recursive( $locs, self::get_locations_caller( $caller ) );
$locs = array_map( 'array_unique', $locs );

//now make sure theres a trailing slash on everything
$locs = array_map('trailingslashit', $locs);
$locs = apply_filters('timber_locations', $locs);
$locs = apply_filters('timber/locations', $locs);
$locs = array_map( function ( $loc ) {
return array_map( 'trailingslashit', $loc );
}, $locs );

$locs = array_map( function ( $loc ) {
return apply_filters( 'timber_locations', $loc );
}, $locs );

$locs = array_map( function ( $loc ) {
return apply_filters( 'timber/locations', $loc );
}, $locs );

return $locs;
}

Expand All @@ -33,19 +42,19 @@ public static function get_locations( $caller = false ) {
protected static function get_locations_theme() {
$theme_locs = array();
$theme_dirs = LocationManager::get_locations_theme_dir();
$roots = array(get_stylesheet_directory(), get_template_directory());
$roots = array_map('realpath', $roots);
$roots = array_unique($roots);
$roots = array( get_stylesheet_directory(), get_template_directory() );
$roots = array_map( 'realpath', $roots );
$roots = array_unique( $roots );
foreach ( $roots as $root ) {
if ( !is_dir($root) ) {
if ( ! is_dir( $root ) ) {
continue;
}
$theme_locs[] = $root;
$root = trailingslashit($root);
$theme_locs[ Loader::MAIN_NAMESPACE ][] = $root;
$root = trailingslashit( $root );
foreach ( $theme_dirs as $dirname ) {
$tloc = realpath($root.$dirname);
if ( is_dir($tloc) ) {
$theme_locs[] = $tloc;
$tloc = realpath( $root . $dirname );
if ( is_dir( $tloc ) ) {
$theme_locs[ Loader::MAIN_NAMESPACE ][] = $tloc;
}
}
}
Expand Down Expand Up @@ -105,17 +114,29 @@ public static function get_locations_theme_dir() {
*/
protected static function get_locations_user() {
$locs = array();
if ( isset(Timber::$locations) ) {
if ( is_string(Timber::$locations) ) {
Timber::$locations = array(Timber::$locations);
if ( isset( Timber::$locations ) ) {
if ( is_string( Timber::$locations ) ) {
Timber::$locations = array( Timber::$locations );
}
foreach ( Timber::$locations as $tloc ) {
$tloc = realpath($tloc);
if ( is_dir($tloc) ) {
$locs[] = $tloc;
foreach ( Timber::$locations as $tloc => $namespace_or_tloc ) {
if ( is_string( $tloc ) ) {
$namespace = $namespace_or_tloc;
} else {
$tloc = $namespace_or_tloc;
$namespace = null;
}

$tloc = realpath( $tloc );
if ( is_dir( $tloc ) ) {
if ( ! is_string( $namespace ) ) {
$locs[ Loader::MAIN_NAMESPACE ][] = $tloc;
} else {
$locs[ $namespace ][] = $tloc;
}
}
}
}

return $locs;
}

Expand All @@ -125,21 +146,22 @@ protected static function get_locations_user() {
*/
protected static function get_locations_caller( $caller = false ) {
$locs = array();
if ( $caller && is_string($caller) ) {
$caller = realpath($caller);
if ( is_dir($caller) ) {
$locs[] = $caller;
if ( $caller && is_string( $caller ) ) {
$caller = realpath( $caller );
if ( is_dir( $caller ) ) {
$locs[ Loader::MAIN_NAMESPACE ][] = $caller;
}
$caller = trailingslashit($caller);
$caller = trailingslashit( $caller );
foreach ( LocationManager::get_locations_theme_dir() as $dirname ) {
$caller_sub = realpath($caller.$dirname);
if ( is_dir($caller_sub) ) {
$locs[] = $caller_sub;
$caller_sub = realpath( $caller . $dirname );
if ( is_dir( $caller_sub ) ) {
$locs[ Loader::MAIN_NAMESPACE ][] = $caller_sub;
}
}
}

return $locs;
}


}
}
22 changes: 13 additions & 9 deletions lib/Timber.php
Expand Up @@ -439,22 +439,26 @@ public static function get_sidebar( $sidebar = 'sidebar.php', $data = array() )
* @return string
*/
public static function get_sidebar_from_php( $sidebar = '', $data ) {
$caller = LocationManager::get_calling_script_dir(1);
$uris = LocationManager::get_locations($caller);
$caller = LocationManager::get_calling_script_dir( 1 );
$uris = LocationManager::get_locations( $caller );
ob_start();
$found = false;
foreach ( $uris as $uri ) {
if ( file_exists(trailingslashit($uri).$sidebar) ) {
include trailingslashit($uri).$sidebar;
$found = true;
break;
foreach ( $uris as $namespace => $uri_locations ) {
if ( is_array( $uri_locations ) ) {
foreach ( $uri_locations as $uri ) {
if ( file_exists( trailingslashit( $uri ) . $sidebar ) ) {
include trailingslashit( $uri ) . $sidebar;
$found = true;
}
}
}
}
if ( !$found ) {
Helper::error_log('error loading your sidebar, check to make sure the file exists');
if ( ! $found ) {
Helper::error_log( 'error loading your sidebar, check to make sure the file exists' );
}
$ret = ob_get_contents();
ob_end_clean();

return $ret;
}

Expand Down
1 change: 1 addition & 0 deletions tests/namespaced/test-namespaced.twig
@@ -0,0 +1 @@
This is a namespaced template.
1 change: 1 addition & 0 deletions tests/namespaced/test-nested.twig
@@ -0,0 +1 @@
{% include '@namespaced/test-namespaced.twig' %}
1 change: 1 addition & 0 deletions tests/namespaced/thumb-test.twig
@@ -0,0 +1 @@
<img src="{{post.thumbnail.src|resize(size.width, size.height)}}" alt="namespaced" />
61 changes: 61 additions & 0 deletions tests/test-timber-loader.php
Expand Up @@ -34,6 +34,7 @@ function testTwigPathFilterAdded() {
function testTwigPathFilter() {
$php_unit = $this;
add_filter('timber/loader/paths', function($paths) use ($php_unit) {
$paths = call_user_func_array('array_merge', $paths);
$count = count($paths);
$php_unit->assertEquals(3, count($paths));
$pos = array_search('/', $paths);
Expand Down Expand Up @@ -134,5 +135,65 @@ function testTwigLoadsFromLocation(){
$this->assertEquals('<img src="" />', trim($str));
}

function testTwigLoadsFromLocationWithNamespace(){
Timber::$locations = array( __DIR__.'/assets' => 'assets' );
$str = Timber::compile('@assets/thumb-test.twig');
$this->assertEquals('<img src="" />', trim($str));
}

function testTwigLoadsFromLocationWithNestedNamespace(){
Timber::$locations = array( __DIR__.'/namespaced' => 'namespaced' );
$str = Timber::compile('@namespaced/test-nested.twig');
$this->assertEquals('This is a namespaced template.', trim($str));
}

function testTwigLoadsFromLocationWithAndWithoutNamespaces(){
Timber::$locations = array( __DIR__.'/namespaced' => 'namespaced', __DIR__ . '/assets' );

// Namespaced location
$str = Timber::compile('@namespaced/test-namespaced.twig');
$this->assertEquals('This is a namespaced template.', trim($str));

// Non namespaced location
$str = Timber::compile('thumb-test.twig');
$this->assertEquals('<img src="" />', trim($str));
}

function testTwigLoadsFromLocationWithAndWithoutNamespacesAndDirs(){
Timber::$dirname = array('foo', 'views');
Timber::$locations = array( __DIR__.'/namespaced' => 'namespaced', __DIR__ . '/assets' );

// Namespaced location
$str = Timber::compile('@namespaced/test-namespaced.twig');
$this->assertEquals('This is a namespaced template.', trim($str));

// Non namespaced location
$str = Timber::compile('thumb-test.twig');
$this->assertEquals('<img src="" />', trim($str));

if (!file_exists(get_template_directory().'/foo')) {
mkdir(get_template_directory().'/foo', 0777, true);
}
copy(__DIR__.'/assets/single-foo.twig', get_template_directory().'/foo/single-foo.twig');

// Dir
$str = Timber::compile('single-foo.twig');
$this->assertEquals('I am single-foo', trim($str));
}

function testTwigLoadsFromMultipleLocationsWithNamespace(){
Timber::$locations = array( __DIR__.'/assets' => 'assets', __DIR__ .'/namespaced' => 'assets' );
$str = Timber::compile('@assets/thumb-test.twig');
$this->assertEquals('<img src="" />', trim($str));

$str = Timber::compile('@assets/test-namespaced.twig');
$this->assertEquals('This is a namespaced template.', trim($str));
}

function testTwigLoadsFirstTemplateWhenMultipleLocationsWithSameNamespace(){
Timber::$locations = array( __DIR__.'/assets' => 'assets', __DIR__ .'/namespaced' => 'assets' );
$str = Timber::compile('@assets/thumb-test.twig');
$this->assertEquals('<img src="" />', trim($str));
}

}