wp-plugin-dto — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited wp-plugin-dto (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 moves structured data between request input, options, meta, custom tables, external APIs, REST controllers, admin screens, cron jobs, and presenters. A DTO gives that data one named shape instead of leaking raw arrays across the plugin.
This skill is intentionally better-data-free. If the project already uses better-data, run bd-data-object; otherwise use this native pattern.
$_POST, WP_Post, meta arrays, or option arrays around: read references/before-after-raw-array.md."A DTO is just an array with nicer comments."
Wrong direction. A DTO is a small immutable boundary object with a named schema, explicit defaults, and one hydration path. It prevents common AI mistakes: unchecked (int) casts, empty() swallowing 0, isset() hiding intentional null, dynamic properties, leaking secrets through to_array(), and passing $_POST or raw post meta directly to business logic.
Trigger when ANY of the following is true:
FooDto, FooData, SettingsData, RequestData, Payload, or a value object.$_GET, $_POST, REST params, options, post meta, user meta, term meta, custom tables, WP_Post, WP_User, or WooCommerce objects.(int), (bool), intval, settype, empty, isset, get_object_vars, or dynamic property assignment.| Layer | Responsibility |
|---|---|
| Source / repository | Read WP objects, options, meta, request params, API responses. Knows WordPress. |
| DTO | Hold normalized values. No DB writes, no HTML, no hooks, no global reads. |
| Validator | Decide whether the DTO is acceptable. Returns true or WP_Error. |
| Presenter | Convert DTO to arrays / JSON-ready values for REST, admin, JS config, email. |
| View / template | Escape and echo HTML. |
The DTO may contain small normalization helpers, but it should not call get_post_meta(), update_option(), wp_remote_get(), add_action(), or echo anything.
Prefer PHP 7.4-compatible immutable objects unless the plugin has a higher minimum PHP version. Use PHP 8.1 readonly only when the plugin declares PHP 8.1+.
namespace MyPlugin\Dto;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
final class ProductDto {
private int $id;
private string $title;
public function __construct( int $id = 0, string $title = '' ) {
$this->id = $id;
$this->title = $title;
}
/** @param array<string,mixed> $data @return self|WP_Error */
public static function from_array( array $data ) {
$errors = new WP_Error();
$id = self::to_int( $data['id'] ?? 0, 'id', $errors );
$title = self::to_string( $data['title'] ?? '', 'title', $errors );
if ( '' === $title ) {
$errors->add( 'missing_title', 'Product title is required.' );
}
if ( $errors->has_errors() ) {
return $errors;
}
return new self( $id, $title );
}
/** @return array<string,mixed> */
public function to_array(): array {
return array(
'id' => $this->id,
'title' => $this->title,
);
}
public function id(): int { return $this->id; }
public function title(): string { return $this->title; }
private static function to_int( $value, string $field, WP_Error $errors ): int {
if ( is_int( $value ) ) {
return $value;
}
if ( is_string( $value ) && preg_match( '/^-?\d+$/', $value ) ) {
return (int) $value;
}
$errors->add( 'invalid_' . $field, sprintf( '%s must be an integer.', $field ) );
return 0;
}
private static function to_string( $value, string $field, WP_Error $errors ): string {
if ( is_string( $value ) ) {
return $value;
}
if ( is_scalar( $value ) ) {
return (string) $value;
}
$errors->add( 'invalid_' . $field, sprintf( '%s must be a string.', $field ) );
return '';
}
}Use references/native-dto-patterns.md for the full pattern: floats, booleans, DateTime, nested DTO collections, secrets, with(), and PHP 8.1 variants.
from_array() or named factories like from_post( WP_Post $post ), from_option( array $option ), from_request( WP_REST_Request $request ).$_POST, use wp_unslash() first, then a field-specific sanitizer.extra property.isset( $data['expires_at'] ) treats explicit null as absent.empty( '0' ) and empty( 0 ) are true.(int) 'abc' becomes 0; (bool) 'false' becomes true. Reject surprising input with WP_Error.items as random arrays when the plugin expects item shape.DTOs often carry API keys, tokens, customer emails, or internal notes.
null, not an empty secret string.to_array() unless the method name says so, e.g. to_private_array().with() to produce a changed copy.get_post_meta(), no update_option(), no $wpdb, no HTTP calls.wp_send_json(), no rest_ensure_response().foreach ( $data as $key => $value ) { $dto->$key = $value; }.(int), (bool), intval, settype, or empty()-driven normalization.created_at to createdAt is a breaking change unless every caller and presenter is updated.in_array( ..., true ).// WRONG - unchecked casts hide bad input.
$dto = new ProductDto( (int) $_POST['id'], (float) $_POST['price'] );
// WRONG - dynamic property hydration.
foreach ( $row as $key => $value ) {
$dto->{$key} = $value;
}
// WRONG - empty() rejects valid values.
if ( empty( $data['quantity'] ) ) {
return new WP_Error( 'missing_quantity', 'Quantity is required.' );
}
// WRONG - leaking secrets by dumping all object properties.
return get_object_vars( $dto );wp_unslash(), sanitization, validation, and escaping.WP_Error for user-input validation failures.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.