REST extra fields and join in Yii2 - php

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;

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;

How to join table with req->id

public function getTourDetail(Request $req)
{
//Get link detail
$tour = Tour::where('id',$req->id)->first();
//I want to take location.city of the location table
$detail = Tour::join('location','tour.id_location','=','location.id')
->whereColumn([
['tour.id_location','=','location.id']
])
->get(array(
'tour.id as id_tour',
'location.image',
'tour.name',
'tour.id_location',
'location.city'
));
return view('page.tour-detail',compact('tour','detail'));
}
I would like to be able to combine two query statements to get information from the location table ($ detail) like the id of the link request ($ tour).
Since you use models, you can use Eloquent relationships to load related data. First, define a relationship in the Tour model:
public function location()
{
return $this->belongsTo(Location::class, 'id_location')
}
Then load Tour and get related location:
$tour = Tour::find($req->id);
$relatedLocation = $tour->location;
First thing, if you are using model then using eloquent relationship will be a better idea to deal with the situation like yours. But if you want to join your table then this will be the way:
public function getTourDetail($id)
{
$tour = Tour::where('id',$id)->first();
//I want to take location.city of the location table
$detail = DB::table('location')
->join('tour','tour.id_location','=','location.id')
->select(
'tour.id as id_tour',
'location.image',
'tour.name',
'tour.id_location',
'location.city'
)->get();
return view('page.tour-detail',compact('tour','detail'));
}
Note: if you are getting id from submitted form then replace first portion of the code with:-
public function getTourDetail(Request $request)
{
$tour = Tour::where('id',$request->id)->first();

Yii2 ActiveDataprovider get the related fields data

I have setup my dataprovider with left join as
$trucks = TblTrucks::find()
->leftJoin("tbl_checks", "tbl_checks.truck_id=tbl_trucks.id")
->Where(["town"=>$town])
->andWhere(["truck_status"=>6])
->andWhere(["between","tbl_trucks.created_at", $ts1, $ts2 ]);
$provider = new ActiveDataProvider([
'query' => $trucks,
'pagination' =>false,
]);
Now am getting thje data via
return $provider->getModels();
This returns only tbl_trucks data how can i still get the data in the tbl_checks
To get related field in model first of all your must have relation model TblChecks define relation in model TblTrucks acording to documentation
In you case it will be some like that:
public function getTblChecks()
{
return $this->hasOne(TblChecks::className(), ['truck_id' => 'id']);
//or, depending on the type of relation (on-to-one or one-to-maty)
return $this->hasMany(TblChecks::className(), ['truck_id' => 'id']);
}
Than use method joinWith:
$trucks = TblTrucks::find()
->joinWith(['tblChecks'])
->Where(["town"=>$town])
->andWhere(["truck_status"=>6])
->andWhere(["between","tbl_trucks.created_at", $ts1, $ts2 ]);
And then you can get relation field just call it:
$models = $provider->getModels();
$models[index]->tblChecks->needed_field
But if you need just array of modesl acording to your query you dont have to use ActiveDataProvider just call method all()
For example:
$trucks = TblTrucks::find()
->joinWith(['tblChecks'])
->Where(["town"=>$town])
->andWhere(["truck_status"=>6])
->andWhere(["between","tbl_trucks.created_at", $ts1, $ts2 ])->all();
And in var $trucks you will have array of models TblTrucks with relation models (TblChecks in this case) acording to your query.

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(), []);
}

use 2 queries on one model or 1 query and make it toarray method to get other query

I need to get an associative array for a select input just like the following code below.
public function create() {
// queries the clients db table, orders by client_name and lists client_name and id
$client_optons = DB::table('clients')->orderBy('client_name', 'asc')->lists('client_name','id');
return View::make('projects.create', array('client_options' => $client_options));
}
However I will also need to get the entire model $clients.
public function create() {
$clients=Clients::all();
// queries the clients db table, orders by client_name and lists client_name and id
$client_optons = DB::table('clients')->orderBy('client_name', 'asc')->lists('client_name','id');
return View::make('projects.create', array('client_options' => $client_options));
}
Since I am already getting the entire model in my query, my question is should I use the 2 queries like shown above or is that bad performance/coding? Should I use 1 query and then play with the model to get $client options? (Like shown below) And do I do this with a loop or are there array functions that do it more succinctly?
public function create() {
$clients=Clients::all();
$clients_array = $clients->toArray();
$client_options = /*some code to create array('client_name'=>'id') */
return View::make('projects.create', array('client_options' => $client_options));
}
Fortunately the lists() function is also available on collections:
$clients = Clients::orderBy('client_name', 'asc')->get();
$client_options = $clients->lists('client_name', 'id');
return View::make('projects.create', array(
'client_options' => $client_options,
'clients' => $clients
));

Categories