Starisian Technologies — PHP/WordPress Reference Implementation
This document is the PHP and WordPress implementation standard for Starisian Technologies. It is a concrete, enforceable rulebook for all PHP and WordPress code.
All rules in the Standards Handbook apply in full. This document adds PHP- and WordPress-specific requirements on top of them.
- PHP 8.2+ (latest stable with active support — not security-only, not end-of-life)
- WordPress 6.8+ (latest stable)
- WordPress VIP standards — override PSR when conflicting
- PSR-1, PSR-4, PSR-12 everywhere else
- MariaDB (latest stable) via
$wpdbor abstracted query layer - Redis (object cache)
- OPcache (bytecode cache)
Code runs behind: Cloudflare → Nginx → Varnish → Apache → PHP-FPM → MariaDB → Redis
Code must not break caching, proxying, or edge behavior at any layer.
This document does not pin to specific version numbers. Version pins create maintenance debt — the document becomes wrong the moment a new release ships. We build at the front of the supported window. We test against the supported minimum. We never write for the unsupported minimum.
| Component | Policy | Rule |
|---|---|---|
| WordPress | Latest stable release | No deprecated WP APIs. Backwards compatibility culture is for adoption, not for development. |
| PHP | Latest stable with active support | Not security-only. Not end-of-life. Strict types required. No dynamic properties. |
| Relational Database | Latest stable — provider-agnostic | No direct SQL interpolation. All queries parameterized. No provider-specific extensions without abstraction layer. |
A plugin can support WordPress 6.x while being written in modern PHP with strict types. These are separable concerns.
// Required at top of every PHP file
declare(strict_types=1);
// All functions must have typed parameters and return types
function process_audio(string $path, int $duration): array { ... }
// Forbidden
function process($path, $duration) { ... } // no types| FAIL | PHP file missing declare(strict_types=1) |
|---|---|
| FAIL | function missing typed parameters or return type |
- (M)
Starisian\{Product}\{Component}— replace{Product}with the product-specific namespace defined in that product's standards - (X) Abbreviations or deviations from this pattern
All WordPress global identifiers must be prefixed. No exceptions.
This applies to:
- functions
- hooks (actions and filters)
- custom post types
- taxonomies
- meta keys
- options
- database tables
Each product defines its own prefix in that product's standards (e.g. myproduct_action, myproduct_post_type). Every global identifier MUST carry the product prefix; unprefixed identifiers are forbidden regardless of name choice.
| FAIL | unprefixed global function, hook, CPT, taxonomy, meta key, option, or DB table |
|---|
All input must be sanitized before use. No raw superglobals. No implicit casting.
// Required
$text = sanitize_text_field($_POST['text'] ?? '');
$key = sanitize_key($_GET['key'] ?? '');
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
// Forbidden
$text = $_POST['text']; // raw superglobal
$id = (int)$_GET['id']; // implicit cast without validationThe order is non-negotiable:
- Sanitize plain-text input on the way in as appropriate (
sanitize_text_field(),sanitize_key(),wp_kses_post()); parse and validate structured payloads with format-appropriate handling instead ofsanitize_text_field() - Validate domain logic (type checks, range checks, business rules)
- Escape output on the way out (
esc_html(),esc_attr(),esc_url(),esc_*())
| Function / Pattern | Use |
|---|---|
sanitize_text_field() |
Single-line plain text user input (e.g., form text fields) — not for structured data |
wp_kses_post() |
Human-authored HTML content |
esc_html() |
HTML output context |
esc_attr() |
HTML attribute context |
esc_url() |
URL output context |
esc_js() |
Inline JavaScript context |
json_decode() + schema validation |
JSON and other structured payloads — never use sanitize_text_field() on structured data |
- All writes require explicit schema mapping and validation before write
- Transactions required wherever atomicity is needed
- No direct SQL string interpolation — prepared statements only via
$wpdb->prepare() - No unbounded queries — all queries must have explicit
LIMIT - No
SELECT *— specify required columns explicitly - Row-level locking or optimistic versioning required for conflicting writes
- Use
$wpdb->prefixalways — never hardcodewp_
// Required
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT id, title FROM {$wpdb->posts} WHERE post_status = %s LIMIT %d",
'publish',
20
)
);
// Forbidden
$results = $wpdb->get_results("SELECT * FROM wp_posts WHERE post_status = 'publish'");| Data Type | Storage |
|---|---|
| Content | Custom Post Types |
| High-volume / structured data | Custom database tables |
| Cache / transient data | Redis (object cache) |
- (P) Use
dbDelta()for standard schema management - (S) Versioned migrations allowed when justified
- (M) Activation must create all required tables for all sites in a Multisite network
| FAIL | direct SQL string interpolation |
|---|---|
| FAIL | SELECT * in any query |
| FAIL | unbounded query without LIMIT |
| FAIL | hardcoded wp_ prefix instead of $wpdb->prefix |
- All cache entries must have defined TTL. No infinite TTL.
- Cache keys must be namespaced to prevent collision
- User-specific data must never enter shared cache
- Write operations must invalidate related cache entries immediately
- The distributed cache is a cache only. Never the source of truth.
// Required — namespaced key with TTL
wp_cache_set('spx_user_profile_' . $user_id, $profile_data, 'spx_profiles', 300);
// Required — invalidate on write
wp_cache_delete('spx_user_profile_' . $user_id, 'spx_profiles');
// Forbidden
wp_cache_set('user_profile', $data); // no namespace, no TTL; Production only
opcache.validate_timestamps = 0
opcache.memory_consumption = 128
opcache.max_accelerated_files = 10000Note: This applies to PHP deployments using OPcache. Apply equivalent bytecode cache configuration for other runtimes.
- All plugins must be namespaced — no global namespace pollution
- Scripts and styles loaded conditionally — never globally
- All fields defined in schema and validated before use
- No implicit field access — no dynamic schema mutation at runtime
// Required — conditional enqueueing
if (!is_page('media-record')) {
return;
}
wp_enqueue_script('spx-record-handle', plugin_dir_url(__FILE__) . 'js/record.js', [], '1.0.0', true);
// Forbidden
wp_enqueue_script('spx-record-handle', ...); // no guard — global enqueueEvery governed action must check ability and verify consent before execution. Bypassing ability checks is forbidden. Assuming consent is forbidden.
// Required
if (!current_user_can('spx_record')) {
return new WP_Error('forbidden', 'Insufficient ability');
}
if (!has_consent($user_id, 'recording')) {
return new WP_Error('consent_required', 'Consent not given');
}| FAIL | governed action without ability check |
|---|---|
| FAIL | governed action without consent verification |
- (M) PHPStan Level 5 minimum
- (P) Level 8+ for core systems
- (M) No suppression without inline reason and linked issue or remediation plan
- (M) PHPCS with WordPress VIP ruleset + PSR-12
- (X) Auto-fix in CI — report mode only
- (M) Lint failures block merge
WordPress Multisite is assumed from line one. It is never retrofitted.
- (M) Network-aware architecture from inception
- (M) Use
$wpdb->prefix— never hardcode table prefixes - (M) Distinguish site vs network options:
get_option()vsget_site_option() - (M) Plugin activation handles all existing sites in the network
- (M) New site creation triggers automatic initialization hook
// Required — handle per-site and network-wide distinctly
$site_setting = get_option('spx_site_mode');
$network_value = get_site_option('spx_network_license');
// Required — activation covers all sites
register_activation_hook(__FILE__, 'spx_activate');
function spx_activate(bool $network_wide): void {
if ($network_wide) {
$sites = get_sites(['fields' => 'ids']);
foreach ($sites as $site_id) {
switch_to_blog($site_id);
spx_activate_for_site();
restore_current_blog();
}
} else {
spx_activate_for_site();
}
}| FAIL | plugin that does not handle network-wide activation |
|---|---|
| FAIL | code that does not distinguish get_option() from get_site_option() when the distinction matters |
These are organization-wide primitives, not optional plugins.
- (M) Used for device, network, and environment context
- (X) Custom device fingerprinting
- (M) Frontend error reporting through the governance SDK
- (M) Trust identity issued by the auth SDK
- (X) Custom frontend auth systems
- (X) Direct use of
wp_set_auth_cookie()for frontend users
- (M) All recording via the approved audio capture SDK
- (X) Raw
MediaRecorderimplementations
- (M) GeoIP2 for geolocation
- (M) IP anonymization — last octet zeroed before logging or storage
- (X) Trusting user-supplied location blindly
- (M) PHPUnit for all backend logic
- (M) Tests must cover: sanitization paths, permission checks, DB write/rollback, authority-layer integration points
- (M) axe-core for rendered admin UI accessibility
- (M) One canonical version source per plugin/theme
On tag (v*):
- Validate version consistency
- Run PHPCS lint
- Run PHPStan analysis
- Run PHPUnit test suite
- Build/minify assets
- Generate translations (
.potfile) - Package distribution zip
- Generate checksums
- Publish release
- (M) All steps required
- (X) Manual releases
- (M) No hardcoded credentials or environment values
- (M) No undocumented public APIs — DocBlocks required on all public interfaces
- (M) Capability-based access control for all features
- (M) License headers in all PHP files
- (M) Dependency license audit before adding any new package
- (X) Commented-out code in production
- (X) Untracked TODOs in production code
Version: 2.0 | Starisian Technologies | May 2026
Applies to: All PHP and WordPress code governed by Starisian Technologies standards.