RSS
 

Posts Tagged ‘cake2.x’

Cake Bake: Custom Templates “Deluxe”

24 Apr

You might have read my almost two year old article about custom bake templates. Much changed since then. In Cake as well in my user-land code.

Note: The “setup” bake theme is written for 2.x but can easily be backported to 1.3 manually.

Outline

We probably all know how xxxxxxx powerful the Bake tool is. It is all about speed for the first outline of a project. So the faster the first CRUD controllers are baked the sooner we can dig into the details. But speed itself is not the only crucial part. If we have a model/table with a lot of meta fields that should not get baked we would have to remove them manually from index/view/form every time we re-bake. That’s not really something you want to waste your time with.

Skip Fields

With some custom template like mine you can skip certain fields for baking. Some like 'password', 'slug', 'lft', 'rght' are automatically skipped by my template. But you can also define additional fields yourself:

class Deadline extends AppModel {
    public $scaffoldSkipFields = array('note');
}
So the textarea field “note” will not be outputted in your ctp files.

An additional feature I want to implement is that you can specify the actions it should skip:

public $scaffoldSkipFields = array('note'=>array('prefix'=>'', 'actions'=>array('index')));
This would mean that it skips this field for all non-prefixed ctps as well as all indexes.
public $scaffoldSkipFields = array('note'=>array('prefix'=>'admin')); // skip for all admin views
This will be included shortly.

Default Values

class User extends AppModel {
    public $scaffoldDefaultValues = array('role_id' => 'ROLE_USER', 'status' => 'User::STATUS_ACTIVE', 'level'=>1, 'text'=>'\'foo\'');
}
Which would result in this inside the controller action add:
if ($this->request->is('post')) {
    ...
} else {
    $this->request->data['User']['role_id'] = ROLE_USER;
    $this->request->data['User']['status'] = User::STATUS_ACTIVE;
    $this->request->data['User']['level'] = 1;
    $this->request->data['User']['text'] = 'foo'; 
}
This will have the above default values set up for adding a new user.

PS: Why not in the form itself? It is always better to put default values as a part of the logic into the controller and outside of the view layer.

Enum support

This article describes how to setup enums in CakePHP the easy way. Do this after you bake the model and BEFORE you bake controller and views. Then you will end up with ctps including all the Enum things automatically like a dropdown box for forms and translated values for index and view.

Smarter theme

We already covered a few basic field types before:

} elseif ($schema[$field]['type'] == 'date') {
    // formatted date
} elseif ($schema[$field]['type'] == 'boolean') {
    // yesNo image
} elseif ($schema[$field]['type'] == 'text') {
    // nl2br(h())
}
I also added decimals for currencies:
} elseif ($schema[$field]['type'] == 'float' && strpos($schema[$field]['length'], ',') !== false) {
    echo "\t\t<dd>\n\t\t\t<?php echo \$this->Numeric->money(\${$singularVar}['{$modelClass}']['{$field}']); ?>\n\t\t</dd>\n";
}
Switch Numeric with Number if you use your own helper. This will print a nicely formatted currency value instead of the default decimal value.

Usage

The files are in the Templates folder of my setup plugin.

Since my plugin theme might contain stuff you cannot and don’t wont to use you can cherrypick the above “tricks” for your own custom bake theme.

Now you know how you can further customize your themes to make baking even more fun as it already is :)

 
No Comments

Posted in CakePHP

 

CakePHP now fully MVC

10 Apr

Yeah :) I was waiting for this day for quite a long time. And pretty happy that my proposal from last year is finally in the live code. Cake2.1 is now fully, really and completely MVC – and officially 200% more awesome, of course!

MCV?

Model - View - Controller.
Mainly the basic principle of many modern frameworks that helps to code in a clean way. Details here

What’s the big deal?

It had been bugging me for years that we had to do something like this in our controllers and models:

# hold on tight - bad part - start (already cake 2.0 style):
App::uses('TimeHelper', 'View/Helper');
App::uses('View', 'View');
$Time = new TimeHelper(new View(null));
$message = 'Your flier expired on ' . $Time->nice($flier['Flier']['expires']); # flash message or email title etc
# bad part - end
We had to import a View and the Helper only to keep it dry and reuse the methods we already have.

With Cake2.1 we can now do:

App::uses('CakeTime', 'Utility');
$message = 'Your flier expired on ' . CakeTime::nice($flier['Flier']['expires']);

Same thing with NumberHelper and CakeNumber:

App::uses('CakeNumber', 'Utility');
$filesizeString = CakeNumber::toReadableSize($filesize);
instead of the helper overhead similar to above.

Last but not least the TextHelper methods have also been moved to the String class:

$shortext = String::truncate($longtext);
Only the autoLink() methods still remain in the helper. Mainly because they contain markup transformation (adding HTML) which belongs to the View layer.

And for the View layer itself nothing changes. The helpers work as before, only they now use the lib classes internally.

Going one step further

You can even pass in your own engines (lib classes) for the view helpers. For example by passing it into the helper settings:

public $helpers = array(..., 'Time'=>array('engine'=>'Tools.TimeLib'));

I use this approach:

class TimeExtHelper extends TimeHelper {
    public function __construct($View = null, $settings = array()) {
        $settings = Set::merge(array('engine' => 'Tools.TimeLib'), $settings); // instead of core CakeTime lib
        parent::__construct($View, $settings);
    }
}
It will then use /APP/Plugin/Tools/Lib/Utility/TimeLib.php instead. Then I alias it back to “Time” (same for Number and Text) and everything is like it was before, only that it uses my enhanced lib classes:
public $helpers = array(..., 'Time'=>array('className'=>'Tools.TimeExt'));
This way you can even enhance the helper itself (add some more functionality etc).

But if…

…if you still need to use helpers in the controller or model context? Well, then you are obviously still doing something wrong. There is absolutely no need for it anymore. And now no excuses anymore, either :)

You should move your code to the Lib folder as a package. Then use it in your controller as Lib class and in your View call the helper which then calls the Lib class. This way you keep it dry and simple. Just look how it is done in those classes above in the core.

Example: MathHelper.php

We might need its basic methods in the controller somewhere. The helper only adds markup or View related goodies. So we have a MathLib class in APP/Lib/Math (the Math package can also be any other name you want). And a MathHelper class in APP/View/Helper which uses the MathLib. Either the lazy way:

public function __call($method, $params) {
    return call_user_func_array(array($this->Math, $method), $params);
}
Or the verbose way for every single method:
public function calculate($value) {
    return $this->MathLib->calculate($value);
}
The latter has the advantage of type-hinting in your IDE.

And in the controller we do (let’s say we put the code in a “Tools” plugin):

App::uses('MathLib', 'Tools.Math'); # would then be in /APP/Plugin/Tools/Lib/Math/
$newValue = MathLib::calculate($value);
And in the view we can still use the helper as always:
$this->loadHelper('Tools.Math'); # would be in /APP/Plugin/Tools/View/Helper/
$newValue = $this->Math->calculate($value);

PS: I like to suffix my lib classes with Lib to avoid naming conflicts with Models or other cakephp or php classes. This is not required, of course. But I would recommend it as long we cannot use namespaces (which will be Cake3.0 and is still quite a few miles away).

Happy coding!

 
No Comments

Posted in CakePHP

 

Auth – inline authorization the easy way

07 Apr

I wrote a wrapper class to make inline authorization easier. Often times you want to check on certain roles inside an action or view and depending on the result display specific content or execute specific code. As an example we only want to display the “admin infos” box on the home screen for an admin. All other users should not see this box.

Status quo

We would need to check the session manually against the roles we want to grant access to. This can get pretty hairy with more than one role allowed (if admins and moderators are allowed to see this box for example).

Preparations

We first need to make the class usable by putting this in our /Config/bootstrap.php:

// these IDs match the role_ids in the DB
define('ROLE_SUPERADMIN', '1');
define('ROLE_ADMIN', '2');
define('ROLE_MOD', '3');
define('ROLE_USER', '4');
 
// enable the Auth class
App::uses('Auth', 'Tools.Lib');
I like to use constants as they are shorter than Configure::read('admin') etc. But Configure would work just as fine.

Then we need to decide whether we use single role (cake default) or multi role Authorization. I usually always use multi-roles. Therefore the default case for the Auth class is exactly this. The session then contains:

Auth.User.Role (with Role being an array of role ids)
If you use single roles, you’re Session array should look like this:
Auth.User.role_id (with role_id being the single role we want to check against)
In this case you should set the following constant manually in your bootstrap:
define('USER_ROLE_KEY', 'role_id');

“Former” usage

For comparison I will outline the manual and “outdated” way of authorization first:

# we want to make sure that piece is only visible to admins and moderators
if ($this->Session->read('Auth.User.role_id') == ROLE_ADMIN || $this->Session->read('Auth.User.role_id') == ROLE_MOD) {}
 
# or with multi-role
if (in_array(ROLE_ADMIN, (array)$this->Session->read('Auth.User.Role')) || in_array(ROLE_MOD, (array)$this->Session->read('Auth.User.Role'))) {}

Quite a lot to write…

Note: This also only works in controller/component and view/helper scope. You would have to use the static CakeSession::read() in order to make this work in the model/behavior one etc.

Usage

Now the fun part. The wrapper class can be found in the Tools Plugin.

# Same thing as above
if (Auth::hasRoles(array(ROLE_ADMIN, ROLE_MOD)) {}
Now isn’t that nicer to write and read?

The default case is that if one of the roles is matched it will return true right away. If you want to connect them with AND instead of OR, you need to make the second param false:

# This only passed if the user has both roles!
if (Auth::hasRoles(array(ROLE_ADMIN, ROLE_MOD), false) {}

If we only want to check against a single role we could also use the shorthand:

if (Auth::hasRole(ROLE_MOD) {}

Advanced usage

You can also pass in the roles you want to check against. This can be useful if you want to check somebody else’s roles (and not your session roles). This can come in handy in CLI (command line / shell) environment and also in the admin backend.

if (Auth::hasRole(ROLE_MOD, $rolesOfThisUser) {}
And
if (Auth::hasRoles(array(ROLE_MOD, ROLE_USER), true, $rolesOfThisUser) {}

And there is more

There are also some convenience methods available.

Instead of $uid = $this->Session->read('Auth.User.id') you can just write

$uid = Auth::id(); // anywhere in your application

The roles can be fetched like this:

$myRoles = Auth::roles(); // string in single-role and array in multi-role context

Last but not least the user data:

$user = Auth::user(); // complete user array
$username = Auth::user('username'); // string: current username
...

Final notes

Although this wrapper can be used about anywhere in your application with ease does not mean one should do that. Try to avoid using the auth (and therefore session) data in the model layer, for instance. Those should be kept state-less. But I also know that there are cases where it is pretty convenient to ignore this warning :)

Disclaimer

Just for clarification: This class does not provide you with the Authentication or Authorization. This has to be up and running already. It is only a wrapper to check user roles a more efficient and cleaner way.

So if you need to setup a fresh authentication, you can just use the cake Form authenticate for example. If you want multi-role authorization you would want to throw in some additional spices: You would need to write the Role array to your session upon successful login for the authorization module to work. See the above notes on how the session data array should look like.

For a new authorization take a look at my TinyAuth. It can handle single and multi role auth and those classes work well together (yeah – they have been designed to do that, of course^^).

 
No Comments

Posted in CakePHP

 

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

 

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

Don’t forget to add the RequestHandler to your Controller components list:

public $components = array('RequestHandler');
This will be an important part in the following auto-magic.

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 we use the RequestHandler Component 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 looked like that without utf8_decode the DOMPDF lib seems to be buggy – although it claims to support utf8.

Example for ics (ical calendar) files

Just a very basic example using an IcalHelper from my Tools Plugin.

Please note: There is a pending ticket on this. Until then you either need to manually patch the ResponseClass (quick solution) by adding the missing mimetype 'ics' => 'text/calendar', or define it in your Controller via $this->response->type(array('ics' => 'text/calendar')); and set all paths manually… Hopefully the second issue for unknown mimetypes will be addressed soon, as well.

Controller code:

$reservation = ...;
$this->plugin = 'Tools'; //not necessary, only that I want to store the layout once (in the plugin)
$this->set(compact('reservation'));
$this->helpers[] = 'Tools.Ical';

Layout (in /Plugin/Tools/View/Layouts/ics/default.ctp):

<?php echo $content_for_layout; ?>

View (in /View/Reservations/ics/export.ctp):

$data = array(
    'start' => $this->Time->toAtom($reservation['Reservation']['time']),
    'end' => $this->Time->toAtom(strtotime($reservation['Reservation']['time'])+HOUR),
    'summary' => 'Reservation',
    'description' => $reservation['Reservation']['headcount'].' persons @ location foo',
    'organizer' => 'CEO',
    'class' => 'public',
    'timestamp' => '2010-10-08 22:23:34',
    'id' => 'reservation-'.$reservation['Reservation']['id'],
    'location' => $reservation['Restaurant']['address'],
);
$this->Ical->add($data);
echo $this->Ical->generate();

And all that is left again is to link this action accordingly:

echo $this->Html->link($this->Html->image('icons/calendar.gif', array('title'=>'Download reservation as ical-file')), array('action'=>'export', $reservation['Reservation']['id'], 'ext'=>'ical'), array('escape'=>false));

 
12 Comments

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/Console/. 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 version of the upgrade 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. For a complete run through all your files with one click:
cake Upgrade.Upgrade all

Also note that I added a CorrectShell which goes beyond anything the Upgrade shell provides. It additionally takes care of

  • request (everything to do with the new request object)
  • vis (public/protected attributes)
  • forms
  • reference (corrections)
  • i18n (translation stuff)
  • amp (& removal)
  • php53 stuff

You can run this with for all tasks at once:

cake Upgrade.Correct all
Make sure you have a backup of everything! I also commit/push everything before running any such shells.

I also opened a ticket for it, but it seems my enhancements will not be made available for everyone.

Rendering elements

If you used to render elements/view ctps from the same view folder, you need to respect the new CamelCase folder structure:

$this->element('../MyControllerName/my_element');

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