What to store in a notification? - laravel

I'm currently utilizing Laravels notification package a bit more, but the following bothers me for weeks now: What should I really store in notifications?
Sometimes notifications are related to specific models, but not always.
Examples: Your blog post was published. or An error occurred while doing something. The entry was deleted.
Sometimes these models have relationships like Post → Category and the message should look like: Your blog post in the category "A Category" was published.
Now the questions:
Should I save a related model completely (eg. Category)? This would make accessing it later easier, but it's also a source for inconsistency. Or should I simply save the category ID? Only saving the ID means that I can reference the current data, but what happens if the category gets deleted? Then the notification cannot be rendered. Also I would need to also query the related models for this notification everytime.
Should I save the full message or only the data and compose the message on the client? (App, SPA Web-Frontend...). What about localization then?
What is a best practice for future scaling and also for extending existing notifications in the future?

So you propose to either go for:
1. Save notifications including all data required to display it
OR
2. Save notifications with just references so it can render message later on
So let's consider the advantages and drawbacks of both options.
Option 1: saving including all data
If a related model is deleted, the notification message can still be rendered as before (as you mentioned)
If a related model is changed (e.g. category title is changed), the notification message does not change
If you want to change a notification later on to include additional fields from related models, you won't have those fields available
Option 2: saving including just references
If a related model is deleted, the notification can not be rendered (as you mentioned). I would however argue that the notification wouldn't make much sense in this case.
If a related model is changed (e.g. category title is changed), the notifciation message changes with it
If you want to change a notification later on to include additional fields from related models, you will have those fields available
Additionaly if you were to serialize the notifications in the database you won't be able to deserialize them if you changed the model for it later on (e.g. a field is deleted).
Implementation of option 2
In order to go for option 2 additional database load can't really be avoided.
Easy way
The easiest way would be to resolve the relationships in the notification would be to query the relationships during the rendering of the notifications array, this however will cause the system to an additional query for each relationship.
NotificationController.php
$user = App\User::find(1);
foreach ($user->notifications as $notification) {
echo $notification->type;
}
MyNotification.php
public function toArray($notifiable)
{
$someRelatedModel = Model::find($this->someRelatedModel_id);
return [
'invoice_id' => $this->invoice->id,
'amount' => $this->invoice->amount,
'relatedModelData' => $someRelatedModel->data,
];
}
Nicer way
The better solution would be to adjust the query currently used for retrieving the notifications so it will include the relationships on the initial load.
NotificationController.php
$notification = App\Notification::byUserId(1)->with('someRelatedModel);
See eager loading for more on this.
Tl;dr Considering the points above I'd go with option 2; only keep references to models you'll need when rendering the notification.

Related

Assert model was not made searchable

I'm building a system to manage some articles for my company using Laravel and Laravel Scout with Algolia as the search backend.
One of the requirements states that whenever something in an article is changed, a backup is kept so we can prove that a certain information was displayed at a specific time.
I've implemented that by cloning the existing article with all its relationships before updating it. Here is the method on the Article model:
public function clone(array $relations = null, array $except = null) {
if($relations) {
$this->load($relations);
}
$replica = $this->replicate($except);
$replica->save();
$syncRelations = collect($this->relations)->only($relations);
foreach($syncRelations as $relation => $models) {
$replica->{$relation}()->sync($models);
}
return $replica;
}
The problem is the $replica->save() line. I need to save the model first, in order for it to have an ID when syncing the relationships.
But: The only thing preventing scout from indexing the model is if the model has its archived_at field set to any non-null value. But since this is a clone of the original model, this field is set to null as expected, and is only changed after the cloning procedure is done.
The problem: Scout is syncing the cloned model to Algolia, so I have duplicates there. I know how to solve this, by wrapping the clone call into the withoutSyncingToSearch (https://laravel.com/docs/5.6/scout#pausing-indexing) callback.
But since this is rather important and the bug is already out there, I want to have a unit test backing me up that it was indeed not synced to Algolia.
I don't have any idea how to test this though and searching for a way to test Scout only leads to answers that tell me not to test Scout, but rather that my model can be indexed etc.
The question: How do I create a Unittest that proves that the cloned model wasn't synced to Algolia?
At the moment I'm thinking about creating a custom Scout driver for testing, but it seems to be a total overkill for testing one single function.

Yii relations result non-existing items (cache issue?)

I have set relations: 'teamDrivers' => array(self::HAS_MANY, 'TeamDriver', 'team_id') in my Team model
So if I want I can: print_r($this->teamDrivers); in my Team. Just for demonstration.
Now the problem is that this kind of code produces a list of items that have already been removed form database! Via CActiveDataProvider with CDbCriteria those items are not reached.
If I log out from my app and then log back in everything seems to be working.
So is there some cache that takes care of those relations or what is this mystery? And how do I clear that cache?
In my case UserIdentity object had old information and team was got through it. Refreshing was the solutions.

How to show feedback/error messages in a backbone application

I'm working on a simple CRUD proof of concept with Rails/Backbone/JST templating. I've been able to find a lot of examples up to this point. But after much searching and reading, I've yet to find a good example of how to handle these scenarios:
info message: new item successfully added to list (shown on list screen)
info message: item successfully deleted from list
error message: problem with field(s) entry
field level error message: problem with entry
The Backbone objects are:
Collection (of "post" Models) -> Model ("post" object) -> List/Edit/New Views (and a JST template for each of these views)
So, I'm looking for a high level description of how I should organize my code and templates to achieve the level of messaging desired. I already have a handle on how to perform my validation routine on the form inputs whenever they change. But not sure what do with the error messages now that I have them.
Here is the approach I'm considering. Not sure if it's a good one:
Create a "Message" Model, which maps to a "View", which is a sub-view (if that's possible) on my existing views. This view/model can display page level messages and errors in the first three scenarios I mention above. Not sure if it's feasible to have a "sub-view" and how to handle the templating for that. But if it's possible, the parent templates could include the "message" sub-template. The message view could show/hide the sub-template based on the state of the message model. Feasible? Stupid?
For the fourth scenario, the model validation will return an error object with specific messages per each erroneous field each time a "model.set" is called by form field changes. I don't want to interrupt the "model.set" but I do want to display the error message(s) next to each field. I want to know how to factor my edit/new template and Post model/view in such a way that I don't violate the MVC pattern. I.e. I don't want to put references to DOM elements in the wrong plage.
Sorry if this is vague. If you're inclined to help, let me know what code snippets could be helpful (or other details) and I'll provide them.
You create a global eventbus. When ever an error appears trigger an event. Your view that should show the message listen to the events on this eventbus. Doing so, your error message view dont needs to know all of your collection and vice versa. The eventbus is simple:
var eventBus = _.extend({}, Backbone.Events);
Add it to your collection and trigger it when ever add was called:
var myCollection = Backbone.Collection.extend({
initialize: function([],eventbus){
this.bind('add', function(obj){eventbus.trigger('added', obj)}
}
})
Take also a look at the article: http://lostechies.com/derickbailey/2011/07/19/references-routing-and-the-event-aggregator-coordinating-views-in-backbone-js/

MVC Putting an action in the most appropriate correct controller

I was just wondering what the best practice approach is for deciding where to create an action/view in certain situations.
If User hasMany Video
where is the best place to create the action/view to show user videos?
So within the Users account page 'My Videos' link do you
just create a users/my_videos action and view.
create videos/my_videos action and view.
or as is most likely you would already have a Controller/Action of videos/index which would have search functionality. Simply use this passing in a user id.
Any thoughts/advice greatly appreciated
Thanks
Leo
One potential option is to do the following:
Since the videos likely have much more code around them than a simple which user has which videos lookup the video list action should be in the VideosController.
In past projects I have (in CakePHP 1.3) used prefix routing to address some of this.
In config/core.php make sure you enable routing.prefixes to include a 'user' prefix.
<?php
... in routes.php ...
Routing.prefixes = array( 'user' );
?>
In the videos controller make an action with the following signature:
<?php
...
public function user_index( $userID = null ){
...
}
?>
and in the views where you link to the list of users videos the html::link call should look similar to the following:
<?php
...
echo $this->Html->link( 'User\'s Videos', array(
'controller' => 'videos',
'action' => 'index',
'prefix' => 'user',
$this->Session->read( 'Auth.User.id' )
));
?>
Of course this assumes you are using the Auth component here to track the logged in user. The Session helper code to read the authenticated user id might need tweaking.
This lets you a) Not worry too much about routing aside from enabling prefix routing and b) will quickly let you have pretty links like so -- site.com/user/videos/index/419
Couple this with some Slug love ( this is the best link for this I have seen - no slug field required on the db layer - http://42pixels.com/blog/slugs-ugly-bugs-pretty-urls )
You could even end up with urls like so quite easily: site.com/user/videos/index/eben-roux
and with just a tiny bit of editing to app/config/routes.php you could eliminate the /index/ portion and the results would be SEO friendly and user friendly in the format:
site.com/user/videos/eben-roux
http://book.cakephp.org/view/945/Routes-Configuration
As always with code you have the two extremes of:
1) Putting everything in a single controller
2) Having every action in a separate controller
The ideal approach will nearly always be somewhere between the two so how to decide what is grouped together and what is separated?
In MVC I tend to look at the Views and see what the commonalities are: as you point out Users have a ref to a collection of Videos in the Model, but would you want both sets of Data in any single View? i.e. In this example is it likely that you would be on a page that both managed user details, and displayed the list of vids? If not then I'd suggest separate controllers.
If either controller would then be extremely simple - e.g. one method, then may be worth considering merging the two.
I like to keeps things separate.
What I'd do is an index action in videos controller, passing user's id as argument and then displaying only current users video.
public function index($id = null){
$this->paginate = array( 'conditions'=> array('Video.user_id' => $id));
$this->set('videos', $this->paginate());
}
My take is that it depends on the responsibility you assign to the controllers.
I would say that something like a User or a Video controller should be concerned with only those entities.
You may want to consider something like a UserDashboard (or something similar but appropriately named) as alluded to by Dunhamzzz in the comments. This can aggegate all the functionality from an "entry" point-of-view. The same way a banner / shortcut / action menu would work.
Your UserDashboard would use whatever data layer / repository is required to get the relevant data (such as the IVideoRepository or IVideoQuery implementation).
Usually when something doesn't feel right it isn't. Try splitting it out and see how it works. You can alsways re-arrange / refactor again later.
Just a thought.
I don't think there's a 'one-rule-fits-all' solution to this question, but I would try to take an approach in which you would determine what the main object is that you're dealing with, and adding the action/view to that object's controller.
In your example I'd say that your main object is a video and that the action you're requiring is a list of video's filtered by a specific property (in this case the user's id, but this could very well be a category, a location, etc.).
One thing I would not do is let your desired URL determine in which controller you put your functionality. URLs are trivially changed with routes.

CakePHP, organize site structure around groups

So, I'm not quite sure how I should structure this in CakePHP to work correctly in the proper MVC form.
Let's, for argument sake, say I have the following data structure which are related in various ways:
Team
Task
Equipment
This is generally how sites are and is quite easy to structure and make in Cake. For example, I would have the a model, controller and view for each item set.
My problem (and I'm sure countless others have had it and already solved it) is that I have a level above the item sets. So, for example:
Department
Team
Task
Equipment
Department
Team
Task
Equipment
Department
Team
Task
Equipment
In my site, I need the ability for someone to view the site at an individual group level as well as move to view it all together (ie, ignore the groups).
So, I have models, views and controls for Depart, Team, Task and Equipment.
How do I structure my site so that from the Department view, someone can select a Department then move around the site to the different views for Team/Task/Equipment showing only those that belong to that particular Department.
In this same format, is there a way to also move around ignoring the department associations?
Hopefully the following example URLs clarifies anything that was unclear:
// View items while disregarding which group-set record they belong to
http://www.example.com/Team/action/id
http://www.example.com/Task/action/id
http://www.example.com/Equipment/action/id
http://www.example.com/Departments
// View items as if only those associated with the selected group-set record exist
http://www.example.com/Department/HR/Team/action/id
http://www.example.com/Department/HR/Task/action/id
http://www.example.com/Department/HR/Equipment/action/id
Can I get the controllers to function in this manner? Is there someone to read so I can figure this out?
Thanks to those that read all this :)
I think I know what you're trying to do. Correct me if I'm wrong:
I built a project manager for myself in which I wanted the URLs to be more logical, so instead of using something like
http://domain.com/project/milestones/add/MyProjectName I could use
http://domain.com/project/MyProjectName/milestones/add
I added a custom route to the end (!important) of my routes so that it catches anything that's not already a route and treats it as a "variable route".
Router::connect('/project/:project/:controller/:action/*', array(), array('project' => '[a-zA-Z0-9\-]+'));
Whatever route you put means that you can't already (or ever) have a controller by that name, for that reason I consider it a good practice to use a singular word instead of a plural. (I have a Projects Controller, so I use "project" to avoid conflicting with it.)
Now, to access the :project parameter anywhere in my app, I use this function in my AppController:
function __currentProject(){
// Finding the current Project's Info
if(isset($this->params['project'])){
App::import('Model', 'Project');
$projectNames = new Project;
$projectNames->contain();
$projectInfo = $projectNames->find('first', array('conditions' => array('Project.slug' => $this->params['project'])));
$project_id = $projectInfo['Project']['id'];
$this->set('project_name_for_layout', $projectInfo['Project']['name']);
return $project_id;
}
}
And I utilize it in my other controllers:
function overview(){
$this->layout = 'project';
// Getting currentProject id from App Controller
$project_id = parent::__currentProject();
// Finding out what time it is and performing queries based on time.
$nowStamp = time();
$nowDate = date('Y-m-d H:i:s' , $nowStamp);
$twoWeeksFromNow = $nowDate + 1209600;
$lateMilestones = $this->Project->Milestone->find('all', array('conditions'=>array('Milestone.project_id' => $project_id, 'Milestone.complete'=> 0, 'Milestone.duedate <'=> $nowDate)));
$this->set(compact('lateMilestones'));
$currentProject = $this->Project->find('all', array('conditions'=>array('Project.slug' => $this->params['project'])));
$this->set(compact('currentProject'));
}
For your project you can try using a route like this at the end of your routes.php file:
Router::connect('/:groupname/:controller/:action/*', array(), array('groupname' => '[a-zA-Z0-9\-]+'));
// Notice I removed "/project" from the beginning. If you put the :groupname first, as I've done in the last example, then you only have one option for these custom url routes.
Then modify the other code to your needs.
If this is a public site, you may want to consider using named variables. This will allow you to define the group on the URL still, but without additional functionality requirements.
http://example.com/team/group:hr
http://example.com/team/action/group:hr/other:var
It may require custom routes too... but it should do the job.
http://book.cakephp.org/view/541/Named-parameters
http://book.cakephp.org/view/542/Defining-Routes
SESSIONS
Since web is stateless, you will need to use sessions (or cookies). The question you will need to ask yourself is how to reflect the selection (or not) of a specific department. It could be as simple as putting a drop down selection in the upper right that reflects ALL, HR, Sales, etc. When the drop down changes, it will set (or clear) the Group session variable.
As for the functionality in the controllers, you just check for the Session. If it is there, you limit the data by the select group. So you would use the same URLs, but the controller or model would manage how the data gets displayed.
// for all functionality use:
http://www.example.com/Team/action/id
http://www.example.com/Task/action/id
http://www.example.com/Equipment/action/id
You don't change the URL to accommodate for the functionality. That would be like using a different URL for every USER wanting to see their ADDRESS, PHONE NUMBER, or BILLING INFO. Where USER would be the group and ADDRESS, PHONE NUMBER< and BILLING INFO would be the item sets.
WITHOUT SESSIONS
The other option would be to put the Group filter on each page. So for example on Team/index view you would have a group drop down to filter the data. It would accomplish the same thing without having to set and clear session variables.
The conclusion is and the key thing to remember is that the functionality does not change nor does the URLs. The only thing that changes is that you will be working with filtered data sets.
Does that make sense?

Resources