Introduction

One to one is a very simple relationship. It says for a single model, it should belong to or own a single another type of model instance. Let me elaborate on that further with an example. Lets say we have a database of citizens and database of passports. A single citizen must not have more than one passport and a passport should not belong to more than one citizen. This is a one to one relationship.

ERD

We will be using the following Entity relationship diagram to demonstrate one to one relationship. One to one ERD

Generate Files

I will be spinning up a new laravel install using the following command and I am going to call it article-demos. After that I am going to create required migrations and models.

laravel new article-demos
cd article-demos
php artisan make:model Citizen -m
php artisan make:model Passport -m

Now you should have migrations and models in your project. Lets continue.

Migrations

Lets create the very basics of these tables to get a better idea.

// Citizens table migration
Schema::create('citizens', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->timestamps();
});
// Passport table migration
Schema::create('passports', function (Blueprint $table) {
    $table->id();
    $table->string('passport_number')->unique();
    $table->date('expire_at');
    $table->foreignId('citizen_id')->constrained()->cascadeOnDelete();
  	$table->timestamps();
});

After we migrate the above table, we should have in our database; a citizens table with an id and name, and passports table with an id, passport number, expire_at and citizen_id. Notice that we have a foreign key in the passports table. It makes sense to put the foreign key in the passports table as a citizen may exist without a passport but a passport cannot exist without an owner/citizen.

Note: You may notice that this table structure looks a lot like a one to many relationship, as we can have multiple instances of passports with same citizen_id. You would not be incorrect if you think that, but we will enforce the one to one mapping in the application level. If you don't know what I am talking about one to many relationship, don't worry, you will understand when we cover the one to many relationships.

Models

Now lets create our models and enforce the one to one relationship.

class Citizen extends Model
{

    protected $fillable = [
        'name'
    ];

    /**
    * Laravel will assume that the foreign key in passports
    * table will be named as citizen_id. If not, you
    * are required to provide the foreign key name
    * as a second parameter. eg: owner_id
    */
    public function passport(): HasOne
    {
        return $this->hasOne(Passport::class);
    }
}
class Passport extends Model
{

    protected $fillable = [
        'passport_number',
        'expire_at',
    ];

    protected $casts = [
        'expire_at' => 'date'
    ];

    /**
    * Here since we named the relationship method as citizen,
    * eloquent will assume that the foreign key name
    * will be citizen_id, which is correct. But if
    * we have named owner() as the method name,
    * we would be required to pass a second
    * parameter defining the foreign key
    */
    public function citizen(): BelongsTo
    {
        return $this->belongsTo(Citizen::class);
    }
}

Don't forget to import the HasOne and BelongsTo classes

It is as simple as that. The one to one relationship setup is done. I admire the readability of the Laravel relationship names. Just try saying "A Citizen hasOne Passport, and a Passport belongsTo a Citizen"

Saving, Updating & Deleting

You may use any of the following methods to save or update your relationships depending on the use case.

$citizen = Citizen::create(['name' => 'Afeef']); // this will throw an error if you have not added name field to fillable fields

$passport = new Passport();
$passport->passport_number = "E0000001";
$passport->expire_at = now()->addYears(5);

// Option 1: Not recommended
$passport->citizen()->associate($citizen);
$passport->save();

// Option 2: Recommended
$citizen->passport()->save($passport);

// Option 3:
$citizen->passport()->create([
  'passport_number' => 'E000020',
  'expire_at' => now()->addYears(5),
]);

// Updating
$citizen->passport->update(['passport_number' => 'EXXXX001']);

// To delete
// Direct delete
$passport->delete();

// Or delete via relationship
$citizen->passport()->delete();

Querying

// You can load relationship using with as any other relationship
Citizen::query()->with('passport')->get();
Passport::query()->with('citizen')->get();

// You can access relationship attributes as follows
$citizen->passport?->passport_number;
$passport->citizen?->name;

Things To Keep In Mind

Suppose you created a citizen with name Ali, and a passport for him, as below

$citizen = Citizen::create(['name' => 'Ali']);
$citizen->passport()->create([
  'passport_number' => 'FIRSTPASSPORT',
  'expire_at' => now()->addYears(5),
]);

You can create another passport and associate it with same citizen. However, you will only get the first created passport through the hasOne relationship.

// The below code will create a second passport for the same citizen.
$passport2 = new Passport();
$passport2->passport_number = 'SECONDPASSPORT';
$passport2->expire_at = now()->addYears(10);
$passport2->citizen()->associate($citizen);

// However if you fetch the passport via citizen, you will still get the first passport.
$citizen->passport?->passport_number; // will be equal to 'FIRSTPASSPORT'

You may want it that way, or not. If you do not want it to behave this way, there are couple of things you can do. Firstly, you can add a unique constraint to passports' citizen_id foreign key as below

$table->foreignId('citizen_id')->unique()->constrained()->cascadeOnDelete();

Note that this will throw an error if you try to save a second passport with same citizen id.

Secondly, before creating a new passport, you may query for an existing record and then update it if it exists, or create it if it does not.

$citizen->passport()
  ->updateOrCreate(
	['passport_number' => '#345453'],
	['expire_at' => now()->addMonth()]
  );

You may also use both depending on your preference.

That's it I believe. If you have any questions, or suggestions, let me know and I will update the post as required. Happy coding!