Symfony-Doctrine : join a table with a view after persisting entity - symfony

In my database, I have a table T and a view V.
The view has some columns of my table and other data (from other tables).
In Symfony, I declared my view as a read-only Entity.
/**
* #ORM\Table(name="V")
* #ORM\Entity(readOnly=true, repositoryClass="AppBundle\Entity\Repository\VRepository")
*/
class V
{
In my T entity, I did a Join :
/**
* #ORM\OneToOne(targetEntity="V")
* #ORM\JoinColumn(name="T_id", referencedColumnName="V_id")
*/
private $view;
And I added just the getter :
/**
* Get view
*
* #return \AppBundle\Entity\V
*/
public function getView()
{
return $this->view;
}
Everything is working well when I want to read and show data.
But I have a problem after persisting a new T entity.
Symfony seems to lost posted data of my form when I create a new T entity (editAction() works perfectly).
An exception occurred while executing 'INSERT INTO T (T_id, T_name, updated_at) VALUES (?, ?, ?)' with params [null, null, "2017-09-01 15:30:41"]:
SQLSTATE[23000]: Integrity constraint violation: 1048 Field 'T_id' cannot be empty (null)
When I remove ORM annotations of the $view property, it creates correctly my new record T in the database.
I think the problem is due to the fact that the V entity (the record in my SQL view) will exist just after the creation of T. And when I persist/flush data in Symfony, V doesn't exist yet. They are "created" at the same time.
I tried to add Doctrine #HasLifecycleCallbacks on my T entity and the #PostPersist event on the getView() method but it doesn't change anything...
Any idea to differ the Join after the creation of the entity ?
I know it's not conventional to use views as entities with Symfony but I haven't other choice.

I've just checked, it works fine with Bidirectional One-To-One relation
In my case tables are defined like:
create table T (`id` int(11) NOT NULL AUTO_INCREMENT, name varchar(100), primary key (id));
create view V as select id as entity, name, '123' as number from T;
Annotations in T:
/**
* #ORM\Table(name="T")
* #ORM\Entity()
*/
class T
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255, nullable=true)
*/
private $name;
/**
* #var V
*
* #ORM\OneToOne(targetEntity="V", mappedBy="entity")
*/
private $view;
Annotations in V:
/**
* #ORM\Table(name="V")
* #ORM\Entity(readOnly=true)
*/
class V
{
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255, nullable=true)
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="number", type="string", length=255, nullable=true)
*/
private $number;
/**
* #var T
*
* #ORM\Id
* #ORM\OneToOne(targetEntity="T", inversedBy="view")
* #ORM\JoinColumn(name="entity", referencedColumnName="id")
*/
private $entity;
And a test snippet to prove that it saves, updates and reads fine:
public function testCRUD()
{
/** #var EntityManager $manager */
$manager = $this->client->getContainer()->get('doctrine.orm.default_entity_manager');
$t = new T();
$t->setName('Me');
$manager->persist($t);
$manager->flush();
$t->setName('He');
$manager->flush();
$manager->clear();
/** #var T $t */
$t = $manager->find(T::class, $t->getId());
$this->assertEquals('He', $t->getView()->getName());
}

Based on the #Maksym Moskvychev answer : Prefer a bidirectional One-to-One relation.
T Entity :
/**
* #ORM\OneToOne(targetEntity="V", mappedBy="entity")
*/
private $view;
V Entity :
/**
* #ORM\OneToOne(targetEntity="T", inversedBy="view")
* #ORM\JoinColumn(name="V_id", referencedColumnName="T_id")
*/
private $entity;
Fix the loss of data after posting the addAction() form (new T instance).
In the form where I list all T records :
$builder->add('entity', EntityType::class, array(
'class' => 'AppBundle:T',
'choice_label' => 'id',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('t')
->orderBy('t.name', 'ASC')
->setMaxResults(25); // limit the number of results to prevent crash
}
))
Fix the too consuming resources problem (show 25 entities instead of 870+).
Ajax request :
$(".select2").select2({
ajax: {
type : "GET",
url : "{{ path('search_select') }}",
dataType : 'json',
delay : 250,
cache : true,
data : function (params) {
return {
q : params.term, // search term
page : params.page || 1
};
}
}
});
Response for Select2 :
$kwd = $request->query->get('q'); // GET parameters
$page = $request->query->get('page');
$limit = 25;
$offset = ($page - 1) * $limit;
$em = $this->getDoctrine()->getManager();
$repository = $em->getRepository('AppBundle:V');
$qb = $repository->createQueryBuilder('v');
$where = $qb->expr()->orX(
$qb->expr()->like('v.name', ':kwd'),
$qb->expr()->like('v.code', ':kwd')
);
$qb->where($where);
// get the DQL for counting total number of results
$dql = $qb->getDQL();
$results = $qb->orderBy('m.code', 'ASC')
->setFirstResult($offset)
->setMaxResults($limit)
->setParameter('kwd', '%'.$kwd.'%')
->getQuery()->getResult();
// count total number of results
$qc = $em->createQuery($dql)->setParameter('kwd', '%'.$kwd.'%');
$count = count($qc->getResult());
// determine if they are more results or not
$endCount = $offset + $limit;
$morePages = $count > $endCount;
$items = array();
foreach ($results as $r) {
$items[] = array(
'id' => $r->getCode(),
'text' => $r->getName()
);
}
$response = (object) array(
"results" => $items,
"pagination" => array(
"more" => $morePages
)
);
if (!empty($results))
return new Response(json_encode($response));

Related

How to order by entity property in symfony?

I'm trying to get the "demands" of a user.
User can have some demands and a demand have only one user (OneToMany)
This is my User entity (Utilisateur in french) :
class Utilisateur extends AbstractEntity implements UserInterface, PasswordAuthenticatedUserInterface
{
/**
* #ORM\Id
* #ORM\Column(type="ulid", unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class=UlidGenerator::class)
*/
private Ulid $id;
/**
* #ORM\OneToMany(targetEntity=DemandeTransport::class, mappedBy="utilisateur", orphanRemoval=true)
*/
private Collection $demandeTransports;
And my demands entity :
class DemandeTransport extends AbstractEntity
{
/**
* #ORM\Id
* #ORM\Column(type="ulid", unique=true)
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class=UlidGenerator::class)
*/
private Ulid $id;
/**
* #ORM\ManyToOne(targetEntity=Utilisateur::class, inversedBy="demandeTransports")
* #ORM\JoinColumn(nullable=false)
*/
private Utilisateur $utilisateur;
My controller receiving the request :
/**
* #throws Exception
*/
#[Route('/liste_propositions_transporteur', name: 'liste_propositions_transporteur', methods: ['GET'])]
public function listePropositionsTransporteur(Request $request): Response
{
return match ($request->getMethod()) {
'GET' => new Response(json_encode(['success' => true, 'data' => $this->propositionsTransportService->handleListePropositionsByUser($this->getUser())])),
default => new Response(404),
};
}
The service handling the request and retreiving the demands :
/**
* #param UserInterface $user
* #return array
*/
public function handleListePropositionsByUser(UserInterface $user) : array
{
$propositions = [];
foreach ($this->propositionTransportRepository->findPropositionsByUtilisateur($user) as $propositionTransport) {
$propositions[] = DemandeTransportHelper::serializePropositionDemande($propositionTransport);
}
return $propositions;
}
And the DQL :
/**
* #param UserInterface $user
* #return mixed
*/
public function findPropositionsByUtilisateur(UserInterface $user) : mixed
{
$q = $this->createQueryBuilder('p')
->where('p.utilisateur = :utilisateur')
->setParameters([
'utilisateur' => $user
])
->orderBy('p.dateCreation', 'DESC');
return $q->getQuery()->getResult();
}
So :
When i'm doing $utilisateur->getDemandesTransports() : it works by showing me all the demands.
Well, but when I'm trying to get them by DQL (cause I want them orderd by), it returns me 0 results...
Solved by setting the parameter type :
->setParameter('utilisateur', $utilisateur->getId(), 'ulid')
I'm using ULID (UUID like) on IDs.
https://symfony.com/doc/current/components/uid.html#working-with-ulids
With annotations
You can order your data by specifying sorting in your $demandeTransports property annotations.
/**
* #ORM\OneToMany(targetEntity=DemandeTransport::class, mappedBy="utilisateur", orphanRemoval=true)
* #ORM\OrderBy({"dateCreation" = "DESC"})
*/
private Collection $demandeTransports;
So when you call $utilisateur->getDemandesTransports() you will get ordered data.
With DQL
Also if you still want to use DQL then you should change your query to this as you need to join the Utilisateur entity then you can use the desired properties
$q = $this->createQueryBuilder('p')
->join('p.utilisateur', 'u')
->where('u.utilisateur = :utilisateur')
->setParameters([
'utilisateur' => $user
])
->orderBy('u.dateCreation', 'DESC');

How get Doctrine sortable "Duplicate entry error"

I have a problem with doctrine sortable extension.
first of all, i have a House entity with 1:n Releation to HouseImage entity (setup to save the image postion)
and a HouseImage entity 1:1 releation to File entity.
class House
{
/**
* #var HouseImage[]|Collection
*
* #ORM\OneToMany(
* targetEntity="HouseImage",
* mappedBy="houses",
* cascade={"persist", "remove"},
* orphanRemoval=true
* )
* #ORM\OrderBy({"position" = "ASC"})
*/
protected $images;
}
/**
* HouseImage
*
* #ORM\Table(name=house_image)
* #ORM\Entity(repositoryClass="Gedmo\Sortable\Entity\Repository\SortableRepository")
*/
class HouseImage {
/**
* #var House
*
* #ORM\ManyToOne(
* targetEntity="House",
* inversedBy="images",
* )
* #ORM\JoinColumn(
* name="house_id",
* referencedColumnName="id",
* onDelete="SET NULL"
* )
* #Gedmo\SortableGroup
*/
protected $houses;
/**
* #var File
*
* #ORM\OneToOne(
* targetEntity="File",
* )
*
* #ORM\JoinColumn(
* name="image_id",
* referencedColumnName="id",
* nullable=false,
* )
*/
protected $image;
/**
* #var integer
*
* #Gedmo\SortablePosition
* #ORM\Column(name="position", type="integer")
*/
protected $position;
....
}
//so i create some HouseImage-Objects
$HouseImage = new HouseImage();
$HouseImage->setImage($myFile);
$HouseImage2 = new HouseImage();
$HouseImage2->setImage($myFile2);
$HouseImage3 = new HouseImage();
$HouseImage3->setImage($myFile3);
//add first image-relation to house
$house->setImages([$HouseImage]);
$em->persist($house);
$em->flush();
//add second image-relation, should be inserted at first position
$house->setImages([$HouseImage2, $HouseImage]);
$em->persist($house);
$em->flush();
//add new list of image-relation
$house->setImages([$HouseImage2, $HouseImage3]);
$em->persist($house);
$em->flush();
///after flush the entitymanager this error occurred
An exception occurred while executing 'INSERT INTO house_image (position, house_id, image_id) VALUES (?, ?, ?)' with params [0, 123, 999]:\n
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '999' for key 'UNIQ_E0C3790C3DA5256D
the some error occurred with form factory
$data['images'] = [
[
'house' => 181,
'image' => 123
],
[
'house' => 181,
'image' => 1234
],
[
'house' => 181,
'image' => 12345
],
];
$form = $formFactory->create(HouseType::class, $houseObject);#
$form->submit($data, false);
Question:
How can i update the HouseImage-postion in the list by add a list of HouseImage
or
How can i cleanup all entites befor insert a complete new list of HouseImage-Releations
Edit:
I have fixed my issue by removing the addImage() method an implement this setImage() Method:
public function setImages(Collection $images): void
{
$col = new ArrayCollection();
$i = 0;
/* #var $image HouseImage */
foreach ($images as $image) {
$image->setHouse($this);
$image->setPosition($i++);
$col->add($image);
}
$this->images = $col;
}
Your way to create the house image entities is a little bit wrong, you can define that House entity should cascade persist your HouseImage collection. You should add this in your House entity:
class House
{
function __construct()
{
$this->images = new ArrayCollection();
}
public function addHouseImage(HouseImage $houseImage)
{
$this->images->add($houseImage);
$houseImage->setHouse($this);
}
}
Also rename $houses to $house in your HouseImage. You are linking HouseImage to House(not many houses). Also don't forget to link HouseImage with File entity. If you want to set the position after the last image on a new image you do that in addHouseImage function or anywhere else in your application. With the above code you code could look like this:
$image1 = ...;
$image2 = ...;
$image3 = ...;
$house->addHouseImage($image1);
add2.
add2.
// This will persist all house images along with house.
$em->persist($house);
$em->flush();
If I didn't cover something related to your question pleas let me know
Also the exception
'INSERT INTO house_image (position, house_id, image_id) VALUES (?, ?, ?)' with params [0, 123, 999]:\n SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '999' for key 'UNIQ_E0C3790C3DA5256D
tells you that there already is a File with id 999 in relation and you are trying to set 2 HouseImage entities to the same File entity. I can't spot it in your code but I'm almost certain that it happens somewhere in your code.

Override Method or EventListener: stop creation process and show warning just the first time in EasyAdmin?

I am using EasyAdmin in my SF 3.3 project but I need to achieve something different from how EasyAdmin has been built for. Take a look at the following picture:
As you might notice a user can be in more than one GroupingRole. Having that information the challenge is:
Check if the user has been assigned to any other GroupingRole
If the criteria meets the condition then show a warning message saying "The user A is already assigned to GroupingRole A" and prevent the record to be created. (this message could be in a popup, a javascript alert or an alert from Bootstrap - since EA already uses it)
When the admin click once again on "Save changes" the record should be created.
What I want to achieve with this approach is to alert the admin that the user is already to any other group but not stop him for create the record.
I have achieve some part of it already by override the prePersist method for just that entity (see below):
class AdminController extends BaseAdminController
{
/**
* Check if the users has been assigned to any group
*/
protected function prePersistGroupingRoleEntity($entity)
{
$usersToGroupRoleEntities = $this->em->getRepository('CommonBundle:UsersToGroupRole')->findAll();
$usersToGroupRole = [];
/** #var UsersToGroupRole $groupRole */
foreach ($usersToGroupRoleEntities as $groupRole) {
$usersToGroupRole[$groupRole->getGroupingRoleId()][] = $groupRole->getUsersId();
}
$usersInGroup = [];
/** #var Users $userEntity */
foreach ($entity->getUsersInGroup() as $userEntity) {
foreach ($usersToGroupRole as $group => $users) {
if (\in_array($userEntity->getId(), $users, true)) {
$usersInGroup[$group][] = $userEntity->getId();
}
}
}
$groupingRoleEnt = $this->em->getRepository('CommonBundle:GroupingRole');
$usersEnt = $this->em->getRepository('CommonBundle:Users');
$message = [];
foreach ($usersInGroup as $group => $user) {
foreach($user as $usr) {
$message[] = sprintf(
'The user %s already exists in %s group!',
$usersEnt->find($usr)->getEmail(),
$groupingRoleEnt->find($group)->getName()
);
}
}
}
}
What I don't know is how to stop the record to be created and instead show the warning just the first time the button is clicked because the second time and having the warning in place I should allow to create the record.
Can any give me some ideas and/or suggestions?
UPDATE: adding entities information
In addition to the code displayed above here is the entities involved in such process:
/**
* #ORM\Entity
* #ORM\Table(name="grouping_role")
*/
class GroupingRole
{
/**
* #ORM\Id
* #ORM\Column(name="id", type="integer",unique=true,nullable=false)
* #ORM\GeneratedValue
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="role_name", type="string", nullable=false)
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="role_description", type="string", nullable=false)
*/
private $description;
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="Schneider\QuoteBundle\Entity\Distributor", inversedBy="groupingRole")
* #ORM\JoinTable(name="grouping_to_role",
* joinColumns={
* #ORM\JoinColumn(name="grouping_role_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="DistributorID", referencedColumnName="DistributorID", nullable=false)
* }
* )
*
* #Assert\Count(
* min = 1,
* minMessage = "You must select at least one Distributor"
* )
*/
private $distributorGroup;
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="CommonBundle\Entity\Users", inversedBy="usersGroup")
* #ORM\JoinTable(name="users_to_group_role",
* joinColumns={
* #ORM\JoinColumn(name="grouping_role_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="users_id", referencedColumnName="users_id", nullable=false)
* }
* )
*
* #Assert\Count(
* min = 1,
* minMessage = "You must select at least one user"
* )
*/
private $usersInGroup;
/**
* Constructor
*/
public function __construct()
{
$this->distributorGroup = new ArrayCollection();
$this->usersInGroup = new ArrayCollection();
}
}
/**
* #ORM\Entity()
* #ORM\Table(name="users_to_group_role")
*/
class UsersToGroupRole
{
/**
* #var int
*
* #ORM\Id()
* #ORM\Column(type="integer",nullable=false)
* #Assert\Type(type="integer")
* #Assert\NotNull()
*/
protected $usersId;
/**
* #var int
*
* #ORM\Id()
* #ORM\Column(type="integer", nullable=false)
* #Assert\Type(type="integer")
* #Assert\NotNull()
*/
protected $groupingRoleId;
}
A little example by using form validation approach in EasyAdminBundle:
class AdminController extends EasyAdminController
{
// ...
protected function create<EntityName>EntityFormBuilder($entity, $view)
{
$builder = parent::createEntityFormBuilder($entity, $view);
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
$flag = false;
if (isset($data['flag'])) {
$flag = $data['flag'];
unset($data['flag']);
}
$key = md5(json_encode($data));
if ($flag !== $key) {
$event->getForm()->add('flag', HiddenType::class, ['mapped' => false]);
$data['flag'] = $key;
$event->setData($data);
}
});
return $builder;
}
protected function get<EntityName>EntityFormOptions($entity, $view)
{
$options = parent::getEntityFormOptions($entity, $view);
$options['validation_groups'] = function (FormInterface $form) {
if ($form->has('flag')) {
return ['Default', 'CheckUserGroup'];
}
return ['Default'];
};
$options['constraints'] = new Callback([
'callback' => function($entity, ExecutionContextInterface $context) {
// validate here and adds the violation if applicable.
$context->buildViolation('Warning!')
->atPath('<field>')
->addViolation();
},
'groups' => 'CheckUserGroup',
]);
return $options;
}
}
Note that PRE_SUBMIT event is triggered before the validation process happen.
The flag field is added (dynamically) the first time upon submitted the form, so the validation group CheckUserGroup is added and the callback constraint do its job. Later, the second time the submitted data contains the flag hash (if the data does not changes) the flag field is not added, so the validation group is not added either and the entity is saved (same if the callback constraint does not add the violation the first time).
Also (if you prefer) you can do all this inside a custom form type for the target entity.

Database Performance issues with Sonata Admin entity listing

I am using Symfony2 and Sonata Admin Bundle but I am encountering performance issues with entity listing.
I Have this Entity with Relations to other Entities:
class Collection
{
/**
* Primary Key - autoincrement value
*
* #var integer $id
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* OWNING SIDE
*
* #ORM\ManyToOne(targetEntity="\App\Entity\Order")
* #ORM\JoinColumn(name="orderId", referencedColumnName="id")
* #var \App\Entity\Order
*/
protected $order;
/* ... */
}
On my AdminClass i'd like to show for each collection some order details and other related entity information.
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('id')
->add(
'order.number',
null,
array(
'template' => 'App:Admin:Field/order-data.html.twig',
'label' => 'Order Number'
)
);
}
But these creates fore each referenced entity one extra query. How can I resolve this?
My first Idea was to extend the createQuery Method:
public function createQuery($context = 'list')
{
/** #var \Doctrine\ORM\QueryBuilder|QueryBuilder $query */
$query = parent::createQuery($context);
$query
->addSelect($query->getRootAliases()[0])
->addSelect('collectionOrder')
;
$query
->leftJoin($query->getRootAliases()[0] . '.order', 'collectionOrder')
;
return $query;
}
But this takes no effect.
So how can i manage to get all needed data with one query to reduce database load time?
Add the select to the table you joined with left join.
Example:
$query
->leftJoin($query->getRootAliases()[0] . '.order', 'collectionOrder')
->addSelect('collectionOrder')
;

Collection Prototype and Doctrine Persistance ManyToOne Relation

Context : I am building my little TodoList bundle (which is a good exercice to go deep progressively with Symfony2), the difficulty comes with recursivity : each Task can has children and parent, so I used Gedmo Tree.
I have a collection of tasks each having a sub collection of children, children collection has prototype enabled so I can display a new sub task form when clicking "add sub task".
I wanted the default name of the subtask to be "New Sub Task" instead of "New Task" set in Task constructor, so I figured out how to pass a custom instance for the prototype and took some care for preventing infinite loop.
So I am almost done and my new task is added with the name I set when saving...
Problem : I am not able to persist the parent task to the new sub task, the new task persist the name well, but not the parentId, I probably forgot somewhere with Doctrine, here is some relevant parts :
// Entity Task
/**
* #Gedmo\Tree(type="nested")
* #ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
* #ORM\HasLifecycleCallbacks
* #ORM\Table(name="task")
*/
class Task {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #Gedmo\Timestampable(on="create")
* #ORM\Column(type="datetime")
*/
protected $created;
/**
* #ORM\Column(type="string", length=255)
* #Assert\NotBlank(message="Name must be not empty")
*/
protected $name = 'New Task';
//....
/**
* #Gedmo\TreeLeft
* #ORM\Column(name="lft", type="integer")
*/
private $lft;
/**
* #Gedmo\TreeLevel
* #ORM\Column(name="lvl", type="integer")
*/
private $lvl;
/**
* #Gedmo\TreeRight
* #ORM\Column(name="rgt", type="integer")
*/
private $rgt;
/**
* #Gedmo\TreeRoot
* #ORM\Column(name="root", type="integer", nullable=true)
*/
private $root;
/**
* #Gedmo\TreeParent
* #ORM\ManyToOne(targetEntity="Task", inversedBy="children")
* #ORM\JoinColumn(name="parentId", referencedColumnName="id", onDelete="SET NULL")
*/
protected $parent = null;//
/**
* #ORM\Column(type="integer", nullable=true)
*/
protected $parentId = null;
/**
* #Assert\Valid()
* #ORM\OneToMany(targetEntity="Task", mappedBy="parent", cascade={"persist", "remove"})
* #ORM\OrderBy({"status" = "ASC", "created" = "DESC"})
*/
private $children;
public function __construct(){
$this->children = new ArrayCollection();
}
/**
* Set parentId
*
* #param integer $parentId
* #return Task
*/
public function setParentId($parentId){
$this->parentId = $parentId;
return $this;
}
/**
* Get parentId
*
* #return integer
*/
public function getParentId(){
return $this->parentId;
}
/**
* Set parent
*
* #param \Dmidz\TodoBundle\Entity\Task $parent
* #return Task
*/
public function setParent(\Dmidz\TodoBundle\Entity\Task $parent = null){
$this->parent = $parent;
return $this;
}
/**
* Get parent
*
* #return \Dmidz\TodoBundle\Entity\Task
*/
public function getParent(){
return $this->parent;
}
/**
* Add children
*
* #param \Dmidz\TodoBundle\Entity\Task $child
* #return Task
*/
public function addChild(\Dmidz\TodoBundle\Entity\Task $child){
$this->children[] = $child;
return $this;
}
/**
* Remove child
*
* #param \Dmidz\TodoBundle\Entity\Task $child
*/
public function removeChild(\Dmidz\TodoBundle\Entity\Task $child){
$this->children->removeElement($child);
}
}
// TaskType
class TaskType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options){
$builder
->add('name', null, ['label' => false])
->add('notes', null, ['label' => 'Notes'])
->add('status', 'hidden')
->add('parentId', 'hidden')
;
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($builder){
$record = $event->getData();
$form = $event->getForm();
if(!$record || $record->getId() === null){// if prototype
$form->add('minutesEstimated', null, ['label' => 'Durée', 'attr'=>['title'=>'Durée estimée en minutes']]);
}elseif($record && ($children = $record->getChildren())) {
// this is where I am able to customize the prototype default values
$protoTask = new Task();
$protoTask->setName('New Sub Task');
// here I am loosely trying to set the parentId I want
// so the prototype form input has the right value
// BUT it goes aways when INSERT in mysql, the value is NULL
$protoTask->setParentId($record->getId());
$form->add('sub', 'collection', [// warn don't name the field 'children' or it will conflict
'property_path' => 'children',
'type' => new TaskType(),
'allow_add' => true,
'by_reference' => false,
// this option comes from a form type extension
// allowing customizing prototype default values
// extension code : https://gist.github.com/jumika/e2f0a5b3d4faf277307a
'prototype_data' => $protoTask
]);
}
});
}
public function setDefaultOptions(OptionsResolverInterface $resolver){
$resolver->setDefaults([
'data_class' => 'Dmidz\TodoBundle\Entity\Task',
'label' => false,
]);
}
public function getParent(){ return 'form';}
}
// my controller
/**
* #Route("/")
* #Template("DmidzTodoBundle:Task:index.html.twig")
*/
public function indexAction(Request $request){
$this->request = $request;
$repo = $this->doctrine->getRepository('DmidzTodoBundle:Task');
$em = $this->doctrine->getManager();
//__ list of root tasks (parent null)
$query = $repo->createQueryBuilder('p')
->select(['p','FIELD(p.status, :progress, :wait, :done) AS HIDDEN field'])
->addOrderBy('field','ASC')
->addOrderBy('p.id','DESC')
->andWhere('p.parent IS NULL')
->setParameters([
'progress' => Task::STATUS_PROGRESS,
'wait' => Task::STATUS_WAIT,
'done' => Task::STATUS_DONE
])
->setMaxResults(20)
->getQuery();
$tasks = $query->getResult();
//__ form building : collection of tasks
$formList = $this->formFactory->createNamed('list_task', 'form', [
'records' => $tasks
])
->add('records', 'collection', [
'type'=>new TaskType(),
'label'=>false,
'required'=>false,
'by_reference' => false,
])
;
//__ form submission
if ($request->isMethod('POST')) {
$formList->handleRequest($request);
if($formList->isValid()){
// persist tasks
// I thought persisting root tasks will persist their children relation
foreach($tasks as $task){
$em->persist($task);
}
$em->flush();
return new RedirectResponse($this->router->generate('dmidz_todo_task_index'));
}
}
return [
'formList' => $formList->createView(),
];
}
As mentionned in the comments in TaskType, the form prototype of the new sub task has the right value for parentId which is posted, BUT the value is gone and NULL on INSERT in db (looking at the doctrine log).
So do you think it is the right way of doing, and then what thing I forgot for persisting correctly the parent task of the new sub task ?
On your child setting you should set the parent when adding, like so..
/**
* Add children
*
* #param \Dmidz\TodoBundle\Entity\Task $children
* #return Task
*/
public function addChild(\Dmidz\TodoBundle\Entity\Task $children){
$this->children->add($children);
$children->setParent($this);
return $this;
}
/**
* Remove children
*
* #param \Dmidz\TodoBundle\Entity\Task $children
*/
public function removeChild(\Dmidz\TodoBundle\Entity\Task $children){
$this->children->removeElement($children);
$children->setParent(null);
}
When your prototype adds and deletes a row it calls addChild and removeChild but it doesn't call the setParent in the associated child.
This way any child that is added or removed/deleted get automatically set in the process.
Also you could change the $children to $child as it makes grammatical sense and it's really bugging me because I am a child(ren).
It seems weird to me that you try using the parentId field as a simple column, whereas it is a relation column. Theoretically, you should not:
$task->getParentId(); //fetching a DB column's value
but instead:
$task->getParent()->getId(); //walking through relations to find an object's attribute
However, if you really need this feature to avoid loading the full parent object and just get its ID, your setParentId method should be transparent (although, as mentionned, I'm not sure using the same DB field is valid):
public function setParent(Task $t = null) {
$this->parent = $t;
$this->parentId = null === $t ? null : $t->getId();
return $this;
}
Back to your issue: in the TaskType class, you should call:
$protoTask->setParent($record);
instead of:
$protoTask->setParentId($record->getId());
The reason:
you tell Doctrine parentId is a relation field (in the $parent attribute declaration), therefore Doctrine expects an object of the proper type
you also tell Doctrine to map this relation field directly to an attribute (the $parentId attribute declaration), I'm neither convinced this is valid, nor convinced this is good practice, but I guess you did some research before going for this structure
you set $parentId, but $parent has not been set (i.e. null), so Doctrine must erase the $parentId value with the $parent value: your code is proof that Doctrine handles attributes first, then computes relations ;)
Keep in mind Doctrine is an Object Relational Mapper, not a simple query helper: mapper is what it does (mapping persistence layer with your code), relational is how it does it (one-to-many and the like), object is what it does it on (therefore not directly using IDs).
Hope this helps!

Resources