Querying for related entities by condition in Symfony(doctrine) - php

For this question you need to know about 2 entities :
Loan and Charge. One Loan has Multiple Charges.
So, in my controller, I am querying for each entity and it seems to be lame.
$query = $this->getDoctrine()
->getRepository('AppBundle:Loan')
->find($id);
$query1 = $em->createQuery(
'SELECT p
FROM AppBundle:Charge p
WHERE p.loanId = :loanId
AND p.isActive = true
')->setParameter('loanId', $id);
I want to transform query1 in smth better, using relationship.
So, from my point of view it must be smth like :
foreach($query->getCharges() as $charge) {
if($charge->getIsActive() == true) {
//what to put here?
}
}
If condition passes, how can I obtain the same object that came from DB from my first code?

You can create an extra method on your Loan class that uses ArrayCollections::filter:
public function getActiveCharges()
{
return $this->getCharges()->filter(function (Charge $charge) {
return $charge->getIsActive() === true;
//or you can omit '=== true'
return $charge->getIsActive();
});
}

Related

data repeating when two tables are joined in codeigniter

I tried to fetch data using joins and the data is repeating,
The controller code is:
public function searchjobs2()
{
//$id=$_SESSION['id'];
$lan = $_POST["picke"]; //var_dump($id);die();
$value['list']=$this->Free_model->get_jobs($lan);//var_dump($value);die();
$this->load->view('free/header');
$this->load->view('free/searchjobs2',$value);
}
And the model:
public function get_jobs($lan)
{
$this->db->select('*');
$this->db->from("tbl_work_stats");
$this->db->join("tbl_work", "tbl_work.login_id = tbl_work_stats.login_id",'inner');
$this->db->where("language LIKE '%$lan%'");
// $this->db->where('tbl_work_stats.login_id',$id);
$this->db->order_by('insertdate','asc');
$query=$this->db->get()->result_array();//var_dump($query);die();
return $query;
}
I have used
foreach ($list as $row){
...
}
for listing.
Using distinct will remove duplicate fields:
$this->db->distinct();
From what I can see, your query has ambiguity, and an error in the join statement, also your where like is part of the problem, I would recommend trying this even do there are some missing info, find out wich field you need to join from the second table.
public function get_jobs($lan){
$this->db->select("tbl_work_stats.*, tbl_work.fields");
$this->db->from("tbl_work_stats");
$this->db->join("tbl_work", "tbl_work_stats.login_id = tbl_work.login_id","inner");
$this->db->where("tbl_work.language LIKE", "%" . $lan . "%" );
$this->db->order_by("tbl_work_stats.insertdate","asc");
$query=$this->db->get()->result_array();
return $query;}
do you mean to join on login_id?
I am guessing that is the user logging in and it is the same for many entries of tbl_work_stats and tbl_work.
you didn't post your schema, , but login_id doesn't seem like right thing to join on. how about something like tbl_work.id = tbl_work_stats.tbl_work_id or similar?
also CI $db returns self, so you can do:
public function get_jobs(string $lan):array
{
return $this->db->select()
->from('tbl_work_stats')
->join('tbl_work','tbl_work.id = tbl_work_stats.work_id')
->like('language',$lan)
->order_by('insertdate')
->get()
->result_array();
}

Laravel Query Builder: order by appends field

My model looks like:
protected $appends = array('status');
public function getStatusAttribute()
{
if ($this->someattribute == 1) {
$status = 'Active';
} elseif ($this->someattribute == 2) {
$status = 'Canceled';
...
} else {
$status = 'Some antoher status';
}
return $status;
}
And I want to order a collection of this models by this status attribute, is it possible?
Model::where(...)->orderBy(???)
p.s. I need exactly orderBy, not sortBy solution.
There is no way to make Eloquent do this because it only creates a SQL query. It does not have the ability to translate the PHP logic in your code into a SQL query.
A work around is to loop over the results afterwards and manually check the appends fields. Collections may be useful here.
Either you can use
Model::where(...)->orderBy('someattribute')->get();
in this case you will only get integer value in place of someattribute, or you can use DB query s follows
DB::select(DB::raw('(CASE WHEN someattribute = 1 THEN "Active" CASE WHEN someattribute = 1 THEN "Canceled" ELSE "Some antoher status" END) AS status'))
->orderBy('someattribute', 'desc');

QueryBuilder/Doctrine Select join groupby

So recently I have been thinking and can't find a solution yet to this problem since my lack of development with doctrine2 and symfony query builder.
I have 2 tables:
Goals: id,user_id,target_value...
Savings: id,goal_id,amount
And I need to make a select from goals (all the informations in my table are from the goals table, except that I need to make a SUM(amount) from the savings table on each goal, so I can show the user how much did he saved for his goal)
This is the MySQL query:
select
admin_goals.created,
admin_goals.description,
admin_goals.goal_date,
admin_goals.value,
admin_goals.budget_categ,
sum(admin_savings.value)
from admin_goals
inner join admin_savings on admin_savings.goal_id=admin_goals.id
where admin_goals.user_id=1
group by admin_goals.id
It returns what I want but I have no idea how to implement it with doctrine or query builder, can you please show me an example in both ways?
I highly appreciate it !
I am going to assume you need this fields only and not your AdminGoals entity. On your AdminGoalsRepository you can do something like this:
public function getGoalsByUser(User $user)
{
$qb = $this->createQueryBuilder('goal');
$qb->select('SUM(savings.value) AS savings_value')
->addSelect('goal.created')
->addSelect('goal.description')
->addSelect('goal.goalDate')
->addSelect('goal.value')
->addSelect('goal.budgetCat') //is this an entity? it will be just an ID
->join('goal.adminSavings', 'savings', Join::WITH))
->where($qb->expr()->eq('goal.user', ':user'))
->groupBy('goal.id')
->setParameter('user', $user);
return $qb->getQuery()->getScalarResult();
}
Keep in mind that the return object will be an array of rows, each row is an associated array with keys like the mappings above.
Edit
After updating the question, I am going to change my suggested function but going to leave the above example if other people would like to see the difference.
First things first, since this is a unidirectional ManyToOne between AdminSavings and AdminGoals, the custom query should be in AdminSavingsRepository (not like above). Also, since you want an aggregated field this will "break" some of your data fetching. Try to stay as much OOP when you are not just rendering templates.
public function getSavingsByUser(User $user)
{
$qb = $this->createQueryBuilder('savings');
//now we can use the expr() function
$qb->select('SUM(savings.value) AS savings_value')
->addSelect('goal.created')
->addSelect('goal.description')
->addSelect('goal.goalDate')
->addSelect('goal.value')
->addSelect('goal.budgetCat') //this will be just an ID
->join('savings.goal', 'goal', Join::WITH))
->where($qb->expr()->eq('goal.user', ':user'))
->groupBy('goal.id')
->setParameter('user', $user);
return $qb->getQuery()->getScalarResult();
}
Bonus
public function FooAction($args)
{
$em = $this->getDoctrine()->getManager();
$user = $this->getUser();
//check if user is User etc depends on your config
...
$savings = $em->getRepository('AcmeBundle:AdminSavings')->getSavingsByUser($user);
foreach($savings as $row) {
$savings = $row['savings_value'];
$goalId = $row['id'];
$goalCreated = $row['created'];
[...]
}
[...]
}
If you use createQuery(), then you can do something like this:
$dqlStr = <<<"DSQL"
select
admin_goals.created,
admin_goals.description,
admin_goals.goal_date,
admin_goals.value,
admin_goals.budget_categ,
sum(admin_savings.value)
from admin_goals
inner join admin_savings on admin_savings.goal_id=admin_goals.id
where admin_goals.user_id=1
group by admin_goals.id
DSQL;
$em = $this->getDoctrine()->getManager();
$query = $em->createQuery($dqlStr);
$query->getResult();
On the other hand, if you would like to use createQueryBuilder(), you can check this link: http://inchoo.net/dev-talk/symfony2-dbal-querybuilder/

Laravel, Datatables, column with relations count

I have two models, User and Training, with Many to many relationship between them. I'm using the Laravel Datatables package to display a table of all the users. This is how the data controller method (which retrieves the query results and creates a Datatables table) looks like:
public function getData()
{
$users = User::select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
->where('users.is_active', '=', 1);
return \Datatables::of($users)
->remove_column('id')
->make();
}
How can I add a column to the created table which displays the total number of relations for each user (that is, how many Trainings does each User have)?
The brute force way would be to try a User::selectRaw(...) which has a built in subquery to get the count of trainings for the user and expose it as a field.
However, there is a more built-in way to do this. You can eager load the relationship (to avoid the n+1 queries), and use the DataTables add_column method to add in the count. Assuming your relationship is named trainings:
public function getData() {
$users = User::with('trainings')->select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
->where('users.is_active', '=', 1);
return \Datatables::of($users)
->add_column('trainings', function($user) {
return $user->trainings->count();
})
->remove_column('id')
->make();
}
The name of the column in add_column should be the same name as the loaded relationship. If you use a different name for some reason, then you need to make sure to remove the relationship column so it is removed from the data array. For example:
return \Datatables::of($users)
->add_column('trainings_count', function($user) {
return $user->trainings->count();
})
->remove_column('id')
->remove_column('trainings')
->make();
Edit
Unfortunately, if you want to order on the count field, you will need the brute force method. The package does its ordering by calling ->orderBy() on the Builder object passed to the of() method, so the query itself needs the field on which to order.
However, even though you'll need to do some raw SQL, it can be made a little cleaner. You can add a model scope that will add in the count of the relations. For example, add the following method to your User model:
Note: the following function only works for hasOne/hasMany relationships. Please refer to Edit 2 below for an updated function to work on all relationships.
public function scopeSelectRelatedCount($query, $relationName, $fieldName = null)
{
$relation = $this->$relationName(); // ex: $this->trainings()
$related = $relation->getRelated(); // ex: Training
$parentKey = $relation->getQualifiedParentKeyName(); // ex: users.id
$relatedKey = $relation->getForeignKey(); // ex: trainings.user_id
$fieldName = $fieldName ?: $relationName; // ex: trainings
// build the query to get the count of the related records
// ex: select count(*) from trainings where trainings.id = users.id
$subQuery = $related->select(DB::raw('count(*)'))->whereRaw($relatedKey . ' = ' . $parentKey);
// build the select text to add to the query
// ex: (select count(*) from trainings where trainings.id = users.id) as trainings
$select = '(' . $subQuery->toSql() . ') as ' . $fieldName;
// add the select to the query
return $query->addSelect(DB::raw($select));
}
With that scope added to your User model, your getData function becomes:
public function getData() {
$users = User::select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
->selectRelatedCount('trainings')
->where('users.is_active', '=', 1);
return \Datatables::of($users)
->remove_column('id')
->make();
}
If you wanted the count field to have a different name, you can pass the name of the field in as the second parameter to the selectRelatedCount scope (e.g. selectRelatedCount('trainings', 'training_count')).
Edit 2
There are a couple issues with the scopeSelectRelatedCount() method described above.
First, the call to $relation->getQualifiedParentKeyName() will only work on hasOne/hasMany relations. This is the only relationship where that method is defined as public. All the other relationships define this method as protected. Therefore, using this scope with a relationship that is not hasOne/hasMany throws an Illuminate\Database\Query\Builder::getQualifiedParentKeyName() exception.
Second, the count SQL generated is not correct for all relationships. Again, it would work fine for hasOne/hasMany, but the manual SQL generated would not work at all for a many to many relationship (belongsToMany).
I did, however, find a solution to both issues. After looking through the relationship code to determine the reason for the exception, I found Laravel already provides a public method to generate the count SQL for a relationship: getRelationCountQuery(). The updated scope method that should work for all relationships is:
public function scopeSelectRelatedCount($query, $relationName, $fieldName = null)
{
$relation = $this->$relationName(); // ex: $this->trainings()
$related = $relation->getRelated(); // ex: Training
$fieldName = $fieldName ?: $relationName; // ex: trainings
// build the query to get the count of the related records
// ex: select count(*) from trainings where trainings.id = users.id
$subQuery = $relation->getRelationCountQuery($related->newQuery(), $query);
// build the select text to add to the query
// ex: (select count(*) from trainings where trainings.id = users.id) as trainings
$select = '(' . $subQuery->toSql() . ') as ' . $fieldName;
// add the select to the query
return $query->addSelect(DB::raw($select));
}
Edit 3
This update allows you to pass a closure to the scope that will modify the count subquery that is added to the select fields.
public function scopeSelectRelatedCount($query, $relationName, $fieldName = null, $callback = null)
{
$relation = $this->$relationName(); // ex: $this->trainings()
$related = $relation->getRelated(); // ex: Training
$fieldName = $fieldName ?: $relationName; // ex: trainings
// start a new query for the count statement
$countQuery = $related->newQuery();
// if a callback closure was given, call it with the count query and relationship
if ($callback instanceof Closure) {
call_user_func($callback, $countQuery, $relation);
}
// build the query to get the count of the related records
// ex: select count(*) from trainings where trainings.id = users.id
$subQuery = $relation->getRelationCountQuery($countQuery, $query);
// build the select text to add to the query
// ex: (select count(*) from trainings where trainings.id = users.id) as trainings
$select = '(' . $subQuery->toSql() . ') as ' . $fieldName;
$queryBindings = $query->getBindings();
$countBindings = $countQuery->getBindings();
// if the new count query has parameter bindings, they need to be spliced
// into the existing query bindings in the correct spot
if (!empty($countBindings)) {
// if the current query has no bindings, just set the current bindings
// to the bindings for the count query
if (empty($queryBindings)) {
$queryBindings = $countBindings;
} else {
// the new count query bindings must be placed directly after any
// existing bindings for the select fields
$fields = implode(',', $query->getQuery()->columns);
$numFieldParams = 0;
// shortcut the regex if no ? at all in fields
if (strpos($fields, '?') !== false) {
// count the number of unquoted parameters (?) in the field list
$paramRegex = '/(?:(["\'])(?:\\\.|[^\1])*\1|\\\.|[^\?])+/';
$numFieldParams = preg_match_all($paramRegex, $fields) - 1;
}
// splice into the current query bindings the bindings needed for the count subquery
array_splice($queryBindings, $numFieldParams, 0, $countBindings);
}
}
// add the select to the query and update the bindings
return $query->addSelect(DB::raw($select))->setBindings($queryBindings);
}
With the updated scope, you can use the closure to modify the count query:
public function getData() {
$users = User::select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
->selectRelatedCount('trainings', 'trainings', function($query, $relation) {
return $query
->where($relation->getTable().'.is_creator', false)
->where($relation->getTable().'.is_speaker', false)
->where($relation->getTable().'.was_absent', false);
})
->where('users.is_active', '=', 1);
return \Datatables::of($users)
->remove_column('id')
->make();
}
Note: as of this writing, the bllim/laravel4-datatables-package datatables package has an issue with parameter bindings in subqueries in the select fields. The data will be returned correctly, but the counts will not ("Showing 0 to 0 of 0 entries"). I have detailed the issue here. The two options are to manually update the datatables package with the code provided in that issue, or to not use parameter binding inside the count subquery. Use whereRaw to avoid parameter binding.
I would setup your DB tables and Eloquent models using the conventions provided at http://laravel.com/docs/4.2/eloquent. In your example you would have three tables.
trainings
training_user
users
Your models would look something like this.
class Training {
public function users() {
return $this->belongsToMany('User');
}
}
class User {
public function trainings() {
return $this->belongsToMany('Training');
}
}
You can then use Eloquent to get a list of users and eager load their trainings.
// Get all users and eager load their trainings
$users = User::with('trainings')->get();
If you want to count the number of trainings per user you can simply iterate over $users and count the size of the trainings array.
foreach ( $users as $v ) {
$numberOfTrainings = sizeof($v->trainings);
}
Or you can simply do it in pure SQL. Note that my example below assumes you follow Laravel's conventions for naming tables and columns.
SELECT
u.*, COUNT(p.user_id) AS number_of_trainings
FROM
users u
JOIN
training_user p ON u.id = p.user_id
GROUP BY
u.id
Now that you have a couple of ways to count the number of relations, you can use whatever method you like to store that value somewhere. Just remember that if you store that number as a value in the user table you'll need to update it every time a user creates/updates/deletes a training (and vice versa!).

Laravel count related data

I have in my database:
Campaigns
hasMany Tasks
hasMany Links
hasMany LinkClicks
If I query
Campaign::find(1)->task->count();
It returns the correct number of tasks asigned to that campaign.
But how can I count link clicks from Campaign id?
Campaign Model:
public function task(){
return $this->hasMany('Task','campaign_id');
}
Task Model:
public function campaign(){
return $this->belongsTo('Campaign','campaign_id');
}
public function links(){
return $this->hasMany('Link','task_id');
}
Link Model:
public function task(){
return $this->belongsTo('Task','task_id');
}
public function clicks(){
return $this->hasMany('LinkClick', 'link_id');
}
public function user(){
return $this->belongsTo('User','user_id');
}
LinkClick Model:
public function link(){
return $this->belongsTo('Link','link_id');
}
So, I want to count linkClicks only for links under specific Campaign, no matter which Task or Link, ofcourse that task and link needs to be under specified Campaign.
Use hasManyThrough for the links:
// Campaign model
public function links()
{
return $this->hasManyThrough('Link', 'Task');
}
// then you can fetch the count either querying db:
$campaign->links()->count(); // SELECT COUNT(*) ...
// returns STRING '25'
// or from the collection of already loaded relation:
$campaign->links->count(); // counts collection items
// returns INT 25
This is not going to work further, for the LinkClicks, but there's easy way yuou can achieve that too:
// This is another way for the above:
$campaign = Campaign::with('tasks.links')->find($someId);
$campaign->tasks->fetch('links')->collapse()->count(); // INT 25
// and the LinkClicks:
$campaign = Campaign::with('links.clicks')->find($someId);
$campaign->links->fetch('clicks')->collapse()->count(); // INT 555
Since going down they are all 'hasMany' relations, you will need to iterate over all of the children at each level in order to generate the sum. Something like this ought to work for you:
$campaign = Campaign::find($campaignId);
$count = 0;
foreach($campaign->task as $task) {
foreach($task->link as $link) {
$count += $link->click->count();
}
}
echo 'Clicks for campaign id ' . $campaignId . ': ' . $count . "\n";
Or, you can skip all that and just issue a bare statement against the database:
$results = DB::select(
'SELECT \'a\' FROM campaign c
INNER JOIN task t ON t.campaign_id = c.id
INNER JOIN links l ON t.task_id = t.id
INNER JOIN link_clicks lc ON lc.link_id = l.id'
);
First idea is probably a better idea though.
Since you are using relationships; you can do something like this:
$count = 0;
$campaign_with_tasks = Campaign::with('tasks')->where('id','=',$campaign_id)->get();
foreach($campaign_with_tasks->tasks as $task){
$links = Links::where('task_id','=',$task->id)->get();
foreach($links as $link){
$count += LinkClicks::where('link_id','=',$link->id)->count();
}
}

Categories