Problems when using join and pagination together in Laravel - php

I have some tables in my database and I'm trying to transact between them. My aim is to show the project name and the employees in that project in the form of cards on my projects page as in the image below. (Other than the method I mentioned, if there are better ways to do this, I ask you to share.)
When trying to do this, the first thing that came to my mind was to use leftJoin. Together with the projects, I thought of pulling the users in that project together and showing them by using foreach nested.
If I just want to shoot projects, there will be no problem and the first 4 projects come and everything works fine. But since I needed to attract the users in that project along with my projects, when I used leftJoin, I got an output as you can see below. I had to use pagination as I needed to show 4 projects per page, and when I used pagination, the lines that came in were inconsistent. While waiting for 4 projects and their employees to arrive, the first 4 lines came. (3 employees of 1 project and the first employee of the other project have arrived.)
My code:
$query = Project::query();
$query->select('prj_name', 'us_name');
$query->where('up_type_id', 3);
$query->leftJoin('user_positions', 'projects.prj_id', '=', 'user_positions.up_source_id');
$query->leftJoin('users', 'user_positions.up_user_id', '=', 'users.us_id');
$data['projectList'] = $query->paginate(4)->toArray();
return view('pages.projects.index', $data);
Output:
[data] => Array
(
[0] => Array
(
[prj_name] => Project Name - 2
[us_name] => Tracey Dyer
)
[1] => Array
(
[prj_name] => Project Name - 2
[us_name] => Joseph Dickens
)
[2] => Array
(
[prj_name] => Project Name - 2
[us_name] => Steven Ogden
)
[3] => Array
(
[prj_name] => Project Name - 3
[us_name] => Warren Nash
)
)
My tables:
Additional info :
It is not directly related to my question, but I would like to state it for informational purposes. My table named "user_position_types" represents fields within the company. The "up_type_id" column in the "user_positions" table specifies the partition type (such as Department, Project).
If this column is 1, it means department and "source_id" indicates which department it is. If it is 3, it means project and again "source_id" indicates which project it is.
I would like to thank everyone who answered in advance.

You may consider using Eloquent relationship instead of left join. In fact left join wouldn't work that well in your case since you will have to group all users in the same project from the rows afterwards. Example of Eloquent relationship using whereHas and with as follows.
$data['projectList'] = Project::select('prj_name', 'us_name')
->whereHas('users, function($query) {
$query->where('users_positions.up_type_id', 3);
})
->with('users')
->paginate(4);
The whereHas part filters only projects with users relationship and has pivot field up_type_id 3.
And with part will load users corresponding to the project.
You will also need to add a users relationship in Project model.
public function users()
{
return $this->belongsToMany(User::class, 'user_positions', 'prj_id', 'us_id')
->withPivot('up_type_id');
}
With this you can show your projects and users like so.
foreach($data['projectList'] as $project) {
$project->prj_name;
foreach($project->users as $user) {
$user->us_name;
}
}
In case you would like to have either projects with no users and projects with users from up_type_id = 3, you may use where closure and filter from both conditions (use orWhereHas).
$data['projectList'] = Project::select('prj_name', 'us_name')
->where(function ($query) {
$query->doesntHave('users')
->orWhereHas('users, function($query) {
$query->where('user_positions.up_type_id', 3);
});
})
->with('users')
->paginate(4);

Related

Having a Laravel query with a 'join' and a 'where' in order to get a recipe based on allergen constraints?

I'm experimenting with Laravel 8.0x Eloquent and Query builder in order to generate a meal plan with recipes chosen by user input constraints. For example, the user inputs they are looking for Dinner (meal_type), Vegetarian (suitable_for), 6 people (feeds), for a meal plan of 2 days (days). The recipe ID is then pulled from the Database based on these constraints.
The input form is all drop down, except for allergens is which a checkbox (as users may have multiple allergens).
When it comes to allergens though, one recipe can have multiple allergens. There are two tables, Recipe and Recipe_Allergens. Recipe_Allergens has fields id, Recipe_ID, and Allergen_Description. The checkbox request requires an implode:
$request->merge([ 'allergens' => implode(',', (array) $request->get('allergens')) ]);
So if two allergens, 'Dairy' and 'Gluten' are checked, the result is saved as 'Dairy,Gluten'. However, I want it to loop through each individual allergen to check if the Recipe contains any of these allergens, and avoid selecting that Recipe_ID if it does.
The user inputs:
$meal = $request->meal_type;
$suited = $request->suitable_for;
$allerg = $request->allergens;
$feeds = $request->no_of_people;
$days = $request->no_of_days;
$allergenarray2 = explode(",", $allerg);
The Eloquent 'join' by the Recipe id and 'where' in order to get the Recipe_ID:
$recipenew = Recipe::join('recipe_allergens', 'recipe_allergens.recipe_id', '=', 'recipe.id')->where('recipe.suitable_for', $suited)->where('recipe_allergens.allergen_description', '!=', $allergenarray2)->where('recipe.feeds_total', $feeds)->get();
Inputting into DB table:
while ($x < $days){
$recidnew = $recipenew[$x]->id;
MealPlanDisplay::create([
'Recipe_ID' => $recipenew[$x],
'Day' => $recipeday,
'user_id' => $currentuserid,
]);
$x = $x + 1;
}
All of the constraints work except for the allergens. Does this sort of join work? It won't seem to accept the exploded array either, and only recognises the first value in it (for example Dairy,Gluten is only recognised as 'Dairy'). What can I do?
use whereNotIn() function instead of where condition and pass the array in the parameter along with the column name like this
$recipenew = Recipe::join('recipe_allergens', 'recipe_allergens.recipe_id', '=', 'recipe.id')->where('recipe.suitable_for', $suited)->whereNotIn('recipe_allergens.allergen_description', $allergenarray2)->where('recipe.feeds_total', $feeds)->get();

Laravel - how to group data by key and save to array?

I have table attribute_values(id, value, attr_group_id).
I need to return the collection grouped by key attr_group_id.
in clear php using ORM RedBean i made:
$data = \DB::table('attribute_values')->get();
$attrs = [];
foreach ($data as $k => $v){
$attrs [$v['attr_group_id']][$k] = $v['value'];
}
return $attrs;
I need same using Laravel, after this one:
$data = \DB::table('attribute_values')->get();
My table
id value attr_group_id
1 one 1
2 two 1
3 three 2
4 four 2
5 five 3
6 six 3
And i need result
Array(
[1] => Array
(
[1] => one
[2] => two
)
[2] => Array
(
[3] => three
[4] => four
)
[3] => Array
(
[5] => five
[6] => six
)
)
Fetch all data, and map it with attribute id of every row will work,
$data = \DB::table('attribute_values')->get();
$attrs = [];
foreach ($data as $key => $value) {
// -> as it return std object
$attrs[$value->attr_group_id][] = $value->value;
}
dd($attrs);
You can use the groupBy() function of collection as:
$data = \DB::table('attribute_values')->get()->groupBy('attr_group_id');
It merges records with same attr_group_id under this field's value as making key of the collection.
Doing all this in raw SQL will be more efficient, SQL database are quite good at these operations. SQL has a group by function, since you are overwriting value, i just get it out with max() (this seems weird, that you overwrite the value, do you actually just want unique results?).
DB::table('attribute_values')
->select('attr_group_id', DB::raw('max(value)'))
->groupBy('attr_group_id')
->get();
EDIT
Since the scope has changed, you can utilize Laravels Collection methods, that is opreations on a Collection.
DB::table('attribute_values')
->get()
->groupBy('attr_group_id')
->toArray();
Friends, this is a ready task that I needed !
I did it myself and you helped me. If anyone interested can read.
I'll explain to you why I needed this particular method. I am doing an online store with a clock and now there was a task to make filters and attributes for filters.
So there are three tables
attribute_groups table
attribute_products table
attribute_values
I need to display the Laravel widget on my .blade.php like as
{{ Widget::run('filter', 'tpl' => 'widgets.filter', 'filter' => null,]) }}
When i creating a new product in the admin panel.
I must to save the product id and attribute_id in attribute_products, but there can be as many attributes as possible for one product. so, if I'll use this option
$data = \DB::table('attribute_values')
->get()
->groupBy('attr_group_id')
->toArray();
I got result:
But! each new array starts with index 0. But I need an index that means its id. attr_group_id from table attribute_value for saving into attribute_products.
And after I see only one method for me.
$data = \DB::table('attribute_values')->get();
$attrs = [];
foreach ($data as $key => $value) {
$attrs[$value->attr_group_id][$value->id] = $value->value;
}
return $attrs;
and the result I was looking for
now you can see what's the difference and what was needed. Array index starts 1,2,3,4,5 and this index = attr_group_id. Unfortunately I could not initially ask the right question. thanks to all.
Laravel Version 5.8
So You need to Group the id
if You need in the Model Way I have created the Model as AttributeValue
$modelWay = \App\AttributeValue::get()
->groupBy('attr_group_id');
if You need in the DBWay I have created the table as attribute_values
$dbWay = \DB::table('attribute_values')
->get()
->groupBy('attr_group_id');
Both Will give the Same Result

CakePHP 3 Find by Keyword group by Page

My current config of tables is as follows
PagesTable
$this->belongsToMany('Keywords', ['through' => 'PagesKeywords']);
PagesKeywordsTable
Schema: id, page_id, keyword_id, relevance
$this->belongsTo('Pages');
$this->belongsTo('Keywords');
KeywordsTable
$this->belongsToMany('Pages', ['through' => 'PagesKeywords']);
Now heres what i'm trying to do..
Find pages via keywords, using an array to be precise then order by PagesKeywords.relevance
(This is basically storing how many time that keyword is repeated per page, so no duplicate keywords in join table)
I've currently got this working fine except it groups the results of keywords by the keyword itself, where as I need them to be grouped by Pages.id
Here is what i have in my Pages controller, search action:
$keywords = explode(" ", $this->request->query['q']);
$query = $this->Pages->Keywords->find()
->where(['keyword IN' => $keywords])
->contain(['Pages' => [
'queryBuilder' => function ($q) {
return $q->order([
'PagesKeywords.relevance' =>'DESC'
])->group(['Pages.id']);
}
]]);
$pages = array();
foreach($query as $result) {
$pages[] = $result;
}
I know this seems like a backward way to do things but its the only way I seemed to be able to order by _joinTable (PagesKeywords.relevance)
This returns the results I need but now it needs to be grouped by Pages.id which is where this whole thing goes to pot..
Just to be clear the structure I want is:
Page data 1
Page data 2
Page data 3
Page data 4
Where as its currently returning:
Keyword "google"
------- Page data 1
------- Page data 2
------- Page data 3
------- Page data 4
Keyword "something"
------- Page data 1
------- Page data 2
------- Page data 3
------- Page data 4
If you are able to help me thats great!
Thanks
If you're having issues with complex queries with an ORM, I find it always easier to figure out the SQL I need to get the results I require, then adapt that to the ORM.
The query you're looking for would be like this (Using MySQL engine... MySQL Handles field selects more liberally in GROUP BY clauses than other SQL engines )
SELECT Pages.*, COUNT(DISTINCT(PagesKeywords.keyword_id)) AS KeywordCount
FROM pages Pages
INNER JOIN pages_keywords PagesKeywords ON (PagesKeywords.page_id = Pages.id)
INNER JOIN keywords Keywords ON (Keywords.id = PagesKeywords.keyword_id)
WHERE Keywords.name IN ('keyword1','keyword2')
GROUP BY Pages.id
This will give you all pages that contain the keyword and KeywordCount will contain the number of distinct attached Keyword.id's
So a finder method for this would look like ( going ad-hoc here so my syntax might be shaky )
** Inside your PagesTable model **
public function findPageKeywordRank(Query $q, array $options) {
$q->select(['Pages.*','KeywordCount'=>$q->func()->count('DISTINCT(PagesKeywords.keyword_id)'])
->join([
'PagesKeywords'=>[
'table'=>'pages_keywords',
'type'=>'inner',
'conditions'=>['PagesKeywords.page_id = Pages.id']
],
'Keywords'=>[
'table'=>'keywords',
'type'=>'inner',
'conditions'=>['Keywords.id = PagesKeywords.keyword_id']
]
])
->group(['Pages.id']);
return $q;
}
Then you can all your finder query
$pages = TableRegistry::get("Pages")->find('PageKeywordRank')
->where(['Keywords.name'=>['keyword1','keyword2']]);

HABTM with Pagination

At first take a look at the following model structure:
Model Building:
id
name
Model BuildingRange:
id
building_id
postalcode
Ok, so BuildingRange $belongsTo Building and Building $hasMany BuildingRange. Should be clear til' here.
Now let
$current_postalcode="12345";
I know want to do something like this in the BuildingController:
$this->paginate('Building',array('Building.BuildingRange.postalcode'=>$current_postalcode));
In text: I want to select all buildings for that an entry "BuildingRange" with $current_postalcode exists. How do you do that?
I appreciate your help!
When dealing with such a hasMany association, CakePHPs auto-magic needs two queries, one on the Building table, and one on the BuildingRange table. When passing conditions via the pagiante method, these conditions will be passed to the first query, and thus this it will fail since the associated models table isn't joined.
This problem can be solved on a few different ways, one would be using an ad-hoc join, for example:
$this->paginate = array
(
'joins' => array
(
array
(
'table' => 'building_ranges',
'alias' => 'BuildingRange',
'type' => 'LEFT',
'conditions' => array('BuildingRange.building_id = Building.id')
)
)
);
$this->paginate('Building', array('BuildingRange.postalcode' => $current_postalcode));
This would result in a query that looks something like this:
SELECT `Building`.`id`,
`Building`.`name`
FROM `buildings` AS `Building`
LEFT JOIN `building_ranges` AS `BuildingRange`
ON ( `BuildingRange`.`building_id` = `Building`.`id` )
WHERE `BuildingRange`.`postalcode` = '12345'
LIMIT 20
Note that in the conditions passed to the paginate method there is no need to reference the BuildingRange model through the Building model, ie no need to use Builduing.BuildingRange (that wouldn't work anyway).
ps, it's always good to mention the CakePHP version you are using!

Complex Zend Query from same user table

I have a rather unique set of conditions and orders in which I need to retrieve data from a "sellers" table for an application I'm building in Zend framework.
The client is basically requesting an application where the directory page lists sellers in a very particular order, which is:
Sellers who have been approved in the last 7 days (then order by #4 below)
Then, selllers who have paid for upgraded features on the site, and are more the 7 days old (then order by #4 below)
Then, Sellers who are more than 7 days old and are more than 7 days old (then order by #4 below)
For all of the above, secondary order by would be their launch date, then alpha by business name
I'm trying to figure out the most effective way to write an action helper that will return the data in the correct sequence above, knowing that some of my views only need 1,2 (and 4), whereas other views within the application will need all 4.
Right now, I've been writing two or three separate queries, and passing them to 2 or 3 partialloop's inside the view, but I strive for properly written code, and would like to either combine my 3 queries into one object I can pass to one partial loop, or.... write one query. How can this be done?
Here's my helper at the moment:
class Plugin_Controller_Action_Helper_ListSellers extends Zend_Controller_Action_Helper_Abstract
{
//put your code here
public function direct($regulars = false, $filter = false)
{
$dateMod = $this->dateMod = new DateTime();
$dateMod->modify('-7 days');
$formattedDate = $dateMod->format('Y-m-d H:i:s');
// get sellers initialized in last 7 days
$sellerTable = new Application_Model_DbTable_Seller();
// get sellers initialized in last 7 days
$select = $sellerTable->select()->setIntegrityCheck(false);
$select->from(array('b' => 'seller'),array('sellerID', 'businessName','sellerPicture'));
// select firstName, lastName, picture from user table, and businessName and sellerID from seller table. All records from seller table
$select->join(array('u' => 'user'), 's.userID = u.userID', array('firstName', 'lastName'));
$select->order('s.launchDate DESC','s.businessName ASC');
$select->where('s.active = 1 AND s.contentApproval = 1 AND s.paymentApproval = 1');
$select->where('s.launchDate > ?', $formattedDate);
if($filter){ $select->where('s.categoryID = ?', $filter);}
$newSellers = $sellerTable->fetchAll($select);
$query = $sellerTable->select()->setIntegrityCheck(false);
$query->from(array('b' => 'seller'),array('sellerID', 'businessName','sellerPicture'));
// select firstName, lastName, picture from user table, and businessName and sellerID from seller table. All records from seller table
$query->join(array('u' => 'user'), 's.userID = u.userID', array('firstName', 'lastName'));
$query->order('s.launchDate DESC','s.businessName ASC');
$query->where('s.active = 1 AND s.contentApproval = 1 AND s.paymentApproval = 1 AND s.featured = 1');
$query->where('s.launchDate < ?', $formattedDate);
if($filter){ $select->where('s.categoryID = ?', $filter);}
$featuredSellers = $sellerTable->fetchAll($query);
if($regulars){
$where = $sellerTable->select()->setIntegrityCheck(false);
$where->from(array('b' => 'seller'),array('sellerID', 'businessName','sellerPicture'));
// select firstName, lastName, picture from user table, and businessName and sellerID from seller table. All records from seller table
$where->join(array('u' => 'user'), 's.userID = u.userID', array('firstName', 'lastName'));
$where->order('s.launchDate DESC','s.businessName ASC');
$where->where('s.active = 1 AND s.contentApproval = 1 AND s.paymentApproval = 1 AND s.featured IS NULL');
$where->where('s.launchDate < ?', $formattedDate);
$regularSellers = $sellerTable->fetchAll($where);
}
}
}
I don't see any limits being applied to your queries. So does that mean you really want to select all matching records? For scalability reasons I'd guess that the answer should be no, there will be limits applied. In this case, you may just have to do 3 different queries.
But if there are no limits to be applied, then you could do a single simple query that selects all sellers, unfiltered and unsorted, and do your sorting and filtering in view helpers or just in your views.
Regardless, I recommend not putting database queries inside your controller layer, assuming you want to use the Model-View-Controller pattern which Zend is built for. Controllers should be thin. Your models should handle all database queries and just spit out the results into your controllers. I use the Data Mapper pattern extensively. Something like:
$mapper = new Application_Model_SellerMapper();
$newSellers = $mapper->fetchNewSellers();
$featuredSellers = $mapper->fetchFeaturedSellers();
$regularSellers = $mapper->fetchRegularSellers();
Each of your fetchX() methods would return an array of Application_Model_Seller instances, rather than Zend_Db_Table_Row instances.
This way you maintain Separation of Concerns and Single Responsibility Principle better, for more maintainable code. Even if you're the only developer on the project over the long-term, 6 months from now you won't remember what you wrote and why. And if someone else comes on the project, clarity becomes really important.

Categories