A JSON-RPC Adapter For The Magento API

Going through my old answers on Magento StackExchange, I found this question about using the Magento API via JavaScript and noticed that the link to GitHub that contained an important part of the solution, namely implementing a JSON-RPC adapter for the Magento-API is dead now.

So I decided to publish my complete module myself (the original link was a core hack, I rewrote it as a clean module):

GitHub: SGH_JsonRpc

The whole module is less than 100 lines of code. In config.xml our controller is added to the api route:

    <frontend>
        <routers>
            <api>
                <args>
                    <modules>
                        <sgh_jsonrpc before="Mage_Api">SGH_JsonRpc_Api</sgh_jsonrpc>
                    </modules>
                </args>
            </api>
        </routers>
    </frontend>

The new API adapter is defined in api.xml:

Continue reading “A JSON-RPC Adapter For The Magento API”

Efficiently Increase/Decrease Magento Attributes

Magento.SE Screenshot

This question arose on Magento StackExchange:

I need to decrement a value with an atomic database operation, is it possible using Magento models?

It is in fact possible, with a lesser known technique using Zend_Db_Expr. I’ll share it here as well:

$object->setNumber(new Zend_Db_Expr('number-1'));

For reference:

The method Mage_Core_Model_Resource_Abstract::_prepareDataForSave() contains the following code:

if ($object->hasData($field)) {
    $fieldValue = $object->getData($field);
    if ($fieldValue instanceof Zend_Db_Expr) {
        $data[$field] = $fieldValue;
    } else {
        ... [normal value processing follows]

EAV Models:

Note that you only can reference the attribute by its name (“number” in the example) if it’s a real column of the main table, not an EAV attribute.

Although the abovementioned method is only used by models with flat tables, Zend_Db_Expr can be used for EAV attributes as well, the method that handles it is Varien_Db_Adapter_Pdo_Mysql::prepareColumnValue().

BUT you always have use the column name “value“:

$product->setNumber(new Zend_Db_Expr('value-1'));

You don’t need to specify a table alias because during save each attribute gets processed with its own query, so “value” is not ambiguous.

Magento: Direct Link To Tab in Adminhtml Tab Widgets

For an extension I was recently working on, I wondered if there was a built-in way to link directly to a tab on a backend page. My research didn’t result in anything useful (read: my Google foo had failed me), so I dug into the core code to see where to start. I’ll share what I found out.

The Problem

In particular, I wanted to link to the “Manage Label / Options” tab on the “Edit Product Attribute Page”:

Screenshot

The Solution

Actually it’s possible with a URL parameter ?active_tab=$id.

How To Find The Right Tab ID

Find the responsible tab container class. This is a child class of Mage_Adminhtml_Block_Widget_Tabs, in my case, Mage_Adminhtml_Block_Catalog_Product_Attribute_Edit_Tabs.

You’ll find calls to $this->addTab(), usually in the methods _beforeToHtml(), or _construct(). The first parameter of addTab() is the tab id:

    $this->addTab('labels', array(
        'label'     => Mage::helper('catalog')->__('Manage Label / Options'),
        'title'     => Mage::helper('catalog')->__('Manage Label / Options'),
        'content'   => $this->getLayout()->createBlock('adminhtml/catalog_product_attribute_edit_tab_options')->toHtml(),
    ));

So, the URL is /admin/catalog_product_attribute/edit/attribute_id/123/?active_tab=labels, generated with this code (within an adminhtml block):

    $this->getUrl('adminhtml/catalog_product_attribute/edit',
        array('attribute_id' => 123, '_query' => array('active_tab' => 'labels'));

How it works

Let’s have a look at the responsible code:
Continue reading “Magento: Direct Link To Tab in Adminhtml Tab Widgets”

Magento ACL: No More “Log Out And Back In” After Installing Extensions

In almost every Magento extension installation guide you find the step “Log out and log in again” which is necessary to gain access to new sections in the backend menu or system configuration. But why exactly do we put up with it? The problem is that the access control list (ACL) is loaded only on login and then cached in the session. Loading it on every request is no viable alternative because it would slow down the backend too much.

But with just a few lines of code we can make reloading the ACL a little more comfortable.

The Code

This controller action reloads the ACL by request:

class SSE_AclReload_Adminhtml_Permissions_AclReloadController
    extends Mage_Adminhtml_Controller_Action
{
    public function indexAction()
    {
        $session = Mage::getSingleton('admin/session');
        $session->setAcl(Mage::getResourceModel('admin/acl')->loadAcl());

        Mage::getSingleton('adminhtml/session')->addSuccess(
            $this->__('ACL reloaded'));
        $this->_redirectReferer();
    }
}

This template provides a button that links to the new controller:

<?php
$request = Mage::app()->getRequest();
?>
<a href="<?php echo $this->getUrl('adminhtml/permissions_aclReload/index'); ?>">
<?php echo $this->__('Reload ACL'); ?>
</a>

And this layout update adds the button to error 404 pages in the backend (those that you get when you don’t have access to a page):

    <adminhtml_noroute>
        <reference name="content">
            <block type="adminhtml/template" name="content.aclReload"
                after="content.noRoute" template="sse_aclreload/button.phtml" />
        </reference>
    </adminhtml_noroute>

The Result

Screenshot Magento Admin 404

Continue reading “Magento ACL: No More “Log Out And Back In” After Installing Extensions”

TranslationHints 0.2 For Magento Published

Screenshot: Translation Hint

I released version 0.2 of my Magento extension SSE_TranslationHints, a developer tool that shows the source of translations together with alternative overridden translations for the same string directly in the frontend.

The configuration is still done in the same way as template hints:

Screenshots: Translation Hints Configuration

News

Together with the source of the translation you see alternative translations that have been overridden by the actual source, and some additional data.

In the following example you see the scope of the translation (Mage_Customer), the translation for this scope, as well as the translation that would be used for global scope, i.e. if there was no scope specific translation. The CACHED tag tells us that the translations have been loaded from translation cache:

Screenshot: Translation Hint

Continue reading “TranslationHints 0.2 For Magento Published”

Mock Admin Session With EcomDev_PHPUnit In Magento Enterprise

In integration tests with EcomDev_PHPUnit_Test_Case_Controller there is a useful helper method adminSession(), to test requests in the Magento backend. With Magento Enterprise you might get this error message if you use it:

Exception: Warning: in_array() expects parameter 2 to be array, null given in /home/vagrant/mwdental/www/app/code/core/Enterprise/AdminGws/Model/Role.php on line 83

As seen on Magento EE 1.14.1.

Without disclosing details about the Enterprise modules, here a solution where the responsible observer gets mocked:

$adminObserverMock = $this->getModelMock(
    'enterprise_admingws/observer',
    array('adminControllerPredispatch'));
$adminObserverMock->expects($this->any())
    ->method('adminControllerPredispatch')
    ->will($this->returnSelf());
$this->replaceByMock('singleton', 'enterprise_admingws/observer',
    $adminObserverMock);
$this->adminSession();

Magento: Lost Block Content After core_block_to_html_after Event

Be careful if you use Mage_Core_Block_Abstract::toHtml() inside an observer for core_block_to_html_after. I did that to inject HTML from another block into an existing block with the result that everything except this new block was removed from output.

Digging through the core, I found the cause in how the event is dispatched:

        /**
         * Use single transport object instance for all blocks
         */
        if (self::$_transportObject === null) {
            self::$_transportObject = new Varien_Object;
        }
        self::$_transportObject->setHtml($html);
        Mage::dispatchEvent('core_block_abstract_to_html_after',
            array('block' => $this, 'transport' => self::$_transportObject));
        $html = self::$_transportObject->getHtml();

As you see, the transport object that is used to allow modification of the HTML in observers, is a class variable of Mage_Core_Block_Abstract, so basically acts like a singleton. I cannot think of a good reason for this, but it smells ridiculously like premature optimization.

Now what’s happening is, that during the event is processed, the new block sets its own output to the same transport object in its toHtml() method and the original output is lost, if you didn’t save it before.

The solution to my particular problem was to generate and pre-render the new block elsewhere but if you don’t, you need to copy the output at the very beginning:

$html = $observer->getTransport()->getHtml(); // should be the first statement

// now you can do what you want, create other blocks, modifiy $html ...

$observer->getTransport()->setHtml($html); // should be the last statement

Magento Tutorial: How to Use Increment Models to Generate IDs (or SKUs)

Did you ever wonder how Magento generates the increment_id values for orders, invoices etc. and how to use or extend this mechanism? Maybe you found the eav_entity_store table which contains the last increment id per entity type and store and possibly a different prefix per store:

mysql> select * from eav_entity_store;
+-----------------+----------------+----------+------------------+-------------------+
| entity_store_id | entity_type_id | store_id | increment_prefix | increment_last_id |
+-----------------+----------------+----------+------------------+-------------------+
|               1 |              5 |        1 | 1                | 100000090         |
|               2 |              6 |        1 | 1                | 100000050         |
|               3 |              8 |        1 | 1                | 100000027         |
|               4 |              7 |        1 | 1                | 100000005         |
|               5 |              1 |        0 | 0                | 000000011         |
|               6 |              5 |        2 | 2                | 200000001         |
|               7 |              5 |        3 | 3                | 300000002         |
|               8 |              8 |        3 | 3                | 300000001         |
|               9 |              6 |        3 | 3                | 300000001         |
+-----------------+----------------+----------+------------------+-------------------+
9 rows in set (0.00 sec)

I will explain how to use this system in other entities in this article.

First of all, the standard method only works with EAV entities and only one attribute per entity can use the increment model and its name must be increment_id. I will explain later, how to overcome these limitations.

Standard Usage

The increment_id attribute should have the backend model eav/entity_attribute_backend_increment and the entity itself needs some additional configuration;

Entity Setup

Example: Setup Script
$installer->installEntities(array(
            'your_entity' => array(
                'entity_model' => 'your/entity_resource',
                'table' => 'your_entity_table',
                'increment_model' => 'eav/entity_increment_numeric',
                'increment_per_store' => 0,
                'increment_pad_length' => 8,
                'increment_pad_char' => '0',
                'attributes' => array(
                    'increment_id', array(
                        'type'      => Varien_Db_Ddl_Table::TYPE_TEXT,
                        'backend'   => 'eav/entity_attribute_backend_increment,
                    ),
                    // ... other attributes
                ),
            ),
        ));

Let’s have a look at the relevant fields:

  • increment_model: The model that is responsible for generating the increment ids. Either one of eav/entity_increment_numeric and eav/entity_increment_alphanum or a custom model. More about custom increment models below.
  • increment_per_store: (default: 0) 0 = the increment id is global for this entity, 1 = the increment id is incremented per store. You can set up a different prefix per store (see below)
  • increment_pad_length: (default: 8) the minimum length of the id. Shorter ids will be left padded with increment_pad_char (default: 0).
Notes
  1. If you want to save the increment id as a field in the main table, use static as type instead.
  2. For existing entities you can use updateEntityType instead of installEntityType

Prefix per Store

If you set “increment_per_store” to “1” in the entity setup, the increment ids get prefixed with the store_id by default, if you set it to “0” (global), they get prefixed with “0”. To set up different prefixes, use the Mage_Eav_Model_Entity_Store model. The corresponding database table eav_entity_store shown above gets automatically filled with one entry per entity and store if the entity has “increment_per_store” set, otherwise with only one entry per entity with store_id 0.
The table contains the prefix as well as the last increment id (which both should be used by the increment model to determine the next id).

Example: Set last id and prefix for product
        $productEntityType = Mage::getModel('eav/entity_type')
            ->loadByCode(Mage_Catalog_Model_Product::ENTITY);
        $entityStoreConfig = Mage::getModel('eav/entity_store')
            ->loadByEntityStore($productEntityType->getId(), 0);
        $entityStoreConfig->setEntityTypeId($productEntityType->getId())
            ->setStoreId(0)
            ->setIncrementPrefix($prefix)
            ->setIncrementLastId($lastId)
            ->save();

In this example, the global prefix (store=0) for the product entity is set to $prefix and the last id to $lastId. Usually this would only be called once from a setup script and once per store after store creation. Note that the automatically generated entries are only generated as soon as a new increment id for the according store is requested and no entry exists yet. The code is in Mage_Eav_Model_Entity_Type::fetchNewIncrementId():

Core Code:
    public function fetchNewIncrementId($storeId = null)
    {
    ...
        if (!$entityStoreConfig->getId()) {
            $entityStoreConfig
                ->setEntityTypeId($this->getId())
                ->setStoreId($storeId)
                ->setIncrementPrefix($storeId)
                ->save();
        }
    ...
    }

Special Case: Arbitrary Attribute

If we take a look at the backend model, we see that it checks if the object is new (i.e. does not have an id yet) and in this case delegates the increment id creation to the entity resource model itself:

Core code
/**
 * Entity/Attribute/Model - attribute backend default
 *
 * @category   Mage
 * @package    Mage_Eav
 * @author      Magento Core Team <core@magentocommerce.com>
 */
class Mage_Eav_Model_Entity_Attribute_Backend_Increment extends Mage_Eav_Model_Entity_Attribute_Backend_Abstract
{
    /**
     * Set new increment id
     *
     * @param Varien_Object $object
     * @return Mage_Eav_Model_Entity_Attribute_Backend_Increment
     */
    public function beforeSave($object)
    {
        if (!$object->getId()) {
            $this->getAttribute()->getEntity()->setNewIncrementId($object);
        }

        return $this;
    }
}

As you can see, the entity resource model does not get any information about the attribute itself and indeed, setNewIncrementId is hard coded to use the attribute increment_id (getIncrementId() and setIncrementId()):

Core code

    /**
     * Set new increment id to object
     *
     * @param Varien_Object $object
     * @return Mage_Eav_Model_Entity_Abstract
     */
    public function setNewIncrementId(Varien_Object $object)
    {
        if ($object->getIncrementId()) {
            return $this;
        }

        $incrementId = $this->getEntityType()->fetchNewIncrementId($object->getStoreId());

        if ($incrementId !== false) {
            $object->setIncrementId($incrementId);
        }

        return $this;
    }

There are two ways to overcome this limitation:

  1. Implement setIncrementId() and getIncrementId() in your entity to access the actual incremented attribute.
  2. Extend the backend model and override beforeSave() to assign the generated increment id to the actual attribute afterwards. A simple version could look like this:
    class Your_Awesome_Model_Entity_Attribute_Backend_Increment extends
            Mage_Eav_Model_Entity_Attribute_Backend_Increment
    {
        public function beforeSave($object)
            parent::beforeSave($object);
            $object->setData($this->getAttribute()->getName(), $object->getIncrementId());
            return $this;
        }
    }

Special Case: Non EAV Models

As you probably know, orders, invoices etc. are no EAV entities 1 but they still have entries in the entity_type table and use increment ids. If they can, you can too, so let’s see how it has been done for orders.

The entity type is registered just as a real EAV entity:

Core Code: Setup Script
/**
 * Install eav entity types to the eav/entity_type table
 */
$installer->addEntityType('order', array(
    'entity_model'          => 'sales/order',
    'table'                 => 'sales/order',
    'increment_model'       => 'eav/entity_increment_numeric',
    'increment_per_store'   => true
));

Because there is no EAV attribute that could use the backend model, setting the increment id must be triggered from the order model itself:

Core Code: Order Model
    protected function _beforeSave()
    {
    ...
        if (!$this->getIncrementId()) {
            $incrementId = Mage::getSingleton('eav/config')
                ->getEntityType('order')
                ->fetchNewIncrementId($this->getStoreId());
            $this->setIncrementId($incrementId);
        }
    ...
    }

And that’s it!

Writing Custom Increment Models

You can specify any class as increment model that implements Mage_Eav_Model_Entity_Increment_Interface. Be aware: This interface pretents to only need one method, getNextId(), but at least the following setters will be called as well:

  • setPrefix
  • setPadLength
  • setPadChar
  • setLastId
  • setEntityTypeId
  • setStoreId

Yeah, Magento doesn’t give much love to interfaces. So if you want to implement your own increment model, you should at least inherit from Varien_Object, better from Mage_Eav_Model_Entity_Increment_Abstract which already provides you with the prefix and padding logic.

In the method getNextId() you will then generate the next increment id based on the last one, that is accessible with $this->getLastId()

Full Example: AutoSKU

A real-life example is my AutoSKU extension, which assigns product SKUs automatically. To achieve this, I set up an increment model for the catalog_product entity, changed the backend model of the SKU attribute, set it to be not required and made it uneditable. Check out the Github repository for implementation details:

https://github.com/schmengler/AutoSKU Continue reading “Magento Tutorial: How to Use Increment Models to Generate IDs (or SKUs)”

Notes:

  1. Once upon a time, they were, and the “flat” tables used to be just index tables.

Mage Unconference 2015 – March 7.-8. – Berlin

Mage Unconference 2015

On March 7th and 8th FireGento presents the first Mage Unconference for Clients, Merchants, Agencies, Service-Providers – and for sure – Developers. The Schedule of the Unconference is whatever the attendees make out of it.

>>> Get our Ticket now and be part of the Community on March 7th and 8th, 2015 in Berlin! <<<

I will be there! If you are interested, don’t wait too long before the event gets cancelled due to not enough tickets being sold!