REST principle of PUT method in Laravel - php

I am preparing a common and current CRUD-type REST API with the users model that laravel brings by default
<?php
namespace App\Models;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
class User extends Authenticatable implements MustVerifyEmail {
use HasApiTokens, HasFactory, Notifiable, CanResetPassword;
/**
* The attributes that are mass assignable.
*
* #var string[]
*/
protected $fillable = [
'name',
'email',
'password'
];
/**
* The attributes that should be hidden for serialization.
*
* #var array
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* #var array
*/
protected $casts = [
'email_verified_at' => 'datetime'
];
}
and the routes I generate with Route::apiResource
Route::apiResource('users', UserController);
Since I want to find the user by the email or the id I make a link or explicit injection of the route parameter in the RouteServiceProvider inside the method boot with Route::bind
Route::bind('user', function($userId){
$user = User::where(fn($query) => $query->where('id', $userId)->orWhere('email', $userId))->first();
if(request()->method() === 'PUT') return $user;
return $userId;
});
This is where the main topic comes in, this method (Route::bind ()) should return the instance of a class, if not, it will return an ExceptionModelNotFound 404 - not found, but I would like to be able to receive the null in my controller and thus validate depending on the method (PUT or PATCH) whether to create the new record (which according to REST principles should be able to do a resource if there is no match) or just update an existing one.
My update method of UserController is as follows
public function update(Request $request, User $user){
$input = $request->all();
$method = $request->method();
if($method === 'PUT') $request->validate($this->rules);
$update = [
'PUT' => fn() => ($user) ? $user->update($input) : $user = $user::create($input),
'PATCH' => function() use($user, $input){
foreach ($input as $key => $val){
$user->$key = (!empty($val)) ? $val : $user->$key;
}
$user->save();
return $user;
}
];
return response()->json($update[$method]());
}
Previously I had it without model injection and the method ùpdate worked, something like this
public function update(Request $request, $user){
But due to the explicit injection this no longer takes the parameter, besides that I am only interested in this behavior when updating since this in the rest of the methods helps in the reduction of code and it is fine to handle the 404 - Not found.
I don't know if I'm failing at something or maybe laravel provides a better way to do it which I don't know.
Thanks in advance.
PS: The code is a bit abstracted, it looks a bit messy because I tried to simplify it.

You are not making it easier on yourself by creating a single controller action for both updating and creating users. I have never done it this way, and always define the REST routes like so:
PUT /user/1 = UPDATE
POST /user = CREATE
You should know on the frontend whether you are making a new user or updating an existing one (simply check for an id element for instance).
Also, your current route bind is not the best logic, since that $user query is always executed, even when it is not a PUT request. Nevertheless that code can be simplified much further if you just firstOrFail() that user since you should be separating the functionality into 2 routes anyway.
Personally I never use PATCH, since my PUT routes allow partial updates as well (e.g. only sending an email property update).

Related

How to validate by default a parameter in a laravel api route

A question has arisen.
Suppose I have an api route that is
get: api/v1/conferences/{conference_id}/languages
The purpose would be to get the languages linked to this conference.
My program, almost always starts with conferences/{conference_id} so the conference_id must always be real. In case the conference_id does not exist, I should throw an exception.
I want to do this without putting any logic in the controllers or any class that has any particular logic of mine. I would like it to be validated by default from the laravel kernel, is that possible?
I mean, i want that every time somebody access to a route which starts with conferences/{conference_id} the program would be able to check if this id is real
Thanks
Use exists in validation rules like:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ConferencesRequest extends FormRequest
{
/**
* #inheritDoc
*/
public function all($keys = null)
{
$data = parent::all();
$data['conference_id'] = $this->route('conference_id');
return $data;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'conference_id' => ['required', 'integer', 'exists:' . App\Models\Conference::class . ',id'],
];
}
}
Or, if you dont want to use a FormRequest class, use this:
p.s. param should be changed to conference instead of conference_id:
use App\Models\Conference;
Route::get('conferences/{conference}/languages', function (Conference $conference) {
//...
});
See: https://laravel.com/docs/9.x/routing#route-model-binding

How should I correctly construct my unit test in Laravel

I am quite new to unit testing and could do with a bit of guidance. I am trying to write a simple unit test using the Factory pattern in Laravel and phpunit for an application that allows you to add Companies to a database.
I have a Company Model Company.php, a Factory class which uses it CompanyFactory.php, and finally the unit test itself CompaniesTest.php.
Models/Company.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Company extends Model
{
use HasFactory;
protected $table = 'companies';
protected $fillable = [
'name',
'email'
];
}
Database/Factories/CompanyFactory.php
namespace Database\Factories;
use App\Models\Company;
use Illuminate\Database\Eloquent\Factories\Factory;
class CompanyFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* #var string
*/
protected $model = Company::class;
/**
* Define the model's default state.
*
* #return array
*/
public function definition()
{
return [
'name' => $this->faker->name,
'email' => $this->faker->email,
'created_at' => now(),
'updated_at' => now(),
];
}
}
Tests/Feature/CompaniesTest.php
namespace Tests\Unit;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use App\Models\Company;
use Tests\TestCase;
use Illuminate\Support\Str;
use Illuminate\Foundation\Testing\WithFaker;
class CompanyTest extends TestCase
{
use WithFaker, DatabaseTransactions;
public function createCompany($name = NULL)
{
if ($name == NULL) $name = Str::random(6);
$company = Company::factory()->create([
'name' => 'TestName_'.$name
]);
return $company->id;
}
/** #test */
public function company_can_be_created()
{
$name = Str::random(6);
//Create a company
$company_id = $this->createCompany($name);
//Check whether the company name exists in the database
$this->assertDatabaseHas('companies', [
'name' => 'TestName_'.$name
]);
}
}
The test seems to work, but it feels like I might have over-complicated it and probably not followed the correct conventions.
What would be a better way to structure it?
The test looks ok, but what are you actually testing? It seems to me that this test is testing the framework's code, which is actually not what you should do.
Don't test a factory, use it to prepare the data needed before each test. And then run your actual code which you want to test, and assert results.
Update: Continue with CompanyVerifier (see comments). Suppose company can be valid and non-valid. Valid companies can be verified. Then a test may look like:
/** #test */
public function test_valid_company_can_be_verified()
{
// here use a CompanyFactory with some pre-defined data to create "valid" company
$validCompany = $this->createValidCompany();
// here goes the actual code of SUT (system under test)
$verifier = new CompanyVerifier();
$result = $verifier->verify($validCompany);
// here check results
$this->assertTrue($result);
}
The good practice for testing is named AAA (arrange-act-assert). Here the creation of a company with some state is an "arrange" stage. Running tested code is "act". And assert is "assert".
Factories are just a helper for "arrange" stage.

How to implement owen-it/laravel-auditing in a model

I want to create an audit trail in my model. I already installed owen-it/laravel-auditing package via Composer. My question is that how can I implement it in my Model or controller. Please see my code for controller and Model below. Thanks
My Controller :
<?php
namespace App\Http\Controllers;
use App\Events\Test;
use App\Letter;
use App\Notifications\LetterNotification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Validator;
class LetterController extends Controller
{
public function viewLetter()
{
return view('welcome');
}
/**
* Saves email into database
*
* #param array $data
* #return Letter
*/
protected function create(array $data)
{
$letter = Letter::create([
'email' => $data['email']
]);
$this->letterNotify($letter);
return $letter;
}
/**
* Validates email
*/
public function createLetter(Request $request)
{
$this->validate($request,[
'email' => 'required|email|max:255|unique:letters'
],[
'email.required' => 'Email is required.',
'email.unique' => 'Already registered.',
'email.email' => 'Please put a valid Email address'
]);
$this->create($request->all());
return redirect('/')->with('info','You are now registered.');
}
protected function letterNotify($letter)
{`enter code here`
Notification::send($letter, new LetterNotification($letter));
}
}
For my Model:
<?php
namespace App;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
class Letter extends Model implements AuditableContract
{
use Notifiable;
use Auditable;
protected $fillable = ['email'];
protected $table = 'letters';
}
Like I stated in my comment, the Laravel Auditing package only triggers an audit on a database operation involving an Eloquent model and event (by default, created, updated, deleted, restored).
Having said that, here's a list of steps to create an audit when logging in/out:
Create a listener for the Illuminate\Auth\Events\Login event;
Once fired, update a column in the users table that keeps track of the latest login date/time (latest_login_at, for example);
(Optional) update a column with the previous login date/time (last_login_at, for example);
By doing those updates to the users table, the Auditor kicks in;
You can also listen for the OwenIt\Auditing\Events\Auditing or OwenIt\Auditing\Events\Audited events and apply more logic if needed;
Follow the same steps for the Illuminate\Auth\Events\Logout event;

Laravel 5.1 - Can't access Auth:user()->data from my controllers

I've been having a little trouble with Auth data in Laravel 5.1
I want to get the id of the Logged user and It works if I use it in a View, lets say:
<?php echo Auth::user()->id;?>
But if I try to access to it in a Controller it dowsnt work, and I get this error:
ErrorException in Con_Mascota.php line 101:
Trying to get property of non-object
In line 101 i've:
$cod_usu=$request->user()->cod_usu;
I changed that from $cod_usu=Auth::user()->cod_usu; cause that didnt work either.
Im also including it in the Controller:
use Auth;
Any help is much appreciated :)
I believe the problem is my Auth session is login out after refresh or after i go to another route:
My login funcions is:
*/
public function ini_ses(Request $datos)
{
//Inicia sesion
// Session::put('ses_correo', Input::get('email'));
$correo = $datos->input('email');
$password= $datos->input('password');
if(Auth::attempt(['correo_elec'=>$correo, 'password'=>$password]))
{
$_session['correo']=$correo;
$_session['contra']=$password;
if(Auth::user()->tipo==0)
{
return view('cliente');
}
elseif(Auth::user()->tipo==1)
{
return view('veterinario');
}
elseif(Auth::user()->tipo==2)
{
echo("Admin");
}
}
else
{
var_dump($correo, $password);
}
}
Model:
namespace App;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
class Usuario extends Model implements AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword;
public $timestamps = false;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'usuario';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['cod_usu', 'correo_elec', 'nombre', 'ape_p', 'ape_m', 'telefono', 'celular', 'pais' , 'tipo'];
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = ['contra', 'remember_token'];
}
I found the solution, i changed this:
if(Auth::attempt(['correo_elec'=>$correo, 'password'=>$password]))
to this
if(Auth::attempt(['correo_elec'=>$correo, 'password'=>$password], true))
that eventyally made me have to change my cod_usu in the table for id, and reading jedrzej.kurylo's answer i guess that did the trick
When you log in, Laravel tries to save your user identifier in the session so that it can load it during subsequent requests. By default, model's ID field is called id, which is not true in your case. Therefore, after you authenticate, Laravel stores null as your user's ID and that's the reason it cannot load it later.
You need to tell Eloquent what is the name of your identifier column. You can do it by setting $primaryKey property of your Usuario class:
protected $primaryKey = 'cod_usu';
You'll need it not only to make your login work, but also to be able to define relations or make find method work for your models , e.g. Usuario::find(123) won't be able to load your user from the database.
Enabling remember-me, that helped in your case, worked by accident. By setting second parameter of attempt() to true you've enabled remember-me feature which results in creating a random identifier that is saved in remember_toke column in your table and later used to load your user data. It's not a proper way of handling your issue, it also will break your application if you decide to disable remember-me in the future.

How to modify request input after validation in laravel?

I found method Request::replace, that allows to replace input parameters in Request.
But currently i can see only one way to implement it - to write same replacing input code in every controller action.
Is it possible somehow to group code, that will be executed after request successful validation, but before controller action is started?
For example, i need to support ISO2 languages in my api, but under the hood, i have to transform them into legacy ones, that are really stored in the database. Currently i have this code in controller:
// Controller action context
$iso = $request->input('language');
$legacy = Language::iso2ToLegacy($iso);
$request->replace(['language' => $legacy]);
// Controller action code starts
I think what you're looking for is the passedValidation() method from the ValidatesWhenResolvedTrait trait
How to use it:
Create custom Request: php artisan make:request UpdateLanguageRequest
Put validation rules into the rules() method inside UpdateLanguageRequest class
Use passedValidation() method to make any actions on the Request object after successful validation
namespace App\Http\Requests;
use App\...\Language;
class UpdateLanguageRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
// here goes your rules, f.e.:
'language' => ['max:255']
];
}
protected function passedValidation()
{
$this->replace(['language' => Language::iso2ToLegacy($this->language)]);
}
}
Use UpdateLanguageRequest class in your Controller instead Request
public function someControllerMethod(UpdateLanguageRequest $request){
// the $request->language data was already modified at this point
}
*And maybe you want to use merge not replace method since replace will replace all other data in request and the merge method will replace only specific values
This solution worked for me based on Alexander Ivashchenko answer above:
<?php
namespace App\Http\Requests\User;
class UserUpdateRequest extends UserRequest
{
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules(): array
{
return [
'name'=>'required|string',
'email'=>'required|string|email',
'password'=>'min:8'
];
}
}
Our parent UserRequest class:
<?php
namespace App\Http\Requests\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Hash;
abstract class UserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize(): bool
{
return true;
}
/**
* Handle a passed validation attempt.
*
* #return void
*/
protected function passedValidation()
{
if ($this->has('password')) {
$this->merge(
['password' => Hash::make($this->input('password'))]
);
}
}
public function validated(): array
{
if ($this->has('password')) {
return array_merge(parent::validated(), ['password' => $this->input('password')]);
}
return parent::validated();
}
}
I am overriding validated method also. If we access each input element individually his answer works but in order to use bulk assignment in our controllers as follow we need the validated overriding.
...
public function update(UserUpdateRequest $request, User $user): JsonResource
{
$user->update($request->validated());
...
}
...
This happens because validated method get the data directly from the Validator instead of the Request. Another possible solution could be a custom validator wit a DTO approach, but for simple stuff this above it's enough.
Is it possible somehow to group code, that will be executed after
request successful validation, but before controller action is
started?
You may do it using a middleware as validator, for example:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\JsonResponse;
class InputValidator
{
public function handle($request, Closure $next, $fullyQualifiedNameOfModel)
{
$model = app($fullyQualifiedNameOfModel);
$validator = app('validator')->make($request->input(), $model->rules($request));
if ($validator->fails()) {
return $this->response($request, $validator->errors());
}
return $next($request);
}
protected function response($request, $errors)
{
if($request->ajax()) {
return new JsonResponse($errors, 422);
}
return redirect()->back()->withErrors($errors)->withInput();
}
}
Add the following entry in the end of $routeMiddleware in App\Http\Kernel.php class:
'validator' => 'App\Http\Middleware\InputValidator'
Add the rules method in Eloquent Model for example, app\Product.php is model and the rules method is declared as given below:
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules(\Illuminate\Http\Request $request)
{
return [
'title' => 'required|unique:products,title,'.$request->route()->parameter('id'),
'slug' => 'required|unique:products,slug,'.$request->route()->parameter('id'),
];
}
Declare the route like this:
$router->get('create', [
'uses' => 'ProductController#create',
'as' => 'Product.create',
'permission' => 'manage_tag',
'middleware' => 'validator:App\Product' // Fully qualified model name
]);
You may add more middleware using array for example:
'middleware' => ['auth', 'validator:App\Product']
This is a way to replace the FormRequest using a single middleware. I use this middleware with model name as argument to validate all my models using a single middleware instead of individual FormRequest class for each controller.
Here, validator is the middleware and App\Product is the model name which I pass as argument and from within the middleware I validate that model.
According to your question, the code inside your controller will be executed only after input validation passes, otherwise the redirect/ajax response will be done. For your specific reason, you may create a specific middleware. This is just an idea that could be used in your case IMO, I mean you can add code for replacing inputs in the specific middleware after validation passes.
Use merge instead of replace
$iso = $request->merge('language');
$legacy = Language::iso2ToLegacy($iso);
$request->merge(['language' => $legacy]);

Categories