I am dealing here with two events of a model, updating and updated as you can see in the below code. My concern is that I want to do some task in the updated event only if the teacherId has been changed, So I figured to check the value at the updating event and use a class property to know if it had been changed or not but that flag always returns false assuming its defined value.
namespace App\Observers;
class SubjectObserver {
private $shouldUpdateThreads = false;
/**
* Listen to the ThreadList created event.
* This events Fire when ThreadList is handled
* #param \App\Observers\App\ThreadList $threadList
*/
public function created(\subject $subject) {
}
/**
* Listen to the ThreadList saved event.
* #param \App\Observers\App\ThreadList $threadList
*/
public function saved(\subject $subject) {
}
/**
* Handling subject updated event
* Used to update the threads and related models
* #param \subject $subject
*/
public function updated(\subject $subject) {
info('After Update Event ' . $this->shouldUpdateThreads);
if ($this->shouldUpdateThreads) {
info_plus($subject);
}
info('After Update Check');
}
/**
* Handling subject being updated event
* Used to check if the teachers data has changed or not
* #param \subject $subject
*/
public function updating(\subject $_subject) {
$subject = \subject::find($_subject->id);
$this->shouldUpdateThreads = ($subject->teacherId != $_subject->teacherId) ? true : false;
info(($subject->teacherId != $_subject->teacherId));
info("Thread update ? " . $this->shouldUpdateThreads);
}
public function deleted(\subject $subject) {
info("Subject deleted");
}
}
Is this even the right apporach ? if not what am I doing wrong ?
In Eloquent Models you can use the getOriginal method when updating attributes.
So if you want to get the original value (before update) of teacherId you can do:
$subject->getOriginal('teacher_id');
See https://laravel.com/api/5.5/Illuminate/Database/Eloquent/Model.html#method_getOriginal
In your code:
public function updated(\subject $subject) {
info('After Update Event ' . $this->shouldUpdateThreads);
if ($subject->getOriginal('teacher_id') !== $subject->teacher_id) {
info_plus($subject);
}
info('After Update Check');
}
Related
I have Team eloquent and TeamObserver. TeamObserver has deleting event and in the event i call TeamDeletingEvent. TeamDeletingEvent dispatch TeamDeletingListener. If TeamDeletingLister will return false will stop the team delete function?
TeamObserver
class TeamObserver
{
/**
* Handle the team "deleting" event.
*
* #param Team $team
* #return void
*/
public function deleting(Team $team)
{
event(new TeamDeletingEvent($team));
}
}
TeamDeletingEvent
class TeamDeletingEvent
{
use SerializesModels;
/**
* #var Team
*/
public $team;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct(Team $team)
{
$this->team = $team;
}
}
TeamDeletingListener
class TeamDeletingListener
{
/**
* Handle the event.
*
* #param TeamDeletingEvent $event
* #return bool
*/
public function handle(TeamDeletingEvent $event)
{
$teamUser = Team::where('id', $event->team->id)->users()->first();
if(is_null($teamUser)){
return true;
}
return false;
}
}
More than this TeamObserver registered in AppServiceProvider and Event and Listener registered in EventServiceProvider
listener return false will not stop the delete function. It will stop the continuous listener call of the event. But Eloquent event returning false will stop the delete function. (model instance values gets updated but these are not updated in database). More info Detail Answer
As stated in the doc, laravel will not fire an event on mass update/insert/delete.
https://laravel.com/docs/5.8/eloquent#events
It uses the Builder for this and will not fire an event.
Is there a way that I can still fire an event after a mass update for example? I would only need the query Builder to extract the needed info myself ( log purposes).
It is actually possible , but you have to extend the Eloquent builder ,overwrite the update/insert methods and send the event there.
Just been playing around with it... Needs work, but the basic idea is the following :
class Test extends Model
{
protected $guarded = [];
public $dispatchesEvents = [
'saved' => SavedTest::class
];
/**
* INCLUDE this as a trait in your model.
* Overwrite the eloquentBuilder.
*
* #param \Illuminate\Database\Query\Builder $query
* #return \Illuminate\Database\Eloquent\Builder|static
*/
public function newEloquentBuilder($query)
{
return new TestBuilder($query);
}
}
Extend the eloquent builder...
class TestBuilder extends Builder
{
/**
* Update a record in the database and fire event.
*
* #param array $values
* #return int
*/
public function update(array $values)
{
// normal eloquent behavior.
$result =$this->toBase()->update($this->addUpdatedAtColumn($values));
/*
* Fire event.
*/
if($result){
if( $event = Arr::get($this->model->dispatchesEvents,'saved')){
// at the attributes.
$this->model->fill($this->addUpdatedAtColumn($values));
$queryBuilder =$this->toBase();
event(new $event($this->model,$queryBuilder));
}
}
}
public function insert(array $values)
{
// same idea..
}
}
The event class :
class SavedTest
{
use SerializesModels;
public $model;
public $query;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct($model,$query =null)
{
$this->model = $model;
$this->query = $query;
}
}
The listener.
class SavedTestEvent
{
/**
* Create the event listener.
*
*
*/
public function __construct()
{
}
/**
* Handle the event.
*
* #param object $event
* #return void
*/
public function handle($event)
{
// The model , with the attributes.
dump($event->model);
// the query builder , you could extract the wheres or whatever to build your own log for it.
dump($event->query);
}
}
#Paolo on batch request it would not be file the event you must have to perform operation on single record.. like
Analytic::where('id', '>', 100)->get()->each(function($analytic) {
$analytic->delete();
});
I'm attempting to add what I hope is a simple voting module to posts and comments. A "Connection" is a type of post in my application. Users can vote up or down a Connection, or a Comment.
The issue I'm running into is when I attempt to attach a vote to a Connection. I receive this error: Class name must be a valid object or a string.
Here's the line of code in question:
$voteToCast = $vote->voteable()->associate($voteable);
I am certain the $voteable var is an instance of an Ardent/Eloquent model, so I can only presume the error lies within the way I am namespacing my models, or some pathetic typo I am too blind to see. Any help would be greatly appreciated.
Thanks!
Connection Model (type of post):
...
public function votes()
{
return $this->morphMany('Acme\Votes\Vote', 'voteable');
}
And the Votes Model:
/* Votes Model */
namespace Acme\Votes;
use Illuminate\Database\Eloquent\Model;
use LaravelBook\Ardent\Ardent;
class Vote extends Ardent {
protected $table = 'votes';
protected $fillable = [
'value',
'votable_id',
'voteable_type'
];
/**
* Establish the polymorphic relationship
*
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function voteable()
{
return $this->morphTo();
}
public function users()
{
return $this->belongsTo('Acme\\Users\\User');
}
/**
* Vote the item up
*
* #param Model $voteable
* #return mixed
*/
public static function up(Model $voteable)
{
return (new static)->cast($voteable, 1);
}
/**
* Vote the item down
*
* #param Model $voteable
* #return mixed
*/
public static function down(Model $voteable)
{
return (new static)->cast($voteable, -1);
}
/**
* Execute the vote
*
* #param Model $voteable
* #param int $value
* #return bool
*/
protected function cast(Model $voteable, $value = 1)
{
if (!$voteable->exists) return false;
$vote = new static;
$vote->value = $value;
$voteToCast = $vote->voteable()->associate($voteable);
$voteToCast->save();
}
/**
* Restrict the votes so the absolute value is 1
*
* #param $value
*/
public function setValueAttribute($value)
{
$this->attributes['value'] = ($value == -1) ? -1 : 1;
}
}
Votes Controller:
...
public function cast($connection)
{
$voteable = Connection::findOrFail($connection);
if (Input::get('value' < 1)){
return Vote::down($voteable);
}
return Vote::up($voteable);
}
After some more troubleshooting, this appears to be an issue with the way Ardent handles relationships. I was able to use Eloquent on my Vote model instead of Ardent and the voting mechanism now works flawlessly.
I have a event preFlush:
/**
* On flush event
*
* #param PreFlushEventArgs $event
*/
public function preFlush(PreFlushEventArgs $event)
{
$em = $event->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if ($entity instanceof Answer) {
// Set last answered
$question = $entity->getQuestion();
$question->setChanged(new \DateTime('now'));
$uow->persist($question);
}
}
}
I want check set questions to uow.
Try create mock UnitOfWork, method - persist, but this is not work, because answer object not set to "UOW:entityInsertions".
I'm trying to get some simple CRUD done with doctrine 2 but when it's time to update a record with one property set as an array collection I don't seem to get removeElement() to work as it's supposed to. I even tried doing it in this ridiculously ugly way:
foreach($entity->getCountries() as $c) {
$entity->getCountries()->removeElement($c);
$this->em->persist($entity);
$this->em->flush();
}
and it didn't work... Anyone knows how to handle this? I've asked for a solution to this in many different forms and haven't got a good response so far... seems there's lack of good examples of Doctrine 2 CRUD handling. I'll post more code at request.
Edit
//in user entity
/**
*
* #param \Doctring\Common\Collections\Collection $property
* #OneToMany(targetEntity="Countries",mappedBy="user", cascade={"persist", "remove"})
*/
private $countries;
//in countries entity
/**
*
* #var User
* #ManyToOne(targetEntity="User", inversedBy="id")
* #JoinColumns({
* #JoinColumn(name="user_id", referencedColumnName="id")
* })
*/
private $user;
I do something similar in a project with Events which have participants not unlike your User/Country relationship. I will just lay out the process and you can see if there's anything you are doing differently.
On the Participant entity
/**
* #ManyToOne(targetEntity="Event", inversedBy="participants", fetch="LAZY")
* #JoinColumn(name="event_id", referencedColumnName="id", nullable="TRUE")
* #var Event
*/
protected $event;
On the Event entity:
/**
* #OneToMany(targetEntity="Participant", mappedBy="event")
* #var \Doctrine\Common\Collections\ArrayCollection
*/
protected $participants;
Also in Event#__constructor I initialize like this:
$this->participants = new \Doctrine\Common\Collections\ArrayCollection();
Here is how I update an event:
public function update(Event $event, Event $changes)
{
// Remove participants
$removed = array();
foreach($event->participants as $participant)
{
if(!$changes->isAttending($participant->person))
{
$removed[] = $participant;
}
}
foreach($removed as $participant)
{
$event->removeParticipant($participant);
$this->em->remove($participant);
}
// Add new participants
foreach($changes->participants as $participant)
{
if(!$event->isAttending($participant->person))
{
$event->addParticipant($participant);
$this->em->perist($participant);
}
}
$event->copyFrom($changes);
$event->setUpdated();
$this->em->flush();
}
The methods on the Event entity are:
public function removeParticipant(Participant $participant)
{
$this->participants->removeElement($participant);
$participant->unsetEvent();
}
public function addParticipant(Participant $participant)
{
$participant->setEvent($this);
$this->participants[] = $participant;
}
The methods on the Participant entity are:
public function setEvent(Event $event)
{
$this->event = $event;
}
public function unsetEvent()
{
$this->event = null;
}
UPDATE: isAttending method
/**
* Checks if the given person is a
* participant of the event
*
* #param Person $person
* #return boolean
*/
public function isAttending(Person $person)
{
foreach($this->participants as $participant)
{
if($participant->person->id == $person->id)
return true;
}
return false;
}
New answer
In your countries entity, should you not have:
#ManyToOne(targetEntity="User", inversedBy="countries")
instead of inversedBy="id"?
Initial answer
You need to set the countries field in your entity as remove cascade. For example, on a bidirectional one to many relationship:
class Entity
{
/**
*
* #OneToMany(targetEntity="Country", mappedBy="entity", cascade={"remove"})
*/
private $countries;
}
This way, when saving your entity, doctrine will also save changes in collections attached to your entity (such as countries). Otherwise you have to explicitly remove the countries you want to remove before flushing, e.g.
$this->em()->remove($aCountry);
This is also valid for persist, merge and detach operations. More information here.