Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Ahondev/portfolio-v2/llms.txt

Use this file to discover all available pages before exploring further.

Almost every page on a site needs a handful of the same values: the site’s contact email, the global navigation items, the full list of services. Without Composers you’d repeat the same database calls in every controller method and risk them going stale independently. Composers solve this by declaring what data belongs to which views in a single place, then letting the framework merge that data automatically before any controller response is serialised — whether that’s an HTML shell for first load or a JSON payload for SPA navigation.

How Composers work

Composers run on wp_loaded, before any routing decision is made. ComposerManager::registerComposers() scans app/Composers/, instantiates every class it finds, calls views() to discover which views the composer targets, calls compose() to get the data, and stores it all in an internal registry keyed by view name. When WebController::view() builds a response, it calls ComposerManager::getView($view), which merges:
  1. All data registered under the wildcard key '*'
  2. All data registered for the specific view name
The merged result is combined with the controller’s own $data array before the final $__wp_data__ payload is assembled.

The ComposerInterface

Every Composer must implement two methods:
namespace Ahon\WPCMS\Composers;

interface ComposerInterface
{
    /**
     * Return the view names this composer targets.
     * Use ['*'] to target every view.
     */
    public function views(): array;

    /**
     * Return the data array to inject into the targeted views.
     */
    public function compose(): array;
}

views(): array

Return an array of view name strings. The special value '*' makes the composer inject its data into every view response.
// Target all views
public function views(): array
{
    return ['*'];
}

// Target specific views only
public function views(): array
{
    return ['home', 'services', 'services_single'];
}

compose(): array

Return an associative array of key-value pairs. Every key becomes a top-level property in the view’s data object.
public function compose(): array
{
    return [
        'site_email'    => get_field('email', 'option'),
        'site_services' => Service::query()->all(),
    ];
}

Creating a Composer

1

Create the class file

Add a PHP file in app/Composers/. The filename must match the class name. The class should extend Base (for access to remember() and service()) and implement ComposerInterface.
2

Implement views() and compose()

Declare which views you’re targeting and return the data array.
3

That's it

ComposerManager discovers and registers composers automatically by scanning the directory on every boot — no manual registration needed.

The AppComposer — real example

This is the global composer that runs on every view in the portfolio. It injects the site contact email and the full list of services (cached via a transient so the DB is only hit once every five minutes):
// app/Composers/AppComposer.php
namespace Ahon\WPCMS\App\Composers;

use Ahon\WPCMS\App\PostTypes\Service;
use Ahon\WPCMS\Base\Base;
use Ahon\WPCMS\Composers\ComposerInterface;

class AppComposer extends Base implements ComposerInterface
{
    public function views(): array
    {
        return ['*'];  // inject into every view
    }

    public function compose(): array
    {
        $services = $this->remember('composer_services_v1', 300, function () {
            return Service::query()->all();
        });

        return [
            'site_email'    => get_field('email', 'option'),
            'site_services' => $services,
        ];
    }
}
After this Composer runs, every view payload automatically contains site_email and site_services in its data block — no controller changes required.

Wildcard vs. targeted Composers

views() return valueBehaviour
['*']Data is injected into all views. Use for truly global data like nav items, site settings, or authentication state.
['home', 'agence']Data is injected only when view() is called with one of those exact view names. Use for data that is expensive to compute and only relevant to a subset of pages.
When a view matches both a wildcard Composer and a targeted Composer, both payloads are merged — targeted data is applied on top, so it wins if there are key collisions.

Accessing Composer data in React

Because Composer data is merged into $data before the $__wp_data__ payload is assembled, it appears inside the data key of window.__wp_data__ on first load, and inside the data key of the JSON response on SPA navigation. On the TypeScript side, use the useWPData() hook to access the full payload:
import { useWPData } from '@/hooks/useWPData'

interface AppData {
  site_email: string
  site_services: Service[]
}

export function Footer() {
  const { data } = useWPData<AppData>()

  return (
    <footer>
      <a href={`mailto:${data.site_email}`}>{data.site_email}</a>
    </footer>
  )
}
Because Composers run on every request, keep compose() lean. Use remember() (inherited from Base) to cache any query that hits the database, especially queries shared between multiple Composers or controllers.

Registering multiple Composers

Drop any number of class files into app/Composers/ — each one is auto-discovered. A useful pattern is to have one global Composer for site-wide data and one per page section for heavier queries:
app/Composers/
├── AppComposer.php        # ['*'] — site email, global nav services
├── BlogComposer.php       # ['blog'] — featured posts, categories
└── ServicesComposer.php   # ['services', 'services_single'] — service metadata
Each file follows the same structure: extend Base, implement ComposerInterface, return data from compose(). The framework handles the rest.

Build docs developers (and LLMs) love