Рубрики
Uncategorized

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

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

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

Сегодня я расскажу обо всем процессе и о том, как создать гибкую и масштабируемую поисковую систему. Если вы хотите просмотреть код, пожалуйста, посетите репозиторий 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

Добавьте явные в предыдущем методе маршрутизации filter() :

Поскольку нам нужно обработать данные, отправленные запросом в методе фильтра, я ввел класс запроса в зависимость. Контейнер службы 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();
    }

    // Search for users based on the city
    if ($request->has('city')) {
        return $user->where('city', $request->input('city'))->get();
    }

    // Continue searching under other conditions

    // There are no other conditions.
    // Returns all eligible users.
    // Paging is needed in practical projects.
    return User::all();
}

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

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

Одним из способов достижения этой цели являются условия гнездования:

// Search for users by username
if ($request->has('name')) {
    // Are'city'search parameters also provided?
    if ($request->has('city')) {
        // Search User 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 for users by username
    if ($request->has('name')) {
        $user->where('name', $request->input('name'));
    }

    // Search Users Based on User Company Information
    if ($request->has('company')) {
        $user->where('company', $request->input('company'));
    }

    // Searching Users Based on User City Information
    if ($request->has('city')) {
        $user->where('city', $request->input('city'));
    }

    // Continue to perform 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'));
}

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

// Search only for users who have docked 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 the users who reply to the invitation (any form of reply is OK)
    if ($request->has('responded')) {
        $user->whereHas('rsvp', function ($query) use ($request) {
            $query->whereNotNull('responded_at');
        });
    }

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

// The final object is retrieved and returned
return $user->get();

Выясни это. Это фантастика! ___________

Есть ли необходимость в рефакторинге?

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

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

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

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

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

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

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

Потому что цель написания интерфейса-упростить код, использующий компоненты, а не код, упрощающий сам интерфейс. “(Извлечено из: c2.com)

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

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

return UserSearch::apply($filters);

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

При поиске пользователей добавьте фильтр и верните результаты поиска.

Это имеет значение как для техников, так и для техников, не являющихся техниками.

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

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

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

        // User-based City Name Search
        if ($filters->has('city')) {
            $user->where('city', $filters->input('city'));
        }

        // Return only users who have been assigned to the Sales Manager
        if ($filters->has('managers')) {
            $user->whereHas('managers', 
                function ($query) use ($filters) {
                    $query->whereIn('managers.name', 
                        $filters->input('managers'));
                });
        }

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

            // Return only users invited to participate in the event
            $user->whereHas('rsvp.event', 
                function ($query) use ($filters) {
                    $query->where('event.slug', 
                        $filters->input('event'));
                });

        
            // Return only users who answered invitations in any form
            if ($filters->has('responded')) {
                $user->whereHas('rsvp', 
                    function ($query) use ($filters) {
                        $query->whereNotNull('responded_at');
                    });
            }

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

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

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

Во-вторых, поскольку 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 Это имя переменной не подходит. Его следует использовать. $запрос Более значимый.

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

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

    return $query->get();
}

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

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

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

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

Далее идет городской класс:

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

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

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

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

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

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

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

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

}

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

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

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

заключение

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

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

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

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

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

От: https://learnku.com/ларавель/т…

Дополнительные статьи: https://learnku.com/laravel/c…