Last active
March 10, 2023 19:17
-
-
Save tanthammar/a012a42c87497fb256773a8ca3ceb199 to your computer and use it in GitHub Desktop.
DTO templates
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
If you're into | |
- Laravel, https://laravel.com/ | |
- Filament https://filamentphp.com/ | |
- DTOs or Json columns. | |
Some templates... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\DTO; | |
use App\Casts\DefaultCast; | |
use Filament\Forms\Components\KeyValue; | |
use Filament\Forms\Components\Repeater; | |
use Filament\Forms\Components\Section; | |
use JetBrains\PhpStorm\Pure; | |
use TantHammar\FilamentExtras\Forms\AddressSection; | |
class Address extends BaseDTO | |
{ | |
public ?string $label = ''; | |
public null|int|string $box = ''; //max:50|min:2 nullable | |
public ?string $street = ''; | |
public ?string $address_line_2 = ''; | |
public int|string|null $zip = ''; //string:25 nullable | |
public ?string $city = ''; | |
public ?string $county = ''; | |
public ?string $state = ''; | |
public string $country = 'Sverige'; //TODO translatable? | |
public string $country_code = 'SE'; //max:3 | |
public null|string|float $latitude = ''; //new Latitude rule | |
public null|string|float $longitude = ''; //new Longitude rule | |
public static function formSection(string $column, string|null $label = 'fields.address'): Repeater|Section|KeyValue | |
{ | |
return AddressSection::make(jsonColumnName: $column, label: $label); | |
} | |
public static function factory(): static | |
{ | |
return new static([ | |
'label' => fake()->word(), | |
'box' => fake()->postcode(), | |
'street' => fake()->streetName(), | |
'address_line_2' => fake()->streetName(), | |
'zip' => fake()->postcode(), | |
'city' => fake()->city(), | |
'county' => fake()->word(), | |
'state' => fake()->word(), | |
'country' => fake()->country(), | |
'country_code' => ctype_upper(fake()->countryCode()), | |
'latitude' => fake()->latitude(), | |
'longitude' => fake()->longitude(), | |
]); | |
} | |
#[Pure] | |
public static function castUsing(array $arguments): DefaultCast | |
{ | |
return new DefaultCast(new static); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\DTO; | |
use Filament\Forms\Components\KeyValue; | |
use Filament\Forms\Components\Repeater; | |
use Filament\Forms\Components\Section; | |
use Illuminate\Contracts\Database\Eloquent\CastsAttributes; | |
use Illuminate\Support\Collection; | |
use TantHammar\FilamentExtras\Forms\AddressFields; | |
class AddressList extends BaseDTO | |
{ | |
public static function formSection(string $column = 'addresses', ?string $label = 'fields.addresses'): Section|KeyValue | |
{ | |
return Section::make(__($label)) | |
->schema([ | |
Repeater::make($column) | |
->schema(AddressFields::make()) | |
->disableLabel() | |
->createItemButtonLabel(__('form.add').' '.__('fields.address')) | |
->columns()->columnSpan('full') | |
->minItems(0)->defaultItems(0), | |
])->columns(1) | |
->collapsible() | |
->columnSpan('full'); | |
} | |
public static function factory(): static|array | |
{ | |
return [ | |
Address::factory(), | |
Address::factory(), | |
]; | |
} | |
public static function castUsing(array $arguments): CastsAttributes | |
{ | |
return new class implements CastsAttributes | |
{ | |
public function get($model, $key, $value, $attributes): Collection | |
{ | |
if (blank($value)) { | |
return collect(); | |
} | |
return collect(json_decode($value, true, 4, JSON_THROW_ON_ERROR)) | |
->map(function ($address) { | |
return new Address($address); | |
}); | |
} | |
public function set($model, $key, $value, $attributes): bool|string|null | |
{ | |
if (blank($value)) { | |
return null; | |
} | |
return json_encode($value, JSON_THROW_ON_ERROR | 4); | |
} | |
}; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\DTO; | |
use Filament\Forms\Components\KeyValue; | |
use Filament\Forms\Components\Repeater; | |
use Filament\Forms\Components\Section; | |
use Illuminate\Contracts\Database\Eloquent\Castable; | |
use Illuminate\Contracts\Support\Arrayable; | |
use Illuminate\Contracts\Support\Jsonable; | |
use JsonException; | |
/** | |
* @method static fields(string $column) | |
*/ | |
abstract class BaseDTO implements Arrayable, Jsonable, Castable | |
{ | |
public function __construct(?array $attributes = []) | |
{ | |
if (is_array($attributes)) { | |
foreach ($attributes as $key => $value) { | |
if (property_exists($this, $key)) { | |
$this->$key = $value ?? $this->$key; | |
} | |
} | |
} | |
} | |
public function toArray(): array | |
{ | |
$array = []; | |
foreach (get_object_vars($this) as $key => $value) { | |
if ($value instanceof \BackedEnum) { | |
$array[$key] = $value->value; | |
continue; | |
} | |
if ($value instanceof Arrayable) { | |
$array[$key] = $value->toArray(); | |
continue; | |
} | |
$array[$key] = $value; | |
} | |
return $array; | |
} | |
/** | |
* @throws JsonException | |
*/ | |
public function toJson(mixed $options = 0): string | |
{ | |
return filled($arr = $this->toArray()) | |
? json_encode($arr, JSON_THROW_ON_ERROR | $options) ?? '{}' | |
: '{}'; | |
} | |
/** | |
* @throws JsonException | |
*/ | |
public static function fromJson(string $json, mixed $options = 0): static | |
{ | |
return new static( | |
filled($json) | |
? json_decode($json, true, $options, JSON_THROW_ON_ERROR) | |
: [] | |
); | |
} | |
/** | |
* Filament form section | |
*/ | |
abstract public static function formSection(string $column, ?string $label = null): Repeater|Section|KeyValue; | |
/** | |
* For Model factories | |
* | |
* @return BaseDTO|array | |
*/ | |
abstract public static function factory(): static|array; | |
public static function defaultFormSection(string $column, string $label): Section | |
{ | |
return Section::make(trans($label)) | |
->schema(static::fields($column)) | |
->columns([ | |
'default' => 2, | |
]) | |
->columnSpan('full') | |
->collapsible(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Casts; | |
use App\DTO\BaseDTO; | |
use Illuminate\Contracts\Database\Eloquent\CastsAttributes; | |
use JsonException; | |
class DefaultCast implements CastsAttributes | |
{ | |
public function __construct( | |
protected BaseDTO $DTO | |
) { | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @throws JsonException | |
*/ | |
public function get($model, string $key, $value, array $attributes) | |
{ | |
if (blank($value)) { | |
return $this->DTO; | |
} | |
return new $this->DTO(json_decode($value, true, 4, JSON_THROW_ON_ERROR) | |
); | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @throws JsonException | |
*/ | |
public function set($model, $key, $value, $attributes) | |
{ | |
if (blank($value)) { | |
return null; | |
} | |
return json_encode((array) $value, JSON_THROW_ON_ERROR, 4); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Enums; | |
use JetBrains\PhpStorm\Pure; | |
enum DueDaysFrom: string | |
{ | |
use EnumDefaults; | |
case invoiceDate = 'invoice_date'; | |
case beforeStart = 'before_start'; | |
public static function default(): self | |
{ | |
return self::beforeStart; | |
} | |
#[Pure] | |
public function label(): string | |
{ | |
return match ($this) { | |
self::invoiceDate => trans('fields.from_invoice_date'), | |
self::beforeStart => trans('fields.from_event_start_date'), | |
}; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Enums; | |
use JetBrains\PhpStorm\Pure; | |
/** | |
* @method static cases() | |
*/ | |
trait EnumDefaults | |
{ | |
abstract public static function default(): self; | |
#[Pure] | |
abstract public function label(): string; | |
public static function defaultValue(): string | |
{ | |
return self::default()->value; | |
} | |
/** Get an array with the enum values. */ | |
public static function values(): array | |
{ | |
return array_column(static::cases(), 'value'); | |
} | |
/** Get an array with the enum names. */ | |
public static function names(): array | |
{ | |
return array_column(static::cases(), 'name'); | |
} | |
/** Get an associative array of [case name => case value]. */ | |
public static function options(): array | |
{ | |
$cases = static::cases(); | |
$options = []; | |
foreach ($cases as $enum) { | |
$options[$enum->value] = $enum->label(); | |
} | |
return $options; | |
} | |
public function equals(self ...$others): bool | |
{ | |
foreach ($others as $other) { | |
if ($this->value === $other->value) { | |
return true; | |
} | |
} | |
return false; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\DTO; | |
use App\Casts\DefaultCast; | |
use App\Enums\DueDaysFrom; | |
use Filament\Forms\Components\KeyValue; | |
use Filament\Forms\Components\Section; | |
use Filament\Forms\Components\Select; | |
use Filament\Forms\Components\Textarea; | |
use Filament\Forms\Components\TextInput; | |
use Filament\Forms\Components\Toggle; | |
use Illuminate\Validation\Rule; | |
use JetBrains\PhpStorm\Pure; | |
use TantHammar\FilamentExtras\Actions\HelpModal; | |
class Invoicing extends BaseDTO | |
{ | |
public string|DueDaysFrom $calc_due_days_from = DueDaysFrom::invoiceDate; | |
public int $invoice_date_num_days = 15; | |
public int $before_start_num_days = 50; | |
public int $breakpoint = 20; | |
public bool $instant_payment = false; | |
public string $currency = 'SEK'; | |
//public string $currency_symbol = 'kr'; Akauning money adds the correct symbol when formatting the value | |
public ?string $invoice_note = null; | |
public ?string $cred_note = null; | |
/* Future features | |
public ?string $inv_prefix = null; | |
public ?string $inv_suffix = null; | |
public ?string $inv_counter = null; | |
public ?string $cred_prefix = null; | |
public ?string $cred_suffix = null; | |
public ?string $cred_counter = null; | |
*/ | |
public static function formSection(string $column = 'invoicing', ?string $label = 'fields.invoicing'): Section|KeyValue | |
{ | |
return self::defaultFormSection($column, $label)->collapsed(); | |
} | |
/** | |
* @throws \Exception | |
*/ | |
public static function fields(?string $column = null): array | |
{ | |
$column = $column && ! str_ends_with($column, '.') ? "$column." : $column; | |
return [ | |
Toggle::make($column.'instant_payment')->label(trans('fields.instant_payment')) | |
->hintAction(HelpModal::make('help.invoice-settings')) | |
->helperText(trans('fields.instant_payment_hint')) | |
->columnSpan(2) | |
->default(false) | |
->reactive() | |
->rules(['boolean', fn ($set, $get) => static function (string $attribute, $value, $fail) use ($set, $get, $column) { | |
if ($value && blank($get('stripe_connect_account_id'))) { | |
$set($column.'instant_payment', false); | |
$fail(__('fields.instant_payment_error')); | |
} | |
}]), | |
Select::make($column.'calc_due_days_from')->label(trans('fields.calc_due_days_from')) | |
->hintAction(HelpModal::make('help.invoice-settings')) | |
->helperText(trans('fields.calc_due_days_from_hint')) | |
->ruleInOptions() | |
->options(DueDaysFrom::options()) | |
->default(DueDaysFrom::defaultValue()) | |
->columnSpan(2) | |
->reactive() | |
->required() | |
->hiddenIfChecked($column.'instant_payment'), | |
TextInput::make($column.'invoice_date_num_days')->label(trans('fields.invoice_date_num_days')) | |
->hintAction(HelpModal::make('help.invoice-settings')) | |
->helperText(trans('fields.invoice_date_num_days_hint')) | |
->numeric()->minValue(3)->maxValue(365) | |
->required() | |
->rules('integer') | |
->columnSpan([ | |
'default' => 2, | |
'md' => 1, | |
]) | |
->default(15) | |
->hiddenIfChecked($column.'instant_payment') | |
->visible(fn ($get): bool => $get($column.'calc_due_days_from') === DueDaysFrom::invoiceDate->value), | |
TextInput::make($column.'before_start_num_days')->label(trans('fields.before_start_num_days')) | |
->hintAction(HelpModal::make('help.invoice-settings')) | |
->helperText(trans('fields.before_start_num_days_hint')) | |
->numeric()->minValue(0)->maxValue(365) | |
->required() | |
->rules('integer') | |
->columnSpan([ | |
'default' => 2, | |
'md' => 1, | |
]) | |
->default(50) | |
->hiddenIfChecked($column.'instant_payment') | |
->visible(fn ($get): bool => $get($column.'calc_due_days_from') === DueDaysFrom::beforeStart->value), | |
TextInput::make($column.'breakpoint')->label(trans('fields.breakpoint')) | |
->helperText(trans('fields.breakpoint_hint')) | |
->hintAction(HelpModal::make('help.invoice-settings')) | |
->numeric()->minValue(1)->maxValue(365) | |
->required() | |
->rules("sometimes|integer|lt:data.{$column}before_start_num_days") | |
->columnSpan([ | |
'default' => 2, | |
'md' => 1, | |
]) | |
->default(20) | |
->hiddenIfChecked($column.'instant_payment'), | |
Select::make($column.'currency')->label(trans('fields.currency')) | |
->helperText(trans('fields.currency_hint')) | |
->rule(Rule::in(['SEK', 'EUR', 'GBP', 'USD'])) | |
->options(['SEK' => 'SEK', 'EUR' => 'EUR', 'GBP' => 'GBP', 'USD' => 'USD']) | |
->default('SEK') | |
->columnSpan(2) | |
->required(), | |
//->onSave(fn ($set, $state) => $set($column."currency_symbol", self::getCurrencySymbol($state))), | |
Textarea::make($column.'invoice_note')->label(trans('fields.invoice_note')) | |
->helperText(trans('fields.invoice_note_hint')) | |
->nullable() | |
->columnSpan([ | |
'default' => 2, | |
'md' => 1, | |
]) | |
->maxLength(200), | |
Textarea::make($column.'cred_note')->label(trans('fields.cred_note')) | |
->helperText(trans('fields.cred_note_hint')) | |
->nullable() | |
->columnSpan([ | |
'default' => 2, | |
'md' => 1, | |
]) | |
->maxLength(200), | |
]; | |
} | |
protected static function getCurrencySymbol(string $code): int | |
{ | |
return match ($code) { | |
'EUR' => '€', | |
'GBP' => '£', | |
'USD' => '$', | |
default => 'kr', | |
}; | |
} | |
public static function factory(): static | |
{ | |
return new static([ | |
'instant_payment' => fake()->boolean(), | |
'calc_due_days_from' => DueDaysFrom::defaultValue(), | |
'invoice_date_num_days' => fake()->randomElement([10, 15, 30]), | |
'before_start_num_days' => fake()->randomElement([30, 40, 50, 60]), | |
'breakpoint' => fake()->randomElement([5, 10, 15, 20, 30]), | |
'currency' => 'SEK', | |
//'currency_symbol' => 'kr', | |
'invoice_note' => fake()->text(150), | |
'cred_note' => fake()->text(150), | |
]); | |
} | |
#[Pure] | |
public static function castUsing(array $arguments): DefaultCast | |
{ | |
return new DefaultCast(new static); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\DTO; | |
use Brick\PhoneNumber\PhoneNumberParseException; | |
use Filament\Forms\Components\KeyValue; | |
use Filament\Forms\Components\Repeater; | |
use Filament\Forms\Components\Section; | |
use Illuminate\Contracts\Database\Eloquent\CastsAttributes; | |
use Illuminate\Support\Collection; | |
class People extends BaseDTO | |
{ | |
/** | |
* {@inheritDoc} | |
*/ | |
public static function formSection(string $column = 'people', ?string $label = 'fields.people'): Section|KeyValue | |
{ | |
return Section::make(__($label)) | |
->schema([Repeater::make($column) | |
->schema(Person::fields()) | |
->disableLabel() | |
->createItemButtonLabel(__('form.add').' '.__('fields.contact')) | |
->columns()->columnSpan(2) | |
->minItems(0)->defaultItems(0), | |
])->columns(1)->collapsible()->columnSpan('full'); | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @throws PhoneNumberParseException | |
*/ | |
public static function factory(): static|array | |
{ | |
return [ | |
Person::factory(), | |
Person::factory(), | |
]; | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
public static function castUsing(array $arguments): CastsAttributes | |
{ | |
return new class implements CastsAttributes | |
{ | |
public function get($model, $key, $value, $attributes): Collection | |
{ | |
if (blank($value)) { | |
return collect(); | |
} | |
return collect(json_decode($value, true, 4, JSON_THROW_ON_ERROR)) | |
->map(function ($person) { | |
return new Person($person); | |
}); | |
} | |
public function set($model, $key, $value, $attributes): bool|string|null | |
{ | |
if (blank($value)) { | |
return null; | |
} | |
return json_encode($value, JSON_THROW_ON_ERROR | 4); | |
} | |
}; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\DTO; | |
use Brick\PhoneNumber\PhoneNumber; | |
use Brick\PhoneNumber\PhoneNumberFormat; | |
use Brick\PhoneNumber\PhoneNumberParseException; | |
use Filament\Forms\Components\Checkbox; | |
use Filament\Forms\Components\KeyValue; | |
use Filament\Forms\Components\Section; | |
use Filament\Forms\Components\TextInput; | |
use Illuminate\Contracts\Database\Eloquent\CastsAttributes; | |
use TantHammar\FilamentExtras\Forms\Email; | |
use TantHammar\FilamentExtras\Forms\FirstName; | |
use TantHammar\FilamentExtras\Forms\LandlineIntlTel; | |
use TantHammar\FilamentExtras\Forms\LastName; | |
use TantHammar\FilamentExtras\Forms\MobileIntlTel; | |
use TantHammar\LaravelRules\Factories\FakeMobileNumber; | |
use TantHammar\LaravelRules\Factories\FakePhoneNumber; | |
class Person extends BaseDTO | |
{ | |
public ?string $first_name = null; | |
public ?string $last_name = null; | |
public ?string $title = null; | |
public ?string $email = null; | |
public bool $bounced = false; //TODO add functionality for bounced emails | |
public null|int|string $mobile = null; | |
public null|int|string $phone = null; | |
//public null|int|string $mobile_formatted = null; //set in castUsing() not needed as we are using PhoneInput | |
//public null|int|string $phone_formatted = null; //set in castUsing() not needed as we are using PhoneInput | |
public static function formSection(?string $column = null, ?string $label = 'field-labels.primary-contact'): Section|KeyValue | |
{ | |
return self::defaultFormSection($column, $label)->columns([ | |
'default' => 1, | |
'sm' => 1, | |
'md' => 2, | |
]); | |
} | |
/** | |
* @throws PhoneNumberParseException | |
*/ | |
public static function factory(): static | |
{ | |
return new static(self::formatPhoneNumbers([ | |
'first_name' => fake()->firstName(), | |
'last_name' => fake()->lastName(), | |
'title' => fake()->title(), | |
'email' => fake()->safeEmail(), | |
'bounced' => false, | |
'mobile' => FakeMobileNumber::make(), | |
'phone' => FakePhoneNumber::make(), | |
])); | |
} | |
public static function castUsing(array $arguments): CastsAttributes | |
{ | |
return new class implements CastsAttributes | |
{ | |
public function get($model, $key, $value, $attributes): Person | |
{ | |
if (blank($value)) { | |
return new Person(); | |
} | |
return new Person(json_decode($value, true, 3, JSON_THROW_ON_ERROR) | |
); | |
} | |
public function set($model, $key, $value, $attributes): bool|string|null | |
{ | |
if (blank($value)) { | |
return null; | |
} | |
//$value = Person::formatPhoneNumbers($value); | |
return json_encode((array) $value, JSON_THROW_ON_ERROR | 2); | |
} | |
}; | |
} | |
/** | |
* @throws PhoneNumberParseException | |
*/ | |
public static function formatPhoneNumbers(Person|array $value): Person|array | |
{ | |
data_set($value, 'mobile_formatted', | |
value: ($nr = data_get($value, 'mobile')) | |
? PhoneNumber::parse((str_starts_with($nr, '+') ? $nr : '+'.$nr))->format(PhoneNumberFormat::INTERNATIONAL) | |
: null, | |
overwrite: ''); | |
data_set($value, 'phone_formatted', | |
value: ($nr = data_get($value, 'phone')) | |
? PhoneNumber::parse((str_starts_with($nr, '+') ? $nr : '+'.$nr))->format(PhoneNumberFormat::INTERNATIONAL) | |
: null, | |
overwrite: ''); | |
return $value; | |
} | |
public static function fields(?string $column = null): array | |
{ | |
$column = $column && ! str_ends_with($column, '.') ? "$column." : $column; | |
return [ | |
FirstName::make(column: $column.'first_name'), | |
LastName::make(column: $column.'last_name'), | |
TextInput::make($column.'title')->nullable()->rules('alpha_space')->minLength(2)->maxLength(125), | |
Email::make(column: $column.'email', unique: false), | |
MobileIntlTel::make($column.'mobile') | |
->lazy() | |
->requiredIfBlank(field: $column.'phone') | |
->nullableIfFilled(field: $column.'phone') | |
->onUpdated(function ($livewire, $component): void { | |
$livewire->validateOnly($component->getStatePath()); | |
}), | |
LandlineIntlTel::make($column.'phone') | |
->lazy() | |
->requiredIfBlank(field: $column.'mobile') | |
->nullableIfFilled(field: $column.'mobile') | |
->onUpdated(function ($livewire, $component): void { | |
$livewire->validateOnly($component->getStatePath()); | |
}), | |
Checkbox::make($column.'bounced')->default(false)->hidden(! user()->isSuperAdmin()), //Checkbox has rule('boolean') as default, TODO add support for bounced emails later | |
]; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\DTO; | |
use App\Enums\PhoneType; | |
use Brick\PhoneNumber\PhoneNumber; | |
use Brick\PhoneNumber\PhoneNumberFormat; | |
use Brick\PhoneNumber\PhoneNumberParseException; | |
use Filament\Forms\Components\Component; | |
use Filament\Forms\Components\KeyValue; | |
use Filament\Forms\Components\Section; | |
use Filament\Forms\Components\TextInput; | |
use Illuminate\Contracts\Database\Eloquent\CastsAttributes; | |
use Illuminate\Validation\Rule; | |
use TantHammar\FilamentExtras\Forms\HiddenOrSelect; | |
use TantHammar\FilamentExtras\Forms\PhoneIntlTel; | |
use TantHammar\LaravelRules\Factories\FakePhoneNumber; | |
use TantHammar\LaravelRules\Rules\MobileNumber; | |
class Phone extends BaseDTO | |
{ | |
public ?string $label = null; | |
public null|int|string $phone = null; | |
//public null|int|string $phone_formatted = null; //set in castUsing(), not needed since we use PhoneIntlTel | |
public string|PhoneType $type = PhoneType::undefined; | |
public static function formSection(string $column = 'phone', ?string $label = 'fields.phone'): Section|KeyValue | |
{ | |
return self::defaultFormSection($column, $label); | |
} | |
public static function fields(?string $column = null): array | |
{ | |
$column = $column && ! str_ends_with($column, '.') ? "$column." : $column; | |
return [ | |
TextInput::make($column.'label') | |
->label(__('fields.phones_label')) | |
->helperText(__('fields.phones_hint')) | |
->prefixIcon('heroicon-o-office-building') | |
->required(), | |
PhoneIntlTel::make($column.'phone') | |
->required() | |
->lazy() | |
->onUpdated(function ($livewire, Component $component, $state, $set) use ($column) { | |
$livewire->validateOnly($component->getStatePath()); | |
$set($column.'type', ((new MobileNumber)->passes(null, $state) ? PhoneType::mobile : PhoneType::landline)); | |
}), | |
HiddenOrSelect::make( | |
user()->isSupport(), | |
$column.'type', | |
'fields.phones_type', | |
rule: [Rule::in(PhoneType::values())], | |
options: PhoneType::options() | |
)->default(PhoneType::undefined)->required(), | |
]; | |
} | |
public static function factory(): static | |
{ | |
return new static([ | |
'label' => fake()->randomElement(['HQ', 'Bookings', 'Invoicing', 'Office', 'Home']), | |
'phone' => FakePhoneNumber::make(), | |
'type' => PhoneType::default(), | |
]); | |
} | |
public static function castUsing(array $arguments): CastsAttributes | |
{ | |
return new class implements CastsAttributes | |
{ | |
public function get($model, $key, $value, $attributes): Phone | |
{ | |
if (blank($value)) { | |
return new Phone(); | |
} | |
return new Phone(json_decode($value, true, 2, JSON_THROW_ON_ERROR) | |
); | |
} | |
public function set($model, $key, $value, $attributes): bool|string|null | |
{ | |
if (blank($value)) { | |
return null; | |
} | |
//$value = Phone::formatPhoneNumber($value); | |
$value = Phone::setPhoneType($value); | |
return json_encode((array) $value, JSON_THROW_ON_ERROR | 3); | |
} | |
}; | |
} | |
/** | |
* @throws PhoneNumberParseException | |
*/ | |
public static function formatPhoneNumber(Phone|array $value): Phone|array | |
{ | |
data_set($value, 'phone_formatted', | |
value: ($nr = data_get($value, 'phone')) | |
? PhoneNumber::parse((str_starts_with($nr, '+') ? $nr : '+'.$nr))->format(PhoneNumberFormat::INTERNATIONAL) | |
: null, | |
overwrite: ''); | |
return $value; | |
} | |
public static function setPhoneType(Phone|array $value): Phone|array | |
{ | |
data_set($value, 'type', | |
value: ((new MobileNumber)->passes(null, data_get($value, 'phone')) ? PhoneType::mobile->value : PhoneType::landline->value), | |
overwrite: PhoneType::undefined->value); | |
return $value; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\DTO; | |
use Filament\Forms\Components\KeyValue; | |
use Filament\Forms\Components\Repeater; | |
use Filament\Forms\Components\Section; | |
use Illuminate\Contracts\Database\Eloquent\CastsAttributes; | |
use Illuminate\Support\Collection; | |
class PhonesList extends BaseDTO | |
{ | |
/** | |
* {@inheritDoc} | |
*/ | |
public static function formSection(string $column = 'phones', ?string $label = 'fields.phones'): Section|KeyValue | |
{ | |
return Section::make(trans($label)) | |
->schema([ | |
Repeater::make($column) | |
->schema(Phone::fields()) | |
->disableLabel() | |
->createItemButtonLabel(__('form.add').' '.__('fields.phone')) | |
->columns(user()->isSupport() ? 3 : 2)->columnSpan('full') | |
->minItems(0)->defaultItems(0), | |
]) | |
->columns(1) | |
->columnSpan('full') | |
->collapsible(); | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
public static function factory(): static|array | |
{ | |
return [ | |
Phone::factory(), | |
Phone::factory(), | |
]; | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
public static function castUsing(array $arguments): CastsAttributes | |
{ | |
return new class implements CastsAttributes | |
{ | |
public function get($model, $key, $value, $attributes): Collection | |
{ | |
if (blank($value)) { | |
return collect(); | |
} | |
return collect(json_decode($value, true, 4, JSON_THROW_ON_ERROR)) | |
->map(function ($phone) { | |
return new Phone($phone); | |
}); | |
} | |
public function set($model, $key, $value, $attributes): bool|string|null | |
{ | |
if (blank($value)) { | |
return null; | |
} | |
return json_encode($value, JSON_THROW_ON_ERROR | 4); | |
} | |
}; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Enums; | |
use JetBrains\PhpStorm\Pure; | |
enum PhoneType: string | |
{ | |
use EnumDefaults; | |
case landline = 'landline'; | |
case mobile = 'mobile'; | |
case undefined = 'undefined'; | |
#[Pure] | |
public function label(): string | |
{ | |
return match ($this) { | |
self::landline => trans('fields.landline'), | |
self::mobile => trans('fields.mobile'), | |
self::undefined => trans('fields.undefined'), | |
}; | |
} | |
public static function default(): self | |
{ | |
return self::undefined; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\DTO; | |
use App\Casts\DefaultCast; | |
use App\Enums\UpfrontMethod; | |
use App\Enums\UpfrontType; | |
use Filament\Forms\Components\KeyValue; | |
use Filament\Forms\Components\Placeholder; | |
use Filament\Forms\Components\Radio; | |
use Filament\Forms\Components\Section; | |
use Filament\Forms\Components\Textarea; | |
use Filament\Forms\Components\TextInput; | |
use Illuminate\Validation\Rule; | |
use JetBrains\PhpStorm\ArrayShape; | |
use JetBrains\PhpStorm\Pure; | |
class Upfront extends BaseDTO | |
{ | |
public string|UpfrontMethod $upfrontMethod = UpfrontMethod::sometimes; | |
public string|UpfrontType $upfrontType = UpfrontType::fixed; | |
public int $upfrontFixedAmount = 200; | |
public int $upfrontPercent = 10; | |
public array $upfrontName = []; // ['en' => 'Upfront', 'sv' => 'Förskott']; | |
public array $upfrontMsg = []; | |
#[Pure] | |
public function __construct(?array $attributes = []) | |
{ | |
parent::__construct($attributes); | |
$this->upfrontName = $this->upfrontName === [] ? self::name() : $this->upfrontName; | |
$this->upfrontMsg = $this->upfrontMsg === [] ? self::messages() : $this->upfrontMsg; | |
} | |
#[ArrayShape(['en' => 'string', 'sv' => 'string'])] | |
public static function messages(): array | |
{ | |
return [ | |
'en' => 'We need to ask you for an upfront payment to minimise the problem with no shows. The amount will be withdrawn from you final invoice. If you cancel within allowed period, (see our booking terms) you will get a full refund. We will handle your booking as soon as we receive your payment.', | |
'sv' => 'Vi har valt att tillämpa delbetalning för att minska antalet utställare som bokar en plats men aldrig dyker upp. Detta medför problem i vår planering och oönskade luckor på marknaden vilket inte är bra för vare sig er eller marknadens besökare. Genom att betala garanteras du en plats på marknaden. När din bokning har tilldelats en plats kommer ev. resterande belopp att faktureras. Vid godkänd avbokning enligt marknadens bokningsvillkor, återbetalas hela beloppet. Vi behandlar din bokning så snart vi mottagit din delbetalning.', | |
]; | |
} | |
#[ArrayShape(['en' => 'string', 'sv' => 'string'])] | |
public static function name(): array | |
{ | |
return [ | |
'en' => 'Upfront payment', | |
'sv' => 'Delbetalning', | |
]; | |
} | |
public static function formSection(string $column = 'upfront', ?string $label = 'fields.upfront'): Section|KeyValue | |
{ | |
return self::defaultFormSection($column, $label)->collapsed(); | |
} | |
/** | |
* @throws \Exception | |
*/ | |
public static function fields(?string $column = null): array | |
{ | |
$column = $column && ! str_ends_with($column, '.') ? $column.'' : $column; | |
return [ | |
Radio::make($column.'upfrontMethod')->label(trans('fields.upfront_method')) | |
->helperText(trans('fields.upfront_method_hint')) | |
->columnSpan('full') | |
->default(UpfrontMethod::defaultValue()) | |
->options(UpfrontMethod::options()) | |
->required() | |
->inline() | |
->rules([Rule::in(UpfrontMethod::values())]), | |
Radio::make($column.'upfrontType')->label(trans('fields.upfront_type')) | |
->rule(Rule::in(UpfrontType::values())) | |
->default(UpfrontType::defaultValue()) | |
->options(UpfrontType::options()) | |
->descriptions([ | |
UpfrontType::fixed->value => trans('fields.upfront_fixed_hint'), | |
UpfrontType::percent->value => trans('fields.upfront_percent_hint'), | |
]) | |
->columnSpan([ | |
'default' => 2, | |
'md' => 1, | |
]) | |
->reactive() | |
->required(), | |
TextInput::make($column.'upfrontFixedAmount')->label(trans('fields.upfront_fixed')) | |
->prefixIcon('sui-coins') | |
->numeric()->minValue(10)->maxValue(100000) | |
->extraInputAttributes(['min' => 10, 'max' => 100000]) | |
->step(5) | |
->required() | |
->rules('integer') | |
->columnSpan([ | |
'default' => 2, | |
'md' => 1, | |
]) | |
->default(200) | |
->visible(fn ($get): bool => $get($column.'upfrontType') === UpfrontType::fixed->value), | |
TextInput::make($column.'upfrontPercent')->label(trans('fields.upfront_percent')) | |
->prefixIcon('carbon-percentage') | |
->numeric()->minValue(5)->maxValue(100) | |
->extraInputAttributes(['min' => 5, 'max' => 100]) | |
->step(5) | |
->required() | |
->rules('integer') | |
->columnSpan([ | |
'default' => 2, | |
'md' => 1, | |
]) | |
->default(10) | |
->visible(fn ($get): bool => $get($column.'upfrontType') === UpfrontType::percent->value), | |
Placeholder::make(trans('fields.upfront')) | |
->content(trans('fields.upfront_name_hint')) | |
->columnSpan('full'), | |
TextInput::make($column.'upfrontName.sv')->label(trans('glossary.swedish')) | |
->required() | |
->columnSpan([ | |
'default' => 2, | |
'md' => 1, | |
]) | |
->default(self::name()['sv']) | |
->minLength(10) | |
->maxLength(100), | |
TextInput::make($column.'upfrontName.en')->label(trans('glossary.english')) | |
->required() | |
->columnSpan([ | |
'default' => 2, | |
'md' => 1, | |
]) | |
->default(self::name()['en']) | |
->minLength(10) | |
->maxLength(100), | |
Placeholder::make(trans('fields.message')) | |
->content(trans('fields.upfront_msg_hint')) | |
->columnSpan('full'), | |
Textarea::make($column.'upfrontMsg.sv')->label(trans('glossary.swedish')) | |
->required() | |
->columnSpan([ | |
'default' => 2, | |
'md' => 1, | |
]) | |
->rows(8) | |
->default(self::messages()['sv']) | |
->minLength(20) | |
->maxLength(200), | |
Textarea::make($column.'upfrontMsg.en')->label(trans('glossary.english')) | |
->required() | |
->columnSpan([ | |
'default' => 2, | |
'md' => 1, | |
]) | |
->rows(8) | |
->default(self::messages()['en']) | |
->minLength(20) | |
->maxLength(200), | |
]; | |
} | |
public static function factory(): static | |
{ | |
return new static([ | |
'upfrontMethod' => UpfrontMethod::defaultValue(), | |
'upfrontType' => UpfrontType::defaultValue(), | |
'upfrontFixedAmount' => fake()->randomElement([100, 200, 300]), | |
'upfrontPercent' => fake()->randomElement([10, 15, 20]), | |
'upfrontName' => self::name(), | |
'upfrontMsg' => self::messages(), | |
]); | |
} | |
#[Pure] | |
public static function castUsing(array $arguments): DefaultCast | |
{ | |
return new DefaultCast(new static); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Enums; | |
use JetBrains\PhpStorm\Pure; | |
enum UpfrontMethod: string | |
{ | |
use EnumDefaults; | |
case sometimes = 'sometimes'; | |
case always = 'always'; | |
public static function default(): self | |
{ | |
return self::sometimes; | |
} | |
#[Pure] | |
public function label(): string | |
{ | |
return match ($this) { | |
self::sometimes => trans('fields.sometimes'), | |
self::always => trans('fields.always'), | |
}; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Enums; | |
use JetBrains\PhpStorm\Pure; | |
enum UpfrontType: string | |
{ | |
use EnumDefaults; | |
case fixed = 'fixed'; | |
case percent = 'percent'; | |
public static function default(): self | |
{ | |
return self::fixed; | |
} | |
#[Pure] | |
public function label(): string | |
{ | |
return match ($this) { | |
self::fixed => trans('fields.upfront_fixed'), | |
self::percent => trans('fields.upfront_percent'), | |
}; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment