Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion src/Lib/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ class View extends Language
*/
public $default_view;

/**
* @var string An optional view directory, within the view path, preferred over $view.
* Set to allow individual views to be overridden for an active template, any view the
* template directory does not contain falls back to $view.
* @see View::setTemplate()
*/
public $template;

/**
* @var string The view directory $template may override. $template is only applied while
* $view matches this, so that a view which has deliberately moved to a directory of its
* own is never overridden.
* @see View::setTemplate()
*/
public $template_view;

/**
* @var string This view's relative path
*/
Expand Down Expand Up @@ -92,6 +108,23 @@ final public function setDefaultView($path)
$this->view_path = $path;
}

/**
* Sets a view directory to prefer over $view, per file, while $view is $template_view.
*
* Any view the template directory does not contain falls back to $template_view, so a
* template may override as few or as many views as it likes. The override is limited to
* $template_view so that a view which has since moved to a directory of its own is left
* alone rather than being silently overridden.
*
* @param string $template The view directory to prefer, within the view path
* @param string $view The view directory $template may override
*/
final public function setTemplate($template, $view)
{
$this->template = $template;
$this->template_view = $view;
}

/**
* Sets the view file and view to be used for this View
*
Expand All @@ -109,7 +142,19 @@ final public function setView($file = null, $view = null)
list($view_path, $view) = $this->getViewPath($view);
$this->view = $view;
$this->view_path = $view_path;
$this->view_dir = str_replace(
$this->view_dir = $this->buildViewDir($view_path, $view);
}

/**
* Builds the web accessible directory for the given view path and view
*
* @param string $view_path The view path the view resides in
* @param string $view The view directory
* @return string The view directory relative to the public web directory
*/
private function buildViewDir($view_path, $view)
{
return str_replace(
"\\",
"/",
str_replace(
Expand Down Expand Up @@ -157,6 +202,33 @@ final public function fetch($file = null, $view = null)
. $this->view_path . 'views' . DIRECTORY_SEPARATOR
. $this->view . DIRECTORY_SEPARATOR . $this->file . $this->view_ext;

// Prefer a view of the same name from the template directory, when one is set and this
// view is still the one it may override. This allows individual views to be overridden
// for the active template, while any view the template directory does not contain falls
// back to the view directory below.
if (!empty($this->template)
&& $this->template !== $this->view
&& $this->template_view === $this->view
) {
$template_file = $this->container->get('minphp.constants')['ROOTWEBDIR']
. $this->view_path . 'views' . DIRECTORY_SEPARATOR
. $this->template . DIRECTORY_SEPARATOR . $this->file . $this->view_ext;

// See the note below regarding macOS and symbolic links
if (!file_exists($template_file) && strpos(strtolower(PHP_OS), 'darwin') !== false) {
$template_file = $this->view_path . 'views' . DIRECTORY_SEPARATOR
. $this->template . DIRECTORY_SEPARATOR . $this->file . $this->view_ext;
}

if (file_exists($template_file)) {
$file = $template_file;

// Point the view directory at the template so that any asset the view
// references resolves against the directory the view was loaded from
$this->view_dir = $this->buildViewDir($this->view_path, $this->template);
}
}

if (is_array($this->vars)) {
if (isset($this->vars['file'])) {
unset($this->vars['file']);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DEFAULT-MAIN|<?php echo $this->view_dir; ?>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DEFAULT-OTHER|<?php echo $this->view_dir; ?>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OTHER-DIR-MAIN|<?php echo $this->view_dir; ?>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TEMPLATE-MAIN|<?php echo $this->view_dir; ?>
209 changes: 209 additions & 0 deletions tests/Unit/Lib/ViewTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<?php

use Minphp\Container\Container;
use Minphp\Bridge\Initializer;

/**
* @coversDefaultClass \View
*/
class ViewTest extends PHPUnit_Framework_TestCase
{
/**
* @var string
*/
protected $fixtureDir;

/**
* @var string The view path of the fixture plugin, relative to the root web directory
*/
protected $pluginPath;

/**
* Set up
*/
public function setUp()
{
$this->fixtureDir = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Fixtures'
. DIRECTORY_SEPARATOR . 'App' . DIRECTORY_SEPARATOR;
$this->pluginPath = 'plugins' . DIRECTORY_SEPARATOR . 'my_plugin' . DIRECTORY_SEPARATOR;

$fixtureDir = $this->fixtureDir;

$init = Initializer::get();
$container = new Container();

$container->set('minphp.constants', function () use ($fixtureDir) {
return [
'ROOTWEBDIR' => $fixtureDir,
'PLUGINDIR' => $fixtureDir . 'plugins' . DIRECTORY_SEPARATOR,
'WEBDIR' => $fixtureDir,
'APPDIR' => 'app' . DIRECTORY_SEPARATOR
];
});
$container->set('minphp.mvc', function () {
return [
'default_controller' => 'main',
'default_structure' => 'structure',
'default_view' => 'default',
'view_extension' => '.pdt',
'cli_render_views' => false,
'404_forwarding' => false,
'error_view' => 'errors'
];
});

$init->setContainer($container);
}

/**
* Builds a View pointed at the fixture plugin, the way Dispatcher and a plugin
* controller leave it
*
* @param string $template The template directory to prefer, if any
* @return View
*/
protected function getPluginView($template = null, $template_view = 'default')
{
$view = new View();
$view->setDefaultView($this->pluginPath);
$view->view = 'default';

if (null !== $template) {
$view->setTemplate($template, $template_view);
}

return $view;
}

/**
* Returns the view directory the given rendered output was loaded from
*
* @param string $output The rendered view
* @return array The marker the view printed and the view directory it reported
*/
protected function parse($output)
{
return explode('|', $output);
}

/**
* @covers ::fetch
* @covers ::setView
* @covers ::setDefaultView
* @covers ::buildViewDir
* @uses \View::__construct
* Tests that a view provided by the template directory is rendered from it
*/
public function testFetchPrefersTemplate()
{
list($marker, $view_dir) = $this->parse($this->getPluginView('my_template')->fetch('main'));

$this->assertEquals('TEMPLATE-MAIN', $marker);
$this->assertStringEndsWith('plugins/my_plugin/views/my_template/', $view_dir);
}

/**
* @covers ::fetch
* @covers ::setView
* @covers ::buildViewDir
* @uses \View::__construct
* @uses \View::setDefaultView
* Tests that a view the template directory does not provide falls back to the view
* directory, even though the template directory provides other views
*/
public function testFetchFallsBackPerFile()
{
$view = $this->getPluginView('my_template');

list($marker) = $this->parse($view->fetch('main'));
$this->assertEquals('TEMPLATE-MAIN', $marker);

list($marker, $view_dir) = $this->parse($view->fetch('other'));
$this->assertEquals('DEFAULT-OTHER', $marker);
$this->assertStringEndsWith('plugins/my_plugin/views/default/', $view_dir);
}

/**
* @covers ::fetch
* @covers ::setView
* @covers ::buildViewDir
* @uses \View::__construct
* @uses \View::setDefaultView
* Tests that no template leaves the view directory in charge
*/
public function testFetchWithoutTemplate()
{
list($marker, $view_dir) = $this->parse($this->getPluginView()->fetch('main'));

$this->assertEquals('DEFAULT-MAIN', $marker);
$this->assertStringEndsWith('plugins/my_plugin/views/default/', $view_dir);
}

/**
* @covers ::fetch
* @covers ::setView
* @covers ::buildViewDir
* @uses \View::__construct
* @uses \View::setDefaultView
* Tests that a template matching the view directory is a no-op
*/
public function testFetchWithTemplateMatchingView()
{
list($marker, $view_dir) = $this->parse($this->getPluginView('default')->fetch('main'));

$this->assertEquals('DEFAULT-MAIN', $marker);
$this->assertStringEndsWith('plugins/my_plugin/views/default/', $view_dir);
}

/**
* @covers ::fetch
* @covers ::setView
* @covers ::buildViewDir
* @uses \View::__construct
* @uses \View::setDefaultView
* @uses \View::setTemplate
* Tests that a template directory which does not exist leaves the view directory in charge
*/
public function testFetchWithUnknownTemplate()
{
list($marker, $view_dir) = $this->parse($this->getPluginView('no_such_template')->fetch('main'));

$this->assertEquals('DEFAULT-MAIN', $marker);
$this->assertStringEndsWith('plugins/my_plugin/views/default/', $view_dir);
}

/**
* @covers ::fetch
* @covers ::setTemplate
* @covers ::setView
* @covers ::buildViewDir
* @uses \View::__construct
* @uses \View::setDefaultView
* Tests that a view which has moved to a directory of its own is not overridden, even
* though the template directory contains a view of the same name
*/
public function testFetchLeavesViewOutsideTemplateViewAlone()
{
$view = $this->getPluginView('my_template');
$view->view = 'my_other_dir';

list($marker, $view_dir) = $this->parse($view->fetch('main'));

$this->assertEquals('OTHER-DIR-MAIN', $marker);
$this->assertStringEndsWith('plugins/my_plugin/views/my_other_dir/', $view_dir);
}

/**
* @covers ::setTemplate
* @uses \View::__construct
* Tests that the template and the view it may override are both recorded
*/
public function testSetTemplate()
{
$view = new View();
$view->setTemplate('my_template', 'default');

$this->assertEquals('my_template', $view->template);
$this->assertEquals('default', $view->template_view);
}
}