Search code examples
phplaraveltimestampphp-carbontimestamp-with-timezone

How to globally treat all Eloquent timestamps as including timezone


I have a Laravel application, and I just switched to timestampTz and timestampsTz in every single migration. As soon as I ran php artisan migrate I immediately ran into "Trailing data" issues with Carbon due to the date format mismatch caused by the change.

I don't want to add the $dateFormat property to every model I create when I have no intention of ever using timezone-less timestamp columns. I also don't want to introduce a trait or make a new superclass that extends Eloquent's Model that I then need to add to every model I already have (and ones that I generate in the future).

Is there any way to avoid all this and just have every timestamp field be treated as if they all had timezones?


Solution

  • This is easily done in Laravel 6 by creating a new Illuminate Grammar class and overriding the getDateFormat method, which is used as a fallback if the dateFormat property is missing on a model.

    Have a look inside vendor/laravel/framework/src/Illuminate/Database/Query/Grammars. Your class will need to extend one of the vendor-specific grammar classes found here based on what database you connect to. For this example, I will be extending PostgresGrammar. Adjust app/Providers/AppServiceProvider.php like so:

    use Illuminate\Support\Facades\DB;
    
    class AppServiceProvider extends ServiceProvider
    {
        /**
         * Register any application services.
         *
         * @return void
         * @throws \Doctrine\DBAL\DBALException
         */
        public function register()
        {
            // ...
    
            $conn = DB::connection(DB::getDefaultConnection());
            $platform = $conn->getDoctrineConnection()->getDatabasePlatform();
            $conn->setQueryGrammar(new class($platform->getDateTimeTzFormatString()) extends PostgresGrammar {
                protected $date_format;
    
                public function __construct(string $date_format)
                {
                    $this->date_format = $date_format;
                }
    
                public function getDateFormat()
                {
                    return $this->date_format;
                }
            });
        }
    }
    

    This will replace the original query grammar with another that will let us take over the date format string. An anonymous class is used to avoid having to create a separate file for this small bit of functionality, but you may choose to move this to its own file for readability. The anonymous class is passed the value of $platform->getDateTimeTzFormatString() as the constructor's only argument, which is then stored for use by the getDateFormat method.

    After this change, any trailing data errors should be gone for good. Just make sure to use timestampTz and timestampsTz in every migration going forward. 3rd-party libraries usually let you publish any migrations bundled with them, allowing you to adjust those as needed.