In a ZF3 custom module, I need to intercept some events.
In the module.config.php, the function init() is :
public function init(ModuleManager $manager)
{
$eventManager = $manager->getEventManager();
$sharedEventManager = $eventManager->getSharedManager();
$sharedEventManager->attach(__NAMESPACE__, MvcEvent::EVENT_DISPATCH, [$this, 'onDispatch'], 100);
$sharedEventManager->attach(__NAMESPACE__, MvcEvent::EVENT_DISPATCH_ERROR, [$this, 'onDispatchError'], 110);
}
In the same class, there are the two functions :
public function onDispatch(MvcEvent $event)
{
echo 'testOnDispatch';
die;
}
public function onDispatchError(MvcEvent $event)
{
echo 'testOnDispatchError';
die;
}
The EVENT_DISPATCH event is triggered without any problem but the EVENT_DISPATCH_ERROR is not. After some tests, I saw that only the EVENT_DISPATCH event is triggered.
In the view manager configuration, both display_not_found_reason and display_exceptions are both set to TRUE. If I replace EVENT_DISPATCH_ERROR with EVENT_DISPATCH (twice EVENT_DISPATCH) all is working fine regarding the priority.
What did I miss?
Related
I'm working in a laravel project where I make some logic while updating data. For this purpose I'm taking advantage of the observer pattern in updating() method.
Here are my simplified snippets:
ModuleObserver
public function updating($module)
{
dd('This must be shown');
}
ModuleController
public function update(ModuleUpdateRequest $request, string $moduleId): ModuleResource
{
try {
$module = Module::findOrFail($moduleId);
} catch (ModelNotFoundException $e) {
throw new NotFoundHttpException('Module not found.');
}
$module->fill($request->all());
$module->save();
}
ModuleControllerTest
public function testModuleUpdate()
{
$module = factory(Module::class)->create(['name' => 'My Module']);
$this->actingAs($this->user)->putJson('/modules/' . $module->id, [ 'name' => 'My Updated Module' ])->assertStatus(200);
}
The test passes ok instead of showing the debugger line.
(My observer is correctly loaded in AppServiceProvider file, BTW it works fine out of the test context)
According to Laravel documentation:
When issuing a mass update via Eloquent, the saving, saved, updating, and updated model events will not be fired for the updated models. This is because the models are never actually retrieved when issuing a mass update.
To make sure of the model event to be fired you can use model properties ...
Something like:
$module->name = $request->input('name');
$module->date = $request->input('date');
// ...........
$module->save();
I have a working code where when a Customer record is created, an event will dispatch and a listener will then send a notification to the Customer's agent.
EventServiceProvider.php
protected $listen = [
'App\Events\CustomerCreated' => [
'App\Listeners\TriggerExternalCustomerCreation',
'App\Listeners\SendNotificationCustomerCreated',
],
]
SendNotificationCustomerCreated.php
public function handle(CustomerCreated $event)
{
$when = now()->addMinutes(0);
$event->customer->manager->notify((new CustomerCreatedNotification($event->customer))->delay($when));
}
Here's my test case:-
public function customer_created_event_dispatch()
{
// $this->markTestIncomplete('This test has not been implemented yet.');
$this->withoutExceptionHandling();
Event::fake();
Notification::fake();
$user = factory(User::class)->states('order management')->create();
$data = $this->data(['address' => true, 'contact' => true]);
$response = $this->actingAs($user)->post(route('customers.store'), $data);
$response->assertSessionHasNoErrors();
$customers = Customer::all();
$customer = $customers->first();
$manager = $customer->manager;
$this->assertCount(1, $customers);
Event::assertDispatched(CustomerCreated::class, function ($event) use ($customers) {
return $event->customer->id === $customers->first()->id;
});
Notification::assertSentTo(
$manager,
CustomerCreatedNotification::class,
function ($notification, $channels) use ($customer) {
return $notification->customer->id === $customer->id;
}
);
}
Initially the listener is queued, but i try removed from queue but error remains.
I can confirmed that the event did dispatch since it passed the Event::assertDispatched. But failed at the last assertion with below errors:-
The expected [App\Notifications\CustomerCreatedNotification] notification was not sent
Faking event overwrites ordinary event logic and therefor will not trigger the listeners. This is useful as you sometimes need to block event chains from firing. Faking is also about not caring about side effects, because they are often really hard to test.
Then how do you test that your code work, i prefer to separate Event side effect testing into unit testing. Same approach if i where to test jobs, as the side effects are harder to assert and tests get quite huge if you need to test an endpoint plus a job chain.
Remove the assert notification from your original test. Create an new one in tests/Unit/TestCustomerCreatedEvent or whatever path makes sense.
public function customer_event_listener_tests() {
// create your data
$customers = Customer::all();
$customer = $customers->first();
$manager = $customer->manager;
// fake notification only
Notification::fake();
$event = new CustomerCreated($customer);
event($event);
Notification::assertSentTo(
$manager,
CustomerCreatedNotification::class,
function ($notification, $channels) use ($customer) {
return $notification->customer->id === $customer->id;
}
);
}
Has anyone ever needed to bind an ActiveRecord event handler in a way that it only triggers on certain scenarios?
In an ideal world the ActiveRecord on() method would also take a $scenarios parameter and trigger the handler only if the ActiveRecord is using that scenario at the time the event occurs. But since that is not the case I am looking for a clean, reliable way to implement this type of functionality.
Edit: This needs to work with any event, including built-in events triggered by the Yii framework (eg. ActiveRecord::EVENT_AFTER_INSERT).
This improvement of my first comment.
class ScenarioEvent extends \yii\base\Event
{
private static $_events = [];
public static function onScenario($scenario, $class, $name, $handler, $data = null)
{
// You may use foreach if you wil use many scenarios for event
if (!isset(self::$_scenarios)) {
self::$_scenarios[$scenario] = [];
}
self::$_scenarios[$scenario][$name][] = $class;
return static::on($class, $name, $handler, $data);
}
public static function trigger($class, $name, $event = null)
{
if (
// If event is null assumed that it has no sender with scenario
$event === null
|| empty(self::$_scenarios[$event->sender->scenario][$name])
) {
parent::trigger($class, $name, $event);
} else {
$classes = array_merge(
[$class],
class_parents($class, true),
class_implements($class, true)
);
foreach ($classes as $class) {
if (in_array($class, self::$_scenarios[$event->sender->scenario][$name])) {
parent::trigger($class, $name, $event);
break;
}
}
}
}
}
I cannot seem to get my events to fire off. This is my first time playing around with them. If you look, i create an event in the init method and then i try to fire it off in the onBootstrap method. The event should produce a die with the string, but its not. any help would be appreciated.
namespace Application;
use Zend\ModuleManager\Feature\AutoloaderProviderInterface;
use Zend\ModuleManager\Feature\ControllerProviderInterface;
use Zend\ModuleManager\Feature\BootstrapListenerInterface;
use Zend\ModuleManager\Feature\ServiceProviderInterface;
use Zend\ModuleManager\Feature\ConfigProviderInterface;
use Zend\ModuleManager\Feature\InitProviderInterface;
use Zend\ModuleManager\Feature\ViewHelperProviderInterface;
use Zend\ModuleManager\ModuleManagerInterface;
use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;
use Zend\EventManager\EventInterface;
use Doctrine\DBAL\DriverManager;
class Module implements
AutoloaderProviderInterface,
ControllerProviderInterface,
BootstrapListenerInterface,
ServiceProviderInterface,
ConfigProviderInterface,
InitProviderInterface,
ViewHelperProviderInterface
{
public function init(ModuleManagerInterface $manager)
{
$eventManager = $manager->getEventManager();
$eventManager->attach('do',function($e){
$event = $e->getName();
$target = get_class($e->getTarget());
$params = $e->getParams();
$str = sprintf(
'Handled event \"%s\" on target \"%s\", with parameters %s',
$event,
$target,
json_encode($params)
);
die($str);
});
}
public function onBootstrap(EventInterface $e)
{
$this->attachEventManagerToModuleRouteListener($e);
$this->setupDoctrineConnectionMappings($e);
$eventManager = $e->getApplication()->getEventManager();
$eventManager->trigger('do',$this,array('j','o','n'));
}
public function getConfig()
{
return array_merge(
include __DIR__ . '/config/module.config.php',
include __DIR__ . '/config/routes.config.php'
);
}
public function getServiceConfig()
{
return include __DIR__ . '/config/services.config.php';
}
public function getControllerConfig()
{
return include __DIR__ . '/config/controllers.config.php';
}
public function getViewHelperConfig()
{
return include __DIR__ . '/config/view.config.php';
}
public function getAutoloaderConfig()
{
return array(
'Zend\Loader\StandardAutoloader' => array(
'namespaces' => array(
__NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__
),
),
);
}
private function attachEventManagerToModuleRouteListener(MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
}
private function setupDoctrineConnectionMappings(MvcEvent $e)
{
$driver = $e->getApplication()->getServiceManager()->get('doctrine.connection.orm_default');
$platform = $driver->getDatabasePlatform();
$platform->registerDoctrineTypeMapping('enum', 'string');
$platform->registerDoctrineTypeMapping('set', 'string');
}
}
The EventManager you're getting from the ModuleManager is a different EventManager than the Application's EventManager which you let trigger the event do.
Since during Module initialization the Application is not yet available, you've to bind your listener to the event via the SharedManager.
So attaching to Application's event do would go like this
$sharedManager = $manager->getEventManager()->getSharedManager();
$sharedManager->attach(Application::class, 'do', function($e) {
// event code
});
Please note that the shared manager requires the (an) identifier of the EventManager that's expected to trigger the event which in this case is (and often is) the classname of Application.
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 ?