Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,112 @@ final class EmailUser
// ...
```

Sometimes we may also need to check not only whether the entity exists, but also the state (for example is an article published).
To do this need to create a new Constraint, which extends from EntityExist instead of the default Symfony Constraint class.

```php
<?php

namespace App\Validator;

use Happyr\Validator\Constraint\EntityExist;

/**
* @Annotation
*/
class ArticleEntityExist extends EntityExist
{
}

```

Next, we create a validator class. Default convention for Symfony is the fully qualified name of the constraint class suffixed with "Validator".
See `Symfony\Component\Validator\Constraint::validatedBy` for more information. You can overwrite this method to return another fully qualified name of the validator class.
In validator class you can inject other services, but not forget to call parent constructor.
The parent validator `EntityExistValidator` calls the method `checkEntity` with fetched entity.
The default implementation always returns true, so you need to overwrite this method.

```php
<?php

namespace App\Validator;

use App\Entity\Article;
use Doctrine\ORM\EntityManagerInterface;
use Happyr\Validator\Constraint\EntityExistValidator;
use Symfony\Component\Validator\Exception\UnexpectedValueException;

class ArticleEntityExistValidator extends EntityExistValidator
{
protected function checkEntity(object $entity): bool
{
if (!$entity instanceof Article) {
throw new UnexpectedValueException($entity, Article::class);
}

return $entity->isPublished();
}
}
```

Another example of validator with constructor injection:

```php
<?php

namespace App\Offer\Validator;

use App\Offer\Entity\Offer;
use App\SalesChannel\SalesChannelContext;
use Doctrine\ORM\EntityManagerInterface;
use Happyr\Validator\Constraint\EntityExistValidator;
use Symfony\Component\Validator\Exception\UnexpectedValueException;

class OfferEntityExistValidator extends EntityExistValidator
{
private SalesChannelContext $salesChannelContext;

public function __construct(SalesChannelContext $salesChannelContext, EntityManagerInterface $entityManager)
{
parent::__construct($entityManager);

$this->salesChannelContext = $salesChannelContext;
}

protected function checkEntity(object $entity): bool
{
if (!$entity instanceof Offer) {
throw new UnexpectedValueException($entity, Offer::class);
}

return $entity->isVisible()
&& $this->salesChannelContext->getCurrentSalesChannel()->equals($entity->getSalesChannel());
}
}

```

Finally, in your entity or DTO, use a new constraint instead of `EntityExist`.

```php
<?php

namespace App\Dto;

use App\Validator\ArticleEntityExist;
use Symfony\Component\Validator\Constraints as Assert;

class CommentArticleRequestDto
{
/**
* @Assert\NotBlank()
* @ArticleEntityExist(entity="App\Entity\Article")
*/
public int $articleId;

//....
```

## Install

```console
Expand Down
2 changes: 1 addition & 1 deletion src/EntityExist.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*
* @author Radoje Albijanic <radoje.albijanic@gmail.com>
*/
final class EntityExist extends Constraint
class EntityExist extends Constraint
{
public $message = 'Entity "%entity%" with property "%property%": "%value%" does not exist.';
public $property = 'id';
Expand Down
10 changes: 8 additions & 2 deletions src/EntityExistValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Radoje Albijanic <radoje@blackmountainlabs.me>
* @author Marcin Morawski <marcin@morawskim.pl>
*/
final class EntityExistValidator extends ConstraintValidator
class EntityExistValidator extends ConstraintValidator
{
private $entityManager;

Expand All @@ -39,12 +40,17 @@ public function validate($value, Constraint $constraint): void
$constraint->property => $value,
]);

if (null === $data) {
if (null === $data || !$this->checkEntity($data)) {
$this->context->buildViolation($constraint->message)
->setParameter('%entity%', $constraint->entity)
->setParameter('%property%', $constraint->property)
->setParameter('%value%', (string) $value)
->addViolation();
}
}

protected function checkEntity(object $entity): bool
{
return true;
}
}
46 changes: 43 additions & 3 deletions tests/EntityExistValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public function testValidateValidEntity(): void
->expects($this->once())
->method('findOneBy')
->with(['id' => 'foobar'])
->willReturn('my_user');
->willReturn((object) ['username' => 'my_user']);

$this->entityManager
->expects($this->once())
Expand Down Expand Up @@ -90,7 +90,7 @@ public function testValidateSkipsIfValueEmptyOrNull($value): void
->expects($this->exactly(0))
->method('findOneBy')
->with(['id' => $value])
->willReturn('my_user');
->willReturn((object) ['username' => 'my_user']);

$this->entityManager
->expects($this->exactly(0))
Expand Down Expand Up @@ -122,7 +122,7 @@ public function testValidateValidEntityWithCustomProperty(): void
->expects($this->once())
->method('findOneBy')
->with(['uuid' => 'foobar'])
->willReturn('my_user');
->willReturn((object) ['username' => 'my_user']);

$this->entityManager
->expects($this->once())
Expand Down Expand Up @@ -158,4 +158,44 @@ public function testValidateInvalidEntity(): void

$this->validator->validate(1, $constraint);
}

public function testCheckEntityCall(): void
{
$violationBuilder = $this->getMockBuilder(ConstraintViolationBuilderInterface::class)->getMock();
$violationBuilder->method('setParameter')->will($this->returnSelf());
$this->context->expects($this->once())->method('buildViolation')->willReturn($violationBuilder);

$constraint = new EntityExist();
$constraint->entity = 'App\Entity\User';

$repository = $this->getMockBuilder(EntityRepository::class)
->disableOriginalConstructor()
->getMock();

$repository
->expects($this->once())
->method('findOneBy')
->with(['id' => 'foobar'])
->willReturn((object) ['username' => 'my_user']);

$this->entityManager
->expects($this->once())
->method('getRepository')
->with('App\Entity\User')
->willReturn($repository);

$validator = $this->getValidatorWhichAlwaysReturnFalseForCheckEntity($this->entityManager);
$validator->initialize($this->context);
$validator->validate('foobar', $constraint);
}

private function getValidatorWhichAlwaysReturnFalseForCheckEntity(EntityManagerInterface $entityManager): EntityExistValidator
{
return new class($entityManager) extends EntityExistValidator {
protected function checkEntity(object $entity): bool
{
return false;
}
};
}
}