WordPress is well-known for its ability to extend to support custom content in the form of custom post types.

WordPress is also well-known for its inability to build relationships between post types, custom or otherwise.

Savvy developers use tools like Posts 2 Posts to overcome this limitation.  It's a powerful abstraction of an oft-needed one in the development world.  Unfortunately, it's an abstraction.

When you use abstractions too often, you neglect the underlying architecture and can run afoul of certain issues endemic to how things are build under the surface.

Abstractions build false confidence for new developers as well - so I'm going to show you how to build a post-to-post relationship system in WordPress from scratch.

Shadow Taxonomies

Essentially, you need to build a custom taxonomy that shadows your custom post type (or core post type) directly in a 1:1 fashion.

When new posts are added, new terms are added.

When posts are deleted, terms are deleted.

Then, rather than relating posts to other posts, you can tag a post with a term from the matching shadow taxonomy.

First Step

Assume you have a custom post type called "movie."[ref]Assume you're building a system to track Oscar nominees.  You might want to have both movies and actors since they can both be nominated.  But let's say you also want to relate authors to movies.  This system is designed for exactly that scenario.[/ref]  For simplicity, we'll create a taxonomy called "_movie."  The leading underscore doesn't mean anything significant, it's just how we're distinguishing one form of content from another.

/**
* Register a taxonomy that shadows our Movie custom post type.
*
* This taxonomy will only be available for Actors.
*
* @uses register_taxonomy()
* @uses __()
*/
function register_shadow_taxonomy() {
register_taxonomy(
'_movie',
'actor',
array(
'label' => __( 'Movie', 'textdomain' ),
'rewrite' => false,
'show_in_menu' => false,
'show_tagcloud' => false,
'hierarchical' => false,
)
);
}
add_action( 'init', 'register_shadow_taxonomy' );

Second Step

When you update posts in the "movie" post type collection, you also want to create entries in the "_movie" taxonomy.

/**
* When a movie CPT entry is saved, automatically create a entry in our pseudo-hidden "_movie" taxonomy.
*
* @uses get_post_type()
* @uses get_post()
* @uses get_term_by()
* @uses wp_insert_term()
*
* @const DOING_AUTOSAVE
*
* @param int $post_id
*/
function update_shadow_taxonomy( $post_id ) {
// If we're running an auto-save, don't create a term
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}

// If we're not working with a movie, don't create a term
if ( 'movie' !== get_post_type( $post_id ) ) {
    return;
}

// If we can't retrieve the movie, don't create a term
$movie = get_post( $post_id );
if ( null === $movie && 'Auto Draft' !== $movie->post_title ) {
    return;
}

// If the movie already exists, don't create a term.
$term = get_term_by( 'name', $movie->post_title, '_movie' );
if ( false === $term ) {
    // Create the term
    wp_insert_term( $movie->post_title, '_movie' );
}

}
add_action( 'save_post', 'update_shadow_taxonomy' );

Third Step

This one is optional - but you might just want to delete terms from the taxonomy as you delete posts from the collection.[ref]I say it's optional because, really, the missing post won't affect much.  But if you're deleting movies (due to editor error) and still present their relationships with actors, things might look a bit funny.[/ref]

/**
* Remove a term from the shadow taxonomy upon post delete.
*
* @uses get_post_type()
* @uses get_post()
* @uses get_term_by()
* @uses wp_delete_term()
*
* @const DOING_AUTOSAVE
*
* @param int $post_id
*/
function delete_shadow_tax_term( $post_id ) {
// If we're running an auto-save, don't delete a term
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}

// If we're not working with a movie, don't delete a term
if ( 'movie' !== get_post_type( $post_id ) ) {
    return;
}

// If we can't retrieve the movie, don't delete a term
$movie = get_post( $post_id );
if ( null === $movie ) {
    return;
}

// If the movie already exists, don't delete anything.
$term = get_term_by( 'name', $movie->post_title, '_movie' );
if ( false !== $term ) {
    // Delete the term
    wp_delete_term( $term->term_id, '_movie' );
}

}
add_action( 'before_delete_post', 'delete_shadow_tax_term' );

Use in Production

Now that you have a shadow taxonomy set up, you can use it to relate any other post objects in your installation to movies.  Extending the Oscar concept, this includes not just actors but also directors, producers, studios, and even reviews.

Post types are useful for storing large chunks of data in the WordPress database.  Taxonomies are great for storing - and efficiently querying - relationships between data.

Pairing them together means you have the best of both world: comprehensive data and efficient query-able relationships.