0

I'm building an inventory system where inventory items have their own page but where standard WordPress post post types can also be tagged with these inventory items. As far as I understand, this means I must implement both a custom post type (inventory) to provide each item's page and a custom, non-hierarchical taxonomy (inventory_item) to represent the relationships between posts in the post post type and pages in the inventory post type. Each custom post type post would be associated with exactly one inventory_item term.

Assuming that the above is the best approach to accomplish this sort of relationship, then this implies that I must handle various edge cases to guarantee the custom-post-to-term relationship is maintained. For example:

  • When an inventory post is edited or deleted, the corresponding inventory_item term must be updated/deleted too.
  • It should not be possible for users to create or edit inventory_item terms on their own, for example via the term edit page or editor term metabox / Gutenberg widget. These should only be created when a corresponding inventory custom post type post is created.
  • Should I create the term when the custom post type post is published, or merely drafted? What about when a post is trashed but not fully deleted - when should I delete the terms and associations?
  • How do I get the inventory_item terms to take the user to the corresponding custom post when the link is clicked (for example in a tag cloud)?

Clearly, maintaining this strict one-to-one relationship will require many hooks to handle the various ways in which it can be disrupted. I'd like to hear thoughts about whether this approach seems like the best one to implement what I need, and whether anyone knows of a plugin that implements this already that I could take a look at.

0

It's difficult, but not impossible to maintain the one-to-one relationship between a taxonomy and a custom post by using many hooks. Because WordPress is designed to allow many posts to tag many terms, there are various hooks that need to be used to prevent the creation of terms without custom posts and vice versa.

The code below shows the complete system, including the registration of the custom post type and taxonomy. It has hooks to trigger the creation of the corresponding taxonomy terms when new posts are made in the custom post type, and to handle when they are edited or deleted. It also disables the term editor and various user permissions to stop users from being able to edit the taxonomy terms directly. It also overrides term links on the front end to no longer point to the term archive pages on posts tagged with the term, and instead link to the corresponding custom single post. Finally, it removes any invalid terms that a normal (non-custom) post get tagged with in the custom taxonomy upon save, because WordPress lets the user create new terms via the editor, which we don't want.

An example of this code in a working environment can be seen in my plugin, Academic Labbook Plugin.

// Register the custom post type.
function wpse_344265_register_post_type() {
    $args = array(
        'labels'       => $labels,
        'description'  => __( 'My custom post type.', 'my-text-domain' ),
        'public'       => true,
        'hierarchical' => false,
        'show_in_rest' => true,
    );

    register_post_type( 'my-custom-post-type', $args );
}
add_action( 'init', 'wpse_344265_register_post_type' );

// Create corresponding custom taxonomy terms whenever posts are created.
function wpse_344265_associate_custom_post_with_term( $post_id, $post ) {
    if ( 'my-custom-post-type' !== $post->post_type ) {
        return;
    }

    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        // Don't create term for autosaves.
        return;
    }

    if ( 'publish' !== $post->post_status ) {
        // Don't create a term unless the post is being published.
        return;
    }

    // Add or update the associated term.
    wpse_344265_update_custom_taxonomy_term( $post );
}
add_action( 'save_post', 'wpse_344265_associate_custom_post_with_term', 10, 2 );

// Delete corresponding custom taxonomy terms whenever posts are deleted.
function wpse_344265_delete_associated_custom_post_term( $post_id ) {
    $post = get_post( $post_id );

    if ( is_null( $post ) ) {
        // Invalid post.
        return;
    }

    if ( 'my-custom-post-type' !== $post->post_type ) {
        return;
    }

    $term = wpse_344265_get_custom_term( $post );

    if ( ! $term ) {
        // No term to delete.
        return;
    }

    wp_delete_term( $term->term_id, 'my-custom-taxonomy' );
    clean_term_cache( array( $term->term_id ), 'my-custom-taxonomy' );
}
add_action( 'deleted_post', 'wpse_344265_delete_associated_custom_post_term' );

function wpse_344265_update_custom_taxonomy_term( $post ) {
    $post = get_post( $post );

    if ( is_null( $post ) ) {
        // Invalid post.
        return;
    }

    // Get custom term, if present.
    $term = wpse_344265_get_custom_term( $post );

    // Temporarily disable the filter that blocks creation of terms.
    remove_filter( 'pre_insert_term', 'wpse_344265_disallow_insert_term', 10, 2 );

    if ( ! $term ) {
        // Term doesn't yet exist.
        $args = array(
            'slug' => wpse_344265_get_custom_taxonomy_term_slug( $post ),
        );

        wp_insert_term( $post->post_title, 'my-custom-taxonomy', $args );
    } else {
        // Update term.
        $args = array(
            'name' => $post->post_title,
            'slug' => wpse_344265_get_custom_taxonomy_term_slug( $post ),
        );

        wp_update_term( $term->term_id, 'my-custom-taxonomy', $args );
    }

    // Re-enable the filter.
    add_filter( 'pre_insert_term', wpse_344265_disallow_insert_term, 10, 2 );
}

function wpse_344265_get_custom_term( $post ) {
    $post = get_post( $post );

    if ( is_null( $post ) ) {
        // Invalid post.
        return;
    }

    $slug = wpse_344265_get_custom_taxonomy_term_slug( $post );
    return get_term_by( 'slug', $slug, 'my-custom-taxonomy' );
}

function wpse_344265_get_custom_taxonomy_term_slug( $post ) {
    $post = get_post( $post );

    if ( is_null( $post ) ) {
        // Invalid post.
        return;
    }

    // Use the custom post's ID since this doesn't change even if e.g. its slug does.
    return $post->ID;
}

function wpse_344265_register_taxonomy() {
    $args = array(
        'hierarchical'      => false,
        'labels'            => array(
            'name'          => __( 'My custom taxonomy', 'my-text-domain' ),
        ),
        'capabilities' => array(
            'manage_terms'  =>   'do_not_allow',
            'edit_terms'    =>   'do_not_allow',
            'delete_terms'  =>   'do_not_allow',
            'assign_terms'  =>   'edit_posts', // Needed to allow assignment in block editor.
        ),
        'public'            => true,
        'show_in_menu'      => false, // Hides the term edit page.
        'show_in_rest'      => true,  // Needed for block editor support.
        'show_admin_column' => true,  // Show associated terms in admin edit screen.
    );

    $supported_post_types = array(
        'post',
    );

    register_taxonomy( 'my-custom-taxonomy', $supported_post_types, $args );
}
add_action( 'init', 'wpse_344265_register_taxonomy' );

// Override default term links to point towards the term custom post instead of term
// archive.
function wpse_344265_override_term_link( $link, $term, $taxonomy ) {
    if ( 'my-custom-taxonomy' !== $taxonomy ) {
        return $link;
    }

    $my_custom_post = wpse_344265_get_post_from_custom_term( $term );

    if ( is_null( $my_custom_post ) ) {
        return $link;
    }

    return get_permalink( $my_custom_post );
}
add_filter( 'term_link', 'wpse_344265_override_term_link', 10, 3 );

function wpse_344265_get_post_from_custom_term( $term ) {
    // The term's slug is the post ID.
    return get_post( $term->slug );
}

// Disallow creation of new terms directly.
function wpse_344265_disallow_insert_term( $term, $taxonomy ) {
    if ( 'my-custom-taxonomy' !== $taxonomy ) {
        return $term;
    }

    // Return an error in all circumstances.
    return new WP_Error(
        'wpse_344265_disallow_insert_term',
        __( 'Your role does not have permission to add terms to this taxonomy', 'my-text-domain' )
    );
}
add_filter( 'pre_insert_term', 'wpse_344265_disallow_insert_term', 10, 2 );

// Delete any invalid custom taxonomy items when post terms are set.
function wpse_344265_reject_invalid_terms( $object_id, $tt_id, $taxonomy ) {
    if ( 'my-custom-taxonomy' !== $taxonomy ) {
        return;
    }

    $term = get_term_by( 'term_taxonomy_id', $tt_id, 'my-custom-taxonomy' );

    if ( ! $term ) {
        // Nothing to do here.
        return;
    }

    // Check term is valid.
    $custom_post = wpse_344265_get_post_from_custom_term( $term );

    if ( ! $custom_post ) {
        // This is not a valid custom taxonomy term - delete it.
        wp_delete_term( $term->term_id, 'my-custom-taxonomy' );
    }
}
add_action( 'added_term_relationship', 'wpse_344265_reject_invalid_terms', 10, 3 );
| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.