It should be simple:
Hook a MODEL_save_after event (or MODEL_save_before if that’s more appropriate).
Check getData() vs getOrigData() to see what changes the user has made.
In the customer_address model, edited through the backend, both save events are triggered twice. The first time customer_address_save_before is triggered, followed by customer_address_save_after.
In both cases getOrigData() and getData() are identical, except getData() has a new updated_at value, and has a store_id set (is this a bug?). So, the model doesn’t have the submitted data from the user yet. The events are both before entering user data or validation, so this is of no use.
customer_address_save_before is triggered, followed by customer_address_save_after a second time. This time (in both cases), getOrigData() is empty, and getData() now has all the submitted data from the user. So I can’t compare these events either! It appears this is after validation, saving, the lot!
Why the save process appears to occur twice? Magento v1.3.2.4.
I wound up hooking customer_address_save_before, and comparing the results to what was in the database like so:
<?php
customer_address_save_before_listener ($event)
{
$address = $event->getCustomerAddress();
$database_address = Mage::getModel('customer/address')->load($address->getId());
}
?>
And comparing the getData() returns from the two. There were three gotcha's that I came across:
Use getEntityTypeId() on $address and check it. Despite hooking 'customer_address_save_before', you also get OrderAddress models being sent to your listener (this seems wrong to me, but ah well).
Check for arrays in $address->getData() values. For example, 'street' is returned from the DB as a single string, while the address your listener is passed has this exploded on endlines.
The CustomerAddress your listener is passed has a 'store_id'. Even though CustomerAddress doesn't store 'store_id', and it doesn't get saved (or loaded from) the database.
Related
My application deals with user payments. In the company, this user has the following status:
compliant (user payed all debts so far)
overdue/default (user registered for 3 months minimum and has hasn't payed at least 1 debt)
inactive (user is registered for less than 3 months and hasn't payed any debt)
How is the best way to deal with those rules in multiple places (and rules) inside the application?
Do I need a field like status_id and a cron to update this every hour?
No status_id field and write the SQL rule in every query that needs to show the status?
Load a User model and call a ->status() method that has the rule? In this case, how can I show "totals", like: We have 3000 overdue users, 15000 inactive users etc...
This is giving me headaches for months and I really need help haha. We currently have a solution but it's too complex to deal with it. As it seems to be something common within apps that deal with payment, there's must be a simplier way to do this :P
Thanks!
Notes
Application has currently 90.000 users
We need this info in real-time.
This info is used in reports to generate chars.
This info is showed inside the user profile.
This info is showed in listings.
Users are notified when a user change between those status (like, "you have debts" when user enters in "overdue").
This info is not managed by application users.
The status need to be tracked.
If you are using this field in multiple places, then you should store the status in a single place and update it as appropriate (I would also keep a history of the statuses, but that is another matter).
If the status changes due to some user actions (such as a payment being processed), then you can use a trigger on the action. However, your status changes seem to be based on time after an event. In that case, you should run a regularly scheduled job (as a cron job or database event).
I am a bit confused about why you would do this every hour. It seems that once per day would be most appropriate. If the "debts" are paid at arbitrary times, then the process of payment should update the status. For the downgrading of the status, a single job once per day should be sufficient.
Interesting question, but also not one with a single answer.
I think the complexity here might come from the surrounding code, rather than the core business logic and requirements. I say this because three status types, all of which are derived from your internal application, isn't too bad.
One possible solution, and I am assuming some level of MVC or similar.
Given your model, user, and extending an ORM like Eloquent (I will Eloquent from Laravel because I am most familiar with it, but any ORM will work):
use Illuminate\Database\Eloquent\Model;
use App\DebtCollector;
public class User extends Model
{
// Assuming model has the following fields
// id, status, registration_date, and a one to many
// relationship with debts
protected $fillable = [
'raw_status',
'registration_date',
];
public function debts()
{
return $this->hasMany(Debt::class);
}
public function refreshStatus()
{
$dc = new DebtCollector();
// Business logic inside the "DebtCollector" class
$this->raw_status = $dc->resolveStatus($this->debts, $this->registration_date);
// Save value to the underlying datebase
$this->save();
}
// If you fetch a status directly, it will refresh first,
// then return the value
//
public function getStatusAttribute()
{
$this->refreshStatus();
return $this->raw_status;
}
}
// Schedule task somewhere - ran nightly, or whenever
//
// This way you can refresh the status only on certain groups
// of data - for example, if the business flow means that once
// they become compliant, they can't go back, there is no need
// to refresh their status anymore
//
User::where('raw_status', '<>', 'compliant')->refreshStatus();
// Alternatively, the schedule could chunk results and do an update
// only to those not updated in the last 24 hours
//
$date = new DateTime;
$date->modify('-24 hours');
$formatted_date = $date->format('Y-m-d H:i:s');
User::where('last_updated', '>', $formatted_data)->refreshStatus();
I would say there are multiple solutions to this problem.
I would suggest not having any defined status. From what I can see you can always "figure out" the current status based on some other data.
For example "user payed all debts so far". This is something you simply know just by analyzing all changes for given period. You can aggregate data to figure out all you need to know. So then you don't need to save the status at all. It is just derived from all the changes to the customer's account that happened over specific period.
Same is for totals. You can do this easily on database level or even by using some document based DBs or ElasticSearch.
This, of course, assumes you trace the history of changes. If you do - problem solved. If you don't - you have to save the status into database and will not be able to get historical data.
I think this is one of the more complex tricks to get right and therefore I have decided to elicit the help of the very knowledgeable people on StackOverflow. My scenario is as follows: I have two entities, a user and an account. A user is always linked to an account upon registration (and depending on the type of user, might be linked to more than one account. Upon registration the function saveUser() is called (via ajax from frontend) and the submitted form data is retrieved from the Request Object. This data is then passed to the function saveAccount($data) (which is called in the saveUser() function) in the form of a parameter and the account is created (sometimes called more than once with different data sets to create various accounts), which is linked to the user.
Now I want to create an account from my admin panel without creating a user, so I want to call saveAccount($data) directly via ajax (from frontend) and pass the form data to it as a PARAMETER (instead of retrieving it in the function via the Request Object), so that I can use the same saveAccount($data) function and that I do not have to create a saveAccount() which works the the Request variable. Does this make sense? Is this possible? If so, how would I go about doing this?
I did not post any code, as I did not see the need for it, this is more a conceptional problem, but if you require the code that I have thus far or if anything is unclear I will be happy to elaborate.
There should not be any saveAccount method, you just rely on relationships between entities, i.e. on a setAccount method, or to a addAccount one in case you need to add an entity to a Collection.
Then Doctrine will take care of saving and persisting everything.
For saving data, I would always rely on a RESTful interface [which you can create easily via FOSRestBundle], and send everything via ajax no matter what; you'll end up with a smoother interface and more maintainable code.
For instances where a controller function can be called either via AJAX with form data or internally by a another controller function the following solution works:
public function saveAccount($data = null)
{
if (empty($data)) $data = $this->getRequest()->request->all();
...
}
Then you can pass an array to the controller function in the same format as your form data array and it will use that data if passed to the function, otherwise it will retrieve the REQUEST (form) data.
I have a question regarding Domain Driven Design. Let's imagine a simple scenario.
I have an Entity called "User" that has some properties. One of these properties is "date_created", "date_modified" and "last_login_ip".
Let's say we have a form that creates a user and if the create is successful, it authenticates him.
The controller gets the POST data
Sends the post data to a UsersService via the method "createAndAuthenticateUser"
The service receives the data, validates it (doing it here and NOT in the entity because the validation is tied to repositories, such as to validate if the email already exists, etc).
If the validation is OK, it creates a new Entity, assigns the data to it and then sends the entity to the repository to save it. The repository then saves this user in a datasource.
So far so good. The problem here is that, the date_created/date_modified/last_login_ip have to be set in this service method.
What if I want to set the date_modified ANYTIME when the user object is updated (for instance,at login I want to update the date_modified, at user update i want it again, at user creation I want it again.
Logically, my own answer would be to put this in the repository like...
(meta code here sort of, the syntax doesn't matter)
function save($User) {
if (!$User->id) $User->date_created = 'YYYY-MM-DD HH:II:SS';
$User->date_modified = 'YYYY-MM-DD HH:II:SS';
$DataSource->Save($User);
return $User;
}
However, from what I've been reading, the repository should always just map data between the caller and the datasource (and the reverse) and that's it. It should never SET data or anything like that.
Of course, you could tell me this is a behavior, so I could have a behavior that says $User->markAsUpdated() which would just set the date_modified property. But again, this means that this method must be called from more than one place, instead of having a centralized place to do it. I don't see the benefit of NOT having this code in the repository.
Any ideas?
If the concept of last login ip is actually central to your user for some reason, then it's valid to update the user on login. The fact that you're expressing concern about performing that update to save the last login IP implies that it's not really a user concept, but a security, audit, or otherwise-external-to-user concept.
As for setting the modify and create dates, I'd make the same argument. If it's a requirement of the system that the user both maintain and expose that information, then create a private method on the user that each public method calls when it modifies the state, which will set the modify date. If it's not, then you pretty much have two options - either create an auditing service that is notified of the update and keeps its own audit record, or have the repository set the fields when it updates the record.
I have a booking form that's built using options that are retrieved via a third party API and because of the need to have up to date information the results from the API can't be cached (at least not for very long).
The problem I'm having is I've noticed when the form is submitted Drupal is re-calling my _form function which is triggering the API calls again and I'd like to stop it doing that to try and reduce the number of API calls that are made.
Obviously if the validation fails it needs to re-draw the form and the API calls will need to be made again but I'm wondering if there's a way to stop it doing this when the form validates so I can stop it making lots of unnecessary calls to the API.
Thanks for any help.
You can not avoid the re-creation of the form, if the form is to be processed by Drupal. It is a central part of Drupals form processing workflow: Every form is at least build twice, once for initial output, and at least once again when the post from the client comes in. It can even be more than that, depending on what the form does afterwards, e.g. on redisplay when any validation errors occurs (Usually a cached version gets used by then, but that depends on the specific form).
What you should do is follow Simons suggestion - on the first call to your form builder function, make your API calls and store the results in $form_state['storage']['yourIdentifier'] ('yourIdentifier' being some string not used by standard form processing - usually starting with your modules name). On every call to your form builder function, you check if that results are already in $form_state['storage']. If they are, you just use those, skipping the API calls.
This will avoid the duplicate API calls, and gives you more control on how/when to do them (e.g. you can also clear those results during validation, if a special condition calls for a refetching from the external API).
Could you store/cache the return values from the API in the $form_state['storage'], so at least if _form gets called every time you can first check storage before making the API calls again.
First thing, If validation fails, Drupal has a copy of the form, so your form function won't actually be called.
Now to the solution.
You can redirect to a new page in your form submit, go avoid the recalling og the form.
Instead of calling your form directly in your menu definition make a callback that calls the form. You can then test if the form has been submitted or not and only call your form function when needed.
If you are this worried about the API calls you could also cache it for 5-10 mins which would make my two suggestions obsolete.
Drupal can't do this? "Can't" isn't in my vocabulary!
I had this same problem, but I couldn't let it beat me. Here's how I got over it:
In the processing form, after I insert the details into the database (or email or whatever), I set 2 drupal status messages like this
function process_form($form){
$success = do_email($form);
if ($success){
drupal_set_message("Success");
drupal_set_message("Your form worked!");
}
}
And in the form generator I read and clear the messages, clear the form and output the status message as markup:
function form_generator($form_state,$parameters){
$form = array();
...// This is where I make the regular form
$messages = drupal_get_messages(); // this gets the messages and clears them
if (isset($messages["status"])){
if (isset($messages['status'][0]) && isset($messages['status'][1])){
if ($messages['status'][0]=="Success"){
$form = array(); // This clears the form I've just made
$form['final_message']= array(
'#type' => 'markup',
'#markup' => $messages['status'][1],
);
}
}
}
}
This doesn't clear the form the first time it's shown because $messages isn't set and when you encounter validation errors they will still appear.
Drupal magic bars our way, but the will of PHP coder is stronger.
So the chain of events is:
The user submits a form.
During the processing of the submission, there is a message generated, such as "Your record was saved."
The user is redirected to a new page, say the search results.
The new page needs to display the message.
So, the question is how to get the message from step 2 to step 3? This is only one simple example...there are many other much more complicated examples.
I am using PHP.
Needs:
supports multiple messages and need to be formatted on the receiving machine as required
messages can be added on the same page (such as within step 4)
messages added from inside any function or object
Some options I have come up with:
store in a session variable as an array and emptied after each display
pass as a get or query parameter; can get annoying as you are constantly processing this and have to remember to get it; as it can get long, it could easily go over the max length of the query string
store in the database on a per session basis (may not always be for a logged in user); this would require an extra insert on each page where they are added, possibly multiple, and an extra select on every page
Currently I have been storing the messages in the session in an array, but I'm wondering if there is a better way. I don't think the other 2 options above are very good.
Edit: I use 2 functions for the session method: AddStatusMsg() (adds an element to the array) and DisplayStatusMsg() (returns an HTML formatted message and empties the array).
I would recommend AGAINST storing these messages either in the database or in the session, for one simple reason: tabs. (Well, really, the stateless nature of HTTP.)
Think of a person who's got multiple tabs open of different sections of your website. This person performs some action and while that loads, switches to another tab and clicks on a link. If you're storing the messages in the session/database and the switched-to tab is a page that can display these messages too, the user has now entered a race condition where depending on which request the server responds to first, the messages may display where they were not intended.
Now, there are some situations where this legitimately might not matter, but it could also be extremely confusing in some cases.
Putting the messages in the request doesn't have to be as bad as it initially seems. Perhaps you could store all the messages you want to display in the database with a numeric (or, for bonus obfuscation, hash) ID, and pass a list of IDs in the query string. This keeps the query string short, and all you have to do is keep track of what ID corresponds to what message in your code.
I would stick with the session approach only perhaps adding support for this messaging system on the master page. You are on the right way as the all other approaches have a greater cost, of simplicity or performance.
I suppose you have a master page which is the template for all other pages. If you don't have it's a good reason to have one, so you don't need to take care of handling the displaying of the messages on every page you need it as long as you have a specific place to show them.
You can also use a specific div rendered by the master page for that and let the position be handled by the current page. If I understand correctly you need some kind of timing between the showing of the message and the user redirection to another page. This could be achieved using any AJAX library to show that div I said before and then redirecting to a new page.
I suggest taking a look into jQuery.
This is how I like to do it:
function set_message($message_type, $message)
{
$_SESSION['messages'][$message_type][] = $message
}
function get_messages()
{
$messages_array = $_SESSION['messages'];
unset($_SESSION['messages']);
return $messages_array;
}
where $message_type can be "warning", "error", "success", etc. and depending on the type you can show the user a different image/color/whatever.
This problem is a classic example of how to have data persist in a "stateless protocol" like http.
Your options are:
Pass it in the GET parameters (not
user friendly)
Store it in the DB
Store it in Session
Options 2) and 3) require the user to have a cookie (otherwise, there's no way to match the user to the message). Between them, I'd go with PHP's built in sessions. Simply set a session variable at your step 2, and have the search page always check for the variable in your step 4
Nothing to it. Don't over complicate things.
Probably the best way is to store it in the session. It's the simplest way and as John said, 'Don't over complicate things'.
Store it in the database as well as the session. This way the user can get to his history if he needs it, and you have easy access through the session data.
Don't use a query parameter, it'll only confuse the user at some point when the message is displayed when it shouldn't be.
Displaying the messages should be a part of your main template (in other words; done once).
Maybe a slight improvement would be to store, instead of an array, an object's instance that gets populated and knows how to display the messages appropriately, deleting the data itself after any display routine gets called. That way you don't have to repeat the display and delete logic everywhere, plus, you can code different output routines in the object depending on the need.
I think you're doing it the right way. You should stay away from the database for this and putting it in the URL is ugly. You could write a nice little class for this which can make it simpler.
Here's a little session class:
<?php class session
{
public function __construct()
{
session_start();
}
public function set($name, $value)
{
$_SESSION[$name] = $value;
}
public function get($name)
{
return (isset($_SESSION[$name])) ? $_SESSION[$name] : false ;
}
public function delete($name)
{
unset($_SESSION[$name]);
}
public function destroy()
{
$_SESSION = array();
#session_destory();
#session_regenerate_id();
}
}
A little message class can be built on that pretty easily.
I'm at this crossroad myself and I've considered all options extensively.
How about storing two browser
cookies, one called page and the
other called message.
On redirect you overwrite the cookie.
When the page loads you check if
said cookie exists (in the http
headers sent by the client).
Check if it's for that page, if it
is, store the message in a variable
and unset the cookies.
If it's not for that page, ignore
it, it will be output on the other
tab that is loading or if it is for
a page that for some reason never
unset the cookie it will eventually
expire.
This avoids using the database and session cookies.