studiometa / foehn
đ A modern WordPress framework powered by Tempest, featuring attribute-based auto-discovery for hooks, post types, blocks, and more.
Installs: 10
Dependents: 1
Suggesters: 0
Security: 0
Stars: 4
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/studiometa/foehn
Requires
- php: ^8.4
- studiometa/twig-toolkit: ^2.1
- tempest/framework: ^2.14
- timber/timber: ^2.0
- vlucas/phpdotenv: ^5.6
Suggests
- studiometa/webpack-config: Required for WebpackManifest asset helper
README
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.
Requirements
- PHP 8.5+
- WordPress 6.4+
- Composer
Installation
composer require studiometa/foehn
Quick Start
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.
Features
Hooks
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; } }
Post Types
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; } }
Taxonomies
<?php use Studiometa\Foehn\Attributes\AsTaxonomy; #[AsTaxonomy( name: 'product_category', singular: 'Category', plural: 'Categories', postTypes: ['product'], hierarchical: true, )] final class ProductCategory {}
ACF Blocks
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); } }
Typed DTOs for Block 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.
Native Gutenberg Blocks with Interactivity API
<?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 { // ... } }
Context Providers
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; } }
Template Controllers
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); } }
Block Patterns
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'), ]; } }
REST API Routes
<?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' => []]; } }
Shortcodes
<?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) ); } }
CLI Commands
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
Dependency Injection
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]); } }
Configuration
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', ], ];
Documentation
For complete documentation, see the Føhn documentation.
License
MIT License. See LICENSE for details.