Symfony 3 UniqueEntity validation on update - symfony

I am facing an issue with UniqueEntity validation.
I have a field "internal_asset_number" which should be unique and it's working fine on create. On update when i edit the existing current data with the same values, it shows "There is already an asset with that internal number!" but it shouldn't because it's the same entry.
The entity:
/**
* Asset
*
* #ORM\Table(schema="assets", name="asset", uniqueConstraints= {#ORM\UniqueConstraint(name="uk_asset_internal_asset_number_client_id", columns={"internal_asset_number", "client_id"})})
* #ORM\Entity(repositoryClass="Api\AssetBundle\Entity\AssetRepository")
* #UniqueEntity(fields={"internalAssetNumber"}, groups={"post", "put"}, message="There is already an asset with that internal number!")
*/
class Asset
{
/**
* #var guid
*
* #ORM\Column(name="id", type="string")
* #ORM\Id
* #ORM\GeneratedValue(strategy="UUID")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="client_id", type="string", length=255, nullable=false)
*/
private $clientId;
/**
* #var string
*
* #ORM\Column(name="internal_asset_number", type="string", length=255, nullable=true, unique=true)
*/
private $internalAssetNumber;
Update method:
public function putAssetAction(Request $request, $id)
{
$data = $this->deserializer('Api\AssetBundle\Entity\Asset', $request, 'put');
if ($data instanceof \Exception) {
return View::create(['error' => $data->getMessage()], 400);
}
$validator = $this->get('validator');
$errors = $validator->validate($data, null, 'put');
if (count($errors) > 0) {
$errorsResponse = [];
foreach ($errors as $error) {
$errorsResponse = $error->getMessage();
}
return View::create(array('error' => $errorsResponse), 400);
}
...

As #xabbuh commented, the problem is that the entity you persist after update is not retrieved through the entityManager so when you persist it the entity manager thinks it is a new entity.
Here is how to solve it:
$entityManager->merge($entity);
This will tell the entitymanager to merge your serialized entity with the managed one
Some more explanation on merge():
https://stackoverflow.com/a/15838232/5758328

Related

Symfony 2.8 Doctrine. Using composite primary key and ManyToMany associations

I have 2 entities - Platform and Product. In the Products table I have composite primary key by [Product ID + Platform ID]. One product can be present at many platforms so and one platform can contain many products, so the association is ManyToMany.
The Platform entity:
/**
* Platform
*
* #ORM\Table(name="Platforms")
*/
class Platform
{
/**
* #var int
*
* #ORM\Column(name="Platform_Id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="Platform_Name", type="string", length=64)
*/
private $name;
/**
* #ORM\ManyToMany(targetEntity="Product", mappedBy="platforms", cascade={"ALL"}, indexBy="numpp")
*/
protected $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
public function addProducts($numpp)
{
$this->products[$numpp] = new Product($numpp, $this);
}
The Product entity:
/**
* Product
*
* #ORM\Table(name="Products")
*/
class Product
{
/**
* #var string
*
* #ORM\Column(name="Numpp", type="string", length=6)
* #ORM\Id
*/
private $numpp;
/**
* #var int
*
* #ORM\ManyToMany(targetEntity="Platform", inversedBy="products")
* #ORM\JoinColumn(name="Platform_Id", referencedColumnName="Platform_Id")
* #ORM\Id
*/
private $platforms;
public function __construct($numpp, Platform $platform)
{
$this->numpp = $numpp;
$this->platforms = new ArrayCollection();
$this->platforms[] = $platform;
}
In my controller when trying to create new Product entity...
$em = $this->getDoctrine()->getManager();
$platform = $em->getRepository("AGAAnalyticsBundle:Platform")->find(1);
$product = new Product('05062', $platform);
$em->persist($product);
$em->flush();
I get an error - Cannot insert the value NULL into column 'Platform_Id', table 'dbo.Products'
And other way using addProduct method...
$em = $this->getDoctrine()->getManager();
$platform = $em->getRepository("AGAAnalyticsBundle:Platform")->find(1);
$platform->addProduct('05062');
$em->flush();
I get an error - The column id must be mapped to a field in class AGA\AnalyticsBundle\Entity\Platform since it is referenced by a join column of another class.
Please help to understand where I am wrong and how I should build this relation between my entities correctly.

I get friends and all other users when trying to load only friends of the current user

I am trying to load friends of a specific user after asking why I get null values of the friend of the user data it was responded that it was because of the lazy loading. So I was advised to add JOIN and I admit this was a miss from my side. But after adding the JOIN I get the data of the friend in the result and then I receive all the users from my users table for which I have not asked.
I have already tried removing the myuser from the SELECT, but this way I get the lazy loading problem again. I have tried LEFT JOIN (I admit it was dumb try from my side). But how can I correct this when there is no ON in the Doctrine Query Language.
My Entity(Friends):
class Friends
{
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\User", inversedBy="myfriends")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false)
*/
private $friendsWithMe;
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\User", inversedBy="friendof")
* #ORM\JoinColumn(name="friend_id", referencedColumnName="id", nullable=false)
*/
private $afriendof;
/**
* #var integer
*
* #ORM\Column(name="status", type="smallint")
*/
private $status;
/**
* #return User
*/
public function getFriendsWithMe()
{
return $this->friendsWithMe;
}
/**
* #param mixed $friendsWithMe
*/
public function setFriendsWithMe($friendsWithMe)
{
$this->friendsWithMe = $friendsWithMe;
}
/**
* #return User
*/
public function getAfriendof()
{
return $this->afriendof;
}
/**
* #param mixed $afriendof
*/
public function setAfriendof($afriendof)
{
$this->afriendof = $afriendof;
}
/**
* #return integer
*/
public function getStatus()
{
return $this->status;
}
/**
* #param integer $status
*/
public function setStatus($status)
{
$this->status = $status;
}
}
My Entity(User):
class User implements UserInterface
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
public function __construct()
{
$this->userPosts = new ArrayCollection();
$this->myfriends = new ArrayCollection();
$this->friendof = new ArrayCollection();
}
/**
* #var
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Friends", mappedBy="afriendof")
*/
private $friendof;
/**
* #var
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Friends", mappedBy="friendsWithMe")
*/
private $myfriends;
My Repository(FriendsRepository):
public function personalFriends($userId){
$em = $this->getEntityManager();
$result = $em->createQuery('SELECT friends, myuser FROM AppBundle\Entity\Friends friends
INNER JOIN AppBundle\Entity\User myuser WHERE friends.friendsWithMe = :userId AND friends.status = 1');
$result->setParameter('userId', $userId);
return $result->getResult();
}
The place where I call the repository:
public function indexAction(Request $request)
{
$user = $this->get('security.token_storage')->getToken()->getUser();
$userId = $user->getId();
$friends = $this->getDoctrine()->getRepository(Friends::class)->personalFriends($userId);
dump($friends);
exit();
Results that I get now:
https://pastebin.com/2M4SYTLb
Results that I expect:
https://pastebin.com/BxsC9QbE
Hope I understand your problem.
But from what I see you are asking for the data of the friends AND the data of the users when you are doing 'SELECT friends, myuser.
Try only selecting friends
Like this:
SELECT friend FROM AppBundle\Entity\Friends friend INNER JOIN AppBundle\Entity\User user WHERE friend.friendsWithMe = :userId AND friend.status = 1
Then you'll only have as a result an array of Friends.
If there is still a problem you can add fetch="EAGER" so it wont be "LAZY"
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\User", inversedBy="myfriends", fetch="EAGER")

Notice: Trying to get property of non-object in vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php line 481

I have a really strange case related to doctrine, loggable (DoctrineExtension) and listeners.
I will explain the situation I am having here and below is all the code I think is related to the problem.
I have two entities (Agreement and Template) where an agreement is based on a specific Template version. Template entity has the DoctrineExtension Loggable annotation. So I can revert an Agreement template to the specific version using the LogEntryRepository->revert() method. (I am using a postLoad listener to do that, so each time an agreement is retrieved, the right Template version is loaded for that Agreement).
If I get a controller action where an agreement is retrieved using a ParamConververter annotation, everything works ok and my agreement is retrieved with the right Template.
If I try to retrieve the very same agreement in the first line of the controller action using a query builder, I get the following exception
Notice: Trying to get property of non-object in /home/administrator{dir}/vendor/doctrine/orm/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php line 481
Any help would be appreciated.
Thanks.
Just copying the parts that are related to the problem:
Entities
/**
* Agreement
*
* #ORM\Table(name="agreement")
* #ORM\Entity
* #Gedmo\Loggable
*/
class Agreement
{
/**
* #var integer
* #ORM\Column(name="id", type="bigint", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var integer
* #ORM\Column(name="template_version", type="bigint", nullable=false)
* #Gedmo\Versioned
*/
private $templateVersion;
/**
* #var \Template
* #ORM\ManyToOne(targetEntity="Template")
* #ORM\JoinColumn(name="template_id", referencedColumnName="id")
*/
private $template;
}
/*
* Template
*
* #ORM\Table(name="template")
* #ORM\Entity
* #ORM\ChangeTrackingPolicy("DEFERRED_EXPLICIT")
* #Gedmo\Loggable
*/
class Template
{
/**
* #var integer
* #ORM\Column(name="id", type="bigint", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string
* #ORM\Column(name="name", type="string", length=255, nullable=false)
* #Gedmo\Versioned
*/
private $name;
}
Doctrine Subscriber
*(services.yml)*
services:
ourdeal.listener.loggable:
class: App\Bundle\Listener\LoggableSubscriber
tags:
- { name: doctrine.event_subscriber }
class LoggableSubscriber implements EventSubscriber
{
public function getSubscribedEvents()
{
return array(
'prePersist',
'postLoad',
);
}
public function prePersist(LifecycleEventArgs $args)
*...Code omitted...*
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
if ($entity instanceof Agreement)
{
$agreement = $entity;
$repo = $entityManager->getRepository('Gedmo\Loggable\Entity\LogEntry');
$repo->revert($agreement->getTemplate(), $agreement->getTemplateVersion());
}
}
}
Actions
With this action, I get the desired agreement without problems.
/**
* #Route("/agreement/send/{id}", name="agreement/send")
* #ParamConverter("agreement", class="Bundle:Agreement")
* #Template()
*/
public function sendAction(Request $request, Agreement $agreement) {
*...Code omitted...*
}
Using this code, I get the exception (the hardcoded id and this code is just for test)
/**
* #Route("/agreement/send", name="agreement/send")
* #Template()
*/
public function sendAction(Request $request) {
$em = $this->get('doctrine')->getManager();
$qb = $em->createQueryBuilder()->select('a')->from('AppBundle:Agreement', 'a')->where('a.id=1378');
$agreements = $qb->getQuery()->getResult();
}
use setParameter()
$em->createQueryBuilder()
->select('a')
->from('AppBundle:Agreement', 'a')
->where('a.id = :id')
->setParameter('id', $request->get('id'));
There is a known bug #52083 that affects PHP versions before 5.3.4, which fails randomly with "Notice: Trying to get property of non-object".
If that is your case, try upgrading PHP will solve your issue. Hope that helps

Symfony2 setting other rows to 0 after/before flush

Here's what I'm having trouble with.
I've a Table which contains a column called shown_on_homepage and only one row should be set to 1, the rest should all be set to 0. I'm trying to add a new row to the database and this one should be set to 1, setting the one that previously had a 1 to 0.
In MySQL I know this can be achieved by issuing an update before the insert:
UPDATE table_name SET shown_on_homepage = 0
Here's my Entity:
class FeaturedPerson {
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="content", type="string", length=2500, nullable=false)
*/
private $content;
/**
* #var \DateTime
*
* #ORM\Column(name="date_updated", type="datetime")
*/
private $dateUpdated;
/**
* #var bool
*
* #ORM\Column(name="shown_on_homepage", type="boolean", nullable=false)
*/
private $isShownOnHomepage;
//...
public function getIsShownOnHomepage() {
return $this->isShownOnHomepage;
}
public function setIsShownOnHomepage($isShownOnHomepage) {
$this->isShownOnHomepage = $isShownOnHomepage;
return $this;
}
}
And for the Controller I've:
$featured = new FeaturedPerson();
$featured->setContent('Test content.');
$featured->setDateUpdated('01/02/2013.');
$featured->setIsShownOnHomepage(TRUE);
$em = $this->getDoctrine()->getManager();
$em->persist($featured);
$em->flush();
It does add the new row, but the one that had a shown_on_homepage set to 1 still has it. I've researched but I couldn't find a way to achieve this, I hope you can help me.
You could execute a query prior to your existing code in your controller:
$queryBuilder = $this->getDoctrine()->getRepository('YourBundleName:FeaturedPerson')->createQueryBuilder('qb');
$result = $queryBuilder->update('YourBundleName:FeaturedPerson', 'd')
->set('d.isShownOnHomepage', $queryBuilder->expr()->literal(0))
->where('d.isShownOnHomepage = :shown')
->setParameter('shown', 1)
->getQuery()
->execute();
Change 'YourBundleName' to your bundle name.

Removing an object with em->remove($user) removes all objects in table

I have a User entity that implements UserInterface to use with a RBAC system. I have not implemented the whole system yet. However, when I try to remove a user with the following code, the action removes all the users and other associated objects in other tables and then throws me an error. I am able to remove objects from other entities without issues.
User entity
class User implements UserInterface
{
**
* #var integer $id
*
* #ORM\Column(name="id", type="smallint")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*
protected $id;
**
* #var string $username
*
* #ORM\Column(name="username", type="string", length=20, unique=TRUE)
*
protected $username;
**
* #var string $password
*
* #ORM\Column(name="password", type="string", length=255)
*
protected $password;
**
* #var string $salt
*
* #ORM\Column(name="salt", type="string", length=255)
*
protected $salt;
**
* #var string $fullName
*
* #ORM\Column(name="full_name", type="string", length=60, unique=TRUE)
*
protected $fullName;
**
* #ORM\ManyToMany(targetEntity="Role", inversedBy="users", cascade={"persist", "remove"})
* #ORM\JoinTable(name="users_roles")
*
* #var ArrayCollection $userRoles
*
protected $userRoles;
public function __construct()
{
$this->userRoles = new ArrayCollection();
}
}
Delete action
public function deleteUserAction($id) {
$user = $em->getRepository('ACMECompanyBundle:User')->find($id);
$currentUser = $this->get('security.context')->getToken()->getUser();
if ($id == $currentUser->getId()) {
return new Response("You cannot delete the current user");
}
if (!$user) {
throw $this->createNotFoundException('No user found for id '.$id);
}
try {
$em->remove($user);
$em->flush();
$msg = "User deleted!";
$code = "OK";
} catch (DBALException $e) {
return new Response($e);
$msg = "User cannot be deleted!";
$code = "ERR";
}
$response = new Response(json_encode(array('code' => $code, 'msg' => $msg)));
$response->headers->set('Content-Type', 'application/json');
return $response;
}
The error returned after all users are removed is
InvalidArgumentException: You cannot refresh a user from the EntityUserProvider that does not contain an identifier. The user object has to be serialized with its own identifier mapped by Doctrine.
You left out the definition for em in your action... define it with
$em = $this->getDoctrine()->getEntityManager();
and it should work. Unless you set it on the class itself, then you would need $this->...
When doctrine removes the user, it also removes all rolles assigned to this user and all users assigned to these roles. So according your annotation schema this is the correct behavior because of cascade={"remove"} in $userRoles annotation and cascade={"remove"} in $users annotation in Role entity.
If you want to prevent cascade removing and want to keep cascade persistent remove "remove" argument from both user and role relations

Resources