08 Aug 2010I just finished writing the test case for a custom AppErrorclass. Writing the tests case was indeed much more difficult than writing the class itself.
Here are some things I've found that may interest anyone wanting to test their own ErrorHandlers. Most of the ideas are taken from the core ErrorHandlerTest.
Anatomy of an error
Every class in cakePHP extends Object. And in Objectis defined a method named cakeError. It means that anywhere in your app you can call $this->cakeError() and the whole app will stop and display the specified error.
What cakeErrordoes is instantly create an instance of either Controlleror CakeErrorController, find the correct view, render it, and stop.
Because of the use of static variables, exitcalls and other happyness, testing several errors in a test case needs us to define some new classes to shortcircuit most of the logic.
Preparing the test
This step is actually pretty short. Just load the default ErrorHandlerby calling App::import('Core','Error');
Creating a dummy AppController
Then, you'll have to create a new AppControllerclass that extends Controller.
We will override the headermethod to prevent 'Headers already set' error when calling several errors in a row.
We will also override the render method to store in an inner property the name of the rendered action. It will help test that the correct error is rendered.
You may also need to define a custom list of helpers if your custom views are using any custom helpers.
Here is what it looks like on my side :
class AppController extends Controller {
// Helpers used in the view. If not set, will generate a fatal error
var $helpers = array('Caracole.Fastcode', 'CaracolePacker.Packer');
// Overriding the header method. If not set, will generate 'Headers already set' errors;
function header($header) {
$this->header = $header;
}
// Overriding render method to keep track of the rendered error
function render($action) {
$this->renderedAction = $action;
return parent::render($action);
}
}
Creating a TestErrorHandler
This class will extends your AppError. It will just overwrite two methods to make it correctly work in a test case.
First, we'll need to overwrite the __construct. The default construct will set the inner $this->controller property to either a Controlleror a CakeErrorControllerinstance depending if its the first error fired or not. I must admit that I haven't really understand the difference between the two. But I know that CakeErrorControllerextends AppControllerwhile Controllerextends Object.
And as we need to overwrite methods of this property, it being a CakeErrorControlleris great, while it being a Controlleris bad. Anyway, what I did was copy/paste the parent __construct into TestErrorHandlerand just force it to always create a CakeErrorControllerinstance.
The other method we need to overwrite is the _stop. If we don't, the whole script will halt after the first error you'll test.
So, enough talk, here's the code :
class TestErrorHandler extends AppError {
// Copy/paste of ErrorHandler construct method, but force a new instance of CakeErrorController as $this->controller each time
// CakeErrorController extends AppController, so we can overwrite its methods
function __construct($method, $messages) {
App::import('Core', 'Sanitize');
// Forcing CakeErrorController
$this->controller =& new CakeErrorController();
$options = array('escape' => false);
$messages = Sanitize::clean($messages, $options);
if (!isset($messages[0])) {
$messages = array($messages);
}
if (method_exists($this->controller, 'apperror')) {
return $this->controller->appError($method, $messages);
}
if (!in_array(strtolower($method), array_map('strtolower', get_class_methods($this)))) {
$method = 'error';
}
if ($method !== 'error') {
if (Configure::read('debug') == 0) {
$parentClass = get_parent_class($this);
if (strtolower($parentClass) != 'errorhandler') {
$method = 'error404';
}
$parentMethods = array_map('strtolower', get_class_methods($parentClass));
if (in_array(strtolower($method), $parentMethods)) {
$method = 'error404';
}
if (isset($code) && $code == 500) {
$method = 'error500';
}
}
}
$this->dispatchMethod($method, $messages);
$this->_stop();
}
// Preventing the error from stopping all the request
function _stop() {
return;
}
}
Writing your tests
Ok, your almost done. You just have now to write your tests. They have to follow a special syntax to correctly work.
First, you'll have to wrap your ErrorHandlercreation between ob_start() and $output= ob_get_content(), otherwise you'll end up with error popping right into your test case because the ErrorHandlerforce the controller to render the view.
You'll be able to access interesting properties through $errorHandler->controller->renderedAction and $errorHandler->controller->header. You can also directly test the view output through $output.
Ok, so here's one of my tests :
// DEV : Error will use the error layout
function testCallingErrorInDevWillUseErrorLayout() {
ob_start();
$errorHandler = new TestErrorHandler('missingController', $this->errorParams);
$result = ob_get_clean();
$this->assertEqual($errorHandler->controller->layout, 'error');
}
Conclusion
It took me some hours to glue all this pieces together, I hope it may be useful to others, too. Writing the AppErroritself was way easier, but as I'm now test infected I don't imagine writing code without the corresponding tests.
06 Aug 2010I sometime need access to a specific model in a component. Say a Usermodel for checking user rights in an Authcomponent. In that cases, I just create an instance of the model by calling a $myModel = &ClassRegistry::init($myModelName). It works perfectly.
Today I found that it wasn't working exactly as I wanted. In fact, behaviors used by models loaded that way are not completely set up. All their callback methods (beforeFind, afterFind, beforeSave, afterSave) works as expected, but the main setup method is never called.
This caused some havoc in my app because some *reaaaaally *important stuff was defined in the setup method
There I fixed it
I just manually fired all the setup methods by calling a simple :
foreach($myModel->Behaviors->_attached as $behaviorName) {
$myModel->Behaviors->{$behaviorName}->setup($myModel);
}
Once again, small fix, but does the trick. I did not file a bug report because I'm not really sure this is bug or if that is so by design.
06 Aug 2010Sometime you write a nice Behavior, that will automate a lot of stuff that almost all your models will need. But, unfortunatly, a couple of models won't need it. Actually, attaching it to them will even break your code.
So what do you do ? Do you resort to manually fill the $actsAs variable of each model, except for the two lonely loosers, or defining the $actsAs of your main AppModelreally seems more appaling ?
Lazyness to the rescue
If, like me, you prefer writing less code, you'd probably go with the AppModel approach. All you have to do is define a setup() method in your Behavior and check if it is applied to the right or wrong type of model. Fortunatly for us, the $model is passed as first argument.
Once you've identified the faulty models, you just have to disable the behavior for them. The BehaviorCollectionthat comes bundle into $model->Behaviors has a nice couple of enable/disable methods. Unfortunatly, they won't work from inside the setup() method because the Behavior is not yet correctly instanciated.
What you can do, however, is to hack inside the BehaviorCollection to update the inner _disabled key to add your own Behavior to the list.
function setup(&$model, $config = array()) {
[...]
if ($faultyModel) {
$model->Behaviors->_disabled[] = 'MyBehavior';
}
}
This is more than a bit hacky, I have to admit that. But it does the trick. Enjoy.
05 Aug 2010Getting the correct mimetype from a file in PHP is not an easy task. I used to find it through extension sniffing of the file combined with a known list of mimetypes.
Today I needed to find the correct mimetype to do some security checks on file uploaded by users. I couldn't rely on an extension-based approach because the filename could easily be faked by an uploader.
So I needed an other way. In fact, in PHP world there are at least 4 methods I'm aware of to get this information.
mimecontenttype
The classic PHP function mimecontenttype is supposed to return just that. Unfortunatly, it is deprecated. And not supported by Dreamhost, my host.
The FileInfo functions
We are now encouraged to use the Fileinfo functions instead of mime_content_type. Unfortunatly, they seems to be returning strange results. Alternatively, they are not supported by Dreamhost either (but it seems that you can ask them to install it on your server).
It is bundled into EasyPHP for Windows, but you need to enable it by uncommenting the line extension=php_fileinfo.dll in your php.ini
And you use it like this :
$finfo = finfo_open(FILEINFO_MIME);
$mimeType = finfo_file($finfo, $filepath);
finfo_close($finfo);
Also note that the mimetype may be returned in a text/plain; charset=us- ascii form. You may need to parse the result to get it in the format you need.
The getimagesize function
The getimagesize function can be called on any image file. It will return an array containing image informations like width, heightand of course the mimetype.
Unfortunatly, it will cause a E_WARNING if called on a non-image file. You can't even catch that using a try/catch. You can suppress the error using @, tho.
Here's how I use it :
$imageData = @getimagesize($filepath);
if (!empty($imageData['mime'])) {
$mimeType = $imageData['mime'];
}
Calling the system file
The last method I'm aware of is simply calling the filecommand on a unix system through exec.
$mimeType = exec("/usr/bin/file -i -b $filepath");
Merging all that into one do-it-all method
If you're not sure what your system is capable of or if you plan on distributing your code, you'd better test for all alternatives. Here's the code I'm using :
/**
* mimetype
* Returns a file mimetype. Note that it is a true mimetype fetch, using php and OS methods. It will NOT
* revert to a guessed mimetype based on the file extension if it can't find the type.
* In that case, it will return false
**/
function mimetype($filepath) {
// Check only existing files
if (!file_exists($filepath) || !is_readable($filepath)) return false;
// Trying finfo
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME);
$mimeType = finfo_file($finfo, $filepath);
finfo_close($finfo);
// Mimetype can come in text/plain; charset=us-ascii form
if (strpos($mimeType, ';')) list($mimeType,) = explode(';', $mimeType);
return $mimeType;
}
// Trying mime_content_type
if (function_exists('mime_content_type')) {
return mime_content_type($filepath);
}
// Trying exec
if (function_exists('exec')) {
$mimeType = exec("/usr/bin/file -i -b $filepath");
if (!empty($mimeType)) return $mimeType;
}
// Trying to get mimetype from images
$imageData = @getimagesize($filepath);
if (!empty($imageData['mime'])) {
return $imageData['mime'];
}
return false;
}
Hope that helps !
05 Aug 2010I often hear how Ruby is better than PHP, that cakePHP is only a port of the Ruby on Rail framework and that the original is much more powerful.
Well, I've been tempted to try and learn Ruby for some quite time now. But I've been a little scared by the fact of having to learn a whole new language when I'm now getting pretty good with cake.
That and the shortage of hosts with a support for Ruby out there keep me away from it.
LESS, a Ruby only little gem
But one thing that may change my mind is LESS, the CSS as it should have been written since day one. It allows, in an extremely simple syntax, to define variables, functions, nested definitions, easier selectors.
Unfortunatly, the parser only exists as a Ruby gem, too bad for my php apps.
Why I don't use the javascript version
There is Javascript version of the parser, but I don't like the idea of having CSS rendering based on script execution. It means that if I'm browsing without scripts enabled, I won't have any styling.
Styling and Scripting are two very different things, one should work without the other and vice versa
So maybe, when the syntax will be parsed directly by browsers (one could dream...), or if a PHP parser is released I'll try it right away, but until them, I'll stick with my rusty CSS.
LESS in PHP ?
Well, some hours after posting this I just stumbled upon that. I guess I'll try LESS in php world after all !