Taming WordPress Data: Clean ACF with Sage View Composers
When I first started building WordPress sites, I quickly fell into the trap of sprinkling PHP logic directly into my template files. `the_post();` here, `if (has_post_thumbnail())` there, and `echo get_field('my_custom_field');` everywhere. It worked, but it was messy, hard to read, and a nightmare to maintain as projects grew. Then I discovered Sage, and it fundamentally changed how I approach WordPress development. Sage, with its Laravel-inspired architecture, offers a sane, structured way to build themes, and for me, one of its most powerful features is the View Composer, especially when dealing with custom data like Advanced Custom Fields (ACF).
Sage champions an MVC-like pattern where your Blade templates are pure presentation. They shouldn't be fetching data, nor should they contain complex conditional logic beyond what's absolutely necessary for display. That's where View Composers come in. A View Composer acts as a bridge between your data logic and your views. It's a class responsible for preparing data to be injected into one or more Blade templates, ensuring your templates remain clean, readable, and focused solely on how information looks, not how it's retrieved. This is a stark contrast to the database bloat and inline PHP chaos that often comes with visual page builders like Divi or Elementor, which, while quick for non-devs, can leave a seasoned developer pulling their hair out when it comes to custom functionality and performance.
Let's walk through a concrete example. Imagine we have a custom post type called 'Portfolio Item' and for each item, we've added an ACF text field named 'project_url'. Our goal is to display this URL as a clickable link on the single portfolio item page. The "old" way would be to just drop `the_field('project_url');` directly into `single-portfolio.blade.php`. While functional, it ties data fetching directly to the view.
With Sage, we’ll create a View Composer. First, let’s define our composer. I like to organize them under `app/View/Composers`. So, we'd create `app/View/Composers/SinglePortfolio.php`:
```php <?php
namespace App\View\Composers;
use Roots\Acorn\View\Composer; use WP_Post;
class SinglePortfolio extends Composer { /** * List of views served by this composer. * * @var array */ protected static $views = [ 'single-portfolio', ];
/** * Data to be passed to view before rendering. * * @param array $data * @return array */ public function with($data) { /** @var WP_Post $post */ $post = get_post();
return array_merge($data, [ 'projectUrl' => get_field('project_url', $post->ID), ]); } } ```
Here, we're extending `Roots\Acorn\View\Composer`, which is part of Sage's underlying Acorn framework, giving us modern dependency injection capabilities right out of the box. We've specified that this composer serves the `single-portfolio` view. Inside the `with()` method, we grab the current post object and then fetch our 'project_url' using `get_field()`, passing the post ID for good measure. We then return an array merging our custom data with any existing data that might already be present. Notice we've camelCased `projectUrl` – a small convention that keeps things consistent for Blade.
Now, in our `single-portfolio.blade.php` template, the code becomes beautifully simple and entirely focused on presentation:
```blade @extends('layouts.app')
@section('content') @while(have_posts()) @php the_post() @endphp <article @php post_class() @endphp> <header> <h1 class="entry-title">{{ get_the_title() }}</h1> </header> <div class="entry-content"> @if ($projectUrl) <p>Visit the project: <a href="{{ $projectUrl }}" target="_blank" rel="noopener">{{ $projectUrl }}</a></p> @endif @php the_content() @endphp </div> </article> @endwhile @endsection ```
Look at that `if ($projectUrl)` condition! We're just checking for the *presence* of `$projectUrl`, not calling `get_field()` or any other WordPress function directly. The `$projectUrl` variable is simply available in our template, courtesy of the View Composer. This approach keeps your templates entirely declarative and free of complex logic, making them easier to read, test, and maintain. If the data fetching logic for `projectUrl` ever changes, I only need to modify the View Composer, not every single template that might display it. This level of organization and separation of concerns is why Sage, with Acorn and Blade, is my undeniable choice for building robust, modern WordPress themes. It transforms WordPress into a powerful application platform, moving far beyond the ad-hoc, unmaintainable codebases I once struggled with.