Doctrine2 - How to remove ManyToMany associations on add of new ones - doctrine-orm

I've took a quick look but can't find the answer.
Take the following entities, users who can have many roles:
/**
* #Entity
**/
class User
{
/**
* #ManyToMany(targetEntity="Role", inversedBy="users")
* #JoinTable(name="user_to_roles")
**/
protected $roles;
public function __construct()
{
$this->roles = new \Doctrine\Common\Collections\ArrayCollection();
}
public function addRole(Role $role)
{
$role->addUser($this);
$this->roles[] = $role;
}
public function getRoles()
{
return $this->roles;
}
}
and the roles:
/**
* #Entity
**/
class Role
{
/**
* #Id
* #Column(type="integer")
* #GeneratedValue(strategy="AUTO")
**/
protected $id;
/**
* #ManyToMany(targetEntity="User", mappedBy="roles")
**/
protected $users;
public function __construct()
{
$this->users = new \Doctrine\Common\Collections\ArrayCollection();
}
public function addUser(User $user)
{
$this->users[] = $user;
}
public function getUsers()
{
return $this->users;
}
}
When I want to update a user I will get the reference, and update like so:
$user = $this>entityManager->getReference('App\Entity\User', $id);
$user->addRole($role);
$this->entityManager->persist($user);
$this->entityManager->flush();
However, this will continuously add new roles to my relational table, whereas I just want to update the current row, or delete it and create a new one (whichever is easier). How can I achieve this?

Try to add this function to your entity:
function setRoles($roles) {
$this->roles = new ArrayCollection($roles);
}
And when you update try this:
$user = $this>entityManager->getReference('App\Entity\User', $id);
$roles = array();
// add your roles to the array roles
$user->setRoles($roles);
$this->entityManager->persist($user);
$this->entityManager->flush();

Related

Doctrine 2 avec Symfony 4.2.5 et FOSRestBundle - OneToMany association - Redondant registering

Here is the problem:
Two entities Books and Auteurs. A book has only one auteur, an auteur can have multiple books.
When the base is populated if I create a book with an existing auteur in the Auteurs table, this one creates (wrongly) a duplicate in the table.
The presence of the cascade = "persist" annotation is at the origin of this duplicate. If I delete this annotation I get the error:
{"error": {"code": 500, "message": "Internal Server Error", "exception": [{"message": "A new entity was found through the relationship 'App \ Entity \ Books # auteur' .....
How to deal with this question?
My code:
Controller:
<?php
namespace App\Controller;
use App\Entity\Books;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Controller\FOSRestController;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
class EcritureController extends FOSRestController
{
/**
* #Rest\Post(
* path = "/creer-book")
* #Rest\View(StatusCode = 201)
* #ParamConverter("book", converter="fos_rest.request_body")
*/
public function creerBook(Books $book)
{
$em->persist($book);
$em->flush();
return $book;
}
}
Entity Books:
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\BooksRepository")
*/
class Books
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $titre;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Books", inversedBy="Books", cascade={"persist"})
* #ORM\JoinColumn(nullable=false)
*/
private $auteur;
public function getId(): ?int
{
return $this->id;
}
public function getTitre(): ?string
{
return $this->Titre;
}
public function setTitre(string $titre): self
{
$this->titre = $titre;
return $this;
}
public function getAuteur(): ?Auteurs
{
return $this->auteur;
}
public function setAuteur(?Auteurs $auteur): self
{
$this->auteur = $auteur;
return $this;
}
}
Entity Auteurs:
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\AuteursRepository")
*/
class Auteurs
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=10)
*/
private $nom;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Books", mappedBy="auteur")
*/
private $books;
public function __construct()
{
$this->books = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getNom(): ?string
{
return $this->nom;
}
public function setNom(string $nom): self
{
$this->nom = $nom;
return $this;
}
/**
* #return Collection|Books[]
*/
public function getBooks(): Collection
{
return $this->books;
}
public function addBook(Books $book): self
{
if (!$this->books->contains($book)) {
$this->books[] = $book;
$book->setBook($this);
}
return $this;
}
public function removeBook(Books $book): self
{
if ($this->books->contains($book)) {
$this->sujets->removeElement($book);
// set the owning side to null (unless already changed)
if ($book->getAuteur() === $this) {
$book->setAuteur(null);
}
}
return $this;
}
}
The error was to "persist" the "Many" side. It's necessary persist the "One" side, as follows:
$auteurId = $book->getAuteur()->getId();
$auteur= $em->getRepository('App:Auteurs')->find($auteurId);
$response = new Response();
if ($auteur) {
$auteur->addBook($book);
$em->persist($auteur);
$em->flush();
$response->setContent('Chain of your choice', Response::HTTP_OK);
} else {
$response->setContent('Auteur selected doesn\'t exist', Response::HTTP_NOT_ACCEPTABLE);
}
return $response;

[PHPUnit], [Symfony]: test that Entity was saved in DB

I have problem with my test. I learn how to write phpunit test and how i can mock object, services etc.. I have this method on my ProductService:
<?php
namespace App\Service;
use App\Entity\Product;
use App\Repository\ProductRepository;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\ORMException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class ProductService
{
/**
* #var ProductRepository
*/
private $productRepository;
/**
* #var EntityManager
*/
private $entityManager;
/**
* #var ValidatorInterface
*/
private $validator;
/**
* ProductService constructor.
* #param ProductRepository $productRepository
* #param EntityManagerInterface $entityManager
* #param ValidatorInterface $validator
*/
public function __construct(ProductRepository $productRepository, EntityManagerInterface $entityManager, ValidatorInterface $validator)
{
$this->productRepository = $productRepository;
$this->entityManager = $entityManager;
$this->validator = $validator;
}
/**
* #param $productData
* #return Product|string
*/
public function createProduct($productData)
{
$name = $productData['name'];
$quantity = $productData['quantity'];
$sku = $productData['sku'];
$product = new Product();
$product->setName($name);
$product->setQuantity($quantity);
$product->setProductSerial($sku);
$errors = $this->validator->validate($product);
if (count($errors) > 0) {
$errorsString = (string)$errors;
return $errorsString;
}
try {
$this->entityManager->persist($product);
$this->entityManager->flush();
return $product;
} catch (\Exception $ex) {
return $ex->getMessage();
}
}
}
and i write this test:
<?php
namespace App\Tests\Service;
use App\Entity\Product;
use App\Repository\ProductRepository;
use App\Service\ProductService;
use Doctrine\Common\Persistence\ObjectRepository;
use PHPUnit\Framework\TestCase;
class ProductServiceTest extends TestCase
{
/**
* Create product test
*/
public function testCreateProduct()
{
$product = new Product();
$product->setName('tester');
$product->setQuantity(2);
$product->setProductSerial('Examplecode');
$productService = $this->createMock(ProductService::class);
$productService->method('createProduct')->will($this->returnSelf());
$this->assertSame($productService, $productService->createProduct($product));
}
}
When i run phpunit test, then i always have success but my database is empty. How can I be sure that the test works correctly? What is worth fixing and what is not? I wanted to make the launch of tests result in, for example, adding records to the test database, but I have no idea how to do it and how to properly mock it. I using phpunit + Symfony 4.
I used to write tests, but those that asked the endpoint API, and here I want to test services and repositories without endpoints.
I would like to learn how to test and mock websites, repositories, various classes etc.
When i apply answer then i have:
PHPUnit 7.5.17 by Sebastian Bergmann and contributors.
Testing Project Test Suite
?[31;1mE?[0m 1 / 1 (100%)
Time: 542 ms, Memory: 10.00 MB
There was 1 error:
1) App\Tests\Service\ProductServiceTest::testCreateProduct
Doctrine\Common\Persistence\Mapping\MappingException: The class 'App\Repository\ProductRepository' was not found in the chain configured namespaces App\Entity, Gesdinet\JWTRefreshTokenBundle\Entity
D:\warehouse-management-api\vendor\doctrine\persistence\lib\Doctrine\Common\Persistence\Mapping\MappingException.php:22
D:\warehouse-management-api\vendor\doctrine\persistence\lib\Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain.php:87
D:\warehouse-management-api\vendor\doctrine\orm\lib\Doctrine\ORM\Mapping\ClassMetadataFactory.php:151
D:\warehouse-management-api\vendor\doctrine\persistence\lib\Doctrine\Common\Persistence\Mapping\AbstractClassMetadataFactory.php:304
D:\warehouse-management-api\vendor\doctrine\orm\lib\Doctrine\ORM\Mapping\ClassMetadataFactory.php:78
D:\warehouse-management-api\vendor\doctrine\persistence\lib\Doctrine\Common\Persistence\Mapping\AbstractClassMetadataFactory.php:183
D:\warehouse-management-api\vendor\doctrine\orm\lib\Doctrine\ORM\EntityManager.php:283
D:\warehouse-management-api\vendor\doctrine\doctrine-bundle\Repository\ContainerRepositoryFactory.php:44
D:\warehouse-management-api\vendor\doctrine\orm\lib\Doctrine\ORM\EntityManager.php:713
D:\warehouse-management-api\vendor\doctrine\persistence\lib\Doctrine\Common\Persistence\AbstractManagerRegistry.php:215
D:\warehouse-management-api\tests\Service\ProductServiceTest.php:28
?[37;41mERRORS!?[0m
?[37;41mTests: 1?[0m?[37;41m, Assertions: 0?[0m?[37;41m, Errors: 1?[0m?[37;41m.?[0m
My Product entity
<?php
namespace App\Entity;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity(repositoryClass="App\Repository\ProductRepository")
*/
class Product
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
* #Assert\NotBlank()
*/
private $name;
/**
* #ORM\Column(type="integer")
* #Assert\NotBlank()
*/
private $quantity;
/**
* #Gedmo\Mapping\Annotation\Timestampable(on="create")
* #ORM\Column(type="datetime")
*/
private $createdAt;
/**
* #Gedmo\Mapping\Annotation\Timestampable(on="update")
* #ORM\Column(type="datetime")
*/
private $updatedAt;
/**
* #ORM\Column(type="string")
* #Assert\NotBlank()
*/
private $product_serial;
public function __construct() {
$this->setCreatedAt(new \DateTime());
$this->setUpdatedAt();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getQuantity(): ?int
{
return $this->quantity;
}
public function setQuantity(int $quantity): self
{
$this->quantity = $quantity;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeInterface $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updatedAt;
}
public function setUpdatedAt(): self
{
$this->updatedAt = new \DateTime();
return $this;
}
public function getProductSerial(): ?string
{
return $this->product_serial;
}
public function setProductSerial(string $product_serial): self
{
$this->product_serial = $product_serial;
return $this;
}
}
ProductRepository
<?php
namespace App\Repository;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;
class ProductRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Product::class);
}
}
doctrine.yaml
doctrine:
dbal:
# configure these for your database server
driver: 'pdo_mysql'
server_version: '5.7'
charset: utf8mb4
default_table_options:
charset: utf8mb4
collate: utf8mb4_unicode_ci
url: '%env(resolve:DATABASE_URL)%'
orm:
auto_generate_proxy_classes: true
naming_strategy: doctrine.orm.naming_strategy.underscore
auto_mapping: true
mappings:
App:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
First of all, when you mock a method the original method doesn't exist any more, in this test. In your case you substitute ProductService::createProduct with something like this:
// This is your mock
class ProductService
{
// ...
public function createProduct($productData)
{
return $this;
}
}
Your test doesn't check anything.
If you want to test the real functionality then
namespace App\Tests\Service;
use App\Repository\ProductRepository;
use App\Service\ProductService;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class ProductServiceTest extends KernelTestCase
{
/**
* Create product test
*/
public function testCreateProduct(): void
{
// We load the kernel here (and $container)
self::bootKernel();
$productData = [
'name' => 'foo',
'quantity' => 1,
'sku' => 'bar',
];
$productRepository = static::$container->get('doctrine')->getRepository(ProductRepository::class);
$entityManager = static::$container->get('doctrine')->getManager();
// Here we mock the validator.
$validator = $this->getMockBuilder(ValidatorInterface::class)
->disableOriginalConstructor()
->setMethods(['validate'])
->getMock();
$validator->expects($this->once())
->method('validate')
->willReturn(null);
$productService = new ProductService($productRepository, $entityManager, $validator);
$productFromMethod = $productService->createProduct($productData);
// Here is you assertions:
$this->assertSame($productData['name'], $productFromMethod->getName());
$this->assertSame($productData['quantity'], $productFromMethod->getQuantity());
$this->assertSame($productData['sku'], $productFromMethod->getSku());
$productFromDB = $productRepository->findOneBy(['name' => $productData['name']]);
// Here we test that product in DB and returned product are same
$this->assertSame($productFromDB, $productFromMethod);
}
}

Foreign key stays empty after persisting entity with OneToMany association

Given are the following two entity classes
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
* #ORM\Table()
*/
class Tree
{
/**
* #ORM\Id()
* #ORM\Column(type="guid")
* #ORM\GeneratedValue(strategy="UUID")
* #var string
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="Apple", mappedBy="tree", cascade={"persist"})
* #var Collection
*/
private $apples;
public function __construct()
{
$this->setApples(new ArrayCollection());
}
public function toArray(): array
{
return [
'id' => $this->getId(),
];
}
public function getId(): string
{
return $this->id;
}
public function setId(string $id): void
{
$this->id = $id;
}
public function getApples(): Collection
{
return $this->apples;
}
public function setApples(Collection $apples): void
{
$this->apples = $apples;
}
}
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
* #ORM\Table()
*/
class Apple
{
/**
* #ORM\Id()
* #ORM\Column(type="guid")
* #ORM\GeneratedValue(strategy="UUID")
* #var string
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Tree", inversedBy="apples")
* #var Tree
*/
private $tree;
public function toArray(): array
{
return [
'id' => $this->getId(),
];
}
public function getId(): string
{
return $this->id;
}
public function setId(string $id): void
{
$this->id = $id;
}
public function getTree(): Tree
{
return $this->tree;
}
public function setTree(Tree $tree): void
{
$this->tree = $tree;
}
}
The database schema looks good except for apple.tree_id being nullable. Is that already an issue in this case?
I'm persisting entries like the following:
<?php
declare(strict_types = 1);
namespace App\Service;
use App\Entity\Apple;
use App\Entity\Tree;
use Doctrine\ORM\EntityManager;
class Gardener
{
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function plantTree(): array
{
$entityManager = $this->entityManager;
$tree = new Tree();
$blueApple = new Apple();
$redApple = new Apple();
$tree->getApples()->add($blueApple);
$tree->getApples()->add($redApple);
$entityManager->persist($tree);
$entityManager->flush();
return (array) $tree;
}
}
When executing the persist and flush there are no errors or warnings. A tree an two apple entries are beingt store, the apple.tree_id is however always null.
It seems like I have a misconfiguration on the entity classes, but am not sure what it is. I also tried adding a JoinColumn annotation #ORM\JoinColumn(name="tree_id", referencedColumnName="id"), but it did not make any difference.
What changes do I need to make, to have appe.tree_id being set properly?
Your missing the adder & remover functions on the *ToMany side.
If your using Symfony >4 then replace setApples function with:
public function addApple(Apple $apple): Tree
{
$this->apples->add($apple);
return $this;
}
public function removeApple(Apple $apple): Tree
{
$this->apples->removeElement($apple);
return $this;
}
If you're using Zend Framework 3, then replace setApples function with:
public function addApples(array $apples): Tree
{
foreach ($apples as $apple) {
if (! $this->apples->contains($apple) {
$this->apples->add($apple)
$apple->setTree($this);
}
}
return $this;
}
public function removeApples(array $apples): Tree
{
foreach ($apples as $apple) {
if($this->apples->contains($apple) {
$this->apples->remove($apple);
}
}
return $this;
}
Have a read of the Working with Association docs, which show examples and explain how to update back 'n' forth.

Creating new entities and changing an entity in prePersist

in a sense I would like to transfer my old question Flushing in postPersist possible or not? to the prePersistevent.
Suppose the following example
class Entity {
/** #PrePersist */
public function doStuffOnPrePersist(LifecycleEventArgs $args)
{
$this->value = 'changed from prePersist callback!';
$other = new OtherEntity();
$args->getEntityManager()->persist($other);
}
}
called like
$entity = new Entity();
$entity->value = "unchanged";
$em->persist($entity);
$em->flush();
I have 2 fundamental questions:
- Will the value written to db be "unchanged" or "Changed from..."?
- Will the OtherEntity be written to DB?
Assuming both questions are answered with yes, let me extend my question:
Both, Entity and OtherEntity have an Autogenerated id field and OtherEntity has a reference (ManyToOne) to Entity:
class Entity {
/*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue
*/
private $id;
/** #PrePersist */
public function doStuffOnPrePersist(LifecycleEventArgs $args)
{
$this->value = 'changed from prePersist callback!';
$other = new OtherEntity();
$other->father = $this; // <-------------
$args->getEntityManager()->persist($other);
}
}
Will this work, too?

must return a string value in zend2. How?

I have a problem with a zend2 form. I made an entity which gets some data from the database and joins some tables...
here is the entity:
class Campaigns
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
protected $id;
/**
*
* #ORM\Column(name="campaign_name", type="string")
*
*/
protected $campaigns;
/**
* #var mixed
*
* #ORM\ManyToMany(targetEntity="Application\Entity\Countries", cascade={"persist"}, orphanRemoval=false)
* #ORM\JoinTable(name="campaigns_countries",
* joinColumns={#ORM\JoinColumn(name="campaign_id", referencedColumnName="id", onDelete="CASCADE")},
* inverseJoinColumns={#ORM\JoinColumn(name="country_id", referencedColumnName="id", onDelete="CASCADE")}
* )
*/
protected $countries;
Below this code are the getters and setters, a construct function, an add and an remove function.
Here they are:
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
return $this;
}
public function getCampaigns()
{
return $this->campaigns;
}
public function setCampaigns($campaigns)
{
$this->campaigns = $campaigns;
return $this;
}
public function addCampaigns($campaigns = null)
{
foreach ($campaigns as $c) {
if (!$this->campaigns->contains($c)) {
$this->campaigns->add($c);
}
}
}
public function removeCampaigns($campaigns)
{
foreach ($campaigns as $c) {
if ($this->campaigns->contains($c)) {
$this->campaigns->removeElement($c);
}
}
}
public function getCountries()
{
return $this->countries;
}
public function setCountries($countries)
{
$this->countries = $countries;
return $this;
}
public function addCountries($countries = null)
{
foreach ($countries as $c) {
if (!$this->countries->contains($c)) {
$this->countries->add($c);
}
}
}
public function removeCountries($countries)
{
foreach ($countries as $c) {
if ($this->countries->contains($c)) {
$this->countries->removeElement($c);
}
}
} //construct for countries
public function __construct()
{
$this->setCountries(new ArrayCollection());
}
My problem is with the protected $countries. If i add in the form the property value, it gives me the "countries" property not found in entity.
If I do not add it, and instead use __toString() function, it gives me an error saying that it could not convert countries to string...in the __toString() function I added the following code:
public function __toString()
{
return $this->countries;
}
Thank you for all your help!
AE
You say you want a string containing all related countries. The following code demonstrates how you could achieve this:
$campaignCountryNames = array();
$campaignCountries = $campaign->getCountries();
foreach ($campaignCountries as $country) {
// I assume your Country entity has a name property
$campaignCountryNames[] = $country->getName();
}
echo implode(', ', $campaignCountryNames);