Laravel - Passing Request to Queue Job - php

I'm unable to pass a request to queue handle() job
public function handle(Request $request) | package.json*
{ | phpunit.xml*
Log::alert('starting process'); | readme.md*
Log::alert($request); | server.php*
|~
if (strpos($request->status, 'Approved') !== false) { |~
$name = Name::where('mId', '=', $request->mId)->get()->first(); |~
|~
$client = new Client(); |~
|~
$client->request('POST', 'http://127.0.0.1:5000/api/email', [ |~
'json' => [ |~
'type' => $request->type, |~
'name' => $name->name,
] |~
]); |~
}
As it is, $request comes empty. If I remove Request and only leave handle($request) I get this stack:
Too few arguments to function App\Jobs\PostAlfred::handle(), 0 passed and exactly 1 expected in
I'm calling this function from the controller when the form is updated.
public function update(UpdateRequest $request) |▸ vendor/
{ | artisan*
$redirect_location = parent::updateCrud($request); | composer.json* | composer.lock*
PostMyJob::dispatch($request);
I tried adding UpdateRequest, such as this: handle(UpdateRequest $request), then I get an authorization error.
Not sure how to proceed.

Keep in mind that a request only exists in the context of an actual HTTP request. It exists only while your app is handling that request. There is no "request" when your queue worker starts taking jobs off of the queue. Laravel can't give you an instance of the request, because there is none.
What you'll need to do is explicitly pass the information your job requires in order to perform it's duty. If you just want the input of the request, you could do something like this - which will provide an array of input to the job's constructor.
PostMyJob::dispatch($request->all())
public function __construct(array $input)
{
$this->input = $input;
}
You may have seen examples of Eloquent models being passed into a job, but don't let that fool you into thinking the whole class will be provided to the handler as-is. Laravel is smart enough to re-fetch the Eloquent model for you when it handles the job, but as described earlier it can't get the original request for you.

When you are passing any arguments to dispatch function, those are passed in the constructor of the job and not in handle method.
See document says The arguments passed to the dispatch method will be given to the job's constructor
In your job do this :
class SomeJob extends Job{
private $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function handle()
{
if (strpos($this->request->status, 'Approved') !== false) {
$name = Name::where('mId', '=', $this->request->mId)->get()->first();
$client = new Client();
$client->request('POST', 'http://127.0.0.1:5000/api/email', [
'json' => [
'type' => $this->request->type,
'name' => $name->name,
]
]);
}
}
}

Related

Call to a member function validated() on null (Unit testing Laravel 9)

I am writing test for some controller method that will validate request data and create document by 3rd party API. Then it should return response with 201 status code. I am using mocking to mock my service class that is creating document. Here is my controller:
public function margin(MarginRequest $request){
$data = $request->validated();
$fileId = $this->documentService->createDocument(auth()->user()->id, SignableDocumentAbstract::MARGIN_CERTIFICATE, new MarginDocument(), $data);
return response()->json([
'code' => 201,
'message' => 'Margin agreement generated successfully',
'data' => [
'uuid' => $fileId
]
], 201);
}
And my test:
public function test_public_margin()
{
$marginData = (new MarginFaker())->fake();
Auth::shouldReceive('user')->once()->andReturn((object)['id' => 999999]);
$this->mock(DocumentService::class, function (MockInterface $mock) {
$mock->shouldReceive('createDocument')
->once()
->andReturn(Str::random());
});
$request = MarginRequest::create('/api/public/documents/margin', 'POST', $marginData);
$response = app(PublicController::class)->margin($request);
$this->assertEquals(201, $response->getStatusCode());
}
Everything look OK but when I run my test it throws error that
Call to a member function validated() on null
It is given in $data = $request->validated(); line of controller. But I can't understand why my $request is recognized as null. Even if I dump request object by dump($request) I can see that it is object and holds all required fields inside.
Then what can be the reason, why I can't call validated() method while testing?
You do not test like that when you want to test a URL. You NEVER mock a controller or do new Controller and call a method inside it.
You have to read the HTTP Test section of the documentation.
So, your test should look like this:
public function test_public_margin()
{
$this->actingAs(User::factory()->create());
$this->mock(DocumentService::class, function (MockInterface $mock) {
$mock->shouldReceive('createDocument')
->once()
->andReturn(Str::uuid());
});
$response = $this->post(
'/api/public/documents/margin',
['pass the needed data as an array, so the validation passes']
);
$response->assertStatus(201);
}

How do I mock a GuzzleHttp client that makes a request to a third-party API in a Laravel feature test?

In a Laravel project (Laravel 8 on PHP 8.0) I have a feature test in which I test an internal endpoint. The endpoint has a Controller calls a method on a Service. The Service then tries to call a third-party endpoint. It is this third-party endpoint that I would like to mock. The situation currently looks like this:
Internal Endpoint Feature Test
public function testStoreInternalEndpointSuccessful(): void
{
// arrange, params & headers are not important in this problem
$params = [];
$headers = [];
// act
$response = $this->json('POST', '/v1/internal-endpoint', $params, $headers);
// assert
$response->assertResponseStatus(Response::HTTP_OK);
}
Internal Endpoint Controller
class InternalEndpointController extends Controller
{
public function __construct(protected InternalService $internalService)
{
}
public function store(Request $request): InternalResource
{
$data = $this.internalService->fetchExternalData();
return new InternalResource($data); // etc.
}
}
Internal Service
use GuzzleHttp\ClientInterface;
class InternalService
{
public function __construct(protected ClientInterface $client)
{
}
public function fetchExternalData()
{
$response = $this->httpClient->request('GET', 'v1/external-data');
$body = json_decode($response->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR);
return $body;
}
}
I have looked at Guzzle's documentation, but it seems like the MockHandler strategy requires you to execute the http request inside of the test, which is not wat I want in my test. I want Guzzle's http client to be mocked and to return a custom http response that I can specify in my test. I have tried to mock Guzzle's http client like this:
public function testStoreInternalEndpointSuccessful(): void
{
// arrange, params & headers are not important in this problem
$params = [];
$headers = [];
$mock = new MockHandler([
new GuzzleResponse(200, [], $contactResponse),
]);
$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack]);
$mock = Mockery::mock(Client::class);
$mock
->shouldReceive('create')
->andReturn($client);
// act
$response = $this->json('POST', '/v1/internal-endpoint', $params, $headers);
// assert
$response->assertResponseStatus(Response::HTTP_OK);
}
But the InternalService does not seem to hit this mock in the test.
I have also considered and tried to use Http Fake, but it didn't work and I assume Guzzle's http client does not extend Laravel's http client.
What would be the best way to approach this problem and mock the third-party endpoint?
Edit
Inspired by this StackOverflow question, I have managed to solve this problem by injecting a Guzzle client with mocked responses into my service. The difference to the aforementioned StackOverflow question is that I had to use $this->app->singleton instead of $this->app->bind because my DI was configured differently:
AppServiceProvider.php
namespace App\Providers;
use App\Service\InternalService;
use GuzzleHttp\Client;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// my app uses ->singleton instead of ->bind
$this->app->singleton(InternalService::class, function () {
return new InternalService(new Client([
'base_uri' => config('app.internal.base_url'),
]));
});
}
}
Depending on your depending injection, you want to bind or singleton-ify your InternalService with a custom Guzzle http client that returns mocked responses, e.g. like this:
public function testStoreInternalEndpointSuccessful(): void
{
// depending on your DI configuration,
// this could be ->bind or ->singleton
$this->app->singleton(InternalService::class, function($app) {
$mockResponse = json_encode([
'data' => [
'id' => 0,
'name' => 'Jane Doe',
'type' => 'External',
'description' => 'Etc. you know the drill',
]
]);
$mock = new GuzzleHttp\Handler\MockHandler([
new GuzzleHttp\Psr7\Response(200, [], $mockResponse),
]);
$handlerStack = GuzzleHttp\HandlerStack::create($mock);
$client = new GuzzleHttp\Client(['handler' => $handlerStack]);
return new InternalService($client);
});
// arrange, params & headers are not important in this problem
$params = [];
$headers = [];
// act
$response = $this->json('POST', '/v1/internal-endpoint', $params, $headers);
// assert
$response->assertResponseStatus(Response::HTTP_OK);
}
See also: Unit Testing Guzzle inside of Laravel Controller with PHPUnit

Laravel 7 - cannot fake 3rd party Http call using a Middleware

I am trying to use a middleware in Laravel 7 to fake Http calls to a 3rd party API. So I can assign that middleware to any route which will make calls to that 3rd party API. So whenever that route is called, it will call to the faked API.
Purpose of this is, when I want to fake the API, I just only have to assign the middleware to the route. When I don't want to fake the API, I will just remove the middleware from the route.
The middleware looks like below.
namespace App\Http\Middleware;
class MockApiEndpoints
{
public function handle($request, Closure $next)
{
// mock api/endpoint
$vacancies = $this->vacancyRepository->all();
$url = 'api/endpoint';
$sessionId = Str::uuid()->toString();
$response = [
'result' => 'OK',
'content' => [
'suggestedVacancies' => array_map(function ($uid) {
return [
'id' => $uid,
'relevance' => bcdiv(rand(9, 99), 100, 2)
];
}, $vacancies->pluck('uid')->all()),
'sessionId' => $sessionId
]
];
$this->mockHttpRequest($url, $response);
return $next($request);
}
protected function mockHttpRequest(string $uri, $response, int $status = 200, array $headers = [])
{
$url = config('api.base_url') . '/' . $uri;
Http::fake([
$url => Http::response($response, $status, $headers)
]);
}
}
Even though I attach this this middleware to the route, route still makes calls to the original API. So the Htpp::fake doesn't work in the middleware it seems. Htpp::fake does work if I use it inside the controller.
Middleware is attached to the route as below. (Middleware is properly registered in the $routeMiddleware array in app/Http/Kernal.php)
namespace App\Providers;
class RouteServiceProvider extends ServiceProvider
{
protected function mapApiRoutes()
{
Route::prefix('api')
->middleware(['MockApiEndpoints'])
->namespace($this->namespace . '\Api')
->group(base_path('routes/api.php'));
}
}
I got my work done by using a workaround. But I want to know why does Http::fake doesn't work in middleware. Thank you for your answers in advance.
Instead of returning $next($request) you should return a response(...) in your middleware.
Perhaps just forward the response from the fake call.
return response($responseFrom3rdPartyApiCall, 200);

Logging headers added in a middleware

I have a some GuzzleClientLoggingHandler which is as following:
class GuzzleClientLoggingHandler
{
/** #var HandlerStack */
private $handlerStack;
public function __construct(HandlerStack $handlerStack)
{
$this->handlerStack = $handlerStack;
}
public function log(): HandlerStack
{
$logger = Logger::getLogger(__CLASS__);
$middleware = Middleware::log($logger, $this->request());
$this->handlerStack->unshift($middleware);
$middleware = Middleware::log($logger, $this->response());
$this->handlerStack->unshift($middleware);
return $this->handlerStack;
}
}
and my client looks like:
new Client([
'base_uri' => 'https://example.com',
'handler' => (new GuzzleClientLoggingHandler($handlerStack))->log()
]);
and variable $handlerStack have n middleware, e.g.:
$handlerStack->unshift(new LanguageMiddleware());
in the middleware LanguageMiddleware I set a header for each request
public function __invoke(callable $handler)
{
return function (RequestInterface $request, array $options) use ($handler) {
$request = modify_request($request, [
'set_headers' => [
'Accept-Language' => Session::get('language')
]
]);
return $handler($request, $options);
};
}
And there is question: how to log this request with changed/setted headers?
Now all request modifications in a middleware doesn't appear in the logs.
You need to change order of middlewares in your stack. Take a look at the docs, there is a good explanation on how middlewares are applied to requests and responses (pay attention to the order!).
So to log requests with all the modifications applied you need to put you middleware in the end of the stack with ->push() instead of ->unshift().

How to separate controller method in Laravel?

I start with Laravel, I write API. I have a method in TestController that checks if the student has correctly inserted data and has access to the exam solution. I do not think it's a good idea to have the whole method in the controller, but I have no idea how to separate it. I think about politics, but I have to have several models for one policy, maybe I can try to put part of the method on AuthorizeStudentRequest or try it in a different way? Of course, now I am returning 200 with the message, but I have to return 422 or another code with errors, but I have not done it because of my problem.
public function authorizeStudent(AuthorizeStudentRequest $request)
{
$hash = $request->input('hash');
$token = $request->input('token');
$exam = Exam::where([['hash', $hash], ['token', $token]])->first();
if($exam == null)
return ['message' => 'Exam does not exist.'];
$user = $exam->user_id;
$studentFirstname = $request->input('firstname');
$studentLastname = $request->input('lastname');
$student = Student::where([
['firstname', $studentFirstname],
['lastname', $studentLastname],
['user_id', $user]
])->first();
if($student == null)
return ['message' => 'Student does not exist.'];
$classroom = Classroom::where([
['name', $classroomName],
['user_id', $user]
])->first();
if($classroom == null)
return ['message' => 'Classroom does not exist.'];
if($student->classroom_id != $classroom->id)
return ['message' => 'Student is not in classroom.'];
if($exam->classrooms()->where(['classroom_id', $classroom->id], ['access', 1])->first() == null)
return ['message' => 'Class does not access to exam yet.'];
}
I would suggest you rather pass the primary keys of the selected $exam, $student and $classroom models to your controller from the form and validate whether they exist in the corresponding tables, rather than having to check their existence using a bunch of different columns.
If you pass the primary keys, you could use the 'exists' validation rule to check if they exist. For example, in your AuthorizeStudentRequest class you could have the following function:
public function rules()
{
return [
'exam_id' => 'required|exists:exams',
'student_id' => 'required|exists:students',
'classroom_id' => 'required|exists:classrooms',
];
}
Otherwise, if you really need to use the different columns to check the existence of the exam, student and classroom, you could create custom validation rules and use them in your AuthorizeStudentRequest class. For example, create a custom validation rule that checks whether the exam exists as follows:
$php artisan make:rule ExamExists
class ExamExists implements Rule
{
private $token;
private $hash;
public function __construct($token, $hash)
{
$this->token = $token;
$this->hash = $hash;
}
public function passes($attribute, $value)
{
return Exam::where([['hash', $hash], ['token', $token]])->count() > 0;
}
}
And then you can use the custom validation rule in your request as follows:
public function rules()
{
return [
'hash' => ['required', new ExamExists($this->hash, $this->token)],
... other validation rules ...
]
}
For checking whether a student has access to a classroom or a class has access to an exam, you could use policies.
API resources present a way to easily transform our models into JSON responses. It acts as a transformation layer that sits between our Eloquent models and the JSON responses that are actually returned by our API. API resources is made of two entities: a resource class and a resource collection. A resource class represents a single model that needs to be transformed into a JSON structure, while a resource collection is used for transforming collections of models into a JSON structure.
Both the resource class and the resource collection can be created using artisan commands:
// create a resource class
$ php artisan make:resource UserResource
// create a resource collection using either of the two commands
$ php artisan make:resource Users --collection
$ php artisan make:resource UserCollection
Before diving into all of the options available to you when writing resources, let's first take a high-level look at how resources are used within Laravel. A resource class represents a single model that needs to be transformed into a JSON structure. For example, here is a simple User resource class:
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
Every resource class defines a toArray method which returns the array of attributes that should be converted to JSON when sending the response. Notice that we can access model properties directly from the $this variable. More information here
https://laravel.com/docs/5.7/eloquent-resources

Categories