Nested queries using symfony findBy method - php

I have two entities Category and Item. I want to access all items under a particular category.
Presently, I'm doing this as follows:
Get entity corresponding to given category
Get all Items by passing category selected in previous step as a parameter to findBy method.
Here is my code:
public function indexAction($category)
{
$em = $this->getDoctrine()->getManager();
$category = $em -> getRepository('AppBundle:Category')
-> findOneBy(array(
"name" => $category
));
$entities = $em->getRepository('AppBundle:Item')
->findBy(array(
'category' => $category
));
return array(
'entities' => $entities,
'title' => $category
);
}
Am I doing right? In this case I need two separate queries. Is there any efficient method?

Does your Category entity have a OneToMany relationship with Item (http://symfony.com/doc/current/book/doctrine.html#entity-relationships-associations)?
If so, you can set up a join and use that to get all the Items corresponding to a Category by defining a new method in the Category entity class. Something like:
public function findOneByNameJoinedToItems($category)
{
$query = $this->getEntityManager()
->createQuery(
'SELECT c, i FROM AppBundle:Category c
JOIN c.item i
WHERE c.name = :name'
)->setParameter('name', $category);
try {
return $query->getSingleResult();
} catch (\Doctrine\ORM\NoResultException $e) {
return null;
}
}
See here for more: http://symfony.com/doc/current/book/doctrine.html#joining-related-records

Related

Laravel relationship conflicts in union

I have following model:
1- User model
/**
* Define user and functional area relationship
*/
public function functionalAreas()
{
return $this->belongsToMany('App\FunctionalArea', 'user_functional_areas', 'user_id', 'functional_area_id')->withPivot('id', 'is_primary')->withTimestamps();
}
and Business model:
/**
* Define business and user functional area relationship
*/
public function functionalAreas()
{
return $this->belongsToMany('App\FunctionalArea', 'business_functional_areas', 'business_id', 'functional_area_id')->withTimestamps();
}
Now I should take all businesses and users and show them in a single list, for this I'm using from union, following is my query:
public function usersAndOrganizations()
{
$users = $this->users();
$organizations = $this->organizations();
$invitees = $users->union($organizations)->paginate(10);
return response()->json($invitees);
}
private function users()
{
$users = User::byState($approved = true, 'is_approved')
->search()->select([
'id',
DB::raw("CONCAT(first_name, ' ', last_name) AS name"),
'about',
'address',
'slug',
'average_reviews',
DB::raw("'freelancer' AS type")
]);
$users = $users->with([
"functionalAreas" => function ($q) {
$q->select([
'functional_areas.id',
DB::raw("functional_areas.name_en AS name"),
]);
}
]);
return $users;
}
private function organizations()
{
$businesses = Business::where('owner_id', '!=', auth()->user()->id)->verified()
->active()->search()
->select([
'id',
'name',
'about',
'address',
'slug',
'average_reviews',
DB::raw("'business' AS type")
]);
$businesses = $businesses
->with([
"functionalAreas" => function ($q) {
$q->select([
'functional_areas.id',
DB::raw("functional_areas.name_en AS name"),
]);
}
]);
return $businesses;
}
But above query not return the business functional area, its output query use from user relationship instead of business, that with section generate twice the following query:
select
`functional_areas`.`id`,
functional_areas.name_en AS name,
`user_functional_areas`.`user_id` as `pivot_user_id`,
`user_functional_areas`.`functional_area_id` as `pivot_functional_area_id`,
`user_functional_areas`.`id` as `pivot_id`,
`user_functional_areas`.`is_primary` as `pivot_is_primary`,
`user_functional_areas`.`created_at` as `pivot_created_at`,
`user_functional_areas`.`updated_at` as `pivot_updated_at`
from `functional_areas`
inner join `user_functional_areas`
on `functional_areas`.`id` = `user_functional_areas`.`functional_area_id`
where `user_functional_areas`.`user_id` in (2, 6, 7)
But in fact 6, and 7 is business id not user only 2 is user id, one of this queries should use business_functional_areas instead of user_functional_areas.
One more thing found is, all items are inside App\User model in result, its like businesses are also as user object.
The only way for now is to use from map.
public function usersAndOrganizations()
{
$users = $this->users();
$organizations = $this->organizations();
$invitees = $users->union($organizations)->paginate(10);
$invitees = $this->getRelatedData($invitees);
return response()->json($invitees);
}
private function getRelatedData($invitees)
{
$invitees->map(function($object) use($functionalAreaName) {
if($object->type == 'business') {
$relationName = 'businesses';
$relationKey = 'business_id';
$attachableType = Business::MORPHABLE_TYPE;
}
if($object->type == 'freelancer') {
$relationName = 'users';
$relationKey = 'user_id';
$attachableType = User::MORPHABLE_TYPE;
}
$functionalAreas = FunctionalArea::whereHas($relationName, function($q) use ($object, $relationKey){
$q->where($relationKey, $object->id);
})->get([$functionalAreaName.' As name', 'id']);
$object->functional_areas = $functionalAreas->toArray();
});
return $invitees;
}
And remove with from your functions, and call this after you get the paginated result.
In simple words, for now you would not be able to achieve it using Eloquent Eager Loading with Unions. This is not supported yet in Laravel. One of such scenario for which they closed as a Non-Fix issue is Union with Eloquent fail....
Reason: While calling UNION function only the first model(user) is considered main model and model type of result set of other model(Business) passed as argument will be converted to the main one(USER) only and the main model relationship is called on all records(not the desired one).
Due to the above issue only relationship of user model is called on each record of result set. So even for business_id = 1, functional_area of user_id =1 are being fetched.
You can debug more about it from below file & function.
File:
<your_laravel_project>\vendor\laravel\framework\src\Illuminate\Database\Query\Builder.php
Function: get
Alternate Solution
You can fetch the both result set as it is and then merge them after data fetch using php.
public function usersAndOrganizations()
{
$users = $this->users()->get();
$organizations = $this->organizations()->get();
$invitees = $users->toBase()->merge($organizations->toBase())->toArray();
dd($invitees);
}
You can not concat incompatible queries with union.
See Unions.
Your users() method return eloquent builder for User entity.
And organizations() return builder for Business entity.
Thus, it is incorrect to select users and organizations in one query.
The correct query is like that:
SELECT City FROM Customers
UNION
SELECT City FROM Suppliers
ORDER BY City;

Call to undefined relationship on model laravel using scope

I'm trying to get 5 posts for each category so I did a little search and ends up here Getting n Posts per category
But I'm getting a weird Call to undefined relationship on model when using with scope but it all works fine If I don't use a scope. Here is the Category Model
//Relationship with posts
public function posts(){
return $this->hasMany('App\Post');
}
scopeNPerGroup
public function scopeNPerGroup($query, $group, $n = 10)
{
// queried table
$table = ($this->getTable());
// initialize MySQL variables inline
$query->from( \DB::raw("(SELECT #rank:=0, #group:=0) as vars, {$table}") );
// if no columns already selected, let's select *
if ( ! $query->getQuery()->columns)
{
$query->select("{$table}.*");
}
// make sure column aliases are unique
$groupAlias = 'group_'.md5(time());
$rankAlias = 'rank_'.md5(time());
// apply mysql variables
$query->addSelect(\DB::raw(
"#rank := IF(#group = {$group}, #rank+1, 1) as {$rankAlias}, #group := {$group} as {$groupAlias}"
));
// make sure first order clause is the group order
$query->getQuery()->orders = (array) $query->getQuery()->orders;
array_unshift($query->getQuery()->orders, ['column' => $group, 'direction' => 'asc']);
// prepare subquery
$subQuery = $query->toSql();
// prepare new main base Query\Builder
$newBase = $this->newQuery()
->from(\DB::raw("({$subQuery}) as {$table}"))
->mergeBindings($query->getQuery())
->where($rankAlias, '<=', $n)
->getQuery();
// replace underlying builder to get rid of previous clauses
$query->setQuery($newBase);
}
Calling Npergroup with relation
public function latestposts()
{
return $this->posts()->latest()->nPerGroup('category_id', 5);
}
Post Model Relationship
//Post belongs to Category
public function category(){
return $this->belongsTo('App\Category');
}
In my category controller I'm calling latestposts through
$categories = Category::with('latestposts')->get();
But I'm getting the error: Call to undefined relationship on model
What I want is:
Get the N number of posts per each category but I'm completely lost at this point. Any help would be appreciated
Reference:
Tweaking Eloquent relations – how to get N related models per parent ?
I am giving this answer based on your purpose that you want 5 posts per category.
So you have Category Model and Post Model.
And in Category Model you have relation with Post model like this
//Relationship with posts
public function posts(){
return $this->hasMany('App\Post');
}
And in Post Model you have relation with Category model like this
//Post belongs to Category
public function category(){
return $this->belongsTo('App\Category');
}
I show your question you have done SQL queries.
Instead of that, You can use two approaches
1) Give condition while eagar loading
$categories = Category::with(['posts' => function ($query) {
$query->orderBy('created_at', 'desc')->take(5);
}])->get();
Note: This approach will only work when you take only one result of parent child using first() method.
To get n number of posts per category Use this.
First, you can retrieve all categories with
$categories = Category::all();
Then you can use foreach loop and in all $category you have to give assign new attribute in it like here latestposts,
foreach ($categories as $category)
{
$category->latestposts = $category->posts()->orderBy('created_at','desc')->take(5)->get();
}
After this foreach loop you will get latest 5 posts in all categories.
Try this in your code and comment your queries and reviews.

Is there a proper way to cross join using yii2?

I'm doing a query for activedataprovider, which I'm going to use for the purpose of search. I need to use cross join. I used joinWith, but it complains about warehouse model not having relation with product. it was calling product. Are there any work around here so that it won't trigger the relation, since it's a cross join?
Other notes: Certain attributes like product_id, product.category, etc doesn't exist on the original model which is based on warehouse. Will it work on the fly if I just add public property/attribute variables or do I need a work around?
public function search($params)
{
$query = Warehouse::find()->joinWith('product')
->select(['product_id' => 'product.id','warehouse.warehouse', 'product.category', 'product.product', 'min_stock' => 'coalesce(critical.min_stock, -1)'])
->leftJoin('critical', 'critical.product_id = product.id AND critical.warehouse_id = warehouse.id')
->where(['warehouse.id' => $this->_id]);
$this->load($params);
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
if (!$this->validate()) {
// uncomment the following line if you do not want to return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}
$query->andFilterWhere(['category' => $this->category]);
$query->andFilterWhere(['like', 'product', $this->product]);
return $dataProvider;
}
For all the query that are difficult (or impossibile) to build using yii2 activeRecord or activeQuery functionalites you can use findBySql
$sql = "select
product.id as id
, warehouse.warehouse as warehouse
, product.category as category
, product.product as product
, coalesce(critical.min_stock, -1) as min_stock
from Warehouse
cross join product
left join critical on ritical.product_id = product.id AND critical.warehouse_id = warehouse.id
where warehouse.id' = " . $this->_id
$model = Warehouse::findBySql($sql );
Get Your Query record as array so you don't require to define relationship
public function search($params)
{
$query = Warehouse::find()->joinWith('product')
->select(['product_id' => 'product.id','warehouse.warehouse', 'product.category', 'product.product', 'min_stock' => 'coalesce(critical.min_stock, -1)'])
->leftJoin('critical', 'critical.product_id = product.id AND critical.warehouse_id = warehouse.id')
->where(['warehouse.id' => $this->_id]);
$query = Warehouse::find()
->select('product.id as id,warehouse.warehouse as warehouse, product.category as category, product.product as product, coalesce(critical.min_stock, -1) as min_stock ')
->leftJoin('product', 'warehouse.product_id = product.id')
->leftJoin('critical', 'critical.product_id = product.id')
->where(['warehouse.id' => $this->_id]);
->andWhere('critical.warehouse_id = warehouse.id')
->asArray();
$this->load($params);
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
if (!$this->validate()) {
// uncomment the following line if you do not want to return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}
$query->andFilterWhere(['category' => $this->category]);
$query->andFilterWhere(['like', 'product', $this->product]);
return $dataProvider;
}
Now you will get record as Array not as object.
Second way is, Define relation between Warehouse and Product in model using hasOne() or hasMany()
->joinWith('product', true,'cross join')
public function getProduct()
{
return $this->hasOne(Product::className(), []);
}

REST extra fields and join in Yii2

I have this schema in my database.
time_sheet
mission
position
order
One mission have many timesheets and one time sheet have just one mission.
One position have many missions and one mission have just one position.
One order have many positions and one position have just one order.
And in my class that extends from yii\rest\IndexAction I have this code (in the prepareDataProvider() method):
$query = $timeSheetModel->find()
->distinct()
->joinWith("mission")
->joinWith("mission.position")
->joinWith("mission.position.order")
->where("public.order.id = $id");
$results = new ActiveDataProvider([
'query' => $query,
]);
return $results;
So how to personalise my extra fields to get a json with the following structure:
timeSheet
mission
position
resource
order
To return specific fields, in your model for example you could override the fields method:
public function fields()
{
return [
'misson' => function($model) {
return $model->mission->id; // or anything else from the mission relation
},
... etc.
];
}
More info about the fields and extraFields methods is available in the yii2 doc - http://www.yiiframework.com/doc-2.0/guide-rest-resources.html#fields
Use ArrayDataProvider (http://www.yiiframework.com/doc-2.0/yii-data-arraydataprovider.html)
$query = new \yii\db\Query();
$query = $timeSheetModel->find()
->distinct()
->joinWith("mission")
->joinWith("mission.position")
->joinWith("mission.position.order")
->where("public.order.id = $id");
$results = new ArrayDataProvider([
'allModels' => $query->all(),
// Add pagination information if required
]);
$datas = $results->allModels;
foreach($datas as &$data) {
// Do your formatting here
}
$results->allModels = $datas;
return $results;

Doctrine2 w/ child entities in the same table

I have a database table that generally (because of a NDA) has the structure of:
Category:
id(int) parent_id(int) title(string) description(text)
Note that I cannot change the table's schema, so that's not a possible solution to my problem.
I need to be able to get all of these categories' info, including a list of all their children, but I'm not quite sure how to do it. I'm fairly certain that I'll need to use recursion, but I'm not sure if there's anything built into Doctrine that can aid in it, or if I need to write raw DQL or even SQL. I currently have the following in my Symfony controller:
$em = $this->get('doctrine')->getManager();
$categoryQuery = $em->getRepository('Acme\MyBundle\Entity\Category')->findBy(array(), array('sortPosition' => 'asc'));
$categories = array();
foreach ($categoryQuery as $category) {
$categories[] = array(
'id' => $category->getId(),
'parent' => $category->getParent(),
'title' => $category->getTitle(),
'description' => $category->getDescription(),
);
}
There's another wrinkle: I need to output this info in JSON.
Ultimately, I'm not sure
A. How to create a query that will get me both a category and all of its child info (which can have more children)
B. How to then output that info as JSON
Any ideas would be greatly appreciated
If you have the category <> category relation defined in doctrine:
$categoryQuery = $em->getRepository('Acme\MyBundle\Entity\Category')
->createQueryBuilder('c')
->leftJoin('c.subCategories sc') // assuming your category <> category relation is called "subCategories
->getQuery()
->getResult()
;
Then in each record of $categoryQuery you will have a Category and its children in ->subCategories (you dont really need the join there)
If you dont have a relation defined in doctrine, then:
$query = $em->createQuery(
'SELECT c
FROM AcmeMyBundle:Category c
LEFT JOIN AcmeMyBundle:Category sc on sc.parent_id = c.id
ORDER BY sortPosition ASC'
);
$categories = $query->getResult();
And return them as JSON
return new JSONResponse($categories);
A. Recursive selection is ... hard/impossible without a path.
See : MySQL parent children one query selection
B. You can use this in Symfony2 :
Symfony\Component\HttpFoundation\JsonResponse

Categories