In a Laravel migration, what does $table->foreignId('seller_id')->constrained()->cascadeOnDelete() create, and why must nullable() come before constrained()?
answer
- unsigned big integer column
- table guessed: seller_id to sellers
- constrained() returns ForeignKeyDefinition
- later modifiers hit the constraint
- dropConstrainedForeignId in down()
basics
~20 sIt adds an unsigned big-integer seller_id column and a foreign key to sellers.id that deletes listings when their seller is deleted. constrained() returns the foreign-key definition, so a nullable() chained after it never reaches the column.
solid answer
~30 s`foreignId('seller_id')` adds an `UNSIGNED BIGINT` equivalent column that matches `$table->id()`. `constrained()` adds a foreign key and, with no arguments, guesses the target from the column name: strip `_id`, pluralise, reference `id`, giving `sellers.id`; pass `constrained(table: 'users')` when the convention does not fit. `cascadeOnDelete()` sets `ON DELETE CASCADE`; siblings include `restrictOnDelete()`, `nullOnDelete()` and `cascadeOnUpdate()`. **Column** modifiers must come before `constrained()`, because it returns an `Illuminate\Database\Schema\ForeignKeyDefinition`, not the column. `->constrained()->nullable()` records `nullable` on the constraint definition, where it has no effect, and the column stays `NOT NULL`. The constraint is named `listings_seller_id_foreign`; in `down()`, `$table->dropConstrainedForeignId('seller_id')` drops the key and then the column.
code
php · 26 lines<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('listings', function (Blueprint $table) {
$table->foreignId('seller_id')->constrained()->cascadeOnDelete();
$table->foreignId('buyer_id')->nullable()->constrained('users')->nullOnDelete();
$table->index(['seller_id', 'created_at']);
});
}
public function down(): void
{
Schema::table('listings', function (Blueprint $table) {
$table->dropIndex(['seller_id', 'created_at']);
$table->dropConstrainedForeignId('buyer_id');
$table->dropConstrainedForeignId('seller_id');
});
}
};go deeper
Recall that foreignId() adds the column, constrained() adds the foreign key by convention, and cascadeOnDelete() sets the delete action.
Explain the table-name convention, why column modifiers precede constrained(), and how index and constraint names are derived and dropped.
Review migrations for silent modifier mistakes, reversible down() methods and referential actions that match the domain's delete rules.
Decide where the database enforces referential integrity and cascades, versus where the application owns deletes.
## The one-liner, unpacked In a marketplace, every listing belongs to a seller. The migration for that relation is usually one line: ```php Schema::table('listings', function (Blueprint $table) { $table->foreignId('seller_id')->constrained()->cascadeOnDelete(); }); ``` That line does three separate things: 1. **`foreignId('seller_id')`** adds a column of the `UNSIGNED BIGINT` family, the same type `$table->id()` creates for primary keys, so the two sides match. 2. **`constrained()`** creates a **foreign key constraint**. With no arguments it applies Laravel's conventions: take the column name, remove the `_id` suffix, pluralise it, and reference its `id` column. `seller_id` therefore points at `sellers.id`. 3. **`cascadeOnDelete()`** sets the constraint's `ON DELETE` action to `CASCADE`, so deleting a seller deletes their listings in the database. When the convention does not fit, pass arguments: `constrained(table: 'users')` for a `seller_id` that references `users`, or `constrained(table: 'users', indexName: 'listings_seller_fk')` to name the constraint yourself. ## Referential actions | Method | SQL action | |---|---| | `cascadeOnDelete()` | `ON DELETE CASCADE` | | `restrictOnDelete()` | `ON DELETE RESTRICT` | | `nullOnDelete()` | `ON DELETE SET NULL`, needs a nullable column | | `noActionOnDelete()` | `ON DELETE NO ACTION` | | `cascadeOnUpdate()` | `ON UPDATE CASCADE` | `onDelete('cascade')` and `onUpdate('cascade')` are the longer equivalents. ## Why the order of modifiers matters The **column** and the **constraint** are two different objects in Laravel's schema builder: - `foreignId()` returns a `ForeignIdColumnDefinition`, which is a column definition. Column modifiers such as `nullable()`, `default()`, `comment()` or `after()` set attributes on it. - `constrained()` returns an `Illuminate\Database\Schema\ForeignKeyDefinition`, which describes the constraint. Both are **fluent** objects that accept any method name and store it as an attribute. So `->constrained()->nullable()` does not fail: it quietly records `nullable` on the constraint, which the grammar ignores, and the column is created `NOT NULL`. The docs state the rule plainly: additional column modifiers must be called before `constrained()`: ```php $table->foreignId('buyer_id')->nullable()->constrained('users')->nullOnDelete(); ``` This matters most with `nullOnDelete()`, which needs a nullable column: getting the order wrong pairs a `SET NULL` action with a `NOT NULL` column, which some databases reject when the constraint is created and others only when a delete tries to write the `NULL`. ## Indexes and names Laravel names indexes and constraints from the table, the columns and a suffix: - `$table->index(['seller_id', 'created_at'])` becomes `listings_seller_id_created_at_index`. - `$table->unique('slug')` becomes `listings_slug_unique`. - The foreign key above becomes `listings_seller_id_foreign`. Passing the same column array to a drop method rebuilds the name for you: `dropIndex(['seller_id', 'created_at'])`, `dropUnique(['slug'])`, `dropForeign(['seller_id'])`. ## Writing `down()` A foreign key usually has to be dropped before its column. Laravel bundles both: ```php public function down(): void { Schema::table('listings', function (Blueprint $table) { $table->dropConstrainedForeignId('seller_id'); }); } ``` `dropConstrainedForeignId('seller_id')` calls `dropForeign(['seller_id'])` and then `dropColumn('seller_id')`. ## Related shortcuts - **`foreignIdFor(Seller::class)`** derives the column name from a model class, `seller_id` here, and chains `constrained()` the same way. - **`foreignUuid('seller_id')`** and **`foreignUlid('seller_id')`** create the matching column types when the referenced table uses UUID or ULID keys; `constrained()` works on them too. - **Explicit form:** `$table->unsignedBigInteger('seller_id')` followed by `$table->foreign('seller_id')->references('id')->on('sellers')` is what the shortcut expands to, and is still useful when the column already exists. ## SQLite caveat SQLite enforces foreign keys only when they are switched on. Laravel's `sqlite` connection turns them on through the `foreign_key_constraints` option, read from `DB_FOREIGN_KEYS` and `true` in the skeleton, so a cascade that works locally works for the right reason.
- Your listings table stores the seller in a column named owner_id that references users. How do you declare it?The convention would guess an `owners` table, so name the target: `$table->foreignId('owner_id')->constrained(table: 'users')`. Everything else stays the same: the column type matches `users.id`, and the constraint is named `listings_owner_id_foreign` unless you pass `indexName`.
- Why might dropColumn('seller_id') fail in down() when the foreign key is still present?Many databases refuse to drop a column that a foreign key constraint still uses. Drop the constraint first with `dropForeign(['seller_id'])`, then the column, or use `dropConstrainedForeignId('seller_id')`, which does both in that order.
saying these in an interview costs you the question
- Chains nullable() after constrained() and expects a nullable column
- Thinks foreignId() alone creates the foreign key constraint
- Believes constrained() on seller_id references the seller table
- Uses nullOnDelete() on a column that is not nullable
- Drops the column in down() before its foreign key