Использование хелперов для моделей

Создание вспомогательных методово в Ларавел для Eloquent моделей

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

Всемогущий Божественный класс

Божественный класс (god class) — антипаттерн ООП. (прим. переводчика)

Среди разработчиков постоянно идут споры, нужно ли избегать Eloquent любой ценой, так как он использует слишком много «магии» Laravel для функционирования. Лично я пришел к выводу, что этот аргумент не имеет значения для большинства разработчиков. Идея в том, что вместо этого нужно создать свой собственный слой доступа к разделенным данным, а также все, сопровождающие его, коды и тесты, а это, для большинства разработчиков, нереалистично, так как слишком сложно и отнимает много времени.

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

Почему я решил это сделать

Это произошло после просмотра одного из удивительных видео Jeffrey Way на Laracasts, где он рассказывает о том, как избежать использования божественных классов. Простым примером этого является модель User, в которой вы, теоретически, можете создавать сотни функций и запускать все возможные действия из пользовательского объекта.

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

Создание хелперов модели

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

if (TextValidator::containsHateSpeech($comment->body)) {
    $user->update(["banned_at" => now()]);
}

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

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

// Где-то в приложении
if (TextValidator::containsHateSpeech($post->body)) {
    $user->ban();
}

// В другом месте приложения
if (TextValidator::containsHateSpeech($comment->body)) {
    $user->ban();
}

// User model
class User
{    
     /**
     * Запретить пользователю на участие в форуме
     *
     */
    public function ban() : void
    {
        $this->update(["banned_at" => now()]);
    }}

Еще одним дополнительным преимуществом этого подхода является то, что поскольку логика существует в публичном методе модели User, мы можем легко написать для нее модульный тест:

/** @test */
public function a_user_can_be_banned()
{
    $adam = factory(User::class, 1)->create(["banned_at" => null]);    
    $this->assertNull($adam->banned_at);    
    $adam->ban();    
    $this->assertEquals($adam->fresh()->banned_at, now());
}

Расширяем подход

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

if ($user->banned !== null) {
    // Выполняем некоторые действия
}

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

Напротив, гораздо проще будет им управлять, если мы переместим его в хелпер модели User. Мы также можем легко добавить обратный метод проверки, не забанен ли пользователь:

// Где-то в приложении
if ($user->isBanned()) {
    // Запрет доступа
}

// В другом месте приложения
if ($user->isNotBanned()) {
    // Выполняем некоторые действия
}

// User model
class User
{    
     /**
     * Определяем, был ли пользователь забанен
     *
     */
    public function isBanned() : bool
    {
        return $this->banned === null;
    }    
    
    /**
     * Определяем, что пользователь не забанен
     *
     */
    public function isNotBanned() : bool
    {
        return ! $this->isBanned();
    }}

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

/** @test */
public function a_user_knows_if_it_is_banned()
{
    $adam = factory(User::class, 1)->create(["banned_at" => now()]);    
    $eve = factory(User::class, 1)->create(["banned_at" => null]);    
    $this->assertTrue($adam->isBanned());    
    $this->assertFalse($eve->isBanned());
}

/** @test */
public function a_user_knows_if_it_is_not_banned()
{
    $adam = factory(User::class, 1)->create(["banned_at" => null]);    
    $eve = factory(User::class, 1)->create(["banned_at" => now()]);    
    $this->assertTrue($adam->isNotBanned());    
    $this->assertFalse($eve->isNotBanned());
}

Использование трейтов для организации связанного функционала

Если у вас есть несколько методов, относящихся к одной и той же «области» модели, возможно стоит извлечь их в отдельный файл. Мы можем использовать для этого trait, а затем просто использовать в модели.

К вашему сведению: Laravel широко это использует. И модель User является ярким примером, поскольку она использует Notifiable, Authenticable и Authorizable. Еще одним дополнительным преимуществом трейтов — это возможность использовать их в нескольких классах, поэтому, если функциональность одинакова в нескольких моделях, то вы можете просто использовать ее повторно.

Давайте перенесем хелперы в трейт и импортируем в модель User:

trait Bannable
{
     /**
     * Запретить пользователю на участие в форуме
     *
     */
    public function ban() : void
    {
        $this->update(["banned_at" => now()]);
    }    
    
     /**
     *  Определяем, был ли пользователь забанен
     *
     */
    public function isBanned() : bool
    {
        return $this->banned === null;
    }    
    
     /**
     * Определяем, что пользователь не забанен
     *
     */
    public function isNotBanned() : bool
    {
        return ! $this->isBanned();
    }
}
    
// User class
use App\Traits\Bannable;

class User
{
    use Bannable;
}

Важно знать

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

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

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

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

Завершение

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

Еще раз спасибо и хорошего кода!

Автор: Matt Kingshott
Перевод: Demiurge Ash