Question

I want to delete a store view programmatically. Looking at Mage_Adminhtml_System_StoreController::deleteStorePostAction(), this is pretty easy (shortened code a little bit):

$model = Mage::getModel('core/store')->load($id);

if ($model->getId() && $model->isCanDelete()) {
    $model->delete();
    Mage::dispatchEvent('store_delete', array('store' => $model));
}

I want to put this code into a data upgrade script so that the deletion will be executed automatically.

The problem is that while executing the upgrade scripts in data/ Magento only calls event observers configured in the global area (see Magento Structure Updates vs. Data Updates). Certain observers like enterprise_cms and enterprise_search for the event store_delete_after are defined in the adminhtml area so they won't get executed. The store view deletion won't be handled like a deletion executed in the backend.

How do you handle operations like this? Load additional event areas yourself in the upgrade scripts (I'm afraid of that)? Don't do data modifications like that in the upgrade script but put your magical scrips in a sacred hidden place and execute it manually?

Was it helpful?

Solution

So shortly after I tweeted this to Matthias I went on radio silence. I hope you felt the suspense as you've awaited this answer for a couple weeks now.

What I mean by "I do these in a queue" is in direct response to:

Certain observers like enterprise_cms and enterprise_search for the event store_delete_after are defined in the adminhtml area so they won't get executed. The store view deletion won't be handled like a deletion executed in the backend.

Queue Method:

When I know that there are certain events that won't fire in the correct context (mostly for EE, but may apply in other contexts) I typically shove the deletion out to a queue so that it will run in the context that it needs to.

In other words, create a queue table (or queue/topic in RabbitMQ, etc.) that would contain details of the transaction and the event hooks that it should be listening to. This can be as elegant or as simplistic as you want it to be. Here's a basic

$queue = Mage::getModel('yourcompany/queue_job')
         ->setJobType('delete')
         ->setEntityType('core/store')
         ->setEntityId(12)
         ->setDispatchEvent('store_delete')
         ->setDispatchEventDataKey('store')
         ->save();

And then work the queue later in a CRON, where you now have control over which store is "running" (aka you're just running it as if it were the admin, store 0):

foreach(Mage::getModel('yourcompany/queue_job')->getCollection() as $job){
    if($job->getJobType()=='delete'){

        $model = Mage::getModel($this->getEntityType())->load($this->getEntityId());

        if ($model->getId() && $model->isCanDelete()) {
            $model->delete();
            Mage::dispatchEvent($job->getDispatchEvent(), array($job->setDispatchEventDataKey() => $model));
        }
    }
}

Obviously if you were getting fancy you wrap in a try/catch and wrap in a transaction. I think you get the gist.

This is feasibly the only way to control the context in which the event fires.

Tandem event method:

You can fire the "adminhtml" method yourself manually - Alan gives a pretty decent explanation of what you would do to affect that, but essentially it's the same as this:

#File: app/code/core/Mage/Adminhtml/controllers/CustomerController.php
public function saveAction()
{
    //...
    $customer->save();
    //...
    Mage::dispatchEvent('adminhtml_customer_prepare_save', array(
        'customer'  => $customer,
        'request'   => $this->getRequest()
    ));        
    //..
}

The admin version of the customer save calls the regular model save and dispatches the adminhtml event afterward. You could do the reverse of this in an observer yourself if you so desired.

OTHER TIPS

Dammit, I love me some philwinkle, but I have to disagree with the complexity/brittleness of transporting the task parameters and the area (adminhtml | crontab | frontend | global | install) to a queue, especially if that queue is going to be executing a Magento context. If there are mixed contexts which need handling then the queue solution is a reimplementation of the current "issue"!

I think the queue approach is brittle. My argument is that loading event areas prematurely is not really an issue at all. To explain this, let's back up and look at the problem:

What is the danger of loading an event area prematurely in an execution scope?

To understand this we must examine event areas in the execution context. Matthias, I imagine that you already know this, but for others' edification:

Data setup scripts are executed in Mage_Core_Model_App::run() prior to dispatching the request to the Front Controller:

public function run($params)
{
    $options = isset($params['options']) ? $params['options'] : array();
    $this->baseInit($options);
    Mage::register('application_params', $params);

    if ($this->_cache->processRequest()) {
        $this->getResponse()->sendResponse();
    } else {
        $this->_initModules();
//Global event area is loaded here
        $this->loadAreaPart(Mage_Core_Model_App_Area::AREA_GLOBAL, Mage_Core_Model_App_Area::PART_EVENTS);

        if ($this->_config->isLocalConfigLoaded()) {
            $scopeCode = isset($params['scope_code']) ? $params['scope_code'] : '';
            $scopeType = isset($params['scope_type']) ? $params['scope_type'] : 'store';
            $this->_initCurrentStore($scopeCode, $scopeType);
            $this->_initRequest();
//Data setup scripts are executed here: 
            Mage_Core_Model_Resource_Setup::applyAllDataUpdates();
        }

        $this->getFrontController()->dispatch();
    }
    return $this;
}

By the time data setup scripts are executing the global event area is loaded. The routing-contextual event areas (frontend or adminhtml) are loaded later on in Mage_Core_Controller_Varien_Action::preDispatch() as a result of router matching a controller action (the area name is set via inheritance):

public function preDispatch()
{
    //...
    Mage::app()->loadArea($this->getLayout()->getArea());
    //...
}

So normally during app initialization only the observers configured under the global event area will be executed. If the setup script does something such as

$this->loadAreaPart(Mage_Core_Model_App_Area::AREA_ADMINHTML, Mage_Core_Model_App_Area::PART_EVENTS);

then there are only two dangers:

  1. An observer has been misconfigured under adminhtml to observe a context-less event such as controller_front_init_before or controller_front_init_routers
  2. The request is a frontend request.

#1 should be easy to grep for. #2 is the real concern, and I think that Reflection can solve the problem (note that I'm woefully inexperienced with using reflection):

<?php

//Start setup script as normal
$installer = $this;
$installer->startSetup()

//Load adminhtml event area
Mage::app()->loadAreaPart(
    Mage_Core_Model_App_Area::AREA_ADMINHTML,
    Mage_Core_Model_App_Area::PART_EVENTS
);

// your setup script logic here

//I hope this isn't a bad idea.
$reflectedApp = new ReflectionClass('Mage_Core_Model_App');
$_areas = $reflectedApp->getProperty('_areas');
$_areas->setAccessible(true);
$areas = $_areas->getValue(Mage::app());
unset($areas['adminhtml']);
$_areas->setValue(Mage::app(),$areas); //reset areas

//End setup script as normal
$installer->endSetup()

I've not tested this, but it does remove the adminhtml event index and corresponding Mage_Core_Model_App_Area object.

Licensed under: CC-BY-SA with attribution
Not affiliated with magento.stackexchange
scroll top