SEF URL: Дружественные адреса для двухуровневой таксономии

# SEF URL: Дружественные адреса для двухуровневой таксономии

## Проблема (Задача):

У нас есть:

- **компания** — тип записи Flat.
- **каталог** — иерархическая таксономия.
- **локация** — иерархическая таксономия.

Нам нужны постоянные ссылки (пермалинки):

Все они должны начинаться с /company/. Мы будем объединять запросы для двух таксономий, с поддержкой пагинации.

### Примеры:
- /company/bmw7/ — пост.
- /company/almaty/ — архив термина локации (уровень 1).
- /company/almaty/auezo/ — архив термина локации (уровень 2).
- /company/almaty/auezo/three/ — архив термина локации (уровень 3).
- /company/avto/ — архив термина каталога (уровень 1).
- /company/avto/light/ — архив термина каталога (уровень 2).
- /company/avto/light/lyuks/ — архив термина каталога (уровень 3).
- /company/avto/almaty/ — архив терминов каталога и локации (уровни 1).
- /company/avto/almaty/auezo/ — архив терминов каталога и локации (уровни 1 + 2).
- /company/avto/light/almaty/auezo/three/ — архив терминов каталога и локации (уровни 2 + 3).

### Все эти ссылки должны поддерживать пагинацию:
- /company/almaty/page/2/ — вторая страница пагинации для локации.
- /company/avto/light/almaty/auezo/three/page/2/ — вторая страница для каталога и локации.

## Код, который решает эту задачу:

```php
/company/bmw7/ — пост.
 * - /company/almaty/ — архив по локации (уровень 1).
 * - /company/avto/ — архив по каталогу (уровень 1).
 *
 * Дополнительные постоянные ссылки для постов:
 * - /site.kz/company/bmw7/reviews/ — пост с обзорами.
 * - /site.kz/company/bmw7/gallery/ — пост с галереей.
 * - /site.kz/company/bmw7/edit/ — редактирование поста.
 * - /site.kz/company/bmw7/price/ — цена поста.
 *
 * @version 1.4
 * @autor 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']);
    }

    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' => 'Каталог',
                'singular_name' => 'Каталог',
            ],
            'public' => true,
            'hierarchical' => true,
            'rewrite' => false,
            'show_admin_column' => false,
        ]);

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

        // Регистрация типа записи для компаний
        register_post_type(self::$post_type, [
            'label' => null,
            'labels' => [
                'name' => 'Компании',
                'singular_name' => 'Компания',
            ],
            'public' => true,
            'hierarchical' => false,
            'supports' => ['title', 'editor'],
            '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){
        // Обработка запроса для компании
        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]);
            };

            // Проверка на наличие термина локации
            if(false !== strpos($location_top, "|$name|")){
                $unset_vars__fn($vars); // очистка
                $vars[self::$location_tax] = $name;
            }
            // Проверка на наличие термина каталога
            elseif($term = get_term_by('slug', $name, self::$catalog_tax)){
                $unset_vars__fn($vars); // очистка
                $vars[self::$catalog_tax] = $name;
            }
        }
        // Обработка специального запроса
        elseif(!empty($vars['catalog_location'])){
            $parts = explode('/', $vars['catalog_location']);
            // Может быть пост с подстраницей
            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];
            }
            // Обработка запроса для каталога и локации
            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);

                // Проверка надежности URL для переадресации
                $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) : '';

                // Обработка двух таксономий
                if($catalog_slug && $location_slug){
                    // Переадресация на правильный 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;
                }
                // Обработка запроса только для каталога
                elseif($catalog_slug){
                    // Переадресация на правильный 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;
                }
                // Обработка запроса только для локации
                elseif($location_slug){
                    // Переадресация на правильный 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_rules = [
            "$_first_part/$_page_part/?$" => 'index.php?catalog_location=$matches[1]&paged=$matches[2]',
            "$_first_part/?$" => 'index.php?catalog_location=$matches[1]',
        ];

        // Удаление конфликтующих правил
        foreach($rules as $regex => $rule){
            if(false === strpos($regex, '/attachment/') && false !== strpos($rule, 'attachment='))
                unset($rules[$regex]);
        }

        // Добавление новых правил
        $rules += $more_rules;

        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;
    }

    /**
     * Получает родительские термины (включая текущий).
     *
     * @param WP_Term|string $term
     * @param string         $taxonomy Если термин передан как строка.
     *
     * @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);
    }

    /**
     * Создает часть URL для термина указанной таксономии. Пример: 'parent/child/childchild'
     *
     * @param WP_Term|string $term
     * @param string         $taxonomy Если термин передан как строка.
     *
     * @return string
     */
    static function _build_tax_uri($term, $taxonomy = null){
        $slugs = wp_list_pluck(self::parent_terms($term, $taxonomy), 'slug');
        return implode('/', $slugs);
    }

    /**
     * Получает верхние уровни слогов таксономии локации как массив. Кэширует результат.
     *
     * @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;
    }
}

Объяснение кода

Этот код создает специальный класс для управления постоянными ссылками в WordPress, используя две иерархические таксономии: каталог и локация. Класс обрабатывает регистрацию постов и таксономий, создание правильных URL и поддержку пагинации.

Важные классы и методы:

  • register_post_types() — регистрирует новый тип записи и таксономии.
  • fix_request() — обрабатывает запросы и определяет, какой контент должен быть загружен.
  • post_rewrite_rules() — определяет правила для формата URL.

Этот код подходит для разработчиков, которые хотят создавать более удобные адреса для сторонних клиентов, давая им возможность легко навигировать по ресурсам сайта.

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *