check(); } /** * Prepare the data for validation. * * @return void */ protected function prepareForValidation(): void { $this->configModel = PageConfig::query()->whereKey($this->input('config_id'))->firstOrFail(); } /** * Get the validation rules that apply to the request. * * @return array */ public function rules(): array { // phpcs:enable $rules = []; foreach ($this->configModel->config as $input) { $rules = array_merge($rules, $this->recursiveRules($input, $this->fieldName)); } //end foreach return $rules; } /** * Get rules from a specific input type. * * @param array $input * @param string $prefix * @return array * @throws \UnexpectedValueException Unrecognized dynamic type. * @phpcs:disable Generic.Metrics.CyclomaticComplexity */ private function recursiveRules(array &$input, string $prefix): array { // phpcs:enable $rules = []; $ruleKey = "$prefix.$input[key]"; switch ($input['type']) { case DynamicFieldTypes::string->value: $rules = array_merge($rules, [ $ruleKey => $this->compileValidate(DynamicFieldTypes::string->getValidation(), $input) ]); break; case DynamicFieldTypes::textarea->value: $rules = array_merge($rules, [ $ruleKey => $this->compileValidate(DynamicFieldTypes::textarea->getValidation(), $input) ]); break; case DynamicFieldTypes::wysiwyg->value: $defaultRules = explode('|', DynamicFieldTypes::wysiwyg->getValidation()); $wysiwygRules = $this->compileValidate(DynamicFieldTypes::wysiwyg->getValidation(), $input); // Get the minimum and maximum values of the ‘between’ rule and // remove them from the rules so that our custom rule WysiwygLength can apply it. $defaults = $this->extractMinMax($defaultRules); ['min' => $min, 'max' => $max] = $this->extractMinMax($wysiwygRules, $defaults); $newWysiwygRules = array_values(array_filter($wysiwygRules, fn($rule) => !( is_string($rule) && preg_match('/^(min|max|between):/', $rule) ))); $rules = array_merge($rules, [ $ruleKey => array_merge($newWysiwygRules, [ new WysiwygLength($min, $max), ]) ]); break; case DynamicFieldTypes::number->value: $rules = array_merge($rules, [ $ruleKey => $this->compileValidate(DynamicFieldTypes::number->getValidation(), $input) ]); break; case DynamicFieldTypes::boolean->value: $all = $this->all(); $modified = $this->castBooleanFields($all); $this->merge($modified); $rules = array_merge($rules, [ $ruleKey => $this->compileValidate(DynamicFieldTypes::boolean->getValidation(), $input) ]); break; case DynamicFieldTypes::url->value: $rules = array_merge($rules, [ $ruleKey => array_merge(explode("|", DynamicFieldTypes::url->getValidation()), [ // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed function ($attribute, $value, $fail) { try { ToolboxHelper::assertNoInternalUrls($attribute, $value); ToolboxHelper::assertNoInternalBoUrls($attribute, $value); } catch (ValidationException $e) { $fail(collect($e->errors())->first()[0]); } } ]), ]); break; case DynamicFieldTypes::email->value: $rules = array_merge($rules, [ $ruleKey => $this->compileValidate(DynamicFieldTypes::email->getValidation(), $input) ]); break; case DynamicFieldTypes::image->value: $imgRules = $this->compileValidate(DynamicFieldTypes::image->getValidation(), $input); $rules = array_merge($rules, [ $ruleKey => $this->pictureRules(implode("|", $imgRules)), ]); break; case DynamicFieldTypes::list->value: $rules = array_merge($rules, [ "$ruleKey.*.order" => "required|numeric|min:1", $ruleKey => $this->compileValidate(DynamicFieldTypes::list->getValidation(), $input), ]); foreach ($input['children'] as $key => $children) { $rules = array_merge($rules, $this->recursiveRules($children, $ruleKey . ".*")); } break; case DynamicFieldTypes::file->value: $imgRules = $this->compileValidate(DynamicFieldTypes::file->getValidation(), $input); $rules = array_merge($rules, [ $ruleKey => $this->pictureRules(implode("|", $imgRules)), ]); break; case DynamicFieldTypes::vimeoid->value: $rules = array_merge($rules, [ $ruleKey => $this->compileValidate(DynamicFieldTypes::vimeoid->getValidation(), $input) ]); break; case DynamicFieldTypes::youtubeid->value: $rules = array_merge($rules, [ $ruleKey => $this->compileValidate(DynamicFieldTypes::youtubeid->getValidation(), $input) ]); break; case DynamicFieldTypes::date->value: $rules = array_merge($rules, [ $ruleKey => $this->compileValidate(DynamicFieldTypes::date->getValidation(), $input) ]); break; case DynamicFieldTypes::color->value: $rules = array_merge($rules, [ $ruleKey => $this->compileValidate(DynamicFieldTypes::color->getValidation(), $input) ]); break; case DynamicFieldTypes::model->value: // Set a new rule key for the model name. $ruleKeyReserved = preg_replace('/([^.\s]+)$/', 'reserved_$1', $ruleKey); $rules = array_merge($rules, [ $ruleKey => $this->compileValidate(DynamicFieldTypes::model->getValidation(), $input), $ruleKeyReserved => 'required|string|between:3,255', ]); break; case DynamicFieldTypes::staticroute->value: $rules = array_merge($rules, [ $ruleKey => array_merge( $this->compileValidate(DynamicFieldTypes::staticroute->getValidation(), $input), [new Enum(StaticRoutesEnum::class)], ), ]); break; // Specific inputs. case DynamicFieldTypes::position->value: $rules = array_merge($rules, [ $ruleKey => array_merge( $this->compileValidate(DynamicFieldTypes::position->getValidation(), $input), [new Enum(ElementPositionEnum::class)], ), ]); break; case DynamicFieldTypes::contactform->value: $rules = array_merge($rules, [ $ruleKey => array_merge( $this->compileValidate(DynamicFieldTypes::position->getValidation(), $input), [new Enum(ContactFormType::class)], ), ]); break; case DynamicFieldTypes::video->value: $rules = array_merge($rules, [ $ruleKey => new Video(), ]); break; default: throw new \UnexpectedValueException("Unrecognized dynamic type: " . ($input['type'] ?? 'null')); } //end switch return $rules; } /** * Get custom attributes for validator errors. * * @return array */ public function attributes(): array { $attributes = []; foreach ($this->configModel->config as $input) { $attributes = array_merge($attributes, $this->recursiveAttributes($input, $this->fieldName)); } return $attributes; } /** * Get attributes from a specific input type. * * @param array $input * @param string $prefix * @return array */ private function recursiveAttributes(array &$input, string $prefix): array { $attributes = []; $attributeKey = sprintf("%s.%s", $prefix, $input['key']); $attributes = array_merge($attributes, [$attributeKey => Str::lower($input['label'])]); if (isset($input['children'])) { foreach ($input['children'] as $key => $children) { $attributes = array_merge($attributes, $this->recursiveAttributes($children, $attributeKey . ".*")); } } return $attributes; } /** * Merge native validations and custom validations. * * @param string|array $defaultValidations * @param array $input * @return array */ private function compileValidate(string|array $defaultValidations, array $input): array { $newValidations = $this->mergeValidations($defaultValidations, $input); // Get unique rule and priorise important rules first. $rules = \collect($newValidations)->unique()->sort(function ($valueA, $valueB) { $valueA = $this->ruleHasToComeFirst($valueA); $valueB = $this->ruleHasToComeFirst($valueB); return $valueA - $valueB; }); // Conditions the rules. $hasRequire = $rules->first(fn($rule) => $rule === 'required') ? true : false; $hasRequireCond = $rules->first(fn($rule) => \strpos($rule, 'required_') !== false) ? true : false; $hasNullable = $rules->first(fn($rule) => \strpos($rule, 'nullable') !== false) ? true : false; $hasSometime = $rules->first(fn($rule) => \strpos($rule, 'sometimes') !== false) ? true : false; $hasPresent = $rules->first(fn($rule) => \strpos($rule, 'present') !== false) ? true : false; if ($hasRequire and ($hasRequireCond or $hasNullable or $hasSometime or $hasPresent)) { $rules = $rules->reject(fn($rule) => $rule === 'required'); } return $rules->all(); } /** * Merge new validations into default validations. * * @param string|array $defaultValidations * @param array $input * @return array */ private function mergeValidations(string|array $defaultValidations, array $input): array { if (\is_string($defaultValidations)) { $defaultValidations = explode('|', $defaultValidations); } /** @var array|string $inputValidations */ $inputValidations = \collect($input)->get('validation', $input); $inputValidations = \is_string($inputValidations) ? explode('|', $inputValidations) : $inputValidations; foreach ($inputValidations as $validate) { // Split a string by a special character. $parts = explode(':', $validate, 2); // Check if there is a special character. if (count($parts) > 1) { [$ruleName, $ruleValue] = $parts; // Deletes any old rule that starts with “ruleName:”. $defaultValidations = collect($defaultValidations) ->reject(function ($existingRule) use ($ruleName) { return is_string($existingRule) && str_starts_with($existingRule, sprintf('%s:', $ruleName)); }) ->values() ->all(); // Add the new rule. $defaultValidations[] = $validate; } else { // Check if rule without special character already exist. if (!in_array($validate, $defaultValidations)) { // Add the input rule. $defaultValidations[] = $validate; } } } //end foreach return $defaultValidations; } /** * Does the rule need to come first ? * * @param string $rule * @return integer */ private function ruleHasToComeFirst(string $rule): int { $rule = \collect(\explode(':', $rule, 2))->filter()->first(null, ''); $rules = \collect([ 'required_if', 'required_unless', 'required_with', 'required_with_all', 'required_without', 'required_without_all', 'required_with_all', 'missing', 'missing_if', 'missing_unless', 'missing_with', 'missing_with_all', 'nullable', 'sometimes', 'present', 'required' ]); $itemKey = $rules->search(fn(string $firstRule) => \strpos($rule, $firstRule) === 0); if (\is_bool($itemKey)) { return $rules->count() + 1; } return $itemKey; } /** * Recursively traverses an array and converts it to Boolean * all values whose key begins with “boolean_”. * * @param array $data * @return array */ private function castBooleanFields(array $data): array { foreach ($data as $key => $value) { // If the value is an array, recursive method. if (is_array($value)) { $data[$key] = $this->castBooleanFields($value); continue; } // Check if the key start with the pattern enum_. if (str_starts_with($key, sprintf("%s_", DynamicFieldTypes::boolean->name))) { // Accepts boolean-like values (true/false, one/zero, on/off) as strings or numbers. $data[$key] = filter_var($value, FILTER_VALIDATE_BOOLEAN); } } return $data; } /** * Extract min and max values from laravel validation. * * @param array $rules * @param array|null $fallback * @return array */ private function extractMinMax(array $rules, ?array $fallback = null): array { $min = $fallback['min'] ?? null; $max = $fallback['max'] ?? null; foreach ($rules as $rule) { if (str_starts_with($rule, 'between:')) { [$min, $max] = array_map( 'intval', explode(',', substr($rule, 8)) ); return compact('min', 'max'); } if (str_starts_with($rule, 'min:')) { $min = (int) substr($rule, 4); } if (str_starts_with($rule, 'max:')) { $max = (int) substr($rule, 4); } } return compact('min', 'max'); } }