Validating array - get current iteration - laravel

I'm trying to validate a POST request using Laravel's FormRequest.
The customer is submitting an order, which has an array of items. We are requiring the user to indicate whether the item needs special_delivery only if the asking_price > 500 and the quantity > 10.
The following are my intended rules:
public function rules() {
'customer_id' => 'required|integer|exists:customers,id',
'items' => 'required|array',
'items.*.name' => 'required|string',
'items.*.asking_price' => 'required|numeric',
'items.*.quantity' => 'required|numeric',
'items.*.special_delivery' // required if price > 500 && quantity > 10
}
I've attempted to do something along these lines:
Rule::requiredIf($this->input('item.*.asking_price') > 500 && $this->input('item.*.quantity' > 10));
The problem with this is that I can't find a way to access the current items iteration index to indicate which item to validate against.
I also tried the following custom validation:
function ($attribute, $value, $fail) {
preg_match('/\d+/', $attribute, $m);
$askingPrice = $this->input('items')[$m[0]]['asking_price'];
$quantity= $this->input('items')[$m[0]]['quantity'];
if ($askingPrice > 500 && $quantity > 10) {
$fail("$attribute is required");
}
}
Although this function gives me access to the current $attribute,the problem is that it will only run if special_delivery exists. Which defeats the entire purpose!
Any help will be much appreciated!
Thank you!

I might've come up with a solution to your problem, a index aware sometimes if you so will.
Since it's unfortunately not possible to add macros to the Validator, you would either have to override the validation factory (that's what I suggest) and use your own custom validation class or make a helper function based off the method, pass the Validator instance as an additional parameter and use this instead of $this.
Sauce first: the indexAwareSometimes validation function
function indexAwareSometimes(
\Illuminate\Contracts\Validation\Validator $validator,
string $parent,
$attribute,
$rules,
\Closure $callback
) {
foreach (Arr::get($validator->getData(), $parent) as $index => $item) {
if ($callback($validator->getData(), $index)) {
foreach ((array) $attribute as $key) {
$path = $parent.'.'.$index.'.'.$key;
$validator->addRules([$path => $rules]);
}
}
}
}
A lot of inspiration obviously came from the sometimes method and not much has changed. We're basically iterating through the array (the $parent array, in your case items) containing all our other arrays (items.*) with actual data to validate and adding the $rules (required) to $attribute (special_delivery) in the current index if $callback evaluates to true.
The callback closure requires two parameters, first being the form $data of your parent validation instance, retrieved by Validator::getData(), second the $index the outer foreach was at the time it called the callback.
In your case the usage of the function would look a little like this:
use Illuminate\Support\Arr;
class YourFormRequest extends FormRequest
{
public function rules()
{
return [
'customer_id' => 'required|integer|exists:customers,id',
'items' => 'required|array',
'items.*.name' => 'required|string',
'items.*.asking_price' => 'required|numeric',
'items.*.quantity' => 'required|numeric',
];
}
public function getValidatorInstance()
{
$validator = parent::getValidatorInstance();
indexAwareSometimes(
$validator,
'items',
'special_delivery',
'required',
fn ($data, $index) => Arr::get($data, 'items.'.$index.'.asking_price') > 500 &&
Arr::get($data, 'items.'.$index.'.quantity') > 10
);
}
}
Extending the native Validator class
Extending Laravel's native Validator class isn't as hard as it sounds. We're creating a custom ValidationServiceProvider and inherit Laravel's Illuminate\Validation\ValidationServiceProvider as a parent. Only the registerValidationFactory method needs to be replaced by a copy of it where we specify our custom Validator resolver that should be used by the factory instead:
<?php
namespace App\Providers;
use App\Validation\CustomValidator;
use Illuminate\Contracts\Translation\Translator;
use Illuminate\Validation\Factory;
use Illuminate\Validation\ValidationServiceProvider as ParentValidationServiceProvider;
class ValidationServiceProvider extends ParentValidationServiceProvider
{
protected function registerValidationFactory(): void
{
$this->app->singleton('validator', function ($app) {
$validator = new Factory($app['translator'], $app);
$resolver = function (
Translator $translator,
array $data,
array $rules,
array $messages = [],
array $customAttributes = []
) {
return new CustomValidator($translator, $data, $rules, $messages, $customAttributes);
};
$validator->resolver($resolver);
if (isset($app['db'], $app['validation.presence'])) {
$validator->setPresenceVerifier($app['validation.presence']);
}
return $validator;
});
}
}
The custom validator inherits Laravel's Illuminate\Validation\Validator and adds the indexAwareSometimes method:
<?php
namespace App\Validation;
use Closure;
use Illuminate\Support\Arr;
use Illuminate\Validation\Validator;
class CustomValidator extends Validator
{
/**
* #param string $parent
* #param string|array $attribute
* #param string|array $rules
* #param Closure $callback
*/
public function indexAwareSometimes(string $parent, $attribute, $rules, Closure $callback)
{
foreach (Arr::get($this->data, $parent) as $index => $item) {
if ($callback($this->data, $index)) {
foreach ((array) $attribute as $key) {
$path = $parent.'.'.$index.'.'.$key;
$this->addRules([$path => $rules]);
}
}
}
}
}
Then we just need to replace Laravel's Illuminate\Validation\ValidationServiceProvider with your own custom service provider in config/app.php and you're good to go.
It even works with Barry vd. Heuvel's laravel-ide-helper package.
return [
'providers' => [
//Illuminate\Validation\ValidationServiceProvider::class,
App\Providers\ValidationServiceProvider::class,
]
]
Going back to the example above, you only need to change the getValidatorInstance() method of your form request:
public function getValidatorInstance()
{
$validator = parent::getValidatorInstance();
$validator->indexAwareSometimes(
'items',
'special_delivery',
'required',
fn ($data, $index) => Arr::get($data, 'items.'.$index.'.asking_price') > 500 &&
Arr::get($data, 'items.'.$index.'.quantity') > 10
);
}

Related

Store json array in the api controller -laravel 8

I'm a little bit confused to how I can store data in my api controller,
My json looks like this:
[
{ a: 1 },
{ a: 2 }
]
I have my rules
$rules = [
'*.a' => 'required',
];
I have my validation
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
$error = $validator->messages()->toJson();
return response($error, 200);
}
and now there's my "problem": I would like to make a cleaner code.
My old option was pass the request->all() to a variable , json decode the contenent and make a foreach cycle to store data as here:
foreach ($datas as $data) {
$data = new rawData([
'a' => $data->a,
]);
$newrawData->save();
}
can I do a cleaner thing?? and How?
You can put your validation logic in a custom form request validator.
First, create the validator
php artisan make:request ExampleRequest
You can find the newly created a new class in app/Http/Requests/ExampleRequest.php and you can add your rules as follows
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ExampleRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'*.a' => 'required',
];
}
}
Now your controller method/action will not be executed unless the request passes the validation rules. you can use it in your controller method as follows:
In app/Http/Controllers/ExampleController.php
public function store(ExampleRequest $request)
{
// Your normal code.
}
that looks quite good. There is only one small thing I would have done differently. But it's a matter of taste.
foreach ($datas as $data) {
rawData::create($data->toArray());
}
And you can you use the request object directly to validate. or you implement an custom Request Object what you pass as parameter in your function.
$request->validate([
'*.a' => 'required',
]);

array_merge(): Expected parameter 1 to be an array, object given when use collection as event parameter

Right now I'm using Laravel 8.26 and Pusher 4.1.
This is my event:
class NotifSeller implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $fields;
public function __construct($fields)
{
$this->fields = $fields->toArray();
}
/**
* Get the channels the event should broadcast on.
*
* #return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
// return new PrivateChannel('notif-seller.'.$this->fields->seller_id);
return new PrivateChannel('notif-seller.'.$this->fields->seller_id);
}
public function broadcastWith()
{
$message = $this->fields;
return $message;
}
}
And this is my controller:
$t = new Transaksi();
$t->item_id = $request->item_id;
$t->seller_id = $request->seller_id;
$t->buyer_id = $request->buyer_id;
$t->category = 'merchandise';
$t->amount = $request->amount;
$t->save();
event(new NotifSeller($t));
return redirect()->back()->with('status', 'Success');
and it will show an error message
array_merge(): Expected parameter 1 to be an array, object given
Did I have wrong code here? I checked so many tutorial, in their tutorial they can use collection as event parameter, but when I tried it, it turn like this.
Sorry if my English is bad, I'm not an English native and this is my first time asking a question on Stack Overflow, so I hope you can understand this. Thanks in advance.
Most likely because you are using SerializesModels Laravel tries to serialize your public $fields; prop because it contains a model.
Different potential solutions:
Try typing the prop public array $fields;
Save the entire model there public Transaksi $fields;
Remove SerializesModels trait, technically not needed.
You can transform a collection to array by calling (chaining) the toArray() method in Laravel.
For example:
$collection = collect(['name' => 'Desk', 'price' => 200]);
$array = $collection->toArray();
Or
$array = collect(['name' => 'Desk', 'price' => 200])->toArray();
/* result
[
['name' => 'Desk', 'price' => 200],
]
*/

Laravel validator error for query parameter array filter[id]=1

I have an issue validating the request parameters to filter the records recieved in the query string
$validator = Validator::make($request->request->all(), [
'filter' =>
[
'array',
Rule::in(implode(',',$columns))
],
'page' =>'integer'
]);
Where the coulmns include id, name, size etc. and the API request has the following format
./findAll?filter[id]=1&filter[name]=test
I want to return a 400 response when any filter is passed which does not exist as a column.
You can use Validator extension to make your own validator:
In AppServiceProvider's put this code: (or in any provider)
public function boot(){
Validator::extend('keys_in', function ($attribute, $value, $arr, $validator) {
if (!is_array($value)) return false;
foreach (array_keys($value) as $key) {
if (!in_array($key, $arr)) return false;
}
return true;
});
Validator::extend('keys_in_columns', function ($attribute, $value, $table, $validator) {
if (!is_array($value)) return false;
$columns = Schema::getColumnListing($table);
foreach (array_keys($value) as $key) {
if (!in_array($key, $columns)) return false;
}
return true;
});
}
The custom validator Closure receives four arguments: the name of the $attribute being validated, the $value of the attribute, an array of $parameters passed to the rule, and the Validator instance.
Then in any controller you can use this two rules:
$validator = Validator::make($request->request->all(), [
'filter' =>['array','keys_in:' . implode(',',$columns)],
'page' =>'integer'
]);
Or use keys_in_columns shortcut which I defined above:
$validator = Validator::make($request->request->all(), [
'filter' =>['array','keys_in_columns:users'],
'page' =>'integer'
]);
don't forget to use use Illuminate\Support\Facades\Schema; and use Illuminate\Support\Facades\Validator; in Service Provider
Hope this helps you

Static model class is null in feature test laravel

I have this test in my feature folder and I've imported model on top of the class but it keeps failing and I think $event is null!
namespace Tests\Feature\Events;
use App\Models\Event;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class EventManagementTest extends TestCase
{
use RefreshDatabase;
/**
* #test
* #group event
* A basic feature test to check event registration
*
*/
public function an_event_can_be_registered()
{
$this->withoutExceptionHandling();
$response = $this->post('/events',$this->data());
$event = Event::first();
$this->assertCount(1,Event::all());
$response->assertRedirect('/events/' . $event->event_id);
}
private function data()
{
return[
'event_title' => 'Internet Businesses',
'event_location' => 'Milad Tower',
'event_description' => 'In this event Amin will present you the most recent methods in Internet Businesses',
'event_start_date' => '2020-06-01',
'event_end_date' => '2020-06-05',
];
}
...
}
And this is the results:
FAIL Tests\Feature\Events\EventManagementTest ✕ an event can be registered
Tests: 1 failed
Failed asserting that two strings are equal.
....
--- Expected
+++ Actual
## ##
-'http://localhost/events/1'
+'http://localhost/events'
these two URIs are different and I think that's because $event is null and I don't know why?!
UPDATE: I've added the Route and the controller:
Route::post('/events','Web\EventsController#store');
and the controller is:
public function store(){
$event = Event::create($this->validateRequest());
return redirect('/events/'.$event->event_id);
}
protected function validateRequest(){
return request()->validate([
'event_title' => 'required',
'event_location' => 'required',
'event_description' => 'required',
'event_start_date' => 'required',
'event_end_date' => 'required',
]);
}
Your do not use the standard primary id column, therefor you need to define it in your model. If it is not defined, it will not set it on create().
class Event extends Model {
protected $primaryKey = 'event_id';
}

Accessor that decrypts model value isn't working

I have a trait that uses accessors and mutators to encrypt model values:
trait Encryptable
{
public function getAttribute($key)
{
$value = parent::getAttribute($key);
if (in_array($key, $this->encryptable)) {
$value = Crypt::decrypt($value);
return $value;
} else {
return $value;
}
}
public function setAttribute($key, $value)
{
if (in_array($key, $this->encryptable)) {
$value = Crypt::encrypt($value);
}
return parent::setAttribute($key, $value);
}
}
Comments Model
protected $fillable = ['content','user_id','commentable_id', 'commentable_type'];
protected $encryptable = [
'content'
];
CommentController
public function storePostComment(Request $request, Post $Post)
{
$this->validate($request, [
'content' => 'required',
]);
$comment = $post->comments()->create([
'user_id' => auth()->user()->id,
'content' => $request->content
]);
dd($comment->content);
//return new CommentResource($comment);
}
What's happening is that when I pass the return new CommentResource($comment); gives me the comments content encrypted, but dd($comment->content); decrypts the comments content. How do I decrypt the entire comment object so I can output it in a resource?
Edit For CommentResource
class CommentResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'content' => $this->content,
'owner' => $this->owner,
];
}
}
Edit 2 for answer
Here's my attempt:
use App\Comment;
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class CommentResource extends JsonResource
{
public function __construct(Comment $resource)
{
$this->resource = $resource;
}
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'content' => $this->content,
'owner' => $this->owner,
];
}
}
Error:
Argument 1 passed to App\Http\Resources\CommentResource::__construct() must be an instance of App\Http\Resources\Comment, instance of App\Comment given, called in /Applications/MAMP/htdocs/my-app/app/Http/Controllers/Api/CommentController.php on line 31
Edit 3 (final edit)
Here's what I figured out:
I tried a bunch of various combinations along with #Edwin Krause answer. I have another model using this encryptable trait and outputting in a resource that works fine.
To give a bit more context to this question I found out there was a problem using assertJsonFragment in a test:
CommentsTest
/* #test **/
public function a_user_can_comment_on_a_post()
{
$decryptedComment = ['content'=>'A new content']
$response = $this->json('POST', '/api/comment/' . $post->id, $decryptedComment);
$response->assertStatus(201);
$response->assertJsonStructure([
'data' => [
'owner',
'content'
]
])
->assertJsonFragment(['content' => $decryptedContent['content']]);
}
assertJsonFragment was returning the encrypted content and therefore failing because it was being tested against the decrypted comments content.
I used dd(new CommentResource($comment)); in the controller to check to see if it the content was decrypting, it wasn't.
I tried various different things trouble shooting with dd() in the controller method and even testing in the browser. Still nothing. I added #Edwin Krause code and still nothing on dd()
I finally got lucky and got rid of dd() with #Edwin Krause and changing my controller to:
Working code combined with #Edwin Krause answer in my CommentResource
$comment = Comment::create([
'user_id' => auth()->user()->id,
'content' => $request->content,
'commentable_type' => 'App\Post',
'commentable_id' => $post->id,
]);
return new CommentResource($comment);
The tests went green. I tried dd(new CommentResource($comment)); and the content was encrypted still. The content output on the broweser and assertJsonFragment worked. I must've tried so many combinations to try and figure this out and I kind of just got lucky.
I'm unsure as to why this is the way it is, but I've already spent hours on this, so I can't troubleshoot why it's breaking. Maybe someone else can.
Just a suggestion to try and override the constructor of the JsonResource and typecast the $resource parameter to your Modelclass.
It work's for other things, not sure if it fixes your issue, that needs to be tested
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Comment;
class CommentResource extends JsonResource
{
public function __construct(Comment $resource)
{
$this->resource = $resource;
$this->resource->content = $resource->content;
}
....
Edit:
I Played around a bit more with the constructor and the modified version should actually work. I don't have any encrypted data to play with, but logically this should work.

Resources