Wednesday 20 May 2009

How many times can I fall victim to the same bug?

I just lost about an hour tracking down a problem resulting from a naive transfer of a running Magento site to another server. If you simply use an old mysqldump client, or phpmyadmin your dump file will be corrupted, don't use it. Use mysqldump from 4.1 or later.

So, the problem revolves around magento using a frown-upon practice of staring primary keys at 0. Normally, in mysql, if you insert a value of NULL or 0 into a primary key field, it will run its sequence generator and give you the next sequence. Combine this fact with the nicety of your dump files specifying which auto increment to start at in the table definition, and you can say goodbye to your admin store (store_id 0) after you import.

The mysqldump tool will spot this and insert a call to change the SQL mode to "NO_AUTO_VALUE_ON_ZERO" for you, but phpmyadmin will not! Also note that Magento's own DB dump feature will put in "NO_AUTO_VALUE_ON_ZERO" for you as well.

Remind me to get rid of this bad, highly error prone, and frustrating "feature" of Magento - primary keys starting at 0 have to go.

Monday 18 May 2009

checkout_cart_product_add_before

There is no event in Magento called "checkout_cart_product_add_before", but it is sorely missed.

Dealing only with "checkout_cart_product_add_after" means that you cannot easily massage the input data for a product. Basically, what this means is, if you are trying to manipulate product attributes which aren't submitted directly in the POST data, then you're in for a rough ride.

If your attributes aren't within certain bounds, or are completely non-existent, then you can't ever get your product into the cart. One solution is to make the attributes not "required" and massage the POST data into your attribute data after adding the product to your cart. But that just means that you have to duplicate all the bounds checking yourself, and go through the extra step of removing the item if it fails attribute validity checking (see: core/Mage/Catalog/Model/Product/Type/Abstract.php: public function prepareForCart)

Going to add the event checkout_cart_product_add_before to Agento-Ohm.

Thursday 14 May 2009

Security Issue

I've found a security issue which may or may not be related to other bug posts on the magento bug tracker. I would not recommend anybody use the back-end order creation tool in Magento or Agent-Ohm until further notice.

Update: It is not recommended to run any multi-store setup under Magento or Agento-Ohm.

Update: Fixed in the latest Agento-Ohm

Thursday 7 May 2009

Admin pages fixed

So, my URL flyweight change had some unexpected problems. Basically, I switched a normal object method into a static one. Any problems with object access like $url->getUrl() should throw an error, right? I should be able to find and fix any old method access. Not so in PHP.

class Foo {
public static function bar() {
echo "foo\n";
}
}
class Zap extends Foo {
public static function bar() {
parent::bar();
}
}
$z = new Zap();
$z->bar();


In PHP, the parent:: token is ambiguous. It works equally well in static and object calling contexts. Basically, it passes along the calling context to the next function up. If you had declared bar() previously as non-static, you wouldn't notice any errors in this setting. But the behavior of calling Zap::bar() would change drastically.

The point is, if you're changing a method from non-static to static, you need to be aware of the internals of all the functions which subclass it. If they call parent::bar(), the result could be unexpected. In the above example, $z would not be modified at all, and no errors, warnings, nor E_STRICTS would be thrown.

Tuesday 5 May 2009

Email Templating

I'm trying to work through some transactional e-mail templating issues. I'm trying to figure out where the "items_html" gets created. In the database I can see

{{var items_html}}

but, I can't find any reference to get/set ItemsHtml() in the code. What I find in the layout XML isn't very promising.

So, I setup a small bootstrap script to get ready to send these emails to myself and then I can get into the code and dissect what's going on.

chdir('../../../../../');
require_once 'app/Mage.php';
umask(0);
Mage::app('default');


$mailTemplate = Mage::getModel('core/email_template');
$recipient = array();
$recipient['email'] = 'me@example.com';
$recipient['name'] = 'me';

$order = Mage::getModel('sales/order')->load(4);

$mailTemplate->setDesignConfig(array('area'=>'frontend', 'store'=>0))
->sendTransactional(
'sales_email_order_template',
array('name'=>'me', 'email'=>'me@example.com'),
$recipient['email'],
$recipient['name'],
array(
'order' => $order,
'billing' => $order->getBillingAddress(),
'payment_html' => '
payment html
',
)
);


It looks okay, I should be able to at least trigger the transactional emails and trace through the code with var_dump() until I can find where items_html is set or used.

There's only one problem. The $order relies on using blocks to format its own "createdat" property, and the blocks don't always work unless you have a front controller and an action. Why? ... because the output templates are tied into the request action.. ?


// from Sales/Model/Order.php
public function getCreatedAtFormated($format)
{
return Mage::getBlockSingleton('core/text')->formatDate($this->getCreatedAt(), $format);
}


//from Mage.php
public static function getBlockSingleton($type)
{
$action = Mage::app()->getFrontController()->getAction();
return $action ? $action->getLayout()->getBlockSingleton($type) : false;
}

Why, oh why, can't I just send a transactional email without having to simulate an entire request?

Item Options

Recently I was trying to debug some MadetoOrder (MTO Homepage) code and found a really odd behavior of the order item (Mage_Sales_Model_Order_Item). I was in the middle of working on a function which prints out special order options and found that

the functions you need to call to get the options ordered are dependent on whether or not the order is placed in the database or still in the customer's session.


//sometimes you call:
$item->getOptionsById()
//sometimes you call:
$item->getProductOptionsById()

//so you always have to do:
$buyRequest = $item->getProductOptionsById('info_buyRequest');
if ($buyRequest === NULL) {
$buyRequest = $item->getOptionsById('info_buyRequest');
}
if ($buyRequest === NULL) {
//do real error handling here, order item
//really doesn't have a buy request.
}



Which do you call when? It doesn't really matter, you have to do both all the time in one function and check for null, then call the other one. I was so horrified by my finding that I didn't bother checking which one was for the placed order and which ones was for the session.

So, if you want to change the way a product looks or deals with options, and you want your change to appear the same way in the "Your Cart" page as it does in the "Your Order History" page... you gotta use both for no apparent reason.

Don't believe me? Try it. How does Magento deal with this? Well, they don't really re-use code, so it doesn't affect them. The block for the shopping cart contains functions which are different than the block for the my account page.

Friday 1 May 2009

How not to use func_get_args()


public function __construct()
{
$args = func_get_args();
if (empty($args[0])) {
$args[0] = array();
}
$this->_data = $args[0];
$this->_construct();
}


instead, do...


public function __construct($data = array())
{
$this->_data = $data;
$this->_construct();
}