I want to display a table with some entity relations from a Doctrine DQL query.
My "main" entity Lead has a relation with Tour like this:
class Lead {
/**
* #var integer $tourId
* #ORM\Column(name="`tour_id`", type="integer")
*/
private $tourId;
/**
* #var Tour $tour
* #ORM\ManyToOne(targetEntity="Tour")
* #ORM\JoinColumn(name="tour_id", referencedColumnName="id")
*/
private $tour;
...
}
And I get the data from DB with a Doctrine2 DQL:
SELECT l, c
FROM BuvMarketplaceBundle:Lead l '
JOIN l.client c
Note that I don't JOIN with Tour becouse not all Leads have a Tour associated, this field can be null.
Then I am printing like this:
{% for lead in leads %}
{{ lead.id }}
{% if lead.tour %}
{{ lead.tour.name }}
{% endif %}
{% endfor %}
The problem comes where lead.tour has a numeric value, but this value does not exists in the tours table (because it has been deleted). I get the "Entity was not found." exception that refers to the lead.tour that does not exist in the DB.
I tried to check lead.tour with is defined, is not null but nothing works.
Twig is not supporting the cheking of types, so there is no basic function available to check lead.tour is object or similar.
Is there any way that can check an object from Twig or left join from the DQL?
A left join will solve the problem of not all leads having a tour.
SELECT lead,tour
FROM BuvMarketplaceBundle:Lead lead
LEFT JOIN lead.tour tour
And as my comment indicates, in a properly setup Doctrine 2 model, it will not be possible for lead to point to a tour record that does not exist.
Related
I made a query for selecting certain fields together with a leftjoin.
However I cannot get it to work both at the same time (have a leftjoin and certain selected fields).
$query = $em->getRepository(Product::class)
->createQueryBuilder('p')
->select('p, p.slug, p.price, p.title_x AS title, pc')
->leftJoin('p.productimgs', 'pc')
->getQuery();
I call the array with
{% for item in article.productimgs %}
but i get the error: Key "productimgs" for array with keys "0, slug, price, title" does not exist
I also tried to call it with the function
{% for item in article.getProductimgs() %}
but then i get this error : Impossible to invoke a method ("getProductimgs") on an array.
I am not so good with doctrine / query building.
The productimages is a onetomany relation in the product entity.
it's a symfony 5 project
All help appreciated, thank you!
Since you are mixing the entities and specific columns in the select the hydrated results will actually be arrays and not Products. You can see it's structure with
{{ dump(article) }}
If you just want to eager load the related product images in the one query use
$query = $em->getRepository(Product::class)
->createQueryBuilder('p')
->select('p, pc')
->leftJoin('p.productimgs', 'pc')
->getQuery();
This will hydrate the results as Products so you can access its properties by the Twig shorthands or as functions:
{{ article.slug }}
{{ article.getSlug() }}
If you access the Product's images it will not execute a new database query to fetch them since you added them to the select part and they were already hydrated into the result objects:
{{ article.productimgs }}
{{ article.getProductimgs() }}
Question really relates to best practice and whether what I have in my head is possible. I am querying for a collection of Member entities using a Repository function, for example (simplified)
/**
* #return Query
*/
public function findAllMembersOrderedByNameResult()
{
return $this->createQueryBuilder('m')
->orderBy('m.lastName', 'ASC')
->getQuery()
->getResult()
;
}
I am calling this in my Controller:
$members = $em->getRepository(Member::class)->findAllMembersOrderedByNameResult();
Now I am passing the result this to my twig template file and can loop through this and display information about each member as part of a foreach loop:
{% for member in members %}
{{member.firstName}}
{% endfor %}
This obviously works fine, however I have a need now to add some additional data to each member to pass to the twig template. So for example using the DateOfBirth in the Member entity I want to run this through a function to determine the age of the member to display in the Twig template. So at the moment I am making this call in the Controller, and then passing it over to the template by creating a MembersArray, looping through the returned $members Collection and adding in the new age and whole Member result as two separate values in the array, for example:
foreach($members as $member)
{
$membersArray[] = array(
'member' => $member,
'age' => $age
)
}
This does work, however in my Twig template I now have to use
{% for member in membersArray %}
{{member.member.firstName}}
{{member.age}
{% endfor %}
It would be much nicer to be able to just add age into the Collection without creating the array so I can just use member.firstName and member.age but for the life of me can't find a way how without having to loop through all values manually and set them to a new array, which seems a huge waste of code.
I will want to add more than just age, this is just a simplified example. Using Symfony 4.4 and PHP 7.3 in case anything that would help requires it
Edit: Entity structure as requested (cutdown):
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\MemberRepository")
*/
class Member
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $firstName;
/**
* #ORM\Column(type="date", nullable=true)
*/
private $dateOfBirth;
...
The simplest way would be adding custom getter methods to an entity itself, like:
public function getAge()
{
return (new DateTime())->diff($this->dateOfBirth)->y;
}
Then you can calculate the age in a twig template by calling member.age.
But usually, it's not recommended to have any logic in an entity itself.
Another way is using twig itself to format data. I would say it is preferable with the age example. Because age looks more like the view format of the dataOfBirth field than extra data, you can use a twig filter to calculate that. The built-in like {{ date().diff(member.dateOfBirgth)).format('%y') }}, or define a custom one with a twig extension, so the syntax would be more straightforward, like {{ member.dateOfBirgth|age }} or even {{ member|age }}.
In case you have to reuse formatting not only in twig but also in some services and don't want to put logic into an entity - you can decouple twig extension from the previous example to use the shared service that is responsible for formatting age. Other parts of the system can use the same formatting service.
Also, it's a common practice to put new methods to work with an entity to a custom entity manager service, like MemberManager. Usually, we use entity manager to manipulate entities, but you can add there the method to format age, like MemberManager->getAge($member). This would violate the single responsibility principle, so I can't recommend it.
If you are looking for extending doctrine entities with some extra methods with listeners or so, it's not possible by doctrine design. Anyway, it's the most, not obvious way.
To summarise, in most cases, when the custom function can be considered as a formatting one, I would recommend using the second option, with decoupling it to the third option whenever you have to reuse the formatting code. But if the project isn't complex, the first option also worth checking, as it's the simplest one.
given the two following intities:
<?php
/**
* User
* #ORM\Entity()
*/
class User implements AdvancedUserInterface, \Serializable, EncoderAwareInterface
{
/**
* #var Vip
* #ORM\OneToOne(targetEntity="Vip", mappedBy="user", fetch="EAGER")
*/
protected $vip;
// …
<?php
/**
* Vip
* #ORM\Entity()
*/
class Vip
{
/**
* #ORM\id #ORM\OneToOne(targetEntity="User", inversedBy="vip", fetch="EAGER")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false)
*/
protected $user;
// …
SHORT :
How can I do this SQL in DQL given above entities:
SELECT u.firstName, v.foo FROM User join Vip v ON v.user_id = u.id
In other words how can I retrieve 10 first users ( with their VIP infos if it exists), using DQL join in such a way that only one SQL query will be generated by Doctrine. Is that possible ?
Long story:
The owning side is the Vip entity because it holds the reference/foreign key to a User underneath in the database.
I am trying to retrieve all User with their Vip datas.
Using the knplabs/knp-paginator-bundle, I first set up a simple query:
$dql = "SELECT u, p FROM AppBundle:User u;
In spite of enforcing the fetch attribute as « EAGER », Vip infos where not part of the initial query. As a consequence, calling getter getVip() on each iteration from inside the twig for in loop like
{% for user in pagination %}
{% if user.getVip() %}
<span class="label label-warning">V.I.P</span>
{% endif %}
{{% endfor %}}
.. caused a query to be issued on each iteration !
The Symfony dev bar shows 6 queries:
DQL documentation and says that one can use JOIN keyword. So my query became:
$dql = "SELECT u, v FROM AppBundle:User u JOIN u.vip v;
But now I get this error:
Warning: spl_object_hash() expects parameter 1 to be object, null
given
Here I'm stuck, wondering how I could fetch Vip datas (or null) along with User datas, in a single query.
In other words how can I retrieve 10 first users ( with their VIP
infos if it exists), using DQL join in such a way that only one SQL
query will be generated by Doctrine. Is that possible ?
You should initialize all related entities using select clause to avoid additional queries when accessing to the related objects.
$repository = $em->getRepository(User::class);
$users = $repository->createQueryBuilder('u')
->addSelect('v') // Initialize Vip's
->join('u.vip', 'v')
->getQuery()
->setMaxResults(10)
->getResult();
Yes, one may add associated entities to the SELECT statement.
But more precisely, one should only add relations that are really involved in the expected result , in other words, entities fetched as "EAGER".
I realized that the vip entity had another relation (oneToMany with a vehicule entity). I just want to retrieve users with their vip metas. Adding another JOIN to the query would just bring more datas since I would not use vehicules anyway (and issue extra work behind the scenes).
-> So I simply changed the fetch attribute from "EAGER" to "LAZY" in the vip OneToMany declaration.
To conclude:
Ask yourself «what are involved intities ?», should it be part
of the result (do you simply need those infos).
if NO, you might turn fetch attribute to "[EXTRA_]LAZY" in the relation declaration like
/**
* #ORM\OneToMany(targetEntity="Vehicule", mappedBy="vip", fetch="LAZY", …)
*/
protected $vehicules;
if YES you will have to select those entities in your query.
Using DQL:
SELECT u, v, w FROM AppBundle:User u LEFT JOIN u.vip v LEFT JOIN v.vehicules w
Using queryBuilder:
$repository = $em->getRepository(User::class);
$users = $repository->createQueryBuilder('u')
->addSelect('v')
->join('u.vip', 'v')
->addSelect('w')
->join('v.vehicules', 'w')
// …
I have "Person" entity that has a property "Status" and this property is an OneToMany relationship in Doctrine.
/**
*
* #ORM\OneToMany(targetEntity="\My\Bundle\Entity\Status", mappedBy="person")
*
**/
protected $status;
What I need to do is to display in my view is just the last status.
How can I get just the last status in my twig view? Is there something like, for exmample, {{ person.status.last }} ?
Or should I query the last status in my controller and pass it to view as another var?
Yes, you can do it exactly like this {{ person.status.last.someField }} to echo a someField property of last status (in natural query order) for person object.
This is possible because person.status is a Doctrine Collection which has methods like first or last. You can check this for more information.
Assuming I have an entity User and an entity Book and they're both joined by User.bookId = Book.id (this marks a user owns a certain book, relation type oneUserToManyBook).
If I now want to execute a performance friendly fetch with Doctrine's DQL or QueryBuilder for all Books a User has read, what is the best way to implement this in a Symfony2/Doctrine2 webapp, so that I can use them in my User loop in a Twig template?
Twig
{% for user in users %}
{{ user.name|e }}
{% for address in user.getAddressesByUserId(user.getId()) %}
{{ address.city }}
{% endfor %}
{% endfor %}
I see two approaches, but both don't lead to my target:
1st approach
Create a custom repository class BookRepository:
public function getBooksOwnedByUser($user_id) {
return $em->createQuery('SELECT b.title
FROM MyBundle\Entity\User u,
MyBundle\Entity\Book b
WHERE u.book_id = b.id'
AND u.id = :user_id)
->setParameter('user_id', $user_id)
->getResult();
}
Problem: Works fine, but I cant call getBooksOwnedByUser() in my Twig template (because it's not tied to the entity User, but to it's repository, which is a subclass of Doctrine\ORM\EntityRepository.
2nd approach
Execute the same query as above - not in my UserRepository, but directly in my User entity class.
Problem here: I could call this method in my Twig template, but I cannot (and should not) use the EntityManager in my User entity class.
It's best if you make a relationship from User to Books. Assuming you have made this relationship you can make your query like this:
public function getBooksOwnedByUser($user_id) {
return $em->createQuery('SELECT u, b
FROM MyBundle\Entity\User u
JOIN u.books b
WHERE u.id = :user_id')
->setParameter('user_id', $user_id)
->getResult();
}
Then in your controller:
$em = $this->getDoctrine()->getManager();
$user_with_books = $em->getRepository('MyBundle\Entity\User')
->getBooksOwnedByUser($user->getId());
return $this->render('YourTemplate.html.twig', array(
'user_with_books' => $user_with_books,
));
In twig:
{% for book in user.books %}
{{ book.title }}
{% endfor %}
Some considerations:
For multiple users you will have to change the query (lazy loading is possible but not advised).
If it's a lot of data you can get a performance boost by getting a scalar result (Array)
If you need different queries for the user that can not be combined you will have to store different variables (objects or arrays). That's why I named it "user_with_books". But if you only have this user in your template you can just as well call it "user".
user.getAddressesByUserId(user.getId()) <-- passing data from one model to query is the responsiblity of the controller (or a service). Best practice is to avoid doing this in your template.
So the answer:
You can not do anything with a custom repository method because it's a function. A function on itself doesn't represent any data. So this is a way you can retrieve the actual data with that function and display that.