user() !== null; } /** * Get the validation rules that apply to the request. * * @return array|string> */ public function rules(): array { /** @var Project $project */ $project = $this->route('project'); return [ 'acceptance_criteria' => ['nullable', 'string'], 'blocked_comment' => ['nullable', 'string'], 'description' => ['nullable', 'string'], 'field_values' => ['array'], 'field_values.*' => ['array'], 'field_values.*.ticket_field_definition_id' => [ 'required', 'integer', Rule::exists('ticket_field_definitions', 'id')->where('project_id', $project->id), ], 'field_values.*.value_number' => ['nullable', 'numeric'], 'field_values.*.value_user_id' => ['nullable', 'integer', Rule::exists('users', 'id')], 'is_blocked' => ['required', 'boolean'], 'parent_ticket_id' => [ 'nullable', 'integer', Rule::exists('tickets', 'id')->where('project_id', $project->id), ], 'ticket_priority_id' => [ 'required', 'integer', Rule::exists('ticket_priorities', 'id')->where('project_id', $project->id), ], 'ticket_type_id' => [ 'required', 'integer', Rule::exists('ticket_types', 'id')->where('project_id', $project->id), ], 'title' => ['required', 'string', 'max:255'], ]; } /** * @return array */ public function after(): array { return [ function (Validator $validator): void { /** @var array $validated */ $validated = $this->validated(); /** @var Project $project */ $project = $this->route('project'); /** @var Ticket $ticket */ $ticket = $this->route('ticket'); $parentTicketId = Arr::get($validated, 'parent_ticket_id'); if ($parentTicketId !== null) { if ($parentTicketId === $ticket->id) { $validator->errors()->add('parent_ticket_id', 'A ticket cannot be its own parent.'); return; } $parentTicket = Ticket::query() ->where('project_id', $project->id) ->with('ticketType') ->find($parentTicketId); if ($parentTicket !== null && $parentTicket->ticketType?->system_key !== 'parent') { $validator->errors()->add('parent_ticket_id', 'Only tickets of type Parent can be selected as a parent.'); return; } if ($parentTicket !== null && $this->createsCycle($ticket, $parentTicket)) { $validator->errors()->add('parent_ticket_id', 'A ticket cannot be assigned to one of its descendants.'); return; } } $requestedTicketTypeId = Arr::get($validated, 'ticket_type_id'); $requestedTicketType = TicketType::query() ->where('project_id', $project->id) ->find($requestedTicketTypeId); if ( $requestedTicketType?->system_key !== 'parent' && $ticket->childTickets()->exists() ) { $validator->errors()->add('ticket_type_id', 'Tickets with child tickets must keep a Parent type.'); } if (Arr::get($validated, 'is_blocked', false) && blank(Arr::get($validated, 'blocked_comment'))) { $validator->errors()->add('blocked_comment', 'Blocked tickets require a blocker comment.'); } }, ]; } private function createsCycle(Ticket $ticket, Ticket $candidateParent): bool { $currentParent = $candidateParent; while ($currentParent !== null) { if ($currentParent->id === $ticket->id) { return true; } if ($currentParent->parent_ticket_id === null) { return false; } $currentParent = Ticket::query()->find($currentParent->parent_ticket_id); } return false; } }