Using and reusing CakePHP plugins

Post on 21-May-2015

5,605 views 1 download

Tags:

description

Using and reusing plugins across CakePHP applications - CakeFest 2010 (Chicago)

transcript

PLUGINSAcross applications

Using and Reusing

CakeFest 2010 - Chicago Pierre MARTIN

ME

@pierremartin http://pierre-martin.fr

June 2008 - CakePHP 1.2 beta

CakePHP-fr

YOU

?Used a plugin

Wrote a plugin

Reuse regularly

WHY

Spaghetti code

Libraries

OOP, TemplatesMVC and Frameworks

+ Reusable classes (Behaviors, Components, Helpers)

Fat ModelsSkinny Controllers

REUSING CODE

Plugins

Or

C P S R

Copy and Paste / Search and Replace :)

HOW

/APP/PLUGINSmy_plugin/ my_plugin_app_model.php my_plugin_app_controller.php models/ behaviors/ my_plugin_foo.php my_plugin_bar.php controllers/ components/ my_plugin_foos_controller.php my_plugin_bars_controller.php views/ helpers/ layouts/ elements/ my_plugin_foos/ index.ctp my_plugin_bars/ add.ctp

locale/ eng/ LC_MESSAGES/ my_plugin.po (__d())webroot/ css/

style.cssother.css

img/logo.png

js/foobar.js

tests/libs/vendors/

MODELS/app/plugins/users/models/user.php

ClassRegistry::init(‘Users.User’);

App::import(‘Model’, ‘Users.User’);

public $belongsTo = array(‘Users.User’);public $belongsTo = array(

‘User’ => array(‘className’ => ‘Users.User’));

public $belongsTo = array(‘User’ => array(

‘className’ => ‘Users.UsersUser’));

PLUGIN.THING

It works for everything!

// Behaviorspublic $actsAs = array(‘Comments.Commentable’);

// Componentspublic $components = array(‘Twitter.TwitterAuth’);

// Helperspublic $helpers = array(‘Tags.TagCloud’);

// Libraries, Vendors, Custom routes...App::import(‘Lib’, ‘Chuck.Norris’);

ACTIONS / ELEMENTS/app/plugins/users/controllers/users_controller.php

/app/plugins/users/views/elements/login.ctp

$this->redirect(‘plugin’ => ‘users’, ‘controller’ => ‘users’, ‘action’ => ‘register’);$this->redirect(‘plugin’ => null, ‘controller’ => ‘pages’, ‘action’ => ‘display’, ‘home’);

$this->Html->link(‘plugin’ => ‘users’, ‘controller’ => ‘users’, ‘action’ => ‘index’);// Will generate http://domain.com/users

$this->element(‘login’, array(‘plugin’ => ‘users’, ‘foo’ => ‘bar’));

ASSETS/app/plugins/jquery/webroot/js/jquery.js

/app/plugins/jquery/webroot/css/jquery.ui.css

/app/plugins/jquery/webroot/img/subdir/logo.png

$this->Html->script(‘/jquery/js/jquery.js’);

$this->Html->css(‘/jquery/css/jquery.ui.css’);

$this->Html->image(‘/jquery/img/subdir/logo.png’);

Source: @dogmatic69

EXTENDING PLUGINS

WHY?

Appearance customization

App specific logic

Changing features, redirections...

Adding features

“USERS” PLUGIN

• User model

• id, username, password

• Users Controller

• login, logout, register, reset_password

• Views

VIEWS/app/plugins/users/views/users/register.ctp/app/views/plugins/users/users/register.ctp

<h1><?php __(‘Create a new account on Awesomeness.org’); ?><?php

echo $this->Form->create(‘User’);echo $this->Form->input(‘username’);echo $this->Form->input(‘password’);echo $this->Form->input(‘password_confirm’);// App specific featureecho $this->Form->input(‘Profile.newsletter’, array(

‘label’ => __(‘Suscribe to our newsletter’, true),‘type’ => ‘checkbox’));

echo $this->Form->end(__(‘I want to be awesome!’, true));?>

MODELS<?phpApp::import(‘Model’, ‘Users.User’);class MyUser extends User {

// [...]public $hasOne = array(‘Profile’);// [...]public function __construct($id = false, $table = null, $ds = null) {

parent::__construct($id, $table, $ds);$this->validate[‘username’][‘length’] = array(

‘rule’ => array(‘minLength’, 5));}// [...]public function register($data) {

$success = parent::register($data);if ($success) {

// Your business logic here}return $success;

}// [...]public function foobar() { }

}?>

CONTROLLERS<?phpApp::import(‘Controller’, ‘Users.Users’);class MyUsersController extends UsersController {

// [...]public function beforeFilter() {

$this->User = ClassRegistry::init('MyUser');parent::beforeFilter();$this->Auth->deny('index');

}// [...]public function register() {

if (!empty($this->data)) {if ($this->User->register($this->data)) {

// Your app specific logic here$this->redirect(‘controller’ => ‘pages’, ‘action’ => ‘display’, ‘welcome’);

}}parent::register();

}// [...]public function foobar() { }

}?>

CONTROLLERSRouter::connect(

'/users/:action/*',array('plugin' => ‘users’, 'controller' => 'users'));

Router ::connect('/users/:action/*',array('plugin' => null, 'controller' => 'my_users'));

public function render($action = null, $layout = null, $file = null) {if (is_null($action)) {

$action = $this->action;}if ($action !== false) {

if (!file_exists(VIEWS . 'my_users' . DS . $action . '.ctp')) { $file = App::pluginPath('users') . 'views' . DS . 'users' . DS . $action . '.ctp'; }

}return parent::render($action, $layout, $file);

}

TODO Improve me

... AND IT WORKS WITH EVERYTHING

App::import(‘Behavior’, ‘Comments.Commentable’);class MyCommentable extends Commentable {

}

Helpers, Libraries, Components, Behaviors...

TIPS AND TRICKS

Serious stuff coming!

DON’T TRUST ME!

Unless you’ve tried it yourself

REUSE EXISTING PLUGINS

CakePackages.com:•548 CakePHP related projects•284 developers

CakePHP’s main feature is its community

KISS

ExtendRefactor

USE OBJECTS ATTRIBUTES// Models$this->alias$this->name$this->displayField$this->primaryKey

$this->data[‘User’][‘id’]; // Before$this->data[$this->alias][$this->primaryKey]; // After

// Controllers$this->plugin$this->modelClass // MyModel$this->modelKey // my_model$this->name

Add attributes to your classes!

COMPONENTS ARE THE KEY!

Add some magic in your plugins

HELPER AUTOLOADING

class CommentManager extends Object {public $autoHelper = true;

public $helperName = ‘Comments.CommentWidget’;

public function beforeRender(Controller $Controller) {if ($this->autoHelper) {

$Controller->helpers[] = $helperName;}

}}

BEHAVIOR AUTOLOADING

class CommentManager extends Object {public $autoBehavior = true;

public $behaviorName = ‘Comments.Commentable’;

public function startup(Controller $Controller) {$Model = $Controller->{$Controller->modelClass};if ($autoBehavior && !$Model->Behaviors->attached($this->behaviorName)) { $Model->Behaviors->attach($this->behaviorName);}

}}

AUTODETECTED ACTIONS

class CommentManager extends Object {public $autoActions = true;

public function startup(Controller $Controller) {if ($autoActions) {

if (!empty($Controller->data[‘Comment’])) {// [...] Automatically save the comment$Controller->redirect($Controller->referer());

}}

}}

AUTO DATA FETCHING

class FoobarManager extends Object {

public function beforeRender(Controller $Controller) {$data = [...]; // Your logic here to get the correct data for the view$Controller->set(‘data_for_foobar_helper’, $data);

}

}

HELPERS THAT HELP

• Reduce PHP code in views

• Unique entry point

• Deal with elements

• Performance optimization

... BEHIND THE SCENE

class FoobarHelper extends AppHelper {

public function beforeRender() { if (ClassRegistry::isKeySet('view')) { $View = ClassRegistry::getObject('view'); $this->_data = $View->getVar('data_for_foobar_helper');

}}

}

public function display($element = 'carts/view', $options) { if (!ClassRegistry::isKeySet('view')) { return; }

if (empty($cartData)) { if (is_a($this->Session, 'SessionHelper') && $this->Session->check('Cart')) { $cartData = $this->Session->read('Cart'); } else { $cartData = $this->requestAction($this->cartRequestUrl); } }

if (empty($cartData)) { trigger_error(__d('cart', 'No cart found.', true), E_USER_NOTICE); } else { // [...] Format the data and add default options (caching...) $options['cartData'] = $cartData; return ClassRegistry::getObject('view')->element($element, $options); }}

USE THE CONFIGURE CLASS•With default values

• Configure::load()

public function __construct($id = false, $table = null, $ds = null) { $userClass = Configure::read('App.UserClass'); if (empty($userClass)) { $userClass = 'User'; } $this->belongsTo['User'] = array( 'className' => $userClass, 'foreignKey' => 'user_id');

// [...]}

CALLBACKS / HOOKS

class StuffableBehavior extends ModelBehavior {

public function doStuff(Model $Model, $id) {if ($Model->isStuffable($id)) {

// [...]if (method_exists($Model, ‘afterStuff’)) {

$Model->afterStuff();}

}}

// Fallback, default logicpublic function isStuffable(Model $Model, $id) {

return true;}

}

HIGHLIGHT ERRORSTrigger errors for the developer

Throw Exceptions for the User

$mandatory = Configure::read('Foo.bar');if (empty($mandatory)) {

trigger_error(‘You must configure your Foobar’, E_USER_ERROR);}

public function doStuff($id) {$Model->id = $id;if (!$Model->exists($id)) {

throw new OutOfBoundsException(__(‘Invalid object’, true));}

}

MAKE MIGRATIONS EASY• Version your code, tag versions (KISS, Extend, Refactor)

•Document API changes between versions

• Use CakeDC’s awesome Migrations plugin!

• Schema updates

• Initial data

• Configuration assistance

... AND ALSO•Write tests

• Use __d(‘myplugin’, ‘This is my text’);

•Document your code

• Provide interfaces to implement (Lib)

•Write tests... really!

WHAT IS MISSING?

NAMESPACES

MyPluginFoobarsController

ClassRegistry downsides

PLUGINS DIRECTORY

CakePackages.com

Reduce plugin duplication

“Diversity is good... with moderation”

PLUGIN MANAGER

Generic installer

Shell

Migrations

METADATA

Implemented “Interface”

Dependencies

Version

QUESTIONS?

@pierremartin http://pierre-martin.fr