Webpack – Setting Up

Initialising

Webpack is installed using NPM. Initialise a new project, which will create a package.json file. Optionally use the flag -y to accept all defaults when initialising the project.

npm init

Now we can install Webpack and Webpack CLI via NPM using the following. The flag -D is for –save-dev, which adds both packages to our devDependencies.

npm install webpack webpack-cli -D

By default, Webpack 4 will use a default configuration file, so one isn’t necessary to get going. The default configuration values can be found here.

By default, Webpack will look in the folder “src” for the file “index.js”, and output to the folder “dist”, with the javascript file “main.js”. A handy Webpack config generator can be found here.

We can now create a file, index.html in the project root and link to the file “dist/main.js”.

<!doctype html>
<html>
 <head>
   <title>Getting Started</title>
 </head>
 <body>
    <script src="dist/main.js"></script>
 </body>
</html>

For our example, we’ll install Lodash, which is a utility library

npm install lodash --save

Now let’s create the entry point file in “src/index.js”, we’ll import lodash.

import _ from 'lodash';

console.log(_.join(["This", "is", "using", "Lodash"], " "));

This will import the default export from lodash in our local “_” variable. We could use any name of our choosing for this to use locally, however underscore is a convention when using this package.

To build our javascript into the dist directory, we can run

npx webpack

NPX is a tool for executing node packages. It is typically only used for single use commands, such as react-create-app. We’ll supersede its use later for our builds, but it’s useful to use it for this example. Executables which are available for use with NPX are symlinked to the node_modules/.bin directory. Running “NPX with any of the files in here can perform useful functions, e.g. mkdirp which will make directories recursively:

mkdirp /directory1/directory2

Importing Individual Methods

If we didn’t want to use the entire lodash library, we could import individual methods from it using the following:

import {join as _join} from 'lodash'
console.log(_join(["This", "is", "using", "Lodash"], " "));

However, this will still import the entire Lodash library into our project. Instead, we can use the lodash-es module which defines each method as an ES module:

import {join as _join} from 'lodash-es'

console.log(_join(["This", "is", "using", "Lodash"], " "));

This example cuts our dist/main.js size down from ~70KB to ~1KB.

Building with a separate configuration file

If we need to test something using a separate configuration file, we can use NPX to achieve this. We can create a file called webpack.test.config.js in our project’s root with the following:

const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'test.js'
    }
};

Note: config files use require to include, which was introduced with Node.js some time ago, and does not use import

And then execute it with NPX:

npx webpack --config webpack.test.config.js

This will bundle our javascript in dist/test.js instead of dist/main.js.

Note that config files are standalone. So if we had a webpack.config.js in our project’s root, any config file passed to Webpack via NPX will not be merged together.

Adding a Script Shortcut

We can assign shortcut’s via NPM’s script facility. At a very basic level, we can setup a build command so that we do not have to rely on NPX. We can do that in our package.json file:

{
...
  "scripts": {
    "build": "webpack"
  },
...
}

We can then add a watch command which will automatically re-build our javascript based on changes to the project:

{
...
  "scripts": {
    "build": "webpack",
    "watch": "webpack  --watch"
  },
...
}

We can also specify other parameters, such as “mode” in our build scripts:

{
...
  "scripts": {
    "build": "webpack --mode production",
    "watch": "webpack --mode development  --watch"
  },
...
}

Note: When watching for changes, the process will have to be broken and re-started when the config file is changed

Setting publicPath

publicPath is used when resolving things like images included in CSS files. It’s prepended to these files. For this example, we’ll set it to the dist folder.

{
...
    output: {
        filename: 'main.js',
        path: path.resolve(__dirname, 'dist'),
        publicPath: 'dist/'
    },
...
}

Source Maps

To allow easier debugging of code, we can set the devtool parameter of our config to one of the sourceMap options. As Webpack itself only deals with Javascript, this will only affect JS SourceMaps, CSS and other loaders will need to be configured in their own right.

By default, setting the config’s mode to ‘development’ will enable SourceMaps, and ‘production’ will disable them, however there are numerous other options which can be useful for both production and development.

Development SourceMaps

These methods generate sourceMaps inline, in the bundled JS files, adding bloat and should not be used in production.

eval

Fast. Files are separated using the original directory structure, but the code generated by Webpack to load modules is present.

cheap-eval-source-map

Pretty Fast. Webpack loading code is removed.

cheap-module-eval-source-map

Medium Speed (but with fast rebuilds). Uses sourcemaps output by defined loaders.

eval-source-map

Slow. Adds column level mappings so inline breakpoints can be set.

Production SourceMaps

none

No SourceMaps are generated. This is the default in production, but leaves code difficult to debug when receiving user reports.

source-map

Source Maps are generated in a separate file. A comment tells dev tools (such as Chrome Dev Tools) where to find them, which are loaded when the dev tools themselves are opened.

hidden-source-map

Source Maps are generated in a separate file, but the comment to tell the browser where to find it is omitted. These can then be Loaded Manually

nosources-source-map

The same as source-map, but only gives the file name and line number. The code itself is not visible to the browser.

Cleaning The Output Folder

When changing Webpack settings, it’s often useful to get rid of old files in the output folder which may be left handing around. To do this, we can use a plugin called Clean Webpack Plugin

Install the plugin

npm install -D clean-webpack-plugin

And then add this to the plugins section of our config file:

module.exports = {
...
    plugins: [
        new CleanWebpackPlugin(['dist'])
    ]
...
}

The option passed into the first parameter of the plugin is an array of paths to be cleaned. This is required, as it does not use Webpack’s output configuration. Supplying a name like above will completely remove that folder, however we can also provide glob strings, such as dist/*.*, which would remove the files within the folder.

This will, by default leave generated files when we’re watching files, so changes will continually generate new files in our dist folder. To re-generate whilst watching, we can use the following option

module.exports = {
...
    plugins: [
        new CleanWebpackPlugin(['dist'], {watch: true})
    ]
...
}

Overcoming Magento’s Double Grand Total Issue

Magento will sometimes calculate the total of the cart as double what it should actually be. This occurs typically when multiple collectTotals() calls are made to the shipping address. To overcome this issue, we can clear the cached items of the quote object and recalculate from there.


$quote = Mage::getSingleton(&#039;checkout/session&#039;)-&gt;getQuote();
$quote-&gt;setTotalsCollectedFlag(false);
$quote-&gt;getShippingAddress()-&gt;unsetData(&#039;cached_items_all&#039;);
$quote-&gt;getShippingAddress()-&gt;unsetData(&#039;cached_items_nominal&#039;);

$quote-&gt;getShippingAddress()-&gt;unsetData(&#039;cached_items_nonnominal&#039;);
$quote-&gt;collectTotals()-&gt;save();


WordPress is an arse and can’t do html entities properly

Modifying Phoenix_Worldpay For Testing in Magento

Worldpay offers an easy way to switch from live to test mode, however if your server is unreachable by them then after completing the test transaction you won’t be redirected back to your Magento store.

We can make the following temporary adaptions to the module which will allow us to manually pass the required data via a GET string:

Phoenix_Worldpay_ProcessingController

   protected function _checkReturnedPost()
    {
//        // check request type
//        if (!$this->getRequest()->isPost()) {
//            Mage::throwException('Wrong request type.');
//        }
//
//        // validate request ip coming from WorldPay/RBS subnet
//        $helper = Mage::helper('core/http');
//        if (method_exists($helper, 'getRemoteAddr')) {
//            $remoteAddr = $helper->getRemoteAddr();
//        } else {
//            $request = $this->getRequest()->getServer();
//            $remoteAddr = $request['REMOTE_ADDR'];
//        }
//        if (!preg_match('/\.worldpay\.com$/', gethostbyaddr($remoteAddr))) {
//            Mage::throwException('Domain can\'t be validated as WorldPay-Domain.');
//        }

        // get request variables
        $request = $this->getRequest()->getParams();
        if (empty($request)) {
            Mage::throwException('Request doesn\'t contain POST elements.');
        }

        // check order id
        if (empty($request['MC_orderid']) || strlen($request['MC_orderid']) > 50) {
            Mage::throwException('Missing or invalid order ID');
        }

        // load order for further validation
        $this->_order = Mage::getModel('sales/order')->loadByIncrementId($request['MC_orderid']);
        if (!$this->_order->getId()) {
            Mage::throwException('Order not found');
        }

        $this->_paymentInst = $this->_order->getPayment()->getMethodInstance();

        // check transaction password
        if ($this->_paymentInst->getConfigData('transaction_password') != $request['callbackPW']) {
            Mage::throwException('Transaction password wrong');
        }

        return $request;
    }

– We remove the remote address checks at the start of the method
– We change the getPost to a getParams, allowing us to provide GET parameters

Phoenix_Worldpay_Model_Cc

public function capture(Varien_Object $payment, $amount)
	{
        $payment->getOrder()->addStatusToHistory($payment->getOrder()->getStatus(), $this->_getHelper()->__('Worldpay transaction has been captured.'));
        return true;
        if (!$this->canCapture()) {
            return $this;
        }

        if (Mage::app()->getRequest()->getParam('transId')) {
            // Capture is called from response action
            $payment->setStatus(self::STATUS_APPROVED);
            return $this;
        }
        $transactionId = $payment->getLastTransId();
        $params = $this->_prepareAdminRequestParams();
        $params['transId']  = $transactionId;
        $params['authMode'] = '0';
        $params['op']       = 'postAuth-full';

        $responseBody = $this->processAdminRequest($params);
        $response = explode(',', $responseBody);

        if (count($response) <= 0 || $response[0] != 'A' || $response[1] != $transactionId) {
            $message = $this->_getHelper()->__('Error during capture online. Server response: %s', $responseBody);
            $this->_debug($message);
            Mage::throwException($message);
        } else {
            $payment->getOrder()->addStatusToHistory($payment->getOrder()->getStatus(), $this->_getHelper()->__('Worldpay transaction has been captured.'));
        }
    }

– If capturing is required, then this will need further modification, however for our needs we only needed to return true from this method.

It should now be possible to provide a GET string with all of the required parameters, E.g.

http://mydomain.local/worldpay/processing/response?MC_orderid=100012592&transStatus=Y&authAmount=158.00&authCurrency=GBP&authMode=A

Converting a UTF-16 CSV to UTF-8 in PHP

The best solution I’ve found to this is to open the file, and re-write it with the new encoding before opening it for CSV reading;

function get_encoding($filename){
    $encoding='';
    $handle = fopen($filename, 'r');
    $bom = fread($handle, 2);
    //	fclose($handle);
    rewind($handle);

    if($bom === chr(0xff).chr(0xfe)  || $bom === chr(0xfe).chr(0xff)){
        // UTF16 Byte Order Mark present
        $encoding = 'UTF-16';
    } else {
        $file_sample = fread($handle, 1000) + 'e'; //read first 1000 bytes
        // + e is a workaround for mb_string bug
        rewind($handle);

        $encoding = mb_detect_encoding($file_sample , 'UTF-8, UTF-7, ASCII, EUC-JP,SJIS, eucJP-win, SJIS-win, JIS, ISO-2022-JP');
    }
    if ($encoding){
        stream_filter_append($handle, 'convert.iconv.'.$encoding.'/UTF-8');
    }
    return $encoding;
}

/**
* Decode UTF-16 encoded strings.
* 
* Can handle both BOM'ed data and un-BOM'ed data. 
* Assumes Big-Endian byte order if no BOM is available.
* 
* @param   string  $str  UTF-16 encoded data to decode.
* @return  string  UTF-8 / ISO encoded data.
* @access  public
* @version 0.1 / 2005-01-19
* @author  Rasmus Andersson {@link http://rasmusandersson.se/}
* @package Groupies
*/
function utf16_decode($str, &$be=null) {
    if (strlen($str) < 2) {
        return $str;
    }
    $c0 = ord($str{0});
    $c1 = ord($str{1});
    $start = 0;
    if ($c0 == 0xFE && $c1 == 0xFF) {
        $be = true;
        $start = 2;
    } else if ($c0 == 0xFF && $c1 == 0xFE) {
        $start = 2;
        $be = false;
    }
    if ($be === null) {
        $be = true;
    }
    $len = strlen($str);
    $newstr = '';
    for ($i = $start; $i < $len; $i += 2) {
        if ($be) {
            $val = ord($str{$i})   << 4;
            $val += ord($str{$i+1});
        } else {
            $val = ord($str{$i+1}) << 4;
            $val += ord($str{$i});
        }
        $newstr .= ($val == 0x228) ? "\n" : chr($val);
    }
    return $newstr;
}


$newFilename = 'path/to/uploaded/file.scv';
$encoding = get_encoding($newFilename);

/* This is the fix for UTF-16 files which come in from iPad */
if($encoding == 'UTF-16'){
    $utf8Contents = utf16_decode(file_get_contents($newFilename));
    $resource = fopen($newFilename, 'w');
    /* Write the file over the uploaded one */
    fwrite($resource, pack("CCC",0xef,0xbb,0xbf));
    fwrite($resource, $utf8Contents);
}


if (($handle = fopen($newFilename, "r")) !== false) {
    while (($data = fgetcsv($handle)) !== false) {
        ...
    }
}

Alphabetic Option Value Ordering in Magento’s Layered Navigation

Magento will by default order layered navigation options by position. To use alphabetic values first and foremost, do the following:

Override the Eav_Entity_Attribute_Source_Table model

<global>
    <models>
        <eav>
            <rewrite>
        	    <entity_attribute_source_table>Namespace_Module_Model_Eav_Entity_Attribute_Source_Table</entity_attribute_source_table>
        	</rewrite>
        </eav>
    </models>
</global>

PHP

Override the getAllOptions method as follows:

<?php
class Namespace_Module_Model_Eav_Entity_Attribute_Source_Table extends Mage_Eav_Model_Entity_Attribute_Source_Table
{
    public function getAllOptions($withEmpty = true, $defaultValues = false)
    {
        $storeId = $this->getAttribute()->getStoreId();
        if (!is_array($this->_options)) {
            $this->_options = array();
        }
        if (!is_array($this->_optionsDefault)) {
            $this->_optionsDefault = array();
        }
        if (!isset($this->_options[$storeId])) {
            $collection = Mage::getResourceModel('eav/entity_attribute_option_collection')
                ->setAttributeFilter($this->getAttribute()->getId())
                ->setStoreFilter($this->getAttribute()->getStoreId());

            $collection->getSelect()->order(['main_table.sort_order asc', 'value asc']);
            $collection->load();

            $this->_options[$storeId]        = $collection->toOptionArray();
            $this->_optionsDefault[$storeId] = $collection->toOptionArray('default_value');
        }



        $options = ($defaultValues ? $this->_optionsDefault[$storeId] : $this->_options[$storeId]);
        if ($withEmpty) {
            array_unshift($options, array('label' => '', 'value' => ''));
        }

        return $options;
    }
}

This uses sort order first, then uses value to order the attribute options.

Magento – Programatic Quote, Order, Invoices

<?php
 
require_once 'app/Mage.php';
 
Mage::app('default');
 
$store = Mage::app()->getStore('default');
 
$customer = Mage::getModel('customer/customer');
$customer->setStore($store);
$customer->loadByEmail('email_address@gmail.com');
 
$quote = Mage::getModel('sales/quote');
$quote->setStore($store);
$quote->assignCustomer($customer);
 
$product1 = Mage::getModel('catalog/product')->load(166); /* HTC Touch Diamond */
$buyInfo1 = array('qty' => 1);
 
$product2 = Mage::getModel('catalog/product')->load(18); /* Sony Ericsson W810i */
$buyInfo2 = array('qty' => 3);
 
$quote->addProduct($product1, new Varien_Object($buyInfo1));
$quote->addProduct($product2, new Varien_Object($buyInfo2));
 
$billingAddress = $quote->getBillingAddress()->addData($customer->getPrimaryBillingAddress());
$shippingAddress = $quote->getShippingAddress()->addData($customer->getPrimaryShippingAddress());
 
$shippingAddress->setCollectShippingRates(true)->collectShippingRates()
                ->setShippingMethod('flatrate_flatrate')
                ->setPaymentMethod('checkmo');
 
$quote->getPayment()->importData(array('method' => 'checkmo'));
 
$quote->collectTotals()->save();
 
$service = Mage::getModel('sales/service_quote', $quote);
$service->submitAll();
$order = $service->getOrder();
 
$invoice = Mage::getModel('sales/service_order', $order)->prepareInvoice();
$invoice->setRequestedCaptureCase(Mage_Sales_Model_Order_Invoice::CAPTURE_ONLINE);
$invoice->register();
 
$transaction = Mage::getModel('core/resource_transaction')
                    ->addObject($invoice)
                    ->addObject($invoice->getOrder());
 
$transaction->save();
  • The $invoice->register() calls pay() in the Mage_Sales_Model_Order_Invoice class. This fires the event sales_order_invoice_pay which is useful for hooking into after an invoice has been paid for; E.g. automatically sending the invoice email upon creation:
   public function sendEmail(Varien_Event_Observer $observer)
    {
        $event = $observer->getEvent();
        /** @var Mage_Sales_Model_Order_Invoice $invoice */
        $invoice = $event->getInvoice();
        if(!$invoice->getEmailSent()){
            $invoice->sendEmail(true);
        }
    }

Fix Logging In on Old Magento 1 Installs

On certain Magento instance installs, a valid customer login will seem to fail and redirect to the login page. We’ve seen this on plenty of Magento 1.6s thus far, however newer versions may also be affected.

The problem lies in the login method of the Mage_Customer_Model_Session class. The fix, in this instance is to remove the renewSession call within that method:

 public function login($username, $password)
{
    /** @var $customer Mage_Customer_Model_Customer */
    $customer = Mage::getModel('customer/customer')
        ->setWebsiteId(Mage::app()->getStore()->getWebsiteId());

    if ($customer->authenticate($username, $password)) {
        $this->setCustomerAsLoggedIn($customer);
        // This breaks certain setups
        //$this->renewSession();
        return true;
    }
    return false;
}

public function setCustomerAsLoggedIn($customer)
{
	$this->setCustomer($customer);
	// This breaks certain setups
//        $this->renewSession();
	Mage::getSingleton('core/session')->renewFormKey();
	Mage::dispatchEvent('customer_login', array('customer'=>$customer));
	return $this;
}

This has been rolled into a module here: https://github.com/llapgoch/fix-old-magento-login