How I can handle nexmo errors, I use try{}catch(){} but it not work, and I got this error Nexmo \ Client \ Exception \ Request (29)
Non White-listed Destination - rejected i know this error but i need handle it.
this is a code:
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\NexmoMessage;
//use App\admin\Course;
class ConfirmedCourse extends Notification
{
use Queueable;
protected $course;
/**
* Create a new notification instance.
*
* #return void
*/
public function __construct($course)
{
$this->course = $course;
}
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return ['nexmo'];
}
public function toNexmo($notifiable)
{
try {
$message = new NexmoMessage();
$message->content($this->course)
->unicode();
return $message;
}catch (\Exception $e) {
$e->getMessage();
}
}
}
I fixed this in the past by wrapping the call to notify() in the try catch:
try {
$variableToCatch = $YourModel->notify(new ConfirmedCourse($data));
} catch (\Exception $e) {
// Do what you want here...
// Log::error('nexmo failed...');
// echo 'Caught exception: ', $e->getMessage(), "\n";
// Log::error($e->getMessage());
// dd($e->getMessage())
}
If you post the line where you are calling notify() I can update to your use exact needed statement.
Remove the try catch from ConfirmedCourse class and put it around the call in the method calling it.
Here is how I handled a slack notification failure:
try {
$slackNotification = $user->notify(new SlackNotification($slackData));
} catch (\Exception $e) {
Log::error('slack notification failed.');
}
Related
I upgraded my Laravel app to the latest version Laravel 8. My problem now is that I cannot figure out how to render an exception thrown by the app to html in order to send that html as an email.
My current code (that worked for Laravel 5) to render and send an exception within the App\Exceptions\Handler class:
public function sendEmail(Exception $exception)
{
try {
$e = FlattenException::create($exception);
$handler = new SymfonyExceptionHandler();
$html = $handler->getHtml($e);
$routeName = URL::full();
Mail::send(new ExceptionEmail($html, $routeName));
} catch (Exception $ex) {
if (env("APP_DEBUG") == true) {
dd($ex);
}
}
}
The problem is that class \Symfony\Component\Debug\Exception\FlattenException does not exist anymore in my upgraded app.
What is the appropriate way to render exceptions as html now in Laravel 8?
Thank you very much in advance.
Alright i managed to receive email, here is the code
Composer requirement
"require": {
"php": "^7.3|^8.0",
.....
"jeremykenedy/laravel-exception-notifier": "^2.2",
"laravel/framework": "^8.40",
.....
},
app/Exceptions/Handler.php
<?php
namespace App\Exceptions;
use App\Mail\ExceptionOccurred;
use Exception;
use Throwable;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpFoundation\Response;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* #var array
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* #var array
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*
* #return void
*/
public function register()
{
$this->reportable(function (Throwable $e) {
//
});
}
/**
* Report or log an exception.
*
* #param Exception $e
* #return void
* #throws Throwable
*/
public function report(Throwable $e)
{
if ($this->shouldReport($e)) {
$this->sendEmail($e); // sends an email
}
parent::report($e);
}
/**
* Render an exception into an HTTP response.
*
* #param Request $request
* #param Throwable $e
* #return Response
*
* #throws Throwable
*/
public function render($request, Throwable $e): Response
{
return parent::render($request, $e);
}
public function sendEmail(Throwable $exception)
{
try {
$e = FlattenException::createFromThrowable($exception);
$handler = new HtmlErrorRenderer(true);
$css = $handler->getStylesheet();
$content = $handler->getBody($e);
Mail::to('your_email_address_here')->send(new ExceptionOccurred($content,$css));
} catch (Throwable $exception) {
Log::error($exception);
}
}
}
app/Mail/ExceptionOccurred.php
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class ExceptionOccurred extends Mailable
{
use Queueable, SerializesModels;
private $content;
private $css;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct($content,$css)
{
$this->content = $content;
$this->css = $css;
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
$emailsTo = str_getcsv(config('exceptions.emailExceptionsTo'), ',');
$ccEmails = str_getcsv(config('exceptions.emailExceptionCCto'), ',');
$bccEmails = str_getcsv(config('exceptions.emailExceptionBCCto'), ',');
$fromSender = config('exceptions.emailExceptionFrom');
$subject = config('exceptions.emailExceptionSubject');
if ($emailsTo[0] === null) {
$emailsTo = config('exceptions.emailExceptionsToDefault');
}
if ($ccEmails[0] === null) {
$ccEmails = config('exceptions.emailExceptionCCtoDefault');
}
if ($bccEmails[0] === null) {
$bccEmails = config('exceptions.emailExceptionBCCtoDefault');
}
if (! $fromSender) {
$fromSender = config('exceptions.emailExceptionFromDefault');
}
if (! $subject) {
$subject = config('exceptions.emailExceptionSubjectDefault');
}
return $this->from($fromSender)
->to($emailsTo)
->cc($ccEmails)
->bcc($bccEmails)
->subject($subject)
->view(config('exceptions.emailExceptionView'))
->with('content', $this->content)
->with('css', $this->css);
}
}
resources/views/emails/exception.blade.php
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<style>{!! $css ?? '' !!}</style>
</head>
<body>
{!! $content ?? '' !!}
</body>
</html>
Please let me know if that works.
You can define your log channels in config/logging.php as I have already done.
Then you can send a log to your new channel like this:
Log::channel('my-channel')->error('message');
Now, when I want to catch an exception without throwing it, I usually use the report() helper function like this:
try{
throw new Exception('message');
}catch(Exception $e){
report($e);
}
That is very useful for continuing a script after an exception is thrown, but it always logs the exception to my default logging channel.
I want to use the report() function and specify that it should be reported to my-channel.
I played around with app/Exceptions/Handler.php without any luck.
So far I decided to just make a simple class to emulate report() instead of trying to use it directly:
<?php
namespace App\Exceptions;
use Log;
use Throwable;
class Reporter
{
/**
* Reports an exception to a specific channel.
*
* #param Throwable $e
* #param string $channel
* #param array $context
*/
public static function report(Throwable $e, $channel = 'daily', $context = [])
{
Log::channel($channel)->error(
$e->getMessage(),
array_merge(
Context::getContext(),
$context,
['exception' => $e],
)
);
}
}
And you would use it like this:
try{
throw new Exception('message');
}catch(Exception $e){
Reporter::report($e, 'my-channel');
}
You can also pass extra context to the log message as an array.
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
There is typical code in controller Laravel:
public function create(CreateInvoiceRequest $request)
{
try {
$invoice = Invoice::create(['']);
return response()->json($model);
} catch (\Exception $e) {
return \Response::json(["errors" => $e->getMessage()], 400);
}
}
In exception case I catch it and show message, how to delagate (move) this in global exception Laravel? Need I do something like this?
try { } } catch (\Exception $e) { throw new Exception($e); }
Laravel has a nice solution for this. In the documentation we are told to do this kind of exception handeling in App\Exceptions\Handler.
A very simple example could be the following:
// Your controller.
try {
$invoice = Invoice::create(['']);
return response()->json($model);
} catch (\Exception $e) {
throw new CustomException('Invoice creation failed.');
}
// app\Exceptions\Handler.php
public function render($request, Exception $exception)
{
if ($exception instanceof CustomException) {
return response()->view('errors.custom', [], 500);
}
return parent::render($request, $exception);
}
I tried to find out if create would throw a specific exception. Unfortunately I could not find it out so quickly. If this was the case, you could remove the try catch and just listen to this specific exception in the render method.
Update
(not tested)
In addition, you can also overwrite the save method to prevent from having to wrap (all) database writing method calls with a try and catch.
We will need a BaseModel class:
<?php
namespace App\Models;
use App\Exceptions\ModelSaveException;
use Illuminate\Database\Eloquent\Model as EloquentModel;
class Model extends EloquentModel
{
/**
* Save the model to the database.
*
* #param array $options
* #return bool
* #throws \App\Exceptions\ModelSaveException
*/
public function save(array $options = [])
{
try {
return parent::save($options);
} catch(\Exception $e) {
throw new ModelSaveException($this);
}
}
}
Your controller will look cleaner without try catch:
$invoice = Invoice::create(['']);
return response()->json($model);
As an extra we can check if our model was being created of updated by making is of the exists property.
<?php
namespace App\Exceptions;
class ModelSaveException extends \Exception
{
/**
* ModelSaveException constructor.
*
* #param \Illuminate\Database\Eloquent\Model $model
* #return void
*/
public function __construct($model)
{
if ($model->exists) {
parent::__construct('Failed updating model.');
} else {
parent::__construct('Failed creating model.');
}
}
}
Ofcourse don't forget to extend from you newly created model:
use App\Models\Model;
class Invoice extends Model
You can create a custom renderable exception.
try {
// Your code...
} catch (\Exception $e) {
throw new \App\Exceptions\CustomException('Your message');
}
Instead of type-checking exceptions in the exception handler's report and render methods, you may define report and render methods directly on your custom exception. When these methods exist, they will be called automatically by the framework:
/**
* Report the exception.
*
* #return void
*/
public function report()
{
//
}
/**
* Render the exception into an HTTP response.
*
* #param \Illuminate\Http\Request
* #return \Illuminate\Http\Response
*/
public function render($request)
{
return response(...);
}
For more information: https://laravel.com/docs/5.8/errors#renderable-exceptions