RSS
 

Archive for the ‘PHP’ Category

Bitmasked – Using bitmasks in CakePHP

26 Feb

Introduction

Based on Mark Story’s Post about this and the need in one of my apps for this lead me to believe that a behavior would be most appropriate to handle this. So I gave it a try.

There already is a Bitmasked Behavior out there – but it uses a secondary table which will get joined to the primary one. I wanted something that works on the same table (and an existing field), though. It is supposed to be simple and yet powerful.

Use cases

In some cases a boolean flag in your model is not enough anymore. It might need a second flag – or even a third. You could do that and add those new fields – or combine them all using a bitmask. You can work with quite a few flags this way using a single database field.

The theoretical limit for a 64-bit integer [SQL: BIGINT unsigned] would be 64 bits (2^64). But don’t use bitmasks if you seem to need more than a hand full. Then you obviously do something wrong and should better use a joined table etc. I highly recommend using tinyint(3) unsigned which can hold up to 8 bits – more than enough. It still only needs 1 byte.

There are two different ways to check against those bitmasks. The first one – in my implementation the default one – assumes that you are using them similar to a list of multiple selects/checkboxes. Those can be individually edited in a form and saved as this single field. On retrieval you usually want to get only those records with certain bit groups set (a kind of OR condition). The second use case is on retrieving data based on the value of single flags. Where it doesn’t matter if other flags are also set or not. This case is covered by the last chapter and those “contains” methods of the behavior which create a SQL snippet to be used in the condition.

Bitmasked Behavior

Basically it encodes the array of bit flags into a single bitmask on save and vice versa on find. I created it as an extension of my pretty well working Enum stuff. It can use this type of enum declaration for our bitmask, as well. We use constants as this is the cleanest approach to define model based field values that need to be hardcoded in your application.

The code can be found in the Tools Plugin.

Basic Usage

We first want to attach the behavior via public $uses() or at runtime:

$this->Comment->Behaviors->attach('Bitmasked', array('mappedField'=>'statuses'));
The mappedField param is quite handy if you want more control over your bitmask. It stores the array under this alias and does not override the bitmask key. So in our case status will always contain the integer bitmask and statuses the verbose array of it.

We then need to define the bitmask in our model:

const STATUS_ACTIVE = 1;
const STATUS_PUBLISHED = 2;
const STATUS_APPROVED = 4;
const STATUS_FLAGGED = 8;
 
public static function statuses($value = null) {
    $options = array(
        self::STATUS_ACTIVE => __('Active'),
        self::STATUS_PUBLISHED => __('Published'),
        self::STATUS_APPROVED => __('Approved'),
        self::STATUS_FLAGGED => __('Flagged'),
    );
    return parent::enum($value, $options);
}
Please not that you need to define MyModel::enum by extending my MyModel or by putting it into your AppModel manually if you want to use that. You don’t have to, of course.

Either way, we need an array of bits that we want to work with. They should start at 1 and can go as high as your database field is designed for (2, 4, 8, 16, 32, …) or common sense cries “stop”.

The behavior tries to find the plural method of your field name automatically (statuses for status here). You can always override this by manually setting param bits to your method name. You may also directly assign the bits to this param. The advantage of the static model method is that we can use it everywhere else, as well. For instance in our forms to display some multiple checkbox form field for those flags.

Now, in the add/edit form we can add the field:

echo $this->Form->input('statuses', array('options'=>Comment::statuses(), 'multiple'=>'checkbox'));
It will save the final bitmask to the field status.

Searching for a record can be done using the bitmask itself:

$conditions = array('status'=>BitmaskedComment::STATUS_ACTIVE | BitmaskedComment::STATUS_APPROVED);
$comment = $this->Comment->find('first', array('conditions'=>$conditions));

If you want to search for a specific record, you can also use the array structure, though:

$conditions = array('statuses'=>array(BitmaskedComment::STATUS_ACTIVE, BitmaskedComment::STATUS_APPROVED));
$comment = $this->Comment->find('first', array('conditions'=>$conditions));
Note that it uses the mappedField – in this case statuses – for the array lookup.

Retrieving the record will then transform the bitmask value back into an array of bits. If you use a mappedField, you will find it there instead of an overwritten field value.

Extended Usage

In your model you should define a rule for it if you want at least one flag to be selected:

public $validate = array(
    'status' => array(
        'notEmpty' => array(
            'rule' => 'notEmpty',
            'last' => true
        )
    )
);
You can always add more rules manually – for example if you want to make sure only some combinations are valid etc.

There are cases where you want to get all records that have a specific bit set, no matter what the other bits are set to. In this case you currently need to wrap it like so:

// contains BitmaskedComment::STATUS_PUBLISHED:
$conditions = $this->Comment->containsBit(BitmaskedComment::STATUS_PUBLISHED);
$res = $this->Comment->find('all', array('conditions'=>$conditions));
 
// dos not contains BitmaskedComment::STATUS_PUBLISHED:
$conditions = $this->Comment->containsNotBit(BitmaskedComment::STATUS_PUBLISHED);
$res = $this->Comment->find('all', array('conditions'=>$conditions));

Tip: Looking at the test cases is usually one of the best ways to figure out how a behavior is supposed to work.

Manual usage

In some cases you might want to control encoding/decoding yourself (or have to by using saveField() method which does not trigger the behavior). Let’s also get the enums of a different model here (using a static method – see my enums for details) for testing purposes:

$this->UserSetting->Behaviors->attach('Tools.Bitmasked', array('field'=>'choices', 'bits'=>'ProfileChoice::types'));
$choices = (int) $this->UserSetting->field('my_choices', array('id'=>$uid)); // a numeric value (0 ... x)
$choicesArray = $this->UserSetting->decodeBitmask($choices); // now the array for the checkboxes
 
if ($this->request->isPost()) {
    $choicesArray = $this->request->data['UserSetting']['choices']; // our posted form array
    $choices = $this->UserSetting->encodeBitmask($choicesArray); // back to integer
    $this->UserSetting->id = $uid;
    $this->UserSetting->saveField('my_choices', $choices);
    // flash message and redirect
} else {
    $this->request->data['UserSetting']['choices'] = $choicesArray;
}
 
// in the view:
echo $this->Form->input('choices', array('options'=>ProfileChoice::types(), 'multiple'=>'checkbox'));

 
2 Comments

Posted in CakePHP

 

Dynamic database switching

25 Feb

Some might remember my old post about development vs. productive setup. It was mainly about how to switch dynamically based on the current environment. This can be useful if you want to have a single DB config file which will be distributed/uploaded to all environments. In my case: synced via shell script.

Since then I rewrote it quite often and it is now published in my Setup plugin (in /Lib). It now has some more useful goodies. The config file can be kept more DRY (Don’t Repeat Yourself) and does allow automatic test config.

The most important new feature is the automatic environment awareness in CLI mode (using path).

Basic usage

Install the plugin in your Plugin folder first.

Your database.php then uses the class this way:

App::uses('BaseConfig', 'Setup.Lib');
 
class DATABASE_CONFIG extends BaseConfig {
 
    public $default = array(
        'environment' => array('localhost', 'domain'), // define the environments - optional
        'path' => array('/some/path/to/app/'), // define the paths - optional
        'datasource' => 'Database/Mysql',
        'persistent' => false,
        'host' => 'localhost',
        'login' => 'root',
        'password' => '',
        'database' => 'table_name',
        'prefix' => 'app_', //optional (I like to use prefixes, though)
        'encoding' => 'utf8'
    );
Actually, using the BaseConfig class you can cut it down to:
public $default = array(
    'environment' => array('localhost', 'domain'),
    'path' => array('/some/path/to/app/'),
    'datasource' => 'Database/Mysql',
    'host' => 'localhost',
    'login' => 'root',
    'password' => '',
    'database' => 'my_app',
    'prefix' => 'app_'
);
encoding defaults to utf8 and persistent defaults to false.

Both environment and path are used to determine the correct config. The first dynamically selects the correct config based on the $_SERVER['HTTP_HOST'] setting and therefore the domain you are running the application with. The latter does the same on CLI (Command Line Interface) – using the cake console. There we have to domain and therefore need the paths to find the correct configuration. If you use the shell only locally then you can skip setting the paths, of course.

Test config

That is probably the most useful goodie. You don’t have to take care of this anymore. It will also ensure that your “live” tables will not get touched, modified, overwritten etc.

If you only defined the default config yet, the BaseConfig class will automatically set the test config to the same database, but with a different prefix (defaults to zzz_). You can overwrite the defaults very easy, though:

public $test = array(
    'database' => 'my_app_test',
    'merge' => true,
    'prefix' => 'app_',
);
In this case we force the merge with our default config and define an own database table as well as the same prefix to completely replicate the default settins on a secondary test evironment. All other config values will be automatically merged.

Manuel fixation

You can also manually enforce a certain DB config in your bootstrap or config.php file:

Configure::write('Environment.name', 'stage');
//or
$config['Environment'] = array(
    'name' => 'stage'
);
This will make the class use the $stage config (needs to be available then, of course).

Example

So for 3 environments, lets call them development, stage and live, we can easily define them like so:

public $default = array(
    'environment' => array('localhost', 'local.domain'),
    'path' => array('/some/path/to/app/'),
    'datasource' => 'Database/Mysql',
    'host' => 'localhost',
    'login' => 'root',
    'password' => '',
    'database' => 'my_app',
);
 
public $stage = array(
    'environment' => array('test.domain.com'),
    'path' => array('/some/stage/path/to/app/'),
    'datasource' => 'Database/Mysql',
    'host' => 'localhost',
    'login' => 'root',
    'password' => '123',
    'database' => 'my_app_stage',
);
 
public $live = array(
    'environment' => array('www.domain.com', 'domain.com'),
    'path' => array('/some/live/path/to/app/'),
    'datasource' => 'Database/Mysql',
    'host' => 'localhost',
    'login' => 'root',
    'password' => '456',
    'database' => 'my_app_live',
);
Note: In all cases the test config will adjust itself to the current environment as we didn’t specify anything for it.

Final notes – some pitfalls and tips

Make sure you put the exact paths to your application in path – including the trailing slash! If you want to confirm that everything works in CLI, as well, use my CurrentConfig shell to output the current configuration for the console:

..\lib\Cake\Console\cake Setup.CurrentConfig
It should then display the default and test DB config as well as the current Cache config.

 
No Comments

Posted in CakePHP

 

Spellchecking with (Cake)PHP

24 Feb

I wrote some cake1.3 libs years ago which would request google’s spellchecker API. This is great for small and unregular lookups. But as soon as you need to use it more excessive an internal server solution is not only much faster but also capable of high frequent lookups. You can – for instance – check a complete book with hundred thousands of words in seconds.

The library pspell seems to be deprecated in PHP5.3. The way to go is enchant.

Enchant PHP Extension

On windows you don’t have to do much. WAMP with PHP5.3 comes with the Enchant extension right away. You only need to activate it (php_enchant) by menu or by manually removing the # char for this extension in php.ini. Don’t forget to restart your Apache.

For linux you might first want to run apt-get install php5-enchant to install the basic library. You will then need to add the extension to php.ini. If someone has any details on this, please let me know and I update this tut.

Once it is running you should see an “enchant” module entry on your phpinfo page. On my system it seems to be Version 1.1.0.

SpellLib for CakePHP2

The lib is in my Tools plugin and is called SpellLib (in /Lib/).

We need to provide the lib with dictionaries. Those can be found online at different locations. One is here. You can probably use all kinds of dictionaries which end with the extension .dic. Now store them in you global vendors folder: /vendors/dictionaries/[engine]/ whereas [engine] is your preferred engine (defaults to myspell). If you want to store it in a different path, see the last chapter on possible options.

Once they are in your vendors directory you can check on them:

$this->SpellLib = new SpellLib();
$dicts = $this->SpellLib->listDictionaries();
This should display a list with at least one tag like en_GB or de_DE based on what you downloaded.

Basic usage (with english spell checking):

$this->SpellLib = new SpellLib();
if ($this->SpellLib->check($word)) {
    //everything is fine
} else {
    //contains an array of words that could be the correct ones
    $suggestions = $this->SpellLib->suggestions($word);
}
See the test case for details.

Options

If you want to store those dictionary files in another vendor path, you can configure this by using Configure class or simply by passing the path on to the class:

// in your configs
$config['Spell'] = array(
    'path' => CakePlugin::path('Tools') . 'Vendor' . DS . 'dictionaries' . DS,
    'engine' => ENCHANT_ISPELL,
    'lang' => 'de_DE'
);
 
// passing it on as `path` param:
$this->SpellLib = new SpellLib(array('path'=>CakePlugin::path('Tools') . 'Vendor' . DS . 'dictionaries' . DS));

To use other languages dynamically, German for example, use the lang param:

$this->SpellLib = new SpellLib(array('lang'=>'de_DE'));

Any feedback is appreciated!

 
No Comments

Posted in CakePHP, PHP

 

What REALLY speeds up your cakephp app

13 Feb

There are already many (partly outdated) blog entries (e.g. 8-ways-to-speed-up-cakephp-apps) and stackoverflow questions/answers (e.g. speeding-up-cakephp) regarding this question. But from years of experience I want to outline the more important ones pretty quick.

I will not talk about the little things or the ones that will only affect 5% of the website. And no – debug level 0 is not a speed improvement. It is an absolute requirement for any live application and can therefore be seen as default setting.

One last note: I want to address dynamic websites here that cannot make use of extensive view caching or even html caching as described in the first link – at least for most of the pages. They undoubtedly are the fasted way to serve content but can usually only applied to very few views. The tips below are valid for all requests across your application.

1. Opcode Cache

Opcode Cache will keep your php files in the memory. It will usually also store the compiled bytecode of it to further improve execution time. But it is not only improving speed a great bit, it is also reducing the memory used for a single request by more than 50% allowing your server to serve twice as much requests (and therefore users) before overworking itself. A mediumsized cake2 application of mine uses 13.9 MB memory. With Opcode Cache it is reduced to 7.7 MB. And the speed is pretty much accordingly.

Installation is almost too easy. Example for ubuntu and apache: apt-get install php-apc and restarting the apache should do the trick. But there are other modules, as well (Xcache, Memcached, …)

Gain: in most cases more than 100% (more than twice as fast)

2. Also use memory caching for all temporary data

This mainly includes “tmp files” via CacheEngine which can be cake core tmp files or app tmp files. All your cached elements will be included faster this way, as well. If you cache your sql queries they will also be affected positively compared to a simple file cache (although a file cache itself is better than nothing).

Cake2.x automatically tries to use the fastest CacheEngine if available (and defined in your core.php):

$engine = 'File';
if (extension_loaded('apc') && function_exists('apc_dec') && (php_sapi_name() !== 'cli' || ini_get('apc.enable_cli'))) {
    $engine = 'Apc';
}

Gain: 10-40%

3. Speed up (Reverse) Routing

This will help for sites with many dynamic links. Without caching the generated urls Router class creates them over and over again. You can use my UrlCache plugin which basically does exactly that. I used the ideas of lorenzo and mcurry and applied some fixes and improvements. It now stores commonly used links (without any params) in a global cache file while using single cache files per site for the specific urls (e.g. from pagination/filtering).

If you only got a bunch of those links on a single page, this is wasted time, though. But for most projects it is a real time saver – from 0.3 seconds up to 1 second less request time.

Important note: You need to manually configure your _cake_core_ cache for this:

Cache::config('_cake_core_', array(
    'engine' => $engine,
    'prefix' => 'cake_core_',
    'path' => CACHE . 'persistent' . DS,
    'serialize' => ($engine === 'File'),
    'duration' => $duration,
    'lock' => true, # !!!
));
Note the exclamation marks. If you do not apply 'lock' => true you will end up with quite a few broken cache files. This will reset the whole thing all the time due to concurrent requests/writing and you gain nothing… See this ticket for details. With this setting the problems seems not to occur.

To further optimize performance I invented a dual cache system with reading unlocked and writing locked. See the plugin for details on how to use it for best results.

Gain: 10-50% (the more links on a page the more gain)

4. Optimize data, compress and cache assets

If you got 50 icons on your site, you might want to create a single sprite instead of creating +50 requests to your server here. You also should combine your js/jss and apply some compressing.

In general it is a good idea to send all data gzip compressed – using apache’s mod_deflate module for example. This way only half the amount of bytes need to be sent to the user. Especially with (slow) mobile connections this will increase speed up to 100%.

Using mod_headers you can make sure asset files are not permanently requested if they didn’t change. The website will feel faster for the user and the server has to serve less requests.

Files of the same type (css, js) should be packed and sent combined to a single file (as opposed to maybe 20-30 single files). Undoubtedly Mark Story’s AssetCompress plugin is one of the best ones out there to address exactly these issues.

Gain: 10-30% (might also depend on the browser)

Some stuff I didn’t try yet

In Cake2 you can even store sessions in the Cache (and therefore probably in the memory). This should also speed up the application remarkably compared to database sessions for example.

And the list of obligatory tips which should already be well known at last

  • Use containable to restrict the amount of data to be fetched as the database connection usually is the bottle neck of any app
  • Try to load stuff dynamically and only if needed (With Cake2 and lazy loading everywhere via App::uses() this shouldn’t be difficult to do anymore)
  • Try to decouple time consuming stuff from the frontend. So rendering large images, creating complicated pdfs, sending bulk emails can all be passed on to some “Queue” which asynchronously works them off piece by piece without slowing down the response time for the action.
  • As mentioned above you can cache complete pages to the View cache preventing the complete dispatch process to run. Such a pre-rendered html file will be served right away. This is useful for pages with mainly static content.
  • Avoiding $uses in the controller (except for its own model, of course) – meaning we should never have more than the primary model attached to it.
  • Make sure you send the right headers for proper client-side caching. Some browsers might not respect it, but the ones that do will profit from the increase in speed.
  • Don’t use requestAction

Update – 2012-02-20 – Benchmark

There is a benchmark site comparing all kinds of frameworks.. Unfortunately, using 1.3 instead of 2.x. But either way it displays how important it is to apply above optimization. If that is the case I bet the results will be way less dramatic.

 
8 Comments

Posted in CakePHP, PHP

 

Qlogin – Quicklogins für CakePHP

08 Feb

Have you ever thought how nice it would be to send emails with an url that automatically logs you in? Especially for a messaging system this can be quite handy: One click in the notification email and you can answer right away.

How does it work?

Here an example with a notification email:

$text = 'You got mail!'.PHP_EOL;
if (!isset($this->Qlogin)) {
    $this->Qlogin = ClassRegistry::init('Tools.Qlogin');
}
$text .= $this->Qlogin->url(array('admin'=>false, 'plugin'=>false, 'controller'=>'conversations', 'action'=>'view', $conversation['Conversation']['id'])), $user['User']['id']).PHP_EOL;
//send email
We create a quicklink using the url where the user should be redirect to after the login as first param and the actual user_id to authenticate with as second param. Pretty straight forward. Let’s say, the generated url is http://domain/qlogin/1234567890 .

Once the user clicks on the link, the “go” action of the QloginController gets triggered and tries to find the corresponding user as well as target url. If found, the user is logged in and directly passed on to the desired page. Note: If the user is already logged in, he will be redirected immediately (skipping the login).

I added this to my routes.php

Router::connect('/qlogin/*', array('plugin'=>'tools', 'controller' => 'qlogin', 'action'=>'go'));
in order to result in the above url which is pretty short and convenient.

The code

Its available at github (currently only for 2.x): Tools plugin. You need the model as well as the controller.

Some notes

The Module is still pretty basic. But it works flawlessly with my setup. I would like your feedback on it, though. AuthComponent::login() should respect the scope you defined as well as the default redirect urls. Right now I am trying out the Qlogins in combination with AutoLogin – if those cookies don’t intefere. But it seems to work fine.

Dependencies

Model CodeKey (@see article) (to store the tokens) as well as the url validation method and get() of my MyModel (can also be put into your AppModel). And since it is deeply integrated in my usual everyday apps it also requires at least the CommonComponent of the Tools plugin. For the optional admin backend there are more dependencies. So basically you need:

//AppController
public $components = array('Tools.Common');
 
//AppModel
App::uses('MyModel', 'Tools.Lib');
class AppModel extends MyModel {}
Of course you may through out any methods you don’t need.

 
No Comments

Posted in CakePHP

 

New year, new cake version – 2.1

09 Jan

The new version 2.1 is almost fully backwards compatible.

The migration guide from 2.0 to 2.1 can be found at book.cakephp.org/2.0/en/appendices/2-1-migration-guide.html

New stuff for the upgrade shell

I used this chance to enhance my version of the upgrade shell and added – besides some minor fixes – a command cake21. It should take care of all api standard updates for 2.1: git commit1 and 2 The Upgrade Plugin can be found here.

It currently updates Auth::allow() and layout stuff ($content_for_layout, $scripts_for_layout etc).

Feel free to add missing replacements.

 
No Comments

Posted in CakePHP

 

Traits – PHP5.4

31 Dec

Since it is new years eve, I don’t want to start a complete new chapter. So I will write one last post this year with some more general topic. And yes – I find it so interesting that I have to mention it in an own post :)

Traits

With PHP5.4 approaching fast, it will probably be the standard sometime in 2012. It will have one major feature that we will all going to love: oop5.traits.

What are they? The above link describes it pretty good. They are some kind of behavior – like the one we attach to models right now. Only way more flexible and powerful. Every class can then use those behaviors.

trait Hello {
    public function sayHello() {
        echo 'Hello ';
    }
}
 
trait World {
    public function sayWorld() {
        echo 'World!';
    }
}
 
trait HelloWorld {
    use Hello, World;
}
 
class MyHelloWorld {
    use HelloWorld;
}
 
$Foo = new MyHelloWorld();
$Foo->sayHello();
$Foo->sayWorld();
The result would be Hello World!

Imagine what would be possible with Controllers, Components, Shells, and other classes that can now not only extend parent classes but also use traits.

Cool things you can do with them

The above link contains a good set of examples. I will outline the most interesting ones:

public function sayWhere() {
    echo __CLASS__; // same with __FILE__ etc
}
As of right now objects that are extended by some subclass would always return the wrong class, because inside a class those magic constants always relate to the current file (and therefore often the parent class defining it). With traits the magic is endless – they will make it possible to inject functionality that overcomes that deficiency:
trait sayWhere {
    public function whereAmI() {
        echo __CLASS__;
    }
}
 
class Hello {
    use sayWHere;
}
 
class World {
    use sayWHere;
}
 
$One = new Hello;
$One->whereAmI(); //Hello
 
$Two = new World;
$Two->whereAmI(); //World

Another neat feature is, that we can override specific methods if needed – both ways:

class Base {
    use SayBye;
 
    // we want this method to be overridden by the trait to do some extra stuff with it
    // can only be done in the inheriting class, though
    public function sayHello() {
        echo 'Hello ';
    }
}
 
trait SayWorld {
    public function sayHello() {
        parent::sayHello();
        echo 'World!';
    }
}
 
trait SayBye {
    // this method will be be overridden by the class method to do some extra stuff with it
    // can only be done in the inheriting class, though
    public function sayBye() {
        echo 'Bye ';
    }
}
 
class MyHelloWorld extends Base {
    use SayWorld;
 
    public function sayBye() {
        echo parent::sayBye();
        echo 'World!';
    }
}
 
$Foo = new MyHelloWorld();
$Foo->sayHello(); //Hello World!
$Foo->sayBye(); //Bye World!

Update 2012-02-24: Interesting Link

There is an interesting page about the potential dangers of those new PHP5.4 features you might want to read before starting to write cutting-edge code :)

 
No Comments

Posted in PHP

 

Tools Plugin – Part 2: Contact Form

15 Dec

I want to show how easy it is to make a solid and universal contact form in Cake(2). The full code can be found in the github rep of the Tools plugin.

Model

The most important part first: We need a solid validation for the form. How many forms are out there that do not have any validation. The first thing I always do on about any public contact form: Hit the submit button and smile it if tells me that my empty form “has just been successfully sent”. We can do better than that :)

class ContactForm extends ToolsAppModel {
 
    protected $_schema = array(
        'name' => array('type' => 'string' , 'null' => false, 'default' => '', 'length' => '30'),
        'email' => array('type' => 'string' , 'null' => false, 'default' => '', 'length' => '60'),
        'subject' => array('type' => 'string' , 'null' => false, 'default' => '', 'length' => '60'),
        'message' => array('type' => 'text' , 'null' => false, 'default' => ''),
    );
 
    public $useTable = false;
 
    public $validate = array(
        'name' => array(
            'notEmpty' => array(
                'rule' => array('notEmpty'),
                'message' => 'valErrMandatoryField',
                'last' => true
            )
        ),
        ...
    );
The _schema var mocks a database table so that we don’t really need one. This helps the FormHelper to generate the inputs (maxlength, type, …). The validation rules will make sure the email is valid and the user actually entered some text.

Controller

The logic cakes care of the validation on POST. /Controller/ContactController.php

<?php
class ContactController extends AppController {
 
    public $uses = array('Tools.ContactForm');
 
    public function index() {
        if ($this->request->is('post') || $this->request->is('put')) {
            if (!$this->Session->check('Auth.User.id')) {
                $this->ContactForm->Behaviors->attach('Tools.Captcha');
            }
            $this->ContactForm->set($this->request->data);
            if ($this->ContactForm->validates()) {
                $name = $this->request->data['ContactForm']['name'];
                $email = $this->request->data['ContactForm']['email'];
                $message = $this->request->data['ContactForm']['message'];
 
                //send email with CakeEmail
            } else {
                $this->Common->flashMessage(__('formContainsErrors'), 'error');
            }
 
        }
        $this->helpers = array_merge($this->helpers, array('Tools.Captcha'));
    }
}
As you can see it only sends emails after successfully validating. For public contact forms I usually like some easy captcha behavior attached so that spam doesnt reach me. You can omit that, of course. Also note: you should use your own setFlash() method instead of mine!

Last but not least: View

/View/Contact/index.ctp

<?php echo $this->Form->create('ContactForm');?>
    <fieldset>
        <legend><?php echo __('contactLegend');?></legend>
    <?php
        echo $this->Form->input('name');
        echo $this->Form->input('email');
 
        echo $this->Form->input('subject');
        echo $this->Form->input('message', array('rows'=>15));
 
        if (!$this->Session->read('Auth.User.id')) {
            echo $this->Captcha->input('ContactForm');
        }
    ?>
    </fieldset>
<?php echo $this->Form->submit(__('Submit')); ?>
<?php echo $this->Form->end();?>

Result

Browse to /contact/ That should display the form right away. After the successful POST you should redirect back to the contact form (emptied then) or to another custom page.

Final notes

Currently, the Model is in the Tools.Plugin. You could put it into your normal app model folder, as well. But I use it in many projects and therefore I want to keep it dry. Feel free to adjust any of the code to your own needs. Same goes for my own custom methods like $this->Common->flashMessage() etc.

Some might wonder why “ContactController” and not “ContactsController”. The latter would be cake conventions. But there are situations where you can and should diverge form those. As in this case where “contacts” would mean more like sth to manage your addressbook. It also makes the url more meaningful out of the box (without any custom routes). With cake2 this all works without any additional customization.

The other thing: I didn’t name the Model “Contact” but “ContactForm” in order to not create possible conflicts which exactly such contacts/contact mangement MVCs (as I did in a contact management suite).

 
No Comments

Posted in CakePHP