[Mini-Tutorial] Custom Shell Scripts in Magento

Here at Demac Media, we often come across problems that can be resolved by writing custom shell scripts. These tasks involve updating products, deleting test data, and running custom exports. There are of course many ways to utilize php scripts to run tasks, but the safest and most effective way is to extend the Magento Shell architecture. Out of the box, the Magento shell/ directory can be used to run indexers, clean logs and run compilations. I have found it to be a very convenient architecture to follow for running custom tasks, such as running an exporter.

magentoshell

The reasons why I would choose to follow this pattern rather than just adding a php file into the project root and then running on production are as follows:

1. It’s safer:

Uploading a php file in the root directory is dangerous, as it can be run by whomever guesses the url. While this can be avoided by adding disallow rules in a custom directory, I still avoid the process as a matter of security. Using the Magento shell classes, the script can only be run from shell, as trying to hit your script from the browser will result in a die('This script cannot be run from Browser. This is the shell script.');

Related: Code Quality in the Magento Ecosystem

2. It’s cleaner:

The design pattern of a class extending Mage_Shell_Abstract is readable and understandable for anyone looking at the script for the first time. It is initialized with a __construct() in the Abstract class, which applies the php variables from the .htacess file in the root and parses any arguments passed to it, and then validates that the request is not coming from the browser. It is then run by calling the run() function.

3. It’s consistent with the Magento design pattern:

The constructer also initializes the Magento app when the script is called. You can then leverage any calls to Magento factory methods to complete whatever task you are working to accomplish. The Abstract also comes with some handy functions to leverage or extend like getArg(), which will grab a specified argument, as well as usageHelp(), which returns a help string when called.

Related: Mini Tutorial: Patching Enterprise_TargetRule

A real-world example:

For the above reasons, I recommend trying to get acquainted with the Magento shell structure. As an example, I will share a script that runs a specified dataflow export.

The basic structure that should be followed is an include_once of the abstract.php file, declaring the custom shell class which extends from the Mage_Shell_Abstract class, and then instantiating that class and calling run() on it. Here is a shell of a shell script that I called dataflow_export.php, without any of the actual logic:

<?php
require_once 'abstract.php';
class Mage_Shell_DataflowExport extends Mage_Shell_Abstract
{
public function run() {
echo 'run!';
}
}
$shell = new Mage_Shell_DataflowExport();
$shell->run();
view raw gistfile1.php hosted with ❤ by GitHub

Now this will not actually do anything aside from telling you the shell script has been run when php dataflow_export.php is called from inside the shell/ directory, but it covers the main architecture of a shell script quite clearly.

Now I will populate the run() function with the rest of the logic necessary for running this export.

public function run()
{
if ($this->getArg(0)) {
$profileId = $this->getArg(0);
}
if (isset($profileId)){
$profile = Mage::getModel('dataflow/profile');
$userModel = Mage::getModel('admin/user');
$userModel->setUserId(0);
Mage::getSingleton('admin/session')->setUser($userModel);
$profile->load($profileId);
if (!$profile->getId()) {
Mage::getSingleton('adminhtml/session')->addError('ERROR: Incorrect profile id');
}
Mage::register('current_convert_profile', $profile);
$profile->run();
$batchModel = Mage::getSingleton('dataflow/batch');
echo "EXPORT COMPLETE. BATCHID: " . $batchModel->getId();
}
else {
echo $this->usageHelp();
}
}
/**
* Retrieve Usage Help Message
*
*/
public function usageHelp()
{
return <<<USAGE
Usage: php dataflow_export.php -- [profile_id]
profile_id Specified dataflow profile to run
USAGE;
}
view raw gistfile1.php hosted with ❤ by GitHub

First, the script checks for a command line argument, and if one is not set, it will dump the usageHelp() message prompting the user to enter an argument for the id of the profile they would like to run. Otherwise, it will load the dataflow/profile from the specified id and run() it.

Last Thoughts

This particular script is not overly complicated and one might even ask why it is necessary if there is a button to run from the backend. A script is not run by the backend pool, so it does not have a major effect on webserver performance. This allows running heavier tasks without the risk of timing out or taking down the backend. Still, you should always always check to see the effect of a heavy script on the server before relying on them heavily. A good way to do this would be to run top or htop while the process is running to make sure it isn’t hijacking the entire cpu.

If I missed any details, let me know in the comments!

Related: Understanding Full Page Cache in Magento Enterprise