Laravel additional parameters in custom local scopes suddenly not being passed properly - laravel

Out of the blue suddenly my custom scopes in Laravel are no longer working. Somehow when used it's failing to separate additional parameters given from the $query parameter, clumping them into single array.
This results for instance a simple scope like the following failing because "Call to a member function whereNull() on array"
// WHERE IS ACTIVE
public function scopeWhereIsActive($query)
{
return $query->whereNull('show_until')->where('is_old', false)
->orWhere('show_until', '>', now())->where('is_old', false)
->orderByDesc('created_at');
}
And if I use a custom scope with additional parameters like the following, I get a missing arguments error (1 passed at least 2 expected). The thing however is that it worked perfectly just before, and I can't find any changes made that would affect this.
/** WHERE HAS PRODUCT
*
* #param \Illuminate\Database\Eloquent\Builder
* #param App\Models\Product $product
* #param boolean|false $return_with_loaded_products
* #param array $included_number_type_sums_array
* #return void
*/
public function scopeWhereHasProduct($query, $product, $return_with_loaded_products = false, $included_number_type_sums_array = null)
{
// dd('Order#scopeWherehasProduct parameters', $query, $product, $return_with_loaded_products , $included_number_type_sums_array);
$query->whereHas('products', function($query) use ($product) {
$query->where('product_id', $product->id);
});
if (isset($included_number_type_sums_array)) {
foreach ($included_number_type_sums_array as $type) {
$query->withSum(['products' => function($query) use($product) {
$query->where('product_id', $product->id);
}], 'int_orders_products.number_' . $type);
}
}
if ($return_with_loaded_products) {
$query->with(['products' => function($query) use($product) {
$query->where('product_id', $product->id);
}]);
}
return $query;
}
The $parameters parameter for the Laravel function calling the scope in Illuminate/Database/Eloquent.Model.php is a standard array, which I think should be correct, but the problem is that it's not being then split up across the separate parameter variables somehow.
/**
* Apply the given named scope if possible.
*
* #param string $scope
* #param array $parameters
* #return mixed
*/
public function callNamedScope($scope, $parameters = [])
{
dd('halt',$parameters);
return $this->{'scope'.ucfirst($scope)}($parameters);
}
This returns for the first scope:
^ array:1 [▼
0 => Illuminate\Database\Eloquent\Builder {#1877 ▼
#query: Illuminate\Database\Query\Builder {#1864 ▶}
#model: App\Models\Announcement {#1923 ▶}
#eagerLoad: []
#localMacros: []
#onDelete: null
#propertyPassthru: array:1 [▶]
#passthru: array:21 [▶]
#scopes: []
#removedScopes: []
}
]
I haven't touched anything 'deep' in the Laravel code that I know off, and rolling back isn't helping. My guess is that something deep in Laravel itself got wonked, but I really don't know where to look for these kind of issues. The laravel version is 9.15.00

I never could confirm what the problem was, apparently it could have been an altered `callNamedScope' function. I solved the issue by upgrading to the latest Laravel version (9.54.00).

Related

Why using additive paramerer in Resource collection raised error?

In laravel 9 app I want to add additive paramerer into Resource and looking at this
Laravel 5.6 - Pass additional parameters to API Resource?
branch I remade in app/Http/Resources/CurrencyResource.php :
<?php
namespace App\Http\Resources;
use App\Library\Services\DateFunctionalityServiceInterface;
use Illuminate\Http\Resources\Json\JsonResource;
use App;
use Illuminate\Support\Facades\File;
use Spatie\Image\Image;
use App\Http\Resources\MediaImageResource;
class CurrencyResource extends JsonResource
{
protected $show_default_image = false;
public function showDefaultImage($value){
$this->show_default_image = $value;
return $this;
}
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
*
* #return array
*/
public function toArray($request)
{
$dateFunctionality = App::make(DateFunctionalityServiceInterface::class);
$currencyImage = [];
$currencyMedia = $this->getFirstMedia(config('app.media_app_name'));
if ( ! empty($currencyMedia) and File::exists($currencyMedia->getPath())) {
$currencyImage['url'] = $currencyMedia->getUrl();
$imageInstance = Image::load($currencyMedia->getUrl());
$currencyImage['width'] = $imageInstance->getWidth();
$currencyImage['height'] = $imageInstance->getHeight();
$currencyImage['size'] = $currencyMedia->size;
$currencyImage['file_title'] = $currencyMedia->file_name;
}
else {
\Log::info( varDump($this->show_default_image, ' -1 $this->show_default_image::') );
$currencyImage['url'] = $this->show_default_image ? '/images/default-currency.jpg' : '';
}
// $currencyMedia = $currency->getFirstMedia(config('app.media_app_name'));
return [
'id' => $this->id,
'name' => $this->name,
...
and with code in control :
'currency' => (new CurrencyResource($currency))->showDefaultImage(false),
its work ok, but I got an error :
Method Illuminate\Support\Collection::showDefaultImage does not exist.
when I applyed this method for collection:
return (CurrencyResource::collection($currencies))->showDefaultImage(true);
But in link above there is a similar way :
UserResource::collection($users)->foo('bar');
What is wrong in my code and how that can be fixed ?
Thanks!
I wonder if there is a reason you can't use this approach: https://stackoverflow.com/a/51689732/8485567
Then you can simply use the request parameters to modify your response.
If you really want to get that example working in that way, it seems you are not following the example correctly.
You need to override the static collection method inside your CurrencyResource class:
public static function collection($resource){
return new CurrencyResourceCollection($resource);
}
You also need to create the CurrencyResourceCollection class and define the showDefaultImage method and $show_default_image property on that class as in the example you referred to.
Then you should be able to do:
CurrencyResource::collection($currencies)->showDefaultImage(true);
The reason the way you are doing it doesn't work is because you haven't defined the static collection method on your resource hence it's defaulting to the normal behavior of returning a default collection object as you can see in your error message.

Model appends including entire relationship in query

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();

Impossible to get my $params in Laravel scope

I'm new in Laravel and I try to do a list a table. On this list I have some filters.
I try to use scope but It doesn't work.
In my controller I have :
class GrillesController extends Controller
{
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index(request $request)
{
$query = DB::table('grilles')
->join('compets as CO','CO.id','=','gri_compet_id')
->join ('journees as JO', 'JO.id','=','gri_journee_id')
->select('com_nm_logo','jou_l_journee_c','grilles.*')
->where('com_c_st','A')
->orderbyRaw('gri_d_deb,gri_h_deb,com_n_ord_aff');
$params = $request->except('_token');
$grilles = Grilles::FilterListe($query,$params)->get();
In my model :
public function scopeFilterListe($query,$params)
{
$value = $params['filter_compets'];
$query->where('gri_compet_id','=',$value);
return $query;
}
In scopeFilterListe in the model, I'd like to get all my variable of my URL to build my where clause.
http://xxx.fr/grilles?_token=DMcay7SkDc1QeFlxRPMqT4DTGJXdgcXRbVbCNRvo&filter_compets=1&filter_etatparis=3&filter_periode=
I tried a lot of things, but I always have this error:
"Cannot use object of type Illuminate\Database\Query\Builder as
array"
It comes from : $value = $params['filter_compets'];
If I write $value=1, it works...(even if I have another problem as the data supposed to come from
->join('compets as CO','CO.id','=','gri_compet_id')
->join ('journees as JO', 'JO.id','=','gri_journee_id')
are not displayed..
What is the problem for my scope ?
Thanks a lot for your help.
I think that you need dynamic scope. Please find documentation here.
Herewith your modified script.
public function scopeOfFilterListe($query,$value)
{
$query->where('gri_compet_id','=',$value);
return $query;
}
$query = DB::table('grilles')
->join('compets as CO','CO.id','=','gri_compet_id')
->join ('journees as JO', 'JO.id','=','gri_journee_id')
->select('com_nm_logo','jou_l_journee_c','grilles.*')
->where('com_c_st','A')
->orderbyRaw('gri_d_deb,gri_h_deb,com_n_ord_aff')
;
$params = $request->except('_token');
$grilles = Grilles::ofFilterListe($params['filter_compets'])->get();

Call to a member function toBase() on array

I am trying to use resource collection and I am receiving this error " Call to a member function toBase() on array"
The following code is in my resource :
class dataCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'data' => $this->collection
];
}
}
and here is code from the controller:
$projects = count(Project::all());
$services = count(Service::all());
$users = count(User::all());
$technologies = count(Technology::all());
$customers = count(Customer::all());
$data = [
'projects' => $projects,
'services' => $services,
'users' => $users,
'technologies' => $technologies,
'customers' => $customers
];
return new dataCollection($data);
Can someone help me please?
I'm not sure what your end goal is, but it appears the problem is that, as the error says, you are sending the wrong type of data into a collection. The resource collection method in this case is looking for a set of objects, and you are sending an array.
By the manual, its looking for something like this with a collection of User objects:
UserResource::collection(User::all()); // User collection, not the raw data array
If you are trying to send data back to someone via some API or json, I think you already have what you need - perhaps skip the whole resource collection method if you can? So try this:
$data = [
'projects' => Project::count(),
'services' => Service::count(),
'users' => User::count(),
'technologies' => Technology::count(),
'customers' => Customer::count()
];
return json_encode($data);
Note I changed the code a little to make it more efficient for you - you don't need to assign into variables first - and by calling ::all() plus count(), it is going to make much heavier database queries
The toArray() method on the resource would actually re-convert objects into the type of array you have above - so I suggest you test by skipping the circular process.

Laravel Nova Metric Trend In Future

I'm stuck I want to create a Laravel Nova Metrics Trend where it goes in the future instead of back in time.
This code will output: 15-March-2019, 14-March-2019, 13-March-2019,
public function calculate(Request $request)
{
return $this->sumByDays($request, Payment::class, 'hours_per_month', 'upcoming_invoice_date')->showLatestValue();
}
/**
* Get the ranges available for the metric.
*
* #return array
*/
public function ranges()
{
return [
3 => '3 Days',
];
}
I've already made some changes to Trend.php but it gave me nothing than errors.
/**
* Return a value result showing a sum aggregate over days.
*
* #param \Illuminate\Http\Request $request
* #param \Illuminate\Database\Eloquent\Builder|string $model
* #param string $column
* #param string $dateColumn
* #return \Laravel\Nova\Metrics\TrendResult
*/
public function sumByDays($request, $model, $column, $dateColumn = null)
{
return $this->aggregate($request, $model, Trend::BY_DAYS, 'sum', $column, $dateColumn);
}
Is this even possible?
Thanks,
The trend range is defined in Trend.php so you were on the right track!
Check out the protected function aggregate.
protected function aggregate($request, $model, $unit, $function, $column, $dateColumn = null)
{
The $endingDate can be changed to whatever you want!
$possibleDateResults = $this->getAllPossibleDateResults(
$startingDate = $this->getAggregateStartingDate($request, $unit),
$endingDate = Chronos::now();
The Chronos API has nice documentation but try something like this
$endingDate = Chronos::tomorrow()->addWeeks(4),
Note: I have done very minimal testing on this so use at your own risk. It does break the showLatestValue() method for trend metrics, but that could be addressed in TrendResult.php if you really needed it.
1) You can change the range dates to the future with a negative number (of days) as key:
public function ranges()
{
return [
-3 => 'Next 3 Days',
];
}
The problem is that it doesn't work because the order of datetimes will be wrong in the query as the endDate is always set to now() in Trend.php:
$startingDate = $this->getAggregateStartingDate($request, $unit),
$endingDate = Chronos::now(),
...
->whereBetween($dateColumn, [$startingDate, $endingDate])
2) Quick/dirty fix. In Trend.php change the whereBetween() second argument array (of datetimes) comparing and setting datetimes in ascending order.
->whereBetween(
$dateColumn,
[ min($startingDate, $endingDate), max($startingDate, $endingDate) ]
)

Resources