Skip to content

Commit

Permalink
Merge pull request #16 from moonshine-software/validation
Browse files Browse the repository at this point in the history
feat: Validation
  • Loading branch information
lee-to authored Feb 7, 2025
2 parents f9090d4 + c8d8dba commit e84372d
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 31 deletions.
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,21 @@ Layouts::make('Content')
->addLayout('Banner section', 'banner', [
Text::make('Title'),
Image::make('Banner image', 'thumbnail'),
]),
], validation: ['title' => 'required']),
```
#### Adding layouts

Layouts can be added using the following method on your Layouts fields:

```php
addLayout(string $title, string $name, iterable $fields, ?int $limit = null)
addLayout(string $title, string $name, iterable $fields, ?int $limit = null, iterable $headingAdditionalFields = null, array $validation = [])
```
1. The `$title` parameter allows you to specify the name of a group of fields that will be displayed in the form.
2. The `$name` parameter is used to store the chosen layout in the field's value.
3. The `$fields` parameter accepts an array of fields that will be used to populate a group of fields in the form.
4. `$limit` allows you to set the max number of groups in the field.
5. `$headingAdditionalFields` components in header
6. `$validation` validation rules.

#### Adding cast

Expand Down Expand Up @@ -91,3 +93,35 @@ Layouts::make('Content')
])
->searchable()
```


#### Validation

```php
use MoonShine\UI\Fields\Email;Layouts::make('Content')
->addLayout('Info section', 'info', [
Email::make('Email')
], validation: ['email' => ['required', 'email']], attributes: ['email' => 'E-mail'])
```

```php
use MoonShine\UI\Fields\Email;Layouts::make('Content')
->addLayout('Info section', 'info', [
Email::make('Email')
]),
->addLayout('Additionally section', 'additionally', [
Text::make('Title')
])
->validation(['info' => ['email' => 'required'], 'additionally' => ['title' => 'required']])
```

```php
use MoonShine\UI\Fields\Email;Layouts::make('Content')
->addLayout('Info section', 'info', [
Email::make('Email')
], validation: ['email' => ['email']], attributes: ['email' => 'E-mail']),
->addLayout('Additionally section', 'additionally', [
Text::make('Title')
])
->validation(['info' => ['email' => ['required']], 'additionally' => ['title' => 'required']], attributes: ['additionally' => ['title' => 'Заголовок']])
```
4 changes: 2 additions & 2 deletions src/Casts/LayoutsCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ public function get(Model $model, string $key, mixed $value, array $attributes):
return $data;
}

return is_array($data) ? $this->_map($data) : null;
return is_array($data) ? $this->map($data) : null;
}

public function set(Model $model, string $key, mixed $value, array $attributes): array
{
return [$key => Json::encode($value)];
}

private function _map(mixed $value): LayoutItemCollection
public function map(mixed $value): LayoutItemCollection
{
$values = [];

Expand Down
177 changes: 150 additions & 27 deletions src/Fields/Layouts.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use MoonShine\AssetManager\Js;
use MoonShine\Contracts\Core\HasComponentsContract;
use MoonShine\Contracts\Core\PageContract;
Expand Down Expand Up @@ -50,6 +53,12 @@ final class Layouts extends Field

private ?PageContract $page = null;

private array $rules = [];

private array $rulesAttributes = [];

private array $rulesMessages = [];

protected function assets(): array
{
return [
Expand All @@ -63,15 +72,30 @@ public function addLayout(
iterable $fields,
?int $limit = null,
?iterable $headingAdditionalFields = null,
array $validation = [],
array $attributes = [],
array $messages = [],
): self {
$this->layouts[] = new Layout(
$title,
$name,
$fields,
$limit,
$headingAdditionalFields
$headingAdditionalFields,
);

if ($validation !== []) {
$this->rules[$name] = $validation;
}

if ($attributes !== []) {
$this->rulesAttributes[$name] = $attributes;
}

if ($messages !== []) {
$this->rulesMessages[$name] = $messages;
}

return $this;
}

Expand All @@ -97,30 +121,56 @@ public function getLayouts(): LayoutCollection

public function getLayoutButtons(): array
{
return $this->getLayouts()
return $this
->getLayouts()
->map(
fn (LayoutContract $layout) => Link::make('#', $layout->title())
fn (LayoutContract $layout)
=> Link::make('#', $layout->title())
->icon('plus')
->customAttributes(['@click.prevent' => "add(`{$layout->name()}`);closeDropdown()"])
->customAttributes(['@click.prevent' => "add(`{$layout->name()}`);closeDropdown()"]),
)
->toArray();
}

protected function resolveOldValue(mixed $old): mixed
{
if (is_array($old) && $old !== []) {
return collect($old)->map(function (array $values): ?array {
$layout = $this->getLayouts()->findByName($values['_layout']);

if (! $layout instanceof LayoutContract) {
return null;
}

unset($values['_layout']);

foreach ($layout->fields()->onlyFields() as $field) {
if ($field instanceof HasFieldsContract) {
unset($values[$field->getColumn()]);
}
}

return [
'name' => $layout->name(),
'key' => $layout->key(),
'values' => $values,
];
})->filter();
}

return parent::resolveOldValue($old);
}

/**
* @throws Throwable
*/
public function getFilledLayouts(): LayoutCollection
{
$layouts = $this->getLayouts();
$values = $this->toValue();
$values = $this->getValue();

if (! $values instanceof LayoutItemCollection) {
$values = (new LayoutsCast())->get(
$this->getData()->getOriginal(),
$this->getColumn(),
$values,
[]
);
$values = (new LayoutsCast())->map($values);
}

$filled = $values ? $values->map(function (LayoutItem $data) use ($layouts) {
Expand All @@ -131,19 +181,20 @@ public function getFilledLayouts(): LayoutCollection
return null;
}

$layout = clone $layout->when(
$this->disableSort,
fn (Layout $l): Layout => $l->disableSort()
)
$layout = clone $layout
->when(
$this->disableSort,
fn (Layout $l): Layout => $l->disableSort(),
)
->when(
$this->isPreviewMode(),
fn (Layout $l): Layout => $l->forcePreview()
fn (Layout $l): Layout => $l->forcePreview(),
)
->setKey($data->getKey());

$fields = $this->fillClonedRecursively(
$layout->fields(),
$data->getValues()
$data->getValues(),
);

$layout
Expand All @@ -152,14 +203,14 @@ public function getFilledLayouts(): LayoutCollection
->prepend(
Hidden::make('_layout')
->customAttributes(['class' => '_layout-value'])
->setValue($data->getName())
->setValue($data->getName()),
)
->prepareAttributes()
->prepareReindexNames($this);

$fields = $this->fillClonedRecursively(
$layout->getHeadingAdditionalFields(),
$data->getValues()
$data->getValues(),
);

$layout
Expand All @@ -176,13 +227,13 @@ private function fillClonedRecursively(ComponentsContract|Collection $collection
return $collection->map(function (mixed $item) use ($data) {
if ($item instanceof HasComponentsContract) {
$item = (clone $item)->setComponents(
$this->fillClonedRecursively($item->getComponents(), $data)->toArray()
$this->fillClonedRecursively($item->getComponents(), $data)->toArray(),
);
}

if ($item instanceof HasFieldsContract) {
$item = (clone $item)->fields(
$this->fillClonedRecursively($item->getFields(), $data)->toArray()
$this->fillClonedRecursively($item->getFields(), $data)->toArray(),
);
}

Expand Down Expand Up @@ -289,6 +340,15 @@ protected function resolvePreview(): View|string
->render();
}

public function validation(array $rules, array $attributes = [], array $messages = []): self
{
$this->rules = array_merge_recursive($this->rules, $rules);
$this->rulesAttributes = array_merge_recursive($this->rulesAttributes, $attributes);
$this->rulesMessages = array_merge_recursive($this->rulesMessages, $messages);

return $this;
}

protected function resolveOnApply(): ?Closure
{
return function ($item) {
Expand All @@ -308,20 +368,20 @@ protected function resolveOnApply(): ?Closure
function (Field $field) use ($value, $index, &$applyValues): void {
$field->appendRequestKeyPrefix(
"{$this->getColumn()}.$index",
$this->getRequestKeyPrefix()
$this->getRequestKeyPrefix(),
);

$apply = $field->apply(
fn ($data): mixed => data_set($data, $field->getColumn(), $value[$field->getColumn()] ?? ''),
$value
$value,
);

data_set(
$applyValues,
$field->getColumn(),
data_get($apply, $field->getColumn())
data_get($apply, $field->getColumn()),
);
}
},
);

return [
Expand All @@ -342,6 +402,68 @@ function (Field $field) use ($value, $index, &$applyValues): void {
*/
protected function resolveBeforeApply(mixed $data): mixed
{
if ($this->rules !== []) {
$value = $this->getRequestValue();

if (! is_array($value)) {
$value = [];
}

$value = Collection::make($value)->mapToGroups(fn ($v) => [$v['_layout'] => $v]);

$rules = [];
$attributes = [];
$messages = [];

foreach ($this->rulesMessages as $layoutName => $allMessages) {
foreach ($allMessages as $key => $message) {
$messages["$layoutName.*.$key"] = $message;
}
}

foreach ($this->rules as $layoutName => $rule) {
$layout = $this->getLayouts()->findByName($layoutName);

if (\is_null($layout)) {
continue;
}

foreach ($rule as $fieldName => $args) {
$rules["$layoutName.*.$fieldName"] = $args;

if (isset($this->rulesAttributes[$layoutName][$fieldName])) {
$attr = $this->rulesAttributes[$layoutName][$fieldName];
$attributes["$layoutName.*.$fieldName"] = is_array($attr) ? Arr::last($attr) : $attr;
}
}
}

$validator = Validator::make($value->toArray(), $rules, messages: $messages, attributes: $attributes);

if ($validator->fails()) {
$errors = [];

$before = array_key_first($validator->errors()->toArray());
$beforeKeys = explode('.', $before);
$index = 1;

foreach ($validator->errors()->toArray() as $key => $error) {
$keys = explode('.', $key);

if ($beforeKeys[0] !== $keys[0] || $beforeKeys[1] !== $keys[1]) {
$index++;
}

$after = collect($keys)->except(0, 1)->implode('.');

$errors["{$this->getColumn()}.$index.$after"] = $error;
$beforeKeys = $keys;
}

throw ValidationException::withMessages($errors)->errorBag($this->getFormName());
}
}

return $this->resolveCallback($data, function (Field $field, mixed $value): void {
$field->beforeApply($value);
});
Expand Down Expand Up @@ -381,12 +503,13 @@ protected function resolveCallback(mixed $data, Closure $callback, bool $fill =
continue;
}

$layout->fields()
$layout
->fields()
->onlyFields()
->each(function (Field $field) use ($data, $index, $value, $callback, $fill): void {
$field->appendRequestKeyPrefix(
"{$this->getColumn()}.$index",
$this->getRequestKeyPrefix()
$this->getRequestKeyPrefix(),
);

$field->when($fill, fn (Field $f): Field => $f->resolveFill($data));
Expand Down

0 comments on commit e84372d

Please sign in to comment.