Model::firstOrCreate

Finds the first row matching the given attributes, or inserts one merged with the extra values.

Signature
Model::firstOrCreate(array $attributes = [], Closure|array $values = []): static

Parameters

ParameterTypeDescription
$$attributesoptionalarray= []Column and value pairs used as the where clause for the lookup, and also written to the new row when nothing matched.
$$valuesoptionalClosure|array= []Extra attributes merged over $attributes only when the record has to be created. A closure is invoked lazily and must return an array.

Return value

staticThe existing model if one matched, otherwise the newly inserted model. Check wasRecentlyCreated to tell them apart.

Errors

Rethrows Illuminate\Database\UniqueConstraintViolationException if the insert violates a unique index and the subsequent lookup still finds no matching row.

Changelog

  • PHP 12.51.0$values may now be a Closure returning an array, so expensive defaults are only computed when a row actually has to be inserted.

firstOrCreate runs a where on the first array. If a row matches it is returned untouched. If not, a row is inserted with both arrays merged, the first array supplying the identifying columns and the second the defaults.

When to reach for it

Use it for get-or-make lookups: a tag by name, a settings row for a user, a daily aggregate bucket. It is the natural fit whenever a job may run twice and must not produce a duplicate.

What trips people up

The second array is create-only. If the row already exists, those values are ignored — the method never updates anything. Expecting them to be applied to an existing record is the single most common misreading; updateOrCreate is the method that does that.

Because the returned model may be old or brand new, use the wasRecentlyCreated property rather than guessing. Note it is not a method.

Internally the insert goes through createOrFirst, which catches a unique constraint violation and re-reads the row from the write connection. That makes concurrent calls safe — but only if the lookup columns actually carry a unique index. Without one, two simultaneous requests happily insert two rows.

Example

Example · php
<?php

use App\Models\Flight;

// Retrieve flight by name or create it if it doesn't exist...
$flight = Flight::firstOrCreate([
    'name' => 'London to Paris',
]);

echo $flight->name;                     // London to Paris
var_dump($flight->wasRecentlyCreated);  // bool(true) on the first call

// Retrieve by name, or create with the extra defaults merged in...
$flight = Flight::firstOrCreate(
    ['name' => 'London to Paris'],
    ['delayed' => 1, 'arrival_time' => '11:30']
);

var_dump($flight->wasRecentlyCreated);  // bool(false) — the row already existed
var_dump($flight->delayed);             // NULL — defaults were NOT applied

When to use it

  • Look up a tag by name and create it the first time it is used.
  • Ensure a settings or profile row exists before writing preferences to it.
  • Make an importer safe to re-run without producing duplicate records.
  • Fetch or open the daily statistics bucket for a given date.

Frequently asked questions

Does firstOrCreate update the record if it already exists?

No. When a matching row is found it is returned exactly as stored and the second array is ignored entirely. Use updateOrCreate when existing rows should also be written to.

How do I know whether firstOrCreate created a new row?

Check the wasRecentlyCreated property on the returned model — it is true only when the model was inserted during the current request. It is a property, not a method, so there are no parentheses.

Is firstOrCreate safe against race conditions?

Yes, provided the lookup columns have a unique index. The insert is wrapped so that a unique constraint violation is caught and the winning row is read back instead. Without the index the database accepts both inserts and you get duplicates.

More examples

firstOrCreate versus firstOrNew

Both merge the two arrays the same way, but firstOrNew stops short of the insert. Use firstOrNew when you want to adjust the model before it hits the database — validate it, attach a computed column, or decide not to save it at all.

Example · php
<?php

use App\Models\Flight;

// Hits the database and inserts if nothing matches.
$created = Flight::firstOrCreate(
    ['name' => 'Tokyo to Sydney'],
    ['delayed' => 1, 'arrival_time' => '11:30']
);

var_dump($created->exists);  // bool(true) — already persisted

// Builds the instance in memory only. Nothing is written.
$new = Flight::firstOrNew(
    ['name' => 'Tokyo to Sydney'],
    ['delayed' => 1, 'arrival_time' => '11:30']
);

var_dump($new->exists);  // bool(false) until you call save()

$new->arrival_time = '12:00';
$new->save();

Lazily computed defaults with a closure

Passing a closure means the hashing only happens on the run that actually inserts a row; on every later call the closure is never invoked. This matters when the defaults are expensive — an API call, a hash, a UUID generated by a slow source. Closure support for $values landed in 12.51.0.

Example · php
<?php

use App\Models\ApiToken;
use Illuminate\Support\Str;

$token = ApiToken::firstOrCreate(
    ['user_id' => 7, 'name' => 'cli'],
    fn () => [
        'token' => hash('sha256', Str::random(40)),
        'abilities' => ['read'],
    ]
);

echo $token->name;   // cli
echo strlen($token->token);  // 64

Race safety needs a unique index

firstOrCreate wraps its insert in a try/catch for UniqueConstraintViolationException and, on a collision, re-reads the row using the write connection. That recovery only fires if the database actually rejects the duplicate, so without the unique index on name both workers insert successfully and you end up with two tags.

Example · php
<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

Schema::table('tags', function (Blueprint $table) {
    $table->unique('name');
});

// Two concurrent workers both find nothing and both try to insert.
// One insert wins; the other raises a unique violation, which
// firstOrCreate catches and turns into a read of the winning row.
$tag = \App\Models\Tag::firstOrCreate(['name' => 'laravel']);

echo $tag->name;  // laravel — exactly one row exists either way

Discussion

  • Be the first to comment on this lesson.