Is it possible and what would be the best way to define a relation with a parameter in Yii2.
Situation is simple. I have table texts and texts_regional. texts_regional of course has foreign keys text_id and lang_id.
Gii generated a method to get all regional texts but I dont need that on the frontend. I just need in the current language.
Generated method is:
public function getTextsRegionals()
{
return $this->hasMany(TextRegional::className(), ['text_id' => 'id']);
}
Tried this but it's probably not right:
public function getReg($langId=null)
{
if($langId === null && Yii::$app->session->has('langId')) {
$langId = Yii::$app->session->get('langId');
}
return $this->hasOne(TextRegional::className(), ['text_id' => 'id', 'lang_id'=>$langId]);
}
I need data from both tables so I'd like to eager load this.
Is it just better to use separate method and manually construct the query?
Read in documentation that it's possible to do ->onCondition so wrote a method like this:
public function getReg($langId=1)
{
if(Yii::$app->session->has('langId')) {
$langId = Yii::$app->session->get('langId');
}
return $this->hasOne(TextRegional::className(), ['text_id' => 'id'])->onCondition(['lang_id' => $langId]);
}
$langId is set in main controller.
But I ended up using TextRegional model and joined with Text model to set condition.
Made a TextRegionalQuery class and added a new method:
public function byCode($code)
{
if(Yii::$app->session->has('langId')) {
$langId = Yii::$app->session->get('langId');
} else {
$langId = 1;
}
$this->joinWith('text0')
->andWhere("lang_id = '".$langId."'")
->andWhere("texts.code = '".$code."'");
return $this;
}
Using it like this:
$ft = TextRegional::find()->byCode("footer_text")->one();
Or
$news = TextRegional::find()->byType(2)->visible()->all();
/**
* relation with current LangContractTemplate
*/
public function getCurLangContractTemplate()
{
if(isset(Yii::$app->user->identity->u_lang) && !empty(Yii::$app->user->identity->u_lang))
$langId = Yii::$app->user->identity->u_lang;
else
$langId = \Yii::$app->language;
return $this->hasOne(LangContractTemplate::className(), ['lcont_cont_id' => 'cont_id'])->onCondition(['lcont_lang_id' => $langId]);
}
//------------------OR------------------
/**
* relation with language table
*/
public function getContractByLang()
{
return $this->hasOne(LangContractTemplate::className(), ['lcont_cont_id' => 'cont_id']);
}
/* and Get data */
$contract_content = ContractTemplate::find()
->joinWith(['contractByLang' => function($query) use ($lang) {
return $query->where(['lcont_lang_id' => $lang]);
}])
->one();
Related
In my Laravel-8 project, I have this controller for Input Field Array Update.
Controller:
public function update(UpdateSaleRequest $request, $id)
{
try {
$sale = Sale::find($id);
$data = $request->all();
$update['date'] = date('Y-m-d', strtotime($data['date']));
$update['company_id'] = $data['company_id'];
$update['name'] = $data['name'];
$update['remarks'] = $data['remarks'];
$sale->update($update);
SaleDetail::where('sale_id', $sale->id)->delete();
foreach ($data['invoiceItems'] as $item) {
$details = [
'sale_id' => $sale->id,
'item_id' => $item['item_id'],
'employee_id' => $item['employee_id'],
'quantity' => $item['qty'],
'price' => $item['cost'],
'total_price' => $item['cost'] * $item['qty'],
'sale_type_id'=>$item['sale_type_id']
];
$saleDetail = new SaleDetail($details );
$saleDetail->save();
}
} catch (JWTException $e) {
throw new HttpException(500);
}
return response()->json($sale);
}
In the form, the user can add more Sales Detail or remove.
Some of the SaleDetail fields are being used somewhere else.
Is there a way to update the input field array without deleting the SaleDetail as shown in what I did here:
SaleDetail::where('sale_id', $sale->id)->delete();
Thanks
I've tried to restructure your code so that's easier to edit. I've left some comments. I can really recommend refactoring.guru. There you will find many ways to improve your code so that it is more extensible, maintainable and testable. If you have any questions, please feel free to ask.
class Sale extends Model
{
// Use a relationship instead of building your own query
public function details() {
return $this->hasMany(SaleDetail::class);
}
}
class SaleDetail extends Model
{
// Use a computed property instead of manually calculating total price
// You can access it with $saleDetail->totalPrice
public function getTotalPriceAttribute() {
return $this->price * $this->quantity;
}
}
class UpdateSaleRequest extends Request
{
public function authorize() {
return true;
}
protected function prepareForValidation() {
$this->merge([
// Create a Carbon instance by string
'date' => Carbon::make($this->date)
]);
}
public function rules() {
// Your validation rules
// Please also validate your invoice items!
// See https://laravel.com/docs/8.x/validation#validating-arrays
}
}
// We let Laravel solve the sale by dependency injection
// You have to rename the variable name in ihr web.php
public function update(UpdateSaleRequest $request, Sale $sale)
{
// At this point, all inputs are validated!
// See https://laravel.com/docs/8.x/validation#creating-form-requests
$sale->update($request->validated());
// Please ensure, that all properties have the same name
// In your current implementation you have price = cost, be consistent!
foreach($request->input('invoiceItems') as $invoiceItem) {
// How we can consider that a detail is already created?
// I assume that each item_id will only occur once, otherwise you'll
// place the id of each detail in your update form (e.g. in a hidden input)
$candidate = $sale->details()
->where('item_id', $properties['item_id'])
->first();
if($candidate) {
$candidate->update($properties);
} else {
$sale->details()->create($properties);
}
}
// A JWT-Exception should not be necessary, since your authentication
// will be handled by a middleware.
return response()->json($sale);
}
I have not tested the code, few adjustments may be needed.
Laravel has a method called updateOrCreate as follow
/**
* Create or update a record matching the attributes, and fill it with values.
*
* #param array $attributes
* #param array $values
* #return \Illuminate\Database\Eloquent\Model|static
*/
public function updateOrCreate(array $attributes, array $values = [])
{
return tap($this->firstOrNew($attributes), function ($instance) use ($values) {
$instance->fill($values)->save();
});
}
That means you could do some thing like
public function update(UpdateSaleRequest $request, $id)
{
try {
$sale = Sale::find($id);
$data = $request->all();
$update['date'] = date('Y-m-d', strtotime($data['date']));
$update['company_id'] = $data['company_id'];
$update['name'] = $data['name'];
$update['remarks'] = $data['remarks'];
$sale->update($update);
foreach ($data['invoiceItems'] as $item) {
$details = [
'item_id' => $item['item_id'],
'employee_id' => $item['employee_id'],
'quantity' => $item['qty'],
'price' => $item['cost'],
'total_price' => $item['cost'] * $item['qty'],
'sale_type_id'=>$item['sale_type_id']
];
$sale->saleDetail()->updateOrCreate([
'sale_id' => $sale->id
], $details);
}
} catch (JWTException $e) {
throw new HttpException(500);
}
return response()->json($sale);
}
I would encourage you to refactor and clean up your code.You can also read more about it here https://laravel.com/docs/8.x/eloquent#upserts
Edit: I was able to see where the relations are being included in my response, but I still don't know why.
On my Customer model, I have:
protected $appends = [
'nps',
'left_feedback',
'full_name',
'url'
];
The accessors are as follows:
/**
* Accessor
*/
public function getNpsAttribute() {
if ($this->reviews->count() > 0) {
return $this->reviews->first()->nps;
} else {
return "n/a";
}
}
/**
* Accessor
*/
public function getLeftFeedbackAttribute() {
if ($this->reviews && $this->reviews->count() > 0 && $this->reviews->first()->feedback != null) {
return "Yes";
} else {
return "No";
}
}
/**
* Accessor
*/
public function getFullNameAttribute() {
return ucwords($this->first_name . ' ' . $this->last_name);
}
/**
* Accessor
*/
public function getUrlAttribute() {
$location = $this->location;
$company = $location->company;
$account_id = $company->account->id;
return route('customers.show', ['account_id' => $account_id, 'company' => $company, 'location' => $location, 'customer' => $this]);
}
So if I comment out the $appends property, I get the response I originally wanted with customer not returning all the relations in my response.
But I do want those appended fields on my Customer object. I don't understand why it would include all relations it's using in the response. I'm returning specific strings.
So is there a way to keep my $appends and not have all the relations it's using in the accessors from being included?
Original Question:
I am querying reviews which belongsTo a customer. I want to include the customer relation as part of the review, but I do not want to include the customer relations.
$reviews = $reviews->with(['customer' => function($query) {
$query->setEagerLoads([]);
$query->select('id', 'location_id', 'first_name', 'last_name');
}]);
$query->setEagerLoads([]); doesn't work in this case.
I've tried $query->without('location'); too, but it still gets included
And I should note I don't have the $with property on the model populated with anything.
Here is the Review model relation:
public function customer() {
return $this->belongsTo('App\Customer');
}
Here is the Customer model relation:
public function reviews() {
return $this->hasMany('App\Review');
}
// I dont want these to be included
public function location() {
return $this->belongsTo('App\Location');
}
public function reviewRequests() {
return $this->hasMany('App\ReviewRequest');
}
In the response, it will look something like:
'review' => [
'id'=> '1'
'customer => [
'somecol' => 'test',
'somecolagain' => 'test',
'relation' => [
'relation' => [
]
],
'relation' => [
'somecol' => 'sdffdssdf'
]
]
]
So a chain of relations ends up being loaded and I don't want them.
As you said in one comment on the main question, you are getting the relations due to the appended accessors.
Let me show you how it should be done (I am going to copy paste your code and simply edit some parts, but you can still copy paste my code and place it in yours and will work the same way but prevent adding the relations) and then let me explain why is this happening:
/**
* Accessor
*/
public function getNpsAttribute() {
if ($this->reviews()->count() > 0) {
return $this->reviews()->first()->nps;
} else {
return "n/a";
}
}
/**
* Accessor
*/
public function getLeftFeedbackAttribute() {
return $this->reviews()->count() > 0 &&
$this->reviews()->first()->feedback != null
? "Yes"
: "No";
}
/**
* Accessor
*/
public function getFullNameAttribute() {
return ucwords($this->first_name . ' ' . $this->last_name);
}
/**
* Accessor
*/
public function getUrlAttribute() {
$location = $this->location()->first();
$company = $location->company;
$account_id = $company->account->id;
return route('customers.show', ['account_id' => $account_id, 'company' => $company, 'location' => $location, 'customer' => $this]);
}
As you can see, I have changed any $this->relation to $this->relation()->first() or $this->relation->get().
If you access any Model's relation as $this->relation it will add it to the eager load (loaded) so it will really get the relation data and store it in the Model's data so next time you do $this->relation again it does not have to go to the DB and query again.
So, to prevent that, you have to access the relation as $this->relation(), that will return a query builder, then you can do ->count() or ->exists() or ->get() or ->first() or any other valid query builder method, but accessing the relation as query builder will prevent on getting the data and store it the model (I know doing ->get() or ->first() will get the data, but you are not directly getting it through the model, you are getting it through the query builder relation, that is different).
This way you will prevent on storing the data on the model, hence giving you problems.
You can also use API Resources, it is used to map a Model or Collection to a desired output.
One last thing, if you can use $this->relation()->exists() instead of $this->relation()->count() > 0 it will help on doing it faster, mostly any DB is faster on looking if data exists (count >= 1) than really counting all the entries it has, so it is faster + more performant on using exists.
Try :
$review->with(‘customer:id,location_id,first_name,last_name’)->get();
Or :
$review->withOnly(‘customer:id,location_id,first_name,last_name’)->get();
I am using OctoberCMS and I have created a custom component. I am trying to create a frontend filter to filter Packages by the Tour they are assigned to.
This is what I have so far. The issue is that the code is looking for a tour field within the packages table rather than using the tour relationship. Does anyone have any ideas?
<?php namespace Jakefeeley\Sghsportingevents\Components;
use Cms\Classes\ComponentBase;
use JakeFeeley\SghSportingEvents\Models\Package;
use Illuminate\Support\Facades\Input;
class FilterPackages extends ComponentBase
{
public function componentDetails()
{
return [
'name' => 'Filter Packages',
'description' => 'Displays filters for packages'
];
}
public function onRun() {
$this->packages = $this->filterPackages();
}
protected function filterPackages() {
$tour = Input::get('tour');
$query = Package::all();
if($tour){
$query = Package::where('tour', '=', $tour)->get();
}
return $query;
}
public $packages;
}
I really appreciate any help you can provide.
Try to query the relationship when the filter input is provided.
This is one way to do it;
public $packages;
protected $tourCode;
public function init()
{
$this->tourCode = trim(post('tour', '')); // or input()
$this->packages = $this->loadPackages();
}
private function loadPackages()
{
$query = PackagesModel::query();
// Run your query only when the input 'tour' is present.
// This assumes the 'tours' db table has a column named 'code'
$query->when(!empty($this->tourCode), function ($q){
return $q->whereHas('tour', function ($qq) {
$qq->whereCode($this->tourCode);
});
});
return $query->get();
}
If you need to support pagination, sorting and any additional filters you can just add their properties like above. e.g;
protected $sortOrder;
public function defineProperties(): array
{
return [
'sortOrder' => [
'title' => 'Sort by',
'type' => 'dropdown',
'default' => 'id asc',
'options' => [...], // allowed sorting options
],
];
}
public function init()
{
$filters = (array) post();
$this->tourCode = isset($filters['tour']) ? trim($filters['tour']) : '';
$this->sortOrder = isset($filters['sortOrder']) ? $filters['sortOrder'] : $this->property('sortOrder');
$this->packages = $this->loadPackages();
}
If you have a more complex situation like ajax filter forms or dynamic partials then you can organize it in a way to load the records on demand vs on every request.e.g;
public function onRun()
{
$this->packages = $this->loadPackages();
}
public function onFilter()
{
if (request()->ajax()) {
try {
return [
"#target-container" => $this->renderPartial("#packages",
[
'packages' => $this->loadPackages()
]
),
];
} catch (Exception $ex) {
throw $ex;
}
}
return false;
}
// call component-name::onFilter from your partials..
You are looking for the whereHas method. You can find about here in the docs. I am not sure what your input is getting. This will also return a collection and not singular record. Use ->first() instead of ->get() if you are only expecting one result.
$package = Package::whereHas('tour', function ($query) {
$query->where('id', $tour);
})->get();
so, I have 2 tables, stage and event. Stage hasMany event, and Event belongsTo Stage. And I want to show all stage and its event as json. Here is my code in controller:
public function getschedule(){
$schedule = Stage::all();
//$event = Event_schedule2020::all();
if (!$schedule) {
return response()->json(['msg'=>'Error not found','code'=>'404']);
}
foreach($schedule->events as $array){
$datax[] = [
'id'=>$array->id,
'time'=>$array->time,
'category'=>$array->category,
'type'=>$array->title,
'designer'=>$array->designer,
];
}
foreach ($schedule as $item) {
$jadwal[] = [
'id'=>$item->id,
'date'=>$item->date,
'place'=>$item->stage,
'data'=>$datax,
];
}
return response()->json($jadwal);
}
but I always get this error
the error
so, is there anything I can do about this?
You can utilize inbuilt functions to do what you want to. Laravel automatically transforms model into JSON, no need to built arrays with it.
public function getschedule() {
// tell laravel you want to eager load events
$stages = Stage::with('events')->get();
// laravel knows you loaded events and therefor you can just return it and it does the rest automatically
return response()->json($stages);
}
in your Stage model you have to create relationship like this
public function events()
{
return $this->hasMany('App\Event');
}
then in your Controller
public function getschedule(){
$schedules = Stage::with('events')->get()->toArray();
return response()->json($schedules );
}
your mistake is call events on a collection for solve this you can change foreach like followings :
public function getschedule(){
$schedules = Stage::all(); // I add a 's' to $schedule because is better set plural name;
if (!$schedule) {
return response()->json(['msg'=>'Error not found','code'=>'404']);
}
foreach($schedules as $schedule){
$datax = [];
foreach($schedule->events as $event){
$datax[] = [
'id'=>$event->id,
'time'=>$event->time,
'category'=>$event->category,
'type'=>$event->title,
'designer'=>$event->designer,
];
}
$jadwal[] = [
'id'=>$item->id,
'date'=>$item->date,
'place'=>$item->stage,
'data'=>$datax,
];
}
return response()->json($jadwal);
}
but above solution is not recommended because send many request to server in any foreach loop, following solution is better:
public function getschedule(){
$schedules = Stage::with('events')->get(); // only this difference with above soloution and the rest is the same
if (!$schedule) {
return response()->json(['msg'=>'Error not found','code'=>'404']);
}
foreach($schedules as $schedule){
$datax = [];
foreach($schedule->events as $event){
$datax[] = [
'id'=>$event->id,
'time'=>$event->time,
'category'=>$event->category,
'type'=>$event->title,
'designer'=>$event->designer,
];
}
$jadwal[] = [
'id'=>$item->id,
'date'=>$item->date,
'place'=>$item->stage,
'data'=>$datax,
];
}
return response()->json($jadwal);
}
I have a function named siblings which fetches all siblings of a user.
select siblings(id) as `siblings` from users where id = 1
I can access the function in Eloquent as
User::where('id', 1)->first([DB::raw(siblings(id) as `siblings`)]->siblings;
I want to make the siblings available via custom attribute.
I added siblings to $appends array
I also created getSiblingsAttribute method in my User model as
public function getSiblingsAttribute()
{
if (!$this->exists()) {
return [];
}
$siblings = User::where('idd', $this->id)
->first([DB::raw('siblings(id) AS `siblings`')])
->siblings;
return explode(',', $siblings);
}
But this is not working as $this->id returns null
My table schema is users(id, username,...), so clearly id is present.
Is there a way by which I can bind the siblings function while querying db and then returning something like $this->siblings from getSiblingsAttribute. If I can bind siblings(id) as siblings with query select globally as we do for scopes using global scope.
That way my code can be simply
public function getSiblingsAttribute()
{
return $this->siblings;
}
The simplest way is to create a view in your database and use that as a table:
protected $table = 'user_view';
Otherwise I need more information about your id == null problem.
If you can fix this by your own in the next step it is important that you use an other column name by selecting as in your accessor otherwise you run in an infinite loop.
public function getSiblingsAttribute()
{
if (!$this->exists()) {
return [];
}
$siblings = User::where('id', $this->id)
->first([DB::raw('siblings(id) AS `siblings_value`')])
->siblings_value;
return explode(',', $siblings);
}
EDIT
Sadly there is no simple way to archieve this.
But after a little bit tinkering I have found a (not very nice) solution.
Give it a try.
You have to add the following class and trait to your app.
app/Classes/AdditionalColumnsTrait.php (additional column trait)
namespace App\Classes;
trait AdditionalColumnsTrait {
public function newEloquentBuilder($query) {
$builder = new EloquentBuilder($query);
$builder->additionalColumns = $this->getAdditionalColumns();
return $builder;
}
protected function getAdditionalColumns() {
return [];
}
}
app/Classes/EloquentBuilder.php (extended EloquentBuilder)
namespace App\Classes;
use Illuminate\Database\Eloquent\Builder;
class EloquentBuilder extends Builder {
public $additionalColumns = [];
public function getModels($columns = ['*']) {
$oldColumns = is_null($this->query->columns) ? [] : $this->query->columns;
$withTablePrefix = $this->getModel()->getTable() . '.*';
if (in_array('*', $columns) && !in_array($withTablePrefix, $oldColumns)) {
$this->query->addSelect(array_merge($columns, array_values($this->additionalColumns)));
} elseif (in_array($withTablePrefix, $oldColumns)) {
$this->query->addSelect(array_values($this->additionalColumns));
} else {
foreach ($this->additionalColumns as $name => $additionalColumn) {
if (!is_string($name)) {
$name = $additionalColumn;
}
if (in_array($name, $columns)) {
if (($key = array_search($name, $columns)) !== false) {
unset($columns[$key]);
}
$this->query->addSelect($additionalColumn);
}
}
if (is_null($oldColumns)) {
$this->query->addSelect($columns);
}
}
return parent::getModels($columns);
}
}
after that you can edit your model like this:
class User extends Model {
...
use App\Classes\AdditionalColumnsTrait;
protected function getAdditionalColumns() {
return [
'siblings' => DB::raw(siblings(id) as siblings)),
];
}
...
}
now your siblings column will be selected by default.
Also you have the option to select only specific columns.
If you don't want to select the additional columns you can use: User::find(['users.*']).
Perhaps it is a solution for you.