Working with Magento’s Wishlist and Multiple Stores/Websites

Published by John on August 29, 2017

Magento LogoRecently, while working on a Magento store front, I ran into an issue with how the wishlist works when you have multiple stores and upon digging into the wishlist code, found a neat feature that I could not find much documentation on.

In this case, several shops were setup on different subdomains, with some sharing products with the main website. When someone added a product to their wishlist, it was added to the wishlist of whatever store they were browsing. So, for example, if the product was on both store1.shops.com and store2.shops.com and you added it while viewing the product on store2, you would be redirected to store1’s wishlist and it would be visible there, but not on store1’s wishlist. For this particular setup, we actually wanted the product to be on store1’s wishlist, even when viewing store2.

I began to do a little digging and found the following code in app/code/core/Mage/Wishlist/Model/Wishlist.php:

public function addNewItem($product, $buyRequest = null, $forciblySetQty = false)
    {
       [...]

        if ($product instanceof Mage_Catalog_Model_Product) {
            $productId = $product->getId();
            // Maybe force some store by wishlist internal properties
            $storeId = $product->hasWishlistStoreId() ? $product->getWishlistStoreId() : $product->getStoreId();
        } else {
           
            [...]

When adding a product to the wishlist and setting the $storeId, the addNewItem function checks to see if the product has a wishlist store ID set and if it does, adds the product to that store’s wishlist. If it does not, it defaults to the current storeId.

A quick grep of the core code files found that the only file that has the phrase ‘WishlistStoreId’ in it was Wishlist.php. It was used in the above function and also the _addCatalogProduct function. I also searched for ‘wishlist_store_id’ , as well as checked a few places in the backend and didn’t see any references to it.

So, this appears to be a feature that was added with some decent foresight by a Magento Dev, but not something setup/configured by default.

How do I use the WishlistStoreId?

If you want to specify a specific store ID to be used when adding products to the wishlist, this can be done relatively easily, as described below:

  1. Goto Catalog -> Attributes -> Manage Attributes -> Add New Attribute
  2. Create a new ‘text field’ attribute, with a code of ‘wishlist_store_id’, probably Global Scope since you are dealing with multiple stores
  3. Goto Catalog -> Attributes -> Manage Attribute Sets and select the main product attribute set.
  4. Drag the new ‘wishlist_store_id’ attribute over to somewhere within the attribute set and save the set, updating indexes if necessary.
  5. Now, edit an existing product and set the ‘wishlist_store_id’ to the store ID of one of your stores.
  6. On the front end, when this product is added to a wishlist, it should now get added to the wishlist of the store you specified above, no matter what store you are actually viewing it on.

Possible Issues with This Setup

From looking at the above code, you can see that the hasWishlistStoreId check occurs after checking to see if the $product variable passed to the addNewItem function is an Mage_Catalog_Model_Product object. If it is not, it defaults to using the storeId from the $buyRequest variable. So, there may be some cases where this does not work, however when adding a product to the wishlist using the links found on the product or category page, I think it should always work, as the wishlist controller throws an exception if it is unable to load the product as an object.

Add Comment

Creating an Authorize.net Subscription Using a CIM Profile

Published by John on July 12, 2017

Recently, while working on a site that manages user profiles, I needed to create a subscription using Authorize.net’s Automated Recurring Billing(ARB.)

Since we already have the user’s credit card information stored using the Customer Information Manager(CIM) in order to be able to charge them for one-off purchases, I wanted to create the subscription using their CIM profile, so as not to require them to re-enter their credit card information.

After some research, I determined that this was a current feature of their API, however the PHP API does not appear to offer it as an option. Fortunately, it is a fairly quick fix(provided you don’t mind editing their PHP API.)

To add support for this, first you need to modify the following file: lib/shared/AuthorizeNetTypes.php

Replace the entire AuthorizeNet_Subscription class with the following:

/**
 * A class that contains all fields for an AuthorizeNet ARB Subscription.
 *
 * @package    AuthorizeNet
 * @subpackage AuthorizeNetARB
 */
class AuthorizeNet_Subscription
{

    public $name;
    public $intervalLength;
    public $intervalUnit;
    public $startDate;
    public $totalOccurrences;
    public $trialOccurrences;
    public $amount;
    public $trialAmount;
    public $creditCardCardNumber;
    public $creditCardExpirationDate;
    public $creditCardCardCode;
    public $bankAccountAccountType;
    public $bankAccountRoutingNumber;
    public $bankAccountAccountNumber;
    public $bankAccountNameOnAccount;
    public $bankAccountEcheckType;
    public $bankAccountBankName;
    public $orderInvoiceNumber;
    public $orderDescription;
    public $customerId;
    public $customerEmail;
    public $customerPhoneNumber;
    public $customerFaxNumber;
    public $billToFirstName;
    public $billToLastName;
    public $billToCompany;
    public $billToAddress;
    public $billToCity;
    public $billToState;
    public $billToZip;
    public $billToCountry;
    public $shipToFirstName;
    public $shipToLastName;
    public $shipToCompany;
    public $shipToAddress;
    public $shipToCity;
    public $shipToState;
    public $shipToZip;
    public $shipToCountry;
    
    public function getXml()
    {
        $xml = "<subscription>
    <name>{$this->name}</name>
    <paymentSchedule>
        <interval>
            <length>{$this->intervalLength}</length>
            <unit>{$this->intervalUnit}</unit>
        </interval>
        <startDate>{$this->startDate}</startDate>
        <totalOccurrences>{$this->totalOccurrences}</totalOccurrences>
        <trialOccurrences>{$this->trialOccurrences}</trialOccurrences>
    </paymentSchedule>
    <amount>{$this->amount}</amount>
    <trialAmount>{$this->trialAmount}</trialAmount>
    <payment>
        <creditCard>
            <cardNumber>{$this->creditCardCardNumber}</cardNumber>
            <expirationDate>{$this->creditCardExpirationDate}</expirationDate>
            <cardCode>{$this->creditCardCardCode}</cardCode>
        </creditCard>
        <bankAccount>
            <accountType>{$this->bankAccountAccountType}</accountType>
            <routingNumber>{$this->bankAccountRoutingNumber}</routingNumber>
            <accountNumber>{$this->bankAccountAccountNumber}</accountNumber>
            <nameOnAccount>{$this->bankAccountNameOnAccount}</nameOnAccount>
            <echeckType>{$this->bankAccountEcheckType}</echeckType>
            <bankName>{$this->bankAccountBankName}</bankName>
        </bankAccount>
    </payment>
    <order>
        <invoiceNumber>{$this->orderInvoiceNumber}</invoiceNumber>
        <description>{$this->orderDescription}</description>
    </order>
    <customer>
        <id>{$this->customerId}</id>
        <email>{$this->customerEmail}</email>
        <phoneNumber>{$this->customerPhoneNumber}</phoneNumber>
        <faxNumber>{$this->customerFaxNumber}</faxNumber>
    </customer>
    <billTo>
        <firstName>{$this->billToFirstName}</firstName>
        <lastName>{$this->billToLastName}</lastName>
        <company>{$this->billToCompany}</company>
        <address>{$this->billToAddress}</address>
        <city>{$this->billToCity}</city>
        <state>{$this->billToState}</state>
        <zip>{$this->billToZip}</zip>
        <country>{$this->billToCountry}</country>
    </billTo>
    <shipTo>
        <firstName>{$this->shipToFirstName}</firstName>
        <lastName>{$this->shipToLastName}</lastName>
        <company>{$this->shipToCompany}</company>
        <address>{$this->shipToAddress}</address>
        <city>{$this->shipToCity}</city>
        <state>{$this->shipToState}</state>
        <zip>{$this->shipToZip}</zip>
        <country>{$this->shipToCountry}</country>
    </shipTo>
</subscription>";
        
        $xml_clean = "";
        // Remove any blank child elements
        foreach (preg_split("/(\r?\n)/", $xml) as $key => $line) {
            if (!preg_match('/><\//', $line)) {
                $xml_clean .= $line . "\n";
            }
        }
        
        // Remove any blank parent elements
        $element_removed = 1;
        // Recursively repeat if a change is made
        while ($element_removed) {
            $element_removed = 0;
            if (preg_match('/<[a-z]+>[\r?\n]+\s*<\/[a-z]+>/i', $xml_clean)) {
                $xml_clean = preg_replace('/<[a-z]+>[\r?\n]+\s*<\/[a-z]+>/i', '', $xml_clean);
                $element_removed = 1;
            }
        }
        
        // Remove any blank lines
        // $xml_clean = preg_replace('/\r\n[\s]+\r\n/','',$xml_clean);
        return $xml_clean;
    }
}

Now, when you are creating a subscription, instead of adding a credit card, set a customer profile ID and customer payment profile ID, in order to create the subscription using the CIM profile(as opposed to using a credit card.) In the event that you have not added both a customerPaymentProfileId and a customerProfileId to the subscription, it should default to attempting to create it using a credit card or bank account.

Basic Example:


	$subscription = new AuthorizeNet_Subscription;
	$subscription->name = $subscription_name;
	$subscription->intervalLength = 1;
	$subscription->intervalUnit = 'months';
			
	$subscription->startDate = $start_date;
	$subscription->totalOccurrences = 24;
			
	$subscription->amount = $this->get_subscription_cost();
	$subscription->orderInvoiceNumber = $invoice_number;
	$subscription->orderDescription = $order_description;
			
	$subscription->customerPaymentProfileId = $payment_profile_id;
	$subscription->customerProfileId = $customer_profile_id;

Add Comment

Debugging Safari’s “A Problem has occurred with this webpage so it was reloaded.” Error

Published by John on May 18, 2017

safari_errorOne of my clients had a custom client portal built, which lets users login and edit things like their Name, Phone Number, and add a profile picture. During debugging, their staff found an issue that when uploading an image, they would sometimes get an error on the iphone:

A Problem has occurred with this webpage so it was reloaded.

The issue appeared to be intermittent and it took me a lot of testing before I identified the issue.

In this case, the old developers had used Cropbox, which is a plugin that lets you update profile pictures and hasn’t been updated in several years. From looking at the issue tracker, I found a few people indicating they were having issues with mobile safari, but no solutions.

This plugin lets you crop and zoom in on an image prior to uploading, storing it as a base64 encoded text blob in a hidden text field.

The issue only occurred if you cropped and enlarged the image. During testing I found that doing so greatly greatly increased the character count of the base64 image.

For example, without doing any zooming, the base64 image text might have a size of 250K Characters. However, if you hit zoom a couple times, this increased to over 700K characters and this is where the issue became present.

While I was unable to find any hard numbers on what is actually the max character count supported by an input, this appears to exceed it for iphone, but does not seem to cause any issues on most desktop browsers or android phones. To test and verify this was the issue, I created a simple form using an hidden input to store a quite long string:

<input type="hidden" name="test_variable" id="test_variable" value="<?php 
     
     for($i =0; $i < 7046000; $i++){
		 echo chr(rand(65,90));
	 } 
     
     ?>"class="">

I found that this would cause the error and replacing it with a textarea did not fix the issue.

I am evaluating the options, but given how old the cropbox module is at this point and the lack of support/development over the past couple years(including a report as early as 2015 of a similar issue,) I will likely be replacing it with a different solution.

Add Comment

Moving the Mysql Data Directory

Published by John on April 4, 2017

Recently, I ran into a space issue on one of my servers and found that 85% of the space was being used by Mariadb(Mysql’s) ibdata1 file and there wasn’t really anywhere else to trim any fat. The ibdata1 file is used to store information about Innodb tables and a rather well known issue is that this file grows over time, but does not shrink.

So, for example, if you were to have an InnoDB database table that was 1GB in size, it might increase ibdata1 by 1GB. Later, if you delete that table, the ibdata1 does not shrink. However, even though it does not shrink in size, it should keep track of how much free space is available and utilize that space first, before it starts growing again.

One of the common recommendations for dealing with this is to export all your databases(except for mysql and performance_schema), drop the databases, delete the ibdata1 and ib_log file, and then re-import them. This should reset your ibdata1 and if you also enable innodb_file_per_table it will result in individual ibdata1 files for each database, making this process less painful down the road if you need to do it again.*

* The above method of shrinking an ibdata1 file just a basic example/overview of the process. If you decide to go this route, please properly research it.

The downside of shrinking the ibdata1 that way is of course the time it is going to take dumping/restoring each database. In mycase, while I will still probabbly do this, I needed a quicker fix and so decided to move the Mysql Data directory to a different partition that had more space.

Moving the Mysql Data Directory

Before you do anything, use mysqldump to backup all your databases! Any sort of change like this has the potential to go wrong, so have a backup in place before you do anything.

The steps below are for a Centos Server, but the steps should be similar on other Linux distributions. The default location for the Mysql Data Folder on Centos is: /var/lib/mysql

You can find it by checking your /etc/my.cnf file and looking for the datadir setting or by using the following query: select @@datadir;

  1. Stop the Mysql Server. On Centos: service mariadb stop
  2. Copy the data folder to the new location, preserving file permissions: cp -rp /var/lib/mysql/ /my_new_location/
  3. Verify the permissions and size of new folder:
    ls -l /my_new_location/mysql
    du -h /my_new_location/mysql
    du -h /var/lib/mysql/
    
  4. Backup your current my.cnf file: cp /etc/my.cnf /etc/my.cnf.back
  5. Edit your my.cnf file to ensure the datadir and socket use the new location:
    [mysqld]
    datadir=/my_new_location/mysql
    socket=/my_new_location/mysql/mysql.sock
    
  6. Add or edit the client section, so it uses the new socket:
    [client]
    socket=/my_new_location/mysql/mysql.sock
    
  7. Start Mysql Server: service mariadb start
  8. Verify you can login to mysql server and access databases/tables.
  9. If you have issues, stop the server, revert to your old settings using the backup file you made(/etc/my.cnf.back) and restart the server.

After this, you should be able to login to mysql and it should be using the new location. To verify, you can run the query: select @@datadir;

It should show somthing like:

MariaDB [(none)]> select @@datadir;
+-------------------------+
| @@datadir               |
+-------------------------+
| /my_new_location/mysql/ |
+-------------------------+
1 row in set (0.00 sec)

Fixing Issues with Your Webserver

If you are running a web-server w/php(or another language) that access mysql, you will need to make sure it knows where to find the new socket. If you do not, you will get a database connection error, as it will not know where to find the new Mysql Socket.

For PHP, you can edit your php.ini file as follows:

mysqli.default_socket = /my_new_location/mysql/mysql.sock
pdo_mysql.default_socket= /my_new_location/mysql/mysql.sock
mysql.default_socket = /my_new_location/mysql/mysql.sock

Just make sure you use the same value as you did for the socket setting in your /etc/my.cnf file.

After you update your php.ini file, restart your webserver and you should be able to connect to the server.

Other languages, like Java, will probably need to be configured similarly.

Add Comment

Programatically Creating a Product or Customer Tax Class

Published by John on April 1, 2017

This is a short post today, but I was unable to find a good reference for manually creating tax classes.

If you need to create a Product Tax Class or a Customer Tax Class programatically, you can use the below code:

Creating a Product Tax Class


$tax_class = Mage::getModel('tax/class');
$tax_class->setClassName('my_new_tax_class')
	->tax_class->setClassType('PRODUCT');
$tax_class->save();

Creating a Customer Tax Class


$tax_class = Mage::getModel('tax/class');
$tax_class->setClassName('my_new_tax_class')
	->tax_class->setClassType('CUSTOMER');
$tax_class->save();

Add Comment
Next Page »