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

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

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

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

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

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

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

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

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

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

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

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

Ответ 1

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Нарушение формы Dayle Rees (автор CodeBright): Здесь я собрал все это вместе, хотя я изменил несколько вещей, чтобы соответствовать моим потребностям.

Развязка вашего кода в Laravel с использованием репозиториев и сервисов Chris Goosey: В этом сообщении хорошо объясняется, что такое "Сервис" и "Шаблон репозитория" и как они сочетаются.

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

Ответ 2

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

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

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

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

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

Ответ 3

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

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

Я НЕ ПЛАНИРУЕТ МОИ МОДЕЛИ В УСЛУГАХ/БИБЛИОТЕКАХ. Все изложенные причины не помогли мне на 100% воспользоваться услугами. Хотя я могу ошибаться, насколько я вижу, они просто приводят к количеству лишних почти пустых файлов, которые мне нужно создавать и переключаться между ними при работе с моделями, а также действительно уменьшать преимущество использования красноречивых (особенно когда речь заходит о моделях RETRIEVING, например, с использованием разбивки на страницы, области и т.д.).

Я поставил бизнес-логику в МОДЕЛИ и получил доступ к красноречивому прямо от моих контроллеров. Я использую ряд подходов, чтобы убедиться, что бизнес-логика не обошла стороной:

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

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

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

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

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

Ответ 4

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

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

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

Длинные (er) ответы:

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

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

Если ваш ответ "Вероятно нет", тогда не реализуйте шаблон репозитория.

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

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

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

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