The point of this question is to figure out what technique is better and heard different opinions from some skilled symfony2 coders.
An example will be ilustrated on challenges and "challenge rating" table where many people can rate certain challenge. (something like stackoverflow vote question system).
The tables look like this: (like_dislike is boolean(1= like, 0 = dislike)
The amount of data will be from 10-200+ rates for challenge.
Working with collections
Challenges entity
/**
* Challanges
*
* #ORM\Table(name="challanges")
* #ORM\Entity(repositoryClass="TB\ChallangesBundle\Entity\ChallangesRepository")
*/
class Challanges
{
/**
* #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)
* #Assert\NotBlank()
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="slug", type="string", length=255, unique=true)
*/
private $slug;
/**
* #var string
*
* #ORM\Column(name="description", type="text")
* #Assert\NotBlank()
*/
private $description;
/**
* #var \DateTime
* #ORM\Column(name="start_date", type="datetime", nullable=false)
* #Assert\DateTime()
*/
private $start_date;
/**
* #var \DateTime
* #ORM\Column(name="end_date", type="datetime", nullable=false)
* #Assert\DateTime()
*/
private $end_date;
/**
* #ORM\ManyToOne(targetEntity="TB\UserBundle\Entity\User", fetch="EXTRA_LAZY")
* #ORM\JoinColumn(name="owner_id", referencedColumnName="id", nullable=false)
*/
protected $owner;
/**
* #ORM\OneToMany(targetEntity="TB\ChallangesBundle\Entity\ChallangeRating", mappedBy="challange", cascade={"persist", "remove"})
*/
protected $likes;
/**
* Constructor
*/
public function __construct()
{
$this->likes = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add likes
*
* #param \TB\ChallangesBundle\Entity\ChallangeRating $likes
* #return Challanges
*/
public function addLike(\TB\ChallangesBundle\Entity\ChallangeRating $likes)
{
$this->likes[] = $likes;
return $this;
}
/**
* Remove likes
*
* #param \TB\ChallangesBundle\Entity\ChallangeRating $likes
*/
public function removeLike(\TB\ChallangesBundle\Entity\ChallangeRating $likes)
{
$this->likes->removeElement($likes);
}
/**
* Get likes
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getLikes()
{
return $this->likes;
}
public function filterLikesInChallenge($like_dislike) {
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('like_dislike', $like_dislike));
return $this->likes->matching($criteria);
}
public function checkIfUserRatedAlready(\TB\UserBundle\Entity\User $user)
{
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('fan', $user));
return $this->likes->matching($criteria);
}
Challenge rating entity
<?php
namespace TB\ChallangesBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* ChallangeRating
*
* #ORM\Table(name="challange_rating")
* #ORM\Entity(repositoryClass="TB\ChallangesBundle\Entity\ChallangeRatingRepository")
*/
class ChallangeRating
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var boolean
*
* #ORM\Column(name="like_dislike", type="boolean")
*/
private $like_dislike;
/**
* #ORM\ManyToOne(targetEntity="TB\UserBundle\Entity\User", inversedBy="fans")
*/
protected $fan;
/**
* #ORM\ManyToOne(targetEntity="TB\ChallangesBundle\Entity\Challanges", inversedBy="likes")
*/
protected $challange;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Get like_dislike
*
* #return boolean
*/
public function getLikeDislike()
{
return $this->like_dislike;
}
/**
* Set like_dislike
*
* #param boolean $like_dislike
* #return ChallangeRating
*/
public function setLikeDislike($like_dislike)
{
$this->like_dislike = $like_dislike;
return $this;
}
/**
* Set fan
*
* #param \TB\UserBundle\Entity\User $fan
* #return ChallangeRating
*/
public function setFan(\TB\UserBundle\Entity\User $fan = null)
{
$this->fan = $fan;
return $this;
}
/**
* Get fan
*
* #return \TB\UserBundle\Entity\User
*/
public function getFan()
{
return $this->fan;
}
/**
* Set challange
*
* #param \TB\ChallangesBundle\Entity\Challanges $challange
* #return ChallangeRating
*/
public function setChallange(\TB\ChallangesBundle\Entity\Challanges $challange = null)
{
$this->challange = $challange;
return $this;
}
/**
* Get challange
*
* #return \TB\ChallangesBundle\Entity\Challanges
*/
public function getChallange()
{
return $this->challange;
}
}
Okay and now i would like:
Display challenge details
Display rate feature (like, dislike) also with numbers of likes and dislikes
Display the list of all users that have rated this challenge
Controller
So classic beginning with obtaining $challenge
// this will take care of point number 1 (display challenge details) (1.)
$challange = $challangesRepo->findOneBy(array('slug'=>$slug));
// display numbers of likes and dislikes for certain challenge (2.)
But now comes a decision...
Question 1
Should i obtain the likes via quering the DB from querybuilder? (classic select count)
OR
Should i use collections and criteria like this ? :
$challangeLikes = $challange->filterLikesInChallenge(1);
$challangeDislikes = $challange->filterLikesInChallenge(0);
**What is better for memory usage? what is better for DB performances? **
If i am not wrong the following two queries are executed by these two methods:
SELECT
t0.id AS id1,
t0.like_dislike AS like_dislike2,
t0.fan_id AS fan_id3,
t0.challange_id AS challange_id4
FROM
challange_rating t0
WHERE
(
t0.like_dislike = ? AND t0.challange_id = ?
)
Parameters: [1, 12]
SELECT
t0.id AS id1,
t0.like_dislike AS like_dislike2,
t0.fan_id AS fan_id3,
t0.challange_id AS challange_id4
FROM
challange_rating t0
WHERE
(
t0.like_dislike = ? AND t0.challange_id = ?
)
And now i can pass the number of likes,dislikes to the view as follow:
'challangeLikes'=>$challangeLikes->count(),
'challangeDislikes'=>$challangeDislikes->count(),
Question 2
What if i want to know if certain user rated this challenge already?
Again...
*Should i use classic querybuilder style with select count *
OR
I should use a method like:
$ratedAlreadyCol = $challange->checkIfUserRatedAlready($user)->first();
That will execute actually another query ? something like classic select count but the collection will do this for me? So it's not a search in some big memory array with allll likes but it's a query to DB ?
SELECT
t0.id AS id1,
t0.like_dislike AS like_dislike2,
t0.fan_id AS fan_id3,
t0.challange_id AS challange_id4
FROM
challange_rating t0
WHERE
(
t0.fan_id = ? AND t0.challange_id = ?
)
Parameters: [25, 12]
Question 3 - probably the most important for performances
I want to display all "fans - people who rated the certain challenge"...
Again...
Should i create a separate querybuilder method in repository with selecting all ratings for certain challenge with inner join to the users table (so i can display profile image and username)
OR
Should i just get all ratings and in twig loop through it like:
$challangeLikesCollection = $challange->getLikes();
{% for bla bla
BUT
If i will do it this way... doctrine will execute a select query to the user table for every "fan" in loop... and when there will be let's say... 200 fans... that's not good right?
BONUS QUESTION
Can somehow please provide his way of dealing with these situations? any suggestions?
Or do you use any other technique?
i care a lot about memory usage and DB load time because this will be used everywhere and every user will have such a list with different challenges. The list will consist of let's say 15 challenges and to connect all the likes,dislikes to every challenge in the list etc etc... performances...
Thank you for your explanations, tips and hints that will help me and other readers to move on another level!
I would do the following:
Denormalize a little and add number of likes and number of dislikes fields to Challenge and update these values in addLike and removeLike
Rename like_dislike to like as it's a boolean field and 1 means like 0 means dislike
Query the list of users with a separate query and use array hydration and INDEXBY username (it must be unique or user id) or maybe create a custom hydrator
SELECT u.username, u.photo FROM User u INNER JOIN u.ratings WITH r.fan = :fan INDEX BY u.username
or something like that. And you can check if the current user's username is in the array or not.
I think this could be performant enough.
Some explanation:
INDEX BY means that the result collection or array key (index) will be the value of a field (this field has to be unique). When you use INDEX BY the result set will contain known keys so you can reach (and e.g. check for existence) individual results directly in constant time (you don't have to search through the whole result set).
Doctrine uses PDO underneath, hydration means how the PDO result set will be processed and transformed into something else. The default object hydration means the result set will be transformed into an object graph, it is a very expensive operation. There are other hydration modes which are less expensive, but you loose some felxibility. E.g. if you use array hydration the result will be an array of arrays so you can't modify it (I mean persist back to the database) so it's just for reading and as the result not entity objects you can't use it's methods, e.g. custom getters. You can create custom hydrators if you want.
Related
I've created two Entity, and i try to create a custom relation between them, without using the default syntax.
See :
/**
* #ORM\Entity(repositoryClass=LandRepository::class)
* #ORM\HasLifecycleCallbacks()
*/
class Land
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $libelle;
/**
* #ORM\Column(type="integer")
*/
private $uid;
/**
* #ORM\OneToMany(targetEntity=Ride::class, mappedBy="uidparent")
*/
private $rides;
}
/**
* #ORM\Entity(repositoryClass=RideRepository::class)
* #ORM\HasLifecycleCallbacks()
*/
class Ride
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $libelle;
/**
* #ORM\ManyToOne(targetEntity=Land::class, inversedBy="rides")
* #ORM\JoinColumn(name="uidparent", referencedColumnName="uid")
*/
private $uidparent;
}
Tables are correctly created, but the last instruction have an error.
In MySQL, i made some test, and i need to create an index on "uid" column in "land" table.
So, i changed the header of my class "Land" to force the index
/**
* #ORM\Entity(repositoryClass=LandRepository::class)
* #ORM\Table(name="land",indexes={#ORM\Index(columns={"uid"})})
* #ORM\HasLifecycleCallbacks()
*/
class Land
{
/ ... /
}
I don't understand why i need to specify this index creation.
I hope to have something like this :
(PS : I know i can use the classic syntax (using in my entity Ride a column auto named "land_id") but I want to understand and master this alternative syntax to manage future complex entities and associations)
Thanks !
Need to manually specify in Entity header annotation :
#ORM\Table(name="land",indexes={#ORM\Index(columns={"uid"})})
I want to use a variable time as the amount of time passed to do something:
/**
* #var \DateTime
*
* #ORM\Column(name="TempsPasse", type="time", nullable=true)
*/
public $tempsPasse;
Code:
$Information->setTempsPasse(new \DateTime($Log->getTempsPasse()->format('H:i:s')));
$Information->setTempsPasse(clone($Log->getTempsPasse()));//Same result
$Information->setTempsPasse(new \DateTime('01:00:00'));//Same result
Where echo($Log->getTempsPasse()->format('H:i:s')); show: 01:00:00
Then when I flush I get this error:
FatalErrorException in TimeType.php line 53: Error: Call to a member
function format() on a non-object
Where am I wrong and how can I handle time with doctrine ?
EDIT
/**
* #var \DateTime
*
* #ORM\Column(name="TempsPasse", type="time")
*/
public $tempsPasse;
/**
* Set tempsPasse
*
* #param \DateTime $tempsPasse
*
* #return Log
*/
public function setTempsPasse($tempsPasse)
{
$this->tempsPasse = $tempsPasse;
return $this;
}
/**
* Get tempsPasse
*
* #return \DateTime
*/
public function getTempsPasse()
{
return $this->tempsPasse;
}
That's the same code for both entities.
I wonder if this works:
$Information->setTempsPasse( new \DateTime($Log->getTempsPasse()) );
The new \DateTime may be expecting a DateTime object...
Otherwise, can you edit your post and show how you create the $Log object first, and the code leading up to that.
The one thing I'm thinking is when you create the new \DateTime it is expecting line this:
new \DateTime( '01:00:00' )
Notice the quotes around the time parameter.
Is it possible to do caching of data from sql server queries when using CSqlDataProvider. If so can anyone please provide some links for documentation about it. Or if you have done it personally please guide.
I did a search but found nothing :(
There is some example of implementing this feature
<?php
class CachedSqlDataProvider extends CDataProvider
{
public $queryCache;
public $queryCacheLife;
/**
* #var CDbConnection the database connection to be used in the queries.
* Defaults to null, meaning using Yii::app()->db.
*/
public $db;
/**
* #var string the SQL statement to be used for fetching data rows.
*/
public $sql;
/**
* #var array parameters (name=>value) to be bound to the SQL statement.
*/
public $params=array();
/**
* #var string the name of key field. Defaults to 'id'.
*/
public $keyField='id';
/**
* Constructor.
* #param string $sql the SQL statement to be used for fetching data rows.
* #param array $config configuration (name=>value) to be applied as the initial property values of this class.
*/
public function __construct($sql,$config=array())
{
$this->sql=$sql;
foreach($config as $key=>$value)
$this->$key=$value;
}
/**
* Fetches the data from the persistent data storage.
* #return array list of data items
*/
protected function fetchData()
{
$sql=$this->sql;
$db=$this->db===null ? Yii::app()->db : $this->db;
$db->active=true;
if(($sort=$this->getSort())!==false)
{
$order=$sort->getOrderBy();
if(!empty($order))
{
if(preg_match('/\s+order\s+by\s+[\w\s,]+$/i',$sql))
$sql.=', '.$order;
else
$sql.=' ORDER BY '.$order;
}
}
if(($pagination=$this->getPagination())!==false)
{
$pagination->setItemCount($this->getTotalItemCount());
$limit=$pagination->getLimit();
$offset=$pagination->getOffset();
$sql=$db->getCommandBuilder()->applyLimit($sql,$limit,$offset);
}
if( $this->queryCache == true && $this->queryCacheLife > 0 )
$command=$db->cache( $this->queryCacheLife )->createCommand($sql);
else
$command=$db->createCommand($sql);
foreach($this->params as $name=>$value)
$command->bindValue($name,$value);
return $command->queryAll();
}
/**
* Fetches the data item keys from the persistent data storage.
* #return array list of data item keys.
*/
protected function fetchKeys()
{
$keys=array();
foreach($this->getData() as $i=>$data)
$keys[$i]=$data[$this->keyField];
return $keys;
}
/**
* Calculates the total number of data items.
* This method is invoked when {#link getTotalItemCount()} is invoked
* and {#link totalItemCount} is not set previously.
* The default implementation simply returns 0.
* You may override this method to return accurate total number of data items.
* #return integer the total number of data items.
*/
protected function calculateTotalItemCount()
{
return 0;
}
}
?>
I have an entity "container" with this property
/**
* #ORM\OneToMany(targetEntity="BizTV\ContentManagementBundle\Entity\Content", mappedBy="container")
*/
private $content;
the property is an array collection...
public function __construct() {
$this->content = new \Doctrine\Common\Collections\ArrayCollection();
}
...with these two standard methods
/**
* Add content
*
* #param BizTV\ContentManagementBundle\Entity\Content $content
*/
public function addContent(\BizTV\ContentManagementBundle\Entity\Content $content)
{
$this->content[] = $content;
}
/**
* Get content
*
* #return Doctrine\Common\Collections\Collection
*/
public function getContent()
{
return $this->content;
}
Now my question is, is there a smooth way to build a sorting feature into this, perhaps on the getContent() call? I am no php wiz and certainly not seasoned in symfony2 but I learn as I go.
The content entity itself has a sorting INT like this that I want to sort it on:
/**
* #var integer $sortOrder
*
* #ORM\Column(name="sort_order", type="integer")
*/
private $sortOrder;
You should be able to use the #ORM\OrderBy statement which allows you to specify columns to order collections on:
/**
* #ORM\OneToMany(targetEntity="BizTV\ContentManagementBundle\Entity\Content", mappedBy="container")
* #ORM\OrderBy({"sort_order" = "ASC"})
*/
private $content;
In fact this may be a duplicate of How to OrderBy on OneToMany/ManyToOne
Edit
Checking for implementation advice it appears that you must fetch the tables with a join query to the collection in order for the #ORM\OrderBy annotation to work: http://www.krueckeberg.org/notes/d2.html
This means that you must write a method in the repository to return the container with the contents table joined.
If you want to be sure that you always get your relations in the order based on current property values, you can do something like this:
$sort = new Criteria(null, ['Order' => Criteria::ASC]);
return $this->yourCollectionProperty->matching($sort);
Use that for example if you've changed the Order property. Works great for a "Last modified date" as well.
You can write
#ORM\OrderBy({"date" = "ASC", "time" = "ASC"})
for multiple criteria ordering.
You can also sort ArrayCollection by Criteria property orderBy like so:
<?php
namespace App/Service;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
/**
* Class SortService
*
* #package App\Service
*/
class SortService {
/**
* #param SomeAbstractObject $object
* #return SomeCollectionItem[]
*/
public function sorted(SomeAbstractObject $object): array {
/** $var ArrayCollection|SomeCollectionItem[] */
$collection = $object->getCollection();
// convert normal array to array collection object
if(\is_array(collection)) {
$collection = new ArrayCollection(collection);
}
// order collection items by position property
$orderBy = (Criteria::create())->orderBy([
'position' => Criteria::ASC,
]);
// return sorted SomeCollectionItem array
return $collection->matching($orderBy)->toArray();
}
}
?>
I have a form to create a new entity. That entity has a collection of other entities that are also entered in that form.
I want to use the validation options of the entity in the collection to validate those entities but it does not work. The validation rules of the "main" entity (Person) are checked, but the validation rules of the entities in the addressList collection (Address) are not checked. When I input invalid information in the fields, the submitted form is successfully validated.
In this example, the annotation for street is not used on validation.
class Person
{
...
/**
* #ORM\OneToMany(targetEntity="Address", mappedBy="owner", cascade={"persist", "detach"})
*/
protected $addressList;
....
}
class Address
{
...
/**
* #ORM\ManyToOne(targetEntity="Person", inversedBy="addressList")
* #ORM\JoinColumn(name="person_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $owner;
/**
* #ORM\Column(type="string", length=75)
* #Assert\MinLength(
* limit=3,
* message="Street must have atleast {{ limit }} characters."
* )
*/
protected $street;
...
}
How can I get the form to validate the supplied Address entities?
I had the same problem but was solved with:
/**
* #ORM\OneToMany(
* targetEntity="Entity",
* mappedBy="mappedEntity",
* cascade={"persist" , "remove"}
* )
* #Assert\Valid
*/
I use this:
use Symfony\Component\Validator\ExecutionContextInterface;
class Person
{
...
/**
* #ORM\OneToMany(targetEntity="Address", mappedBy="owner", cascade={"persist", "detach"})
*/
protected $addressList;
....
/**
* #Assert\Callback
*/
public function validate(ExecutionContextInterface $context)
{
if (!$this->getAddressList()->count()) {
$context->addViolationAt(
'addressList',
'You must add at least one address',
array(),
null
);
}
}
}
http://symfony.com/doc/current/reference/constraints/Callback.html
Just add annotation assert like following
/**
* #Assert\Count(
* min = "1",
* minMessage = "You must specify at least one"
* )
* #Assert\Valid
*
*/
protected $name_of_collection_property;
You could also use the "Valid" constraint with the "All" constraint :
/**
* #ORM\OneToMany(targetEntity="Address", mappedBy="owner", cascade={"persist", "detach"})
* #Assert\All({
* #Assert\Valid
* })
*/
protected $addressList;