diff --git a/app/Form/Actions/HasValidation.php b/app/Form/Actions/HasValidation.php index 92b59613..b3daae21 100644 --- a/app/Form/Actions/HasValidation.php +++ b/app/Form/Actions/HasValidation.php @@ -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', ]; } diff --git a/app/Form/Actions/RegisterAction.php b/app/Form/Actions/RegisterAction.php index 50961511..b0f2521c 100644 --- a/app/Form/Actions/RegisterAction.php +++ b/app/Form/Actions/RegisterAction.php @@ -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; } diff --git a/app/Form/Casts/CollectionCast.php b/app/Form/Casts/CollectionCast.php new file mode 100644 index 00000000..47ec9a2e --- /dev/null +++ b/app/Form/Casts/CollectionCast.php @@ -0,0 +1,30 @@ + $target + */ + public function __construct(public string $target) + { + } + + /** + * @param array> $value + * @param array $context + * @return Collection + */ + public function cast(DataProperty $property, mixed $value, array $context): mixed + { + return collect($value)->map(fn ($item) => $this->target::from($item)); + } +} diff --git a/app/Form/Casts/FieldCollectionCast.php b/app/Form/Casts/FieldCollectionCast.php new file mode 100644 index 00000000..0661dfb9 --- /dev/null +++ b/app/Form/Casts/FieldCollectionCast.php @@ -0,0 +1,21 @@ +> $value + * @param array $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()); + } +} diff --git a/app/Form/Data/ColumnData.php b/app/Form/Data/ColumnData.php new file mode 100644 index 00000000..e69a2865 --- /dev/null +++ b/app/Form/Data/ColumnData.php @@ -0,0 +1,16 @@ + + */ +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)); + } +} diff --git a/app/Form/Data/FormConfigData.php b/app/Form/Data/FormConfigData.php new file mode 100644 index 00000000..6e97ac0a --- /dev/null +++ b/app/Form/Data/FormConfigData.php @@ -0,0 +1,32 @@ + $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([]) + ); + } +} diff --git a/app/Form/Data/SectionData.php b/app/Form/Data/SectionData.php new file mode 100644 index 00000000..eb57c208 --- /dev/null +++ b/app/Form/Data/SectionData.php @@ -0,0 +1,21 @@ + $input @@ -76,14 +81,6 @@ abstract class Field extends Data return $fieldClass; } - /** - * @param array $config - */ - public static function fromConfig(array $config): static - { - return static::classFromType($config['type'])::withoutMagicalCreationFrom($config); - } - /** * @param mixed $value * @return mixed diff --git a/app/Form/Fields/NamiField.php b/app/Form/Fields/NamiField.php index 48dd8e6a..36149206 100644 --- a/app/Form/Fields/NamiField.php +++ b/app/Form/Fields/NamiField.php @@ -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); diff --git a/app/Form/Fields/TextareaField.php b/app/Form/Fields/TextareaField.php index cceee2b9..d224a7c2 100644 --- a/app/Form/Fields/TextareaField.php +++ b/app/Form/Fields/TextareaField.php @@ -9,6 +9,7 @@ use Faker\Generator; class TextareaField extends Field { public bool $required; + public int $rows; public static function name(): string { diff --git a/app/Form/Models/Form.php b/app/Form/Models/Form.php index 77cabae1..3200a17b 100644 --- a/app/Form/Models/Form.php +++ b/app/Form/Models/Form.php @@ -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 - */ - 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, ]); } diff --git a/app/Form/Models/Formtemplate.php b/app/Form/Models/Formtemplate.php index 4152f032..1efae888 100644 --- a/app/Form/Models/Formtemplate.php +++ b/app/Form/Models/Formtemplate.php @@ -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, ]; } diff --git a/app/Form/Resources/ParticipantResource.php b/app/Form/Resources/ParticipantResource.php index 577a41af..33c1f771 100644 --- a/app/Form/Resources/ParticipantResource.php +++ b/app/Form/Resources/ParticipantResource.php @@ -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(), ]) ]; } diff --git a/app/Form/Transformers/CollectionTransformer.php b/app/Form/Transformers/CollectionTransformer.php new file mode 100644 index 00000000..ee7f8af5 --- /dev/null +++ b/app/Form/Transformers/CollectionTransformer.php @@ -0,0 +1,25 @@ + $value + * @return array + */ + public function transform(DataProperty $property, mixed $value): mixed + { + return $value->toArray(); + } +} diff --git a/app/Form/Transformers/FieldCollectionTransformer.php b/app/Form/Transformers/FieldCollectionTransformer.php new file mode 100644 index 00000000..c6012b4d --- /dev/null +++ b/app/Form/Transformers/FieldCollectionTransformer.php @@ -0,0 +1,24 @@ + $value + * @return array + */ + public function transform(DataProperty $property, mixed $value): mixed + { + return $value->map(fn ($field) => [ + ...$field->toArray(), + 'type' => class_basename($field), + ])->toArray(); + } +} diff --git a/database/factories/Form/Models/FormtemplateFactory.php b/database/factories/Form/Models/FormtemplateFactory.php index 55b41024..3970aaaf 100644 --- a/database/factories/Form/Models/FormtemplateFactory.php +++ b/database/factories/Form/Models/FormtemplateFactory.php @@ -24,7 +24,9 @@ class FormtemplateFactory extends Factory { return [ 'name' => $this->faker->words(4, true), - 'config' => [], + 'config' => [ + 'sections' => [], + ], ]; } diff --git a/phpstan.neon b/phpstan.neon index 99ad4acc..271e24a8 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -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 diff --git a/tests/Feature/Form/FormStoreActionTest.php b/tests/Feature/Form/FormStoreActionTest.php index aca4fce1..2ce35b11 100644 --- a/tests/Feature/Form/FormStoreActionTest.php +++ b/tests/Feature/Form/FormStoreActionTest.php @@ -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.'); diff --git a/tests/Feature/Form/FormUpdateActionTest.php b/tests/Feature/Form/FormUpdateActionTest.php index f3db4f3d..804bdca3 100644 --- a/tests/Feature/Form/FormUpdateActionTest.php +++ b/tests/Feature/Form/FormUpdateActionTest.php @@ -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 diff --git a/tests/Feature/Form/FormtemplateFieldRequest.php b/tests/Feature/Form/FormtemplateFieldRequest.php index 56ca47b9..bf5645e2 100644 --- a/tests/Feature/Form/FormtemplateFieldRequest.php +++ b/tests/Feature/Form/FormtemplateFieldRequest.php @@ -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, ]; diff --git a/tests/Feature/Form/FormtemplateStoreActionTest.php b/tests/Feature/Form/FormtemplateStoreActionTest.php index f39efddd..c4f616d2 100644 --- a/tests/Feature/Form/FormtemplateStoreActionTest.php +++ b/tests/Feature/Form/FormtemplateStoreActionTest.php @@ -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.'); }