rilwis/meta-box
Meta Box is a powerful WordPress framework for creating custom fields, meta boxes, and settings pages without the hassle. Build custom post types, taxonomies, and user profiles with a clean API and 40+ field types, all developer-friendly and performance-optimized for production sites.
Meta Box is a WordPress plugin, not a plain Composer library, so treat it as a runtime dependency of the site rather than something you require into an application bootstrapper.
Install (pick one):
wp plugin install meta-box --activatecomposer/installers and an installer-path of wp-content/plugins/{$name}, then composer require rilwis/meta-box. You still have to activate it in WP; Composer only puts the files in place.require_once __DIR__ . '/vendor/meta-box/meta-box.php';) if you cannot control the plugin stack — but you then own updates and conflict handling.Where to look first:
meta-box/inc/fields/ — one class per field type; the fastest way to understand what a field actually stores.meta-box/inc/meta-box.php — the field-group schema (id, title, post_types, context, priority, fields, validation, include/exclude).docs.metabox.io — the field-type reference and the "Meta Box Builder" extension, which lets you click together a group and export it as PHP.First use case — add a subtitle to posts and print it in the theme:
// functions.php or a small site plugin
add_filter( 'rwmb_meta_boxes', function ( $meta_boxes ) {
$meta_boxes[] = [
'id' => 'post_extras',
'title' => __( 'Post Extras', 'mytheme' ),
'post_types' => [ 'post' ],
'context' => 'normal',
'priority' => 'high',
'fields' => [
[
'id' => 'subtitle',
'name' => __( 'Subtitle', 'mytheme' ),
'type' => 'text',
'desc' => __( 'Shown under the post title.', 'mytheme' ),
],
],
];
return $meta_boxes;
} );
// single.php
$subtitle = rwmb_meta( 'subtitle', [ 'post_id' => get_the_ID() ] );
if ( $subtitle ) {
printf( '<h2 class="subtitle">%s</h2>', esc_html( $subtitle ) );
}
That is the whole loop: register a group, read a value, escape it.
Keep field groups as data, not markup. Every group is an array passed through rwmb_meta_boxes. Put one file per group in a meta-boxes/ directory and require them from a bootstrap file; it keeps the config diffable and reviewable, and avoids a 600-line functions.php.
Three read helpers, three jobs:
rwmb_meta( $id, $args ) — get the raw/semantic value. $args accepts post_id (and size, limit for media fields).rwmb_get_value( $id, $args ) — same, but a bit more explicit about the current object.rwmb_the_value( $id, $args ) — echoes ready-made output (<img> for image fields, links for file fields). Use it for "just show it" cases, rwmb_meta() when you need your own markup.Repeatable content is the highest-leverage feature. 'clone' => true turns any field into an array; add 'sort_clone' => true for drag-ordering and 'max_clone' to cap it:
[
'id' => 'facts',
'name' => 'Facts',
'type' => 'text',
'clone' => true,
'sort_clone' => true,
]
foreach ( (array) rwmb_meta( 'facts' ) as $fact ) {
echo '<li>' . esc_html( $fact ) . '</li>';
}
Media fields (image, image_advanced, file_advanced) return attachment data. For a grid of thumbnails:
$images = rwmb_meta( 'gallery', [ 'size' => 'medium' ] );
foreach ( $images as $image ) {
printf( '<img src="%s" alt="%s">', esc_url( $image['url'] ), esc_attr( $image['alt'] ) );
}
Use rwmb_the_value( 'gallery', [ 'size' => 'medium' ] ) when you want the plugin's <img> markup and no per-image logic.
Data lives in normal post meta. The field id is the meta key, so get_post_meta( $post_id, 'subtitle', true ), REST exposure, WP_Query meta queries, and migrations all work exactly as you expect. Cloneable/multiple fields store one serialized array under a single key. This is the main reason Meta Box is low-risk: there is no proprietary storage layer to escape from.
Conditional display uses include/exclude callbacks in the group config, e.g. only render a "book details" group when a book_type taxonomy term matches. For intra-form show/hide driven by another field's value, that is the separate Conditional Logic extension.
Frontend authoring is the MB Frontend Submission extension ([rwmb_frontend_form id="post_extras" post_fields="title,content"]), which handles nonce, validation, media uploads, and post creation. Do not hand-roll a frontend form around the field config unless you are prepared to enqueue the plugin's media/date/color scripts yourself.
Object types beyond posts — terms, users, comments, settings pages — are each a separate extension. Budget for them up front; the free core is post meta only.
Admin UX: the Builder extension is worth it just for generating and exporting config arrays. For list-table columns of your custom values, that is the MB Columns extension.
Pin the source, not this Packagist snapshot. The package metadata shows a last release of 2018-01-08, which is a stale 4.x artefact. Install from wordpress.org or the vendor's own distribution and stay current; 4.x on PHP 8 / recent WP is a compatibility question you should test rather than assume.
Free vs. paid is the real cost centre. Core is free; Settings Page, Views, Blocks, Relationships, Group, Conditional Logic, Frontend Submission, Columns, and Tooltip are commercial. A "simple custom fields" requirement can turn into a licence line item once the brief grows.
Empty values are '', not null. Always guard before echoing, and never assume a clone/multiple field returns an array — cast with (array) or check is_array().
Escape on output anyway. Meta Box sanitizes on save per field type, but rwmb_meta() returns stored data. esc_html, esc_url, wp_kses_post as appropriate; never trust it just because it came through a meta box.
Autosave ('autosave' => true) writes via AJAX. Any custom save_post logic that inspects $_POST will see nothing on autosave. Hook rwmb_before_save_post / rwmb_after_save_post (4.x) or read the meta after the box has saved, and always skip revisions.
Registration timing. rwmb_meta_boxes fires after plugins load. If the target post type is registered by another plugin at a later priority, the box silently does not appear. Same class of bug when the post_types slug is a typo — the box just never shows.
Removing a field does not remove its data. Orphaned meta rows accumulate. Clean up deliberately with a one-off migration when you delete fields.
Debugging checklist when rwmb_meta() returns nothing:
rwmb_meta_boxes array.id match exactly (case, underscores)?post_id — inside a secondary loop, get_the_ID() is the wrong post?wp_postmeta row directly; serialized arrays look like a:2:{...} and are expected for cloneable fields.Performance. rwmb_meta() is a get_post_meta() call; in a loop over hundreds of posts, prefetch with update_meta_cache() or read the meta once. Serialized clone arrays cannot be efficiently meta_query-filtered by sub-value — if you need to query on it, model it as separate meta keys.
Extension points worth knowing: rwmb_meta_boxes for registration, rwmb_before_save_post / rwmb_after_save_post for save-time logic, and the custom field type filter for adding your own field class when none of the ~40 built-ins fit.
Keep field IDs namespaced (mytheme_subtitle, not subtitle). Field IDs are global meta keys; generic names collide with other plugins and with future core features.
How can I help you explore Laravel packages today?