ITの隊長のブログ

ITの隊長のブログです。Rubyを使って仕事しています。最近も色々やっているお(^ω^ = ^ω^)

いまさらCakePHP2.xを使ったここ2ヶ月のことをメモする

スポンサードリンク

CakePHP Pancakes

ここ2ヶ月、CakePHP2.xを久々に触り、立ち上げたプロジェクトで学んだことをメモする。

本当はCakePHP3.xを触りたかったけどね。

ここ最近のCakePHP2.xを使った俺のまとめ

Object志向って何? おいしいの? という、プログラマwが書いた内容です。やさしい気持ちでご覧になって頂けますと幸いです。(厳しいお言葉でも構いませんが、アドバイスほしい)

今回使ったバージョン

  • CakePHP2.8

composer & bake を使ったCakePHPのinstall

公式ドキュメントにも用意されているので、詳しくは書きませんが、composer & bakeを使ったほうが便利と思います。

ただ、composerがインストールされていないWindowsユーザーに仕事ふるのはちょっと面倒だった。...

応用インストール

$ composer install

# installが終わったら、bakeを使ってCakePHPの準備
$ ls -d Vendor
Vendor

# 任意のプロジェクトディレクトリを作成して、bakeで指定する
$ mkdir app
$ ./Vendor/bin/cake bake project app/

複数のログイン・認証のシステム

CakePHPで、認証が必要な要件を満たすときは、AuthComponentを使うと思います。

しかし、ECサイトのように、下記の要件があると、満たすことが去年までできませんでした。

例)ECサイト

  • 管理者ユーザー(商品情報の管理、注文の管理、サイト管理)
  • サイト登録ユーザー(認証必須、注文、過去の注文履歴閲覧、自分の情報を登録・編集)
  • ゲストユーザー(注文不可今はできるサイトがほとんどだけど、サイト閲覧のみ)

調べてみると「できる」と言うサイトは少なくともありました。

sugi511.hatenablog.com

これまでは面倒だったので、管理用と顧客用としてアプリケーションを分けていましたが、今回から相互で使う処理が多く、メンテナンスがすごく面倒になったので、一緒のアプリケーションで複数ログインを実装することになりました。

prefixルーティングという機能を使うと実装できました。

ルーティング

この機能は、その名と通り設定したルーティングにマッチすると接頭辞(prefix)を付けてくれます。

例えば/admin/items/edit/5のようなURLにアクセスしたなら、フレームワークではItemsController.phpadmin_edit($id = null)というアクションへアクセスします。

設定する場合は、app/Config/core.phpapp/Config/routes.phpへ設定が必要です。

  • app/Config/core.php
<?php
...
// 142行目ぐらい
  Configure::write('Routing.prefixes', array('admin'));
  • app/Config/routes.php
<?php
...
// 任意の行で
Router::connect('/admin', array('controller' => 'indexes', 'action' => 'index', 'admin' => true));

上記設定と、AuthCompoentを組み合わせます。

要は、/admin/がついてるかついていないかで、管理者ユーザー、サイト登録ユーザーを分ければ良い。

これをわける処理はapp/Controller/AppController.phpに実装します。

  • app/Controller/AppController.php
<?php
...
class AppController extends Controller
{
...
  // 通常はUserモデルを使った認証にする
  public $components = [
    'Auth' => [
      'authenticate' => [
        'userModel' => 'User',
        'fields' => [
            'username' => 'email'
        ]
      ]
    ]
  ];
...
  // beforeFilter()でprefixを確認して、adminだったらAuthComponentを管理者用に上書きすれば良い
  public function beforeFilter()
  {
    if (isset($this->params['prefix']) && $this->params['prefix'] === 'admin') {
      $this->layout = 'admin';
      $this->_setAdminAuthParameter();
    }
    parent::beforeFilter();
  }

  private function _setAdminAuthParameter() {
    // 管理者ユーザーの場合はAdminUserモデルで認証する
    $this->Auth->authenticate = [
      'Form' => [
        'userModel' => 'AdminUser',
        'passwordHasher' => 'Blowfish',
        'fields' => [
          'username' => 'email'
        ]
      ]
    ];
    // セッションのキーも変更する(通常と同じにすると色々変な動きが)
    AuthComponent::$sessionKey = 'Auth.Owner';
  }
...
}

こんな感じでよし。

あとは、prefixルーティングを使ったControllerのアクション、Viewのテンプレートファイル名の接頭辞にadminを入れ忘れなければおk。

ディレクトリ階層を別ける

クラスを大きくし過ぎると、テストやらデバッグやら面倒になってくるので、できるだけわけたほうが良い。

さらに、先ほどのprefixルーティングを使っていると、管理ユーザー、ログインユーザーがそれぞれアクセスするクラスをディレクトリ階層でわけて、ぱっと見てすぐ判断できるようにしたい。要はメンテナンスのし易い構造にしたい。

そこで、それぞれわけるためにapp/Config/bootstrap.phpへ追記する。

  • app/Config/bootstrap.php
<?php
...
// 43行目ぐらいに追記すればいいかも
App::build(array(
    'Controller' => array(
        ROOT . DS . APP_DIR . DS . 'Controller' . DS,
        ROOT . DS . APP_DIR . DS . 'Controller' . DS . 'Admin' . DS,
    ),
    'Model' => array(
        ROOT . DS . APP_DIR . DS . 'Model' . DS,
        ROOT . DS . APP_DIR . DS . 'Model' . DS . 'Table' . DS,                   // あとで説明
        ROOT . DS . APP_DIR . DS . 'Model' . DS . 'Action' . DS . 'Front' . DS,   // ログインユーザー用ActionModel
        ROOT . DS . APP_DIR . DS . 'Model' . DS . 'Action' . DS . 'Admin' . DS,   // 管理ユーザー用ActionModel
    ),
    'View' => array(
        ROOT . DS . APP_DIR . DS . 'View' . DS,
        ROOT . DS . APP_DIR . DS . 'View' . DS . 'Admin' . DS,
    ),
));

こんな感じで分けると良い。

気をつけてほしい点として、ディレクトリをわけたから、同じクラス名は使えないということ。これはちょっと面倒だし、複数人で開発しているといつか事件になりそう。namespaceとか使えればいいけど、まだ試していません。

MVCそれぞれでアクセスできる共通クラスの用意

今回のプロジェクトでは、認証しているか認証していないか、認証しているユーザーは管理者か、ログインユーザーかなどなど、MVCどの場面においても確認したい要件があった。

MVCにはそれぞれ共通処理を書くところがあり、MはBehavior、CはCompoent、VはHelperという考え方(?)が用意されている。

しかし、今回の要件でいう認証については、全体に向けて共通化してもらわないと困ることが多かった。例えば、Componentで用意したメソッドをViewで使いたい。Modelで使っていた処理をHelperに書き足したなどなど、メンテの面でも結構面倒なことが多々あった。

なので、どっからでも使える共通クラスをVendorディレクトリに用意することにした。(Pluginとして用意するのもありだと思ったが、別にプラグインとして使うものでもなかったので)

  • app/Vendor/util/AuthUtility.php
<?php
namespace Util;
\App::uses('AuthComponent', 'Controller/Component');

class AuthUtility
{
  private $auth;

  public function __construct()
  {
    $collection = new \ComponentCollection();
    $this->auth = new \AuthComponent($collection);
  }

  /**
   * @return array|null
   */
  public function getLoginUser()
  {
    return $this->auth->user();
  }
}

このクラスをApp::import()を使って、使えるようにする。また、オブジェクト化する際にnamespaceを指定して使うのも忘れずに。

<?php
App::import('Vendor', 'util/AuthUtility');

...
  public function testModel()
  {
    $auth = new Util\AuthUtility();
    return $auth->getLoginUser();
  }

これで共通化することができる。

ほんの最近困っていることは、呼び出すたびにオブジェクト化しているので、今後認証共通処理が追加していく毎に重くなると予想している。

なので、MVC呼び出しの初回の処理で一回オブジェクト化して、それを使いまわせるようなEntitiyクラスが作れないかなと考えています。

Controllerとのつきあい方

油断しているとどんどんコードが増えてくるので、なるべく小さく書くように意識しています。

よく太らせる原因とその移動場所

ビジネスロジック、findのパラメータ...というかfindもすべて、paginateのパラメータ作成・値のチェック処理などなどすべてModelへぶっこみました。

なるべく、Controllerでしかできないことを意識してコーディングするようにしています。

  • httpメソッドのチェック
  • redirect
  • modelから値をもらって、viewへ渡す

また、リクエストを受けとる処理もControllerの仕事だと思っていましたが、Modelでも取得できるようです。

<?php

class HogeHoge extends AppModel
{
...
  public function getRequest()
  {
    // これでrequestObjectが取得できる
    $requestObject = Router::getRequest();
  }
}

こいつができれば、リクエストの引数チェックもすべてModelに持っていけるようになりました。

また、Actionについて。Actionを1つにして、引数で削除処理なのか、更新処理なのかを処理する方法を1度とったことがあります。

<?php
...
  public function action()
  {
    if ($this->request->is('post')) {
      $requestData = $this->request->data;
      if ($requestData['request'] === 'edit') {
        // edit
        $this->_edit($requestData);
      } else if ($requestData['reqest'] === 'delete') {
        // delete
        $this->_delete($requestData);
      }
      ...
    }
  }

これはすぐにやめました。最初viewも使いまわせて少なくなるし便利かなーと考えた方法でしたが、運用のフェーズになるとすごくめんどくさくなりました。

処理を追加したいとか、削除の処理だけこうしたいとかなったときに、すごく触りづらいコードだとこと。

bakeもそうですが、addアクションとeditアクションをそれぞれ生成するので、アクションメソッドもなるべく別けたほうが良いです。

ただし、PaginateComponentを使うページだけは、index()search_index()と別けることもありました。が、これに関してはわけないようが良いです。

PaginateComponentでは、ページネーションを使う上で、ソートしたり、検索ができるようにしたいなど要件が追加されることが多いので、同じメソッドで管理したほうが、コードもまとめやすいし組みやすかったです。

Componentの利用

だんだん使わなくなりました。。。

最初は、ファイルアップロードの処理とか、CakeEmailを使う前の準備Componentなどで利用していましたが、ほとんどModelとBehaviorに移動してしまいました。

もしかしたらあんまり使わないかもしれません。。。

Viewとのつきあいかた

Viewにロジックを書かないで! というのを意識しました。とわいえ、条件でViewを変えたいなど絶対でてきますので、そういう場合はHelperを使ったほうが良いです。テストもし易くなります。

さらに、デザインが汎用的で綺麗に用意されているのであれば、Elementがすごく力を発揮してくれます。ほとんどviewのblockがつかいまわせるようなhtml構造であれば、迷わずelement化しましょう。商品のリストとか、ログイン後にでてくるヘッダーとか。

ViewからModelを利用

正確にはHelperの中で、Modelを利用しました。

とあるModelのステータスを確認するだけで、「ControllerからModelにアクセスして、データを確認して、その値をViewに渡す」って流れがすごく面倒だったので、Helperからアクセスすればフローが短くなると思ってそうするようになりました。

<?php
...
  public function myHelper()
  {
    $hoge = ClassRegistry::init('Hoge');
  }
...

ちなみに、更新とか削除、データの取得はHelperからしません。あくまでデータの参照だけです。

Modelとのつきあい方

ほとんどの処理をModelに詰め込みました。なので、すげぇーモデルのクラスが多いです。

アソシーエーションは使わない

完全に使わないわけではないですが、常時つけていると外す処理などが結構面倒です。

なので、常はどこも接続しない状況にして、3つのテーブルを全部更新とかの保存処理とか面倒なところだけ、bindModelを使ってアソシエーションを使う。ってやりかたがいいと思った。

保存、更新、削除処理はtry ~ catchで囲む

想定していないデータが渡ったときに、よくエラーが発生してお客さんびっくり!ってなります。それを回避するためにtry ~ catchを使いましょう。

また、catchの中で、ログを吐いたり、ロールバックできる処理を書けば、安全なシステムを作ることができる。

共通データ処理、共通ビジネスロジック、ActionModel

処理・目的 移動Class
save、update、deleteなどの処理の更新処理 ~/app/Model/Table/Model.php
find()のパラメータと処理 ~/app/Model/Table/Model.php
アソシエーション、bindModel() ~/app/Model/Table/Model.php
ビジネスロジック ~/app/Model/Behavior/Behavior.php
ControllerのActionに紐づく処理(リクエストデータの引数チェックなど) ~/app/Model/Action/ActionModel.php

こんな感じ。これまでは全部~/app/Model/ディレクトリに全部ぶっこんでいましたが、さすがに見づらかったので、~/app/Config/bootstrap.phpで構造を変更できるようにしました。

~/app/Model/Table/に入るクラスは、これまでCakePHPで用意するModelクラスが入ります。

~/app/Model/Action/は、ControllerのActionメソッド毎に作成します。(用意する必要がないActionの場合、Modelは用意しません)主に、リクエストパラメータの存在チェックや、保存までの受け渡し処理。本来ならController側で書かないといけないことをこのActionクラスに持たせるようします。そうすることで、Controller側で書くことが少なくなり、テストがしやすくなります。

JavaScript、またはJavaScriptフレームワークPHPの友情

(なんかうまいタイトルを思いつきませんでした)

viewにこだわる案件が増えてきました。となると、活躍するのがjavascriptでしょう。

例えば、注文画面で個数を変えたら自動で金額が変わったり、在庫以上に個数を入力したらアラート発生したり。などなど。

そういう要件が増えてくると、jsのコードも多くなるはず。

ここで(個人的に)問題になるのが、jsとphpでのコードの重複です。

最初はPHPでviewを用意するのですが、あとからjsで動きをつけているって感じの実装をしていきます。

jqueryでよくあるのが、ユーザーが選択してviewが増えていくような実装をしていくようになると、phpで用意するviewとjsで用意するviewが被ってしまい、メンテナンスするときに両方手を入れないといけないため、すごく面倒なことが多かったです。

そもそもの要件定義などで汲み取るのが下手くそだったってことも原因のひとつだとは思いますが、お客さんのほとんどはリテラシーもなく、実際に使わないとイメージできない人が多数なので、そこまで完璧な要件汲み取りはほとんど不可能だと思います。

そこで、最近考えついたのはPHPAPI化です。

操作が多い管理画面などは、ページ遷移しない、作業のステップが少ないほうが喜ばれることが多かったので、viewはすべてjsで管理するようにします。

PHP側、CakePHPはデータを引っ張ってきて、フィルターをかけるなりデータを加工し、jsonで返すだけのAPI化にすれば、役割としてはっきりするようになって、メンテしやすくなると考えます。

実際、それが嫌になって、一部ページだけそれを試してみたところ。すごくすっきりしました。(そのあとjQueryがだるくなって、AngularJSに変えたのは別の話)

クライアント側が担う責務は、これからどんどん増してくると思うので、こんな感じの組み方にしていこうかなと思います。

PHPが本気でテンプレートエンジンをやめるときが来るかも。

マイグレーションの利用

ここを参考にコマンドを実行していました。

migrations/Generate-Migrations-Without-DB-Interaction.md at master · CakeDC/migrations · GitHub

ただ、マイグレーションのコマンドだけでは実現できないことが多かったりするので(俺が知らないだけかもしれませんが)、そういうときは、マイグレーションファイルだけ作成して、before & afterのメソッドに処理を書いていました。

【alter ... modify ...】binaryって指定すると、blobにしかならないカラムをmediumblobに変更したい

画像とか保存するときに、blobでは小さい場合がほとんどです。なので、mediumblobにしようとしたら、コマンドでは認識させることができなかった。

マイグレーションを実行する前後で処理されるcallbackが用意されているので、そこであとから書き換えるsqlを実行するようにしました。

また、callbackには$directionというパラメータが渡されます。マイグレーションのバージョンが前に進む(up)のか、後ろに戻る(down)のかを判断するときに使います。

<?php
...
  'image' => array('type' => 'binary', 'null' => false, 'default' => null),
...
  public function after($direction) {
    if ($direction === 'up') {
      $topSliderManager = ClassRegistry::init('TopSliderManager');
      $topSliderManager->query('alter table top_slider_managers modify image mediumblob not null');
    }
        return true;
  }

【alter ... add ... after xxx】カラムを追加するときに、特定のフィールドを指定してその後ろに追加

これもコマンドでは用意できないので。

マイグレーションファイルを作成後、追加するカラムの連想配列に追記する。

keyをafter、valueをフィールド名にする。

<?php
...
  'image' => array('type' => 'binary', 'null' => false, 'default' => null, 'after' => 'image_name'),
...

cakephp3.xでは使いやすくなっていることを祈る (人∀・)タノム

感想

CakePHP2.xの利用は恐らくこれが最後になるかなと思います。

次回は3で攻める予定です。どんだけ変わっているかなー。。。。。