Skip to content

Commit 358de89

Browse files
committed
Add: UniqueTemplateTitleValidator
1 parent c5fd9c1 commit 358de89

6 files changed

Lines changed: 175 additions & 0 deletions

File tree

config/services/validators.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ services:
2727
PhpList\RestBundle\Messaging\Validator\Constraint\ContainsPlaceholderValidator:
2828
tags: ['validator.constraint_validator']
2929

30+
PhpList\RestBundle\Messaging\Validator\Constraint\UniqueTemplateTitleValidator:
31+
autowire: true
32+
autoconfigure: true
33+
tags: [ 'validator.constraint_validator' ]
34+
3035
PhpList\RestBundle\Identity\Validator\Constraint\UniqueLoginNameValidator:
3136
autowire: true
3237
autoconfigure: true

src/Messaging/Request/CreateTemplateRequest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PhpList\Core\Domain\Messaging\Model\Dto\CreateTemplateDto;
99
use PhpList\RestBundle\Common\Request\RequestInterface;
1010
use PhpList\RestBundle\Messaging\Validator\Constraint\ContainsPlaceholder;
11+
use PhpList\RestBundle\Messaging\Validator\Constraint\UniqueTemplateTitle;
1112
use Symfony\Component\HttpFoundation\File\UploadedFile;
1213
use Symfony\Component\Validator\Constraints as Assert;
1314

@@ -54,6 +55,7 @@ class CreateTemplateRequest implements RequestInterface
5455
{
5556
#[Assert\NotBlank(normalizer: 'trim')]
5657
#[Assert\NotNull]
58+
#[UniqueTemplateTitle]
5759
public string $title;
5860

5961
#[ContainsPlaceholder]

src/Messaging/Request/UpdateTemplateRequest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PhpList\Core\Domain\Messaging\Model\Dto\UpdateTemplateDto;
1010
use PhpList\RestBundle\Common\Request\RequestInterface;
1111
use PhpList\RestBundle\Messaging\Validator\Constraint\ContainsPlaceholder;
12+
use PhpList\RestBundle\Messaging\Validator\Constraint\UniqueTemplateTitle;
1213
use Symfony\Component\HttpFoundation\File\UploadedFile;
1314
use Symfony\Component\Validator\Constraints as Assert;
1415

@@ -53,8 +54,11 @@
5354
)]
5455
class UpdateTemplateRequest implements RequestInterface
5556
{
57+
public ?int $templateId = null;
58+
5659
#[Assert\NotBlank(normalizer: 'trim')]
5760
#[Assert\NotNull]
61+
#[UniqueTemplateTitle]
5862
public string $title;
5963

6064
#[ContainsPlaceholder]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Messaging\Validator\Constraint;
6+
7+
use Symfony\Component\Validator\Constraint;
8+
9+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
10+
class UniqueTemplateTitle extends Constraint
11+
{
12+
public string $message = 'Template title already exists.';
13+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Messaging\Validator\Constraint;
6+
7+
use PhpList\Core\Domain\Messaging\Repository\TemplateRepository;
8+
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
9+
use Symfony\Component\Validator\Constraint;
10+
use Symfony\Component\Validator\ConstraintValidator;
11+
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
12+
use Symfony\Component\Validator\Exception\UnexpectedValueException;
13+
14+
class UniqueTemplateTitleValidator extends ConstraintValidator
15+
{
16+
public function __construct(private readonly TemplateRepository $templateRepository)
17+
{
18+
}
19+
20+
public function validate($value, Constraint $constraint): void
21+
{
22+
if (!$constraint instanceof UniqueTemplateTitle) {
23+
throw new UnexpectedTypeException($constraint, UniqueTemplateTitle::class);
24+
}
25+
26+
if (null === $value || '' === $value) {
27+
return;
28+
}
29+
30+
if (!is_string($value)) {
31+
throw new UnexpectedValueException($value, 'string');
32+
}
33+
34+
$existingTemplate = $this->templateRepository->findOneBy(['title' => $value]);
35+
$dto = $this->context->getObject();
36+
$updatingId = $dto->templateId ?? null;
37+
38+
if ($existingTemplate && (null === $updatingId || $existingTemplate->getId() !== $updatingId)) {
39+
throw new ConflictHttpException('Template title already exists.');
40+
}
41+
}
42+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Tests\Unit\Messaging\Validator\Constraint;
6+
7+
use PhpList\Core\Domain\Messaging\Model\Template;
8+
use PhpList\Core\Domain\Messaging\Repository\TemplateRepository;
9+
use PhpList\RestBundle\Messaging\Validator\Constraint\UniqueTemplateTitle;
10+
use PhpList\RestBundle\Messaging\Validator\Constraint\UniqueTemplateTitleValidator;
11+
use PHPUnit\Framework\MockObject\MockObject;
12+
use PHPUnit\Framework\TestCase;
13+
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
14+
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\Context\ExecutionContextInterface;
16+
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
17+
use Symfony\Component\Validator\Exception\UnexpectedValueException;
18+
19+
class UniqueTemplateTitleValidatorTest extends TestCase
20+
{
21+
private TemplateRepository&MockObject $templateRepository;
22+
private UniqueTemplateTitleValidator $validator;
23+
private ExecutionContextInterface&MockObject $context;
24+
25+
protected function setUp(): void
26+
{
27+
$this->templateRepository = $this->createMock(TemplateRepository::class);
28+
$this->context = $this->createMock(ExecutionContextInterface::class);
29+
30+
$this->validator = new UniqueTemplateTitleValidator($this->templateRepository);
31+
$this->validator->initialize($this->context);
32+
}
33+
34+
public function testValidateSkipsNull(): void
35+
{
36+
$this->templateRepository->expects($this->never())->method('findOneBy');
37+
$this->validator->validate(null, new UniqueTemplateTitle());
38+
$this->assertTrue(true);
39+
}
40+
41+
public function testValidateSkipsEmptyString(): void
42+
{
43+
$this->templateRepository->expects($this->never())->method('findOneBy');
44+
$this->validator->validate('', new UniqueTemplateTitle());
45+
$this->assertTrue(true);
46+
}
47+
48+
public function testValidateThrowsUnexpectedTypeException(): void
49+
{
50+
$this->expectException(UnexpectedTypeException::class);
51+
$this->validator->validate('title', $this->createMock(Constraint::class));
52+
}
53+
54+
public function testValidateThrowsUnexpectedValueException(): void
55+
{
56+
$this->expectException(UnexpectedValueException::class);
57+
$this->validator->validate(123, new UniqueTemplateTitle());
58+
}
59+
60+
public function testValidateThrowsConflictHttpExceptionIfTemplateTitleExists(): void
61+
{
62+
$existingTemplate = $this->createMock(Template::class);
63+
64+
$this->templateRepository
65+
->expects($this->once())
66+
->method('findOneBy')
67+
->with(['title' => 'Newsletter Template'])
68+
->willReturn($existingTemplate);
69+
70+
$this->expectException(ConflictHttpException::class);
71+
$this->expectExceptionMessage('Template title already exists.');
72+
73+
$this->validator->validate('Newsletter Template', new UniqueTemplateTitle());
74+
}
75+
76+
public function testValidatePassesIfTemplateTitleIsUnique(): void
77+
{
78+
$this->templateRepository
79+
->expects($this->once())
80+
->method('findOneBy')
81+
->with(['title' => 'Unique Template'])
82+
->willReturn(null);
83+
84+
$this->validator->validate('Unique Template', new UniqueTemplateTitle());
85+
$this->assertTrue(true);
86+
}
87+
88+
public function testValidateSkipsConflictForSameTemplateOnUpdate(): void
89+
{
90+
$existingTemplate = $this->createConfiguredMock(Template::class, ['getId' => 10]);
91+
92+
$this->templateRepository
93+
->expects($this->once())
94+
->method('findOneBy')
95+
->with(['title' => 'Existing Title'])
96+
->willReturn($existingTemplate);
97+
98+
$dto = new class {
99+
public int $templateId = 10;
100+
};
101+
102+
$this->context
103+
->method('getObject')
104+
->willReturn($dto);
105+
106+
$this->validator->validate('Existing Title', new UniqueTemplateTitle());
107+
$this->assertTrue(true);
108+
}
109+
}

0 commit comments

Comments
 (0)