<?php

class WP_Tax_Group extends WP_Tax_Query {

	function __construct($operator, $args) {
		$this->operator = $operator;
		$this->args = $args;
	}

	function get_sql() {
		$args = $this->traverse($this->args);

		return "(" . implode(" $this->operator ", $args) . ")";
	}

	function traverse($arg) {
		if ( is_array($arg) )
			return array_map(array($this, 'traverse'), $this->args);

		if ( is_a($arg, 'WP_Tax_Query') )
			return $arg->get_sql();

		return $arg;
	}
}

class WP_Tax_Item extends WP_Tax_Query {
	function __construct($taxonomy, $term_ids, $not = false) {
		$this->taxonomy = $taxonomy;
		$this->term_ids = $term_ids;
		$this->not = $not;
	}

	function get_sql() {
		return _wp_tax($this->taxonomy, $this->term_ids, $this->not);
	}
}

class WP_Tax_Query {
	function get_sql() {
		return '';
	}
}


function wp_tax_and() {
	$args = func_get_args();

	return new WP_Tax_Group('AND', $args);
}

function wp_tax_or() {
	$args = func_get_args();

	return new WP_Tax_Group('OR', $args);
}


function wp_tax($taxonomy, $term_ids) {
	return new WP_Tax_Item($taxonomy, $term_ids);
}

function wp_tax_not($taxonomy, $term_ids) {
	return new WP_Tax_Item($taxonomy, $term_ids, true);
}

function _wp_tax($taxonomy, $term_ids, $not = false) {
	global $wpdb;

	$term_ids = implode(',', array_map('intval', (array) $term_ids));

	$operator = $not ? 'NOT IN' : 'IN';

	return $wpdb->prepare("(
		SELECT object_id
		FROM $wpdb->term_relationships
		WHERE term_taxonomy_id $operator (
			SELECT term_taxonomy_id
			FROM $wpdb->term_taxonomy
			WHERE taxonomy = %s
			AND term_id IN ($term_ids)
		)
	)", $taxonomy);
}

// Example
$query = wp_tax_and(
	wp_tax_or(
		wp_tax( 'post_category', 59 ),
		wp_tax( 'post_tag', 'web' )
	),
	wp_tax_not( 'post_category', 24 ),
	wp_tax_not( 'post_tag', 'wordpress' )
);

var_dump($query->get_sql());

