Hiding fields in API Resources Using Gates in Laravel - laravel

I have a Product API resource in my application like so
/**
* Transform the resource collection into an array.
*
* #param Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'desc' => $this->desc,
'color' => $this->color,
'amount' => $this->amount,
'available' => $this->available,
'createdAt' => $this->created_at,
'updatedAt' => $this->updated_at,
];
}
I have few roles in my application, like admin, viewer.
When admin access the api, the api returns all fields but when the viewer access the api it returns only limited fields.
How can I handle this using Gates & Policies?
Can I do something like this
'createdAt' => $this->when($this->authorize('product.list'), $this->created_at)

You could use an Eloquent Accessor in your Product model:
public function getCreatedAtAttribute($createdAt)
{
if (Gate::allows('see-product-details', $this)) {
return $createdAt;
} else {
return null;
}
}
Of course you also have to write the see-product-details gate.
Otherwise this may work as well (not tested):
public function getCreatedAtAttribute($createdAt)
{
if ($this->authorize('view', [Product::class, $this])) {
return $createdAt;
} else {
return null;
}
}

Related

Json response returns nested object

I am new in vue laravel and trying to get data from API controller with json response. I feel its not in best practices that I get data in nested object. Whats the best way to implement this so that I can get this.user_data.designations = All_Designation(with res.data) and some other like this.user_data.employment = All_Employments, etc.
Also when I try to send data via props I can access it as
designations: this.data.map(d => ({label: d.designation_name, value: d.designation_name, id:d.id}))
I want other data also so I guess it should be like this.data.designations, this.data.employment. This made me confused. how can I manage everything without changing things in trait?
This is my controller method:
public function index()
{
$designations = Designation::all();
if (!$designations->isEmpty()) {
$this->responseData['response'] = 1;
$this->responseData['message'] = "Designations has been Recieved.";
$this->responseData['data'] = $designations;
$this->status = 200;
}
return $this->apiResponse();
}
Api trait:
protected $responseData = [
'response' => 0,
'message' => "Not Found",
'data' => null
];
/**
* #var int
*/
protected $status = 404;
/**
* #return mixed
*/
public function apiResponse()
{
return response()->json($this->responseData, $this->status);
}
Axios Call:
this.$store.dispatch('employee/getDesignations' )
.then(res => { this.user_data.designations = res.data.data })
.catch(err => {
console.error(err)
})
Just return response like this
return response()->json(['response' => ['status' => true, 'data' => $apiResponse]], JsonResponse::HTTP_OK);
HTTP_OK =200 and its a enum type of use Illuminate\Http\JsonResponse;
status =true mean you returning the data .
for example if your data is empty then return something like this
return response()->json(['response' => ['status' => false, 'message' => 'Unable to find data ']], JsonResponse::HTTP_BAD_REQUEST);
just return this .
return response()->json($data, JsonResponse::HTTP_OK);
hope this will hel you

How can I feature test for a valid duration using Laravel's validation helpers?

I am trying to write a Feature test that checks that the provided end_time does not occur before the provided start_time. I've spent the last few days; I think I've seen about every SO post and still having trouble getting it through my head.
I have been able to successfully test/validate based on a condition:
MyTest.php
/** #test */
public function a_blackout_that_does_not_close_the_location_requires_an_end_time()
{
$table = 'my_table';
$blackout = factory(Blackout::class)->create([
'closed' => 0,
]);
$this->assertDatabaseHas($table, [
'end_time' => $blackout->end_time,
]);
}
MyController.php
public function store(Request $request) {
$attributes = $request->validate([
...
'date' => ['required'],
'closed' => ['nullable'],
'start_time' => ['required_if:closed,0'], // works great
'end_time' => ['required_if:closed,0'], // works great
]);
$blackout = new Blackout($attributes);
...
}
Now I want to make sure that any provided end_time is not before a provided start_time.
MyTest.php
/** #test */
public function a_blackout_end_time_cannot_be_before_the_start_time()
{
$blackout = factory(Blackout::class)->raw([
'closed' => 0,
'start_time' => '08:00:00',
'end_time' => '07:00:00',
]);
$response = $this->post('api/v1/blackouts', $blackout)
->assertSessionHasErrors('end_time');
}
This is what I'm trying to do:
MyController.php
public function store(Request $request) {
$attributes = $request->validate([
...
'date' => ['required'],
'closed' => ['nullable'],
'start_time' => ['required_if:closed,0'],
'end_time' => ['required_if:closed,0|after:start_time'],
]);
$blackout = new Blackout($attributes);
...
}
I keep getting an error in my test:
1) Tests\Feature\BlackoutsTest::a_blackout_end_time_cannot_be_before_the_start_time
Session is missing expected key [errors].
Failed asserting that false is true.
That suggests that it isn't getting the correct error(s) set/returned from the validation. I've tried a bunch of ways to format the start_time and end_time to try to compare.
Further in my controller, I'm able to compare the times relatively easily:
MyController.php
...
$start_time = strtotime($blackout->start_time);
$end_time = strtotime($blackout->end_time);
if ($end_time <= $start_time) {
// Invalid duration
} else {
$blackout->save();
}
...
I am struggling how to validate through the validator so I can leverage the error message(s) better.
Thank you for any suggestions!
EDIT
Thank you #Thomas Van der Veen. Clearly, I'm learning so while looking through the json examples I believe I'm doing a number of things wrong (big surprise).
I've updated my test to now use postJson instead of post. I'm checking for a 422 response and that there should be an end_time error.
/** #test */
public function a_blackout_end_time_cannot_be_before_the_start_time()
{
$blackout = factory(Blackout::class)->raw([
'closed' => 0,
'start_time' => '08:00:00',
'end_time' => '07:00:00',
]);
$response = $this->postJson('api/v1/blackouts', $blackout)
->assertStatus(422)
->assertJsonValidationErrors('end_time');
}
MyController.php
$attributes = $request->validate([
...
'date' => ['required'],
'closed' => ['nullable'],
'start_time' => ['required_if:closed,0|date|'],
'end_time' => ['required_if:closed,0|date|after:start_time'],
]);
...
$blackout->save();
return response()->json(['data' => $blackout], 201);
The test response is:
1) Tests\Feature\BlackoutsTest::a_blackout_end_time_cannot_be_before_the_start_time
Expected status code 422 but received 201.
Failed asserting that false is true.
Which tells me my validation is bad since it's getting through to a 201.
Thank you both for your help. I feel like I'm starting to talk myself in circles & I'm making this way harder than it is.
Thank you both for your suggestions! They both helped get me to this answer. Although I believe I am still writing/executing tests incorrectly
Here is the solution I ended up with, If there is a better way please let me know so I can update my answer.
I ended up writing a custom validation rule and passing in my time_start to compare the two times.
MyTest.php
/** #test */
public function a_blackout_end_time_cannot_be_before_the_start_time()
{
$blackout = factory(Blackout::class)->raw([
'closed' => 0,
'time_start' => '08:00',
'time_end' => '07:00',
]);
$response = $this->postJson('api/v1/blackouts', $blackout)
->assertStatus(422)
->assertJsonValidationErrors('time_end');
}
MyController.php
public function store(Request $request)
{
$attributes = $request->validate([
...
'date' => ['required', new DateFormatRule], // Custom rule
'closed' => 'nullable',
'time_start' => 'required_if:closed,0',
'time_end' => ['required_if:closed,0', new TimeDurationRule($request->time_start)],
]);
$blackout = new Blackout($attributes);
...
}
TimeDurationRule.php
**
* Create a new rule instance.
*
* #return void
*/
public function __construct($time_start)
{
$this->time_start = $time_start;
}
/**
* Determine if the validation rule passes.
*
* #param string $attribute
* #param mixed $value
* #return bool
*/
public function passes($attribute, $value)
{
return strtotime($value) > strtotime($this->time_start);
}
/**
* Get the validation error message.
*
* #return string
*/
public function message()
{
return 'Time duration is invalid.';
}
In my test I am getting a 422 response as expected. I can verify things are working by changing the name of the expected error from time_end to time_end123 and I'll get an error -- saying It was expecting time_end not `time_end123.

Find data before validate form request laravel

I want to update the data using the request form validation with a unique email role, everything works normally.
Assume I have 3 data from id 1-3 with url:
127.0.0.1:8000/api/user/update/3
Controller:
use App\Http\Requests\Simak\User\Update;
...
public function update(Update $request, $id)
{
try {
// UPDATE DATA
return resp(200, trans('general.message.200'), true);
} catch (\Exception $e) {
// Ambil error
return $e;
}
}
FormRequest "Update":
public function rules()
{
return [
'user_akses_id' => 'required|numeric',
'nama' => 'required|max:50',
'email' => 'required|email|unique:users,email,' . $this->id,
'password' => 'required',
'foto' => 'nullable|image|max:1024|mimes:jpg,png,jpeg',
'ip' => 'nullable|ip',
'status' => 'required|boolean'
];
}
but if the updated id is not found eg:
127.0.0.1:8000/api/user/update/4
The response gets The email has already been taken.
What is the solution so that the return of the data is not found instead of validation first?
The code looks like it should work fine, sharing a few things below that may help.
Solution 1: Check if $this->id contains the id you are updating for.
Solution 2: Try using the following changes, try to get the id from the URL segment.
public function rules()
{
return [
'user_akses_id' => 'required|numeric',
'nama' => 'required|max:50',
'email' => 'required|email|unique:users,email,' . $this->segment(4),
'password' => 'required',
'foto' => 'nullable|image|max:1024|mimes:jpg,png,jpeg',
'ip' => 'nullable|ip',
'status' => 'required|boolean'
];
}
Sharing one more thing that may help you.
Some person uses Request keyword at the end of the request name. The Update sounds generic and the same as the method name you are using the request for. You can use UpdateRequest for more code readability.
What I understand from your question is, you need a way to check if the record really exists or not in the form request. If that's the case create a custom rule that will check if the record exists or not and use that rule inside your request.
CheckRecordRule
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class CheckRecordRule implements Rule
{
protected $recordId;
public function __construct($id)
{
$this->recordId = $id;
}
public function passes($attribute, $value)
{
// this will check and return true/false
return User::where('id', $this->recordId)->exists();
}
public function message()
{
return 'Record not found.';
}
}
Update form request
public function rules()
{
return [
'email' => 'required|email|unique:users,email,' . $this->id.'|'. new CheckRecordRule($this->id),
];
}
So when checking for duplicate it will also check if the record really exists or not and then redirect back with the proper message.

Laravel Resources many different requests for the same Resource instance

I'm using Laravel Resource as a tool to sending API data. However, when I make just a basic controller with CRUD methods - all methods are different, for example
public function show(Offer $offer)
{
return $offer;
}
public function index()
{
return Offer::orderBy('created_at','desc')->get();
}
So I created OfferResource and for show method it's:
return new OfferResource($offer);
class OfferResource extends Resource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'body' => $this->body,
'title' => $this->title,
'specialities' => $this->specialities()->pluck('speciality_id'),
'specialities_name' => $this->specialities()->pluck('name')
];
}
}
how should I call OfferResource for method index()?
Should I create another Resource "Controller"? or is there a different approach to use the same OfferResource?
You can use collection() method to use this resource for index -
return OfferResource::collection(Offer::orderBy('created_at','desc')->get());
And it is best to use a resource for multiple purpose if you can use rather than creating new one for each purpose.
As we all know the concepts of code re-use, the very basic foundation of OOP, we want to write ones but use multiple times. That's why we write Resource class to use whenever we need to transform our data to a array in predefined format. And requirement always change, so it would be much better to use resource to keep up with changes.
A resource class represents a single model that needs to be transformed into a JSON structure.
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
Laravel also provide a way to use this method to transform a collection of resources using collection() method.

Laravel action when form validation generates error

I am working with a form request file like this:
ProjectCreateRequest.php
public function rules()
{
$project_name = $this->project_name;
$meta_activity = $this->meta_activity;
return [
'project_name' => 'required|max:255|unique:projects',
'customer_name' => 'required|max:255',
'otl_project_code' => 'sometimes|max:255|unique:projects,otl_project_code,NULL,id,meta_activity,'.$meta_activity,
'estimated_start_date' => 'date',
'estimated_end_date' => 'date',
'LoE_onshore' => 'numeric',
'LoE_nearshore' => 'numeric',
'LoE_offshore' => 'numeric',
'LoE_contractor' => 'numeric',
'revenue' => 'numeric',
'win_ratio' => 'integer'
];
}
There is the otl_project_code that must be unique with the meta_activity.
In case someone enters a pair of otl_project_code and meta_activity that already exists, it goes back to the create page with the error written below.
I would like to get instead that in the controller, I can catch this information, do something on the database then redirect to an update url.
Because I am working with a form validation request file, everything is entered in my controller like this:
public function postFormCreate(ProjectCreateRequest $request)
and I don't know how to catch this specific error in my controller to execute some actions with all the fields I submitted and not go back to the create page. Of course, this needs to happen only when there is the specific error I mentionned above.
Override the FormRequest response function in your ProjectCreateRequest:
/**
* Get the proper failed validation response for the request.
*
* #param array $errors
* #return \Symfony\Component\HttpFoundation\Response
*/
public function response(array $errors)
{
if ($this->expectsJson()) {
return new JsonResponse($errors, 422);
}
return $this->redirector->to($this->getRedirectUrl())
->withInput($this->except($this->dontFlash))
->withErrors($errors, $this->errorBag);
}
That's the public response on the FormRequest class so you can write your own logic to perform DB queries and redirect where needed.

Resources