| @@ -1,52 +1,90 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | namespace FluentCart\App\Http\Requests; |
| 4 | 4 | |
| 5 | +use FluentCart\App\Models\AttributeGroup; | |
| 5 | 6 | use FluentCart\Framework\Foundation\RequestGuard; |
| 7 | +use FluentCart\Framework\Support\Arr; | |
| 6 | 8 | |
| 7 | 9 | class AttrTermRequest extends RequestGuard |
| 8 | 10 | { |
| 9 | - | |
| 10 | - /** | |
| 11 | - * @return array | |
| 12 | - */ | |
| 13 | 11 | public function rules() |
| 14 | 12 | { |
| 15 | - $termId = $this->get('term_id'); | |
| 16 | - $tbl = 'fct_atts_terms'; | |
| 13 | + // Term slugs are uniquely scoped per group at the DB layer (composite UNIQUE | |
| 14 | + // index on (group_id, slug) in AttributeTermsMigrator), so two groups can | |
| 15 | + // share a term slug ("red" in Color and "red" in Theme). The Resource layer | |
| 16 | + // handles per-group dedup + auto-suffixing; let the DB enforce final uniqueness | |
| 17 | + // instead of duplicating a global validator rule that would over-reject. | |
| 18 | + $rules = [ | |
| 19 | + 'terms' => ['required', function ($_, $value) { | |
| 20 | + if (\is_array($value) && \count($value) > 10) { | |
| 21 | + return esc_html__('Cannot create more than 10 terms at once.', 'fluent-cart'); | |
| 22 | + } | |
| 23 | + }], | |
| 24 | + 'terms.*.title' => 'required|sanitizeText|maxLength:50', | |
| 25 | + 'terms.*.settings' => 'nullable', | |
| 26 | + ]; | |
| 17 | 27 | |
| 28 | + // Type-aware settings validation. The group's `settings.type` decides | |
| 29 | + // whether each term row needs a color (hex string) or image (URL) — | |
| 30 | + // an `image` group with no thumbnail would render an empty swatch in | |
| 31 | + // the picker chips, and a `color` group with no hex breaks the dot. | |
| 32 | + $groupType = $this->getGroupType(); | |
| 33 | + if ($groupType === 'color') { | |
| 34 | + // The sanitize() map runs `sanitize_hex_color` on this field, | |
| 35 | + // which returns an empty string for anything that isn't a valid | |
| 36 | + // `#rgb` / `#rrggbb` — so `required` here doubles as the hex | |
| 37 | + // validator without a separate regex rule. | |
| 38 | + $rules['terms.*.settings.color'] = ['required']; | |
| 39 | + } elseif ($groupType === 'image') { | |
| 40 | + $rules['terms.*.settings.image'] = ['required', 'url']; | |
| 41 | + } | |
| 42 | + | |
| 43 | + return $rules; | |
| 44 | + } | |
| 45 | + | |
| 46 | + public function messages() | |
| 47 | + { | |
| 18 | 48 | return [ |
| 19 | - 'title' => 'required|sanitizeText|maxLength:50', | |
| 20 | - 'slug' => 'required|sanitizeText|maxLength:50|unique:' . $tbl . ',slug,'.$termId.',id', | |
| 21 | - 'description' => 'nullable|sanitizeTextArea', | |
| 22 | - 'serial' => 'nullable|numeric' | |
| 49 | + 'terms.required' => esc_html__('At least one term is required.', 'fluent-cart'), | |
| 50 | + 'terms.*.title.required' => esc_html__('Each term must have a title.', 'fluent-cart'), | |
| 51 | + 'terms.*.title.maxLength' => esc_html__('Each title must be 50 characters or fewer.', 'fluent-cart'), | |
| 52 | + 'terms.*.settings.color.required' => esc_html__('A color is required for each term in a color group.', 'fluent-cart'), | |
| 53 | + 'terms.*.settings.image.required' => esc_html__('An image is required for each term in an image group.', 'fluent-cart'), | |
| 54 | + 'terms.*.settings.image.url' => esc_html__('Image must be a valid URL.', 'fluent-cart'), | |
| 23 | 55 | ]; |
| 24 | 56 | } |
| 25 | 57 | |
| 26 | - | |
| 27 | - /** | |
| 28 | - * @return array | |
| 29 | - */ | |
| 30 | - public function messages() | |
| 58 | + public function sanitize() | |
| 31 | 59 | { |
| 32 | 60 | return [ |
| 33 | - 'title.required' => esc_html__('Title is required', 'fluent-cart'), | |
| 34 | - 'slug.required' => esc_html__('Slug is required', 'fluent-cart'), | |
| 35 | - | |
| 61 | + 'terms.*.title' => 'sanitize_text_field', | |
| 62 | + // sanitize_hex_color() returns null for anything that isn't a | |
| 63 | + // valid `#rgb` / `#rrggbb` — the rules() `required` check then | |
| 64 | + // surfaces the user-facing "color is required" error. | |
| 65 | + 'terms.*.settings.color' => 'sanitize_hex_color', | |
| 66 | + 'terms.*.settings.image' => 'esc_url_raw', | |
| 36 | 67 | ]; |
| 37 | 68 | } |
| 38 | 69 | |
| 39 | - | |
| 40 | 70 | /** |
| 41 | - * @return array | |
| 71 | + * Look up the parent attribute group's type ('color' | 'image' | 'options') | |
| 72 | + * so the rules() can require the matching settings field. The {group_id} | |
| 73 | + * URL param is merged into the request inputs via WP_REST_Request::get_params(), | |
| 74 | + * so $this->get() resolves it without touching the controller signature. | |
| 75 | + * | |
| 76 | + * @return string|null | |
| 42 | 77 | */ |
| 43 | - public function sanitize() | |
| 78 | + protected function getGroupType() | |
| 44 | 79 | { |
| 45 | - return [ | |
| 46 | - 'title' => 'sanitize_text_field', | |
| 47 | - 'serial' => 'sanitize_text_field', | |
| 48 | - 'description' => 'sanitize_text_field', | |
| 49 | - 'slug' => 'sanitize_text_field' | |
| 50 | - ]; | |
| 80 | + $groupId = (int) $this->get('group_id', 0); | |
| 81 | + if ($groupId <= 0) { | |
| 82 | + return null; | |
| 83 | + } | |
| 84 | + $group = AttributeGroup::query()->find($groupId); | |
| 85 | + if (!$group) { | |
| 86 | + return null; | |
| 87 | + } | |
| 88 | + return Arr::get($group->settings ?: [], 'type'); | |
| 51 | 89 | } |
| 52 | 90 | } |