Laravel Multi level relationship in API Resource - laravel

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.

Related

Making Laravel 9 validation rule that is unique on 2 columns

I am trying to update a row in the pages table.
The slug must be unique in the pages table on the slug and app_id field combined.
i.e. there can be multiple slugs entitled 'this-is-my-slug' but they must have unique app_id.
Therefore I have found that formula for the unique rule is:
unique:table,column,except,idColumn,extraColumn,extraColumnValue
I have an update method and getValidationRules method.
public function update($resource,$id,$request){
$app_id=22;
$request->validate(
$this->getValidationRules($id,$app_id)
);
// ...store
}
When I test for just a unique slug the following works:
public function getValidationRules($id,$app_id){
return [
'title'=> 'required',
'slug'=> 'required|unique:pages,slug,'.$id
];
}
However, when I try and add the app_id into the validation rules it returns server error.
public function getValidationRules($id,$app_id){
return [
'title'=> 'required',
'slug'=> 'required|unique:pages,slug,'.$id.',app_id,'.$app_id
];
}
I have also tried to use the Rule facade, but that also returns server error. Infact I can't even get that working for just the ignore id!
public function getValidationRules($id,$app_id){
return [
'title'=> 'required',
'slug'=> [Rule::unique('pages','slug')->where('app_id',$app_id)->ignore($id)]
];
}
Any help is much appreciated :)
Thanks for the respsonses. It turned out a couple of things were wrong.
Firstly if you want to use the Rule facade for the validation rules, make sure you've included it:
use Illuminate\Validation\Rule;
The other method for defining the validation rule seems to be limited to the following pattern:
unique:table,column,except,idColumn
The blog post that I read that showed you could add additional columns was for laravel 7, so i guess that is no longer the case for laravel 9.
Thanks for your responses and help in the chat!
I recommend you to add your own custom rule.
First run artisan make:rule SlugWithUniqueAppIdRule
This will create new file/class inside App\Rules called SlugWIthUniqueAppRule.php.
Next inside, lets add your custom rule and message when error occured.
public function passes($attribute, $value)
{
// I assume you use model Page for table pages
$app_id = request()->id;
$pageExists = Page::query()
->where('slug', $slug)
->where('app_id', $app_id)
->exists();
return !$pageExists;
}
public function message()
{
return 'The slug must have unique app id.';
}
Than you can use it inside your validation.
return [
'title'=> 'required|string',
'slug' => new SlugWithUniqueAppIdRule(),
];
You can try it again and adjust this custom rule according to your needs.
Bonus:
I recommend to move your form request into separate class.
Run artisan make:request UpdateSlugAppRequest
And check this newly made file in App\Http\Requests.
This request class by default will consists of 2 public methods : authorize() and rules().
Change authorize to return true, or otherwise this route can not be accessed.
Move your rules array from controller into rules().
public function rules()
{
return [
'title'=> 'required|string',
'slug' => new SlugWithUniqueAppIdRule(),
];
}
To use it inside your controller:
public function update(UpdateSlugAppRequest $request, $resource, $id){
// this will return validated inputs in array format
$validated = $request->validated();
// ...store process , move to a ServiceClass
}
This will make your controller a lot slimmer.

Laravel custom attributes loads relationships even when attribute is not asked

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

Laravel unique request update

Ive read some things about this on laracasts and Stackoverflow.
I have an update function with validation:
public function update(Customer $customer, StoreCustomer $request)
{
$customer->update($request->validated());
exit();
}
And the validation rules:
public function rules()
{
return [
'code' => 'required|unique:customers,code',
]
}
Now I tried to add a 3rd argument after the unique, so if it would exist it would continue. I tried it like this:
public function rules(Customer $customer)
{
return [
'code' => 'required|unique:customers,code,'.$customer->code,
]
}
but that doesn't seem to do anything. It seems to work if you do the validation in my controller itself, but this looks way cleaner. Any solutions?
If you want to ignore the current customer, you need to change the $customer->code to $customer->id, assuming your primary key is id.
unqiue validation documentation
Ignoring current customer:
'code' => 'required|unique:customers,code,'.$customer->id,
Your Form Request is a Request. It gets filled with the data from the current Request. You can pull the customer from your route as it is bound as a parameter currently. $this->route('customer')
public function rules()
{
return [
'code' => 'required|unique:customers,code,'. $this->route('customer')->code,
// perhaps this should be ignoring by id though?
'code' => 'required|unique:customers,code,'. $this->route('customer')->id,
];
}
Using method injection here could only give you a binding, if one was registered with the container for that exact class, or a new instance of that class, the case here. There is no link between that class name and the concept that there might be a route parameter that currently contains a Model that happens to be of that class.
It seems that the right way to do this was
'code' => 'required|unique:customers,code,'.$this->route('customer')->code.',code',
Since the $customer parameter isnt available in rules() you needed to get the customer another way.
I think the best way do this with Rule:unique
return [
'code' => ['required', Rule::unique('customers', 'code')->whereNot('code', $customer->code)]
]

URL friendly in routes

I'm creating url friendly in my app, but it's not working, the app is giving me some issues related with "-".
It's giving me an error of:
ErrorException in PostController.php line 60:
Trying to get property of non-object
My ideal URL is:
http://domain.com/CATEGORY-title-of-post-ID
My route is:
Route::get('{category}-{title}-{id}', 'PostController#show');
PostController show function:
public function show($category,$title,$id)
{
$post = Post::find($id);
$user = Auth::user();
$comments = Comment::where('post_id',$id)
->where('approved',1)
->get();
return view('posts.show',compact('post','comments','user'));
}
Blade View:
<?php
$title_seo = str_slug($feature->title, '-');
?>
<a href="{{url($feature->categories[0]->internal_name."-".$title_seo."-".$feature->id)}}" rel="bookmark">
...</a>
There's a library called Eloquent-Sluggable that will create a unique slug for each post and correctly URL encode it.
To install (taken from the docs):
composer require cviebrock/eloquent-sluggable:^4.1
Then, update config/app.php by adding an entry for the service provider.
'providers' => [
// ...
Cviebrock\EloquentSluggable\ServiceProvider::class,
];
Finally, from the command line again, publish the default configuration file:
php artisan vendor:publish --provider="Cviebrock\EloquentSluggable\ServiceProvider"
To use, add the Sluggable trait to your model:
use Cviebrock\EloquentSluggable\Sluggable;
class Post extends Model
{
use Sluggable;
/**
* Return the sluggable configuration array for this model.
*
* #return array
*/
public function sluggable()
{
return [
'slug' => [
'source' => 'title'
]
];
}
}
When you save an instance of your model, the library will automatically create a slug and save it to the newly created slug column of your model's table. So to access the slug you'd use $model->slug
To achieve your desired slug, rather than create it from title set by default. You can pass the source attribute of the sluggable method an array of field names, using a dot notation to access the attributes of a related model, like so:
public function sluggable()
{
return [
'slug' => [
'source' => ['category.name','title','id']
]
];
}
}
Why are you genering your "friendly URL" manually?
You have route helper function that builds for you a URL based on the given parameters.
Route::get('{category}-{title}-{id}', [
'as => 'post.show',
'uses' => 'PostController#show'
]);
echo route('post.show', ['testing', 'title', 'id']); // http://domain.dev/testing-title-id
This is not the best approach to implement SEO friendly URLs, anyway.
In your controller you ALWAYS use your ID to find a post, that means that category and title are completely useless to determine which resource needs to be served to the user.
You can make your life easier by doing something like:
Route::get('{id}-{slug}', [
'as => 'post.show',
'uses' => 'PostController#show'
]);
echo route('post.show', ['id', 'slug']); // http://domain.dev/id-slug
In your model you create an helper function that generates the slug for your post:
class Post
{
[...]
public function slug()
{
return str_slug("{$this->category}-{$this->title}");
}
}
Then, in your controller you need to check that the slug used to access the article is correct or not, since you don't want Google to index post with wrong slugs. You essentially force a URL to be in a certain way, and you don't lose index points.
class PostController
{
[...]
public function show($id, $slug)
{
$post = Post::findOrFail($id);
$user = Auth::user();
if ($post->slug() !== $slug) {
return redirect()->route('posts.show', ['id' => 1, 'slug' => $post->slug()]);
}
$comments = Comment::where('post_id', $id)->where('approved', 1)->get();
return view('posts.show', compact('post', 'comments', 'user'));
}
}

yii2 custom validation not working

I need to compare 2 attribute value in the model and only if first value is lower than second value form can validate.I try with below code but it not worked.
controller
public function actionOpanningBalance(){
$model = new Bill();
if ($model->load(Yii::$app->request->post())) {
$model->created_at = \Yii::$app->user->identity->id;
$model->save();
}else{
return $this->render('OpanningBalance', [
'model' => $model,
]);
}
}
Model
public function rules()
{
return [
[['outlet_id', 'sr_id', 'bill_number', 'bill_date', 'created_at', 'created_date','bill_amount','credit_amount'], 'required'],
[['outlet_id', 'sr_id', 'created_at', 'updated_at'], 'integer'],
[['bill_date', 'd_slip_date', 'cheque_date', 'created_date', 'updated_date','status'], 'safe'],
[['bill_amount', 'cash_amount', 'cheque_amount', 'credit_amount'], 'number'],
[['comment'], 'string'],
['credit_amount',function compareValue($attribute,$param){
if($this->$attribute > $this->bill_amount){
$this->addError($attribute, 'Credit amount should less than Bill amount');
}],
[['bill_number', 'd_slip_no', 'bank', 'branch'], 'string', 'max' => 225],
[['cheque_number'], 'string', 'max' => 100],
[['bill_number'], 'unique']
];
}
}
It's going in to the validator function but not add the error like i wanted
$this->addError($attribute, 'Credit amount should less than Bill amount');
anyone can help me with this?
If the validation is not adding any error, it's most likely being skipped. The issue is most likely becasue of default rules behaviour whereby it skips empty or already error given values as per here: https://www.yiiframework.com/doc/guide/2.0/en/input-validation#inline-validators
Specifically:
By default, inline validators will not be applied if their associated attributes receive empty inputs or if they have already failed some validation rules. If you want to make sure a rule is always applied, you may configure the skipOnEmpty and/or skipOnError properties to be false in the rule declarations.
So you would need to set up the skipOnEmpty or skipOnError values depending on what works for you:
[
['country', 'validateCountry', 'skipOnEmpty' => false, 'skipOnError' => false],
]
Try this:
public function actionOpanningBalance(){
$model = new Bill();
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
$model->created_at = \Yii::$app->user->identity->id;
$model->save();
}else{
return $this->render('OpanningBalance', [
'model' => $model,
]);
}
}
For Validation
You can use anonymous function :
['credit_amount',function ($attribute, $params) {
if ($this->$attribute > $this->bill_amount)) {
$this->addError($attribute, 'Credit amount should less than Bill amount.');
return false;
}
}],
you can use like this below answer is also write
public function rules(){
return [
['credit_amount','custom_function_validation', 'on' =>'scenario'];
}
public function custom_function_validation($attribute){
// add custom validation
if ($this->$attribute < $this->cash_amount)
$this->addError($attribute,'Credit amount should less than Bill amount.');
}
I've made custom_function_validation working using 3rd params like this:
public function is18yo($attribute, $params, $validator)
{
$dobDate = new DateTime($this->$attribute);
$now = new DateTime();
if ($now->diff($dobDate)->y < 18) {
$validator->addError($this, $attribute, 'At least 18 years old');
return false;
}
}
This is a back end validation and it will trigger on submit only. So you can try something like this inside your validation function.
if (!$this->hasErrors()) {
// Your validation code goes here.
}
If you check the basic Yii2 app generated you can see that example in file models/LoginForm.php, there is a function named validatePassword.
Validation will trigger only after submitting the form.

Resources