diff --git a/formwork/fields/array.php b/formwork/fields/array.php index a5211ff15..e3f5b43d5 100644 --- a/formwork/fields/array.php +++ b/formwork/fields/array.php @@ -8,28 +8,37 @@ return function (App $app) { return [ - 'validate' => function (Field $field, $value) { - if (Constraint::isEmpty($value)) { - return []; - } + 'methods' => [ + /** + * Return whether the field is associative + */ + 'isAssociative' => function (Field $field): bool { + return $field->is('associative', false); + }, - if ($value instanceof Arrayable) { - $value = $value->toArray(); - } + 'validate' => function (Field $field, $value): array { + if (Constraint::isEmpty($value)) { + return []; + } + + if ($value instanceof Arrayable) { + $value = $value->toArray(); + } - if (!is_array($value)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } + if (!is_array($value)) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); + } - if ($field->is('associative')) { - foreach (array_keys($value) as $key) { - if (is_int($key)) { - unset($value[$key]); + if ($field->isAssociative()) { + foreach (array_keys($value) as $key) { + if (is_int($key)) { + unset($value[$key]); + } } } - } - return array_filter($value); - }, + return array_filter($value); + }, + ], ]; }; diff --git a/formwork/fields/checkbox.php b/formwork/fields/checkbox.php index 75c22fda3..c654c694a 100644 --- a/formwork/fields/checkbox.php +++ b/formwork/fields/checkbox.php @@ -7,20 +7,22 @@ return function (App $app) { return [ - 'validate' => function (Field $field, $value) { - if (Constraint::isTruthy($value)) { - return true; - } + 'methods' => [ + 'validate' => function (Field $field, $value): bool { + if (Constraint::isTruthy($value)) { + return true; + } - if (Constraint::isFalsy($value)) { - return false; - } + if (Constraint::isFalsy($value)) { + return false; + } - if ($value === null) { - return false; - } + if ($value === null) { + return false; + } - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - }, + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); + }, + ], ]; }; diff --git a/formwork/fields/color.php b/formwork/fields/color.php index 44a712fca..67bee5935 100644 --- a/formwork/fields/color.php +++ b/formwork/fields/color.php @@ -7,12 +7,14 @@ return function (App $app) { return [ - 'validate' => function (Field $field, $value) { - if (!Constraint::matchesRegex($value, '/^#[0-9A-Fa-f]{6}$/')) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } + 'methods' => [ + 'validate' => function (Field $field, $value): string { + if (!Constraint::matchesRegex($value, '/^#[0-9A-Fa-f]{6}$/')) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); + } - return strtolower($value); - }, + return strtolower($value); + }, + ], ]; }; diff --git a/formwork/fields/date.php b/formwork/fields/date.php index 0d0966dab..154aa3e07 100644 --- a/formwork/fields/date.php +++ b/formwork/fields/date.php @@ -9,69 +9,96 @@ return function (App $app): array { return [ - /** - * By default the date is formatted using the YYYY-MM-DD format. - * This is at the same time human-readable and comparable when sorting. - */ - 'format' => function (Field $field, string $format = 'YYYY-MM-DD', string $type = 'pattern') use ($app): string { - $translation = $app->translations()->getCurrent(); + 'methods' => [ + /** + * Return the field value formatted as a date string + * + * By default the date is formatted using the `YYYY-MM-DD` format. + * This is at the same time human-readable and comparable when sorting. + * + * @param string $format the format to use for the date string + * @param string $type the type of format to use, either `pattern` or `date` + */ + 'format' => function (Field $field, string $format = 'YYYY-MM-DD', string $type = 'pattern') use ($app): string { + $translation = $app->translations()->getCurrent(); - $format = match (strtolower($type)) { - 'pattern' => Date::patternToFormat($format), - 'date' => $format, - default => throw new InvalidArgumentException('Invalid date format type'), - }; + $format = match (strtolower($type)) { + 'pattern' => Date::patternToFormat($format), + 'date' => $format, + default => throw new InvalidArgumentException('Invalid date format type'), + }; - return $field->isEmpty() ? '' : Date::formatTimestamp($field->toTimestamp(), $format, $translation); - }, + return $field->isEmpty() ? '' : Date::formatTimestamp($field->toTimestamp(), $format, $translation); + }, - 'toTimestamp' => function (Field $field) use ($app): ?int { - $formats = [ - $app->config()->get('system.date.dateFormat'), - $app->config()->get('system.date.datetimeFormat'), - ]; - return $field->isEmpty() ? null : Date::toTimestamp($field->value(), $formats); - }, + /** + * Return the field value as a timestamp + */ + 'toTimestamp' => function (Field $field) use ($app): ?int { + $formats = [ + $app->config()->get('system.date.dateFormat'), + $app->config()->get('system.date.datetimeFormat'), + ]; + return $field->isEmpty() ? null : Date::toTimestamp($field->value(), $formats); + }, - 'toDuration' => function (Field $field) use ($app): string { - return $field->isEmpty() ? '' : Date::formatTimestampAsDistance($field->toTimestamp(), $app->translations()->getCurrent()); - }, + /** + * Return the field value as a duration string. + * + * The duration is formatted as a human-readable translated string representing + * the time difference between the field value and the current time. + */ + 'toDuration' => function (Field $field) use ($app): string { + return $field->isEmpty() ? '' : Date::formatTimestampAsDistance($field->toTimestamp(), $app->translations()->getCurrent()); + }, - 'toDateTimeString' => function (Field $field): string { - return $field->isEmpty() ? '' : Str::removeEnd($field->format('YYYY-MM-DD[T]hh:mm:ss'), ':00'); - }, + /** + * Return the field value as a date string in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format + * + * The time part is removed if the field does not have a time component. + */ + 'toDateTimeString' => function (Field $field): string { + return $field->isEmpty() ? '' : Str::removeEnd($field->format('YYYY-MM-DD[T]hh:mm:ss'), ':00'); + }, - 'toString' => function (Field $field): string { - return $field->isEmpty() ? '' : $field->format(); - }, + /** + * Return the field value as a formatted string + */ + 'toString' => function (Field $field): string { + return $field->isEmpty() ? '' : $field->format(); + }, - 'return' => function (Field $field): Field { - return $field; - }, + 'return' => function (Field $field): Field { + return $field; + }, - 'hasTime' => function (Field $field): bool { - return $field->is('time', true); - }, + /** + * Return whether the field has a time component + */ + 'hasTime' => function (Field $field): bool { + return $field->is('time', true); + }, - 'validate' => function (Field $field, $value) use ($app): ?string { - if (Constraint::isEmpty($value)) { - return null; - } + 'validate' => function (Field $field, $value) use ($app): ?string { + if (Constraint::isEmpty($value)) { + return null; + } - $inputFormats = [ - $app->config()->get('system.date.dateFormat'), - $app->config()->get('system.date.datetimeFormat'), - ]; + $inputFormats = [ + $app->config()->get('system.date.dateFormat'), + $app->config()->get('system.date.datetimeFormat'), + ]; - $format = $field->hasTime() - ? 'Y-m-d H:i:s' - : 'Y-m-d'; + $format = $field->hasTime() + ? 'Y-m-d H:i:s' + : 'Y-m-d'; - try { - return date($format, Date::toTimestamp($value, $inputFormats)); - } catch (InvalidArgumentException $e) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s":%s', $field->name(), $field->type(), Str::after($e->getMessage(), ':'))); - } - }, + try { + return date($format, Date::toTimestamp($value, $inputFormats)); + } catch (InvalidArgumentException $e) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s":%s', $field->name(), $field->type(), Str::after($e->getMessage(), ':'))); + } + }, + ], ]; }; diff --git a/formwork/fields/duration.php b/formwork/fields/duration.php index c39bc5629..410d6a3e3 100644 --- a/formwork/fields/duration.php +++ b/formwork/fields/duration.php @@ -6,27 +6,55 @@ return function (App $app) { return [ - 'validate' => function (Field $field, $value): int|float { - if (!is_numeric($value)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } + 'methods' => [ + /** + * Return the unit of the duration field + * + * This can be `seconds`, `minutes`, `hours`, `days`, `weeks`, `months`, or `years`. + * + * The default is `seconds`. + */ + 'unit' => function (Field $field): string { + return $field->get('unit', 'seconds'); + }, - // This reliably casts numeric values to int or float - $value += 0; + /** + * Return the intervals of the duration field + * + * This is an array of intervals that can be used to display the duration in a more human-readable format. + * + * The default is `['days', 'hours', 'minutes', 'seconds']`. + */ + 'intervals' => function (Field $field): array { + return $field->get('intervals', ['days', 'hours', 'minutes', 'seconds']); + }, - if ($field->has('min') && $value < $field->get('min')) { - throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be greater than or equal to %d', $field->name(), $field->type(), $field->get('min'))); - } + 'validate' => function (Field $field, $value): int|float { + if (!is_numeric($value)) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); + } - if ($field->has('max') && $value > $field->get('max')) { - throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be less than or equal to %d', $field->name(), $field->type(), $field->get('max'))); - } + // This reliably casts numeric values to int or float + $value += 0; - if ($field->has('step') && ($value - $field->get('min', 0)) % $field->get('step') !== 0) { - throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not conform to the step value %d', $field->name(), $field->value(), $field->get('step'))); - } + if ($field->has('min') && $value < $field->get('min')) { + throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be greater than or equal to %d', $field->name(), $field->type(), $field->get('min'))); + } - return $value; - }, + if ($field->has('max') && $value > $field->get('max')) { + throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be less than or equal to %d', $field->name(), $field->type(), $field->get('max'))); + } + + if ($field->has('step') && ($value - $field->get('min', 0)) % $field->get('step') !== 0) { + throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not conform to the step value %d', $field->name(), $field->value(), $field->get('step'))); + } + + if (!in_array($field->unit(), ['seconds', 'minutes', 'hours', 'days', 'weeks', 'months', 'years'])) { + throw new ValidationException(sprintf('Invalid unit for field "%s" of type "%s"', $field->name(), $field->type())); + } + + return $value; + }, + ], ]; }; diff --git a/formwork/fields/email.php b/formwork/fields/email.php index 562e71761..648adf1ca 100644 --- a/formwork/fields/email.php +++ b/formwork/fields/email.php @@ -7,32 +7,35 @@ return function (App $app) { return [ - 'validate' => function (Field $field, $value): string { - if (Constraint::isEmpty($value)) { - return ''; - } - - if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { - throw new ValidationException(sprintf('The value of field "%s" of type "%s" is not a valid e-mail address', $field->name(), $field->value())); - } - - if (!is_string($value)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } - - if ($field->has('min') && strlen($value) < $field->get('min')) { - throw new ValidationException(sprintf('The minimum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->get('min'))); - } - - if ($field->has('max') && strlen($value) > $field->get('max')) { - throw new ValidationException(sprintf('The maximum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->get('max'))); - } - - if ($field->has('pattern') && !Constraint::matchesRegex($value, $field->get('pattern'))) { - throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not match the required pattern', $field->name(), $field->value())); - } - - return $value; - }, + 'extend' => 'text', + 'methods' => [ + 'validate' => function (Field $field, $value): string { + if (Constraint::isEmpty($value)) { + return ''; + } + + if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { + throw new ValidationException(sprintf('The value of field "%s" of type "%s" is not a valid e-mail address', $field->name(), $field->value())); + } + + if (!is_string($value)) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); + } + + if ($field->has('minLength') && strlen($value) < $field->minLength()) { + throw new ValidationException(sprintf('The minimum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->minLength())); + } + + if ($field->has('maxLength') && strlen($value) > $field->maxLength()) { + throw new ValidationException(sprintf('The maximum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->maxLength())); + } + + if ($field->has('pattern') && !Constraint::matchesRegex($value, $field->pattern())) { + throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not match the required pattern', $field->name(), $field->value())); + } + + return $value; + }, + ], ]; }; diff --git a/formwork/fields/file.php b/formwork/fields/file.php index 82e070075..0086cb7ce 100644 --- a/formwork/fields/file.php +++ b/formwork/fields/file.php @@ -11,51 +11,59 @@ return function (App $app) { return [ - 'return' => function (Field $field): ?File { - return $field->value() !== null - ? $field->getFiles()->get($field->value()) - : null; - }, - - 'getFiles' => function (Field $field): FileCollection { - if (!$field->has('options')) { - $model = $field->parent()?->model(); - - if ($model === null || !method_exists($model, 'files')) { - throw new InvalidValueException(sprintf('Field "%s" of type "%s" must have a model with files', $field->name(), $field->type())); - } + 'methods' => [ + 'return' => function (Field $field): ?File { + return $field->value() !== null + ? $field->getFiles()->get($field->value()) + : null; + }, - return $model->files(); - } + /** + * Get the collection of files associated with the field + */ + 'getFiles' => function (Field $field): FileCollection { + if (!$field->has('options')) { + $model = $field->parent()?->model(); - return $field->get('options'); - }, + if ($model === null || !method_exists($model, 'files')) { + throw new InvalidValueException(sprintf('Field "%s" of type "%s" must have a model with files', $field->name(), $field->type())); + } - 'validate' => function (Field $field, $value): ?string { - if (Constraint::isEmpty($value)) { - return null; - } + return $model->files(); + } - if (!is_string($value)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } + return $field->get('options'); + }, - return $value; - }, + 'validate' => function (Field $field, $value): ?string { + if (Constraint::isEmpty($value)) { + return null; + } - 'options' => function (Field $field): array { - $collection = $field->getFiles(); + if (!is_string($value)) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); + } - if ($field->has('fileType')) { - $collection = $collection->filter(static fn(File $file) => in_array($file->type(), (array) $field->get('fileType'), true)); - } + return $value; + }, + + /** + * Get the field dropdown options + */ + 'options' => function (Field $field): array { + $collection = $field->getFiles(); + + if ($field->has('fileType')) { + $collection = $collection->filter(static fn(File $file) => in_array($file->type(), (array) $field->get('fileType'), true)); + } - return $collection - ->map(static fn(File $file) => [ - 'value' => $file->name(), - 'icon' => 'file-' . $file->type(), - 'thumb' => $file instanceof Image ? $file->square(300, 'contain')->uri() : null, - ])->toArray(); - }, + return $collection + ->map(static fn(File $file) => [ + 'value' => $file->name(), + 'icon' => 'file-' . $file->type(), + 'thumb' => $file instanceof Image ? $file->square(300, 'contain')->uri() : null, + ])->toArray(); + }, + ], ]; }; diff --git a/formwork/fields/files.php b/formwork/fields/files.php index 4be82e2cf..38e2c974d 100644 --- a/formwork/fields/files.php +++ b/formwork/fields/files.php @@ -11,73 +11,87 @@ return function (App $app) { return [ - 'getFiles' => function (Field $field): FileCollection { - if (!$field->has('options')) { - $model = $field->parent()?->model(); - - if ($model === null || !method_exists($model, 'files')) { - throw new InvalidValueException(sprintf('Field "%s" of type "%s" must have a model with files', $field->name(), $field->type())); + 'methods' => [ + /** + * Get the collection of files associated with the field + */ + 'getFiles' => function (Field $field): FileCollection { + if (!$field->has('options')) { + $model = $field->parent()?->model(); + + if ($model === null || !method_exists($model, 'files')) { + throw new InvalidValueException(sprintf('Field "%s" of type "%s" must have a model with files', $field->name(), $field->type())); + } + + return $model->files(); } - return $model->files(); - } - - return $field->get('options'); - }, - - 'toString' => function ($field) { - return implode(', ', $field->value() ?? []); - }, + return $field->get('options'); + }, - 'return' => function (Field $field): FileCollection { - return $field->getFiles()->filter(static fn(File $file) => in_array($file->name(), $field->value(), true)); - }, + 'toString' => function ($field) { + return implode(', ', $field->value() ?? []); + }, - 'validate' => function (Field $field, $value): array { - if (Constraint::isEmpty($value)) { - return []; - } + 'return' => function (Field $field): FileCollection { + return $field->getFiles()->filter(static fn(File $file) => in_array($file->name(), $field->value(), true)); + }, - if (is_string($value)) { - $value = array_map(trim(...), explode(',', $value)); - } - - if (!is_array($value)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } + 'validate' => function (Field $field, $value): array { + if (Constraint::isEmpty($value)) { + return []; + } - if ($field->has('pattern')) { - $value = array_filter($value, static fn($item): bool => Constraint::matchesRegex($item, $field->get('pattern'))); - } + if (is_string($value)) { + $value = array_map(trim(...), explode(',', $value)); + } - if ($field->limit() !== null && count($value) > $field->limit()) { - throw new ValidationException(sprintf('Field "%s" of type "%s" has a limit of %d items', $field->name(), $field->type(), $field->get('limit'))); - } + if (!is_array($value)) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); + } - return array_values(array_filter($value)); - }, + if ($field->has('pattern')) { + $value = array_filter($value, static fn($item): bool => Constraint::matchesRegex($item, $field->get('pattern'))); + } - 'options' => function (Field $field): array { - $collection = $field->getFiles(); + if ($field->limit() !== null && count($value) > $field->limit()) { + throw new ValidationException(sprintf('Field "%s" of type "%s" has a limit of %d items', $field->name(), $field->type(), $field->get('limit'))); + } - if ($field->has('fileType')) { - $collection = $collection->filter(static fn(File $file) => in_array($file->type(), (array) $field->get('fileType'), true)); - } + return array_values(array_filter($value)); + }, - return $collection - ->map(static fn(File $file) => [ - 'value' => $file->name(), - 'icon' => 'file-' . $file->type(), - 'thumb' => $file instanceof Image ? $file->square(300, 'contain')->uri() : null, - ])->toArray(); - }, + /** + * Get the field dropdown options + */ + 'options' => function (Field $field): array { + $collection = $field->getFiles(); - 'limit' => function (Field $field): ?int { - return $field->get('limit', null); - }, + if ($field->has('fileType')) { + $collection = $collection->filter(static fn(File $file) => in_array($file->type(), (array) $field->get('fileType'), true)); + } - 'isOrderable' => function ($field): bool { - return $field->is('orderable', true); - }, + return $collection + ->map(static fn(File $file) => [ + 'value' => $file->name(), + 'icon' => 'file-' . $file->type(), + 'thumb' => $file instanceof Image ? $file->square(300, 'contain')->uri() : null, + ])->toArray(); + }, + + /** + * Return the maximum number of items allowed in the field + */ + 'limit' => function (Field $field): ?int { + return $field->get('limit', null); + }, + + /** + * Return whether the field items are orderable + */ + 'isOrderable' => function ($field): bool { + return $field->is('orderable', true); + }, + ], ]; }; diff --git a/formwork/fields/image.php b/formwork/fields/image.php index 40a340159..9521cea81 100644 --- a/formwork/fields/image.php +++ b/formwork/fields/image.php @@ -11,47 +11,55 @@ return function (App $app) { return [ - 'return' => function (Field $field): ?Image { - return $field->value() !== null - ? $field->getImages()->get($field->value()) - : null; - }, - - 'getImages' => function (Field $field): FileCollection { - if (!$field->has('options')) { - $model = $field->parent()?->model(); - - if ($model === null || !method_exists($model, 'files')) { - throw new InvalidValueException(sprintf('Field "%s" of type "%s" must have a model with files', $field->name(), $field->type())); + 'methods' => [ + 'return' => function (Field $field): ?Image { + return $field->value() !== null + ? $field->getImages()->get($field->value()) + : null; + }, + + /** + * Get the collection of images associated with the field + */ + 'getImages' => function (Field $field): FileCollection { + if (!$field->has('options')) { + $model = $field->parent()?->model(); + + if ($model === null || !method_exists($model, 'files')) { + throw new InvalidValueException(sprintf('Field "%s" of type "%s" must have a model with files', $field->name(), $field->type())); + } + + $files = $model->files(); + } else { + $files = $field->get('options'); } - $files = $model->files(); - } else { - $files = $field->get('options'); - } - - return $files->filter(static fn(File $file) => $file instanceof Image); - }, - - 'validate' => function (Field $field, $value): ?string { - if (Constraint::isEmpty($value)) { - return null; - } - - if (!is_string($value)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } - - return $value; - }, - - 'options' => function (Field $field): array { - return $field->getImages() - ->map(static fn(Image $image) => [ - 'value' => $image->name(), - 'icon' => 'image', - 'thumb' => $image->square(300, 'contain')->uri(), - ])->toArray(); - }, + return $files->filter(static fn(File $file) => $file instanceof Image); + }, + + 'validate' => function (Field $field, $value): ?string { + if (Constraint::isEmpty($value)) { + return null; + } + + if (!is_string($value)) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); + } + + return $value; + }, + + /** + * Get the field dropdown options + */ + 'options' => function (Field $field): array { + return $field->getImages() + ->map(static fn(Image $image) => [ + 'value' => $image->name(), + 'icon' => 'image', + 'thumb' => $image->square(300, 'contain')->uri(), + ])->toArray(); + }, + ], ]; }; diff --git a/formwork/fields/images.php b/formwork/fields/images.php index a15179527..1fada1d34 100644 --- a/formwork/fields/images.php +++ b/formwork/fields/images.php @@ -11,69 +11,83 @@ return function (App $app) { return [ - 'getImages' => function (Field $field): FileCollection { - if (!$field->has('options')) { - $model = $field->parent()?->model(); + 'methods' => [ + /** + * Get the collection of images associated with the field + */ + 'getImages' => function (Field $field): FileCollection { + if (!$field->has('options')) { + $model = $field->parent()?->model(); + + if ($model === null || !method_exists($model, 'files')) { + throw new InvalidValueException(sprintf('Field "%s" of type "%s" must have a model with files', $field->name(), $field->type())); + } + + $files = $model->files(); + } else { + $files = $field->get('options'); + } + + return $files->filter(static fn(File $file) => $file instanceof Image); + }, + + 'toString' => function ($field) { + return implode(', ', $field->value() ?? []); + }, + + 'return' => function (Field $field): FileCollection { + return $field->getImages()->filter(static fn(File $file) => in_array($file->name(), $field->value(), true)); + }, + + 'validate' => function (Field $field, $value): array { + if (Constraint::isEmpty($value)) { + return []; + } + + if (is_string($value)) { + $value = array_map(trim(...), explode(',', $value)); + } + + if (!is_array($value)) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); + } + + if ($field->has('pattern')) { + $value = array_filter($value, static fn($item): bool => Constraint::matchesRegex($item, $field->get('pattern'))); + } - if ($model === null || !method_exists($model, 'files')) { - throw new InvalidValueException(sprintf('Field "%s" of type "%s" must have a model with files', $field->name(), $field->type())); + if ($field->limit() !== null && count($value) > $field->limit()) { + throw new ValidationException(sprintf('Field "%s" of type "%s" has a limit of %d items', $field->name(), $field->type(), $field->get('limit'))); } - $files = $model->files(); - } else { - $files = $field->get('options'); - } - - return $files->filter(static fn(File $file) => $file instanceof Image); - }, - - 'toString' => function ($field) { - return implode(', ', $field->value() ?? []); - }, - - 'return' => function (Field $field): FileCollection { - return $field->getImages()->filter(static fn(File $file) => in_array($file->name(), $field->value(), true)); - }, - - 'validate' => function (Field $field, $value): array { - if (Constraint::isEmpty($value)) { - return []; - } - - if (is_string($value)) { - $value = array_map(trim(...), explode(',', $value)); - } - - if (!is_array($value)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } - - if ($field->has('pattern')) { - $value = array_filter($value, static fn($item): bool => Constraint::matchesRegex($item, $field->get('pattern'))); - } - - if ($field->limit() !== null && count($value) > $field->limit()) { - throw new ValidationException(sprintf('Field "%s" of type "%s" has a limit of %d items', $field->name(), $field->type(), $field->get('limit'))); - } - - return array_values(array_filter($value)); - }, - - 'options' => function (Field $field): array { - return $field->getImages() - ->map(static fn(Image $image) => [ - 'value' => $image->name(), - 'icon' => 'image', - 'thumb' => $image->square(300, 'contain')->uri(), - ])->toArray(); - }, - - 'limit' => function (Field $field): ?int { - return $field->get('limit', null); - }, - - 'isOrderable' => function ($field): bool { - return $field->is('orderable', true); - }, + return array_values(array_filter($value)); + }, + + /** + * Get the field dropdown options + */ + 'options' => function (Field $field): array { + return $field->getImages() + ->map(static fn(Image $image) => [ + 'value' => $image->name(), + 'icon' => 'image', + 'thumb' => $image->square(300, 'contain')->uri(), + ])->toArray(); + }, + + /** + * Return the maximum number of items allowed in the field + */ + 'limit' => function (Field $field): ?int { + return $field->get('limit', null); + }, + + /** + * Return whether the field items are orderable + */ + 'isOrderable' => function ($field): bool { + return $field->is('orderable', true); + }, + ], ]; }; diff --git a/formwork/fields/markdown.php b/formwork/fields/markdown.php index 26b1ac195..d51c1a2cb 100644 --- a/formwork/fields/markdown.php +++ b/formwork/fields/markdown.php @@ -2,56 +2,52 @@ use Formwork\Cms\App; use Formwork\Cms\Site; -use Formwork\Fields\Exceptions\ValidationException; use Formwork\Fields\Field; use Formwork\Parsers\Markdown; -use Formwork\Utils\Constraint; use Formwork\Utils\Str; return function (App $app, Site $site) { return [ - 'toHTML' => function (Field $field) use ($app, $site): string { - $currentPage = $site->currentPage(); - return Markdown::parse( - (string) $field->value(), - [ - 'site' => $site, - 'safeMode' => $app->config()->get('system.pages.content.safeMode'), - 'baseRoute' => $currentPage !== null ? $currentPage->route() : '/', - ] - ); - }, - - 'toString' => function (Field $field): string { - return $field->toHTML(); - }, - - 'toPlainText' => function (Field $field): string { - return Str::removeHTML($field->toHTML()); - }, - - 'return' => function (Field $field): Field { - return $field; - }, - - 'validate' => function (Field $field, $value): string { - if (Constraint::isEmpty($value)) { - return ''; - } - - if (!is_string($value) && !is_numeric($value)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } - - if ($field->has('min') && strlen((string) $value) < $field->get('min')) { - throw new ValidationException(sprintf('The minimum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->get('min'))); - } - - if ($field->has('max') && strlen((string) $value) > $field->get('max')) { - throw new ValidationException(sprintf('The maximum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->get('max'))); - } - - return str_replace("\r\n", "\n", (string) $value); - }, + 'extend' => 'textarea', + 'methods' => [ + /** + * Return the rows displayed by default in the textarea + * + * If not set, the default is 15 rows + */ + 'rows' => function (Field $field): int { + return $field->get('rows', 15); + }, + + /** + * Convert the field value to HTML + */ + 'toHTML' => function (Field $field) use ($app, $site): string { + $currentPage = $site->currentPage(); + return Markdown::parse( + (string) $field->value(), + [ + 'site' => $site, + 'safeMode' => $app->config()->get('system.pages.content.safeMode'), + 'baseRoute' => $currentPage !== null ? $currentPage->route() : '/', + ] + ); + }, + + 'toString' => function (Field $field): string { + return $field->toHTML(); + }, + + /** + * Get the field value as plain text + */ + 'toPlainText' => function (Field $field): string { + return Str::removeHTML($field->toHTML()); + }, + + 'return' => function (Field $field): Field { + return $field; + }, + ], ]; }; diff --git a/formwork/fields/number.php b/formwork/fields/number.php index c39bc5629..365e4bd43 100644 --- a/formwork/fields/number.php +++ b/formwork/fields/number.php @@ -6,27 +6,56 @@ return function (App $app) { return [ - 'validate' => function (Field $field, $value): int|float { - if (!is_numeric($value)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } + 'methods' => [ + /** + * Return the minimum allowed value for the field + * + * If not set, no minimum value is enforced + */ + 'min' => function (Field $field): ?int { + return $field->get('min'); + }, - // This reliably casts numeric values to int or float - $value += 0; + /** + * Return the maximum allowed value for the field + * + * If not set, no maximum value is enforced + */ + 'max' => function (Field $field): ?int { + return $field->get('max'); + }, - if ($field->has('min') && $value < $field->get('min')) { - throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be greater than or equal to %d', $field->name(), $field->type(), $field->get('min'))); - } + /** + * Return the step value for the field + * + * If not set, no step value is enforced + */ + 'step' => function (Field $field): ?int { + return $field->get('step'); + }, - if ($field->has('max') && $value > $field->get('max')) { - throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be less than or equal to %d', $field->name(), $field->type(), $field->get('max'))); - } + 'validate' => function (Field $field, $value): int|float { + if (!is_numeric($value)) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); + } - if ($field->has('step') && ($value - $field->get('min', 0)) % $field->get('step') !== 0) { - throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not conform to the step value %d', $field->name(), $field->value(), $field->get('step'))); - } + // This reliably casts numeric values to int or float + $value += 0; - return $value; - }, + if ($field->has('min') && $value < $field->min()) { + throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be greater than or equal to %d', $field->name(), $field->type(), $field->get('min'))); + } + + if ($field->has('max') && $value > $field->max()) { + throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be less than or equal to %d', $field->name(), $field->type(), $field->get('max'))); + } + + if ($field->has('step') && ($value - $field->get('min', 0)) % $field->step() !== 0) { + throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not conform to the step value %d', $field->name(), $field->value(), $field->get('step'))); + } + + return $value; + }, + ], ]; }; diff --git a/formwork/fields/page.php b/formwork/fields/page.php index 03987ad77..36f16b7f1 100644 --- a/formwork/fields/page.php +++ b/formwork/fields/page.php @@ -8,39 +8,51 @@ return function (Site $site) { return [ - 'return' => function (Field $field) use ($site) { - if ($field->value() === '.' && $field->get('allowSite', false)) { - return $site; - } - return $site->findPage($field->value() ?? ''); - }, - - 'collection' => function (Field $field) use ($site): PageCollection { - return $field->get('collection', $site->descendants()); - }, - - 'setValue' => function (Field $field, $value) use ($site): ?string { - if ($value === $site) { - return '.'; - } - - if ($value instanceof Page) { - return $value->route(); - } - - return $value; - }, - - 'validate' => function (Field $field, $value) { - if ($value === '') { - return null; - } - - if ($value === '.' && !$field->get('allowSite', false)) { - throw new ValidationException('Invalid Site'); - } - - return $value; - }, + 'methods' => [ + 'return' => function (Field $field) use ($site) { + if ($field->value() === '.' && $field->get('allowSite', false)) { + return $site; + } + return $site->findPage($field->value() ?? ''); + }, + + /** + * Return whether the field should allow selecting the Site + */ + 'allowSite' => function (Field $field): bool { + return $field->is('allowSite', false); + }, + + /** + * Get the collection of pages associated with the field + */ + 'collection' => function (Field $field) use ($site): PageCollection { + return $field->get('collection', $site->descendants()); + }, + + 'setValue' => function (Field $field, $value) use ($site): ?string { + if ($value === $site) { + return '.'; + } + + if ($value instanceof Page) { + return $value->route(); + } + + return $value; + }, + + 'validate' => function (Field $field, $value) { + if ($value === '') { + return null; + } + + if ($value === '.' && !$field->get('allowSite', false)) { + throw new ValidationException('Invalid Site'); + } + + return $value; + }, + ], ]; }; diff --git a/formwork/fields/password.php b/formwork/fields/password.php index 341a1320e..a94e315f4 100644 --- a/formwork/fields/password.php +++ b/formwork/fields/password.php @@ -1,34 +1,9 @@ function (Field $field, $value): string { - if (Constraint::isEmpty($value)) { - return ''; - } - - if (!is_string($value) && !is_numeric($value)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } - - if ($field->has('min') && strlen((string) $value) < $field->get('min')) { - throw new ValidationException(sprintf('The minimum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->get('min'))); - } - - if ($field->has('max') && strlen((string) $value) > $field->get('max')) { - throw new ValidationException(sprintf('The maximum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->get('max'))); - } - - if ($field->has('pattern') && !Constraint::matchesRegex((string) $value, $field->get('pattern'))) { - throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not match the required pattern', $field->name(), $field->value())); - } - - return (string) $value; - }, + 'extend' => 'text', ]; }; diff --git a/formwork/fields/range.php b/formwork/fields/range.php index c39bc5629..07879165b 100644 --- a/formwork/fields/range.php +++ b/formwork/fields/range.php @@ -1,32 +1,18 @@ function (Field $field, $value): int|float { - if (!is_numeric($value)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } - - // This reliably casts numeric values to int or float - $value += 0; - - if ($field->has('min') && $value < $field->get('min')) { - throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be greater than or equal to %d', $field->name(), $field->type(), $field->get('min'))); - } - - if ($field->has('max') && $value > $field->get('max')) { - throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be less than or equal to %d', $field->name(), $field->type(), $field->get('max'))); - } - - if ($field->has('step') && ($value - $field->get('min', 0)) % $field->get('step') !== 0) { - throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not conform to the step value %d', $field->name(), $field->value(), $field->get('step'))); - } - - return $value; - }, + 'extend' => 'number', + 'methods' => [ + /** + * Return whether the field should display ticks + */ + 'ticks' => function (Field $field): bool { + return $field->is('ticks', false); + }, + ], ]; }; diff --git a/formwork/fields/select.php b/formwork/fields/select.php index 40307fc76..f76b460a3 100644 --- a/formwork/fields/select.php +++ b/formwork/fields/select.php @@ -8,25 +8,30 @@ return function (App $app) { return [ - 'options' => function (Field $field) { - return Arr::from($field->get('options', [])); - }, + 'methods' => [ + /** + * Get the field dropdown options + */ + 'options' => function (Field $field) { + return Arr::from($field->get('options', [])); + }, - 'validate' => function (Field $field, $value) { - if (Constraint::isEmpty($value)) { - return ''; - } + 'validate' => function (Field $field, $value) { + if (Constraint::isEmpty($value)) { + return ''; + } - if (!array_key_exists($value, $field->options())) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } + if (!array_key_exists($value, $field->options())) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); + } - if (is_numeric($value)) { - // This reliably casts numeric values to int or float - return $value + 0; - } + if (is_numeric($value)) { + // This reliably casts numeric values to int or float + return $value + 0; + } - return $value; - }, + return $value; + }, + ], ]; }; diff --git a/formwork/fields/slug.php b/formwork/fields/slug.php index a5fcbd572..7a8622115 100644 --- a/formwork/fields/slug.php +++ b/formwork/fields/slug.php @@ -7,67 +7,78 @@ return function (App $app) { return [ - 'validate' => function (Field $field, $value): string { - if (Constraint::isEmpty($value)) { - return ''; - } - - if (!is_string($value) && !is_numeric($value)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } - - if ($field->has('min') && strlen((string) $value) < $field->get('min')) { - throw new ValidationException(sprintf('The minimum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->get('min'))); - } - - if ($field->has('max') && strlen((string) $value) > $field->get('max')) { - throw new ValidationException(sprintf('The maximum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->get('max'))); - } + 'methods' => [ + 'validate' => function (Field $field, $value): string { + if (Constraint::isEmpty($value)) { + return ''; + } - if ($field->has('pattern') && !Constraint::matchesRegex((string) $value, $field->get('pattern'))) { - throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not match the required pattern', $field->name(), $field->value())); - } + if (!is_string($value) && !is_numeric($value)) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); + } - if (!$field->hasUniqueValue()) { - throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be unique', $field->name(), $field->value()), 'alreadyExists'); - } + if ($field->has('min') && strlen((string) $value) < $field->get('min')) { + throw new ValidationException(sprintf('The minimum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->get('min'))); + } - return (string) $value; - }, + if ($field->has('max') && strlen((string) $value) > $field->get('max')) { + throw new ValidationException(sprintf('The maximum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->get('max'))); + } - 'source' => function (Field $field): ?Field { - if (($source = $field->get('source')) === null) { - return null; - } - return $field->parent()?->get($source); - }, + if ($field->has('pattern') && !Constraint::matchesRegex((string) $value, $field->get('pattern'))) { + throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not match the required pattern', $field->name(), $field->value())); + } - 'autoUpdate' => function (Field $field): bool { - return $field->is('autoUpdate', true); - }, + if (!$field->hasUniqueValue()) { + throw new ValidationException(sprintf('The value of field "%s" of type "%s" must be unique', $field->name(), $field->value()), 'alreadyExists'); + } - 'hasUniqueValue' => function (Field $field): bool { - $root = $field->get('root'); + return (string) $value; + }, - if ($root === null) { - return true; - } + /** + * Get the source field from which this field derives its value + */ + 'source' => function (Field $field): ?Field { + if (($source = $field->get('source')) === null) { + return null; + } + return $field->parent()?->get($source); + }, + + /** + * Return whether the field is set to auto-update its value + */ + 'autoUpdate' => function (Field $field): bool { + return $field->is('autoUpdate', true); + }, + + /** + * Check if the field has a unique value among the slugs of the sibling pages + */ + 'hasUniqueValue' => function (Field $field): bool { + $root = $field->get('root'); + + if ($root === null) { + return true; + } - $parentField = $field->parent()?->get($root); + $parentField = $field->parent()?->get($root); - if ($parentField === null || $parentField->type() !== 'page') { - throw new ValidationException(sprintf('Invalid parent reference for field "%s" of type "%s"', $field->name(), $field->type())); - } + if ($parentField === null || $parentField->type() !== 'page') { + throw new ValidationException(sprintf('Invalid parent reference for field "%s" of type "%s"', $field->name(), $field->type())); + } - $children = $parentField->return()->children(); + $children = $parentField->return()->children(); - foreach ($children as $child) { - if ($child->slug() === $field->value()) { - return false; + foreach ($children as $child) { + if ($child->slug() === $field->value()) { + return false; + } } - } - return true; - }, + return true; + }, + ], ]; }; diff --git a/formwork/fields/tags.php b/formwork/fields/tags.php index 9322fe29e..8d83e4b6d 100644 --- a/formwork/fields/tags.php +++ b/formwork/fields/tags.php @@ -9,54 +9,68 @@ return function (App $app) { return [ - 'toString' => function ($field) { - return implode(', ', $field->value() ?? []); - }, + 'methods' => [ + 'toString' => function ($field) { + return implode(', ', $field->value() ?? []); + }, - 'return' => function (Field $field): Collection { - return Collection::from($field->value() ?? []); - }, + 'return' => function (Field $field): Collection { + return Collection::from($field->value() ?? []); + }, - 'validate' => function (Field $field, $value): array { - if (Constraint::isEmpty($value)) { - return []; - } + 'validate' => function (Field $field, $value): array { + if (Constraint::isEmpty($value)) { + return []; + } - if (is_string($value)) { - $value = array_map(trim(...), explode(',', $value)); - } + if (is_string($value)) { + $value = array_map(trim(...), explode(',', $value)); + } - if (!is_array($value)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } + if (!is_array($value)) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); + } - if ($field->has('pattern')) { - $value = array_filter($value, static fn($item): bool => Constraint::matchesRegex($item, $field->get('pattern'))); - } + if ($field->has('pattern')) { + $value = array_filter($value, static fn($item): bool => Constraint::matchesRegex($item, $field->get('pattern'))); + } - if ($field->limit() !== null && count($value) > $field->limit()) { - throw new ValidationException(sprintf('Field "%s" of type "%s" has a limit of %d items', $field->name(), $field->type(), $field->get('limit'))); - } + if ($field->limit() !== null && count($value) > $field->limit()) { + throw new ValidationException(sprintf('Field "%s" of type "%s" has a limit of %d items', $field->name(), $field->type(), $field->get('limit'))); + } - return array_values(array_filter($value)); - }, + return array_values(array_filter($value)); + }, - 'options' => function ($field): ?array { - $options = $field->get('options', null); + /** + * Get the field dropdown options + */ + 'options' => function ($field): ?array { + $options = $field->get('options', null); - return $options !== null ? Arr::from($options) : null; - }, + return $options !== null ? Arr::from($options) : null; + }, - 'accept' => function ($field): string { - return $field->get('accept', 'options'); - }, + /** + * Return whether the field accepts dropdown options + */ + 'accept' => function ($field): string { + return $field->get('accept', 'options'); + }, - 'limit' => function ($field): ?int { - return $field->get('limit', null); - }, + /** + * Return the maximum number of tags allowed in the field + */ + 'limit' => function ($field): ?int { + return $field->get('limit', null); + }, - 'isOrderable' => function ($field): bool { - return $field->is('orderable', true); - }, + /** + * Return whether the field tags are orderable + */ + 'isOrderable' => function ($field): bool { + return $field->is('orderable', true); + }, + ], ]; }; diff --git a/formwork/fields/template.php b/formwork/fields/template.php index 97af5b868..122d0f570 100644 --- a/formwork/fields/template.php +++ b/formwork/fields/template.php @@ -5,16 +5,18 @@ return function (Site $site) { return [ - 'return' => function (Field $field) use ($site) { - return $site->templates()->get($field->value()); - }, + 'methods' => [ + 'return' => function (Field $field) use ($site) { + return $site->templates()->get($field->value()); + }, - 'validate' => function (Field $field, $value) { - if ($value === '') { - return null; - } + 'validate' => function (Field $field, $value) { + if ($value === '') { + return null; + } - return $value; - }, + return $value; + }, + ], ]; }; diff --git a/formwork/fields/text.php b/formwork/fields/text.php index 341a1320e..0f925013e 100644 --- a/formwork/fields/text.php +++ b/formwork/fields/text.php @@ -7,28 +7,58 @@ return function (App $app) { return [ - 'validate' => function (Field $field, $value): string { - if (Constraint::isEmpty($value)) { - return ''; - } + 'methods' => [ + /** + * Return the minimum allowed length for the field + * + * If not set, no minimum length is enforced + */ + 'minLength' => function (Field $field): ?int { + return $field->get('minLength'); + }, - if (!is_string($value) && !is_numeric($value)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } + /** + * Return the maximum allowed length for the field + * + * If not set, no maximum length is enforced + */ + 'maxLength' => function (Field $field): ?int { + return $field->get('maxLength'); + }, - if ($field->has('min') && strlen((string) $value) < $field->get('min')) { - throw new ValidationException(sprintf('The minimum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->get('min'))); - } + /** + * Return the pattern that the field value must match + * + * This is a regular expression that the value must match. + * If not set, no pattern validation is performed. + */ + 'pattern' => function (Field $field): ?string { + return $field->get('pattern'); + }, - if ($field->has('max') && strlen((string) $value) > $field->get('max')) { - throw new ValidationException(sprintf('The maximum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->get('max'))); - } + 'validate' => function (Field $field, $value): string { + if (Constraint::isEmpty($value)) { + return ''; + } - if ($field->has('pattern') && !Constraint::matchesRegex((string) $value, $field->get('pattern'))) { - throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not match the required pattern', $field->name(), $field->value())); - } + if (!is_string($value) && !is_numeric($value)) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); + } - return (string) $value; - }, + if ($field->has('minLength') && strlen((string) $value) < $field->minLength()) { + throw new ValidationException(sprintf('The minimum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->minLength())); + } + + if ($field->has('maxLength') && strlen((string) $value) > $field->maxLength()) { + throw new ValidationException(sprintf('The maximum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->maxLength())); + } + + if ($field->has('pattern') && !Constraint::matchesRegex((string) $value, $field->pattern())) { + throw new ValidationException(sprintf('The value of field "%s" of type "%s" does not match the required pattern', $field->name(), $field->value())); + } + + return (string) $value; + }, + ], ]; }; diff --git a/formwork/fields/textarea.php b/formwork/fields/textarea.php index 9cbf4b85a..759bc54b9 100644 --- a/formwork/fields/textarea.php +++ b/formwork/fields/textarea.php @@ -7,24 +7,71 @@ return function (App $app) { return [ - 'validate' => function (Field $field, $value): string { - if (Constraint::isEmpty($value)) { - return ''; - } + 'methods' => [ + /** + * Return the minimum allowed length for the field + * + * If not set, no minimum length is enforced + */ + 'minLength' => function (Field $field): ?int { + return $field->get('minLength'); + }, - if (!is_string($value) && !is_numeric($value)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); - } + /** + * Return the maximum allowed length for the field + * + * If not set, no maximum length is enforced + */ + 'maxLength' => function (Field $field): ?int { + return $field->get('maxLength'); + }, - if ($field->has('min') && strlen((string) $value) < $field->get('min')) { - throw new ValidationException(sprintf('The minimum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->get('min'))); - } + /** + * Return the rows displayed by default in the textarea + * + * If not set, the default is 5 rows + */ + 'rows' => function (Field $field): int { + return $field->get('rows', 5); + }, - if ($field->has('max') && strlen((string) $value) > $field->get('max')) { - throw new ValidationException(sprintf('The maximum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->get('max'))); - } + /** + * Return whether the entered text can be autocompleted by the browser + * + * By default, autocomplete is `false`, meaning that the browser will not suggest previously entered values + */ + 'autocomplete' => function (Field $field): bool { + return $field->is('autocomplete'); + }, - return str_replace("\r\n", "\n", (string) $value); - }, + /** + * Return whether the field should be spellchecked by the browser + * + * By default, spellcheck is `false`, meaning that the browser will not check the spelling of the entered text + */ + 'spellcheck' => function (Field $field): bool { + return $field->is('spellcheck', true); + }, + + 'validate' => function (Field $field, $value): string { + if (Constraint::isEmpty($value)) { + return ''; + } + + if (!is_string($value) && !is_numeric($value)) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s"', $field->name(), $field->type())); + } + + if ($field->has('min') && strlen((string) $value) < $field->minLength()) { + throw new ValidationException(sprintf('The minimum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->minLength())); + } + + if ($field->has('max') && strlen((string) $value) > $field->maxLength()) { + throw new ValidationException(sprintf('The maximum allowed length for field "%s" of type "%s" is %d', $field->name(), $field->value(), $field->maxLength())); + } + + return str_replace("\r\n", "\n", (string) $value); + }, + ], ]; }; diff --git a/formwork/fields/togglegroup.php b/formwork/fields/togglegroup.php index a3a590f88..eeb5f3926 100644 --- a/formwork/fields/togglegroup.php +++ b/formwork/fields/togglegroup.php @@ -6,21 +6,30 @@ return function (App $app) { return [ - 'validate' => function (Field $field, $value) { - if (Constraint::isTruthy($value)) { - return true; - } + 'methods' => [ + /** + * Return the field options + */ + 'options' => function (Field $field): array { + return $field->get('options', []); + }, - if (Constraint::isFalsy($value)) { - return false; - } + 'validate' => function (Field $field, $value) { + if (Constraint::isTruthy($value)) { + return true; + } - if (is_numeric($value)) { - // This reliably casts numeric values to int or float - return $value + 0; - } + if (Constraint::isFalsy($value)) { + return false; + } - return $value; - }, + if (is_numeric($value)) { + // This reliably casts numeric values to int or float + return $value + 0; + } + + return $value; + }, + ], ]; }; diff --git a/formwork/fields/upload.php b/formwork/fields/upload.php index 0b63671fd..bc3bcc9a0 100644 --- a/formwork/fields/upload.php +++ b/formwork/fields/upload.php @@ -11,59 +11,109 @@ return function (App $app) { return [ - 'acceptMimeTypes' => function (Field $field) use ($app) { - $allowedExtensions = $app->config()->get('system.files.allowedExtensions', ''); - - $accept = is_string($field->get('accept')) - ? preg_split('/\s*,\s*/', $field->get('accept'), flags: PREG_SPLIT_NO_EMPTY) - : $field->get('accept', $allowedExtensions); - - return Arr::map($accept, MimeType::fromExtension(...)); - }, - - 'collection' => function (Field $field): FileCollection { - return $field->get('collection', new FileCollection()); - }, + 'methods' => [ + /** + * Return the accepted MIME types for the field + */ + 'acceptMimeTypes' => function (Field $field) use ($app) { + $allowedExtensions = $app->config()->get('system.files.allowedExtensions', ''); + + $accept = is_string($field->get('accept')) + ? preg_split('/\s*,\s*/', $field->get('accept'), flags: PREG_SPLIT_NO_EMPTY) + : $field->get('accept', $allowedExtensions); + + return Arr::map($accept, MimeType::fromExtension(...)); + }, + + /** + * Return the collection of files associated with the field + */ + 'collection' => function (Field $field): FileCollection { + return $field->get('collection', new FileCollection()); + }, + + /** + * Return whether the field is set to auto-upload files + */ + 'autoUpload' => function (Field $field): bool { + return $field->is('autoUpload'); + }, + + /** + * Return whether the field accepts multiple files + */ + 'isMultiple' => function (Field $field): bool { + return $field->is('multiple'); + }, + + /** + * Return the destination path for uploaded files + */ + 'destination' => function (Field $field): ?string { + return $field->get('destination'); + }, + + /** + * Return whether the field allows overwriting existing files + */ + 'overwrite' => function (Field $field): bool { + return $field->is('overwrite'); + }, + + /** + * Return the filename for the uploaded file + * + * This is only applicable when the field does not allow multiple files. + */ + 'filename' => function (Field $field): ?string { + return $field->get('filename'); + }, + + 'validate' => function (Field $field, $value) use ($app) { + if (Constraint::isEmpty($value)) { + return null; + } - 'autoUpload' => function (Field $field): bool { - return $field->is('autoUpload'); - }, + $allowedExtensions = $app->config()->get('system.files.allowedExtensions', ''); + $allowedMimeTypes = Arr::map($allowedExtensions, MimeType::fromExtension(...)); + $acceptMimeTypes = $field->acceptMimeTypes(); - 'isMultiple' => function (Field $field): bool { - return $field->is('multiple'); - }, + if (($unallowedMimeTypes = array_diff($acceptMimeTypes, $allowedMimeTypes)) !== []) { + throw new ValidationException(sprintf('Invalid accept attribute for field "%s" of type "%s". Found unallowed MIME types: %s', $field->name(), $field->type(), implode(', ', $unallowedMimeTypes))); + } - 'destination' => function (Field $field): ?string { - return $field->get('destination'); - }, + if (!$field->isMultiple()) { + if (!($value instanceof UploadedFile)) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s". Expected an instance of %s', $field->name(), $field->type(), UploadedFile::class)); + } - 'overwrite' => function (Field $field): bool { - return $field->is('overwrite'); - }, + if ($value->isEmpty()) { + if ($field->isRequired()) { + throw new ValidationException(sprintf('Required field "%s" of type "%s" cannot be empty', $field->name(), $field->type())); + } + return null; + } - 'filename' => function (Field $field): ?string { - return $field->get('filename'); - }, + return $value; + } - 'validate' => function (Field $field, $value) use ($app) { - if (Constraint::isEmpty($value)) { - return null; - } + if (!is_array($value)) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s". Expected an array of %s', $field->name(), $field->type(), UploadedFile::class)); + } - $allowedExtensions = $app->config()->get('system.files.allowedExtensions', ''); - $allowedMimeTypes = Arr::map($allowedExtensions, MimeType::fromExtension(...)); - $acceptMimeTypes = $field->acceptMimeTypes(); + if ($field->filename() !== null) { + throw new ValidationException(sprintf('Field "%s" of type "%s" cannot have a filename set when multiple files are allowed', $field->name(), $field->type())); + } - if (($unallowedMimeTypes = array_diff($acceptMimeTypes, $allowedMimeTypes)) !== []) { - throw new ValidationException(sprintf('Invalid accept attribute for field "%s" of type "%s". Found unallowed MIME types: %s', $field->name(), $field->type(), implode(', ', $unallowedMimeTypes))); - } + $value = Arr::filter($value, function ($file) use ($field) { + if (!($file instanceof UploadedFile)) { + throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s". Expected an instance of %s', $field->name(), $field->type(), UploadedFile::class)); + } - if (!$field->isMultiple()) { - if (!($value instanceof UploadedFile)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s". Expected an instance of %s', $field->name(), $field->type(), UploadedFile::class)); - } + return $file->isUploaded(); + }); - if ($value->isEmpty()) { + if ($value === []) { if ($field->isRequired()) { throw new ValidationException(sprintf('Required field "%s" of type "%s" cannot be empty', $field->name(), $field->type())); } @@ -71,32 +121,7 @@ } return $value; - } - - if (!is_array($value)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s". Expected an array of %s', $field->name(), $field->type(), UploadedFile::class)); - } - - if ($field->filename() !== null) { - throw new ValidationException(sprintf('Field "%s" of type "%s" cannot have a filename set when multiple files are allowed', $field->name(), $field->type())); - } - - $value = Arr::filter($value, function ($file) use ($field) { - if (!($file instanceof UploadedFile)) { - throw new ValidationException(sprintf('Invalid value for field "%s" of type "%s". Expected an instance of %s', $field->name(), $field->type(), UploadedFile::class)); - } - - return $file->isUploaded(); - }); - - if ($value === []) { - if ($field->isRequired()) { - throw new ValidationException(sprintf('Required field "%s" of type "%s" cannot be empty', $field->name(), $field->type())); - } - return null; - } - - return $value; - }, + }, + ], ]; }; diff --git a/formwork/src/Fields/FieldFactory.php b/formwork/src/Fields/FieldFactory.php index 75a63b435..07835221d 100644 --- a/formwork/src/Fields/FieldFactory.php +++ b/formwork/src/Fields/FieldFactory.php @@ -2,10 +2,12 @@ namespace Formwork\Fields; +use Closure; use Formwork\Config\Config; use Formwork\Services\Container; use Formwork\Translations\Translations; use Formwork\Utils\FileSystem; +use InvalidArgumentException; final class FieldFactory { @@ -26,12 +28,39 @@ public function make(string $name, array $data = [], ?FieldCollection $parentFie $field->setTranslation($this->translations->getCurrent()); - $methods = FileSystem::joinPaths($this->config->get('system.fields.path'), $field->type() . '.php'); + $config = $this->getFieldConfig($field->type()); - if (FileSystem::exists($methods)) { - $field->setMethods($this->container->call(require $methods)); + $type = $field->type(); + + $extend = $config['extend'] ?? $type; + + while ($extend !== $type) { + $baseConfig = $this->getFieldConfig($extend); + + $type = $extend; + $extend = $baseConfig['extend'] ?? $type; + + unset($baseConfig['extend']); + + $config = array_replace_recursive($baseConfig, $config); } + $field->setMethods($config['methods'] ?? []); + return $field; } + + /** + * @return array{extend?: string, methods?: array} + */ + private function getFieldConfig(string $type): array + { + $configPath = FileSystem::joinPaths($this->config->get('system.fields.path'), $type . '.php'); + + if (!FileSystem::exists($configPath)) { + throw new InvalidArgumentException(sprintf('Field type "%s" does not exist', $type)); + } + + return $this->container->call(require $configPath); + } } diff --git a/panel/views/fields/array.php b/panel/views/fields/array.php index c3baf5d16..a9033d285 100644 --- a/panel/views/fields/array.php +++ b/panel/views/fields/array.php @@ -1,6 +1,6 @@ layout('fields.field') ?>
attr([ - 'class' => ['form-input-array', $field->get('associative') ? 'form-input-array-associative' : ''], + 'class' => ['form-input-array', $field->isAssociative() ? 'form-input-array-associative' : ''], 'id' => $field->name(), 'hidden' => $field->isHidden(), 'data-name' => $field->formName(), @@ -8,7 +8,7 @@ value() ?: ['' => ''] as $key => $value) : ?>
icon('grabber') ?> - get('associative')) : ?> + isAssociative()) : ?> attr([ 'type' => 'text', 'class' => ['form-input', 'form-input-array-key'], @@ -19,7 +19,7 @@ attr([ 'type' => 'text', 'class' => ['form-input', 'form-input-array-value'], - 'name' => $field->formName() . ($field->get('associative') ? '[' . $key . ']' : '[]'), + 'name' => $field->formName() . ($field->isAssociative() ? '[' . $key . ']' : '[]'), 'value' => $value, 'placeholder' => $field->get('placeholderValue'), ]) ?>> diff --git a/panel/views/fields/email.php b/panel/views/fields/email.php index 0ebb8cfd3..98f0bbb82 100644 --- a/panel/views/fields/email.php +++ b/panel/views/fields/email.php @@ -10,9 +10,9 @@ 'name' => $field->formName(), 'value' => $field->value(), 'placeholder' => $field->placeholder(), - 'minlength' => $field->get('min'), - 'maxlength' => $field->get('max'), - 'pattern' => $field->get('pattern'), + 'minlength' => $field->minLength(), + 'maxlength' => $field->maxLength(), + 'pattern' => $field->pattern(), 'required' => $field->isRequired(), 'disabled' => $field->isDisabled(), 'hidden' => $field->isHidden(), diff --git a/panel/views/fields/markdown.php b/panel/views/fields/markdown.php index 75bf5e3fb..ce7045ac6 100644 --- a/panel/views/fields/markdown.php +++ b/panel/views/fields/markdown.php @@ -11,14 +11,14 @@ 'id' => $field->name(), 'name' => $field->formName(), 'placeholder' => $field->placeholder(), - 'minlength' => $field->get('min'), - 'maxlength' => $field->get('max'), - 'autocomplete' => $field->get('autocomplete', 'off'), - 'spellcheck' => $field->get('spellcheck', 'false'), - 'rows' => $field->get('rows', 15), + 'minlength' => $field->minLength(), + 'maxlength' => $field->maxLength(), + 'autocomplete' => $field->autocomplete() ? 'on' : 'off', + 'spellcheck' => $field->spellcheck() ? 'true' : 'false', + 'rows' => $field->rows(), 'required' => $field->isRequired(), 'disabled' => $field->isDisabled(), 'hidden' => $field->isHidden(), 'data-base-uri' => $field?->parent()?->model()?->uri(), ]) ?>>escape($field->value() ?? '') ?> -
+
\ No newline at end of file diff --git a/panel/views/fields/page.php b/panel/views/fields/page.php index 401242758..3e1f9aa88 100644 --- a/panel/views/fields/page.php +++ b/panel/views/fields/page.php @@ -15,7 +15,7 @@ 'selected' => $field->value() === '', ]) ?>>translate('page.none') ?> - get('allowSite')) : ?> + allowSite()) : ?>