MVC (Laravel) где добавить логику

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

Обычно я вижу этот тип логики в контроллерах. Это все прекрасно, пока вы не захотите воспроизвести эту функцию во многих местах. Когда вы начинаете разбираться в частичках, создавая API и создавая фиктивный контент, становится проблемой сохранить вещи СУХОЙ.

Я видел способы управления этим: события, репозитории, библиотеки и добавление в модели. Вот мое понимание каждого:

Службы: это, где большинство людей, вероятно, поместили бы этот код. Моя главная проблема со службами заключается в том, что иногда трудно найти в них определенные функции, и я чувствую, что они забывают о том, когда люди сосредоточены на использовании Eloquent. Откуда мне знать, что мне нужно вызвать метод publishPost() в библиотеке, когда я могу просто сделать $post->is_published = 1?

Единственное условие, в котором я вижу, что это работает хорошо, - это если вы используете ТОЛЬКО сервисы (и в идеале делаете Eloquent недоступным как-то для контроллеров все вместе).

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

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

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

Модели: Традиционно у меня были классы, которые выполняли CRUD, а также обрабатывали критическую связь. Это на самом деле упростило задачу, потому что вы знали все функциональные возможности CRUD +, что бы с ним ни было, все было.

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

Каковы преимущества / недостатки каждого метода? Я что-то пропустил?

4 ответа

Я думаю, что все шаблоны / архитектуры, которые вы представляете, очень полезны, если вы следуете принципам SOLID.

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

Краткий ответ: где это имеет смысл для вас (с услугами).

Длинный ответ:

Контроллеры: Какова ответственность контроллеров? Конечно, вы можете поместить всю свою логику в контроллер, но это ответственность контроллера? Я так не думаю.

Для меня контроллер должен получить запрос и вернуть данные, и это не место для проверки, вызова методов БД и т. Д.

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

Я думаю, что модель должна представлять сущность. С Laravel я использую только класс модели для добавления таких вещей, как fillable, guarded, table и отношения (это потому, что я использую шаблон репозитория, в противном случае модель также будет иметь save, update, findи т. д. методы).

Репозитории (Repository Pattern): В начале я был очень смущен этим. И, как и вы, я подумал: "Ну, я использую MySQL и все".

Тем не менее, я уравновесил плюсы и минусы использования шаблона репозитория, и теперь я его использую. Я думаю, что сейчас, в этот самый момент, мне нужно будет использовать только MySQL. Но если через три года мне нужно перейти на что-то вроде MongoDB, большая часть работы будет выполнена. Все за счет одного дополнительного интерфейса и $app->bind(«interface», «repository»),

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

Услуги: до сих пор у вас есть возможность добавить логику в контроллеры или модели. Для меня имеет смысл добавить логику в Сервисы. Посмотрим правде в глаза, Услуги это модное название для классов. И вы можете иметь столько классов, сколько это имеет смысл для вас в вашем приложении.

Возьмите этот пример: недавно я разработал что-то вроде Google Forms. Я начал с CustomFormService и в конечном итоге CustomFormService, CustomFormRender, CustomFieldService, CustomFieldRender, CustomAnswerService а также CustomAnswerRender, Зачем? Потому что это имело смысл для меня. Если вы работаете с командой, вы должны поставить свою логику там, где это имеет смысл для команды.

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

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

app/
    controllers/
    MyCompany/
        Composers/
        Exceptions/
        Models/
        Observers/
        Sanitizers/
        ServiceProviders/
        Services/
        Validators/
    views
    (...)

Я использую каждую папку для определенной функции. Например, Validators каталог содержит BaseValidator класс, ответственный за обработку проверки, основанный на $rules а также $messages конкретных валидаторов (обычно по одному для каждой модели). Я мог бы так же легко поместить этот код в Службу, но для меня имеет смысл иметь для этого специальную папку, даже если она используется только внутри Службы (на данный момент).

Я рекомендую вам прочитать следующие статьи, так как они могут объяснить вам кое-что получше:

Дэйл Рис (автор CodeBright): "Здесь ломаем форму". Именно здесь я собрал все это вместе, хотя я изменил несколько вещей в соответствии со своими потребностями.

Разделение вашего кода в Laravel с использованием репозиториев и сервисов Криса Гуси: этот пост хорошо объясняет, что такое сервис и шаблон репозитория и как они сочетаются друг с другом.

У Laracasts также есть Репозитории с Упрощенной и Единой Ответственностью, которые являются хорошими ресурсами с практическими примерами (даже если вы должны заплатить).

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

В итоге я использовал существующую структуру, которую предоставляет Laravel, а это означает, что я хранил свои файлы в основном в виде Model, View и Controller. У меня также есть папка "Библиотеки" для повторно используемых компонентов, которые на самом деле не являются моделями.

Я не оборачивал свои модели в услуги / библиотеки. Все приведенные причины не на 100% убедили меня в пользе использования услуг. Хотя я могу ошибаться, насколько я вижу, они просто приводят к множеству дополнительных почти пустых файлов, которые мне нужно создавать и переключаться между ними при работе с моделями, а также реально уменьшают преимущества использования eloquent (особенно когда речь идет о ПОЛУЧЕНИИ моделей). например, с использованием нумерации страниц, областей и т. д.).

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

  • Аксессоры и мутаторы: у Laravel есть отличные аксессоры и мутаторы. Если я хочу выполнить действие всякий раз, когда сообщение перемещается из черновика в опубликованное, я могу вызвать это, создав функцию setIsPublishedAttribute и включив в нее логику
  • Переопределение Create/Update и т. Д. Вы всегда можете переопределить методы Eloquent в своих моделях для включения пользовательских функций. Таким образом, вы можете вызывать функциональность в любой операции CRUD. Редактировать: я думаю, что есть ошибка с переопределением create в более новых версиях Laravel (поэтому я использую события, теперь зарегистрированные в boot)
  • Проверка: я проверяю свою проверку таким же образом, например, я запускаю проверку, переопределяя функции CRUD, а также средства доступа / мутаторы, если это необходимо. См. Esensi или dwightwatson/validating для получения дополнительной информации.
  • Магические методы: я использую методы __get и __set моих моделей, чтобы подключиться к функциональности там, где это необходимо
  • Расширение Eloquent: если вы хотите выполнить все обновления / создания, вы можете даже расширить eloquent и применить его к нескольким моделям.
  • События: Это прямое и общепризнанное место, где можно сделать это. Я думаю, что самым большим недостатком событий является то, что исключения трудно отследить (возможно, это не новый случай с новой системой событий Laravel). Мне также нравится группировать мои события по тому, что они делают, а не по тому, как они называются... например, иметь подписчика MailSender, который прослушивает события, отправляющие почту.
  • Добавление событий Pivot/BelongsToMany. Одна из вещей, с которой я долго боролся, заключалась в том, как связать поведение с модификацией отношений ownToMany. Например, выполнение действия всякий раз, когда пользователь присоединяется к группе. Я почти закончил полировать пользовательскую библиотеку для этого. Я еще не опубликовал это, но это функционально! Постараюсь опубликовать ссылку в ближайшее время. РЕДАКТИРОВАТЬ Я закончил превращать все мои стержни в нормальные модели, и моя жизнь стала намного проще...

Решение проблем людей с использованием моделей:

  • Организация: Да, если вы добавите больше логики в модели, они могут быть длиннее, но в целом я обнаружил, что 75% моих моделей все еще довольно маленькие. Если я решил организовать более крупные, я могу сделать это, используя черты (например, создать папку для модели с некоторыми другими файлами, такими как PostScopes, PostAccessors, PostValidation и т. Д.). Я знаю, что это не обязательно то, для чего нужны черты, но эта система работает без проблем.

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

КОГДА ИСПОЛЬЗОВАТЬ УСЛУГИ: В этой статье очень хорошо сформулированы ОТЛИЧНЫЕ примеры того, когда пользоваться услугами (подсказка: это не очень часто). Он говорит, что в основном, когда ваш объект использует несколько моделей или моделей в странных частях их жизненного цикла, это имеет смысл. http://www.justinweiss.com/articles/where-do-you-put-your-code/

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

  1. Контроллер получает запрошенное пользователем действие и отправляет параметры, а все делегирует классу обслуживания.
  2. Класс обслуживания выполняет всю логику, связанную с операцией: проверка ввода, регистрация событий, операции с базой данных и т. Д.
  3. Модель содержит информацию о полях, преобразовании данных и определениях валидации атрибутов.

Вот как я это делаю:

Это метод контроллера для создания чего-либо:

public function processCreateCongregation()
{
    // Get input data.
    $congregation                 = new Congregation;
    $congregation->name           = Input::get('name');
    $congregation->address        = Input::get('address');
    $congregation->pm_day_of_week = Input::get('pm_day_of_week');
    $pmHours                      = Input::get('pm_datetime_hours');
    $pmMinutes                    = Input::get('pm_datetime_minutes');
    $congregation->pm_datetime    = Carbon::createFromTime($pmHours, $pmMinutes, 0);

    // Delegates actual operation to service.
    try
    {
        CongregationService::createCongregation($congregation);
        $this->success(trans('messages.congregationCreated'));
        return Redirect::route('congregations.list');
    }
    catch (ValidationException $e)
    {
        // Catch validation errors thrown by service operation.
        return Redirect::route('congregations.create')
            ->withInput(Input::all())
            ->withErrors($e->getValidator());
    }
    catch (Exception $e)
    {
        // Catch any unexpected exception.
        return $this->unexpected($e);
    }
}

Это класс обслуживания, который выполняет логику, связанную с операцией:

public static function createCongregation(Congregation $congregation)
{
    // Log the operation.
    Log::info('Create congregation.', compact('congregation'));

    // Validate data.
    $validator = $congregation->getValidator();

    if ($validator->fails())
    {
        throw new ValidationException($validator);
    }

    // Save to the database.
    $congregation->created_by = Auth::user()->id;
    $congregation->updated_by = Auth::user()->id;

    $congregation->save();
}

И это моя модель:

class Congregation extends Eloquent
{
    protected $table = 'congregations';

    public function getValidator()
    {
        $data = array(
            'name' => $this->name,
            'address' => $this->address,
            'pm_day_of_week' => $this->pm_day_of_week,
            'pm_datetime' => $this->pm_datetime,
        );

        $rules = array(
            'name' => ['required', 'unique:congregations'],
            'address' => ['required'],
            'pm_day_of_week' => ['required', 'integer', 'between:0,6'],
            'pm_datetime' => ['required', 'regex:/([01]?[0-9]|2[0-3]):[0-5]?[0-9]:[0-5][0-9]/'],
        );

        return Validator::make($data, $rules);
    }

    public function getDates()
    {
        return array_merge_recursive(parent::getDates(), array(
            'pm_datetime',
            'cbs_datetime',
        ));
    }
}

Для получения дополнительной информации об этом способе я использую, чтобы организовать свой код для приложения Laravel: https://github.com/rmariuzzo/Pitimi

На мой взгляд, у Laravel уже есть много вариантов для хранения вашей бизнес-логики.

Короткий ответ:

  • Используйте Laravel's Request объекты для автоматической проверки вашего ввода, а затем сохранить данные в запросе (создать модель). Поскольку все входные данные пользователей непосредственно доступны в запросе, я считаю, что имеет смысл выполнить это здесь.
  • Используйте Laravel's Job объекты для выполнения задач, которые требуют отдельных компонентов, а затем просто отправлять их. Я думаю JobОхватывает классы обслуживания. Они выполняют задачу, такую ​​как бизнес-логика.

Длинный (эр) ответ:

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

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

Если ваш ответ "Наверное, нет", не используйте шаблон хранилища.

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

Используйте сервисы экономно: для меня классы сервисов - это просто место для хранения бизнес-логики для выполнения конкретной задачи с учетом ее зависимостей. У Laravel они есть из коробки, они называются "рабочие места", и они обладают гораздо большей гибкостью, чем пользовательский класс обслуживания.

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

Пример:

Запрос:

namespace App\Http\Requests;

use App\Post;
use App\Jobs\PostNotifier;
use App\Events\PostWasCreated;
use App\Http\Requests\Request;

class PostRequest extends Request
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'title'       => 'required',
            'description' => 'required'
        ];
    }

    /**
     * Save the post.
     *
     * @param Post $post
     *
     * @return bool
     */
    public function persist(Post $post)
    {
        if (!$post->exists) {
            // If the post doesn't exist, we'll assign the
            // post as created by the current user.
            $post->user_id = auth()->id();
        }

        $post->title = $this->title;
        $post->description = $this->description;

        // Perform other tasks, maybe fire an event, dispatch a job.

        if ($post->save()) {
            // Maybe we'll fire an event here that we can catch somewhere else that
            // needs to know when a post was created.
            event(new PostWasCreated($post));

            // Maybe we'll notify some users of the new post as well.
            dispatch(new PostNotifier($post));

            return true;
        }

        return false;
    }
}

Контроллер:

namespace App\Http\Controllers;

use App\Post;
use App\Http\Requests\PostRequest;

class PostController extends Controller
{

   /**
    * Creates a new post.
    *
    * @return string
    */
    public function store(PostRequest $request)
    {
        if ($request->persist(new Post())) {
            flash()->success('Successfully created new post!');
        } else {
            flash()->error('There was an issue creating a post. Please try again.');
        }

        return redirect()->back();
    }

   /**
    * Updates a post.
    *
    * @return string
    */
    public function update(PostRequest $request, $id)
    {
        $post = Post::findOrFail($id);

        if ($request->persist($post)) {
            flash()->success('Successfully updated post!');
        } else {
            flash()->error('There was an issue updating this post. Please try again.');
        }

        return redirect()->back();
    }
}

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

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

Другие вопросы по тегам