Layouts
Marko uses PHP attributes to compose page layouts from components. Every piece of a page — the layout shell, header, sidebar, content blocks — is a component. No layout inheritance, no template extends chains.
Creating a Layout
Section titled “Creating a Layout”A layout is just a component with slots. Define it with #[Component] and list the slots it provides:
<?php
declare(strict_types=1);
namespace App\Theme\Component;
use Marko\Layout\Attributes\Component;
#[Component( template: 'theme::layouts/default', slots: ['header', 'content', 'footer'],)]class DefaultLayout {}The template name theme::layouts/default resolves to resources/views/layouts/default.latte with the Latte driver, or resources/views/layouts/default.twig with the Twig driver, in the theme module; the view driver adds the extension. The template marks where child components render. In Latte, use {slot name/} tags:
<!doctype html><html><head><title>My App</title></head><body> <header>{slot header/}</header> <main>{slot content/}</main> <footer>{slot footer/}</footer></body></html>In Twig, use the slot() function. It prints the slot’s rendered HTML unescaped:
<!doctype html><html><head><title>My App</title></head><body> <header>{{ slot('header') }}</header> <main>{{ slot('content') }}</main> <footer>{{ slot('footer') }}</footer></body></html>Attaching a Layout to a Controller
Section titled “Attaching a Layout to a Controller”Use #[Layout] on a controller class to assign a layout to all its actions:
<?php
declare(strict_types=1);
namespace App\Blog\Controller;
use App\Theme\Component\DefaultLayout;use Marko\Layout\Attributes\Layout;use Marko\Routing\Attributes\Get;
#[Layout(component: DefaultLayout::class)]class PostController{ #[Get('/posts')] public function index(): void {}
#[Get('/posts/{id}')] public function show(int $id): void {}}Controllers with #[Layout] don’t return responses — the layout system assembles the page from components. The controller action still runs (for side effects like authorization), but its return value is ignored.
Method-Level Override
Section titled “Method-Level Override”A method-level #[Layout] overrides the class-level one:
#[Layout(component: DefaultLayout::class)]class PostController{ #[Get('/posts/{id}/print')] #[Layout(component: PrintLayout::class)] public function print(int $id): void { // Uses PrintLayout instead of DefaultLayout }}Adding Page Components
Section titled “Adding Page Components”Components declare which slot they fill and which pages they appear on:
<?php
declare(strict_types=1);
namespace App\Blog\Component;
use Marko\Layout\Attributes\Component;
#[Component( template: 'blog::components/post-list', handle: [PostController::class, 'index'], slot: 'content',)]class PostList{ public function data(): array { return ['posts' => ['First Post', 'Second Post']]; }}The data() method provides template variables. Route parameters are injected by name automatically:
#[Component( template: 'blog::components/post-detail', handle: [PostController::class, 'show'], slot: 'content',)]class PostDetail{ public function data(int $id): array { return ['postId' => $id]; }}Handle Matching
Section titled “Handle Matching”The handle parameter controls which pages a component appears on:
| Handle | Matches |
|---|---|
'default' | Every page |
'posts_*' | All routes whose handle starts with posts_ |
'posts_post_show' | Only PostController::show (exact match) |
[PostController::class, 'show'] | Same, using class reference |
Handles are auto-generated from routes: {first-segment}_{controller}_{method}. The route /posts/{id} on PostController::show becomes posts_post_show.
A trailing _* enables prefix matching — 'posts_*' catches posts_post_index, posts_post_show, posts_comment_index, etc. Without the wildcard, handles are matched exactly.
Sorting Components
Section titled “Sorting Components”Multiple components in the same slot are sorted by sortOrder (lower renders first). The default is 0, and negative values are supported:
#[Component( slot: 'sidebar', sortOrder: 10,)] // Renders firstclass SearchWidget {}
#[Component( slot: 'sidebar', sortOrder: 20,)] // Renders secondclass RecentPosts {}For explicit ordering, use before and after — they take priority over sortOrder:
#[Component( slot: 'sidebar', sortOrder: 50, before: RecentPosts::class,)]class AdWidget {}Nested Slots
Section titled “Nested Slots”Components can define their own sub-slots for deeper composition:
#[Component( template: 'blog::components/tabs', handle: [PostController::class, 'show'], slot: 'content', slots: ['tab.details', 'tab.reviews'],)]class PostTabs {}<div class="tabs"> <div class="tab-panel">{slot tab.details/}</div> <div class="tab-panel">{slot tab.reviews/}</div></div>The same template in Twig:
<div class="tabs"> <div class="tab-panel">{{ slot('tab.details') }}</div> <div class="tab-panel">{{ slot('tab.reviews') }}</div></div>Other components target the sub-slots using dot-notation:
#[Component( template: 'blog::components/reviews', handle: 'default', slot: 'tab.reviews',)]class ReviewsList{ public function data(int $id): array { return ['postId' => $id]; }}Parent components render first, then their sub-slots are filled — this happens automatically.
Cross-Module Extensibility
Section titled “Cross-Module Extensibility”Any module can inject components into any page. A shipping module can add a component to the product page without modifying the product module:
#[Component( template: 'shipping::components/estimate', handle: 'products_product_show', slot: 'content',)]class ShippingEstimate{ public function data(int $id): array { return ['productId' => $id]; }}To remove or reorder another module’s component, use an #[After] plugin on ComponentCollectorInterface::collect():
use Marko\Core\Attributes\After;use Marko\Core\Attributes\Plugin;use Marko\Layout\ComponentCollection;use Marko\Layout\ComponentCollectorInterface;
#[Plugin(target: ComponentCollectorInterface::class)]class RemoveVendorWidgetPlugin{ #[After] public function collect(ComponentCollection $result): ComponentCollection { $result->remove(VendorWidget::class);
return $result; }}Next Steps
Section titled “Next Steps”- Routing — define the routes that layouts attach to
- Error handling — handle exceptions in components
- Layout package reference — full API details