user() !== null; } /** * Prepare the data for validation. */ protected function prepareForValidation(): void { $this->merge([ 'acceptance_criteria' => TicketAcceptanceCriteriaData::normalize( $this->input('acceptance_criteria'), ), ]); } /** * 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', 'array'], 'acceptance_criteria.summary' => ['nullable', 'string', 'max:255'], 'acceptance_criteria.items' => ['array'], 'acceptance_criteria.items.*' => ['array'], 'acceptance_criteria.items.*.text' => ['required', 'string'], 'acceptance_criteria.items.*.checked' => ['required', 'boolean'], 'acceptance_criteria.items.*.indent' => ['required', 'integer', 'min:0', 'max:'.TicketAcceptanceCriteriaData::MAX_INDENT_LEVEL], 'assignee_user_id' => ['nullable', 'integer', Rule::exists('users', 'id')], 'author_user_id' => ['nullable', 'integer', Rule::exists('users', 'id')], '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'], 'story_points' => ['nullable', 'integer', 'min:0'], ]; } /** * @return array */ public function after(): array { return [ function (Validator $validator): void { /** @var array $validated */ $validated = $this->safe()->all(); /** @var Project $project */ $project = $this->route('project'); /** @var Ticket $ticket */ $ticket = $this->route('ticket'); $canManageProject = app(PermissionService::class) ->allows($this->user(), Permission::ProjectAdmin, $project); $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?->is_parent) { $validator->errors()->add('parent_ticket_id', 'Only tickets with a parent-enabled type 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?->is_parent && $ticket->childTickets()->exists() ) { $validator->errors()->add('ticket_type_id', 'Tickets with child tickets must keep a parent-enabled 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.'); } if ( Arr::exists($validated, 'author_user_id') && (int) Arr::get($validated, 'author_user_id') !== (int) $ticket->author_user_id && ! $canManageProject ) { $validator->errors()->add('author_user_id', 'Only project admins can change the author.'); } }, ]; } 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; } }