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
- Hook system — full API reference and built-in hook list
- The service provider — where to register your hooks
- Plugin anatomy — file structure and
plugin.json