Consume raw json queue messages in Laravel - php

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` ...

Related

Symfony : Mock LDAP Component in functional tests

I want to do functional tests on my Symfony (5.1) application, this application uses an Active Directory server as a "datas" database (creating , listing , updating datas). I'm using the Symfony ldap component. Code example below may contain typos.
Controller
class DatasController
{
/**
* #Route("/datas", name="datas")
* #IsGranted("ROLE_USER")
*
* #return Response
* #desc Displays LDAP datas
*/
public function datasList(DatasRepository $datasRepository)
{
$datas = $datasRepository->findAll();
return $this->render('datas/list.html.twig', [
'datas' => $datas,
]);
}
}
Repository
class DatasRepository
{
private Ldap $ldap;
private EntryManagerInterface $manager;
/**
* DatasRepository constructor.
* Service injected params
*/
public function __construct(Ldap $ldap, string $ldapAdminLogin, string $ldapAdminPwd)
{
$this->ldap = $ldap->bind($ldapAdminLogin, $ldapAdminPwd);
$this->manager = $ldap->getEntryManager();
}
public function create(Data $data): void
{
// ... some $data to Symfony\Component\Ldap\Entry $entry logic
$this->manager->add( $entry );
}
/**
* #return datas[]
*/
public function findAll()
{
$this->ldap->query('ou=test', '(&(objectclass=person))');
$entries = $query->execute()->toArray();
// ... some $entries to $datas logic
return $datas;
}
}
Test
class DatasControllerTest extends WebTestCase
{
public function testDatasList()
{
$client = static::createClient();
$client->request('GET', '/datas');
# Crash can't contact LDAP and thats logical
$this->assertResponseIsSuccessful();
}
}
So, how to do functional test on "GET /datas" ?
What part of the code should i mock to maximize test efficiency and coverage ?
Some additional information :
I can't have a dedicated LDAP server for tests (tests are run under
Docker via gitlab-ci)
I'm aware of the "don't mock what you don't
own".
I've read many posts/articles saying "you should mock the
LdapAdapter" but i have no idea on how to achieve this and haven't
found any example.
Any suggestion is welcome.
Thanks
Eric
About mockin external services: you can extend test service from the original one and make it methods behave how you want. Ex.:
class TestService extends \Symfony\OrAnyOtherExternalService
{
public function getConnection()
{
return new Connection([]);
}
}
then in your services_test.yaml change the class of this service to you tests service:
services:
Symfony\OrAnyOtherExternalService:
class: TestData\Services\TestService
this way in test environment application will use TestService instead of original

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.

Passing Argument to Lumen Job

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.

Why does laravel IoC does not provisioning my class with my method?

I can't get why laravel tries to create my class itself, without using my method. I can see that IoC binding is executed (POINT 1 is shown). But singleton method is being never executed. Why?
In my service provider (not deferred):
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
echo "POINT 1"; // I can see this one
$this->app->singleton(\App\Services\FooBar::class, function($app)
{
echo "POINT 2\n"; // Does not comes here
return new FooBar($params);
});
}
I try to use typehinting to resolve dependencies when creating a class:
class Test
{
public function __construct(FooBar $fooBar)
{
}
}
I see that laravel tries to create FooBar to inject it, but can't resolve FooBar's dependencies. They could be resolved, if laravel would call service provider callback, but it does not. Why? How to make laravel use that callback for that class?
Instead of closure (that will not work), use boot() method to initiate your service.
/**
* #param \App\Services\FooBar $foobar
*/
public function boot(\App\Services\FooBar $foobar)
{
$foobar->setOptions(['option' => 'value']);
}
It will launch right after service will be instantiated.
It is because when you are binding a class to IoC container you are not immediately calling the closure. Instead when you need to actually do some action on your class from container you call App::make('class') which would fire the closure and give you the value that was returned from it. So for example
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
echo "POINT 1"; // I can see this one
$this->app->singleton(\App\Services\FooBar::class, function($app)
{
echo "POINT 2\n"; // Does not comes here
return new FooBar($params);
});
$this->app->make(\App\Services\FooBar::class); //here POINT 2 will be called first.
}

Categories