Codeskill

Learn to code, step by step

WordPress plugin and theme PHP done properly

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.

PreviousTesting – PHPUnit habits that pay off