Laravel Virgin: Setting up and destroying database in phpunit integration tests - laravel

Using the nilportuguess' eloquent repository library, I made the following (with bugs) repository:
namespace App\Repositories;
use NilPortugues\Foundation\Infrastructure\Model\Repository\Eloquent\EloquentRepository;
use App\Model\Rover;
class RoverRepository extends EloquentRepository
{
/**
* {#inheritdoc}
*/
protected function modelClassName()
{
return Rover::class;
}
/**
* {#inheritdoc}
*/
public function find(Identity $id, Fields $fields = null)
{
$eloquentModel = parent::find($id, $fields);
return $eloquentModel->toArray();
}
/**
* {#inheritdoc}
*/
public function findBy(Filter $filter = null, Sort $sort = null, Fields $fields = null)
{
$eloquentModelArray = parent::findBy($filter, $sort, $fields);
return $this->fromEloquentArray($eloquentModelArray);
}
/**
* {#inheritdoc}
*/
public function findAll(Pageable $pageable = null)
{
$page = parent::findAll($pageable);
return new Page(
$this->fromEloquentArray($page->content()),
$page->totalElements(),
$page->pageNumber(),
$page->totalPages(),
$page->sortings(),
$page->filters(),
$page->fields()
);
}
/**
* #param array $eloquentModelArray
* #return array
*/
protected function fromEloquentArray(array $eloquentModelArray) :array
{
$results = [];
foreach ($eloquentModelArray as $eloquentModel) {
//This is required to handle findAll returning array, not objects.
$eloquentModel = (object) $eloquentModel;
$results[] = $eloquentModel->attributesToArray();
}
return $results;
}
}
And In order to locate them I thought to make an Integration test on an sqlite inmemory db:
namespace Test\Database\Integration\Repositories;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Repositories\RoverRepository;
use App\Model\Rover;
use App\Model\Grid;
class RoverRepositoryTest extends TestCase
{
use RefreshDatabase;
private $repository=null;
public function setUp(): void
{
parent::setUp();
$grid=factory(Grid::class)->create([
'width'=>5,
'height'=>5
]);
$rover=factory(Rover::class, 5)->create([
'grid_id' => $grid->id,
'grid_pos_x' => rand(0, $grid->width),
'grid_pos_y' => rand(0, $grid->height),
]);
//How do I run Migrations and generate the db?
$this->repository = new RoverRepository();
}
public function tearDown(): void
{
parent::tearDown();
//How I truncate and destroy Database?
}
/**
* Testing Base Search
*
* #return void
*/
public function testBasicSearch(): void
{
//Some Db test
}
}
But I have some questions:
How do I save the generated via factory Models?
How do I nuke my database in tearDown()?

Related

Date and Time localization not works in Laravel-mix

I have Laravel mix installed on my server. there is a chat part on website and I use some kind of class :
class ActivityCell extends Component {
getTimestamp() {
const {message} = this.props;
return (
<span className="font-weight-semi-bold">
{utcDateCalendarTime(message.created_at)}
</span>
);
}
And here is my AppServiceProvider.php file :
<?php
namespace App\Providers;
use Illuminate\Http\Resources\Json\Resource;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function boot()
{
setlocale(LC_ALL, Config::get('app.lc_all'));
Carbon::setLocale(Config::get('app.locale'));
}
public function register()
{
$this->registerPlugins();
}
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
$this->bootDatabase();
$this->bootResource();
}
/**
* Boot database schema
*
* #return void
*/
private function bootDatabase()
{
Schema::defaultStringLength(191);
}
/**
* Boot resource
*
* #return void
*/
private function bootResource()
{
Resource::withoutWrapping();
}
/**
* Register plugins
*
* #return void
*/
private function registerPlugins()
{
$pluginDirs = File::directories(base_path('app/Plugins'));
foreach ($pluginDirs as $pluginDir) {
$class = "App\\Plugins\\" . basename($pluginDir) . "\\PluginServiceProvider";
if (class_exists($class) && is_subclass_of($class, ServiceProvider::class)) {
$this->app->register($class);
}
}
}
}
I tried to put setlocale(LC_TIME, 'tr'); on top of the class file but there is no success. Then tried to use carbon in order to make the date is viewed in different languages when I change the website language.
I added the following codes in app/config.php :
'locale' => env('APP_LOCALE', 'az'),
'lc_all' => env('APP_LC_ALL', 'az_AZ.UTF-8'),
and added following to the env file :
APP_LOCALE = az
APP_LC_ALL = az_AZ.UTF-8
in both methods, I was not successful. I am pretty sure that I am doing a mistake somewhere but can not find where exactly. Maybe I am missing to add something else to add. Any help would be highly appreciated.
EDIT : Adding Chat.php :
<?php
namespace App\Models;
use App\Events\ChatParticipationChanged;
use App\Events\ChatUpdated;
use App\Http\Resources\ChatMessage as ChatMessageResource;
use App\Http\Resources\MarketplaceTrade as MarketplaceTradeResource;
use ArrayObject;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use JSsVPSDioNXpfRC;
use DateTimeInterface;
class Chat extends Model
{
protected $lastMessageAttribute;
protected $lastMarketplaceTradeAttribute;
/**
* The attributes that aren't mass assignable.
*
* #var array
*/
protected $guarded = [];
/**
* The event map for the model.
*
* #var array
*/
protected $dispatchesEvents = [
'updated' => ChatUpdated::class
];
/**
* Indicates if the IDs are auto-incrementing.
*
* #var bool
*/
public $incrementing = false;
/**
* Get the route key for the model.
*
* #return string
*/
protected function serializeDate(DateTimeInterface $date)
{
return $date->translatedFormat('A B M');
}
public function getRouteKeyName()
{
return 'id';
}
/**
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function creator()
{
return $this->belongsTo(User::class, 'creator_id', 'id');
}
/**
* Participants for this chat
*
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function participants()
{
return $this->hasMany(ChatParticipant::class, 'chat_id', 'id');
}
/**
* Messages for this chat
*
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function messages()
{
return $this->hasMany(ChatMessage::class, 'chat_id', 'id');
}
/**
* Update user's participation record
*
* #param User $user
*/
public function updateParticipation($user)
{
$this->participants()->where('user_id', $user->id)
->update(['last_read_at' => now()]);
broadcast(new ChatParticipationChanged($this, $user));
}
/**
* All marketplace trades hosted by this chat
*
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function marketplaceTrades()
{
return $this->hasMany(MarketplaceTrade::class, 'chat_id', 'id')
->has('buyer')->has('seller');
}
/**
* #return Model|\Illuminate\Database\Eloquent\Relations\HasMany|mixed|object|null
*/
public function getLatestMarketplaceTrade()
{
if (!isset($this->lastMarketplaceTradeAttribute)) {
$trade = $this->marketplaceTrades()->latest()->first();
$this->lastMarketplaceTradeAttribute = new MarketplaceTradeResource($trade);
}
return $this->lastMarketplaceTradeAttribute;
}
/**
* Last chat message
*
* #return ChatMessageResource|ArrayObject|mixed
*/
public function getLatestMessage()
{
if (!isset($this->lastMessageAttribute)) {
$message = $this->messages()->latest()->first();
if ($message) {
$this->lastMessageAttribute = new ChatMessageResource($message);
} else {
$this->lastMessageAttribute = new ArrayObject();
}
}
return $this->lastMessageAttribute;
}
/**
* #param User $user
* #return array
*/
public function getParticipation($user)
{
$participant = $this->participants()
->where('user_id', $user->id)->without('user')
->first();
$unreadMessagesCount = ($participant && $participant->last_read_at) ?
$this->messages()->where('user_id', '!=', $user->id)
->where('created_at', '>', $participant->last_read_at)
->count() :
$this->messages()->where('user_id', '!=', $user->id)
->count();
return [
'user_id' => $user->id,
'unread_messages_count' => $unreadMessagesCount
];
}
/**
* If user should be allowed in this chat
*
* #param User $user
* #return bool
*/
public function shouldAllowUser($user)
{
$isParticipant = $this->participants()
->where('user_id', $user->id)->exists();
return (
$isParticipant ||
$user->can('moderate_chats')
);
}
/**
* #return string
*/
public function attachmentsDir()
{
return "chats/{$this->id}/message-attachments";
}
}
The problem is on your namespace :
// Using PHP callable syntax
use Carbon\Carbon;
Or,
// Using string syntax
\Carbon\Carbon::setLocale('ru');
You also need to use translatedFormat() method on your blade for use the translate format, like :
{{ Carbon\Carbon::now()->translatedFormat('A B M') }} // утра 428 фев
You can use serializeDate() method on your model, to change timestamp column as a translated dataTime format :
use DateTimeInterface;
protected function serializeDate(DateTimeInterface $date)
{
return $date->translatedFormat('A B M');
}

how to avoid creating a slug that already exists as a Route "Symfony 4"

I have the entity Page identified by slug. Also I have the action to view a page in the Page controler :
class PageController extends AbstractController
{
/**
* #Route("/{slug}", name="fronend_page")
*/
public function show(Page $page)
{
return $this->render("font_end/page/show.html.twig", [
"page" => $page,
]);
}
}
I am looking for good practice to validate the slug ( check if exist in routes) before save it in the database without use prefixes
Example :
route exist : #route ("/blog")
check if blog exist before create slug : /{slug} = /blog
thanks
You can use the UniqueEntity annotation for checking slug for unique.
For example, in your entity add UniqueEntity annotation with the slug field.
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #UniqueEntity("slug")
*/
class Page
{
/**
* #ORM\Column(type="string", length=255, unique=true)
* #Assert\NotBlank
*/
private $slug;
public function __construct(string $slug)
{
$this->slug = $slug;
}
}
Then you can create some service or do validation in your controller.
/**
* #Route("/{slug}")
*/
public function show($slug, ValidatorInterface $validator)
{
$page = new Page($slug);
$errors = $validator->validate($author);
if (count($errors) > 0) {
// handle errors
}
// save entity
}
Update:
For checking already existing routes probably you can do something like this
public function isRouteExist(string $slug): bool
{
/** #var Symfony\Component\Routing\RouterInterface $router */
$routes = array_filter(array_map(function (\Symfony\Component\Routing\Route $route) {
if (!$route->compile()->getVariables()) {
return $route->getPath();
}
return null;
}, $router->getRouteCollection()->all()));
return in_array(sprintf('/%s', $slug), $routes, true);
}
I created a validation Symfony :
class ContainsCheckSlugValidator extends ConstraintValidator
{
private $router;
public function __construct(UrlGeneratorInterface $router)
{
$this->router = $router;
}
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof ContainsCheckSlug) {
throw new UnexpectedTypeException($constraint, ContainsCheckSlug::class);
}
// custom constraints should ignore null and empty values to allow
// other constraints (NotBlank, NotNull, etc.) take care of that
if (null === $value || "" === $value) {
return;
}
$routes = $this->router->getRouteCollection()->all();
$routes = array_map(function($route){
return $route->getPath();
},$routes);
if (in_array(sprintf("/{_locale}/%s/", $value), $routes, true)) {
$this->context->buildViolation($constraint->message)
->setParameter("{{ string }}", $value)
->addViolation();
}
}
}
Thanks #Ihor_Kostrov :)

Laravel 5.7 - Access Auth::User() inside a service provider

I need to access to the current user in a service provider of a module. The Auth::user() returns null. I know the middleware is called after the service provider and that is why this is null. Is there any solution to this problem? it is my code
namespace Modules\User\Providers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
use Nwidart\Modules\Facades\Module;
class ViewComposerProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* #var bool
*/
protected $defer = false;
/**
* Register the service provider.
*
* #return void
*/
public function boot()
{
$this->buildMenu();
$this->buildAvatar();
}
public function register()
{
dd(Auth::user());//null
}
private function buildAvatar(){
$f = Auth::user();
dd($f); // null
}
public function buildMenu()
{
view()->composer('layouts.subnavbar', function ($view) {
$t = \Nwidart\Modules\Facades\Module::getByStatus(1);
$modules = [];
foreach ($t as $item)
$modules[] = $item->name;
$view->with('modules', $modules);
});
}
/**
* Get the services provided by the provider.
*
* #return array
*/
public function provides()
{
return [];
}
}
Instead of calling the user in the provider you could make 2 view composers, 1 for the menu and 1 for the avatar
AvatarComposer.php
class AvatarComposer
{
public function compose(View $view)
{
$avatar = Auth::user()->avatar//AVATAR HERE
$view->with('avatar', $avatar);
}
}
ModuleComposer.php
class ModuleComposer
{
public function compose(View $view)
{
$t = \Nwidart\Modules\Facades\Module::getByStatus(1);
$modules = [];
foreach ($t as $item)
$modules[] = $item->name;
$view->with('modules', $modules);
}
}
and then in the boot of your provider:
//make it public for all routes
View::composer('*', AvatarComposer::class);
View::composer('layouts.subnavbar', ModuleComposer::class);

Extra data on a collection operation

Does anybody know how to add extra data on a collection?
The doc says much about how to add extra data on an item which translates into decorating the ItemNormalizer service, and it works pretty well.
But I’m struggling in finding out which normalizer to decorate when it comes to add some data on a collection of entities. The extra data could be anything: the current user logged in, a detailed pager, some debug parameters, ... that are not related to a specific entity, but rather on the request itself.
The only working solution for now is to hook on a Kernel event but that's definitely not the code I like to write:
use ApiPlatform\Core\EventListener\EventPriorities;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class SerializeListener implements EventSubscriberInterface
{
/**
* #var Security
*/
private $security;
/**
* #var NormalizerInterface
*/
private $normalizer;
public function __construct(
Security $security,
NormalizerInterface $normalizer
) {
$this->security = $security;
$this->normalizer = $normalizer;
}
public function addCurrentUser(GetResponseForControllerResultEvent $event)
{
$request = $event->getRequest();
if ($request->attributes->has('_api_respond')) {
$serialized = $event->getControllerResult();
$data = json_decode($serialized, true);
$data['hydra:user'] = $this->normalizer->normalize(
$this->security->getUser(),
$request->attributes->get('_format'),
$request->attributes->get('_api_normalization_context')
);
$event->setControllerResult(json_encode($data));
}
}
/**
* #inheritDoc
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => [
'addCurrentUser',
EventPriorities::POST_SERIALIZE,
],
];
}
}
Any ideas?
Thank you,
Ben
Alright, I finally managed to do this.
namespace App\Api;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class ApiCollectionNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
/**
* #var NormalizerInterface|NormalizerAwareInterface
*/
private $decorated;
public function __construct(NormalizerInterface $decorated)
{
if (!$decorated instanceof NormalizerAwareInterface) {
throw new \InvalidArgumentException(
sprintf('The decorated normalizer must implement the %s.', NormalizerAwareInterface::class)
);
}
$this->decorated = $decorated;
}
/**
* #inheritdoc
*/
public function normalize($object, $format = null, array $context = [])
{
$data = $this->decorated->normalize($object, $format, $context);
if ('collection' === $context['operation_type'] && 'get' === $context['collection_operation_name']) {
$data['hydra:meta'] = ['foo' => 'bar'];
}
return $data;
}
/**
* #inheritdoc
*/
public function supportsNormalization($data, $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
/**
* #inheritdoc
*/
public function setNormalizer(NormalizerInterface $normalizer)
{
$this->decorated->setNormalizer($normalizer);
}
}
# config/services.yaml
services:
App\Api\ApiCollectionNormalizer:
decorates: 'api_platform.hydra.normalizer.collection'
arguments: [ '#App\Api\ApiCollectionNormalizer.inner' ]
Keep it for the records :)

How to validate unique entities in an entity collection in symfony2

I have an entity with a OneToMany relation to another entity, when I persist the parent entity I want to ensure the children contain no duplicates.
Here's the classes I have been using, the discounts collection should not contain two products with the same name for a given client.
I have a Client entity with a collection of discounts:
/**
* #ORM\Entity
*/
class Client {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=128, nullable="true")
*/
protected $name;
/**
* #ORM\OneToMany(targetEntity="Discount", mappedBy="client", cascade={"persist"}, orphanRemoval="true")
*/
protected $discounts;
}
/**
* #ORM\Entity
* #UniqueEntity(fields={"product", "client"}, message="You can't create two discounts for the same product")
*/
class Discount {
/**
* #ORM\Id
* #ORM\Column(type="string", length=128, nullable="true")
*/
protected $product;
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Client", inversedBy="discounts")
* #ORM\JoinColumn(name="client_id", referencedColumnName="id")
*/
protected $client;
/**
* #ORM\Column(type="decimal", scale=2)
*/
protected $percent;
}
I tried using UniqueEntity for the Discount class as you can see, the problem is that it seems the validator only checks what's loaded on the database (which is empty), so when the entities are persisted I get a "SQLSTATE[23000]: Integrity constraint violation".
I have checked the Collection constraint buy it seems to handle only collections of fields, not entities.
There's also the All validator, which lets you define constraints to be applied for each entity, but not to the collection as a whole.
I need to know if there are entity collection constraints as a whole before persisting to the database, other than writing a custom validator or writing a Callback validator each time.
I've created a custom constraint/validator for this.
It validates a form collection using the "All" assertion, and takes an optional parameter : the property path of the property to check the entity equality.
(it's for Symfony 2.1, to adapt it to Symfony 2.0 check the end of the answer) :
For more information on creating custom validation constraints, check The Cookbook
The constraint :
#src/Acme/DemoBundle/Validator/constraint/UniqueInCollection.php
<?php
namespace Acme\DemoBundle\Validator\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class UniqueInCollection extends Constraint
{
public $message = 'The error message (with %parameters%)';
// The property path used to check wether objects are equal
// If none is specified, it will check that objects are equal
public $propertyPath = null;
}
And the validator :
#src/Acme/DemoBundle/Validator/constraint/UniqueInCollectionValidator.php
<?php
namespace Acme\DemoBundle\Validator\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Form\Util\PropertyPath;
class UniqueInCollectionValidator extends ConstraintValidator
{
// We keep an array with the previously checked values of the collection
private $collectionValues = array();
// validate is new in Symfony 2.1, in Symfony 2.0 use "isValid" (see below)
public function validate($value, Constraint $constraint)
{
// Apply the property path if specified
if($constraint->propertyPath){
$propertyPath = new PropertyPath($constraint->propertyPath);
$value = $propertyPath->getValue($value);
}
// Check that the value is not in the array
if(in_array($value, $this->collectionValues))
$this->context->addViolation($constraint->message, array());
// Add the value in the array for next items validation
$this->collectionValues[] = $value;
}
}
In your case, you would use it like this :
use Acme\DemoBundle\Validator\Constraints as AcmeAssert;
// ...
/**
* #ORM\OneToMany(targetEntity="Discount", mappedBy="client", cascade={"persist"}, orphanRemoval="true")
* #Assert\All(constraints={
* #AcmeAssert\UniqueInCollection(propertyPath ="product")
* })
*/
For Symfony 2.0, change the validate function by :
public function isValid($value, Constraint $constraint)
{
$valid = true;
if($constraint->propertyPath){
$propertyPath = new PropertyPath($constraint->propertyPath);
$value = $propertyPath->getValue($value);
}
if(in_array($value, $this->collectionValues)){
$valid = false;
$this->setMessage($constraint->message, array('%string%' => $value));
}
$this->collectionValues[] = $value;
return $valid
}
Here is a version working with multiple fields just like UniqueEntity does. Validation fails if multiple objects have same values.
Usage:
/**
* ....
* #App\UniqueInCollection(fields={"name", "email"})
*/
private $contacts;
//Validation fails if multiple contacts have same name AND email
The constraint class ...
<?php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class UniqueInCollection extends Constraint
{
public $message = 'Entry is duplicated.';
public $fields;
public function validatedBy()
{
return UniqueInCollectionValidator::class;
}
}
The validator itself ....
<?php
namespace App\Validator\Constraints;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class UniqueInCollectionValidator extends ConstraintValidator
{
/**
* #var \Symfony\Component\PropertyAccess\PropertyAccessor
*/
private $propertyAccessor;
public function __construct()
{
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
}
/**
* #param mixed $collection
* #param Constraint $constraint
* #throws \Exception
*/
public function validate($collection, Constraint $constraint)
{
if (!$constraint instanceof UniqueInCollection) {
throw new UnexpectedTypeException($constraint, UniqueInCollection::class);
}
if (null === $collection) {
return;
}
if (!\is_array($collection) && !$collection instanceof \IteratorAggregate) {
throw new UnexpectedValueException($collection, 'array|IteratorAggregate');
}
if ($constraint->fields === null) {
throw new \Exception('Option propertyPath can not be null');
}
if(is_array($constraint->fields)) $fields = $constraint->fields;
else $fields = [$constraint->fields];
$propertyValues = [];
foreach ($collection as $key => $element) {
$propertyValue = [];
foreach ($fields as $field) {
$propertyValue[] = $this->propertyAccessor->getValue($element, $field);
}
if (in_array($propertyValue, $propertyValues, true)) {
$this->context->buildViolation($constraint->message)
->atPath(sprintf('[%s]', $key))
->addViolation();
}
$propertyValues[] = $propertyValue;
}
}
}
For Symfony 4.3(only tested version) you can use my custom validator.
Prefered way of usage is as annotaion on validated collection:
use App\Validator\Constraints as App;
...
/**
* #ORM\OneToMany
*
* #App\UniqueProperty(
* propertyPath="entityProperty"
* )
*/
private $entities;
Difference between Julien and my solution is, that my Constraint is defined on validated Collection instead on element of Collection itself.
Constraint:
#src/Validator/Constraints/UniqueProperty.php
<?php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class UniqueProperty extends Constraint
{
public $message = 'This collection should contain only elements with uniqe value.';
public $propertyPath;
public function validatedBy()
{
return UniquePropertyValidator::class;
}
}
Validator:
#src/Validator/Constraints/UniquePropertyValidator.php
<?php
namespace App\Validator\Constraints;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class UniquePropertyValidator extends ConstraintValidator
{
/**
* #var \Symfony\Component\PropertyAccess\PropertyAccessor
*/
private $propertyAccessor;
public function __construct()
{
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
}
/**
* #param mixed $value
* #param Constraint $constraint
* #throws \Exception
*/
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof UniqueProperty) {
throw new UnexpectedTypeException($constraint, UniqueProperty::class);
}
if (null === $value) {
return;
}
if (!\is_array($value) && !$value instanceof \IteratorAggregate) {
throw new UnexpectedValueException($value, 'array|IteratorAggregate');
}
if ($constraint->propertyPath === null) {
throw new \Exception('Option propertyPath can not be null');
}
$propertyValues = [];
foreach ($value as $key => $element) {
$propertyValue = $this->propertyAccessor->getValue($element, $constraint->propertyPath);
if (in_array($propertyValue, $propertyValues, true)) {
$this->context->buildViolation($constraint->message)
->atPath(sprintf('[%s]', $key))
->addViolation();
}
$propertyValues[] = $propertyValue;
}
}
}
I can't manage to make the previous answer works on symfony 2.6. Because of the following code on l. 852 of RecursiveContextualValidator, it only goes once on the validate method when 2 items are equals.
if ($context->isConstraintValidated($cacheKey, $constraintHash)) {
continue;
}
So, here is what I've done to deals with the original issue :
On the Entity :
* #AcmeAssert\UniqueInCollection(propertyPath ="product")
Instead of
* #Assert\All(constraints={
* #AcmeAssert\UniqueInCollection(propertyPath ="product")
* })
On the validator :
public function validate($collection, Constraint $constraint){
$propertyAccessor = PropertyAccess::getPropertyAccessor();
$previousValues = array();
foreach($collection as $collectionItem){
$value = $propertyAccessor->getValue($collectionItem, $constraint->propertyPath);
$previousSimilarValuesNumber = count(array_keys($previousValues,$value));
if($previousSimilarValuesNumber == 1){
$this->context->addViolation($constraint->message, array('%email%' => $value));
}
$previousValues[] = $value;
}
}
Instead of :
public function isValid($value, Constraint $constraint)
{
$valid = true;
if($constraint->propertyPath){
$propertyAccessor = PropertyAccess::getPropertyAccessor();
$value = $propertyAccessor->getValue($value, $constraint->propertyPath);
}
if(in_array($value, $this->collectionValues)){
$valid = false;
$this->setMessage($constraint->message, array('%string%' => $value));
}
$this->collectionValues[] = $value;
return $valid
}
Can be used Unique built-in validator for Symfony >= 6.1
The fields option was introduced in Symfony 6.1.

Resources