sort objects with several parameters - symfony2 - symfony

I want to display a list of objects ordered by a parameter that depends of several fields of the entity.
I have created an entity, let's call it Object for instance, which has an id and 2 integers as fields. I work with Symfony 2 and Doctrine as ORM.
`/**
* Object
*
* #ORM\Table(name="object")
* #ORM\Entity(repositoryClass="AppBundle\Repository\ObjectRepository")
*/
class Object
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var int
*
* #ORM\Column(name="field1", type="int")
*/
private $field1;
/**
* #var int
*
* #ORM\Column(name="field2", type="int")
*/
private $field2;
}
Now I would like to get the top-5 instances of Object from my database ordered by, for example, the average between field1 and field2, to display them in a twig template.
I know I can use this:
$list = $this->getDoctrine()->getRepository('AppBundle:Object')->findAll();
to get a list of all existing instances of Object in the DB, but although it might be a relatively simple question to experimented Symfony programmers, I have no idea about how to order it by avg(field1,field2).

You could create a method in your repository that builds a query containing a special ORDER BY clause and that limits the number of results to 5. It could look like this:
class ObjectRepository extends \Doctrine\ORM\EntityRepository
{
/* ... other methods ... */
public function retrieveTopObjects($limit = 5)
{
$qb = $this->createQueryBuilder('o')->orderBy('(o.field1 + o.field2) / 2', 'DESC');
$qb->setMaxResults($limit);
return $qb->getQuery()->getResult();
}
}
If you would like to have the score used to sort entities along with your objects, you would need to add $qb->addSelect('(o.field1 + o.field2) / 2 AS score') and then use $qb->orderBy('score', 'DESC') instead of the expression shown above.
In such a case, you would end up with a mixed result as explained in the documentation.

Related

Query an inherited table via a relation table

I want to achieve a pretty simple query in theory but I didn't manage to make it work: I want the number of active CVs grouped by Elo (which is an attribute in an inherited table).
The error:
[Semantical Error] line 0, col 22 near 'elo FROM MyNamespace\CvBundle\Entity\Cv':
Error: Class MyNamespace\CvBundle\Entity\Cv\Feature has no field or association named elo.
It complains about not having a field in MyNamespace\CvBundle\Entity\Cv\Feature which is true because it's the "master" table. This field is contained in the MyNamespace\CvBundle\Entity\Cv\Lol which is a table inherited from Cv\Feature
Here's the query:
// CvRepository.php
public function getStats()
{
$query = $this->createQueryBuilder('c')
->select('COUNT(f.id), f.elo')
->leftJoin('c.feature', 'f')
->groupBy('f.elo')
->where('f INSTANCE OF MyNameSpace\CvBundle\Entity\Cv\Lol')
->andWhere('c.active = :active')
->andWhere('c.expiresAt > :now')
->setParameters(array(
'now' => new \DateTime("now"),
'active' => 1,
))
->getQuery();
return $query->execute();
}
And the the table Cv:
// Cv.php
/**
* #ORM\Table(name="cv")
* #ORM\Entity(...)
*/
class Cv
{
/**
* #ORM\OneToOne(targetEntity="MyNameSpace\CvBundle\Entity\Cv\Feature", cascade={"all"})
*/
protected $feature;
}
The Feature.php
/**
* #ORM\Entity()
* #ORM\Table(name="cv_feature")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="type", type="string")
* #ORM\DiscriminatorMap({"lol" = "Lol", ...})
*/
abstract class Feature
{
/**
* #ORM\OneToOne(targetEntity="MyNameSpace\CvBundle\Entity\Cv")
* #ORM\JoinColumn(name="cv_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $cv;
And the Lol.php
/**
* #ORM\Entity()
*/
class Lol extends Feature
{
/**
* #var integer $elo
*
* #ORM\Column(name="elo", type="string")
*/
private $elo;
....
Pretty sure you will have to move $elo to your Feature class.
Your 'where instance of' will restrict the results to Lol classes but I doubt if DQl is smart enough to realize that all features will then be lol's.
You could probably change Cv to point to Lol but thats probably not what you want either.
You could also implement the group by in php.
But try this and verify it works:
abstract class Feature
{
/**
* #ORM\OneToOne(targetEntity="MyNameSpace\CvBundle\Entity\Cv")
* #ORM\JoinColumn(name="cv_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $cv;
/**
* #var integer $elo
*
* #ORM\Column(name="elo", type="string")
*/
protected $elo;
You would only put getter/setters for elo on your Lol class. So it's basically hidden from it's siblings. And it's already going to be in the database table anyway. You might even be able to keep it as private and add it to Lol only so siblings would have no access to it at all. Not sure about that but I think doctrine might still hydrate it.

Many-to-Many, Doctrine's Entity Generator and Pluralization

Doctrine's many-to-many logic is confusing me a bit. I have a pretty simple many-to-many relationship of recipes to categories. My base entity classes are equally simple.
The Recipe entity class...
class Recipe
{
/**
* #ORM\ManyToMany(targetEntity="Category", inversedBy="categories")
* #ORM\JoinTable(name="recipe_category")
**/
private $categories;
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=255)
*/
private $title;
public function __construct() {
$this->categories = new \Doctrine\Common\Collections\ArrayCollection();
}
}
And the Category entity class...
class Category
{
/**
* #ORM\ManyToMany(targetEntity="Recipe")
**/
private $recipes;
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
public function __construct() {
$this->recipes = new \Doctrine\Common\Collections\ArrayCollection();
}
}
Seems pretty strait forward and matches Doctrine (and Symfony2's) documentation examples. The strange behavior comes when I try and generate the getters and setters for these classes via the Symfony console app.
The relationship setters/getters are incorrect. Take, for instance, the Category setter in the Recipe class that's generated...
/**
* Add categories
*
* #param \Namespace\CookbookBundle\Entity\Category $categories
* #return Recipe
*/
public function addCategorie(\Namespace\CookbookBundle\Entity\Category $categories)
{
$this->categories[] = $categories;
return $this;
}
It looks like the auto-generation of the method name is off. It should be "addCategory" and should be passed a "category."
While I can just correct this manually, if I re-run the entity generator, it will just add them again.
Am I doing this incorrectly or is this just a quirk of the entity generator? Can I specify an over-ride via annotation?
You're not doing anything wrong as that's how symfony generates them. I usually don't use the app/console to generate them as currently they're not doing a good job. One example is as you've mentioned the pluralization of words as you've mentioned. Another obvious one is the fact that it's using the [] notation which is pretty much treating an ArrayCollection object as a PHP array. You should never treat ArrayCollections as arrays.
This is how I have implemented it myself:
public function addCategory(Category $category)
{
if (!$this->categories->contains($category)
$this->categories->add($category);
return $this;
}
Which doesn't add duplicates to the Array collection if it's already added. Same thing goes with remove:
public function removeCategory(Category $category)
{
if ($this->categories->contains($category)
$this->categories->remove($category);
}
What I've run into many times is let's say you have 4 categories and you add and remove them
$r = new Recipe();
$c1 = new Category();
$c2 = new Category();
$r->addCategory($c1);
$r->addCategory($c2);
// at this point $r->getCategories()->toArray()[0] contains $c1
// and $r->getCategories()->toArray()[1] contains $c2
$r->removeCategory($c1);
// now $r->getCategories()->toArray()[0] is empty and
// $r->getCategories()->toArray()[1] contains $c2 still
// so in order to get the first category you need to:
$r->getCategories()->first();
You are not doing anything wrong. It is just that Doctrine automatically tries to singularize the names of method stubs whenever there is a plural name for a collection property. This is the function that Doctrine calls when you run the command doctrine:generate:entities:
$methodName = Inflector::singularize($methodName);
In your case, Doctrine tries to 'singularize' the word categories but fails to recognize the singular form correctly, so it just removes an 's' from the end returning categorie.
Also, as you see, Doctrine does not singularize the parameter passed to the method stubs, leaving $categories instead of being consistent and modifying it to $categorie.
If you want to avoid this, either you do not use plural words for collections, or use plural words and change the methods afterwards. As #keyboardSmasher comments to your post, doctrine won't overwrite methods you already have when using the command doctrine:generate:entities, and wrong methods won't hurt much if left there alone.
A final note: using ArrayCollections as arrays is perfectly fine, so this code is correct:
$this->categories[] = $category;
ArrayCollection object implements Collection, which in turn implements ArrayAccess. It is done this way precisely to be able to use ArrayCollections as Arrays.

Generate unique id - doctrine - symfony2

I want to generate a unique ticket ID for my tickets. But how to let doctrine generate a unique id?
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id()
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
little more explain:
id must be 6 charters like: 678915
id must be unique
As of version 2.3, you can just add the following annotations to your property:
/**
* #ORM\Column(type="guid")
* #ORM\Id
* #ORM\GeneratedValue(strategy="UUID")
*/
protected $id;
Use custom GeneratedValue strategy:
1. In your Entity class:
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="AppBundle\Doctrine\RandomIdGenerator")
*/
protected $id;
2. Then create file AppBundle/Doctrine/RandomIdGenerator.php with content
namespace AppBundle\Doctrine;
use Doctrine\ORM\Id\AbstractIdGenerator;
class RandomIdGenerator extends AbstractIdGenerator
{
public function generate(\Doctrine\ORM\EntityManager $em, $entity)
{
$entity_name = $em->getClassMetadata(get_class($entity))->getName();
// Id must be 6 digits length, so range is 100000 - 999999
$min_value = 100000;
$max_value = 999999;
$max_attempts = $min_value - $max_value;
$attempt = 0;
while (true) {
$id = mt_rand($min_value, $max_value);
$item = $em->find($entity_name, $id);
// Look in scheduled entity insertions (persisted queue list), too
if (!$item) {
$persisted = $em->getUnitOfWork()->getScheduledEntityInsertions();
$ids = array_map(function ($o) { return $o->getId(); }, $persisted);
$item = array_search($id, $ids);
}
if (!$item) {
return $id;
}
// Should we stop?
$attempt++;
if ($attempt > $max_attempts) {
throw new \Exception('RandomIdGenerator worked hardly, but failed to generate unique ID :(');
}
}
}
}
You can use the PrePersist annotation, like this:
/**
* #ORM\PrePersist()
*/
public function preSave() {
$this->id = uniqid();
}
As the annotation name suggest, it will be run before object persistence into database.
For unique id, I simply use a native php uniqid() function http://php.net/manual/en/function.uniqid.php which will return 13 characters. To get only 6 characters, refer to this PHP Ticket ID Generation
In the $id property, I think you also need to remove this line to prevent auto generated value of it:
#ORM\GeneratedValue(strategy="AUTO")
Doctrine will treat this field as your primary key (because of the #Id annotation), so this field is already unique. If you have the #GeneratedValue annotation on AUTO strategy Doctrine will figure out which strategy to use dependend on the db platform. It will default to IDENTITY on MySql and the field will be a auto_increment then.
You can write the id annotation without the brackets as follows.
ORM\Id
While I'm seconding the UUID approach suggested by Jonhathan, you could prefer a shorter, more readable, identifier. In this case you can use ShortId Doctrine bundle.

ManyToMany relationship with extra fields in symfony2 orm doctrine

Hi i have that same question as here: Many-to-many self relation with extra fields? but i cant find an answer :/ I tried first ManyToOne and at the other site OneToMany ... but then i could not use something like
public function hasFriend(User $user)
{
return $this->myFriends->contains($user);
}
because there was some this problem:
This function is called, taking a User type $user variable and you then use the contains() function on $this->myFriends.
$this->myFriends is an ArrayCollection of Requests (so different type than User) and from the doctrine documentation about contains():
The comparison of two elements is strict, that means not only the value but also the type must match.
So what is the best way to solve this ManyToMany relationship with extra fields? Or if i would go back and set the onetomany and manytoone relationship how can i modify the hasFriend method? To example check if ID is in array collection of ID's.
EDIT: i have this table... and what i need is:
1. select my friends... and my followers ...check if i am friend with him or not. (because he can be friend with me and i dont have to be with him... like on twitter). I could make manytomany but i need extra fields like: "viewed" "time when he subscribe me" as you can see at my table.
And make query like this and then be able in twig check if (app.user.hasFriend(follower) or something like that)
$qb = $this->createQueryBuilder('r')
->select('u')
->innerJoin('UserBundle:User', 'u')
->Where('r.friend_id=:id')
->setParameter('id', $id)
->orderBy('r.time', 'DESC')
->setMaxResults(50);
return $qb->getQuery()
->getResult();
I was trying to have a many to many relationship with extra fields, and couldn't make it work either... The thing I read in a forum (can't remember where) was:
If you add data to a relationship, then it's not a relationship anymore. It's a new entity.
And it's the right thing to do. Create a new entity with the new fields, and if you need it, create a custom repository to add the methods you need.
A <--- Many to many with field ---> B
would become
A --One to many--> C (with new fields) <-- One to many--B
and of course, C has ManyToOne relationships with both A and B.
I searched everywhere on how to do this, but in the end, it's the right thing to do, if you add data, it's no longer a relationship.
You can also copy what contains usually do, or try to overwrite it in a custom repository, to do whatever you need it to do.
I hope this helps.
I'm adding another answer since it has nothing to do with my original answer. Using the new info you posted, I'm calling the table/entity you posted "Follower". The original entity, "User".
What happens if you create the following associations:
namespace Acme\UserBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Acme\UserBundle\Entity\User
*
* #ORM\Table()
* #ORM\Entity
*/
class User
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="Acme\FollowerBundle\Entity\Follower", mappedBy="followeduser")
*/
protected $followers;
/**
* #ORM\OneToMany(targetEntity="Acme\FollowerBundle\Entity\Follower", mappedBy="followeeuser")
*/
protected $followees;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
public function __construct()
{
$this->followers = new \Doctrine\Common\Collections\ArrayCollection();
$this->followees = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add followers
*
* #param Acme\FollowerBundle\Entity\Follower $follower
*/
public function addFollower(\Acme\FollowerBundle\Entity\Follower $follower)
{
$this->followers[] = $follower;
}
/**
* Add followees
*
* #param Acme\FollowerBundle\Entity\Follower $followee
*/
public function addFollowee(\Acme\FollowerBundle\Entity\Follower $followee)
{
$this->followees[] = $followee;
}
/**
* Get followers
*
* #return Doctrine\Common\Collections\Collection
*/
public function getFollowers()
{
return $this->followers;
}
/**
* Get followees
*
* #return Doctrine\Common\Collections\Collection
*/
public function getFollowees()
{
return $this->followees;
}
}
namespace Acme\FollowerBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Acme\FollowerBundle\Entity\Follower
*
* #ORM\Table()
* #ORM\Entity
*/
class Follower
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Acme\UserBundle\Entity\User", inversedBy="followers")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $followeduser;
/**
* #ORM\ManyToOne(targetEntity="Acme\UserBundle\Entity\User", inversedBy="followees")
* #ORM\JoinColumn(name="followee_id", referencedColumnName="id")
*/
protected $followeeuser;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set followeduser
*
* #param Acme\UserBundle\Entity\User $followeduser
*/
public function setFolloweduser(\Acme\UserBundle\Entity\User $followeduser)
{
$this->followeduser = $followeduser;
}
/**
* Get followeduser
*
* #return Acme\UserBundle\Entity\User
*/
public function getFolloweduser()
{
return $this->followeduser;
}
/**
* Set followeeuser
*
* #param Acme\UserBundle\Entity\User $followeeuser
*/
public function setFolloweeuser(\Acme\UserBundle\Entity\User $followeeuser)
{
$this->followeeuser = $followeeuser;
}
/**
* Get followeeuser
*
* #return Acme\UserBundle\Entity\User
*/
public function getFolloweeuser()
{
return $this->followeeuser;
}
}
I'm not sure if this would do the trick, I really don't have much time to test it, but if it doesn't, I thnk that it's on it's way. I'm using two relations, because you don't need a many to many. You need to reference that a user can have a lot of followers, and a follower can follow a lot of users, but since the "user" table is the same one, I did two relations, they have nothing to do with eachother, they just reference the same entity but for different things.
Try that and experiment what happens. You should be able to do things like:
$user->getFollowers();
$follower->getFollowedUser();
and you could then check if a user is being followed by a follower whose user_id equals $userThatIwantToCheck
and you could search in Followers for a Follower whose user = $user and followeduser=$possibleFriend

Implementing a friends list in Symfony2.1 with Doctrine

I want to implement friends list of particular user in Symfony2.1 and Doctrine.
Lets say friends table:
User1 User2 Status //0-pending request,1-accepted
A B 0
A C 1
D A 1
E A 1
Now I want to get A's friends name in the list. For this SQL query can be implemented using UNION as read in many other answers. But I want to implement this in doctrine query builder.
One option is like query separately for two columns and combine the result and sort. But this takes more time to execute and get result. I want to get quick response as soon as possible. Is there any way to query it?
You don't need any additional effort, e.g. by using Doctrine Query Builder!
Simply design the entity class User to have a many-to-many self-reference with User, e.g.:
* #ORM\Table()
* #ORM\Entity()
*/
class User
{
....
/**
* #var string $name
*
* #ORM\Column(name="name", type="string", unique=true, length=255)
*
*/
private $name;
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="myFriends")
**/
private $friendsWithMe;
/**
* #ORM\ManyToMany(targetEntity="User", inversedBy="friendsWithMe")
* #ORM\JoinTable(name="friends",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="friend_user_id", referencedColumnName="id")}
* )
**/
private $myFriends;
public function __construct() {
$this->friendsWithMe = new \Doctrine\Common\Collections\ArrayCollection();
$this->myFriends = new \Doctrine\Common\Collections\ArrayCollection();
}
}
Then you can simply get the User entity and obtains all the friends as follows:
$user = $this->getDoctrine()
->getRepository('AcmeUserBundle:User')
->findOneById($anUserId);
$friends = $user->getMyFriends();
$names = array();
foreach($friends as $friend) $names[] = $friend->getName();

Resources