НейроАгент

Автоматическая генерация исключений ModelNotFoundException в Laravel

Как заставить Laravel автоматически генерировать текст для исключения ModelNotFoundException при кастомных первичных ключах без запросов к БД. Узнайте о встроенных методах валидации.

Вопрос

Как заставить Laravel автоматически генерировать текст для исключения ModelNotFoundException при поиске модели с кастомным форматированием первичного ключа? Моя модель имеет первичный ключ в виде специфически отформатированной строки. Я хочу избежать запроса к базе данных, если строка имеет неверный формат. При ручном выбрасывании исключения throw new ModelNotFoundException(); сообщение об ошибке пустое. Я могу сам сформировать сообщение ‘No query results for model [$classname] $value’ на основе входных параметров и имени класса, но существует ли встроенный способ, чтобы фреймворк сделал это автоматически (на случай изменения формата сообщения в будущем) без дополнительного запроса к базе данных?

НейроАгент

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

Содержание


Проблема с ModelNotFoundException при кастомных первичных ключах

По умолчанию Laravel генерирует сообщение об ошибке “No query results for model [ClassName] $value” при выбрасывании ModelNotFoundException. Однако при использовании кастомных первичных ключей с определенным форматированием возникают две основные проблемы:

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

Из документации Laravel видно, что findOrFail и firstOrFail методы выбрасывают ModelNotFoundException при отсутствии результатов, но не валидируют формат ключа до выполнения запроса.


Встроенная валидация формата с использованием UUID и ULID

Laravel предоставляет встроенные механизмы валидации формата первичных ключей через различные трейты, которые проверяют формат ключа до выполнения запроса к базе данных:

php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Concerns\HasUuids;

class YourModel extends Model
{
    use HasUuids; // или HasUlids, HasVersion7Uuids
    
    // Laravel автоматически будет проверять формат UUID перед запросом
    protected $keyType = 'string';
    public $incrementing = false;
}

Как объясняется в статье cosmastech, при использовании этих трейтов:

“Если ваша модель использует трейт HasUniqueStringIds или его наследников (HasUuids, HasUlids, HasVersion7Uuids), то ModelNotFoundException будет выброшен, если параметр маршрута не соответствует указанному валидному типу. Например, если вы используете HasUuids, а пользователь запрашивает GET /podcasts/this-is-not-a-uuid, то this-is-not-a-uuid не является UUID и возвращает 404. Хотя это выбрасывает то же исключение, что и если бы строка не была найдена в базе данных, база данных никогда не запрашивается, потому что привязка не проходит во время валидации ключа.”


Настройка неявного связывания моделей в маршрутах

Для автоматической валидации кастомных форматов ключей настройте неявное связывание моделей в маршрутах:

php
// В файле routes/web.php или routes/api.php
use App\Models\YourModel;

Route::get('/models/{model}', function (YourModel $model) {
    return $model;
})->where('model', '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}');

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


Кастомная валидация формата первичного ключа

Если ваш ключ имеет специфический формат, не являющийся стандартным UUID/ULID, реализуйте кастомный валидатор:

1. Создание кастомного трейта

php
namespace App\Traits;

trait HasCustomPrimaryKeyFormat
{
    public function getRouteKeyName()
    {
        return 'your_custom_key';
    }
    
    public function resolveRouteBinding($value, $field = null)
    {
        // Сначала валидируем формат ключа
        if (!$this->isValidCustomKeyFormat($value)) {
            throw new \Illuminate\Database\Eloquent\ModelNotFoundException(
                "No query results for model [".get_class($this)."] ".$value
            );
        }
        
        // Затем выполняем запрос только если формат корректен
        return parent::resolveRouteBinding($value, $field);
    }
    
    protected function isValidCustomKeyFormat($value)
    {
        // Реализуйте вашу логику валидации формата
        return preg_match('/^[A-Z]{2}-\d{6}$/', $value) === 1;
    }
}

2. Использование трейта в модели

php
use Illuminate\Database\Eloquent\Model;
use App\Traits\HasCustomPrimaryKeyFormat;

class YourModel extends Model
{
    use HasCustomPrimaryKeyFormat;
    
    protected $primaryKey = 'your_custom_key';
    public $incrementing = false;
    protected $keyType = 'string';
}

Обработка исключений с правильными сообщениями

Чтобы гарантировать, что сообщения об ошибках генерируются автоматически без ручного конструирования, используйте механизм преобразования исключений:

php
// В вашем App\Exceptions\Handler.php
public function register()
{
    $this->renderable(function (\Illuminate\Database\Eloquent\ModelNotFoundException $e, $request) {
        if ($request->wantsJson()) {
            return response()->json([
                'message' => $e->getMessage(),
                'model' => class_basename($e->getModel()),
                'key' => $e->getKey()
            ], 404);
        }
    });
}

Для обеспечения правильного формата сообщения без дополнительных запросов, добавьте в вашу модель:

php
use Illuminate\Database\Eloquent\ModelNotFoundException;

class YourModel extends Model
{
    // ... другие свойства модели
    
    public static function findOrFail($id, $columns = ['*'])
    {
        // Проверяем формат перед запросом
        if (!static::isValidPrimaryKeyFormat($id)) {
            throw new ModelNotFoundException(
                "No query results for model [".static::class."] ".$id
            );
        }
        
        return parent::findOrFail($id, $columns);
    }
    
    protected static function isValidPrimaryKeyFormat($value)
    {
        // Ваша логика валидации
        return true; // реальная реализация ваша
    }
}

Источники

  1. Laravel Eloquent: Getting Started - ModelNotFoundException
  2. Avoid Leaking Model Info: Securing Responses When a Model Is Not Found - cosmastech
  3. Laravel eloquent find() having a custom primary key returns not found - Stack Overflow
  4. Validator Error: Forcing A Unique Rule To Ignore A Given ID fails if primary key is not named “id” - GitHub

Заключение

Чтобы автоматически генерировать сообщения для ModelNotFoundException при кастомных первичных ключах без запросов к базе данных:

  1. Используйте встроенные трейты HasUuids, HasUlids или HasVersion7Uuids для стандартных форматов UUID/ULID
  2. Реализуйте кастомный трейт с валидацией формата для специфических форматов ключей
  3. Настройте неявное связывание моделей для автоматической валидации в маршрутах
  4. Переопределите методы findOrFail или resolveRouteBinding для добавления логики валидации
  5. Используйте механизм преобразования исключений для автоматического форматирования сообщений

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