I would like to count the number of tags given a specific article. I've got two entities (Article, Tag) which are related by a many-to-many association:
//Bundle/Entity/Article.php
/**
* #ORM\ManyToMany(targetEntity="Tag")
*/
private $tags;
Now I've got n articles with m tags and I would like to know how often a specific tag has been used.
I'm relatively new to both Symfony2 and Doctrine. The problem is that I don't know where to fit such a query (I guess it should reside in the ArticleRepository but on the other hand it would make sense to have it in the TagRepository) and how to JOIN the correct tables (in this case Article, article_tag, Tag).
The simplest way that I can think of is to just set up a bidirectional relationship between Article and Tag:
class Article
{
/**
* #ORM\ManyToMany(targetEntity="Tag", inversedBy="articles")
*/
private $tags;
}
class Tag
{
/**
* #ORM\ManyToMany(targetEntity="Article", mappedBy="tags")
*/
private $articles;
}
Then you can (assuming you've set up standard getters and setters) use $tag->getArticles()->count();, where $tag is a managed Tag entity, to get the number of articles attached to that tag. This works because when populating ToMany relationship properties, Doctrine uses an instance of Doctrine\Common\Collections\ArrayCollection. Check out the source here.
Also, if you go this route, make sure you read the documentation on picking an owning and inverse side here.
You can count the number of tags by using a specific article..lets say article id=5:
$query=$em->createQuery("SELECT count(t.id) FROM Tag t WHERE ?1 MEMBER OF t.articles");
$query->setParameter(1,5 );
$result = $query->getSingleScalarResult();
This gives me the number of tags in Article Entity only for article id = 5.
Related
I have an entity called event, the event can have many rooms and a room can have many participants.
If I access all events (with a specific user) I can filter events where the user has no access right (no room with a connection to the specific user) by using extensions.
That works fine.
The response contains all events which have at least one room with access rights.
But If the event has multiple rooms and the user has only access to one room. The response includes both rooms. I created a RoomExtension, but this class will not be invoked.
Thanks
Your problem is caused by the fact that filters and extensions only work on the query that retrieves the primary entities. The related entities are retrieved using Doctrines associations wich are part of the domain model that is meant to be the single source of truth for all purposes. What you need is a user-specic view on that model, which in the context of api's usually consists of DTOs.
I think there are basically two solutions:
Query primarily for Events and convert the into EventDTOs, then either query for - or filter out - the related Rooms,
Query primarily for Rooms, then group them into EventDTOs.
I explain the second solution here because i guesss that it is simpeler and it shoud make your RoomExtension work out of the box, which makes it the better fit to your question, but also because i happen to have built and tested something similar in a tutorial so it is a lot less work to write an answer with confidence.
The downside of this solution is that it does not support pagination.
Bucause this solution primarily queries for Rooms, the the operation is on the Room resource. If it where the only collectionOperation of Room it could be like this:
(..)
* #ApiResource(
* collectionOperations={
* "get_accessible_events"={
* "method"="GET",
* "path"="/rooms/accessible-events",
* "output"=EventDTO::class,
* "pagination_enabled"=false
* }
* }
* }
*/
class Room {
(..)
(This does not have to be your only collectionOperation, you can still have "get", "post" and others).
Right now this still produces a flat collection of Rooms, you need to group them into EventDTOs. The DTOs page of the docs suggest to make a DataTransformer to produce the DTOs, but that only works if your DTOs are one to one with the entities retrieved by the query. But a CollectionDataProvider can do the trick. Because you do not need to adapt the query itself you can simply decorate the default CollectionDataProvider service:
namespace App\DataProvider;
use ApiPlatform\Core\Api\OperationType;
use App\DTO\EventDTO;
use ApiPlatform\Core\DataProvider\ContextAwareCollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use App\Entity\Room;
class RoomAccessibleEventCollectionDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface
{
/** #var CollectionDataProviderInterface */
private $dataProvider;
/**
* #param CollectionDataProviderInterface $dataProvider The built-in orm CollectionDataProvider of API Platform
*/
public function __construct(CollectionDataProviderInterface $dataProvider)
{
$this->dataProvider = $dataProvider;
}
/**
* {#inheritdoc}
*/
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return Room::class === $resourceClass
&& $operationName == 'get_accessible_events';
}
/**
* {#inheritdoc}
*/
public function getCollection(string $resourceClass, string $operationName = null, array $context = []): array
{
$rooms = $this->dataProvider->getCollection($resourceClass, $operationName, $context);
$dtos = [];
foreach ($rooms as $room) {
$key = $room->getId();
if (isset($dtos[$key])) {
$dtos[$key]->addRoom($room);
} else {
$dto = new EventDTO($room->getEvent());
$dto->addRoom($room);
$dtos[$key] = $dto;
}
}
return $dtos;
}
}
You do need to configure the service in config/services.yaml:
'App\DataProvider\RoomAccessibleEventCollectionDataProvider':
arguments:
$dataProvider: '#api_platform.doctrine.orm.default.collection_data_provider'
This does not replace the default CollectionDataProvider but adds another one that gets the default one injected.
I guess you can make the EventDTO class yourself now. Then it should work. Filters defined on Room will also work as usual, for example if rooms can be filtered by the date of their event ?event.date[gte]=2020-10-10 will only find rooms with events on or after 2020-10-10 and return their EventDTO's.
However, in the swagger docs the get_accessible_events operations summary and descriptions still come from Room. You can look up how to add a SwaggerDecorator in the docs or take a look at the chapter9-api branch of the tutorial. The latter also contains complete explanations and tested code for entities, the DTO (Report Model) and an extension for only showing data the user is authorized for, but is not taylored to your questions and would all together be way beyond what a to the point answer.
I can not give you any more hints on this site with respect the other solution because this site will probably see them as an incomplete or unclear answer and punish me for it.
I have been trying to get my head around these polymorphic relationships all day. I might be over complicating/thinking it but. Can Laravel handle inverse polymorphic relationships? I have a registration flow that can have two types of field Models- normal field and customField.
When I loop through all the fields available it could pull the attributes from either NormalField or CustomField.
<?php
foreach($registrationFlow->fields->get() as $field)
{
echo $field->name; // could be custom field or could be normal field
}
?>
My difficulty is that, the example given in the docs works if you want to assign a photo to either staff or orders, but i want to assign either a customField or a normalField to a registrationFlow
*Edit
If you follow the example for the polymorphic many to many relationship, The tag class contains posts and videos- while i would want just a simple fields() method that relates to customField or normalField dependent on the type
First of all, you should take a look at the updated docs for Laravel 5.1: https://laravel.com/docs/5.1/eloquent-relationships#polymorphic-relations.
I think the difficulty with the example they provide is that the relationship between Photo and Staff/Product are "has-a" relationships, whereas you are trying to model an "is-a" relationship. However, you can model "is-a" essentially the same way. Take a look at this article: http://richardbagshaw.co.uk/laravel-user-types-and-polymorphic-relationships/.
Basically, the strategy is to define a generic model (and a generic table), perhaps in your case Field, that relates to your RegistrationFlow. You then have two subtype models, NormalField and CustomField, that have one-to-one relationships with Field. (there's your "is-a"). Thus, RegistrationFlow is indirectly related to your field subtypes.
Polymorphism comes in when you want to access the specific subtypes:
class Field extends Model {
public function fieldable()
{
return $this->morphTo();
}
}
Your base field table should have fieldable_id and fieldable_type columns defined (see the Eloquent docs).
You can then add methods to NormalField and CustomField that let you access the base model (your "inverse relationship"):
class NormalField {
public function field()
{
return $this->morphOne('Field', 'fieldable');
}
}
class CustomField {
public function field()
{
return $this->morphOne('Field', 'fieldable');
}
}
Usage:
$field = Field::find(1);
// Gets the specific subtype
$fieldable = $field->fieldable;
I'm tinkering with Neo4j 2.3.0 and Laravel 5.1 using NeoEloquent. I've set up a couple of dummy nodes and some relationships between them:
image of Neo4j model - apologies, I cannot insert images directly yet :)
So articles can use a template. The inverse of this relationship is that a template is used by an article.
I've set up the classes like so:
Class Template extends Model
{
public function articles()
{
return $this->hasMany('App\Article', 'USED_BY');
}
}
And:
Class Article extends Model
{
public function template()
{
return $this->belongsTo('App\Template', 'USES');
}
}
So far, so good, I think.
I have a page where I am wanting to eventually list all of the articles in the system, along with some useful metadata, like the template each ones uses. For this, I have set something up in the controller:
$articles = array();
foreach (Article::with('template')->get() as $article) {
array_push($articles, $article);
}
return $articles;
Not the most elegant, but it should return the data for both the article and it's associated template. However:
[{"content":"Some test content","title":"Test Article","id":28,"template":null},{"content":"Some flibble content","title":"Flibble","id":31,"template":null}]
So the question is - why is this returning null?
More interestingly, if I set up the relationship to the same thing in BOTH directions, it returns the values. i.e. if I change the USED_BY to USES, then the data is returned, but this doesn't make sense from an architectural point of view - a template does not 'use' an article.
So what am I missing?
More interestingly, if I set up the relationship to the same thing in BOTH directions, it returns the values.
That's correct, because this is how it operates. It is worth knowing that the relationship methods you have defined represent the relationship itself, which means for both models Template and Article to target the USED_BY relationship from any side it has to be the same in articles() and template.
The solution would be to use something like USES (or any notion you like) on both sides. This reference should help you make good decisions regarding your relationships.
On the other hand, if you still wish to have different relations on the sides then kindly note that in your model (image) both relationships are in outgoing direction. i.e. Fibble-[:USES]->Template and Template-[:USED_BY]->Fibble which means template() should be an outgoing relationship such as hasOne instead of belongsTo which is incoming.
I've been trying to figure out how to get this to work for along time but without any luck. Due to a complex logic in an app I'm working on, I need to create an isolated clone of a entity collection without preserving what so ever relation to the database. Whatever changes I do on the cloned collection should not be tracked by Doctrine at all and should be treated as if it doesn't exist at all.
Here's an example code:
/*
* #ORM\Entity()
*/
class Person
{
/**
* #var integer
*
* #ORM\Id
* #ORM\Column(name="person_id", type="integer",nullable=false)
* #ORM\GeneratedValue(strategy="AUTO")
*/
public $id;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Car", mappedBy="person", cascade={"persist"})
*/
public $cars;
}
/**
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks()
*/
class Car
{
/**
* #var integer
*
* #ORM\Id
* #ORM\Column(name="car_id", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/*
* #ORM\JoinColumn(name="person_id", referencedColumnName="person_id", nullable=true)
* #ORM\ManyToOne(targetEntity="Person", inversedBy="cars", cascade={"persist"})
*/
private $person;
}
I've already tried the following code in my controller to store the collection into the session but it still somehow stores the relationships:
$tmp = clone $person;
$this->get('session')->set('carCollection', $tmp->getCars());
$tmpCars = clone $person->getCars();
$tmpCollection = new ArrayCollection();
foreach($tmpCars as $car) {
$tmpCollection->add(clone $car);
}
$this->get('session')->set('carCollection', $tmpCollection);
$tmpCars = clone $person->getCars();
$tmpCollection = new ArrayCollection();
foreach($tmpCars as $car) {
$clone = clone $car;
$entityManager->detach($car);
$tmpCollection->add(clone $clone);
}
$this->get('session')->set('carCollection', $tmpCollection);
Apparently I'm doing something wrong here because I end up having more results in the Car collection when flushing the entity even though the collection itself has the correct number of records. I have a suspicion that somewhere in the chain Doctrine doesn't compute correctly what needs to be done.
Any ideas or directions on how to solve or debug this?
Follow-up question: When retrieving back the cloned collection from the session will it still be an isolated clone or Doctrine will try merge it back?
I'm writing this answer to give directions to anybody who might have similar issues. I couldn't find many topics or documentation in this manner which is why I decided to share my experience. I am no deep expert on Doctrine an how it internally works, so I won't go into big details of "how it works". I will rather focus on the end result.
Storing entities which have relations to other entities into a session is quite problematic. When you retrieve it from the session, Doctrine loses track of the relationships (OneToMany, ManyToOne, etc). This leads to some undesired effects:
Doctrine wrongly decides to insert a new record of an existing entity.
Doctrine might throw exceptions such as A new entity was found through the relationship 'Acme\MyBundle\Entity\Person#cars' that was not configured to cascade persist operations for entity: Opel. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). and at least 2 other types of exceptions which might seem totally irrelevant at first.
Apparently when fetching a result from the database and it "as-is" in your session things get really messy, specially if the entity has relations to other entities (which was my case). Pay big attention if you have entity relationships - they might need to be "refreshed" if you start getting strange exceptions.
There are a couple of ways to overcome this issue. One of which is to use the data sent via the form (as #flec suggested) by using $myForm->getData(). This approach might work well for you, but unfortunately it was not the case with me (too complex to explain).
What I ended up doing was implementing the \Serializable in the entity. I also created a method called __toArray() which converted my entity into an array. What data you return in the __toArray() method is totally up to you and your business logic. The array data is stored into the session and you use it to re-create a fresh object with all necessary relations.
Hope this helps somebody.
I think hydrators/extractors would be the way to go for you.
They can extract the data from an entity and you can pass them to a newly created instance of that entity via the hydrator.
The only thing you'll need to do in between is the unsetting of the relation properties.
They should be fetchable via a metadata class via doctrine somehow.
I have an Entity that has bidirectional ManyToOne/OneToMany relationship with another entity:
class BookShelf {
/**
* #OneToMany(targetEntity="Book", mappedBy="shelf", cascade={"persist"})
*/
public $books;
}
class Book {
/**
* #ManyToOne(targetEntity="BookShelf", inversedBy="books", cascade={"persist"})
*/
public $shelf;
}
I'm trying to create a new book, and have that object be listed in the bookshelf.
$book = new Book();
$book->shelf = $shelf;
$em->persist($book); $em->flush();
$shelf->showBooks();
After that, the $shelf->books does not contain the book, but instead it contains NULL. However the book is correctly inserted into the database, and when I run $shelf->showBooks() on another pageload, the book is listed properly.
I tried adding $em->refresh($book) and $em->refresh($shelf) but it doesn't help, the association still isn't refreshed.
Doctrine manual does suggest that I could use $shelf->books->add($book) to manually synchronize the association, but since there are initially no books, $shelf->books is NULL and I cannot call any methods on it.
How can I make Doctrine reload the association to include the newly created associated entity?
(Related: "Doctrine and unrefreshed relationships")
And as I later noticed, the very same Doctrine manual I linked to tells me to set the property as an ArrayCollection in the constructor, excatly so that the $shelf->books->add($book) does work. That is:
public function __construct() {
$this->books = new \Doctrine\Common\Collections\ArrayCollection();
}
Stupid me. I'll post the answer here in case someone else happens to come looking for the same issue, after being just as stupid. Which is unlikely, I guess.