To transform a database entity to an API response Laravel support resources, eg. UserResource extends JsonResource. The resource allows me to cleanly define which fields from the entity should be included in the response, how to transform them etc.
Is there a similar functionality for requests? My requests typically look like this:
public function create(JsonRequest $request): UserResource
{
$data = $request->json()->all();
/* Remove, transform, add request fields etc. */
$user = User::create($data);
$user->save();
return new UserResource($user);
}
In our case we have a legacy database behind a modern API so there are a number of fields that need to transformed, renamed etc. before pushing them into the entity class. The fields differ from request to request but the steps are very similar. Is there a less boilerplate-y way to do this, something similar to how resources transform entities to responses?
Something like:
class UserRequest extends JsonRequest {
public function fromArray(JsonRequest $request) {
…
}
}
Then the request could look like this:
public function create(UserRequest $request): UserResource
{
$user = User::create($request);
$user->save();
return new UserResource($user);
}
I suppose, that most of your problems can solve form request. See example below
Form request class:
namespace App\Http\Requests;
use Carbon\Carbon;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
class TestRequest 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 [
'date' => 'required|date_format:Y-m-d H:i:s',
'name' => 'required|string',
];
}
// here you can specify custom error messages
public function messages()
{
return [
'date.required' => 'No date specified',
'date.date_format' => 'Invalid date format',
'name.required' => 'No name specified',
'name.string' => 'Invalid name format',
];
}
// here you can implement some data mapping before validation
protected function validationData()
{
return $this->transform($this->all());
}
// some data transformation logic
// You can place it anywhere in your applciation services
protected function transform($input)
{
$transformed = [];
foreach ($input as $field => $value) {
if ($field == 'name') {
$value = strtoupper($value);
} elseif ($field == 'date') {
$value = Carbon::parse($value)->toDateTimeString();
}
$transformed[$field] = $value;
}
return $transformed;
}
public function failedValidation(Validator $validator)
{
// here you can implement custom validation failure
parent::failedValidation($validator);
}
}
Here is my test route: Route::get('/test', 'TestController#index');
And controller:
use App\Http\Requests\TestRequest;
class TestController extends Controller
{
public function index(TestRequest $request)
{
return response()->json($request->validated());
}
}
So, then requesting route: curl -H 'Accept: application/json' 'http://localhost:8000/test?date=01.01.2019&name=petya'
And getting response: {"date":"2019-01-01 00:00:00","name":"PETYA"}
And dont be shy to see source code of request and form request, cause of not all methods you wish are described in docs. Hope this helps
Related
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',
]);
I am working on a Laravel project. I am writing integration/ feature tests for my application. I am now writing a test where I need to assert the data passed to the email notification and the data passed to its view. I found this link to do it, https://medium.com/#vivekdhumal/how-to-test-mail-notifications-in-laravel-345528917494.
This is my notification class
class NotifyAdminForHelpCenterCreated extends Notification
{
use Queueable;
private $helpCenter;
public function __construct(HelpCenter $helpCenter)
{
$this->helpCenter = $helpCenter;
}
public function via($notifiable)
{
return ['mail'];
}
public function toMail($notifiable)
{
return (new MailMessage())
->subject("Help Center registration")
->markdown('mail.admin.helpcenter.created-admin', [
'helpCenter' => $this->helpCenter,
'user' => $notifiable
]);
}
}
As you can see in the code, I am passing data to mail.admin.helpcenter.created-admin blade view.
This is my test method.
/** #test */
public function myTest()
{
$body = $this->requestBody();
$this->actingAsSuperAdmin()
->post(route('admin.help-center.store'), $body)
->assertRedirect();
$admin = User::where('email', $body['admin_email'])->first();
$helpCenter = HelpCenter::first();
Notification::assertSentTo(
$admin,
NotifyAdminForHelpCenterCreated::class,
function ($notification, $channels) use ($admin, $helpCenter) {
$mailData = $notification->toMail($admin)->toArray();
//here I can do some assertions with the $mailData
return true;
}
);
}
As you can see my comment in the test, I can do some assertions with the $mailData variable. But that does not include the data passed to the view. How can I assert or get the data or variables passed to the blade view/ template?
As you can see here, there is a viewData property on the MailMessage class which contains all the data passed to the view, no need to turn the notification into an array.
$notification->toMail($admin)->viewData
So it would be something like this in your case:
/** #test */
public function myTest()
{
$body = $this->requestBody();
$this->actingAsSuperAdmin()
->post(route('admin.help-center.store'), $body)
->assertRedirect();
$admin = User::where('email', $body['admin_email'])->first();
$helpCenter = HelpCenter::first();
Notification::assertSentTo(
$admin,
NotifyAdminForHelpCenterCreated::class,
function ($notification, $channels) use ($admin, $helpCenter) {
$viewData = $notification->toMail($admin)->viewData;
return $admin->is($viewData['user']) && $helpCenter->is($viewData['helpCenter']);
}
);
}
Trying to use Laravel Nova with Spatie Media Library. I created upload field like this:
Image::make('Logo')
->store(function (Request $request, $model) {
$model->addMediaFromRequest('logo')->toMediaCollection('manufacturers');
}),
Seams ok, but Nova still trying to save file name to "logo" column in manufacturers table.
Original excample to customize this field was:
File::make('Attachment')
->store(function (Request $request, $model) {
return [
'attachment' => $request->attachment->store('/', 's3'),
'attachment_name' => $request->attachment->getClientOriginalName(),
'attachment_size' => $request->attachment->getSize(),
];
})
I found a work around by setting an empty mutator on the model. In your case it would be:
class Manufacturer extends Model implements HasMedia
{
use HasMediaTrait;
public function setLogoAttribute() {}
//...
}
Here's an example of my entire implementation. Note that currently with Nova 1.0.6, the preview() method is not working, it's returning the thumbnail() url.
App/GalleryItem
class GalleryItem extends Model implements HasMedia
{
use HasMediaTrait;
public function setImageAttribute() {}
public function registerMediaConversions(Media $media = null)
{
$this->addMediaConversion('thumbnail')
->fit(Manipulations::FIT_CROP, 64, 64);
$this->addMediaConversion('preview')
->fit(Manipulations::FIT_CROP, 636, 424);
$this->addMediaConversion('large')
->fit(Manipulations::FIT_CONTAIN, 1920, 1080)
->withResponsiveImages();
}
public function registerMediaCollections()
{
$this->addMediaCollection('images')->singleFile();
}
}
App/Nova/GalleryItem
class GalleryItem extends Resource
{
public static $model = 'App\GalleryItem';
public static $with = ['media'];
public function fields(Request $request)
{
return [
Image::make('Image')
->store(function (Request $request, $model) {
$model->addMediaFromRequest('image')->toMediaCollection('images');
})
->preview(function () {
return $this->getFirstMediaUrl('images', 'preview');
})
->thumbnail(function () {
return $this->getFirstMediaUrl('images', 'thumbnail');
})
->deletable(false);
];
}
}
As with Nova 3 (and Laravel 8) you need to return true from the fillUsing or store method:
File::make('Attachment')
->store(function (Request $request, $model) {
$model->addMediaFromRequest('logo')->toMediaCollection('manufacturers');
return true;
// This will tell nova that you have taken care of it yourself.
})
As soon as you return anything but true nova will assume, that it needs to save something to the database. This leads to an error if the field does not exist in db (as to expect with spatie-medialibrary) or it will overwrite your precious data if the field exists but serves another purpose.
Nova allows you to return true from the callback to indicate that the processing is complete and that it shouldn't set any attributes itself.
This is the code that runs the callback:
protected function fillAttribute(NovaRequest $request, $requestAttribute, $model, $attribute)
{
//...
$result = call_user_func($this->storageCallback, $request, $model);
if ($result === true) {
return;
}
if (! is_array($result)) {
return $model->{$attribute} = $result;
}
foreach ($result as $key => $value) {
$model->{$key} = $value;
}
}
So either true or any empty array will achieve the same thing, but personally feels clearer to do the former.
Image::make('Logo')
->store(function (Request $request, $model) {
$model->addMediaFromRequest('logo')->toMediaCollection('manufacturers');
return [];
}),
Maybe returning an empty array prevent nova from saving the name.
I am validating a user registration form
for EX: (Requests validation class)
class UserCreateAccountRequest extends Request
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'email' => 'required|unique:users,email',
'password' => 'required|min:6|max:32'
];
}
}
If the email is already registered I want to redirect to the password reset page.
How can i achieve this using request validation class without putting the validation in to the controller?
Inside the authorize function check whether the email already exists,
public function authorize()
{
$email = Request::input('email');
$result = User::where('email',$email)
->exists();
if($result)
{
return false;
}
return true;
}
If this returns false, forbiddenResponse function will be fired, therefore you need to include that function and inside that you can redirect to the page you want. This function will only returns false if the email is already exist.
public function forbiddenResponse()
{
return redirect('password_reset');
}
That's all.For your reference following is the structure of the Request class,
<?php namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Response;
class FriendFormRequest extends FormRequest
{
public function rules()
{
return [
'first_name' => 'required',
'email_address' => 'required|email'
];
}
public function authorize()
{
// Only allow logged in users
// return \Auth::check();
// Allows all users in
return true;
}
// OPTIONAL OVERRIDE
public function forbiddenResponse()
{
// Optionally, send a custom response on authorize failure
// (default is to just redirect to initial page with errors)
//
// Can return a response, a view, a redirect, or whatever else
return Response::make('Permission denied foo!', 403);
}
// OPTIONAL OVERRIDE
public function response()
{
// If you want to customize what happens on a failed validation,
// override this method.
// See what it does natively here:
// https://github.com/laravel/framework/blob/master/src/Illuminate/Foundation/Http/FormRequest.php
}
}
When validating a form with a request class you can manually validate the data using the validate() method but what do you return back I've tried return $this and return $this->errors but it just shows SQL integrity constraint duplicate entry which is correct but it doesn't show my form with the errors. When doing validation inside the controller you return the model and the errors but what do I return and set errors on validate method in the request class.
Request Class:
namespace App\Http\Requests;
use App\Http\Requests\Request;
use Auth;
class ProductRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
protected $action;
public function authorize()
{
if(Auth::check()) {
return true;
}
}
public function validate() {
$v = \Validator::make(parent::all(), $this->rules());
if ($v->passes()) return true;
$this->errors = $v->messages();
// tried returning $this; and $this->errors
return false;
}
public function all()
{
$data = parent::all();
if( $data['slug'] === '') {
// if the slug is blank, create one from title data
$data['slug'] = str_slug( $data['title'], '-' );
}
return $data;
}
public function messages()
{
}
public function rules() {
}
}
your rule method is empty your not validating any thing the error you got is an SQL exception not a validation error.