I am using an Observer to watch if a user was updated.
Whenever a user is updated I would like to check if his email has been changed.
Is something like this possible?
class UserObserver
{
/**
* Listen to the User created event.
*
* #param \App\User $user
* #return void
*/
public function updating(User $user)
{
// if($user->hasChangedEmailInThisUpdate()) ?
}
}
Edit: Credits to https://stackoverflow.com/a/54307753/2311074 for getOriginal
As tadman already said in the comments, the method isDirty does the trick:
class UserObserver
{
/**
* Listen to the User updating event.
*
* #param \App\User $user
* #return void
*/
public function updating(User $user)
{
if($user->isDirty('email')){
// email has changed
$new_email = $user->email;
$old_email = $user->getOriginal('email');
}
}
}
If you want to know the difference between isDirty and wasChanged, see https://stackoverflow.com/a/49350664/2311074
You don't have to get the user again from the database. The following should work:
public function updating(User $user)
{
if($user->isDirty('email')){
// email has changed
$new_email = $user->email;
$old_email = $user->getOriginal('email');
}
}
A little late to the party, but might be helpful for future devs seeking a similar solution.
I recommend using the package Laravel Attribute Observer, as an alternative to polluting your Service Providers or filling your Observers with isDirty() and wasChanged() boilerplate.
So your use case would look like this:
class UserObserver
{
/**
* Handle changes to the "email" field of User on "updating" events.
*
* #param \App\User $user
* #param string $newValue The current value of the field
* #param string $oldValue The previous value of the field
* #return void
*/
public function onEmailUpdating(User $user, string $newValue, string $oldValue)
{
// Your logic goes here...
}
}
Laravel Attribute Observer is especially useful when you have a lot of attributes to observe on one or more models.
Disclaimer: I am the author of Laravel Attribute Observer. If it saves you some development time, consider buying me a coffee.
Instead of using the updating event, You should use the updated model event to compare the column changes against the original. Please have a look at the below code.
AppServiceProvider.php
<?php
namespace App\Providers;
use App\Models\User;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
User::updated(function($user){
$changes = array_diff($user->getOriginal(), $user->getAttributes());
if(array_key_exists('email',$changes)){
/* do your things. */
}
});
}
}
Explanation:
Once you get the difference then you need to use array_key_exists to check whether the email field got updated or not.
Related
I'm trying to send a notification with a mention of a user in a general channel. This is what I have:
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
class WeeklyTasksResponsible extends Notification
{
use Queueable;
protected $employee;
/**
* Create a new notification instance.
*
* #return void
*/
public function __construct(\App\Employee $employee)
{
$this->employee = $employee;
}
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return ['slack'];
}
/**
* Get the Slack representation of the notification.
*
* #param mixed $notifiable
* #return SlackMessage
*/
public function toSlack($notifiable)
{
return (new SlackMessage)
->content('Reponsible for this week is: ' . $this->employee->slack_name);
}
}
This will sent a weekly notification in the general slack channel of our company. The message is "Responsible for this week is: nameofuser". The problem is the user doesn't see a notification of this.
I've also tried do this:
public function toSlack($notifiable)
{
return (new SlackMessage)
->content('Reponsible for this week is: #' . $this->employee->slack_name);
}
But it isn't the same as mentioning someone myself in the channel.
How can I do this?
As mentioned by #HCK you can enable matching of usernames for #username mentions in chat.postMessages by setting the optional parameter link_names to true.
However, creating mentions with usernames is deprecated and should no longer be used.
The recommended approach is to create mentions with the user ID, which will work by default.
Example:
<#U12345678>
See the post A lingering farewell to the username from Slack for details about username deprecation.
I just found the following in the Laravel 6.18.0 source code:
/**
* Find and link channel names and usernames.
*
* #return $this
*/
public function linkNames()
{
$this->linkNames = 1;
return $this;
}
vendor/laravel/slack-notification-channel/src/Messages/SlackMessage.php
So you can use it like this:
public function toSlack($notifiable)
{
return (new SlackMessage)
->linkNames()
...
}
As #erik-kalkoken pointed out, use the Slack user ID, enclosed by <> and with the # sign. You can find it in your profile in the Slack App:
Actually, I tried that way and it's works pretty well.
$content .= "New order!\r\n #person";
return (new SlackMessage)
->content($content)->linkNames();
You would need to add the # before name of person or team and in order for slack to recognize them as mentions not just text, you would need to chain the content with linkNames
I am building a small app in Laravel, but I needed to use some help. I read similar tasks but I don't see them related to what I have.
The app that I'm building is a dictionary and I really don't get the part where words are displayed by id in the URL. I tried to revert the logic for the curds (store, update, delete, show) so that they perform actions using the word name and not the word id. That worked. The redirecting to route app/{name} after the actions does work too.
Now, I presume this is not how it needs to be done, when the curds aren't taking actions on the id?
I also read about slugs, but since I'm building a dictionary, don't understand why I need to separate the word by a "-"? It's a single word, not like posts. And why do I need to have an extra field in my form for the slug, as the slug must be equal to the name? I'm getting here an error: the slug->unique() field must be not null.
If somebody has the time, please explain to me what's the best way for making the names of the words being displayed in the URL. Thank you, I appreciate it.
Edit
Here is my code for the update crud:
public function update(UpdateWordRequest $request, $name)
{
Word::query()->where('name', $name)->firstOrFail()->update($request->input());
return redirect()->route('dictionary.show', ['name' => $name])->with('success', 'Edited successfully!');
}
It's working fine, but when I re-edit the word and change it to word2, with a number, I then get this error: Trying to get property 'name' of non-object.
What do I need to make of it? Thank you :)!
I think you should be able to use a trait to fix this issue.
I would create a trait call hasSlug and have the following code:
<?php
namespace App\Traits;
trait HasSlug
{
protected static function bootHasSlug()
{
static::creating(function ($model) {
$columnName = static::getSlugColumn();
$model->$columnName = str_slug($model->name); //convert your name to slug
});
}
public function getSlugAttribute()
{
$columnName = static::getSlugColumn();
return (string) $this->attributes[$columnName];
}
protected static function getSlugColumn()
{
if (isset(static::$slugColumn)) {
return static::$slugColumn;
}
return 'slug';
}
}
So now if you use HasSlug trait to your model every time you create a record it will go and look for name field from your model and create a slug out of it.
Be sure to apply unique constrain to your name field if needed.
For more robust have a look at spatie slug package https://github.com/spatie/laravel-sluggable
Here is what I finally come up with.
Word.php
I used as #usrNotFound suggested spatie/laravel-sluggable.
<?php
namespace App;
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
class Word extends Model
{
use HasSlug;
/**
* #return SlugOptions
*/
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->slugsShouldBeNoLongerThan(20);
}
/**
* #param $query
* #param $slug
* #return mixed
*/
public function scopeFindBySlug($query, $slug)
{
return $query->where('slug', $slug)
->get();
}
}
WordController.php
<?php
namespace App\Http\Controllers;
use App\Word;
use App\Http\Requests\CreateWordRequest;
use App\Http\Requests\UpdateWordRequest;
class WordController extends Controller
{
/**
* Store a newly created resource in storage.
* #param CreateWordRequest $request
* #return mixed
*/
public function store(CreateWordRequest $request)
{
$word = Word::query()
->create($request->input());
return redirect()
->route('dictionary.show', $word->slug)
->with('success', 'Created successfully!');
}
/**
* Display the specified resource.
* #param $slug
* #return mixed
*/
public function show($slug)
{
$word = Word::whereSlug($slug)
->firstOrFail();
return view('word.show')
->with('word', $word);
}
/**
* Update the specified resource in storage.
* #param UpdateWordRequest $request
* #param $slug
* #return mixed
*/
public function update(UpdateWordRequest $request, $slug)
{
Word::whereSlug($slug)
->firstOrFail()
->update($request->input());
return redirect()
->route('dictionary.show', $slug)
->with('success', 'Edited successfylly!');
}
/**
* Remove the specified resource from storage.
* #param $slug
* #return mixed
*/
public function destroy($slug)
{
Word::whereSlug($slug)
->delete();
return redirect()
->route('dictionary.index')
->with('success', 'Deleted successfully');
}
}
Route.php
Route::get('/dictionary/{slug}', 'WordController#show')
->name('dictionary.show')
->where('slug', '[-A-Za-z0-9_-]+');
Creating, updating, destroying new words works fine. Eventually there is a small problem. When I want to update a particular word and change the name of it, after saving the word, I'm getting
Sorry, the page you are looking for could not be found.
and if I look at the url I see the old name instead of the new one. Has this to do with my update method and how do I fix it?
My use case is that an user is/owns a company, which has employees.
Using form controllers along with model policies i am trying to figure out what the best/proper way to do it should be.
routes:
Route::resource('company', \App\Http\Controllers\Api\v1\CompanyController::class);
Route::resource('employee', \App\Http\Controllers\Api\v1\EmployeeController::class);
employee store request:
namespace App\Http\Requests;
use App\Models\Employee;
use Illuminate\Foundation\Http\FormRequest;
class EmployeeStoreRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return $this->user()->can('create', Employee::class);
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'company_id' => 'required|integer|exists:companies,id'
];
}
}
employee policy:
...
/**
* Determine whether the user can create employees.
*
* #param \App\User $user
* #return mixed
*/
public function create(User $user)
{
return $user->can('update', Company::find(
app('request')->get('company_id')
));
}
...
So i am not particularly happy in how the policy checks if the user can edit the company the employee will belong to, since this only happens on http, for console/tests this will break.
Then the most logical way to add this check is in the form request's authorize() function, but then you are checking permissions outside the policies, which sounds illogical.
So in short, the question: how & why would you do this using form requests & model policies?
You only need to add the id of the company to the EmployeePolicy#create method, and you will be able to use it outside http
EmployeePolicy
public function create(User $user, $companyId)
{
return $user->can('update', $companyId);
}
EmployeeStoreRequest
public function authorize()
{
return $this->user()->can('create', Employee::class, $this->request->get('company_id'));
}
You can test it outside http with tinker
php artisan tinker
$user = User::find(2); // or whatever user you want to test with
$user->can('create', Employee::class, 3); // 3 = company_id
So i am sending an email when a certain value on an entity is changed. I only want the email to send after the update in case the update fails for what ever reason. so on the preUpdate I can do this
public function preUpdate(LifecycleEventArgs $args){
if ($args->hasChangedField('value') && is_null($args->getOldValue('value'))) {
$this->sendEmail();
}
}
but i need to do this on postUpdate and as these methods are not available on postUpdate i refactored it to look like this:
public function postUpdate(LifecycleEventArgs $args){
$entity = $args->getEntity();
$changeSet = $args->getEntityManager()->getUnitOfWork()->getEntityChangeSet($entity);
if ($entity instanceof Entity && isset( $changeSet['value'] ) && empty( $changeSet['value'][0] )) {
$this->sendEmail();
}
}
However this returns an empty change set, but changes have been made and can be seen in preUpdate. Can anyone see what i am doing wrong? help would be much appreciated :)
On preUpdate event you get event object of class PreUpdateEventArgs where You have change set for entity.
On postUpdate you just get event object of class LifecycleEventArgs where you can ask only for Updated entity (and get latest state of it).
If you want to play with changeset then you need to do it before actual updating entity (preUpdate event).
A workaround could be to save change set somewhere by yourself and later retrieve it in postUpdate. It is a siplified exaple I've implement once:
<?php
namespace Awesome\AppBundle\EventListener;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
/**
* Store last entity change set in memory, so that it could be
* usable in postUpdate event.
*/
class EntityChangeSetStorageListener implements EventSubscriber
{
/**
* #var ArrayCache
*/
private $cache;
/**
* #param ArrayCache $cacheStorage
*/
public function __construct(ArrayCache $cacheStorage)
{
$this->cache = $cacheStorage;
}
/**
* Store last entity change set in memory.
*
* #param PreUpdateEventArgs $event
*/
public function preUpdate(PreUpdateEventArgs $event)
{
$entity = $event->getEntity();
$this->cache->setNamespace(get_class($entity));
$this->cache->save($entity->getId(), $event->getEntityChangeSet());
}
/**
* Release the memory.
*/
public function onClear()
{
$this->clearCache();
}
/**
* Clear cache.
*/
private function clearCache()
{
$this->cache->flushAll();
}
/**
* {#inheritdoc}
*/
public function getSubscribedEvents()
{
return [
Events::preUpdate,
Events::onClear,
];
}
}
Later inject ChangeSetStorage service to the listener where it is necessary on postUpdate event.
I had a really annoying issue with the changeset data, sometimes I got the collection of changes and sometimes not.
I sorted out by adding this line $event->getEntityManager()->refresh($entity); in the prePersist and preUpdate events inside a doctrine.event_subscriber
After the refresh line, changesetdata was updated so the following line started to work:
/** #var array $changeSet */
$changeSet = $this->em->getUnitOfWork()->getEntityChangeSet($entity);
Long story short: I'm building a "privacy" page where uses can chose what shows up and what does not show up on their profiles.
I am considering having a 1:m table user:privacy and just have entries for the keys they want private. If they don't exist they are public. Hope this makes sense.
Table would be user_privacy and will have 3 columns: id, user_id, privacy_key (string, i.e. email/phone/cell/etc)
Is there a way to simple query by the keys i will define that i can run to determine if the user has a key or not or do i have to go extra lengths to add a function to the user model to do this (trying to avoid, love the magic-ness of eloquent)
Basically i want to have a condition that sounds like "if ($user->privacy->email or $user->privacy->phone)"
Thanks and hope i was clear enough, lol
You could add a function to your user model:
public function isPrivate($attribute){
$privacyAttribute = $this->privacy->first(function($model) use ($attribute){
return $model->key == $attribute; // key being the column in the privacy model
});
return !is_null($privacyAttribute);
}
And then do your if statement this way:
if ($user->isPrivate('email') or $user->isPrivate('phone'))
Or a different implementation (usage is the same)
private $privacyAttributes = null;
public function isPrivate($attribute){
if($this->privacyAttributes == null){
$this->privacyAttributes = $this->privacy()->lists('key');
}
return in_array($attribute, $this->privacyAttributes);
}
User Model header:
/**
* Class User
* #package Interallmas
*/
class User extends Model implements AuthenticatableContract, CanResetPasswordContract {
/**
* #var null|array
*/
protected $privacy_keys = NULL;
Privacy Relationship:
/**
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function privacy() {
return $this->hasMany('Interallmas\Privacy');
}
Privacy functions:
/**
* #return bool
*/
public function privacy_initialized() {
return ($this->privacy_keys !== NULL);
}
/**
* #return void
*/
public function initialize_privacy() {
if (!$this->privacy_initialized()) {
$this->privacy_keys = [];
foreach ($this->privacy as $privacy) {
$this->privacy_keys[] = $privacy->privacy_key;
}
}
}
/**
* #param $key
* #return bool
*/
public function isPrivate($key) {
$this->initialize_privacy();
return (in_array($key,$this->privacy_keys));
}
So: Whenever i access the isPrivate($key) method, i cache the result for the next use so i don't hit the server too hard - the function may be accessed once or more - i just query once, the first time. I believe for my needs, this is the best way to do it.
I think a simple count > 0 check should suffice. This requires you to have defined the relationship with the hasMany method for the User Model.
if (count($user->privacy) > 0) {
...
}