How to register events from within a module in Yii? - events

I'm trying to register events from within a submodule in Yii.
It just doesn't seem to work.
The init method is definitely called.
class TestModule extends CWebModule
{
public function init()
{
$this->setImport(array(
'test.models.*',
'test.components.*',
));
Yii::app()->onBeginRequest = array($this, 'onBeginRequest');
}
public function onBeginRequest($event) {
die('Request!');
}
public function beforeControllerAction($controller, $action)
{
if (parent::beforeControllerAction($controller, $action))
{
return true;
}
else
return false;
}
}

To register an event you can do:
$this->getEventHandlers($eventName)->add($eventHandler);
Where $eventHandler is the name of the callback you want to define for the $eventName event.
You can also do it with the following way:
$this->attachEventHandler($eventName, $eventHandler);

I solved the problem myself.
The problem was, that i was actually too late for onBeginRequest (Request was alrdy processed).
So what i did was writing a component with Event Handlers for onBeginRequest and onEndRequest, registering the event handlers in config/main.php and call my Module from this Component.
I basically had to proxy all these events.

Related

Laravel implement assertSentFrom specific address Mailable Testing

Trying to get to grips with Mocking and test cases, I want to test that a Mailable TestMail is sent from company#company.com, the documentation provides hasTo, hasCc, and hasBcc but doesn't look like it uses something like hasFrom. Is there any solutions to this?
https://laravel.com/docs/9.x/mocking#mail-fake
public function testEmailAlwaysFrom()
{
Mail::fake();
Mail::to('foo#bar.com')->send(new TestMail);
Mail::assertSent(TestMail::class, function ($mail) {
return assertEquals('company#company.com', $mail->getFrom());
// return $mail->hasTo($user->email) &&
// $mail->hasCc('...') &&
// $mail->hasBcc('...');
});
}
MailFake doesn't provide hasFrom method in the class and therefore will return false.
The workaround below however doesn't work when using the environmental variable MAIL_FROM_ADDRESS, ->from() has to be called within build().
A couple of GitHub issues have been reported suggesting a workaround below:
https://github.com/laravel/framework/issues/20056
https://github.com/laravel/framework/issues/20059
public function testEmailAlwaysFrom()
{
Mail::fake();
Mail::to('foo#bar.com')
->send(new TestMail);
Mail::assertSent(TestMail::class, function ($mail) {
$mail->build(); // <-- workaround
return $mail->hasTo('foo#bar.com') and
$mail->hasFrom('company#company.com');
});
}

Handle Exception From Within Method

I am implementing payments for my website using the API of an external service (ie. the service of the payment provider).
Let's say the user clicks 'BUY', and then we go to my controller which says something along the lines of:
public function buyFunction() {
$result = $this->ExternalService->pay();
if ($result->success == true) {
return 'We are happy';
}
}
I have also created the aforementioned externalService which has the pay() method:
class ExternalService {
public function pay() {
response = //Do stuff with Guzzle to call the API to make the payment
return response;
}
}
Now, sometimes things go wrong.
Let's say the API returns an error - which means that it throws a GuzzleException - how do I handle that?
Ideally, if there is an error, I would like to log it and redirect the user to a page and tell him that something went wrong.
What I've tried
I have tried using a try/catch statement within the pay() function and using abort(500) but this doesn't allow me to redirect to the page I want to.
I have tried using a try/catch statement within the pay() function and using return redirect('/mypage') but this just returns a Redirect object to the controller, which then fails when it tries to call result->success
I have tried using number 2 but also adding a try/catch block to the controller method, but nothing changed.
In the end, I have found two solutions. In both, I use a try/catch block inside the pay() method. Then I either return 0; and check in the controller if (result == 0) or I use abort( redirect('/mypage') ); inside the try/catch block of the pay() method.
What is the right way to handle this?
How to use the try/catch blocks?
In my experience, avoid handling exceptions let them pass through and handle them accordingly with try catches. This is the most pragmatic approach. Alternatively you will end up checking result is correct in weird places, eg. if ($result) {...}. Just assume it went good, except if the exception is thrown. Bonus: never do Pokemon catches with Exception $e unless you specifically needs it!
class ExternalService {
public function pay() {
try {
response = $client->get(...);
} catch (BadResponseException $exception) {
Log::warning('This should not happen check payment api: ' . $exception->getMessage());
throw new PaymentException('Payment did not go through');
}
return response;
}
}
Assuming you have your own Exception.
class PaymentException extends HttpException
{
public function __construct(?\Exception $previous = null)
{
parent::__construct(Response::HTTP_BAD_REQUEST, 'Unexpected error processing the payment', $previous);
}
}
This enables you to handle the flow in a controller, where it would make sense to handle the redirect. Sometimes if the exception is very integral or common to the web app, it can also be handled by the exception handler instead.
class PaymentController {
public function pay(PaymentService $service) {
try {
$payment = $service->buyFunction();
} catch (PaymentException $exception) {
return redirect()->route('app.payment.error');
}
return view('app.payment.success', compact('payment'));
}
}

How to mock a Job object in Laravel?

When it comes to Queue testing in Laravel, I use the provided Queue Fake functionality. However, there is a case where I need to create a Mock for a Job class.
I have the following code that pushes a job to a Redis powered queue:
$apiRequest->storeRequestedData($requestedData); // a db model
// try-catch block in case the Redis server is down
try {
App\Jobs\ProcessRunEndpoint::dispatch($apiRequest)->onQueue('run');
$apiRequest->markJobQueued();
} catch (\Exception $e) {
//handle the case when the job is not pushed to the queue
}
I need to be able to test the code in the catch block. Because of that, I'm trying to mock the Job object in order to be able to create a faker that will throw an exception.
I tried this in my Unit test:
ProcessRunEndpoint::shouldReceive('dispatch');
That code returns: Error: Call to undefined method App\Jobs\ProcessRunEndpoint::shouldReceive().
I also tried to swap the job instance with a mock object using $this->instance() but it didn't work as well.
That said, how can I test the code in the catch block?
According to the docs, you should be able to test jobs through the mocks provided for the Queue.
Queue::assertNothingPushed();
// $apiRequest->storeRequestedData($requestedData);
// Use assertPushedOn($queue, $job, $callback = null) to test your try catch
Queue::assertPushedOn('run', App\Jobs\ProcessRunEndpoint::class, function ($job) {
// return true or false depending on $job to pass or fail your assertion
});
Making the line App\Jobs\ProcessRunEndpoint::dispatch($apiRequest)->onQueue('run'); throw an exception is a bit complicated. dispatch() just returns an object and onQueue() is just a setter. No other logic is done there. Instead, we can make everything fail by messing with the configuration.
Instead of Queue::fake();, override default queue driver with one that just won't work: Queue::setDefaultDriver('this-driver-does-not-exist'); This will make every job dispatch fail and throw an ErrorException.
Minimalist example:
Route::get('/', function () {
try {
// Job does nothing, the handle method is just sleep(5);
AJob::dispatch();
return view('noError');
} catch (\Exception $e) {
return view('jobError');
}
});
namespace Tests\Feature;
use App\Jobs\AJob;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class AJobTest extends TestCase
{
/**
* #test
*/
public function AJobIsDispatched()
{
Queue::fake();
$response = $this->get('/');
Queue::assertPushed(AJob::class);
$response->assertViewIs('noError');
}
/**
* #test
*/
public function AJobIsNotDispatched()
{
Queue::setDefaultDriver('this-driver-does-not-exist');
$response = $this->get('/');
$response->assertViewIs('jobError');
}
}
I found a solution. Instead of using a facade for adding a job to the queue (App\Jobs\ProcessRunEndpoint::dispatch($apiRequest)->onQueue('run');), I injected it into the action of the controller:
public function store(ProcessRunEndpoint $processRunEndpoint)
{
// try-catch block in case the Redis server is down
try {
$processRunEndpoint::dispatch($apiRequest)->onQueue('run');
} catch (\Exception $e) {
//handle the case when the job is not pushed to the queue
}
}
With this, the job object is resolved from the container, so it can be mocked:
$this->mock(ProcessRunEndpoint::class, function ($mock) {
$mock->shouldReceive('dispatch')
->once()
->andThrow(new \Exception());
});
Although still not sure why the shouldReceive approach doesn't work for the facade: https://laravel.com/docs/8.x/mocking#mocking-facades

Laravel - Event not working - __construct() must be an instance

I want to send mail to admin, when users send message to him in website. I create my events and my mailable class..
At the first, I tried without using event listener and it worked but When I tried with event listener, it didn't work..
it gives this error;
Type error: Argument 1 passed to App\Events\SendMessage::__construct() must be an instance of App\Contact, instance of App\Models\Contact given, called in /var/www/parti/app/Http/Controllers/ContactController.php on line 32
Event
public $mesaj;
public function __construct(Contact $mesaj)
{
$this->mesaj = $mesaj;
}
Listener
public function handle(SendMessage $event)
{
Mail::to(User::first()->email)->send(new SendMessageToAdminAsMail($event->mesaj));
}
Mail Class
public $mesaj;
public function __construct(Contact $mesaj)
{
$this->mesaj = $mesaj;
}
public function build()
{
return $this
->from($this->mesaj->email)
->subject($this->mesaj->name." send a message")
->view('backend.mail.message');
}
What is my mistake ? Thank's for your help...

Symfony2 Properly Hook in Events from Services

I have a class which is used to generate navigation from a variety of interconnected bundles. I have a Navigation service to accomplish this.
In order to connect this service with the other bits of Navigation, I want to allow the other bundles to define their own services which then listen to the event listener and add their navigation items at the proper time.
The problem is, I can't figure out how to have a service listen to an event without first calling that service manually in order to create it.
Any ideas?
To give a more concrete idea, I have something like this:
// Set up as a service in the bundle.
class Navigation {
// ...
protected $dispatcher; // event dispatcher passed in to service
// ...
public function generateNavigation() {
$items = array();
// add some items
$event = new NavigationEvent($items); // custom event
$this->eventDispatcher->dispatchEvent('navigation_event', $event);
}
}
// Set up as a service in some secondary bundle.
class NavigationWorker {
/**
* #param $dispatcher Same instance as Navigation
*/
public function __construct(EventDispatcher $dispatcher) {
$dispatcher->addListener('navigation_event', array($this, 'doSomething'));
}
}
With this set up, it should work if the NavigationWorker is called at some point and is constructed, but I can't always call them directly, so it is never constructed and the listener is never added.
The way I currently do it is to pass all of the NavigationWorkers to Navigation and have it add their listener, but this is very ugly.
See the Event Listener Documentation. Make NavigationWorker and event listener and it won't need to be explicitly constructed.
I'm changing the answer to this because while that set me on the right path, it wasn't the complete answer. That article really only allows you to hook in to pre-defined kernel events. I however needed my own, so I started working back from there.
In the end, I ended up creating my own tags, a compiler pass to process those tasks. I also added my own extension of EventDispatcher, though that wasn't super-necessary (you could just use the normal one).
Here is what the file solution looked like.
Configuration:
parameters:
my_bundle.navigation.event.class: My\Bundle\DependencyInjection\NavigationEvent
my_bundle.event_dispatcher.class: My\Bundle\DependencyInjection\EventDispatcher
my_bundle.navigation.class: My\Bundle\DependencyInjection\NavigationGenerator
my_bundle.navigation_listener1.class: My\Bundle\DependencyInjection\NavigationListener
my_bundle.navigation_listener2.class: My\Bundle\DependencyInjection\NavigationListener
services:
my_bundle.event_dispatcher:
class: %my_bundle.event_dispatcher.class%
my_bundle.navigation:
class: %my_bundle.navigation.class%
arguments:
- #my_bundle.event_dispatcher
my_bundle.navigation_listener1.class:
class: %my_bundle.navigation_listener1.class%
tags:
- { name: my_bundle.event_listener, event: my_bundle.navigation.generate, method: onGenerateNavigation }
my_bundle.navigation_listener2.class:
class: %my_bundle.navigation_listener2.class%
tags:
- { name: my_bundle.event_listener, event: my_bundle.navigation.generate, method: onGenerateNavigation }
CompilerPass:
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class EventListenerCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('my_bundle.event_dispatcher')) {
return;
}
$definition = $container->getDefinition(
'my_bundle.event_dispatcher'
);
$taggedServices = $container->findTaggedServiceIds(
'my_bundle.event_listener'
);
foreach ($taggedServices as $id => $tagAttributes) {
foreach ($tagAttributes as $attributes) {
$definition->addMethodCall(
'addListener',
array($this->getEventString($attributes['event'], $container), array(new Reference($id), $attributes['method']))
);
}
}
}
protected function getEventString($str, ContainerBuilder $container)
{
preg_match('/(.*)\.([^.]*)$/', $str, $matches);
$parameterName = $matches[1];
$constName = strtoupper($matches[2]);
$eventClass = $container->getParameter($parameterName . '.event.class');
if (!$eventClass) {
throw new Exception('Unable to find parameter: ' . $eventClass . '.event.class');
}
// Return the value of the constant.
return constant($eventClass . '::' . $constName);
}
Add a function like this to your compiler class (something like MyBundleBundle).
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new EventListenerCompilerPass());
}
Now the EventListener will have added listeners for each of those events. You than just implement everything else exactly as you would expect (Navigation dispatches events which it listens too). You can than hook in new event listeners from any bundle, and they don't even need to share a common class/interface.
This also works for any custom event, as long as the object which has the constant for the event is registered in the parameters with ".event.class" at the end (so my_bundle.navigation.generate looks for the parameter my_bundle.navigation.event.class, uses that class and the constant GENERATE).
Hopefully that'll help anyone else looking to do something similar.

Resources