Passing Argument to Lumen Job - php

I am building an app in Lumen that sends text messages with Twilio. I am in the process of creating the Job that actually handles the sending of the message. I dispatch the Job directly from the routes file. Eventually, the "To" phone number will be entered in a form so I'm having it passed into the job as an argument.
Here is my Job class:
<?php
namespace App\Jobs;
class FiveMessageJob extends Job
{
protected $number;
/**
* Create a new job instance.
*
* #return void
*/
public function __construct($number)
{
$this->number = $number;
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
//dd($this->number);
// this line loads the library
require base_path('twilio-php-master/Services/Twilio.php');
$account_sid = 'xxxxxxxxxxxx';
$auth_token = 'xxxxxxxxxxxx';
$client = new \Services_Twilio($account_sid, $auth_token);
$client->account->messages->create(array(
'To' => $this->number,
'From' => "xxxxxxxxxxx",
'Body' => "What's up Stackoverflow?"
));
}
}
Here is the route that dispatches the job:
$app->get('/', function () use ($app) {
$number = "+15555555555";
dispatch(new \App\Jobs\FiveMessageJob($number));
return view('index');
});
I am getting the Twilio error: A 'To' phone number is required.
When I dd($this->number) inside of the handle function, it returns null.
Obviously the $number argument isn't being passed through. I'm sure I'm missing something obvious so I could use a second set of eyes and any help y'all can offer.
Thanks.

Related

send mail returns null

When trying to send a message, In my view verifyEmail.blade.php $agent is null and $agent->name says trying to get property of non object.
verifyEmail.blade.php
<body>
<h2> Welcome to our website{{ $agent->name }} </h2>
click here to verify your email
</body>
This is how I am using the Mail class. In my Mail folder in verifyEmail file I have a construct function which collects the $agent model.
verifyEmail.php
class verifyEmail extends Mailable
{
public $agent;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct($agent)
{
$this->$agent = $agent;
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
return $this->view('emails.verifyEmail');
}
}
And in my admin controller which does the registration for the user looks like this. The send method passes the agent model to the verifyEmail.php which was working in the tutorial I watched. How can I make agent model available in verifyEmail.blade.php
AdminController
$agent = new agent($data);
$agent->name = $data["name"];
$agent->email = $data["email"];
$agent->nrc = $data["nrc"];
$agent->resident = $data["residents"];
$agent->password = Hash::make($data["password"]);
$agent->save();
verifyUser::create(
[
'token' => Str::random(60),
'agent_id' => $agent->id,
]
);
Mail::to($agent->email)->send(new verifyEmail($agent));
I think there can be a slight modification in the code provided to solve this.
Inside the file verifyEmail.php, the line
$this->$agent = $agent;
should be
$this->agent = $agent;
Because $this->$agent might not be able to find the class level variable 'agent' and update its value that is provided in the constructor so it will have the default value null which is shown afterwards.
Configuring The Sender
Using The from Method
First, let's explore configuring the sender of the email. Or, in other words, who the email is going to be "from". There are two ways to configure the sender. First, you may use the from method within your mailable class' build method:
/**
* Build the message.
*
* #return $this
*/
public function build()
{
return $this->from('example#example.com')
->view('emails.orders.shipped');
}
Using A Global from Address
However, if your application uses the same "from" address for all of its emails, it can become cumbersome to call the from method in each mailable class you generate. Instead, you may specify a global "from" address in your config/mail.phpconfiguration file. This address will be used if no other"from" address is specified within the mailable class:
'from' => ['address' => 'example#example.com', 'name' => 'App Name'],
In addition, you may define a global "reply_to" address within your config/mail.php configuration file:
'reply_to' => ['address' => 'example#example.com', 'name' => 'App Name'],
So you can try this In verifyEmail.php Change this
public function build()
{
return $this->view('emails.verifyEmail');
}
To this
public function build()
{
return $this->from('info#domain.com')->view('emails.verifyEmail');
}

How to test the laravel Mailer using Phpunit

I need to test the Laravel Mailer using PHPunit, I am using CRUD Operations, where if any one the method fails, It should trigger the mail. I need to test the mail part, below is the code.
public function index()
{
$response = Http::withBasicAuth(userName,passWord)
->get(connection());
$this->html_mail($response);
return $response->json();
}
public function show($id)
{
$response = Http::withBasicAuth(userName, passWord)
->get(connection());
// check response & send mail if error
$this->html_mail($response);
$record = collect($response->json() ['output'])
->where($this->primaryKeyname, $id)->first();
return $record;
}
Mailer method:
public function html_mail($response)
{
if ($response->failed() || $response->serverError() || $response->clientError()) {
Mail::send([], [], function ($message) use ($response) {
$message->to('foo#example.com');
$message->subject('Sample test');
$message->setBody($response, 'text/html');
});
}
return 'Mail Sent Successfully';
}
}
Could someone please help to test the Mailer method using PHPunit.
Thanks.
It looks like there might be some code missing in your examples, but generally you're looking for Laravel's Mail::fake() method:
# tests/Feature/YourControllerTest.php
use Illuminate\Support\Facades\Mail;
/**
* #test
*/
public function index_should_send_an_email_if_authentication_fails(): void
{
Mail::fake();
$this->withToken('invalidToken', 'Basic')
->get('your.route.name');
Mail::assertSent(function ($mail) {
// Make any assertions you need to in here.
return $mail->hasTo('foo#example.com');
});
}
There's also an opportunity to clean up your controller methods here by leveraging middleware for authentication rather than repeating it in every method.
Digging into Illuminate\Auth\SessionGuard, Laravel automaticallys fire an Illuminate\Auth\Events\Failed event if authentication fails. Instead of sending directly from your controller, you might consider registering an event listener and attaching it to that event, then letting the listener dispatch a mailable notification.
# app/Providers/EventServiceProvider
/**
* The event listener mappings for the application.
*
* #var array
*/
protected $listen = [
'Illuminate\Auth\Events\Failed' => [
'App\\Listeners\\FailedAuthAttempt',
],
];
With those changes, your testing also becomes easier:
# tests/Feature/Notifications/FailedAuthAttemptTest.php
use App\Notifications\FailedAuthAttempt;
use Illuminate\Notifications\AnonymousNotifiable;
use Illuminate\Support\Facades\Notification;
/**
* #test
*/
public function it_should_send_an_email_upon_authentication_failure(): void
{
Notification::fake();
$this->withToken('invalidToken', 'Basic')
->get('your.route.name');
Notification::assertSentTo(new AnonymousNotifiable(), FailedAuthAttempt::class);
}
Now, any route in your application that uses Laravel's auth.basic middleware will automatically send the FailedAuthAttempt notification upon failure. This also makes it easier to, for example, send these notices to a Slack channel rather than sending emails.

Laravel 7 set log path dynamically in Job class

Im building project on Laravel 7.3 with multiple Jobs that run at the same time.
I need to make each Job write logs to different daily rotated file. The name of the log file should be based on model, that Job is processing.
The issue is I cant find smart solution.
What I have tried:
1) creating multiple channels in config/logging.php.
That works as expected but at the moment there are about 50 different Jobs and amount keeps growing. Method is ugly and hardly maintained.
2) setting up Config(['logging.channels.CUSTOMCHANNEL.path' => storage_path('logs/platform/'.$this->platform->name.'.log')]);.
Messing with Config variable was bad idea because of many Jobs running one time. As a result messages from one job often were written in another Job log.
3) using Log::useDailyFiles()
Seems like this stops working since laravel 5.5 or 5.6. Just getting error Call to undefined method Monolog\Logger::useDailyFiles(). Any thoughts how to make with work in laravel 7?
4) using tap parameter for channel in config/logging.php.
Example in laravel docs
No ideas how to pass model name into CustomizeFormatter to setup file name.
Im almost sure there is smart solution and Im just missing something.
Any suggests? Thanks!
You could inherit the log manager to allow a dynamic configuration
<?php
namespace App\Log;
use Illuminate\Support\Str;
use Illuminate\Log\LogManager as BaseLogManager;
class LogManager extends BaseLogManager
{
/**
* Get the log connection configuration.
*
* #param string $name
* #return array
*/
protected function configurationFor($name)
{
if (!Str::contains($name, ':')) {
return parent::configurationFor($name);
}
[$baseName, $model] = explode(':', $name, 2);
$baseConfig = parent::configurationFor($baseName);
$baseConfig['path'] = ...; //your logic
return $baseConfig;
}
}
Likewise about Laravel's log service provider except this one can be totally replaced
<?php
namespace App\Log;
use Illuminate\Support\ServiceProvider;
class LogServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->app->singleton('log', function ($app) {
return new LogManager($app);
});
}
}
EDIT: I've just seen that Laravel's log service provider is missing from config/app.php, this is because it's "hard-loaded" by the application. You still can replace it by inheriting the application itself
<?php
namespace App\Foundation;
use App\Log\LogServiceProvider;
use Illuminate\Events\EventServiceProvider;
use Illuminate\Routing\RoutingServiceProvider;
use Illuminate\Foundation\Application as BaseApplication;
class Application extends BaseApplication
{
/**
* Register all of the base service providers.
*
* #return void
*/
protected function registerBaseServiceProviders()
{
$this->register(new EventServiceProvider($this));
$this->register(new LogServiceProvider($this));
$this->register(new RoutingServiceProvider($this));
}
}
And finally in bootstrap/app.php, replace Illuminate\Foundation\Application with App\Foundation\Application
For example, if you try this
app('log')->channel('single:users')->debug('test');
Laravel will use the single channel's config and write to users.log if your resolution logic is
$baseConfig['path'] = $model + '.log';
I got a solution that I've been using since Laravel 4 that works, although it doesn't follow 'Laravel' way of doing things.
class UserTrackLogger
{
/**
* #var $full_path string
*/
protected $full_path;
/**
* #var $tenant string
*/
protected $tenant;
/**
* #var $user User
*/
protected $user;
/**
* #var $request Request
*/
protected $request;
public static function log(string $message, Request $request, User $user, array $data = []): void
{
/** #noinspection PhpVariableNamingConventionInspection */
$userTrack = new static($request, $user);
$userTrack->write($message, $data);
}
protected function __construct(Request $request, User $user)
{
$this->request = $request;
$this->user = $user;
$this->tenant = app()->make('tenant')->tenant__name;
$path = storage_path() . "/logs/{$this->tenant}/users";
$filename = $this->user->username_with_name;
$this->full_path = Formatter::formatPath("{$path}/{$filename}.log");
self::makeFolder($this->full_path);
}
protected function write(string $message, array $data = []): void
{
$formatter = $this->getFormat();
$record = [
'message' => $message,
'context' => $data,
'extra' => [],
'datetime' => date(Utility::DATETIME_FORMAT_DEFAULT),
'level_name' => 'TRACK',
'channel' => '',
];
file_put_contents($this->full_path, $formatter->format($record), FILE_APPEND);
}
protected function getFormat(): FormatterInterface
{
$ip = $this->request->getClientIp();
$method = strtoupper($this->request->method());
$format = "[%datetime%][{$this->tenant}][{$this->user->username}][{$this->user->name}]: $ip $method %message% %context%\n";
return new LineFormatter($format, null, true);
}
protected static function makeFolder(string $full_path): bool
{
$path = dirname($full_path);
if ( !is_dir($path) ) {
return mkdir($path, 0755, true);
}
return false;
}
}
And when I want to log something, I do UserTrackLogger::log($request->fullUrl(), $request, $user, $data);
What I would suggest is creating a logger similar to this but extends RotatingFileHandler.

ZF2 - How to Listen for Events and Trigger a Service because of it?

Have been trying to learn how to implement Services because they get Triggered by a Listener. Have been doing a serious lot of reading the last few days to get it to work, but have been finding it difficult. Thus I'm thinking my understanding of the order of things might be flawed.
The use case I'm trying to get to work is the following:
Just before an Address Entity (with Doctrine, but that's not
important) gets saved (flushed), a Service must be triggered to check
if the Coordinates for the Address are set, and if not, create and
fill a new Coordinates Entity and link it to the Address. The
Coordinates are to be gotten from Google Maps Geocoding API.
Will show below what and how I'm understanding things in the hope I make myself clear. Will do it in steps to show added code in between and tell you what does and doesn't work, as far as I know.
Now, my understanding of all of the information I've gotten the last few days is this:
A Listener has to be registered with ZF2's ServiceManager. The listener "attaches" certain conditions to the (Shared)EventManager. An EventManager is unique to an object, but the SharedEventManager is 'global' in the application.
In the Address module's Module.php class I've added the following function:
/**
* #param EventInterface $e
*/
public function onBootstrap(EventInterface $e)
{
$eventManager = $e->getTarget()->getEventManager();
$eventManager->attach(new AddressListener());
}
This gets works, the AddressListener gets triggered.
The AddressListener is as follows:
use Address\Entity\Address;
use Address\Service\GoogleCoordinatesService;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
use Zend\Stdlib\CallbackHandler;
class AddressListener implements ListenerAggregateInterface
{
/**
* #var CallbackHandler
*/
protected $listeners;
/**
* #param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events)
{
$sharedEvents = $events->getSharedManager();
// Not sure how and what order params should be. The ListenerAggregateInterface docblocks didn't help me a lot with that either, as did the official ZF2 docs. So, been trying a few things...
$this->listeners[] = $sharedEvents->attach(GoogleCoordinatesService::class, 'getCoordinates', [$this, 'addressCreated'], 100);
$this->listeners[] = $sharedEvents->attach(Address::class, 'entity.preFlush', [GoogleCoordinatesService::class, 'getCoordinates'], 100);
}
/**
* #param EventManagerInterface $events
*/
public function detach(EventManagerInterface $events)
{
foreach ($this->listeners as $index => $listener) {
if ($events->detach($listener)) {
unset($this->listeners[$index]);
}
}
}
public function addressCreated()
{
$foo = 'bar'; // This line is here to as debug break. Line is never used...
}
}
I was expecting a Listener to work as a sort-of stepping stone point to where things get triggered, based on the ->attach() functions in the function attach(...){}. However, this does not seem to work, as nothing gets triggered. Not the addressCreated() function and not the getCoordinates function in the GoogleCoordinatesService.
The code above is supposed to trigger the GoogleCoordinatesService function getCoordinates. The Service has a few requirements though, such as the presence of the EntityManager of Doctrine, the Address Entity it concerns and configuration.
To that effect, I've created the following configuration.
File google.config.php (gets loaded, checked that)
return [
'google' => [
'services' => [
'maps' => [
'services' => [
'geocoding' => [
'api_url' => 'https://maps.googleapis.com/maps/api/geocode/json?',
'api_key' => '',
'url_params' => [
'required' => [
'address',
],
'optional' => [
'key'
],
],
],
],
],
],
],
];
And in module.config.php I've registered the Service with a Factory
'service_manager' => [
'factories' => [
GoogleCoordinatesService::class => GoogleCoordinatesServiceFactory::class,
],
],
The Factory is pretty standard ZF2 stuff, but to paint a complete picture, here is the GoogleCoordinatesServiceFactory.php class. (Removed comments/typehints/etc)
class GoogleCoordinatesServiceFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator, $options = [])
{
$serviceManager = $serviceLocator->getServiceLocator();
$entityManager = $serviceManager->get(EntityManager::class);
$config = $serviceManager->get('Config');
if (isset($options) && isset($options['address'])) {
$address = $options['address'];
} else {
throw new InvalidArgumentException('Must provide an Address Entity.');
}
return new GoogleCoordinatesService(
$entityManager,
$config,
$address
);
}
}
Below is the GoogleCoordinatesService class. However, nothing ever gets triggered to executed in there. As it doesn't even gets called I'm sure the problem lies in the code above, but cannot find out why. From what I've read and tried, I'm expecting that the class itself should get called, via the Factory and the getCoordinates function should be triggered.
So, the class. I've removed a bunch of standard getters/setters, comments, docblocks and typehints to make it shorter.
class GoogleCoordinatesService implements EventManagerAwareInterface
{
protected $eventManager;
protected $entityManager;
protected $config;
protected $address;
/**
* GoogleCoordinatesServices constructor.
* #param EntityManager $entityManager
* #param Config|array $config
* #param Address $address
* #throws InvalidParamNameException
*/
public function __construct(EntityManager $entityManager, $config, Address $address)
{
$this->config = $config;
$this->address = $address;
$this->entityManager = $entityManager;
}
public function getCoordinates()
{
$url = $this->getConfig()['api_url'] . 'address=' . $this->urlFormatAddress($this->getAddress());
$response = json_decode(file_get_contents($url), true);
if ($response['status'] == 'OK') {
$coordinates = new Coordinates();
$coordinates
->setLatitude($response['results'][0]['geometry']['location']['lat'])
->setLongitude($response['results'][0]['geometry']['location']['lng']);
$this->getEntityManager()->persist($coordinates);
$this->getAddress()->setCoordinates($coordinates);
$this->getEntityManager()->persist($this->getAddress());
$this->getEntityManager()->flush();
$this->getEventManager()->trigger(
'addressReceivedCoordinates',
null,
['address' => $this->getAddress()]
);
} else {
// TODO throw/set error/status
}
}
public function urlFormatAddress(Address $address)
{
$string = // format the address into a string
return urlencode($string);
}
public function getEventManager()
{
if ($this->eventManager === null) {
$this->setEventManager(new EventManager());
}
return $this->eventManager;
}
public function setEventManager(EventManagerInterface $eventManager)
{
$eventManager->addIdentifiers([
__CLASS__,
get_called_class()
]);
$this->eventManager = $eventManager;
return $this;
}
// Getters/Setters for EntityManager, Config and Address
}
So, that's the setup to handle it when a certain event gets triggered. Now it should, of course, get triggered. For this use case I've setup a trigger in the AbstractActionController of my own (extends ZF2's AbstractActionController). Doing that like so:
if ($form->isValid()) {
$entity = $form->getObject();
$this->getEntityManager()->persist($entity);
try {
// Trigger preFlush event, pass along Entity. Other Listeners can subscribe to this name.
$this->getEventManager()->trigger(
'entity.preFlush',
null,
[get_class($entity) => $entity] // key = "Address\Entity\Address" for use case
);
$this->getEntityManager()->flush();
} catch (\Exception $e) {
// Error thrown
}
// Success stuff, like a trigger "entity.postFlush"
}
So yea. At the moment at a bit of a loss on how to get it working.
Any help would be very much appreciated and would love explanations as to the "why" of it is that a solution works. That would really help me out making more of these services :)
Been at it for a while, but have managed to figure out why it was not working. I was attaching Listeners to EventManagers, but should have been attaching them to the SharedEventManager. This is because I have the triggers (in this instance) in the AbstractActionController, thus they all create their own EventManager (as they're unique) when instantiated.
Has been a tough few days wrapping my head around it all, but this article helped me out most, or perhaps it just made things click with my original research in the question and subsequent trial & error + debugging.
Below the code as it is now, in working order. I'll try to explain along as the code comes as to how I understand that it works. If I get it wrong at some point I hope someone corrects me.
First up, we need a Listener, a class which registers components and events to "listen" for them to trigger. (They listen for certain (named) objects to trigger certain events)
The realization quickly came that pretty much every Listener would need the $listeners = []; and the detach(EventManagerInterface $events){...} function. So I created an AbstractListener class.
namespace Mvc\Listener;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
/**
* Class AbstractListener
* #package Mvc\Listener
*/
abstract class AbstractListener implements ListenerAggregateInterface
{
/**
* #var array
*/
protected $listeners = [];
/**
* #param EventManagerInterface $events
*/
public function detach(EventManagerInterface $events)
{
foreach ($this->listeners as $index => $listener) {
if ($events->detach($listener)) {
unset($this->listeners[$index]);
}
}
}
}
After the above mentioned realization about having to use the SharedEventManager and with the AbstractListener created, the AddressListener class has ended up like so.
namespace Address\Listener;
use Address\Event\AddressEvent;
use Admin\Address\Controller\AddressController;
use Mvc\Listener\AbstractListener;
use Zend\EventManager\EventManagerInterface;
/**
* Class AddressListener
* #package Address\Listener
*/
class AddressListener extends AbstractListener
{
/**
* #param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events)
{
$sharedManager = $events->getSharedManager();
$sharedManager->attach(AddressController::class, 'entity.postPersist', [new AddressEvent(), 'addCoordinatesToAddress']);
}
}
The main difference with attaching events to EventManager versus the SharedEventManager is that the latter listens for a specific class to emit a trigger. In this instance it will listen for the AddressController::class to emit the trigger entity.postPersist. Upon "hearing" that it's triggered it will call a callback function. In this case that is registered with this array parameter: [new AddressEvent(), 'addCoordinatesToAddress'], meaning that it will use the class AddressEvent and the function addCoordinatesToAddress.
To test if this works, and if you're working along with this answer, you can create the trigger in your own Controller. I've been working in the addAction of the AbstractActionController, which gets called by the addAction of the AddressController. Below the trigger for the Listener above:
if ($form->isValid()) {
$entity = $form->getObject();
$this->getEntityManager()->persist($entity);
$this->getEventManager()->trigger(
'entity.postPersist',
$this,
[get_class($entity) => $entity]
);
try {
$this->getEntityManager()->flush();
} catch (\Exception $e) {
// Error stuff
}
// Remainder of function
}
The ->trigger() function in the above code shows the usage of the following parameters:
'entity.postPersist' - This is the event name
$this - This is the "component" or object the event is called for. In this instance it will be Address\Controller\AddressController
[get_class($entity) => $entity] - These are parameters to send along with this Event object. It will cause you to have available $event->getParams()[Address::class] which will have the $entity value.
The first two parameters will trigger the Listener in the SharedEventManager. To test if it all works, it's possible to modify the Listener's attach function.
Modify it to this and create a function within the the Listener so you can see it working:
public function attach(EventManagerInterface $events)
{
$sharedManager = $events->getSharedManager();
$sharedManager->attach(AddressController::class, 'entity.postPersist', [$this, 'test']);
}
public function test(Event $event)
{
var_dump($event);
exit;
}
Lastly, to make sure that the above actually works, the Listener must be registered with the EventManager. This happens in the onBootstrap function in the Module.php file of the module (Address in this case). Register like below.
public function onBootstrap(MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$eventManager->attach(new AddressListener());
}
If you debug the code of the addAction in the AbstractActionController, see it pass the trigger and next you're in the test function, then your Listener works.
The above code also implies that the AddressListener class can be used to attach more than one listener. So you could also register stuff for entity.prePersist, entity.preFlush, entity.postFlush and anything else you can think of.
Next up, revert the Listener back to what it was at the beginning (revert the attach function and remove the test function).
I also noticed that pretty much every Event handling class would need to be able to set and get the EventManager. Thus, for this I've created an AbstractEvent class, like below.
namespace Mvc\Event;
use Zend\EventManager\EventManager;
use Zend\EventManager\EventManagerAwareInterface;
use Zend\EventManager\EventManagerInterface;
abstract class AbstractEvent implements EventManagerAwareInterface
{
/**
* #var EventManagerInterface
*/
protected $events;
/**
* #param EventManagerInterface $events
*/
public function setEventManager(EventManagerInterface $events)
{
$events->setIdentifiers([
__CLASS__,
get_class($this)
]);
$this->events = $events;
}
/**
* #return EventManagerInterface
*/
public function getEventManager()
{
if (!$this->events) {
$this->setEventManager(new EventManager());
}
return $this->events;
}
}
To be honest, I'm not quite sure why we set 2 identifiers in the setEventManager function. But suffice to say that it's used to register callbacks for Events. (this could use more/detailed explanation if someone feels so inclined as to provide it)
In the AddressListener we're trying to call the addCoordinatesToAddress function of the AddressEvent class. So we're going to have to create that, I did it like below.
namespace Address\Event;
use Address\Entity\Address;
use Address\Service\GoogleGeocodingService;
use Country\Entity\Coordinates;
use Mvc\Event\AbstractEvent;
use Zend\EventManager\Event;
use Zend\EventManager\Exception\InvalidArgumentException;
class AddressEvent extends AbstractEvent
{
public function addCoordinatesToAddress(Event $event)
{
$params = $event->getParams();
if (!isset($params[Address::class]) || !$params[Address::class] instanceof Address) {
throw new InvalidArgumentException(__CLASS__ . ' was expecting param with key ' . Address::class . ' and value instance of same Entity.');
}
/** #var Address $address */
$address = $params[Address::class];
if (!$address->getCoordinates() instanceof Coordinates) {
/** #var GoogleGeocodingService $geocodingService */
$geocodingService = $event->getTarget()->getEvent()->getApplication()->getServiceManager()->get(GoogleGeocodingService::class);
$geocodingService->addCoordinatesToAddress($address);
}
$params = compact('address');
$this->getEventManager()->trigger(__FUNCTION__, $this, $params);
}
}
In the above you can see that first we check if the parameter we expect has been passed along with the Event $event parameter. We know what we should expect and what name the key should have, so we check explicitly.
Next we check if the received Address Entity object already has a Coordinates object associated with it, if it doesn't, we call a Service to make it happen.
After the if() statement has run, we fire another trigger. We pass along this Event object and the parameters. This last step is not required, but can be handy if you wish to chain events.
In the question I mentioned a use case. The above code enables the Service (GoogleGeocodingService) to get passed the it's requirements and combined with the configuration for the Factory, it gets created via Zend Magic with the ServiceManager.
The code to add a new Coordinates object to the existing Address object was not modified, so I won't make it part of the answer, you can find that in the question.

Consume raw json queue messages in Laravel

Normally Laravel expects that it queued up any messages that it later consumes. It creates a payload with a job attribute that later indicates how to handle the queue message. When you do queue up jobs with Laravel, and later process them with Laravel, it works great!
However, I have some non-Laravel apps that are posting json messages to a queue. I need Laravel to pick up these messages and handle them.
I can write a command bus job to handle the messages, but I haven't been able to figure out how to tell queue:work to send the messages on to my specific handler.
It seems Laravel has a hard assumption that any queue messages it is asked to handle will be properly formatted, serialized, and structured the way it expects them to be.
How can I have Laravel pick up these raw json payloads, ignore the structure (there's nothing there for it to understand), and simply hand the payload off to my handler?
For example, if I have a queue message similar to:
{
"foo" : "bar"
}
So again, there's nothing for Laravel to inspect or understand here.
But I have a job handler that knows how to handle this:
namespace App\Jobs;
class MyQueueHandler {
public function handle($payload) {
Log::info($payload['foo']); // yay!
}
}
Now how to get queue:work and queue:listen to simply hand off any payloads to this App\Jobs\MyQueueHandler handler, where I can do the rest on my own?
If you're using Laravel 5.6+, check out this package.
You didn't specify which version of Laravel, so I'm guessing 5.1 (huge difference in how this is handled in L4.2 and L5.x).
If you've already set up your App\Jobs\MyQueueHandler, and want to queue up a job from a controller, using any data you wish, you can just do this:
use App\Jobs\MyQueueHandler;
class MyController
{
public function myFunction()
{
$this->dispatch(new MyQueueHandler(['foo' => 'bar']));
}
}
In your MyQueueHandler-class, the payload actually enters your constructor. The handle-method is still fired when your queue is processed though. You can however use parameters on your handle-method if you rely on dependancy injection (read more here, just above "When Things Go Wrong") So something like this should do it:
namespace App\Jobs;
class MyQueueHandler
{
protected $payload;
public function __construct($payload)
{
$this->payload = $payload;
}
public function handle() {
Log::info($this->payload['foo']); // yay!
}
}
Note: If you want to dispatch the job from outside a main controller (that inherits from the standard App\Http\Controller-class, use the DispatchesJobs trait;
MyClass
{
use DispatchesJobs;
public function myFunction()
{
$this->dispatch(new MyQueueHandler(['foo' => 'bar']));
}
}
(Code tested with Laravel 5.1.19 and the beanstalkd queue-adapter).
What you ask for is not possible as Laravel tries to execute the Gearman payload (see \Illuminate\Bus\Dispatcher).
I was in the same situation and just created a wrapper command around the Laravel job class. This is not the nicest solution as it will re-queue events, coming on the json queue, but you don't have to touch existing job classes. Maybe someone with more experience knows how to dispatch a job without actually sending it over the wire again.
Lets assume we have one regular Laravel worker class called GenerateIdentApplicationPdfJob.
class GenerateIdentApplicationPdfJob extends Job implements SelfHandling, ShouldQueue
{
use InteractsWithQueue, SerializesModels;
/** #var User */
protected $user;
protected $requestId;
/**
* Create a new job instance.
*
* QUEUE_NAME = 'ident-pdf';
*
* #param User $user
* #param $requestId
*/
public function __construct(User $user, $requestId)
{
$this->user = $user;
$this->requestId = $requestId;
}
/**
* Execute the job.
*
* #return void
*/
public function handle(Client $client)
{
// ...
}
}
To be able to handle this class, we need to provide the constructor arguments our own. Those are the required data from our json queue.
Below is a Laravel command class GearmanPdfWorker, which does all the boilerplate of Gearman connection and json_decode to be able to handle the original job class.
class GearmanPdfWorker extends Command {
/**
* The console command name.
*
* #var string
*/
protected $name = 'pdf:worker';
/**
* The console command description.
*
* #var string
*/
protected $description = 'listen to the queue for pdf generation jobs';
/**
* #var \GearmanClient
*/
private $client;
/**
* #var \GearmanWorker
*/
private $worker;
public function __construct(\GearmanClient $client, \GearmanWorker $worker) {
parent::__construct();
$this->client = $client;
$this->worker = $worker;
}
/**
* Wrapper listener for gearman jobs with plain json payload
*
* #return mixed
*/
public function handle()
{
$gearmanHost = env('CB_GEARMAN_HOST');
$gearmanPort = env('CB_GEARMAN_PORT');
if (!$this->worker->addServer($gearmanHost, $gearmanPort)) {
$this->error('Error adding gearman server: ' . $gearmanHost . ':' . $gearmanPort);
return 1;
} else {
$this->info("added server $gearmanHost:$gearmanPort");
}
// use a different queue name than the original laravel command, since the payload is incompatible
$queueName = 'JSON.' . GenerateIdentApplicationPdfJob::QUEUE_NAME;
$this->info('using queue: ' . $queueName);
if (!$this->worker->addFunction($queueName,
function(\GearmanJob $job, $args) {
$queueName = $args[0];
$decoded = json_decode($job->workload());
$this->info("[$queueName] payload: " . print_r($decoded, 1));
$job = new GenerateIdentApplicationPdfJob(User::whereUsrid($decoded->usrid)->first(), $decoded->rid);
$job->onQueue(GenerateIdentApplicationPdfJob::QUEUE_NAME);
$this->info("[$queueName] dispatch: " . print_r(dispatch($job)));
},
[$queueName])) {
$msg = "Error registering gearman handler to: $queueName";
$this->error($msg);
return 1;
}
while (1) {
$this->info("Waiting for job on `$queueName` ...");
$ret = $this->worker->work();
if ($this->worker->returnCode() != GEARMAN_SUCCESS) {
$this->error("something went wrong on `$queueName`: $ret");
break;
}
$this->info("... done `$queueName`");
}
}
}
The class GearmanPdfWorker needs to be registered in your \Bundle\Console\Kernel like this:
class Kernel extends ConsoleKernel
{
protected $commands = [
// ...
\Bundle\Console\Commands\GearmanPdfWorker::class
];
// ...
Having all that in place, you can call php artisan pdf:worker to run the worker and put one job into Gearman via commandline: gearman -v -f JSON.ident-pdf '{"usrid":9955,"rid":"ABC4711"}'
You can see the successful operation then
added server localhost:4730
using queue: JSON.ident-pdf
Waiting for job on `JSON.ident-pdf` ...
[JSON.ident-pdf] payload: stdClass Object
(
[usrid] => 9955
[rid] => ABC4711
)
0[JSON.ident-pdf] dispatch: 1
... done `JSON.ident-pdf`
Waiting for job on `JSON.ident-pdf` ...

Categories