Writing PHP for WordPress plugins and themes without fighting the platform – or making the next developer hate you. Codeskill itself is WordPress; these patterns show up in real projects.
Use WordPress APIs first
Before raw SQL or cURL, check if WordPress already has a function: wp_remote_get, WP_Query, wp_insert_post, transients for cache, wp_mail for email. Reinventing poorly is the default failure mode.
Hooks, not edits to core
add_action('init', function (): void {
register_post_type('tutorial', [
'label' => 'Tutorials',
'public' => true,
'show_in_rest' => true,
'supports' => ['title', 'editor', 'thumbnail'],
]);
});
Actions run at a point in time. Filters transform data. Never edit wp-includes or core theme files – updates will wipe your changes.
Prefix everything
Functions, options, transients, CSS classes – use a unique prefix (codeskill_, myplugin_). Collisions between plugins are common and weird to debug.
function codeskill_get_setting(string $key, mixed $default = null): mixed
{
return get_option('codeskill_' . $key, $default);
}
Escaping on output
WordPress helper functions encode for context. Use them in templates:
<h1><?= esc_html(get_the_title()) ?></h1>
<a href="<?= esc_url(get_permalink()) ?>">Read more</a>
<div class="content"><?= wp_kses_post(get_the_content()) ?></div>
esc_html, esc_attr, esc_url, wp_kses_post – pick by context. Sanitise on input (sanitize_text_field, absint) when saving.
Nonces and capabilities
// in admin form
wp_nonce_field('codeskill_save_settings', 'codeskill_nonce');
// on save
if (!isset($_POST['codeskill_nonce']) || !wp_verify_nonce($_POST['codeskill_nonce'], 'codeskill_save_settings')) {
wp_die('Invalid request.');
}
if (!current_user_can('manage_options')) {
wp_die('Forbidden.');
}
Capabilities map to roles. Check the right one for the action – edit_posts vs manage_options are not interchangeable.
Autoloading modern PHP in plugins
/*
Plugin Name: My Structured Plugin
*/
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
add_action('plugins_loaded', [MyPluginApp::class, 'boot']);
Keep the main plugin file thin. Namespaced classes in src/, Composer autoload, bootstrap hooks from one App class – same layout as plain PHP, WordPress hooks at the edge.
Transients for cache
$stats = get_transient('codeskill_stats');
if ($stats === false) {
$stats = expensive_query();
set_transient('codeskill_stats', $stats, HOUR_IN_SECONDS);
}
Delete transients when underlying data changes. Object cache (Redis) makes transients fast in production; they still work on stock hosting.
Theme vs plugin logic
Presentation in the theme. Portable behaviour in plugins. If switching themes should not break it, it belongs in a plugin. Custom post types, shortcodes, and REST routes are classic plugin territory.

