From fe083f8cbbfafad5031f54675c4839708ba68230 Mon Sep 17 00:00:00 2001
From: "Md.Harun-Ur-Rashid"
Date: Tue, 18 Aug 2026 08:12:28 +0600
Subject: [PATCH 1/4] feat: implement master layout system with SectionManager
and custom layout support
---
src/CoreServiceProvider.php | 2 +
src/Routing/SiteRouter.php | 9 +-
src/View/SectionManager.php | 138 ++++++++++++++++++
src/View/TemplateEngine.php | 54 ++++++-
src/View/View.php | 37 ++++-
src/View/ViewContext.php | 19 +++
src/View/layout-wrapper.php | 35 ++++-
src/helpers.php | 59 ++++++++
tests/Unit/View/MasterLayoutTest.php | 188 +++++++++++++++++++++++++
tests/Unit/View/SectionManagerTest.php | 118 ++++++++++++++++
10 files changed, 646 insertions(+), 13 deletions(-)
create mode 100644 src/View/SectionManager.php
create mode 100644 tests/Unit/View/MasterLayoutTest.php
create mode 100644 tests/Unit/View/SectionManagerTest.php
diff --git a/src/CoreServiceProvider.php b/src/CoreServiceProvider.php
index 9be9f20..c3a9492 100644
--- a/src/CoreServiceProvider.php
+++ b/src/CoreServiceProvider.php
@@ -26,6 +26,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;
@@ -49,6 +50,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 () {
diff --git a/src/Routing/SiteRouter.php b/src/Routing/SiteRouter.php
index bfdce77..f60309b 100644
--- a/src/Routing/SiteRouter.php
+++ b/src/Routing/SiteRouter.php
@@ -362,12 +362,19 @@ 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);
+ }
+
if ($result->uses_layout()) {
return $engine->layout_wrapper_path();
}
diff --git a/src/View/SectionManager.php b/src/View/SectionManager.php
new file mode 100644
index 0000000..1e0177a
--- /dev/null
+++ b/src/View/SectionManager.php
@@ -0,0 +1,138 @@
+
+ *
+ * @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;
+ }
+}
diff --git a/src/View/TemplateEngine.php b/src/View/TemplateEngine.php
index 1206318..2c6a850 100644
--- a/src/View/TemplateEngine.php
+++ b/src/View/TemplateEngine.php
@@ -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);
@@ -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) {
@@ -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.
*
diff --git a/src/View/View.php b/src/View/View.php
index b5f119f..99200fe 100644
--- a/src/View/View.php
+++ b/src/View/View.php
@@ -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
*/
@@ -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;
}
@@ -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;
}
/**
diff --git a/src/View/ViewContext.php b/src/View/ViewContext.php
index 05008ab..d070488 100644
--- a/src/View/ViewContext.php
+++ b/src/View/ViewContext.php
@@ -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.
*
diff --git a/src/View/layout-wrapper.php b/src/View/layout-wrapper.php
index 2082119..4ac4ca4 100644
--- a/src/View/layout-wrapper.php
+++ b/src/View/layout-wrapper.php
@@ -1,6 +1,7 @@
resolve_path($active['master_layout']);
+
+ if ($master_path === '') {
+ return;
+ }
+
+ $sections = app(SectionManager::class);
+ $sections->clear();
+
+ // Execute the child template to populate sections.
+ ob_start();
+ require $path;
+ ob_end_clean();
+
+ // Render the master layout which yields the captured sections.
+ ob_start();
+ require $master_path;
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Assembled layout HTML; dynamic data is escaped in view templates via esc_*.
+ echo (string) ob_get_clean();
+
+ $sections->clear();
+
+ return;
+}
+
+// Standard theme layout: wrap with header/footer.
ob_start();
require $path;
$content = (string) ob_get_clean();
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Assembled layout HTML; dynamic data is escaped in view templates via esc_*.
-echo app(TemplateEngine::class)->wrap_layout($content);
+echo $engine->wrap_layout($content);
diff --git a/src/helpers.php b/src/helpers.php
index 92cc959..ee24660 100644
--- a/src/helpers.php
+++ b/src/helpers.php
@@ -333,6 +333,65 @@ function include_view(string $view, array $data = [])
}
}
+if (!function_exists('Framework\start_section')) {
+ /**
+ * Begin capturing a named section for a master layout.
+ *
+ * @param string $name Section name.
+ *
+ * @return void
+ *
+ * @since 2.2.0
+ * @throws \RuntimeException When a section is already being captured.
+ */
+ function start_section(string $name)
+ {
+ app(\Framework\View\SectionManager::class)->start($name);
+ }
+}
+
+if (!function_exists('Framework\end_section')) {
+ /**
+ * End the current section capture.
+ *
+ * @return void
+ *
+ * @since 2.2.0
+ * @throws \RuntimeException When no section is being captured.
+ */
+ function end_section()
+ {
+ app(\Framework\View\SectionManager::class)->end();
+ }
+}
+
+if (!function_exists('Framework\render_section')) {
+ /**
+ * Render a named section in a master layout template.
+ *
+ * Echoes the captured section content, or the default value
+ * if the section was not defined by the child template.
+ *
+ * @param string $name Section name.
+ * @param string $default Fallback content when the section was not defined.
+ *
+ * @return void
+ *
+ * @since 2.2.0
+ */
+ function render_section(string $name, string $default = '')
+ {
+ $content = app(\Framework\View\SectionManager::class)->get($name, $default);
+
+ if ('' === $content) {
+ return;
+ }
+
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Section content is trusted template HTML; dynamic values are escaped within the templates.
+ echo $content;
+ }
+}
+
if (!function_exists('Framework\redirect')) {
/**
* Create a redirect response.
diff --git a/tests/Unit/View/MasterLayoutTest.php b/tests/Unit/View/MasterLayoutTest.php
new file mode 100644
index 0000000..998d3e1
--- /dev/null
+++ b/tests/Unit/View/MasterLayoutTest.php
@@ -0,0 +1,188 @@
+views = sys_get_temp_dir() . '/framework-master-layout-' . uniqid();
+ mkdir($this->views . '/site/account', 0777, true);
+
+ $app = $this->bootstrap_application();
+ $app->use_view_path($this->views);
+ $app->instance(TemplateEngine::class, new TemplateEngine());
+ $app->instance(ViewContext::class, new ViewContext());
+ $app->instance(SectionManager::class, new SectionManager());
+ }
+
+ protected function tearDown(): void
+ {
+ $this->remove_directory($this->views);
+
+ parent::tearDown();
+ }
+
+ public function test_view_layout_accepts_string(): void
+ {
+ $view = view('shop.product', ['id' => 1]);
+
+ $view->layout('site.account.master');
+
+ $this->assertTrue($view->uses_layout());
+ $this->assertSame('site.account.master', $view->get_master_layout());
+ }
+
+ public function test_view_layout_true_returns_null_master(): void
+ {
+ $view = view('shop.product');
+
+ $view->layout(true);
+
+ $this->assertTrue($view->uses_layout());
+ $this->assertNull($view->get_master_layout());
+ }
+
+ public function test_view_layout_false_disables_layout(): void
+ {
+ $view = view('shop.product');
+
+ $view->layout(false);
+
+ $this->assertFalse($view->uses_layout());
+ $this->assertNull($view->get_master_layout());
+ }
+
+ public function test_partial_overrides_string_layout(): void
+ {
+ $view = view('shop.product');
+ $view->layout('site.account.master');
+ $view->partial();
+
+ $this->assertFalse($view->uses_layout());
+ $this->assertNull($view->get_master_layout());
+ }
+
+ public function test_render_with_master_layout_composes_sections(): void
+ {
+ // Child template: defines title and content sections.
+ file_put_contents(
+ $this->views . '/site/account/dashboard.php',
+ 'Welcome
"; \Framework\end_section();'
+ );
+
+ // Master layout: yields sections.
+ file_put_contents(
+ $this->views . '/site/account/master.php',
+ ''
+ . ''
+ );
+
+ $engine = app(TemplateEngine::class);
+ $output = $engine->render('site.account.dashboard', [], 'site.account.master');
+
+ $this->assertSame(
+ 'Welcome
',
+ $output
+ );
+ }
+
+ public function test_render_with_master_layout_uses_default_for_missing_section(): void
+ {
+ // Child template: only defines content.
+ file_put_contents(
+ $this->views . '/site/account/dashboard.php',
+ 'views . '/site/account/master.php',
+ ''
+ . ''
+ . ''
+ );
+
+ $engine = app(TemplateEngine::class);
+ $output = $engine->render('site.account.dashboard', [], 'site.account.master');
+
+ $this->assertSame(
+ 'My Account
Body',
+ $output
+ );
+ }
+
+ public function test_render_with_master_layout_throws_when_master_not_found(): void
+ {
+ file_put_contents($this->views . '/site/account/dashboard.php', 'expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Master layout [site.account.nonexistent] not found.');
+
+ $engine = app(TemplateEngine::class);
+ $engine->render('site.account.dashboard', [], 'site.account.nonexistent');
+ }
+
+ public function test_sections_are_cleared_after_render(): void
+ {
+ file_put_contents(
+ $this->views . '/site/account/dashboard.php',
+ 'views . '/site/account/master.php',
+ ''
+ );
+
+ $engine = app(TemplateEngine::class);
+ $engine->render('site.account.dashboard', [], 'site.account.master');
+
+ // Sections should be cleared after render.
+ $sections = app(SectionManager::class);
+ $this->assertFalse($sections->has('title'));
+ }
+
+ protected function remove_directory(string $directory): void
+ {
+ if (!is_dir($directory)) {
+ return;
+ }
+
+ $items = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
+ \RecursiveIteratorIterator::CHILD_FIRST
+ );
+
+ foreach ($items as $item) {
+ if ($item->isDir()) {
+ rmdir($item->getPathname());
+ continue;
+ }
+
+ unlink($item->getPathname());
+ }
+
+ rmdir($directory);
+ }
+}
diff --git a/tests/Unit/View/SectionManagerTest.php b/tests/Unit/View/SectionManagerTest.php
new file mode 100644
index 0000000..ff5a701
--- /dev/null
+++ b/tests/Unit/View/SectionManagerTest.php
@@ -0,0 +1,118 @@
+manager = new SectionManager();
+ }
+
+ public function test_start_and_end_captures_section_content(): void
+ {
+ $this->manager->start('title');
+ echo 'Hello World';
+ $this->manager->end();
+
+ $this->assertSame('Hello World', $this->manager->get('title'));
+ }
+
+ public function test_get_returns_default_when_section_not_defined(): void
+ {
+ $this->assertSame('', $this->manager->get('missing'));
+ $this->assertSame('Fallback', $this->manager->get('missing', 'Fallback'));
+ }
+
+ public function test_has_returns_true_for_defined_section(): void
+ {
+ $this->assertFalse($this->manager->has('title'));
+
+ $this->manager->start('title');
+ echo 'Content';
+ $this->manager->end();
+
+ $this->assertTrue($this->manager->has('title'));
+ }
+
+ public function test_clear_removes_all_sections(): void
+ {
+ $this->manager->start('title');
+ echo 'Content';
+ $this->manager->end();
+
+ $this->assertTrue($this->manager->has('title'));
+
+ $this->manager->clear();
+
+ $this->assertFalse($this->manager->has('title'));
+ $this->assertNull($this->manager->get_active());
+ }
+
+ public function test_start_throws_when_section_already_active(): void
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Cannot start section [content] while section [title] is already being captured.');
+
+ $this->manager->start('title');
+ $this->manager->start('content');
+ }
+
+ public function test_end_throws_when_no_section_active(): void
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Cannot end section: no section is being captured.');
+
+ $this->manager->end();
+ }
+
+ public function test_get_active_returns_active_section_name(): void
+ {
+ $this->assertNull($this->manager->get_active());
+
+ $this->manager->start('content');
+ $this->assertSame('content', $this->manager->get_active());
+
+ // Clean up the output buffer started by start().
+ $this->manager->end();
+ $this->assertNull($this->manager->get_active());
+ }
+
+ public function test_multiple_sections_can_be_captured_sequentially(): void
+ {
+ $this->manager->start('title');
+ echo 'Page Title';
+ $this->manager->end();
+
+ $this->manager->start('content');
+ echo 'Page Content';
+ $this->manager->end();
+
+ $this->assertSame('Page Title', $this->manager->get('title'));
+ $this->assertSame('Page Content', $this->manager->get('content'));
+ }
+
+ public function test_later_section_overrides_earlier_with_same_name(): void
+ {
+ $this->manager->start('title');
+ echo 'First';
+ $this->manager->end();
+
+ $this->manager->start('title');
+ echo 'Second';
+ $this->manager->end();
+
+ $this->assertSame('Second', $this->manager->get('title'));
+ }
+}
From 9a5a5eefa50762c8eab3aaa455c7ba28f5aec887 Mon Sep 17 00:00:00 2001
From: "Md.Harun-Ur-Rashid"
Date: Wed, 26 Aug 2026 15:25:35 +0600
Subject: [PATCH 2/4] applied request changes
---
src/helpers.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/helpers.php b/src/helpers.php
index 3d8359d..f8d5fb7 100644
--- a/src/helpers.php
+++ b/src/helpers.php
@@ -529,7 +529,7 @@ function render_section(string $name, string $default = '')
{
$content = app(\Framework\View\SectionManager::class)->get($name, $default);
- if ('' === $content) {
+ if ($content === '') {
return;
}
From 039c7586364522f9272fddcf900d7d801ffed134 Mon Sep 17 00:00:00 2001
From: "Md.Harun-Ur-Rashid"
Date: Wed, 26 Aug 2026 15:37:44 +0600
Subject: [PATCH 3/4] test: add unit tests for SectionManager and MasterLayout
rendering functionality
---
tests/Unit/View/MasterLayoutTest.php | 6 ++++++
tests/Unit/View/SectionManagerTest.php | 6 ++++++
2 files changed, 12 insertions(+)
diff --git a/tests/Unit/View/MasterLayoutTest.php b/tests/Unit/View/MasterLayoutTest.php
index 998d3e1..161d6f4 100644
--- a/tests/Unit/View/MasterLayoutTest.php
+++ b/tests/Unit/View/MasterLayoutTest.php
@@ -12,6 +12,12 @@
use function Framework\app;
use function Framework\view;
+/**
+ * Class MasterLayoutTest.
+ *
+ * Run the testcase by running this command:
+ * vendor/bin/phpunit --prepend tests/prepend.php --filter=MasterLayoutTest --testdox
+ */
class MasterLayoutTest extends TestCase
{
/**
diff --git a/tests/Unit/View/SectionManagerTest.php b/tests/Unit/View/SectionManagerTest.php
index ff5a701..497e42b 100644
--- a/tests/Unit/View/SectionManagerTest.php
+++ b/tests/Unit/View/SectionManagerTest.php
@@ -6,6 +6,12 @@
use Framework\View\SectionManager;
use RuntimeException;
+/**
+ * Class SectionManagerTest.
+ *
+ * Run the testcase by running this command:
+ * vendor/bin/phpunit --prepend tests/prepend.php --filter=SectionManagerTest --testdox
+ */
class SectionManagerTest extends TestCase
{
/**
From 9f61bd292a560c7cd7c0ab6741f9b12d7989d9aa Mon Sep 17 00:00:00 2001
From: "Md.Harun-Ur-Rashid"
Date: Wed, 26 Aug 2026 15:45:41 +0600
Subject: [PATCH 4/4] fix: Test code or tested code did not (only) close its
own output buffers
---
tests/Unit/View/SectionManagerTest.php | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/tests/Unit/View/SectionManagerTest.php b/tests/Unit/View/SectionManagerTest.php
index 497e42b..0305ddb 100644
--- a/tests/Unit/View/SectionManagerTest.php
+++ b/tests/Unit/View/SectionManagerTest.php
@@ -26,6 +26,15 @@ protected function setUp(): void
$this->manager = new SectionManager();
}
+ protected function tearDown(): void
+ {
+ while ($this->manager !== null && $this->manager->get_active() !== null) {
+ $this->manager->end();
+ }
+
+ parent::tearDown();
+ }
+
public function test_start_and_end_captures_section_content(): void
{
$this->manager->start('title');
@@ -72,7 +81,12 @@ public function test_start_throws_when_section_already_active(): void
$this->expectExceptionMessage('Cannot start section [content] while section [title] is already being captured.');
$this->manager->start('title');
- $this->manager->start('content');
+
+ try {
+ $this->manager->start('content');
+ } finally {
+ $this->manager->end();
+ }
}
public function test_end_throws_when_no_section_active(): void