I'm in a situation that need to update a Doctrine2 Entity and exclude some fields.
With ZF2 i have an action to handle update using Zend\Form and validation filter. In particular Dish Entity have a blob column called photo that is required. During an update i want to replace the photo only if a new file is provided.
Here there are the source code for the entity and the controller action that update dish.
Dishes\Entity\Dish.php
<?php
namespace Dishes\Entity;
use Doctrine\ORM\Mapping as ORM;
/** #ORM\Entity **/
class Dish
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
**/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $name;
/**
* #ORM\Column(type="text")
*/
protected $description;
/**
* #ORM\Column(type="integer")
*/
protected $time;
/**
* #ORM\Column(type="integer")
*/
protected $complexity;
/**
* #ORM\Column(type="blob")
*/
protected $photo;
/**
* Magic getter to expose protected properties.
*
* #param string $property
* #return mixed
*/
public function __get($property)
{
return $this->$property;
}
/**
* Magic setter to save protected properties.
*
* #param string $property
* #param mixed $value
*/
public function __set($property, $value)
{
$this->$property = $value;
}
}
Dishes\Controller\AdminController.php
public function editDishAction()
{
//Params from url
$id = (int) $this->params()->fromRoute('id', 0);
$objectManager = $this->objectManager;
$hydrator = new DoctrineObject($objectManager, false);
$form = new DishForm();
$existingDish = $objectManager->find('Dishes\Entity\Dish', $id);
if ($existingDish === NULL)
$this->notFoundAction();
$request = $this->getRequest();
if ($request->isPost())
{
$filter = new DishFilter();
$filter->get('photo')->setRequired(false);
$form->setHydrator($hydrator)
->setObject($existingDish)
->setInputFilter($filter);
$post = array_merge_recursive(
$request->getPost()->toArray(),
$request->getFiles()->toArray()
);
//Backup photo stream
$imageData = $existingDish->photo;
$form->setData($post);
if ($form->isValid())
{
//If user upload a new image read it.
if(!empty($existingDish->photo['tmp_name']))
$imageData = file_get_contents($existingDish->photo['tmp_name']);
$existingDish->photo = $imageData;
$objectManager->flush();
$this->redirect()->toRoute('zfcadmin/dishes');
}
}
else
{
$data = $hydrator->extract($existingDish);
unset($data['photo']);
$form->setData($data);
}
return new ViewModel(array('form' => $form));
}
Actually i set $dish->photo property to NULL but this violate DB NOT NULL constraint.
How can I tell Doctrine to exclude a particular entity field from update at runtime?
Doctrine maps every column's nullable property in database level to false by default since you don't set any nullable flag in your annotation:
/**
* #ORM\Column(type="blob")
*/
protected $photo;
This means, "Photo is required, you can't insert or update row's photo column with a null value".
If you want to have null values in your database, use the following annotation:
/**
* #ORM\Column(type="blob", nullable=true)
*/
protected $photo;
and in it's setter don't forget the null default argument value:
public function setPhoto($photo = null)
{
$this->photo = $photo;
}
For the question; seems like you're setting a new Dish object on every edit operation in the action:
$form->setHydrator($hydrator)
->setObject(new Dish)
->setInputFilter($filter);
This is correct when creating new Dish objects. When editing, you have to set an already persisted Dish instance to the form:
// I'm just writing to explain the flow.
// Accessing the object repository in action level is not a good practice.
// Use a DishService for example.
$id = 32; // Grab it from route or elsewhere
$repo = $entityManager->getRepository('Dishes\Entity\Dish');
$existingDish = $repo->findOne((int) $id);
$form->setHydrator($hydrator)
->setObject($existingDish)
->setInputFilter($filter);
I'm assuming this is edit action for an existing Dish.
So, the hydrator will correctly handle both changed and untouched fields on next call since you give an already populated Dish instance via the form.
I also recommend fetching the DishFilter from the InputFilterManager instead of creating it manually in action level:
// $filter = new DishFilter();
$filter = $serviceLocator->get('InputFilterManager')->get(DishFilter::class);
// Exclude the photo on validation:
$filter->setValidationGroup('name', 'description', 'time', 'complexity');
Hope it helps.
Related
I have the following structure:
Category property that contains link to property and its value:
<?php
class CategoryProperty
{
// ...
/**
* #var Property
*
* #ORM\ManyToOne(targetEntity="App\Entity\Property")
* #ORM\JoinColumn(onDelete="cascade", nullable=false)
*/
private $property;
/**
* Набор значений свойства доступных в product builder, null если любое значение.
*
* #var PropertyValueEntry
* #Assert\Valid
*
* #ORM\OneToOne(targetEntity="App\Entity\Properties\PropertyValues\PropertyValueEntry",
* cascade={"persist", "remove"})
*/
private $propertyValue;
// ...
}
Abstract property value type with a discriminator map:
<?php
/**
* #ORM\Entity
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="type", type="integer")
* #ORM\DiscriminatorMap({
* "1": "StringValue",
* "2": "IntegerValue",
* "3": "BooleanValue",
* "4": "TextValue",
* "6": "EnumValue",
* "7": "SetValue",
* "9": "LengthValue",
* "10": "AreaValue",
* "11": "VolumeValue",
* "12": "MassValue",
* })
* #ORM\Table(name="properties_values__value_entry")
*/
abstract class PropertyValueEntry
{
/**
* #var Property
*
* #ORM\ManyToOne(targetEntity="App\Entity\Property")
*/
private $property;
public function __construct(Property $property)
{
$this->property = $property;
}
public function getProperty(): Property
{
return $this->property;
}
/**
* #return mixed
*/
abstract public function getValue();
/**
* #param mixed $value
*/
abstract public function setValue($value): void;
}
And a sample concrete value type:
<?php
/**
* #ORM\Entity
* #ORM\Table(name="properties_values__integer_value")
*/
class IntegerValue extends PropertyValueEntry
{
/**
* #var int
* #Assert\NotNull
*
* #ORM\Column(type="integer")
*/
private $value = 0;
public function getValue(): int
{
return $this->value;
}
/**
* #param int|null $value
*/
public function setValue($value): void
{
if (!\is_int($value)) {
throw new InvalidArgumentException('BooleanValue accepts integer values only');
}
$this->value = $value;
}
}
For some reason, when form is submitted, instead of updating a value for IntegerValue, a new entity gets created, and new row in properties_values__value_entry / properties_values__integer_value. I tried tracking through the $this->em->persist($entity), where $entity is CategoryProperty, and it seems that IntegerValue gets marked as dirty and created anew. How can I track the cause of this happening? My form processing is pretty standard:
<?php
public function editAction(): Response
{
$id = $this->request->query->get('id');
$easyadmin = $this->request->attributes->get('easyadmin');
$entity = $easyadmin['item'];
$isReload = 'reload' === $this->request->request->get('action');
$editForm = $this->createForm(CategoryPropertyType::class, $entity, [
'category' => $this->getCatalog(),
'is_reload' => $isReload,
]);
$deleteForm = $this->createDeleteForm($this->entity['name'], $id);
$editForm->handleRequest($this->request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
if (!$isReload) {
$this->em->persist($entity);
$this->em->flush();
return $this->redirectToReferrer();
}
}
return $this->render($this->entity['templates']['edit'], [
'form' => $editForm->createView(),
'entity' => $entity,
'delete_form' => $deleteForm->createView(),
]);
}
UPDATE #1
What I already tried:
Retrieve category property by ID from entity manager through
$entity = $this->em->find(CategoryProperty::class, $id);
Altogether it seems this may be related to the fact that I have a dynamic form being created based on the selection. When I add a category property, I display a dropdown with a list of property types (integer, string, area, volume etc), and after selection a new form for that property is displayed. Though this works fine and adds new property without a problem, it seems that the code for EDITING same property is missing something, and instead of update it creates it anew.
Possibility #1: Load entity from entity manager directly
You don't appear to be retrieving an existing entity to modify at all.
$entity = $easyadmin['item'];
Shouldn't this be using Doctrine to retrieve an existing entity? For example:
if (!($entity = $this->getRepository(CategoryProperty::class)->findOneById($id))) {
throw $this->createNotFoundException("Category property not found.");
}
Semi-related: You may also want to check that a integer ID was specified at all, as $id = $this->request->query->get('id'); is very assumptive:
if (intval($id = $this->request->query->get('id')) < 1) {
throw $this->createNotFoundException("Category property not specified.");
}
Possibility 2: Missing identifier reference with one-to-one relationship
I think you may be getting duplication because CategoryProperty doesn't persist any reference to a PropertyValueEntry.
/**
* Набор значений свойства доступных в product builder, null если любое значение.
*
* #var PropertyValueEntry
* #Assert\Valid
*
* #ORM\OneToOne(targetEntity="App\Entity\Properties\PropertyValues\PropertyValueEntry", cascade={"persist", "remove"})
*/
private $propertyValue;
However PropertyValueEntry doesn't have an inverse relationship back to CategoryProperty.
A unidirectional one-to-one is fine, but it must have a #ORM\JoinColumn directive to ensure the identifier of the foreign PropertyValueEntry is persisted. Otherwise an edit form won't have any information to know which existing PropertyValueEntry (or derivative) it needs to edit. This is why your "properties_values__value_entry" form field is being reset with a new instance of PropertyValueEntry (or derivative) created when submitting the form.
You've not shown the source for entity class Property so I can't inspect for any further issues in your entity relationships.
Thanks to everyone participating, I have been reading through Symfony documentation and came across the 'by_reference' form attribute. Having considered that my form structure overall looks like this:
Category => CategoryPropertyType => PropertyValueType => [Set, Enum, Integer, Boolean, String, Volume]
for the form, I decided to set it to true in PropertyValueType configureOptions method. As it is explained in the documentation, with it being set to false, the entity is getting cloned (which in my case is true), thus creating a new object at the end.
Note that I'm still learning Symfony and will be refining the answer when I get a better understanding of what's going on behind the scenes.
I have two entities, for products and translations:
class ProductEntity
{
/**
* #Id
* #var string
* #Column(type="string", length=3)
*/
protected $code;
/**
* #OneToMany(targetEntity="ProductTranslationEntity", mappedBy="product")
*/
private $translations;
public function __construct()
{
$this->translations = new ArrayCollection();
}
/.../ getters and setters
public function addTranslation(ProductTranslationEntity $productTranslation)
{
$this->translations->add($productTranslation);
}
public function clearTranslations()
{
$this->translations->clear();
}
}
.
class ProductTranslationEntity
{
/**
* #ManyToOne(targetEntity="ProductEntity", inversedBy="translations")
* #JoinColumn(name="product_code", referencedColumnName="code", onDelete="CASCADE")
* #Id
*/
private $product;
/**
* #var string
* #Column(type="string", name="language_code", length=5)
* #Id
*/
protected $languageCode;
/**
* #var string
* #Column(type="string", name="product_name", length=128)
*/
protected $productName;
/.../ getters and setters
}
I like to replace all translations with new ones, from array like that:
['en' => ['name' => 'name_en'], 'de' => ['name' => 'name_de']];
Because in this array I have set of all supported languages the bast way I can see is to remove all existing translations and put new ones:
$product // Existing product entity
$product->clearTranslations();
$this->entityManager->flush($product);
foreach ($translations as $code => $translation) {
$t = new ProductTranslationEntity();
$t->setProduct($product);
$t->setLanguageCode($code);
$t->setProductName($translation['name']);
$this->entityManager->persist($t);
$product->addTranslation($t);
}
$this->entityManager->flush($product);
This solution doesn't work because after first $this->entityManager->flush($product); there are still translations in database so i get error about duplicates.
What have I done wrong in my solution? Or maybe there is another way to solve this kind of issue?
As Doctrine documentation refers:
Changes made only to the inverse side of an association are ignored.
Make sure to update both sides of a bidirectional association (or at
least the owning side, from Doctrine's point of view).
So to properly clear the translations of a product you should change the clearTranslations() function inside Product entity to:
public function clearTranslations()
{
foreach ($this->translations as $translation) {
$translation->setProduct(null);
}
$this->translations->clear();
}
so that you also update the owning side of the association before removal.
This maybe a bit overload but still it does not use extra requests to database:
$current_translations = $product->getTranslations();
foreach ($translations as $code => $translation) {
$translation_found = false;
foreach ($current_translations as $current_translation) {
if ($current_translation->getLanguageCode() === $code) {
// you've found the translation - replace value
$current_translation->setProductName($translation['name']);
$translation_found = true;
break;
}
}
if (!$translation_found) {
// translation with such code not found - add a new one
$t = new ProductTranslationEntity();
$t->setProduct($product);
$t->setLanguageCode($code);
$t->setProductName($translation['name']);
$this->entityManager->persist($t);
$product->addTranslation($t);
}
}
$this->entityManager->persist($product);
Use orphanRemoval=true:
/**
* #OneToMany(targetEntity="ProductTranslationEntity", mappedBy="product", orphanRemoval=true)
*/
private $translations;
i have two entities Survey.php and Choice.php I create a form to add new survey and multi choices, I used a many-to-one relation between the two entities, the problem is when I submit for a new survey, the foreign key of choice entity return null
here's my code
Survey.PHP
/**
* Survey
*
* #ORM\Table(name="survey")
* #ORM\Entity(repositoryClass="AppBundle\Repository\SurveyRepository")
*/
class Survey
{
/....
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Choice", mappedBy="survey_id",cascade="persist")
* #ORM\JoinColumn(nullable=false, referencedColumnName="id")
*/
private $choice;
public function __construct()
{
$this->choice = new ArrayCollection();
}
/**
* Add choice
*
* #param \AppBundle\Entity\Choice $choice
*
* #return Survey
*/
public function addChoice(\AppBundle\Entity\Choice $choice)
{
$this->choice[] = $choice;
$choice->setSurvey($this);
return $this;
}
/**
* Remove choice
*
* #param \AppBundle\Entity\Choice $choice
*/
public function removeChoice(\AppBundle\Entity\Choice $choice)
{
$this->choice->removeElement($choice);
}
/**
* Get choice
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getChoice()
{
return $this->choice;
}
}
Choice.php
/**
* Choice
* #ORM\Table(name="choice")
* #ORM\Entity(repositoryClass="AppBundle\Repository\ChoiceRepository")
*/
class Choice
{
/**
* #var int
* #ORM\ManyToOne(targetEntity="Survey",inversedBy="choice")
*/
private $survey;
/**
* Set survey
*
* #param \AppBundle\Entity\Survey $survey
*
* #return Choice
*/
public function setSurveyId(\AppBundle\Entity\Survey $survey)
{
$this->survey = $survey;
return $this;
}
/**
* Get surveyId
*
* #return \AppBundle\Entity\Survey
*/
public function getSurveyId()
{
return $this->survey_id;
}
}
SurveyController.php
<?php
namespace AppBundle\Controller;
use AppBundle\Entity\Survey;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
/**
* Survey controller.
*
*/
class SurveyController extends Controller
{
/**
* Creates a new survey entity.
* #param Request $request
* #return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function newAction(Request $request)
{
$survey = new Survey();
//
$form = $this->createForm('AppBundle\Form\SurveyType', $survey);
$form->handleRequest($request);
$survey->setCreateDate(new \DateTime('NOW'));
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($survey);
$em->flush();
return $this->redirectToRoute('survey_show', ['id' => $survey->getId()]);
}
return $this->render('survey/new.html.twig', [
'survey' => $survey,
'form' => $form->createView(),
]);
}
any suggestion, btw I think the problem is in the getters and setters )
Link the choice to the survey:
// Survey
public function addChoice(\AppBundle\Entity\Choice $choice)
{
$this->choice[] = $choice;
$choice->setSurvey($this);
return $this;
}
And change the survey_id stuff to survey. Dealing with objects not ids. And of course Survey::choice should be Survey::choices. The name changes might seem minor but will make your easier to maintain.
I fixed the problem by adding a for each loop inside the SurveyController.php
and it works just fine
SurveyController.php
if ($form->isSubmitted() && $form->isValid())
{
foreach ($survey->getChoices() as $choice){
$choice->setSurvey($survey);
}
$em = $this->getDoctrine()->getManager();
$em->persist($survey);
$em->flush();
not "THE best solution" but it gets the job done
It worked with me, but I had to remove the explicit foreign key mapping with the "inversedBy" setting from the class definition. I use a composite foreign key (using two columns), which maybe makes things harder though...
I create Event Listener for preUpdate of Post entity, that triggered fine, but when I try to update the related entity Category, it thrown an error:
Field "category" is not a valid field of the entity "BW\BlogBundle\Entity\Post" in PreUpdateEventArgs.
My Event Listener code is:
public function preUpdate(PreUpdateEventArgs $args) {
$entity = $args->getEntity();
$em = $args->getEntityManager();
if ($entity instanceof Post) {
$args->setNewValue('slug', $this->toSlug($entity->getHeading())); // work fine
$args->setNewValue('category', NULL); // throw an error
// other code...
My Post entity code is:
/**
* Post
*
* #ORM\Table(name="posts")
* #ORM\Entity(repositoryClass="BW\BlogBundle\Entity\PostRepository")
*/
class Post
{
/**
* #var string
*
* #ORM\Column(name="slug", type="string", length=255)
*/
private $slug;
/**
* #var integer
*
* #ORM\ManyToOne(targetEntity="\BW\BlogBundle\Entity\Category")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
// other code
How can I update this Category entity in this EvenetListener together with Post entity like in my example?
This answer work, but only for Post changes. But I also need change some values of Category entity, for example:
$entity->getCategory()->setSlug('some-category-slug'); // changes not apply, nothing happens with Category entity.
I guess the method setNewValue only works for a field that has changed. Maybe your category is already NULL. That's why it's throw the error. Here's the sample code from documentation.
/**
* Set the new value of this field.
*
* #param string $field
* #param mixed $value
*/
public function setNewValue($field, $value)
{
$this->assertValidField($field);
$this->entityChangeSet[$field][1] = $value;
}
/**
* Assert the field exists in changeset.
*
* #param string $field
*/
private function assertValidField($field)
{
if ( ! isset($this->entityChangeSet[$field])) {
throw new \InvalidArgumentException(sprintf(
'Field "%s" is not a valid field of the entity "%s" in PreUpdateEventArgs.',
$field,
get_class($this->getEntity())
));
}
Firstly, this question is similar to How to re-save the entity as another row in Doctrine 2
The difference is that I'm trying to save the data within an entity that has a OneToMany relationship. I'd like to re-save the entity as a new row in the parent entity (on the "one" side) and then as new rows in each subsequent child (on the "many" side).
I've used a pretty simple example of a Classroom having many Pupils to keep it simple.
So me might have ClassroomA with id=1 and it has 5 pupils (ids 1 through 5). I'd like to know how I could, within Doctrine2, take that Entity and re-save it to the database (after potential data changes) all with new IDs throughout and the original rows being untouched during the persist/flush.
Lets first define our Doctrine Entities.
The Classroom Entity:
namespace Acme\TestBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="classroom")
*/
class Classroom
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $miscVars;
/**
* #ORM\OneToMany(targetEntity="Pupil", mappedBy="classroom")
*/
protected $pupils;
public function __construct()
{
$this->pupils = new ArrayCollection();
}
// ========== GENERATED GETTER/SETTER FUNCTIONS BELOW ============
}
The Pupil Entity:
namespace Acme\TestBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="pupil")
*/
class Pupil
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $moreVars;
/**
* #ORM\ManyToOne(targetEntity="Classroom", inversedBy="pupils")
* #ORM\JoinColumn(name="classroom_id", referencedColumnName="id")
*/
protected $classroom;
// ========== GENERATED FUNCTIONS BELOW ============
}
And our generic Action function:
public function someAction(Request $request, $id)
{
$em = $this->getDoctrine()->getEntityManager();
$classroom = $em->find('AcmeTestBundle:Classroom', $id);
$form = $this->createForm(new ClassroomType(), $classroom);
if ('POST' === $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
// Normally you would do the following:
$em->persist($classroom);
$em->flush();
// But how do I create a new row with a new ID
// Including new rows for the Many side of the relationship
// ... other code goes here.
}
}
return $this->render('AcmeTestBundle:Default:index.html.twig');
}
I've tried using clone but that only saved the parent relationship (Classroom in our example) with a fresh ID, while the children data (Pupils) was updated against the original IDs.
Thanks in advance to any assistance.
The thing with clone is...
When an object is cloned, PHP 5 will perform a shallow copy of all of the object's properties. Any properties that are references to other variables, will remain references.
If you are using Doctrine >= 2.0.2, you can implement your own custom __clone() method:
public function __clone() {
// Get current collection
$pupils = $this->getPupils();
$this->pupils = new ArrayCollection();
foreach ($pupils as $pupil) {
$clonePupil = clone $pupil;
$this->pupils->add($clonePupil);
$clonePupil->setClassroom($this);
}
}
NOTE: before Doctrine 2.0.2 you cannot implement a __clone() method in your entity as the generated proxy class implements its own __clone() which does not check for or call parent::__clone(). So you'll have to make a separate method for that like clonePupils() (in Classroom) instead and call that after you clone the entity. Either way, you can use the same code inside your __clone() or clonePupils() methods.
When you clone your parent class, this function will create a new collection full of child object clones as well.
$cloneClassroom = clone $classroom;
$cloneClassroom->clonePupils();
$em->persist($cloneClassroom);
$em->flush();
You'll probably want to cascade persist on your $pupils collection to make persisting easier, eg
/**
* #ORM\OneToMany(targetEntity="Pupil", mappedBy="classroom", cascade={"persist"})
*/
protected $pupils;
I did it like this and it works fine.
Inside cloned Entity we have magic __clone(). There we also don't forget our one-to-many.
/**
* Clone element with values
*/
public function __clone(){
// we gonna clone existing element
if($this->id){
// get values (one-to-many)
/** #var \Doctrine\Common\Collections\Collection $values */
$values = $this->getElementValues();
// reset id
$this->id = null;
// reset values
$this->elementValues = new \Doctrine\Common\Collections\ArrayCollection();
// if we had values
if(!$values->isEmpty()){
foreach ($values as $value) {
// clone it
$clonedValue = clone $value;
// add to collection
$this->addElementValues($clonedValue);
}
}
}
}
/**
* addElementValues
*
* #param \YourBundle\Entity\ElementValue $elementValue
* #return Element
*/
public function addElementValues(\YourBundle\Entity\ElementValue $elementValue)
{
if (!$this->getElementValues()->contains($elementValue))
{
$this->elementValues[] = $elementValue;
$elementValue->setElement($this);
}
return $this;
}
Somewhere just clone it:
// Returns \YourBundle\Entity\Element which we wants to clone
$clonedEntity = clone $this->getElement();
// Do this to say doctrine that we have new object
$this->em->persist($clonedEntity);
// flush it to base
$this->em->flush();
I do this:
if ($form->isValid()) {
foreach($classroom->getPupils() as $pupil) {
$pupil->setClassroom($classroom);
}
$em->persist($classroom);
$em->flush();
}