wp-plugin-presenter — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited wp-plugin-presenter (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
WP_Post, WC_Order, get_object_vars(), or pre-escaped REST payloads: read references/before-after-controller-output.md."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.
Trigger when ANY of the following is true:
FooPresenter, FooViewModel, ResponseMapper, AdminRow, JsonPresenter, or similar classes.WP_Post, WP_User, WooCommerce objects, options, or custom table rows.wp_send_json_success(), rest_ensure_response(), wp_add_inline_script(), or builds admin table rows.| Layer | Responsibility |
|---|---|
| DTO | Normalized data. No audience-specific output. |
| Presenter | Context-specific arrays and computed fields. No DB writes. |
| Controller | Permission check, nonce/REST validation, calls presenter, sends response. |
| View/template | Escapes 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().
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().
esc_html(), esc_attr(), esc_url(), or wp_kses_post().wp_json_encode() inside wp_add_inline_script().See references/presenter-context-patterns.md for complete examples.
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.
get_object_vars( $dto ), json_encode( $dto ), or return raw WP/WC objects.for_rest() and for_admin_table() should not share a leaky "everything" array.render_*_html() and escape inside it.update_option(), $wpdb, or remote APIs.// 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() );wp_add_inline_script().wp-plugin-dto.rest_ensure_response() for REST controllers.wp_send_json_success() / wp_send_json_error() for AJAX.wp_json_encode() and wp_add_inline_script() for safe JS config.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.