Add data objects for form

This commit is contained in:
philipp lang 2024-03-07 00:58:14 +01:00
parent 6ac539417f
commit c844f2143a
22 changed files with 253 additions and 83 deletions

View File

@ -22,7 +22,6 @@ trait HasValidation
'config.sections.*.fields.*.name' => 'required|string',
'config.sections.*.fields.*.type' => ['required', 'string', Rule::in(array_column(Field::asMeta(), 'id'))],
'config.sections.*.fields.*.key' => ['required', 'string', 'regex:/^[a-zA-Z_]*$/'],
'config.sections.*.fields.*.default' => 'present',
'config.sections.*.fields.*.columns' => 'required|array',
'config.sections.*.fields.*.*' => '',
'config.sections.*.fields.*.columns.mobile' => 'required|numeric|gt:0|lte:2',
@ -41,7 +40,6 @@ trait HasValidation
'config.sections.*.fields.*.name' => 'Feldname',
'config.sections.*.fields.*.type' => 'Feldtyp',
'config.sections.*.fields.*.key' => 'Feldkey',
'config.sections.*.fields.*.default' => 'Standardwert',
];
}

View File

@ -22,7 +22,7 @@ class RegisterAction
'data' => $input
]);
$form->getFields()->each(fn ($field) => Field::fromConfig($field)->afterRegistration($form, $participant, $input));
$form->getFields()->each(fn ($field) => $field->afterRegistration($form, $participant, $input));
return $participant;
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Form\Casts;
use App\Form\Fields\Field;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Casts\Cast;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Support\DataProperty;
class CollectionCast implements Cast
{
/**
* @param class-string<Data> $target
*/
public function __construct(public string $target)
{
}
/**
* @param array<int, array<string, mixed>> $value
* @param array<string, mixed> $context
* @return Collection<int, Data>
*/
public function cast(DataProperty $property, mixed $value, array $context): mixed
{
return collect($value)->map(fn ($item) => $this->target::from($item));
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Form\Casts;
use App\Form\Data\FieldCollection;
use App\Form\Fields\Field;
use Spatie\LaravelData\Casts\Cast;
use Spatie\LaravelData\Support\DataProperty;
class FieldCollectionCast implements Cast
{
/**
* @param array<int, array<string, mixed>> $value
* @param array<string, mixed> $context
* @return FieldCollection
*/
public function cast(DataProperty $property, mixed $value, array $context): mixed
{
return new FieldCollection(collect($value)->map(fn ($value) => Field::classFromType($value['type'])::from($value))->all());
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Form\Data;
use Spatie\LaravelData\Data;
class ColumnData extends Data
{
public function __construct(
public int $mobile,
public int $tablet,
public int $desktop,
) {
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Form\Data;
use App\Form\Fields\Field;
use App\Form\Fields\NamiField;
use Illuminate\Support\Collection;
/**
* @extends Collection<int, Field>
*/
class FieldCollection extends Collection
{
public function forMembers(): self
{
return $this->filter(fn ($field) => $field->forMembers === true);
}
public function noNamiType(): self
{
return $this->filter(fn ($field) => $field->namiType === null);
}
public function noNamiField(): self
{
return $this->filter(fn ($field) => !is_a($field, NamiField::class));
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Form\Data;
use App\Form\Casts\CollectionCast;
use App\Form\Transformers\CollectionTransformer;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Attributes\WithTransformer;
class FormConfigData extends Data
{
/**
* @param Collection<int, SectionData> $sections
*/
public function __construct(
#[WithCast(CollectionCast::class, target: SectionData::class)]
#[WithTransformer(CollectionTransformer::class, target: SectionData::class)]
public Collection $sections
) {
}
public function fields(): FieldCollection
{
return $this->sections->reduce(
fn ($carry, $current) => $carry->merge($current->fields->all()),
new FieldCollection([])
);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Form\Data;
use App\Form\Casts\FieldCollectionCast;
use Spatie\LaravelData\Data;
use App\Form\Transformers\FieldCollectionTransformer;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Attributes\WithTransformer;
class SectionData extends Data
{
public function __construct(
public string $name,
#[WithCast(FieldCollectionCast::class)]
#[WithTransformer(FieldCollectionTransformer::class)]
public FieldCollection $fields
) {
}
}

View File

@ -2,6 +2,7 @@
namespace App\Form\Fields;
use App\Form\Data\ColumnData;
use App\Form\Enums\NamiType;
use App\Form\Models\Form;
use App\Form\Models\Participant;
@ -10,15 +11,19 @@ use App\Form\Presenters\Presenter;
use Faker\Generator;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
abstract class Field extends Data
{
public string $key;
public string $name;
public ?NamiType $namiType = null;
public ColumnData $columns;
public bool $forMembers;
/**
* @param array<array-key, mixed> $input
@ -76,14 +81,6 @@ abstract class Field extends Data
return $fieldClass;
}
/**
* @param array<string, mixed> $config
*/
public static function fromConfig(array $config): static
{
return static::classFromType($config['type'])::withoutMagicalCreationFrom($config);
}
/**
* @param mixed $value
* @return mixed

View File

@ -42,11 +42,9 @@ class NamiField extends Field
{
$rules = [$this->key => 'present|array'];
$c = $form->getFields()
->filter(fn ($field) => $field['for_members'] === true)
->filter(fn ($field) => $field['nami_type'] === null)
->filter(fn ($field) => $field['type'] !== class_basename(static::class))
->map(fn ($field) => Field::fromConfig($field)->getRegistrationRules($form));
$c = $form->getFields()->forMembers()->noNamiType()->noNamiField()
->map(fn ($field) => $field->getRegistrationRules($form))
->toArray();
foreach ($c as $field) {
foreach ($field as $ruleKey => $rule) {
@ -72,10 +70,7 @@ class NamiField extends Field
return [];
}
$c = $form->getFields()
->filter(fn ($field) => $field['type'] !== class_basename(static::class))
->filter(fn ($field) => $field['for_members'] === true)
->map(fn ($field) => Field::fromConfig($field));
$c = $form->getFields()->noNamiField()->forMembers();
foreach ($c as $field) {
foreach ($field->getRegistrationRules($form) as $ruleKey => $rule) {
@ -118,8 +113,6 @@ class NamiField extends Field
$member = Member::firstWhere(['mitgliedsnr' => $memberData['id']]);
$data = [];
foreach ($form->getFields() as $field) {
$field = Field::fromConfig($field);
$data[$field->key] = $field->namiType === null
? data_get($memberData, $field->key, $field->default())
: $field->namiType->getMemberAttribute($member);

View File

@ -9,6 +9,7 @@ use Faker\Generator;
class TextareaField extends Field
{
public bool $required;
public int $rows;
public static function name(): string
{

View File

@ -2,6 +2,8 @@
namespace App\Form\Models;
use App\Form\Data\FieldCollection;
use App\Form\Data\FormConfigData;
use App\Form\Fields\Field;
use Cviebrock\EloquentSluggable\Sluggable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -27,7 +29,7 @@ class Form extends Model implements HasMedia
public $guarded = [];
public $casts = [
'config' => 'json',
'config' => FormConfigData::class,
'meta' => 'json',
'description' => 'json',
];
@ -70,14 +72,10 @@ class Form extends Model implements HasMedia
*/
public function getRegistrationRules(): array
{
return $this->getFields()->reduce(function ($carry, $current) {
$field = Field::fromConfig($current);
return [
...$carry,
...$field->getRegistrationRules($this),
];
}, []);
return $this->getFields()->reduce(fn ($carry, $field) => [
...$carry,
...$field->getRegistrationRules($this),
], []);
}
/**
@ -85,14 +83,10 @@ class Form extends Model implements HasMedia
*/
public function getRegistrationMessages(): array
{
return $this->getFields()->reduce(function ($carry, $current) {
$field = Field::fromConfig($current);
return [
...$carry,
...$field->getRegistrationMessages($this),
];
}, []);
return $this->getFields()->reduce(fn ($carry, $field) => [
...$carry,
...$field->getRegistrationMessages($this),
], []);
}
/**
@ -100,22 +94,15 @@ class Form extends Model implements HasMedia
*/
public function getRegistrationAttributes(): array
{
return $this->getFields()->reduce(function ($carry, $current) {
$field = Field::fromConfig($current);
return [
...$carry,
...$field->getRegistrationAttributes($this),
];
}, []);
return $this->getFields()->reduce(fn ($carry, $field) => [
...$carry,
...$field->getRegistrationAttributes($this),
], []);
}
/**
* @return Collection<string, mixed>
*/
public function getFields(): Collection
public function getFields(): FieldCollection
{
return collect($this->config['sections'])->reduce(fn ($carry, $current) => $carry->merge($current['fields']), collect([]));
return $this->config->fields();
}
@ -142,7 +129,7 @@ class Form extends Model implements HasMedia
if (is_null($model->meta)) {
$model->setAttribute('meta', [
'active_columns' => $model->getFields()->count() ? $model->getFields()->take(4)->pluck('key')->toArray() : null,
'sorting' => $model->getFields()->count() ? [$model->getFields()->first()['key'], 'asc'] : null,
'sorting' => $model->getFields()->count() ? [$model->getFields()->first()->key, 'asc'] : null,
]);
}

View File

@ -2,9 +2,13 @@
namespace App\Form\Models;
use App\Form\Data\FormConfigData;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property FormConfigData $config
*/
class Formtemplate extends Model
{
use HasFactory;
@ -12,6 +16,6 @@ class Formtemplate extends Model
public $guarded = [];
public $casts = [
'config' => 'json',
'config' => FormConfigData::class,
];
}

View File

@ -23,7 +23,7 @@ class ParticipantResource extends JsonResource
$attributes = collect([]);
foreach ($this->form->getFields() as $field) {
$attributes = $attributes->merge(Field::fromConfig($field)->presentValue($this->data[$field['key']]));
$attributes = $attributes->merge($field->presentValue($this->data[$field->key]));
}
return $attributes->toArray();
@ -40,12 +40,11 @@ class ParticipantResource extends JsonResource
'update_form_meta' => route('form.update-meta', ['form' => $form]),
],
'columns' => $form->getFields()
->map(fn ($field) => Field::fromConfig($field))
->map(fn ($field) => [
'name' => $field->name,
'base_type' => class_basename($field),
'id' => $field->key,
'display_attribute' => $field->getdisplayAttribute(),
'display_attribute' => $field->getDisplayAttribute(),
])
];
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Form\Transformers;
use App\Form\Fields\Field;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Support\DataProperty;
use Spatie\LaravelData\Transformers\Transformer;
class CollectionTransformer implements Transformer
{
public function __construct(public string $target)
{
}
/**
* @param Collection<int, Field> $value
* @return array<string, mixed>
*/
public function transform(DataProperty $property, mixed $value): mixed
{
return $value->toArray();
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Form\Transformers;
use App\Form\Fields\Field;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Support\DataProperty;
use Spatie\LaravelData\Transformers\Transformer;
class FieldCollectionTransformer implements Transformer
{
/**
* @param Collection<int, Field> $value
* @return array<string, mixed>
*/
public function transform(DataProperty $property, mixed $value): mixed
{
return $value->map(fn ($field) => [
...$field->toArray(),
'type' => class_basename($field),
])->toArray();
}
}

View File

@ -24,7 +24,9 @@ class FormtemplateFactory extends Factory
{
return [
'name' => $this->faker->words(4, true),
'config' => [],
'config' => [
'sections' => [],
],
];
}

View File

@ -538,16 +538,6 @@ parameters:
count: 1
path: app/Form/Models/Form.php
-
message: "#^Unable to resolve the template type TKey in call to function collect$#"
count: 1
path: app/Form/Models/Form.php
-
message: "#^Unable to resolve the template type TValue in call to function collect$#"
count: 1
path: app/Form/Models/Form.php
-
message: "#^Unable to resolve the template type TKey in call to function collect$#"
count: 1

View File

@ -35,7 +35,7 @@ class FormStoreActionTest extends FormTestCase
$this->postJson(route('form.store'))->assertOk();
$form = Form::latest()->first();
$this->assertEquals('sname', $form->config['sections'][0]['name']);
$this->assertEquals('sname', $form->config->sections->get(0)->name);
$this->assertEquals('formname', $form->name);
$this->assertEquals('avff', $form->excerpt);
$this->assertEquals($description->paragraphBlock(10, 'Lorem'), $form->description);
@ -45,8 +45,8 @@ class FormStoreActionTest extends FormTestCase
$this->assertEquals('2023-07-07 01:00', $form->registration_until->format('Y-m-d H:i'));
$this->assertEquals('2023-07-07', $form->from->format('Y-m-d'));
$this->assertEquals('2023-07-08', $form->to->format('Y-m-d'));
$this->assertEquals('Geburtstag', $form->config['sections'][0]['fields'][0]['nami_type']);
$this->assertFalse($form->config['sections'][0]['fields'][0]['for_members']);
$this->assertEquals('Geburtstag', $form->config->sections->get(0)->fields->get(0)->namiType->value);
$this->assertFalse($form->config->sections->get(0)->fields->get(0)->forMembers);
$this->assertCount(1, $form->getMedia('headerImage'));
$this->assertEquals('formname.jpg', $form->getMedia('headerImage')->first()->file_name);
Event::assertDispatched(Succeeded::class, fn (Succeeded $event) => $event->message === 'Veranstaltung gespeichert.');

View File

@ -26,7 +26,7 @@ class FormUpdateActionTest extends FormTestCase
$form = $form->fresh();
$this->assertTrue(data_get($form->config, 'sections.0.fields.0.max_today'));
$this->assertTrue($form->config->sections->get(0)->fields->get(0)->maxToday);
}
public function testItUpdatesActiveColumnsWhenFieldRemoved(): void

View File

@ -31,7 +31,6 @@ class FormtemplateFieldRequest extends RequestFactory
'name' => $this->faker->words(5, true),
'key' => str($this->faker->words(5, true))->snake()->toString(),
'columns' => ['mobile' => 2, 'tablet' => 4, 'desktop' => 6],
'default' => '',
'nami_type' => null,
'for_members' => true,
];

View File

@ -2,6 +2,9 @@
namespace Tests\Feature\Form;
use App\Form\Fields\TextareaField;
use App\Form\Fields\TextField;
use App\Form\Models\Form;
use App\Form\Models\Formtemplate;
use App\Lib\Events\Succeeded;
use Illuminate\Foundation\Testing\DatabaseTransactions;
@ -19,7 +22,7 @@ class FormtemplateStoreActionTest extends FormTestCase
$this->login()->loginNami()->withoutExceptionHandling();
FormtemplateRequest::new()->name('testname')->sections([
FormtemplateSectionRequest::new()->name('Persönliches')->fields([
$this->textField('a')->name('lala1')->columns(['mobile' => 2, 'tablet' => 2, 'desktop' => 1])->required(false)->default('zuzu'),
$this->textField('a')->name('lala1')->columns(['mobile' => 2, 'tablet' => 2, 'desktop' => 1])->required(false),
$this->textareaField('b')->name('lala2')->required(false)->rows(10),
]),
])->fake();
@ -27,15 +30,14 @@ class FormtemplateStoreActionTest extends FormTestCase
$this->postJson(route('formtemplate.store'))->assertOk();
$formtemplate = Formtemplate::latest()->first();
$this->assertEquals('Persönliches', $formtemplate->config['sections'][0]['name']);
$this->assertEquals('lala1', $formtemplate->config['sections'][0]['fields'][0]['name']);
$this->assertEquals('TextField', $formtemplate->config['sections'][0]['fields'][0]['type']);
$this->assertEquals('zuzu', $formtemplate->config['sections'][0]['fields'][0]['default']);
$this->assertEquals('TextareaField', $formtemplate->config['sections'][0]['fields'][1]['type']);
$this->assertEquals(false, $formtemplate->config['sections'][0]['fields'][1]['required']);
$this->assertEquals(['mobile' => 2, 'tablet' => 2, 'desktop' => 1], $formtemplate->config['sections'][0]['fields'][0]['columns']);
$this->assertEquals(10, $formtemplate->config['sections'][0]['fields'][1]['rows']);
$this->assertFalse($formtemplate->config['sections'][0]['fields'][0]['required']);
$this->assertEquals('Persönliches', $formtemplate->config->sections->get(0)->name);
$this->assertEquals('lala1', $formtemplate->config->sections->get(0)->fields->get(0)->name);
$this->assertInstanceOf(TextField::class, $formtemplate->config->sections->get(0)->fields->get(0));
$this->assertInstanceOf(TextareaField::class, $formtemplate->config->sections->get(0)->fields->get(1));
$this->assertEquals(false, $formtemplate->config->sections->get(0)->fields->get(1)->required);
$this->assertEquals(['mobile' => 2, 'tablet' => 2, 'desktop' => 1], $formtemplate->config->sections->get(0)->fields->get(0)->columns->toArray());
$this->assertEquals(10, $formtemplate->config->sections->get(0)->fields->get(1)->rows);
$this->assertFalse($formtemplate->config->sections->get(0)->fields->get(0)->required);
Event::assertDispatched(Succeeded::class, fn (Succeeded $event) => $event->message === 'Vorlage gespeichert.');
}