I am building a Laravel application. I am using Laravel Cashier to handle subscription in my application. I am writing unit test for the Stripe Webhook creating a custom event listener for that.
I have an event listener like this:
class StripeEventListener
{
/**
* Create the event listener.
*
* #return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* #param object $event
* #return void
*/
public function handle(WebhookReceived $event)
{
if ($event->payload['type'] === 'invoice.payment_succeeded') {
// Handle the incoming event...
}
// TODO: write unit test for this event.
if ($event->payload['type'] === 'customer.subscription.updated') {
// Handle the incoming event...
// handleCustomerSubscriptionUpdated
if ($user = $this->getUserByStripeId($event->payload['data']['object']['customer'])) {
$data = $event->payload['data']['object'];
if (
isset($data['status']) &&
($data['status'] === StripeSubscription::STATUS_INCOMPLETE_EXPIRED ||
$data['status'] == StripeSubscription::STATUS_INCOMPLETE)
) {
if (! $user->payment_method_needs_updating) {
$user->payment_method_needs_updating = true;
$user->save();
}
}
}
}
}
protected function getUserByStripeId($stripeId)
{
return Cashier::findBillable($stripeId);
}
}
Then I bind that to WebhookReceived event in the EventServiceProvider like this:
protected $listen = [
WebhookReceived::class => [
StripeEventListener::class
],
Registered::class => [
SendEmailVerificationNotification::class,
],
];
I am unit testing the listener dispatching the WebhookReceived event like this:
WebhookReceived::dispatch([
.... mock data
]);
But I am getting this error:
Illuminate\Contracts\Container\BindingResolutionException
Target class [events] does not exist.
at vendor/laravel/framework/src/Illuminate/Container/Container.php:879
875▕
876▕ try {
877▕ $reflector = new ReflectionClass($concrete);
878▕ } catch (ReflectionException $e) {
➜ 879▕ throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
880▕ }
How can I fix it or how can I unit test it?
Related
I have successfully implemented pusher on my laravel app but I want to make, when the user succeeds in making an order the default status_message for the order is pending, the case is when the admin changes the status_message to processed, the user who has ordered gets a notification that the order he has made is processed.
this is my code but this code sends notifications to all users.
Controller
if ($data->status_message == 'processed') {
event(new OrderEvent('Hi, Your order is processed!'));
//...
}
My Event OrderEvent.php
public function broadcastOn()
{
return new Channel('notif-channel');
}
/**
* Broadcast order event.
*
* #return void
*/
public function broadcastAs()
{
return 'order-event';
}
in App blade
var channel = pusher.subscribe('notif-channel');
channel.bind('order-event', function(data) {
const obj = JSON.parse(JSON.stringify(data));
const message = obj.message;
blah blah blah
}
Both user and admin should be on the same channel. For example if user is subscribed for channel 'order-channel-SetUserID'.
Admin should send the message to that channel and you should look for it on the front end and make the changes on the DOM.
In your controller when you submit the changes of the status of the order run the event with the channel name
event(new OrderEvent($user_id, 'Hi, Your order is processed!'));
Now your event should look similar to this:
class OrderEvent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $user_id;
public $message;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct($user_id, $message)
{
$this->user_id = $user_id;
$this->message = $message;
}
/**
* Get the channels the event should broadcast on.
*
* #return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new Channel('order-channel.' . $this->user_id);
}
public function broadcastAs()
{
return 'order-event';
}
}
Of course you can change your class Name etc... I'm just giving an idea.
it's important to send the changes on the same channel with this user or else you will make changes to other users that are visiting your website.
EDITED
Here is what else you need to configure.
In app/Providers/EventServiceProvider.php
You need to put the event in protected $listen
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
OrderEvent::class => [
OrderEventListener::class,
],
];
In app/Listeners You should create OrderEventListener.php and set it up as follow:
<?php
namespace App\Listeners;
use App\Events\OrderEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Pusher;
class OrderEventListener
{
/**
* Create the event listener.
*
* #return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* #param \App\Events\OrderEvent $event
* #return void
*/
public function handle(OrderEvent $event)
{
$pusher = new Pusher(env('PUSHER_APP_KEY'),
env('PUSHER_APP_SECRET'), env('PUSHER_APP_ID'), [
'cluster' => env('PUSHER_APP_CLUSTER'),
'useTLS' => true
]);
$pusher->trigger($event->broadcastOn(),
$event->broadcastAs(), $event->data);
}
}
check your Debug Console in pusher dashboard? If you can see the event firing there all you need to do is show the message with javascript. If no event is running then something in your code is missing.
Usually Laravel's form request returns a generic 400 code on failing validation like this:
{
"message": "The given data is invalid",
"errors": {
"person_id": [
"A person with ID c6b853ec-b53e-4c35-b633-3b1c2f27869c does not exist"
]
}
}
I'd like to return a 404 if a request does not pass my custom rule.
My custom rule checks if a record exists in the DB:
class ValidatePersonExists implements Rule
{
/**
* Determine if the validation rule passes.
*
* #param string $attribute
* #param mixed $value
* #return bool
public function passes($attribute, $value)
{
return Person::where('id', $value)->exists();
}
/**
* Get the validation error message.
*
* #return string
*/
public function message()
{
return "A person with ID :input does not exist";
}
}
If I throw a ModelNotFoundException on failure of the exists() check, where can I catch it to respond with a friendly 404 response?
Here's my form request where I am using the rule:
public function rules()
{
return [
'person_id' => ['bail', 'required', 'uuid', new ValidatePersonExists],
];
}
you can modify app/Exceptions/Handler.php, here is how i used it
use Illuminate\Database\Eloquent\ModelNotFoundException;
public function render($request, Exception $e)
{
$status = method_exists($e, 'getStatusCode') ? $e->getStatusCode() : 500;
//here im using translation, you can set your own message
$response = [
'errors' => trans("response.$status.response"),
'message' => trans("response.$status.message")
];
if (config('app.debug')) {
$response['exception'] = get_class($e);
$response['message'] = $e->getMessage();
$response['trace'] = $e->getTrace();
}
//ModelNotFoundException statusCode is 0 so i need to pass it manually
if($e instanceof ModelNotFoundException){
$response['errors'] = new \Illuminate\Support\ViewErrorBag;
$response['response'] = 404; // im using translation here
return response()->view("errors.index", $response, 404);
}
return parent::render($request, $e);
}
in my blade a simple translation message:
<p>{{trans("response.{$response}.response")}}</p>
<p>{{trans("response.{$response}.message")}}</p>
I've found a solution, but I'm not 100% happy with it.
Basically I've created a class (ApiRequest.php) to extend Illuminate's FormRequest class, and in this ApiRequest class I'm intercepting the IlluminateValidationException and doing some logic on the failed validation, checking if the Request failed on my exists() rule. If it does, I'm changing the status code to 404:
Here's my class:
abstract class ApiRequest extends IlluminateFormRequest
{
/**
* Handle a failed validation attempt.
*
* #param \Illuminate\Contracts\Validation\Validator $validator
* #return void
*
* #throws \GetCandy\Api\Exceptions\ValidationException
*/
protected function failedValidation(Validator $validator)
{
$failedRules = $validator->failed();
$statusCode = $this->getStatusCode($failedRules);
$response = new JsonResponse([
'message' => 'The given data is invalid',
'errors' => $validator->errors(),
], $statusCode);
throw new IlluminateValidationException($validator, $response);
}
private function getStatusCode($failedRules)
{
$statusCode = 400;
foreach ($failedRules as $rule) {
if (Arr::has($rule, "App\Http\Requests\Rules\ValidatePersonExists")) {
$statusCode = 404;
}
}
return $statusCode;
}
}
Hope this helps someone, if anyone has a better solution feel free to post an answer.
I am new with laravel so please don't be harsh.
I am bulding a simple web which connects to an external API(several endpoints) via Guzzle,fetching some data,cleaning them and storing them.
At the moment -and from one endpoint- i have something the following Job:
public function handle(Client $client)
{
try {
$request= $client->request('GET', 'https://api.url./something', [
'headers' => [
'X-RapidAPI-Key'=> env("FOOTBALL_API_KEY"),
'Accept' => 'application/json'
]
]);
$request = json_decode($request->getBody()->getContents(), true);
foreach ($request as $array=>$val) {
foreach ($val['leagues'] as $id) {
League::firstOrCreate(collect($id)->except(['coverage'])->toArray());
}
}
} catch (GuzzleException $e) {
};
}
Therefore i would like some code recommendations, how can i make my code better from design point of view.
My thoughts are:
a)Bind Guzzle as service provider.
b)use a design pattern for implementing calls to endpoints.URI builder maybe?
Any assistance will be appreciated.
May the force be with you.
Detailed feedback
Some pointers specific to the provided code itself:
A guzzle client request returns a response, which does not match the name of the parameter you assign it to
Calls to json_decode can fail in which case they'll return null. In terms of defensive programming it's good to check for those fail cases
Your case makes some assumptions about the data in the response. It's best to check if the response is in the actual format you expect before using it.
You catch all GuzzleExceptions, but do nothing in those cases. I think you could improve this by either:
Logging the exception
Throwing another exception which you will catch at a class, calling the handle() method
Both of the options above
You could choose to inject the api key, rather than fetching it directly via the env() method. This will prevent issues described in the warning block here
General feedback
It feels like your code is mixing responsibilities, which is considered bad practice. The handle() method now does the following:
Send API requests
Decode API requests
Validate API responses
Parse API responses
Create models
You could consider moving some or all of these to separate classes, like so:
ApiClient which is responsible for sending out requests
ResponseDecoder which is responsible for turning a response into \stdClass
ResponseValidator which is responsible for checking if the response has the expected data structure
RepsonseParser which is responsible for turning the response \stdClass into collections
LeagueFactory which is responsible for turning collections into League models
One could argue that the first four classes should be put into a single class called ApiClient. That's purely up to you.
So in the end you would come up with something like this:
<?php
namespace App\Example;
use Psr\Log\LoggerInterface;
class LeagueApiHandler
{
/**
* #var ApiClient
*/
private $apiClient;
/**
* #var ResponseDecoder
*/
private $decoder;
/**
* #var ResponseValidator
*/
private $validator;
/**
* #var ResponseParser
*/
private $parser;
/**
* #var LeagueFactory
*/
private $factory;
/**
* #var LoggerInterface
*/
private $logger;
public function __construct(
ApiClient $apiClient,
ResponseDecoder $decoder,
ResponseValidator $validator,
ResponseParser $parser,
LeagueFactory $factory,
LoggerInterface $logger
) {
$this->apiClient = $apiClient;
$this->decoder = $decoder;
$this->validator = $validator;
$this->parser = $parser;
$this->factory = $factory;
$this->logger = $logger;
}
public function handle()
{
try {
$response = $this->apiClient->send();
} catch (\RuntimeException $e) {
$this->logger->error('Unable to send api request', $e->getMessage());
return;
};
try {
$decodedResponse = $this->decoder->decode($response);
} catch (\RuntimeException $e) {
$this->logger->error('Unable to decode api response');
return;
};
if (!$this->validator->isValid($decodedResponse)) {
$this->logger->error('Unable to decode api response');
return;
}
$collections = $this->parser->toCollection($decodedResponse);
foreach ($collections as $collection) {
$this->factory->create($collection);
}
}
}
namespace App\Example;
use GuzzleHttp\Client;
class ApiClient
{
/**
* #var Client
*/
private $client;
/**
* #var string
*/
private $apiKey;
public function __construct(Client $client, string $apiKey)
{
$this->client = $client;
$this->apiKey = $apiKey;
}
public function send()
{
try {
return $this->client->request('GET', 'https://api.url./something', [
'headers' => [
'X-RapidAPI-Key' => $this->apiKey,
'Accept' => 'application/json'
]
]);
} catch (GuzzleException $e) {
throw new \RuntimeException('Unable to send request to api', 0, $e);
};
}
}
namespace App\Example;
use Psr\Http\Message\ResponseInterface;
class ResponseDecoder
{
public function decode(ResponseInterface $response): \stdClass
{
$response = json_decode($response->getBody()->getContents(), true);
if ($response === null) {
throw new \RuntimeException('Unable to decode api response');
}
return $response;
}
}
namespace App\Example;
class ResponseValidator
{
public function isValid(\stdClass $response): bool
{
if (is_array($response) === false) {
return false;
}
foreach ($response as $array) {
if (!isset($array['leagues'])) {
return false;
}
}
return true;
}
}
namespace App\Example;
use Illuminate\Support\Collection;
class ResponseParser
{
/**
* #param \stdClass $response
* #return Collection[]
*/
public function toCollection(\stdClass $response): array
{
$collections = [];
foreach ($response as $array => $val) {
foreach ($val['leagues'] as $id) {
$collections[] = collect($id)->except(['coverage'])->toArray();
}
}
return $collections;
}
}
namespace App\Example;
use Illuminate\Support\Collection;
class LeagueFactory
{
public function create(Collection $collection): void
{
League::firstOrCreate($collection);
}
}
Here is my case
I have cron job(console command)
/**
* Execute the console command.
*
* #return mixed
*/
public function handle(OrdersMagentoService $magentoService)
{
try {
$orders = $magentoService->getRemoteOrders();
print_r($orders);
} catch (SoapFault $e) {
$this->line('Error connect to soap api, error: ' . $e->getMessage());
die;
} catch (\Throwable | \Exception $e) {
print_r($e->getMessage());
die;
}
}
In handle method i automaticaly inject OrdersMagentoService, this service do connect to magento soap api and exctend from BaseMagentoService
class OrdersMagentoService extends BaseMagentoService
{
public function getRemoteOrders()
{
$complex = [
'complex_filter' => [
[
'key' => 'status',
'value' =>
[
'key' => 'in',
'value' => 'closed,canceled,holded,processing,complete'
]
],
[
'key' => 'updated_at',
'value' => [
'key' => 'from',
'value' => now()->subDays(200),
]
]
]
];
return $this->salesOrderList($complex);
}
}
class BaseMagentoService
{
/**
* #var
*/
private $client;
/**
* #var
*/
private $session;
/**
* #var \Illuminate\Config\Repository|mixed
*/
protected $config;
/**
* BaseMagentoService constructor.
*/
public function __construct()
{
$this->config = config('services.magento');
$this->connect();
}
/**
* Do connect to soap api v2
*
* #throws \SoapFault
*/
private function connect()
{
$this->client = new SoapClient($this->getApiUrl());
$this->session = $this->client->login($this->config['user_name'], $this->config['password']);
}
public function __call($resource, $arguments)
{
return $this->client->$resource($this->session, ...$arguments);
}
}
In BaseMagentoService constructor i create connection to magento soap api. But if connection throw error(for example wrong username and pass) then i can't handle this in cron job file. I understand that laravel at first create OrdersMagentoService, it throw error and try catch in handle function not works, but i dont know how to fix this.
I can add in handle method
try {
$magentoService = resolve(OrdersMagentoService::class)
$orders = $magentoService->getRemoteOrders();
print_r($orders);
} catch (SoapFault $e) {
$this->line('Error connect to soap api, error: ' . $e->getMessage());
die;
} catch (\Throwable | \Exception $e) {
print_r($e->getMessage());
die;
}
And remove automatic DI, and tit should work well, but i dont want to do it.
Also if i add some try catch in connect method of BaseMagentoService then i cant log this error in my cron job.
What is best way to handle this?
First I would switch the handling from the artisan command to a Job or an Event that might be more suited to the problem. Then your artisan command simply fires off this new job or event each time is executed.
This is also somehow stated in the docs as a best practice:
For greater code reuse, it is good practice to keep your console commands light and let them defer to application services to accomplish their tasks.
Then if you look at the documentation about jobs, there is a section about error handling for failed jobs where you are told that you can define a method that gets triggered once he job has failed. Example excerpt taken from the docs:
class YourJob implements ShouldQueue
{
/**
* Execute the job.
*
* #param AudioProcessor $processor
* #return void
*/
public function handle(AudioProcessor $processor)
{
// Process uploaded podcast...
}
/**
* The job failed to process.
*
* #param Exception $exception
* #return void
*/
public function failed(Exception $exception)
{
// Send user notification of failure, etc...
}
}
You can read more in the documentation. There is also a paragraph about global jobs failure handling that might suit your use case
I need to retry a job after 7 minutes after failed.
I try with $this->release(7); but on sometimes the job is running more that 1 time.
<?php
namespace Froakie\Listeners;
use Froakie\Components\Locks\LocksFactory;
use Froakie\Components\CRM\CrmFactory;
use Froakie\Events\NewLeadDataIncoming;
use Froakie\Services\LeadsService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
/**
* Class CreateLead
*
* #package Froakie\Listeners
* #author Miguel Borges <miguel.borges#edirectinsure.com>
*/
class CreateOrUpdateLead implements ShouldQueue
{
use InteractsWithQueue;
const LOCKS_PREFIX = 'lead_event_lock_';
/**
* #var \Froakie\Services\LeadsService
*/
protected $leadsService;
/**
* Create the event listener.
*
* #param \Froakie\Services\LeadsService $leadsService
*/
public function __construct(LeadsService $leadsService)
{
$this->leadsService = $leadsService;
}
/**
* Handle the event.
*
* #param \Froakie\Events\NewLeadDataIncoming $event
* #throws \Exception
*/
public function handle(NewLeadDataIncoming $event)
{
app('log')->debug('CreateOrUpdateLead listener has catch a NewLeadDataIncoming', ['event' => $event]);
$lead = $this->leadsService->getLeadById($event->leadId);
LocksFactory::getInstance()->getMutexAdapter(self::LOCKS_PREFIX . $lead->getId(), null, 60)
->synchronized(function () use ($lead, $event) {
if (!$lead->isCreated()) {
$lead->setCreated(true)->save();
try {
$lead->crm_id = CrmFactory::getInstance()->getCRMLeadAdapter($lead->getCrm())
->createLead($event->leadDto);
$lead->save();
app('log')->info("A new lead has been created in {$lead->getCrm()}", [
'reference' => $lead->getReference(),
'crm_id' => $lead->getCrmId()
]);
return;
} catch (\Exception $exception) {
$lead->setCreated(false)->save();
throw $exception;
}
}
});
if (null !== $lead->getCrmId()) {
CrmFactory::getInstance()->getCRMLeadAdapter($lead->getCrm())
->updateLead($lead->getCrmId(), $event->leadDto);
app('log')->info("A lead has been updated in {$lead->getCrm()}", [
'reference' => $lead->getReference(),
'crm_id' => $lead->getCrmId()
]);
return;
}
$this->release(7);
}
}
I would create a task scheduler entry that sweeps the failed_jobs table and retries the job based on the failed_at column:
protected function schedule(Schedule $schedule)
{
$schedule->call(function () {
$jobs = DB::table('failed_jobs')->where('failed_at', '<=', now()->subMinutes(7))->get();
foreach ($jobs as $job) {
Artisan::call('queue:retry', [
'id' => $job->id
]);
}
})->everyMinute();
}