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();
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 working on a Laravel project. I am using Scout based on Algolia. Now, I cannot apply whereHas to the search.
I have a model called Place which has many to many relationships with Category with the following code.
class Place extends Model
{
use Searchable, Localizable;
protected $with = [
'images',
'phones',
'emails',
'categories'
];
protected $casts = [
'is_featured' => 'boolean'
];
public function categories()
{
return $this->belongsToMany(Category::class, 'place_category');
}
public function searchableAs()
{
return "places_index";
}
public function toSearchableArray()
{
$record = $this->toArray();
$record['_geoloc'] = [
'lat' => $record['latitude'],
'lng' => $record['longitude'],
];
unset($record['created_at'], $record['updated_at'], $record['latitude'], $record['longitude']);
return $record;
}
}
As you can see it will be indexed on Algolia.
I am searching based on geolocation and keyword using the following code.
Place::search($keyword, function ($algolia, $query, $options) use ($latitude, $longitude) {
$location = [
'aroundLatLng' => $latitude . ',' . $longitude,
'aroundRadius' => config('scout.algolia.search_radius'),
];
$options = array_merge($options, $location);
return $algolia->search($query, $options);
});
The code is working fine until I also tried to filter by category. I changed my code to something like this.
$query = Place::search($keyword, function ($algolia, $query, $options) use ($latitude, $longitude) {
$location = [
'aroundLatLng' => $latitude . ',' . $longitude,
'aroundRadius' => config('scout.algolia.search_radius'),
];
$options = array_merge($options, $location);
return $algolia->search($query, $options);
});
if ($category) {
$query = $query->whereHas('categories', function ($query) use ($category) {
$query->where('categories.id', $category);
});
}
As you can see now, I am now filtering by categories too using whereHas. When I run the code, I got the following error.
BadMethodCallException
Method Laravel\Scout\Builder::whereHas does not exist.
Literally, I cannot use whereHas with Algolia search. How can I fix it? Also, I am thinking of indexing the categories and filter the records on the Algolia side.But I am going to be filtering by id. How can I customise the query for it?
enter code hereMy question about the combination filters in laravel by using eloquent.
I am trying to filter with a combination of the following:
username
Category
Sub_category
started_at
created_at
status
I use where conditions but it not working as required.
public function filter(Request $request, User $user)
{
$user = $user->newQuery();
// Search for a user based on their name.
if ($request->has('username')) {
$user->where('name', $request->input('username'));
}
// Search for a user based on their Category.
if ($request->has('Category')) {
$user->where('Category', $request->input('Category'));
}
// Search for a user based on their Sub_category.
if ($request->has('Sub_category')) {
$user->where('Sub_category', $request->input('Sub_category'));
}
// Search for a user based on their started_at.
if ($request->has('started_at')) {
$user->where('started_at', $request->input('started_at'));
}
// Search for a user based on their status.
if ($request->has('status')) {
$user->where('status', $request->input('status'));
}
// Continue for all of the filters.
// Get the results and return them.
return $user->get();
}
You should save your where conditions to the $user variable.
$user = $user->where($dbField, $request->input($requestParam));
For improved readability, I'd suggest using a loop with all of your filtering cases.
public function filter(Request $request)
{
$users = User::query();
$filters = [
'username' => 'name',
'Category' => 'Category',
'Sub_category' => 'Sub_category',
'started_at' => 'started_at',
'status' => 'status'
];
foreach ($filters as $requestParam => $dbField){
if ($request->has($requestParam)) {
$users = $users->where($dbField, $request->input($requestParam));
}
}
return $users->get();
}
Bear in mind $request->has does not check whether the parameter value is empty, use $request->filled if you wish so.
This is My examle refer this
public function filter(Request $request)
{
$q = User::query();
$email = $request->input('email');
$username= $request->input('username');
$q->when($email,function ($query) use ($email){
$query->where('email',$email);
});
$q->when($username,function ($query) use ($username){
$query->where('username',$username);
});
$results = $q->get();
//code
}
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();