Retrieve model from notification - php

With Laravel, I can create database notifications.
Assuming, my notifications are about Resources (a model named Resource), I can give a resource instance to the notification constructor:
public function __construct(Resource $resource)
{
$this->resource = $resource;
}
// ...
public function toArray($notifiable)
{
return [
'resource_id' => $this->resource->id
];
}
I would like to load notifications for a user and display some resources information. I can get all notifications and do something like this:
foreach ($user->notifications as $notification) {
$resource_id = $notification->resource_id;
$resource = Resource::find($resource_id);
// Do something with resource information...
echo $resource->name;
}
But it's slow (one query for each notification) and not à la Laravel. I would prefer something like:
foreach ($user->notifications as $notification) {
// Do something with resource information...
echo $notification->resource->name;
}
Ideally, it would like to have some kind of eager loading. Is there an elegant (or at least working) way to do this?

You can reduce your queries down to one by doing:
Resource::whereIn('id', collect($user->notifications)->pluck('resource_id'))
This will only perform a single SQL query.
Digging deeper into the Laravel database notifications. The Laravel boilerplate notification may be able to work with:
class User extends Model {
// ...
public function resourceNotifications() {
$this->morphToMany(Resource::class, 'notifiable', 'notifications');
}
}
Then retrieving all the corresponding notifications is as simple as:
$notifications = $user->resourceNotifications()->get();
Have not tested that part though.

Related

Design pattern that handles multiple steps

So I have a complicated onboarding process that does several steps. I created a class that handles the process but I've added a few more steps and I'd like to refactor this into something a bit more manageable. I refactored to use Laravel's pipeline, but feel this may not be the best refactor due to the output needing to be modified before each step.
Here is an example before and after with some pseudo code.
before
class OnboardingClass {
public $user;
public $conversation;
public function create($firstName, $lastName, $email){
// Step 1
$user = User::create();
// Step 2
$conversation = Conversation::create(); // store information for new user + existing user
// Step 3
$conversation->messages()->create(); // store a message on the conversation
// Step 4
// Send api request to analytics
// Step 5
// Send api request to other service
return $this;
}
}
after
class OnboardingClass{
public $user;
public $conversation;
public function create($firstName, $lastName, $email){
$data = ['first_name' => $firstName, ...]; // form data
$pipeline = app(Pipeline::Class);
$pipeline->send($data)
->through([
CreateUser::class,
CreateNewUserConversation::class,
AddWelcomeMessageToConversation::class,
...
])->then(function($data){
// set all properties returned from last class in pipeline.
$this->user = $data['user'];
$this->conversation = $data['conversation'];
});
return $this;
}
}
Now within each class I modify the previous data and output a modified version something like this
class CreateUser implements Pipe {
public function handle($data, Closure $next) {
// do some stuff
$user = User::create():
return $next([
'user' => $user,
'other' => 'something else'
]);
}
}
In my controller I am simply calling the create method.
class someController() {
public function store($request){
$onboarding = app(OnboardingClass::class);
$onboarding->create('John', 'Doe', 'john#example.com');
}
}
So the first pipe receives the raw form fields and outputs what the second pipe needs to get the job done in its class, then the next class outputs the data required by the next class, so on and so forth. The data that comes into each pipe is not the same each time and you cannot modify the order.
Feels a bit weird and I'm sure there is a cleaner way to handle this.
Any design pattern I can utilize to clean this up a bit?
I think you could try using Laravel Service Provider, for example, you could build a login service provider; or Event & Listener, for example, you could build an listener for login and triggers a event to handle all the necessary logics. Can't really tell which one is the best since outcome is the same and it makes same amount of network requests, but it's more on personal preferences

Infinite loop while listening model event

I have an Laravel application for Properties, let's say somewhere in my code I do:
$property = new Property();
$property->city = "New York";
...
$property->save();
Then I have Event Listener that listens for specific event:
$events->listen(
'eloquent.saved: Properties\\Models\\Property',
'Google\Listeners\SetGeoLocationInfo#fire'
);
And finally in SetGeoLocationInfo.php I have
public function fire($event)
{
$property = $event;
...
//get GPS data from Google Maps
$property->latitude = $googleMapsObject->latitude;
$property->longitude = $googleMapsObject->longitude;
$property->save();
}
And when I save model in goes to infinite recursion, because of save() evoked in the handler.
How I can change my code to make it fill location data just one time after saving and avoid recursion?
I cannot use flushEventListeners() because in this case other listeners stop working (e.g. property photo attaching).
I hit the same. In Laravel 5.6 you can simply override the finishSave() function in your model:
protected function finishSave(array $options)
{
// this condition allow to control whenever fire the vent or not
if (!isset($options['nosavedevent']) or empty($options['nosavedevent'])) {
$this->fireModelEvent('saved', false);
}
if ($this->isDirty() && ($options['touch'] ?? true)) {
$this->touchOwners();
}
$this->syncOriginal();
}
Then you can use it like this:
public function fire($event)
{
$property = $event;
...
//get GPS data from Google Maps
$property->latitude = $googleMapsObject->latitude;
$property->longitude = $googleMapsObject->longitude;
$property->save(['nosavedevent' => true]);
}
in my case using saveQuietly() instead of save() resolved the issue as the saveQuietly does not trigger any events so you are not stuck in an infinite loop of events.
edit: i think the saveQuietly() method is only available in laravel 8 for now.
In this case probably better would be using saving method. But be aware that during saving you should not use save method any more, so your fire method should look like this:
public function fire($event)
{
$property = $event;
...
//get GPS data from Google Maps
$property->latitude = $googleMapsObject->latitude;
$property->longitude = $googleMapsObject->longitude;
}
Other solution would be adding condition to to set and save GPS location only if it's not set yet:
if (empty($property->latitude) || empty($property->longitude)) {
$property->latitude = $googleMapsObject->latitude;
$property->longitude = $googleMapsObject->longitude;
$property->save();
}
Your Property save method (you must define property constants for it):
public function save($mode = Property::SAVE_DEFAULT)
{
switch ($mode) {
case Property::SAVE_FOO:
// something for foo
break;
case Property::SAVE_BAR:
// something for bar
break;
default:
parent::save();
break;
}
}
Call it:
public function fire($event)
{
$property = $event;
...
//get GPS data from Google Maps
$property->latitude = $googleMapsObject->latitude;
$property->longitude = $googleMapsObject->longitude;
$property->save(Property::SAVE_FOO);
}
or
$property->save(); // as default
What good?
All conditions are in one place (in save method).
You can user forget() to unset an event listener.
Event::listen('a', function(){
Event::forget('a');
echo 'update a ';
event("b");
});
Event::listen('b', function(){
Event::forget('b');
echo 'update b ';
event("a");
});
event("a"); // update a update b
The model event keys are named as "eloquent.{$event}: {$name}" eg "eloquent.updated: Foo"
Inside you're fire method, you could have it call $property->syncOriginal() before applying new attributes and saving.
Model classes have a concept of 'dirty' attributes vs. 'original' ones, as a way of knowing which values have already been pushed to the DB and which ones are still slated for an upcoming save. Typically, Laravel doesn't sync these together until after the Observers have fired. And strictly-speaking, it's an ant-pattern to have your Listener act like it's aware of the context in which it's fired; being that you're triggering it off the saved action and can therefore feel confident that the data has already reached the DB. But since you are, the problem is simply that the Model just doesn't realize that yet. Any subsequent update will confuse the Observer into thinking the original values that got you there are brand new. So by explicitly calling the syncOriginal() method yourself before applying any other changes, you should be able to avoid the recursion.

Is there a way to centrally preprocess all logs created by monolog?

I am currently working on a big application that uses monolog for logging and was asked to hide any sensitive information like passwords.
What I tried to do, was extending monolog so it would automatically replace sensitive information with asterics, but even though the data seems to be altered, in the end the original text gets logged.
use Monolog\Handler\AbstractProcessingHandler;
class FilterOutputHandler extends AbstractProcessingHandler
{
private $filteredFields = [];
public function __construct(array $filteredFields = [], $level = Monolog\Logger::DEBUG, $bubble = true)
{
$this->filteredFields = array_merge($filteredFields, $this->filteredFields);
parent::__construct($level, $bubble);
}
protected function write(array $record)
{
foreach($record['context'] as $key=>$value){
if(in_array($key, $this->filteredFields)){
$record['context'][$key] = '*****';
}
}
return $record;
}
}
And when I initialize my logger I do this:
$logger->pushHandler(new FilterOutputHandler(['username', 'password']));
$logger->debug('Sensitive data incoming', ['username'=> 'Oh noes!', 'password'=> 'You shouldn\'t be able to see me!']);
I also tried overridding the handle and processRecord methods of the AbstractProcessingHandler interface but in vain. Can this be done in monolog?
Looks like I was trying the wrong thing.
Instead of adding a new handler to my logger, I had to add a new processor by using the pushProcessor(callable) method.
So, in my specific use case, I can add filters to my context like this:
function AddLoggerFilteringFor(array $filters){
return function ($record) use($filters){
foreach($filters as $filter){
if(isset($record['context'][$filter])){
$record['context'][$filter] = '**HIDDEN FROM LOG**';
}
}
return $record;
};
}
And later I can add filters simply by
(init)
$logger->pushProcessor(AddLoggerFilteringFor(['username', 'password']));
...
(several function definition and business logic later)
$logger->debug('Some weird thing happened, better log it', ['username'=> 'Oh noes!', 'password'=> 'You shouldn\'t be able to see me!']);

How to test a class which depends on an Eloquent model with relationships?

What is the best way to write a unit test for a class which depends on an Eloquent model with relationships? E.g.
real object (with database). This is easy, but slow.
real object (no database). I can create a new object but I can't see how to set the related models without writing to the database.
mock object. I run into issues using Mockery with Eloquent models (e.g. see this question).
other solutions?
context: I'm using Laravel with Authority RBAC for access control. I want to find the best way to test my access rules in a unit test. Which means I need to pass the user dependencies to Authority during the test.
If you're writing unit tests, you shouldn't ever use a database. Testing against a database would be considered an integration test. Check out Roy Osherove's videos.
To answer your question, (and not having delved into Authority RBAC, I'd do something like this:
// assuming some RBAC class
SomeRBACClass extends RBACBaseClass {
function validate(UserClass $user) {
if (!$roles = $user->getRoles())
{
return false;
}
$allowed = array('admin', 'superadmin');
foreach ($roles as $role) {
if (in_array($role->name, $allowed)) {
return true;
}
}
return false;
}
}
SomeRBACClassTest extends TestCase {
function test_validate_WhenPassedUser_callsGetRolesOnUserWithNoArgs()
{
$rbac = new SomeRBACClass();
$user = Mockery::mock('UserClass');
$user->shouldReceive('getRoles')->once()->withNoArgs();
$rbac->validate($user);
}
function test_validate_getRolesOnUserReturnsCollectionOfRoles_CallsGetAttributeWithNameOnFirstRole() {
$rbac = new SomeRBACClass();
$user = Mockery::mock('UserClass');
// assuming $user->getRoles() returns a collection
$collection = new \Illuminate\Support\Collection(array(
$role1 = Mockery::mock('Role'),
$role2 = Mockery::mock('Role'),
));
$user->shouldReceive('getRoles')->andReturn($collection);
$role1->shouldReceive('getAttribute')->once()->with('name');
$rbac->validate($user);
}
function test_validate_getAttributeWithNameOnRoleReturnsValidRole_ReturnsTrue() {
$rbac = new SomeRBACClass();
$user = Mockery::mock('UserClass');
// assuming $user->getRoles() returns a collection
$collection = new \Illuminate\Support\Collection(array(
$role1 = Mockery::mock('Role'),
$role2 = Mockery::mock('Role'),
));
$user->shouldReceive('getRoles')->andReturn($collection);
$role1->shouldReceive('getAttribute')->andReturn('admin');
$result = $rbac->validate($user);
$this->assertTrue($result);
}
This is not a thorough example of all the unit tests that I would write, but it's a start. E.g., I would also validate that when no roles are returned, that the result is false.

Managing different output formats or device-types

I have to display different views for mobile devices and I want to provide a simple JSON-API.
I wrote a little module for the Kohana Framework which loads different views depending on some circumstances, which should help me in this case: https://github.com/ClaudioAlbertin/Kohana-View-Factory
However, I'm not very happy with this solution because I can't set different assets for different device-types. Also, when I'd output JSON with a JSON-view, it's still wrapped in all the HTML-templates.
Now, I'm looking for a better solution. How do you handle different output formats or device-types in your MVC-applications?
I had an idea: just split the controller into two controllers: a data-controller and an output-controller.
The data-controller gets and sets data with help of the models, does
all the validating etc. It gets the data from the models and write it to a data-object
which is later passed to the view.
The output-controller loads the views and give them the data-object from the data-controller. There is an output-controller for each format or device-type: an output-controller for mobile-devices could load the mobile-views and add all the mobile-versions of stylesheets and scripts. A JSON-output-controller could load a view without all the html-template stuff and convert the data into JSON.
A little example:
<?php
class Controller_Data_User extends Controller_Data // Controller_Data defines a data-object $this->data
{
public function action_index()
{
$this->request->redirect('user/list');
}
public function action_list()
{
$this->data->users = ORM::factory('user')->find_all();
}
public function action_show($id)
{
$user = new Model_User((int) $id);
if (!$user->loaded()) {
throw new HTTP_Exception_404('User not found.');
}
$this->data->user = $user;
}
}
class Controller_Output_Desktop extends Controller_Output_HTML // Controller_Output_HTML loads a HTML-template
{
public function action_list($data)
{
$view = new View('user/list.desktop');
$view->set($data->as_array());
$this->template->body = $view;
}
public function action_show($data)
{
$view = new View('user/show.desktop');
$view->set($data->as_array());
$this->template->body = $view;
}
}
class Controller_Output_JSON extends Controller_Output // Controller_Output doesn't load a template
{
public function action_list($data)
{
$view = new View('user/list.json');
$view->users = json_encode($data->users->as_array());
$this->template = $view;
}
public function action_show($data)
{
$view = new View('user/show.json');
$view->user = json_encode($data->user);
$this->template = $view;
}
}
What do you think?
Hmm... From the 1st view it loooks strange, and somehow like fractal -- we are breaking on MVC one of our MVC -- C.
But why is this app returns so different results, based on point-of-entry (or device)?
The task of the controller is only to get the data and choose the view -- why do we need standalone logic for choosing something based on point-of-entry (device)?
I think these questions should be answered first. Somewhere could be some problem.
Also the cotroller should select only one view ideally, and dont' do "encode" or else with data, based on current output. I think all this should be in some kind of "layouts" or else. As data always the same and even different views should be the same -- only some aspects changes.

Categories