Taming Custom Post Type Data with Sage View Composers
One of the biggest frustrations I used to have with traditional WordPress theme development was the inevitable mess that emerged from mixing presentation logic with data retrieval. You know the drill: opening a PHP tag in the middle of your HTML to run `new WP_Query()`, `have_posts()` loops, and `the_post()` calls directly within your templates. While it works, it quickly leads to spaghetti code that’s hard to read, harder to maintain, and a nightmare to debug. That’s precisely why I fell in love with Sage.
Sage, powered by Acorn, elevates WordPress theme development to a modern, MVC-like paradigm. It brings the best of Laravel to the WordPress ecosystem, allowing us to build themes with clean, separated concerns. Our Blade templates become truly presentation-focused, free from database queries or complex data manipulation. The magic happens elsewhere, and for pulling data into those templates cleanly, View Composers are an absolute game-changer. They act as a bridge, preparing data and injecting it into specific views before they are rendered, keeping your templates beautifully lean.
Let's imagine you have a custom post type called "Projects" and you want to display a list of these projects on your front page, or perhaps on their dedicated archive page. In the old way, you'd likely drop a `WP_Query` right into your `front-page.php` or `archive-project.php`. With Sage, we leverage View Composers to inject this data directly into the view before it’s even rendered. This keeps our Blade files pristine and purely for displaying content.
First, we define our View Composer. I typically place these in `app/View/Composers`. Let’s create `app/View/Composers/ProjectComposer.php`. This class specifies which views it will serve and what data it will provide.
<?php
namespace App\View\Composers;
use Roots\Acorn\View\Composer; use WP_Query;
class ProjectComposer extends Composer { /** * List of views served by this composer. * * @var array */ protected static $views = [ 'partials.projects', 'archive-project', ];
/** * Data to be passed to view before rendering. * * @return array */ public function with() { return [ 'projects' => $this->getProjects(), ]; }
/** * Get the latest projects. * * @return array */ protected function getProjects() { $args = [ 'post_type' => 'project', 'posts_per_page' => -1, // Get all projects 'post_status' => 'publish', 'orderby' => 'date', 'order' => 'DESC', ];
$query = new WP_Query($args);
return collect($query->posts)->map(function ($post) { return (object) [ // Return as an object for easy access in Blade 'id' => $post->ID, 'title' => get_the_title($post->ID), 'permalink' => get_permalink($post->ID), 'thumbnail' => get_the_post_thumbnail_url($post->ID, 'medium'), 'excerpt' => get_the_excerpt($post->ID), ]; })->all(); } }
In this code, the `$views` array tells Sage which Blade templates this composer should apply to. Here, it’s `partials.projects` (perhaps for a section on the front page) and `archive-project` (the default archive for our CPT). The `with()` method returns an associative array, making the `projects` key available to our Blade views. Inside `getProjects()`, we perform our `WP_Query`, but notice how we then process the results into a clean collection of objects. This further abstracts the raw WordPress `WP_Post` object, giving us exactly what we need for presentation.
Now, our `resources/views/partials/projects.blade.php` file becomes incredibly simple and readable, consuming the data prepared by the composer:
@if (!empty($projects)) <section class="latest-projects"> <h2>Our Latest Projects</h2> <div class="project-grid"> @foreach ($projects as $project) <article class="project-item"> @if ($project->thumbnail) <a href="{{ $project->permalink }}"> <img src="{{ $project->thumbnail }}" alt="{{ $project->title }}"> </a> @endif <h3><a href="{{ $project->permalink }}">{{ $project->title }}</a></h3> <p>{{ $project->excerpt }}</p> </article> @endforeach </div> </section> @endif
See how clean that Blade template is? There’s no `WP_Query`, no `while (have_posts()) : the_post();`, no `setup_postdata()`. It's purely focused on displaying the `projects` variable that was handed to it. All the data fetching and processing logic is neatly encapsulated within `ProjectComposer.php`. This separation of concerns makes your code vastly more organized, easier to test, and much more enjoyable to work with.
This is just one example of how Sage, with its Laravel-inspired architecture and features like View Composers and Acorn’s dependency injection, empowers developers to build truly maintainable and performant WordPress themes. It frees us from the database bloat and unmaintainable inline PHP that plagues traditional development or page builder-reliant sites, allowing us to craft a modern, programmatic codebase. Embracing tools like View Composers is a direct path to cleaner, more efficient theme development.