RSS
 

Archive for the ‘PHP’ Category

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!
 
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.

 
No Comments

Posted in CakePHP

 

Unit-Testing Tips for 2.0 and PHPUnit

04 Dec

Quite some time ago I wrote about Unit testing. But that was still in 1.3 and with SimpleTest.
A lot has changed since then.

Execution Order

The documentation wasn’t all that clear about it. So I tried it with the following test file:

App::uses('MyCakeTestCase', 'Tools.Lib');
 
class TestCaseExecutionOrderTest extends MyCakeTestCase {
 
	public function setUp() {
		parent::setUp();
 
		$this->out('setUp');
	}
 
	public function tearDown() {
		$this->out('tearDown');
 
		parent::tearDown();
	}
 
	public function startTest() {
		$this->out('startTest');
	}
 
	public function endTest() {
		$this->out('endTest');
	}
 
 
	public function testFoo() {
		$this->out('* foo *');
	}
 
	public function testBar() {
		$this->out('* bar *');
	}
 
}

Note the parent calls for setUp and tearDown. Those need to be set. The other 2 methods don’t require this.

The result was pretty obvious:

setUp
startTest
* foo *
endTest
tearDown
 
setUp
startTest
* bar *
endTest
tearDown

Debug Output with PHPUnit >= 3.6

On the release notes of CakePHP2.0.3 you could find the note

A big difference people will notice when writing unit tests is that all output is swallowed by PHPUnit and not presented in either the web tester page nor in the CLI tester. To overcome this annoyance use the--debug modifier if you are using the CLI interface

Well, that broke a lot of existing debug code, of course.
So I tried to come up with a solution. The Shell uses a specific output(). Why not use sth like that in test cases, too?

/**
 * outputs debug information during a web tester (browser) test case
 * since PHPUnit>=3.6 swallowes all output by default
 * this is a convenience output handler since debug() or pr() have no effect
 * @param mixed $data
 * @param bool $pre should a pre tag be enclosed around the output
 * @return void
 * 2011-12-04 ms
 */
public function out($data, $pre = true) {
	if ($pre) {
		$data = pre($data);
	}
	echo $data;
	if (empty($_SERVER['HTTP_HOST'])) {
		# cli mode / shell access: use the --debug modifier if you are using the CLI interface
		return;
	}
	ob_flush();
}

As you probably noticed, I use it in the above “execution tryout”.
You can make yourself a custom “MyCakeTestCase” class with then extends the core test case class and put it in there.

UPDATE January 2012

The core (>=2.0) now contains a native way to enable debugging output for your tests.
You need to append &debug=1 to the url. Not ideal, but it works.

 
 

Serving views as files in Cake2

21 Nov

Actually, its not that different in Cake1.3. But as I just played around with it in 2.0, I will stick to that version for examples.

How to start

Skip this, if you want to cut to the chase.

In your routes.php you need to add Router::parseExtensions(); (or only specific ones).
That tells cake that urls ending with “.xyz” will be served as files (either inline or as download attachment).

Setup

Let’s say you want to display an invoice as pdf. The normal url is /invoices/view/1.
Now, we set a link in the view to the file like so:

$this->Html->link('View as PDF', array('action'=>'view', 'ext'=>'pdf', 1));

Since cake automatically detects that this will be a pdf file, it will
a) set the correct header (application/pdf)
b) will try to find the specific pdf view in /View/Invoices/pdf/index.ctp and the pdf layout in /View/Layouts/pdf/default.ctp

That’ all :)

Download it right away

Files like pdf can be displayed inline. So the browser will usually not force you to download it.
If you want this, though, you need to call $this->request->download($filename); in your controller action or in your layout.

Note: If your browser does not understand the file format (in this case pdf) it will probably trigger the download right away.

Browser bugs and easter eggs

Well, you could also call it a bug. But during my tryouts I found out that files served inline (Content-Disposition: inline; filename=”…”) will not use the given filename on save. They will be saved with the name in the url instead. In the above example it would be “1.pdf”. I did some research: Thats a well known browser deficiency that nobody yet fixed. Or so it seems.
Ok, but nobody wants his invoice to be “1.pdf”. So what can we do about it?

I found a pretty well working workaround:

$this->Html->link('View as PDF', array('action'=>'view', 'ext'=>'pdf', 1, 'invoice-2011-11-01_some_customer_tag'));

As you can see we simply add our filename to the url – after the id (!).

Since it is only “filename cosmetics” we dont need to add this second passed param to our method:

public function view($id = null) {}

It will be ignored in the action itself.

So the generated url is /invoices/view/1/invoice-2011-11-01_some_customer_tag.pdf and will result in a file saved as “invoice-2011-11-01_some_customer_tag.pdf”.
Job done.

Let me know what you think.

Example for PDFs

A pretty quick example how to output your content as pdf using DOMPDF.

In your layout (default.ctp in /pdf/):

App::import('Vendor', 'dompdf/dompdf.php');
$dompdf = new DOMPDF();
$dompdf->load_html(utf8_decode($content_for_layout), Configure::read('App.encoding'));
$dompdf->render();
echo $dompdf->output();

For me it seemed that without utf8_decode the DOMPDF lib seems to be buggy – although it claims to support utf8.

 
1 Comment

Posted in CakePHP

 

Freshly baked Cake2.0 tips

31 Oct

With 2.0 there are many new features available.
Some of them I want to introduce here.

Using the console for 2.0

I once wrote about how to use the Cake1.3 console.
The 2.0 shell is not so much different. The Console folder was only moved to /lib/Cake/Command/.
Make sure you use this new path. Also note, that it now seems to be important to have a “base app”.
You can’t just use the console anymore anywhere you like. It needs to have an app to get configuration and additional information on.
It also might be important which direction you are using the console – at least in 2.0 now. Being in the Command path with -app param might result in sth different than using the default way of using the console from within your current app directory.
So you should always go with the latter:

E:\...\trunk\app\>..\lib\Cake\Console\cake [command]

And if you put your “…/Console” path in your System Environment path (see the other article for details), you can use “cake” as standalone again:

E:\...\trunk\app\>cake [command]

Cronjobs now work this way (using crontab):

*/30 * * * * /srv/www/.../trunk/vendors/cakeshell app -cli /usr/bin -console /srv/www/.../trunk/lib/Cake/Console -app /srv/www/.../trunk/site >> /tmp/log.log

This runs a the AppShell (defined in your /App/Console/Command/ folder) every 30 minutes and logs errors/results to the given log file.

Note: cakeshell is a file containing the script from the cookbook.
Also note: If you use windows you might need to run “dos2unix /…/Cake/Console/cake” in order to make the cake console working.

Upgrade using the upgrade shell

cake upgrade all -params

I use an enhanced shell which provides more functionality.
For params I use –svn. It cakes care of the moving process if you use subversion. git is also supported.
Even if you already upgraded you should run it. Just in case you missed anything. The shell also auto-corrects some styling errors or deprecated syntax.

Rendering elements from helpers

This is now possible because the View is available in the helpers now.
So inside a helper method just do:

$elementContent = $this->_View->element('...');

Note: element() now has three params. The second still is the one where you can pass variables to the element scope. But the third param is now the options param (cache, plugin, callbacks, …).

It is even possible to alter the output from a helper directly:

$output = $this->_View->output; // contains the current rendered output

If you need any information on the current url or params:

$this->_View->request // contains all information about the current request

Overriding plugin views in the app

Another feature that finally arrived. Now plugins don’t have to cover all possible cases.
For specific apps you can override the plugin view locally.
Place the view in /app/View/Plugin/PluginName/Controller/action.ctp. The same goes for layouts, of course.

Enhancing objects and aliasing

public $helpers = array(
    'Html' => array(
        'className' => 'Tools.HtmlExt' // Tools Plugin - HtmlExtHelper
    )
);

Declared anywhere in the code the HtmlExt helper will then be used instead of the core Html helper.
The important change is, that it will use the old namespace, though. $this->Html is still the right way to use it. So you can enhance every helper, component, … to suite your needs without having to change the existing code itself.

This opens whole new possibilities. Until now you would have used a FormExt helper and manually called it with $this->FormExt->foo().
For 2.0 I use and recommend the following structure:
Add Ext to helpers/components etc from the core you want to Extend:
- FormExt(Helper)
- RequestHandlerExt(Component)
alias them as above to the original one.
Best to place them in a plugin all your apps have in common (Tools in my case).
Now if you want to go even further, you can override that one again specifically for this app by prefixing it with My:
- MyForm
- MyFormExt

All app specific helpers are not in a plugin but the app dir structure and start with My.
And using aliasing you can still call the helper with $this->Form->foo().
Same goes for components, behaviors, …

Working with exceptions

$post = $this->Post->read(null, $id);
if (!$post) {
	throw new NotFoundException();
}

The exception should then display a 404 error page.

Including scripts

Before 2.0 you always used App::import();
But with the switch to App::uses() the files do not get included anymore at startup due to the Super-lazy-loading. This should only be used for real classes. Also start to group by PackageName:

App::uses('GeolocationLib', 'Tools.Geo');  # in /Plugin/Tools/Geo

It will only be included by Cake if actually needed. One reason why Cake2 is so much faster.

If you have a file without a class (containing functions or constants or whatever) you still need to use the old method:

App::import('Lib', 'Tools.Bootstrap/MyBootstrap'); # in package "Bootstrap"

Using own classes instead of core ones

Let’s say, you want to apply a fix to a core file without overriding the core folder. Or you want to replace a file altogether. Simply use the same folder structure inside the /Lib folder.
For your own “FormAuthenticate”
/app/Lib/Controller/Component/Auth/FormAuthenticate.php

Cleanup!

If you are upgrading to 2.0 – or already have – you should get rid of old PHP4 chunk.
For starters, the &+object needs to get eradicated once and for all.

Old style:

public function setup(&$Model, $config = array()) {}

A correct piece of a behavior now looks like:

public function setup(Model $Model, $config = array()) {} # type Model and no &

Same goes for components, controllers, helpers, …
Note: You can do this for 1.3 already, as well. At least if you are using PHP5 (and you probably are).

You should also change all “var $x” to “public $x” and add “public” to all methods in classes which don’t have a visibility attribute yet.

I wrote a “CorrectShell” which extends the UpgradeShell. It does take care of almost all changes. I will publish it soon.

Things I did find out the hard way

Most of my libs and helpers which use the Xml class had to be modified quite a bit.
The class itself is not accessed statically, and more important does return only lowercase keys.
This was some piece of work to find and correct all those necessary changes.

If you make use of the internal HttpSocket class you might want to be interested in the fact, that it now returns an object as response. So you need to use $response->body to access the result content.

You cannot really set the headers and content type in your layouts anymore manually using “header()”. You need to do this using the response object:

$this->response->type('pdf'); //or as mimetype:
$this->response->type('application/pdf');

Either from the controller or the view/layout/helper.

The core.php, routes.php and bootstrap.php need new settings in order for your app to fully function under 2.0. Tip: Look at the original 2.0 app files and compare them to your upgraded files.
Add anything that is missing.

There seem to be dependency problems now with components and helpers. So the order in which you add them in your controller is now important (wasn’t in 1.3). If they depend on a class you should add them after that one to prevent the collection to fatal error. (UPDATE: Some if it seems to be fixed now).

Shell tips not mentioned in any upgrade guide

Shells are one of the most useful tools out there. I have dozens of them and those were the first classes I ported to 2.0. Therefore I ran into problems with those, first. So in case, you write your own ones, too:

The usage of shells has changed quite a lot. You don’t need a help method anymore. The consoleOptionParser object will take care of that using “-h”.
For using params/args you will now need to set up a “getOptionParser” method (see the core shells for examples).
For string options you should set an empty default value: 'default' => '' (prevents you from warnings). For boolean values set 'boolean' => true.

A main() command should always be called explicitly (“cake Shell main -f” instead of “cake Shell -f”) if you want to pass params or args. Otherwise it won’t run.

And there is more

Make sure you check all your forms. Especially the ones where you customized. Since 2.0 now adds HTML5 types automatically, it can get unwanted results when you use names for fields that are already matched in the schema. In my case many normal “text” fields suddenly ended up being “type”=>”number”. Which screwed up the whole form (could not validate anymore).
Some normal selects (for search etc) suddenly ended up being “multiple”=>”multiple”. Why? I am not yet sure :)

The FormHelper does not support some field type aliases anymore. So for textareas you have to use 'type'=>'textarea'. “textfield” doesn’t work anymore – if you have that still somewhere in your forms.

Custom Joins do not need the prefix anymore. In 1.3 you had to do:

array(
	'table'=>$this->Conversation->tablePrefix.'conversation_messages',
	'alias'=>'ConversationMessage',
	'type'=>'inner',
	'conditions'=>array('Conversation.last_message_id = ConversationMessage.id'),
), ...

In 2.0 it’s just:

'table'=>'conversation_messages'

Otherwise it will prepend the prefix twice which, of course, breaks the query!

The Routing has changed quite a bit. Some to the better – some to the worse (or I coudn’t figure out yet how it is done in 2.0):
In 1.3 this was working to connect an url and all its prefixes:

Router::connect('/shortcut', array('controller' => 'overview', 'action'=>'some_action'));

But this will not in 2.0 anymore. Here all prefixes get treated as normal named params.
A quick fix (which does not enable the prefixed urls, though!) would be:

Router::connect('/shortcut', array('admin'=>false, 'controller' => 'overview', 'action'=>'some_action'));

At least the normal user url is working, again.
Although this doesn’t seem to work flawlessy, either…

Custom joins don’t need a model prefix anymore:

'joins'=>array(
	array(
		'table'=>$this->tablePrefix . 'conversation_users',
		'alias'=>'ConversationUser',
		'type'=>'inner',
		'conditions'=>array('Conversation.id = ConversationUser.conversation_id')
	),
)

becomes

'joins'=>array(
	array(
		'table'=>'conversation_users',
		'alias'=>'ConversationUser',
		'type'=>'inner',
		'conditions'=>array('Conversation.id = ConversationUser.conversation_id')
	),
)

Cake is now capable of prefixing the table names automatically.

 
5 Comments

Posted in CakePHP

 

More great news: CakePHP 2.0 stable is out!

18 Oct

After 1.5 years of development, Cake catches up with other frameworks.
It is now as modern as most of the others while remaining the most powerful of all.
Almost none if its automatic got lost during the process of rewriting.

Some of the important aspects of 2.0

  • Faster (20-40% depending on the application)
  • Modern (>=PHP5.2 and state of the art functionality)
  • More Flexible and extensible (almost every class can be switched out with own ones)
  • Exceptions as error handling
  • PHPUnit as test suite
  • Hundreds of fixes which had to stay in 1.3 due to compatibility issues
  • Lots of enhancements and new functionality like “aliasing” – details

If you start a new project dive right into 2.0. That’s the future :)

For all those who like to assimilate the changes as online video:
tv.cakephp.org/video/CakeFoundation/2011/10/06/ch-ch-anges_cakephp_2_0_-_by_mark_story

Tips for upgrading

The migration guide will help to upgrade existing applications to the new version.
This will be quite a bit of work, though. Almost all classes are renamed, many methods and object variables have been dropped.

Your biggest help will be the cake shell script “ugrade” which is available in 2.0 now. It helps you to automatically upgrade some of the code. You can either select all or single tasks like renaming components etc.
Simply type “cake upgrade” in the console to list all available methods.
It really is a huge time safer. You should backup or commit everything before you attempt to run this script, though.

UPDATE:
I tried to upgrade a medium sized 1.3 app to 2.0. After finding several problems I opened a ticket.
Hopefully the upgrade process will be made smoother the next couple of weeks :)

Last Words

I am a little bit proud to see many of my tickets and proposed changes/fixes being now part of the framework.
Contributing to such a popular and large framework is sometimes easier than one might think. Sometimes it needed only hours to approve one of my tickets. OK, sometimes it needs months.
Providing a good reason and attaching proposed changes as diff or patch can really speed up things and should encourage you to involve yourself in cake core development, as well.

 
2 Comments

Posted in CakePHP

 

Working with virtual fields in Cake1.3

13 Oct

In earlier versions of CakePHP we had to use Behaviors like “MultipleDisplayFields” or whatever.
With $Model->virtualFields it is now possible to natively use find() methods combined with virtual fields.
The nice thing about it is, that the pagination can work with those fields, too. You can even sort on them in paginated views. A huge advantage over earlier Cake versions.

An example (User model):

$virtualFields = array('fullname'=>'CONCAT(User.firstname, " ", User.lastname)'); // "john doe"

It has several downsides, though.
Virtual fields do not work across models. They cannot be used in combination with “fields” in the find() options.
More on it can be found in the official cookbook.

I tried to come up with a solution that fixes some of those incapabilities.

Introducing the custom model method

$fields = am(array('username', 'email'), $this->User->virtualFields('fullname'));
$user = $this->User->find('first', array('fields'=>$fields, 'conditions'=>...));

This allows us to only fetch specific “fields” while including the virtualField “fullname”

$fields = $this->User->virtualFields(array('fullname', 'OtherUser.fullname'));
$fields = am(array('User.id', 'OtherUser.id'), $fields);
$user = $this->User->find('first', array('contain'=>array('OtherUser'), 'fields'=>$fields, ...));

Virtual fields of other models are also no problem. In this case I simply joined the model User to itself (hasOne OtherUser).
The result would be sth like:

array (
  'User' =>
  array (
    'id' => '2',
    'fullname' => 'First+Last',
  ),
  'Test' =>
  array (
    'id' => '2',
    'fullname' => 'First-Last',
  ),
);

Note the difference in the join part (+/-). It shows that the query works (OtherUser has – and User has + as joining piece).

A little bit trickier were the temporary virtual fields. Those could be passed along and would be used only for this query (TODO! right now they are added to virtualFields but not removed anymore).

$fields = $this->User->virtualFields(array('fullname', 'othername'=>'CONCAT('.$this->User->alias.'.firstname, "-", '.$this->User->alias.'.lastname)', 'OtherUser.othername'=>'CONCAT('.$this->OtherUser->alias.'.firstname, "*", '.$this->OtherUser->alias.'.lastname)'));
$fields = am(array('User.id', 'OtherUser.id'), $fields);
$user = $this->User->find('first', array('contain'=>array('Test'), 'fields'=>$fields, 'order'=>array()));

The result again – as expected:

array (
  'User' =>
  array (
    'id' => '2',
    'fullname' => 'First+Last',
    'othername' => 'First-Last',
  ),
  'OtherUser' =>
  array (
    'id' => '2',
    'othername' => 'First*Last',
  ),
)

Even plugin models should work (not tried yet) with

$this->User->virtualFields(array('Plugin.PluginModel.fullname'));

Although the “Plugin” part usually can be omitted if the relation from User to PluginModel is already established (and the User model can access the model property without initializing the foreign model).

Code

The code to be placed in your AppModel:

/**
 * combine virtual fields with fields values of find()
 * USAGE:
 * $this->Model->find('all', array('fields' => $this->Model->virtualFields('full_name')));
 * Also adds the field to the virtualFields array of the model (for correct result)
 * TODO: adding of fields only temperory!
 * @param array $virtualFields to include
 * 2011-10-13 ms
 */
public function virtualFields($fields = array()) {
	$res = array();
	foreach ((array)$fields as $field => $sql) {
		if (is_int($field)) {
			$field = $sql;
			$sql = null;
		}
		$plugin = $model = null;
		if (($pos = strrpos($field, '.')) !== false) {
			$model = substr($field, 0, $pos);
			$field = substr($field, $pos+1);
 
			if (($pos = strrpos($model, '.')) !== false) {
				list($plugin, $model) = pluginSplit($model);
			}
		}
		if (empty($model)) {
			$model = $this->alias;
			if ($sql === null) {
				$sql = $this->virtualFields[$field];
			} else {
				$this->virtualFields[$field] = $sql;
			}
		} else {
			if (!isset($this->$model)) {
				$fullModelName = ($plugin ? $plugin.'.' : '') . $model;
				$this->$model = ClassRegistry::init($fullModelName);
			}
			if ($sql === null) {
				$sql = $this->$model->virtualFields[$field];
			} else {
				$this->$model->virtualFields[$field] = $sql;
			}
		}
		$res[] = $sql.' AS '.$model.'__'.$field;
	}
	return $res;
}

If anyone sees a cleaner approach etc, let me know :)

 
1 Comment

Posted in CakePHP