Post Types
Foehn uses #[AsPostType] to register custom post types with Timber integration.
Basic Post Type
php
<?php
// app/Models/Product.php
namespace App\Models;
use Studiometa\Foehn\Attributes\AsPostType;
use Timber\Post;
#[AsPostType(
name: 'product',
singular: 'Product',
plural: 'Products',
)]
final class Product extends Post
{
}This registers a post type with sensible defaults and automatically maps it in Timber's classmap.
Full Configuration
php
<?php
namespace App\Models;
use Studiometa\Foehn\Attributes\AsPostType;
use Timber\Post;
#[AsPostType(
name: 'product',
singular: 'Product',
plural: 'Products',
public: true,
hasArchive: true,
showInRest: true,
menuIcon: 'dashicons-cart',
supports: ['title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'],
taxonomies: ['product_category', 'product_tag'],
rewriteSlug: 'shop',
)]
final class Product extends Post
{
}Custom Methods
Add business logic directly to your post type class:
php
<?php
namespace App\Models;
use Studiometa\Foehn\Attributes\AsPostType;
use Timber\Post;
#[AsPostType(
name: 'product',
singular: 'Product',
plural: 'Products',
hasArchive: true,
menuIcon: 'dashicons-cart',
)]
final class Product extends Post
{
/**
* Get the product price.
*/
public function price(): ?float
{
$price = $this->meta('price');
return $price ? (float) $price : null;
}
/**
* Get the formatted price.
*/
public function formattedPrice(): string
{
$price = $this->price();
return $price ? sprintf('$%.2f', $price) : 'Price on request';
}
/**
* Check if the product is on sale.
*/
public function isOnSale(): bool
{
return (bool) $this->meta('on_sale');
}
/**
* Get the sale price if on sale.
*/
public function salePrice(): ?float
{
if (!$this->isOnSale()) {
return null;
}
$salePrice = $this->meta('sale_price');
return $salePrice ? (float) $salePrice : null;
}
/**
* Get related products.
*
* @return Product[]
*/
public function relatedProducts(int $limit = 4): array
{
$categories = $this->terms('product_category');
if (empty($categories)) {
return [];
}
return \Timber\Timber::get_posts([
'post_type' => 'product',
'posts_per_page' => $limit,
'post__not_in' => [$this->ID],
'tax_query' => [
[
'taxonomy' => 'product_category',
'terms' => wp_list_pluck($categories, 'term_id'),
],
],
]);
}
}Using in Templates
Your custom methods are available in Twig templates:
twig
{# views/single-product.twig #}
{% extends 'base.twig' %}
{% block content %}
<article class="product">
<h1>{{ post.title }}</h1>
{% if post.thumbnail %}
<img src="{{ post.thumbnail.src('large') }}" alt="{{ post.title }}">
{% endif %}
<div class="product-price">
{% if post.isOnSale %}
<span class="original-price">{{ post.formattedPrice }}</span>
<span class="sale-price">${{ post.salePrice|number_format(2) }}</span>
{% else %}
<span class="price">{{ post.formattedPrice }}</span>
{% endif %}
</div>
<div class="product-content">
{{ post.content }}
</div>
{% set related = post.relatedProducts(4) %}
{% if related %}
<section class="related-products">
<h2>Related Products</h2>
<div class="grid">
{% for product in related %}
{% include 'partials/product-card.twig' with { product: product } %}
{% endfor %}
</div>
</section>
{% endif %}
</article>
{% endblock %}Advanced Configuration
For complex post types, implement ConfiguresPostType interface:
php
<?php
namespace App\Models;
use Studiometa\Foehn\Attributes\AsPostType;
use Studiometa\Foehn\Contracts\ConfiguresPostType;
use Timber\Post;
#[AsPostType(name: 'event', singular: 'Event', plural: 'Events')]
final class Event extends Post implements ConfiguresPostType
{
/**
* Customize the post type arguments.
*/
public static function postTypeArgs(array $args): array
{
// Add custom capabilities
$args['capability_type'] = 'event';
$args['map_meta_cap'] = true;
// Customize labels
$args['labels']['menu_name'] = 'Calendar';
$args['labels']['all_items'] = 'All Events';
return $args;
}
}Multiple Post Types
Each post type is a separate class:
app/Models/
├── Product.php
├── Event.php
├── Team.php
├── Testimonial.php
└── Portfolio.phpAttribute Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
name | string | required | Post type slug |
singular | ?string | null | Singular label |
plural | ?string | null | Plural label |
public | bool | true | Public visibility |
hasArchive | bool | false | Enable archive pages |
showInRest | bool | true | REST API & Gutenberg support |
menuIcon | ?string | null | Dashicon or URL |
supports | string[] | ['title', 'editor', 'thumbnail'] | Supported features |
taxonomies | string[] | [] | Associated taxonomies |
rewriteSlug | ?string | null | Custom URL slug |