Contensio logo

Contensio hooks vs WordPress hooks

A side-by-side comparison for WordPress developers — same mental model, same function names, with a few deliberate improvements.

If you're coming from WordPress, Contensio's hook system will feel immediately familiar. The function names are identical, the priority parameter works the same way, and the mental model — actions do things, filters modify values — is exactly the same.

This page walks through the differences so you know what to expect, and what's actually better.


The function names are the same

// WordPress
add_action('save_post', $callback, $priority, $accepted_args);
do_action('save_post', $post_id, $post, $update);

add_filter('the_content', $callback, $priority, $accepted_args);
apply_filters('the_content', $content);

remove_action('save_post', $callback, $priority);
remove_filter('the_content', $callback, $priority);
has_action('save_post');
has_filter('the_content');
// Contensio — same names, same signatures
add_action('contensio/content/created', $callback, $priority);
do_action('contensio/content/created', $content);

add_filter('contensio/api/content-item', $callback, $priority);
apply_filters('contensio/api/content-item', $data, $content);

remove_action('contensio/content/created', $callback, $priority);
remove_filter('contensio/api/content-item', $callback, $priority);
has_action('contensio/content/created');
has_filter('contensio/api/content-item');

A WordPress plugin author can read Contensio plugin code — and write it — from day one.


Hook names use / instead of _

WordPress hook names use underscores and are sometimes hard to read:

save_post
transition_post_status
wp_insert_post_data
pre_get_posts
the_excerpt_rss

Contensio uses forward slashes, which naturally group hooks by domain:

contensio/content/created
contensio/content/status-changed
contensio/frontend/head
contensio/user/registered
contensio/media/uploaded

The group before the first / is always the domain. The last segment is always the event. This makes hook names self-documenting and easy to search for.


Side-by-side: a real plugin

Here's the same plugin written for both platforms:

WordPress

<?php
/**
 * Plugin Name: Social Notifier
 * Description: Tweets when content is published.
 */

add_action('transition_post_status', function ($new, $old, $post) {
    if ($new !== 'publish' || $old === 'publish') return;
    tweet("Published: {$post->post_title}");
}, 10, 3);

add_filter('the_excerpt', function ($excerpt) {
    return $excerpt . ' — Read more on our site.';
});

add_action('wp_head', function () {
    echo '<meta name="theme-color" content="#ff5500">';
});

Contensio

<?php
/**
 * Plugin Name: Social Notifier
 * Description: Tweets when content is published.
 */

use Contensio\Models\Content;

add_action('contensio/content/status-changed', function (Content $content, string $from, string $to) {
    if ($to !== 'published' || $from === 'published') return;
    tweet("Published: {$content->translations->first()?->title}");
}, 10);

add_filter('contensio/frontend/content-body', function (string $html) {
    return $html . '<p>— Read more on our site.</p>';
});

add_action('contensio/frontend/head', function () {
    echo '<meta name="theme-color" content="#ff5500">';
});

The structure is identical. The differences are in what you receive — and that's where Contensio improves on WordPress.


Difference 1 — You get the full model, not a bare ID

WordPress passes primitive IDs and you have to fetch everything yourself:

// WordPress
add_action('save_post', function ($post_id) {
    $post   = get_post($post_id);               // extra query
    $author = get_user_by('id', $post->post_author); // extra query
    $tags   = get_the_tags($post_id);           // extra query

    notify($post->post_title, $author->display_name, $tags);
});

Contensio passes the full Eloquent model with relations already eager-loaded:

// Contensio
add_action('contensio/content/created', function (Content $content) {
    notify(
        $content->translations->first()?->title,
        $content->author?->name,
        $content->terms
    );
    // no extra queries — everything is already there
});

Difference 2 — No $accepted_args parameter

WordPress requires you to declare how many arguments you want to receive:

// WordPress — must tell WP you want all 3 args, or it passes only 1 by default
add_action('transition_post_status', function ($new, $old, $post) {
    // ...
}, 10, 3); // ← the magic number you always forget

If you forget the 3, WordPress only passes $new and $old and $post is null. This is a common source of bugs.

Contensio uses PHP variadic arguments internally — just declare what you want and it's there:

// Contensio — no counting needed
add_action('contensio/content/status-changed', function (Content $content, string $from, string $to) {
    // all three are available, no configuration needed
});

// Want fewer? Just use fewer parameters
add_action('contensio/content/status-changed', function (Content $content) {
    // $from and $to simply ignored — PHP handles it
});

Difference 3 — Plugin registration is via a service provider

WordPress discovers plugins by scanning wp-content/plugins/ for PHP files with a header comment. There's no autoloading, no namespaces unless you set them up manually.

Contensio plugins are standard Composer packages (or local packages) with a Laravel service provider. Hooks are registered from boot():

// WordPress — hooks at file scope, no class required (but also no autoloading)
add_action('save_post', 'my_plugin_save_post');

// Contensio — hooks registered from the service provider's boot()
class MyPluginServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        add_action('contensio/content/created', [$this, 'onContentCreated']);
        add_filter('contensio/api/content-item', [$this, 'filterApiItem']);
    }

    public function onContentCreated(Content $content): void { ... }

    public function filterApiItem(array $data, Content $content): array
    {
        $data['my_field'] = 'value';
        return $data;
    }
}

You get full Composer autoloading, dependency injection, typed methods, testability — everything Laravel provides — without any extra setup.


Difference 4 — Error handling is built in

In WordPress, an exception inside a hook callback can crash the page or produce a white screen. You have to wrap everything in try/catch yourself.

In Contensio, every callback is wrapped automatically. If your callback throws, the exception is passed to Laravel's report() (so it ends up in your logs / error tracker), and execution continues with the next callback. The page renders normally.

add_action('contensio/content/created', function (Content $content) {
    // If this throws — network error, bad API key, whatever —
    // it gets logged and the save still completes successfully.
    SlackNotifier::send("New content: {$content->id}");
});

Hook name mapping — WordPress to Contensio

WordPress hook Contensio equivalent
save_post (new post) contensio/content/created
post_updated contensio/content/updated
transition_post_status contensio/content/status-changed
before_delete_post contensio/content/deleting
deleted_post contensio/content/deleted
user_register contensio/user/registered
wp_login contensio/user/login
wp_logout contensio/user/logout
add_attachment contensio/media/uploaded
delete_attachment contensio/media/deleted
comment_post contensio/comment/submitted
wp_head contensio/frontend/head
wp_footer contensio/frontend/footer
the_content filter contensio/frontend/content-body
the_excerpt filter contensio/frontend/content-body (same hook)
wp_title filter contensio/frontend/page-title

What Contensio doesn't have (intentionally)

A few WordPress patterns that Contensio deliberately omits:

WordPress Contensio Why
$accepted_args (4th param) Not needed PHP variadic args handle it
current_filter() Not implemented Rarely needed in practice
did_action() counter Not implemented Use a static flag in your plugin if needed
WP_Query filters Not applicable Use Eloquent scopes or query macros instead
wp_enqueue_scripts Not needed Use Laravel Mix / Vite asset pipeline

Summary

If you know WordPress hooks, you know Contensio hooks. The same four functions (add_action, do_action, add_filter, apply_filters), the same priority system, the same remove/has helpers.

What's better: you get full Eloquent models instead of IDs, no $accepted_args counting, Laravel's service container for dependency injection, and automatic error isolation so one broken plugin never crashes the page.


See also