52
CakePHP Workshop Build fast, grow solid.

CakePHP workshop

Embed Size (px)

Citation preview

Page 1: CakePHP workshop

CakePHP WorkshopBuild fast, grow solid.

Page 2: CakePHP workshop

Walther LalkCakePHP core team member

Croogo core team member

Lead Software Developer at

Troop Scouter at 9th Pretoria (Irene) Air Scouts

Husband

Page 3: CakePHP workshop

Development environmentVirtualBox with VagrantRecommended vagrant box is the FriendsOfCake vagrant box.

Download it by running $ git clone https://github.com/FriendsOfCake/vagrant-chef.git

Then start the vagrant box up with$ cd vagrant-chef$ vagrant up

$ vagrant ssh (For windows you’ll probably need to use Putty)

Add this to your hosts file192.168.13.37 app.dev

Linux or OSX : /etc/hostsWindows: C:\Windows\System32\drivers\etc\hosts

PHP built in serverEnsure you have PHP 5.5.9+ with the Intl and Mbstring and a database server running (with the corresponding PDO extension).

Supported:MySQL,PostgreSQL,SQLite,SQL Server

I will be using this with SQLite.

Page 4: CakePHP workshop

Following along at home

https://github.com/dakota/phpsa2016-complete

Page 5: CakePHP workshop

Installing CakePHPRun$ composer create-project cakephp/app app

Then $ cd app

Page 6: CakePHP workshop

If you are using the vagrant box, then visit http://app.dev

in your browser.Otherwise, wait for the next slide

Page 7: CakePHP workshop

PHP ServerRun $ bin/cake server

Visit http://localhost:8765/

Page 8: CakePHP workshop

Getting baked

Page 9: CakePHP workshop

Database configurationIf you’re using the Vagrant box, and have a green tick for database, then you’re good to go.

PHP Server users probably need to do some config. Recommend simply using SQLite (It’s the easiest to get going).

Open config/app.phpFind the Datasources, replace the default datasource with'default' => [ 'className' => 'Cake\Database\Connection', 'driver' => 'Cake\Database\Driver\Sqlite', 'database' => ROOT . DS . 'database.sqlite', 'encoding' => 'utf8', 'cacheMetadata' => true, 'quoteIdentifiers' => false,],

Page 10: CakePHP workshop

Bake a database$ bin/cake bake migration CreateMembers first_name:string last_name:string email:string created modified $ bin/cake bake migration CreateEvents title:string description:text start:datetime end:datetime created modified $ bin/cake bake migration CreateEventsMembers event_id:integer:primary member_id:integer:primary

Page 11: CakePHP workshop

Bake a databaseThe migration bake task can sometimes get confused with composite primary keys, and tries to make each primary key an autoincrement field. We need to fix that!

Open config/Migrations/…_CreateEventsMembers.php and remove the two ‘autoIncrement’ => true, lines.

If you are using SQLite, you need to open all the files in the config/Migrations, and change all instances of ‘default’ => null to be ‘default’ => ‘’

Page 12: CakePHP workshop

Seed that database$ bin/cake bake seed Members

Open config/Seeds/MembersSeed.php, and update the $data variable

$data = [ [ 'first_name' => 'Walther', 'last_name' => 'Lalk', 'email' => '[email protected]', 'created' => date('Y-m-d H:i:s'), 'modified' => date('Y-m-d H:i:s'), ]];

Page 13: CakePHP workshop

Seed that database$ bin/cake bake seed Events

Open config/Seeds/EventsSeed.php, and update the $data variable

$data = [ [ 'title' => 'PHP South Africa 2016', 'description' => '', 'start' => '2016-09-28 08:00', 'end' => '2016-09-30 17:00', 'created' => date('Y-m-d H:i:s'), 'modified' => date('Y-m-d H:i:s'), ]];

Page 14: CakePHP workshop

Running migrations$ bin/cake migrations migrate$ bin/cake migrations seed

Page 15: CakePHP workshop

Bake your application$ bin/cake bake all Members$ bin/cake bake all Events

Visit /members in your browser, there should be 1 member. Add another one.

Now visit /events in your browser, there should be 1 event. Add another one.

Notice how both of them have a multi-select box for members or events? We’re going to remove that at a later stage and change how it works.

Page 16: CakePHP workshop

Anatomy of a CakePHP application

Page 17: CakePHP workshop

Marshalling

Page 18: CakePHP workshop

Persisting

Page 19: CakePHP workshop

ValidationOpen src/Model/Table/EventsTable.php, find in the validationDefault method.

Change notEmpty() to be allowEmpty()

$validator ->requirePresence('description', 'create') ->notEmpty('description');

Page 20: CakePHP workshop

Application rulespublic function buildRules(RulesChecker $rules){ $rules->add( function (Event $event) { return $event->start <= $event->end; }, 'endAfterStart', [ 'errorField' => 'end', 'message' => 'The event cannot end before it has started' ] );

return parent::buildRules($rules);}

Page 21: CakePHP workshop

The art of knowing who’s there

Page 22: CakePHP workshop

Adding a password$ bin/cake bake migration AddPasswordToMembers password:string[60]$ bin/cake migrations migrate

Add to src/Model/Entity/MemberEntity.php

protected function _setPassword($password){ if (strlen($password) > 0) { return (new \Cake\Auth\DefaultPasswordHasher())->hash($password); }

return $this->password;}

Page 23: CakePHP workshop

Adding a passwordAdd to src/Templates/Members/add.ctp and src/Templates/Members/edit.ctpecho $this->Form->input('password', [ 'value' => '']);echo $this->Form->input('password_confirm', [ 'label' => 'Confirm password', ’type' => 'password', 'value' => '']);

Page 24: CakePHP workshop

Adding a passwordAdd validation to the password, and password confirmation fields.$validator ->requirePresence('password', 'create') ->notEmpty('password', 'You need to provide a password.', 'create') ->minLength('password', 6, 'Your password must be 6 characters or longer');

$condition = function ($context) { return !empty($context['data']['password']);};$validator ->requirePresence('password_confirm', $condition) ->notEmpty('password_confirm', 'Please confirm your password', $condition) ->add('password_confirm', 'mustMatch', [ 'rule' => function ($check, $context) { return $context['data']['password'] === $check; }, 'on' => $condition, 'message' => 'Password does not match' ]);

Page 25: CakePHP workshop

Adding a passwordEdit your existing members and give them passwords. They will be automatically hashed.

Page 26: CakePHP workshop

Creating the login actionOpen src/Controller/MembersController.phppublic function login(){ if ($this->request->is('post')) { $member = $this->Auth->identify(); if ($member) { $this->Auth->setUser($member);

return $this->redirect($this->Auth->redirectUrl()); } else { $this->Flash->error(__('Email address or password is incorrect'), [ 'key' => 'auth' ]); } }}

public function logout(){ return $this->redirect($this->Auth->logout());}

Page 27: CakePHP workshop

Creating the login templateCreate src/Templates/Members/login.ctp

<div class="login form large-12 columns content"> <?php echo $this->Form->create(); echo $this->Form->input('email'); echo $this->Form->input('password'); echo $this->Form->button('Login'); echo $this->Form->end(); ?></div>

Page 28: CakePHP workshop

Enabling AuthenticationOpen src/Controller/AppController.php, and add to the initialize method.$this->loadComponent('Auth', [ 'loginAction' => [ 'controller' => 'Members', 'action' => 'login', ], 'authenticate' => [ 'Form' => [ 'fields' => ['username' => 'email', 'password' => 'password'], 'userModel' => 'Members' ] ]]);

Try to visit /members now

Page 29: CakePHP workshop

Event securityWe are adding a organiser to our events. Only the organiser is allowed to change the event.

$ bin/cake bake migration AddOrganiserToEvents organiser_id:integer$ bin/cake migrations migrate

$this->belongsTo('Organiser', [ 'foreignKey' => 'organiser_id', 'className' => 'Members']);

Open src/Model/Table/EventsTable.php, and add to the initialize method.

Open src/Model/Table/MembersTable.php, and add to the initialize method.$this->hasMany('OrganisingEvents', [ 'foreignKey' => 'organiser_id', 'className' => 'Events']);

Page 30: CakePHP workshop

Event securityEnforce foreign key constraints at an application level by adding the following to the buildRules method in the EventsTable class.$rules->existsIn('organiser_id', 'Organiser', 'That member does not exist in the database.');

Page 31: CakePHP workshop

Who’s the organiser?The member creating the event should be able to choose who the event organiser is.

In src/Templates/Events/add.ctpecho $this->Form->input('organiser_id', [ 'empty' => '-- Select organiser --', 'options' => $members, 'default' => $this->request->session()->read('Auth.User.id')]);

In src/Templates/Events/edit.ctpecho $this->Form->input('organiser_id', [ 'empty' => '-- Select organiser --', 'options' => $members,]);

Page 32: CakePHP workshop

Who’s that?Who is organiser 1?

protected $_virtual = [ 'full_name'];

protected function _getFullName(){ return sprintf('%s %s (%s)', $this->first_name, $this->last_name, $this->email);}

Add to the Member entity class

In the MembersTable class, change the displayField to ‘full_name’

Better!

Page 33: CakePHP workshop

Event securityTo enforce that an event can only be modified by it’s organiser, open src/Controller/EventsController.php and add to the edit method, just after the get() call.if ($event->organiser_id !== $this->Auth->user('id')) { throw new ForbiddenException();}

Page 34: CakePHP workshop

Member securityTo prevent a member from editing another member’s profile, simply addif ($id !== $this->Auth->user('id')) { throw new ForbiddenException();}

To the edit method in the MembersController.

Page 35: CakePHP workshop

Allow registrationsAllow new members to register by adding a beforeFilter method to the MembersController.public function beforeFilter(\Cake\Event\Event $event){ $this->Auth->allow('add');

return parent::beforeFilter($event);}

Add make a pretty URL for registration and login. Add to config/routes.php$routes->connect('/register', ['controller' => 'Members', 'action' => 'add']);$routes->connect('/login', ['controller' => 'Members', 'action' => 'login']);

Page 36: CakePHP workshop

I’m going

Page 37: CakePHP workshop

Belongs to manyCreate a join table object$ bin/cake bake model EventsMembers

Change'joinTable' => 'events_members'

to'through' => 'EventsMembers','saveStrategy' => \Cake\ORM\Association\BelongsToMany::SAVE_APPEND

in the Members and Events table objects.

Page 38: CakePHP workshop

Event attendanceAdd a field to capture the type of event attendance

$ bin/cake bake migration AddTypeToEventsMembers type:string[10]$ bin/cake migrations migrate

Add to EventsMember entityconst TYPE_GOING = 'going';const TYPE_INTERESTED = 'interested';const TYPE_NOT_GOING = 'notGoing';

Page 39: CakePHP workshop

Event attendanceIn EventsTable, addpublic function linkMember(\App\Model\Entity\Event $event, $memberId, $type){ $member = $this->Members->get($memberId); //Add the join data $member->_joinData = new \App\Model\Entity\EventsMember([ 'type' => $type ]);

return $this->association('Members')->link($event, [$member]);}

Page 40: CakePHP workshop

Event attendanceIn EventsController, addpublic function linkActiveMember($eventId, $type){ $event = $this->Events->get($eventId); if ($this->Events->linkMember($event, $this->Auth->user('id'), $type)) { $this->Flash->success('Registered!'); } else { $this->Flash->error('Something went wrong.'); }

return $this->redirect($this->referer());}

Page 41: CakePHP workshop

Event attendanceIn the Event entitypublic function memberStatus($memberId, $type){ if (!$this->has('members')) { return false; }

$member = collection($this->members) ->firstMatch(['id' => $memberId]);

if (!$member) { return false; }

return $member->_joinData->type === $type;}

Page 42: CakePHP workshop

Event attendanceIn src/Template/Events/index.ctp, in the actions table cell<br><?phpif (!$event->memberStatus($this->request->session() ->read('Auth.User.id'), \App\Model\Entity\EventsMember::TYPE_GOING)) { echo $this->Html->link(__('Going'), [ 'action' => 'linkActiveMember', $event->id, \App\Model\Entity\EventsMember::TYPE_GOING ]);}if (!$event->memberStatus($this->request->session() ->read('Auth.User.id'), \App\Model\Entity\EventsMember::TYPE_INTERESTED)) { echo $this->Html->link(__('Interested'), [ 'action' => 'linkActiveMember', $event->id, \App\Model\Entity\EventsMember::TYPE_INTERESTED ]);}if (!$event->memberStatus($this->request->session() ->read('Auth.User.id'), \App\Model\Entity\EventsMember::TYPE_NOT_GOING)) { echo $this->Html->link(__('Not going'), [ 'action' => 'linkActiveMember', $event->id, \App\Model\Entity\EventsMember::TYPE_NOT_GOING ]);}?>

Page 43: CakePHP workshop

Event attendanceOpen src/Templates/Events/add.ctp and src/Templates/Events/edit.ctp and removeecho $this->Form->input('members._ids', ['options' => $members]);

Open src/Templates/Members/add.ctp and src/Templates/Members/edit.ctp and remove

echo $this->Form->input('events._ids', ['options' => $events]);

Page 44: CakePHP workshop

We don’t need code where we’re going

Page 45: CakePHP workshop

Installing CRUD$ composer require friendsofcake/crud$ bin/cake plugin load Crud

use \Crud\Controller\ControllerTrait;

Add the Crud trait to your AppController

And, load the Crud component in the initialize method$this->loadComponent('Crud.Crud', [ 'actions' => [ 'Crud.Index', 'Crud.View', 'Crud.Edit', 'Crud.Add', 'Crud.Delete' ], 'listeners' => [ 'Crud.RelatedModels' ]]);

Page 46: CakePHP workshop

Remove all your codeOr, at least all index, view, edit, add and delete actions.

Page 47: CakePHP workshop

It still works!?Thanks to the power of CRUD, everything still works

(Mostly)

Page 48: CakePHP workshop

Adding back functionalityCreate a AuthListener class in the src/Listener directory<?php

namespace App\Listener;

use Cake\Event\Event;use Cake\Network\Exception\ForbiddenException;use Crud\Listener\BaseListener;

/** * Class AuthListener */class AuthListener extends BaseListener{ /** * Settings * * @var array */ protected $_defaultConfig = [ 'property' => 'id', 'message' => 'You are not allowed to access the requested resource.', 'actions' => ['edit', 'delete'] ];

Page 49: CakePHP workshop

Adding back functionality public function afterFind(Event $event) { if (!in_array($this->_request()->param('action'), $this->config('actions')) ) { return; }

$entity = $event->subject()->entity; $userId = $this->_controller()->Auth->user('id');

if ($entity->get($this->config('property')) !== $userId) { throw new ForbiddenException($this->config('message')); } }}

Page 50: CakePHP workshop

Adding back functionalityMembersController beforeFilter, add:$this->Crud->addListener('Auth', 'Auth', [ 'property' => 'id']);

Add a beforeFilter for EventsControllerpublic function beforeFilter(\Cake\Event\Event $event){ $this->Crud->addListener('Auth', 'Auth', [ 'property' => 'organiser_id' ]);

return parent::beforeFilter($event);}

Page 51: CakePHP workshop

Questions?

Page 52: CakePHP workshop

Thank You.