DEV Community

hamed pakdaman
hamed pakdaman

Posted on • Originally published at unfoldcms.com

Why WordPress DX Fell Behind — A Developer's Honest Take

A version of this is on my own site too. (I work on UnfoldCMS — but this post is about WordPress DX, not a pitch.)

A developer joins a WordPress project in 2026. They open functions.php, scroll past 800 lines of add_action and add_filter, and quietly open LinkedIn in another tab.

I've watched this happen. I've been this developer. And I want to be fair: WordPress DX isn't bad because the people maintaining it are bad. It's bad because the platform was designed in 2003, around PHP 4, and has grandfathered every API decision since. The gap to modern stacks isn't closing — it's growing.

This isn't a "WordPress bad" rant. It's a look at why the developer experience fell behind, with the same task written both ways so you can judge for yourself.

TL;DR: WordPress's DX problems are structural — global functions, hooks-and-filters as the main extension model, untyped data in wp_postmeta, no first-class testing, IDE autocomplete that dies at the hook boundary, and a deploy model still shaped around FTP. Modern stacks fix all of these by default. The gap decides your hiring pool, how maintainable the code stays, and how fast you ship the next thing.


The language gap: PHP 2003 vs today

WordPress's core API was designed around PHP 4 — no real namespaces, no type hints, no dependency injection. So the API became a museum of PHP 4 idioms, carried forward through every release.

Here's how you do basically anything in WordPress:

add_action('init', 'my_setup_function');
add_filter('the_content', 'modify_post_content');

function my_setup_function() {
    // do stuff with global $wpdb, global $post, etc.
}

function modify_post_content($content) {
    return str_replace('foo', 'bar', $content);
}
Enter fullscreen mode Exit fullscreen mode

Five things a 2026 developer notices immediately:

  1. Global functions everywhereadd_action, add_filter, the_content all live in the global namespace. Name collision? Good luck.
  2. String-typed hook names'init' is a string. Typo it and nothing happens, silently. Your IDE can't catch it.
  3. Hidden global stateglobal $wpdb, global $post, global $wp_query. The callback depends on state set somewhere else you can't see.
  4. No type infoadd_filter('the_content', $cb) has no idea what $content is. No autocomplete on the parameter.
  5. No return contract — what should the callback return? Whatever WordPress handed you, roughly.

Same idea in a modern Laravel stack:

namespace App\Listeners;

use App\Events\PostPublished;
use App\Services\ContentModifier;

class TransformPostContent
{
    public function __construct(private ContentModifier $modifier) {}

    public function handle(PostPublished $event): void
    {
        $event->post->body = $this->modifier->replace($event->post->body, 'foo', 'bar');
    }
}
Enter fullscreen mode Exit fullscreen mode

Namespaces, type hints, dependency injection, zero globals, autocomplete on every line. It's more code — but it's code the compiler can verify and the IDE can help you write.


The data gap: untyped meta vs real columns

This is the one that quietly costs you weeks. WordPress stores custom fields in wp_postmeta as serialized strings — key/value rows, no types, no constraints.

// WordPress: read a custom field
$price = get_post_meta($post_id, 'product_price', true);
// $price is a string. Always. Even if you saved a float.
// Forgot the third arg? You get an array. Surprise.
Enter fullscreen mode Exit fullscreen mode

You don't know the type. You don't know if the key exists. You can't constrain it at the DB level. And every read is a separate query unless you remember to prime the cache.

A modern CMS gives you a real schema:

// Typed model attribute, real column, one query
$price = $product->price; // float, guaranteed, autocompleted
Enter fullscreen mode Exit fullscreen mode

The difference compounds. With wp_postmeta, every feature adds untyped rows to one giant table. With real columns, every feature is a typed, queryable, indexable field.


The testing gap

Modern frameworks ship with testing as a first-class citizen. You scaffold a project and the test runner is already there.

// Laravel: this works out of the box
it('publishes a post', function () {
    $post = Post::factory()->create(['status' => 'draft']);
    $post->publish();
    expect($post->fresh()->status)->toBe('published');
});
Enter fullscreen mode Exit fullscreen mode

WordPress can be tested — but it fights you. Global state, database fixtures that need a full WP bootstrap, hooks firing in the background. Most WordPress projects I've seen ship with zero tests, not because the team is lazy, but because the cost of the first test is so high nobody pays it.


The deploy gap

A lot of the WordPress world still deploys by FTP-ing files to a server and clicking "update" in /wp-admin. No build step, no atomic releases, no easy rollback. The database holds config and content and serialized PHP, so "just copy the DB" isn't safe either.

Modern stacks deploy with a build, a migration step, and a rollback path:

git push        # CI builds
php artisan migrate --force
# assets built, atomic swap, rollback = redeploy previous tag
Enter fullscreen mode Exit fullscreen mode

One of these you can do at 2pm on a Friday. The other you do at 2am on a Sunday with your heart rate up.


To be fair to WordPress

WordPress isn't going anywhere, and for a lot of sites that's the right call:

  • A blog or brochure site where nobody touches code → WordPress is fine.
  • A non-technical team that needs Gutenberg and a plugin for everything → WordPress is fine.
  • An existing WordPress site that works → don't rewrite it for vibes.

The DX gap matters when developers own the project long-term. It decides who you can hire, how maintainable the code stays, and how fast you ship feature #50.


So what do you use instead?

Depends on your stack. If you're already in Laravel, a Laravel-native CMS keeps you in typed, testable, DI'd territory instead of dropping back into globals. If you're on Next.js / Astro / Svelte, a headless CMS with a real API does the same.

I ended up building one (UnfoldCMS) because I wanted the content layer to feel like the rest of my codebase — typed models, real migrations, a test suite, a normal deploy. That's the pitch and I'll leave it at one line.

But the broader point stands without my product: a 2026 CMS should give you 2026 developer experience. WordPress gives you 2003's, carefully preserved. For a lot of teams that's the dealbreaker — not the features, the daily experience of working in it.

What's your take — is WordPress DX salvageable, or is the PHP-4-era foundation just too deep to fix? Genuinely curious where other devs land on this.


Originally published on unfoldcms.com. Happy to go deeper on any of these in the comments.

Top comments (0)