How can I compare 2 columns in the same table but not in the same row with Laravel? - mysql

I want to display the values of columns with the same menu_id and the same menu_parentid. However, the array is empty when I execute in Postman. I want the values of columns with common menu_id and menu_parentid displayed in an array.
Controller
public function showMenu()
{
return [
'menus' => Menu::whereColumn('menu_parentid', 'menu_id')
->distinct()
->get()
->map(function ($item) {
return [
'menu_id' => $item->menu_id,
'menu_name' => $item->menu_name,
'menu_icon' => $item->menu_icon,
];
})
];
}
When I test on Postman, I get the following
{
"menus": []
}
Screenshot of database

I think you're going about this the wrong way. Instead of doing a query like that, you can make a relationship of the model with itself to get the $menu->children and then perform the array mapping the way you like. I don't want to write it out and guess how you like it to be displayed, so can you elaborate how you would like the array to be structured? Is it like this?:
"menus": [
[parent_id1, child_id1],
[parent_id1, child_id2],
[parent_id2, child_id3]
];
Or is it a different structure where the parent id's are all mixed with the children? I can only give you hints without you clarifying your structure.

You can you have to select a One to many relation in model.
public function childs()
{
return $this->hasMany(Menu::class, 'menu_parentid', 'menu_id');
}
Then, you can select items with this query,
Menu::with('childs')->distinct()->get();

Related

Laravel Eloquent Limit in Relation that has Sub Relation

I have hasmany relation from category to contents, and I want limitation 4 content for each category.
I would like to limit the result of relation contents that has sub relation to languages
My Code
Category::with(['contents.languages' => function($query){
$query->limit(4);
}])
->get();
But I see in log the limit works on languages relation, not contents, that I wanted is limit on contents
take() and limit() functions will not work with eager loading if you retrieve parent model more than one using get().
So you have to do another way,
$categories = Category::with('contents')->get();
After retrieving $categories, you can do foreach loop like below,
$contents = [];
foreach($categories as $category){
$category->limitedContents = $category->contents()->with('languages')->limit(4);
}
And by doing this you will get 4 contents per category in all categories with limitedContents.
Note: Here I used name as 'limitedContents' because you have already defined contents relationship.
This question is basically something akin to Get top n records for each group of grouped results
As far as I can see there's not much choice but to perform N+1 queries. You can achieve this by doing:
$categories = Category::get();
$categories->each(function ($category) {
$category->load([ 'contents' => function ($q) {
return $q->limit(4);
}, 'contents.languages']);
});
Can we do better? I doubt it doubtful, though I am open to ideas. While we can optimise this to send a less queries to the database, the database internally will still need to compute the N+1 queries.
This also works
foreach(Category::with('contents')->get() as $category)
{
foreach($category->contents->take(4) as $content)
{
$languages = $content->with('languages')->get();
foreach($languages as $language)
{
//your code
}
}
}
You should try this:
Category::with(['contents.languages'])->limit(4)->get();
You could set additional method in Category model and make a call with that one:
public function contentsTake4()
{
return $this->hasMany('App\Content')->limit(4);
}
Then you would call it in controller as :
Category::with('contentsTake4.products')->get();
Try this
Category::with([
'contents' => function($q) { $q->limit(4); },
'contents.languages'
])->get();

Yii2 Restful. How can i receive data from 3 tables

I want to receive data like this:
categories
----category1
----category2
topProducts
----product1
--------photo1
--------photo2
----product2
--------photo1
--------photo2
I need get all categories and top x products.
Each product has two photos.
How can i do this by using yii2 restful?
Thanks.
the query shold look something like this
Category::find()
->with(['subcategories','topProducts', 'topProducts.images'])
->all();
you can use joinWith if you absolutely want a single query
if you retrieve your data with an ActiveController, you need to specify extraFields to the Category model. (here's a rest-specific usage example - rest of the guide should prove usefull as well)
Category model:
public function extraFields() {
return ['subcategories', 'topProducts'];
}
// product relation
public function getTopProducts(){
return $this->hasMany(Product::className(), ['category_id' => 'id'])
// ->order()->where() // your criterias
->limit(10);
}
// subcategories
public function getChildren(){
return $this->hasMany(Category::className(), ['id' => 'parent_id']);
}
Product model:
public function extraFields() {
return ['iamges'];
}
public function getImages(){
return $this->hasMany(Image::className(), ['product_id' => 'id'])
}
ps. since you haven't posed any code or table structure, all relations in my example are based on standard naiming convention

yii2 ActiveRecord query to get a specific relation object

Lets say I have a product. That product can have multiple texts. In my query I only want to get a specific text. I dont want to get all the texts and then make a loop to ckeck. Is that possible? I tried a couple things:
$nameArticle = Product::find([
'id',
'pld.product_name as productName'
])
->from('product p')
->joinWith('productLanguageData.language')
->where(['p.id' => $value['product']])
->andWhere(['pl.iso_language' => 'es']) //query, please give me only spanish text.
->one();
This returns product with all the languages.
How can i make this query work?
public function getLanguage()
{
return $this->hasOne(ProductLanguage::className(), ['id' => 'language_id'])
->from(['pl' => ProductLanguage::tableName()]);
}

Yii2 Model search query

How can I add where condition to my Articles model so that slug(From category model) is equal to $slug?
And this is a function that Gii generated:
public function getCategory()
{
return $this->hasOne(Categories::className(), ['id' => 'category_id']);
}
Here's my code:
public function specificItems($slug)
{
$query = Articles::find()->with('category');
$countQuery = clone $query;
$pages = new Pagination(['totalCount' => $countQuery->count(),'pageSize' => 12]);
$articles = $query->offset($pages->offset)
->limit($pages->limit)
->all();
return ['articles' => $articles,'pages' => $pages];
}
Your SQL query should contain columns from both article and category table. For that you need to use joinWith().
$result = Articles::find()
->joinWith('category')
->andWhere(['category.slug' => $slug])
->all();
Where 'category' is then name of your category table.
However, in your code you deviate from certain best practices. I would recommend the following:
Have both table name and model class in singular (Article and article). A relation can be in plural, like getCategories if an article has multiple categories.
Avoid functions that return result sets. Better return ActiveQuery class. If you have a query object, all you need to get the actual models is ->all(). However, you can further manipulate this object, add more conditions, change result format (->asArray()) and other useful stuff. Returning array of results does not allow that.
Consider extending ActiveQuery class into ArticleQuery and implementing conditions there. You'll then be able to do things like Article::find()->joinWith('category')->byCategorySlug('foo')->all().

Can indexby be used on associations directly in the query?

I have a number of situations where I need to cross-reference various records by ID, and find it's easiest to do so when the array is indexed by that ID. For example, Divisions hasMany Teams, Divisions hasMany Games, and Games belongTo HomeTeam and AwayTeam. When I want to read all of the teams and games in a division, I do something like this:
$division = $this->Divisions->get($id, [
'contain' => ['Teams', 'Games']
]);
I don't do
$division = $this->Divisions->get($id, [
'contain' => ['Teams', 'Games' => ['HomeTeam', 'AwayTeam']]
]);
because it seems that would increase memory requirements, especially when I'm further containing other models (People, etc.) in the Teams. So, instead I do
$division->teams = collection($division->teams)->indexBy('id')->toArray();
after the get to reindex that array, and then when I'm iterating through $division->games, to get the home team record I use $division->teams[$game->home_team_id]. This is all well and good (except that it sets the teams property as being dirty, a minor inconvenience).
But it seems that the queryBuilder functionality of the ORM is pretty magical, and I know that I can do
$teams = $this->Divisions->Teams->find()
->where(['division_id' => $id])
->indexBy('id')
->toArray();
to get an array of teams indexed how I want, so I'm wondering if there's some way to include indexBy on the associations. I tried
$division = $this->Divisions->get($id, [
'contain' => [
'Teams' => [
'queryBuilder' => function (Query $q) {
return $q->indexBy('id');
},
],
'Games',
]
]);
but, unsurprisingly, this didn't work. Any ideas?
Just for the record, guess you know this already, indexBy() doesn't belong to the query, but to the result set, so being able to call it requires the query to be executed first. It's not possible to use this for an association query builder, as it must return a query, not a result set.
While it would be possible to use result formatters for the associations and modify the result set accordingly, the problem is that the result set will hold all team results for all divisions, and when the team entities are being distributed on the various division entities that they belong to, the arrays will be "reindexed", respectively, the arrays will be populated without respect to the indices of the result set, so long story short, that won't work.
Global result formatter
However, a result formatter for the main query should work fine, and as you probably already figured, you can simply reset the dirty state afterwards in case it causes any problems, something like
$division = $this->Divisions
->find()
->contain([
'Teams'
])
->where([
'Divisions.id' => $id
])
->formatResults(function($results) {
/* #var $results \Cake\Datasource\ResultSetInterface|\Cake\Collection\CollectionInterface */
return $results
->map(function ($row) {
if (isset($row['teams'])) {
$row['teams'] = collection($row['teams'])->indexBy('id')->toArray();
}
if ($row instanceof EntityInterface) {
$row->dirty('teams', false);
}
return $row;
});
})
->firstOrFail();
Custom association and association specific result formatters
Another option would be to use a custom association class, which overrides ExternalAssociationTrait::_buildResultMap(), so that it respects the indices of the result set, as this is where the problem starts.
By default the associated entities are fetched from the result set and appended to a new array, which is later assigned to the respective association property on the entity the results belong to. So this is where the the keys from the possible custom indexed result set are being lost.
Here's an example, the change is really small, but I'm not sure about possible side effects!
src/Model/Association/IndexAwareHasMany.php
namespace App\Model\Association;
use Cake\ORM\Association\HasMany;
class IndexAwareHasMany extends HasMany
{
protected function _buildResultMap($fetchQuery, $options)
{
$resultMap = [];
$key = (array)$options['foreignKey'];
// grab the index here
foreach ($fetchQuery->all() as $index => $result) {
$values = [];
foreach ($key as $k) {
$values[] = $result[$k];
}
// and make use of it here
$resultMap[implode(';', $values)][$index] = $result;
}
return $resultMap;
}
}
Original: https://github.com/cakephp/...ORM/Association/ExternalAssociationTrait.php#L109
Now you must of course make use of the new association, to simplify it for this example, let's just override the table class' default hasMany() method
public function hasMany($associated, array $options = [])
{
$options += ['sourceTable' => $this];
$association = new \App\Model\Association\IndexAwareHasMany($associated, $options);
return $this->_associations->add($association->name(), $association);
}
And now, finally, you could use a result formatter for the association:
$division = $this->Divisions->get($id, [
'contain' => [
'Teams' => [
'queryBuilder' => function (Query $query) {
return $query
->formatResults(function($results) {
/* #var $results \Cake\Datasource\ResultSetInterface|\Cake\Collection\CollectionInterface */
return $results->indexBy('id');
});
}
],
'Games',
]
]);