Laravel custom attributes loads relationships even when attribute is not asked - laravel

I have a custom attribute that calculates the squad name (to make our frontend team lives easier).
This requires a relation to be loaded and even if the attribute is not being called/asked (this happens with spatie query builder, an allowedAppends array on the model being passed to the query builder and a GET param with the required append(s)) it still loads the relationship.
// Model
public function getSquadNameAttribute()
{
$this->loadMissing('slots');
// Note: This model's slots is guaranteed to all have the same squad name (hence the first() on slots).
$firstSlot = $this->slots->first()->loadMissing('shift.squad');
return ($firstSlot) ? $firstSlot->shift->squad->name : null;
}
// Resource
public function toArray($request)
{
return [
'id' => $this->id,
'squad_name' => $this->when(array_key_exists('squad_name', $this->resource->toArray()), $this->squad_name),
'slots' => SlotResource::collection($this->whenLoaded('slots')),
];
}
Note: squad_name does not get returned if it's not being asked in the above example, the relationship is however still being loaded regardless
A possible solution I found was to edit the resource and includes if's but this heavily reduces the readability of the code and I'm personally not a fan.
public function toArray($request)
{
$collection = [
'id' => $this->id,
'slots' => SlotResource::collection($this->whenLoaded('slots')),
];
if (array_key_exists('squad_name', $this->resource->toArray())) {
$collection['squad_name'] = $this->squad_name;
}
return $collection;
}
Is there another way to avoid the relationship being loaded if the attribute is not asked without having spam my resource with multiple if's?

The easiest and most reliable way I have found was to make a function in a helper class that checks this for me.
This way you can also customize it to your needs.
-- RequestHelper class
public static function inAppends(string $value)
{
$appends = strpos(request()->append, ',') !== false ? preg_split('/, ?/', request()->append) : [request()->append];
return in_array($value, $appends);
}
-- Resource
'squad_name' => $this->when(RequestHelper::inAppends('squad_name'), function () {
return $this->squad_name;
}),

Related

Laravel: adding policy to stop other users visualizing other user's profiles

I need to block users from visualizing other users profiles I have the following in my web.php:
Route::get('companyuser/{id}', [CompanyUserController::class, 'show'])
->middleware(['role:companyuser', 'can:show,user']);
I defined a policy
public function show(User $authenticatedUser, $user_model)
{
return $authenticatedUser === $user_model->id ? Response::allow() : Response::deny();
}
and added it to the AuthServiceProvider
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
User::class => CompanyUserPolicy::class,
];
But now the user is blocked from entering his own profile as well. What am I missing? Thanks for the help.
It looks like the policy show() method is comparing an object to an ID value. This will always return false because the $authenticatedUser variable is a User object, while $user_model->id is likely an integer. Using strict type comparison === an int can never be equal to an object:
$authenticatedUser === $user_model->id
Instead the code should probably compare the id of both objects:
$authenticatedUser->id === $user_model->id
Laravel Models also have a built-in method public function is($model): bool that can be used to verify two objects represent the same model (database record). Here is the same code using that method:
$authenticatedUser->is($user_model)
The final solution might look like this:
public function show(User $authenticatedUser, $user_model)
{
return $authenticatedUser->is($user_model) ? Response::allow() : Response::deny();
}

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

Simplify API boilerplate in Laravel's controllers?

When I write APIs with Laravel, I often use the same method as GitHub API v3. I add URLs to ease the navigation during development and also for the users that will develop using that API.
In this example, I manually add the URLs on every field then add a count for the pagers on the frontend. I sometime have more complicated stuff if I want to add the necessary to filter the results (if used with Vuetify or Kendo Grid).
class UserController extends Controller
{
function index() {
$users = User::all()->each(function ($item, $key) {
$item['activities'] = url("/api/users/{$item['id']}/activities");
$item['classrooms'] = url("/api/users/{$item['id']}/classrooms");
});
return [
'count' => count($users),
'courses' => $users,
];
}
}
Is there a way to make this code less boilerplate? Is there a package that does everything out of the box?
I'm a big fan of fractal especially spaties fractal package. This enables you to transform objects into responses.
There are two concepts in fractal serializers and transformers. Serializers is about the whole request, meta information data etc. Transformer is how you transform each model or object. Normally you would not make custom serializers, but in your case in can solve most of your trouble.
use League\Fractal\Serializer\ArraySerializer;
class YourSerializer extends ArraySerializer {
public function collection($resourceKey, array $data)
{
return [
$resourceKey => $data,
'count' => count($data),
];
}
}
Create your transformer for your user. The other big thing you gain for using this, is you have one plays to be responsible for transformation. Instead of each controller having to have the logic, which will be spread out and you have to remember to include it.
use League\Fractal\TransformerAbstract;
class AccountBalanceTransformer extends TransformerAbstract
{
public function transform(User $user)
{
return [
'id' => $user->id,
'name' => $user->id,
// other user fields
'activities' => url("/api/users/{$user->id}/activities"),
'classrooms' => url("/api/users/{$user->id}/classrooms"),
];
}
}
You have to assign the serializer in your fractal.php config.
'default_serializer' => YourSerializer::class,
Now you should be able to transform you responses like so.
return fractal($users, new UserTransformer())
->withResourceName('courses')
->respond(Response::HTTP_OK);
For making it easier to use and avoid repeating your self, i usually do a method on a parent controller like so. While setting the transformer on the object.
public function respond($data, $resourceKey, $status = Response::HTTP_OK) {
return fractal($data, $this->transformer)
->withResourceName($resourceKey)
->respond($status);
}
This will produce a response similar to what was specified in the question.

Map controller functions into database records in Laravel

I try to implement a chain of approval methodology in my Laravel app.
Lets say that I have a standard CRUD controller with standard REST routes.
[URL]/products
In the controller I have 3 functions (Index, Store, Update)
I want that for Store and Update a certain condition will happen before, something like that (pseudo):
if (fucntion requires chain of approval) {
if (auth()->user !== one of the approvers){
email all approvers;
return 201 "pending approval";
}
}
// the logged in user is allowed to execute the function
rest of the code...
I'm struggling with a few things here:
The only thing I can think of something that might be similar to this inside Laravel is the $this->authorize() function but I don't think that it was meant to be used like this, it is meant to authorize or not to authorize, not for 201 codes, and using it means aborting with 201 and it doesn't sit right
I want to allow the admins to control which functions need approval and because I don't want to maintain my controller functions together with a seeder that will contain a list of all the functions I'm thinking about creating an artisan command to be run before production and mapping all the functions into a database table that will be used as a model and all the process will use a proper many to many relations between the functions and the approvers, but I don't know how to map the functions with artisan command and I don't know if this is even the right way to go
I want to avoid from writing a certain code in all the functions that might require approval, and don't know how and if it is even possible
The return of the functions should return a JsonResource of the specific model for example ProductResource, What should I return when I need approval? Just to mock a proper response with status pending?
Thanks for everybody who is willing to give me some guidance.
You can do something similar to below... just have a single class that defines how each user type (or even permission) is handled.
public function index(IndexRequest $request) // verify that use is authorized to do this action
{
$response = (new MyJobDirector)->handle(Auth::user());
.. handle response
}
MyJobDirector Class
class MyJobDirector
{
const STRATEGY = [
'user' => 'userHandler',
'manager' => 'managerHandler',
];
public function handle(User $user): array
{
return $this->{static::STRATEGY[$user->role]}();
}
protected function userHandler(): array
{
event(EmailApprovers::class);
return [
'code' => 201,
'response' => 'pending approval',
];
}
}
You can make it even more complicated by defining a separate class for each handler and specifying constants for status and a response that each class should return
class User extends BaseJobHandle
{
const CODE = 201;
const RESPONSE = 'pending approval';
}
abstract class BaseJobHandler
{
const CODE;
const RESPONSE;
public function handle(): array
{
$this->additionalProcesses();
return [
'code' => static::CODE,
'response' => static::RESPONSE,
];
}
protected function additionalProcesses(): void {}
}
class MyJobDirector
{
const STRATEGY = [
'user' => User::class,
'manager' => Manager::class,
];
public function handle(User $user): array
{
$class = static::STRATEGY[$user->role];
return (new $class)->handle();
}
}
Regarding The return of the functions should return a JsonResource of the specific model for example ProductResource, What should I return when I need approval? Just to mock a proper response with status pending?
You can have a ProductResouce class, in which you decide which specific resource to return depending on a use case. It's all depends on what data you need to return.

Laravel Multi level relationship in API Resource

My problem is, the api resource loading which i really not needed.
Look into my Api Resource files
//BoxItemResource.php
public function toArray($request)
{
return [
'box_id'=> $this->box_id,
'item_id'=> $this->item_id,
'item'=> new ItemResource($this->item)
];
}
//ItemResource.php
public function toArray($request)
{
return [
'id' => $this->id,
'shipping_price' => $this->shipping_price,
'condition_id' => $this->condition_id,
'condition' => new ConditionResource($this->condition)
];
}
//ConditionResource.php
public function toArray($request)
{
return [
'id'=> $this->id,
'name'=> $this->name
];
}
//controller
return BoxItemResource::collection(
BoxItem::with([
'item'
])->paginate(1)
);
My problem is, I just need only BoxItem and Item here. I don't really want to load condition.
If i remove the condition relation from ItemResource.php, it will work. but the problem is I am using the ItemResource.php in some other place which need this condition.
Is it possible to deny loading condition relation ship here.
more clearly, I want to load the relationship which I mention in controller(in ->with()) .
Thanks in advance.
API resources allow for conditional attribtues and conditional relationships.
For attributes, which in my opinion is sufficient to use in your case, this means you can simply wrap the attribute value in $this->when($condition, $value) and the whole attribute will be removed from the result if $condition == false. So a concrete example of your code:
return [
'box_id'=> $this->box_id,
'item_id'=> $this->item_id,
'item'=> $this->when($this->relationLoaded('item'), new ItemResource($this->item))
];
Or if you prefer using the relationship style:
return [
'box_id'=> $this->box_id,
'item_id'=> $this->item_id,
'item'=> new ItemResource($this->whenLoaded('item'))
];
Maybe you are looking for Conditional Relationships?
Ideally, it should look like the following:
public function toArray($request)
{
return [
'box_id'=> $this->box_id,
'item' => ItemResource::collection($this->whenLoaded('item'))
];
}
The item key will only be present if the relationship has been loaded.

Resources