I was reading about lazy associations in Doctrine 2 and how I could avoid the following situation:
In this paragraph in the documentation is explained how to enable lazy associations for your entity. I am missing how I could use this within my entity repository.
So far I tried some adjustments to the entity repository but without any success. I also tried this post, this post and this post but they seem to handle ManyToMany or a complete other situation.
Could somebody explain how and where to use lazy association to avoid the above example?
Irrelevant private properties and getters/setters have been removed from this code snippets due to the length.
src/AppBundle/Entity/News.php
class News
{
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Account", fetch="EXTRA_LAZY")
* #ORM\JoinColumn(name="author", referencedColumnName="id")
*/
private $author;
}
src/AppBundle/Entity/Repositories/NewsRepository.php
class NewsRepository extends EntityRepository
{
/**
* #param $id
* #return mixed
* #throws \Doctrine\ORM\NonUniqueResultException
*/
public function findOneById($id) {
return $this->createQueryBuilder('a')
->andWhere('a.id = :id')
->setParameter('id', $id)
->getQuery()
->getOneOrNullResult();
}
}
src/AppBundle/Controller/NewsController.php
/**
* #Route("/{id}", name="news_item")
* #Method({"GET"})
* #Template("AppBundle:news:item.html.twig")
*/
public function articleAction(Request $request, $id)
{
$news_item = $this->getDoctrine()->getRepository('AppBundle:News')->findOneById($id);
if (!$news_item) {
throw $this->createNotFoundException('No news item found by id ' . $id);
}
return array(
'news_item' => $news_item
);
}
src/AppBundle/Resources/views/news/item.html.twig
{% extends 'base.html.twig' %}
{% block body %}
{{ dump(news_item) }} }}
{% endblock %}
You don't have to do anything special to enable lazy loading. Extra lazy loading in the relationship you show isn't necessary for News to not load an Author. It just means you can make calls like ->contains on a collection without loading the entire collection & a few other conveniences.
dump() should show something on Author like:
protected 'author' =>
object(Proxies\__CG__\YourNamespace\YourBundle\Entity\Author)
This doesn't mean the entity has been fetched from the db. To quote the docs.
Instead of passing you back a real Author instance and a collection of
comments Doctrine will create proxy instances for you. Only if you
access these proxies for the first time they will go through the
EntityManager and load their state from the database.
If you don't get back a proxy class, it's probably because you've already accessed that relationship earlier in your code. Or you've explicitly fetched that entity in your query.
Related
I'm new to OOP and MVC with PHP, and I'm currently learning by making my own custom framework from scratch, for testing purposes. I have set up my controllers, models and views and everything works fine.
My app has the following architecture :
It’s a small blog that follows the rules of the MVC pattern. To summarize, it works like this :
The called Controller will fetch the data using the right models
Models return objects of the class \Classes\{MyObject}
Controller call the right template to render the view, and passes it the data and objects to display
The problem
In some views, I need to display related data. For example, in the article view, I need to display the author's first name. In the database, an article contains only the author’s ID, not his first name : this is the same thing in my class \Classes\Article.
What I've tried
To display the author’s first name in my view, I've updated the model Find method to use a LEFT JOIN in the SQL query. Then, I've updated my \Classes\Article class to have a user_firstname property :
class Article
{
private $pk_id;
private $title;
private $excerpt;
private $content;
private $created_at;
private $fk_user_id;
private $updated_at;
private $user_firstname; // <-- I've added this property to retrieve author's firstname
// (...)
}
What I did works well, but my teacher tells me it’s not the right way to do it because the author’s firstname is not part of the definition of an article.
In this case, my teacher tells me to use a DTO (Data Transfert Object) between Article and User classes.
Questions
What is the right way to set up a DTO in this case?
Do I need to create a new ArticleUserDTO class in a new namespace ?
How to use it ?
I think I understood the problem : the Article class should only contain what defines an article. But I can’t understand the logic of setting up a DTO. I’ve done some research on it, I understand the usefulness of the DTO but I can’t set up into my app.
Setting up a DTO in my app was easy ! As mentioned by #daremachine, the diagram below helped me to understand what a DTO is for.
Diagram source : martinfowler.com
We can see DTOs as an object assembler, in which we place all the elements we need on the view side.
For example, in the post view of an article, I needed to display other items, such as the author and posted comments. So I have created a Post class that groups all these items.
Setting up a DTO
In my \Classes\ namespace, I've created a new Post class. First, we define the properties we will need. Then we add the getters and setters for each of them. Finally, we set up the constructor, which will call each of the classes we need in the view.
namespace Classes;
use DateTime;
class Post
{
private int $pk_id;
private string $title;
private string $excerpt;
private string $content;
private DateTime $created_at;
private DateTime $updated_at;
private int $author_id;
private string $author_firstname;
private array $comments;
public function __construct(Article $article, User $author, array $comments)
{
$this->setPkId($article->getId());
$this->setTitle($article->getTitle());
$this->setExcerpt($article->getExcerpt());
$this->setContent($article->getContent());
$this->setCreatedAt($article->getCreatedAt());
$this->setUpdatedAt($article->getUpdatedAt());
$this->setAuthorId($article->getAuthorId());
$this->setAuthorFirstname($author->getFirstname());
$this->setComments($comments);
}
/**
* #param int $pk_id
*/
public function setPkId(int $pk_id): void
{
$this->pk_id = $pk_id;
}
/**
* #return int
*/
public function getPkId(): int
{
return $this->pk_id;
}
// (etc)
}
We now need to update the ArticleController, which should no longer pass the Article, Comment and User objects, but only the new Post object.
namespace Controllers;
class ArticleController extends Controller
{
// (...)
/**
* Get an article and display it
*
* #return void
*/
public function show(): void
{
// (...)
// Find Article :
$article = $this->articleModel->find($article_id);
if (!$article) {
Http::error404();
}
// Find Comments :
$commentaires = $this->commentModel->findAllByArticle($article_id);
// Find User (author)
$user = $this->userModel->find($article->getAuthorId());
// Data Transfert Object instance :
$post = new Post($article, $user, $commentaires);
$pageTitle = $post->getTitle();
// Pass DTO to view :
Renderer::render('articles/show', compact('pageTitle', 'post'));
}
}
We just need to update our view to use the new Post object and it's done ! Thanks to #daremachine for his help :)
Suppose I have two entities Page and Block. It's bi-directional mapping. Each page can have more than one block. Each block could belong to single page.
/**
* #ORM\Entity
* #ORM\Table(name="page")
*/
class Page
{
...
/**
* #ORM\OneToMany(targetEntity="Block", mappedBy="page", cascade={"all"}, orphanRemoval=true)
**/
private $blocks;
...
}
/**
* #ORM\Entity
* #ORM\Table(name="block")
*/
class Block
{
...
/**
* #ORM\ManyToOne(targetEntity="Page", inversedBy="blocks")
* #ORM\JoinColumn(name="page_id", referencedColumnName="id")
*
**/
private $page;
...
}
From time to time blocks that belong to page will change. Some blocks will be added, some of them will be removed. The next case doesn't work for me:
$page->setBlocks([1, 2, 3]);
$em->merge($page)
$em->flush() //Page will have blocks 1, 2, 3
$page->setBlocks([1, 4])
$em->merge($page)
$em->flush() //Page will have blocks 1, 2, 3, 4
Expected result after second flush() call is: //Page will have 1, 4
So I need to overwrite completely collection of blocks with merge method.
Constraints:
I can't implement deleteBlock in Page class
I can only call merge() and flush() on $em
Is it possible to implement desired result via annotations or some other trick?
Addressing the Constraints
It cannot be done with your constraints.
Suggested Solution
I'd create a custom function in the BlockRepository Class that deletes all blocks of the given Page, then call that function before calling the setBlock function.
Add this to your Block Repository class:
public function deleteBlocksByPageId($page_id)
{
$qb = $this->createQueryBuilder()
->delete('Block', 'b');
->where('b.page', ':page'));
->setParameter(':page', $page_id);
$numDeleted = $qb->execute();
return $numDeleted;
}
In your Controller:
//Delete all the existing blocks before adding them back in.
$repo = $this->getDoctrine()->getRepository('BundleName:BlockRepository');
$repo->deleteBlocksByPageId($page->getId());
$page->setBlocks([1, 4])
$em->merge($page)
$em->flush()
Conclusion
This is a common pattern for managing items that change often. It required no loops.
If the constraints are self-imposed then you need to loosen them and find another solution, such as the one I offer above. If they are constraints imposed by some higher-up then it's time to have a conversation with him/her about it being counter productive in this instance.
I've got this model;
Itinerary, Venue, ItineraryVenue.
I needed many to many relation between itineraries and venues but also I wanted to store some specific data about the relation (say notes, own photo, etc.), so I decided to introduce a new entity named ItineraryVenue.
So Itinerary has collection of ItineraryVenues which in turn, refer to Venues.
My problem is that I can't remove ItineraryVenue from a Itinerary object.
$itinerary->itineraryVenues->removeElement($itineraryVenue);
$em->flush();
removes element from the php collection, but doesn't remove this $itineraryVenue from database.
I've managed to force Doctrine2 to remove $itineraryVenue, but only when I annotate the Itinerary::$itineraryVenues with orphanRemoval=true.
Since orphan removal treats Venue as a private property it also removes Venue entity, I don't want that.
Is there an relation configuration option or is removing "by hand" the olny way to make it work as I want?
Hard to believe it, it's a common relation pattern.
Entities definitions:
class Itinerary
{
/**
* #ORM\OneToMany(targetEntity="ItineraryVenue", mappedBy="itinerary", cascade={"persist", "remove"})
*/
private $itineraryVenues;
function __construct()
{
$this->itineraryVenues = new ArrayCollection();
}
}
class ItineraryVenue
{
/**
* #ORM\ManyToOne(targetEntity="Itinerary", inversedBy="itineraryVenues")
*/
private $itinerary;
/**
* #ORM\ManyToOne(targetEntity="Venue")
*/
private $venue;
function __construct()
{
}
}
class Venue
{
}
You are doing things right: orphanRemoval - is what you need. So, you should override default Itinerary::removeItineraryVenue like
public function removeItineraryVenue(\AppBundle\Entity\ItineraryVenue $itineraryVenue)
{
$itineraryVenue->setItinerary(null);
$this->itineraryVenues->removeElement($itineraryVenue);
}
The full working example is here https://github.com/kaduev13/removing-onetomany-elements-doctrine2.
I got confuse in doctrine one to many relationship.
Question 1:
Correct me if I am wrong. I assume that when I try to
$em = $this->getDoctrine()->getManager();
$product_repo = $em->getRepository('MyBundle:Product');
$products = $product_repo->findAll();
dump($products);
I will see the related features attached to the $features variable, so when I use $products->getFeatures() I will have Feature object in array form. But from the dump debug I didn't not see anything attached to it instead I got this:
On the other end I also do this
$em = $this->getDoctrine()->getManager();
$feature_repo = $em->getRepository('MyBundle:Features');
$features = $product_repo->findAll();
dump($features);
This time I can see the Product object is attached to the $product variable.
My question is, is there any problem why I can't get data from the variable $features? Or doctrine wouldn't load the related data by default.
Question 2:
If we were assumed that the data is able to load into the feature $variable, is it possible that I can filter the data (eg. where feature.name = 'fly') instead of load all of the related feature.
==========================================================================
My Demo Entity
<?php
use Doctrine\Common\Collections\ArrayCollection;
/** #Entity **/
class Product
{
// ...
/**
* #OneToMany(targetEntity="Feature", mappedBy="product")
**/
private $features;
// ...
public function __construct() {
$this->features = new ArrayCollection();
}
}
/** #Entity **/
class Feature
{
// ...
/**
* #ManyToOne(targetEntity="Product", inversedBy="features")
* #JoinColumn(name="product_id", referencedColumnName="id")
**/
private $product;
// ...
}
Product table (in database): id, description, name
Feature table (in database): id, description, name, table_id
Assuming that your dump function is symfony/var-dumper and not a custom function
Question 1
Yes, nested collection are not displayed by default by dump function, its about performance. This is not a Doctrine related issue. Your data are loaded here.
You can play around with advanced use of var-dumper, like casters ( http://symfony.com/doc/current/components/var_dumper/advanced )
Question 2
You have differents ways to solve your question :
In Controller : Create your custom method in Product
Criteria better solution
Product::getFeaturesByName($name='fly'){
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('name', $name));
return $this->features->matching($criteria);
}
Filter
Product::getFeaturesByName($name='fly'){
return $this -> features ->filter(
function($entry) use ($name) {
return $entry->getName() == $name;
}
);
}
);
}
In Twig Template : filter in loop
{% for product in products %} {# start browse products #}
{% for feature in product.features if feature.name='ok' %}{# start browse features #}
{% endfor %}{# end browse features #}
{% endfor %}{# end browse products #}
Hope this will help you
regards
I am really new to Symfony so I apologize in advanced if this sounds stupid and I will really appreciate if anyone to correct my understanding.
I am reading about Databases and Doctrine and while reading I thought why not create a dummy blog app to practice.
The dummy blog app i am working on is very simple just three tables and its Entity
post (where the blog posts go) its Entity is Entity/Post.php,
comments (where to post comments go) its Entity is Entity/Comments.php
category (where the post categories go) its Entity is Entity/Category.php.
I am able to get the post/category/comments to save, show, update, delete all that is working fine.
What i am working on now is when the blog is displayed, its category appears as a number (category id), so i am trying to link the post table with category table to display the category name rather than number.
Question 1, Since the post is also linked with the comments table and i need to link the same post table with category table can we do this inside the Entity/Post.php?
class Post
{
/**
* #ORM\OneToMany(targetEntity="Comments", mappedBy="post")
*/
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="post")
* #ORM\JoinColumn(name="category", referencedColumnName="id")
*/
protected $comment;
protected $categories;
If not then what is the correct way to handle these relationships?
Question 2, While reading "Fetching Related Objects", it seems that I should be able to get the category name my doing the following
$posts = $this->getDoctrine()->getRepository('BlogBundle:Post')->findBy(array('category' => $id), array('id' => 'DESC'));
$category_name = $posts->getCategory();
but this gives me an error
Error: Call to a member function getCategory() on a non-object in
I can confirm that this getCategory() method does exist in Post entity
I will really appreciate any assistance here.
Question 1
The annotations are fine, but you have to write them right on top of the property they belong to, otherwise they are ignored:
class Post
{
/**
* #ORM\OneToMany(targetEntity="Comment", mappedBy="post")
*/
protected $comments;
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="posts")
* #ORM\JoinColumn(name="category", referencedColumnName="id")
*/
protected $category;
public function __constructor()
{
$this->comments = new ArrayCollection();
}
// ...
}
Make shure you have the correct counterpart set in the other entities:
class Category
{
/**
* #ORM\OneToMany(targetEntity="Post", mappedBy="category")
*/
protected $posts;
public function __constructor()
{
$this->posts = new ArrayCollection();
}
// ...
}
class Comment
{
/**
* #ORM\ManyToOne(targetEntity="Post", inversedBy="comments")
* #ORM\JoinColumn(name="post", referencedColumnName="id")
*/
protected $post;
// ...
}
Note that I changed some singular / plural of properties and Comments class name. Should be more intuitive like that.
Question 2
findBy returns an array of found objects, even if only one object was found. Use either findOneBy or foreach through your result.