Sonata User Admin - Custom field dependency - symfony

I have extended the SonataAdmin class for FOSUser and added 2 custom fields (choice type from external data source): Company and Sector
I'd like to make Sector dependent on Company, so if the user selects a Company it filters the available Sectors.
I though about using FormEvents for filtering at page load, but I don't even know how to get the Company value of the current form.
Here is a part of my custom SectorType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SET_DATA
, function(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
// Need to get the company value here if set
});
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'choices' => $this->getSectors(),
));
}
public function getSectors()
{
$sects = array();
// Need to pass the selected company value to my getList
// (which gets the list of sector as you can imagine)
if (($tmp_sects = $this->ssrs->getList('Sector'))) {
foreach ($tmp_sects as $sect) {
$label = $sect['id'] ? $sect['label'] : '';
$sects[$sect['id']] = $label;
}
}
return $sects;
}
So the question is:
How to get the selected Company from my custom SectorType ?
After that I'll need to be able to refresh the Sector with Ajax, but that will be another question

I had a similar problem. I needed to create a sale entity that needed to be associated in a many to one relationship with an enterprise entity and a many to many relationship with services entities. Here is the Sale Entity:
The thing is that services where available depending on the companies chosen. For instance services a and b could only be provided to company x. And services b and c could only be provided to company y. So in my admin, depending on the chosen company I had to display the available services. For these I needed to do 2 things:
First create a dynamic form with my sale admin, so that on the server side I could get the right services available for the company specified in my sale record. And second, I had to create a custom form type for my company form element, so that when it was changed by the user on the client side, It would send an ajax request to get the right services for the company chosen.
For my first problem, I did something similar to what you were trying to achieve, but instead of creating an specific custom type for my services element, I added de event listener directly in the admin.
Here is the Sale entity:
/**
*
* #ORM\Table(name="sales")
* #ORM\Entity
* #ORM\HasLifecycleCallbacks()
*/
class Sale
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
public $id;
/**
* #ORM\ManyToOne(targetEntity="Branch")
* #ORM\JoinColumn(name="branch_id", referencedColumnName="id", nullable = false)
* #Assert\NotBlank(message = "Debe especificar una empresa a la cual asignar el precio de este exámen!")
*/
private $branch;
/** Unidirectional many to many
* #ORM\ManyToMany(targetEntity="Service")
* #ORM\JoinTable(name="sales_services",
* joinColumns={#ORM\JoinColumn(name="sale_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="service_id", referencedColumnName="id")}
* )
* #Assert\Count(min = "1", minMessage = "Debe especificar al menos un servicio a realizar!")
*/
private $services;
public function __construct() {
$this->services = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set branch
*
* #param Astricom\NeurocienciasBundle\Entity\Branch $branch
*/
//default value always have to be null, because when validation constraint is set to notblank,
//if default is not null, before calling the validation constraint an error will pop up explaining
//that no instance of Branch was passed to the $branch argument.
public function setBranch(\Astricom\NeurocienciasBundle\Entity\Branch $branch = null)
{
$this->branch = $branch;
}
/**
* Get branch
*
* #return Astricom\NeurocienciasBundle\Entity\Branch
*/
public function getBranch()
{
return $this->branch;
}
/**
* Add service
*
* #param \Astricom\NeurocienciasBundle\Entity\Service|null $service
*/
public function addServices(\Astricom\NeurocienciasBundle\Entity\Service $service = null)
{
$this->services[] = $service;
}
/**
* Get services
*
* #return Doctrine\Common\Collections\Collection
*/
public function getServices()
{
return $this->services;
}
/**
* Sets the creation date
*
* #param \DateTime|null $createdAt
*/
public function setCreatedAt(\DateTime $createdAt = null)
{
$this->createdAt = $createdAt;
}
/**
* Returns the creation date
*
* #return \DateTime|null
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* Sets the last update date
*
* #param \DateTime|null $updatedAt
*/
public function setUpdatedAt(\DateTime $updatedAt = null)
{
$this->updatedAt = $updatedAt;
}
So then in the Admin form builder:
protected function configureFormFields(FormMapper $formMapper) {
$em = $this->container->get('doctrine')->getEntityManager();
$branchQuery = $em->createQueryBuilder();
$branchQuery->add('select', 'b')
->add('from', 'Astricom\NeurocienciasBundle\Entity\Branch b')
->add('orderBy', 'b.name ASC');
$formMapper
->with('Empresa/Sucursal')
->add('branch','shtumi_ajax_entity_type',array('required' => true, 'label'=>'Empresa/Sucursal','error_bubbling' => true, 'empty_value' => 'Seleccione una empresa/sucursal', 'empty_data' => null, 'entity_alias'=>'sale_branch', 'attr'=>array('add_new'=>false), 'model_manager' => $this->getModelManager(), 'class'=>'Astricom\NeurocienciasBundle\Entity\Branch', 'query' => $branchQuery))
->end()
;
$builder = $formMapper->getFormBuilder();
$factory = $builder->getFormFactory();
$sale = $this->getSubject();
$builder->addEventListener(FormEvents::PRE_SET_DATA,
function(DataEvent $event) use ($sale,$factory, $em) {
$form = $event->getForm();
$servicesQuery = $em->createQueryBuilder();
$servicesQuery->add('select','s')
->add('from','Astricom\NeurocienciasBundle\Entity\Service s');
if (!$sale || !$sale->getId()) {
$servicesQuery
->where($servicesQuery->expr()->eq('s.id', ':id'))
->setParameter('id', 0);
}
else {
$servicesQuery
->join('s.branch', 'b')
->where($servicesQuery->expr()->eq('b.id', ':id'))
->setParameter('id', $sale->getBranch()->getId());
}
$form->add($factory->createNamed('services','entity',null,array('required' => true, 'label'=>'Servicios','error_bubbling' => true, 'attr'=>array('show_value_label'=>true),'class'=>'Astricom\NeurocienciasBundle\Entity\Service','multiple'=>true,'expanded'=>true,'query_builder'=>$servicesQuery)));
}
);
}
The trick thing was to pass the forms data. It doesn't work to use evet->getData() in the event listener's function. Instead I passed it through the admin->getSubject() method. Then instead of adding a sonataadmin form type, inside the event listener's function, I had to use a plain symfony form type.
The Ajax part as you mentioned is another question. All the weird things on the branch add method in the form builder is related to a customized field type for this matter. Don't worry about it.

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');

Check start date and end date to be a period of two weeks in Symfony

Could someone assist me with this. I'm having a trouble with creating a query or how to add changes to createAction to achieve the following. On clicking create it checks if a payroll period is valid, because in the payroll week table it is populated with a one week while the user enters a two week period.
Payroll period: payrollperiodid, start date, enddate and state
Payroll Week: id, startDAte, enddate, numofdays, normal hours.
for eg. user enters startdate: 16-07-2017 enddate: 29-07-2017 in payroll period then in the payroll week table period 1 startdate: 16-07-2017 endDate:22-07-2017 period 2 startdate:23-07-2017 enddate:29-07-2017.
Thus the period would be considered valid else error, and then also on create once the period is valid checks if it exists in the payroll period table else error. But i'm not sure how to add the part that ensures that the user enters a period of 2weeks. >7days <=14days, I wouldn't want to use hard numbers how could i achieve this
public function createAction(Request $request,$startDate, $endDate)
{
$entity = new Payrollperiod();
$form = $this->createCreateForm($entity);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('comtwclagripayrollBundle:Payrollperiod')->findByPayrollPeriod(array('startDate'=>$startDate,'endDate'=> $endDate));
if ($entity){
$this->addFlash('error','ERROR! Payroll Period exist');
return $this->redirect($this->generateUrl('payrollperiod_create'));
}
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('payrollperiod_show', array('payrollperiodid' => $entity->getpayrollperiodid())));
}
return array(
'entity' => $entity,
'form' => $form->createView(), );
}
public function findByPayrollPeriod($startDate, $endDate)
{
return $this->getEntityManager()
->createQuery(
'SELECT p FROM comtwclagripayrollBundle:PayrollWeek
WHERE startDate = :startDate or endDate = :endDate'
)
->setParameter('startDate', $startDate)
->setParameter('endDate', $endDate)
->getResult();
}
****Updates****
<?php
namespace com\twcl\agripayrollBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
//use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* Payrollperiod
*
* #ORM\Table(name="PayrollPeriod")
* #ORM\Entity
*
*/
class Payrollperiod
{
/**
* #var integer
*
* #ORM\Column(name="payrollperiodid", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $payrollperiodid;
/**
* #var \DateTime
*
* #ORM\Column(name="startDate", type="datetime", nullable=false)
* #Assert\Type("DateTime")
*/
private $startdate;
/**
* #var \DateTime
*
* #ORM\Column(name="endDate", type="datetime", nullable=false)
* #Assert\Type("DateTime")
*
*/
private $enddate;
/**
* #var integer
*
* #ORM\Column(name="State", type="integer", nullable=false)
*
*/
private $state;
public function getPayrollperiodid() {
return $this->payrollperiodid;
}
public function getStartdate() {
return $this->startdate;
}
public function getEnddate() {
return $this->enddate;
}
public function getState() {
return $this->state;
}
public function setPayrollperiodid($payrollperiodid) {
$this->payrollperiodid = $payrollperiodid;
}
public function setStartdate(\DateTime $startdate) {
$this->startdate = $startdate;
}
public function setEnddate(\DateTime $enddate) {
$this->enddate = $enddate;
}
public function setState($state) {
$this->state = $state;
}
/**
* Render a payrollPeriodID as a string.
*
* #return string
*/
public function __toString()
{
return (string) $this->getPayrollperiodid();
}
/**
* #Assert\Callback
*/
public function validatePayrollPeriod(Payrollperiod $Payrollperiod,ExecutionContextInterface $context)
{
$conflicts = $this->getDoctrine()
->getRepository('comtwclagripayrollBundle:Payrollperiod')
->findbyPayrollPeriod($Payrollperiod->getstartDate(), $Payrollperiod->getendDate())
;
if (count($conflicts) > 0) {
$context->buildViolation('Start date and end date exists')
->atPath('startdate')
->addViolation();
}
}
}
public function findbyPayrollPeriod(\DateTime $startDate, \DateTime $endDate)
{
$qb = $this->createQueryBuilder('e');
return $qb->andWhere('e.startDate = :startDate AND e.endDate = :endDate')
->setParameter('startDate', $startDate)
->setParameter('endDate', $endDate)
->getQuery()
->execute()
;
}
But i'm still not getting the error message, am I missing something
I think you can solve the issue, the following way
//create new trait
<?php
namespace yourBundlePath\Form\Type\Utility;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
*
* #package Daim\CoreBundle\Form\Type\Utility
*/
trait ContainerTrait
{
/**
* #var ContainerInterface
*/
private $containerObject;
/**
* #param ContainerInterface $container
* #return ContainerInterface
*/
public function setContainer(ContainerInterface $container)
{
return $this->containerObject = $container;
}
/**
* #return ContainerInterface
*/
public function getContainer()
{
return $this->containerObject;
}
}
//form
use yourBundlePath\Form\Type\Utility\ContainerTrait;
class yourFormClass
{
//call after the class declaration
use ContainerTrait;
$builder->addEventListener(
FormEvents::SUBMIT,
function (FormEvent $event) {
$form = $event->getForm();
$em = $this->getContainer()->get('doctrine');
$startDate = $form->get('startDate')->getData();
$endDate = $form->get('endDate')->getData();
$entity = $em->getRepository('comtwclagripayrollBundle:Payrollperiod')->findByPayrollPeriod(array('startDate'=>$startDate,'endDate'=> $endDate));
if ($entity){
$form->get('startDate')->addError(
new FormError(
'ERROR! Payroll Period exist'
)
);
}
}
);
Also you can refer the url: https://symfony.com/doc/current/form/dynamic_form_modification.html
One can solve this in various ways. If this is a common problem (and/ or you prefer a global solution) use the Class Constraint Validator. If you don't mind a 'local' solution look at Callback Constraint.
Both are explained in the documentation pages. Another reference is this SO question.
All that is left is how to calculate the difference between dates for that I'd suggest using PHP's DateTime::diff as something like:
$days = $startDate->diff($endDate)->days;
if ($days <= 7 && $days > 14) {
// build my constraint error message because the period is invalid.
}
Update #1
So let me first say after our comment spam, maybe start somewhere a little lighter. It seems your diving in right in the middle without any foundation on Symfony and/ or even PHP. Symfony has excellent tutorials and examples, but if you can't apply those your going to have a hard time.
The callback validate is only to check the difference between the two dates. An entity in general should not talk to the database but just itself / related entity classes.
class Payrollperiod {
...
/**
* #Assert\Callback
*/
public function validatePayrollPeriod(ExecutionContextInterface $context) {
$days = $this->startdate->diff($this->enddate)->days;
if ($days <= 7 && $days > 14) {
$context->buildViolation('There have to be at least 7 and a maximum of 13 days for your payroll period.')
->atPath('enddate') // this is where the message is bound to, can be either start or end date depending on where you prefer.
->addViolation();
}
}
}
Your findbyPayrollPeriod seems valid, as long as it is in your PayrollperiodRepository class file. And you do want to have a single equals check and not see if ranges overlap etc.
This function could also be handled using doctrine's unique constraints on multiple columns eg (user, startdate) and (user, enddate). This should give you an error when you attempt to add it as it then requires a unique value for the two. Even without the findbyPayrollPeriod function.
In your controller your repository line has multiple problems.
You are using an array for arguments not two arguments as the function has.
You are overwriting your form data entity because you are using the same variable name.
And your $startdate and $enddate appear like magic. They are from the entity, so use the getters.
And as a side note you might not want to redirect on the flash, but just continue as normal (so you don't loose your form data).
All in all you would get something partially like:
...
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$entityExists = $em->getRepository('comtwclagripayrollBundle:Payrollperiod')
->findByPayrollPeriod($entity->getStartdate(), $entity->getEnddate());
// If the entity does not exist we can add it and redirect to the new page.
if (!$entityExists) {
// Add our form entity to the database.
$em->persist($entity);
$em->flush();
// redirectToRoute is a controller helper function and is a tiny bit shorter.
return $this->redirectToRoute('payrollperiod_show', array(
'payrollperiodid' => $entity->getPayrollperiodid()
));
}
// Form is valid but we didn't return anything so far.
// So there is an entity with the same period start or end.
// Add a flash and show the form again.
$this->addFlash('error', 'A payroll period is already present
with the same start or end date.');
}
return ...

Payum Stripe data flow with Symfony

I am trying to create a checkout that allows a user to create an account for a fee (premium accounts, if you will). A user will create an account (marked as unpaid), the user will pay, and then on a successful payment the account is marked as paid. I can create an account, and I can make a charge. My problem is linking the two things together. I'm not sure how to reference the created account from the successful charge. Here is what I have so far.
Payment.php
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Payum\Core\Model\ArrayObject;
/**
* #ORM\Table
* #ORM\Entity
*/
class Payment extends ArrayObject
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*
* #var integer $id
*/
protected $id;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
}
CreateProfileController.php
CreateAction
public function createASquareAction(Request $request, $coupon)
{
$newUser = new User();
$registrationForm = $this->getRegistrationForm($newUser);
$registrationForm->handleRequest($request);
if ($registrationForm->isSubmitted() && $registrationForm->isValid()) {
$newSquare = new Square();
$newSquare->setProduct($registrationForm->get('product')->getData());
$newUser->addSquare($newSquare);
$cost = $this->getTotal($newSquare->getProduct(), $registrationForm->get('coupon')->getData());
$noPayment = $this->isAdmin() || $cost == 0;
$em = $this->getDoctrine()->getManager();
$em->persist($newUser);
$em->flush();
if ($noPayment) {
$newSquare->setVerified(true);
$em->persist($newSquare);
$em->flush();
return $this->redirectToRoute('edit', array(
'id' => $newSquare->getMsid()
));
} else {
$gatewayName = 'stripe_checkout_test';
$storage = $this->get('payum')->getStorage('AppBundle\Entity\Payment');
$payment = $storage->create();
$payment["amount"] = $cost;
$payment["currency"] = 'USD';
$payment["description"] = "Memorysquare";
$storage->update($payment);
$captureToken = $this->get('payum')->getTokenFactory()->createCaptureToken(
$gatewayName,
$payment,
'test_payment_done' // the route to redirect after capture;
);
return $this->redirect($captureToken->getTargetUrl());
}
}
return $this->render('create-a-square/create-a-square.html.twig', [
'registrationForm' => $registrationForm->createView(),
'coupon' => $coupon,
]);
}
Payment Complete Action
public function testPaymentDoneAction(Request $request)
{
$token = $this->get('payum')->getHttpRequestVerifier()->verify($request);
$identity = $token->getDetails();
$model = $this->get('payum')->getStorage($identity->getClass())->find($identity);
$gateway = $this->get('payum')->getGateway($token->getGatewayName());
// or Payum can fetch the model for you while executing a request (Preferred).
$gateway->execute($status = new GetHumanStatus($token));
$details = $status->getFirstModel();
return new JsonResponse(array(
'status' => $status->getValue(),
'details' => iterator_to_array($details),
));
}
Any help to get me on track would be greatly appreciated.
The answer to this was adding my order entity (or any entity you'd like) to the Payment (or PaymentDetails) entity like so (NOTE the cascade persist):
Payment.php
// ... all previous code ... //
/**
* #ORM\OneToOne(targetEntity="Order", cascade={"persist"})
* #ORM\JoinColumn(name="orderid", referencedColumnName="orderid")
*/
private $order;
/**
* Set order
*
* #param \AppBundle\Entity\Order $order
*
* #return Payment
*/
public function setOrder(\AppBundle\Entity\Order $order = null)
{
$this->order = $order;
return $this;
}
/**
* Get order
*
* #return \AppBundle\Entity\Order
*/
public function getOrder()
{
return $this->order;
}
Then in the payment preparation, I add the new order to the $payment object
public function createASquareAction(Request $request, $coupon)
{
// ... previous code ... //
$newOrder = new Order();
// do some setting on my order
$payment->setOrder($newOrder);
$storage->update($payment);
// ... rest of code ... //
}
Maybe this will help someone in the future. I also created an event subscriber to check the order onUpdate, and mark as paid if the stripe payment was successful.

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!

Symfony2: do not update a form field if not provided

I have a form for my "Team" entity. This entity has an "image" field. This field is required on creation process, but not required on edit process. But right now, during edit process, if I don't provide any image in the file input, the empty input is still persisted, and so my database field is emptied during the process.
How can I do to avoid persistence of this field, if nothing is provided in the form file input? So the entity keeps its old value for this field. Of course, if a file is provided, I want him to erase the old one.
My controller looks like this:
if ($request->getMethod() == 'POST') {
$form->bind($request);
if ($form->isValid()) {
$em->persist($team);
$em->flush();
...
}
}
and part of my entity, dealing with image (I'm pretty sure I have to do something in here, but don't know what exactly):
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function uploadImage() {
// the file property can be empty if the field is not required
if (null === $this->image) {
return;
}
if(!$this->id){
$this->image->move($this->getTmpUploadRootDir(), $this->image->getClientOriginalName());
}else{
$this->image->move($this->getUploadRootDir(), $this->image->getClientOriginalName());
}
$this->setImage($this->image->getClientOriginalName());
}
EDIT
Ok, I did some changes to this answer's code code, because apparently the event listener asks for a FormEvent instance in his callback, not a FormInterface instance.
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
// Retrieve submitted data
$form = $event->getForm();
$item = $event->getData();
// Test if upload image is null (maybe adapt it to work with your code)
if (null !== $form->get('image')->getData()) {
var_dump($form->get('image')->getData());
die('image provided');
$item->setImage($form->get('image')->getData());
}
});
When I provide an image, the script goes into the test, and die(), as expected. When I do not provide any file, the script doesn't go into the test if(), but my field in the database is still erased with an empty value. Any idea?
And as asked below, here is the Form
// src/Van/TeamsBundle/Form/TeamEditType.php
namespace Van\TeamsBundle\Form;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class TeamEditType extends TeamType // Ici, on hérite de ArticleType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// On fait appel à la méthode buildForm du parent, qui va ajouter tous les champs à $builder
parent::buildForm($builder, $options);
// On supprime celui qu'on ne veut pas dans le formulaire de modification
$builder->remove('image')
->add('image', 'file', array(
'data_class' => null,
'required' => false
))
;
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
// Retrieve submitted data
$form = $event->getForm();
$item = $event->getData();
// Test if upload image is null (maybe adapt it to work with your code)
if (null !== $form->get('image')->getData()) {
var_dump($form->get('image')->getData());
die('image provided');
$item->setImage($form->get('image')->getData());
}
});
}
// On modifie cette méthode car les deux formulaires doivent avoir un nom différent
public function getName()
{
return 'van_teamsbundle_teamedittype';
}
}
and the whole Team entity:
<?php
namespace Van\TeamsBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Team
*
* #ORM\Table()
* #ORM\HasLifecycleCallbacks
* #ORM\Entity
* #ORM\Entity(repositoryClass="Van\TeamsBundle\Entity\TeamRepository") #ORM\Table(name="van_teams")
*/
class Team
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=100)
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="countryCode", type="string", length=2)
*/
private $countryCode;
/**
* #ORM\ManyToOne(targetEntity="Van\TeamsBundle\Entity\Game")
* #ORM\JoinColumn(nullable=false)
*/
private $game;
/**
* #ORM\ManyToOne(targetEntity="Van\TeamsBundle\Entity\Statut")
* #ORM\JoinColumn(nullable=false)
*/
private $statut;
/**
* #var string $image
* #Assert\File( maxSize = "1024k", mimeTypesMessage = "Please upload a valid Image")
* #ORM\Column(name="image", type="string", length=255)
*/
private $image;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Team
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set countryCode
*
* #param string $countryCode
* #return Team
*/
public function setCountryCode($countryCode)
{
$this->countryCode = $countryCode;
return $this;
}
/**
* Get countryCode
*
* #return string
*/
public function getCountryCode()
{
return $this->countryCode;
}
/**
* Set image
*
* #param string $image
* #return Team
*/
public function setImage($image)
{
$this->image = $image;
return $this;
}
/**
* Get image
*
* #return string
*/
public function getImage()
{
return $this->image;
}
/**
* Set game
*
* #param \Van\TeamsBundle\Entity\Game $game
* #return Team
*/
public function setGame(\Van\TeamsBundle\Entity\Game $game)
{
$this->game = $game;
return $this;
}
/**
* Get game
*
* #return \Van\TeamsBundle\Entity\Game
*/
public function getGame()
{
return $this->game;
}
/**
* Set statut
*
* #param \Van\TeamsBundle\Entity\Statut $statut
* #return Team
*/
public function setStatut(\Van\TeamsBundle\Entity\Statut $statut)
{
$this->statut = $statut;
return $this;
}
/**
* Get statut
*
* #return \Van\TeamsBundle\Entity\Statut
*/
public function getStatut()
{
return $this->statut;
}
public function getFullImagePath() {
return null === $this->image ? null : $this->getUploadRootDir(). $this->image;
}
protected function getUploadRootDir() {
// the absolute directory path where uploaded documents should be saved
// return $this->getTmpUploadRootDir();
return __DIR__ . '/../../../../web/uploads/';
}
protected function getTmpUploadRootDir() {
// the absolute directory path where uploaded documents should be saved
return __DIR__ . '/../../../../web/uploads_tmp/';
}
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function uploadImage() {
// the file property can be empty if the field is not required
if (null === $this->image) {
return;
}
if(!$this->id){
$this->image->move($this->getTmpUploadRootDir(), $this->image->getClientOriginalName());
}else{
$this->image->move($this->getUploadRootDir(), $this->image->getClientOriginalName());
}
$this->setImage($this->image->getClientOriginalName());
}
/**
* #ORM\PostPersist()
*/
public function moveImage()
{
if (null === $this->image) {
return;
}
if(!is_dir($this->getUploadRootDir())){
mkdir($this->getUploadRootDir());
}
copy($this->getTmpUploadRootDir().$this->image, $this->getFullImagePath());
unlink($this->getTmpUploadRootDir().$this->image);
}
/**
* #ORM\PreRemove()
*/
public function removeImage()
{
unlink($this->getFullImagePath());
rmdir($this->getUploadRootDir());
}
}
EDIT 2
I did that. When I provide an image, it is saved in the image field in the database and redirection to my index page occurs. When I don't provide any image, redirection doesn't occur, and the following message appears above my file input in my form: "The file could not be found." In my TeamEditType class, I did the following, so the image should not be required.
$builder->remove('image')
->add('image', 'file', array(
'data_class' => null,
'required' => false
))
;
As of Symfony 2.3, you can simply use the PATCH http method, as documented here.
$form = $this->createForm(FooType::class, $foo, array(
'action' => $this->generateUrl('foo_update', array('id' => $foo->getId())),
'method' => 'PATCH',
));
It's an easy way to make a partial update of an entity using your main form, without rendering all the fields.
One approach in Symfony 2.4 (further information in Symfony2 coockbook) :
public function buildForm(FormBuilderInterface $builder, array $options)
{
// $builder->add() ...
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormInterface $form) {
// Retrieve submitted data
$form = $event->getForm();
$image = $form->getData();
// Test if upload image is null (maybe adapt it to work with your code)
if (null !== $form->get('uploadImage')->getData()) {
$image->setUploadImage($form->get('uploadImage')->getData());
}
});
}
Edit
It seems that you already test your prepersit data. Try the following :
public function setImage($image)
{
if($image !== null) {
$this->image = $image;
return $this;
}
}
I'm using symfony 3.3 and faced the same issue, below is the solution how I overcome it.
You need to create a extra property for image uploading in your entity other than image property, something like $file;
Product.php
/**
* #var string
*
* #ORM\Column(name="image", type="string")
*
*/
private $image;
/**
*
* #Assert\File(mimeTypes={ "image/jpeg", "image/jpg", "image/png" })
*/
private $file;
public function setFile($file)
{
$this->file = $file;
return $this;
}
public function getFile()
{
return $this->file;
}
// other code i.e image setter getter
...
ProductType.php
$builder->add('file', FileType::class, array(
'data_class' => null,
'required'=>false,
'label' => 'Upload Image (jpg, jpeg, png file)')
);
Form.html.twig
<div class="form-group">
{{ form_label(form.file) }}
{{ form_widget(form.file, {'attr': {'class': 'form-control'}}) }}
</div>
Finally ProductController.php
...
if ($form->isSubmitted() && $form->isValid()) {
$file = $item->getFile();
if($file instanceof UploadedFile) {
$fileName = md5(uniqid()).'.'.$file->guessExtension();
$file->move(
$this->getParameter('upload_directory'),
$fileName
);
$item->setImage($fileName);
}
...
}
...
Little more about upload_directory
app/config/config.html
parameters:
upload_directory: '%kernel.project_dir%/web/uploads'
Create a uploads directory in web dir.
I resolved this issue by using PHP Reflection API. My approach was to browse the attributes of the entity class and replace the values not provided in the query with those already saved.
/**
* #param \ReflectionClass $reflectionClass
* #param $entity
* #param Request $request
* #return Request
*/
public function formatRequest($class, $entity, Request $request){
$reflectionClass = new \ReflectionClass('AppBundle\Entity\\'.$class);
foreach ($reflectionClass->getProperties() as $attribut)
{
$attribut->setAccessible(true); // to avoid fatal error when trying to access a non-public attribute
if($request->request->get($attribut->getName()) == null) { // if the attribute value is not provided in the request
$request->request->set($attribut->getName(), $attribut->getValue($entity));
}
}
return $request;
}
And then I use it like this :
$request = $this->formatRequest("EntityName", $entity, $request);
This is really generic.

Resources