Nullable dates in spatie/laravel-settings

Can anyone provide a basic example of how to define a settings class with a nullable date property? I have this:
class MySettings extends Settings
{
public ?DateTimeInterface $dateFrom;
public ?DateTimeInterface $dateFrom;

public static function group(): string
{
return 'dates';
}
}
class MySettings extends Settings
{
public ?DateTimeInterface $dateFrom;
public ?DateTimeInterface $dateFrom;

public static function group(): string
{
return 'dates';
}
}
and a Page which provides a form with DateTimePicker::make('dateFrom')->seconds(false)->native(false); but every time I try to save where there is a date set, I am getting an error saying:
Cannot assign string to property App\Settings\MySettings::$dateFrom of type ?DateTimeInterface
From reading the docs, I can't tell what else would be required? The global cast for DateTimeInterface is set up per the docs and default config publication.
Solution:
I think that the problem is that you are saying the property has to be a Carbon/DateTime instance but your trying to set it as a string. Easyist thing to do would probably be to convert the string to a carbon instance. ```php DateTimePicker::make('showFrom') ->label('Show From')...
Jump to solution
12 Replies
Anthaas
Anthaas5mo ago
It seems that casts just aren't running/working? With the global casters, it shouldn't try to assign a string to that property, should it?
ConnorHowell
ConnorHowell5mo ago
Don't type it as DateTimeInterface (that's an interface so it won't be casting correctly). You need to type it as a class that uses DateTimeInterface. Something like DateTime or Carbon
Anthaas
Anthaas5mo ago
@ConnorHowell I’ve tried both of those. Getting the same error.
toeknee
toeknee5mo ago
I think it actually needs to be a string since that's how livewire handles it, then modal cast it to a DateTimeInferface?
awcodes
awcodes5mo ago
can you dd() the string value. I"m wondering if the ->seconds(false) is making it an invalid dateTime string and cast is failing to convert it. also why do you have 2 properties with the same name?
Anthaas
Anthaas5mo ago
@awcodes Sorry for the delay. The duplicate property name was poor copy/paste on my part to show a minimal example. I have modified the fill function in laravel-settings/src/Settings to catch the TypeError as follows:
foreach ($properties as $name => $payload) {
try {
$this->{$name} = $payload;
} catch (\TypeError $e) {
dd($name, $payload);
}
}
foreach ($properties as $name => $payload) {
try {
$this->{$name} = $payload;
} catch (\TypeError $e) {
dd($name, $payload);
}
}
The value I get out is when selecting the 24th Jan 2024 at midnight is 2024-01-24 00:00. Appending the seconds(false) call with ->format('Y-m-d H:i:s') results in a full Y-m-d H:i:s format: 2024-01-24 00:00:00. I continue to get the same TypeError thrown. Hang on - I can easily just share the two classes involved here without needing to obfuscate. AnnouncementSettings:
<?php

namespace App\Settings;

use Carbon\Carbon;
use Spatie\LaravelSettings\Settings;

class AnnouncementSettings extends Settings
{
public string $content;
public ?Carbon $showFrom;
public ?Carbon $showUntil;

public static function group(): string
{
return 'announcement';
}
}
<?php

namespace App\Settings;

use Carbon\Carbon;
use Spatie\LaravelSettings\Settings;

class AnnouncementSettings extends Settings
{
public string $content;
public ?Carbon $showFrom;
public ?Carbon $showUntil;

public static function group(): string
{
return 'announcement';
}
}
ManageAnnouncement:
<?php

namespace App\Filament\Pages;

use App\Settings\AnnouncementSettings;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Pages\SettingsPage;

class ManageAnnouncement extends SettingsPage
{
protected static ?string $navigationIcon = 'heroicon-o-megaphone';
protected static ?string $navigationLabel = 'Announcement';
protected static ?string $navigationGroup = 'CONTENT';

protected ?string $heading = 'Announcement';

protected static string $settings = AnnouncementSettings::class;

public function form(Form $form): Form
{
return $form
->columns(1)
->schema([
TextInput::make('content')
->label('Content')
->maxLength(120),
DateTimePicker::make('showFrom')
->label('Show From')
->prefixIcon('heroicon-o-calendar')
->seconds(false)
->native(false)
->format('Y-m-d H:i:s'),
DateTimePicker::make('showUntil')
->label('Show Until')
->prefixIcon('heroicon-o-calendar')
->seconds(false)
->native(false)
->format('Y-m-d H:i:s'),
]);
}
}
<?php

namespace App\Filament\Pages;

use App\Settings\AnnouncementSettings;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Pages\SettingsPage;

class ManageAnnouncement extends SettingsPage
{
protected static ?string $navigationIcon = 'heroicon-o-megaphone';
protected static ?string $navigationLabel = 'Announcement';
protected static ?string $navigationGroup = 'CONTENT';

protected ?string $heading = 'Announcement';

protected static string $settings = AnnouncementSettings::class;

public function form(Form $form): Form
{
return $form
->columns(1)
->schema([
TextInput::make('content')
->label('Content')
->maxLength(120),
DateTimePicker::make('showFrom')
->label('Show From')
->prefixIcon('heroicon-o-calendar')
->seconds(false)
->native(false)
->format('Y-m-d H:i:s'),
DateTimePicker::make('showUntil')
->label('Show Until')
->prefixIcon('heroicon-o-calendar')
->seconds(false)
->native(false)
->format('Y-m-d H:i:s'),
]);
}
}
The up function of the migration:
public function up(): void
{
$this->migrator->add('announcement.content', '');
$this->migrator->add('announcement.showFrom');
$this->migrator->add('announcement.showUntil');
}
public function up(): void
{
$this->migrator->add('announcement.content', '');
$this->migrator->add('announcement.showFrom');
$this->migrator->add('announcement.showUntil');
}
Solution
awcodes
awcodes5mo ago
I think that the problem is that you are saying the property has to be a Carbon/DateTime instance but your trying to set it as a string. Easyist thing to do would probably be to convert the string to a carbon instance.
DateTimePicker::make('showFrom')
->label('Show From')
->prefixIcon('heroicon-o-calendar')
->seconds(false)
->native(false)
->format('Y-m-d H:i:s')
->dehydrateStateUsing(fn ($state) => Carbon::parse($state)),
DateTimePicker::make('showFrom')
->label('Show From')
->prefixIcon('heroicon-o-calendar')
->seconds(false)
->native(false)
->format('Y-m-d H:i:s')
->dehydrateStateUsing(fn ($state) => Carbon::parse($state)),
Anthaas
Anthaas5mo ago
Interesting. That's done it. I completely understand why that is the case - wonder if this could somehow be abstracted away with some sort of sensible default behaviour for the users. The underlying library suggests that this isn't necessary, so I guess its the filament package that needs this handling. Cheers!
awcodes
awcodes5mo ago
if you want to do it with the settings package you'll need to define the cast in the class.
public ?string $showFrom;

public static function casts(): array
{
return [
'showFrom' => DateTimeInterfaceCast::class
];
}
public ?string $showFrom;

public static function casts(): array
{
return [
'showFrom' => DateTimeInterfaceCast::class
];
}
Anthaas
Anthaas5mo ago
Ahhh, so the prop needs to be a string for that. Again - slightly at odds with the underlying package, but I get why. Wondering what the best approach is. Tempted to suggest the dehydrate method, because at least when you access the settings class at some other point the property is correctly typed.
awcodes
awcodes5mo ago
yea, this is a tricky situation because theres no real model for the settings, so you can't just do a normal cast
Anthaas
Anthaas5mo ago
Yeah - I think I'll go with the dehydrate method, that way I am only concerned with ever casting it between the two types when serialising/deserialising to and from the repository. Thanks again.