For a Web-Application with many Database related events I want to build a Changelog. That should log what users have done, so its a Userlog too.
The App is huge, has a complex role based user access system and there will be hundreds of different events (changes) that may occur.
This all should be Database-Driven, in PHP and needs at least a View to search the Logs.
But in short, I have totally no idea how to design that all and need some tips or inspirations, maybe what others have done.
I've done this in the past and found basically 2 approaches: Model-based and Controller-based.
Model-based within the model itself override the save and update methods (assuming an ActiveRecord pattern) to create a new entry in the ChangeLog table.
Controller-based add the ChangeLog record creation logic to each controller that you want to track.
I prefer the Controller-based approach since you have more control over what's going on and when. Also, you have full access to the user session so it's easier to add tracking for auditing purposes.
Have solved that much more easy as it appears with my first thoughts.
This should do it most times without a comment:
protected function log($comment = ''){
$user = $this->user();
ORM::factory('Changelog')->vaules(array(
'user_id' => $user->pk(),
'section_id' => $user->section->pk(),
'username' => $user->username.'#'.$user->section->name,
'time' => time(),
'uri' => $this->uri($this->request->param(), $this->request->query()),
'controller' => $this->request->controller(),
'action' => $this->request->action(),
'post' => serialize($this->request->post()),
'comment' => $comment,
))->save();
}
A simple $this->log() and all is done.
Result: http://charterix.sourceforge.net/log.jpg
Related
I've done quite a few Lithium tutorials (links below in case they help someone else, and also to show I've done my homework:) and I understand the most basic parts of creating models, views, controllers and using MVC to create a DB record based on form input.
However, I'm new to MVC for webapps and Lithium, and I'm not sure how I should write my code in more complicated situations. This is a general question, but two specific validation questions that I have are:
How should I validate date data submitted from the form?
How should I check that the two user email fields have the same value?
I would be very grateful for any help with these questions, and concrete examples like this will also really help me understand how to do good MVC coding in other situations as well!
Date entry - validating data split across multiple form inputs
For UI reasons, the sign up form asks users to enter their DOB in three fields:
<?=$this->form->field('birthday', array('type' => 'select', 'list' => array(/*...*/))); ?>
<?=$this->form->field('birthmonth', array('type' => 'select', 'list' => array(/*...*/))); ?>
<?=$this->form->field('birthyear', array('type' => 'select', 'list' => array(/*...*/))); ?>
What is the best way to validate this server-side? I think I should take advantage of the automagic validation, but I'm not sure of the best way do that for a set of variables that aren't really part of the Model. E.g.:
Should I post-process the $this->request->data in UsersController? E.g. modify $this->request->data inside UsersController before passing it to Users::create.
Should I pull the form fields out of $this->request->data and use a static call to Validator::isDate inside UsersController?
Is there a way to write a validation rule in the model for combinations of form variables that aren't part of the model?
should I override Users::create and do all the extra validation and post-processing there?
All of these seem like they could work, although some seem a little bit ugly and I don't know which ones could cause major problems for me in the future.
[EDIT: Closely related to this is the problem of combining the three form fields into a single field to be saved in the model]
Email entry - checking two form fields are identical, but only storing one
For common sense/common practice, the sign up form asks users to specify their email address twice:
<?=$this->form->field('email_address'); ?>
<?=$this->form->field('verify_email_address'); ?>
How can I write an automagic validation rule that checks these two form fields have the same value, but only saves email_address to the database?
This feels like it's pretty much the same question as the above one because the list of possible answers that I can think of is the same - so I'm submitting this as one question, but I'd really appreciate your help with both parts, as I think the solution to this one is going to be subtle and different and equally enlightening!
[EDIT: Closely related to this is the problem of not storing verify_email_address into my model and DB]
Some background reading on Lithium
I've read others, but these three tutorials got me to where I am with users and sign up forms now...
Blog tutorial
Extended blog tutorial
MySQL blog tutorial
Some other StackOverflow questions on closely related topics (but not answering it and also not Lithium-specific)
One answer to this question suggests creating a separate controller (and model and...?) - it doesn't feel very "Lithium" to me, and I'm worried it could be fragile/easily buggy as well
This wonderful story convinced me I was right to be worried about putting it in the controller, but I'm not sure what a good solution would be
This one on views makes me think I should put it in the model somehow, but I don't know the best way to do this in Lithium (see my bulleted list under Date Entry above)
And this Scribd presentation asked the question I'm hoping to answer on the last page... whereupon it stopped without answering it!
NB: CakePHP-style answers are fine too. I don't know it, but it's similar and I'm sure I can translate from it if I need to!
I'd recommend doing this in the Model rather than the Controller - that way it happens no matter where you do the save from.
For the date field issue, in your model, override the save() method and handle converting the multiple fields in the data to one date field before calling parent::save to do the actual saving. Any advanced manipulation can happen there.
The technique described in your comment of using a hidden form field to get error messages to display sounds pretty good.
For comparing that two email fields are equal, I'd recommend defining a custom validator. You can do this in your bootstrap using Validator::add.
use lithium\util\Validator;
use InvalidArgumentException;
Validator::add('match', function($value, $format = null, array $options = array()) {
$options += array(
'against' => '',
'values' => array()
);
extract($options);
if (array_key_exists($against, $values)) {
return $values[$against] == $value;
}
return false;
});
Then in your model:
public $validates = array(
"email" => array(
"match",
"message" => "Please re-type your email address.",
"against" => "email2"
)
);
Edit: Per the comments, here's a way to do custom rule validation in a controller:
public function save() {
$entity = MyModel::create($this->request->data);
$rules = array(
"email" => array(
"match",
"message" => "Please re-type your email address.",
"against" => "email2"
)
);
if (!$entity->validates($rules)) {
return compact('entity');
}
// if your model defines a `$_schema` and sets `$_meta = array('locked' => true)`
// then any fields not in the schema will not be saved to the db
// here's another way using the `'whitelist'` param
$blacklist = array('email2', 'some', 'other', 'fields');
$whitelist = array_keys($entity->data());
$whitelist = array_diff($whitelist, $blacklist);
if ($entity->save(null, compact('whitelist'))) {
$this->redirect(
array("Controller::view", "args" => array($entity->_id)),
array('exit' => true)
);
}
return compact('entity');
}
An advantage of setting the data to the entity is that it will be automatically prefilled in your form if there's a validation error.
99% of what REST API's do is serve as a controlled interface between client and DB, and yet I can't for the life of me find any libraries that do just that.
All libraries focus on providing a REST interface to the developer, who then sets up the communication with the database. It seems like a no-brainer to me to create a library that already interfaces with the database, and all the developer needs to do is define some ACL rules and plug in some logic here or there.
So, before I continue and put my thoughts into actions by actually creating this sort of library, may I just ask anyone with knowledge on the subject; has anyone implemented anything like this yet? Will I be re-inventing the wheel?
I'm talking strictly about a PHP based solutions by the way, I have nothing against other languages, PHP is simply my cup of tea. But for that matter, I haven't found any implementations in other languages either.
And in case my explanation doesn't make it very clear, this is essentially what I'd want:
<?php
class post_controller extends controller {
protected static $config = array(
'select' => true,
'insert' => true,
'update' => true,
'delete' => false,
'fields' => array(
'id' => array(
'select' => true,
'update' => false
),
'name' => array(
'select' => true,
'update' => true
),
'content' => array(
'select' => true,
'update' => true
)
)
);
/**
* GET, POST, DELETE are implemented already by the parent controller
* Just overriding PUT to modify the content entry
*/
function put($data) {
$data->content = htmlentities($data);
return parent::put($data);
}
}
?>
Thanks in advance for anyone giving their input and apologies if this is not a proper Stackoverflow question.
Edit:
To clarify, this type of service would be for specific use-cases, I don't imagine it to be a type of thing that anyone can use for any type of web service.
I have built a similar system for SOAP and must say it's very easy to do so. I haven't seen any prebuilt libraries that would help you do it (and I doubt they exist - see next paragraph), but it shouldn't take you more than a few hours to build your own solution (a day max - with testing and documentation writing included).
It is however a whole another question if you really want to do that. REST can be (mis)used for this purpose, but it is meant for manipulating resources. Records in a database only rarely have a one-to-one mapping with resources. If they do in your case (as they did in mine) then feel free to do it, otherwise it would be nicer to provide a proper REST API. Why expose your internal DB structure to the world? YMMV, of course.
I'm new to CakePHP and I'm stuck in reading a Model using other fields. I did a cake bake command to generate a simple users CRUD. I can view the user using the url CakePHP provided.
/users/view/1
I can view the user using id = 1. What if I want to view a user by name instead of id?
/users/view/username
By default the view function reads the User model by id.
$this->User->read(null, $id)
Thank you.
you can use find function or findBy<Field>() in your case findByUsername()
check this
I've never used cakePHP myself but I'm going to suggest that you will likely have to implement a new user model method, something like getUserByUsername($username)
This would then in turn interface with your DAL that would get the details of that user based on the username and return a user object that can be used however you wish...
It seems that CakePHP is focusing to deprecate some functions, such as findAll(). Perhaps soon the magic methods such as findBy<field>() will have the same fate.
I can recommend what martswite is suggesting, you should create your custom function:
function findUser($username=''){
return $this->find('first', array(
'conditions' => array(
'User.username' => $username
)
));
}
Perhaps you have a status field, maybe the profile isn't public, you can add a condition:
function findUser($username=''){
return $this->find('first', array(
'conditions' => array(
'User.username' => $username,
'User.status' => 1
)
));
}
I think that's more modular than findBy<Field>.
Okay, so I'm fairly new to CakePHP. This is the setup:
I've got a Model (Reservation), and a controller, (ReservationController).
I'm trying to provide a simple add() functionality.
The request url is: www.example.com/reservations/add/3
Where 3 is the ID of the event this reservation is for.
So far, so good. The problem is, the form is constructed like this:
<h2>Add reservation</h2>
<?php echo $form->create('Reservation');
echo $form->input('first_name');
echo $form->input('last_name');
echo $form->input('email');
echo $form->input('tickets_student'); echo $form->input('tickets_nstudent');
echo $form->end('Add');?>
When I hit the send button, the request URL becomes simply www.example.com/reservations/add/, and the event id is lost.
I've solved it now by grabbing the id in the controller, and make it available to the form:
// make it available for the form
$this->set('event_id', $eventid);
And then, in the form:
$form->create('Reservation',array('url' => array($event_id)));
It works, but it strikes me as a bit ugly. Isn't there an easier way to make sure the form POST action gets made to the current url, instead of the url without the id?
Nik's answer will fail if the website isn't in the server public_html root.
This answer is more solid:
<?php echo $form->create('Reservation',array('url'=>'/reservation/add/'.$data['id']));?>
Following the strictest convention for just a moment, reading a URL like /reservations/add/3 would be, well, confusing. You're calling on the ReservationsController to act on the Reservation model, but passing it an event ID. Calling /reservations/edit/3 is far less confusing, but just as wrong for your situation since the id value, "3", would be assumed to be a reservation identifier.
Essentially, you're making an ambiguous request at best. This is a simple form to create a reservation and that reservation has to be associated with an event. In a "traditional" scenario, the form would allow the user to select an event from some kind of list. After all, the foreign key, probably event_id in this case, is really just another property of a reservation. The list would have to be populated in the controller; this is usually done via a find( 'list' ) call.
If you already know the event that you want to create the reservation against (which you apparently do), then I'd probably select the analogous method of using a hidden field in the form. You still have to set the value in the controller just as you're doing, but the end result is a bit more Cake-y. The hidden field would be named data[Reservation][event_id] (again, I'm assuming the name of your foreign key field).
$form->create('Reservation',array('action' => 'add',$eventId);
and in the controller:
function add($eventId = null)
{
if($eventId == null)
{
// error state
throw new NotFoundException('Invalid id');
}
...
}
I do it all the time.
You can do following:
$form->create('Reservation',array('url' => $this->Html->url()));
this way all your variables from the url will be added in the form action :)
As Rob Wilkerson suggests, the issue is your URL route doesn't accurately describe the operation being performed. It becomes further confusing when you want to edit the reservation: /reservations/edit/6. Now the number in the URL means something different.
The URL convention I use for situations like these (adapted to your particular case) is /events/3/reservations/add. It takes a bit more up-front to configure your routes, but I've found it's superior for clarity down the road.
Sample routes.php:
Router::connect(
'/events/:event_id/reservations/:action',
array('controller'=>'reservations','action'=>'index'),
array(
'pass' => array('event_id'), // pass the event_id as a param to the action
'event_id' => '[0-9]+',
'actions'=>'add|index' // only reverse-routes with 'action'=>'add' or
// 'action'=>'index' will match this route
)
)
// Usage: array('controller'=>'reservations','action'=>'add','event_id'=>3)
Router::connect(
'/events/:event_id/reservations/:id/:action',
array('controller'=>'reservations'),
array(
'pass' => array('id'), // pass the reservation id as a param to the action
'id' => '[0-9]+',
'event_id' => '[0-9]+',
'actions'=>'edit|delete' // only reverse-routes with 'action'=>'edit' or
// 'action'=>'delete' will match this route
)
)
// Usage: array('controller'=>'reservations','action'=>'edit','event_id'=>3,'id'=>6)
In your FormHelper::create call, you'd have to specify most of the reverse-route you want to follow, but again, the up-front cost will pay dividends in the future. It's usually better to be explicit with Cake than to hope its automagic always works correctly, especially as the complexity of your application increases.
I want to save an object or form to the database. Only I can't find the easiest (or normal) way for how to do this.
I found a lot of tutorials, but none seem to be easy or current. Can someone please help me with this?
I use version 1.9.3 of the Zend Framework.
The easiest way (aka the way using the smallest amount of code) to insert a row into a database table using Zend_Db is:
$data = array(
'created_on' => '2007-03-22',
'bug_description' => 'Something wrong',
'bug_status' => 'NEW'
);
$db->insert('bugs', $data);
The above code will insert a new row into the bugs table whereas $db is the Zend_Db_Adapter_Abstract-subclass you created with Zend_Db::factory(). Please see Writing Changes to the Database in the Zend Framework manual for more details and the whole spectrum of features Zend_Db provides.
For the sake of completeness, the above code will issue a query to the database similar to:
INSERT INTO bugs (created_on, bug_description, bug_status)
VALUES ('2007-03-22', 'Something wrong', 'NEW')
The next step would be a more sophisticated approach using Zend_Db_Table.
EDIT:
Given that you have a Zend_Form ($form) with the appropriate fields created_on, bug_description and bug_status and provided that you have the right filters and validators in place, adding a new row with values given in the form is as easy as
if ($form->isValid($_POST)) {
$db->insert('bugs', $form->getValues());
}
Storing a custom object is also very easy:
// $bug is your custom object representing a bug
$db->insert('bugs', array(
'created_on' => $bug->getCreatedOn(),
'bug_description' => $bug->getDescription(),
'bug_status' => $bug->getStatus()
));
Instantiate any object that you need and serialize it. Once serialized, you can store it or transmit it to pretty much any medium. Is this what you are referring to?