Tab is rendered in left block instead of content

I want to add tabs to a custom form in the backend.

There are a couple of great tutorials how to add tabs, like from Inchoo or Erfan Imani.

But I wanted this:

And got this:

The problem is simple - if you know it (costs me an hour and a few hairs):

The tabs are rendered with JS. So they live originally in left, but they are shown in content, to be more specific in the node, defined by:

class My_Module_Block_Adminhtml_Project_Edit_Tabs extends Mage_Adminhtml_Block_Widget_Tabs {

    public function __construct()
    { 
        // ...
        $this->setDestElementId('project_form');
    }
}

And this id needs to be the same as in

class My_Module_Block_Adminhtml_Project_Edit_Form extends Mage_Adminhtml_Block_Widget_Form
{
    protected function _prepareForm()
    {
        $form = new Varien_Data_Form(
            [
                'id'     => 'project_form',
            ]);
    }
}

Hope this helps anyone.

Magento doesn't trigger setup script

I found a nice new, rarely trigger Magento bug.

In one of our projects, we have an example file to create new products:

data-upgrade-example-new-product-import.php

Beside this we had data scripts:

data-install-1.0.1.php
data-upgrade-1.0.0-1.0.1.php
data-upgrade-1.0.5-1.0.6.php

Unfortunately data-upgrade-1.0.5-1.0.6.php didn't fire.

It took me a while, but I found the problem:

// \Mage_Core_Model_Resource_Setup::_getModifySqlFiles
protected function _getModifySqlFiles($actionType, $fromVersion, $toVersion, $arrFiles)
{
    $arrRes = array();
    switch ($actionType) {

        // ... 

        case self::TYPE_DB_UPGRADE:
        case self::TYPE_DATA_UPGRADE:
            uksort($arrFiles, 'version_compare');
            foreach ($arrFiles as $version => $file) {
                $versionInfo = explode('-', $version);

                // In array must be 2 elements: 0 => version from, 1 => version to
                if (count($versionInfo)!=2) {
                    break;
                }

in $arrFiles we find an array with all files in the data dir which match a certain regex in \Mage_Core_Model_Resource_Setup::_getAvailableDataFiles. In short, when the files starts with data-.

The problem is, that data-upgrade-example-new-product-import.php doesn't meet the if (count($versionInfo)!=2) check and then break is called, which kills the complete loop, but should only be continue;.

So either we hack the core or rename the data-upgrade-example-new-product-import.php, I decided for renaming.

Empty order emails (no subject, no body)

One of my customers had a problem this week:

Emails looked like this:
empty email, no body, no subject

It was literally empty. The server filled a few head fields, but there was nothing left of the content we want to send (new order email).

It took a while, but finally I think I found the problem (unfortunatelly not sure about the cause).

Magento saves stuff while going down this trace:

\Mage_Core_Model_Abstract::save
\Mage_Core_Model_Resource_Db_Abstract::save
\Mage_Core_Model_Resource_Db_Abstract::_prepareDataForSave
\Mage_Core_Model_Resource_Abstract::_prepareDataForTable
\Varien_Db_Adapter_Pdo_Mysql::describeTable

This method should return an array like this:

...
[created_at] => Array
        (
            [SCHEMA_NAME] =>
            [TABLE_NAME] => core_email_queue
            [COLUMN_NAME] => created_at
            [COLUMN_POSITION] => 8
            [DATA_TYPE] => timestamp
            [DEFAULT] =>
            [NULLABLE] => 1
            [LENGTH] =>
            [SCALE] =>
            [PRECISION] =>
            [UNSIGNED] =>
            [PRIMARY] =>
            [PRIMARY_POSITION] =>
            [IDENTITY] =>
        )

    [processed_at] => Array
        (
            [SCHEMA_NAME] =>
            [TABLE_NAME] => core_email_queue
            [COLUMN_NAME] => processed_at
            [COLUMN_POSITION] => 9
            [DATA_TYPE] => timestamp
            [DEFAULT] =>
            [NULLABLE] => 1
            [LENGTH] =>
            [SCALE] =>
            [PRECISION] =>
            [UNSIGNED] =>
            [PRIMARY] =>
            [PRIMARY_POSITION] =>
            [IDENTITY] =>
        )
...

But it returned

[data] => a:9:{s:10:"message_id";a:14:{s:11:"SCHEMA_NAME";N;s:10:"TABLE_NAME";s:16:"core_email_queue";s:11:"COLUMN_NAME";s:10:"message_id";s:15:"COLUMN_POSITION";i:1;s:9:"DATA_TYPE";s:3:"int";s:7:"DEFAULT";N;s:8:"NULLABLE";b:0;s:6:"LENGTH";N;s:5:"SCALE";N;s:9:"PRECISION";N;s:8:"UNSIGNED";b:1;s:7:"PRIMARY";b:1;s:16:"PRIMARY_POSITION";i:1;s:8:"IDENTITY";b:1;}s:9:"entity_id";a:14...

As you might guess, this is a serialized array.

When we have a look into \Varien_Db_Adapter_Pdo_Mysql::loadDdlCache we see, that the schema is cached, but I think there is no way, the serialized data are returned:

// lib/Varien/Db/Adapter/Pdo/Mysql.php:1548
$data = $this->_cacheAdapter->load($cacheId);
if ($data !== false) {  
    $data = unserialize($data);

To be continued...

REAL file sizes on Mac OS X

I'm searching for exceptions containing logic, not only class ... extends ..., so I searched for a way to get the real file size.

What didn't work

# only lists directories
find . -type f | du | grep Exception

# lists files in block size (4kb) which isn't accurate enough
find . -type f | du -a | grep Exception

What I ended up with:

find . -type f -print0 | xargs -0 stat -f "%z - %N" | grep Exception | sort

(x)Debug PHP CLI script via SSH without port forwarding

There are lots of examples on the net which all tell you that you should forward port 9000 to your local machine. This is helpful and needed if you want to debug a "real" remote machine, like a production or staging server.

But I want to debug a CLI script on a virtual machine (VM) and want a script which connects back on my local network to my host machine.

I know puphpet is doing this with the builtin xdebug script, but I can't find it on the net, so here it is:

#!/bin/bash
XDEBUG_CONFIG="idekey=xdebug" php -dxdebug.remote_host=`echo $SSH_CLIENT | cut -d "=" -f 2 | awk '{print    $1}'` "$@"

Just put it in a file ~/bin/xdebug and make it executable, then you can run your cli file with

xdebug file.php

and it should reconnect to your PHPStorm (or whatever other IDE you use).

Magento Core Cache Bug

Thanks very much to my colleagues from iWelt who found this bug and allow me to publish it here to get all the reputation ;-)

Magento can overwrite a cached block with a different one.

Magento Cache Key

Magento generates the cache key by default using the array returned by getCacheKeyInfo:

\Mage_Core_Block_Abstract::getCacheKey
public function getCacheKey()
{
    if ($this->hasData('cache_key')) {
        return $this->getData('cache_key');
    }
    $key = $this->getCacheKeyInfo();
    ...
}

The default implementation of getCacheKeyInfo only takes the name in the layout in consideration:

\Mage_Core_Block_Abstract::getCacheKeyInfo
public function getCacheKeyInfo()
{
    return array(
        $this->getNameInLayout()
    );
}

So far so good.

What is the name of a block, which is created WITHOUT a name?

Block name if no name is given

public function createBlock($type, $name='', array $attributes = array())
{
    ...  
    $block = $this->_getBlockInstance($type, $attributes);
    ...
    $name = 'ANONYMOUS_'.sizeof($this->_blocks);
    ...
    $block->setNameInLayout($name);
    ...
    $this->_blocks[$name] = $block;
    ...
    }
}

As you can see, the name of the block is set automatically. In case you don't know sizeof: it's an alias for count.

So the raising number is the current block count of the whole page.

This means, the name of the block is ANONYMOUS_1, ANONYMOUS_2, etc.

This means, the cache key is generated from [ANONYMOUS_1].

The problem

Now imagine, you have two different pages, which have a block, without a name. These two blocks accidentally have the same sequential number during generation.

Same cache key means: one entry

So in the end you'll get the same cache key and this means: Magento treats it as the same block.

In our case the homepage content block was overwritten by a block of the sidebar.

Solution

My colleague came up with this solution:

We add the current action to the block name:

$name = 'ANONYMOUS_'.sizeof($this->_blocks).Mage::app()->getFrontController()->getRequest()->getPathInfo();

We gave our best to fix the problem itself (which is a wrong generated cache key), but there is no event somewhere in the context of name generation and cache key generation. And because the key is generated in abstract class Mage_Core_Block_Abstract the only solution would be to copy the class to local/Mage which is no solution by definition.

So the rewrite of Mage_Core_Model_Layout is our best bet.