Taming Custom Data in Sage: A View Composer Recipe
If you've spent any time digging through traditional WordPress theme code, you've likely encountered the "Wild West" approach to data retrieval. Queries run haphazardly, global variables are set with abandon, and your template files end up looking like a confusing mix of HTML, PHP logic, and database calls. It's the kind of code that makes you long for the clean structure of an MVC framework, a structure that Sage, powered by Laravel's Acorn and Blade, delivers beautifully. Today, I want to walk through one of my favorite Sage features for keeping our templates lean and focused: the View Composer.
Sage’s philosophy is to keep your Blade templates purely about presentation. They should receive data and render it, nothing more. This means fetching posts, options, or any other dynamic content shouldn't happen directly inside resources/views. That's where View Composers step in, acting as the perfect bridge between your application logic and your views. They allow us to gather all the necessary data for a specific view or set of views and then inject it cleanly, keeping our template files beautifully free of clutter.
Let's say you have a custom post type for "Team Members" and you want to display a list of them on various pages using a reusable partial, partials.team-members. The traditional WordPress way might involve putting a WP_Query directly into that partial, or even worse, running it higher up and making the results global. With Sage, we leverage a View Composer to handle that data fetching, ensuring a clean separation of concerns.
First, we create our View Composer. Following Sage's conventions, we'll put it in app/View/Composers/TeamMembersComposer.php:
<?php
namespace App\View\Composers;
use Roots\Acorn\View\Composer; use WP_Query;
class TeamMembersComposer extends Composer { /** * List of views served by this composer. * * @var array */ protected static $views = [ 'partials.team-members', ];
/** * Data to be passed to view before rendering. * * @return array */ public function with() { return [ 'team_members' => $this->teamMembers(), ]; }
/** * Retrieve a list of team members. * * @return array */ protected function teamMembers() { $args = [ 'post_type' => 'team_member', // Replace with your actual CPT slug 'posts_per_page' => -1, // Get all 'order' => 'ASC', 'orderby' => 'title', ];
$query = new WP_Query($args);
if (!$query->have_posts()) { return []; }
return collect($query->posts)->map(function ($post) { return [ 'title' => get_the_title($post), 'link' => get_permalink($post), 'thumbnail' => get_the_post_thumbnail_url($post, 'thumbnail'), 'job_title' => get_field('job_title', $post->ID), // Example ACF field ]; })->all(); } }
In this file, static $views tells Acorn which Blade template(s) this composer applies to. The with() method returns an associative array, where the keys become variables available in your Blade view. Here, we're calling a protected teamMembers() method to encapsulate our WP_Query logic, transforming the raw WP_Post objects into a cleaner array of data using Laravel's collect() helper. Notice how we're even grabbing an ACF field, job_title, directly here.
Now, our resources/views/partials/team-members.blade.php can be blissfully unaware of how its data was fetched:
@if (!empty($team_members)) <section class="team-members"> <h2>Meet Our Team</h2> <div class="grid grid-cols-1 md:grid-cols-3 gap-8"> @foreach ($team_members as $member) <article class="team-member-card bg-white p-6 shadow-lg rounded-lg"> @if ($member['thumbnail']) <img src="{{ $member['thumbnail'] }}" alt="{{ $member['title'] }}" class="w-24 h-24 rounded-full mx-auto mb-4 object-cover"> @endif <h3 class="text-xl font-bold text-center">{{ $member['title'] }}</h3> @if ($member['job_title']) <p class="text-gray-600 text-center text-sm">{{ $member['job_title'] }}</p> @endif <a href="{{ $member['link'] }}" class="block text-center mt-3 text-blue-600 hover:underline">View Profile</a> </article> @endforeach </div> </section> @endif
See how clean that template is? No WP_Query, no get_posts(), no raw WP_Post objects being manipulated. Just a simple loop over a $team_members array that we *know* will be there, thanks to our View Composer. We've separated our data concerns from our presentation concerns, making both easier to read, test, and maintain. This organized, programmatic approach is a stark contrast to the unmanageable database bloat and inline logic often found in page builder solutions like Divi or Elementor, where true separation of concerns is an afterthought, if it exists at all.
By adopting View Composers, you're not just writing better Sage code; you're elevating your entire WordPress development workflow. It's a small change with massive benefits for code readability, maintainability, and the overall sanity of your development team. Start composing your views today, and experience the clean architecture Sage offers.