Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save tanthammar/a012a42c87497fb256773a8ca3ceb199 to your computer and use it in GitHub Desktop.
Save tanthammar/a012a42c87497fb256773a8ca3ceb199 to your computer and use it in GitHub Desktop.
DTO templates
If you're into
- Laravel, https://laravel.com/
- Filament https://filamentphp.com/
- DTOs or Json columns.
Some templates...
<?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);
}
}
<?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);
}
};
}
}
<?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();
}
}
<?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);
}
}
<?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'),
};
}
}
<?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;
}
}
<?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);
}
}
<?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);
}
};
}
}
<?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
];
}
}
<?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;
}
}
<?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);
}
};
}
}
<?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;
}
}
<?php
namespace App\DTO;
use App\Casts\DefaultCast;
use App\Enums\SocialChannel;
use Closure;
use Exception;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Illuminate\Validation\Rule;
use JetBrains\PhpStorm\Pure;
class Social extends BaseDTO
{
public ?string $channel = null;
public ?string $url = null;
public static function formSection(string $column = 'social', ?string $label = 'fields.phone'): Section|KeyValue
{
return self::defaultFormSection($column, $label);
}
public static function factory(): static
{
$random = fake()->randomElement(SocialChannel::values());
return new static([
'channel' => $random,
'url' => SocialChannel::from($random)->urlStartsWith().fake()->word(),
]);
}
#[Pure]
public static function castUsing(array $arguments): DefaultCast
{
return new DefaultCast(new static);
}
public static function fields(?string $column = null): array
{
$column = $column && ! str_ends_with($column, '.') ? "$column." : $column;
return [
Select::make($column.'channel')
->label(__('fields.channel'))
->options(SocialChannel::options())
->default(SocialChannel::default()->value)
->rules([Rule::in(SocialChannel::values())])
->required()
->lazy(),
TextInput::make($column.'url')
->url()
->required()
->rules([
function (Closure $get) use ($column) {
return function ($attribute, $value, Closure $fail) use ($column, $get) {
try {
$startWith = SocialChannel::from($get($column.'channel'))->urlStartsWith();
if (! str_starts_with($value, $startWith)) {
$fail(trans('validation.url_start_with', ['attribute' => 'url', 'value' => $startWith]));
}
} catch (Exception) {
$fail(trans('validation.url', ['attribute' => 'url']));
}
};
},
])
->prefixIcon(fn ($get): string => SocialChannel::from($get($column.'channel'))->icon()),
];
}
}
<?php
namespace App\Enums;
use JetBrains\PhpStorm\Pure;
enum SocialChannel: string
{
use EnumDefaults;
case website = 'website';
case facebook = 'facebook';
case twitter = 'twitter';
case youtube = 'youtube';
case instagram = 'instagram';
case linkedin = 'linkedin';
case vimeo = 'vimeo';
case other = 'other';
public static function default(): self
{
return self::website;
}
#[Pure]
public function label(): string
{
return match ($this) {
self::website => trans('fields.website'),
self::facebook => 'Facebook',
self::twitter => 'Twitter',
self::youtube => 'Youtube',
self::instagram => 'Instagram',
self::linkedin => 'LinkedIn',
self::vimeo => 'Vimeo',
self::other => trans('fields.other'),
};
}
/** returns dynamic blade icon name */
#[Pure]
public function icon(): string
{ //fontawesome icons are copied to resources/svg
return match ($this) {
self::website => 'heroicon-o-globe-alt',
self::facebook => 'fab-facebook-square',
self::twitter => 'fab-twitter-square',
self::youtube => 'fab-youtube',
self::instagram => 'fab-instagram',
self::linkedin => 'fab-linkedin',
self::vimeo => 'fab-vimeo-v',
self::other => 'heroicon-o-external-link',
};
}
public function urlStartsWith(): string
{
return match ($this) {
self::website, self::other => 'https://',
self::facebook => 'https://www.facebook.com/',
self::twitter => 'https://twitter.com/',
self::youtube => 'https://www.youtube.com/',
self::instagram => 'https://www.instagram.com/',
self::linkedin => 'https://www.linkedin.com/',
self::vimeo => 'https://vimeo.com/',
};
}
}
<?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 SocialsList extends BaseDTO
{
/**
* {@inheritDoc}
*/
public static function formSection(string $column = 'socials', ?string $label = 'fields.socials'): Section|KeyValue
{
return Section::make(trans($label))
->schema([
Repeater::make($column)
->schema(Social::fields())
->disableLabel()
->createItemButtonLabel(__('form.add').' '.__('fields.link'))
->columns()->columnSpan('full')
->minItems(0)->defaultItems(0),
])
->columns(1)
->columnSpan('full')
->collapsible()
->collapsed();
}
/**
* {@inheritDoc}
*/
public static function factory(): static|array
{
return [
Social::factory(),
Social::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, 2, JSON_THROW_ON_ERROR))
->map(function ($social) {
return new Social($social);
});
}
public function set($model, $key, $value, $attributes): bool|string|null
{
if (blank($value)) {
return null;
}
return json_encode($value, JSON_THROW_ON_ERROR | 2);
}
};
}
}
<?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);
}
}
<?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'),
};
}
}
<?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