Top Banner
CakePHP Workshop Build fast, grow solid.
52

CakePHP workshop

Jan 14, 2017

Download

Education

Walther Lalk
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
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.