Symfony2 doctrine passing a parameter on foreign key in findBy function - php

i'm trying to load an entity passing a parameter to a foreign key which is the inverse side
i have this two entities
Ad
/**
* Ad
*
* #ORM\Table(name="Ad")
* #ORM\Entity(repositoryClass="Symarket\MarketBundle\Repository\AdRepository")
*/
class Ad
{
/**
* #var AdImage
*
* #ORM\OneToMany(targetEntity="AdImage", mappedBy="ad", cascade={"persist", "merge"})
*/
private $images;
//...
}
AdImage
/**
* AdImage
*
* #ORM\Table(name="AdImage")
* #ORM\Entity(repositoryClass="Symarket\MarketBundle\Repository\AdImageRepository")
*/
class AdImage {
/**
* #var boolean
*
* #ORM\Column(name="adi_is_visible", type="boolean")
*/
protected $isVisible;
/**
* #var Ad
*
* #ORM\ManyToOne(targetEntity="Ad", inversedBy="images", cascade={"persist"})
* #ORM\JoinColumn(name="adi_ad_id", referencedColumnName="ad_id")
*/
protected $ad;
}
now, by getting a Ad from the database i want to get ONLY the images which are "isVisibile" => true
how is this possible with queryBuilder?
what i tried so far with the findBy function is this
public function findById($adId) {
$res = $this->findBy(array("id" => $adId, "isVisible" => true, "images" => array("isVisible" => true)));
$ad = reset($res);
return $ad;
}
and i got this error
You cannot search for the association field 'Symarket\MarketBundle\Entity\Ad#images', because it is the inverse side of an association. Find methods only work on owning side associations.
then i tried this way with the querybuilder
public function findById($adId) {
$res = $query = $this->createQueryBuilder('ad')
->leftJoin('ad.images', 'img')
->where('img.isVisible = :adVisible')
->andWhere('ad.id = :id')
->setParameter('adId', $adId)
->setParameter('imgVisible', true)
->getQuery();
$ad = reset($res);
return $ad;
}
and i get NULL
Thanks in advance.

for those who may encounter this problem here goes my solution:
public function findById($adId) {
$ad = $this->getEntityManager()
->createQuery("SELECT a, i from MarketBundle:Ad a LEFT JOIN a.images i with i.isVisible = :visible where a.id = :adId")
->setParameter("adId", $adId)
->setParameter("visible", true)->getSingleResult();
return $ad;
}

You can do this with the default repository for your AdImage entity. Consider this example:
$entityManager = $this->getEntityManager();
// You said you already have the Ad entity instance you care about so just grabbing by id for demonstration purposes
$ad = $entityManager->getRepository('Ad')->findOneById(5);
$adImagesVisible = $entityManager->getRepository('AdImage')->findBy(array('isVisible' => true, 'ad' => $ad->getId()));
If you approach this from the entity on the "Many" side of the relationship you should be able to get what you want without having to write a custom DQL query.

Related

Symfony - creating new entity instead of updating existing

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.

Filter sub entity in doctrine

I have two entities that have a one to many relation.
There is a project:
class Project
{
// ...
/**
* #var \Doctrine\ORM\PersistentCollection|Template[]
*
* #ORM\OneToMany(targetEntity="Template", mappedBy="project")
*/
private $templates;
// ...
}
And I have templates which might be limited to certain users:
class Template
{
/**
* #var Project
*
* #ORM\ManyToOne(targetEntity="Project", inversedBy="templates")
* #ORM\JoinColumn(referencedColumnName="project_id", nullable=false)
*/
private $projectId;
/**
* #var array|null
*
* #ORM\Column(type="simple_array", nullable=true)
*/
private $userIds;
// ...
}
I now want to get all the projects. But the templates variable of each project should only have the templates that have either userIds NULL or the userId of the current user.
In my repository I already tried the following:
public function findForUser(int $userId): array
{
$qb = $this->entityManager->createQueryBuilder();
$qb->select('p')
->from(Project::class, 'p')
->leftJoin(Template::class, 't', Join::WITH, 't.project = p.projectId')
->where(
$qb->expr()
->isNull('t.userIds')
)
->orWhere('FIND_IN_SET(:userId, t.userIds) > 0');
$qb->setParameter(':userId', $userId);
return $qb->getQuery()
->execute();
}
But when I call getTemplates() I still have all templates in it even the ones the user is not allowed to see.
I also tried instead of ->select('p') to use ->select('p, t') as I hoped doctrine would then already fill the templates field with the selected templates but instead it then returns a mixed array of Templates and Projects.
I think that you entity schema is not quite optimised for this kind of operation.
You must not save the IDs of the Users in a simple array, but instead, use a ManyToMany relation :
class Template
{
// ...
/**
* #var User[]|Collection
*
* #ORM\ManyToMany(targetEntity="User", inversedBy="templates")
*/
private $users;
}
Then, the dql query should look something like that :
public function findForUser(User $user): array
{
$qb = $this->entityManager->createQueryBuilder();
$qb->select('p')
->from(Project::class, 'p')
->leftJoin(Template::class, 't', Join::WITH, 't.project = p.projectId')
->leftJoin(User::class, 'u', Join::WITH, 't.users = u.id')
->where('COUNT(t.users) = 0')
->orWhere('u = :user');
$qb->setParameter(':user', $user);
return $qb->getQuery()
->execute();
}

"NOT EXISTS" Query with Many to Many Relation Doctrine Symfony3

I would like to build a query that brings me all the games for a logged in user that he has not yet joined. For this I have built these 2 Entities. They are connected by many to many.
class Game
{
public function __construct()
{
$this->users = new ArrayCollection();
}
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
*
* #var Users[]
*
* #ORM\ManyToMany(
* targetEntity="Domain\Entity\User",
* inversedBy="user",
* fetch="EAGER"
* )
*/
private $users;
/**
* #return array
*/
public function getUsers() : array
{
return $this->users->getValues();
}
/**
* #param User $users
*/
public function setUser($users)
{
if(is_array($users)){
/** #var User $user */
foreach ($users as $user){
$this->users->add($user);
}
} else {
$this->users->add($users);
}
}
}
And the User Entity
class User implements AdvancedUserInterface
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
}
The Entities has more attributes but i think they are not important.
Also I tried these Query, but it doesn't work.
/**
* #param User $user
* #return array
*/
public function fetchAllNonActivatedWhereYouNotJoin(User $user): array
{
$qb = $this->createQueryBuilder('g');
$qb->select('g')
->innerJoin('g.users', 'u')
->where('u.id != :user')
->andWhere('g.activate = 0')
->setParameter('user', $user->getId())
->getQuery()->getResult();
return $qb->getQuery()->getResult();
}
Does anyone know a solution? Its Symfony 3 and Doctrine in PHP 7.1
One way to do it is left join the 2 entities starting from the game repository as you do, with a join condition to the logged in user and have a condition that users is empty:
$qb->leftJoin('g.users', 'u', Join::WITH, 'u.id = :user')
->andWhere('g.activate = 0')
->having('COUNT(u) = 0')
->groupby('g')
->setParameter('user', $user->getId())
->getQuery()->getResult();
This works because of doctrine hydration, which hydrates the users property on the limited joined query(in this case each game will either have the logged in user or not in the users collection).
There are also other ways to achieve this
Be careful with this if you are doing consecutive queries with the query builder, as the entity manager keeps references to the already hydrated relations. Reference of the issue

How to query the inverse side of a many to many relationship with Doctrine

I want to know which professional diseases are included in all the medical records of a company production unit. The entity MedicalRecord has a many to many relationship with DiseaseTypology as follows:
/**
* AppBundle\Entity\HealthData\MedicalRecord
*
* #ORM\Table(name="medical_record")
* #ORM\Entity(repositoryClass="MedicalRecordRepository")
* #ORM\HasLifecycleCallbacks
*/
class MedicalRecord
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string $companyProductionUnitId
*
* #ORM\Column(name="company_production_unit_id", type="integer",nullable=true)
*/
protected $companyProductionUnitId;
/**
* #var ArrayCollection $professionalDiseases
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\HealthData\Core\DiseaseTypology")
* #ORM\JoinTable(name="medical_record_professional_disease",
* joinColumns={#ORM\JoinColumn(name="medical_record_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="professional_disease_id", referencedColumnName="id")}
* )
*
*/
protected $professionalDiseases;
In the MedicalRecordReposity class I created the following method:
public function getProfessionalDiseasesByProductionUnit($productionUnitId)
{
$em = $this->getEntityManager();
$repository = $em->getRepository('AppBundle:MedicalRecord');
return $repository->createQueryBuilder('m')
->select('m.professionalDiseases')
->where('m.companyProductionUnitId = :productionUnitId')
->setParameter('productionUnitId', $productionUnitId)
->getQuery()
->getArrayResult();
}
But I get the error:
[Semantical Error] line 0, col 9 near 'professionalDiseases': Error: Invalid PathExpression. Must be a StateFieldPathExpression.
How to query the inverse side of a many to many relationship? Thank you!
I don't know if I can understand what you want, but here's my try:
class MedicalRecordRepository extends \Doctrine\ORM\EntityRepository
{
public function getProfessionalDiseasesByProductionUnit($productionUnitId)
{
$qb = $this->createQueryBuilder('m');
$qb
->select('m, pd')
->innerJoin('m.professionalDiseases', 'pd')
->where('m.companyProductionUnitId = :productionUnitId')
->setParameter('productionUnitId', $productionUnitId)
;
return $qb->getQuery()->getArrayResult();
}
}
Explanation: i think you need a join between MedicalRecord and DiseaseTypology, and for that, if you have this setup (in both your entities):
#Entity/MedicalRecord.php
private $companyProductionIUnitId;
/**
* #var \AppBundle\Entity\DiseaseTypology
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\DiseaseTypology", mappedBy="medicalRecords")
*/
private $professionalDiseases;
First, you have to have that mappedBy option, to tell doctrine the inverse side of the relationship.
And
# Entity/DiseaseTypology.php
/**
* #var \AppBundle\Entity\MedicalRecord
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\MedicalRecord", inversedBy="professionalDiseases")
*/
private $medicalRecords;
You have to have that inversedBy option to tell doctrine the owning side of the relationship.
Once we've clarified that, to let doctrine do its things related to joins, you just need to tell it on which field to make the join. And as in my example, the relation between MedicalRecord and DiseaseTypology is made through $professionalDiseases fields. So this one will be the field to make the join with:
->innerJoin('m.professionalDiseases', 'pd') // this professionalDiseases is the $professionalDiseses from MedicalRecord entity
Ok, I've did all those explanations, because I saw how you've did your query, and I feel is not the right approach.
And my results after running the getProfessionalDiseasesByProductionUnit() method was like this:
Note: Use getResult() instead of getArrayResult(), because you fetch entities (DiseaseTypology), not set of fields
There are 2 options here:
Make relation MedicalRecord <=> DiseaseTypology bidirectional See documentation. Then your method would look very simple:
public function getProfessionalDiseasesByProductionUnit($productionUnitId)
{
$em = $this->getEntityManager();
$repository = $em->getRepository(DiseaseTypology::class);
return $repository->createQueryBuilder('dt')
->select('dt')
->join('dt.medicalRecords', 'm')
->where('m.companyProductionUnitId = :productionUnitId')
->setParameter('productionUnitId', $productionUnitId)
->getQuery()
->getResult();
}
Keep existing DB structure and add some logic after query
public function getProfessionalDiseasesByProductionUnit($productionUnitId)
{
$em = $this->getEntityManager();
$repository = $em->getRepository(MedicalRecord::class);
$mediaRecords = $repository->createQueryBuilder('m')
->select('m, dt')
//note: with this join all professionalDiseases will be loaded within one query for all MedicalRecords
->join('m.professionalDiseases', 'dt')
->where('m.companyProductionUnitId = :productionUnitId')
->setParameter('productionUnitId', $productionUnitId)
->getQuery()
->getResult();
$professionalDiseases = [];
foreach($mediaRecords as $mediaRecord) {
foreach($mediaRecord->professionalDiseases as $professionalDisease) {
$professionalDiseases[professionalDisease->id] = $professionalDisease;
}
}
return $professionalDiseases;
}

Doctrine: QueryBuilder Where Exists

Answer
I was able to do the query using IS NOT EMPTY in the where clause.
/**
* Finds all developments having at least one image.
*
* #param string
* #return array
*/
public function withImages()
{
return $this->query->createQueryBuilder('d')
->where('d.images IS NOT EMPTY')
->getQuery()
->getResult();
}
Question
I am using the Doctrine ORM. I would like to be able to get all developments which have at least one image, such that for every Development selected in the query, the following property would be true. $development->getImages()->count()) > 0.
I have a Development entity which has a One to Many relationship with an Image entity.
/**
* The Development class provides an abstraction of a development.
*
* #Entity
* #HasLifecycleCallbacks
* #Table(name="developments")
**/
class Development extends BaseEntitiy {
/** #OneToMany(targetEntity="Exquisite\Entity\Image", mappedBy="development") **/
protected $images;
I have a DevelopmentRepository which has an instance of a EntityManager and an instance of the Repository for the Entity. I have made an attempt to do this in my withImages() method in the DevelopmentRepository class, but not having much luck.
class DevelopmentRepository implements DevelopmentRepositoryInterface {
/**
* Class Constructor
*
* #param Doctinre\ORM\EntityManager The EntityManager instance.
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
$this->query = $this->em->getRepository('Exquisite\Entity\Development');
}
/**
* Finds all developments having at least one image.
*
* #param string
* #return array
*/
public function withImages()
{
$qb = $this->query->createQueryBuilder('d');
$qb2 = $this->em->createQueryBuilder();
return $qb->where($qb->expr()->exists($qb2->select('i.development')->from('Exquisite\Entity\Image', 'i')->getDql()))
->getQuery()
->getResult();
}
Any help would be much appreciated!
I had the same problem, and this worked for me.
I fixed just doing a join (would return only the orders with items):
return $this->createQueryBuilder('so')
->select('so')
->join('so.orderItems', 'soi')
->getQuery()
->getResult();
Or doing a sub query with DQL
SELECT so
FROM Entity\\SalesOrder so
WHERE (SELECT count(soi.id) FROM Entity\\SalesOrderItem soi WHERE soi.salesOrder = so.id ) > 0
I hope this can be helpful

Categories