Рубрики
Uncategorized

Галантерея: строительный комплекс красноречивый поисковый фильтр

Автор оригинала: David Wong.

Статья была переслана из профессионального сообщества разработчиков laravel. Оригинальная ссылка: https://learnku.com/laravel/t

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

Сегодня я расскажу обо всем процессе и о том, как создать гибкую и масштабируемую поисковую систему. Если вы хотите увидеть код, посетите репозиторий GIT.

Что мы создадим

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

Нам нужно, чтобы все сотрудники компании имели доступ к информации о приглашенных наших клиентах и их статусе ответа на приглашение.

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

Скриншот использования расширенного фильтра поиска для поиска

Найти пользователя

Распространенные методы фильтрации пользователей:

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

Хотя этот список не является полным, он может сообщить нам, сколько фильтров нам нужно. Это будет непростая задача!

Скриншот условной фильтрации на переднем конце.

Модель и ассоциация моделей

В этом примере мы используем множество моделей:

  • Пользователь представляет пользователя, приглашенного на мероприятие. Пользователь может участвовать во многих мероприятиях.
  • Мероприятие — мероприятие, проводимое от имени нашей компании. Может быть несколько видов деятельности.
  • RSVP — представляет ответ пользователя на приглашение на мероприятие. Реакция пользователя на действие однозначна.
  • Менеджер – пользователь может переписываться с несколькими менеджерами по продажам нашей компании

Потребности в поиске

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

Вот пример:

{
    "name": "Billy",
    "company": "Google",
    "city": "London",
    "event": "key-note-presentation-new-york-05-2016",
    "responded": true,
    "response": "I will be attending",
    "managers": [
        "Tom Jones",
        "Joe Bloggs"
    ],
}

Суммируйте критерии поиска, которые хотят выразить приведенные выше данные:

Гостя зовут “Билли”, из компании “Google”, в настоящее время проживающий в ” Лондоне “, ответил на приглашение” презентация ключевых заметок Нью-Йорк-05-2016″, и содержание ответа “Я буду присутствовать”. Менеджер по продажам, ответственный за сопровождение гостя, – это “Том Джонс” или “Джо блогс”.

Начало – Лучшие практики

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

Во-первых, на маршрутах. php Добавьте в файл следующий код:

Route::post('/search', '[email protected]');

Затем создайте SearchController .

php artisan make:controller SearchController

Добавить явный фильтр() Метод:

Поскольку нам нужно обработать данные, отправленные запросом в методе фильтра, я ввел класс запроса с зависимостью. Контейнер службы Laravel устранит эту зависимость. Мы можем напрямую использовать экземпляр запроса в методе, который является $request. То же самое верно и для класса user, из которого нам нужно извлечь некоторые данные.

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

Это мой первоначальный код:

public function filter(Request $request, User $user)
{
    //Find users by name
    if ($request->has('name')) {
        return $user->where('name', $request->input('name'))->get();
    }

    //Find users by company name
    if ($request->has('company')) {
        return $user->where('company', $request->input('company'))
            ->get();
    }

    //Find users by city
    if ($request->has('city')) {
        return $user->where('city', $request->input('city'))->get();
    }

    //Continue to find by other criteria

    //There are no other conditions,
    //Returns all eligible users.
    //Paging is needed in the actual project.
    return User::all();
}

Очевидно, что приведенная выше логика кода неверна.

Во-первых, он будет извлекать таблицу пользователей только в соответствии с одним условием, а затем вернется. Таким образом, с помощью приведенной выше логики кода мы не можем получить пользователей с именем “Билли”, которые живут в “Лондоне”.

Один из способов сделать это-выполнить условия вложенности:

//Search users by user name
if ($request->has('name')) {
    //Is the 'city' search parameter also provided
    if ($request->has('city')) {
        //Search users based on user name and city
        return $user->where('name', $request->input('name'))
            ->where('city', $request->input('city'))
            ->get();
    }
    return $user->where('name', $request->input('name'))->get();
}

Я уверен, что вы видите, что это работает с двумя или тремя параметрами, но как только мы добавим больше опций, управлять этим будет сложно.

Улучшите наш поисковый API

Итак, как нам заставить это работать, не сходя с ума от вложенных условий?

Мы можем продолжить рефакторинг, используя пользовательскую модель, чтобы использовать builder вместо прямого возврата к модели.

public function filter(Request $request, User $user)
{
    $user = $user->newQuery();

    //Search users by user name
    if ($request->has('name')) {
        $user->where('name', $request->input('name'));
    }

    //Search users based on their company information
    if ($request->has('company')) {
        $user->where('company', $request->input('company'));
    }

    //Search users based on their city information
    if ($request->has('city')) {
        $user->where('city', $request->input('city'));
    }

    //Continue with other filtering

    //Get and return results
    return $user->get();
}

Намного лучше! Теперь мы можем добавить каждый параметр поиска в качестве модификатора в $user->newQuery() В возвращаемом экземпляре запроса.

Теперь мы можем искать по всем параметрам, независимо от того, скольких параметров мы боимся

Давайте потренируемся вместе:

$user = $user->newQuery();

//Find users by name
if ($request->has('name')) {
    $user->where('name', $request->input('name'));
}

//Find users by company name
if ($request->has('company')) {
    $user->where('company', $request->input('company'));
}

//Find users by city
if ($request->has('city')) {
    $user->where('city', $request->input('city'));
}

//Only find users who connect with our sales manager
if ($request->has('managers')) {
    $user->whereHas('managers', function ($query) use ($request) {
        $query->whereIn('managers.name', $request->input('managers'));
    });
}

//If there is an 'event' parameter
if ($request->has('event')) {

    //Find only invited users
    $user->whereHas('rsvp.event', function ($query) use ($request) {
        $query->where('event.slug', $request->input('event'));
    });
    
    //Find only users who reply to the invitation (reply in any form is OK)
    if ($request->has('responded')) {
        $user->whereHas('rsvp', function ($query) use ($request) {
            $query->whereNotNull('responded_at');
        });
    }

    //Find only users who reply to the invitation (limit the specific content of the reply)
    if ($request->has('response')) {
        $user->whereHas('rsvp', function ($query) use ($request) {
            $query->where('response', 'I will be attending');
        });
    }
}

//Finally get the object and return
return $user->get();

Дело сделано. Отлично!

Вам все еще нужно провести рефакторинг?

С помощью приведенного выше кода мы понимаем бизнес-требования и можем возвращать правильную информацию о пользователе в соответствии с критериями поиска. Но можем ли мы сказать, что это лучшая практика? Очевидно, что нет.

Действительно ли целесообразно реализовывать бизнес-логику путем вложения ряда условных суждений, и вся логика находится в контроллере?

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

Однако, если вы строите более сложный проект, нам все равно нужно более элегантное и масштабируемое решение.

Напишите новый поисковый API

Когда я захочу написать функциональный интерфейс, я не буду сразу писать основной код. Обычно я сначала думаю о том, как использовать этот интерфейс. Это может быть известно как “программирование, ориентированное на результат” (или “мышление, ориентированное на результат”).

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

Потому что цель написания интерфейса состоит в том, чтобы упростить код с помощью компонентов, а не кода самого интерфейса. (с сайта C2. Com)

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

Следующий код добавляется в функцию поиска, и я хочу, чтобы мой API поиска был написан следующим образом:

return UserSearch::apply($filters);

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

Добавьте фильтр для поиска пользователей и возврата результатов поиска.

Это важно как для технического, так и для нетехнического персонала.

Я думаю, что мне нужно создать новый класс поиска пользователей и статическую функцию apply для получения условий фильтра. Позвольте мне начать:

Проще всего, давайте скопируем код в контроллере в функцию apply:

newQuery();

        //Search based on user name
        if ($filters->has('name')) {
            $user->where('name', $filters->input('name'));
        }

        //User based company name search
        if ($filters->has('company')) {
            $user->where('company', $filters->input('company'));
        }

        //City name search based on users
        if ($filters->has('city')) {
            $user->where('city', $filters->input('city'));
        }

        //Only users assigned sales managers are returned
        if ($filters->has('managers')) {
            $user->whereHas('managers', 
                function ($query) use ($filters) {
                    $query->whereIn('managers.name', 
                        $filters->input('managers'));
                });
        }

   
        //Does the search criteria include 'event'?
        if ($filters->has('event')) {

            //Only return users invited to the activity
            $user->whereHas('rsvp.event', 
                function ($query) use ($filters) {
                    $query->where('event.slug', 
                        $filters->input('event'));
                });

        
            //Only users who replied to the invitation in any form are returned
            if ($filters->has('responded')) {
                $user->whereHas('rsvp', 
                    function ($query) use ($filters) {
                        $query->whereNotNull('responded_at');
                    });
            }

          
            //Only users who answered the invitation in some way are returned
            if ($filters->has('response')) {
                $user->whereHas('rsvp', 
                    function ($query) use ($filters) {
                        $query->where('response', 
                            'I will be attending');
                    });
            }
        }

        //Return search results
        return $user->get();
    }
}

Мы внесли ряд изменений, прежде всего, мы $запросим Переменную, переименованную в фильтры , Чтобы улучшить читаемость.

Во-вторых, поскольку newQuery() Метод не является статическим методом и не может быть вызван статически через пользовательский класс, поэтому нам нужно сначала создать пользовательский объект, а затем вызвать этот метод:

$user = (new User)->newQuery();

Вызовите интерфейс поиска пользователя выше, чтобы восстановить код контроллера:

Намного лучше, не так ли? Ряд суждений об условиях передается специальной обработке класса, что делает введение кода контроллера свежим.

Давайте войдем в момент, когда мы станем свидетелями чудес

В примере этой статьи существует семь условий фильтрации, но реальность все больше и больше. Таким образом, в этом случае для обработки всей логики фильтрации используется только один файл, что неудовлетворительно. Масштабируемость не очень хорошая и не соответствует принципам s.o.l.i.d.. В настоящее время метод apply() должен обрабатывать эту логику:

  • Проверьте, существует ли параметр
  • Преобразование параметров в условия запроса
  • Запрос на выполнение

Если я хочу добавить новое условие фильтра или изменить логику существующего условия фильтра, я буду постоянно изменять класс usersearch, потому что вся обработка условий фильтра находится в этом классе, и с увеличением бизнес-логики это будет казаться немного подавляющим. Поэтому необходимо создать файл класса для каждого условия фильтра.

Сначала из Имя Давайте начнем с условий. Но, как мы уже говорили ранее, подумайте о том, как нам нужно использовать этот единый интерфейс условной фильтрации.

Я хочу назвать этот интерфейс следующим образом:

$user = (new User)->newQuery();
$user = static::applyFiltersToQuery($filters, $user);
return $user->get();

Но используйте его здесь $user Это имя переменной не подходит. Вы должны использовать $query Более осмысленно.

public static function apply(Request $filters)
{
    $query = (new User)->newQuery();

    $query = static::applyFiltersToQuery($filters, $query);

    return $query->get();
}

Затем поместите всю логику условной фильтрации в применить фильтры к запросу() В этом новом интерфейсе.

Давайте начнем создавать первый класс условного фильтра: имя.

where('name', $value);
    }
}

Определите статический метод в этом классе apply() , этот метод принимает два параметра, один из которых является экземпляром построителя, а другой-значением условия фильтра (в данном случае значение “Билли”). Затем верните новый экземпляр builder с этим фильтром.

Следующий – городской класс:

where('city', $value);
    }
}

Как вы можете видеть, логика кода класса city такая же, как и у класса name, за исключением того, что условием фильтра становится ” город “. Пусть каждый класс фильтра условий имеет только один простой apply() Метод, а параметры, полученные методом, и логика обработки одинаковы. Мы можем рассматривать это как протокол, что очень важно. Я подробно объясню это ниже.

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

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

Затем реализуйте интерфейс фильтра в классах name и city соответственно:

where('name', $value);
    }
}

так же как

where('city', $value);
    }
}

Идеальный. Теперь есть два класса условных фильтров, которые идеально соответствуют этому протоколу. Моя структура каталогов прикреплена ниже для вашей справки:

Это файловая структура поиска на данный момент.

Я помещаю все файлы фильтров в отдельную папку, что дает мне четкое представление о существующих условиях фильтрации.

Используйте новый фильтр

Теперь, оглядываясь назад на метод apply filters to query () класса usersearch, мы видим, что можем выполнить еще некоторую оптимизацию.

Прежде всего, построение операторов запроса в каждом суждении об условиях присваивается соответствующему классу фильтров.

//Search users by name
if ($filters->has('name')) {
    $query = Name::apply($query, $filters->input('name'));
}

//Search users by city
if ($filters->has('city')) {
    $query = City::apply($query, $filters->input('city'));
}

Теперь работа по построению операторов запроса на основе условий фильтра была перенесена в каждый соответствующий класс фильтров, но работа по определению того, существует ли каждое условие фильтра, выполняется с помощью ряда условий. Более того, все параметры оценки условий записаны, и один параметр соответствует одному классу фильтра. Таким образом, каждый раз, когда я добавляю новое условие фильтра, я должен снова изменять его UserSearch Class. Это явно проблема, которую необходимо решить.

На самом деле, в соответствии с правилами именования, которые мы ввели ранее, мы можем легко изменить код, оцениваемый по этому условию, на динамический:

Имя Фильтров поиска пользователей приложения

Фильтры поиска пользователей приложения Город

Он заключается в динамическом создании класса фильтра путем объединения имени пространства имен и условия фильтра (конечно, для полученных параметров условия фильтра должна быть выполнена соответствующая обработка).

Вероятно, в этом и заключается идея. Ниже приводится конкретная реализация:

private static function applyFiltersToQuery(
                           Request $filters, Builder $query) {
    foreach ($filters->all() as $filterName => $value) {

        $decorator =
            __NAMESPACE__ . '\Filters\' . 
                str_replace(' ', '', ucwords(
                    str_replace('_', ' ', $filterName)));

        if (class_exists($decorator)) {
            $query = $decorator::apply($query, $value);
        }

    }

    return $query;
}

Проанализируйте код строка за строкой:

foreach ($filters->all() as $filterName => $value) {

Просмотрите все параметры фильтра и установите имя параметра (например, город ), присвоенное переменной $Имя фильтра , значение параметра (например, Лондон ), Скопируйте в переменную $значение .

$decorator =
            __NAMESPACE__ . '\Filters\' . 
                str_replace(' ', '', ucwords(
                    str_replace('_', ' ', $filterName)));

Здесь необходимо обработать имя параметра, изменить подчеркивание на пробел, сделать каждое слово заглавным, а затем удалить пробел, как показано в следующем примере:

"name"            => App\UserSearch\Filters\Name,\
"company"         => App\UserSearch\Filters\Company,\
"city"            => App\UserSearch\Filters\City,\
"event"           => App\UserSearch\Filters\Event,\
"responded"       => App\UserSearch\Filters\Responded,\
"response"        => App\UserSearch\Filters\Response,\
"managers"        => App\UserSearch\Filters\Managers

Если имена параметров подчеркнуты, например has_respond В соответствии с приведенными выше правилами, они будут обработаны в hasrespond , поэтому имя соответствующего класса фильтра должно быть таким.

if (class_exists($decorator)) {

Это делается для того, чтобы сначала убедиться, что класс фильтра существует, а затем выполнить следующие операции, иначе будет неловко сообщать об ошибке на клиенте.

$query = $decorator::apply($query, $value);

Это волшебное место. PHP допускает переменные $декоратор Как класс и вызывайте его методы (в данном случае метод apply ()). Теперь посмотрите на код этого интерфейса, и мы обнаружим, что наша сила доказывает, что заточка ножа не мешает рубить дрова. Теперь мы можем убедиться, что каждый класс фильтров последовательно реагирует на внешнюю среду и обрабатывает свою собственную внутреннюю логику.

Окончательная оптимизация

Сейчас Поиск пользователя Код класса должен быть намного лучше, чем раньше, но я думаю, что он может быть лучше, поэтому я внес некоторые изменения. Это окончательная версия:

newQuery()
            );

        return static::getResults($query);
    }
    
    private static function applyDecoratorsFromRequest(Request $request, Builder $query)
    {
        foreach ($request->all() as $filterName => $value) {

            $decorator = static::createFilterDecorator($filterName);

            if (static::isValidDecorator($decorator)) {
                $query = $decorator::apply($query, $value);
            }

        }
        return $query;
    }
    
    private static function createFilterDecorator($name)
    {
        return return __NAMESPACE__ . '\Filters\' . 
            str_replace(' ', '', 
                ucwords(str_replace('_', ' ', $name)));
    }
    
    private static function isValidDecorator($decorator)
    {
        return class_exists($decorator);
    }

    private static function getResults(Builder $query)
    {
        return $query->get();
    }

}

Я, наконец, решил избавиться от него применить фильтры к методу запроса () , потому что основное имя метода смысла и интерфейса применить() Есть небольшой конфликт.

Более того, чтобы реализовать принцип единой ответственности, я применяю Фильтры к запросу() Более сложная логика в методе разделена. Отдельные методы написаны для динамического создания имен классов фильтров и подтверждения существования классов фильтров.

Таким образом, даже если я захочу расширить интерфейс поиска, мне не нужно будет повторно изменять его UserSearch Class. Нужно добавить новые условия фильтрации? Просто, если Приложение\Поиск пользователя\Фильтры Создать класс фильтра в каталоге и реализовать его Фильтр Интерфейс в порядке.

заключение

Мы сохранили огромный метод контроллера со всей логикой поиска в виде модульной системы фильтрации, которая позволяет включать фильтры без добавления или изменения основного кода, как предложил @ rockroxx в комментариях, другая схема рефакторинга заключается в извлечении всех методов в признак И будет Пользователь Настроен const Затем из Интерфейса Реализации.

class UserSearch implements Searchable {
    const MODEL = App\User;
    use SearchableTrait;
}

Если вы хорошо понимаете этот шаблон проектирования, вы можете использовать полиморфизм вместо нескольких условий.

Код будет отправлен на GitHub, где вы сможете раскошелиться, протестировать и поэкспериментировать.

Как решить многокритериальный расширенный поиск, я надеюсь, вы сможете оставить свои мысли, предложения и комментарии.