Maybe I'm searching it all wrong but I haven't been able to figure out an answer.. Say I have a model Building, which always has n Floor(s)
I would like to write a constructor for Building, in which I could specify a number of Floor(s) to be created. The problem is that I can't link back a Floor to the Building because when the constructor for Building is being called, it doesn't have a primary key yet...
Basically, my code looks like this but doesn't work:
class Building extends Model {
public function __construct($nbFloors) {
for($i=0; $i<$nbFloors; $i++) {
$foo = new Floor();
$foo->building_id = $this->id;
$foo->save();
}
}
}
What would be the correct solution to achieve something like that?
The primary key will never be available in the constructor and your constructor's definition is not compatible with Model which expects an array of attributes as the first argument.
You're performing too much logic in your constructor, a constructor is meant to just instantiate an object and its dependencies, not perform business logic. By doing this in your constructor, you're actually going to be attempting to create new floors EVERY time your Model is instantiated which includes when your model is retrieved from the database.
I'd recommend adding a new method like:
public function createWithFloors($n) {
$this->save();
...
}
Now, you can use the model as it's expected and call the create method:
$building = new Building(['name' => 'Empire State']);
$building->createWithFloors(102);
Besides the solutions already suggested, you could create an event that is fired when a Building is created. A listener could then store your Floors. For event reference, have a look at the documentation.
First, create an event called BuildingCreated with php artisan make:event BuildingCreated and use below code:
namespace App\Events;
use App\Building;
use Illuminate\Queue\SerializesModels;
class BuildingCreated extends Event
{
use SerializesModels;
public $building;
public function __construct(Building $building)
{
$this->building = $building;
}
}
Then, register the event within your Building model:
use App\Events\BuildingCreated;
class Building
{
protected $dispatchesEvents = [
'created' => BuildingCreated::class,
];
}
Next, you will need a listener that creates the floors. Create it with php artisan make:listener AddFloorsToNewBuilding and adapt it as you need:
namespace App\Listeners;
use App\Building;
use App\Events\BuildingCreated;
class AddFloorsToNewBuilding
{
public function handle(BuildingCreated $event)
{
$floors = ...;
$event->building->floors()->saveMany($floors);
$event->building->save();
}
}
Lastly, have the listener listen for the event by adding it to the $listen array in the EventServiceProvider:
class EventServiceProvider
{
protected $listen = [
\App\Events\BuildingCreated::class => [
\App\Listeners\AddFloorsToNewBuilding::class,
],
];
}
since, you can't bind Floor to a building that is not created yet, you should make the "new floors number" an attribute of the Building instance. Then you overload the save method to create the new floors.
class Building extends Model {
/** number of floors to be created on save
* #var int
*/
private $newFloorsCount;
/**
* Building constructor.
* #param array $attributes
* #param int $nbFloors
*/
public function __construct(array $attributes = [], $nbFloors = 0) {
parent::__construct($attributes);
$this->newFloorsCount = $nbFloors;
}
/**
* #param array $options
* #return bool
*/
public function save(array $options = [])
{
$return = parent::save($options);
for($i=0; $i<$this->newFloorsCount; $i++) {
$foo = new Floor();
$foo->building_id = $this->id;
$foo->save();
}
return $return;
}
}
now you can just do
$building = new Building([],5);
$building->save();
Related
In ZF3 you would normally attach your event listener for the MvcEvent's in your module's Module.php like so:
<?php
namespace MyModule;
class Module
{
public function onBootstrap(MvcEvent $event)
{
$eventManager = $event->getApplication()->getEventManager();
$eventManager->attach(MvcEvent::EVENT_DISPATCH, function(MvcEvent $event) {
// Do someting...
});
}
}
Now there are two typical situations where your Module.php can grow big:
Your module has to handle multiple (or even all) MvcEvent's and maybe even treat them in different ways.
Your module has to perform multiple actions on a single MvcEvent.
What I'd like to be able to do is to specify a class name in my module.config.php along with one or multiple MvcEvent names to keep my Module.php nice and clean.
Is there a way to do this in Zend Framework 3?
#Nukeface has a great example but it does not directly answer my specific question.
To answer my own question:
This is possible with the use of listeners. A listener can be configured in the configuration files but it cannot be mapped to an event directly from the configuration alone.
It is possible to check for a specific setting in the configuration and determine what classes to map to what events. Even MvcEvents can be mapped this way.
Here's how to set it up:
1. The listener
We want to listen to multiple MvcEvents with one simple class. Note the class it extends.
namespace Demo\Listener;
class MyListener extends EventClassMapListener
{
public function handleEvent(MvcEvent $event)
{
// Do something
\Zend\Debug\Debug::dump($event->getName());
}
}
2. The abstract listener class
The above class needs a bit more body but that can be provided by the abstract listener class:
namespace Demo\Listener;
abstract class EventClassMapListener implements ListenerAggregateInterface
{
private $configuration;
public function __construct(array $configuration)
{
$this->configuration = $configuration;
}
public function attach(EventManagerInterface $events, $priority = 1)
{
$sharedManager = $events->getSharedManager();
foreach ($this->configuration as $identifier => $settings) {
foreach ($settings as $event => $configPriority) {
$sharedManager->attach($identifier, $event, [$this, 'handleEvent'], $configPriority ?: $priority);
}
}
}
public function detach(EventManagerInterface $events)
{
// Do the opposite of attach
}
abstract public function handleEvent(MvcEvent $event);
}
3. The factory
Now we need a factory that we can reuse for all our classes that need to listen to multiple events:
namespace Demo\Factory\Listener;
class EventClassmapListenerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$globalConfiguration = $container->get('config');
$configuration = [];
if (array_key_exists('event_classmap', $globalConfiguration)
&& array_key_exists($requestedName, $globalConfiguration['event_classmap'])
) {
$configuration = $globalConfiguration['event_classmap'][$requestedName];
}
return new $requestedName($configuration);
}
}
4. Configuration
In your module.config.php:
'service_manager' => [
'factories' => [
Listener\MyListener::class => Factory\Listener\EventClassmapListenerFactory::class,
],
],
'listeners' => [
Listener\MyListener::class,
],
'event_classmap' => [
// Name of the class that needs to listen to events
Listener\MyListener::class => [
// Identifier
\Zend\Mvc\Application::class => [
// List of event names and priorities
MvcEvent::EVENT_BOOTSTRAP => 1,
],
// Another identifier
MyEventEmitterClass::class => [
MyEventEmitterClass::EVENT_ONE,
MyEventEmitterClass::EVENT_TWO,
MyEventEmitterClass::EVENT_THREE,
],
],
],
Conclusion:
Although it might not really refined, I really like this idea. It is now fairly easy to add another listener and make it listen to a list of events from one or more emitters.
My opinion after some research
A listener itself should state what it wants to listen to, to keep things strict. Putting that information in a configuration file might result in a more complicated situation when it is not needed.
You need a few things for Listener classes:
Events
Listeners
Handlers
Factories
Config
Now, 2 & 3 are usually in the same class as you would usually have a Listener class for a specific purpose. Such as "Listen for Rocket launch and steer Rocket to Mars".
As such, you would need to "create" these "events" to listen for somewhere. Such as a DemoEvents class!
namespace Demo\Event;
use Zend\EventManager\Event;
class DemoEvent extends Event
{
const THE_STRING_TO_LISTEN_FOR = 'rocket.ready.for.launch';
const ANOTHER_STRING_TO_LISTEN_FOR = 'rocket.steer.to.mars';
}
Now that we have "events", we need to "listen" for them. For that we need a Listener. Because I'm limiting this to 1 example, the Handler (function(-ality) to be executed when the "event" we're "listening" for is "heard") will be in the same class.
namespace Demo\Listener;
use Demo\Event\DemoEvent;
use Zend\EventManager\Event;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
class DemoListener 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]);
}
}
}
/**
* #param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events, $priority = 1)
{
$sharedManager = $events->getSharedManager();
$sharedManager->attach(Demo::class, DemoEvent::THE_STRING_TO_LISTEN_FOR, [$this, 'doSomethingOnTrigger'], -10000);
}
/**
* Apart from triggering specific Listener function and de-registering itself, it does nothing else. Add your own functionality
*
* #param Event $event
*/
public function doSomethingOnTrigger(Event $event)
{
// Gets passed along parameters from the ->trigger() function elsewhere
$params = $event->getParams();
$specificClass = $params[SpecificClass::class];
// Do something useful here
$specificClass->launchRocketIntoOrbit();
// Detach self to prevent running again
$specificClass->getEventManager()->getSharedManager()->clearListeners(get_class($specificClass), $event->getName());
// NOTE: USE THIS TRIGGER METHODOLOGY ELSEWHERE USING THE STRING FROM THE ATTACH() FUNCTION TO TRIGGER THIS FUNCTION
// Trigger events specific for the Entity/class (this "daisy-chains" events, allowing for follow-up functionality)
$specificClass->getEventManager()->trigger(
DemoEvent::ANOTHER_STRING_TO_LISTEN_FOR,
$specificClass ,
[get_class($specificClass) => $specificClass ] // Params getting passed along
);
}
}
Excellent. We now have a events, a listener and a handler. We just need a factory to create this class when needed.
namespace Demo\Factory;
use Demo\Listener;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
class DemoListenerFactory implements FactoryInterface
{
/**
* #param ContainerInterface $container
* #param string $requestedName
* #param array|null $options
* #return object|DemoListener
* #throws \Psr\Container\ContainerExceptionInterface
* #throws \Psr\Container\NotFoundExceptionInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
// If you're implementation of the Listener has any requirements, load them here and add a constructor in the DemoListener class
return new DemoListener();
}
}
Lastly, we need some config. Obviously we need to register the Listener + Factory combination. Let's do that first.
namespace Demo;
use Demo\Listener\DemoListener;
use Demo\Listener\DemoListenerFactory;
'service_manager' => [
'factories' => [
DemoListener::class => DemoListenerFactory::class,
],
],
Now for a little known bit of config to make sure that the Listener gets registered as a Listener:
'listeners' => [
DemoListener::class
],
Yep, that's it.
Make sure to add both of these bits of config at the first level of config, they're siblings.
I'm developing an application where my data comes from external server in JSON format.
I would like to set a relationships between each models, but without using a database table.
Is it possible ?
Something like that:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'https://.../server/flights.json';
}
You could make a service class which handles the request and returns class instances:
namespace App\Services;
class FlightService
{
/**
* #var FlightFactory
*/
private $flightFactory;
public function __construct(FlightFactory $flightFactory)
{
$this->flightFactory = $flightFactory;
}
public function getAllFlights()
{
$flightsJson = $this->getFromExternalCurl();
return $this->flightFactory->buildFlightList($flightsJson);
}
private function getFromExternalCurl()
{
return Curl::to('http://www.foo.com/flights.json')
->withData( array( 'foz' => 'baz' ) )
->asJson()
->get();
}
}
Basically the service would make the external API call and the response is passed to a factory which creates the instances.
Note that you just need to add the factory in the construct and it's binded because laravel uses https://laravel.com/docs/5.4/container
namespace App\Factories;
class FlightFactory
{
public function buildFlightList($flightJsonList)
{
$flightCollection = collect();
foreach($flightJsonList as $flightJson) {
$flightCollection->push($this->buildFlight($flightJson));
}
return $flightCollection;
}
public function buildFlight($flightJson)
{
$flight = new Flight();
// add properties
return $flight;
}
}
The factory will return a Collection which is verry usefull because it contains usefull methods, or you can return an array.
In this example I used a curl library https://github.com/ixudra/curl but it can be replaced with native php or other libraries.
Then you can use by injecting the FlightService in your controllers.
P.S: Code not tested but represents a possible approach
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.
So the way I see it is that a good Laravel application should be very model- and event-driven.
I have a Model called Article. I wish to send email alerts when the following events happen:
When an Article is created
When an Article is updated
When an Article is deleted
The docs say I can use Model Events and register them within the boot() function of App\Providers\EventServiceProvider.
But this is confusing me because...
What happens when I add further models like Comment or Author that need full sets of all their own Model Events? Will the single boot() function of EventServiceProvider just be absolutely huge?
What is the purpose of Laravel's 'other' Events? Why would I ever need to use them if realistically my events will only respond to Model CRUD actions?
I am a beginner at Laravel, having come from CodeIgniter, so trying to wrap my head around the proper Laravel way of doing things. Thanks for your advice!
In your case, you may also use following approach:
// Put this code in your Article Model
public static function boot() {
parent::boot();
static::created(function($article) {
Event::fire('article.created', $article);
});
static::updated(function($article) {
Event::fire('article.updated', $article);
});
static::deleted(function($article) {
Event::fire('article.deleted', $article);
});
}
Also, you need to register listeners in App\Providers\EventServiceProvider:
protected $listen = [
'article.created' => [
'App\Handlers\Events\ArticleEvents#articleCreated',
],
'article.updated' => [
'App\Handlers\Events\ArticleEvents#articleUpdated',
],
'article.deleted' => [
'App\Handlers\Events\ArticleEvents#articleDeleted',
],
];
Also make sure you have created the handlers in App\Handlers\Events folder/directory to handle that event. For example, article.created handler could be like this:
<?php namespace App\Handlers\Events;
use App\Article;
use App\Services\Email\Mailer; // This one I use to email as a service class
class ArticleEvents {
protected $mailer = null;
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
public function articleCreated(Article $article)
{
// Implement mailer or use laravel mailer directly
$this->mailer->notifyArticleCreated($article);
}
// Other Handlers/Methods...
}
Recently I came to same problem in one of my Laravel 5 project, where I had to log all Model Events. I decided to use Traits. I created ModelEventLogger Trait and simply used in all Model class which needed to be logged. I am going to change it as per your need Which is given below.
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Event;
/**
* Class ModelEventThrower
* #package App\Traits
*
* Automatically throw Add, Update, Delete events of Model.
*/
trait ModelEventThrower {
/**
* Automatically boot with Model, and register Events handler.
*/
protected static function bootModelEventThrower()
{
foreach (static::getModelEvents() as $eventName) {
static::$eventName(function (Model $model) use ($eventName) {
try {
$reflect = new \ReflectionClass($model);
Event::fire(strtolower($reflect->getShortName()).'.'.$eventName, $model);
} catch (\Exception $e) {
return true;
}
});
}
}
/**
* Set the default events to be recorded if the $recordEvents
* property does not exist on the model.
*
* #return array
*/
protected static function getModelEvents()
{
if (isset(static::$recordEvents)) {
return static::$recordEvents;
}
return [
'created',
'updated',
'deleted',
];
}
}
Now you can use this trait in any Model you want to throw events for. In your case in Article Model.
<?php namespace App;
use App\Traits\ModelEventThrower;
use Illuminate\Database\Eloquent\Model;
class Article extends Model {
use ModelEventThrower;
//Just in case you want specific events to be fired for Article model
//uncomment following line of code
// protected static $recordEvents = ['created'];
}
Now in your app/Providers/EventServiceProvider.php, in boot() method register Event Handler for Article.
public function boot(DispatcherContract $events)
{
parent::boot($events);
$events->subscribe('App\Handlers\Events\ArticleEventHandler');
}
Now create Class ArticleEventHandler under app/Handlers/Events directory as below,
<?php namespace App\Handlers\Events;
use App\Article;
class ArticleEventHandler{
/**
* Create the event handler.
*
* #return \App\Handlers\Events\ArticleEventHandler
*/
public function __construct()
{
//
}
/**
* Handle article.created event
*/
public function created(Article $article)
{
//Implement logic
}
/**
* Handle article.updated event
*/
public function updated(Article $article)
{
//Implement logic
}
/**
* Handle article.deleted event
*/
public function deleted(Article $article)
{
//Implement logic
}
/**
* #param $events
*/
public function subscribe($events)
{
$events->listen('article.created',
'App\Handlers\Events\ArticleEventHandler#created');
$events->listen('article.updated',
'App\Handlers\Events\ArticleEventHandler#updated');
$events->listen('article.deleted',
'App\Handlers\Events\ArticleEventHandler#deleted');
}
}
As you can see from different answers, from different Users, there are more than 1 way of handling Model Events. There are also Custom events That can be created in Events folder and can be handled in Handler folder and can be dispatched from different places. I hope it helps.
I found this the cleanest way to do what you want.
1.- Create an observer for the model (ArticleObserver)
use App\Article;
class ArticleObserver{
public function __construct(Article $articles){
$this->articles = $articles
}
public function created(Article $article){
// Do anything you want to do, $article is the newly created article
}
}
2.- Create a new ServiceProvider (ObserversServiceProvider), remember to add it to you config/app.php
use App\Observers\ArticleObserver;
use App\Article;
use Illuminate\Support\ServiceProvider;
class ObserversServiceProvider extends ServiceProvider
{
public function boot()
{
Article::observe($this->app->make(ArticleObserver::class));
}
public function register()
{
$this->app->bindShared(ArticleObserver::class, function()
{
return new ArticleObserver(new Article());
});
}
}
You can opt for the Observer approach to deal with Model Events. For example, here is my BaseObserver:
<?php
namespace App\Observers;
use Illuminate\Database\Eloquent\Model as Eloquent;
class BaseObserver {
public function saving(Eloquent $model) {}
public function saved(Eloquent $model) {}
public function updating(Eloquent $model) {}
public function updated(Eloquent $model) {}
public function creating(Eloquent $model) {}
public function created(Eloquent $model) {}
public function deleting(Eloquent $model) {}
public function deleted(Eloquent $model) {}
public function restoring(Eloquent $model) {}
public function restored(Eloquent $model) {}
}
Now if I am to create a Product Model, its Observer would look like this:
<?php
namespace App\Observers;
use App\Observers\BaseObserver;
class ProductObserver extends BaseObserver {
public function creating(Eloquent $model)
{
$model->author_id = Sentry::getUser()->id;
}
public function created(Eloquent $model)
{
if(Input::hasFile('logo')) Image::make(Input::file('logo')->getRealPath())->save(public_path() ."/gfx/product/logo_{$model->id}.png");
}
public function updating(Eloquent $model)
{
$model->author_id = Sentry::getUser()->id;
}
public function updated(Eloquent $model)
{
if(Input::has('payment_types')) $model->paymentTypes()->attach(Input::get('payment_types'));
//Upload logo
$this->created($model);
}
}
Regarding listeners, I create an observers.php file inside Observers dir and I include it from the AppServiceProvider. Here is a snippet from within the observers.php file:
<?php
\App\Models\Support\Ticket::observe(new \App\Observers\Support\TicketObserver);
\App\Models\Support\TicketReply::observe(new \App\Observers\Support\TicketReplyObserver);
All of this is regarding Model Events.
If you need to send an e-mail after a record is created, it would be cleaner to use the Laravel 'other' Events, as you will have a dedicated class to deal with just that, and fire it, when you wish, from the Controller.
The 'other' Events will have much more purpose as the more automated your app becomes, think of all the daily cronjobs you will need at some point. There will be no more cleaner way to deal with that other than 'other' Events.
You've tagged this question as Laravel 5, so I would suggest not using model events as you'll end up with lots of extra code in your models which may make things difficult to manage in future. Instead, my recommendation would be to make use of the command bus and events.
Here's the docs for those features:
http://laravel.com/docs/5.0/bus
http://laravel.com/docs/5.0/events
My recommendation would be to use the following pattern.
You create a form which submits to your controller.
Your controller dispatches the data from the request generated to a command.
Your command does the heavy lifting - i.e. creates an entry in the database.
Your command then fires an event which can be picked up by an event handler.
Your event handler does something like send an email or update something else.
There are a few reasons why I like this pattern: Conceptually your commands handle things that are happening right now and events handle things that have just happened. Also, you can easily put command and event handlers onto a queue to be processed later on - this is great for sending emails as you tend not to want to do that in real time as they slow the HTTP request down a fair bit. You can also have multiple event handlers for a single event which is great for separating concerns.
It would be difficult to provide any actual code here as your question more about the concepts of Laravel, so I'd recommend viewing these videos so you get a good idea of how this pattern works:
This one describes the command bus:
https://laracasts.com/lessons/laravel-5-events
This one describes how events work:
https://laracasts.com/lessons/laravel-5-commands
You can have multiple listeners on an event. So you may have a listener that sends an email when an article is updated, but you could have a totally different listener that does something totally different—they’ll both be executed.
1) You may create an event listener for each new Model (ArticleEventSubscriber,CommentEventSubscriber) at boot method:
EventServiceProvider.php
public function boot(DispatcherContract $events)
{
parent::boot($events);
$events->subscribe('App\Listeners\ArticleEventListener');
$events->subscribe('App\Listeners\CommentEventListener');
}
or you may also use $subscribe property
protected $subscribe = [
'App\Listeners\ArticleEventListener',
'App\Listeners\CommentEventListener',
];
There are many ways to listen and handle events. Take a look to current master documentation for discovering more ways(like usings closures) to do so : Laravel Docs (master) and this other answer
2) Model events are just events provided by default by Eloquent.
https://github.com/illuminate/database/blob/491d58b5cc4149fa73cf93d499efb292cd11c88d/Eloquent/Model.php#L1171
https://github.com/illuminate/database/blob/491d58b5cc4149fa73cf93d499efb292cd11c88d/Eloquent/Model.php#L1273
I might come after the battle, but If you do not want all the fuss of extending classes or creating traits, you might want to give a try to this file exploration solution.
Laravel 5.X solution
Beware the folder you choose to fetch the models should only contain models to make this solution to work
Do not forget to add the use File
app/Providers/AppServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use File;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
$model_location = base_path() . '/app'; // Change to wherever your models are located at
$files = File::files( $model_location );
foreach( $files as $data ) {
$model_name = "App\\" . pathinfo($data)['filename'];
$model_name::creating(function($model) {
// ...
});
$model_name::created(function($model) {
// ...
});
$model_name::updating(function($model) {
// ...
});
$model_name::updated(function($model) {
// ...
});
$model_name::deleting(function($model) {
// ...
});
$model_name::deleted(function($model) {
// ...
});
$model_name::saving(function($model) {
// ...
});
$model_name::saved(function($model) {
// ...
});
}
}
/**
* Register any application services.
*
* #return void
*/
public function register()
{
//
}
}
Hope it helps you write the less code possible!
Laravel 6, the shortest solution
BaseSubscriber class
namespace App\Listeners;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Str;
/**
* Class BaseSubscriber
* #package App\Listeners
*/
abstract class BaseSubscriber
{
/**
* Returns the first part of an event name (before the first dot)
* Can be a class namespace
* #return string
*/
protected abstract function getEventSubject(): string;
/**
* Register the listeners for the subscriber.
* #param Dispatcher $events
*/
public function subscribe($events)
{
$currentNamespace = get_class($this);
$eventSubject = strtolower(class_basename($this->getEventSubject()));
foreach (get_class_methods($this) as $method) {
if (Str::startsWith($method, 'handle')) {
$suffix = strtolower(Str::after($method, 'handle'));
$events->listen("$eventSubject.$suffix", "$currentNamespace#$method");
}
}
}
}
OrderEventSubscriber class. Handlers for Order model events
use App\Models\Order;
/**
* Class OrderEventSubscriber
* #package App\Listeners
*/
class OrderEventSubscriber extends BaseSubscriber
{
/**
* #return string
*/
protected function getEventSubject(): string
{
return Order::class; // Or just 'order'
}
/**
* #param Order $order
*/
public function handleSaved(Order $order)
{
// Handle 'saved' event
}
/**
* #param Order $order
*/
public function handleCreating(Order $order)
{
// Handle 'creating' event
}
}
ModelEvents trait. It goes to your models, in my case - App\Model\Order
namespace App\Traits;
use Illuminate\Database\Eloquent\Model;
/**
* Trait ModelEvents
* #package App\Traits
*/
trait ModelEvents
{
/**
* Register model events
*/
protected static function bootModelEvents()
{
foreach (static::registerModelEvents() as $eventName) {
static::$eventName(function (Model $model) use ($eventName) {
event(strtolower(class_basename(static::class)) . ".$eventName", $model);
});
}
}
/**
* Returns an array of default registered model events
* #return array
*/
protected static function registerModelEvents(): array
{
return [
'created',
'updated',
'deleted',
];
}
}
Register the subscriber in a service provider, e.g AppServiceProvider
/**
* #param Dispatcher $events
*/
public function boot(Dispatcher $events)
{
$events->subscribe(OrderEventSubscriber::class);
}
How just add the ModelEvents trait into your model, adjust the events you want to register instead of default ones:
protected static function registerModelEvents(): array
{
return [
'creating',
'saved',
];
}
Done!
I have been trying to learn how event works in Yii from the Yii Guide. I have found there are most important things: Event Handlers, Attaching Event Handlers and Triggering Events. I have read entire the article properly. But I don't understand how to implement these three things properly. How to see the effect of it's implementation. I have extended component class as:
namespace app\components;
use yii\base\Component;
use yii\base\Event;
use yii\web\View;
class Foo extends Component{
const EVENT_HELLO = 'hello';
public function bar()
{
$this->trigger(self::EVENT_HELLO);
}
}
I do not understand what is the next part to do. Where I should write the Attaching Event Handlers. Can you help me someone, at least I can see a simple output using event.
You may create init() method in model:
public function init()
{
$this->on(Event::ACTION_ADD, ['app\models\Event', 'sendInLog']);
$this->on(Event::ACTION_DELETE, ['app\models\Event', 'sendInLog']);
$this->on(Event::ACTION_UPDATE, ['app\models\Event', 'sendInLog']);
}
In initialize events in second parameter you may use current model or set other model. If you want use current model set like that:
[$this, 'sendInLog']
sendInLog it is method in model. In method sendInLog one parameter it is $event. This is object \yii\base\Event. In property $event->name - it is event name. In property $event->sender - it is model class from trigger event.
In my class app\models\Event like that:
namespace app\models;
class Event extends Component
{
const ACTION_ADD = 1;
const ACTION_DELETE = 2;
const ACTION_UPDATE = 3;
const TYPE_PROJECT = 10;
const TYPE_BIDS = 20;
const TYPE_BIDS_DATA = 30;
/**
* #param $event
*/
public static function sendInLog($event)
{
/** #var \yii\base\Event $event */
/** #var ActiveRecord $event->sender */
$userId = Yii::$app->user->id;
$model = new Logs([
'type' => $event->sender->getType(),
'action' => $event->name,
'id_user' => $userId,
'old_data' => Json::encode($event->sender->attributes),
'new_data' => Json::encode($event->sender->oldAttributes),
]);
$model->save();
}
}
Run trigger like that:
public function afterDelete()
{
$this->trigger(Event::ACTION_DELETE);
parent::afterDelete();
}
Or
public function actionView()
{
$this->trigger(Event::ACTION_VIEW);
$this->render(...);
}
EDIT:
For example. If you want run trigger after delete, insert, update. You may use trigger in afterDelete, afterSave in model. If you want run trigger in controller run trigger like that:
public function actionCreate()
{
$model = new Bids();
$model->id_project = Yii::$app->request->get('projectId');
$fieldsDefaults = BidsFieldsDefaults::find()->orderBy(['order' => SORT_ASC])->all();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
$model->trigger(Event::ACTION_ADD);
return $this->redirect(['view', 'id' => $model->id]);
} else {
return $this->render('create', [
'model' => $model,
'fieldsDefaults' => $fieldsDefaults
]);
}
}
I show you two different ways to run trigger. Which one to use is up to you :)