WordPress plugin: native presenters

SkillMedia

Design and review native presenter classes in WordPress

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the WordPress plugin: native presenters skill

What this skill tells your AI

The instructions your AI receives, as published by lonsdale201/wp-agent-skills in plugin-scaffold/wp-plugin-presenter/SKILL.md and read by ahel’s review.

For plugin code that turns DTOs / domain objects into output shapes. A presenter chooses fields, computes labels, formats dates/numbers, redacts sensitive values, and returns arrays that controllers can send to REST, AJAX, admin tables, JS config, email templates, exports, or views.

This skill is intentionally better-data-free. If the project already uses better-data, run bd-presenter; otherwise use this native pattern.

When to load references

Misconception this skill corrects

"The DTO already has to_array(), so the controller can return that everywhere."

Wrong. to_array() is usually the DTO's canonical data snapshot. REST output, admin table rows, export rows, JS config, and email variables have different audiences and redaction rules. A presenter makes those contexts explicit instead of letting every controller hand-edit arrays.

When to use this skill

Trigger when ANY of the following is true:

  • Adding or reviewing FooPresenter, FooViewModel, ResponseMapper, AdminRow, JsonPresenter, or similar classes.
  • REST/AJAX code returns arrays derived from DTOs, WP_Post, WP_User, WooCommerce objects, options, or custom table rows.
  • Code calls wp_send_json_success(), rest_ensure_response(), wp_add_inline_script(), or builds admin table rows.
  • A DTO has sensitive fields and the output needs redaction.
  • A controller currently contains formatting, labels, computed fields, or output-specific conditionals.

Layer boundaries

LayerResponsibility
DTONormalized data. No audience-specific output.
PresenterContext-specific arrays and computed fields. No DB writes.
ControllerPermission check, nonce/REST validation, calls presenter, sends response.
View/templateEscapes and echoes HTML.

Presenter output for REST/JSON should be raw JSON-safe primitives, not pre-escaped HTML. Presenter output for an HTML-only view may include already escaped markup, but the method name must make that clear, e.g. render_badge_html().

Minimal class shape

namespace MyPlugin\Presenter;

use MyPlugin\Dto\ProductDto;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

final class ProductPresenter {
    private ProductDto $product;

    public function __construct( ProductDto $product ) {
        $this->product = $product;
    }

    /** @return array<string,mixed> */
    public function for_rest(): array {
        return array(
            'id'      => $this->product->id(),
            'title'   => $this->product->title(),
            'enabled' => $this->product->enabled(),
        );
    }

    /** @return array<string,string|int> */
    public function for_admin_table(): array {
        return array(
            'id'     => $this->product->id(),
            'title'  => $this->product->title(),
            'status' => $this->product->enabled() ? __( 'Enabled', 'my-plugin' ) : __( 'Disabled', 'my-plugin' ),
        );
    }
}

Use explicit context methods instead of a generic to_array( $context ) until the contexts genuinely share most of the same shape. Method names make reviews easier: for_rest(), for_admin_table(), for_export(), for_email(), for_js_config().

Output rules by context

  • REST/AJAX: return unescaped scalars, arrays, and nulls. Let WP JSON-encode them.
  • Admin table / template: presenter chooses values; view escapes with esc_html(), esc_attr(), esc_url(), or wp_kses_post().
  • Inline JS config: pass presenter output through wp_json_encode() inside wp_add_inline_script().
  • Email: present subject/body variables separately from HTML template rendering.
  • Export: use stable machine-readable keys and raw scalar values unless the export is explicitly human-facing.

See references/presenter-context-patterns.md for complete examples.

Redaction by default

Presenter methods should be public-safe by default. Sensitive values require explicit opt-in:

public function for_admin_table(): array {
    return array(
        'name'    => $this->credential->name(),
        'api_key' => '***',
    );
}

public function for_private_admin( bool $can_reveal_secret ): array {
    if ( ! $can_reveal_secret ) {
        return $this->for_admin_table();
    }

    $api_key = $this->credential->api_key();

    return array(
        'name'    => $this->credential->name(),
        'api_key' => $api_key ? $api_key->reveal() : '',
    );
}

The controller passes $can_reveal_secret = current_user_can( 'manage_options' ); the presenter applies that already-made authorization decision. Do not create convenience methods like reveal_all() or include secrets in a generic for_rest() response.

Critical rules

  • Presenter never mutates the DTO. Compute output values into arrays.
  • Allowlist fields. Never get_object_vars( $dto ), json_encode( $dto ), or return raw WP/WC objects.
  • One context, one method. for_rest() and for_admin_table() should not share a leaky "everything" array.
  • Redact sensitive fields by default. Explicit reveal only in narrowly named methods, and pass the authorization decision in from the controller.
  • Escape at the final HTML boundary. REST/AJAX/JS config arrays are not HTML.
  • Do not put HTML in REST payloads. If a method returns HTML, name it render_*_html() and escape inside it.
  • Keep DB and WP writes out. Presenter may call formatting/i18n helpers, but not repositories, update_option(), $wpdb, or remote APIs.
  • Collections replay per-item presenters. No duplicated mapping logic.

Common mistakes

// WRONG - exposes every public property and misses redaction.
return get_object_vars( $dto );

// WRONG - REST payload contains HTML from an admin use case.
return array( 'status' => '<span class="badge">Enabled</span>' );

// WRONG - escaping too early for JSON.
return array( 'title' => esc_html( $dto->title() ) );

// WRONG - side effect in presenter.
update_option( 'myplugin_last_presented', time() );

Cross-references

  • Run wp-plugin-dto when the presenter input is still a raw array or unclear object.
  • Run wp-plugin-architecture when deciding folder placement, namespaces, or by-feature vs by-type organization.
  • Run wp-plugin-assets-loading when passing presenter output into wp_add_inline_script().
  • Run bd-presenter only if the project intentionally uses the better-data library. better-data automates builder-style presentation; this skill is the native no-library version.

What this skill does NOT cover

  • DTO hydration and validation. Use wp-plugin-dto.
  • Template partial organization or block rendering architecture.
  • better-data Presenter internals.
  • REST route registration and permission callbacks.

References

Signals

GitHub stars
22
Forks
2
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
wp-plugin-presenter
Source
github.com/lonsdale201/wp-agent-skills