Deploy Magento With Capistrano

| Comments

So you have a Magento project and there must be a better way than you currently have to deploy your code to a server? Well there are many ways to do this from FTP/SFTP, rsync or SCM checkouts. You could even build a package and deploy using a package manager. However there is one options that from experience fits all eventualities and can be turned into a repeatable process where you don’t have to remember a list of manual steps. It can even have inbuilt functionality to revert a deployment if for some reason you need to.

What would we like from a deployment system

  • Fast deployment.
  • Repeatable process.
  • Remove all manual steps or tasks.
  • Easy and quick roll back to a previous state.
  • Simple to use once setup.

Extra credits

  • Extensible to cover your specific needs.
  • Work across a cluster of servers .
  • Work across servers with different roles for the application.

Capistrano originally developed to deploy Ruby on Rails applications has evolved through a number of iterations to become a general purpose deployment tool that can be used for deploying any type of code base or packaged application. Capistrano - provides all of the features we are looking for, plus all the extra credit features and more. It has been used for many projects and is tried and trusted in the PHP community too. You will find many existing Capistrano scripts or pre-packaged Gems that provide functionality to deploy specific PHP applications and to deal with their specific requirements including Magentify.

Updated Blog

| Comments

It has been an absolute age since I last created any new content for this blog. The main reason has been simply that I have been very very busy doing other things. However it is also that due to the fact that creating content through a browser and storing it in a database just did not fit with the way I work.

I spend most of my day working in a terminal window. I choose a text editor over an IDE, speed and flexibility over features. Okay I don’t go all the way back to basics I use TextMate and iTerm with ZSH and oh-my-zsh. All of these things let me work in the simplest most flexible way.

My blog on the other hand always felt restrictive. So after some conversation on twitter /cc @mpmlopes @__debo I spent a couple of hours over the weekend making the leap to octopress. Although not loaded with features this lets me create content in the simplest format, markdown in this case and using Git and some Ruby it is baked into a static blog hosted on Github using Github pages.

As you can see this has allowed me to create new content. Okay it is just an ‘Updated Blog’ statement but it is a statement of more to come. Some of what has been keeping me busy for the last 12 months should be good enough to create new posts.

Package and Install Your Latest Magento 1.5+ Extension

| Comments

After spending many hours crafting the perfect Magento module you need to package it either for easy distribution to your client/team or to upload to Magento Connect.

Package the extension

First of all I would recommend reading the following wiki page;

Note - If your planning to upload your module to Magento Connect ensure the package name and the name you enter on Magento Connect match

Testing Magento Checkout

| Comments

While developing new functionality in Magento one of the key elements that you must ensure continues to work as expected is the checkout. After all an commerce website that can not trade is rather worthless.

Using Mage-Test you can easily create integration tests for the checkout process. This will allow you to ensure that your store still functions correctly after you have made any modifications.

Automated Integration Test

The example test method below is run within a Mage-Test controller test case.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
<?php
/**
 * @author Alistair Stead
 * @test
 */
public function magentoCheckoutIntegrationTest()
{
    // Add the product to the basket
    $this->request->setMethod('POST')
        ->setPost(
            array(
                'product' => '16',
                'related_product' => '',
                'qty' => '1'
            )
        );
    // Set an initial cookie so that the Magento cookie validation passes
    $this->request->setCookie('mage-test', true);
    $this->dispatch('/checkout/cart/add/');
    $this->assertResponseCode('302', '/checkout/cart/add/ has not redirected to the cart');
    $this->assertRedirectRegex("/^.*checkout.*$/", 'We are not directed to the checkout as expected');
    $this->reset();

    // Set the checkout as guest
    $this->request->setMethod('POST')
        ->setPost(
            array(
                'method' => 'guest'
            )
        );
    $this->dispatch('/checkout/onepage/saveMethod/');
    $this->assertResponseCode('200', '/checkout/onepage/saveMethod/ has not responded as expected');
    $this->reset();

    // Save billing address
    $this->request->setMethod('POST')
        ->setPost(
            array(
                'billing' => array(
                    'address_id' => '',
                    'firstname' => 'Authorize',
                    'lastname' => 'Capture',
                    'company' => '',
                    'email' => 'user@mage-test.co.uk',
                    'street' => array('Flat 14b', 'Baker Street'),
                    'city' => 'London',
                    'region_id' => '',
                    'postcode' => 'W1B OW3',
                    'country_id' => 'GB',
                    'telephone' => '+44 1234 123456',
                    'fax' => '+44 1234 123456',
                    'customer_password' => '',
                    'confirm_password' => '',
                    'save_in_address_book' => '1',
                    'use_for_shipping' => '1'
                )
            )
        );
    $this->dispatch('/checkout/onepage/saveBilling/');
    $this->assertResponseCode('200', '/checkout/onepage/saveBilling/ has not responded as expected');
    $this->reset();

    // Set shipping method
    $this->request->setMethod('POST')
        ->setPost(
            array(
                'shipping_method' => 'flatrate_flatrate'
            )
        );
    $this->dispatch('/checkout/onepage/saveShippingMethod/');
    $this->assertResponseCode('200', '/checkout/onepage/saveShippingMethod/ has not responded as expected');
    $this->reset();

    // Save payment
    $this->request->setMethod('POST')
        ->setPost(
            array(
                'payment' => array(
                    'method' => 'paypal_direct',
                    'cc_type' => 'SM',
                    'cc_number' => 'XXXXXXXXXXXXXXX',
                    'cc_exp_month' => '1',
                    'cc_exp_year' => (date('Y') + 6),
                    'cc_cid' => '444',
                    'cc_ss_issue' => '1',
                    'cc_ss_start_month' => '1',
                    'cc_ss_start_year' => (date('Y') - 2)
                )
            )
        );
    $this->dispatch('/checkout/onepage/savePayment/');
    $this->assertResponseCode('200', '/checkout/onepage/savePayment/ has not responded as expected');
    $this->reset();

    // Save order
    $this->request->setMethod('POST')
        ->setPost(
            array(
                'payment' => array(
                    'method' => 'paypal_direct',
                    'cc_type' => 'SM',
                    'cc_number' => 'XXXXXXXXXXXXXXX',
                    'cc_exp_month' => '1',
                    'cc_exp_year' => (date('Y') + 6),
                    'cc_cid' => '444',
                    'cc_ss_issue' => '1',
                    'cc_ss_start_month' => '1',
                    'cc_ss_start_year' => (date('Y') - 2)
                )
            )
        );
    $this->dispatch('/checkout/onepage/saveOrder/');
    $this->assertResponseCode('200', '/checkout/onepage/saveOrder/ has not responded as expected');
    // Decode the JSON response so that we can evaluate the success
    $data = json_decode($this->response->getBody());
    $this->assertTrue(
        $data->success,
        "Unexpected status expected result :: {$data->error_messages}"
    );
}
?>

You can use a PHPUnit data provider to supply a set of data fixtures to be used for credit card numbers, address information and other values. Using a data provider you can test for not only the successful checkout but also the error handling within the checkout process.

MageTool Update v0.3.0

| Comments

While at Magento Imagine I have used the time where I was awake due to jet lag to finish a couple of new features to MageTool.

MageTool v0.3.0 now includes:

  • Improved feedback when running commands for mage-core-resource
  • Added commands to query and run the Magento indexer
  • Added commands to query and run the Magento compiler
  • Improved mage-core-cache commands
  • Added functionality to run individual module setup classes directly
  • Added functionality to dispatch Magento events to test your observers

All of these commands are intended to make development of Magento extensions easier and more streamlined. If you have feature requests please let me know. Or if you wish feel free to fork the project on GitHub and add the command that you need.

If you find and bugs with MageTool please raise a ticket on GitHub.

Magento Command Line Tools MageTool

| Comments

If you have spent any time developing with Magento I expect that like me you have found a number of tasks you find yourself repeating many many times. While developing new modules or themes there are many times when you need to clear the Magento cache or even disable it completely. Now of course you can log into the admin system and carry out these actions but have you every thought there must be an easier way?

Many development frameworks or other open source projects have tool that can be used by developers to issue commands quickly and easily. Magento however does not have any built in tools to improve the development workflow. This is where MageTool fits in!

MageTool

How can MageTool help? Well hopefully in a number of ways. MageTool extends the existing Zend Framework command line tool zf, adding new commands that are specific to Magento development workflow. Rather then navigating the Magento admin system to clear cache or the enable or disable caching you can issue a simple command in your terminal window and achieve the same results. You can also modify the Magento configuration quickly or issue commands that will selectively update many configuration values.

In my standard development practices I maintain a number of Magento installs on different servers. These are used for Development, Staging and UAT each using different domains to access the stores. This means however that I often need to move databases between servers, resulting in the ‘base_url’ configuration being incorrect. This can be easily updated if you have database access and is fairly simple if you only have a couple of sites setup. However if you have more sites setup this can become very very difficult to maintain and calls into question the idea of working with many environment servers during development.

MageTool can help again in any situation where you need to move a Magento install and then update the ‘base_url’ configuration. You can construct a command that will quickly and easily update all URLs in the Magento config.

Symfony APC Cache and Memcache Session Storage

| Comments

In a previous post I described some of the reasons why you would want to store your session data in an alternate location to temporary files on your server. I explained the setup of database session storage using Doctrine and how this would allow you to take advantage of storing session data in a database. However depending upon your application requirements storing your session data in a database may cause excessive load on you database. You could of course use a separate dedicated database server to store your session data. However there is an alternate setup that will allow you to centralise your session storage ready for horizontal scaling of your hosting e.g. adding additional web servers.

Symfony Doctrine Database Session Storage

| Comments

Sessions are normally managed by your server and depending upon how involved you get with the PHP setup you need never know how this data is stored. The default setup for PHP is to store session data in temporary files on the server. So why would we want to change how sessions are handled and what advantage can we gain from any changes we make.

Why use database session management?

Sessions allow you maintain persistence within PHP applications. With every HTTP request a PHP application must re-assign every variable and object needed in your application. Sessions allow you to store data that is required to persist past each HTTP request allowing it to be easily retrieved and used.

YUI Connect Asynchronous File Uploads

| Comments

I’m sure the problems with multi-part data that I have been working through this morning is very simple but there are a number of forum posts about this, so I will share my findings. When using YUI Connection Manager in conjunction with multi-part form data I have experienced problems creating the asynchronous request and canceling the form submission. Even after setting up the request and callback object and checking the YUI API repeatedly the problem resides elsewhere.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
YAHOO.util.Event.addListener(forms, 'submit', this._onFormSubmit, this);
_onFormSubmit: function(e, o) {
    YAHOO>util.Event.preventDefault(e);
    // Set the form  YAHOO.util.Connect.setForm(e.currentTarget);
    // Configure the callback object
    callBackObj = {
        success: o._loadRemoteFormSubmissionTemplateComplete,
        failure: o._loadRemoteFormSubmissionTemplateFailed,
        argument: {
            objType: objType,
            region: regionObj,
            mode: submitbutton.value
        }
    }
    // Make the request
    YAHOO.util.Connect.asyncRequest('POST', e.currentTarget.action, callBackObj);
}

With non multi-part forms you can hijack the submit event of the form and create an asynchronous request. However once you try with multi-part data no matter how you set this up and attempt to cancel the form submit action the form will still make an HTTP request in the main window.

The YUI Multi-Part Data Solution

The solution is very simple once you find it but it is easy to overlook! In order to use YUI connect to create an asynchronous request you must remove the ‘submit’ button and replace it with a ‘button’ then assign it an eventListener that will create the asyncRequest.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var alternateButton = document.getElementById('alternateButtonId')
YAHOO.util.Event.addListener(alternateButton, 'click', this._onFormSubmit, this);
_onFormSubmit: function(e, o) {
    YAHOO.util.Event.preventDefault(e);
    // Set the form
    YAHOO.util.Connect.setForm(e.currentTarget);
    // Configure the callback object
    callBackObj = {
        success: o._loadRemoteFormSubmissionTemplateComplete,
        failure: o._loadRemoteFormSubmissionTemplateFailed,
        argument: {
            objType: objType,
            region: regionObj,
            mode: submitbutton.value
        }
    }
    // Make the request
    YAHOO.util.Connect.asyncRequest('POST', e.currentTarget.action, callBackObj);
}

When trying to use multi-part data YUI Connection Manager will create an iframe in the document that your form will submit the data to. If you try and use the submit handler YUI currently does not change the target value of the submit event.

The YUI examples do all show the use of alternate buttons rather than a standard submit button however it can be easily overlooked. I hope this helps someone!

Propel doUpdate Update Multiple Records in a Single Statement

| Comments

I’ve been very busy with a number of project launches lately. While working on a little problem today I re-discovered a little gem for use with Propel to update multiple rows in a single statement, rather than iterating through a collection updating and saving each object in turn.

This task is simple in SQL but after a while using any abstraction layer you may find like me you forget about the simple solutions as you spend you day working with complex objects and trying to hydrate custom objects. Blah blah blah…

The complex solution to a simple problem:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
// Obtain the connection configured
$conn = Propel::getConnection(YourObjectPeer::DATABASE_NAME);
// Create a Criteria object that will select the correct rows from the database
$selectCriteria = new Criteria();
$selectCriteria->add(YourObjectPeer::COLUMB_TO_SELECT, 'value_to_match');
// Create a Criteria object includes the value you want to set
$updateCriteria = new Criteria();
$updateCriteria->add(YourObjectPeer::COLUMB_TO_CHANGE, 'value_to_be_set');
// Execute the query
BasePeer::doUpdate($selectCriteria, $updateCriteria, $conn);
?>

Thats it! I hope it helps…