I have a web route for manufactures module (Backed route that handle resource)
Route::resource('/admin/manufactures', App\Http\Controllers\Back\ManufacturerController::class);
I have create a create a ManufacturerRequest with a simple rule name=>required
and i want to use Laravel Test to test the resource (CRUD)
In my controller the store method is as follow
public function store(ManufacturerRequest $request)
{
//db
$request->validate();
Manufacturer::create($request->all());
}
I have a simple test
$response = $this->post('/admin/manufactures' ,
[
'_token' => csrf_token(),
'name' => 'test'
]);
$response->assertStatus(200);
which is return 403, but besides that the store Method takes ManufacturerRequest object that handles validation, and in the case of the test i pass an array because it only accepts array.
So how can I create a Test that "simulate" form and pass request to controller in order to test validation and CRUD
What you want to do is very easy and is also explained on the Documentation, it is very important that you fully read the documentation so you have a rough idea of what you can do with the framework, specially because you are new with it.
As you did not specify which version you are using, I will be using Laravel 8, but it is roughly the same across the board.
Based on your code:
Resource route
Route::resource('/admin/manufactures', ManufacturerController::class);
Controller
public function store(ManufacturerRequest $request)
{
//db
$request->validate();
Manufacturer::create($request->all());
}
You need to change your controller to:
public function store(ManufacturerRequest $request)
{
//db
Manufacturer::create($request->all());
}
Yes, just remove the $request->validate(); as the framework will automatically resolve the FormRequest and authorize and validate. If you read part of the validate explanation you will see this:
So, how are the validation rules evaluated? All you need to do is type-hint the request on your controller method. The incoming form request is validated before the controller method is called, meaning you do not need to clutter your controller with any validation logic.
So, when the first line of the controller is run, it means the FormRequest passed the authorization check and validated the input.
What you can also update on your controller is:
public function store(ManufacturerRequest $request)
{
//db
Manufacturer::create($request->validated());
}
See I have changed $request->all() with $request->validated(), validated will only return the fields you have a key on the FormRequest's rules, if you use all you will be passing everything you have on the request (also passing non-validated data and that is not good).
Before you try anything, I recommend you read my answer on this post, so you can have a clearer picture about testing.
So, you are getting a 403 maybe because you have a middleware asking for you to be logged in, and you did not use $this->actingAs().
Just because you did not share the FormRequest rules, I will just give a super small example. If you have a this rule inside:
'name' => ['required', 'string'],
What you can do to test that is:
public function test_manufacturer_is_created(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->post('/admin/manufactures', ['name' => $name = 'Manufacturer 1']);
$response->assertSuccessful();
$this->assertDatabaseHas(
'manufacturers',
[
'name' => $name
]
);
}
/**
* #depends test_manufacturer_is_created
*/
public function test_unauthorized_error_is_thrown_when_the_user_is_not_logged_in(): void
{
$response = $this->post('/admin/manufactures', ['name' => 'Manufacturer 1']);
$response->assertUnauthorized();
}
/**
* #depends test_manufacturer_is_created
* #dataProvider invalidDataProvider
*/
public function test_error_should_be_returned_when_invalid_data_is_sent($value, bool $deleteField): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->post(
'/admin/manufactures',
! $deleteField ? ['name' => $value] : []
);
$response->assertInvalid(['name']);
}
public function invalidDataProvider(): array
{
return [
'Missing name' => [null, true],
'Empty name' => ['', false],
'Null name' => [null, false],
'Array name' => [[], false],
'Number name' => [123, false],
'Boolean name' => [true, false],
];
}
Have in mind I used a lot of things in here:
I have tested if the normal insertion works, if it is checking the a valid name is input (FormRequest rules) and that if the user is not logged in it should throw an unauthorized exception.
I have used #depends, that is used to run tests ONLY if the dependant test passes, that way we can prevent running the "negative" tests just because the normal flow did not succeed, so it makes no sense to run the other ones and also get a "the test did not pass".
I have also used #dataProvider, that is used to share data with a test, so instead of copy-pasting the test a lot of times with data variation, you just vary what data you want the test to use.
Related
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.
I wonder if I should do form validation before retrieving input values or vice versa.
I usually do validation first as I see no benefit in trying to access input values that might not be valid. However, a coworker looked at my code recently and found it strange. Is there any correct order for these steps?
public function createGroups(Request $request)
{
$this->validate($request, [
'courses' => 'required_without:sections',
'sections' => 'required_without:courses',
'group_set_name' => 'required',
'group_number' => 'required|integer|min:1'
]);
$courses = $request->input('courses');
$sections = $request->input('sections');
$group_set_name = $request->input('group_set_name');
$group_number = $request->input('group_number');
Positioning the validation for your controller logic at the beginning of a method is probably the way to go here, as you have required parameters defined. If you receive data that does not fully satisfy the requirements, you produce a validation error back to the user. This follows the productive "Fail Fast" line of thinking: https://en.wikipedia.org/wiki/Fail-fast
It's also important that you're not using any data that hasn't passed your stringent requirements from validation. Data that fails validation should no longer be trusted. Unless there's some other reason you need to be, say, logging any incoming data from the frontend, the order here looks good to me.
I totally agree with #1000Nettles response, to elaborate a little bit more on his/her answer (who should be the accepted one): There isn't any need to continue with your business logic when the data doens't comply with your specifications. Let's say you expected a string of a N characters long, because you defined your database with that limitation (in order to optimize the db desing), will you try to persist it even when it'll throw an exception? Not really.
Besides, Laravel has a particular way to extract validation classes: Form Request. This are injected in controllers. When a call reach the controller it means that already passed the validation, if not, an 422error be returned.
Create a custom request and keep the mess out of your controller, it doesn't even hit your controller function if validation failed and can just grab the data in your controller if validation passed.
php artisan make:request GroupRequest
In app/Http/Requests/GroupRequest.php:
public function authorize()
{
// return true;
return request()->user()-isAdmin; // <-- example, but true if anyone can use this form
}
public function rules()
{
return [
'courses' => ['required_without:sections'],
'sections' => ['required_without:courses'],
'group_set_name' => ['required'],
'group_number' => ['required', 'integer', 'min:1'],
];
}
The best part is you can even manipulate the data in here (GroupRequest.php) after it has been validated:
public function validated()
{
$validated = $this->getValidatorInstance()->validate();
// EXAMPLE: hash password here then just use new hashed password in controller
$validated['password'] = Hash::make($validated['password']);
return $validated;
}
In your controller:
public function createUser(UserRequest $request) // <- in your case 'GroupRequest'
{
$validated = $request->validated(); // <-- already passed validation
$new_user = User::create($validated); // <-- password already hashed in $validated
return view('dashboard.users.show')->with(compact('user'));
}
In your case, if you use my GroupRequest block above, you can return to view in 1 line of code:
public function createGroups(GroupRequest $request)
{
return view('example.groups.show')->with($request->validated()); // <-- already an array
}
In you blade view file, you can then use your variables like {{ $group_set_name }} and {{ $group_number }}
Is there any benefit of using laravel requests classes for store and update methods in developing restful API's? Or do I have to make custom Validator::make response?
I have been facing difficulty in modifying the response format from failed requests as to follow some development standards requirements.
Can we modify the failed responses format from request class for API's?
I prefer to use independent from request class because there is at least one benefit: more clear code.
you can generate response as you wish like this (this is my solution, maybe there are more better solutions. i use this solution to return only one validation error not all. you can modify it as you wish):
in your Form request class add this method:
protected function failedValidation(Validator $validator)
{
$this->validator = $validator;
foreach ($validator->messages()->getMessages() as $key => $value) {
$first_messages_only[$key] = $value[0];
}
throw new ValidationException($first_messages_only);
}
and then in your Exception handler class, write this block of code in your render() method:
if ($exception instanceof ValidationException) {
$response = [
'status' => false,
'message' => trans('api.general.validation_not_passed'), // $exception->getMessage()
'data' => ['validation_errors' => $exception->validator]
];
return response()->json($response);
}
Since you asked its usage in API development then you can easily tell request class that you want json response by adding application/json header in your request then it will return json response.
Request class is best approach to validate incoming input from user which provides a lot of other features as well.
In Request class you can write validation rules for all request types e.g. get,post,put|patch or delete
You can allow or disallow anyone using authorize method based on your project logic.
You can write custom messages and send them custom error message bags.
If you write whole thing in a controller method then that will not be a good approach and difficult to manage while request class will make you comfortable while dealing with validations only.
protected $errorBag = 'custom_errors_bag'
public function authorize()
{
return true; //or any other logic here to authorize the user
}
public function rules()
{
switch ($this->method()){
case 'POST':
return [
'username' => 'required|string|max:20|unique:users',
//...
];
case 'PUT':
case 'PATCH':
return [
'username' => 'required|string|max:20|unique:users,id',
//...
];
case 'DELETE':[
'id' => 'required'
//...
];
default:break;
}
}
public function messages()
{
return [
'username.required' => 'Please provide username',
'username.unique' => 'Username must be unique',
//...
];
}
I am a newcomer to laravel. I have a controller ProductController like this
public function buy(Request $request, User $user) {
\Stripe\Stripe::setApiKey("sk_test_xxxxxxxxxxxxxxxxxxxxxxxx");
$token = $_POST['stripeToken'];
$charge = \Stripe\Charge::create([
'amount' => 100,
'currency' => 'aud',
'description' => 'Example charge',
'source' => $token,
]);
if ($charge->status === "succeeded") {
//-- Processing... --//
}
I would like to ask the more appropriate design style, should I put the part of STTRIE in other places, such as the validator.
If yes, it is to make a rule and a request than verify it in the validator ?
Can someone tell me how to use the rule in the request?
Creating a Stripe charge is not request validation. It's an API call to Stripe. So, it should definitely not stay in the validator.
You can have this logic in a controller for small apps, but for medium/large scale apps with abstraction (e.g. if you want to have the option later to change the payment provider from Stripe to say Braintree), it should be in a service class.
Also, never use $_POST directly. Use $request->input instead. As a thumb rule, if you have 2 ways to do something in code, always use the way that implements higher level libraries (libraries > then framework > then core PHP).
To write the validation using Laravel 5.5+
public function buy(Request $request, User $user)
{
// first define your rules
$rules = [
'amount' => 'required|numeric'
];
$validatedData = $request->validate($rules);
// The purchase is valid...
}
For Laravel 5.0 - 5.4:
public function buy(Request $request, User $user)
{
// first define your rules
$rules = [
'amount' => 'required|numeric'
];
$validatedData = $this->validate($rules);
// The purchase is valid...
}
The typical responsibilities of a controller in my opinion:
Take in a request
Return a response
I think it is probably fine to have one conditional or validation check.
I'm facing a little issue with Form Request Validation and how to handle it with one API route.
The resource that I need to create depends on an other resource.
(Here an EmailSettings belongs to a Tenant)
So the look of my route should be something like : /api/tenants/{id}/email_settings
And my request validation expects several fields including the tenantId :
public function rules() {
return [
'email' => 'bail|required|email|unique:email_settings,email',
'name' => 'bail|required',
'username' => 'bail|required',
'password' => 'bail|required'
'imapHost' => 'bail|required',
'imapPort' => 'bail|required',
'imapEncryption' => 'bail|required',
'imapValidateCert' => 'bail|required',
'smtpHost' => 'bail|required',
'smtpPort' => 'bail|required',
'smtpEncryption' => 'bail|required',
'tenantId' => 'bail|required',
];
}
And I send the request like this :
try {
const response = await this.tenantForm.post('/api/tenants')
let newTenant = helpers.getNewResourceFromResponseHeaderLocation(response)
let tenantId = parseInt(newTenant.id);
try {
await this.emailSettingsForm.post('/api/tenants/' + tenantId + '/email_settings')
this.requestAllTenants()
} catch ({response}) {
$('.second.modal').modal({blurring: true, closable: false}).modal('show');
}
} catch ({response}) {
$('.first.modal').modal({blurring: true}).modal('show');
}
So the tenantId is passed as a parameter and not in the request body to respect the REST convention.
But the problem is in my Controller, when I merge the data to create the resource, the validation has already took place only on body data before the merge.
public function store(EmailSettingValidation $request, $tenant_id) {
$emailSetting = $this->emailSettingService->create(
array_merge($request->all(), compact($tenant_id))
);
return $this->response->created($emailSetting);
}
So what is the best way to handle it properly ?
Pass the id in the body ? Seems messy
Use Validator to validate manually ? I would prefer to keep Form Validation
Remove the tenantId rule and check it manually ?
Any suggestions ?
Thank you
If you define your api route like this:
Roue::post('tenants/{tenant}/emails_settings', 'Controller#store');
and modify your controller method to type-hint the model with a variable name that matches your route definition:
public function store(EmailSettingValidation $request, Tenant $tenant) {}
then Laravel will automatically find the Tenant by ID and inject it into the controller, throwing a ModelNotFoundException (404) if it doesn't exist. That should take care of validating the id.
Authorization is another matter.
So the solution I found to trigger 404 is the following :
Remove the tenantId from EmailSettings Validation
Add a provider to add a custom error when the exception 'ModelNotFoundException' occurs like here
No query results for model in Laravel with Dingo - how to make a RESTful response on failure?
Try to throw this Exception with the findOrFail method if invalid ID :
public function store(EmailSettingValidation $request, $tenant_id) {
Tenant::findOrFail($tenant_id);
$emailSetting = $this->emailSettingService->create(
array_merge($request->all(), ['tenantId' => $tenant_id])
);
return $this->response->created($emailSetting);
}
Travis Britz and Guillaumehanotel each have half of your answer, but you're still missing a detail.
From Travis Britz- Yes, include the tenant_id on the URI so it gets injected into the controller.
From Guillaumehanotel- Also used the Eloquent findOrFail in that Id in your Controller (or whatever class the Controller is leveraging to do this, like a Repository or Service class).
The last piece you're missing though is handling the error. You can do this in the Controller if you like, but I generally like making it a rule for my entire system that the Illuminate\Database\Eloquent\ModelNotFoundException Exceptions that come out of findOrFail() should always result in a 404.
Go to app/Exceptions/Handler.php. I'm pretty sure Laravel auto-generates a meat and potatoes version of this file for you, but if you don't already have one, it should look something like this:
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
/**
* Class Handler
* #package App\Exceptions
*/
class Handler extends ExceptionHandler
{
/**
* Render an exception into an HTTP response.
*
* For our API, we need to override the call
* to the parent.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $e
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $error)
{
$exception = [
'title' => 'Internal Error',
'message' => $error->getMessage();
];
$statusCode = 500;
$headers = [
'Content-Type', 'application/json'
];
return response()->json($exception, $statusCode, $headers, JSON_PRETTY_PRINT);
}
}
Laravel basically has a system-wide try/catch that sends all errors through here first. That's how errors get rendered into something the browser can actually interpret when you're in debug-mode, rather than just kill the process outright. This also gives you the opportunity to apply a few special rules.
So all you need to do is tell Handler::render() to change the default error code that occurs when it sees the type of error that can only come from findOrFail(). (This kind of thing is why it's always good to make your own 'named exceptions', even if they do absolutely nothing except inherit the base \Exception class.)
Just add this just before render() returns anything:
if ($error instanceof Illuminate\Database\Eloquent\ModelNotFoundException) {
$statusCode = 404;
}