I have a shopping cart I built myself that uses session variables to maintain the state of cart across requests. I have an increment and decrement button that allows users to increase or decrease the quantity of a product in their cart. this happens via an Ajax request. The cart class operates by restoring the cart out of the session when constructed, and saving the cart back to the session when destructed.
<?php
class Cart {
/**
* constructor
*/
public function __construct(){
//restore cart
$this->restore();
}
/**
* destructor
*/
public function __destruct(){
//save cart
$this->save();
}
/**
* restore
*/
public function restore(){
//retrieve session info
if(Session::has('cart')){
//get cart
$session = Session::get('cart');
//assign session info
$this->data = ($session['data']);
$this->rates = $session['rates'];
$this->lines = $session['lines'];
}
}
/**
* save
*/
public function save(){
Session::put('cart', $this->forSession());
}
the problem I'm running into is a race condition with multiple Ajax requests. the user can hit the button many times, sending multiple Ajax requests. each request is therefore pulling the current state of the session, doing the operation, and then saving it. the problem is the previous transaction is not necessarily completed and saved when it restores the cart. my first fix was to make any subsequent Ajax requests cancel the previous one, to both cut down on unnecessary (immediately overwritten) requests, and also to help avoid this race condition. while it seemed to help, it still acts quirky. So my next thought was to attack it at the source, namely the cart class itself. my idea was to implement some type of 'locking' to prevent the cart from being accessed until a previous operation was completed. The idea looked something like this.
<?php
/**
* is cart locked
*/
public function isCartLocked(){
if(Session::get('cartLock') === 1){
sleep(1);
$this->isCartLocked();
}
}
public function restore(){
Session::put('cartLock', 0);
//check if cart is locked
$this->isCartLocked();
//lock cart
Session::put('cartLock', 1);
...
}
public function save(){
//unlock the cart
Session::put('cartLock', 0);
...
}
now the first question is, SHOULD I be doing something like this, with the locking? And then, if so, is this a decent way to handle it?
After my first attempt at it, the problem I seem to be running into is the destructor is not necessarily called all the time, which is causing my cart to stay locked, and eventually causing a timeout error.
Thanks for any help!
I'd think that you would actually want to debounce the function call to the AJAX request so that you're not concerning yourself with locking/unlocking or cancelling previous requests - just make one once you're relatively sure they're done adjusting the quantity.
I would recommend this jQuery plugin for throttling or debouncing JavaScript function calls (though jQuery actually isn't required to use this, it's just accessible through the jQuery namespace if available), assuming that's how you're making your AJAX request.
I would have to say that if you are running into race conditions then you have some issues in regards to your application design, and these sorts of issues may haunt your application for a while since concurrent accesses of the session will be smashing your session store relatively frequently
The easiest way to do something like this in my opinion would be with a database table and instead of updating a field one would be adding a row for each user event ( so a increment action would insert , , , 1. And a decrement would insert a row of -1 )
This way you can simply do a sum operation to tally your counts, and when the users checkout process is complete you can clear up the entire order in the cart table .
Related
I'm using Drupal 7 with Commerce, although that doesn't really matter I think.
There's a hook (function) hook_entity_update() that is called when an entity (my shopping cart) is updated.
I want to recalculate my shipping in this hook, the issue is that it saves the entity again once the shipping is recalculated and so it calls the hook again, getting me stuck in an infinite loop.
What would be the best way to solve something like this?
I was thinking of using a simple $_SESSION variable, like $_SESSION['updating'] and set it to TRUE at the start of the hook, then set it to FALSE again at the end of the hook and prevent the function from running with a return at the start if the variable is set to true? I'm not sure if this is a good solution?
Using a session variable because it's a form and the function seems to be called multiple times when submitting.
Not sure if a regular variable would work?
One issue I'm already running into is that the variable is somehow not reset at the end of the function and so the function is never called again ...
Using the session variable seems to work.
Stupid function is somehow called like 7 times when clicking one button ...
Added this at the start of the function:
// Check if entity is a commerce_order
if ($type != 'commerce_order') {
return;
}
// Only apply if order is still in cart
if ($entity->status != 'cart') {
return;
}
if (isset($_SESSION['bab_checkout']['updating']) &&
$_SESSION['bab_checkout']['updating'] === true) {
return;
}
$_SESSION['bab_checkout']['updating'] = true;
And then this at the end of the function:
// We're done updating
$_SESSION['bab_checkout']['updating'] = false;
Still wonder if there is no better solution?
Laravel Cashier makes it pretty simple to swap plans:
$user->subscription('premium')
->swapAndInvoice();
This feature is also great because it invoices the user right away, HOWEVER, it also forces the user to pay right away rather than waiting an hour like it normally would. This seems to get in the way of my invoice.created webhook because once swapAndInvoice happens, I don't seem to be able to make further changes to the invoice.
If I just use ->swap(), it doesn't appear to create an invoice at all, but rather it just creates line items waiting to be added to the next invoice. Thoughts are appreciated.
Since posting this question, I have actually submitted a pull request to the Laravel Cashier repository to add a method within the StripeGateway.php file that can perform such functionality.
/**
* Invoice the billable entity outside of regular billing cycle without charging the user immediately.
*
* #return bool
*/
public function invoiceWithoutImmediateCharge()
{
try
{
$customer = $this->getStripeCustomer();
// Removed this because it immediately charges the user which doesn't allow the invoice to be modified by webhooks
//Stripe_Invoice::create(['customer' => $customer->id], $this->getStripeKey())->pay();
Stripe_Invoice::create(['customer' => $customer->id], $this->getStripeKey());
return true;
}
catch (\Stripe_InvalidRequestError $e)
{
return false;
}
}
Am creating a web application with the codeigniter framework, am working with version 2.0.3.
My makes ajax requests to update the page from time to time, and fetch notifications.
I've visited the codeigniter forums and asking questions about codeigniter sessions and ajax and found this snippet of code which i used, and saved in libraries and class "My_session.php"
class MY_Session extends CI_Session {
/**
* Update an existing session
*
* #access public
* #return void
*/
function sess_update() {
// skip the session update if this is an AJAX call! This is a bug in CI; see:
// https://github.com/EllisLab/CodeIgniter/issues/154
// http://codeigniter.com/forums/viewthread/102456/P15
if ( !($this->CI->input->is_ajax_request()) ) {
parent::sess_update();
}
}
}
But ever since i added this code i am unable to stay logged for more than five minutes without being logged out, or sometimes not being able to login in at all.
Does anyone have a similar experience?
If you want to stay logged in longer you have set sess_expiration for more then five minutes in your application/config/config.php
what happens when you evaluate the is_ajax_request()? in that code snippet add:
echo 'AJAX: ' . $this->CI->input->is_ajax_request(); exit;
to see if the if statement is working correctly. it might be returning false and updating the session each time. just a quick place to start :)
I am trying to get the ID of the product that was most recently added to a user’s cart. A quick google search revealed this function
Mage::getSingleton('checkout/session')->getLastAddedProductId(true);
which is also used in Mage_Checkout_Block_Cart_Crosssell. When I try calling the function in my own controller however, it returns nothing.
I have tried to instantiate a core session via
Mage::getSingleton('core/session', array('name'=>'frontend'))
however this approach does not seem to work. I also tried to create the Crossell block and making the protected method that wraps around getLastAddedProductId function public however that returns null just like it does when I try calling it on its own.
Is there something I have to call or instantiate in order to use this function? Here’s my source listing for reference.
class Mymodule_Addcartaftermath_ItemaddedController extends Mage_Core_Controller_Front_Action {
public function generatemessageAction() {
$parameters = $this->getRequest()->getParams();
if (isset($parameters['ajax_call'])) {
$latest_item_id = Mage::getSingleton('checkout/session')->getLastAddedProductId(true);
$response = array('response' => $latest_item_id);
echo json_encode($response);
} else {
$this->_redirect('/');
}
}
}
I tried poking through the source code, particularly the checkout/model/session.php file in the core and I cannot seem to find the definition of the function. I also looked at it’s parent’s class definition but could not find it there either.
If this method is not available to me is there another way of retrieving the most recent item added? I know that the items are added sequentially and I could perhaps just get the last item of the list of items from the cart however this would not work in the case where the user adds the same item to the cart essentially increasing the quantity rather than actual item itself (e.g. the user adds a laptop the cart when there already is one)
The call to
Mage::getSingleton('checkout/session')->getLastAddedProductId(true);
Is actually clearing the session variable after it is read. Magento uses magic methods extensively. In this case you are using the __call magic method which in turn uses the getData() method. In Mage_Core_Model_Session_Abstract_Varien you will see that they override the default behaviour of getData() to expect the second parameter to be a boolean (The first parameter to getData is the key name for the value you are looking for). That boolean is a flag telling the session to clear the variable after reading.
You could always listen for the checkout_cart_product_add_after event and add the item to your own variable in the session. That event is actually fired on the line before setLastAddedProductId() is called.
try to grep the variable you are looking for. As they are coming from magic methods then its hard to find the exact function you are after so it's easier to see the places where data gets set than where it is used
grep '>setLastAddedProductId' app/code -rsn
to see where the product id gets set to that session variable
app/code/core/Mage/Checkout/Model/Cart.php:255: $this->getCheckoutSession()->setLastAddedProductId($product->getId());
and then you can ask this variable (if it is set else empty())
Mage::getSingleton('checkout/session')->getLastAddedProductId();
and you can see all the things that are in checkout/session and verify if the data is there.
var_dump(Mage::getSingleton('checkout/session'));
Haven't a clue why it works this way but it works for me in Magento 1.6...
<?php
require_once ( "app/Mage.php" );
umask(0);
Mage::app("default");
Mage::getSingleton('core/session', array('name'=>'frontend'));
$session = Mage::getSingleton('checkout/session');
$lastadded = $session->getData("last_added_product_id");
print_r($lastadded);
Apparently you have to instantiate the core/session and then the checkout/session. I've tried it every other way but this is the only way I've found it to work. Perhaps someone can explain why it works this way. Hope this helps you out!
I am using the customer_save_after event in magento, and all is working fine apart from 1 annoying thing - it is always fired twice.
There are no other modules rewriting this and I can find no other reason for this happening. When I look through all of the events getting fired at this time and this event is definately getting fired twice.
Anyone explain this?
I am writing a web service that hooks into this and its turning out to be quite inefficient to duplicate things.
I've noticed this double-save behaviour too. The way to prevent issue with your observer is to set a flag in the request that can be checked e.g.
if(Mage::registry('customer_save_observer_executed')){
return $this; //this method has already been executed once in this request (see comment below)
}
...execute arbitrary code here....
/* Customer Addresses seem to call the before_save event twice,
* so we need to set a variable so we only process it once, otherwise we get duplicates
*/
Mage::register('customer_save_observer_executed',true);
I ran into this as well and did a stack trace in the observer for each method, and can tell you at least ONE reason why it fires twice (there may be others):
When a new user creates an account, createPostAction() runs when the form is submitted. This action does a save() on the customer.
THEN, after the customer has been created, setCustomerAsLoggedIn() is called by createPostAction(). This in turn calls setCustomer(), which has this little bit of code:
if ((!$customer->isConfirmationRequired()) && $customer->getConfirmation()) {
$customer->setConfirmation(null)->save(); // here is the second save
$customer->setIsJustConfirmed(true);
}
Those are the two save()s which dispatch the save event. I only know this for sure for account creation in Magento 1.5. I doubt if it gets fired twice when creating users in the Admin area, or when a user edit's their information... but I don't know for sure.
I hope this helps!
Be careful with Jonathans solution, 'customer_save_observer_executed' stays in the session, so event will not be fired again in the browser session. So it's generally a bad idea, because it will not allow to register two or more customers in a row(actually, it will, but events will not be fired)
I suggest the following solution:
public function customerRegister(Varien_Event_Observer $observer)
{
$customer = $observer->getEvent()->getCustomer();
if (!$customer->getId())
return $this;
if(Mage::registry('customer_save_observer_executed_'.$customer->getId()))
return $this;
//your code goes here
Mage::register('customer_save_observer_executed_'.$customer->getId(),true);
}
I used a static var:
private static $_handleCustomerFirstSearchCounter = 1;
public function Savest($observer) {
if (self::$_handleCustomerFirstSearchCounter > 1) {
return $this;
}
$customerData = Mage::getSingleton('customer/session')->getCustomer();
$model = Mage::getModel('customerst/customerst')
->setQueryText(Mage::app()->getRequest()->getParam('q'))
->setCustomerId($customerData->getId())
->setCustomerName($customerData->getName())
->save();
self::$_handleCustomerFirstSearchCounter++;
}
The difference between these 2 events is one of them can't get customer info, while the other can. So the solution is
public function email_CustomerRegister(Varien_Event_Observer $observer){
$customer = Mage::getSingleton('customer/session')->getCustomer();
$customer_email = $customer->getEmail();
if(empty($customer_email)){
return;
}
// do something
}