Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

ignium's avatar
Level 21

Livewire assuming date inputs are in UTC

I'm struggling to get a javascript date picker (pikaday) working using the example on the Livewire Site after some tweaking I finally got it working.

The issue is I have two inputs on the page to select a start and end date, when changing an input and the model is updated, it seems livewire is making the assumption that the times are being sent in UTC so it adds the offset to both model attributes. This is a problem because when it initially hydrates the model, it's already adding the offset, and if for some reason a user changes the end date, 3 times, 28 hours will have been added to the start time (changing it to a day later) without firing an input event so the in the input looks like it has not changed.

I was able to prevent this problem from happening by reverting the timezone setting in /config/app.php back to 'timezone' => 'UTC'. Obviously this isn't what I want to do, as all of stored timestamps are now off by 7 or 8 hours.

Here are the results of a dd() in the updatedModelEndDate($value) hook:

"Sat Aug 01 2020 14:00:00 GMT-0700"    //$this->model->start_date->toString()
"Tue Mar 23 2021 23:59:59 GMT-0700"   //$this->model->end_date->toString() 
"Tue Mar 23 2021 07:00:00 GMT+0000"  //Carbon::parse($value)->toString()
"2021-03-23T07:00:00.000Z"                     //$value

I'll be the first to admit that time has always been a pain point for me in programming, but I think there must be a config issue somewhere that I'm missing.

Any help would be much appreciated as I've been ripping my hair out on this one all weekend.

PS: As I said, I'm pretty sure this is a configuration issue, so I haven't posted any code, but can do that if it will help find a solution.

0 likes
11 replies
Snapey's avatar

probably related to laravel sending all json times in UTC

ignium's avatar
Level 21

@neilstee Here is the model, but it's not an issue with persisting the data is the hydration of the data on input that's the problem.

###ServicePeriodsTable.php

<?php

namespace App\Http\Livewire;

use App\ServicePeriod;
use App\Traits\HasEditableRows;
use Livewire\Component;

class ServicePeriodsTable extends Component
{
    use HasEditableRows;

    protected $model_class = ServicePeriod::class;

    protected function rules()
    {
        return [
            'model.name' => ['nullable'],
            'model.start_date' => ['nullable'],
            'model.end_date' => ['nullable']
        ];
    }

    public function render()
    {
        return view('livewire.service-periods-table', [
            'service_periods' => ServicePeriod::query()
            ->orderBy('start_date')
            ->get()
        ]);
    }
}

HasEditableRows.php (trait)

<?php

namespace App\Traits;

use App\Exceptions\NotImplementedException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;

trait HasEditableRows
{
    use FlashesCRUDStatus, AuthorizesRequests;

    public function initializeHasEditableRows() {
        $this->listeners = [
            'newModel' => 'showCreateForm',
            'deleteModel' => 'delete'
        ];
    }

    /**
     * Index of row being edited
     *
     * @var int|null
     */
    public $editedRowIndex = null;

    /**
     * Details of model being edited
     *
     * @var Model
     */
    public $model = null;

    /**
     * Name of field to be used for flashing CRUD messages
     *
     * @var string
     */
    protected $flash_field = 'name';

    /**
     * Check that model class is set
     *
     * @throws NotImplementedException
     * @return void
     */
    protected function checkModelClassSet()
    {
        if (! $this->model_class) {
            throw new NotImplementedException('Model type has not been defined in the base class. Please add a protected $model_type attribute.');
        }
    }

    /**
     * Show the new model modal
     *
     * @return void
     */
    public function showCreateForm()
    {
        $this->resetForm();

        $this->dispatchBrowserEvent('new-model');
    }

    /**
     * Cancel creating a model and close modal
     *
     * @return void
     */
    public function cancelCreate()
    {
        $this->resetForm();
    }

    /**
     * Store a new model in the database
     *
     * @return void
     */
    public function store()
    {
        $this->checkModelClassSet();
        $this->authorize('create', $this->model_class);

        $model = $this->model_class::create($this->validate()['model']);
        $this->dispatchBrowserEvent('close-modal');
        $this->dispatchBrowserEvent('flash', $this->getAddMessage($model->{$this->flash_field}));

        $this->resetForm();
    }

    /**
     * Set the given row to edit mode
     *
     * @param array $model
     * @param int $index
     * @return void
     */
    public function editRow($model, $index)
    {
        $this->model = $this->retreiveModel($model['id']);
        $this->editedRowIndex = $index;
    }

    /**
     * Cancel a row being edited
     *
     * @return void
     */
    public function cancelEdit()
    {
        $this->resetForm();
    }

    /**
     * Update the model in the database
     *
     * @return void
     */
    public function update()
    {
        dd($this->model);
        $this->checkModelClassSet();
        $this->authorize('update', $this->model);

        $this->model->update($this->validate()['model']);
        $this->dispatchBrowserEvent('flash', $this->getUpdateMessage($this->model->{$this->flash_field}));

        $this->resetForm();
    }

    /**
     * Delete the model from the database
     *
     * @param array $args
     * @return void
     */
    public function delete($args)
    {
        $this->checkModelClassSet();
        $model = $this->retreiveModel($args['model']['id']);

        $this->authorize('delete', $model);
        $model->delete();
        $this->dispatchBrowserEvent('flash', $this->getDeleteMessage($model->{$this->flash_field}));

        $this->resetForm();
    }

    /**
     * Listener when a field is updated
     *
     * @param mixed $value
     * @return void
     */
    public function updated($value)
    {
        $this->resetErrorBag($value);
    }

    /**
     * Reset the form to a read-only state
     *
     * @return void
     */
    protected function resetForm()
    {
        if (method_exists($this, 'resetModel')) {
            $this->resetModel();
        } else {
            $this->model = new $this->model_class;
        }
        $this->editedRowIndex = null;
        $this->resetErrorBag();
    }

    /**
     * Retrieve the model from the database
     *
     * @param string $id
     * @return Model
     */
    protected function retreiveModel($id)
    {
        return $this->model_class::findOrFail($id);
    }
}

Livewire/Alpine input

<input
    x-data
    x-ref="input"
    x-init="new Pikaday({
        field: $refs.input,
        format: 'DD/MM/YY',
        onSelect: function(dateText) {
            @this.set('model.start_date', moment(dateText))
            },
        })"
        type="text"
        value="{{ $model && $model->start_date ? $model->start_date->format('m/d/y') : today() }}"
        class="form-input block w-full pr-10 sm:text-sm sm:leading-5"
>
ignium's avatar
Level 21

ServicePeriod.php

<?php

namespace App;

use App\Settings\GlobalSettings;

class ServicePeriod extends UniqueTimePeriod
{
    /**
     * {@inheritDoc}
     *
     * @var array
     */
    protected $guarded = [];

    /**
     * Return the uri to the resource
     *
     * @return string
     */
    public function path()
    {
        return route('service-periods.show', $this);
    }

    /**
     * {@inheritDoc}
     */
    protected function getAttributesForBeforeSplit() {
        return [];
    }

    /**
     * {@inheritDoc}
     */
    protected function getAttributesForAfterSplit() {
        $start = $this->dayAfter($this->end_date)->format('m/d/y');
        $end = $this->envelope->end_date->format('m/d/y');

        return [
            'name' => "$start - $end"
        ];
    }

    /**
     * Return a the model of the current service period
     *
     * @return ServicePeriod|null
     */
    public static function current()
    {
        return ServicePeriod::find(GlobalSettings::get(GlobalSettings::CURRENT_SERVICE_PERIOD_ID));
    }

    /**
     * Set this ServicePeriod as the current service period
     *
     * @return void
     */
    public function setCurrent()
    {
        GlobalSettings::set(GlobalSettings::CURRENT_SERVICE_PERIOD_ID, $this->id);
    }

    /**
     * Return if this ServicePeriod is the current service period
     *
     * @return boolean
     */
    public function isCurrent()
    {
        return $this->id == GlobalSettings::get(GlobalSettings::CURRENT_SERVICE_PERIOD_ID);
    }

    /**
     * Return invoices associated with this service period
     *
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function invoices()
    {
        return $this->hasMany(Invoice::class);
    }
}

The UniqueTimePeriod parent class just uses a trait, and the only things that would apply here from that trait are the following


    /**
     * Initialize the trait
     *
     * @return void
     */
    public function initializeHasUniquePeriods()
    {
        $this->dates[] = 'start_date';
        $this->dates[] = 'end_date';

        $this->casts['period'] = CarbonPeriodCast::class;
    }

    /**
     * Accessor returns end_date attribute as end of day
     *
     * @param string|Carbon $value
     * @return Carbon
     */
    public function getEndDateAttribute($value = null)
    {
        return $value ? \Carbon\Carbon::parse($value)->endOfDay() : null;
    }
neilstee's avatar

@ignium try to remove/comment that $dates property first from your UniqueTimePeriod class to see if it will work.

neilstee's avatar
neilstee
Best Answer
Level 34

@ignium including the one that is initialize like protected $dates in UniqueTimePeriod class

ignium's avatar
Level 21

So yes and no, it's still pulling the time with the offset, but it doesn't continually add the offset, so the days will remain the same.

On the component rendering (from Livewire Devtools):

model: Object
    end_date: "2021-07-31"
    name: "2020 - 2021"
    start_date: "2020-08-01"

The when after updating start_date

model: Object
    end_date: "2021-07-31"
    name: "2020 - 2021"
    start_date: "2021-03-22T07:00:00.000Z"

Previously this would add the offset to the end_date as well and push it to the next day, so that part works at least. But this seems like a bigger issue since it's still adding the offset. I think @snapey was on the right path in that the JSON is being sent in UTC in the first place.

I'm also a little weary since, that trait is used in a couple of models, not just the ServicePeriods model.

ignium's avatar
Level 21

So this wasn't a complete fix, but removing the accessor for the end date property as you mentioned, but keeping the date casting, and then adding the following serializeDate() method to the HasUniquePeriods trait, has got it to the point I can at least go to bed without this keeping me awake. I think the accessor was basically signaling an additional update to Livewire and this was causing things to get even more out of wack.

Many thanks to you and @snapey for the help and getting me to look in places I was missing before!

For reference, here's the method I mentioned (I basically just removed the UTC "Z" flag at the end):

    protected function serializeDate(DateTimeInterface $date)
    {
        return $date->format('Y-m-d\TH:i:s.u');
    }

Reference: Laravel Docs - Date Serialization

1 like
gbrits's avatar

@ignium Thanks mate, this helped me with WireUI usage of the datetime picker where it had issues. I posted your solution on a Github issue for their library to help others.

1 like

Please or to participate in this conversation.