NeuroAgent

Automatic Generation of ModelNotFoundException in Laravel

How to make Laravel automatically generate text for ModelNotFoundException with custom primary keys without database queries. Learn about built-in validation methods.

Question

How can I make Laravel automatically generate text for ModelNotFoundException when searching for a model with a custom formatted primary key? My model has a primary key that’s a specifically formatted string. I want to avoid database queries if the string has an invalid format. When manually throwing throw new ModelNotFoundException();, the error message is empty. I can manually create the message ‘No query results for model [$classname] $value’ based on input parameters and class name, but is there a built-in way for the framework to do this automatically (in case the message format changes in the future) without additional database queries?

NeuroAgent

To make Laravel automatically generate text for ModelNotFoundException when searching for a model with a custom formatted primary key and avoid database queries for incorrectly formatted values, use Laravel’s built-in key format validation mechanisms before executing the query.

Contents


Problem with ModelNotFoundException for custom primary keys

By default, Laravel generates the error message “No query results for model [ClassName] $value” when throwing ModelNotFoundException. However, when using custom primary keys with specific formatting, two main issues arise:

  1. Unnecessary database queries when passing incorrectly formatted keys
  2. Empty or incomplete error messages when manually throwing the exception

As seen in the Laravel documentation, the findOrFail and firstOrFail methods throw ModelNotFoundException when no results are found, but they don’t validate the key format before executing the query.


Built-in format validation using UUID and ULID

Laravel provides built-in mechanisms for validating primary key formats through various traits that check the key format before executing the database query:

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

class YourModel extends Model
{
    use HasUuids; // or HasUlids, HasVersion7Uuids
    
    // Laravel will automatically validate UUID format before querying
    protected $keyType = 'string';
    public $incrementing = false;
}

As explained in the cosmastech article, when using these traits:

“If your model uses the HasUniqueStringIds trait or its descendants (HasUuids, HasUlids, HasVersion7Uuids), then a ModelNotFoundException will be thrown if the route parameter doesn’t match the specified valid type. For example, if you’re using HasUuids and a user requests GET /podcasts/this-is-not-a-uuid, then this-is-not-a-uuid is not a UUID and returns a 404. Although this throws the same exception as if the string wasn’t found in the database, the database is never queried because the binding fails during key validation.”


Setting up implicit model binding in routes

For automatic validation of custom key formats, configure implicit model binding in your routes:

php
// In routes/web.php or 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}');

When using the HasUuids or HasUlids traits, validation happens automatically without needing to specify a regular expression in the route.


Custom primary key format validation

If your key has a specific format that’s not a standard UUID/ULID, implement a custom validator:

1. Creating a custom trait

php
namespace App\Traits;

trait HasCustomPrimaryKeyFormat
{
    public function getRouteKeyName()
    {
        return 'your_custom_key';
    }
    
    public function resolveRouteBinding($value, $field = null)
    {
        // First validate the key format
        if (!$this->isValidCustomKeyFormat($value)) {
            throw new \Illuminate\Database\Eloquent\ModelNotFoundException(
                "No query results for model [".get_class($this)."] ".$value
            );
        }
        
        // Then execute the query only if the format is correct
        return parent::resolveRouteBinding($value, $field);
    }
    
    protected function isValidCustomKeyFormat($value)
    {
        // Implement your format validation logic
        return preg_match('/^[A-Z]{2}-\d{6}$/', $value) === 1;
    }
}

2. Using the trait in your model

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';
}

Handling exceptions with proper messages

To ensure error messages are automatically generated without manual construction, use the exception rendering mechanism:

php
// In your 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);
        }
    });
}

To ensure proper message formatting without additional queries, add to your model:

php
use Illuminate\Database\Eloquent\ModelNotFoundException;

class YourModel extends Model
{
    // ... other model properties
    
    public static function findOrFail($id, $columns = ['*'])
    {
        // Check format before querying
        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)
    {
        // Your validation logic
        return true; // actual implementation is yours
    }
}

Sources

  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

Conclusion

To automatically generate messages for ModelNotFoundException with custom primary keys without database queries:

  1. Use built-in traits like HasUuids, HasUlids, or HasVersion7Uuids for standard UUID/ULID formats
  2. Implement a custom trait with format validation for specific key formats
  3. Configure implicit model binding for automatic validation in routes
  4. Override methods like findOrFail or resolveRouteBinding to add validation logic
  5. Use the exception rendering mechanism for automatic message formatting

These approaches help avoid unnecessary database queries when passing incorrectly formatted keys and ensure automatic error message generation according to Laravel standards.