Eloquent: уникальный слаг записи

Опубликовано 0 комментариев 2411 просмотров
Eloquent: уникальный слаг записи

ЧПУ (человеко-понятный урл) уже давно является одним из факторов, о которых не говорят во время обсуждения списка работ над сайтом но который де-факто является обязательным пунктом в списке. Лично у меня за последние несколько лет (а быть может и больше) не было ни одного сайта, где бы отсутствовал этот элемент.

Для каждой модели, имеющей отдельную страницу для вывода информации со строкой из хранилища при создании и/или обновлении строки генерируется слаг. Самый простой вариант если вы используете Laravel или отдельно пакет illuminate/database - воспользоваться идущей в комплекте функцией str_slug. Она заменяет невалидные символы на валидные, чистит строку от невозможных для преобразования символов и приводит ее к нижнему регистру.

$slug = str_slug('Какая-то запись.'); // kakaya-to-zapis

Но на этапе разработки необходимо предусмотреть вариант развития событий, когда в хранилище уже будет существовать строка с указанным пользователем слагом. Я хочу предложить свое решение в виде трейта, который можно использовать в любом месте при создании новой или обновлении существующей строки.

Трейт содержит:

  • приватное свойство $slugMaxLength - максимальное число символов слага
  • приватное свойство $slugKeyName - название колонки таблицы с слагом
  • публичный метод generateSlug($model, string $slug) - возвращает уникальный слаг

Первым аргументом метода указывается название модели или экземпляр существующей модели Eloquent. Первый вариант ищет совпадения по всей таблице, при втором из выборки исключается указанная строка. Проще говоря, указание аргумента как строки необходимо при создании элемента, как экземпляра модели - при обновлении.

Второй аргумент - строка, которая будет преобразована в слаг. Форматирование строки функцией str_slug не обязательно - эта работа будет выполнена внутри.

<?php

namespace App\Traits;

use Illuminate\Database\Eloquent\Model;

trait UniqueModelSlug
{
    private $slugMaxLength = 40;

    private $slugKeyName = 'slug';

    /**
     * @param string|Model $model
     * @param string $slug
     *
     * @return string
     */
    public function generateSlug($model, string $slug)
    {
        $model = $model instanceof Model ? $model->where($model->getKeyName(), '!=', $model->getKey()) : new $model;
        $slug = str_limit(str_slug($slug), $this->slugMaxLength, '');

        // Если слаг не существует - возвращаем исходную строку
        if(!$model->where($this->slugKeyName, $slug)->exists()) {
            return $slug;
        }

        // Удаляем число в начале строки
        $slug = preg_replace('/^(\d+)-(\w+)$/', '$2', $slug);

        // Ищем в базе совпадения
        $lastSlugNumModel = $model
            ->whereRaw("`$this->slugKeyName` REGEXP ?", "^[[:digit:]]+-$slug$")
            ->orderByDesc($this->slugKeyName)
            ->first();

        // не находим - возвращаем слаг с номером 1
        // иначе - номер перед слагом + 1
        if(is_null($lastSlugNumModel)) {
            return str_limit("1-$slug", $this->slugMaxLength, '');
        }

        preg_match('/^(\d+)-(\w+)$/', $lastSlugNumModel->{$this->slugKeyName}, $m);

        $lastSlugNum = $m[1] + 1;

        return str_limit("$lastSlugNum-$slug", $this->slugMaxLength, '');
    }
}

Как работает метод? Первым делом $slug форматируется функцией str_slug с обрезанием лишних символов. Затем происходит запрос в хранилище на существование строки с таким слагом. Если результат отрицательный - возврашается отформативанный слаг.

Регулярным выражение удаляется число в начале слага если оно существует. Происходит получение первой записи с конца со слагом, подходящим под регулярное выражение число-$slug. Если ничего не найдено - возвращается строка 1-$slug.

Достаем из полученного слага число, увеличиваем его на 1 и возвращаем число+1-$slug. Разберем на примере.

<?php

namespace App\Http\Controllers;

use App\Traits\UniqueModelSlug;
use App\Models\Post;

class PostController()
{
    use UniqueModelSlug;

    public function store()
    {
        $existsPost = Post::where('slug', 'blog')
            ->firstOrFail(); // Строка с слагом blog существует

        $name = 'Блог';

        $post = new Post();
        $post->slug - $this->generateSlug(
            Post::class, // указываем модель строкой, т.к. добавляем новую строку
            $name
        ); // 1-blog

        $post->save();
    }

    public function update(Post $post)
    {    
        $existsPost = Post::where('slug', 'blog')
            ->where('id', '!=', $post->id)
            ->firstOrFail(); // Другая строка с слагом blog существует

        $name = 'Блог';

        $post->slug = $this->generateSlug(
            $post, // указываем модель экземпляром, т.к. обновляем существующую строку
            $name
        ); // 1-blog

        $post->save();
    }
}

В ответ на сообщение

Доступна разметка Markdown. А еще вы можете использовать крутой пак эмоций.

Нажимая на кнопку «Отправить» вы даете свое согласие на обработку персональных данных в соответствии с законом №152-ФЗ «О персональных данных» от 27.07.2006 и принимаете условия Политики конфеденциальности.