[wp-hackers] Taxonomy Schema Proposal

Jamie Talbot wphackers at jamietalbot.com
Thu Apr 19 15:18:02 GMT 2007


Ryan Boren wrote:
> I caught a bug that's laying me low at the moment.  The stuff you are
> proposing sounds pretty good, but my head isn't clear enough to think
> it all the way through.  Would you mind taking the proposed schema,
> modifying it with your amendments, sprinkling in a few comments, and
> submitting back to the list so we can see it as a whole?
> 
> Ryan

So here are my initial thoughts.  The schema is almost identical to your proposed one, with the
changes I suggested for an integer join, as well as alias stuff from elsewhere on this thread:

<?php

// terms contains the actual categories/tags/terms/classifiers/whatevers.  It stores ID, name, and
slug and alias group.
CREATE TABLE $wpdb->terms (
 term_id bigint(20) NOT NULL auto_increment,
 term_name varchar(55) NOT NULL default '',
 term_slug varchar(200) NOT NULL default '',
 term_group bigint(10) NOT NULL default 0
 PRIMARY KEY  (term_ID),
 UNIQUE KEY term_slug (term_slug)
);

// term_taxonomy puts a term in the context of a taxonomy (link category, post category, or tag).
Hierarchy is put here as well as counts.
CREATE TABLE $wpdb->term_taxonomy (
 term_taxonomy_id bigint(20) NOT NULL auto_increment,
 term_id bigint(20) NOT NULL default 0,
 taxonomy varchar(20) NOT NULL default 0,
 term_description longtext NOT NULL,
 parent bigint(20) NOT NULL default 0,
 count bigint(20) NOT NULL default 0,
 PRIMARY KEY (term_taxonomy_id),
 KEY (term_idtaxonomy)
);

// term_relationships relates a term to a post or link or undeclared future object thingy.  The
relationship is placed within the context of a given taxonomy.
CREATE TABLE $wpdb->term_relationships (
 object_id bigint(20) NOT NULL default 0,
 term_taxonomy_id bigint(20) NOT NULL default 0,
 PRIMARY KEY  (object_ID),
 KEY (term_taxonomy_id)
);

// Some default definitions that might be useful.
define ('TAXONOMY_POST_CATEGORY', 1);
define ('TAXONOMY_LINK_CATEGORY', 2);
define ('TAXONOMY_POST_TAG', 4);

// Very basic example API.  This is half pseudocode, mostly real.  Not by any means final.
class WPTaxonomy {

	static var taxonomies = array(
		TAXONOMY_POST_CATEGORY => 'post_category',
		TAXONOMY_LINK_CATEGORY => 'link_category',
		TAXONOMY_POST_TAG => 'post_tag'
	);

	// Adds a new term, also adds alias relationship if necessary.  Handles numeric or slug-based aliases.
	function add_term($term, $alias_of = '') {
		global $wpdb;
		$term_slug = sanitize($term);		
		if ($alias_of) {
			// ctype_digit is faster than is_numeric, with the caveat that the arg must be in a string.
			$clause = (ctype_digit("$alias_of")) ? "term_id = $alias_of" : "term_slug = '$alias_of'";
			$alias = $wpdb->fetch_row("SELECT term_id, term_group FROM $wpdb->terms WHERE $clause");
			if ($alias->term_group)
				// The alias we want is already in a group, so let's use that one.
				$term_group = $alias->term_group;
			} else {
				// The alias isn't in a group, so let's create a new one and firstly add the alias term to it.
				$term_group = $wpdb->get_var("SELECT MAX() term_group FROM $wpdb->terms GROUP BY term_group") + 1;
				$wpdb->query("UPDATE $wpdb->terms SET term_group = $term_group WHERE term_id = $alias->term_id");
			}
		} else {
			$term_group = 0;
		}		
		// Because we have a unique key on term_slug, this will update or insert.  This means we can use
the same function
		// just to make an alias relationship between existing terms.  The term group has already been set
above.
		$wpdb->query("REPLACE INTO $wpdb->terms (term_name, term_slug, term_group) VALUES ($term,
$term_slug, $term_group)");
	}

	// Adds or increments a term taxonomy entry (for example, 'city' as a post_tag).
	// Accepts either term_id or term_slug, for completeness...
	function add_term_taxonomy($term, $taxonomy, $description = '', $parent = 0) {
		$clause = (ctype_digit("$term")) ? "t.term_id = $term" : "t.term_slug = '$term'";
		if ($count = $wpdb->query("SELECT tt.count FROM $wpdb->term_taxonomy AS tt INNER JOIN $wpdb->terms
AS t ON tt.term_id = t.term_id WHERE tt.taxonomy = '$taxonomy' AND $clause")) {
      // We'll be updating whatever's there, and incrementing the count.  Not a huge fan of
      // storing counts by the way, but accept that there might be valid optimisation reasons for
doing so.
      $count++;
			$wpdb->query("UPDATE $wpdb->term_taxonomy SET count = $count");
		} else {
      // There was no existing entry for this term and taxonomy type, so add a new one, using the
supplied
			// description and parent, with a count of 1.
      $wpdb->query("INSERT INTO $wpdb->term_taxonomy (term_id, taxonomy, term_description, parent,
count) SELECT term_id, '$taxonomy', '$description', $parent, 1 FROM $wpdb->terms AS t1 WHERE $clause");
		}
	}

	// Accepts either term_id or term_slug.	
	function get_aliases($term) {
		global $wpdb;
		$clause = (ctype_digit("$term")) ? "t1.term_id = $term" : "t1.term_slug = '$term'";
		return $wpdb->fetch("SELECT t2.* FROM $wpdb->terms AS t1 INNER JOIN $wpdb->terms AS t2 ON
t1.term_group = t2.term_group WHERE t1.term_group != 0 AND $clause");
	}
	
	// Gets the counts of supplied term, for specified taxonomies.
	// Could be extended to accept an array of terms.
	// Returns an object with members for each taxonomy if more than one is specified.
	// This example has taxonomies specified as strings, but you could also do it based on integers,
either from the default defines, or from a taxonomy index added by a plugin using add_taxonomy();
	function get_count($term, $taxonomies) {
		global $wpdb;
		if (ctype_digit("$term")) {
			$clause = "term_id = $term";
		} else {
			$join = "INNER JOIN $wpdb->terms AS t ON t.term_id = tt.term_id";
			$clause = "t.term_slug = '$term'";
		}

		if (is_array($taxonomies)) {
			$taxonomies = "'" implode("','", $taxonomies) . "'";
			if (!$counts = $wpdb->fetch("SELECT taxonomy, count FROM $wpdb->term_taxonomy AS tt $join WHERE
tt.taxonomy IN ($taxonomies) AND $clause")) {
				// Does $wpdb->fetch return an empty array, when there are no results?
				$counts = array();
			}
			foreach ($counts as $count) {
			  // Note that for this to work, taxonomies must use underscores, not hyphens, in their names...
				$return->{$count->taxonomy} = $count->count;
			}
		} else {
			$return = $wpdb->get_var("SELECT count FROM $wpdb->term_taxonomy AS tt $join WHERE tt.taxonomy =
'$taxonomies' AND $clause LIMIT 1");
		}
		return $return;
	}
	
	// Adds a new taxonomy and returns the index it was added at.
	static function add_taxonomy($taxonomy) {
		$index = last(array_flip(WPTaxonomy::taxonomies)) * 2;
		WPTaxonomy::taxonomies[$index] = $taxonomy;
		return $index;
	}
}

Plugins can say <?php $my_taxonomy_index = add_taxonomy('my_awesome_taxonomy'); ?> then use that
index, or 'my_awesome_taxonomy' to refer to it later on.  Pretty flexible, nice and extensible,
mostly normalised and with the opportunity to flesh out a full API.

For a more classical OO approach, rather than just using a class as a container, something like
class WPTerm {} might be nice, so you could do:

$term = new WPTerm('city');
$term->get_aliases();
$counts = $term->get_count(TAXONOMY_LINK_CATEGORY | TAXONOMY_POST_TAG);
// counts->link_category = 5;
// counts->post_tag = 8;
$term->find_ancestors();

Again where the WPTerm constructor would accept either an term_id or a term_slug.  Not sure what
people would think of that?

I'll leave it there for now :)

Cheers,

Jamie.
?>

--
http://jamietalbot.com


More information about the wp-hackers mailing list