Infinite loop while listening model event - php

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.

Related

Getting a property from a mocked model's relationship (Laravel, PHPUnit)

In the code I have this:
if (!$check = $this->getCheck()) {
return false;
}
if (!$user = $check->user) {
return false;
}
$user->verification->some_id;
Method getCheck is in a trait which is used by a service, which I am mocking by binding it in the test
$app->bind(Service::class, function () {
return $this->mock(Service::class, function(MockInterface $mock) {
$mock->makePartial();
$check = app(Check::class);
$expect = $mock->shouldReceive('getCheck');
$expect->andReturn($check);
}
});
I want to mock getCheck so that it returns a check model with a user model which has a verification model which has the property some_id
I read about using with(), but just can't seem to get this to work.
In Laravel you should not mock Models, there is weird side effects with it. You can without a problem use models without saving them to the database, which i assume is the best approach for you.
$check = new Check();
$check->user = new User();
$expect->andReturn($check);
If the models are needed for database connections, you need to have a local test database either Sqlite or a proper SQL server. Using factories to create the test data you need.

Add a custom attribute when retrieving a model

Is possible to attach a custom attribute when retrieving a model in laravel?.
The problem is that I need to return some data that is not in the database along the info from the database. I've been doing it manually but I guess that there might be a way to do it in the model.
Example: I have an application table. Each application contains a folder with documents with the same application id. I need to attach the amount of files the folder that correspond to each application.
This is what I do:
$application = Application::get();
$application = $application->map(function($a){
$a->files = $this->getFiles($a->id); // This gets the amount of files
return $a;
})
Is there some way to do it in the model in a way that $application->files is already contained in $application when doing Application::get()
class User extends Model
{
public function getFooBarAttribute()
{
return "foobar";
}
}
And access to that attribute like:
$user->foo_bar;
or like,
$user->fooBar;
More detailed documentation;
https://laravel.com/docs/5.7/eloquent-mutators#defining-an-accessor
in the Application model
public function getFilesAttribute()
{
return 'lala'; // return whatever you need;
}
now application model has an attribute named files.
$application->files // returns lala.
example code.
$applications = Application::get();
$application_files = applications->map->files;
official documentation https://laravel.com/docs/5.7/eloquent-mutators#defining-an-accessor

PHP Unit, Testing laravel log messages with unit testing

I'm using PHPUnit to create a Unit test for a store function that stores data in a database.
Currently, i have a test verifies that it has stored data.
However, I also want to create a test that proves that a laravel log message has been produced if the model save function fails.
The code below shows the store function. The "log::info" is the line I want to test.
Thanks.
public function store(Venue $venue){
$saved = $venue->save();
if($saved == false){
Log::info('Failed to Save Venue'. $venue);
}
}
This what I have so far, i pass an empty model that will cause the save to fail due to database constraints
public function test_venue_store_failed(){
$venue = new Venue();
$venueRepo = new VenueRepository();
$this->withExceptionHandling();
$venueRepo->store($venue);
}
You can mock the Log facade in your unit test as follows, as per the docs:
public function test_venue_store_failed(){
$venue = new Venue();
Log::shouldReceive('info')
->with('Failed to Save Venue'. $venue);
$venueRepo = new VenueRepository();
$this->withExceptionHandling();
$venueRepo->store($venue);
}
Maybe you can use event listener on Models.
using this you can get logs on Create or other Events.
Check out the Example Below.
Hope to help .
Info 1.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use User;
class Venue extends Model
{
protected $fillable = ['title', 'ect..'];
public static function boot() {
parent::boot();
static::created(function($item) {
\Log::info('venue.created');
});
static::updated(function($item) {
\Log::info('venue.created');
});
static::deleted(function($item) {
\Log::info('venue.created');
});
}
}
Info 2.
Also there is an exists method on model
if ($venue->exists()) {
// saved
} else {
// not saved
}
Info 3
To get the insert queries when $venue->save(); error, you can try to catch the exception like this:
try{
$venue = new Venue;
$venue->fields = 'example';
$venue->save(); // returns false
}
catch(\Exception $e){
// do task when error
\Log::info($e->getMessage()); // insert query
}
Hope this helps :)

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!']);

Problems with redeclaring classes when using PHP's class_alias in a functional test

I'm using PHP 5.3's class_alias to help process my Symfony 1.4 (Doctrine) forms. I use a single action to process multiple form pages but using a switch statement to choose a Form Class to use.
public function executeEdit(sfWebRequest $request) {
switch($request->getParameter('page')) {
case 'page-1':
class_alias('MyFormPage1Form', 'FormAlias');
break;
...
}
$this->form = new FormAlias($obj);
}
This works brilliantly when browsing the website, but fails my functional tests, because when a page is loaded more than once, like so:
$browser->info('1 - Edit Form Page 1')->
get('/myforms/edit')->
with('response')->begin()->
isStatusCode(200)->
end()->
get('/myforms/edit')->
with('response')->begin()->
isStatusCode(200)->
end();
I get a 500 response to the second request, with the following error:
last request threw an uncaught exception RuntimeException: PHP sent a warning error at /.../apps/frontend/modules/.../actions/actions.class.php line 225 (Cannot redeclare class FormAlias)
This makes it very hard to test form submissions (which typically post back to themselves).
Presumably this is because Symfony's tester hasn't cleared the throughput in the same way.
Is there a way to 'unalias' or otherwise allow this sort of redeclaration?
As an alternate solution you can assign the name of the class to instantiate to a variable and new that:
public function executeEdit(sfWebRequest $request) {
$formType;
switch($request->getParameter('page')) {
case 'page-1':
$formType = 'MyFormPage1Form';
break;
...
}
$this->form = new $formType();
}
This doesn't use class_alias but keeps the instantiation in a single spot.
I do not know for sure if it is possible, but judging from the Manual, I'd say no. Once the class is aliased, there is no way to reset it or redeclare it with a different name. But then again, why do use the alias at all?
From your code I assume you are doing the aliasing in each additional case block. But if so, you can just as well simply instantiate the form in those blocks, e.g.
public function executeEdit(sfWebRequest $request) {
switch($request->getParameter('page')) {
case 'page-1':
$form = new MyFormPage1Form($obj);
break;
...
}
$this->form = $form;
}
You are hardcoding the class names into the switch/case block anyway when using class_alias. There is no advantage in using it. If you wanted to do it dynamically, you could create an array mapping from 'page' to 'className' and then simply lookup the appropriate class.
public function executeEdit(sfWebRequest $request) {
$mapping = array(
'page-1' => 'MyFormPage1Form',
// more mappings
);
$form = NULL;
$id = $request->getParameter('page');
if(array_key_exists($id, $mapping)) {
$className = $mapping[$id];
$form = new $className($obj);
}
$this->form = $form;
}
This way, you could also put the entire mapping in a config file. Or you could create FormFactory.
public function executeEdit(sfWebRequest $request) {
$this->form = FormFactory::create($request->getParameter('page'), $obj);
}
If you are using the Symfony Components DI Container, you could also get rid of the hard coded factory dependency and just use the service container to get the form. That would be the cleanest approach IMO. Basically, using class_alias just feels inappropriate here to me.
function class_alias_once($class, $alias) {
if (!class_exists($alias)) {
class_alias($class, $alias);
}
}
This doesn't solve the problem itself, but by using this function it is ensured that you don't get the error. Maybe this will suffice for your purpose.

Categories