Laravel skip sending mail based on conditions - laravel

I'm looping through users in my controller function and sending an email to each user, but there's a special user who will sometimes be in the list who shouldn't receive emails.
I'd like to put an if statement in to skip that loop iteration if it's that user, but when I add return, return null etc, or just nothing in my if/else in the mailable class build function I get
InvalidArgumentException
Invalid view.
I could add the conditional in the controller and I'm sure that would be fine, except I have these emails in a whole load of my controllers so would be writing the conditional many times. If I can get it into a central location (and the mailable class is the only one I'm aware of) then I can write it once.
EDIT: the loop code has been requested so added below, but this is just one of many email loops in my controllers so NOT where I'd like to add my conditional.
$objNotification = new \stdClass();
$objNotification->message_body = "stuff";
$user_ids = DB::select('select user_id from users_to_things where thing_id = ?', [$thing->id]);
foreach($user_ids as $user_id) {
$user = User::find($user_id->user_id);
$objNotification->receiver = $user->name;
Mail::to($user->email)->send(new NotificationEmail($objNotification));
}
Here's my mailable class's build function (where I DO want to add the conditional if at all possible):
public function build()
{
if($this->notification->receiver == 'the special user') {
return;
} else {
return $this->from('somebody#somewhere.com', 'Sender Name')
->subject('some subject')
->view('emails.notification')
->text('emails.notification_plain');
}
}

What you want would conflict with the Single Responsibility Principle. The class is only suppose to send the email.
The build() in your mailable class returns error cause it expect you to return a message ( subject ,body, template or view, etc) for your mail.
1.) maybe instead you can assert a fake mail in your condition;
2.) Create a function in email class
public function needSend($mail)
{
... loads of conditions
return (condition) ? true : false;
}
then
if ($mail->needSend($mail)) {
\Mail::send($mail);
}
3.) Or the easiest but dirtiest way is you can put the condition inside your Controller in your for loop
foreach($user_ids as $user_id) {
$user = User::find($user_id->user_id);
$objNotification->receiver = $user->name;
if(condition == true) continue;
Mail::to($user->email)->send(new NotificationEmail($objNotification));
}
You can also read this thread on github. https://github.com/laravel/ideas/issues/519

Using the notification class, check your condition like a suppression list or maybe runtime condition. Return an empty array and it'll exit.
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return $notifiable->notificationSuppression()->exists() ? [] : ['mail'];
}

Related

How to send data in the mailgun header with LARAVEL?

I am working with webhooks for the first time, in which I have to pass some 3 variables defined for later when Laravel takes it again I can update an action of the email sent for some reports.
The problem is that I can't pass data in the header of the email.
This is the structure that commonly sent the email to the users:
public $data;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct($view, $subject, $data)
{
$this->view = $view;
$this->subject = $subject;
$this->data = $data;
}
public function build()
{
$message = $this->data;
// print_r($variables);
// exit;
return $this->from(config('mail.from.address'), config('mail.from.name'))
->view($this->view)
->subject($this->subject); //WORKED
/**NO WORKED*/
->withSwiftMessage(function ($message) use ($v){
$v->getHeaders()
->addTextHeader('Custom-Header', 'HeaderValue1')
->addTextHeader('Custom-Header2', 'HeaderValue2');
});
}
The emails if sent in that there is no problem, with the view and the data that is filled in the mail, but in the header the data is not filled in at least in this case, the 2 variables set ['Custom-Header', 'HeaderValue1', 'Custom-Header', 'HeaderValue2].
I had the same problem too and it took me quite a bit of research and testing. It seems that it needs to be in json format and you need to use 'X-Mailgun-Variables' instead of 'Custom-Header'. It should look like this
->addTextHeader('X-Mailgun-Variables', '{"variable1": "1", "variable2": "2"}')
the webhook should give you this result
"user-variables":{
"variable1":"1",
"variable2":"2"
},

Operation without entity

I've been looking for a solution for a while but none of the one I find really allows me to do what I want. I would just like to create routes that don't necessarily require an entity or id to be used. Can you help me the documentation is not clear to do this.
Thank you beforehand.
As you can read in the General Design Considerations, just make an ordinary PHP class (POPO). Give it an ApiResource annontation like this:
* #ApiResource(
* collectionOperations={
* "post"
* },
* itemOperations={}
* )
Make sure the folder your class is in is in the paths list in api/config/packages/api_platform.yaml. There usually is the following configuration:
api_platform:
mapping:
paths: ['%kernel.project_dir%/src/Entity']
You should add your path if your class is not in the Entity folder.
Api Platform will expect json to be posted and try to unserialize it into an instance of your class. Make a custom DataPersister to process the instance, for example if your class is App\ApiCommand\Doit:
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\ApiCommand\Doit;
use App\ApiResult\DoitResult;
final class DoitDataPersister implements ContextAwareDataPersisterInterface
{
public function supports($data, array $context = []): bool
{
return $data instanceof Doit;
}
public function persist($data, array $context = [])
{
// code to process $data
$result = new DoitResult();
$result->description = 'Hello world';
return $result;
}
public function remove($data, array $context = [])
{
// will not be called if you have no delete operation
}
}
If you need Doctrine, add:
public function __construct(ManagerRegistry $managerRegistry)
{
$this->managerRegistry = $managerRegistry;
}
See Injecting Extensions for how to use it.
Notice that the result returned by ::persist is not an instance of Doit. If you return a Doit api platform will try to serialize that as the result of your operation. But we have marked Doit as an ApiResource so (?) api platform looks for an item operation that can retrieve it, resulting in an error "No item route associated with the type App\ApiCommand\Doit". To avoid this you can return any object that Symfonies serializer can serialize that is not an ApiResource. In the example an instance of DoitResult. Alternatively you can return an instance of Symfony\Component\HttpFoundation\Response but then you have to take care of the serialization yourself.
The post operation should already work, but the swagger docs are made from metadata. To tell api platform that it should expect a DoitResult to be returned, change the #ApiResource annotation:
* collectionOperations={
* "post"={
* "output"=DoitResult::class
* }
* },
This will the add a new type for DoitResult to the swagger docs, but the descriptions are still wrong. You can correct them using a SwaggerDecorator. Here is one for a 201 post response:
namespace App\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class SwaggerDecorator implements NormalizerInterface
{
private $decorated;
public function __construct(NormalizerInterface $decorated)
{
$this->decorated = $decorated;
}
public function normalize($object, string $format = null, array $context = [])
{
$summary = 'short explanation about DoitResult';
$docs = $this->decorated->normalize($object, $format, $context);
$docs['paths']['/doit']['post']['responses']['201']['description'] = 'Additional explanation about DoitResult';
$responseContent = $docs['paths']['/doit']['post']['responses']['201']['content'];
$this->setByRef($docs, $responseContent['application/ld+json']['schema']['properties']['hydra:member']['items']['$ref'],
'description', $summary);
$this->setByRef($docs, $responseContent['application/json']['schema']['items']['$ref'],
'description', $summary);
return $docs;
}
public function supportsNormalization($data, string $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
private function setByRef(&$docs, $ref, $key, $value)
{
$pieces = explode('/', substr($ref, 2));
$sub =& $docs;
foreach ($pieces as $piece) {
$sub =& $sub[$piece];
}
$sub[$key] = $value;
}
}
To configure the service add the following to api/config/services.yaml:
'App\Swagger\SwaggerDecorator':
decorates: 'api_platform.swagger.normalizer.api_gateway'
arguments: [ '#App\Swagger\SwaggerDecorator.inner' ]
autoconfigure: false
If your post operation is not actually creating something you may not like the 201 response. You can change that by specifying the response code in the #ApiResource annotation, for example:
* collectionOperations={
* "post"={
* "output"=DoitResult::class,
* "status"=200
* }
* },
You may want to adapt the SwaggerDecorator accordingly.
Creating a "get" collection operation is similar, but you need to make a DataProvider instead of a DataPersister. The chapter9-api branch of my tutorial contains an example of a SwaggerDecorator for a collection response.
Thanks you for answer. I had some information but not everything. I will try the weekend.

How to cancel queued job in Laravel or Redis

How can I browse all the pending jobs within my Redis queue so that I could cancel the Mailable that has a certain emailAddress-sendTime pair?
I'm using Laravel 5.5 and have a Mailable that I'm using successfully as follows:
$sendTime = Carbon::now()->addHours(3);
Mail::to($emailAddress)
->bcc([config('mail.supportTeam.address'), config('mail.main.address')])
->later($sendTime, new MyCustomMailable($subject, $dataForMailView));
When this code runs, a job gets added to my Redis queue.
I've already read the Laravel docs but remain confused.
How can I cancel a Mailable (prevent it from sending)?
I'd love to code a webpage within my Laravel app that makes this easy for me.
Or maybe there are tools that already make this easy (maybe FastoRedis?)? In that case, instructions about how to achieve this goal that way would also be really helpful. Thanks!
Update:
I've tried browsing the Redis queue using FastoRedis, but I can't figure out how to delete a Mailable, such as the red arrow points to here:
UPDATE:
Look at the comprehensive answer I provided below.
Make it easier.
Don't send an email with the later option. You must dispatch a Job with the later option, and this job will be responsible to send the email.
Inside this job, before send the email, check the emailAddress-sendTime pair. If is correct, send the email, if not, return true and the email won't send and the job will finish.
Comprehensive Answer:
I now use my own custom DispatchableWithControl trait instead of the Dispatchable trait.
I call it like this:
$executeAt = Carbon::now()->addDays(7)->addHours(2)->addMinutes(17);
SomeJobThatWillSendAnEmailOrDoWhatever::dispatch($contactId, $executeAt);
namespace App\Jobs;
use App\Models\Tag;
use Carbon\Carbon;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Log;
class SomeJobThatWillSendAnEmailOrDoWhatever implements ShouldQueue {
use DispatchableWithControl,
InteractsWithQueue,
Queueable,
SerializesModels;
protected $contactId;
protected $executeAt;
/**
*
* #param string $contactId
* #param Carbon $executeAt
* #return void
*/
public function __construct($contactId, $executeAt) {
$this->contactId = $contactId;
$this->executeAt = $executeAt;
}
/**
* Execute the job.
*
* #return void
*/
public function handle() {
if ($this->checkWhetherShouldExecute($this->contactId, $this->executeAt)) {
//do stuff here
}
}
/**
* The job failed to process.
*
* #param Exception $exception
* #return void
*/
public function failed(Exception $exception) {
// Send user notification of failure, etc...
Log::error(static::class . ' failed: ' . $exception);
}
}
namespace App\Jobs;
use App\Models\Automation;
use Carbon\Carbon;
use Illuminate\Foundation\Bus\PendingDispatch;
use Log;
trait DispatchableWithControl {
use \Illuminate\Foundation\Bus\Dispatchable {//https://stackoverflow.com/questions/40299080/is-there-a-way-to-extend-trait-in-php
\Illuminate\Foundation\Bus\Dispatchable::dispatch as parentDispatch;
}
/**
* Dispatch the job with the given arguments.
*
* #return \Illuminate\Foundation\Bus\PendingDispatch
*/
public static function dispatch() {
$args = func_get_args();
if (count($args) < 2) {
$args[] = Carbon::now(TT::UTC); //if $executeAt wasn't provided, use 'now' (no delay)
}
list($contactId, $executeAt) = $args;
$newAutomationArray = [
'contact_id' => $contactId,
'job_class_name' => static::class,
'execute_at' => $executeAt->format(TT::MYSQL_DATETIME_FORMAT)
];
Log::debug(json_encode($newAutomationArray));
Automation::create($newAutomationArray);
$pendingDispatch = new PendingDispatch(new static(...$args));
return $pendingDispatch->delay($executeAt);
}
/**
* #param int $contactId
* #param Carbon $executeAt
* #return boolean
*/
public function checkWhetherShouldExecute($contactId, $executeAt) {
$conditionsToMatch = [
'contact_id' => $contactId,
'job_class_name' => static::class,
'execute_at' => $executeAt->format(TT::MYSQL_DATETIME_FORMAT)
];
Log::debug('checkWhetherShouldExecute ' . json_encode($conditionsToMatch));
$automation = Automation::where($conditionsToMatch)->first();
if ($automation) {
$automation->delete();
Log::debug('checkWhetherShouldExecute = true, so soft-deleted record.');
return true;
} else {
return false;
}
}
}
So, now I can look in my 'automations' table to see pending jobs, and I can delete (or soft-delete) any of those records if I want to prevent the job from executing.
Delete job by id.
$job = (new \App\Jobs\SendSms('test'))->delay(5);
$id = app(Dispatcher::class)->dispatch($job);
$res = \Illuminate\Support\Facades\Redis::connection()->zscan('queues:test_queue:delayed', 0, ['match' => '*' . $id . '*']);
$key = array_keys($res[1])[0];
\Illuminate\Support\Facades\Redis::connection()->zrem('queues:test_queue:delayed', $key);
Maybe instead of canceling it you can actually remove it from the Redis, from what Ive read from official docs about forget command on Redis and from Laravel official doc interacting with redis you can basically call any Redis command from the interface, if you could call the forget command and actually pass node_id which in this case I think it's that number you have in your image DEL 1517797158 I think you could achieve the "cancel".
hope this helps
$connection = null;
$default = 'default';
//For the delayed jobs
var_dump( \Queue::getRedis()->connection($connection)->zrange('queues:'.$default.':delayed' ,0, -1) );
//For the reserved jobs
var_dump( \Queue::getRedis()->connection($connection)->zrange('queues:'.$default.':reserved' ,0, -1) );
$connection is the Redis connection name which is null by default, and The $queue is the name of the queue / tube which is 'default' by default!
source : https://stackoverflow.com/a/42182586/6109499
One approach may be to have your job check to see if you've set a specific address/time to be canceled (deleted from queue). Setup a database table or cache a value forever with the address/time in an array. Then in your job's handle method check if anything has been marked for removal and compare it to the mailable's address/time it is processing:
public function handle()
{
if (Cache::has('items_to_remove')) {
$items = Cache::get('items_to_remove');
$removed = null;
foreach ($items as $item) {
if ($this->mail->to === $item['to'] && $this->mail->sendTime === $item['sendTime']) {
$removed = $item;
$this->delete();
break;
}
}
if (!is_null($removed)) {
$diff = array_diff($items, $removed);
Cache::set(['items_to_remove' => $diff]);
}
}
}
I highly recommend checking out the https://laravel.com/docs/master/redis (I run dev/master) but it shows you where they are headed. Most of it works flawlessly now.
Under laravel 8.65 you can just set various status's depending.
protected function listenForEvents()
{
$this->laravel['events']->listen(JobProcessing::class, function ($event) {
$this->writeOutput($event->job, 'starting');
});
$this->laravel['events']->listen(JobProcessed::class, function ($event) {
$this->writeOutput($event->job, 'success');
});
$this->laravel['events']->listen(JobFailed::class, function ($event) {
$this->writeOutput($event->job, 'failed');
$this->logFailedJob($event);
});
}
You can even do $this->canceled;
I highly recommend Muhammads Queues in action PDF. Trust me well worth the money if your using. queues for very important things.... especially with redis . At first TBH I was turned off a bit cause hes a Laravel employee and I thought he should just post things that are helpful but he goes into specific use cases that they do with forge and other items he does plus dives deep into the guts of how queue workers work whether its horizon or whatever. Total eyeopener for me.
Removing all queued jobs:
Redis::command('flushdb');
Using redis-cli I ran this command:
KEYS *queue*
on the Redis instance holding queued jobs,
then deleted whatever keys showed up in the response
DEL queues:default queues:default:reserved
Delete the job from the queue.
$this->delete();

Adding methods to Eloquent Model in Laravel

I'm a bit confused how I am to add methods to Eloquent models. Here is the code in my controller:
public function show($id)
{
$limit = Input::get('limit', false);
try {
if ($this->isExpand('posts')) {
$user = User::with(['posts' => function($query) {
$query->active()->ordered();
}])->findByIdOrUsernameOrFail($id);
} else {
$user = User::findByIdOrUsernameOrFail($id);
}
$userTransformed = $this->userTransformer->transform($user);
} catch (ModelNotFoundException $e) {
return $this->respondNotFound('User does not exist');
}
return $this->respond([
'item' => $userTransformed
]);
}
And the code in the User model:
public static function findByIdOrUsernameOrFail($id, $columns = array('*')) {
if (is_int($id)) return static::findOrFail($id, $columns);
if ( ! is_null($user = static::whereUsername($id)->first($columns))) {
return $user;
}
throw new ModelNotFoundException;
}
So essentially I'm trying to allow the user to be retrieved by either user_id or username. I want to preserve the power of findOrFail() by creating my own method which checks the $id for an int or string.
When I am retrieving the User alone, it works with no problem. When I expand the posts then I get the error:
Call to undefined method
Illuminate\Database\Query\Builder::findByIdOrUsernameOrFail()
I'm not sure how I would go about approaching this problem.
You are trying to call your method in a static and a non-static context, which won't work. To accomplish what you want without duplicating code, you can make use of Query Scopes.
public function scopeFindByIdOrUsernameOrFail($query, $id, $columns = array('*')) {
if (is_int($id)) return $query->findOrFail($id, $columns);
if ( ! is_null($user = $query->whereUsername($id)->first($columns))) {
return $user;
}
throw new ModelNotFoundException;
}
You can use it exactly in the way you are trying to now.
Also, you can use firstOrFail:
public function scopeFindByIdOrUsernameOrFail($query, $id, $columns = array('*')) {
if (is_int($id)) return $query->findOrFail($id, $columns);
return $query->whereUsername($id)->firstOrFail($columns);
}
Your method is fine, but you're trying to use it in two conflicting ways. The one that works as you intended is the one in the else clause, like you realised.
The reason the first mention doesn't work is because of two things:
You wrote the method as a static method, meaning that you don't call it on an instantiated object. In other words: User::someStaticMethod() works, but $user->someStaticMethod() doesn't.
The code User::with(...) returns an Eloquent query Builder object. This object can't call your static method.
Unfortunately, you'll either have to duplicate the functionality or circumvent it someway. Personally, I'd probably create a user repository with a non-static method to chain from. Another option is to create a static method on the User model that starts the chaining and calls the static method from there.
Edit: Lukas's suggestion of using a scope is of course by far the best option. I did not consider that it would work in this situation.

laravel controller action structure

After watching many laracasts, one statement is everywhere: keep the controller as light as possible.
Ok, I am trying to familiarize myself with laravel concepts and philosophy, with the Repository and the separation of concerns patterns and I have some questions that bother me, let's assume the following:
Route::resource('/item', 'ItemController');
class Item extends \Eloquent {}
the repo
class EloquentItemRepo implements ItemRepo {
public function all()
{
return Item::all();
}
public function find($id)
{
return Item::where('id', '=', $id);
}
}
and the controller:
class ItemController extends BaseController {
protected $item;
public function __construct(ItemRepo $item)
{
$this->item = $item;
}
public function index()
{
$items = $this->item->all();
return Response::json(compact('items'))
}
}
For now, everything is simple and clean (assume that the repo is loaded by providers etc.) the controller is really simple and does nothing except loading and returning the data (I used json but anything will do).
Please assume that I am using an auth filter that checks that the user
is logged in and exists, or return an error if it doesn't, so I don't
have to do any further check in the controller.
Now, what if I need to do more checks, for instance:
response_* methods are helpers that format a Json response
public function destroy($id)
{
try {
if ($this->item->destroy($id)) {
return Response::json(['success' => true]);
}
return response_failure(
Lang::get('errors.api.orders.delete'),
Config::get('status.error.forbidden')
);
} catch (Exception $e) {
return response_failure(
Lang::get('errors.api.orders.not_found'),
Config::get('status.error.notfound')
);
}
}
In this case I have to test many things:
The desctuction worked? (return true)
The destruction failed? (return false)
There was an error during deletion ? (ex.: the item wasn't found with firstOrFail)
I have methods where many more tests are done, and my impression is that the controller is growing bigger and bigger so I can handle any possible errors.
Is it the right way to manage this ? The controller should be full of checks or the tests should be moved elsewhere ?
In the provider I often use item->firstOrFail() and let the exception bubble up to the controller, is it good ?
If someone could point me to the right direction as all the laracasts or other tutorials always use the simpler case, where not many controls are needed.
Edits: Practical case
Ok so here a practical case of my questioning:
controller
/**
* Update an order.
* #param int $id Order id.
* #return \Illuminate\Http\JsonResponse
*/
public function update($id)
{
try {
$orderItem = $this->order->update($id, Input::all());
if (false === $orderItem) {
return response_failure(
Lang::get('errors.api.orders.update'),
Config::get('status.error.forbidden')
);
}
return response_success();
} catch (Exception $e) {
return response_failure(
Lang::get('errors.api.orders.not_found'),
Config::get('status.error.notfound')
);
}
}
repo
public function update($id, $input)
{
$itemId = $input['itemId'];
$quantity = $input['quantity'] ?: 1;
// cannot update without item id
if (!$itemId) {
return false;
}
$catalogItem = CatalogItem::where('hash', '=', $itemId)->firstOrFail();
$orderItem = OrderItem::fromCatalogItem($catalogItem);
// update quantity
$orderItem->quantity = $quantity;
return Order::findOrFail($id)->items()->save($orderItem);
}
In this case thare are 3 possible problems:
order not found
catalogItem not found
itemId not set in post data
In the way I have organized that, the problem is that the top level error message won't be clear, as it will alway state: "order not found" even if it's the catalog item that couldn't be found.
The only possibility that I see is to catch multiple exceptions codes in the controller and raise a different error message, but won't this overload the controller ?

Resources