What is kata?
Kata is a passive MVC Framework utilizing inversion-of-control and class-factory patterns.See http://de.wikipedia.org/wiki/Model_View_Controller for an explanation what MVC is.
See http://codeninja.de/kata_php_framework/#eclipse on how to set up Eclipse for kata.
First step: Display "Hello World"
Put the following code into the file controllers/main_controller.php:class MainController extends AppController { function index() { $this->setPageTitle('My first view title'); } }
Now put the following code into the file views/main/index.thtml:
Hello World! <? echo ' ...And hello again!'; ?>
And finally put the following code into the file views/layouts/default.thtml:
<html> <head> <title><?=$title_for_layout?></title> </head> <body> <!-- this is where kata injects the html of a view: --> <?=$content_for_layout?> </body> </html>
If you now call http://localhost/ (presuming you have kata installed directly into your apache webroot) you will see the following output:
<html> <head> <title>My first view title</title> </head> <body> <!-- this is where kata injects the html of a view: --> Hello World! ...And hello again! </body> </html>
You can also call http://localhost/main or http://localhost/main/index because main is the default controller, and index is the default action.
What happens when i request http://myhost/games/list/param1/param2
For a complete Flowchart see http://codeninja.de/kata_php_framework/#flowchartThis (simplified) is what basically happens: - kata loads the file controllers/games_controller.php because thats the first parameter in the url
- inside that file kata expects a class of type GamesController
- inside that class kata expects a method list() because thats the second parameter in the url
- kata calls GameController->list('param1','param2');
- kata renders template views/games/list.thtml
- kata renders views/layouts/default.thtml and injects the template it rendered in the previous step
...access GET-parameters
adding ?foo=1 to the url.class MainController extends AppController { function index() { var_dump($this->params['url']['foo']); die; } }results in '1'
...access POST-parameters
using the following form:<form action="<?=$html->url('main/index/?param1=red')?>" method="post"> <input type="text" name="foo" value="1" /> <input type="text" name="bar[]" value="1" /> </form>To access the GET and POST-parameters:
class MainController extends AppController { function index() { var_dump($this->params['form']['foo']); var_dump($this->params['form']['bar']); die; } }results in array('1',array('1')) Inside a view you also have access to $this->params.
...get variables from the controller into the view
Inside the controller:class MainController extends AppController { function index() { $this->set('var1','somestring'); } }
Inside the view (views/main/index.thtml in this case):
var_dump($var);results in 'somestring'
Any variables you set in the controller are also available in the layout.
...redirect browser to a different url
Inside the controller:class MainController extends AppController { function index() { $this->redirect(''); //=main/index // the following lines are never executed because // redirect terminates the program flow: $this->redirect('games/'); //=games/index $this->redirect('games/list2/'); } }
...render a different view
class MainController extends AppController { function index() { $this->render('inbox'); //renders views/games/inbox.thtml // the second render is never executed because // we did already render $this->render('../user/banned'); //renders views/user/banned.thtml } }
Rendering different views instead of heaving a view that displays different things depending on how a variable is set is much cleaner and easier to read.
Hint: If you have reoccuring pieces of html that you need in multiple views use an element.
...use a different layout
You can change the layout before you render:class MainController extends AppController { function index() { $this->layout = 'black'; //uses views/layouts/black.thtml } function dummy() { $this->layout = null; //dont use a layout, just return the view } }
You can also change the layout for the whole controller:
class MainController extends AppController { public $layout = 'black';
...execute something before any method of a controller is called
Inside the controller:class MainController extends AppController { function beforeAction() { $this->checkForLogin(); } }
beforeAction() is normally used for code that always needs to be executed, for example check if a user is logged in or if the current user is allowed to execute the current action.
You should put beforeAction() into the AppController if you want all Controllers to act like this.
...execute something before a view is rendered
class MainController extends AppController { function beforeFilter() { $this->set('mycash',$this->User->getCash()); } }
beforeFilter() is normally used for code that only needs to be executed if you really want to display something. It is not executed if you do a redirect. Use it if you need to fetch some data from the model to be display to display a view. Example:
class MainController extends AppController { function beforeFilter() { //automatically set pagetitle. expects localization in //"controller/action title" format $this->set('title', __($this->params['controller'].'/'. $this->params['action'].' title')); } }
...do ajax
Just prepend ajax/ to your url: Change games/list to ajax/games/listYou will find 1 in $this->params['isAjax'] and the view is rendered "naked" (read: without a layout). You can react inside your beforeAction() and beforeFilter() accordingly to skip code that is not needed for ajax-calls. Examples are: Check if the user is logged in inside beforeAction(),
...use the same methods in all controllers
There are situations where you need some code in all controllers, for example a method that checks if the user is online. Create a file controllers/app_controller.php and put the following content in it:class AppController extends Controller { public $userArray; function checkForLogin() { $user = $this->Session->read('user'); if (empty($user)) { $this->redirect('user/login'); } $this->userArray = $user; }
Because all controllers extend AppController they all inherit the checkForLogin method. Possible Gotcha: If you overwrite a method thats in the AppController in your own controller you have to do a parent::myMethodName(); if you want to execute both methods.
...use a session
kata comes with a session-component (see 'what is a component' for reference). Simply add public $components = array('Session'); to your AppController.After this you can manipulate session-data in the following way:
$var = $this->Session->read('myvariable'); //read variables from the session $this->Session->write('user',$userArray); //write something into the session $this->Session->delete('user'); //user logout $this->Session->destroy(); //end session here and now $this->Session->getIp(); //get the users ip
The Session-component alters its behaviour depending on certain settings in config/core.php:
- SESSION_TIMEOUT sets after how many seconds the session expires.
- SESSION_COOKIE sets the name of the cookie to store the session id in.
- SESSION_STRING is used if you have multiple kata-installations running on the same server. If you set this to the same value you can access the same sessions throughout different installations.
SESSION_STORAGE sets how session data is stored:
- 'FILE' uses normal php filesystem based sessions
- 'CLIENT' uses an encrypted cookie in the clients browser
- 'MEMCACHED' uses memcache to store sessions. you have to configure MEMCACHED_SERVERS in this case.
...use localized strings
kata comes with a locale-component (see 'what is a component' for reference). Simply add public $components = array('Locale'); to your AppController. (We assume you have set LANGUAGENOTPRINTF to true in your config/core.php, which is the default).Example: you have defined LANGUAGE as de in your config/core.php. You have to create controllers/lang/de.php that contains the strings you want in the following format:
$messages = array( 'GAMENAME' => 'Turbo Challenge', 'email title' => 'We welcome %name% to %group%' );
Now you can access this string by
$gamename = __('GAMENAME');
If you have parameters you want to inject into the locale-strings simply use
echo __('email title', array( 'name' => $username, 'group' => $groupname ) );
...access a helper inside a view
Add the helper you want to use to your controller:class MyController extends AppController { public $helpers = array('html');After this you can do the following inside your view:
echo $html->url('my/index'); // returns absolute url echo $html->relUrl('my/index'); // returns relative url echo $html->url('http://somesite.com/foo?bla=1'); // returns http://somesite.com/foo?bla=1 echo $html->image('img/foo.jpg'); // returns complete image-tag echo $html->image('img/foo.jpg', array('align'=>'left')); // image with align="left"
For a full reference what helpers pre-exists and what they offer: see kata class reference, "Helpers" tree.
...access a database
Configure the database to use by modifying config/database.php:class DATABASE_CONFIG { public static $default = array( 'driver' => 'mysql', 'host' => 'localhost', 'login' => 'username', 'password' => 'password', 'database' => 'www13', 'prefix' => '', 'encoding' => '' ); }
Create the file models/users.php with the following content:
class Users extends AppModel { /* returns: array( * [0] => array('user_id'=>4, 'group_id'=>1, 'username'=>'Alice'), * [1] => array('user_id'=>5, 'group_id'=>1, 'username'=>'Bob'), * [2] => array('user_id'=>6, 'group_id'=>2, 'username'=>'Eve') * ); */ function getAllUsers() { return $this->query('SELECT * FROM users'); } /* returns: array( * [4] => array('user_id'=>4, 'group_id'=>1, 'username'=>'Alice'), * [5] => array('user_id'=>5, 'group_id'=>1, 'username'=>'Bob'), * [6] => array('user_id'=>6, 'group_id'=>2, 'username'=>'Eve') * ); */ function getAllUsersById() { return $this->query('SELECT * FROM users','user_id'); } /* returns: array( * [1] => array( * [4] => array('user_id'=>4, 'group_id'=>1, 'username'=>'Alice'), * [5] => array('user_id'=>5, 'group_id'=>1, 'username'=>'Bob'), * ), * [2] => array( * [6] => array('user_id'=>6, 'group_id'=>2, 'username'=>'Eve') * ), * ); */ function getAllUsersByGroupAndId() { return $this->query('SELECT * FROM users',array('group_id','user_id')); } }
To use the model inside your controller do:
class UserController extends AppController { public $uses = array('User'); //load user-model public function listusers() { $this->set('allUsers', $this->User->getAllUsers()); } }
If you have only read-access data it may be useful to do something like this:
class Items { //note: we dont extend AppModel! protected $itemnames = array( 1 => 'Tool', 2 => 'Weapon', 3 => 'Junk' ); function getItemnameById($id) { if (isset($this->itemnames[$id]]) { return $this->itemnames[$id]; } return false; } }
...my params do not survive a redirect
thats normal, because a redirect simply instructs the users browser to fetch a new url. if you need your data to survice a redirect you have to append them to the redirect-url (as GET params) or you have to write them into the session....how to i access a helper from a model
You dont. Redesign your code so you dont have to....how to i access the model from a view
You dont. Redesign your code so you dont have to....i need the same code inside a view and inside a model
An example for this problem would be a bbcode-parser. You need the code to display bbcode, and you need the code to send emails.Solution: Put your code into a utility.
...use a model inside a model
Example:class User extends Model { function isAllowedToLogin($userid,$password) { $userblob = $this->readUserBlob($userid); if ($userblob['password'] != $password) { return false; } $accessModel = getModel('Access'); return $accessModel->hasAccess($userid,'LOGIN'); } }
...use active record model functions
Active record allows simplified access independent of the currently used RDBMS. You dont have to write SQL.For the following examples we show you what to write and what the resuling SQL-code kata generates is. We assume a simple table with the following layout:
CREATE TABLE `test` ( `id` int(11) NOT NULL auto_increment, `f1` int(11) NOT NULL, `f2` int(11) NOT NULL, `f3` int(11) NOT NULL, `f4` int(11) NOT NULL, PRIMARY KEY (`id`) );
We also assume your Model knows what table to access, because you supplied a tablename:
class Test extends Model { public $useTable = 'test';
Insert data into a table:
$this->create(array( 'id'=>1, 'f1'=>10, 'f2'=>11, 'f3'=>12, 'f4'=>13 ));results in:
INSERT INTO `test` (`id`,`f1`,`f2`,`f3`,`f4`) VALUES ('1','10','11','12','13')
Insert data, primary key is autoincrement:
$this->create(array( //primary key missing, but its autoincrement 'f1'=>20, 'f2'=>21, 'f3'=>22, 'f4'=>23 ));results in:
INSERT INTO `test` (`f1`,`f2`,`f3`,`f4`) VALUES ('20','21','22','23')
Replace data (like insert, but deleted any record with the same primary key(s)):
$this->replace(array( 'id'=>1, 'f1'=>1, 'f2'=>1, 'f3'=>1, 'f4'=>1 ));results in:
REPLACE INTO `test` SET `id`='1',`f1`='1',`f2`='1',`f3`='1',`f4`='1'
Update data:
$this->update(1, array( 'f1'=>3, 'f2'=>33, 'f3'=>33, 'f4'=>33 ));results in:
UPDATE `test` SET `f1`='3',`f2`='33',`f3`='33',`f4`='33' WHERE `id`='1'
Remove data:
$this->delete(5); //does the same: $this->delete(array('id'=>5));results in:
DELETE FROM `test` WHERE `id`='5'If you have multiple indexes you would use:
$this->delete(array('id1'=>6, 'id2'=>2));
Read data:
var_dump( $this->read(1) ); //does the same: var_dump( $this->read(array('id'=>1)) );results in:
SELECT * FROM `test` WHERE `id`='1'which returns:
array
0 =>
array
'id' => string '1' (length=1)
'f1' => string '4' (length=1)
'f2' => string '44' (length=2)
'f3' => string '44' (length=2)
'f4' => string '44' (length=2)
Read data with prefiltered result-array:
var_dump( $this->read(1,array('f1')) );results in:
SELECT f1 FROM `test` WHERE `id`='1'which returns:
array
0 =>
array
'f1' => string '4' (length=1)
Complex read data:
var_dump($this->find('all',array( 'conditions'=>array( 'f1'=>1 ), )));results in:
SELECT * FROM `test` WHERE `f1`='1'
Conditions can also contain <,>,IN,LIKE and so on:
var_dump($this->find('all',array( 'conditions'=>array( 'f1 >'=>1 ), )));results in:
SELECT * FROM `test` WHERE `f1` > '1'
var_dump($this->find('all',array( 'conditions'=>array( 'f1 in'=>array(1,2), ), )));results in:
SELECT * FROM `test` WHERE f1 IN ('1','2')
Complicated complex read example:
var_dump($this->find('all',array( 'conditions'=>array( 'f1 >'=>1, array( 'f1'=>2, 'or', 'f2'=>3, array( 'f3 <='=>4, 'f3 like'=>'%foo' ) ) ), )));results in:
SELECT * FROM `test` WHERE `f1` > '1' AND ( `f1`='2' OR `f2`='3' OR ( `f3` <= '4' AND `f3` like '%foo' ) )
Complex read with automatic pagination (even for RDBMS that dont support 'LIMIT'):
var_dump($this->find('all',array( 'page'=>1, //which page to return 'limit'=>1, //how many rows per page )));results in:
SELECT * FROM `test` LIMIT 0,1
We can tell kata to filter the result (modus 'list'):
$data = $this->find('list',array( 'listby'=>array( 'id', // keys of the returned array are // filled with the value of the id-field ) ));results in:
SELECT * FROM `test`which returns:
Array
(
[1] => Array
(
[id] => 1
[f1] => 4
[f2] => 44
[f3] => 44
[f4] => 44
)
[2] => Array
(
[id] => 2
[f1] => 20
[f2] => 21
[f3] => 22
[f4] => 23
)
...
You can also do this multi-dimensionally:
$data = $this->find('list',array( 'listby'=>array( 'id','f1' // 2-dimensional array which has its keys filled // with the value of id and f1 ) ));results in:
SELECT * FROM `test`which returns:
Array
(
[1] => Array
(
[4] => Array
(
[id] => 1
[f1] => 4
[f2] => 44
[f3] => 44
[f4] => 44
)
)
[2] => Array
(
[20] => Array
(
[id] => 2
[f1] => 20
[f2] => 21
[f3] => 22
[f4] => 23
)
)
...
...cache database queries
Currently only ->query() can be cached (that will change). Just replace query() with cachedQuery() inside your Model:// simples form function getHighscore() { return $this->cachedQuery('SELECT * FROM scores ORDER BY score'); } // cachedQuery supports the parameter to order the result by, too function getHighscore() { return $this->cachedQuery('SELECT * FROM scores ORDER BY score','place'); } // you can supply the cache-identifier to use yourself, normally kata will // generate one for you function getHighscore() { return $this->cachedQuery('SELECT * FROM scores ORDER BY score','place', 'highscorecached'); } // cache only for 300 seconds, and let kata generate an cache-identifier function getHighscore() { return $this->cachedQuery('SELECT * FROM scores ORDER BY score','place', false,300); }
...cache anything anywhere
Simply use the cacheUtility. The cacheutility autoselects the best caching- method for you, but you can override that behaviour. Example:function writeHighscore() { $highscore = $this->calculateHighscore(); $cacheUtil = getUtil('Cache'); //write data to the cache, under the name 'highscore' and cache //for 300 seconds. $cacheUtil->write($highscore,'highscore',300); //same as above, but force using the filesystem to persist the data //to we survive a reboot $cacheUtil->write($highscore,'highscore',300,CacheUtility::CM_FILE); } function readHighscores() { $cacheUtil = getUtil('Cache'); $highscore = $cacheUtil->read('highscore'); //did we success? return if (false !== $highscore) { return $highscore; } //next try: read it from the filesystem. $cacheUtil->read('highscore',CacheUtility::CM_FILE); if (false !== $highscore) { return $highscore; } //still no luck? calculate it. return $this->caluclateHighscore(); }
...create a protection for XSRF
Such a protection involves three steps: - Create a token (if the user logs in) and write it into the users session- use $html->tokenUrl() instead of $html->url() for all
POST-FORMs you want to protect - Check (for example if you detect post-parameters) if the token is there and
valid.
An example implementation would look like this:
class UserController extends Controller { function generateToken() { return md5(rand(0,PHP_MAX_INT)); } //put a generated token function login() { //.... //if user did login successfully: $this->Session->write('usertoken', $this->generateToken()); $this->redirect('inbox/index'); }And the AppController:
class AppController extends Controller { function checkToken() { $tokenShould = $this->Session->read('usertoken'); $tokenIs = is($this->params['url']['__token'], ''); if ($tokenShould != $tokenIs) { //user sees only 'internal server error' throw new Exception('XSRF Protection'); } } function beforeFilter() { $this->set('__token',$this->Session->read('usertoken')); } function beforeAction() { //do we have post parameters appended? if (count(is($this->params['form'], array())) > 0) { $this->checkToken(); } }
...use different databases
You have a second database-connection inside your config/database.php:class DATABASE_CONFIG { public static $default = array( 'driver' => 'mysql', 'host' => 'server1', 'login' => 'username', 'password' => 'password', 'database' => 'www13', ); public static $second = array( 'driver' => 'mysql', 'host' => 'server2', 'login' => 'username', 'password' => 'password', 'database' => 'www13', ); }To use it with a second Model you just have to add:
class User extends AppModel { public $connection = 'second';You can also swap the connection inside your model dynamically, even if you swap (for example) between mysql and mssql:
$this->changeConnection('second'); //do stuff $this->changeConnection('default'); //do stuff again $this->changeConnection('second'); //no reconnect needed because its cached
...use the same helpers, components or models in all controllers
All models, components, helpers you add inside your AppController are accessible to all descendant controllers. All models, components, helpers you use inside a controller are simply added to the list.Example:
class AppControllers extends Controller { $uses = array('User');and your own controller looks like this:
class MyController extends AppController { $uses = array('Ships');then MyController has access to the User-Model and the Ships-Model.
...make a php-file accessible via webserver without using kata
All files you dump into the webroot/ directory are directly accessible without using kata.Example: You have a file webroot/moo.php that contains
echo 'moooooo!';Then you can simply call http://localhost/moo.php.
...include external classes
Put your php-file into vendors/ and include it with vendor('file') (without vendors/ and .php).Example: If you want to use your class (for example vendors/mailer/class.php) simply use:
vendor('mailer/class');kata makes shure you dont include a file multiple times.
You can use vendor() anywhere you want.
...render a view and have it returned to me
class MainController extends AppController { function sendMail() { $html = $this->renderView('emailTemplate'); mail('staff@example.org', 'Welcome!', $html); // after sendMail returns kata renders // views/main/sendMail.thtml as if nothing happend } }
...kata and debugging
Edit config/core.php and set DEBUG TO 2. You will now see a debug-output at the bottom of the page containing various informations: current GET/POST parameters, sql-queries and results, cache-operations and results, loaded Classes and so on.If an exception occurs kata will render the error-layout which will output what went wrong, how we got there, and what parameters we used.
Hint: If you want to debug a caching-problem you can disable all caching-operations by setting DEBUG to 3
There are several debugging-aides that you can enable inside your core.php. These mostly only work if DEBUG is at least 2.
Uncommenting include(LIB.'boot_firephp.php'); will redirect all debugging-output from kata to the firebug console (if DEBUG equals 1).
Uncommenting include(LIB.'boot_coverage.php'); will output a detailed listing of all your used models, controllers etc. and will hilite lines that were actually executed. Very helpful to find unreachable code or to get to the bottom of unexpected behaviour.
Uncommenting include(LIB.'boot_profile.php'); will output a detailed listing which commands or functions were executed how often, and how many microseconds processing power they consumed.
Uncommenting include(LIB.'boot_strict.php'); will turn any notice, warning, error into a hard warning.
what is a helper
A helper is a class (thats a descendant class of Helper) that is accessible inside a view. Let's create and use one:Drop this into views/helpers/beans.php:
Class BeansHelper extends Helper { protected $gas=0; function eat() { $this->gas++; if ($gas<3) { return 'You have eaten a bean'; } return 'FFFFFRTZZZ!'; } }
To make this helper accessible inside a view add this line to your controller (if you only use it inside one controller) or your AppController (if you use it everywhere):
class SomeController extends AppController { $helpers = array('beans'); }
Now you can do the following inside your view OR layouts:
echo $beans->eat();
what is a component
A component is a lightweight class to support a controller. It has access to all public methods and properties of a controller. Let's create and use one:Drop this into controllers/components/access.php:
class AccessComponent { // always needed: protected $controller = null; // also always needed: public function startup($controller) { $this->controller = $controller; } // your own method: public function check() { $user = is(env('PHP_AUTH_USER'),''); $password = is(env('PHP_AUTH_PW'),''); if (empty($user) || empty($env) || ($user<>'admin') || ($user<>'password')) { header('WWW-Authenticate: Basic realm="uh."'); header('HTTP/1.0 401 Unauthorized'); $this->controller->render('auth_needed'); die; } } }
To make this helper accessible inside a controller add this line to your controller (if you only use it inside one controller) or your AppController (if you use it everywhere):
class SomeController extends AppController { $components = array('Access'); }
Now you can do the following inside your view OR layouts:
$this->Access->check();
what is a model
A model is a class (thats a descendant of model) that is used to access/store data.//TODO see How to access a database
what is a utility
A utility is a lightweight class (without dependencies) that can be used anywhere. Let's build and use one: put this into utils/bbcodeUtility.php:class BbcodeUtility { function parse($bbcode) { return str_replace( array( '[b]','[/b]' ),array( '<span style="font-weight:bold">','</span>' ),$bbcode); } }
To use it do the following:
$bbcodeUtil = getUtil('Bbcode'); echo $bbcodeUtil->parse('[b]wow[/b]');
what is an element
An element is a simplified view that you can use inside any view. Let's build and use one: put this into views/elements/legend.thtml:I'm a element. i have access to helpers: <?=$html->url('help/')?> I also have access to all view variables you set() inside your controller. I can be supplied with custom variables: <?=$test?>
To use it do the following inside your view:
echo $this->renderElement('legend',array('test'=>123));
what is the registry
The registry can be used to store application wide configuration-parameters that are automatically written to disk.Example:
kataReg::set('foo','bar'); kataReg::set('test',false); kataReg::set('kata.session_start',false); kataReg::set('kata.special',true); var_dump(kataReg::get('foo','notbar')); var_dump(kataReg::get('foox','notbar')); var_dump(kataReg::get('test')); var_dump(kataReg::get('kata','mist')); var_dump(kataReg::get('kata.special'));
Results in:
(string) 'bar' (string) 'notbar' (bool) false array( 'session_start'=>false, 'special'=>true ) (bool) true