WP Manifestindependent plugin directory
manifest / developer / rootstuff-relationships

Rootstuff Relationships

A developer-first relationship layer for WordPress. Laravel-style belongsTo, hasMany, and belongsToMany on dedicated database tables with Gutenberg integration.

by Rootstuff · github.com/rootstuff/rootstuff-relationships · website

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/rootstuff/rootstuff-relationships/archive/refs/heads/main.zip

A developer-first relationship layer for WordPress content modeling. Define belongsTo, hasMany, and belongsToMany relationships between any post types using dedicated database tables — no postmeta abuse, no ACF dependency.

Requirements

  • WordPress 6.4+
  • PHP 8.0+

Installation

composer install
npm install && npm run build

Activate the plugin through the WordPress admin or WP-CLI:

wp plugin activate rootstuff-relationships

The plugin automatically creates two database tables on activation:

  • {prefix}rootstuff_rel_relationships — stores connections between objects
  • {prefix}rootstuff_rel_relationship_meta — optional metadata per connection

Defining Relationships

Register relationships on the rootstuff_relationships_init action using the Schema class:

use Rootstuff\Relationships\Schema;

add_action('rootstuff_relationships_init', function () {

    // Many-to-many: resources ↔ posts
    Schema::belongsToMany('resource', 'post', 'resource_posts', [
        'labels' => [
            'from' => 'Related Posts',
            'to'   => 'Related Resources',
        ],
    ]);

    // One-to-many: author → books
    Schema::hasMany('author', 'book', 'author_books');

    // One-to-many from the child side
    Schema::belongsTo('book', 'author', 'book_author');

    // One-to-one
    Schema::hasOne('employee', 'badge', 'employee_badge');
});

All relationships are bidirectional by default.

Same-Type Relationships

When both sides share the same post type, you must either define roles (for directional relationships) or mark the relationship as symmetric:

// Directional: parent/child pages
Schema::belongsToMany('post', 'post', 'related_posts', [
    'roles' => ['source', 'related'],
]);

// Symmetric: related posts (direction doesn't matter)
Schema::belongsToMany('post', 'post', 'similar_posts', [
    'symmetric' => true,
]);

PHP API

The Relation class provides the core CRUD operations:

use Rootstuff\Relationships\Relation;

// Connect two objects
Relation::connect('resource_posts', $resource_id, $post_id);

// Disconnect
Relation::disconnect('resource_posts', $resource_id, $post_id);

// Check if a connection exists
Relation::exists('resource_posts', $resource_id, $post_id);

// Get related posts — auto-detects which side you're on
$posts = Relation::get('resource_posts', $post_id);

// Explicit post type (zero-overhead, skips auto-detect)
$resources = Relation::get('resource_posts', $post_id, 'post');

// Get related IDs only (lightweight, cached)
$ids = Relation::getIds('resource_posts', $resource_id);

// Sync: replace all connections with a new ordered set
Relation::sync('resource_posts', $resource_id, 'resource', [10, 23, 45]);

Side Resolution

The optional $side parameter tells the plugin which side of the relationship your object is on. It accepts:

  • A post type name'post', 'resource', 'author', etc.
  • A role name — for same-type relationships with roles
  • null (default) — auto-detects via get_post_type($object_id)

For same-type relationships with roles:

$related = Relation::get('related_posts', $post_id, 'source');

For symmetric relationships, no side is needed — results from both directions are merged automatically.

WP_Query Integration

Query related posts directly with WP_Query using the rootstuff_relationships_related parameter:

$related = new WP_Query([
    'post_type'                       => 'book',
    'rootstuff_relationships_related' => [
        'rel_type'  => 'author_books',
        'object_id' => $author_id,
        'side'      => 'author',    // optional, auto-detects if omitted
    ],
    'orderby' => 'rootstuff_relationships_sort_order',
    'order'   => 'ASC',
]);

REST API

Endpoints are registered under /wp-json/rootstuff-relationships/v1/:

Endpoint Method Description
/relationships GET List all registered relationship schemas
/connections/{rel_type}/{object_id} GET Get connections for an object
/connections/{rel_type} POST Create a connection
/connections/{rel_type}/{from_id}/{to_id} DELETE Remove a connection
/connections/{rel_type}/{object_id}/sync POST Sync (replace) all connections
/search GET Search posts by type for the editor selector

GET and sync endpoints accept a side parameter (post type or role name). All mutation endpoints require edit_posts capability.

Gutenberg Integration

When relationships are registered, a sidebar panel automatically appears in the block editor for each relevant post type. The panel provides:

  • Searchable selector to find and add related content
  • Ordered list of connected items with remove buttons
  • Automatic sync on post save

Hooks

Actions

  • rootstuff_relationships_connected — fires after a connection is created
  • rootstuff_relationships_disconnected — fires after a connection is removed
  • rootstuff_relationships_disconnected_all — fires after all connections for an object are removed

Caching

All getIds() queries are cached using the WordPress object cache (rootstuff-relationships group). Caches are automatically invalidated on connect, disconnect, and sync operations.

Uninstall

Deactivating the plugin does nothing to the data. Deleting it through the WordPress admin runs uninstall.php, which drops both database tables permanently.

License

GPL-2.0-or-later