A modern WordPress framework powered by Tempest Framework, featuring attribute-based auto-discovery for hooks, post types, blocks, and more.
Note This package is part of the Føhn Framework monorepo. Please report issues and submit pull requests in the main repository.
- PHP 8.5+
- WordPress 6.4+
- Composer
composer require studiometa/foehn
Bootstrap Føhn in your theme's functions.php:
<?php
declare(strict_types=1);
use Studiometa\Foehn\Kernel;
Kernel::boot(__DIR__ . '/app');
That's it! Føhn will automatically discover and register all your classes in the app/ directory.
Register WordPress actions and filters directly on your methods:
<?php
use Studiometa\Foehn\Attributes\AsAction;
use Studiometa\Foehn\Attributes\AsFilter;
final class ThemeHooks
{
#[AsAction('after_setup_theme')]
public function setup(): void
{
add_theme_support('post-thumbnails');
add_theme_support('title-tag');
}
#[AsFilter('excerpt_length')]
public function excerptLength(): int
{
return 30;
}
}
Define custom post types as classes with automatic Timber classmap integration:
<?php
use Studiometa\Foehn\Attributes\AsPostType;
use Studiometa\Foehn\Models\Post;
#[AsPostType(
name: 'product',
singular: 'Product',
plural: 'Products',
public: true,
hasArchive: true,
menuIcon: 'dashicons-cart',
supports: ['title', 'editor', 'thumbnail'],
)]
final class Product extends Post
{
public function price(): ?float
{
return $this->meta('price') ? (float) $this->meta('price') : null;
}
}
<?php
use Studiometa\Foehn\Attributes\AsTaxonomy;
#[AsTaxonomy(
name: 'product_category',
singular: 'Category',
plural: 'Categories',
postTypes: ['product'],
hierarchical: true,
)]
final class ProductCategory {}
Create ACF blocks with dependency injection:
<?php
use Studiometa\Foehn\Attributes\AsAcfBlock;
use Studiometa\Foehn\Contracts\AcfBlockInterface;
use Studiometa\Foehn\Contracts\ViewEngineInterface;
use StoutLogic\AcfBuilder\FieldsBuilder;
#[AsAcfBlock(
name: 'hero',
title: 'Hero Banner',
category: 'layout',
icon: 'cover-image',
)]
final readonly class HeroBlock implements AcfBlockInterface
{
public function __construct(
private ViewEngineInterface $view,
) {}
public static function fields(): FieldsBuilder
{
return (new FieldsBuilder('hero'))
->addImage('background')
->addWysiwyg('content')
->addLink('cta');
}
public function compose(array $block, array $fields): array
{
return [
'background' => $fields['background'] ?? null,
'content' => $fields['content'] ?? '',
'cta' => $fields['cta'] ?? null,
];
}
public function render(array $context, bool $isPreview = false): string
{
return $this->view->render('blocks/hero', $context);
}
}
Instead of returning plain arrays from compose(), use typed DTOs for autocompletion and type safety:
<?php
use Studiometa\Foehn\Concerns\HasToArray;
use Studiometa\Foehn\Contracts\Arrayable;
use Studiometa\Foehn\Data\ImageData;
use Studiometa\Foehn\Data\LinkData;
final readonly class HeroContext implements Arrayable
{
use HasToArray;
public function __construct(
public string $title,
public ?ImageData $background = null,
public ?LinkData $cta = null,
public string $height = 'medium',
) {}
}
Then use it in your block:
public function compose(array $block, array $fields): HeroContext
{
return new HeroContext(
title: $fields['title'] ?? '',
background: ImageData::fromAttachmentId($fields['background'] ?? null),
cta: LinkData::fromAcf($fields['cta_link'] ?? null),
height: $fields['height'] ?? 'medium',
);
}
The DTO is automatically flattened to a snake_case array before reaching render() and Twig templates. Property names like imageUrl become image_url in the template context.
Built-in DTOs:
| DTO | Description | Factory |
|---|---|---|
LinkData |
Link/button fields | LinkData::fromAcf($acfLink) |
ImageData |
Image/attachment fields | ImageData::fromAttachmentId($id) |
SpacingData |
Spacing fields | SpacingData::fromAcf($fields, $prefix) |
All compose() methods on AcfBlockInterface, BlockInterface and BlockPatternInterface accept either array or Arrayable return types.
<?php
use Studiometa\Foehn\Attributes\AsBlock;
use Studiometa\Foehn\Contracts\InteractiveBlockInterface;
use WP_Block;
#[AsBlock(
name: 'theme/accordion',
title: 'Accordion',
category: 'widgets',
interactivity: true,
)]
final readonly class AccordionBlock implements InteractiveBlockInterface
{
public static function attributes(): array
{
return [
'items' => ['type' => 'array', 'default' => []],
'allowMultiple' => ['type' => 'boolean', 'default' => false],
];
}
public static function initialState(): array
{
return [];
}
public function initialContext(array $attributes): array
{
return [
'openItems' => [],
'allowMultiple' => $attributes['allowMultiple'] ?? false,
];
}
public function render(array $attributes, string $content, WP_Block $block): string
{
// ...
}
}
Inject data into specific templates:
<?php
use Studiometa\Foehn\Attributes\AsContextProvider;
use Studiometa\Foehn\Contracts\ContextProviderInterface;
#[AsContextProvider(templates: ['single', 'single-*'])]
final readonly class SingleContext implements ContextProviderInterface
{
public function provide(array $context): array
{
$post = $context['post'] ?? null;
$context['reading_time'] = $this->calculateReadingTime($post->content());
$context['related_posts'] = $this->getRelatedPosts($post);
return $context;
}
}
Handle template rendering with full control:
<?php
use Studiometa\Foehn\Attributes\AsTemplateController;
use Studiometa\Foehn\Contracts\ViewEngineInterface;
use Timber\Timber;
#[AsTemplateController('single', 'single-*')]
final readonly class SingleController
{
public function __construct(
private ViewEngineInterface $view,
) {}
public function __invoke(): string
{
$context = Timber::context();
return $this->view->renderFirst([
"pages/single-{$context['post']->post_type}",
'pages/single',
], $context);
}
}
Register block patterns with Twig templates:
<?php
use Studiometa\Foehn\Attributes\AsBlockPattern;
use Studiometa\Foehn\Contracts\BlockPatternInterface;
#[AsBlockPattern(
name: 'theme/hero-full-width',
title: 'Hero Full Width',
categories: ['heroes'],
)]
final readonly class HeroFullWidth implements BlockPatternInterface
{
public function compose(): array
{
return [
'heading' => __('Welcome', 'theme'),
'cta_text' => __('Learn more', 'theme'),
];
}
}
<?php
use Studiometa\Foehn\Attributes\AsRestRoute;
use WP_REST_Request;
final class ProductsApi
{
#[AsRestRoute('theme/v1', '/products', methods: ['GET'])]
public function index(WP_REST_Request $request): array
{
return ['products' => []];
}
#[AsRestRoute('theme/v1', '/products/(?P<id>\d+)', methods: ['GET'])]
public function show(WP_REST_Request $request): array
{
return ['product' => []];
}
}
<?php
use Studiometa\Foehn\Attributes\AsShortcode;
final class ButtonShortcode
{
#[AsShortcode('button')]
public function render(array $atts, ?string $content = null): string
{
$atts = shortcode_atts([
'url' => '#',
'style' => 'primary',
], $atts);
return sprintf(
'<a href="%s" class="btn btn--%s">%s</a>',
esc_url($atts['url']),
esc_attr($atts['style']),
esc_html($content)
);
}
}
Føhn provides WP-CLI commands for scaffolding:
# Generate a new block
wp foehn make:block Hero --acf
# Generate a new post type
wp foehn make:post-type Product
# Generate a new taxonomy
wp foehn make:taxonomy ProductCategory --post-types=product
# Clear discovery cache
wp foehn discovery:clear
# Warm discovery cache
wp foehn discovery:cache
All discovered classes support constructor injection via Tempest's container:
<?php
use Studiometa\Foehn\Attributes\AsAction;
final readonly class NewsletterHooks
{
public function __construct(
private NewsletterService $newsletter,
private LoggerInterface $logger,
) {}
#[AsAction('user_register')]
public function onUserRegister(int $userId): void
{
$user = get_user_by('id', $userId);
$this->newsletter->subscribe($user->user_email);
$this->logger->info('User subscribed to newsletter', ['user_id' => $userId]);
}
}
Føhn can be configured in your theme:
<?php
// config/foehn.php
return [
'cache' => [
'enabled' => wp_get_environment_type() === 'production',
'path' => get_template_directory() . '/storage/cache',
],
'views' => [
'paths' => ['templates'],
],
'blocks' => [
'namespace' => 'theme',
],
];
For complete documentation, see the Føhn documentation.
MIT License. See LICENSE for details.