Is it possible to override injected Request from a Laravel Package? - php

I'm building a custom package in Laravel that has a resource Controller. For this example the resource is an Organization In this Controller the basic index, show, store etc actions are defined.
This is my store method in the controller:
/**
* Store a newly created Organization in storage.
*
* #param OrganizationStoreRequest $request
* #return JsonResponse
*/
public function store($request): JsonResponse
{
$newOrganization = new Organization($request->all());
if ($newOrganization->save()) {
return response()->json($newOrganization, 201);
}
return response()->json([
'message' => trans('organilations::organisation.store.error')
], 500);
}
My OrganzationStoreRequest is pretty basic for now:
class OrganizationStoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => 'required'
];
}
}
The store method can be called on a API When the package is being used in a Laravel application. My problem here is that I want to give the users of my package the ability to override my OrganizationStoreRequest with there own requests, as they might have to use different authorize or validation methods.
I've tried building a middleware and Binding my own instance to the OrganizationStoreRequests but I'm not getting the results I want.
Is there anyway for the user of the package to be able to override the OrganizationStoreRequets in the controller of my package ?

With the help of fellow developers on Larachat we've come to the following (easy) solution.
When creating an own request we can bind an instance of it to the IoC Container. For example, creating the OrganizationOverrideRule and extending the original:
class OrganizationOverrideRules extends OrganizationStoreRequest
{
public function rules(): array
{
return [
'name' => 'required',
'website' => 'required',
'tradename' => 'required'
];
}
}
Then in the AppServiceProvider you can bind the new instance like so:
App::bind(
OrganizationStoreRequest::class,
OrganizationOverrideRules::class
);
Then the OrganizationOverrideRules will be used for validation and authorization.

Related

Laravel resource toArray() not working properly

I've created a new API resource called UserResource:
class UserResource extends JsonResource
{
public function toArray($request)
{
/** #var User $this */
return [
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'email_verified_at' => $this->email_verified_at,
];
}
}
And then I'm trying to get current user object and pass it to the resource:
class UserController extends Controller
{
public function index(Request $request)
{
/** #var User $user */
$user = $request->user();
return new UserResource($user);
}
}
But it always throws an exception Trying to get property 'first_name' of non-object. $user variable contains current user.
I'm using Xdebug and I checked that the resource contains formatted json in $this->resource, but not current user's model:
So why? Laravel's documentation says that I'll be able to get current resource (User model in my case) in $this->resource parameter, but it does not work. Any ideas?
UPDATE: here is break point for xbedug in UserResource:
It seems strange to me that you get the User object from the index's request and I reckon that somethings probably wrong there.
Normally, the index method is used to return a listing of all instances of the resp. model, e.g. something like
public function index() {
return UserResource::collection(User::all());
}
Try to return a user object (as resource) which you are sure exists
$user = Users::findOrFail(1);
return new UserResource($user);
and see if the error still persists. If it does not, something is wrong with the data you pass in the request.
I had a look at the JsonResource Class. Look at the contructor:
public function __construct($resource)
{
$this->resource = $resource;
}
Seems like the $user is saved to $this->resource. So you have to change your return statement to:
return [
'first_name' => $this->resource->first_name,
'last_name' => $this->resource->last_name,
'email_verified_at' => $this->resource->email_verified_at,
];
Does that work?
As #Clément Baconnier said in comments to the question, the right way to fix it is reinstalling vendor folder. The problem is that project has been created via Laravel new command from local with php 7.2, but there's php 7.4 in container.

Custom Recaller Laravel

I want change default cookie remember_web_59ba36addc2b2f9401580f014c7f58ea4e30989d to myprefix_web_59ba36addc2b2f9401580f014c7f58ea4e30989d
I find code in Illuminate\Auth\SessionGuard
/**
* Get the name of the cookie used to store the "recaller".
*
* #return string
*/
public function getRecallerName() {
return 'remember_'.$this->name.'_'.sha1(static::class);
}
How i can create custom SessionGuard? Somebody can help me?
Since the built in SessionGuard does not have a way to change this, you will need to create your own Guard class to override the method, and tell Auth to use your Guard class. This information is also explained in my answer here, which explains how to customize the TokenGuard.
First, start by creating a new Guard class that extends the base SessionGuard class. In your new Guard class, you will override the getRecallerName() method to return the name you want. In this example, it is created at app/Services/Auth/MySessionGuard.php:
namespace App\Services\Auth;
use Illuminate\Auth\SessionGuard;
class MySessionGuard extends SessionGuard
{
/**
* Get the name of the cookie used to store the "recaller".
*
* #return string
*/
public function getRecallerName()
{
return 'myprefix_'.$this->name.'_'.sha1(static::class);
}
}
Once you've created your class, you need to let Auth know about it. You can do this in the boot() method on your AuthServiceProvider service provider:
public function boot(GateContract $gate)
{
$this->registerPolicies($gate);
Auth::extend('mysession', function($app, $name, array $config) {
$provider = $this->createUserProvider($config['provider']);
$guard = new \App\Services\Auth\MySessionGuard($name, $provider, $app['session.store']);
// When using the remember me functionality of the authentication services we
// will need to be set the encryption instance of the guard, which allows
// secure, encrypted cookie values to get generated for those cookies.
if (method_exists($guard, 'setCookieJar')) {
$guard->setCookieJar($app['cookie']);
}
if (method_exists($guard, 'setDispatcher')) {
$guard->setDispatcher($app['events']);
}
if (method_exists($guard, 'setRequest')) {
$guard->setRequest($app->refresh('request', $guard, 'setRequest'));
}
return $guard;
});
}
And finally, you need to tell Auth to use your new mysession guard. This is done in the config/auth.php config file.
'guards' => [
'web' => [
'driver' => 'mysession',
'provider' => 'users',
],
],

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]);

Yii2 REST Simplify BasicAuth

I'm impressed with how simple it was to create a REST api in Yii2. However, i'm having a little trouble understanding the Basic Authentication. My needs are utterly simple and i'd like my solution to follow suit.
I need Basic token authentication here. I'm not even against hardcoding it for now, but here's what i've done thus far.
I have database table to hold my singular token ApiAccess(id, access_token)
ApiAccess.php - Model - NOTE: IDE shows syntax error on this first line
class ApiAccess extends base\ApiAccessBase implements IdentityInterface
{
public static function findIdentityByAccessToken($token, $type = null)
{
return static::findOne(['access_token' => $token]);
}
}
Module.php - in init() function
\Yii::$app->user->enableSession = false;
I made an ApiController that each subsequent noun extends
ApiController.php
use yii\rest\ActiveController;
use yii\filters\auth\HttpBasicAuth;
use app\models\db\ApiAccess;
class ApiController extends ActiveController
{
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => HttpBasicAuth::className(),
];
return $behaviors;
}
}
As it stands, accessing an api endpoint in the browser prompts for a username and password. Request via REST Client displays access error.
How do I properly tie HttpBasicAuth to my ApiAccess model?
OR
How do I hardcode an api access token? (First option is obviously best)
Let's watch and try to understand "yii" way basic auth for REST.
1st. When you adding behavior to your REST controller, you enabling basic auth:
$behaviors['authenticator'] = [
'class' => HttpBasicAuth::className(),
];
As you did. What does it mean? It means that your application will parse your authorization header. It looks like:
Authorization : Basic base64(user:password)
Here is a trick for yii2. If you look at code more carefully, you will see that yii uses access_token from user field, so your header should look like:
Authorization : Basic base64(access_token:)
You can parse this header by your own, if you want to change this behavior:
$behaviors['authenticator'] = [
'class' => HttpBasicAuth::className(),
'auth' => [$this, 'auth']
];
....
public function auth($username, $password)
{
return \app\models\User::findOne(['login' => $username, 'password' => $password]);
}
2nd thing to do. You must implement findIdentityByAccessToken() function from identityInterface.
Why your IDE complaining?
class User extends ActiveRecord implements IdentityInterface
Here's how your user class declaration should look.
From your implementation and structure:
public static function findIdentityByAccessToken($token, $type = null)
{
return static::findOne(['access_token' => $token]);
}
you not returning object of class which implements identity interface.
How to make it properly?
Add column access_token to your users table, and return back your user model (you can look how it must look here - https://github.com/yiisoft/yii2-app-advanced/blob/master/common/models/User.php)
If you do this - default code will work with your findIdentityByAccessToken() implementation.
If you don't want to add field to users table - make new one with user_id,access_token fields. Then your implementation should look like:
public static function findIdentityByAccessToken($token, $type = null)
{
$apiUser = ApiAccess::find()
->where(['access_token' => $token])
->one();
return static::findOne(['id' => $apiUser->user_id, 'status' => self::STATUS_ACTIVE]);
}
Hope i could cover all of your questions.

Laravel & Mockery: Unit testing the update method without hitting the database

Alright so I'm pretty new to both unit testing, mockery and laravel. I'm trying to unit test my resource controller, but I'm stuck at the update function. Not sure if I'm doing something wrong or just thinking wrong.
Here's my controller:
class BooksController extends \BaseController {
// Change template.
protected $books;
public function __construct(Book $books)
{
$this->books = $books;
}
/**
* Store a newly created book in storage.
*
* #return Response
*/
public function store()
{
$data = Input::except(array('_token'));
$validator = Validator::make($data, Book::$rules);
if($validator->fails())
{
return Redirect::route('books.create')
->withErrors($validator->errors())
->withInput();
}
$this->books->create($data);
return Redirect::route('books.index');
}
/**
* Update the specified book in storage.
*
* #param int $id
* #return Response
*/
public function update($id)
{
$book = $this->books->findOrFail($id);
$data = Input::except(array('_token', '_method'));
$validator = Validator::make($data, Book::$rules);
if($validator->fails())
{
// Change template.
return Redirect::route('books.edit', $id)->withErrors($validator->errors())->withInput();
}
$book->update($data);
return Redirect::route('books.show', $id);
}
}
And here are my tests:
public function testStore()
{
// Add title to Input to pass validation.
Input::replace(array('title' => 'asd', 'content' => ''));
// Use the mock object to avoid database hitting.
$this->mock
->shouldReceive('create')
->once()
->andReturn('truthy');
// Pass along input to the store function.
$this->action('POST', 'books.store', null, Input::all());
$this->assertRedirectedTo('books');
}
public function testUpdate()
{
Input::replace(array('title' => 'Test', 'content' => 'new content'));
$this->mock->shouldReceive('findOrFail')->once()->andReturn(new Book());
$this->mock->shouldReceive('update')->once()->andReturn('truthy');
$this->action('PUT', 'books.update', 1, Input::all());
$this->assertRedirectedTo('books/1');
}
The issue is, when I do it like this, I get Mockery\Exception\InvalidCountException: Method update() from Mockery_0_Book should be called exactly 1 times but called 0 times. because of the $book->update($data) in my controller. If I were to change it to $this->books->update($data), it would be mocked properly and the database wouldn't be touched, but it would update all my records when using the function from frontend.
I guess I simply just want to know how to mock the $book-object properly.
Am I clear enough? Let me know otherwise. Thanks!
Try mocking out the findOrFail method not to return a new Book, but to return a mock object instead that has an update method on it.
$mockBook = Mockery::mock('Book[update]');
$mockBook->shouldReceive('update')->once();
$this->mock->shouldReceive('findOrFail')->once()->andReturn($mockBook);
If your database is a managed dependency and you use mock in your test it causes brittle tests.
Don't mock manage dependencies.
Manage dependencies: dependencies that you have full control over.

Categories