Monday, September 25, 2006

New Feature: bindModel/unbindModel

 

This feature allows you to bind or turn off associations on the fly, and will be available in the next release (RC6?) of CakePHP.

Let us start with an unbindModel example. We have three models: User, Project and Supportrequest. A user has many projects and many support requests. If we do

debug($this->User->findAll());

the output would be something like:

Array
(
[0] => Array
(
[User] => Array
(
[id] => 1
[firstname] => Daniel
[lastname] => Hofstetter
)
[Project] => Array
(
[0] => Array
(
[id] => 1
[user_id] => 1
[name] => project A
)
)
[Supportrequest] => Array
(
[0] => Array
(
[id] => 1
[user_id] => 1
[comment] => a support request
)
[1] => Array
(
[id] => 2
[user_id] => 1
[comment] => another support request
)
)
)
)

Ok, but what if we are only interested in the users and the projects? Well, we could simply ignore the support requests. Or we could turn off the association to the support requests with unbindModel:

$this->User->unbindModel(array('hasMany' => array('Supportrequest')));
debug($this->User->findAll());

That gives us the following output:

Array
(
[0] => Array
(
[User] => Array
(
[id] => 1
[firstname] => Daniel
[lastname] => Hofstetter
)
[Project] => Array
(
[0] => Array
(
[id] => 1
[user_id] => 1
[name] => project A
)
)
)

Notice: The associations are reset after each call to a find function.

Ok, that was the unbindModel example. Let us go to the bindModel example. That example will be a little bit artificial as I do not see a real use case for bindModel at the moment. Maybe you know one? Ok, we have two Models: User and Supportrequest. The model do not have any associations. If we do

debug($this->User->findAll());

we get the following output:

Array
(
[0] => Array
(
[User] => Array
(
[id] => 1
[firstname] => Daniel
[lastname] => Hofstetter
)
)
)

For some reason we want to have a temporary “hasMany” association between user and support request. We define it in the following way using the usual association syntax:

$this->User->bindModel(array('hasMany' => array('Supportrequest' =>
array('foreignKey' => 'user_id'))));
debug($this->User->findAll());

And that is the output:

Array
(
[0] => Array
(
[User] => Array
(
[id] => 1
[firstname] => Daniel
[lastname] => Hofstetter
)
[Supportrequest] => Array
(
[0] => Array
(
[id] => 1
[user_id] => 1
[comment] => a support request
)
[1] => Array
(
[id] => 2
[user_id] => 1
[comment] => another support request
)
)
)
)

That’s it :) (original)

Monday, September 18, 2006

21 Things You Must Know About CakePHP

Original >>

Easily creating static pages

I needed to create several pages that didn't use any models and contained static data inside the default layout. My first thought was to create a controller for these pages and define an action for each static page I needed. However, this solution seemed tedious and would make it difficult to quickly add new pages. Enter the pages controller - simply create a view inside the views/pages/ folder and it'll automatically be rendered in /pages. For example, if I created /views/pages/matt.thtml it would be accessible via http://www.example.com/pages/matt

Static pages - Adjusting the page title

If you're using the pages controller and you need to change the page title, add the following to your view:
<? $this->pageTitle = 'Title of your page.'; ?>

Static pages - Adjusting other data sent to the layout

If you need to send data to the layout (such as a variable indicating what section to highlight on the nav bar), add this to your view:

<? $this->_viewVars['somedata'] = array('some','data'); ?>

That array should then be accessible as $somedata inside your layout.

Creating a simple admin center

If you need to create an administrative back-end for your CakePHP site and would like all the actions with administrative capabilities to exist under a specific folder, open up config/core.php and uncomment:
define('CAKE_ADMIN', 'admin');
This will then make all actions that are prefixed with "admin_" to be accessible via:
/admin/yourcontroller/youraction. For instance, if I created an action in my posts controller called "admin_add," I would access this via: www.example.com/admin/posts/add
From there I could simply password the admin folder to prohibit unwanted users from adding posts.

Viewing the SQL queries that are running behind the scenes

You can easily see the SQL queries that CakePHP is running by adjusting the DEBUG constant in config/core.php. 0 is production, 1 is development, 2 is full debug with SQL, and 3 is full debug with SQL and dump of the current object. I typically have debug set at 2, which renders a table at the bottom of the page that contains SQL debug information.
If rendering a table at the bottom of your site is constantly breaking your layout during development (especially if you're making AJAX calls and you're getting SQL inside your pages, not just the bottom), you can easily style this table to be hidden by adding this to your CSS:
.cakeSqlLog { display: none; }
This will allow you to view debug information in the HTML source code without your layout getting mangled, just don't forget to set debug back to 0 when your site goes live.

Multiple sources of documentation

Don't just rely on the manual. The wiki and the API are invaluable sources of information. The tutorials in the wiki are especially useful, and the API may be daunting at first, but you'll quickly find the information in there is crucial to building a site with CakePHP.

Using bake.php

Bake is a command line PHP script that will automagically generate a model, controller, and views based on the design of your database. I highly recommend using scaffolding to get a prototype going of a table that may change a lot in the beginning. If you're fairly certain the data is not subject to any drastic change, I recommend using bake instead. With bake all the files are generated and written to disk and you can make modifications from there. It saves a lot of time doing the repetitive tasks such as creating associations, views, and the basic CRUD controller operations.
Using bake is really easy. Once you have a table(s) in your database created, change directories to the /cake/scripts/ folder and run:
php bake.php
If you choose to bake interactively it'll walk you through the steps required to create your model, controller, and views. Once everything has been baked I usually go through all the generated code and make custom modifications as needed.

Mind permissions when moving cake around

When I changed from the development server to the live server I tarred up my entire cake directory and scp'd it to the new server. Immediately I started having an issue where any time the debug level was set to 0 (production mode), data would not be returned for certain database calls. This was a bit of a catch 22 since I needed to view debug information to troubleshoot the problem.
Someone in #cakephp kindly pointed out that permissions on the /app/tmp folder need to be writeable by apache. I changed the permissions to 777 and the issue went away.

Complex model validation

I needed to validate beyond just checking to make sure a field wasn't empty or it matched a regular expression. In particular, I needed a way to verify that the email address users registered with was unique. In the wiki I found this gem: this advanced validation tutorial, which covers some advanced methods of validation that were very useful.

Logging errors

$this->log('Something broke');
This will log your error to /tmp/logs/ (I initially made the mistake of thinking it would log it to the apache error log)

Creating a controller that uses other models

Suppose you have a controller that needs data from a bunch of different models, simply add this to the top of your controller:
class yourController extends AppController
{
var $uses = array('Post','User');
}

This controller would then have access to both the Post and the User model.

Creating a model for a table that doesn't actually exist in the database

I needed a way to create a model and controller without actually having an associated table in the database. I particularly wanted to make use of the $validate array so I could easily validate my fields and keep the validation logic in the model. CakePHP will throw an error if you create a model for a table that doesn't exist. Adding this to the model fixed the problem:
var $useTable = false;
You can use this to change tables names as well.
var $useTable = 'some_table';

Call exit() after redirecting

This should be no surprise to anyone who has done any serious web development in the past, but make sure you call exit() after running $this->redirect() if there's code afterward that you don't want to run. I've always done this in the past, but I made the assumption that $this->redirect() would make an exit call for me (which it didn't).

Advanced model functions

Unless you delve in to the API, there are some very useful model functions at your disposal you might not know exist. I highly recommend reading over the Model Class Reference at least once. Here's a few key functions I wasn't aware of that I found to be very useful:

  • generateList() - I use this function primarily to populate select boxes with data from associated tables
  • findBySql() - Sometimes you just need to write your own SQL
  • findCount() - Returns number of rows matching given SQL condition
  • hasAny() - Returns true if a record that meets the given conditions exists.

Again, I highly recommend reading over the entire model class reference, you'll be surprised at what you learn.

Inserting multiple rows in succession

I had a situation where I needed to iterate through a list of items and insert new rows for each. I quickly discovered that if you insert an item and then immediately insert another, the item that is inserted next doesn't insert at all. Instead the previously inserted row was being updated. For example:
$items = array('Item 1','Item 2','Item 3');
foreach ($items as $item) {
  $this->Post->save(array('Post' => array('title' => $item)));
}

This code will result in a single entry in the posts table: "item 3." CakePHP inserted "item 1", but then updates it to become "item 2," then "item 3" because $this->Post->id gets the value of the last inserted ID. Normally this functionality is very useful, but in this particular instance it was not. I found was to setting $this->Post->id = false after each insert solved the problem.

Inserting logic before or after controller functions

Suppose you needed an array of colors to be available to every view rendered by your controller but you don't want to have to define this data in every action. Using the beforeRender() callback will allow you to do this:
function beforeRender() {
  $this->set('colors',array('red','blue','green');
}

This would make $colors accessible in every view rendered by that controller. beforeRender() is called after the controller logic and just before a view is rendered.
There's also beforeFilter() and afterFilter(), which are called before and after every controller action. For more information, read up on callbacks in the models section of the manual.

Adding a WYSIWYG editor to CakePHP

I found this great tutorial on getting TinyMCE set up with CakePHP. Basically you just link the tiny_mce .js file to your page and then add a small bit of init code to every page that you want textareas to be converted into TinyMCE editors.

Writing your own SQL for HABTM relationships

I had an issue with trying to create a HABTM (has-and-belongs-to-many) relationship where I needed to specify my own SQL statement. According to the docs (at the time of this writing) you should set finderSql in your model, but according to the cakePHP source you should set finderQuery instead. It's just a foul-up in the docs, but I figured it'd be worth noting to save others from having to figure it out for themselves. Trac ticket here: https://trac.cakephp.org/ticket/1217

Sending email

I found two tutorials in the wiki: Sending email and Sending email with PHPMailer
I highly recommend the latter of the two, sending emails with PHPMailer is more secure and there's less of a headache because you don't have to deal with constructing the mail headers yourself.

Customizing HTML generated by the Helper

I needed to change the default <option> generated when I called $html->selectTag() to say something like "Please Select" rather than an empty space (default). I also wanted radio buttons to have labels so the user doesn't have to click exactly on the radio button itself but can instead click anywhere on the text associated with it.
Create the file /app/config/tags.ini.php and add the following:
; Tag template for a input type='radio' tag.
radio = "<input type="radio" name="data[%s][%s]" id="%s" %s /><label for="%3$s">%s</label>"
; Tag template for an empty select option tag.
selectempty = "<option value="" %s>-- Please Select --</option>"

You can get a full list of available tags in /cake/config/tags.ini.php. I wouldn't recommend modifying that file, however, because you could lose your changes when you upgrade CakePHP.

Creating a custom 404 error page

If you need to change the page that users see when a document is not found, create:
/app/views/errors/error404.thtml

Sunday, September 17, 2006

Fixed Security Exploit

Larry E. Masters aka PhpNut:

There was an security exploit brought to my attention today. I have fixed this exploit in the trunk and branched versions. Please replace the  app/webroot/js/vendors.php with this file.

https://trac.cakephp.org/browser/trunk/cake/1.x.x.x/app/webroot/js/ve...

This exploit is important to correct since it would allow reading files outside of the vendors/javascript directory when magic_quotes_gpc = Off.

And I put this file here:

<?php
/* SVN FILE: $Id$ */
/**
* Short description for file.
*
* This file includes js vendor-files from /vendor/ directory if they need to
* be accessible to the public.
*
* PHP versions 4 and 5
*
* CakePHP : Rapid Development Framework <http://www.cakephp.org/>
* Copyright (c) 2006, Cake Software Foundation, Inc.
* 1785 E. Sahara Avenue, Suite 490-204
* Las Vegas, Nevada 89104
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @filesource
* @copyright Copyright (c) 2006, Cake Software Foundation, Inc.
* @link http://www.cakefoundation.org/projects/info/cakephp CakePHP Project
* @package cake
* @subpackage cake.app.webroot.js
* @since CakePHP v 0.2.9
* @version $Revision$
* @modifiedby $LastChangedBy$
* @lastmodified $Date$
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
/**
* Enter description here...
*/
$file = $_GET['file'];
$pos = strpos($file, '..');
if ($pos === false) {
if(is_file('../../vendors/javascript/'.$file) && (preg_match('/(\/.+)\\.js/', $file)))
{
readfile('../../vendors/javascript/'.$file);
}
} else {
header('HTTP/1.1 404 Not Found');
}
?>

Saturday, September 16, 2006

Easy Way To Upload Files Up To Your Server

The easyest answer to question: "How to I can upload files to server?" Is - File Handler Component, written by Chris Partridge.

(You can find fresh version of this component at Code Snippet)

Just one moment! Do not use

$html->file ('UserFile');

Because construction

$_FILES['data[User][File]'] doesn't work (PHP 4.1.* at least).

So use standart html code:

<input type="file" name="userfile">

Then

$this->filehandler->upload('userfile','dir');

Remember, You must creating directory at your webroot ;-)

With 777 access ;-)

 

<?php

/***************************
* File Handler Component
***************************
* Author: Chris Partridge
* License: MIT
* Release Date: 09/03/2006
* Version: 1.0
* Revision: 2
*
* Description: This class is designed to be extended for
* a specific purpouse. However, it can act
* as a file repository component "out of the box".
***************************/
class FileHandlerComponent extends Object
{
/*
Probably the most important variable, this determines how the
file is handled within the component.
There are two options, db and array.
db:
This will use the dbModel and dbFields variables to save the
file data to the database. When getLastUploadInfo() is requested,
it will return the id aswell as all the other information.
A specific table and model need to be created for this to work.
When saved, it will be placed in $dir/$file_id/$filename
(e.g 'files/25/some_file.zip')
array:
This will take the uploaded file and store it. When getLastUploadInfo()
is run, it will return all information related to the file.
The information will be returned in the format of the dbFields array.
The unique directory name will be appended to the dir variable.
When saved, it will be placed in $dir/$unique_id/$filename
(e.g 'files/a438sdu4s/some_file.zip')
If you wish to save information about array uploads, you must
do so manually with the return data from getLastUploadInfo()
*/
var $handlerType = 'array';
/*
You can specify a different model to store the file
upload information. You need to create the correct model
file and database table for this to work correctly.
*/
var $dbModel = 'FileUpload';
/*
You can modify the array key values below, if you
want/have to change the field names in your database.
*/
var $dbFields = array( 'dir' => 'dir', // The directory the file was uploaded to
'file_name' => 'file_name', // The file name it was saved with
'mime_type' => 'mime_type', // The mime type of the file
'file_size' => 'file_size', // The size of the file
);
/*
This array stores all allowed mime types, a mime type
determines the type of file.
The specified mime types below should be safe for uploads,
however the compressed formats could be a touch unsafe.
*/
var $allowedMime = array( 'image/jpeg', // images
'image/pjpeg',
'image/png',
'image/gif',
'image/tiff',
'image/x-tiff',
'application/pdf', // pdf
'application/x-pdf',
'application/acrobat',
'text/pdf',
'text/x-pdf',
'text/plain', // text
'application/msword', // word
'application/mspowerpoint', // powerpoint
'application/powerpoint',
'application/vnd.ms-powerpoint',
'application/x-mspowerpoint',
'application/x-msexcel', // excel
'application/excel',
'application/x-excel',
'application/x-compressed', // compressed files
'application/x-zip-compressed',
'application/zip',
'multipart/x-zip',
'application/x-tar',
'application/x-compressed',
'application/x-gzip',
'application/x-gzip',
'multipart/x-gzip'
);
/*
This variable stores the maximum allowed file
size(in kilobytes) for uploaded files.
Please note that this can also be limited on the form
and by php itself. Default is 3584(3.5 MB).
*/
var $maxFileSize = 3584;
/*
These variables are used to store information about
errors or/information that needs to be stored while
using this component.
Do not modify these.
*/
var $errorMessage = null;
var $isError = false;
var $lastUploadData;
/*
__construct
Initiates the database model
and runs the parent constructor.
*/
function __construct()
{
if(!in_array($this->handlerType, array('db','array')))
{
$this->setError('The specified handler type is invalid.');
}
if($this->handlerType == 'db')
{
if(!class_exists($this->dbModel))
{
$this->setError('The specified database model does not exist.');
}
$this->{$this->dbModel} = &new $this->dbModel;
if(!is_subclass_of($this->{$this->dbModel}, 'AppModel'))
{
unset($this->{$this->dbModel});
$this->setError('The specified database model is not a cake database model.');
}
}
parent::__construct();
}

/*
getLastFileId
Returns the id of the last
uploaded file if using db
handler type.
*/
function getLastUploadInfo()
{
if(!is_array($this->lastUploadData))
{
$this->setError('No upload detected.');
}
else
{
return $this->lastUploadData;
}
}
function getMime($file)
{
if (!function_exists('mime_content_type'))
{
return system(trim('file -bi ' . escapeshellarg ($file)));
}
else
{
return mime_content_type($file);
}
}

/*
upload
Passed a form field and dir,
the class will check and attempt
to upload the file.
If uploaded, the details are stored
and the id is returned.
*/
function upload($field, $dir)
{
// Check that the two method variables are set
if(empty($field) || empty($dir))
{
$this->setError('You must supply a file field name and a directory on the server.');
return false;
}
// Check that the upload file field exists
if(!isset($_FILES[$field]))
{
$this->setError('No file supplied.');
return false;
}
// Check that the file upload was not errornous
if($_FILES[$field]['error'] != 0)
{
switch($_FILES[$field]['error'])
{
case 1:
$this->setError('The file is too large (server).');
break;
case 2:
$this->setError('The file is too large (form).');
break;
case 3:
$this->setError('The file was only partially uploaded.');
break;
case 4:
$this->setError('No file was uploaded.');
break;
case 5:
$this->setError('The servers temporary folder is missing.');
break;
case 6:
$this->setError('Failed to write to the temporary folder.');
break;
}
return false;
}
// Check that the supplied dir ends with a DS
if($dir[(strlen($dir)-1)] != DS)
{
$dir .= DS;
}

// Check that the given dir is writable
if(!is_dir($dir) || !is_writable($dir))
{
$this->setError('The supplied upload directory does not exist or is not writable.');
return false;
}
// Check that the file is of a legal mime type
if(!in_array($_FILES[$field]['type'], $this->allowedMime))
{
$this->setError('The file upload is of an illegal mime type.');
return false;
}
// Check that the file is smaller than the maximum filesize.
if((filesize($_FILES[$field]['tmp_name'])/1024) > $this->maxFileSize)
{
$this->setError('The file is too large (application).');
return false;
}
// Get the mime type for the file
$mime_type = $_FILES[$field]['type'];

// Update the database is using db
if($this->handlerType == 'db')
{
// Create database update array
$file_details = array($this->dbModel => array( $this->dbFields['dir'] => $dir,
$this->dbFields['file_name'] => basename($_FILES[$field]['name']),
$this->dbFields['mime_type'] => $file_type,
$this->dbFields['file_size'] => (filesize($_FILES[$field]['tmp_name'])/1024)
)
);
// Update database, set error on failure
if(!$this->{$this->dbModel}->save($file_details))
{
$this->setError('There was a database error');
return false;
}
// Get the database id
$file_id = $this->{$this->dbModel}->getLastInsertId();
$dir = $dir . $file_id . DS;
}
// Generate dir name if using handler type of array
if($this->handlerType == 'array')

{
$dir = $dir . uniqid('') . DS;
}
// Create a folder for the file, on failure delete db record and set error
if(!mkdir($dir))
{
// Remove db record if using db
if($this->handlerType == 'db')
{
$this->{$this->dbModel}->del($file_id);
}
// Set the error and return false
$this->setError('The folder for the file upload could not be created.');
return false;
}
// Move the uploaded file to the new directory
if(!move_uploaded_file($_FILES[$field]['tmp_name'], $dir . basename($_FILES[$field]['name'])))
{
// Remove db record if using db
if($this->handlerType == 'db')
{
$this->{$this->dbModel}->del($file_id);
}
// Set the error and return false
$this->setError('The uploaded file could not be moved to the created directory');
return false;
}
// Set the data for the lastUploadData variable
$this->lastUploadData = array( 'dir' => $dir,
'file_name' => basename($_FILES[$field]['name']),
'mime_type' => $mime_type,
'file_size' => (filesize($_FILES[$field]['tmp_name'])/1024)
);
// Add the id if using db
if($this->handlerType == 'db')
{
$this->_lastUploadData['id'] = $file_id;
}
// Return true
return true;
}
/*
setError
Set the errorMessage and isError
variables.
*/
function setError($error)
{
$this->isError = true;
$this->errorMessage = $error;
}
}
?>

Friday, September 15, 2006

How To Integrate WYSIWYG Editor into CakePHP?

Easy way to integrate Xihna into CakePHP.

Easy way to integrate TinyMCE into CakePHP.

And, of couse, it's SO EASY to integrate FCKEditor into CakePHP ;-)

But if you asked me about WYSIWYG Editor, I say to you: "I don't like WYSIWYG! And use MarkDown PHP.

So, about MarkDown "non-visual editor" next time. Ok? ;)

Security Component

Some tricks for extra security in cake.

If you call http://example.com/users/render/delete (you’ve an UsersController) you will get rendered the delete.thtml view.
Try it at home with any controller.
The render() method from Controller gets called. I’m not sure if something dangerous can be done, but some other functions can be called like redirect. For forbidding all the methods from Controller base class, use this component.

<?php
/**
 * MySecurity.php
 * Some security things for Cake
 *
 * Features:
 * - The public functions from controller & object now can’t be called from url
 * -
 *
 * @author RosSoft
 * @version 0.1
 * @license MIT
 *
 * @package components
 */

class MySecurityComponent extends Object
{
    var $components=array(’Security’);

    /**
     * Extra forbidden actions
     *
     * @var array $forbidden_actions
     */
    var $forbidden_actions=array();

    function startup(&$controller)
    {
        $this->forbidden_actions=am($this->forbidden_actions, get_class_methods(’Controller’));

        $this->Security->startup($controller);
        if (in_array($controller->action,$this->forbidden_actions))
        {
            $this->Security->blackHoleCallback=null;
            $this->Security->blackHole($this);
        }
    }
}
?>

 

Original at RosSoft.

Thursday, September 14, 2006

Need Different Title For Page? It's simple!

Just add this code in <head> your views:

<h2><?php echo $this->pageTitle="Page title goes here"?></h2>
But remember, in cakephp 1.2.xx you can achieve this by

$this->set(‘title’, ‘Foo bar’);
Of couse, you must setup this variable (pageTitle) in your controller ;-)
 

Navigation Helper The Best of!

If you want get code like this:

<ul id="main_nav"> <li><a href="/users/account' class="active">My account</a></li> <li><a href="/items/browse'>Browse</a></li> <li><a href="/items/add' class="active">Create item</a></li> <li><a href="/users/add' class="active">Create User</a></li> </ul>

You can use useful ;) helper - Navigation Helper!

Code like this:

<?$navigation->link('My account',"/users/account/",array('activeAction'=>'account'))?>
<?$navigation->link('Browse',"/items/browse/",array('activeAction'=>'browse'))?>
<?$navigation->link('Create Item',"/items/add/",array('activeAction'=>'add','activeController'=>'items'))?>
<?$navigation->link('Create Item',"/users/add/",array('activeAction'=>'add','activeController'=>'users'))?>
 

So... When user travel into your site and change /items/add -> users/add - the different menu be displayed (class = "active" I mean).


Just add code like this

<?php echo $navigation->render(null,array('id'=>'main_nav'))?>

In your views! ;-)


Easy Way? Yes! Easy!


You can find more detail information here. And download navigation helper (sorry it's not direct link, because this file present in other blog).

How To Make Model Without a Table? Easy Decision!

The answer is simple:

class ModelWithoutTable extends AppModel
{
var $useTable = false;
}
 

How To Make Controller Without a Model?

 

class MyController extends AppController
{
// var $uses = null; works too
var $uses = array();

function index()
{
}
}

If you omit the “var $uses = array();” you will get a “missing model” error.

Thank's to CakeBaker!

Wednesday, September 13, 2006

Useful Site for CakePHP Developer

Here you can monitore a lot of useful cakephp-oriented site (but without this ;)

Just reload this page every 2-3 hours and you can read all news, all discovery, all new hints.

CakePHP - is fastest developing framework in the world. Just try it!

How To Validate data Without Saving?

You know, CakePHP check data only when you saving:

$this -> User -> save ($this->data)

But if you do not needing to save data?

In this case, you can use this "hint":

if ( $this -> User -> validates ( $this -> data ) )
{
        // OK, good to go

}

else
{
        $this->set( 'data', $this->params['data'] );
        $this->validateErrors( $this->User );
        $this->render();

}

This code snippet made by Devo.

Hi!

The goal of this blog - concentrate useful tips, hints and original metod of programming... All about Cakephp - one of the best framework in the world ;-)