Calculate remaining days until birthday

Constantin Druc · 08 Mar, 2022

Here I have an Event model. You can think of it as being something like a birthday, wedding anniversary, things like that. The event has a date attribute, and we need to use this date to determine the next anniversary, the next date we celebrate the event.

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Factories\HasFactory;
6use Illuminate\Database\Eloquent\Model;
7 
8class Event extends Model
9{
10 use HasFactory;
11 
12 protected $guarded = [];
13 protected $dates = ['date'];
14}

Let's look at an axis representing time:

Today is 08-03-2022. If we are to get the next anniversary for a date, let’s say 01-06-1991, what we’d need to do is to bring that date to the current year.

However, some dates might still end up in the past. For example, 01-03-1991 will turn into 01-03-2022, which is in the past compared to our current 08-03-2022 date.

So if the normalized date is still in the past, we need to add one more year to it.

Here’s the accessor:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Factories\HasFactory;
6use Illuminate\Database\Eloquent\Model;
7 
8class Event extends Model
9{
10 use HasFactory;
11 
12 protected $guarded = [];
13 protected $dates = ['date'];
14 
15 public function getNextAnniversaryAttribute()
16 {
17 $date = $this->date;
18 $date->setYear(now()->year);
19 
20 if ($date->isPast()) {
21 $date->addYear();
22 }
23 
24 return $date;
25 }
26}

Since the next anniversary is a carbon date, we can get the remaining time in different formats. So we can do: diffInDays(), diffInMonths(), diffInYears(), and more.

One particular nice format is the diffForHumans() - this will give you the remaining time in a nice, readable format like: “11 months from now” or “8 days from now”.

Testing the next anniversary date

We can test the accessor by creating two events: one with a date that, when normalized, will end up in the past compared to our current date, and another one that will end up in the future.

1<?php
2 
3namespace Tests\Feature\Models;
4 
5use App\Models\Event;
6use Carbon\Carbon;
7use Tests\TestCase;
8 
9class EventTest extends TestCase
10{
11 /** @test */
12 public function get_next_anniversary()
13 {
14 $eventA = new Event([
15 'date' => '01-03-1991'
16 ]);
17 
18 $this->assertTrue($eventA->next_anniversary->is('01-03-2023'));
19 
20 $eventB = new Event([
21 'date' => '01-06-1991'
22 ]);
23 
24 $this->assertTrue($eventB->next_anniversary->is('01-06-2022'));
25 }
26}

One thing to keep in mind when testing dates is that it’s a good idea to control whatever date is used as the current date; otherwise, the test will break at some point in the future.

To set the current date, we can use Carbon::setTestNow() and pass it the date in string format.

1<?php
2 
3namespace Tests\Feature\Models;
4 
5use App\Models\Event;
6use Carbon\Carbon;
7use Tests\TestCase;
8 
9class EventTest extends TestCase
10{
11 /** @test */
12 public function get_next_anniversary()
13 {
14 Carbon::setTestNow('08-03-2022'); [tl! highlight]
15 
16 $eventA = new Event([
17 'date' => '01-03-1991'
18 ]);
19 
20 $this->assertTrue($eventA->next_anniversary->is('01-03-2023'));
21 
22 $eventB = new Event([
23 'date' => '01-06-1991'
24 ]);
25 
26 $this->assertTrue($eventB->next_anniversary->is('01-06-2022'));
27 }
28}

Defining accessors in Laravel 9

My favorite part is that we can remove the prefix and suffix and that both the accessor and the mutator are defined in the same place, under the same method.

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Database\Eloquent\Casts\Attribute;
6use Illuminate\Database\Eloquent\Factories\HasFactory;
7use Illuminate\Database\Eloquent\Model;
8 
9class Event extends Model
10{
11 use HasFactory;
12 
13 protected $guarded = [];
14 protected $dates = ['date'];
15 
16 public function nextAnniversary(): Attribute
17 {
18 return Attribute::make(
19 get: function () {
20 $date = $this->date;
21 $date->setYear(now()->year);
22 
23 if ($date->isPast()) {
24 $date->addYear();
25 }
26 
27 return $date;
28 }
29 );
30 }
31}