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
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 :)
In order to solve a problem I asked about earlier, I am trying to create a custom repository function that will determine whether an instance of Repair is unique, based on the device, name, and colors constraints.
Here's my Doctrine Annotation for class Repair. Mind that the device property is Many To One (many Repairs for one Device), and that colors is Many to Many.
/**
* #ORM\Table(name="repair")
* #ORM\Entity(repositoryClass="AppBundle\Repository\RepairRepository")
* #UniqueEntity(fields={"name", "device", "colors"}, repositoryMethod="getSimilarRepairs", message="Repair {{ value }} already exists for this name, device and colour combination.")
*/
This is my RepairRepository.php, in which $criteria['colors'] is an array.
public function getSimilarRepairs(array $criteria) {
$builder = $this->createQueryBuilder('r')
->where('r.device = :device')
->andWhere('r.colors = :colors')
->andWhere('r.name = :name')
->setParameters(['deviceid'=>$criteria['device'],'colors'=>$criteria['colors'],'name'=>$criteria['name']]);
return $builder->getQuery();
}
I have three problems that can probably be brought back to one:
editing: with every change, causing a duplicate or not, I get the message that a duplicate entity exists.
editing: despite the error message, name changes are performed anyway!
adding: I can create as many duplicates as I like, there never is an error message.
Your problem is that the colors relation is a ManyToMany.
In SQL you can not query '=' on this relation.
It is very complicated, that's why Doctrine (and we probably) can't make it alone .
A partial solution to build a query :
public function getSimilarRepairs(array $criteria) {
$builder = $this->createQueryBuilder('r')
->where('r.device = :device')
->andWhere('r.name = :name')->setParameter('name',$criteria['name'])
->andWhere('r.colors = :colors')->setParameter('deviceid',$criteria['device']);
// r matches only if each of your colors exists are related to r :
$i=0;
foreach($criteria['colors'] as $color){
$i++;
$builder->join('r.colors','c'.$i)->andWhere('c = :color'.$i)->setParameter('color'.$i,$color);
}
// Then you had also to check than there is no other color related to r :
// I don't know how
return $builder->getQuery();
}
But let me propose another solution :
In your repair entity, your can store a duplicate of your related colours :
/**
* #var string
*
* #ORM\Column(name="name_canonical", type="string")
*/
private $serializedColors;
set it with doctrine lifecycle events :
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function updateColors()
{
$serializedColors = '';
foreach($this->colors as $color){
$serializedColors .= $color->getId().'#';
}
$this->serializedColors = $serializedColors;
}
Don't forget to add #HasLifecycleCallbacks
Then change your UniqueEntityConstraint to fields={"name", "device", "serializedColors"}, forget the custom query, and it will work.
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.
I have a Entity called Event which has
a field "associatedEntity" containing the class name of another Entity in the Bundle
a field "targetId" of that specific "associatedEntity" Entity
I would now like to access this target entity inside my Event-Entity somehow but im now sure how to do it. I'd like to access the different target Entities in a twig template using something like
{% if event.getClassName() == "User" %}
{{ if event.getUser().getName() }}
{% endif %}
Edit: Just to be clear, the only thing im interested so far is how to create the relation properly. Outside a ORM World you would probably use a join statement for this. It is like i have many target Entities mapped by one field.
So far im using the entity repository and DI to load the associated Entities, but i find that ugly knowing there is a JOIN Statement which i could use:
public function getUpcomingEvents(){
$query = $this->createQueryBuilder('E')
->where('E.resolved = false')
->orderBy('E.notify_date', 'ASC')
->setMaxResults( $limit );
$res = $query->getQuery()->getResult();
$res = $this->attachAssociatedObjects($res);
return $res;
}
public function attachAssociatedObjects($res){
foreach ($res as $key => $entity) {
$assocObject = $this->getEntityManager()->getReference('My\Bundle\Entity\\'.$entity->getClassName(), $entity->getTargetId());
$res[$key]->setAssociatedObject($assocObject);
}
return $res;
}
Twig attribute function is what you need.
I have a serious doubt about doing a combo box with nested records from an entity in Symfony2. I have read about nested tree extension for Doctrine 2 in http://gediminasm.org/article/tree-nestedset-behavior-extension-for-doctrine-2, it appears to be interesting but it does not refer how to implementing this nested tree into an entity field in a form.
Also, I have read more about recursive functions in PHP, and I have found an interesting blog where it is analyzed, here is the link http://www.sitepoint.com/hierarchical-data-database/, it explains specifically about this recursive function:
function display_children($parent, $level) {
// Retrieve all children of $parent
$result = mysql_query('SELECT title FROM tree WHERE parent="'.$parent.'"');
// Display each child
while ($row = mysql_fetch_array($result)) {
// Indent and display the title of this child
echo str_repeat(' ',$level).$row['title']."\n";
// Call this function again to display this child's children
display_children($row['title'], $level+1);
}
}
Somebody knows how to translate this code into Symfony2 and where it would be stored (Controller, Entity, etc.). If someone has other ideas about working nested records with Twig Extensions, It would be appreciate too.
Thanks a lot your help.
This is how we implemented Nested tree for Categories (indented dropdown) for use in Product edit form:
Define your Category entity class as shown in the documentation
Add a method to the Category entity class that shows the name indented by nesting level
/**
* #ORM\Table()
* #ORM\Entity(repositoryClass="CP\YourBundle\Entity\CategoryRepository")
* #Gedmo\Tree(type="nested")
*/
class Category
{
public function getOptionLabel()
{
return str_repeat(
html_entity_decode(' ', ENT_QUOTES, 'UTF-8'),
($this->getLevel() + 1) * 3
) . $this->getName();
}
Define Product entity relations with the Category entities using Doctrine2 annotations (in our case we have multiple categories support for one product)
class Product
{
/**
* #var ArrayCollection
* #ORM\ManyToMany(targetEntity="Category", cascade={"persist", "remove"})
*/
private $categories;
...
Now all you have to do is add the following to the ProductType form class
class ProductType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('categories', null, array('property' => 'optionLabel'));
}
Now the form should show the dropdown with a correctly indented Category list
You can have a look to this tree implementation that is not based on nested sets but on materialzed paths: https://github.com/KnpLabs/materialized-path .
You could imagine use its API to get a flat resultset of the tree, like in your code snippet:
$root = $repo->find($id);
$repo->buildTree($root);
$flatArray = $root->toFlatArray(function(NodeInterface $node) {
$pre = $node->getLevel() > 1 ? implode('', array_fill(0, $node->getLevel(), '--')) : '';
return $pre.(string)$node;
});
return $this->get('templating')->render('::tree.html.twig', $flatArray);