Detecting changes in the model; php yii framework - php

I'm creating an audit trail module that i will put in a larger system; and i've created a table to store the trail entries , as an "auditor" what i want to see the currently logged on user, the page where he/she is in, what action he/she did, and what were the changes and when...
these are basically what i want to see; my audit trail table looks like:
User| Timestamp| Module Name| Action| Old Value| New Value| Description
i basically had no problem getting the user, by
Yii::app()->session['username'];
the page/module and action by getting the controller's :
$this->module->getName();
$this->action->id;
My problem lies with the changes old value to new value, the edits done by the user.
i could sort of "sniff" out what edits/ changes he/she did by literally copying the variables and passing it through my function where i create the log.. How do i do this dynamically?
i sort of want to detect if a certain model's properties or attributes has been changed and see what changes were made so that i could get a detail log...Thanks ! sorry, i'm really trying hard to explain this.

In each model that you want to observe you can write a afterFind() method, where you store the current DB attributes into some private variable, e.b. _dbValues. Then in beforeSave() you verify the current attributes with the ones in _dbValues and create an audit record if there was a change.
After you have this working, you can take it a step further and create a behavior from it. You'd put the private variable, the afterFind() and the beforeSave() method there. Then you can attach that behavior to many records.

Quick example:
class Book extends CActiveRecord
{
private $oldAttrs = array();
public static function model($className = __CLASS__)
{
return parent::model($className);
}
public function tableName()
{
return 'book';
}
protected function afterSave()
{
// store history
if (!$this->isNewRecord) {
$newAttrs = $this->getAttributes();
$oldAttrs = $this->getOldAttributes();
// your code
}
return parent::afterSave();
}
protected function afterFind()
{
// Save old values
$this->setOldAttributes($this->getAttributes());
return parent::afterFind();
}
public function getOldAttributes()
{
return $this->oldAttrs;
}
public function setOldAttributes($attrs)
{
$this->oldAttrs = $attrs;
}
}

Your solution is good, but what if there're 2 threads that call ->save() at the same time?
Assume that:
the 1st thread find record, save the A status.
the 2nd thread find record, save the A status.
then 1st thread change record to B, call ->save(). System will log A->B
then 2nd thread change record to C, call ->save(). System will log A->C
summary, there're 2 log: A->B, A->C. If this is not problem for you, just ignore it and do the above solution.

Related

Laravel send multiple posts but save only the last one to database

In Laravel I'm developing a web app where users will answer true or false questions on separate pages. On each page I'd like to somehow save their answer and in the end save the results to the database.
I could make a POST form on each page which would increment 'points' field in the database if the answer was correct. But that would require many accesses to the database by each user as answers would be saved on each page. Is there a way to store their points to a variable on the server and then save that variable to the database in the end?
I've thought of saving points to session but that wouldn't be safe as sessions can be modified by users.
Right now my controller only returns the intro page.
class QuizController extends Controller {
public function index() {
return view("quiz.pages.1");
}
public function addPoint() {
$points++;
}
public function getPoints() {
return $points;
}
}
And a route to redirect to the next pages.
Route::get('quiz/{page}', function($page) {
return View::make('quiz.pages.' . $page);
});
The way i would achieve this in my opinion is to send hidden inputs to your pages and not save() them until the end. So it would look something like this.
firstpage.blade.php
<input type="text" name="yourinputname">
So then you can pass this input from your controller to the next page without saving it. Something like:
public function index(Request $request) {
$yourVariablename = new YourModelName;
$yourVariableName->yourDatabaseColumnName = $request->input('yourinputname');
return view("quiz.pages.1", [
'anyName' => $yourVariableName
]);
}
Notice there is no save() on the controller yet because we want to save it at the end so we just store it. From this points onward you can just do something like this in the next pages until you get to the point where you want to save.
anotherpage.blade.php
<input type="hidden" name="yourinputname">
When you gather all the data, just match them with your database columns and then save() it. I hope it solves your problem.

Need More Clarification on Events YII2

Trying to learn events in Yii 2. I found a few resources. The link I got more attention is here.
How to use events in yii2?
In the first comment itself he explains with an example. Say for an instance we have 10 things to do after registration - events comes handy in that situation.
Calling that function is a big deal? The same thing is happening inside the model init method:
$this->on(self::EVENT_NEW_USER, [$this, 'sendMail']);
$this->on(self::EVENT_NEW_USER, [$this, 'notification']);
My question is what is the point of using events? How should I get full benefit of using them. Please note this question is purely a part of learning Yii 2. Please explain with an example. Thanks in advance.
I use triggering events for written (by default) events like before validation or before deletion. Here's an example why such things are good.
Imagine that you have some users. And some users (administrators, for example) can edit other users. But you want to make sure that specific rules are being followed (let's take this: Only main administrator can create new users and main administrator cannot be deleted). Then what you can do is use these written default events.
In User model (assuming User models holds all users) you can write init() and all additional methods you have defined in init():
public function init()
{
$this->on(self::EVENT_BEFORE_DELETE, [$this, 'deletionProcess']);
$this->on(self::EVENT_BEFORE_INSERT, [$this, 'insertionProcess']);
parent::init();
}
public function deletionProcess()
{
// Operations that are handled before deleting user, for example:
if ($this->id == 1) {
throw new HttpException('You cannot delete main administrator!');
}
}
public function insertionProcess()
{
// Operations that are handled before inserting new row, for example:
if (Yii::$app->user->identity->id != 1) {
throw new HttpException('Only the main administrator can create new users!');
}
}
Constants like self::EVENT_BEFORE_DELETE are already defined and, as the name suggests, this one is triggered before deleting a row.
Now in any controller we can write an example that triggers both events:
public function actionIndex()
{
$model = new User();
$model->scenario = User::SCENARIO_INSERT;
$model->name = "Paul";
$model->save(); // `EVENT_BEFORE_INSERT` will be triggered
$model2 = User::findOne(2);
$model2->delete(); // `EVENT_BEFORE_DELETE` will be trigerred
// Something else
}

Laravel best practice - Query

I need to pass some data down to my view. I have two models a user and a tips model.
The User model has a method that returns the user hasMany(Tip::Class) and the Tip model has a method that returns that the tip belongsTo(User::class).
I'm doing a profile page for a user and using route model binding to return a user model when accessing the profile.
public function tipsterProfileShow(User $tipster)
{
if (!$tipster->isTipster())
{
return redirect()->route('home');
}
return view('profile.index')->with([
'tipster' => $tipster,
'tips' => $tipster->tips(),
]);
}
I want to display some data such as the amount of tips that are correct which are indicated by the status column in the tips table.
At the moment in the blade view I'm using
{{$tips->where('status','Won')->count()}}
I feel this isn't best practice but I may be wrong.
Would it be better doing something like the below?
public function tipsterProfileShow(User $tipster)
{
if (!$tipster->isTipster())
{
return redirect()->route('home');
}
return view('profile.index')->with([
'tipster' => $tipster,
'tips' => $tipster->tips(),
'wins' => $tipster->tips()->where('status', 'Won')->count()
]);
}
That way I would be keeping the queries out of the view. I'm really new to laravel and 'best practice' so trying to get some advice.
You're asking about a best practice, which is generally frowned upon, but this really is something essential that every beginner should learn, so I still think it merits an answer.
In brief: YES! What you're doing is a great first step towards keeping your code separated by logic. Your views should be responsible for displaying data, and your controllers for handling data over to the views. Whether your controllers should actually be responsible for calculating the data is another topic, and one which is constantly debated.
That said, you could get this down to just a single line in the controller if you apply a little bit of other logic:
public function tipsterProfileShow(User $tipster)
{
return view('profile.index', compact('tipster'));
}
The first step is to add a method to your User model, something like this:
public function winCount()
{
return $this->tips()->where('status', 'Won')->count();
}
Now you can access $tipster->winCount() from your view. You can also access $tipster->tips() straight away in your view - most would agree that's perfectly fine.
The second step is to extract the redirect call for non-tipsters into a middleware, which you can read about here: https://laravel.com/docs/5.3/middleware
There are further steps you might take from there, but that's a good starting point. Good luck! :)
I will advice you creating a POPO (Plain Old PHP Object) that will contain a full description of a user more synonymous to a User profile.
A model in laravel represents a row in the table and it will lazy-load any relationship attached to it.
So if you have a POPO, you will be able to define everything that is related to the user and pass it on to the view without having to query in the view.
Take below as an example:
Class UserPorfile{
private $id;
private $username;
private $tips;
public function setId($id){
$this->id = $id;
}
public function getId(){
return $this->id;
}
public function setUsername($username){
$this->username = $username;
}
public function getUsername(){
return $this->username;
}
public function setTips(array $tips){
$this->tips = $tips;
}
public function getTips(){
return $this->tips;
}
}
passing an object of this class to your view seems much more better
Yes, you are correct. Given you are trying to follow the MVC software development pattern, you should avoid putting business logic in your view.
This line of code:
{{$tips->where('status','Won')->count()}}
is actually doing 2 things:
Query the model for all objects with a certain criteria
Count the result
By following the MVC principles it should be your controller that sends the commands to the model, not the view.
Good luck!

phpactiverecord get the next id

I'm using phpactiverecord. I have an Order object that has an id and code. On database, the id is the primary key int and auto increment. code is a varchar and unique and is composed by the id and other chars.
The problem is that I need to set the code value before to save and for this, I need to get the id.
Currently I did:
class Order extends ActiveRecord\Model{
function save() {
if (parent::save()): // I save all data
$this->code = "{$this->id}w"; // Once it was saved I get the id
parent::save(); // I save the code
return true;
endif;
return false;
}
}
Is there a more elegant way to do it?
You're technically not supposed to override save() in that way, as even in this case you're changing the method signature, (it's public function save($validate=true)). And there are a bunch of possible callbacks for your case. The better way for your case is:
public static $after_create = array('after_create');
public function after_create()
{
$this->code = $this->id.'w';
$this->save();
}
Also it's soo awkward to use templating if else inside of class code :P.
This code may possibly fail if you don't have the latest version from github, as there was a bug earlier on where after_create didn't know that the object was already saved.

how can I define a value for model's field in symfony?

I'm building an app for the company I work for in symfony, non the less the app might be pretty useful outside our company so we decided to write it in a more general form so we can make it multi company. I'm facing a problem on how to define a default value for a field that is going to be in every single model (company_id) so we don't need to select which company we belong to every time we want to add data. can anyone help me?
I've tried
class TestForm extends BaseTestForm
{
function configure()
{
$this->setDefault('company_id', '1');
}
}
and when I submit the form I get a missing value for model ....
I did it, in the action of course, before the processForm and after the $this->form = new TestForm();
I used:
public function executeCreate(sfWebRequest $request)
{
...
$this->form->getObject()->setCompanyId('1');
...
}

Categories