Link 1 entity with many others kind of entities - symfony

Let say I have a Company for which I manage Employees, Cars, Contracts, Buildings, Sites, Products, etc. As you can guess, these are quite independant things, so no inheritance is possible.
For each of these elements (i.e. Entities), I want to be able to attach one or several Documents (click on a button, form opens, select one/several Document or upload a new one).
Linking Document to one kind of entity is not a problem, my problem is that there are many kinds of entities. How should I manage that? I have 2 ideas which have their own problems...:
Create a ManyToMany relationship between Document and Employee, another between Document and Car, etc.
Problem: I have to duplicate the Controller code to attach Document, duplicate the forms, etc.
Create a single join table containing the Document's ID, the related entity's ID and the related entity's class name.
Problem: it doesn't look really clean to me, I didn't really dig in this way but I feel I'll have a lot of "entity mapping" problems.
Any suggestion?
[EDIT]
In fact I have to do the same for Event as well: I need to link some Events to some Employees and/or to some Cars, etc. And in my real case, I have more than 10 Entities to be linked to Event and/or Document, which means duplicating more tha 20 times the code if I go with the solution 1!

Assuming you're using Doctrine ORM, i think you're searching for the Mapped Superclasses inheritance.
The docs are better than words :
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#mapped-superclasses

So finally I managed to solve my problem, following #Rpg600 idea about Mapped Superclasses.
This is probably not the best and cleanest solution ever, I'm not really proud of it but it does the job and it is still better than my first ideas.
I create a BaseEntity which is my a mapped superclass (Employee, Car, etc. Entities have to extend this Class):
/**
* BaseEntity
* #ORM\MappedSuperclass
*/
class BaseEntity {
/**
* #ORM\OneToOne(targetEntity="MyProject\MediaBundle\Entity\Folder")
*/
private $folder;
/**
* Set folder
* #param \Webobs\MediaBundle\Entity\Folder $folder
* #return BaseEntity
*/
public function setFolder(\Webobs\MediaBundle\Entity\Folder $folder = null){
$this->folder = $folder;
return $this;
}
/**
* Get folder
* #return \Webobs\MediaBundle\Entity\Folder
*/
public function getFolder(){
return $this->folder;
}
}
As it is not possible to have a Many-to-Many relationship in a superclass, I use a Folder which will contain one or several Document. This is the dirty part of the solution ; the folder table basically contain only one field which is the id...
class Folder
{
private $id;
/**
* Note : Proprietary side
* #ORM\ManyToMany(targetEntity="MyProject\MediaBundle\Entity\Document", inversedBy="folders", cascade={"persist"})
* #ORM\JoinTable(name="document_in_folder")
*/
private $documents;
// setters and getters
Then I create a helper class (which is declared as a service) to manage the link between any Entity and the Document:
class DocumentHelper extends Controller
{
protected $container;
/** ************************************************************************
* Constructor
* #param type $container
**************************************************************************/
public function __construct($container = null)
{
$this->container = $container;
}
/** ************************************************************************
* Attach Document(s) to an $entity according to the information given in the
* form.
* #param Entity $entity
* #param string $redirectRouteName Name of the route for the redirection after successfull atachment
* #param string $redirectParameters Parameters for the redirect route
* #return Response
**************************************************************************/
public function attachToEntity($entity, $redirectRouteName, $redirectParameters)
{
$folder = $entity->getFolder();
if($folder == NULL){
$folder = new Folder();
$entity->setFolder($folder);
}
$form = $this->createForm(new FolderType(), $folder);
// ------------- Request Management ------------------------------------
$request = $this->get('request');
if ($request->getMethod() == 'POST') {
$form->bind($request); // Link Request and Form
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($folder);
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl($redirectRouteName, $redirectParameters));
}
}
return $this->render('MyProjectMediaBundle:Folder:addDocument.html.twig', array(
'form' => $form->createView(),
'entity' => $entity,
));
}
Doing that way, I just have to add one small action in each relevant controller, let say EmployeeController.php:
public function addDocumentAction(Employee $employee)
{
$redirectRouteName = 'MyProjectCore_Employee_see';
$redirectParameters = array('employee_id' => $employee->getId());
return $this->get('myprojectmedia.documenthelper')->attachToEntity($employee,$redirectRouteName,$redirectParameters);
}
Same principle for the display, in the helper I have the common function which I call in my already-existing seeAction() and in the TWIG file I import the common "Document list" display.
That's all folks!
I hope this can help :)

Related

symfony factory pass custom parameters for use after persist - ZenstruckFoundryBundle

EDIT
Forgot to include the package i am using: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html
I am trying to figure out how i can pass extra data to the factory to use afer persist in some custom code.
I have a factory that generates a University, and after that is inserted in the database i need to then use the Course factory to assign courses which i want to do in afterPersist.
I have tried to add my own method to set the array of courses on the object:
/**
* #param array $courses
* #return $this
*/
public function setCourses(array $courses) : self
{
$this->courses = $courses;
return $this;
}
And then call it like this:
foreach($this->universities as $university){
UniversityFactory::new()->setCourses($university['courses'])->create([
'name'=>$university['name'],
'slug'=>$university['slug']
]);
}
However, i end up with an empty array in the afterPersists (this->courses is empty):
return $this->afterPersist(function(University $university){
foreach($this->courses as $type => $courseName){
CourseFactory::new()->create([
'name'=>$courseName,
'type'=>$type,
'uni'=>$university
]);
}
});
The array of courses does get assigned, but when running create() it gets emptied.
Edit 2
Dont forget to:
remove the code of $this->afterPersist
use Course entity class namespace in the University class file
update what you pass to setCourses:
foreach($this->universities as $university){
UniversityFactory::new()->setCourses(
array_map(function ($type,$courseName){
return (new Course())->setType($type)->setName($courseName);
}, array_keys($university['courses']), array_values($university['courses']))
)->create([
'name'=>$university['name'],
'slug'=>$university['slug']
]);
}
Edit 1
You can actualy handle persisting courses with their uni relation without using afterPersist hook. you can just set cascade={"persist"} as in below and assign uni of each course to $this in the university setCourses Method
/**
* #param Course[] $courses
* #return $this
*/
public function setCourses(array $courses) : self
{
$this->courses = [];
foreach($courses as $course){
if (!$this->courses->contains($course)) {
$this->courses[] = $course;
$course->setUni($this);
}
}
return $this;
}
Original Ansewar
Based on the fact that the $courses property is set but not persised
Its most likely you forgot to add cascade={"persist"} in the doctrine relation annotation
/**
* #ORM\OneToMany(... cascade={"persist"})
*/
private Collection|array $courses;
More about Transitive persistence / Cascade Operations

Setting a default value for a one-on-one association

Doctrine is a beast in combination with Symfony, but I can't seem to find good examples on how to achieve what I need.
My user schema is pretty standard. I want to set a one-to-one association that points to the forum permission a user has.
For this to work, I need to create a "default entity" that holds the default permissions given to the user upon creation.
Here is the User#forumPermission association
/**
* #var $forumPermissions ?ForumPermission
* One User instance has One Forum Permission instance.
* #ORM\OneToOne(targetEntity="ForumPermission")
* #JoinColumn(name="forum_permission_id", referencedColumnName="id")
*/
private $forumPermission;
/**
* #return ForumPermission
*/
public function getForumPermission() : ?ForumPermission
{
return $this->forumPermission;
}
/**
* #param ForumPermission $forumPermission
*/
public function setForumPermission(ForumPermission $forumPermission): void
{
$this->forumPermission = $forumPermission;
}
This means the forum_permission table is empty at this time.
After that, I read in the doctrine docs that you can listen to all kinds of events related to flushing and persisting. Here is the docs sections for onFlush
This is what I came up with
public function onFlush(OnFlushEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() as $entity) {
/* #var $entity User */
if (true === $entity instanceof User
&& null === $entity->getForumPermission()) {
$entity->setForumPermission($this->getDefaultForumPermission($em, $uow));
}
}
}
private function getDefaultForumPermission(
\Doctrine\ORM\EntityManagerInterface $em,
\Doctrine\ORM\UnitOfWork $uow)
{
// Get the default permissions form the database
$defaultForumPermission = $em->getRepository(ForumPermission::class)->find(1);
// I will create a new entity if the default permissions do not exist
if (null === $defaultForumPermission) {
$defaultForumPermission = new ForumPermission();
$uow->persist($defaultForumPermission);
$uow->computeChangeSet($em->getClassMetadata('\App\Entity\ForumPermission'), $defaultForumPermission);
}
return $defaultForumPermission;
}
The docs aren't very clear on what you can do and where you should do it.
I figure I could do this all in the controller, but I like to keep everything where it belongs as intended by design.
So I'm wondering if this is the best way to do it, should I be doing this during prePersist or another event? Any help is greatly appreciated.
If I get it right, each time a User is created you want to create a new ForumPermission associated to this User. The best way to do it is to listen to the prePersist Doctrine event.
See https://symfony.com/doc/current/doctrine/event_listeners_subscribers.html for additional information.

Symfony add data to object on pre persist

I have a form to create documents. On the one side I can add names and descriptions and next to that I can select one or several agencies to whom the created document belongs.
Each of the agencies is assigned to one specific market (there are 7 markets in total, so one market can have several agencies but one agency belongs only to one market!)
What I want to achieve is a "prePersist" function which automatically adds the correct market(s) (depending on the number of agencies selected) to the document.
My document entity has the two entities (markets and agencies) with the according getters and setters:
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Market", inversedBy="uploadProfiles", cascade={"persist"})
* #ORM\JoinTable(name="document_uploadprofile_markets",
* joinColumns={#ORM\JoinColumn(name="uploadprofile_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="market_id", referencedColumnName="id")})
**/
private $markets;
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Agency", inversedBy="uploadProfiles", cascade={"persist"})
* #ORM\JoinTable(name="document_uploadprofile_agencies",
* joinColumns={#ORM\JoinColumn(name="uploadprofile_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="iata8", referencedColumnName="iata8")})
**/
private $agencies;
public function __construct()
{
$this->agencies = new \Doctrine\Common\Collections\ArrayCollection();
$this->markets = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add market
*
* #param \AppBundle\Entity\Market $market
*
* #return UploadProfile
*/
public function addMarket(\AppBundle\Entity\Market $market)
{
$this->markets[] = $market;
return $this;
}
/**
* Remove market
*
* #param \AppBundle\Entity\Market $market
*/
public function removeMarket(\AppBundle\Entity\Market $market)
{
$this->markets->removeElement($market);
}
/**
* Get markets
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getMarkets()
{
return $this->markets;
}
/**
* Add agency
*
* #param \AppBundle\Entity\Agency $agency
*
* #return UploadProfile
*/
public function addAgency(\AppBundle\Entity\Agency $agency)
{
$this->agencies[] = $agency;
return $this;
}
/**
* Remove agency
*
* #param \AppBundle\Entity\Agency $agency
*/
public function removeAgency(\AppBundle\Entity\Agency $agency)
{
$this->agencies->removeElement($agency);
}
/**
* Get agencies
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getAgencies()
{
return $this->agencies;
}
I know that I can add the prePersist function to my document entity and try to code what I want to achieve but I don't think that's working because I need something like that:
foreach($document->getAgencies() as $agency) {
$document->setMarket($em->getRepository('AppBundle:Agency')->getMarket($agency));
}
I'm not even sure whether that foreach loop is correct since (so far) the result is always null. I've already asked a question to that topic here: Symfony use setter for Arraycollection in CreateController
I've also tried to write an own repository function to get all the distinct markets from my agency entity but so far that's not working either.
Another idea was a POST_SUBMIT event listener in my form class but so far that didn't make sense to me either.
Any ideas? If more code is needed, let me know!
edit
I edited and changed my code above in order to have a manytomany relationship between my markets and documents. What I then tried is, adding the prePersist function to my document entity and it actually worked fine while still having the OneToMany relationship (it was just always overwriting the previous market, but that doesn't matter now)
I'm now trying to edit that function so that several markets can be added to the document.
Two ideas I had, but they both didn't work out:
if(count($this->getAgencies()) > 0){
foreach($this->getAgencies() as $agency) {
$this->addMarket($agency->getMarket());
}
}
--> market is always null
or
if(count($this->getAgencies()) > 0){
$upId = rtrim($this->getId(),"_up");
$query = $em->createQuery("SELECT DISTINCT (a.market) FROM UserBundle\Entity\User u JOIN u.agencies a WHERE u.id = $userId");
$marketIds = $query->getResult();
$em = $this->getDoctrine ()->getManager ();
$repository = $this->getDoctrine()
->getRepository('AppBundle:Market');
$markets = $repository->findOneById($marketIds);
$this->addMarket($markets);
}
}
update
here is my prepersist function within my document entity and then the getMarkets() function, that has been suggested in one of the comments. I changed the name to addMarkets instead of getMarkets
/**
* #ORM\PrePersist
*/
public function prePersist() {
if(count($this->getAgencies()) > 0){
foreach($this->getAgencies() as $agency) {
$this->addMarkets($agency->getMarket());
}
}
}
public function addMarkets(\AppBundle\Entity\Market $market)
{
$markets = array();
foreach($this->agencies as $agency) {
$market = $agency->getMarket();
$id = $market->getId();
// Skip duplicates
if (isset($markets['id'])) {
continue;
}
$markets[$id] = $market;
}
return $markets;
}
another approach
so I edited it again, now my function looks like that
$markets = $this->getMarkets();
if(count($this->getAgencies()) > 0){
foreach($this->getAgencies() as $agency) {
if(!$this->markets->contains($markets)) {
$this->addMarket($agency->getMarket());
}
return;
dump($markets);
}
}
I thought that this might work to eliminate my duplicates but it does not.. any idea why?
This is a structural error in logic. And the clue is in your question and code.
automatically adds the correct market(s)
And:
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Market")
* #ORM\JoinColumn(name="market", referencedColumnName="id")
* #var \AppBundle\Entity\Market
**/
private $market;
Are incompatible.
If a document can have many agencies and agencies can have one market then your document must allow for many markets. For example:
DocumentA has Agency1 and Agency2.
Agency1 has MarketParis.
Agency2 has MarketMarrakesh.
This necessarily means that DocumentA has (Agency1's) MarketParis and (Agency2's) MarketMarrakesh -- many markets.
The question you're asking is a much larger topic than just setting or getting. If you want ONLY one market per document then you'll have to enforce uniqueness among document agencies. For example:
Your form creates DocumentA.
The user tries to set Agency1 (MarketParis) and Agency2 (MarketMarrakesh).
This throws an error because there can be ONLY ONE market.
Or another example:
Your form creates DocumentA
The user tries to set Agency1 (MarketParis) and Agency3 (MarketParis).
This is successful because the uniqueness of the Document's Market is enforced.
There are many strategies for this and is a much larger topic than your question.
EDIT
If your cardinality logic is correct (fixing any issues I described above) AND your doctrine annotations include cascading persist in all your entities, which look correct from the code above. The only thing I can think of that would not be working correctly is the "by_reference" attribute of your form. Setting "by_reference" to false in your form, with cascade-persist set in your entity, should persist all entities associated with the form. See the by_reference doc here: http://symfony.com/doc/current/reference/forms/types/collection.html#by-reference
If I understand what you said correctly, I believe Documents should not have a reference to Markets at all. But only references Agency.
A Document will have a ManyToMany relation with Agency and that's it.
Then in Document you can do something like:
public function getMarkets()
{
$markets = array();
foreach($this->agencies as $agency) {
$market = $agency->getMarket();
$id = $market->getId();
// Skip duplicates
if (isset($markets[id]) {
continue;
}
$markets[$id] = $market;
}
return $markets;
}
This looks like wrong approach. It makes more sense to have oneToMany relationship between Market and Agency and oneToMany between Agency and Document.

Symfony2 best practices for stay DRY

I'm new to Symfony2. I have to learn it for my new job (it starts this monday). Before that, I used a lot CodeIgniter... so this change a bit.
After reading tons of documentations, tuts, best practices ... create my Own intranet for testing (customers has websites, websites has accesses, accesses has website, website has category, accesses has accesscategory) I still have some questions.
First Question :
When you have a website with frontend and backend you have all the time some repetitives actions like :
- create new entity
- read entity
- update entity
- delete entity
...
In CI, I create a BaseController and a BaseModel and with some extends, I was OK.
This practice is still OK for Symfony 2 or do Symfony have another way to handle that ?
Like AppBundle\Controller\BaseController extended by a AppBundle\Controller\AdminController (and FrontController) extended by AppBundle\Controller\MyEntityController ?
Because Actually, each time, in each controller I have the same code. When I edit an entity (for example), it's the same process : load the entity by id, throw exception if no entity, create and hydrate the form, handleRequest the post and valid the form, reidrect or display the view... but... I always cut/paste the same code... aweful T__T
So I'm searching for the best way to handle that
** Second Question : **
What is the best and elegent way to work with the DoctrineManager ?
Do I have to call it, each time in my actions ? $em = $this->get... or, can I create something like MyEntityManager which call the EntityManager and the repository of my entity ?
Actually, this is what I do :
I create an abstract AppBundle\Manager\BaseManager with loadAndFlush
<?php
namespace AppBundle\Manager;
abstract class BaseManager
{
protected function persistAndFlush($entity)
{
$this->em->persist($entity);
$this->em->flush();
}
}
Then, for each Entity, I create his own manager :
<?php
namespace AppBundle\Manager;
use Doctrine\ORM\EntityManager;
use AppBundle\Manager\BaseManager;
use AppBundle\Entity\Customer;
class CustomerManager extends BaseManager
{
/**
* #var EntityManager
*/
protected $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* #param $customerId
* #return null|object
*/
public function loadCustomer($customerId)
{
return $this->getRepository()
->findOneBy(array('id' => $customerId));
}
/**
* #param Customer $customer
*/
public function saveCustomer(Customer $customer)
{
$this->persistAndFlush($customer);
}
/**
* #return \Doctrine\ORM\EntityRepository
*/
public function getRepository()
{
return $this->em->getRepository('AppBundle:Customer');
}
}
Then, I define this manager as a service :
parameters:
app.customer_manager.class: AppBundle\Manager\CustomerManager
services:
app.customer_manager:
class: %app.customer_manager.class%
arguments: [#doctrine.orm.entity_manager]
And Then I use the service in my Controller :
/**
* #Route("/edit/{customerId}", name="customer_edit")
* #Security("has_role('ROLE_ADMIN')")
*/
public function editAction($customerId, Request $request)
{
if (!$customer = $this->get('app.customer_manager')->loadCustomer($customerId)) {
throw new NotFoundHttpException($this->get('translator')->trans('This customer does not exist.'));
}
$form = $this->get('form.factory')->create(new CustomerType(), $customer);
if($form->handleRequest($request)->isValid()) {
$this->get('app.customer_manager')->saveCustomer($customer);
$request->getSession()->getFlashBag()->add('notice', 'Client bien enregistré.');
return $this->redirect(
$this->generateUrl(
'customer_show', array(
'customerId' => $customer->getId()
)
)
);
}
return $this->render('default/customer/add.html.twig', array(
'form' => $form->createView(),
'customer' => $customer
));
}
Is it a good practice, is it too complicated ? Is there any better other way to process in symfony ?
For first question Symfony2 provides CRUD Generator, take a look at this.
For second one you should use Repository Pattern provided by framework, for more information about this checkout following links:
http://msdn.microsoft.com/en-us/library/ff649690.aspx
http://symfony.com/doc/current/book/doctrine.html#custom-repository-classes

how displaying two ManyToMany entities Symfony2

I'm under SF2.0.15 with Doctrine2 and I have two entities.
-Expedition
- Step
To explain, one expedition can have several steps and one step can belong to several expeditions. In addition, one expedition belongs to his founder (named "owner" and stored in the User entity). So, I have chosen to make a ManyToMany joining between Expedition and Steps tables. In your opinion, is it a good choice or a wrong choice ?
I want to create a method which select all the steps which belong to one expedition (I have the expedition's Id which is contained in $id_exp). So, I have read lots of topics in the Internet but it always fail and I want to know why...
The entity Expedition.php
/**
* #ORM\ManyToOne(targetEntity="Easymuth\UserBundle\Entity\User")
* #ORM\JoinColumn(nullable=false)
*/
private $owner;
/**
* #ORM\ManyToMany(targetEntity="Easymuth\ExpeditionBundle\Entity\Step", cascade={"persist"})
*/
private $steps;
/**
* Add steps
*
* #param Easymuth\ExpeditionBundle\Entity\Step $steps
*/
public function addStep(\Easymuth\ExpeditionBundle\Entity\Step $step)
{
$this->steps[] = $step;
}
public function __construct()
{
$this->steps = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get steps
*
* #return Doctrine\Common\Collections\Collection
*/
public function getSteps()
{
return $this->steps;
}
ExpeditionRepository.php :
namespace Easymuth\ExpeditionBundle\Entity;
use Doctrine\ORM\EntityRepository;
class ExpeditionRepository extends EntityRepository
{
public function getStepsFromExpedition($id_exp) {
$qb = $this->createQueryBuilder('e')
->leftJoin('e.steps', 's')
->addSelect('s')
->where('e.id = :id')
->setParameter('id', $id_exp);
return $qb->getQuery()->getResult();
}
}
And finally in my controller, I have :
namespace Easymuth\ExpeditionBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Easymuth\ExpeditionBundle\Entity\Expedition;
class MainController extends Controller
{
public function stepsAction($id_exp) {
$expedition = $this->getDoctrine()
->getEntityManager()
->getRepository('EasymuthExpeditionBundle:Expedition')
->getStepsFromExpedition($id_exp);
print_r($expedition->getSteps()); // it displays a very long contents........
//return $this->render('EasymuthExpeditionBundle:Main:steps.html.twig'));
}
}
The displayed error on the print_r (or var_dump) is :
Fatal error: Call to a member function getSteps() on a non-object in /Applications/MAMP/htdocs/easymuth/src/Easymuth/ExpeditionBundle/Controller/MainController.php
Thank you very much for your help !
It is a good choice, you have to use ManyToMany associations for this design, good point !
But be careful, if you want to add information in your association (like order for example, can be useful for step in expedition), you have to create a new entity.
Check here for more info.
Then, the problem is in your controller. (You don't need additional function in your repository)
If you want to get all the steps from one expedition, just do in your controller :
//find your expedition
$expedition = $this->getDoctrine()
->getEntityManager()
->getRepository('EasymuthExpeditionBundle:Expedition')
->find($id_exp);
//then get the steps from this expedition
$steps = $expedition->getSteps();
You have to be sure that the expedition with $id_exp does exist or it will throw an error when you want to use your $expedition variable (because it is set at null)
You can check existence this way :
if(!$expedition) {
//do some stuff like throwing exception
}

Resources