Friday, July 19, 2013

MySQL: ALTER TABLE Gotcha When It Comes to Large Tables

Maybe you already know this, but I sure didn't, and it came around to bite me in a big way. When you use ALTER TABLE in MySQL, MySQL first copies the original table, modifies the copy, and then renames it to the original table's name. I'll repeat an important part of that sentence: it copies the original table. So, who cares? Well, if you've got a really big table, then maybe you do!

I ran into this little fun fact one night in the middle of a code deployment when trying to run a seemingly harmless database migration to add a column to our Really Big Table. This particular table had been consuming mass quantities of space in our db, threatening to consume a small town if we didn't feed it more storage. The main reason it was growing so large is that each row contained a column that stored an XML dump that was anywhere from 0 to 60MB. (Don't ask...) We'd decided that the solution was to add a column for storing a file name, move the XML out to the file system, and then store the new file's name in the newly-created column. Easy peasy, right? Wrong.

So, there I was, running the migration, and it was taking forever. I decided to do some research on the ALTER TABLE syntax (hindsight is 20/20), and I discovered this:

Why an “alter table” query takes so long time?

Oh, joy. The great irony in all of this is that the point of this change was to reduce the size of the db, but the result of attempting to add the column was to further grow our ibdata1 file so much it used up the rest of our available diskspace. We had to get the sys admin involved to find us another drive to mount and archive some files to get storage back. Very stressful.

Anyway, this article wasn't really about posing a good solution. Unfortunately, I don't really have one, if you need to keep all your existing data. Lucky for us, we really didn't need all of the rows in that particular table, as they're supposed to be purged by the app when they're no longer useful (but it simply wasn't happening). Our solution ended up being to delete the rows we didn't need (which dropped the table size from about 200GB to just over 1GB) which made things more manageable. I just  wanted to post this so that others wouldn't run into the problem at an inopportune moment like I did.

Afterthought: if you do need to keep the data, one thing you might try (and I haven't) would be to use a CREATE TABLE LIKE sort of syntax to make a new table just like the old one (with no data and a new name). Then you could alter THAT (empty) table. Now you have an empty table into which to copy the data, 1000 rows at a time, deleting the rows from the other table as you go. This might take a while, but limiting the operation to 1000 rows or less should speed it up. Unfortunately, if you're not using a data file per table, your ibdata1 file is going to continue to grow. Shrinking that file will be the subject of another post soon.

Friday, November 16, 2012

PHP is_int() vs. is_numeric()

These two functions seem similar, don't they? In fact, is_int() seems like a more specific version of is_numeric(). You may, for example, want to make sure that the ID of a blog post that is being passed through the query string is not only numeric but, specifically, an integer. After all, 7.145 is numeric, but you want to make sure the incoming value hasn't been tinkered with and is simply a 7. You'd be tempted to do something like the following:

if(isset($_GET['blog_id']) && is_int($_GET['blog_id'])) {
     // do something with the blog ID
}

Unfortunately, this won't work. It turns out that is_int() will return false when the value passed to it is a string, whereas is_numeric is perfectly capable of evaluating a string and deciding whether it's something numeric. So, even if the value of blog_id as passed through the query string is a 7, this will return false.

"Wait... What?" you may be saying to yourself, right now. Yes, you read that correctly. It will be false, even if it's a 7. That's because (and here's the important bit) anything passed in the query string is considered by PHP to be a string. Even though there are no quotes around it, even though, to you and me, it's a number 7, PHP reads that as a string. This goes for all superglobals, in fact. In short, anything that is accessed through a '$_<something or other>' will return as a string: $_POST, $_SESSION, you get the idea.

One way around this might be to cast the value as an int, like so:

(int)$_GET['blog_id']

Or you could settle for is_numeric(), knowing that it will be fine with scientific notation, floats, etc. But that might be enough to make sure the value is safe, even if it doesn't match.

The two main takeaways for me, here, are that:

  1. Values stored in superglobals translate as strings in PHP.
  2. is_numeric() can reliably check to see whether strings are numeric or not, whereas is_int() will return false for strings, even if it looks like an integer.
Hope this helps someone!

Sunday, September 9, 2012

Getting the 'protected' Directory's Parent in Yii

I needed the directory just above "protected." Here's how you do it:


Yii::getPathOfAlias('webroot')

Accessing a Module from Anywhere

I recently had the need to access the configuration variables for a module from within a controller that didn't own the module. It was a little frustrating that I was inside a widget that was a component of the module. Someone new to Yii or to MVC might assume, "Hey! I'm in the modules own widget. Why can't I get to the variables?" But think of it a bit like a stream: data moves down from controllers, which do the processing bits, and flow to relatively "dumb" things, like views which are only supposed to display, you know, stuff. If things were to move the other direction, it would like the stream getting backed up or even polluted. So, we keep things flowing in the right direction, letting the smarter parts of the app pass data down to the intentionally less smart parts.

All that being said, I *could* have passed the values to the widget - which is basically a view - but this was already a relatively poorly designed 3rd party module, and I just wanted to move forward. (This is also a very useful module that provides a lot of functionality that I'd prefer not to write myself, so I'm making modifications to get it to work a little better.) This meant finding some other way to gain access to some variables that had been set in the config file for the module. This is done like so:


// other Yii confi things go here, like components, import, etc

'import' => array(

      ...

),

'components' => array(

     ...

),

// and here is where the module goes

'modules' => array(

     'moduleName' => array(

          'moduleVar1' => 'value',

          'moduleVar2' => 'otherValue',

          'moduleArray' => array(
               'key' => 'val'
         ),

     ),

// More things happen here


In this example, I wanted to do something like find out what the value of "moduleVar1" was. There's a way to get at these sorts of things by basically starting from the top (the application level) and working your way down by using Yii::app(). Here, for example, is how you can see which modules are loaded across the entire application:


print_r(Yii::app()->getModules());


To get a specific module, you do this:

Yii::app()->getModule('moduleName');


To get the value for a variable from that module, you'd do this:

Yii::app()->getModule('moduleName')->moduleVar1


And actually, I had an array of values called 'config' like 'moduleArray' above, in which case I needed to get the value of a key of that array, like so:

Yii::app()->getModule('moduleName')->moduleArray['key']


Hope this helps someone.


Monday, July 30, 2012

Migrating a Non-MVC Site to MVC Gradually


I'm currently working on a (really awful) site that is unfortunately not using an MVC (model-view-controller) framework. In fact, this (incredibly horrid) application doesn't use any kind of best practices. It might even be considered a top candidate for the poster child for How To Do Nearly Everything Poorly: it isn't DRY (don't repeat yourself), tightly-coupled, almost wholly uncommented, and on and on. You get the idea.* But it's also in production, and management has been in constant "need" of adding features and "fixing" things. I am sure that none of you know what I'm talking about...

By some miracle, I have found myself with a moment to breathe and to reflect on the architecture while the business tests a major upcoming release, so I've started poking around to see how to address our greatest pain points. Our dev team has been lamenting the lack of an MVC framework for some time, now. I have been a big fan of the Yii Framework for over a year, and I think I've managed to convince a couple of people that matter (including my main team mate) that it's an excellent choice for our app. The problem is convincing the Powers that Be to let us rewrite the entire site from the ground up! I just don't think they're going to allow us that kind of time to make changes that they won't immediately see.



Rather than simply give up, however, I've been using this momentary slow-down to consider how we might make the changes gradually and try a couple of things out. After a bit of Googling and reading up at StackOverflow, things were looking a bit bleak; most people who responded to questions about how to gradually migrate from a non-MVC site to an MVC framework pretty much said, "You have to do it all at once. Forget doing it sections at a time!" (Perhaps I was missing the more constructive articles, but the ones I saw were very similar to each other.) I found that to be defeatist and not particularly creative. It seems to me that, given the right combination of tools, you can do just about anything. I was pretty certain I'd actually worked on projects that went back and forth between different code bases that worked in parallel, using Apache's mod_rewrite to determine which root to use to serve up documents.

That became my mission, this morning - to figure out how to fall back to the old codebase if my attempts to respond to a request using a controller/action pair were unsuccessful. Although I haven't put it through serious rigors, yet, I'm happy to say that my early tests are actually working. I wanted to provide the basic approach, here, in case it helps someone else. Feel free to comment on it for improvements.

First off, some additional background:

We're using LAMP - Linux, Apache, MySQL, PHP. It couldn't get more classic LAMP than that, unless we used Perl. So, I'm discussing this in terms of PHP features, and I'll be using mod_rewrite, because I can. So there.

I also have the ability to edit my Apache conf files. If you can edit just your vhosts directives, you can probably do this. I'm not sure it works if you're using .htaccess. I think it might not, but perhaps someone can help with that.

It seems like we'd like to use Yii, but it's possible that we'll start with a much simpler (less nice) homemade framework that will be easier to then refactor into Yii. (We're going to have to redesign the entire db, and I think it will be better to hold off on Yii until that's done.)

I'm writing this with the assumption that you know something about MVC frameworks.

Finally, the site has a relatively straightforward directory structure. There are some twists and turns, but let's just say they dumped most of the public files in the main document root - call it "application" - and the administrative areas into a subdirectory called "admin." There are images and include directories, as well. In a nutshell, the original application structure looks something like this:

.
|----- application
|   |----- index.php
|   |----- faq.php
|   |----- contact.php
|   |----- images (images go here)
|   |----- includes (some includes)
|   |   |----- header.php
|   |   |----- footer.php
|   | ----- admin
|   |   |----- index.php
|   |   |----- do_important_things.php
|   |   |----- mangle_accounts.php

I think you get the idea. With "application" as the Apache document root, you can refer to "images" as "/images" from anywhere in the app and it "just works." The includes directory is just "/includes" - very convenient. And there's an admin directory with it's own index.php. Everything is accessed by going to the domain followed by some file or directory, e.g., www.crummyapp.com/faq.php or www.crummyapp.com/admin/mangle_accounts.php.

But I really despise this app, and I want to set up an MVC framework and start rewriting functionality like that handled in do_important_things.php in a way that lets me continue to use mangle_accounts.php until I've had a chance to rebuild it and then not simply delete the old file but somehow send it to a fiery File Hell. Until that time, it has to work. Well, it has to "work" as well is it does now.

My main strategy will be to:

  1. Add a front controller to intercept the request for my new MVC way.
  2. Leave the old files intact, somehow, and let them live, for the time being.
  3. Not get too crazy about models and views, just yet. Or maybe I will. But I'm mainly interested in that front controller. 
  4. Not try to set up a fresh Yii app and move it all over there in one fell swoop. This is an in-between solution, for now, that will borrow a lot from Yii, because it's awesome and because it will make later migration to Yii easier.


Oh, if I had a nickel for every time I'd lamented, "My kingdom for a front controller!" And although I do want all the other goodies that come with MVC, the first step will be getting a front controller working. Why is that? Because that's really the lynch pin for everything else. If I can gain control of that initial request, I can do pretty much whatever I want with it. It doesn't need to be fancy - I just need to be able to wrangle control away from the old faq.php and take my requests somewhere else. Whether that ends up initially being cleaner static files that are then further refactored into views and models and such isn't as important to me right now. (One thing I've learned as I've gotten older, my young grasshoppers, is that patience is a really good thing. Rome wasn't built in a day, etc., etc.)

My new application directory structure is something like this:


.
|----- application
|   |----- index.php (front controller)
|   |----- images
|   |----- css
|   | ----- protected
|   |   |----- config
|   |   |----- controllers
|   |   |----- models
|   |   |----- views


|   | ----- framework (core libraries for the framework)

That's probably familiar to a lot of you. The "framework" directory is like the "yii" directory. The "application" directory is still the document root. index.php is the front controller, as labeled.

The main question is, "How in the world do I make this new thing work in parallel with the old thing?" To answer that, I started by planning the directory structure. I decided to leave the old application in it's own directory at the top of the document root. Here's what it all looks like combined:


.
|----- application

|   |----- index.php (front controller)
|   |----- images
|   |----- css
|   |----- protected
|   |   |----- config
|   |   |----- controllers
|   |   |----- models
|   |   |----- views


|   |----- framework (core libraries for the framework)
|   |----- oldapplication

|   |   |----- index.php
|   |   |----- faq.php
|   |   |----- contact.php
|   |   |----- images
|   |   |----- includes (some includes)
|   |   |   |----- header.php
|   |   |   |----- footer.php
|   |   |----- admin
|   |   |   |----- index.php
|   |   |   |----- do_important_things.php
|   |   |   |----- mangle_accounts.php


All of the pre-existing files are now in a separate directory, oldapplication.

If that's where I left it, assuming I was using a .htaccess file typical of MVC frameworks like this (Google it - they're everywhere), when the visitor arrived at the application, Apache would serve up the index.php front controller file. That would do some not terribly fancy front controller stuff that I won't get into, but the main thing to note is that, like most MVC front controllers, it would be expecting the URL to contain some reference to a controller and an action. For example, the URL might look like:

http://www.crummyapp.com/home/index

in which case it would know to execute the "index" function (the action) contained in the controller named "home" (i.e., protected/controllers/HomeController.php).

That's great for my new sections of the app, but what if I want to oldapplication/faq.php where it is, for now, and serve it up old-school? Unfortunately, my standard .htaccess file would have a hard time with that.

The solution came, in part, from this article: MVC Framework Routing (static content vs. dynamic). The writer modified their app's Apache config file, adding the following to their vhosts directives (edited to match my directory structure):


RewriteEngine On
RewriteCond %{DOCUMENT_ROOT}/oldapplication%{REQUEST_URI} !-f
RewriteCond %{DOCUMENT_ROOT}/oldapplication/admin/%{REQUEST_URI} !-f
RewriteRule ^(.*)$ %{DOCUMENT_ROOT}/index.php [L]
RewriteRule (.*) %{DOCUMENT_ROOT}/oldapplication$1 [L]


A brief dissection, line by line:

  1. Make sure the RewriteEngine is set to "On" or this might not work at all.
  2. Once that is taken care of, there are two conditions specified by the "RewriteCond" directive. The first condition checks to see if the request is NOT a file in the old application's main directory. The next condition checks to see if the request is NOT a file in the old application's "admin" subdirectory. (The "!-f" means, "see if this is not a regular file.")
  3. After that, is the first RewriteRule. That RewriteRule is what happens if those two conditions are BOTH true. (The conditions are basically chained together by an "and" unless I explicitly use "OR" which isn't what I want.)
  4. That second RewriteRule is not preceded by any conditions, so it stands as-is.

Without getting into too much detail about mod_rewrite, these lines will check for the existence of the requested file in both the original app's home directory and in it's admin subdirectory. If it cannot find the file, it will go to the front controller (index.php in the doc root). But, if those conditions fail, and it doesn't go to the front controller, it proceeds to the next rule which will rewrite the request as www.crummyapp.com/oldapplication/whatevertherequestwas.php.

So, that's pretty great! I can now serve up both old and new files. If it doesn't find the old, it will go to the MVC version. Nice! But I still had a problem, at that point. Do you remember those "images" and "includes" directories? Well, throughout the application, there are references to "/images" and "/includes" - ugh! I'm not going to edit all of those URLs!

To solve that problem, I used PHP's ability to set configuration options dynamically using ini_set(). The configuration setting I needed to edit is called "include_path" which does what it sounds like it does, sets the default include path for PHP. In other words, when PHP goes looking to include something, it checks any and all include paths specified until it runs out of them. The line of code looks like this:

ini_set("include_path", ini_get("include_path").";".$_SERVER['DOCUMENT_ROOT']."/oldapplication");

So, the include_path setting now has the "oldapplication"directory in it when looking for things like "images."

Believe it or not, the basic tests I've run serve up the old files beautifully, with no broken images or missing included classes, headers, or whatever. Neat!

One loose end that I still need to address is:

index.php - Because this is the default index page in any directory of my application, it is in conflict with my front controller's name. I could rename the front controller (and I might just do that), but it is still going to be a problem for those links that have the directory name but do not include index.php as the desired file! (This is a pretty common thing to do, e.g., http://www.crummyapp.com/admin/.) mod_rewrite won't know which filename is being requested, so I think I have to go through and explicitly include that for all URLs that need it.

There are probably others. We'll see.

Anyway, I'm pretty excited, because that was a pretty big hurdle, and it appears to be working! If I run into other things, I'll try to remember to post them here.

Cheers!

* I did NOT have a hand in writing or designing this thing. My present employer purchased it from a competitor that was sinking like the Titanic, and no techies had a chance to look behind the curtain before they agreed to buy it. 'Nuff said.

Thursday, March 1, 2012

Helpful Mod Rewrite Tips

This didn't solve the problem I was trying to fix, but I didn't want to forget about it! It's a wealth of useful info:

http://www.askapache.com/htaccess/modrewrite-tips-tricks.html

Monday, January 30, 2012

Vim Notes

I love vim. I mean, there are a couple of great GUI editors that I like, but when I'm working in a terminal, vim is not just my editor of choice, but I actually prefer it to a bunch of other GUI editors I could choose from. The problem is that there is SO much to know. I've got the basics down, since I've been using it for over 20 years, but I honestly haven't taken the time to learn some of the more intermediate to advanced features until recently. I thought I'd keep some notes along the way in this blog post. Enjoy!

To paste in yanked lines and have them indent with the surrounding code, use ']p' instead of just 'p'
Use '>' to indent and '5>>' to indent 5 lines

Source: http://stackoverflow.com/questions/235839/how-do-i-indent-multiple-lines-quickly-in-vi

Using :split to open another document is great. But you don't always want the two windows to be equally divided, you need to make some adjustments. You could do this:

ctrl-w +

but you might have to do it over and over. Instead of typing that twenty times, just do this:

20 ctrl-w +

More goodies:
http://www.oualline.com/vim-cook.html#copy_block

Thursday, November 17, 2011

Firefox4 on OSX 10.5 (leopard) failing to open from command line : bang_head_on_wall

I needed to launch Firefox from the command line in OS X 10.5.8, and it just wouldn't work. This blog post saved me:

Firefox4 on OSX 10.5 (leopard) failing to open from command line : bang_head_on_wall

So very easy to fix!

Tuesday, November 15, 2011

Selenium 2 from PHP code | Web Builder Zone

This article breaks down the current array of choices for PHP and Selenium better than anything I've seen so far. Nice work!

Selenium 2 from PHP code | Web Builder Zone

Thursday, November 10, 2011

Continuous Integration Pt IV - Working with PHPUnit, Selenium, and SauceLabs

Ah, yes, the saga continues!

Today, I am going to try to get our PHP Selenium tests to run on SauceLabs' OnDemand service. SauceLabs is a cross-browser testing service. You can run your tests by hand - by opening an instance of a particular browser on their server via your own Web browser - or in a more automated fashion. I want to do the latter. What this entails is writing tests to tell the SauceLabs servers with which browser we wish to test on which operating system (e.g., IE7 on Windows). You can run the same test across multiple browsers, and any failed tests will be recorded for you to see exactly what went wrong. We're planning to automate the running of these tests, kicking them off when a developer pushes their code to the staging server.

The tests can be written in a number of languages for either Selenium RC or WebDriver. Because I'm using PHPUnit (since I code primarily in PHP), I'm stuck with Selenium RC, which is quite old but is supported by extensions in PHPUnit. I've successfully run tests on my local development machine, but now I'm going to adding one more layer of complexity, i.e. the SauceLabs servers. This requires one more set of extensions be added to the PHPUnit test framework. Here goes!

Install the SauceLabs SauceOnDemand packages from Pear:


sudo pear channel-update pear.php.net
sudo pear upgrade pear
sudo pear channel-discover pear.phpunit.de
sudo pear channel-update pear.phpunit.de
sudo pear channel-discover components.ez.no
sudo pear channel-update components.ez.no
sudo pear channel-discover pear.symfony-project.com
sudo pear channel-update pear.symfony-project.com
sudo pear channel-discover saucelabs.github.com/pear
sudo pear channel-update saucelabs.github.com/pear

No problems there. I'd already done the "discovering" of pear.phpunit.de and pear.symfony-project.com, so I only needed to channel-update for those.

Next, install the SauceLabs Selenium PHPUnit extensions:

sudo pear install -a saucelabs/PHPUnit_Selenium_SauceOnDemand

Great! That worked for me with no problems.

The next steps requires a SauceLabs account, I think. It was very cryptic, at first, because, without being logged in, it just says to do the following:


sauce configure


But that didn't do anything but throw a strange error and then tell me I was ready to run saucy tests and that I feel hot and saucy (which I didn't, btw).


After creating a free account (which anyone can do) and logging in, returning to the instructions page revealed the same instructions to do "sauce configure," but that command was followed immediately by my username and a hash code. So, I ran that. No errors, and now I really am feeling hot and saucy.


I'm going to run a test of a test. (I guess I'm feeling saucy and testy.)


curl -s https://saucelabs.com/example/se1/php/private-JJHyCGMF | bash

Well, this threw up a big ugly error with stacktrace and junk. It looks like something is calling an undefined method:

undefined method PHP_CodeCoverage_Filter::getInstance()

That's no good. I'm wondering if I need to update my version of PHPUnit is old? Maybe I should just upgrade. I did this

pear upgrade

and suddenly faced all kinds of ugly. Upgrade failed. Doh! Oh, I forgot to use 'sudo':

sudo pear upgrade

Much better! Now I've got upgraded Pear stuff.

I ran the curl command from above again, and got exactly the same results. Hmmm...

Looking at my Pear channels and packages, I've noticed that I have two versions of PHPUnit installed, one in the traditional pear.phpunit.de channel, and one under saucelabs.github.com/pear. the saucelabs version is older than the other one. There is also a different version of PHPUnit_Selenium in that channel, though that's not the one that is active. I've emailed the SauceLabs people for some help on this. My concern is that it's due to conflicts in one version vs. another, and I'd like to nip that possibility in the bud.

In the meantime, I'm going to look at the problem code. The error message is:

Call to undefined method PHPUnit_Util_Test::getParallelismSettings() in /opt/local/lib/php/PHPUnit/Extensions/SeleniumTestCase.php on line 352

I won't write about that in this post, however. For the time being, this is a work in progress.

Wednesday, November 9, 2011

Continuous Integration Pt III - Working with PHPUnit and Selenium

The continuing adventures of Hollyii as I attempt to implement continuous integration at my job. I've got:


  • Ant (my build tool), with a very small, very basic, mostly useless build script just to make sure it goes
  • PHPUnit (unit testing), which is running happily on my local OS X laptop
  • A means of bootstrapping PHPUnit to integrate it into the Codeigniter application, along with very few (meaningless) tests
  • Selenium Server installed, which hasn't been put to the test at all, except to ascertain through ps auxwww that it's genuinely running

So, what's next? Well, I think the next thing is to set up the Selenium extension for PHPUnit. This, I'm happy to report, is very easy. Assuming you're using Pear and have installed PHPUnit through Pear, this is all you do:

sudo pear install phpunit/PHPUnit_Selenium

I need to 'sudo' due to my permissions, but you might not need to.

Everything went well for me, and it's installed. I can verify using

pear list -a

Yep. It's there. Good.

I'd like to run a basic test with Selenium. I'm going to be referring to the PHPUnit documentation, found here, and a Zend Dev Zone article, found here.

Before I can do that, it turns out that I need one more Pear package, Testing_Selenium. I've installed it thusly:

sudo pear install pear/Testing_Selenium-beta

Note the "-beta" on the end. This was necessary due to the fact that my preferred state for Pear packages is "stable," but this package default state is "alpha." Including the "-beta" resulted in my installing v0.4.3 (beta) not 0.4.4 (alpha).

I've copied and pasted a test right out of the PHPUnit manual. It looks like this:

<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';

class WebTest extends PHPUnit_Extensions_SeleniumTestCase
{
    protected $captureScreenshotOnFailure = TRUE;
    protected $screenshotPath = '/var/www/localhost/htdocs/screenshots';
    protected $screenshotUrl = 'http://localhost/screenshots';

    protected function setUp()
    {
        $this->setBrowser('*firefox');
        $this->setBrowserUrl('http://www.example.com/');
    }

    public function testTitle()
    {
        $this->open('http://www.example.com/');
        $this->assertTitle('Example WWW Page');
    }
}
?>

This, alas, did not work. It should have failed, because the HTML title property for www.example.com is not really "Example WWW Page." But my test skipped the test altogether. It comes back as "OK, but incomplete or skipped tests!" Not what I wanted.

I did a little searching in the PHPUnit documentation, and I've learned that skipped tests are often due to the environment not supporting the test you're trying to run. You can even mark things as skipped, if you determine through your code within the test that you don't have access to something. They're example was:

<?php
class DatabaseTest extends PHPUnit_Framework_TestCase
{
    protected function setUp()
    {
        if (!extension_loaded('mysqli')) {
            $this->markTestSkipped(
              'The MySQLi extension is not available.'
            );
        }
    }

    public function testConnection()
    {
        // ...
    }
}
?>

I'm not doing this in my code (that is, checking for loaded extensions, marking things as skipped), but the fact that I am getting a "skipped" message suggests that my Selenium set-up isn't quite right. So, now I need to figure out how to make sure Selenium is set up correctly.

After some sleuthing, I have discovered that my version of Selenium is perhaps too new for PHPUnit's extension. When I originally started this quest for continuous integration, I intended to use Selenium RC but found that it had be deprecated in favor of the new Selenium 2 (a.k.a. Selenium WebDriver), so I downloaded that one. (Who wants a deprecated application?) But that was not what I needed, so I am now backtracking out of it.

The first step was to download the older model from the seleniumhq.org site. The page the link directs you to has all of the available downloads, including all the versions of Selenium RC. I downloaded the newest version of that program. This gave me a zipped archive containing a slew of files. All I needed was the selenium-server.jar file. I moved it to the directory in which I like to keep jar archives.

Moving the file was all that I really needed to do to "install" it; jar files are executable, and all you really need to do to run them is invoke java and pass it the location of the jar archive. But I'm on a Mac, and I want to take advantage of the launchctl program to launch the file for me. I created a plist file (almost identical to the one in my previous post), stored it where the plist files go (usually ~/Library/LaunchAgents), and issued two commands to 1) load the plist file and 2) to start the executable:

launchctl load ~/Library/LaunchAgents/org.seleniumhq.selenium.plist
launchctl start org.seleniumhq.selenium

But it didn't work. It failed silently! I know this because I ran

ps auxwww | grep selenium

All I saw was my old version. Doh! I need to stop the old one and start the new. They're both listening on the same port, so the new one couldn't start. It would have been nice to have some feedback, but oh well.

launchctl unload ~/Library/LaunchAgents/org.nhabit.Selenium.plist

That stopped the old one. I ran the same previous two commands as before:

launchctl load ~/Library/LaunchAgents/org.seleniumhq.selenium.plist
launchctl start org.seleniumhq.selenium

And now I can try to run my test again.

Well, it still skipped my test, but I know that what I've done was not a waste of time (at least, I think it wasn't), because I really needed to install Selenium RC anyway. Now, it's just a matter of making sure I've got it set up properly.

Alright. I poked around some more, using mindfulness based stress reduction to keep from having an actual tantrum on the hard, tile floor. Here is a tip: use the '--verbose' option when running phpunit. Let me say it again: use '--verbose' to help debug your phpunit testing! For example:

phpunit --verbose mytest.php

I had been using it to debug other tests, and it didn't give me anything. But this time, it let me know that it was attempting to connect to Selenium on port 4444 and couldn't. Very useful! Upon inspection, I realized I was running Selenium on port 4443. A-ha! Shut the server down, changed the port number, restarted it, and presto! It works. Amazing. I can now die happy.

Continuous Integration Pt II - Unit Testing and Codeigniter

I'm still plugging away at getting our shop set up for continuous integration. We use Codeigniter for our application, and this presents a huge can of worms with regards to unit testing. First of all, Codeigniter does include some minimal unit testing support within the app, but, by all accounts, it's pretty darn small. The holy grail for unit testing in PHP, IMHO, is PHPUnit. It's pretty much the standard, and it's well supported. So, it makes sense to use that, since our tests will continue to be relevant, whether we change versions of Codeigniter, whether developers come and go, etc. It's a nice constant to shoot for.

The big problem is that PHPUnit doesn't just naturally hook into Codeigniter. Things get even more complicated when you start looking for solutions for bootstrapping it, as it greatly varies depending on the version of Codeigniter you're using. Ellis Labs (the developers of Codeigniter) is working on integrating PHPUnit into the framework at some point in the future, which will be version 2.?. In the meantime, another developer has started with their code and completed it to make it work, and it works with version 2.0.3. Well, we're on version 2.0.2. So, that's a bummer. The earlier attempts to integrate PHPUnit with Codeigniter are usually for 1.7.x and below.

In the end, I managed to get another developer's much simpler bootstrapping code to work. You can find it here. That's a Codeigniter forum post. Look for CarloGI's post about his technique. He includes a download of example files. Download the files, put the where they belong, and make sure you modify the _getDBObject function of the bootstrap.php file to match your mysqli settings for your app. You'll want to include the --stderr argument when you run phpunit so that you don't get the output buffers error. One other thing I had to do was to comment out an echo statement in myControllerTest.php. Otherwise, it will cause the output buffer error, as well.

Sorry these instructions are so cryptic. I wanted to get them out there (mostly for myself), but they might help someone else. If I have time, I will add more detail.

Thursday, November 3, 2011

Format My Source Code for Blogging

This is very useful for blogging source code.

Format My Source Code for Blogging

I'll go through and fix some previous posts soon.

Notes on Apache Ant

I'm an Apache Ant newbie. I've known for years that it exists, but I've never used it. I just installed it, today, and now I'm trying to understand what it does and how to use it. Here are some notes:


  • An Ant build contains one project
  • That project has at least one target
  • Each target contains tasks


The build file defines these things and is written in XML and is called build.xml. It has to define at least one target. The target defines one or more tasks to perform when it is run. Here is my first build file:

<?xml version="1.0" encoding="UTF-8"?>  
<project name="helloworld" default="init" basedir=".">  
    <description>  
    Build file for the Hello World application.  
    </description>  
    <target name="init" description="Initialize the build.">
        <touch file="testinit" />
    </target>
</project>

It includes a description (of the project) and one target. The target is simply a set of tasks you want to run. It can be called whatever you want, really. The target name attribute is what you will call from the command line when you want to run those tasks. In this example, my target has the name attribute "init". In that target, the only task I've included is the "touch" task. This will execute the *nix "touch" command. The "file" attribute within the "touch" task specifies which file to touch. (See the Ant manual for more available tasks. They are legion.)

Now, to run this, I go to the directory containing the build.xml file and issue the following command:

ant init

Notice that I didn't say "ant build.xml" or "ant build." This is because I'm telling ant to run a specific target. Also, if I change the name of the file from build.xml to moo.xml, it will break. Ant expects the build file to be called build.xml.

Wasn't that fun?

Continuous Integration Part I

I'm setting up a suite of continuous integration tools for my current job. We are a PHP shop using Mercurial for version control. My catalogue of tools is as follows (so far):

  • Ant (automated build tool)
  • PHPUnit (unit testing)
  • Selenium (functional testing)


I haven't decided on a CI server, yet. I'm looking at Hudson, as it has an extension to support Mercurial. Eventually, I'm hoping we'll use a service, like SauceLabs' Sauce OnDemand to perform our functional tests, since I'd like to take advantage of their cross-browser tests. They use Selenium RC, so we'll want to get started using that, ourselves. As of version 3, PHPUnit has support for writing tests for Selenium. I don't know, yet, whether those would work with SauceLabs, nor do I know whether we'll use those or write our scripts the traditional way, as per the documentation on the Selenium site. Another thing to note is that Selenium RC (Selenium 1) is officially deprecated in favor of Selenium Webdriver (Selenium 2), but they're going to support Selenium RC, doing bug fixes and the like, as it has more features than Webdriver (at least for now).

To get Ant to work with Mercurial, I'm going to need ANT4HG.

I still need to ascertain what all, exactly, will go into my build process. I want it to represent a complete rebuild of our entire Web app from scratch, complete with a set of clean test data and the application code, as well as all tests. I'm suspicious, however, that there will be more to it than doing a clean check out (in hg terms, a clone) of our code base and grabbing a db schema from somewhere, but we'll see.

Tasks done so far:

  1. I've installed PHPUnit using Pear (which I had already installed through Macports when I installed php-5). 
  2. I have installed Ant (an Apache project) using Macports.
  3. Installed ANT4HG as follows:
To install ANT4HG, I downloaded the binary from the SourceForge downloads. I extracted the zip file and ended up with a jar file. I then moved the jar file to the ant lib directory. On my system, that is located here: /opt/local/share/java/apache-ant/lib. It should now be available for Ant to use.


OK, now for Selenium (deep breath). I need the Selenium standalone server. Eventually, I won't need this (I hope), since we'll rely on SauceLabs for this service. But for the purposes of setting up a test of our chosen tools - and also because we're not going to subscribe to a service, yet - I need to have it up and running locally. I'm following instructions I found here, with the exception that I put my selenium jar file in /opt/local/share/java/selenium instead of /usr/lib/selenium. (Either way, I needed to create the selenium directory.)

I then created a launch file so that Selenium would be launched on system startup. The file is copied from the instructions I used on Dan Straw's site. Since my "selenium" directory is in a different location than his, I had to modify my launch file to reflect the proper location. I also had to change the name of the jar file, as he mentions in his write-up. I loaded the plist file and started the service. Using "ps auxwww | grep -i selenium" I learned that it's up and running. Good.

Everything is installed, now, so I just need to get my feet wet and play with it. I'll save that for another post. In the meantime, I'll be looking this.

Monday, October 17, 2011

Searching Relations in Yii's CGridview

[NOTE: I'm typing this pretty frantically, just to get it out there and then move on to my next task! So, it might be a bit confusing. You should familiarize yourself with Yii's CGridView widget before trying to understand what the heck I'm rambling on about below.]

In the default grid view widget usage in admin.php (when you use gii to generate it), all of the fields displayed are directly tied to the model you're viewing. Many times, you don't want to see the value of a foreign key but rather the actual string that would be associated with that particular foreign element. For example, if I have a grid view to display a series of Company models, I don't want the "state" column to display "2" if it really could display "California." That's easy enough to fix by providing some additional information in the CGridView widget definition. Instead of including the state column as "stateId", include an array that specifies the column in the Company table ("stateId"), a header to display at the top of the grid column ("State"), and the value that should actually be shown ("$data->state->name" which represents the "name" column from the related model, "state," for the current data element - $data - of the many Company models returned):


<?php $this->widget('zii.widgets.grid.CGridView', array(
'id'=>'company-grid',
'dataProvider'=>$model->search(),
'filter'=>$model,
'selectableRows'=>2,
'columns'=>array(
'name',
'street',
'street2',
'city',
array(
'name'=>'stateId',
'header'=>'State',
'value'=>'$data->state->name',
),
'zipCode',
array(
'class'=>'CButtonColumn',
),
),
)); ?>

The problem, however, is that you lose the search field at the top of the column that is used for filtering the result set shown in the grid! To get that to show up again, you'll need to do a couple of other things. Both steps involve the model itself. In my case, I'm dealing with my Company model. Here goes.

First, you need to change the way the comparison is handled in the search() function when it comes to the model's state information. By default, you probably would have something like this:

$criteria->compare('stateId', $this->stateId);

But this doesn't get you what you want, because we're not really comparing the value of a foreign key with an integer that has been entered. (I mean, who's gonna enter an integer when they're searching for Georgia?) So, make it look like this:

$criteria->compare('state.name',$this->stateId, true);

This tells the comparison to compare the name of the state with the value returned from the incoming 'stateId' field (which is now going to be a textual representation of the state's name).

Notice that I'm using the "dot" notation, as in "state.name". This is the syntax for querying a table with the alias "state" for the value of it's "name" column, of course. The reason I can do this, here, is that I've already made sure that

  1. I have a relation in Company with the State table that is referenced with the alias "state" (part of the Company model's relations() function that returns an array of relations).
  2. I have included "with" in my search() function's criteria to let Yii know to join the State table when it selects the Company models:  $criteria->with=array('user','image','video','state','approvedBy0','deletedBy0');
  3. I have included "together" in the criteria and set it to true: $criteria->together=true;
These are important for defining for Yii how to retrieve the related states information in a way that makes it available to search().

The other piece of the puzzle involves the rules() function of the Company model. You have to declare that the "name" column of the State table is safe on search. Most likely, you already have a rule for searching that looks something like this:

array('name, street, street2, city', 'safe', 'on'=>'search')

This means that the columns, "name," "street," and "street2" are safe to include for the search scenario. But we also want the "name" column from our State table to be safe. The way we add this is to take advantage of our relation (which has provided us with the alias, "state," for the State table). Because states are retrieved actively - since "with" includes the state alias and "together" is set to true - we can refer to it's member variables. To include the State tables "name" column in the search, then, just add it to the array:

array('name, street, street2, city, state.name', 'safe', 'on'=>'search')

That's all there is to it! You should now have:


  • A column in your grid that displays the state's actual name, rather than the foreign key for the state
  • A header at the top of the column that reads "State" instead of "stateId" (how gauche)
  • A text field for searching on states that compares what you've typed in with the actual state name values from the available models.

I've seen examples of how to make the search field a pull down menu rather than a text field (which makes sense for states), but I'm not going to post on that, at the moment. I would point out, however, that you can also now sort the state names by clicking the "State" header. Sweet!


Friday, October 14, 2011

Updating a Listview in Yii with Ajax

This was an unbelievable exasperating feature to add. I have no idea why this didn't work in the myriad ways I tried to make it work, but I've got it working, now, so I wanted to post.

I looked at every example of updating a CListview that I could find on Google. None of them were doing what I wanted to do (and what I'd assumed was a very common task). In a nutshell, I wanted to have a listview on my view page and then update the results when someone clicked on a link. All of the examples I saw involved submitting a form. I saw one that was using a link, but the details provided in how they solved it were so miniscule as to be almost humorous (had I not been so frustrated).

I won't discuss this solution in great detail. I'll simply include the information below.

The View
At the top of the index.php view, I have this:

<?php

Yii::app()->clientScript->registerScript('ajaxUpdate',
"
$('.ajax_link').click(function(){
$.fn.yiiListView.update('companyList')
});
return false;
", CClientScript::POS_READY);
?>

Inside the view itself is the listview:


<?php $this->widget('zii.widgets.CListView', array(
'dataProvider'=>$dataProvider,
'itemView'=>'_view',
'id'=>'companyList',
)); ?>

The links are generated in the view, too, of course. They look like this:


foreach($allLetters as $letter) {
if(in_array($letter, $dbInitials)) {
/*
echo CHtml::ajaxLink(strtoupper($letter), CHtml::normalizeUrl(array('index', 'initial'=>$letter)),
array('success'=>"$.fn.yiiListView.update('companyList')")
);
*/
echo CHtml::link(strtoupper($letter), CHtml::normalizeUrl(array('index','initial'=>$letter)), array(
'class'=>'ajax_link',
));
} else {
echo strtoupper($letter);
}
// Add some whitespace.
echo '&nbsp;&nbsp;';

}

In fact, that's pretty much all there is in my index.php page. A Javascript portion that is registered as a client script, a list view with the id set to companyList so I can refer to it later, and a series of basic html links with a class of ajax_link so that the click event is triggered in the Javascript.

On the server side, the controller action for index looks like this:

public function actionIndex($initial='')
{
if($initial) {
$criteria = new CDbCriteria;
$criteria->compare('name', $initial . '%', true, 'AND', false);
} else {
// Retrieve all companies to list
$criteria = new CDbCriteria;
$criteria->with=array('video','image','state');
$criteria->together = TRUE;
}
// Only admin users can see ALL records. Others can only see approved records.
if(!Yii::app()->user->checkAccess('admin')) {
$criteria->addCondition('t.isApproved=:isApproved');
$criteria->params[':isApproved']=1;
}
$dataProvider=new CActiveDataProvider('Company', array('criteria'=>$criteria));

$this->render('index',array(
'dataProvider'=>$dataProvider,
'dbInitials'=>$this->dbInitials,
'allLetters'=>$this->allLetters,
));
}

If no parameters are submitted, it just renders all of the results. If an initial letter is submitted, it filters the results. Either way, the index view is rendered.

The main problems I encountered were that:
  1. The AJAX request would return the results I wanted, but the div wouldn't update.
  2. Once I finally got the div to update, I was getting an error from the yiiListView.update() method. Adding the "return false" in the Javascript code fixed that.
Done.