*/ public function validate(Request $request, Ticket $ticket, User $user): array { $form = new UpdateProjectTicketRequest; $route = new Route('PUT', '/', fn () => null); $route->bind(\Illuminate\Http\Request::create('/', 'PUT')); $route->setParameter('project', $ticket->project); $route->setParameter('ticket', $ticket); $form->setRouteResolver(fn () => $route); $form->setUserResolver(fn () => $user); $rules = $form->rules(); foreach ($rules as $key => &$rule) { if (! str_contains($key, '.')) { $rule = ['sometimes', ...$rule]; } } unset($rule); $rules['description'][] = new VisualTicketDescription; $rules['acceptance_criteria'] = ['sometimes', 'array', 'list']; $rules['acceptance_criteria.*'] = ['array:id,text,checked,indent']; $rules['acceptance_criteria.*.id'] = ['sometimes', 'integer', 'min:1', 'distinct', Rule::exists('acceptance_criteria', 'id')->where('ticket_id', $ticket->id)]; $rules['acceptance_criteria.*.checked'] = ['sometimes', 'boolean']; $rules['acceptance_criteria.*.indent'] = ['sometimes', 'integer', 'min:0', 'max:'.AcceptanceCriteriaChecklistMapper::MAX_INDENT_LEVEL]; $rules['field_values'] = ['sometimes', 'array', 'list']; $rules['field_values.*'] = ['array:ticket_field_definition_id,value_number,value_user_id']; $rules['field_values.*.ticket_field_definition_id'][] = 'distinct'; $rules['ticket_status_id'] = ['prohibited']; $rules['acceptance_criteria.*.text'][] = function (string $attribute, mixed $value, \Closure $fail): void { if (is_string($value) && trim($value) === '') { $fail('Criterion text must not be blank.'); } }; $input = $request->validate($rules); $previousIndent = -1; foreach (Arr::get($input, 'acceptance_criteria', []) as $index => $item) { $indent = Arr::get($item, 'indent', 0); if ($indent > $previousIndent + 1) { throw ValidationException::withMessages(["acceptance_criteria.{$index}.indent" => 'Indentation cannot skip a level; the first item must have indent 0.']); } $previousIndent = $indent; } $merged = array_replace($this->kanbanGitMcp->updatePayload($ticket), $input); $merged['acceptance_criteria'] = Arr::exists($input, 'acceptance_criteria') ? AcceptanceCriteriaChecklistMapper::normalize($input['acceptance_criteria']) : AcceptanceCriteriaChecklistMapper::fromModelsWithIds($ticket->acceptanceCriteria()->get()); $validator = Validator::make($merged, $form->rules()); $form->setValidator($validator); $validator->validate(); foreach ($form->after() as $callback) { $callback($validator); } $people = User::query()->visibleToProject($ticket->project)->pluck('id')->all(); foreach (['assignee_user_id', 'author_user_id'] as $field) { if (Arr::exists($input, $field) && $input[$field] !== null && ! in_array((int) $input[$field], $people, true)) { $validator->errors()->add($field, 'The person must be able to view this project.'); } } $definitions = $ticket->project->ticketFieldDefinitions()->get()->keyBy('id'); foreach (Arr::get($input, 'field_values', []) as $index => $value) { $definition = $definitions->get($value['ticket_field_definition_id']); $incompatible = $definition->field_type === 'user' ? 'value_number' : 'value_user_id'; if (Arr::get($value, $incompatible) !== null) { $validator->errors()->add("field_values.{$index}.{$incompatible}", 'The value must match the custom field type.'); } if (Arr::get($value, 'value_user_id') !== null && ! in_array((int) $value['value_user_id'], $people, true)) { $validator->errors()->add("field_values.{$index}.value_user_id", 'The person must be able to view this project.'); } } if ($validator->errors()->isNotEmpty()) { throw new ValidationException($validator); } if (Arr::exists($input, 'field_values')) { $replacement = $definitions->map(fn (TicketFieldDefinition $definition): array => [ 'ticket_field_definition_id' => $definition->id, 'value_number' => null, 'value_user_id' => null, ])->all(); foreach ($input['field_values'] as $value) { $replacement[$value['ticket_field_definition_id']] = $value; } $merged['field_values'] = array_values($replacement); } return $merged; } }