よしかわーるど

プログラミングで世界を変える

2018-11-25

CakePHPで簡単にSchedule管理アプリを作る

どうも、よしかわです。

今回は、学校のサークルなどで PHP で Web サービスを作成したいとのことだったので綴っていきます。

コードはこちらから見ることが出来ます.

GitHub:YoshikawaTaiki/web3-programming

目次

対象読者

  • PHP で Web サービスを作りたい人
  • CakePHP フレームワークを使いたい人

データベースの設定

マイグレーション

以下のコマンドをターミナルで実行します.

bin/cake bake migration CreateUsers username:string email:string:unique:EMAIL_INDEX password:string created modified

config/Migrations/CreateUsers.php

<?php
use Migrations\AbstractMigration;

class CreateUsers extends AbstractMigration
{
    /**
     * Change Method.
     *
     * More information on this method is available here:
     * http://docs.phinx.org/en/latest/migrations.html#the-change-method
     * @return void
     */
    public function change()
    {
        $table = $this->table('users');
        $table->addColumn('username', 'string', [
            'default' => null,
            'limit' => 255,
            'null' => false,
        ]);
        $table->addColumn('email', 'string', [
            'default' => null,
            'limit' => 255,
            'null' => false,
        ]);
        $table->addColumn('password', 'string', [
            'default' => null,
            'limit' => 255,
            'null' => false,
        ]);
        $table->addColumn('created', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->addColumn('modified', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->addIndex([
            'email',
        ], [
            'name' => 'EMAIL_INDEX',
            'unique' => true,
        ]);
        $table->create();
    }
}

以下のコマンドをターミナルで実行します.

bin/cake bake migration CreateScehdules user_id:int:foreign title:string description:text schedulememo:string scheduledate:date starttime:time endtime:time created modified

config/Migrations/CreateSchedules.php

<?php
use Migrations\AbstractMigration;

class CreateSchedules extends AbstractMigration
{
    /**
     * Change Method.
     *
     * More information on this method is available here:
     * http://docs.phinx.org/en/latest/migrations.html#the-change-method
     * @return void
     */
    public function change()
    {
        $table = $this->table('schedules');
        $table->addColumn('user_id', 'string', [
            'default' => null,
            'limit' => 255,
            'null' => false,
        ]);
        $table->addColumn('title', 'string', [
            'default' => null,
            'limit' => 255,
            'null' => false,
        ]);
        $table->addColumn('description', 'text', [
            'default' => null,
            'null' => false,
        ]);
        $table->addColumn('schedulememo', 'string', [
            'default' => null,
            'limit' => 255,
            'null' => false,
        ]);
        $table->addColumn('scheduledate', 'date', [
            'default' => null,
            'null' => false,
        ]);
        $table->addColumn('starttime', 'time', [
            'default' => null,
            'null' => false,
        ]);
        $table->addColumn('endtime', 'time', [
            'default' => null,
            'null' => false,
        ]);
        $table->addColumn('created', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->addColumn('modified', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->addIndex([
            'user_id',
        ], [
            'name' => 'BY_USER_ID',
            'unique' => false,
        ]);
        $table->create();
    }
}

Bake で自動生成する

ターミナルで以下のコマンドを実行していきます.

bin/cake bake all users
bin/cake bake all schedules

多分これで,ほとんどのファイルが自動で生成されたはずです.

次にログイン機構などを作成していきます.

ログイン機構を作成

シンプルな認証と認可のアプリケーション

上記を参考にして,作成していきます.

src/Model/Entity/User.php を編集していきます.

<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;
use Cake\Auth\DefaultPasswordHasher;

/**
 * User Entity
 *
 * @property int $id
 * @property string $username
 * @property string $email
 * @property string $password
 * @property \Cake\I18n\FrozenTime $created
 * @property \Cake\I18n\FrozenTime $modified
 *
 * @property \App\Model\Entity\Schedule[] $schedules
 */
class User extends Entity
{

    /**
     * Fields that can be mass assigned using newEntity() or patchEntity().
     *
     * Note that when '*' is set to true, this allows all unspecified fields to
     * be mass assigned. For security purposes, it is advised to set '*' to false
     * (or remove it), and explicitly make individual fields accessible as needed.
     *
     * @var array
     */
    protected $_accessible = [
        'username' => true,
        'email' => true,
        'password' => true,
        'created' => true,
        'modified' => true,
        'schedules' => true
    ];

    /**
     * Fields that are excluded from JSON versions of the entity.
     *
     * @var array
     */
    protected $_hidden = [
        'password'
    ];

    // _setPassword is to set password
    protected function _setPassword($password)
    {
        return (new DefaultPasswordHasher)->hash($password);
    }
}

src/Controller/UsersController.php

<?php
namespace App\Controller;

use App\Controller\AppController;
use Cake\Utility\Security;
use Cake\Event\Event;

/**
 * Users Controller
 *
 * @property \App\Model\Table\UsersTable $Users
 *
 * @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
 */
class UsersController extends AppController
{
    public function beforeFilter(Event $event)
    {
        parent::beforeFilter($event);
        // ユーザーの登録とログアウトを許可します。
        $this->Auth->allow(['add', 'logout']);
    }

    public function login()
    {
        if ($this->request->is('post')) {
            $user = $this->Auth->identify();
            if ($user) {
                $this->Auth->setUser($user);
                return $this->redirect($this->Auth->redirectUrl());
            }
            $this->Flash->error(__('Invalid email or password, try again'));
        }
    }

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

    /**
     * Index method
     *
     * @return \Cake\Http\Response|void
     */
    public function index()
    {
        $users = $this->paginate($this->Users);

        $this->set(compact('users'));
    }

    /**
     * View method
     *
     * @param string|null $id User id.
     * @return \Cake\Http\Response|void
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function view($id = null)
    {
        $user = $this->Users->get($id, [
            'contain' => ['Schedules']
        ]);

        $this->set('user', $user);
    }

    /**
     * Add method
     *
     * @return \Cake\Http\Response|null Redirects on successful add, renders view otherwise.
     */
    public function add()
    {
        $user = $this->Users->newEntity();
        if ($this->request->is('post')) {
            $user = $this->Users->patchEntity($user, $this->request->getData());
            if ($this->Users->save($user)) {
                $this->Flash->success(__('The user has been saved.'));

                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The user could not be saved. Please, try again.'));
        }
        $this->set(compact('user'));
    }

    /**
     * Edit method
     *
     * @param string|null $id User id.
     * @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise.
     * @throws \Cake\Network\Exception\NotFoundException When record not found.
     */
    public function edit($id = null)
    {
        $user = $this->Users->get($id, [
            'contain' => []
        ]);
        if ($this->request->is(['patch', 'post', 'put'])) {
            $user = $this->Users->patchEntity($user, $this->request->getData());
            if ($this->Users->save($user)) {
                $this->Flash->success(__('The user has been saved.'));

                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The user could not be saved. Please, try again.'));
        }
        $this->set(compact('user'));
    }

    /**
     * Delete method
     *
     * @param string|null $id User id.
     * @return \Cake\Http\Response|null Redirects to index.
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function delete($id = null)
    {
        $this->request->allowMethod(['post', 'delete']);
        $user = $this->Users->get($id);
        if ($this->Users->delete($user)) {
            $this->Flash->success(__('The user has been deleted.'));
        } else {
            $this->Flash->error(__('The user could not be deleted. Please, try again.'));
        }

        return $this->redirect(['action' => 'index']);
    }
}

src/Controller/AppController.php

<?php
/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link      https://cakephp.org CakePHP(tm) Project
 * @since     0.2.9
 * @license   https://opensource.org/licenses/mit-license.php MIT License
 */
namespace App\Controller;

use Cake\Controller\Controller;
use Cake\Event\Event;

/**
 * Application Controller
 *
 * Add your application-wide methods in the class below, your controllers
 * will inherit them.
 *
 * @link https://book.cakephp.org/3.0/en/controllers.html#the-app-controller
 */
class AppController extends Controller
{

    /**
     * Initialization hook method.
     *
     * Use this method to add common initialization code like loading components.
     *
     * e.g. `$this->loadComponent('Security');`
     *
     * @return void
     */
    public function initialize()
    {
        parent::initialize();

        $this->loadComponent('RequestHandler', [
            'enableBeforeRedirect' => false,
        ]);
        $this->loadComponent('Flash');

        $this->loadComponent('Auth', [
            'authenticate' => [
                'Form' => [
                    'fields' => [
                        'username' => 'email',
                        'password' => 'password'
                    ]
                ]
            ],
            'loginRedirect' => [
                'controller' => 'Schedules',
                'action' => 'index'
            ],
            'logoutRedirect' => [
                'controller' => 'Users',
                'action' => 'login'
            ]
        ]);

        /*
         * Enable the following component for recommended CakePHP security settings.
         * see https://book.cakephp.org/3.0/en/controllers/components/security.html
         */
        //$this->loadComponent('Security');
    }
}

src/Template/Users/login.ctp

<?php /** * Copyright 2010 - 2017, Cake Development Corporation
(https://www.cakedc.com) * * Licensed under The MIT License * Redistributions of
files must retain the above copyright notice. * * @copyright Copyright 2010 -
2017, Cake Development Corporation (https://www.cakedc.com) * @license MIT
License (http://www.opensource.org/licenses/mit-license.php) */ use
Cake\Core\Configure; ?> <?= $this->Html->css('users') ?> <?=
$this->fetch('head') ?>
<div class="login-form">
  <?= $this->Flash->render('auth') ?> <?= $this->Form->create() ?>
  <fieldset>
    <legend class="form-title"><?= __d('Users', 'Login') ?></legend>
    <?= $this->Form->input('email', array('label' => false, 'required' => true,
    'placeholder' => 'email')) ?> <?= $this->Form->input('password',
    array('label' => false, 'required' => true, 'placeholder' => 'password')) ?>
  </fieldset>
  <?= $this->Form->button(__d('Users', 'Login')); ?> <?= $this->Form->end() ?>
</div>

ログイン画面も作成していきましょう.

src/Template/Users/login.ctp

<?php /** * Copyright 2010 - 2017, Cake Development Corporation
(https://www.cakedc.com) * * Licensed under The MIT License * Redistributions of
files must retain the above copyright notice. * * @copyright Copyright 2010 -
2017, Cake Development Corporation (https://www.cakedc.com) * @license MIT
License (http://www.opensource.org/licenses/mit-license.php) */ use
Cake\Core\Configure; ?> <?= $this->Html->css('users') ?> <?=
$this->fetch('head') ?>

<div class="login-form">
  <?= $this->Flash->render('auth') ?> <?= $this->Form->create() ?>
  <fieldset>
    <legend class="form-title"><?= __d('Users', 'Login') ?></legend>
    <?= $this->Form->input('email', array('label' => false, 'required' => true,
    'placeholder' => 'email')) ?> <?= $this->Form->input('password',
    array('label' => false, 'required' => true, 'placeholder' => 'password')) ?>
  </fieldset>
  <?= $this->Form->button(__d('Users', 'Login')); ?> <?= $this->Form->end() ?>
</div>

スケジュールを見れるようにする

src/Controller/SchedulesController.php

<?php
namespace App\Controller;

use App\Controller\AppController;

/**
 * Schedules Controller
 *
 * @property \App\Model\Table\SchedulesTable $Schedules
 *
 * @method \App\Model\Entity\Schedule[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
 */
class SchedulesController extends AppController
{

    /**
     * Index method
     *
     * @return \Cake\Http\Response|void
     */
    public function index()
    {
        $this->paginate = [
            'contain' => ['Users']
        ];
        $schedules = $this->paginate($this->Schedules);

        $this->set(compact('schedules'));
    }

    /**
     * View method
     *
     * @param string|null $id Schedule id.
     * @return \Cake\Http\Response|void
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function view($id = null)
    {
        $schedule = $this->Schedules->get($id, [
            'contain' => ['Users']
        ]);

        if ($this->Auth->user('id') == $schedule['user_id']) {
            $this->set('schedule', $schedule);
            $this->set('_serialize', ['schedule']);
        } else {
            $this->Flash->error(__('ユーザーIDが違います。同じユーザーIDのみ視聴できます。'));
            return $this->redirect(['action' => 'index']);
        }
    }

    /**
     * Add method
     *
     * @return \Cake\Http\Response|null Redirects on successful add, renders view otherwise.
     */
    public function add()
    {
        $schedule = $this->Schedules->newEntity();
        if ($this->request->is('post')) {
            $schedule = $this->Schedules->patchEntity($schedule, $this->request->getData());
            if ($this->Schedules->save($schedule)) {
                $this->Flash->success(__('The schedule has been saved.'));

                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The schedule could not be saved. Please, try again.'));
        }
        $users = $this->Schedules->Users->find('list', ['limit' => 200]);
        $this->set(compact('schedule', 'users'));
    }

    /**
     * Edit method
     *
     * @param string|null $id Schedule id.
     * @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise.
     * @throws \Cake\Network\Exception\NotFoundException When record not found.
     */
    public function edit($id = null)
    {
        $schedule = $this->Schedules->get($id, [
            'contain' => []
        ]);
        if ($this->request->is(['patch', 'post', 'put'])) {
            $schedule = $this->Schedules->patchEntity($schedule, $this->request->getData());
            if ($this->Schedules->save($schedule)) {
                $this->Flash->success(__('The schedule has been saved.'));

                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The schedule could not be saved. Please, try again.'));
        }
        $users = $this->Schedules->Users->find('list', ['limit' => 200]);
        $this->set(compact('schedule', 'users'));
    }

    /**
     * Delete method
     *
     * @param string|null $id Schedule id.
     * @return \Cake\Http\Response|null Redirects to index.
     * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
     */
    public function delete($id = null)
    {
        $this->request->allowMethod(['post', 'delete']);
        $schedule = $this->Schedules->get($id);
        if ($this->Schedules->delete($schedule)) {
            $this->Flash->success(__('The schedule has been deleted.'));
        } else {
            $this->Flash->error(__('The schedule could not be deleted. Please, try again.'));
        }

        return $this->redirect(['action' => 'index']);
    }

    /**
     * Weekly method
     *
     * @return \Cake\Http\Response|void
     */
    public function weekly()
    {
        $schedules = $this->Schedules->find()->where(["user_id = " => $this->Auth->user('id')])
                                            ->andwhere(["DATE(scheduledate) >= CURDATE()"])
                                            ->andwhere(["DATE(scheduledate) <= DATE(DATE_ADD(CURDATE(), INTERVAL 7 DAY))"]);
        $this->paginate($schedules);
        $this->set(compact('schedules'));
    }

    /**
     * Monthly method
     *
     * @return \Cake\Http\Response|void
     */
    public function monthly()
    {
        $schedules = $this->Schedules->find()->where(["user_id = " => $this->Auth->user('id')])
                                            ->andwhere(["DATE(scheduledate) >= CURDATE()"])
                                            ->andwhere(["DATE(scheduledate) <= DATE(DATE_ADD(CURDATE(), INTERVAL 30 DAY))"]);
        $this->paginate($schedules);
        $this->set(compact('schedules'));
    }
}

src/Template/Schedules/weekly.ctp

<?php /** * @var \App\View\AppView $this * @var
\App\Model\Entity\Schedule[]|\Cake\Collection\CollectionInterface $schedules */
?>
<nav class="large-3 medium-4 columns" id="actions-sidebar">
  <ul class="side-nav">
    <li class="heading"><?= __('Actions') ?></li>
    <li><?= $this->Html->link(__('New Schedule'), ['action' => 'add']) ?></li>
    <li>
      <?= $this->Html->link(__('List Users'), ['controller' => 'Users', 'action'
      => 'index']) ?>
    </li>
    <li>
      <?= $this->Html->link(__('New User'), ['controller' => 'Users', 'action'
      => 'add']) ?>
    </li>
  </ul>
</nav>
<div class="schedules index large-9 medium-8 columns content">
  <h3><?= __('Schedules') ?></h3>
  <table cellpadding="0" cellspacing="0">
    <thead>
      <tr>
        <th scope="col"><?= $this->Paginator->sort('id') ?></th>
        <th scope="col"><?= $this->Paginator->sort('title') ?></th>
        <th scope="col"><?= $this->Paginator->sort('scheduledate') ?></th>
        <th scope="col"><?= $this->Paginator->sort('created') ?></th>
        <th scope="col"><?= $this->Paginator->sort('modified') ?></th>
        <th scope="col" class="actions"><?= __('Actions') ?></th>
      </tr>
    </thead>
    <tbody>
      <?php foreach ($schedules as $schedule): ?>
      <tr>
        <td><?= $this->Number->format($schedule->id) ?></td>
        <td><?= h($schedule->title) ?></td>
        <td><?= h($schedule->scheduledate) ?></td>
        <td><?= h($schedule->created) ?></td>
        <td><?= h($schedule->modified) ?></td>
        <td class="actions">
          <?= $this->Html->link(__('View'), ['action' => 'view', $schedule->id])
          ?> <?= $this->Html->link(__('Edit'), ['action' => 'edit',
          $schedule->id]) ?> <?= $this->Form->postLink(__('Delete'), ['action'
          => 'delete', $schedule->id], ['confirm' => __('Are you sure you want
          to delete # {0}?', $schedule->id)]) ?>
        </td>
      </tr>
      <?php endforeach; ?>
    </tbody>
  </table>
  <div class="paginator">
    <ul class="pagination">
      <?= $this->Paginator->first('<< ' . __('first')) ?> <?=
      $this->Paginator->prev('< ' . __('previous')) ?> <?=
      $this->Paginator->numbers() ?> <?= $this->Paginator->next(__('next') . '
      >') ?> <?= $this->Paginator->last(__('last') . ' >>') ?>
    </ul>
    <p>
      <?= $this->Paginator->counter(['format' => __('Page {{page}} of {{pages}},
      showing {{current}} record(s) out of {{count}} total')]) ?>
    </p>
  </div>
</div>

src/Template/Schedules/monthly.ctp

<?php /** * @var \App\View\AppView $this * @var
\App\Model\Entity\Schedule[]|\Cake\Collection\CollectionInterface $schedules */
?>
<nav class="large-3 medium-4 columns" id="actions-sidebar">
  <ul class="side-nav">
    <li class="heading"><?= __('Actions') ?></li>
    <li><?= $this->Html->link(__('New Schedule'), ['action' => 'add']) ?></li>
    <li>
      <?= $this->Html->link(__('List Users'), ['controller' => 'Users', 'action'
      => 'index']) ?>
    </li>
    <li>
      <?= $this->Html->link(__('New User'), ['controller' => 'Users', 'action'
      => 'add']) ?>
    </li>
  </ul>
</nav>
<div class="schedules index large-9 medium-8 columns content">
  <h3><?= __('Schedules') ?></h3>
  <table cellpadding="0" cellspacing="0">
    <thead>
      <tr>
        <th scope="col"><?= $this->Paginator->sort('id') ?></th>
        <th scope="col"><?= $this->Paginator->sort('title') ?></th>
        <th scope="col"><?= $this->Paginator->sort('scheduledate') ?></th>
        <th scope="col"><?= $this->Paginator->sort('created') ?></th>
        <th scope="col"><?= $this->Paginator->sort('modified') ?></th>
        <th scope="col" class="actions"><?= __('Actions') ?></th>
      </tr>
    </thead>
    <tbody>
      <?php foreach ($schedules as $schedule): ?>
      <tr>
        <td><?= $this->Number->format($schedule->id) ?></td>
        <td><?= h($schedule->title) ?></td>
        <td><?= h($schedule->scheduledate) ?></td>
        <td><?= h($schedule->created) ?></td>
        <td><?= h($schedule->modified) ?></td>
        <td class="actions">
          <?= $this->Html->link(__('View'), ['action' => 'view', $schedule->id])
          ?> <?= $this->Html->link(__('Edit'), ['action' => 'edit',
          $schedule->id]) ?> <?= $this->Form->postLink(__('Delete'), ['action'
          => 'delete', $schedule->id], ['confirm' => __('Are you sure you want
          to delete # {0}?', $schedule->id)]) ?>
        </td>
      </tr>
      <?php endforeach; ?>
    </tbody>
  </table>
  <div class="paginator">
    <ul class="pagination">
      <?= $this->Paginator->first('<< ' . __('first')) ?> <?=
      $this->Paginator->prev('< ' . __('previous')) ?> <?=
      $this->Paginator->numbers() ?> <?= $this->Paginator->next(__('next') . '
      >') ?> <?= $this->Paginator->last(__('last') . ' >>') ?>
    </ul>
    <p>
      <?= $this->Paginator->counter(['format' => __('Page {{page}} of {{pages}},
      showing {{current}} record(s) out of {{count}} total')]) ?>
    </p>
  </div>
</div>

まとめ

今回は,簡単にログイン機構やスケジュール機能を作成しただけなのですが,結構コード量が多いですね…

関連記事

CakePHP を学ぶ 1 日目

CakePHP3 を docker で環境構築