Setup and Teardown the Database Once Per Test Suite in PHPUnit

I'm working on a series of integration tests where I want to set up and reset the database for each run. This could easily be done in the setUp and tearDown methods, but doing the full db each time is slow. Yes, I could just do the tables I need, but I was curious, and now I don't have to worry about which tables are setup in my testing DB. In this example, I'm using Laravel's migrations and SQLite as the test DB.

UPDATE 05/05/2016: As Sebastian points out below (thanks!) there is a much more appropriate way. Using setUpBeforeClass and tearDownAfterClass we achieve the same effect

public static function setUpBeforeClass()
{
    parent::setUpBeforeClass();
    exec('php artisan migrate --database sqlite_test');
}

public static function tearDownAfterClass()
{
    exec('php artisan migrate:reset --database sqlite_test');
    parent::tearDownAfterClass(); 
}

PHPUnit has listeners that you can tap into at various parts of your tests' lifecycle. I'm particularly interested in when a specific suite starts and ends. We'll need to do 2 things:

  1. Create our Listener
  2. Register this Listener in phpunit.xml

The listener is as follows:

class FullDBListener extends PHPUnit_Framework_BaseTestListener
{
    protected $suites = ['UserIntegrationTest', 'AccountIntegrationTest'];

    public function startTestSuite(PHPUnit_Framework_TestSuite $suite)
    {
        if (in_array($suite->getName(), $this->suites)) {
            exec('php artisan migrate --database sqlite_test');
        }
    }

    public function endTestSuite(PHPUnit_Framework_TestSuite $suite)
    {
        if (in_array($suite->getName(), $this->suites)) {
            exec('php artisan migrate:reset --database sqlite_test');
        }
    }
}

This does a few things:

  1. Stores the class names of each suite we want to run the db migrations for
  2. In startTestSuite it checks to see if we're in the right suite
  3. If we are, run an artisan migration on our test db
  4. In endTestSuite it checks to see if we're in the right suite
  5. If we are, run an artisan migration:reset on our test db

Next, we need to make PHPUnit aware of this listener. Update your path accordingly

<listeners>
    <listener class="FullDBListener" file="./tests/app/FullDBListener.php"></listener>
</listeners>

That's it!

I do do some basic teardown in my test suite's setUp method to give me a blank slate. This has proven faster then doing the migrations each time

public function setUp()
{
    parent::setUp();

    User::truncate();
}

I hope this helped you, if you have any questions, let me know!

PHP7 Vagrant Box

I’m keeping a close eye (or as much as I can anyway) on PHP7 and what it means for the future of the language. Installing in your local dev machine is risky, especially if you have ongoing work. As usual, Vagrant comes to the rescue!

Rasmus Lerdorf has put together a Vagrant box to ease in the setup and isolate your testing.

If you use Atlas, check out the box here. Otherwise, the readme is available on Github

Fixing YouTube embeds in Wordpress

In some wordpress themes, youtube embeds just show up as a black screen. As discussed here, the solution is adding a transparency setting to the iframe's src.

However, the solution in that thread only works if the src is right next to the frameborder. Updated code below if you are running into this problem

function add_video_wmode_transparent( $html ) {
    $pattern = '#(src=&quot;https?://www.youtube(?:-nocookie)?.com/(?:v|embed)/([a-zA-Z0-9-]+).&quot;)#';
    preg_match_all( $pattern, $html, $matches );

    if ( count( $matches ) &gt; 0) {
        foreach ( $matches[0] as $orig_src ) {
            if ( !strstr($orig_src, 'wmode=transparent' ) &amp;&amp; !strstr( $orig_src, 'hd=1' ) ) {
                $add = 'hd=1&amp;wmode=transparent&quot;';

                if ( !strstr($orig_src, '?') ) {
                    $add = '?' . $add;
                }
                $new_src = substr( $orig_src, 0, -1 ) . $add;
                $html = str_replace( $orig_src, $new_src, $html );
            }
        }
    }
    return $html;
}
add_filter( 'the_content', 'add_video_wmode_transparent', 10 );

New thread in the wordpress forums can be found here. (The original was closed)

How to Remove or Change the way Wordpress Links to Images in Posts

By default, WordPress will link directly to an image in the category or post view. In a project I was working on today I wanted to change that. On the category view I wanted the image just to link to the post, and in the post, I didn’t want a link at all. Useful trick I found below:

function change_image_permalink($content){
    $format = get_post_format();

    //category listing page. link image to post
    if (is_single() === FALSE AND $format == 'image'){
        $content =
        preg_replace(
            array('{<a(.*?)(wp-att|wp-content/uploads)[^>]*><img}','{ wp-image-[0-9]*&quot; /></a>}'),
            array('<a href=&quot;' . get_permalink() . '&quot;><img','&quot; /></a>'),
            $content
        );
    }

    //post page. remove link
    else if ($format == 'image'){
        $content =
            preg_replace(
                array('{<a(.*?)(wp-att|wp-content/uploads)[^>]*><img}','{ wp-image-[0-9]*&quot; /></a>}'),
                array('<img','&quot; />'),
                $content
            );
    }

    return $content;
}
add_filter('the_content', 'change_image_permalink');

Important Magento Security Update – Zend Platform Vulnerability

While doing routine sanity checks, on of our QA Engineers, Sammy Shaar, was alerted about an important Magento security update. The vulnerability potentially allows an attacker to read any file on the web server where the Zend XMLRPC functionality is enabled. This might include password files, configuration files, and possibly even databases if they are stored on the same machine as the Magento web server. To see if you site has been affected, please see this page. Luckily, Magento has released patches for all supported versions:

  • Magento Enterprise Edition and Professional Edition merchants: You may access the Zend Security Upgrade patch from Patches & Support for your product in the Downloads section of your Magento account. Account log-in is required. Download
  • Magento Community Edition merchants: Community Edition 1.4.0.0 through 1.4.1.1 Community Edition 1.4.2.0 Community Edition 1.5.0.0 through 1.7.0.1 To install the patch, place the patch file in the root of your Magento site and run the following command: patch -p0 < zendxml_fix.patch If you don't have ssh access or patch installed on your machine, please see this stack overflow post for alternative methods.

Creating a stateless request in Magento

Have you ever wanted to create a stateless request in Magento? Something that doesn't touch any of Magento's sessions?  We were having issues with some of the ajax calls on our cart and checkout pages mucking with the user's cart and had get stateless on these calls.  The issue we were having was our checkout page was loading, then a javascript include was going out and bringing code from a 3rd party relevance engine into our dom, which was in turn calling back an ajax request to our servers.  This issue with this being that at the start of the page load, the checkout session was being set to a certain state.  This state was then being sent through the rest of the page load, and the ajax calls. Unfortunately, by the time the ajax call got back to our server, the session was different in both locations, creating a race condition.  The ajax request usually won, removing the work the full page load had done with trying to process checkout.  The good news was there was nothing in the ajax call that needed to touch the session, it was just some data lookup. So, nix the session part of that call, and our troubles should be over... Magento's api controller is the only place that implements a stateless request this but its fairly easy to do (after a bit of digging).

As long as Mage_Core_Controller_Varien_Action is a parent in your controller's hierchy, you are good to go (it probably is).  This class has a const FLAG_NO_START_SESSION which looks promising. Digging into the code a little we see that it controls whether cookies are processed or the session is started:

<?php
...
        if (!$this->getFlag('', self::FLAG_NO_START_SESSION)) {
            $checkCookie = in_array($this->getRequest()->getActionName(), $this->_cookieCheckActions);
            $checkCookie = $checkCookie &amp;&amp; !$this->getRequest()->getParam('nocookie', false);
            $cookies = Mage::getSingleton('core/cookie')->get();
            if ($checkCookie &amp;&amp; empty($cookies)) {
                $this->setFlag('', self::FLAG_NO_COOKIES_REDIRECT, true);
            }
            Mage::getSingleton('core/session', array('name' => $this->_sessionNamespace))->start();
        }

By adding to the preDispatch() method of our Action or Controller we can toggle this:

<?php
class Ai_AjaxCatalog_Controller_Action extends Mage_Core_Controller_Front_Action
{
    public function preDispatch()
    {
        $this->setFlag('', self::FLAG_NO_START_SESSION, 1); // Do not start standard session
        parent::preDispatch();
        return $this;
    }
}

Now, any action in this controller will be stateless and not effect any sessions.

Extending a Magento Controller

We're ajaxing part of the Magento shopping cart so we need to modify/extend some of the cart controller functionality.  Sometimes when modifying controller's you have to worry about updating the routes. For this, we don't need to, we still want all the urls to be used the same way.

app/code/local/Ai/Checkout/etc/config.xml:

<config>
    <modules>
        <Ai_Checkout>
             <version>0.0.1</version>
        </Ai_Checkout>
    </modules>
...
    <frontend>
        <routers>
            <checkout>
                <use>standard</use>
                <args>
                    <module>Ai_Checkout</module>
                    <frontName>checkout</frontName>
                </args>
            </checkout>
        </routers>
    </frontend>    
</config>

app/code/local/Ai/Checkout/controllers/CartController.php:

```PHP require_once Mage::getModuleDir('controllers', 'Mage_Checkout') . DS . 'CartController.php';

class Ai_Checkout_CartController extends Mage_Checkout_CartController { public function updatePostAction() { Mage::log("NEW CONTROLLER", null, 'tim.log'); try { [/php]

Want to trace the call stack in Magento?

Update: This code is also available on Github as a Mageno module

This has helped me immensely in situations like "Where is this getting called from??!?"

Create a helper like so:

class Timbroder_Stack_Helper_Callstack extends Mage_Core_Helper_Abstract
{
    private function get_callstack($delim=&quot;\n&quot;) {
      $dt = debug_backtrace();
      $cs = '';
      foreach ($dt as $t) {
        $cs .= $t['file'] . ' line ' . $t['line'] . ' calls ' . $t['function'] . &quot;()&quot; . $delim;
      }

      return $cs;
    }

    public function toLog() {
        Mage::log($this-&gt;get_callstack());
    }

    public function toFirePhp() {
        $stack = $this-&gt;get_callstack();
        foreach (explode(&quot;\n&quot;, $stack) as $line) {
            Mage::helper('firephp')-&gt;send($line);
        }
    }
}

That can be called from anywhere:

``PHP Mage::helper('stack/callstack')->toFirePhp(); Mage::helper('stack/callstack')->toLog();


I've also wrapped this into a module that you can drop right into your project. Details here: [https://bitbucket.org/broderboy/magento_callstack/src](https://bitbucket.org/broderboy/magento_callstack/src "https://bitbucket.org/broderboy/magento_callstack/src") Example output:

.../app/code/community/Timbroder/Stack/Helper/Callstack.php line 16 calls get_callstack() .../app/design/frontend/mongoose/default/template/catalog/cms/bikes_bmx.phtml line 12 calls toLog() .../app/design/frontend/mongoose/default/template/catalog/cms/bikes.phtml line 21 calls require_once() .../app/code/core/Mage/Core/Block/Template.php line 212 calls include() .../app/code/core/Mage/Core/Block/Template.php line 239 calls fetchView() .../app/code/core/Mage/Core/Block/Template.php line 253 calls renderView() .../app/code/core/Mage/Core/Block/Abstract.php line 668 calls _toHtml() .../app/code/core/Mage/Core/Model/Email/Template/Filter.php line 190 calls toHtml() .../lib/Varien/Filter/Template.php line 134 calls call_user_func() .../app/code/core/Mage/Core/Model/Email/Template/Filter.php line 501 calls filter() .../app/code/core/Mage/Cms/Block/Page.php line 100 calls filter() .../app/code/core/Mage/Core/Block/Abstract.php line 668 calls _toHtml() .../app/code/core/Mage/Core/Block/Abstract.php line 513 calls toHtml() .../app/code/core/Mage/Core/Block/Abstract.php line 460 calls _getChildHtml() .../app/code/local/Mage/Page/Block/Html/Wrapper.php line 52 calls getChildHtml() .../app/code/core/Mage/Core/Block/Abstract.php line 668 calls _toHtml() .../app/code/core/Mage/Core/Block/Text/List.php line 43 calls toHtml() .../app/code/core/Mage/Core/Block/Abstract.php line 668 calls _toHtml() .../app/code/core/Mage/Core/Block/Abstract.php line 513 calls toHtml() .../app/code/core/Mage/Core/Block/Abstract.php line 464 calls _getChildHtml() .../app/design/frontend/mongoose/default/template/page/1column.phtml line 55 calls getChildHtml() .../app/code/core/Mage/Core/Block/Template.php line 212 calls include() .../app/code/core/Mage/Core/Block/Template.php line 239 calls fetchView() .../app/code/core/Mage/Core/Block/Template.php line 253 calls renderView() .../app/code/core/Mage/Core/Block/Abstract.php line 668 calls _toHtml() .../app/code/core/Mage/Core/Model/Layout.php line 529 calls toHtml() .../app/code/local/Mage/Core/Controller/Varien/Action.php line 389 calls getOutput() .../app/code/core/Mage/Cms/Helper/Page.php line 130 calls renderLayout() .../app/code/core/Mage/Cms/Helper/Page.php line 52 calls _renderPage() .../app/code/core/Mage/Cms/controllers/PageController.php line 45 calls renderPage() .../app/code/local/Mage/Core/Controller/Varien/Action.php line 418 calls viewAction() .../app/code/core/Mage/Core/Controller/Varien/Router/Standard.php line 254 calls dispatch() .../app/code/core/Mage/Core/Controller/Varien/Front.php line 177 calls match() .../app/code/core/Mage/Core/Model/App.php line 304 calls dispatch() .../app/Mage.php line 598 calls run() .../index.php line 155 calls run() ```

Thanks to nextide for some of the code