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
2 changes: 2 additions & 0 deletions src/CoreServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use Framework\Http\Response;
use Framework\Supports\MessagesBag;
use Framework\Supports\Somoy;
use Framework\View\SectionManager;
use Framework\View\TemplateEngine;
use Framework\View\ViewContext;

Expand All @@ -57,6 +58,7 @@ public function register()
$this->app->singleton(Response::class);
$this->app->singleton(TemplateEngine::class);
$this->app->singleton(ViewContext::class);
$this->app->singleton(SectionManager::class);

if (class_exists(\Faker\Factory::class)) {
$this->app->singleton(\Faker\Factory::class, function () {
Expand Down
8 changes: 7 additions & 1 deletion src/Routing/SiteRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -370,12 +370,18 @@ public function handle_template_include(string $template, int $priority)
$resolved = $engine->resolve_path($path);

if ($resolved !== '') {
app(ViewContext::class)->prepare(
$view_context = app(ViewContext::class);
$view_context->prepare(
$result,
(string) $route->get_name(),
$resolved
);

$master_layout = $result->get_master_layout();
if ($master_layout !== null) {
$view_context->set_active_attribute('master_layout', $master_layout);
}

// WordPress includes the returned path immediately after this
// filter, which starts output. This is the last point at which
// the identifier cookie can still be sent.
Expand Down
138 changes: 138 additions & 0 deletions src/View/SectionManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php
/**
* Section manager for master layout template composition.
*
* Manages named content sections via output buffering, allowing child
* templates to define sections that master layouts can yield.
*
* @package Framework
* @subpackage View
* @since 2.2.0
*/
namespace Framework\View;

defined('ABSPATH') || exit;

use RuntimeException;

class SectionManager
{
/**
* Stored section contents keyed by name.
*
* @var array<string, string>
*
* @since 2.2.0
*/
protected $sections = [];

/**
* Name of the section currently being captured, or null.
*
* @var string|null
*
* @since 2.2.0
*/
protected $active_section = null;

/**
* Begin capturing a named section.
*
* @param string $name Section name.
*
* @return void
*
* @throws RuntimeException When a section is already being captured.
*
* @since 2.2.0
*/
public function start(string $name)
{
if ($this->active_section !== null) {
throw new RuntimeException(
sprintf(
'Cannot start section [%s] while section [%s] is already being captured.',
$name,
$this->active_section
)
);
}

$this->active_section = $name;

ob_start();
}

/**
* End the current section capture and store the buffered content.
*
* @return void
*
* @throws RuntimeException When no section is being captured.
*
* @since 2.2.0
*/
public function end()
{
if ($this->active_section === null) {
throw new RuntimeException('Cannot end section: no section is being captured.');
}

$this->sections[$this->active_section] = (string) ob_get_clean();
$this->active_section = null;
}

/**
* Get a section's content, or the default value if not defined.
*
* @param string $name Section name.
* @param string $default Fallback content when the section was not defined.
*
* @return string
*
* @since 2.2.0
*/
public function get(string $name, string $default = '')
{
return $this->sections[$name] ?? $default;
}

/**
* Check if a section has been defined.
*
* @param string $name Section name.
*
* @return bool
*
* @since 2.2.0
*/
public function has(string $name)
{
return isset($this->sections[$name]);
}

/**
* Clear all stored sections and reset active capture state.
*
* @return void
*
* @since 2.2.0
*/
public function clear()
{
$this->sections = [];
$this->active_section = null;
}

/**
* Get the name of the currently active section, or null.
*
* @return string|null
*
* @since 2.2.0
*/
public function get_active()
{
return $this->active_section;
}
}
54 changes: 50 additions & 4 deletions src/View/TemplateEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,17 @@ public function get_shared()
/**
* Render a view template to a string.
*
* @param string $view The view name in dot notation.
* @param array $data The data to pass to the view.
* @param bool $layout Whether to wrap with theme header/footer.
* @param string $view The view name in dot notation.
* @param array $data The data to pass to the view.
* @param bool|string $layout Layout mode: true for theme wrapping,
* false for no wrapping, or a master
* layout template name.
*
* @return string
*
* @since 1.0.0
*/
public function render(string $view, array $data = [], bool $layout = true)
public function render(string $view, array $data = [], $layout = true)
{
$path = $this->resolve_path($view);

Expand All @@ -100,6 +102,10 @@ public function render(string $view, array $data = [], bool $layout = true)
]);

try {
if (is_string($layout)) {
return $this->render_with_master_layout($path, $layout);
}

$content = $this->render_file($path);

if (!$layout) {
Expand All @@ -112,6 +118,46 @@ public function render(string $view, array $data = [], bool $layout = true)
}
}

/**
* Render a child template within a master layout.
*
* The child template is executed first, populating sections via
* SectionManager. Then the master layout is rendered, yielding
* those sections with render_section().
*
* @param string $child_path Absolute path to the child template.
* @param string $master_view Master layout template name in dot notation.
*
* @return string
*
* @throws RuntimeException When the master layout cannot be resolved.
*
* @since 2.2.0
*/
protected function render_with_master_layout(string $child_path, string $master_view)
{
$master_path = $this->resolve_path($master_view);

if ($master_path === '') {
throw new RuntimeException(sprintf('Master layout [%s] not found.', $master_view));
}

$sections = app(SectionManager::class);
$sections->clear();

// Render the child template – its start_section() / end_section()
// calls populate the SectionManager.
$this->render_file($child_path);

// Render the master layout, which calls render_section() to yield
// the captured sections.
$output = $this->render_file($master_path);

$sections->clear();

return $output;
}

/**
* Resolve a view name to an absolute file path.
*
Expand Down
37 changes: 31 additions & 6 deletions src/View/View.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@ class View
protected $data = [];

/**
* Whether to wrap the view in the theme layout.
* Layout wrapping mode for this view.
*
* @var bool
* - true: Wrap with the standard theme header/footer.
* - false: No theme wrapping (partial).
* - string: Wrap with the specified custom master layout template.
*
* @var bool|string
*
* @since 1.0.0
*/
Expand Down Expand Up @@ -74,15 +78,19 @@ public function partial()
/**
* Enable or set layout wrapping for this view.
*
* @param bool $enabled Whether layout wrapping is enabled.
* Pass `true` for standard theme wrapping, `false` to disable,
* or a template name string (e.g. 'site.account.master') for
* a custom master layout.
*
* @param bool|string $layout Layout mode or master template name.
*
* @return $this
*
* @since 1.0.0
*/
public function layout($enabled = true)
public function layout($layout = true)
{
$this->with_layout = (bool) $enabled;
$this->with_layout = is_string($layout) ? $layout : (bool) $layout;

return $this;
}
Expand Down Expand Up @@ -130,13 +138,30 @@ public function get_data()
/**
* Whether the view uses layout wrapping.
*
* Returns true for both standard theme layout (true) and
* custom master layout (string). Returns false only when
* layout is explicitly disabled.
*
* @return bool
*
* @since 1.0.0
*/
public function uses_layout()
{
return $this->with_layout;
return $this->with_layout !== false;
}

/**
* Get the master layout template name, if set.
*
* @return string|null Template name in dot notation, or null when
* using standard theme layout or no layout.
*
* @since 2.2.0
*/
public function get_master_layout()
{
return is_string($this->with_layout) ? $this->with_layout : null;
}

/**
Expand Down
19 changes: 19 additions & 0 deletions src/View/ViewContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,25 @@ public function get_active()
return $this->stack[count($this->stack) - 1];
}

/**
* Set an attribute on the topmost context frame.
*
* @param string $key Attribute key.
* @param mixed $value Attribute value.
*
* @return void
*
* @since 2.2.0
*/
public function set_active_attribute(string $key, $value)
{
if ($this->stack === []) {
return;
}

$this->stack[count($this->stack) - 1][$key] = $value;
}

/**
* Find the innermost stack frame that authorizes the current caller.
*
Expand Down
Loading
Loading