EDIT
PART DEUX OR PART DEUX - The Dirty Sequal, The Term Saga Continues...
Before anyone read this section, I will urge you to read everything I explained in ORIGINAL ANSWER. wp_insert_term()
does not allow duplicate term names in non hierarchical taxonomies or in hierarchical taxonomies within the same hierarchy.
From comments to my answer, the answer from OP and from the OP
What i need is a way to have the same Name in the Taxonomy Hierarchy.
There is a way to achieve this, lets look at how:
CAUTION - caution - CAUTION - caution !!!!
I went with a modified class of the wp_insert_term()
function. As I said, there are protection inside wp_insert_term()
to avoid duplicate term names. What I have done is to get rid of this protection in order to allow duplicate names within the same hierarchy. You basically need to remove the name check section and then the section that delete accidental duplicates when the term is already inserted.
Because of this, this class will allow an unlimited amount of terms with the same name, so it should be used with great care. 100 accidental page loads will insert 100 terms with the same name.
Here is the class with a few notes
NOTE:
This requires at least PHP 5.4.
Although I ran a few test to check the class\' operation, I have not run any complete test on this, so PLEASE PLEASE PLEASE, run this on a local test install first and verify everything properly
The class can be improved and modified as needed. I might have mixed syntaxes and scrambled code. This is just a very basic backbone with only the essentials
Lets look at the code:
/**
* WPInsertTerm class
*
* Class to insert terms into the db
*
* If term names are identical, in stead of throwing an error like
* wp_insert_term(), the function will add valid term meta_key
*
* @author Pieter Goosen
* @version 1.0.0
* @access public
*/
class WPInsertTerm
{
/**
* Arguments to hold defaults
* @since 1.0.0
* @var array
*/
protected $defaults = [
\'alias_of\' => \'\',
\'description\' => \'\',
\'parent\' => 0,
\'slug\' => \'\'
];
/**
* Arguments set by user
* @since 1.0.0
* @var array
*/
protected $args = [];
/**
* Term to insert
* @since 1.0.0
* @var string
*/
protected $term = null;
/**
* Taxonomy the term should belong to
* @since 1.0.0
* @var string
*/
protected $taxonomy = null;
/**
* Constructor
*
* @param string $term = null
* @param string $taxonomy = null
* @param array $args = []
* @since 1.0.0
*/
public function __construct( $term = null, $taxonomy = null, $args = [] )
{
$this->term = $term;
$this->taxonomy = $taxonomy;
if ( is_array( $args ) ) {
$this->args = array_merge( $this->defaults, $args );
} else {
$this->args = $this->defaults;
}
}
/**
* Public method wpdb()
*
* Returns the global wpdb class
*
* @since 1.0.0
* @return $wpdb
*/
public function wpdb()
{
global $wpdb;
return $wpdb;
}
/**
* Private method validateVersion()
*
* Validate the current WordPress version
*
* @since 1.0.0
* @return $validateVersion
*/
private function validateVersion()
{
global $wp_version;
$validateVersion = false;
if ( \'4.4\' > $wp_version ) {
throw new InvalidArgumentException(
sprintf(
__( \'Your WordpPress version is too old. A minimum version of WordPress 4.4 is expected. Please upgrade\' ),
__METHOD__
)
);
}
return $validateVersion = true;
}
/**
* Private method validateTaxonomy()
*
* Validate the $taxonomy value
*
* @since 1.0.0
* @return $validateTaxonomy
*/
private function validateTaxonomy()
{
$validateTaxonomy = filter_var( $this->taxonomy, FILTER_SANITIZE_STRING );
// Check if taxonomy is valid
if ( !taxonomy_exists( $validateTaxonomy ) ) {
throw new InvalidArgumentException(
sprintf(
__( \'Your taxonomy does not exists, please add a valid taxonomy\' ),
__METHOD__
)
);
}
return $validateTaxonomy;
}
/**
* Private method validateTerm()
*
* Validate the $term value
*
* @since 1.0.0
* @return $validateTerm
*/
private function validateTerm()
{
/**
* Filter a term before it is sanitized and inserted into the database.
*
* @since 1.0.0
*
* @param string $term The term to add or update.
* @param string $taxonomy Taxonomy slug.
*/
$validateTerm = apply_filters( \'pre_insert_term\', $this->term, $this->validateTaxonomy() );
// Check if the term is not empty
if ( empty( $validateTerm ) ) {
throw new InvalidArgumentException(
sprintf(
__( \'$term should not be empty, please add a valid value\' ),
__METHOD__
)
);
}
// Check if term is a valid integer if integer is passed
if ( is_int( $validateTerm )
&& 0 == $validateTerm
){
throw new InvalidArgumentException(
sprintf(
__(\'Invalid term id supplied, please asdd a valid value\'),
__METHOD__
)
);
}
// Term is not empty, sanitize the term and trim any white spaces
$validateTerm = filter_var( trim( $validateTerm ), FILTER_SANITIZE_STRING );
if ( empty( $validateTerm ) ){
throw new InvalidArgumentException(
sprintf(
__( \'Invalid term supplied, please asdd a valid term name\' ),
__METHOD__
)
);
}
return $validateTerm;
}
/**
* Private method parentExist()
*
* Validate if the parent term exist if passed
*
* @since 1.0.0
* @return $parentexist
*/
private function parentExist()
{
$parentExist = $this->args[\'parent\'];
if ( $parentExist > 0
&& !term_exists( (int) $parentExist )
) {
throw new InvalidArgumentException(
sprintf(
__( \'Invalid parent ID supplied, no term exists with parent ID passed. Please add a valid parent ID\' ),
__METHOD__
)
);
}
return $parentExist;
}
/**
* Private method sanitizeTerm()
*
* Sanitize the term to insert
*
* @since 1.0.0
* @return $sanitizeTerm
*/
private function sanitizeTerm()
{
$taxonomy = $this->validateTaxonomy();
$arguments = $this->args;
$arguments[\'taxonomy\'] = $taxonomy;
$arguments[\'name\'] = $this->validateTerm();
$arguments[\'parent\'] = $this->parentExist();
// Santize the term
$arguments = sanitize_term( $arguments, $taxonomy, \'db\' );
// Unslash name and description fields and cast parent to integer
$arguments[\'name\'] = wp_unslash( $arguments[\'name\'] );
$arguments[\'description\'] = wp_unslash( $arguments[\'description\'] );
$arguments[\'parent\'] = (int) $arguments[\'parent\'];
return (object) $arguments;
}
/**
* Private method slug()
*
* Get or create a slug if no slug is set
*
* @since 1.0.0
* @return $slug
*/
private function slug()
{
$term = $this->sanitizeTerm();
$new_slug = $term->slug;
if ( !$new_slug ) {
$slug = sanitize_title( $term->name );
} else {
$slug = $new_slug;
}
return $slug;
}
/**
* Public method addTerm()
*
* Add the term to db
*
* @since 1.0.0
*/
public function addTerm()
{
$wpdb = $this->wpdb();
$term = $this->sanitizeTerm();
$taxonomy = $term->taxonomy;
$name = $term->name;
$parent = $term->parent;
$term_group = $term->term_group;
$term_group = 0;
if ( $term->alias_of ) {
$alias = get_term_by(
\'slug\',
$term->alias_of,
$term->taxonomy
);
if ( !empty( $alias->term_group ) ) {
// The alias we want is already in a group, so let\'s use that one.
$term_group = $alias->term_group;
} elseif ( ! empty( $alias->term_id ) ) {
/*
* The alias is not in a group, so we create a new one
* and add the alias to it.
*/
$term_group = $wpdb->get_var(
"SELECT MAX(term_group)
FROM $wpdb->terms"
) + 1;
wp_update_term(
$alias->term_id,
$this->args[\'taxonomy\'],
[
\'term_group\' => $term_group,
]
);
}
}
$slug = wp_unique_term_slug(
$this->slug(),
$term
);
if ( false === $wpdb->insert( $wpdb->terms, compact( \'name\', \'slug\', \'term_group\' ) ) ) {
return new WP_Error( \'db_insert_error\', __( \'Could not insert term into the database\' ), $wpdb->last_error );
}
$term_id = (int) $wpdb->insert_id;
// Seems unreachable, However, Is used in the case that a term name is provided, which sanitizes to an empty string.
if ( empty( $slug ) ) {
$slug = sanitize_title(
$slug,
$term_id
);
/** This action is documented in wp-includes/taxonomy.php */
do_action( \'edit_terms\', $term_id, $taxonomy );
$wpdb->update( $wpdb->terms, compact( \'slug\' ), compact( \'term_id\' ) );
/** This action is documented in wp-includes/taxonomy.php */
do_action( \'edited_terms\', $term_id, $taxonomy );
}
$tt_id = $wpdb->get_var(
$wpdb->prepare( "
SELECT tt.term_taxonomy_id
FROM $wpdb->term_taxonomy AS tt
INNER JOIN $wpdb->terms AS t
ON tt.term_id = t.term_id
WHERE tt.taxonomy = %s
AND t.term_id = %d
",
$taxonomy,
$term_id
)
);
if ( !empty($tt_id) ) {
return [
\'term_id\' => $term_id,
\'term_taxonomy_id\' => $tt_id
];
}
$wpdb->insert(
$wpdb->term_taxonomy,
compact( \'term_id\', \'taxonomy\', \'description\', \'parent\') + [\'count\' => 0]
);
$tt_id = (int) $wpdb->insert_id;
/**
* Fires immediately after a new term is created, before the term cache is cleaned.
*
* @since 2.3.0
*
* @param int $term_id Term ID.
* @param int $tt_id Term taxonomy ID.
* @param string $taxonomy Taxonomy slug.
*/
do_action( "create_term", $term_id, $tt_id, $taxonomy );
/**
* Fires after a new term is created for a specific taxonomy.
*
* The dynamic portion of the hook name, `$taxonomy`, refers
* to the slug of the taxonomy the term was created for.
*
* @since 2.3.0
*
* @param int $term_id Term ID.
* @param int $tt_id Term taxonomy ID.
*/
do_action( "create_$taxonomy", $term_id, $tt_id );
/**
* Filter the term ID after a new term is created.
*
* @since 2.3.0
*
* @param int $term_id Term ID.
* @param int $tt_id Taxonomy term ID.
*/
$term_id = apply_filters( \'term_id_filter\', $term_id, $tt_id );
clean_term_cache($term_id, $taxonomy);
/**
* Fires after a new term is created, and after the term cache has been cleaned.
*
* @since 2.3.0
*
* @param int $term_id Term ID.
* @param int $tt_id Term taxonomy ID.
* @param string $taxonomy Taxonomy slug.
*/
do_action( \'created_term\', $term_id, $tt_id, $taxonomy );
/**
* Fires after a new term in a specific taxonomy is created, and after the term
* cache has been cleaned.
*
* The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug.
*
* @since 2.3.0
*
* @param int $term_id Term ID.
* @param int $tt_id Term taxonomy ID.
*/
do_action( "created_$taxonomy", $term_id, $tt_id );
return [
\'term_id\' => $term_id,
\'term_taxonomy_id\' => $tt_id
];
}
}
You can then use it as follow:
$q = new WPInsertTerm( \'My Term\', \'category\', [\'parent\' => 1] );
$q->addTerm();
Here we add our term, My Term
to the parent term with ID 1
in build in taxonomy category
.
Note, running this 100 times will add 100 terms called My Term
under parent term 1
.
ADDITIONAL NOTES
A final note, having several terms with the same name will be very very confusing and might open a few cans of rotten worms, so you will need to be absolutely sure that this is what you want
ORIGINAL ANSWER
wp_insert_term()
is quite a busy function with a lot of stuff that goes on before a term is successfully added to a taxonomy.
The first important section is this:
$slug_provided = ! empty( $args[\'slug\'] );
if ( ! $slug_provided ) {
$slug = sanitize_title( $name );
} else {
$slug = $args[\'slug\'];
}
In short, what this means is, if you haven\'t explicitly set a slug, a slug will be created from the name given via the santitize_title()
function. This is the basis which will decide whether a term will be added or not as the slug is the important part moving forward.
So if we would want to insert a term with the name My Term
, we would end up with a slug like my-term
if we did not set a slug.
The next important part is this: (Which runs regardless if slug being where set or not)
/*
* Prevent the creation of terms with duplicate names at the same level of a taxonomy hierarchy,
* unless a unique slug has been explicitly provided.
*/
$name_matches = get_terms( $taxonomy, array(
\'name\' => $name,
\'hide_empty\' => false,
) );
/*
* The `name` match in `get_terms()` doesn\'t differentiate accented characters,
* so we do a stricter comparison here.
*/
$name_match = null;
if ( $name_matches ) {
foreach ( $name_matches as $_match ) {
if ( strtolower( $name ) === strtolower( $_match->name ) ) {
$name_match = $_match;
break;
}
}
}
$name
($name = wp_unslash( $args[\'name\'] );) which is the name you have explicitely set is used to return all terms with a name matching $name
to compare to the term you are trying to insert, if there are any. If there are any terms that exists by $name
, a more exact match is done to match terms.
As the description/docblock says get_terms()
doesn\'t differentiate accented characters, so we might end up with terms having names like my Term
and My terM
. All letters are set to lower in this match, so all term names are set to my term
for matching. This makes sense, because if those terms (or any of the returned terms) were created (either back end or front end or programmically) without having a slug set, names like my Term
and My terM
and our term My Term
would all produce the same slug, my-term
, so we would need elimanate such issues.
The last important section here before a unique slug is created and the term inserted, is the following section
if ( $name_match ) {
$slug_match = get_term_by( \'slug\', $slug, $taxonomy );
if ( ! $slug_provided || $name_match->slug === $slug || $slug_match ) {
if ( is_taxonomy_hierarchical( $taxonomy ) ) {
$siblings = get_terms( $taxonomy, array( \'get\' => \'all\', \'parent\' => $parent ) );
$existing_term = null;
if ( $name_match->slug === $slug && in_array( $name, wp_list_pluck( $siblings, \'name\' ) ) ) {
$existing_term = $name_match;
} elseif ( $slug_match && in_array( $slug, wp_list_pluck( $siblings, \'slug\' ) ) ) {
$existing_term = $slug_match;
}
if ( $existing_term ) {
return new WP_Error( \'term_exists\', __( \'A term with the name provided already exists with this parent.\' ), $existing_term->term_id );
}
} else {
return new WP_Error( \'term_exists\', __( \'A term with the name provided already exists in this taxonomy.\' ), $name_match->term_id );
}
}
}
This section only runs when we have positively identified any other term with the same name as the one we would like to insert. Again, this section runs regardless whther a slug has been set or not.
The first thing that this section do is to return any matching term from db which matches our slug from the term we would want to insert which comes from the first section of code in the answer. Our slug is my-term
. get_term_by()
either return a term object if there is a term that already in db which matches the term by slug of the term we would want to insert, or false if there is no term that already matches our term by slug.
Now a lot of check is done here, the first is a condition that runs the following checks
Check if we have explicitely set a slug (! $slug_provided
), return true if haven\'t
If the slug from a matching term (from the second block of code in this answer) matches the slug from the term we want to insert (in this case my-term
). Retruns true if there is a match
Whether we have a term object returned from get_term_by()
.
If any of these three conditions return true, a check is then done to check if our taxonomy is hierarchical. If this returns false, ie, our taxonomy is non hierarchical like tags, the following error is returned:
A term with the name provided already exists in this taxonomy.
quite obviously as we cannot have terms with the same name in a non hierarchical taxonomy.
If this returns true, ie our taxonomy is hierarchical, all sibling terms is queried by parent (which will be either 0
or whatever integer value we have explicitely set).
wp_insert_term()
now do its final check before deciding to go ahead with inserting the term or not. A failure here would throw the error message you are refering to in your question
A term with the name provided already exists with this parent
This is what the final check does:
All the direct child terms are queried from db from the given taxonomy and parent (which is either 0
or the value specified by you). Remember that we are still inside the condition which states that there are terms matching the name of the term we want to insert. Two checks are done on the returned array of child categories:
The first check involves the following: The term\'s slug from the matched term $name_match
are checked against the slug from our term, my-term
(remember this comes from block one of code in the answer). Also, our term name that we have set, My Term
are checked against all names from the direct children from our parent.
The second check is the following: A check is done to see if there was a term returned by get_term_by()
and if the slug from the first block of code in this answer matches any slugs from the direct children from the parent term.
If any of these conditions returns true, the
A term with the name provided already exists with this parent.
error message is thrown and the term is not inserted. A false will in all probably lead to the term successfully being created and inserted.
Now we can look at your question
Why should the name be unique, don\'t understand as the slug is.
You can have duplicate names within a hierarchical taxonomy if, and only if
There are no term with a matching name on the same level in the hierarchy. So any two top level terms, or any two direct children from the same parent term cannot have the same name. This line:
if ( $name_match->slug === $slug && in_array( $name, wp_list_pluck( $siblings, \'name\' ) ) )
of code prohibits that.
You can have the same names for terms in a hierarchical taxonomy if the term is in one top level parent, a child to that top level parent and one grandchild to that specific child, if and only if their slugs are unique. This line
elseif ( $slug_match && in_array( $slug, wp_list_pluck( $siblings, \'slug\' ) ) )
prohibits terms with the same name having the same slug
You cannot have terms with the same name in a non hierarchical taxonomy as they do not have parent/child type relationships
So, if you are sure your taxonomy is hierarchical, and you slug is unique, it still does not mean you can have the same name, as you need to take parent into consideration. If your parent remain the same, regardless of unique slug being set, your term will not be inserted and will throw the error message you are seeing
POSSIBLE WORKAROUND TO MAKE NAME UNIQUE
The best will be to write a wrapper function where you use the same logic as wp_insert_term()
to first check if there are already a term with the same name with the same parent, and if so, take the name and run some kind of function on the name to make it unique.
I would encourage you to take all the logic in this answer, use it in a custom wrapper function to create a unique name, and play around with ideas. If you get stuck, feel free to post waht you have in a new question, and I\'ll be happy to take a look at it when I have time.
Best of luck