Skip to content

marko/layout

Everything is a component. Layouts, page sections, and widgets are composed via #[Component] attributes and assembled automatically per route. Cross-module injection uses the same Plugin/Preference system as the rest of Marko.

Terminal window
composer require marko/layout

A layout component is the page root. It declares the top-level slots that other components fill:

DefaultLayout.php
use Marko\Layout\Attributes\Component;
#[Component(
template: 'layouts/default.html',
slots: ['header', 'content', 'footer'],
)]
class DefaultLayout {}
layouts/default.html
<!doctype html>
<html>
<body>
{slot header}{/slot}
{slot content}{/slot}
{slot footer}{/slot}
</body>
</html>
ProductController.php
use Marko\Layout\Attributes\Layout;
use Marko\Routing\Attributes\Get;
#[Layout(component: DefaultLayout::class)]
class ProductController
{
#[Get('/products/{id}')]
public function show(int $id): void
{
// Side effects only — data comes from component data() methods
}
}

Components declare which slot they render into and which routes they appear on:

ProductContent.php
use Marko\Layout\Attributes\Component;
#[Component(
template: 'components/product-content.html',
handle: 'products_*',
slot: 'content', // matches all products_* routes
)]
class ProductContent
{
public function data(int $id): array
{
return ['productId' => $id];
}
}

Route parameters are injected into data() by name automatically.

Components can define their own sub-slots for deeper composition:

#[Component(
template: 'components/tabs.html',
handle: 'products_product_show',
slot: 'content',
slots: ['tab.details', 'tab.reviews'],
)]
class ProductTabs {}

Child components target these sub-slots using dot-notation:

#[Component(
template: 'components/reviews.html',
handle: 'default',
slot: 'tab.reviews',
)]
class ReviewsTab {}

Any module can modify the component collection via 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 CustomizeLayoutPlugin
{
#[After]
public function collect(ComponentCollection $result): ComponentCollection
{
$result->remove(OtherComponent::class);
$result->move(MyComponent::class, 'sidebar', sortOrder: 5);
return $result;
}
}

Handles control which pages a component appears on:

Handle valueMatches
'default'Every page
'products_*'All routes whose handle starts with products_
'products_product_show'Only ProductController::show (exact match)
[ProductController::class, 'show']Resolved to the exact handle

Register LayoutMiddleware in your application middleware stack. When a controller has a #[Layout] attribute, it delegates rendering to LayoutProcessor automatically.

Override any component via Preferences to swap implementations without modifying vendor code.

// Attributes
#[Component(
template: string,
handle: string|array,
slot: ?string,
slots: array,
sortOrder: int,
before: ?string,
after: ?string,
)]
#[Layout(component: string)]
// ComponentCollection
$componentCollection->add(ComponentDefinition $definition): void
$componentCollection->remove(string $className): void
$componentCollection->get(string $className): ComponentDefinition
$componentCollection->move(string $className, string $slot, ?int $sortOrder = null): void
$componentCollection->forSlot(string $slot): array
$componentCollection->count(): int
// LayoutProcessor
$processor->process(string $controllerClass, string $action, string $routePath, array $routeParameters, Request $request): Response