I have an event called UserWasRegistered also I have a listener called UserWasRegistered from there I intned to develop job commands called:
EmailRegistrationConfirmation
NotifyAdminsNewRegistration
CreateNewBillingAccount
All these jobs would be executed within the UserWasRegistered event listener class.
Is this the correct approach or should i just have multiple listeners for UserWasRegistered? I feel using the jobs approach enabled me to call those "jobs" from other areas in my application at different times. e.g. calling CreateNewBillingAccount might be called if a user changed their details...?
I recommend to change the listener names that's more explicit about what's happening, so I'd avoid directly pairing listeners with events.
We're using an anemic event/listener approach, so listeners pass the actual task to "doers" (jobs, services, you name it).
This example is taken from a real system:
app/Providers/EventServiceProvider.php:
OrderWasPaid::class => [
ProvideAccessToProduct::class,
StartSubscription::class,
SendOrderPaidNotification::class,
ProcessPendingShipment::class,
LogOrderPayment::class
],
StartSubscription listeners:
namespace App\Modules\Subscription\Listeners;
use App\Modules\Order\Contracts\OrderEventInterface;
use App\Modules\Subscription\Services\SubscriptionCreator;
class StartSubscription
{
/**
* #var SubscriptionCreator
*/
private $subscriptionCreator;
/**
* StartSubscription constructor.
*
* #param SubscriptionCreator $subscriptionCreator
*/
public function __construct(SubscriptionCreator $subscriptionCreator)
{
$this->subscriptionCreator = $subscriptionCreator;
}
/**
* Creates the subscription if the order is a subscription order.
*
* #param OrderEventInterface $event
*/
public function handle(OrderEventInterface $event)
{
$order = $event->getOrder();
if (!$order->isSubscription()) {
return;
}
$this->subscriptionCreator->createFromOrder($order);
}
}
This way you can invoke jobs/services (SubscriptionCreator in this example) in other areas of your application.
It's also possible to bind the listener to other events as well, other than OrderWasPaid.
Related
I'm banging my head to find a solution to this but still i'm unable, I'm looking for a design vice solution not a hack to fix the issue.
I have following classes
class CourseService{
public function getCourse($courceId){
$course = $this->courseRepo->getCourse($courseId);
$restrictions = $this->invoiceService->getRestrictions($course->courseid);
$course->restrictions = [];
if($restrictions != null){
$course->restrictions = $restrictions;
}
}
}
Now this course service is injected in the constructor of the StudentService because when students need to enroll to a cource i use this course service there.
also you can see that I have used CourseRepo to get Course object and then InvoiceService to say which fields are restricted to update, basically restrictions attributes gives an array of strings defining which fields are not allowed to edit and I expect UI developer will use it to disable those fields, and I had to inject InvoiceService because there are some processing to do to the raw db records that are fetched from the InvoiceRepo so invoice repo is encapsulated in the invoiceService
now lets look at the InvoiceService
Class InvoiceService{
public function getAmountToPay($courseid, $studentid){
//now I need to inject StduentService inorder to get student info which needed for the calculation
}
}
but I can't inject StudentService into here because StudentService -> CourceService -> InvoiceService
Options I see and the consequences
One option I see is to get rid of InvoiceService from the CourseService and use InvoiceService in the place where the getCourse() get called and then modify the result but the problem is, CourseService is used mainly in controllers and next thing is that getCourse() get called from many controllers and service and expects the restrictions to be there so if I want to get rid of the InvoiceService then I'll have many places to add the removing lines and it crates a code repetition.
I can move getAmountToPay() to student service but then that service has already doing many student related tasks and i'm happy to extract just the invoice part to another service so I have a clear place to look when I need to check for bugs on invoices.
Student service:
First of all you have to see - actually to decide - that a student service uses an invoice service, not the reciprocal. When I enroll myself as a history student, I go to the registration/students office first. They are calling the financial/invoice office to ask about how much should I pay. The financial office checks in the database and returns the response regarding the amount to be payed by me.
Course service:
...The time passed by. Now I'm a student. I don't need to go to the registration office anymore. If I have to know something about my courses I go to the secretariat/course service. They'll give me all the informations I need about my courses. But, if I want to visit some special archeology course, where one must pay something, the course service will call the financial/invoice service to ask about that for me. They, in turn, will return the infos. The same applies if the course service wants to know about some financial restrictions I should have: they call the financial/invoice service.
Invoice service - student service, invoice service - course service:
Now, what should happen, if the invoice service needs infos about a student or a course? Should it call the student service, or the course service for that? The answer is no. The invoice service should receive a student id, a course id, a domain object Student, or a domain object Course as constructor/methods dependencies, but not the corresponding service(s). And it will fetch the infos it needs by itself. More of it, the invoice service should work with its specific invoice/financial tables, not with the course tables or the student details tables (except their id's).
Conclusions:
To enroll a student is the job of the StudentService. Though the
CourseService can assist the enrollment process.
StudentService verifies the amount to be paid by a student by calling
the InvoiceService. I know you don't want to have getAmountToPay()
inside the StudentService, but it's a natural workflow. You may think
of separate the other many things, for which the StudentService is
responsible, to another services.
The CourseService is responsible for finding a course, together with
the course restrictions, for which it calls the InvoiceService. So,
the CourseService will be assisted by the InvoiceService.
Down under I passed you the PHP version of my vision. I renamed some functions, to give you a better perspective.
Good luck!
P.S: I hope I understood right, that the sense of "invoice sevice" is a "financial department" one. Sorry, but I'm not a native english speaker, so I can't know all the senses.
<?php
class StudentService {
protected $courseService;
protected $invoiceService;
/**
* Even if the course service uses the invoice service,
* doesn't mean that the student service shouldn't use it too.
*
* #param CourseService $courseService
* #param InvoiceService $invoiceService
*/
public function __construct(CourseService $courseService, InvoiceService $invoiceService) {
$this->courseService = $courseService;
$this->invoiceService = $invoiceService;
}
/**
* Enroll a student to a course.
*
* #param integer $studentId
* #param integer $courseId
* #return bool Enrolled or not.
*/
public function enrollToCourse($studentId, $courseId) {
//... Use here the CourseService too - for what you said regarding the enrollment.
$enrolled = $this->studentRepo->enrollToCourse($studentId, $courseId);
return $enrolled;
}
/**
* Get the amount to be payed by a student on the enrollment moment.
*
* #param integer $studentId
* #param integer $courseid
* #return integer Amount to be payed.
*/
public function getAmountToPayOnEnrollment($studentId, $courseid) {
$amount = $this->invoiceService->getAmountToPayOnEnrollment($studentId, $courseid);
return $amount;
}
}
class CourseService {
protected $invoiceService;
/**
* Invoice service is used to get the (financial) restrictions for a course.
*
* #param InvoiceService $invoiceService
*/
public function __construct(InvoiceService $invoiceService) {
$this->invoiceService = $invoiceService;
}
/**
* Get a course and its corresponding (financial) restrictions list.
*
* #param integer $courseId
* #return Course Course domain object.
*/
public function getCourse($courseId) {
$course = $this->courseRepo->getCourse($courseId);
$course->restrictions = $this->getRestrictionsForCourse($course->courseId);
return $course;
}
/**
* Get the (financial) restrictions for a specified course.
*
* #param integer $courseId
* #return array Restrictions list.
*/
public function getRestrictionsForCourse($courseId) {
$restrictions = $this->invoiceService->getRestrictionsForCourse($courseId);
return $restrictions;
}
}
Class InvoiceService {
/**
* No student service needed!
*/
public function __construct() {
//...
}
/**
* Again, no student service needed: the invoice service
* fetches by itself the needed infos from the database.
*
* Get the amount to be payed by a student on the enrollment moment.
*
* #param integer $studentId
* #param integer $courseid
* #return integer Amount to be payed.
*/
public function getAmountToPayOnEnrollment($studentId, $courseid) {
$amount = $this->invoiceRepo->getAmountToPayOnEnrollment($studentId, $courseid);
return $amount;
}
/**
* Get the (financial) restrictions for a course.
*
* #param integer $studentId
* #param integer $courseid
* #return array Restrictions list.
*/
public function getRestrictionsForCourse($courseid) {
$restrictions = $this->invoiceRepo->getRestrictionsForCourse($courseid);
return isset($restrictions) ? $restrictions : [];
}
/*
* Quote: "Some processing to do to the raw
* db records that are fetched from the InvoiceRepo".
*/
//...
}
I would change your invoiceService to not depend on student service. Pass in what you need to the invoiceService. The logic of what to do with those student details can stay in invoiceService, but the content can be passed in.
Using Laravel 5.4
I have a Job model which has a enum field on it with different statuses. These statuses change in many different places. I made a JobHistory model and migration which tracks those changes .
On my Job model i define the new laravel 5.4 way of tracking eloquent events:
/**
* The events that should be fired for eloquent actions
*
* #var array
*/
protected $events = [
'updating' => JobChangedStatus::class
];
Status changes are done like this:
/**
* Change the job status
*
* #param $status
*/
public function changeStatus($status)
{
$this->update([
'status' => $status,
]);
}
EventServiceProvider:
'App\Events\Jobs\JobChangedStatus' => [
'App\Listeners\Jobs\CreateJobHistory',
],
CreateJobHistory Listener:
$job = $event->job;
$jobHistory = new JobHistory();
$jobHistory->old_status = $job->getOriginal('status');
$jobHistory->new_status = $job->status;
$jobHistory->job()->associate($job);
$jobHistory->executor()->associate(Auth::user());
$jobHistory->save();
When i change my job status from e.g New to In_progress
My JobHistory table will look like this:
So the new_status and the old_status both give the old value. I tried using $job->getDirty() but when i print it it just gives back a empty array.
What am i doing wrong?
Usually I would achieve this inside an Observer. It feels a little awkward to see the events/listener setup like that.
In your AppServiceProvider.php:
public function boot()
{
App\Job::observe(App\Observers\JobObserver::class);
}
And then in App\Observers\JobObserver.php:
use App\Job;
use App\JobHistory;
class JobObserver{
public function updating(Job $job)
{
$jobHistory = new JobHistory();
$jobHistory->old_status = $job->getOriginal('status');
$jobHistory->new_status = $job->status;
$jobHistory->job()->associate($job);
$jobHistory->executor()->associate(Auth::user());
$jobHistory->save();
}
}
For eloquent models it makes the most sense (just my opinion) to use observers. The events/listeners model I use for listening to mail events, job events, possibly notifications, etc.
The other reason this may help you in your situation is if the event is being queued instead of ran synchronously, you will end up with a race condition and most cases the model will have been saved and that is why getDirty() has no keys. With observers, the operations are always ran synchronously and you will not run into a timing issue.
I have two models, Show and Episode, with a one to many relationship. I have an Observer for each model to watch when they are deleted and do some tasks. If my ShowObserver looks like this everything works properly and cascades down, the EpisodeObserver fires its deleting() method for each Episode that is deleted along with the show:
<?php
/**
* Listen to the Show deleting event.
*
* #param Show $show
* #return void
*/
public function deleting(Show $show)
{
if ($show->isForceDeleting()) {
foreach ($show->episodes()->onlyTrashed()->get() as $episode) {
$episode->forceDelete();
}
} else {
$show->episodes()->delete();
}
}
However, if I change it to looks like this the EpisodeObserver#deleting() methods never fire even though the Episodes do get forceDeleted:
<?php
/**
* Listen to the Show deleting event.
*
* #param Show $show
* #return void
*/
public function deleting(Show $show)
{
if ($show->isForceDeleting()) {
$show->episodes()->onlyTrashed()->forceDelete();
} else {
$show->episodes()->delete();
}
}
Is there something about $show->episodes()->onlyTrashed()->forceDelete(); that is incorrect, or is this potentially a bug?
Check out the documentation (on the red warning block): https://laravel.com/docs/5.3/eloquent#deleting-models
When executing a mass delete statement via Eloquent, the deleting and deleted model events will not be fired for the deleted models. This is because the models are never actually retrieved when executing the delete statement.
This is also same to update call.
So if you need to fire the events, you have no choice but to delete it one by one, or fire your own custom event if performance is critical.
I have entities in Doctrine Symfony2: User, Channel, Video and Comment; user can report one of them. I designed Report entity with these fields:
userId
status
reportTime
description
how can I reference to reported Entity ?? because all reported fields are similar for all entities I want to use just one table for Report and add these fields to Report Entity:
referenceEntityName(a string and may be one of these: User, Channel, Video, Comment)
Channel(ManytoOne relation to Channel entity)
Video(ManytoOne relation to Video entity)
Comment(ManytoOne relation to Comment entity)
User(ManytoOne relation to User entity)
Is this best practice or I should create separate tables for each kind of report ??
Edit:
based on #Alex answer, I improved Report class and add these methods:
setEntity($entity){
if ($obj instanceof Video){
$this->referenceEntityName = 'Video';
$this->setVideo();
}
elseif($obj instanceof Comment){
$this->referenceEntityName == 'Comment'
$this->setComment();
}
//...
}
getEntity(){
if($this->referenceEntityName == 'Video'){
$this->getVideo()
}// ifelse statements for other entities ...
}
I till have 4 relation that just one of them is used for each instance, isn't it a bit messy!?
and again is this best practice or I should do something else?
what if I want to use FormBuilder class, isn't there any problem??
In a simple solution, whereby for example you only had Users (and not Videos, Comments and Channels), the solution would be simple; each User can have many Reports, and each Report must belong to only one User. This is a one-to-many relationship - one User has many Reports. In Symfony 2 and Doctrine, this would be modelled as such:
// src/Acme/DemoBundle/Entity/User.php
// ...
use Doctrine\Common\Collections\ArrayCollection;
class User
{
// ...
/**
* #ORM\OneToMany(targetEntity="Report", mappedBy="user")
*/
protected $reports;
public function __construct()
{
$this->reports = new ArrayCollection();
}
// ...
}
and
// src/Acme/DemoBundle/Entity/Report.php
// ...
class Report
{
// ...
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="reports")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $user;
// ...
}
In this instance, to create a Report and associate it with a User, we would:
// get the User the Report will belong to
$user = $em->getRepository('AcmeDemoBundle:User')->find(1);
// create the Report
$report = new Report();
// add the User to the Report
$report->setUser($user);
// then persist it, etc ...
Note, the setUser() method is available because the console command was run to generate them automatically. This is highly recommended as it created the necessary type hinting for you. For pre Symfony 2.5 installations, the command is:
php app/console doctrine:generate:entities Acme
>= 2.5 installations, the command is:
php bin/console doctrine:generate:entities Acme
Your requirements complicate this simple example somewhat, as Reports can also belong to Comments and Videos etc. For the sake of the example, let's call these things Entities. A bad approach would be to simply add 3 new properties to the Report, one for each of the new Entities, and then add 3 new setter methods for the Entities. This is bad for 2 reasons: a Report will only ever belong to one of the Entities, and therefore 3 of the properties and setter methods will never be used for each Report entity. Secondly, if you add a new Entity to your business model, or remove one, you need to edit your Report entity, and also the database schema.
A better method is to simply have one property and set method in your Report, that can be applied to all of your Entities. So instead of calling setUser, we could call a setEntity, and have it accept any of the 4. With this approach in mind, let's look back at the first example, and take note of the type hinting in the function signature that would have been produced for the setUser method:
public function setUser(Acme\DemoBundle\Entity\User $user)
See that it requires to be of type Acme\DemoBundle\Entity\User. How do we overcome this, and have it accept any of the 4 Entities? The solution is to have all Entities be derived from a parent class. Then make the function type hint at the base class:
public function setUser(Acme\DemoBundle\Entity\Base $entity)
The base class will contain all common elements, notably a 'name', and as array collection of Reports:
// src/Acme/DemoBundle/Entity/Base.php
// ...
use Doctrine\Common\Collections\ArrayCollection;
class Base
{
// ...
/**
* #ORM\Column(name="name", type="text")
*/
protected $name
/**
* #ORM\OneToMany(targetEntity="Report", mappedBy="baseEntity")
*/
protected $reports;
public function __construct()
{
$this->reports = new ArrayCollection();
}
// ...
}
and then for each child, for example a User and a Video:
// src/Acme/DemoBundle/Entity/User.php
// ...
use AcmeDemoBundle\Entity\Base;
class User extends Base
{
/**
* #ORM\Column(name="firstname", type="text")
*/
protected $firstName;
// ...
}
and the Video
// src/Acme/DemoBundle/Entity/Video.php
// ...
use AcmeDemoBundle\Entity\Base;
class Video extends Base
{
/**
* #ORM\Column(name="title", type="text")
*/
protected $title;
// ...
and change our Report Entity:
// src/Acme/DemoBundle/Entity/Report.php
// ...
class Report
{
// ...
/**
* #ORM\ManyToOne(targetEntity="Base", inversedBy="reports")
* #ORM\JoinColumn(name="base_id", referencedColumnName="id")
*/
protected $baseEntity;
// ...
}
Remember to run the doctrine command to generate the setBaseEntity method. When you do, notice that it will now accept any class derived of Base
Then, to put on a Report on a Video for example, we get the Video, create a Report, and add the Video to the Report:
$video = // get the video you want
$report = new Report();
$report->setBaseEntity($video);
To retrieve all Reports belonging to a Comment, we get the Comment, and get the Reports:
$video = // get the video you want
$reports = $video->getReports();
foreach($reports as $report){
$reportText = $report->getText(); // assuming the Report has a `text` field
}
Update:
The inheritance relationship between these Entities can be modelled in the database with Doctrine using Single Table Inheritance:
/**
* #ORM\Entity
* #ORM\Table(name="base_entities")
* #ORM\InheritanceType("SINGLE_TYPE")
* #ORM\Discriminator(name="entity_type", type="string")
* #ORM\DiscriminatorMap({"user" = "User", "comment" = "Comment", "video" = "Video", "channel" = "Channel"})
*/
I am new to Magento. I want to build an observer which on cancellation of an order will perform a query to my database and will decide whether the order is cancellable or not (This is decided on the basis of a certain state.). If it can't be cancelled, then it should break the cancel event and display a message that the order cannot be cancelled.
Which event I should choose, order_cancel_after or sales_order_item_cancel, and how can I break out of this event in between?
Thanks in advance. :)
There is no general answer to this, it depends on the context where the event is triggered and what happens there afterwards.
The events don't have an interface to "stop" them and they are not tied to the actual "event" (i.e. order cancellation) other than by name.
So you will have to look at the code of Mage_Sales_Model_Order_Item where sales_order_item_cancel gets triggered (order_cancel_after is obviously the wrong place to look because at that point the order is already cancelled):
/**
* Cancel order item
*
* #return Mage_Sales_Model_Order_Item
*/
public function cancel()
{
if ($this->getStatusId() !== self::STATUS_CANCELED) {
Mage::dispatchEvent('sales_order_item_cancel', array('item'=>$this));
$this->setQtyCanceled($this->getQtyToCancel());
$this->setTaxCanceled($this->getTaxCanceled() + $this->getBaseTaxAmount() * $this->getQtyCanceled() / $this->getQtyOrdered());
$this->setHiddenTaxCanceled($this->getHiddenTaxCanceled() + $this->getHiddenTaxAmount() * $this->getQtyCanceled() / $this->getQtyOrdered());
}
return $this;
}
You see that there is no additional check after the event was dispatched, but it would be possible to set the qty_to_cancel attributes to 0 to uneffect the cancelling.
Your observer method:
public function salesOrderItemCancel(Varien_Event_Observer $observer)
{
$item = $observer->getEvent()->getItem();
if (!$this->_isCancellable($item->getOrder())) {
$item->setQtyToCancel(0);
$this->_showErrorMessage();
}
}
Note that you don't have to set tax_canceled or hidden_tax_canceled because they depend on qty_canceled and thus will stay 0.