Symfony optional one to one relationship - php

I have one entity Photo and another PhotoNote ( 0 to 10 )
A photo can have note but can’t too
When I request a photo entity I get the following error
Entity of type 'App\Entity\PhotoNote' for IDs idPhoto(1737) was not found
Using this in my controller
$photo = $photoRepository->findOneBy(['idPhoto' => $idPhoto]);
// check if there is a note
$note = (null !== $photo->getPhotoNote() ? $photo->getPhotoNote()->getNotePhoto() : 0);
// Also tried following
//$note = (null !== $photo->getPhotoNote()->getNotePhoto() ? $photo->getPhotoNote()->getNotePhoto() : null);
/**
* --> this throws the error : $photo->getPhotoNote()->getNotePhoto()
*/
And here is the dumping of $photo->getPhotoNote() in App\Entity\Photo :
Photo.php on line 443:
Proxies\__CG__\App\Entity\PhotoNote {#5710 ▼
+__isInitialized__: false
-idPhoto: 1737
-notePhoto: null
…2
}
Actually $photo->getPhotoNote() is not null, and photoNote is populated with the photoId. When using $photo->getPhotoNote()->getNotePhoto() doctrine generates the query to get the associated note, but that photo doesn’t have a note. Note is not mandatory.
What I want is ‘getPhotoNote’ returns null or even 0 but seems that one to one relation requires an existing id.
How to say to doctrine returns null ?
Class Photo {
/**
* #var int
*
* #ORM\Column(name="id_photo", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $idPhoto.
// some fields
/**
* #ORM\OneToOne(targetEntity=“App\Entity\PhotoNote”)
* #ORM\JoinColumn(name="id_photo", referencedColumnName="id_photo", nullable=true)
*/
private $photoNote;
public function setPhotoNote(PhotoNote $photoNote = null)
{
$this->photoNote = $photoNote;
return $this;
}
public function getPhotoNote()
{
return $this->photoNote;
}
}
Class PhotoNote {
/**
* #ORM\Id()
* #ORM\Column(name="id_photo", type="integer")
*/
private $idPhoto;
/**
* #ORM\Column(name="note_photo", type="smallint")
*/
private $notePhoto;
public function getNotePhoto(): ?int
{
return $this->notePhoto;
}
public function setNotePhoto(int $notePhoto): self
{
$this->notePhoto = $notePhoto;
return $this;
}
}
Generated query :
SELECT
t0.id_photo AS id_photo_1,
t0.note_photo AS note_photo_2
FROM
photo_note t0
WHERE
t0.id_photo = 1737;

For now the only way that I found is to not use a relationship between these two entities.
I get the photo’s note using the associated repository with something like
$note = $photoNoteRepo->findOneBy([
'idPhoto' => $idPhoto
]);
if ( empty($note))
$note = null;
I think it’s should be better with relation but I’m stuck with this issue.
This is a working workaround but it’s not answering the issue.
Maybe someone here will give me a way to use the relationship.
EDIT
I found a way based on this
public function getPhotoNote()
{
if ($this->photoNote instanceof \Doctrine\ORM\Proxy\Proxy) {
try {
$this->photoNote->__load();
} catch (\Doctrine\ORM\EntityNotFoundException $e) {
return null;
}
}
return $this->photoNote;
}
I have read something about doctrine's events especially postLoad event which seems cleaner but I did not implement it with success yet

Related

How can I get a method from an installed bundle?

Like this I created a method to get an array of the Ids of my category bundle:
class Event
{
/**
* #ORM\ManyToMany(targetEntity="Bundle\CategoryBundle\Entity\Category")
*/
private $categories;
/**
* #return array<string, mixed>
* #Serializer\VirtualProperty()
* #Serializer\SerializedName("categories")
*/
public function getCategories(): ?array
{
if($this->categories != NULL){
return $this->categories->map(fn($a) => $a->getId())->toArray();
} else {
return null;
}
}
But instead if "id" I try to get the "name". So I simply try to change this line:
return $this->categories->map(fn($a) => $a->getName())->toArray();
But I get the error:
Attempted to call an undefined method named "getName" of class
"Bundle\CategoryBundle\Entity\Category".
Did I need to join name field before?

Symfony - Update a unique OneToMany relation property

A Company can have multiple emails and all emails have to be unique.
This my Entites for Company and CompanyEmail
CompanyEmail Entity:
/**
* #ORM\Entity(repositoryClass="App\Repository\CompanyEmailRepository")
* #UniqueEntity("name")
*/
class CompanyEmail
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=128, unique=true)
* #Assert\Email()
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Company", inversedBy="emails")
* #ORM\JoinColumn(nullable=false)
*/
private $company;
// ...
}
Company Entity:
/**
* #ORM\Entity(repositoryClass="App\Repository\CompanyRepository")
*/
class Company
{
// ...
/**
* #ORM\OneToMany(targetEntity="App\Entity\CompanyEmail", mappedBy="company", orphanRemoval=true, cascade={"persist"})
* #Assert\Valid
*/
private $emails;
// ...
}
and I'm using an custom EmailsInputType that use this DataTransformer
class EmailArrayToStringTransformer implements DataTransformerInterface
{
public function transform($emails): string
{
return implode(', ', $emails);
}
public function reverseTransform($string): array
{
if ($string === '' || $string === null) {
return [];
}
$inputEmails = array_filter(array_unique(array_map('trim', explode(',', $string))));
$cEmails = [];
foreach($inputEmails as $email){
$cEmail = new CompanyEmail();
$cEmail->setName($email);
$cEmails[] = $cEmail;
}
return $cEmails;
}
}
and in the Controller a use this edit method
/**
* #Route("/edit/{id}", name="admin_company_edit", requirements={"id": "\d+"}, methods={"GET", "POST"})
*/
public function edit(Request $request, $id): Response
{
$entityManager = $this->getDoctrine()->getManager();
$company = $entityManager->getRepository(Company::class)->find($id);
$form = $this->createForm(CompanyType::class, $company);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager->flush();
}
}
There is two problems with this code
1 - In the edit form when i try to keep an already saved email Symfony generate a validation error that tells that this email is already exits.
2 - When I remove the validation restriction from the code, Symfony thrown the database error "*Integrity constraint violation: 1062 Duplicate entry ... *"
What i should do to make my code work as expected !
The problem is right here
public function reverseTransform($string): array
{
[...]
foreach($inputEmails as $email){
$cEmail = new CompanyEmail();
[...]
}
[...]
}
You need to retrieve the email instead of creating new one.
So basically, inject a CompanyEmailRepository, try to find if email already exists (findOneBy(['name'])), if it does not exists, create a new one but if exists, use what you've retrieved.
Just few notes
Pay attention to email owner (so the retrieve should be do per user I guess as no one can share the same mail UNLESS you can specify some aliases or shared address)
Maybe you don't need an extra entity like CompanyEmail as you can use a json field where you can store them in a comma separated fashion (unless you need some extra parameters or unless you need to perform some indexing/querying operation on the emails)

undefined method in controller from entity symfony

Hello I try to create an entity when another linked entity is created via a postPersist method but I find myself making this error someone knows why? I can not find the reason.
In ClientAdmin.php like the Sonata Documentation advice to do. Sonata Doc
public function postPersist($client)
{
if ($client instanceof Client )
{
$money = new Money();
$money->setClient($client);
$money->setSurname($client->getSurname());
$money->setFirstname($client->getFirstname());
}
}
Client.php :
/**
* #ORM\OneToOne(targetEntity="Money", mappedBy="client", cascade={"persist", "remove"})
*/
protected $money;
/**
* Set money
*
* #param \AppBundle\Entity\Money $money
*
* #return Client
*/
public function setMoney(\AppBundle\Entity\Money $money )
{
$this->money = $money;
}
/**
* Get money
*
* #return \AppBundle\Entity\Money
*/
public function getMoney()
{
return $this->money;
}
The error :
Solution :
Working but nothing is create is the table "Money" so i'm supposed it because I don't persist and flush it but I can't do it in it . :/
Working on Symfony 3.3 with SonataAdmin 3.19
Thanks in advance !
Edit : Solution found :
public function postPersist($client)
{
$em = $this->getConfigurationPool()->getContainer()->get('doctrine.orm.entity_manager');
if ($client instanceof Client )
{
$money = new Money();
$money->setClient($client);
$money->setSurname($client->getSurname());
$money->setFirstname($client->getFirstname());
$em->persist($money);
$em->flush();
}
}
}
your code is totally wrong.
$this->setMoney(new Money()); }
this means you call setMoney method of the class ClientAdminController(which is $this)
but ClientAdminController does not have the method setMoney(Money). You have to call it on a Client instance.

Exclude entity field from update in ZF2 Doctrine2

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.

Can't persist object in symfony2 console command

I've made a simple symfony2 console script which is supposed to convert data from old model to the new one.
Here's what it looks like:
class ConvertScreenshotsCommand extends Command
{
[...]
protected function execute(InputInterface $input, OutputInterface $output)
{
$em = $this->getContainer()->get('doctrine')->getManager();
$output->writeln('<info>Conversion started on ' . date(DATE_RSS) . "</info>");
$output->writeln('Getting all reviews...');
$reviews = $em->getRepository('ACCommonBundle:Review')->findAll(); // Putting all Review entities into an array
$output->writeln('<info>Got ' . count($reviews) . ' reviews.</info>');
foreach ($reviews as $review) {
$output->writeln("<info>Screenshots for " . $review->getTitle() . "</info>");
if ($review->getLegacyScreenshots()) {
foreach ($review->getLegacyScreenshots() as $filename) { // fn returns array of strings
$output->writeln("Found " . $filename);
$screenshot = new ReviewScreenshot(); // new object
$screenshot->setReview($review); // review is object
$screenshot->setFilename($filename); // filename is string
$em->persist($screenshot);
$em->flush(); // this is where it dies
$output->writeln("Successfully added to the database.");
}
} else $output->writeln("No legacy screenshots found.");
}
$output->writeln('<info>Conversion ended on ' . date(DATE_RSS) . "</info>");
}
}
The script breaks on $em->flush(), with the following error:
[ErrorException]
Warning: spl_object_hash() expects parameter 1 to be object, string given in
/[...]/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php line 1324
Obviously I'm doing something wrong, but can't figure out what it is. Thanks in advance!
** Update **
Review Entity mapping:
class Review
{
[...]
/**
* #ORM\OneToMany(targetEntity="ReviewScreenshot", mappedBy="review")
*/
protected $screenshots;
/**
* Won't be stored in the DB
* #deprecated
*/
private $legacyScreenshots;
/**
* New method to get screenshots, currently calls old method for the sake of compatibility
* #return array Screenshot paths
*/
public function getScreenshots()
{
// return $this->getLegacyScreenshots(); // Old method
return $this->screenshots; // New method
}
/**
* Get Screenshot paths
* #return array Screenshot paths
* #deprecated
*/
public function getLegacyScreenshots()
{
$dir=$this->getUploadRootDir();
if (file_exists($dir)) {
$fileList = scandir($dir);
$this->screenshots = array();
foreach ($fileList as $fileName)
{
preg_match("/(screenshot-\d+.*)/", $fileName, $matches);
if ($matches)
$this->screenshots[]=$matches[1];
}
return $this->screenshots;
}
else return null;
}
ReviewScreenshot mapping:
class ReviewScreenshot
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string $filename
*
* #ORM\Column(name="filename", type="string", length=255)
*/
private $filename;
/**
* #ORM\ManyToOne(targetEntity="Review", inversedBy="screenshots")
* #ORM\JoinColumn(name="review_id", referencedColumnName="id")
*/
protected $review;
/**
* #var integer $priority
*
* #ORM\Column(name="priority", type="integer", nullable=true)
*/
protected $priority;
/**
* #var string $description
*
* #ORM\Column(name="description", type="string", nullable=true)
*/
protected $description;
/**
* #Assert\File(maxSize="2097152")
*/
public $screenshot_file;
protected $webPath;
UnitOfWork.php
/**
* Gets the state of an entity with regard to the current unit of work.
*
* #param object $entity
* #param integer $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
* This parameter can be set to improve performance of entity state detection
* by potentially avoiding a database lookup if the distinction between NEW and DETACHED
* is either known or does not matter for the caller of the method.
* #return int The entity state.
*/
public function getEntityState($entity, $assume = null)
{
$oid = spl_object_hash($entity); // <-- Line 1324
if (isset($this->entityStates[$oid])) {
return $this->entityStates[$oid];
}
if ($assume !== null) {
return $assume;
}
// State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
// Note that you can not remember the NEW or DETACHED state in _entityStates since
// the UoW does not hold references to such objects and the object hash can be reused.
// More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
$class = $this->em->getClassMetadata(get_class($entity));
$id = $class->getIdentifierValues($entity);
if ( ! $id) {
return self::STATE_NEW;
}
switch (true) {
case ($class->isIdentifierNatural());
// Check for a version field, if available, to avoid a db lookup.
if ($class->isVersioned) {
return ($class->getFieldValue($entity, $class->versionField))
? self::STATE_DETACHED
: self::STATE_NEW;
}
// Last try before db lookup: check the identity map.
if ($this->tryGetById($id, $class->rootEntityName)) {
return self::STATE_DETACHED;
}
// db lookup
if ($this->getEntityPersister($class->name)->exists($entity)) {
return self::STATE_DETACHED;
}
return self::STATE_NEW;
case ( ! $class->idGenerator->isPostInsertGenerator()):
// if we have a pre insert generator we can't be sure that having an id
// really means that the entity exists. We have to verify this through
// the last resort: a db lookup
// Last try before db lookup: check the identity map.
if ($this->tryGetById($id, $class->rootEntityName)) {
return self::STATE_DETACHED;
}
// db lookup
if ($this->getEntityPersister($class->name)->exists($entity)) {
return self::STATE_DETACHED;
}
return self::STATE_NEW;
default:
return self::STATE_DETACHED;
}
}
I think the problem lies within Review::$screenshots:
You map it as a OneToMany association, so the value should be a Collection of ReviewScreenshot entities. But the method Review::getLegacyScreenshots() will change it into an array of strings.
You're probably using the change-tracking policy DEFERRED_IMPLICIT (which is the default). So when the property Review::$screenshots changes, Doctrine will try to persist that change, encounters strings where it expects entities, so throws the exception.

Categories