Skip to content

🍃 A modern WordPress framework powered by Tempest — auto-discovery for hooks, post types, blocks, and more.

Notifications You must be signed in to change notification settings

studiometa/foehn

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🍃 Føhn

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.

About

🍃 A modern WordPress framework powered by Tempest — auto-discovery for hooks, post types, blocks, and more.

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages