Laravel — это зрелый фреймворк для веб-приложений на PHP со встроенной поддержкой практически всего, что необходимо современным приложениям. Но мы не собираемся рассматривать все эти возможности здесь! Вместо этого мы рассмотрим тему, о которой мало говорят: Laravel имеет множество функций безопасности, которые могут помочь предотвратить болезненные ошибки.

Мы рассмотрим следующие механизмы безопасности:

  • N+1 prevention (Предотвращение N+1)
  • Partially hydrated model protection (Защита частично гидратированной модели)
  • Attribute typos and renamed columns (Опечатки в атрибутах и переименованные столбцы)
  • Mass assignment protection (Защита от массового присвоения)
  • Model strictness (Строгость модели)
  • Polymorphic mapping enforcement (Применение полиморфных отображений)
  • Long-running event monitoring (Мониторинг длительных событий)

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

N+1 prevention (Предотвращение N+1)

Многие ORM, в том числе и Eloquent, предлагают «функцию», которая позволяет вам лениво загружать отношения модели. Ленивая загрузка удобна тем, что вам не нужно заранее думать о том, какие отношения выбирать из базы данных, но она часто приводит к кошмару производительности, известному как «проблема N+1».

Проблема N+1 — одна из самых распространенных проблем, с которыми люди сталкиваются при использовании ORM, и это часто является причиной, по которой люди избегают ORM вообще. Это немного перебор, поскольку мы можем просто отключить ленивую загрузку вообще!

Представьте себе наивный список записей в блоге. Мы покажем название блога и имя автора.

$posts = Post::all();

foreach($posts as $post) {
    // `author` is lazy loaded.
    echo $post->title . ' - ' . $post->author->name;
}

Это пример проблемы N+1! В первой строке выбираются все записи блога. Затем для каждого отдельного сообщения мы выполняем еще один запрос, чтобы получить автора сообщения.

SELECT * FROM posts;
SELECT * FROM users WHERE user_id = 1;
SELECT * FROM users WHERE user_id = 2;
SELECT * FROM users WHERE user_id = 3;
SELECT * FROM users WHERE user_id = 4;
SELECT * FROM users WHERE user_id = 5;

Обозначение «N+1» связано с тем, что для каждой из n-многих записей, возвращенных первым запросом, выполняется дополнительный запрос. Один начальный запрос плюс n-много других. N+1.

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

В Laravel вы можете использовать метод preventLazyLoading в классе Model, чтобы полностью отключить ленивую загрузку. Проблема решена! Воистину, все так просто.

Вы можете добавить метод в свой AppServiceProvider:

use Illuminate\Database\Eloquent\Model;

public function boot()
{
    Model::preventLazyLoading();
}

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

// Eager load the `author` relationship.
$posts = Post::with('author')->get();

foreach($posts as $post) {
    // `author` is already loaded.
    echo $post->title . ' - ' . $post->author->name;
}

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

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

Чтобы предотвратить ленивую загрузку в non-production средах, вы можете добавить это в свой AppServiceProvider:

use Illuminate\Database\Eloquent\Model;

public function boot()
{
    // Prevent lazy loading, but only when the app is not in production.
    Model::preventLazyLoading(!$this->app->isProduction());
}

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

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

use Illuminate\Database\Eloquent\Model;

public function boot()
{
    // Prevent lazy loading always.
    Model::preventLazyLoading();

    // But in production, log the violation instead of throwing an exception.
    if ($this->app->isProduction()) {
        Model::handleLazyLoadingViolationUsing(function ($model, $relation) {
            $class = get_class($model);

            info("Attempted to lazy load [{$relation}] on model [{$class}].");
        });
    }
}

Partially hydrated model protection (Защита частично гидратированной модели)

Почти в каждой книге по SQL одной из рекомендаций по производительности является «выбирайте только те столбцы, которые вам нужны». Это хороший совет! Вы хотите, чтобы база данных получала и возвращала только те данные, которые вы действительно собираетесь использовать, потому что все остальное просто отбрасывается.

До недавнего времени это была непростая (а иногда и опасная!) рекомендация, которой можно было следовать в Laravel.

Модели Laravel Eloquent являются реализацией паттерна active record, где каждый экземпляр модели поддерживается строкой в базе данных.

Чтобы получить пользователя с идентификатором 1, вы можете использовать метод User::find() в Eloquent, который выполняет следующий SQL-запрос:

SELECT * FROM users WHERE id = 1;

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

$user = User::find(1);
// -> SELECT * FROM users where id = 1;

// Fully hydrated model, every column is present as an attribute.

// App\User {#5522
//   id: 1,
//   name: "Aaron",
//   email: "[email protected]",
//   is_admin: 0,
//   is_blocked: 0,
//   created_at: "1989-02-14 08:43:00",
//   updated_at: "2022-10-19 12:45:12",
// }

Выбор всех столбцов в этом случае, вероятно, не помешает! Но если ваша таблица пользователей очень широкая, имеет столбцы LONGTEXT или BLOB, или вы выбираете сотни или тысячи строк, вы, вероятно, захотите ограничить столбцы только теми, которые вы планируете использовать.

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

$user = User::select('id', 'name')->find(1);
// -> SELECT id, name FROM users where id = 1;

// Partially hydrated model, only some attributes are present.
// App\User {
//   id: 1,
//   name: "Aaron",
// }

Вот тут-то и возникает опасность.

Если вы обращаетесь к атрибуту, который не был выбран из базы данных, Laravel просто возвращает null. Ваш код будет думать, что атрибут равен null, но на самом деле он просто не был выбран из базы данных. Он может быть вовсе не null!

В следующем примере модель частично гидратируется только с помощью id и name, затем далее происходит обращение к атрибуту is_blocked. Поскольку is_blocked никогда не выбирался из базы данных, значение атрибута всегда будет равно null, рассматривая каждого заблокированного пользователя как незаблокированного.

// Partially hydrate a model.
$user = User::select('id', 'name')->find(1);

// is_blocked was not selected! It will always be `null`.
if ($user->is_blocked) {
    throw new \Illuminate\Auth\Access\AuthorizationException;
}

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

При особой осторожности и 100%-ном покрытии тестами вы, возможно, сможете предотвратить это, но это все равно будет заряженный пистолет, направленный прямо вам в ногу. По этой причине мы рекомендуем никогда не изменять оператор SELECT, который заполняет модель Eloquent.

До настоящего времени!

Выпуск Laravel 9.35.0 принес нам новую функцию безопасности для предотвращения этого.

В версии 9.35.0 вы можете вызвать Model::preventAccessingMissingAttributes(), чтобы предотвратить доступ к атрибутам, которые не были загружены из базы данных. Вместо того чтобы вернуть null, будет выброшено исключение, и все застопорится. Это очень хорошая вещь.

Вы можете включить это новое поведение, добавив это в свой AppServiceProvider:

use Illuminate\Database\Eloquent\Model;

public function boot()
{
    Model::preventAccessingMissingAttributes();
}

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

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

Доступ к атрибутам, которые не были выбраны, может привести к разного рода катастрофическому поведению:

  • Потеря данных
  • Перезапись данных
  • Отношение к бесплатным пользователям как к платным
  • Отношение к платным пользователям как к бесплатным
  • Отправка фактически неверных электронных писем
  • Отправка одного и того же письма десятки раз

Список можно продолжать и продолжать.

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

Attribute typos and renamed columns (Опечатки в атрибутах и переименованные столбцы)

Это продолжение предыдущего раздела и еще одна просьба включить Model::preventAccessingMissingAttributes() в вашем рабочем окружении.

Мы только что долго рассматривали, как функция preventAccessingMissingAttributes() защищает вас от частично гидратированных моделей, но есть еще два сценария, когда этот метод может защитить вас!

Первое — это опечатки.

Продолжая сценарий is_blocked, описанный выше, если вы случайно неправильно напишете слово «blocked«, Laravel просто вернет null вместо того, чтобы сообщить вам о вашей ошибке.

// Fully hydrated model.
$user = User::find(1);

// Oops! Spelled "blocked" wrong. Everyone gets through!
if ($user->is_blokced) {
    throw new \Illuminate\Auth\Access\AuthorizationException;
}

Этот конкретный пример, скорее всего, будет обнаружен при тестировании, но зачем рисковать?

Второй сценарий — это переименованные столбцы. Если ваша колонка сначала называлась blocked, а затем вы решили, что логичнее было бы назвать ее is_blocked, вам придется вернуться в код и обновить все ссылки на blocked. А если вы пропустите одну? Она просто станет null.

// Fully hydrated model.
$user = User::find(1);

// Oops! Used the old name. Everyone gets through!
if ($user->blocked) {
    throw new \Illuminate\Auth\Access\AuthorizationException;
}

Включение Model::preventAccessingMissingAttributes() превратит этот тихий сбой в явный.

Mass assignment protection (Защита от массового присвоения)

Массовое присвоение — это уязвимость, которая позволяет пользователям устанавливать атрибуты, которые они не должны иметь права устанавливать.

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

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

class User extends Model
{
    protected $fillable = [
        'name',
        'email',
    ];
}

Не имеет значения, сколько атрибутов вы передадите при создании или сохранении модели. Только name и email будут сохранены:

// It doesn’t matter what the user passed in, only `name`
// and `email` are updated. `is_admin` is discarded.
User::find(1)->update([
    'name' => 'Aaron',
    'email' => '[email protected]',
    'is_admin' => true
]);

Многие разработчики Laravel предпочитают вообще отключить защиту от массового назначения и полагаются на валидацию запроса для исключения атрибутов. Это совершенно разумно! Вам просто нужно убедиться, что вы никогда не передаете $request->all() в методы сохранения модели.

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

use Illuminate\Database\Eloquent\Model;

public function boot()
{
    // No mass assignment protection at all.
    Model::unguard();
}

Помните: вы рискуете, когда снимаете защиту со своих моделей! Никогда не передавайте все данные запроса вслепую.

// Only update `name` and `email`.
User::find(1)->update($request->only(['name', 'email']));

Если вы решите сохранить защиту от массового присвоения, есть еще один метод, который вам пригодится: метод Model::preventSilentlyDiscardingAttributes().

В случае, когда ваши заполняемые атрибуты состоят только из name и email, и вы пытаетесь обновить birthday, то birthday будет молча отброшен без предупреждения.

// Мы пытаемся обновить `birthday`, но он не сохраняется!
User::find(1)->update([
    'name' => 'Aaron',
    'email' => '[email protected]',
    'birthday' => '1989-02-14'
]);

Атрибут birthday отбрасывается, потому что он не заполняется. Это защита от массового присвоения в действии, и это то, чего мы хотим! Это просто немного сбивает с толку, потому что это неявное, а не прямое действие.

Теперь Laravel предоставляет возможность сделать эту тихую ошибку явной:

use Illuminate\Database\Eloquent\Model;

public function boot()
{
    // Предупреждение при попытке установить незаполняемое свойство.
    Model::preventSilentlyDiscardingAttributes();
}

Вместо того чтобы молча отбросить атрибуты, будет выброшено MassAssignmentException, и вы сразу поймете, что происходит.

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

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

use Illuminate\Database\Eloquent\Model;

public function boot()
{
    // Предупреждайте нас, когда мы пытаемся установить незаполняемое свойство,
    // в любой среде!
    Model::preventSilentlyDiscardingAttributes();
}

Model strictness (Строгость модели)

Laravel 9.35.0 предоставляет вспомогательный метод Model::shouldBeStrict(), который управляет тремя настройками «строгости» Eloquent:

  • Model::preventLazyLoading()
  • Model::preventSilentlyDiscardingAttributes()
  • Model::preventsAccessingMissingAttributes()

Идея здесь в том, что вы можете поместить вызов shouldBeStrict() в ваш AppServiceProvider и включить или выключить все три настройки одним вызовом метода. Давайте быстро вспомним наши рекомендации для каждого параметра:

  • preventLazyLoading: В первую очередь для производительности приложения. Выключено для рабочего процесса, включено локально. (Если только вы не регистрируете нарушения на производстве).
  • preventSilentlyDiscardingAttributes: В первую очередь для корректности приложения. Включено везде.
  • preventAccessingMissingAttributes: В первую очередь для корректности работы приложения. Включено везде.

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

use Illuminate\Database\Eloquent\Model;

public function boot()
{
    // Everything strict, all the time.
    Model::shouldBeStrict();

    // In production, merely log lazy loading violations.
    if ($this->app->isProduction()) {
        Model::handleLazyLoadingViolationUsing(function ($model, $relation) {
            $class = get_class($model);

            info("Attempted to lazy load [{$relation}] on model [{$class}].");
        });
    }
}

Если вы не планируете регистрировать нарушения ленивой нагрузки (что является разумным решением!), то вы настроите свои параметры таким образом:

use Illuminate\Database\Eloquent\Model;

public function boot()
{
    // As these are concerned with application correctness,
    // leave them enabled all the time.
    Model::preventAccessingMissingAttributes();
    Model::preventSilentlyDiscardingAttributes();

    // Since this is a performance concern only, don’t halt
    // production for violations.
    Model::preventLazyLoading(!$this->app->isProduction());
}

Polymorphic mapping enforcement (Применение полиморфных отображений)

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

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

В таблице images вы увидите два столбца, которые Laravel использует для поиска родительской модели: imageable_type и imageable_id.

В столбце imageable_type хранится тип модели в виде полностью квалифицированного имени класса (FQCN), а imageable_id является первичным ключом модели.

mysql> select * from images;
+----+-------------+-----------------+------------------------------+
| id | imageable_id | imageable_type | url                          |
+----+-------------+-----------------+------------------------------+
|  1 |           1 | App\Post        | https://example.com/1001.jpg |
|  2 |           2 | App\Post        | https://example.com/1002.jpg |
|  3 |           3 | App\Post        | https://example.com/1003.jpg |
|  4 |       22001 | App\User        | https://example.com/1004.jpg |
|  5 |       22000 | App\User        | https://example.com/1005.jpg |
|  6 |       22002 | App\User        | https://example.com/1006.jpg |
|  7 |           4 | App\Post        | https://example.com/1007.jpg |
|  8 |           5 | App\Post        | https://example.com/1008.jpg |
|  9 |       22003 | App\User        | https://example.com/1009.jpg |
| 10 |       22004 | App\User        | https://example.com/1010.jpg |
+----+-------------+-----------------+------------------------------+

Это поведение Laravel по умолчанию, но хранить FQCN в базе данных — не лучшая практика. Привязка данных в базе данных к конкретному имени класса очень хрупкая и может привести к непредвиденным поломкам, если вы когда-нибудь будете рефакторить свои классы.

Чтобы предотвратить это, Laravel дает нам возможность контролировать, какие значения попадают в базу данных с помощью метода Relation::morphMap. Используя этот метод, вы можете дать каждому морфированному классу уникальный ключ, который никогда не изменится, даже если имя класса изменится:

use Illuminate\Database\Eloquent\Relations;

public function boot()
{
    Relation::morphMap([
        'user' => \App\User::class,
        'post' => \App\Post::class,
    ]);
}

Теперь мы разорвали связь между именем нашего класса и данными, хранящимися в базе данных. Вместо того, чтобы видеть \App\User в базе данных, мы увидим user. Хорошее начало!

Однако у нас все еще есть одна потенциальная проблема: это сопоставление не является обязательным. Мы можем создать новую модель Comment и забыть добавить ее в morphMap, и Laravel по умолчанию будет использовать FQCN, что приведет к некоторому беспорядку.

mysql> select * from images;
+----+-------------+-----------------+------------------------------+
| id | imageable_id | imageable_type | url                          |
+----+-------------+-----------------+------------------------------+
|  1 |           1 | post            | https://example.com/1001.jpg |
|  2 |           2 | post            | https://example.com/1002.jpg |
| .. |         ... | ....            |  . . . . . . . . . . . . . . |
| 10 |       22004 | user            | https://example.com/1010.jpg |
| 11 |          10 | App\Comment     | https://example.com/1011.jpg |
| 12 |          11 | App\Comment     | https://example.com/1012.jpg |
| 13 |          12 | App\Comment     | https://example.com/1013.jpg |
+----+-------------+-----------------+------------------------------+

Некоторые из наших значений imageable_type правильно разделены, но поскольку мы забыли сопоставить модель App\Comment с ключом, FQCN все еще оказывается в базе данных!

Laravel прикрыл нас (снова), предоставив нам метод для принудительного отображения каждой морфированной модели. Вы можете изменить вызов morphMap на вызов enforceMorphMap, и поведение fall-through-to-FQCN будет отключено.

use Illuminate\Database\Eloquent\Relations;

public function boot()
{
    // Enforce a morph map instead of making it optional.
    Relation::enforceMorphMap([
        'user' => \App\User::class,
        'post' => \App\Post::class,
    ]);
}

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

Самые губительные неудачи — тихие; всегда лучше иметь явные неудачи!

Предотвращение случайных HTTP-запросов

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

Laravel уже давно предлагает нам способ сделать это, вызывая Http::fake(), который подделывает все исходящие HTTP-запросы. Однако чаще всего вы хотите подделать конкретный запрос и предоставить ответ:

use Illuminate\Support\Facades\Http;

// Fake GitHub requests only.
Http::fake([
    'github.com/*' => Http::response(['user_id' => '1234'], 200)
]);

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

В Laravel 9.12.0 появился метод preventStrayRequests для защиты от ошибочных запросов.

// Не пропускайте ни одного запроса.
Http::preventStrayRequests();

// Только поддельные запросы на GitHub.
Http::fake([
    'github.com/*' => Http::response(['user_id' => '1234'], 200)
]);

// Не подделано, поэтому возникает исключение.
Http::get('https://planetscale.com');

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

protected function setUp(): void
{
    parent::setUp();

    Http::preventStrayRequests();
}

В любых тестах, где вам нужно разрешить отправку немокированных запросов, вы можете снова включить эту возможность, вызвав Http::allowStrayRequests() в этом конкретном тесте.

Long-running event monitoring (Мониторинг длительных событий)

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

В Laravel 9.18.0 появился метод DB::whenQueryingForLongerThan(), который позволяет запускать обратный вызов, когда суммарное время выполнения всех ваших запросов превышает определенный порог.

use Illuminate\Support\Facades\DB;

public function boot()
{
    // Выводит предупреждение, если мы тратим на запрос более 2000 мс.
    DB::whenQueryingForLongerThan(2000, function (Connection $connection) {
        Log::warning("Database queries exceeded 2 seconds on {$connection->getName()}");
    });
}

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

use Illuminate\Support\Facades\DB;

public function boot()
{
    // Выдавать предупреждение, если мы тратим более 1000 мс на один запрос.
    DB::listen(function ($query) {
        if ($query->time > 1000) {
            Log::warning("An individual database query exceeded 1 second.", [
                'sql' => $query->sql
            ]);
        }
    });
}

Жизненный цикл запроса и команды

Аналогично мониторингу длительных запросов, вы можете отслеживать, когда жизненный цикл вашего запроса или команды длится дольше определенного порога. Оба этих метода доступны начиная с версии Laravel 9.31.0.

use Illuminate\Contracts\Http\Kernel as HttpKernel;
use Illuminate\Contracts\Console\Kernel as ConsoleKernel;

public function boot()
{
    if ($this->app->runningInConsole()) {
        // Log slow commands.
        $this->app[ConsoleKernel::class]->whenCommandLifecycleIsLongerThan(
            5000,
            function ($startedAt, $input, $status) {
                Log::warning("A command took longer than 5 seconds.");
            }
        );
    } else {
        // Log slow requests.
        $this->app[HttpKernel::class]->whenRequestLifecycleIsLongerThan(
            5000,
            function ($startedAt, $request, $response) {
                Log::warning("A request took longer than 5 seconds.");
            }
        );
    }
}

Сделайте неявное явным

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

Вам и так есть о чем беспокоиться. Снимите с себя часть забот, включив эти средства защиты!