- Add Assert\Choice on ClientTicket type and status with typed constants - Add Assert\Url on GiteaConfiguration, BookStackConfiguration, TaskBookStackLink, ClientTicket - Fix concurrent task/ticket numbering: use pg_advisory_xact_lock instead of FOR UPDATE with MAX() - Wrap CreateTaskTool numbering in transaction - Harmonize repository contracts: both return max number, caller adds +1 Tickets: T-004, T-008, T-011, T-012 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
46 lines
1.4 KiB
PHP
46 lines
1.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Repository;
|
|
|
|
use App\Entity\ClientTicket;
|
|
use App\Entity\Project;
|
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
|
use Doctrine\Persistence\ManagerRegistry;
|
|
|
|
/**
|
|
* @extends ServiceEntityRepository<ClientTicket>
|
|
*/
|
|
class ClientTicketRepository extends ServiceEntityRepository
|
|
{
|
|
public function __construct(ManagerRegistry $registry)
|
|
{
|
|
parent::__construct($registry, ClientTicket::class);
|
|
}
|
|
|
|
/**
|
|
* Returns the max ticket number for a project, using an advisory lock
|
|
* to prevent race conditions when creating tickets concurrently.
|
|
*/
|
|
public function findMaxNumberByProjectForUpdate(Project $project): int
|
|
{
|
|
$conn = $this->getEntityManager()->getConnection();
|
|
|
|
// Use PostgreSQL advisory lock instead of FOR UPDATE
|
|
// because FOR UPDATE is not allowed with aggregate functions in PostgreSQL.
|
|
// Offset by 1000000 to avoid collision with task locks on the same project ID.
|
|
$conn->executeStatement(
|
|
'SELECT pg_advisory_xact_lock(:lockKey)',
|
|
['lockKey' => $project->getId() + 1000000],
|
|
);
|
|
|
|
$result = $conn->fetchOne(
|
|
'SELECT COALESCE(MAX(number), 0) FROM client_ticket WHERE project_id = :project',
|
|
['project' => $project->getId()],
|
|
);
|
|
|
|
return (int) $result;
|
|
}
|
|
}
|