CakePHP

トップ > PHP入門 > CakePHP

目次

広告

概要

CakePHP1.3の導入&アプリケーション作成メモ。

で躓いたので、それらの導入までをメモしておきます。基本的に自分用のメモなので、詳細はその都度解説サイトや書籍などを参照することをお勧めします。

自分で色々調べつつ書いているメモなので、見当違いな事を書いてある可能性があります。

導入

設置

公式サイトからCakePHP1.3をダウンロードします。

入手したファイルを適当なディレクトリにコピーします。今回は、公開フォルダ直下に cakephp フォルダを作成し、その中に各フォルダとファイルを格納するものとします。URLは http://localhost/cakephp/ でアクセスできるものとします。

実際に運営するときは、/cakephp/app/webroot/ をドキュメントルートにするか、CakePHPの設定を変更して webroot/ の上位階層にアクセスできないようにします。

設定

mod_rewriteが利用できない環境の場合、/cakephp/app/config/core.php にある

//Configure::write('App.baseUrl', env('SCRIPT_NAME'));

のコメントを解除しておきます。そうすると、index.php/xxx/yyy/zzz のようなURLでアクセスできるようになります。

adminルーティングを利用する場合、/cakephp/app/config/core.php にある

//Configure::write('Routing.prefixes', array('admin'));

のコメントも解除しておきます。そうすると、admin/xxx のようなURLで管理ページを作成できるようになります。

/cakephp/app/config/core.php にある

Configure::write('Security.level', 'medium');

この値を high に変更しておきます。これでブラウザを閉じるとセッションが消去されます。(セキュリティ対策。)

/cakephp/app/config/core.php にある

Configure::write('Security.salt', 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi');

の値を適当な値に変更しておきます。セキュリティ対策上必須。

さらに、同ファイルにある

Configure::write('Security.cipherSeed', '76859309657453542496749683645');

の値も適当な値に変更しておきます。これもセキュリティ対策上必須。

/cakephp/app/tmp/ とその中にあるすべてのディレクトリのパーミッションを 707 に設定します。

/cakephp/app/config/database.php.default/cakephp/app/config/database.php にリネームします。これがデータベースの設定ファイルになります。

/cakephp/app/config/database.php にある DATABASE_CONFIG クラスの内容を自分の環境にあわせて設定します。日本語を扱うために、以下の設定も追加しておきます。

'encoding' => 'utf8'

データベースはあらかじめ作成しておきます。

メモ管理ツールを作成

まずはメモ管理ツールを作成します。通常のURLからは表示のみでき、管理用のURLからは登録・編集・削除ができるようにします。

この内容が解らなければ、公式のマニュアルブログチュートリアルを参照してください。同じコードではありませんが、処理内容は大体同じです。

あらかじめデータベースに、メモ管理用テーブルを作成します。

CREATE TABLE memos(
  id       INT UNSIGNED NOT NULL AUTO_INCREMENT,
  created  DATETIME NOT NULL,
  modified DATETIME NOT NULL,
  title    VARCHAR(255) NOT NULL,
  content  TEXT,
  PRIMARY KEY(id)
);

このテーブルには、以下のコードでデータの登録ができます。

INSERT INTO memos(id, created, modified, title, content) VALUES(NULL, NOW(), NOW(), 'title', 'content');

/cakephp/app/models/memo.php を作成し、以下の内容を記述します。ファイルの文字コードは UTF-8N です。(以降のファイルもすべて。)

<?php

class Memo extends AppModel
{
  public $validate = array(
    'title' => array(
      'notEmpty' => array(
        'rule'    => 'notEmpty',
        'message' => 'タイトルの入力は必須です。'
      ),
      'maxLength' => array(
        'rule'    => array('maxLength', 80),
        'message' => 'タイトルは80文字以内で入力してください。'
      )
    ),
    'content' => array(
      'notEmpty' => array(
        'rule'    => 'notEmpty',
        'message' => '詳細の入力は必須です。'
      ),
      'maxLength' => array(
        'rule'    => array('maxLength', 1000),
        'message' => '詳細は1000文字以内で入力してください。'
      )
    )
  );
}

?>

/cakephp/app/controllers/memos_controller.php を作成し、以下の内容を記述します。

<?php

class MemosController extends AppController
{
  /* フィルター */
  public function beforeFilter()
  {
    if (isset($this->params['admin'])) {
      $this->layout = 'admin';
    }
  }

  /* メモ一覧 */
  public function index()
  {
    $this->set('memos', $this->Memo->find('all'));

    $this->set('title_for_layout', 'メモ一覧');
  }

  /* メモ表示 */
  public function view($id = null)
  {
    $this->Memo->id = $id;
    $this->set('memo', $this->Memo->read());

    $this->set('title_for_layout', 'メモ表示');
  }

  /* 管理者用 | メモ一覧 */
  public function admin_index()
  {
    $this->set('memos', $this->Memo->find('all'));

    $this->set('title_for_layout', 'メモ一覧');
  }

  /* 管理者用 | メモ追加 */
  public function admin_add()
  {
    if (!empty($this->data)) {
      if ($this->Memo->save($this->data['Memo'])) {
        $this->redirect('/admin/memos/index?exec=add');
      }
    }

    $this->set('title_for_layout', 'メモ追加');

    $this->render('admin_form');
  }

  /* 管理者用 | メモ更新 */
  public function admin_edit($id = null)
  {
    if (empty($this->data)) {
      $this->Memo->id = $id;
      $this->data = $this->Memo->read();
    } else {
      if ($this->Memo->save($this->data['Memo'])) {
        $this->redirect('/admin/memos/index?exec=edit');
      }
    }

    $this->set('title_for_layout', 'メモ更新');

    $this->render('admin_form');
  }

  /* 管理者用 | メモ削除 */
  public function admin_delete($id)
  {
    if ($this->Memo->delete($id)) {
      $this->redirect('/admin/memos/index?exec=delete');
    }
  }
}

?>

/cakephp/app/views/memos/index.ctp を作成し、以下の内容を記述します。

<table summary="メモ">
  <tr>
    <th>記事ID</th>
    <th>タイトル</th>
    <th>作成日</th>
    <th>修正日</th>
    <th>処理</th>
  </tr>
  <?php foreach ($memos as $memo) : ?>
  <tr>
    <td><?php echo $memo['Memo']['id'] ?></td>
    <td><?php echo $memo['Memo']['title'] ?></td>
    <td><?php echo $memo['Memo']['created'] ?></td>
    <td><?php echo $memo['Memo']['modified'] ?></td>
    <td><?php echo $html->link('詳細', '/memos/view/' . $memo['Memo']['id']) ?></td>
  </tr>
  <?php endforeach ?>
</table>

<ul>
  <li><?php echo $html->link('管理ページ', '/admin/memos') ?></li>
</ul>

/cakephp/app/views/memos/view.ctp を作成し、以下の内容を記述します。

<table summary="メモ">
  <tr>
    <th>記事ID</th>
    <td><?php echo $memo['Memo']['id'] ?></td>
  </tr>
  <tr>
    <th>タイトル</th>
    <td><?php echo $memo['Memo']['title'] ?></td>
  </tr>
  <tr>
    <th>詳細</th>
    <td><?php echo nl2br($memo['Memo']['content']) ?></td>
  </tr>
  <tr>
    <th>作成日</th>
    <td><?php echo $memo['Memo']['created'] ?></td>
  </tr>
  <tr>
    <th>修正日</th>
    <td><?php echo $memo['Memo']['modified'] ?></td>
  </tr>
</table>

/cakephp/app/views/memos/admin_index.ctp を作成し、以下の内容を記述します。

<?php

if (isset($this->params['url']['exec'])) {
  $exec = $this->params['url']['exec'];

  if ($exec == 'add') {
    $message = 'メモを追加しました。';
  } elseif ($exec == 'edit') {
    $message = 'メモを更新しました。';
  } elseif ($exec == 'delete') {
    $message = 'メモを削除しました。';
  }
}

?>
<?php if (isset($message)) : ?>
<p><?php echo $message ?></p>
<?php endif ?>

<table summary="メモ">
  <tr>
    <th>記事ID</th>
    <th>タイトル</th>
    <th>作成日</th>
    <th>修正日</th>
    <th>処理</th>
  </tr>
  <?php foreach ($memos as $memo) : ?>
  <tr>
    <td><?php echo $memo['Memo']['id'] ?></td>
    <td><?php echo $memo['Memo']['title'] ?></td>
    <td><?php echo $memo['Memo']['created'] ?></td>
    <td><?php echo $memo['Memo']['modified'] ?></td>
    <td>
      <?php echo $html->link('編集', '/admin/memos/edit/' . $memo['Memo']['id']) ?>
      <?php echo $html->link('削除', '/admin/memos/delete/' . $memo['Memo']['id'], null, '記事を削除してもよろしいですか?') ?>
    </td>
  </tr>
  <?php endforeach ?>
</table>

<ul>
  <li><?php echo $html->link('新規投稿', '/admin/memos/add') ?></li>
</ul>

/cakephp/app/views/memos/admin_form.ctp を作成し、以下の内容を記述します。

<?php

$options = array(
  'action' => $this->action,
  'type'   => 'post',
  'inputDefaults' => array(
    'legend' => false,
    'label'  => false,
    'div'    => false
  )
);

?>
<?php echo $form->create('Memo', $options) ?>
  <fieldset>
    <legend>メモ登録フォーム</legend>
    <?php echo $form->hidden('Memo.id') ?>
    <dl>
      <dt><?php echo $form->label('Memo.title', 'タイトル') ?></dt>
        <dd>
          <?php echo $form->input('Memo.title', array('size' => 50, 'error' => false)) ?>
          <?php echo $form->error('Memo.title') ?>
        </dd>
      <dt><?php echo $form->label('Memo.content', '詳細') ?></dt>
        <dd>
          <?php echo $form->textarea('Memo.content', array('cols' => 50, 'rows' => 10)) ?>
          <?php echo $form->error('Memo.content') ?>
        </dd>
    </dl>
    <p><?php echo $form->submit('登録する', array('div' => false)) ?></p>
  </fieldset>
<?php echo $form->end() ?>
<ul>
  <li><?php echo $html->link('記事一覧', '/admin/memos/index') ?></li>
</ul>

/cakephp/app/views/layouts/default.ctp を作成し、以下の内容を記述します。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title><?php echo $this->action == 'index' ? '' : $title_for_layout . ' | ' ?>Sample</title>
    <link rel="stylesheet" href="<?php echo $this->webroot ?>css/common.css" />
  </head>
  <body>
    <header>
      <h1>Sample</h1>
    </header>
    <h2><?php echo $title_for_layout ?></h2>
    <?php echo $content_for_layout ?>
    <footer>
      <p>CakePHP : Rapid Development Framework</p>
    </footer>
  </body>
</html>

/cakephp/app/views/layouts/admin.ctp を作成し、以下の内容を記述します。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title><?php echo $this->action == 'index' ? '' : $title_for_layout . ' | ' ?>管理ページ | Sample</title>
    <link rel="stylesheet" href="<?php echo $this->webroot ?>css/common.css" />
  </head>
  <body>
    <header>
      <h1>Admin</h1>
    </header>
    <h2><?php echo $title_for_layout ?></h2>
    <?php echo $content_for_layout ?>
    <footer>
      <p>CakePHP : Rapid Development Framework</p>
    </footer>
  </body>
</html>

/cakephp/app/webroot/css/common.css を作成し、適当な内容を記述します。

ブラウザから以下のURLにアクセスすると、メモの表示・登録・編集・削除ができます。

改ページの導入

改ページは自前で実装してもそれほど難しいものではないですが、Paginatorを利用すると簡単に実装できます。さらに、項目ごとのソート機能も提供されます。

/cakephp/app/controllers/memos_controller.php

class MemosController extends AppController
{

この直後に、以下の処理を追加します。今回は1ページに10件ずつ表示・表示順はIDの降順としています。

public $paginate = array(
  'limit' => 10,
  'order' => array(
    'Memo.id' => 'desc'
  )
);

さらに、2箇所ある

$this->Memo->find('all')

この部分を、以下のように修正します。これで、$paginate の設定内容に従ってデータを取得できます。

$this->paginate('Memo')

/cakephp/app/views/memos/index.ctp/cakephp/app/views/memos/admin_index.ctp に、以下の内容を追加します。

<p><?php echo $paginator->counter('全%pages%ページ中、%pages%ページ目を表示しています。') ?></p>
<ul>
  <li><?php echo $paginator->prev('前のページ') ?></li>
  <li><?php echo $paginator->next('次のページ') ?></li>
</ul>
<p><?php echo $paginator->numbers() ?></p>

以上で完了ですが、/cakephp/app/views/memos/admin_index.ctp/cakephp/app/views/memos/index.ctp にある

<tr>
  <th>記事ID</th>
  <th>タイトル</th>
  <th>作成日</th>
  <th>修正日</th>
  <th>処理</th>
</tr>

この部分を以下のように変更すると、各項目ごとにソートできるようになります。

<tr>
  <th><?php echo $paginator->sort('記事ID', 'Memo.id') ?></th>
  <th><?php echo $paginator->sort('タイトル', 'Memo.title') ?></th>
  <th><?php echo $paginator->sort('作成日', 'Memo.created') ?></th>
  <th><?php echo $paginator->sort('修正日', 'Memo.modified') ?></th>
  <th>処理</th>
</tr>

参考ページ。

ログイン機能の導入

管理ページへのアクセスに認証を必須にします。ログイン機能はAuthコンポーネントを利用すると簡単に実装できます。

あらかじめデータベースに、ユーザー管理用テーブルを作成します。

CREATE TABLE users(
  id       INT UNSIGNED NOT NULL AUTO_INCREMENT,
  created  DATETIME NOT NULL,
  modified DATETIME NOT NULL,
  username VARCHAR(80) NOT NULL,
  password VARCHAR(40) NOT NULL,
  PRIMARY KEY(id)
);

このテーブルには、以下のコードでデータの登録ができます。

INSERT INTO users(id, created, modified, username, password) VALUES(NULL, NOW(), NOW(), 'ユーザー名', '暗号化したパスワード');

「暗号化したパスワード」は、Authコンポーネントのpasswordメソッドで求めることができます。一例ですが、/cakephp/app/controllers/memos_controller.php にある

/* メモ一覧 */
public function index()
{

この直後に以下のコードを追加すると、暗号化されたパスワードを表示できます。

exit($this->Auth->password('暗号化したいパスワード'));

これでメモの一覧ページにアクセスすると暗号化されたパスワードが表示されるので、この値をデータベースに登録します。登録後、上のコードは削除しておきます。

/cakephp/app/models/user.php を作成し、以下の内容を記述します。

<?php

class User extends AppModel
{
}

?>

/cakephp/app/controllers/users_controller.php を作成し、以下の内容を記述します。ログイン処理やログアウト処理は、Authコンポーネントが行ってくれます。

<?php

class UsersController extends AppController
{
  public $components = array('Auth');

  /* フィルター */
  public function beforeFilter()
  {
    $this->Auth->loginError = 'ユーザー名もしくはパスワードが違います。';
    $this->Auth->loginRedirect = 'admin/memos/index';
  }

  /* ログイン */
  public function login()
  {
    $this->set('title_for_layout', 'ログイン');
  }
  public function admin_login()
  {
    $this->render('login');
  }

  /* ログアウト */
  public function logout()
  {
    $this->redirect($this->Auth->logout());
  }
  public function admin_logout()
  {
  }
}

?>

/cakephp/app/controllers/memos_controller.php

class MemosController extends AppController
{

この直後に、以下の処理を追加します。

public $components = array('Auth');

さらに、

/* フィルター */
public function beforeFilter()
{
  if (isset($this->params['admin'])) {
    $this->layout = 'admin';
  }
}

この部分を、以下のように変更します。

/* フィルター */
public function beforeFilter()
{
  if (isset($this->params['admin'])) {
    $this->Auth->authError = 'このページへのアクセスは認証が必要です。';
    $this->set('username', $this->Auth->user('username'));

    $this->layout = 'admin';
  } else {
    $this->Auth->allow($this->params['action']);
  }
}

/cakephp/app/views/users/login.ctp を作成し、以下の内容を記述します。

<?php

$options = array(
  'action' => 'login',
  'type'   => 'post',
  'inputDefaults' => array(
    'legend' => false,
    'label'  => false,
    'div'    => false
  )
);

?>
<ul>
  <?php if ($session->check('Message.auth')) : ?>
  <li><?php echo $session->flash('auth') ?></li>
  <?php else : ?>
  <li>ユーザー名とパスワードを入力してください。</li>
  <?php endif ?>
</ul>
<?php echo $form->create('User', $options) ?>
  <fieldset>
    <legend>認証フォーム</legend>
    <dl>
      <dt><?php echo $form->label('User.username', 'ユーザー名') ?></dt>
        <dd><?php echo $form->input('User.username', array('size' => 50, 'error' => false)) ?></dd>
      <dt><?php echo $form->label('User.password', 'パスワード') ?></dt>
        <dd><?php echo $form->input('User.password', array('size' => 50, 'error' => false)) ?></dd>
    </dl>
    <p><?php echo $form->submit('認証する', array('div' => false)) ?></p>
  </fieldset>
<?php echo $form->end() ?>

/cakephp/app/views/memos/index.ctp にある

<ul>
  <li><?php echo $html->link('管理ページ', '/admin/memos') ?></li>
</ul>

この部分を以下のように変更します。

<ul>
  <li><?php echo $html->link('管理ページ', '/users/login') ?></li>
</ul>

/cakephp/app/views/layouts/admin.ctp の任意の場所に、以下の内容を追加します。

<ul>
  <li>現在 <em><?php echo $username ?></em> としてログインしています。</li>
  <li><?php echo $html->link('ログアウト', '/users/logout') ?></li>
</ul>

参考ページ。

ログイン状態保持機能の導入

ログインの手間を省くため、ログイン時に「ログイン状態を記憶する」にチェックを入れると一定期間ログイン状態を保持するようにします。

ログイン状態の保持はセッションの有効期間を長く設定すれば可能ではありますが、余計な情報も長期間保持されてしまいます。(セキュリティ上好ましくありません。)

そこで、

という方針でログイン状態の保持を実装してみます。

あらかじめデータベースに、パスポート管理用テーブルを作成します。

CREATE TABLE passports(
  id       INT UNSIGNED NOT NULL AUTO_INCREMENT,
  user_id  INT NOT NULL,
  created  DATETIME NOT NULL,
  modified DATETIME NOT NULL,
  passport VARCHAR(40) NOT NULL UNIQUE,
  PRIMARY KEY(id)
);

/cakephp/app/models/passport.php を作成し、以下の内容を記述します。

<?php

class Passport extends AppModel
{
  public $belongsTo = array(
    'User' => array(
      'foreignKey' => 'user_id'
    )
  );
}

?>

/cakephp/app/controllers/users_controller.php の内容を、以下のように変更します。

<?php

class UsersController extends AppController
{
  public $components = array('Auth', 'Cookie');
  public $uses = array('Passport');
  public $expires = 1209600; //60 * 60 * 24 * 14

  /* フィルター */
  public function beforeFilter()
  {
    $this->Auth->loginError = 'ユーザー名もしくはパスワードが違います。';
    $this->Auth->loginRedirect = 'admin/memos/index';
    $this->Auth->autoRedirect = false;
  }

  /* ログイン */
  public function login()
  {
    $user = $this->Auth->user();

    if (!empty($this->data)) {
      //パスポートを保存するか
      if ($this->data['User']['remember_me']) {
        $this->passportWrite($user);
      } else {
        $this->passportDelete($user);
      }
      unset($this->data['User']['remember_me']);
    }

    if ($user) {
      $this->redirect($this->Auth->redirect());
    } else {
      $cookie_user = $this->Cookie->Read('User');

      if ($cookie_user) {
        //パスポートでログイン
        $options = array(
          'conditions' => array(
            'Passport.passport'   => $cookie_user['passport'],
            'Passport.modified >' => date('Y-m-d H:i:s', $this->expires)
          )
        );
        $passport = $this->Passport->find('first', $options);

        if ($passport){
          $user['username'] = $passport['User']['username'];
          $user['password'] = $passport['User']['password'];

          if ($this->Auth->login($user)) {
            $this->passportWrite($passport);
            $this->redirect($this->Auth->redirect());
          }
        }
      }
    }

    $this->set('title_for_layout', 'ログイン');
  }
  public function admin_login()
  {
    $this->render('login');
  }

  /* ログアウト */
  public function logout()
  {
    $user = $this->Auth->user();
    $this->passportDelete($user);
    $this->redirect($this->Auth->logout());
  }
  public function admin_logout()
  {
  }

  /* パスポート発行 */
  private function passportWrite($user)
  {
    $passport = array();
    $passport['user_id'] = $user['User']['id'];
    $passport['passport'] = Security::generateAuthKey();

    if (isset($user['Passport']['id'])) {
      $passport['id'] = $user['Passport']['id'];
    }
    $this->Passport->save($passport);

    $cookie = array(
      'passport' => $passport['passport']
    );
    $this->Cookie->write('User', $cookie, true, $this->expires);
  }

  /* パスポート削除 */
  private function passportDelete($user)
  {
    $this->Cookie->delete('User');

    $conditions = array(
      'Passport.user_id' => $user['User']['id']
    );
    $this->Passport->deleteAll($conditions);
  }
}

?>

/cakephp/app/views/users/login.ctp に、以下の内容を追加します。

<ul>
  <li><?php echo $form->checkbox('User.remember_me') ?><?php echo $form->label('User.remember_me', 'ログイン状態を記憶する') ?></li>
</ul>

参考ページ。

ワンタイムトークン(CSRF対策)の導入

CSRF対策に、ワンタイムトークンを導入します。

CakePHPではSecurityコンポーネントでワンタイムトークンを実装できますが、POST経由での投稿しかチェックしないので使い勝手がよくありません。(GET経由はチェックされない。)

そこで、

という方針で実装してみます。

/cakephp/app/controllers/memos_controller.php

public $components = array('Auth');

この部分を、以下のように変更します。

public $components = array('Auth', 'Security');

これだけでトークンが発行され、フォームにトークン送信用のタグが追加されます。ただ、上に書いたようにトークンのチェック処理は自分で実装するので、さらに

/* 管理者用 | メモ追加 */
public function admin_add()
{
  if (!empty($this->data)) {
/* 管理者用 | メモ更新 */
public function admin_edit($id = null)
{
  if (empty($this->data)) {
    $this->Memo->id = $id;
    $this->data = $this->Memo->read();
  } else {

これらの直後に、以下の処理を追加します。

if ($this->data['_Token']['key'] != $this->params['_Token']['key']) {
  exit;
}

さらに、

/* 管理者用 | メモ削除 */
public function admin_delete($id)
{

この直後に、以下の処理を追加します。

if ($this->params['url']['token'] != $this->params['_Token']['key']) {
  exit;
}

フォームからの投稿はこれで完了ですが、リンクの場合は自動でトークンが送られませんので、自分でトークンを追加します。/cakephp/app/views/memos/admin_index.ctp にある

<?php echo $html->link('削除', '/admin/memos/delete/' . $memo['Memo']['id'], null, '記事を削除してもよろしいですか?') ?>

この部分を、以下のように変更します。

<?php echo $html->link('削除', '/admin/memos/delete/' . $memo['Memo']['id'] . '?token=' . $this->params['_Token']['key'], null, '記事を削除してもよろしいですか?') ?>

参考ページ。