Symfony2: Prevent duplicate in database with form Many to One - php

I have a Parents form embedded into another form Student containing the data of the parents of a student with an association of Many to one.
When a new student registration are recorded his parents in another table in the database. Then if a new student who is brother of an existing need to register, meaning that parents are already registered in the database, should be prevented from parents to register again in the database, could only upgrade .
I'm told that this is solved using data transformers, but I do not know how to use it. If someone could help me I would appreciate it. Here I leave the code:
StudentType.php
//...
->add('responsible1', new ParentsType(),array('label' => 'Mother'))
->add('responsible2', new ParentsType(),array('label'=> 'Father'))
Entity Parents
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
//National identity document
//we have removed "#UniqueEntity(fields={"NID"}, message="...")"
//so you can put any NID on the form and then check its existence to insert or not.
/**
* #var string
*
* #ORM\Column(name="NID", type="string", length=10)
* #Assert\NotBlank()
*/
private $nid;
//more properties...
/**
* #ORM\OneToMany(targetEntity="Student", mappedBy="$responsible1")
* #ORM\OneToMany(targetEntity="Student", mappedBy="$responsible2")
*/
private $students;
//...
public function addStudent(\Cole\BackendBundle\Entity\Student $students)
{
$this->students[] = $students;
return $this;
}
public function removeStudent(\Cole\BackendBundle\Entity\Student $students)
{
$this->students->removeElement($students);
}
public function getStudents()
{
return $this->students;
}
Entity Student
//...
/**
* #ORM\ManyToOne(targetEntity="Parents", inversedBy="students", cascade={"persist"})
*/
private $responsible1;
/**
* #ORM\ManyToOne(targetEntity="Parents", inversedBy="students", cascade={"persist"})
*/
private $responsible2;
//...
public function setResponsible1($responsible1)
{
$this->responsible1 = $responsible1;
return $this;
}
public function getResponsible1()
{
return $this->responsible1;
}
public function setResponsible2($responsible2)
{
$this->responsible2 = $responsible2;
return $this;
}
public function getResponsible2()
{
return $this->responsible2;
}
ParentsRepository.php
class ParentsRepository extends EntityRepository
{
public function findResponsible($nid)
{
return $this->getEntityManager()->createQuery(
'SELECT p FROM BackendBundle:Parents p WHERE p.nid=:nid')
->setParameter('nid',$nid)
->setMaxResults(1)
->getOneOrNullResult();
}
}
StudentController.php
/**
* Creates a new Student entity.
*
*/
public function createAction(Request $request)
{
$entity = new Student();
$form = $this->createCreateForm($entity);
$form->handleRequest($request);
if ($form->isValid()) {
$responsible1 = $em->getRepository('BackendBundle:Parents')->findResponsible($entity->getResponsible1()->getNid());
$responsible2 = $em->getRepository('BackendBundle:Parents')->findResponsible($entity->getResponsible2()->getNid());
if($responsible1){
$entity->setResponsible1($responsible1->getId());
}
if($responsible2){
$entity->setResponsible2($responsible2->getId());
}
$entity->getResponsible1()->setUsername($entity->getResponsible1()->getNid());
$entity->getResponsible2()->setUsername($entity->getResponsible2()->getNid());
$entity->getResponsible1()->setPassword($entity->getResponsible1()->getNid());
$entity->getResponsible2()->setPassword($entity->getResponsible2()->getNid());
$em = $this->getDoctrine()->getManager();
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('student_show', array('id' => $entity->getId())));
}
return $this->render('BackendBundle:Student:new.html.twig', array(
'entity' => $entity,
'form' => $form->createView(),
));
}
With the above code attempts to solve the problem but it gives me error to persist data to the database and will not let me add to the database, but if you use the following code to test the new student creates and assigns parents corresponding not create them again (assuming you were already created earlier).
$responsible1 = $em->getRepository('BackendBundle:Parents')->findResponsible(4); //The number corresponds to the id of the parent
$responsible2 = $em->getRepository('BackendBundle:Parents')->findResponsible(5);
$entity->setResponsible1($responsible1->getId());
$entity->setResponsible2($responsible2->getId());
I do not know if what I'm doing is right.I read something to use Data Transformers or event listener as PrePersist and Preupdate, but I don't know how to use this.
Thanks in advance for your answers.

Instead of
if($responsible1){
$entity->setResponsible1($responsible1->getId());
}
if($responsible2){
$entity->setResponsible2($responsible2->getId());
}
$entity->getResponsible1()->setUsername($entity->getResponsible1()->getNid());
$entity->getResponsible2()->setUsername($entity->getResponsible2()->getNid());
$entity->getResponsible1()->setPassword($entity->getResponsible1()->getNid());
$entity->getResponsible2()->setPassword($entity->getResponsible2()->getNid());
you can write
if($responsible1){
$entity->setResponsible1($responsible1);
}
if($responsible2){
$entity->setResponsible2($responsible2);
}
And it should work.
But I think a better solution will be to add an event listener to the FormEvents::SUBMIT event. This event allows you to change data from the normalized representation of the form data. So all you need to do is something like this:
public function onSubmit(FormEvent $event)
{
$student = $event->getData();
if ($student->getResponsible1()) {
$parentNid = $student->getResponsible1()->getNid();
// here you check the database to see if you have a parent with this nid
// if a parent exists, replace the current submitted parent data with the parent entity existing in your db
}
Hope this helps. Let me know if I have to give more details.

Judging from your relationship, you want to avoid that the same student is added twice to the Parents entity. There is a simple trick for that, ArrayCollaction class has a method named contains it returns true if a value or object is already found in the collection. A better in_array.
So, you need to check inside the adder if the $parent already contains the $student that is about to be added and act accordingly. Like shown below:
public function addStudent(\Cole\BackendBundle\Entity\Student $student)
{
if (!$this->students->contains($student)) {
$this->students[] = $students;
}
return $this;
}

Here's my thoughts, from the comments, you said you are using a national identity document(hopefull its an integer representation), make this the primary key of the parent table and make this unique, so when the second student who is the sibling of a another student enters the same details and submits, the database will throw an error, handle that error and continue on
edit: it may not even be required to make the national identity the primary key, just make it unique, you were supposed to do this regardless, you missed this one.
you can use symfony entity form type to load (ajax) the parent entity when the student enters the national identity

Related

Symfony Doctrine relationship empty in PhpUnit test

I have a Symfony app with a User entity with a many-to-many relation to a Cat entity. I also have a PhpUnit test which checks that deleting a cat (that belongs to 2 users) from 1 user doesn't actually delete the cat:
public function testDeletingACatBelongingToTwoUsersOnlyDeletesTheAssociationNotTheCat()
{
$cat = $this->createCat();
// Associate with user 1
$user1 = new User();
$user1->setEmail('test#example.com');
$user1->setPassword('pwdpwd');
$user1->addCat($cat);
$this->em->persist($user1);
// Associate with user 2
$user2 = new User();
$user2->setEmail('another#example.com');
$user2->setPassword('pwdpwd');
$user2->addCat($cat);
$this->em->persist($user2);
$this->em->flush();
// Sanity check:
$this->assertCount(1, $user1->getCats()); // PASS
$this->assertCount(1, $user2->getCats()); // PASS
$this->assertCount(2, $cat->getUsers()); // FAIL (0)
// ... perform the test (not shown here)
}
private function createCat(): Cat
{
$cat = new Cat();
$cat->setName($this->name);
$this->em->persist($cat);
$this->em->flush();
return $cat;
}
My question is, why does $cat->getUsers() return 0 in my test? At runtime it doesn't, it returns the correct value. It's only in the test that it returns 0.
Here are the relevant excerpts from my entities, auto-generated by Symfony:
/**
* #ORM\Entity(repositoryClass=UserRepository::class)
*/
class User implements UserInterface
{
/**
* #ORM\ManyToMany(targetEntity=Cat::class, inversedBy="users")
*/
private $cats;
/**
* #return Collection|Cat[]
*/
public function getCats(): Collection
{
return $this->cats;
}
public function addCat(Cat $cat): self
{
if (!$this->cats->contains($cat)) {
$this->cats[] = $cat;
}
return $this;
}
public function removeCat(Cat $cat): self
{
$this->cats->removeElement($cat);
return $this;
}
}
/**
* #ORM\Entity(repositoryClass=CatRepository::class)
*/
class Cat
{
/**
* #ORM\ManyToMany(targetEntity=User::class, mappedBy="cats")
*/
private $users;
/**
* #return Collection|User[]
*/
public function getUsers(): Collection
{
return $this->users;
}
}
The reason is, that collections are not synchronized with the database and synchronization between owning and inverse side is not automatically done either.
The category entries of your user entity probably will be persisted to the database (although I'm missing some cascade statements, but what do I know). When the category is created, it's collection of users is empty (obviously), then users add the category to the many-to-many relation in database.
BUT, the collection is a plain collection. If you loaded the category from the database, it would be a lazy-loaded PersistentCollection (or something alike), which would - only at the moment of access - fetch the items from the database (definition of lazy loading). Your test code has the plain collection (since you created the object yourself).
Not quite sure, if it'll work, but you could try refreshing the cat ($em->refresh($cat);) I'm not quite certain though, if that will replace the collection. Alternatively, you could make your User::addCat that it also calls $cat->addUser($this) (which you might have to add, beware the infinite recursion, which already should be prevented by the "contains" checks.).

Symfony 3.4 - updating entity after soft-deleting related entity row

I've got an entity called Logs that has a ManyToOne relation to an HourlyRates entity. Both Logs and HourlyRates have date properties. When adding a log with a specific date, an hourlyRate is assigned to it if the log-date fits within the rate's time range. I'm using the Doctrine Extensions Bundle, so the data in each entity can be soft-deleted.
What needs to be done:
After soft-deleting an HourlyRate the related Log has to be updated, so that the nearest existing past HourlyRate takes the place of the deleted one.
I tried to use preSoftDelete, postSoftDelete, preRemove and postRemove methods inside an HourlyRate entity listener. The code was being executed and the setters were working properly, but the database hasn't been updated in any of said cases. An "EntityNotFoundException" was being thrown everytime.
My second approach was to use the preRemove event along with setting the cascade option to "all" by using annotations in the HourlyRate class. As a result, soft-deleting an hourlyRate caused soft-deleting of the related log.
The Log entity:
class Log
{
use SoftDeleteableEntity;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\HourlyRate", inversedBy="logs")
* #ORM\JoinColumn(nullable=false)
*/
private $hourlyRate;
public function setHourlyRate(?HourlyRate $hourlyRate): self
{
$this->hourlyRate = $hourlyRate;
return $this;
}
}
The HourlyRate entity:
class HourlyRate
{
use SoftDeleteableEntity;
//other code
/**
* #ORM\OneToMany(targetEntity="App\Entity\Log", mappedBy="hourlyRate", cascade={"all"})
*/
private $logs;
}
The HourlyRate entity listener:
class HourlyRateEntityListener
{
public function preRemove(HourlyRate $hourlyRate, LifecycleEventArgs $args)
{
$entityManager = $args->getObjectManager();
/** #var HourlyRateRepository $HRrepo */
$HRrepo = $entityManager->getRepository(HourlyRate::class);
foreach ($hourlyRate->getLogs() as $log)
{
$rate = $HRrepo->findHourlyRateByDate($log->getDate(), $log->getUser(), $hourlyRate);
$log->setHourlyRate($rate);
}
}
}
The repository method:
class HourlyRateRepository extends ServiceEntityRepository
{
public function findHourlyRateByDate(?\DateTimeInterface $datetime, User $user, ?HourlyRate $ignore = null): ?HourlyRate
{
$qb = $this->createQueryBuilder('hr')
->where('hr.date <= :hr_date')
->andWhere('hr.user = :user')
->orderBy('hr.date', 'DESC')
->setMaxResults(1)
->setParameters(array('hr_date' => $datetime, 'user' => $user));
//ignore the "deleted" hourlyRate
if($ignore){
$qb->andWhere('hr.id != :ignored')
->setParameter('ignored', $ignore->getId());
}
return $qb->getQuery()
->getOneOrNullResult()
;
}
}
Thank you in advance for any of your help.
EDIT:
Okay so after a whole week of trials and errors i finally managed to achieve the result I wanted.
I removed the One-To-Many relation between the hourlyRates and the logs from the entities, but left the $hourlyRate property inside the Log class. Then I got rid of the HourlyRateEntityListener and the preRemove() method from the LogEntityListener. Instead, I implemented the postLoad() method:
class LogEntityListener
{
public function postLoad(Log $log, LifeCycleEventArgs $args)
{
$entityManager = $args->getObjectManager();
$HRrepo = $entityManager->getRepository(HourlyRate::class);
/** #var HourlyRateRepository $HRrepo */
$rate = $HRrepo->findHourlyRateByDate($log->getDate(), $log->getUser());
$log->setHourlyRate($rate);
}
}
This approach allows me to set the proper hourlyRate for each log without involving the database. Idk if this solution is acceptable though.

ReferenceMany in Doctorine with Symfony

I am using Symfony with Doctrine.
The annotation for $members (getMembers() returns this variable):
/**
* #var User
* #MongoDB\ReferenceMany(targetDocument="something", storeAs="dbRef")
* #Assert\NotNull
* #JMS\Groups({"Default", "something"})
*/
protected $members;
The controller: (I used $form->submit)
public function updateAction($id, Request $request)
{
$project = $this->fetchProject($id);
$oldMembers = $project->getMembers();
$form = $this->createForm(...);
$form->submit($request->request->all(), false);
$newMembers = $project->getMembers();
...
$this->persist(...);
I add new members in the form and submit it but the '$oldMembers' and the '$newMembers' are the same! which is not desirable!
both of them are referring to the the new data (getMembers()). but I need to keep $oldMembers separate from $newMembers. how?
try to refresh the entity manager like this:
$em = $this->getDoctrine();
$em->refresh($project);
Or you can add member manually if refresh doesn't work
You are using a Form, but if you can add manually members like this:
$project->addMember($member);
Into your entity you can have a method like this:
public function addMember(Member $member)
{
$this->member[] = $member;
return $this;
}

Symfony 3 doctrine show content from relationship on repo

I have two Symfony entities SrvrsServers and NcBackupEvents:
NCbrtBundle:SrvrsServers (id, name, description, etc)
Relationship code:
/**
* One SrvrsServers has Many NcBackupEvents.
* #ORM\OneToMany(targetEntity="NcBackupEvents", mappedBy="SrvrsServers")
*/
private $ncBackupEvents;
public function __construct() {
$this->ncBackupEvents = new ArrayCollection();
}
NCbrtBundle:NcBackupEvents (id, date, status, etc)
This is on NcBackupEvents:
/**
* #var \SrvrsServers
*
* #ORM\ManyToOne(targetEntity="SrvrsServers")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="srvrs_servers_id", referencedColumnName="id")
* })
*/
private $srvrsServers;
The relationship is OneToMany from SrvrsServers to NcBackupEvents. On the repo of NcBackupEvents i'm able to pull information about itself. This works and I think the SrvrsServers object gets pulled, but I am not 100% sure and also do not know how to access it:
class NcBackupEventsRepository extends \Doctrine\ORM\EntityRepository
{
public function findByServerBackup($parameters)
{
$dql = 'SELECT n FROM NCbrtBundle:NcBackupEvents n
WHERE n.backupmethod LIKE :backupmethod
AND n.backupType LIKE :backup_type
AND n.log LIKE :log';
$query = $this->getEntityManager()
->createQuery($dql)
->setParameter('backupmethod',
'%'.$parameters['backupmethod'].'%')
->setParameter('backup_type',
'%'.$parameters['backup_type'].'%')
->setParameter('log', '%'.$parameters['log'].'%');
try {
return $query->getResult();
} catch (\Doctrine\ORM\NoResultException $e) {
return null;
}
}
}
How can I QUERY from Controller using the SrvrsServers->name field for instance? I have tried many things and nothing seems to work. This should be very simple, but I do not understand how to get it done. I have tried to follow the official docs and can not get it to work for some reason. Can someone point me the way to do this? Please keep present that I am a newbie.
EDIT: SAMPLE Controller.
/**
* #Route("/")
*/
public function indexAction()
{
$paramaters = array('backupmethod' => 't-py',
'backup_type' => '', 'log' => 'fuc');
$em = $this->getDoctrine()
->getRepository('NCbrtBundle:NcBackupEvents')
->findByServerBackup($paramaters);
foreach($em as $value){
print_r(gettype($value));
print_r($value->getSrvrsServers()->getName());
}
return $this->render('NCbrtBundle:Default:index.html.twig');
}
EDIT2: I want to include the server name on the $paramaters variable from the SrvrsServers entity

Symfony Doctrine Sluggable extension in existing database

I had to add in an existing entity a slug field to slugify the field 'name'. But there is already data in this entity and I can't delete them.
I would like to create a console script which can slugify all my 'name' field.
I don't know how to do it because this is not an insertion but just an update...
class SlugCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
->setName('generate:geo:slug')
->setDescription('Slug generation for GeoBundle ');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$em = $this->getContainer()->get('doctrine')->getManager();
$regions = $em->getRepository('FMGeoBundle:Region')->findAll();
if($regions === null){
throw new Exception('No Region found');
}
foreach($regions as $region){
// ????? Generate the slug here ??
$em->persist($region);
}
$em->flush();
$output->writeln('Slugs Generated ;) ...');
}
}
The 'slug' field in my entity:
/**
* #var string
*
* #ORM\Column(name="slug", type="string", length=255)
* #Gedmo\Slug(fields={"name"})
*/
protected $slug;
The Sluggable extension works on both create and update actions. Therefore, you could just simulate an update by putting a row's name with its own.
I found an easier way. You can apparently just set the slug manually like that. And it will slugify the field needed.
foreach ($regions as $region) {
$region->setSlug($region->getName());
$this->em->persist($region);
}
Just saw that the Gedmo library documentation directly answers this question :
Regenerating slug
In case if you want the slug to regenerate itself based on sluggable
fields, set the slug to null.
<?php $entity = $em->find('Entity\Something', $id);
$entity->setSlug(null);
$em->persist($entity); $em->flush();
So, in your case you have just to persist the entity, nothing else. Because $slug is already null.

Categories