SEF urls: Friendly URLs for a Dual Multi-level Taxonomy

The problem (Task):

We have:

  • company — Flat post type.
  • catalog — Hierarchical taxonomy.
  • location — Hierarchical taxonomy.

We need permalinks:

/company/ prefix for all! Combined queryes for the two taxes. Working pagination!
Examples:

  • /company/bmw7/ — post.
  • /company/almaty/ — location term archive (top level).
  • /company/almaty/auezo/ — location term archive (level 2).
  • /company/almaty/auezo/three/ — location term archive (level 3).
  • /company/avto/ — catalog term archive (top level).
  • /company/avto/light/ — catalog term archive (level 2).
  • /company/avto/light/lyuks/ — catalog term archive (level 3).
  • /company/avto/almaty/ — catalog + location terms archive (top levels).
  • /company/avto/almaty/auezo/ — catalog + location terms archive (level 1 + level 2).
  • /company/avto/light/almaty/auezo/three/ — catalog + location terms archive (level 2 + level 3).

All of these must support pagination. Ex:

  • /company/almaty/page/2/ — location term pagination page 2.
  • /company/avto/light/almaty/auezo/three/page/2/ — catalog + location terms page 2.
  • etc.

The code which do all things:

GitHub
<?php

new Same_Prefix_Post_Taxes_Rewrite();

/**
 * How it works.
 *
 * We have:
 * - ``company`` — Flat post type.
 * - ``catalog`` — Hierarchical taxonomy.
 * - ``location`` — Hierarchical taxonomy.
 *
 * We need permalinks:
 * ``/company/`` prefix for all! Combined queryes for the two taxes. Working pagination!
 * Examples:
 * - ``/company/bmw7/``                           — post.
 * - ``/company/almaty/``                         — location term archive (top level).
 * - ``/company/almaty/auezo/``                   — location term archive (level 2).
 * - ``/company/almaty/auezo/three/``             — location term archive (level 3).
 * - ``/company/avto/``                           — catalog term archive (top level).
 * - ``/company/avto/light/``                     — catalog term archive (level 2).
 * - ``/company/avto/light/lyuks/``               — catalog term archive (level 3).
 * - ``/company/avto/almaty/``                    — catalog + location terms archive (top levels).
 * - ``/company/avto/almaty/auezo/``              — catalog + location terms archive (level 1 + level 2).
 * - ``/company/avto/light/almaty/auezo/three/``  — catalog + location terms archive (level 2 + level 3).
 *
 * All of these must support pagination. Ex:
 * - ``/company/almaty/page/2/``                         — location term pagination page 2.
 * - ``/company/avto/light/almaty/auezo/three/page/2/``  — catalog + location terms page 2.
 * - etc.
 *
 * For posts we need additional permalinks. Ex:
 * - ``site.kz/company/bmw7/reviews/``       — post.
 * - ``site.kz/company/bmw7/gallery/``       — post.
 * - ``site.kz/company/bmw7/edit/``          — post.
 * - ``site.kz/company/bmw7/price/``         — post.
 *
 * @version 1.4
 *
 * @author Kama (wp-kama.com)
 */
class Same_Prefix_Post_Taxes_Rewrite {

	static $post_type = 'company';

	static $catalog_tax = 'catalog';

	static $location_tax = 'location';

	static $post_subpages = [
		'edit' => 'Редактирование',
		'price' => 'Цена',
		'gallery' => 'Галлерея',
		'reviews' => 'Обзоры',
	];

	static $request = [];

	function __construct(){

		add_action( 'init', [ __CLASS__, 'register_post_types' ] );

		add_action( 'request', [ __CLASS__, 'fix_request' ] );

		add_filter( self::$post_type . '_rewrite_rules', [ __CLASS__, 'post_rewrite_rules' ] );

		add_filter( 'saved_' . self::$location_tax, [ __CLASS__, '_location_top_slugs_refresh_cache' ] );
		add_filter( 'delete_' . self::$location_tax, [ __CLASS__, '_location_top_slugs_refresh_cache' ] );

		add_filter( 'pre_term_link', [ __CLASS__, 'make_term_link' ], 10, 2 );

		add_filter( 'query_vars', static function( $vars ){
			$vars[] = 'catalog_location';
			$vars[] = 'post_subpage';
			return $vars;
		} );

		add_filter( 'get_canonical_url', [ __CLASS__, 'subpages_canonical' ], 10, 2 );

		add_filter( 'single'.'_template_hierarchy', [ __CLASS__, 'subpages_template_hierarchy' ] );

		isset( $_GET['rwdebug'] ) && add_action( 'wp', [ __CLASS__, '_debug' ] ); // debug
	}

	static function is_multi_tax_query(){
		return count( self::$request ) > 1;
	}

	static function register_post_types(){

		register_taxonomy( self::$catalog_tax, [ self::$post_type ], [
			'label'             => '', // определяется параметром $labels->name
			'labels'            => [
				'name'          => 'Каталог',
				'singular_name' => 'Каталог',
			],
			'public'            => true,
			'hierarchical'      => true,
			'rewrite'           => false, //[ 'slug'=>self::$post_type, 'hierarchical'=>true, 'with_front'=>false, 'feed'=>false ],
			'show_admin_column' => false,

		] );

		register_taxonomy( self::$location_tax, [ self::$post_type ], [
			'label'             => '', // определяется параметром $labels->name
			'labels'            => [
				'name'          => 'Локация',
				'singular_name' => 'Локация',
			],
			'public'            => true,
			'hierarchical'      => true,
			'rewrite'           => false, //[ 'slug'=>self::$post_type, 'hierarchical'=>true, 'with_front'=>false, 'feed'=>false ],
			'show_admin_column' => false,
		] );

		register_post_type( self::$post_type, [
			'label'  => null,
			'labels' => [
				'name'          => 'Компании', // основное название для типа записи
				'singular_name' => 'Компания', // название для одной записи этого типа
			],
			'public'        => true,
			'menu_position' => null,
			'menu_icon'     => null,
			'hierarchical'  => false,
			'supports'      => [ 'title', 'editor' ],
			// 'title','editor','author','thumbnail','excerpt','trackbacks','custom-fields','comments','revisions','page-attributes','post-formats'
			'taxonomies'    => [ self::$catalog_tax, self::$location_tax ],
			'has_archive'   => true,
			'rewrite'       => [ 'with_front'=>false, 'feeds'=>false, 'endpoints'=>false ],
			'query_var'       => true,
		] );

	}

	static function fix_request( $vars ){

		// post request like catalog|location
		if( ! empty( $vars['name'] ) && self::$post_type === $vars['post_type'] ){

			$name = $vars['name'];
			$location_top = self::_location_top_slugs();

			$unset_vars__fn = static function( & $vars ){
				unset( $vars['post_type'], $vars['name'], $vars[ self::$post_type ] ); // clear
			};

			// location
			if( false !== strpos( $location_top, "|$name|" ) ){
				$unset_vars__fn( $vars ); // clear
				$vars[ self::$location_tax ] = $name;
			}
			// catalog
			elseif( $term = get_term_by( 'slug', $name, self::$catalog_tax ) ){
				$unset_vars__fn( $vars ); // clear
				$vars[ self::$catalog_tax ] = $name;
			}
		}
		// special catalog_location request
		elseif( ! empty( $vars['catalog_location'] ) ){

			$parts = explode( '/', $vars['catalog_location'] );

			// maybe company post with post_subpage
			if(
				count( $parts ) === 2
				&& isset( self::$post_subpages[ $parts[1] ] )
				&& $post = get_page_by_path( $parts[0], OBJECT, [ self::$post_type ] )
			){
				$vars[ self::$post_type ] = $parts[0];
				$vars['post_type'] = self::$post_type;
				$vars['name'] = $parts[0];
				$vars['post_subpage'] = $parts[1];
			}
			// catalog_location request
			else {

				$location_top = self::_location_top_slugs();

				$catalogs = $locations = [];
				$lnk = & $catalogs;
				foreach( $parts as $part ){

					if( false !== strpos( $location_top, "|$part|" ) )
						$lnk = & $locations;

					$lnk[] = $part;
				}
				unset( $lnk );

				$catalog_slug = end( $catalogs );
				$location_slug = end( $locations );

				// for check reliability of the URL to redirect to right one
				$build_catalog  = $catalog_slug ? self::_build_tax_uri( $catalog_slug, self::$catalog_tax ) : '';
				$build_location = $location_slug ? self::_build_tax_uri( $location_slug, self::$location_tax ) : '';

				// two taxs
				if( $catalog_slug && $location_slug ){

					// 301 redirect to correct URL
					if(
						( implode( '/', $catalogs ) !== $build_catalog )
						||
						( implode( '/', $locations ) !== $build_location )
					){
						wp_redirect( user_trailingslashit( '/'. self::$post_type ."/$build_catalog/$build_location" ), 301 );
						exit;
					}

					self::$request['catalog'] = get_term_by( 'slug', $catalog_slug, self::$catalog_tax );
					self::$request['location'] = get_term_by( 'slug', $location_slug, self::$location_tax );

					$vars[ self::$catalog_tax ] = $catalog_slug;
					$vars[ self::$location_tax ] = $location_slug;
				}
				// catalog
				elseif( $catalog_slug ){

					// 301 redirect to correct URL
					if( implode( '/', $catalogs ) !== $build_catalog ){
						$url = get_term_link( $catalog_slug, self::$catalog_tax );

						if( ! is_wp_error( $url ) ){
							wp_redirect( $url, 301 );
							exit;
						}
					}

					self::$request['catalog'] = get_term_by( 'slug', $catalog_slug, self::$catalog_tax );

					$vars[ self::$catalog_tax ] = $catalog_slug;
				}
				// location
				elseif( $location_slug ){

					// 301 redirect to correct URL
					if( implode( '/', $locations ) !== $build_location ){
						wp_redirect( get_term_link( $location_slug, self::$location_tax ), 301 );
						exit;
					}

					self::$request['location'] = get_term_by( 'slug', $location_slug, self::$location_tax );

					$vars[ self::$location_tax ] = $location_slug;
				}

			}

			unset( $vars['catalog_location'] );
		}

		return $vars;
	}

	static function post_rewrite_rules( $rules ){

		$_first_part = self::$post_type . "/(.+?)";
		$_page_part = 'page/?([0-9]{1,})';
		$more_riles = [
			"$_first_part/$_page_part/?$" => 'index.php?catalog_location=$matches[1]&paged=$matches[2]',
			"$_first_part/?$"             => 'index.php?catalog_location=$matches[1]',
		];

		// delete conflict attachment rules
		foreach( $rules as $regex => $rule ){
			if( false === strpos( $regex, '/attachment/' ) && false !== strpos( $rule, 'attachment=' ) )
				unset( $rules[ $regex ] );
		}

		$rules += $more_riles;

		return $rules;
	}

	static function make_term_link( $url, $term ){

		if( $term->taxonomy === self::$catalog_tax || $term->taxonomy === self::$location_tax ){
			return user_trailingslashit( '/'. self::$post_type .'/'. self::_build_tax_uri( $term ) );
		}

		return $url;
	}

	/**
	 * Gets parent terms (including current).
	 *
	 * @param WP_Term|string $term
	 * @param string         $taxonomy IF term passed as string.
	 *
	 * @return WP_Term[]|array
	 */
	static function parent_terms( $term, $taxonomy = null ){

		if( is_string( $term ) )
			$term = get_term_by( 'slug', $term, $taxonomy );

		if( ! $term )
			return [];

		$path = [ $term ];

		while( $term->parent ){
			$term = get_term( $term->parent );
			$path[] = $term;
		}

		return array_reverse( $path );
	}

	/**
	 * Build URL part of term of specified taxonomy. EX: 'parent/child/childchild'
	 *
	 * @param WP_Term|string $term
	 * @param string         $taxonomy IF term passed as string.
	 *
	 * @return string
	 */
	static function _build_tax_uri( $term, $taxonomy = null ){
		$slugs = wp_list_pluck( self::parent_terms( $term, $taxonomy ), 'slug' );
		return implode( '/', $slugs );
	}

	/**
	 * Get top level location taxonomy slugs as array. Cache the result.
	 *
	 * @param bool $refresh
	 */
	static function _location_top_slugs( $refresh = false ){

		$trans_name = 'location_top_slugs';

		$top_slugs = get_transient( $trans_name );

		if( $refresh || ! $top_slugs ){

			$top_slugs = get_terms( [
				'taxonomy' => self::$location_tax,
				'parent' => 0,
				'hide_empty' => false,
			] );

			$top_slugs = '|'. implode( '|', wp_list_pluck( $top_slugs, 'slug' ) ) .'|';

			set_transient( $trans_name, $top_slugs, HOUR_IN_SECONDS );
		}

		return $top_slugs;
	}

	static function _location_top_slugs_refresh_cache(){
		self::_location_top_slugs( 'refresh' );
	}

	static function subpages_template_hierarchy( $templates ){

		if( $subpage = get_query_var('post_subpage') ){
			$subpage_file = 'single-'. self::$post_type ."-$subpage.php";
			array_unshift( $templates, $subpage_file );
		}

		return $templates;
	}

	static function subpages_canonical( $canonical_url, $post ){

		if( $subpage = get_query_var('post_subpage') ){
			$canonical_url = user_trailingslashit( rtrim( get_permalink( $post ), '/' ) ."/$subpage" );
		}

		return $canonical_url;
	}

	static function _debug(){
		print_r( get_queried_object() );
		print_r( $GLOBALS['wp'] );
		print_r( $GLOBALS['wp_query'] );
		print_r( $GLOBALS['wp_rewrite'] );
		exit;
	}

}