yano3nora
7/2/2017 - 12:19 PM

[cakephp: CakePHP2] CakePHP2 note. #php #cakephp

[cakephp: CakePHP2 note] CakePHP2 note. #php #cakephp

Overview

version3 と違い古いシステムでの利用が想定される。PHPのバージョン新しめの機能 ( array() -> [] ) とかは使わないほうが無難。ORMで取得するのが連想配列だったりなかなか負の遺産だけどCakePHPの稼働バージョンで一番多いのはこいつ。


TIPS & REFERENCES

App クラス

http://www.cxmedia.co.jp/school/cakephp_doc/core-utility-libraries/app.html

命名規則とアソシエーション

bake

共通処理の順番

  1. beforeFilter
  2. view's action
  3. beforeRender
  4. view's render
  5. afterFilter

MVCの書き分け

http://qiita.com/katsuakikon/items/7a8fcaf85b342cb61bcc

ユーザ定義定数・関数

  1. /app/config/functions.php とかに記述
  2. bootstrap.phpへConfigure::load('functions'); 記述

セッションタイムアウトの設定

https://book.cakephp.org/2.0/ja/development/sessions.html

Configure::write('Session', array(
    'defaults' => 'php',
    'timeout' => 2160, // 36 hours
    'ini' => array(
        'session.gc_maxlifetime' => 129600 // 36 hours - こいつを timeout より長くしないと意味ない
    )
));

recursive 2 から必要モデルだけ contain で取得

https://book.cakephp.org/2.0/ja/core-libraries/behaviors/containable.html

// Contain Sample
    $this->Seat->Behaviors->attach('Containable');
    $seats = $this->Seat->find('all', array(
      'contain'    => array('Event', 'Seat', 'Student', 'SeatVote' => array('Company')),
      'conditions' => array('Seat.event_id' => $event_id),
      'order'      => array('Seat.position' => 'asc'),
      'recursive'  => 2,
    ));

別モデル呼び出しは2パターン

このコントローラのどこでも使うよ : public $uses = array('Model1, 'Model2); この処理でしか使わないよ : $this->loadModel('Model1'); $this->loadModel() のが早いけど使いすぎると依存関係がごちゃる。複数のモデルと連携するコントローラクラスなのか、特定エンティティ(モデル)に特化したコントローラなのか設計段階で決めておくべき。

Chromeだと target="_blank" リンクでログアウトしちゃう

IEとかなら大丈夫。セキュリティ頑張るブラウザだとNGのよう。 以下でsessionのAgentチェックを無効にすると別タブログインOKに。

// core.php
  Configure::write('Session.checkAgent', false);

自作 Component

自作 Behavior

バーチャルフィールドで独自カラム作成

as みたいなやつ http://qiita.com/kazu56/items/82e049118a70a735fdc7

$this->request->dataの存在確認はdata()でやれ

if($this->request->data['User']['name']) だと 空値で notice 。isset() と組み合わせると冗長になるので data() メソッドを使う if ($this->request->data('User.name')) でアクセスすると存在しない場合にエラーにならないので便利。

パスワードハッシャ

AuthComponent::password() でOK

booleanはtinyint(1)なのでフォームでも0, 1 で

booleanのフォームインプットの値が true , false ではじかれた...?←未検証

find()で重複回避 DISTINCT

'fields'=>array('DISTINCT table.id'....); 上記のようにfieldsオプションの中で重複削除の基準になるカラムを指定してあげれば返り値 $query とかには重複したものは入らなくなる。ちなみにfieldsの先頭に書かないとエラー。 http://wataame.sumomo.ne.jp/archives/1235 また、find('count')のオプションでDISTINCTを使う場合、fieldsに配列で他の指定をするとアウト。http://book.cakephp.org/2.0/ja/models/retrieving-your-data.html#find-count

モデルのプライマリーキーを[id]から別カラムへ

// 規約通りでない(くそみたいな)DB構成のときにキーの再設定が可能
public $primaryKey = 'no';
//複数指定するといみわからんくなる
public $primaryKey = array('id','no');  

Paginationで用いられるクエリ取得

https://teratail.com/questions/10090 page:2 とかは $this->params['named']['page'] にいるよ

Paginator + JOINS

public $pagenate = array('諸々'); で設定しろってマニュアルに書いてあるくせにきかねえぞこらってときはコンポーネントロード時に配列でオプション渡したれ。そもそもControllerに定義した$pagenateにはアクセスしていない。$pagenateの設定を反映させるには、コンポーネントコンストラクタの引数に渡す/コンポーネントのインスタンスの内部変数書き換え(多分 $this->Paginator->settings=array() だね)/リクエスト時にパラメータとして渡す。https://blog.doizaki.com/entry/2015/04/21/001600


SAMPLE CODE & SNIPETTS

get huge data using with splitting query by loop

$count = $this->Model->find('count');
$limit = 5000;
$loop  = ceil($count / $limit); 
for ($i = 0; $i < $loop; $i++){
    $offset = $limit * $i;
    $data = $this->Model->query("select * from hogehoge as limit {$limit} offset {$offset};", $cachequeries = false);
    // do something
}

$this->log & sql data log

$this->log($this->{$this->modelClass}->getDataSource()->getLog());
// default-dest: `tmp/logs/error.log` 
// require: `CakeLog::config('default', array('engine' => 'File'));` in `bootstrap.php`

loadModel() in Model

/**
 * Model -> loadModel()
 * @param str $model_name
 * @return - (getting available loadModel() at ModelArea)
 */
public function loadModel($model_name) {
  App::uses($model_name,'Model');
  $this->{$model_name} = new $model_name();
}

// $this->loadModel('User');
// $this->User->find('first');

AuthComponent

// in class AppAdminsController extends AppController {  
public function beforeFilter() {
  parent::beforeFilter();  // extend parent method

  // set auth component
  $this->layout = 'admin';
  $this->Auth->loginRedirect = array('controller' => 'admins', 'action' => 'index');
  $this->Auth->logoutRedirect = array('controller' => 'admins', 'action' => 'login');
  $this->Auth->loginAction = array('controller' => 'admins', 'action' => 'login');
  $this->Auth->authenticate = array(
    'Form' => Array(
      'fields' => Array(
        'username' => 'username',
        'password' => 'password',
      ),
      'userModel' => 'Admin',  // "$useTable = 'users'" in App\Model\Admin
    ),
  );
  AuthComponent::$sessionKey = "Auth.Admins";  // for another Auth
  
  // check & redirect by authority
  $allowActions = array('login', 'logout', 'api');
  $this->Auth->allow($allowActions);
  if (!in_array(strtolower($this->request->action), $allowActions)) {
    if (!$this->Auth->loggedIn()) {
      $this->Flash->error(MSG_ERROR_LOGIN);
      $this->redirect($this->Auth->logout());
    }
    if ($this->Auth->user('group_id') != 1) {
      $this->Flash->error(MSG_ERROR_ROLE);
      $this->redirect($this->Auth->logout());
    }
  }
}

PaginatorComponent

// associated : Student hasMany Seat
// in StudentsController extends AppController
  public $components = array(
    'Paginator' => array(
      'Student'   => array(),
      'Seat'      => array(
        'fields'    => 'Seat.*, Student.*',
        'limit'     => 30,
        'order'     => array('Student.namekana1' => 'asc', 'Student.namekana2' => 'asc'),
        'recursive' => -1,  //disuse join
        'joins'     => array(
          array(
            'table'      => 'students',
            'alias'      => 'Student',
            'type'       => 'LEFT',
            'conditions' => 'Student.id = Seat.student_id',
          ),
        ),
      ),
    ),
  );

  public function index() {
    $options = array();
    $event_id = Sanitize::clean($this->request->query('event_id'));
    $studentid = Sanitize::clean($this->request->query('studentid'));
    $email = Sanitize::clean($this->request->query('email'));
    $name1 = Sanitize::clean($this->request->query('name1'));
    $name2 = Sanitize::clean($this->request->query('name2'));
    $this->set(compact('event_id', 'studentid', 'email', 'name1', 'name2'));
    if (!empty($studentid)) { $options += array('Student.studentid'=>$studentid); }
    if (!empty($email)) { $options += array('Student.email like'=>'%'.$email.'%'); }
    if (!empty($name1)) { $options += array('Student.name1 like'=>'%'.$name1.'%'); }
    if (!empty($name2)) { $options += array('Student.name2 like'=>'%'.$name2.'%'); }
    if (!empty($event_id)) {
      $options += array('Seat.event_id' => $event_id);
      $this->set('students', $this->paginate('Seat', $options));
    } else {
      $this->set('students', $this->paginate('Student', $options));
    }
    $this->loadModel('Event');
    $eventOptions = $this->Event->getOptions();
    $this->set(compact('eventOptions'));
  }

Transaction in Model

// http://qiita.com/uedatakeshi/items/780189183eb3a61d5bdc
public function saveNewMember() {
    $this->Comment = new Comment();// 別モデル読み込み
    $datasource = $this->getDataSource();
    try{
        $datasource->begin();
        $data = array(
            'Member' => array(
                'name' => 'maeda',
                'email' => 'maeda@sample.com',
            ));
        if (!$this->save($data)) {
            throw new Exception();
        }
        $data = array(
            'Comment' => array(
                'post_id' => '4',
                'comment' => 'res3-1',
            ));
        if (!$this->Comment->save($data)) {
            throw new Exception();
        }
        $datasource->commit();
    } catch(Exception $e) {
        $datasource->rollback();
    }
}

Sql直書き+プリペアド

// in model
// refs: http://log.noiretaya.com/157
$this->getDataSource()->fetchAll('
  select * from users where username = :username and password = :password',
  array('username' => 'jhon','password' => 'test@example.com')
);

saveAssociations() で関連モデルをまとめてsave()

// https://book.cakephp.org/2.0/ja/models/saving-your-data.html#hasmany

// Controller/CourseMembershipController.php
class CourseMembershipsController extends AppController {
    public $uses = array('CourseMembership');
    public function index() {
        $this->set(
             'courseMembershipsList',
             $this->CourseMembership->find('all')
         );
    }
    public function add() {
        if ($this->request->is('post')) {
            if ($this->CourseMembership->saveAssociated($this->request->data)) {
                return $this->redirect(array('action' => 'index'));
            }
        }
    }
}

// View/CourseMemberships/add.ctp
<?php echo $this->Form->create('CourseMembership'); ?>
    <?php echo $this->Form->input('Student.first_name'); ?>
    <?php echo $this->Form->input('Student.last_name'); ?>
    <?php echo $this->Form->input('Course.name'); ?>
    <?php echo $this->Form->input('CourseMembership.days_attended'); ?>
    <?php echo $this->Form->input('CourseMembership.grade'); ?>
    <button type="submit">Save</button>
<?php echo  $this->Form->end(); ?>

// 重要
// hasOne or belongsTo を保存時と hasMany を保存時で用意する配列が違うのでビューを工夫する必要があるよ!!

// hasOne / belongsTo
$data = array(
    'User' => array('username' => 'billy'),
    'Profile' => array('sex' => 'Male', 'occupation' => 'Programmer'),
);
// hasMany
$data = array(
    'Article' => array('title' => 'My first article'),
    'Comment' => array(
        array('body' => 'Comment 1', 'user_id' => 1),
        array('body' => 'Comment 2', 'user_id' => 12),
        array('body' => 'Comment 3', 'user_id' => 40),
    ),
);

sqlのバックアップ機能をアプリ側で実装する

// refs: http://norm-nois.com/blog/archives/2914
$db = ConnectionManager::getDataSource('default');
$tables = $db->listSources();
$config = $db->config;
foreach($tables as $table) {
  exec('mysqldump --host='.$config['host'].' --user='.$config['login'].' --password='.$config['password'].' '.$config['database'].' '.$table.' > '.$table.'.sql');
  exec('gzip -f '.$table.'.sql');   // zipのがいいか?
}

composer.json

// refs
// https://book.cakephp.org/2.0/ja/installation/advanced-installation.html
{
    "name": "project-name",
    "repositories": [
        {
            "type": "pear",
            "url": "http://pear.cakephp.org"
        }
    ],
    "require": {
        "cakephp/cakephp": ">=2.6.4,<3.0.0"
    },
    "require-dev": {
        "phpunit/phpunit": "3.7.37",
        "cakephp/debug_kit" : ">=2.2.4,<3.0.0"
    }
}

$this->Model->recursive ?

// recursive : -1 でアソシエーション無効
$this->Model->recursive = -1;
$this->Model->find('all', array('conditions'=>array('code'=>'1')));
// option配列につっこめる

$programs = $this->Program->find('all', array('recuresive' => -1));
// paginationでも同じ

public $paginate   = array(
    'fields'    => array('Event.*'),
    'limit'     => 10,
    'order'     => array('Event.date' => 'desc'),
    'recursive' => -1,  //disuse join
);

BUILT-IN APIs

common

// Call Another Class
App:import('ClassName', 'Model');

$this->Another = new Another();
$this->Another->find('all');

// get webroot
echo $this->webroot;
echo $this->request->webroot;
echo Router::url( "/" );
echo $this->Html->url( "/" );

// get now *
$this->modelClass // in controller
$this->name;  // in all mvc
$this->action  // in controller or view 
router::reverse($this->request) //path with query
router::reverse($this->request,true) //fullpath

model

find()

http://monmon.hateblo.jp/entry/20110207/1297068346
http://dim5.net/cakephp/conditions-find.html

// base
$this->find('all',$conditions);

// OR (same column)
$this->find('all', array(
  'conditions' => array(
    'OR' => array(
        array('user_id' => 1),
        array('user_id' => 3),
      ),
    ),
  ),
);

// OR (diffrence columns)
$this->find('all', array(
  'conditions' => array(
    'id' => 1,
    array('OR' => array(
      'status' => 1,
      'flg'       => 1,
    )),
    array('OR' => array(
      'status' => 2,
      'flg'    => 2,
    )),
  ),
));

// LIKE
$data = $this->MODEL->find('all', array(
    'conditions' => array(
        'MODEL.tag like' => "%{$query_param}%",
        'MODEL.status' => '1'
        ),
    )
);

// NOT
$data = $this->MODEL->find('all', array(
  'conditions' => array(
      'Model.status' => '1',
      'NOT' => array(
          'Model.tag' => ""
      )
    ),
  )
);

// AND & OR
$data = $this->MODEL->find('all', array(
    'conditions' => array(
        'and' => array(
            'MODEL.email' => $param[0][MODEL]["email"],
            'MODEL.id <' => $param[0][MODEL]["id"],
                'or' => array(
                    array('MODEL.status' => 1),
                    array('MODEL.status' => 3),
                )
            )
        ),
    )
);

// findBy, findAllBy - findBy('カラム条件', fields, order, recursive)
$this->findById(8);           // = find('first', ['conditions' => ['id' => 8]])
$this->findAllByName('hoge'); // = find('all', ['conditions' => ['name' => 'hoge']])

// getLastInsertId
$this->Model->getLastInsertID();

// saveAll ( save MultiArray and Associated )
$data = array(
  'User' => array(
    0 => array('name'=>'taro'),
    1 => array('name'=>'jiro'),
  )
);
$this->saveAll($data['User']);

// validation control
'isUnique'   => array(  // validation name
    'rule'    => array('isUnique'),  // validate type
    'message' => 'inputs exists!!',
    'on'=>'create',  // create only

controller

// Component and Helper
public $components = array('Auth', 'Session', 'Cookie', 'Flash', 'MyComponent');
public $helpers = array('Html', 'Form', 'MyHelper');

// Branch request
if ($this->request->is('post')) {}

// data()
$this->request->data('User.name')
// safe call : $this-request->data['User']['name'] 

// loadModel
$this->loadModel($table);
$this->$table = new $table();
$query = $this->$table->find('all', $options);

// find in controller
$results = $this->{Model}->find('all);
foreach ($results as $result) {
    echo $result['Model']['column'];
}
$this->set(compact('results'));

// Session Component
public $components = array('Session'); //prepare
$this->Session->write('key','value');  //write
$this->Session->read('key');    //read
$this->Session->check('key');  //isset
$this->Session->delete('key');  //delete
$this->Session->destroy();  //delete-all

// Flash Component
public $components = array('Session', 'Flash'); //prepare
$this->Flash->success('oh yeah!');
$this->Flash->error('fxxk!!');

// Create URL by Router class
Router::url(array('controller'=>'hoge',action'=>''));

// set view var
$this->set(compact('hogehoge'));

// get view var
$this->viewVars['hogehoge'];

// call another action
$this->actionName()  //not api

// change action
$this->requestAction('hoge');

// change action without redirect
$this->setAction('hoge');
/* note
render hoge's View.
And thereafter, the process returns to the original action.
*/

// change render (view)
$this->render('hoge);

// change layout
$this->layout = 'default';
  //default.ctp

// Redirect (with exit)
$this->redirect(['controller'=>'orders','action'=>'confirm',7]);
$this->redirect('http://example/orders/confirm');
$this->redirect($this->referer()); 
$this->redirect(array('action'=>'hoge',$param));

// Sanitize (DEPRECATION)
App::uses('Sanitize', 'Utility'); 
$param = Sanitize::clean($param);

// ConnectionManager (DEPRECATION)
App::uses('Model','ConnectionManager');
$db = ConnectionManager::getDataSource('default');
$result = $db->query('select * from datatable1');

view

// get view var
<?php echo $this->get('hoge') ?>

// insert str before (after) input
<?php echo $this->Form->input('hoge',array('after'=>'円')) ?>

// form input defaults
<?php echo $this->Form->create('User', array(
  'inputDefaults' => array(
    'label' => false,
    'div' => false
))) ?>

// file upload form 
echo $this->Form->create('User',array(
  'enctype'=>'multipart/form-data',
));
echo $this->Form->file('image');
// in controller
// $tmp_name = $this->request->['User']['image']['tmp_name'];


// disabled, readonly in select
<?php echo $this->Form->input('piyo',array(
    'type'  => 'select',
    'empty' => ' select here !!',
    'options'  => array(
        1   =>  $options[1],
        2   =>  $options[2],
        3   =>  $options[3],
        4   =>  $options[4],
    ),
    'disabled' => array(1,2,3),
)) ?>

// link
$this->html->url(array('controller'=>'hoge', 'action'=>'index'));

// html5 input api
echo $this->Form->control('date', array('type' => 'date'));

// disuse escape (using fontawesome)
$this->Html->link('<i class=fa fa-file></i> ADD', array('controller' => 'users', 'action' => 'add'), array('escape' => false));

ShellClass - バッチ処理

バッチ非同期処理+ポーリング

大量の DB データ(数十万件~)を取得してプログラムのメモリに格納してごにょるようなケースでは、データ量によっては「メモリ不足で落ちて」しまう。これについて一度に数千件ずつ取得するようなループを組み対処すると、今度はある一定量を超えた段階でブラウザが「サーバからの応答なし=タイムアウト」で落ちてしまう。このような 絞り込みを行った上で数万に達するようなテーブルを扱う 場合にはサーバサイドでタスクを「DBアクセス→データ取得」「待機ページ→データ取得完了後に出力ページ」のように2つに分割し PHPで非同期処理を行う 必要がある。以下サンプルコード。

Sample: FugeExportShell

App::uses('AppShell', 'Console/Command');
App::uses('User', 'Model');

/**
 * FugeExportShell
 * @see           https://goo.gl/grcoHk
 * @package       app.Console.Command
 */
class FugeExportShell extends AppShell {
  public $uses = array('User');
  
  /**
   * test method
   * @param mix $args
   * @return mix $args
   */
  public function hello() {
    $this->out('hello, world.');  //stdout
  }

  /**
   * main: called default method by exec() 
   * get huge data using with splitting query by loop
   */
  public function main() {
    ini_set('memory_limit', -1);
    set_time_limit(0);
    $args   = unserialize($this->args[0]);
    $tmpfile = $args['tmpfile'];
    $params = $args['params'];
    $count = $this->DenpyoTbl->find('count', $params);
    $limit = 5000;  // get records number per loop
    $loop  = ceil($count / $limit); 
    for ($i = 0; $i < $loop; $i++){
      $offset = $limit * $i;
      $paramsWithOffset = $params;
      $paramsWithOffset += array('limit' => $limit, 'offset' => $offset);
      $rows = $this->User->find('all', $paramsWithOffset);
      foreach ((array)$rows as $row) $results[] = $row;
      if ($i > 0 && !file_exists(ROOT . DS . APP_DIR . DS .'tmp' . DS . $tmpfile.'__progress.txt')) exit;
      $fp_progress = fopen(ROOT . DS . APP_DIR . DS .'tmp' . DS . $tmpfile.'__progress.txt', 'w');
      fprintf($fp_progress, round((count($query)/$count * 100), 1));
      fclose($fp_progress);
    }
    $serializedResults = serialize($results);
    $fp_results = fopen(ROOT . DS . APP_DIR . DS .'tmp' . DS . $tmpfile.'.txt', 'w');
    fprintf($fp_results, $serializedResults);
    fclose($fp_results);
    exit;
  }

}

Sample: ExportController

public function async(){
  $this->autoLayout = false;
  $this->autoRender = false;
  $params = [/* something */];
  $tmpfile = 'tmpfile';
  $token = session_id();
  $querystring = http_build_query(compact('tmpfile', 'token'));
  $args = serialize(compact('tmpfile', 'params'));

  // running async process by shell (FugeExportShell)
  exec(ROOT.DS.APP_DIR.DS.'Console'.DS."cake fuge_export > /dev/null main '{$args}' &");
  echo "
    <script>
      window.onload = function() {
        setTimeout(function(){
          location.href = '/{$this->request->controller}/await?{$querystring}';
        }, 1000);
      }
    </script>
  ";
  echo "<p>Loading... <span id=\"progress\">0</span> %</p>";
  exit;
}


public function await() {
  $this->autoLayout = false;
  $this->autoRender = false;
  if (session_id() != $this->request->query('token')) throw new BadRequestException();
  $tmpfile = $this->request->query('tmpfile');
  $token = $this->request->query('token');
  $querystring = http_build_query(compact('tmpfile', 'token'));
  if (!file_exists(ROOT . DS . APP_DIR . DS .'tmp' . DS . $tmpfile.'.txt')) {
    $progress = (float)file_get_contents(ROOT . DS . APP_DIR . DS . 'tmp' . DS . $tmpfile.'__progress.txt');
    echo "
      <script>
        window.onload = function(){
          setTimeout(function(){
            location.href = '/{$this->request->controller}/await?{$querystring}';
          }, 1000);
        }
      </script>
    ";
    echo "<p>Loading... <span id=\"progress\">{$progress}</span> %</p>";
    if ((int)$progress == 100) {
      echo "<p>データ取得完了</p>";
      echo "<p>ページレンダリング中...しばらくお待ちください。</p>";
    }
  } else {
    $results = unserialize(file_get_contents(ROOT . DS . APP_DIR . DS . 'tmp' . DS .$tmpfile.'.txt'));
    unlink(ROOT . DS . APP_DIR . DS .'tmp' . DS . $tmpfile.'.txt');
    unlink(ROOT . DS . APP_DIR . DS .'tmp' . DS . $tmpfile.'__progress.txt');
    $this->setAction('export', $results);
  }
}

public function export($results) {
  $this->autoLayout = false;
  foreach ($results as $result) { /* ごにょごにょ */ }
  /* CSVで出すなりHTML出力するなり */
}

Cake1.xからの2.xへの移行

公式のアップグレードガイドを参考にしつつ https://goo.gl/FDscWi 大体のプロジェクトでは規約をちょい破ってるのでそこを手作業でかばあするhttps://goo.gl/cbrba5

  1. 不要ファイル殺す
  2. プラグインの対応チェック
  • composerにだいたい代替R
  • 優先度の見極め
  1. ディレクトリ構成の調整
  2. 3に伴う index.php なんかの調整
  3. 公式ガイド参考に2系のコアを設置
  4. upgradeシェル ぶったたく
  • ソース内の命名規則調整してくれる
  • 廃止関数の置き換えしてくれる
  • ファイル名は手動ぽい?
  • あとソースの調整も漏れが結構ある?
  1. コアファイル移動(5)に伴う index.php なんかの調整
  2. 自動調整(シェル)できなかった箇所を手作業でなおす
  • $form-> などコンポーネント全て $this->Form などへ
  • 全廃止APIの書き換え $this->Auth->user();$this->Auth->login();
  • 自作Component調整
    • ロード順序別メソッドはやす initialize() とか
    • オートロード規約に従っている → App::uses() で読む
    • オートロード規約に従っていない → App::import() で読む
  • 自作 Helper, Behavior も調整必要
  • $this->data > $this->request->data
  • $this->params['url']['url'] > $this->request->url
  • $this->params['contoller'] > $this->request->controller
  • $this->params['action'] or $this->action > $this->request->action
  • $this->params['pass'] or $this->passedArgs > $this->request->pass
  • $this->params['named'] or $this->passedArgs > $this->request->named
  • $this->request->params > $this->request->query
  • $this->header > $this->request->header
  • find($conditions) > find('all', $conditions)
  • App::import(モデル) > ClassRegistry::init()
  • $this->cakeError > Exception, ExceptionRender 利用
  • $this->log() > CakeLog::config 利用
/*
Sample Common CRUD API in CakePHP2 AppController
===
RESTっぽいやつ。
ControllerをApiにしてView側JSからAjaxする想定。

## Auth認証
リダイレクトが発生するとAjaxはセキュリティの都合上大体無効化される。よって  `$this->Auth->allow('action_name');` とかでまずは認証スルーさせる。手書きで二重チェックしたり Flash 仕込んだりしてるときは勿論そっちもスルーするように。その後認証が必要なケースのみ api メソッド(アクション)内で `$this->Auth->loggedIn()` とかで振り分け。

また、認証の是非 `$this->Auth->loggedIn()` についてはCake側でCookie/Sessionで管理しており通常のJS経由Ajaxなら問題なくログイン済みかどうかの判定が行われる。が、DHC RestClient なんかのブラウザツールだとはじかれるので注意。(ブラウザツールでデバッグするときはAjaxに対するAuth認証を解除する必要あり。

## JSONレスポンスにするために
加えて、ヘッダーにコンテンツタイプJSON指定してjson_encodeしたいところだけど `header()` 自体がCake制御下なので `return new CakeResponse()` してやる。これ3系だとレスポンスタイプ指定できるよね。もしかして2系でもできたのかな。
*/


/**
 * [api] ajax api base method
 * @param str $request
 * @return void
 * @throws BadRequestException
 */
public function api() {
  $this->autoRender = false;
  $allowedMethods = array(
    'get'    => 'get',
    'post'   => 'post',
    'put'    => 'put',
    'delete' => 'delete',
  );
  if ( /* remove this in debug by DHC REST Client */
    !$this->Auth->loggedIn() || 
    !$this->request->is('ajax') || 
    strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) != 'xmlhttprequest' || 
    empty($method = array_search(strtolower($_SERVER['REQUEST_METHOD']), $allowedMethods))
  ) {
    throw new BadRequestException();
  }
  return $this->{'_'.$method}($this->request);
}

/**
 * print json response
 * @param array $results
 * @param mix   $debug
 * @param string $successMessage
 * @param string $errorMessage
 * @return json CakeResponse [bool $status, int $code, str $message, array $response, mix $debug]
 */
protected function _printJsonResponse(
  $results,  // data type of array
  $debug=null,       // unset this param when product mode
  $successMessage='processing done.',
  $errorMessage='error has occurred.'
) {
  if($status = !empty($results)){
    $code = 200;
    $message = $successMessage;
    $response = $results;
  } else {
    $code = 404;
    $message = $errorMessage;
    $response = [];
  }
  if (!Configure::read('debug')) { unset($debug); }
  return new CakeResponse(array('type' => 'json', 'body' => json_encode(compact('status', 'code', 'message', 'response', 'debug'))));
}

/**
 * [api:get] find existing resource 
 * @param array $this->request
 * @return json CakeResponse
 */
private function _get($request) {
  unset($request->query['url']);
  if ( /* use findAllBy() magic method */
    // algorithm of to adjust query-string for findAllBy()
    // ["url?findAllBy=Id&args=8" => "findAllById(8)"]
    isset($request->query['findAllBy'])) { 
    $findAllBy = $request->query['findAllBy'];
    $args = $request->query['args'];
    $rows = $this->{$this->modelClass}->{'findAllBy'.$findAllBy}($args);
  } else { /* use find('all') method with conditions */
    // algorithm of to adjust query-string for like-search
    // ['__'=>'.', '.like'=>' like', '*'=>%]
    foreach ((array)$request->query as $key => $value) {
      $adjustedKey = str_replace('.like', ' like', str_replace('__', '.', (string)$key));
      $adjustedValue = str_replace('*', '%', (string)$value);
      $conditions[] = array((string)$adjustedKey => (string)$adjustedValue);
    }
    // access to resource
    $options = (empty($request->query)) ? null : array('conditions' => $conditions) ;
    $rows = $this->{$this->modelClass}->find('all', $options);
  }
  foreach ((array)$rows as $row) { $results[] = $row[$this->modelClass]; }
  return $this->_printJsonResponse($results, $request, count($results).'items found.', 'not found.');
}

/**
 * [api:post] create new resource
 * @param array $this->request
 * @return json CakeResponse
 */
private function _post($request) {
  $params = array(); //create record's param
  $saveData = array($this->modelClass => $request->data);
  $this->{$this->modelClass}->create();
  $this->{$this->modelClass}->save($saveData);
  $id = $this->{$this->modelClass}->id;
  $newRecord = $this->{$this->modelClass}->findById($id);
  $results = $newRecord[$this->modelClass];
  return $this->_printJsonResponse($results, $request, 'id:'.$id.' was created.', 'item not created.');
}

/**
 * [api:put] update new resource
 * @param array $this->request
 * @return json CakeResponse
 */
private function _put($request) {
  if ($id = $request->data('id')) {
    $params = array(); 
    $saveData = array($this->modelClass => $request->data);
    $this->{$this->modelClass}->save($saveData);
    $newRecord = $this->{$this->modelClass}->findById($id);
    $results = $newRecord[$this->modelClass];
  }
  return $this->_printJsonResponse($results, $request, count($results).' items updated.', 'item not updated.');
}

/**
 * [api:delete] delete exists resources
 * @param array $this->request
 * @return json CakeResponse
 */
private function _delete($request) {
  if ($request->data('deleteAll')) { /* branch of delete or deleteAll */
    $conditions = array(); //use $this->deleteAll()
      foreach ((array)$request->data as $key => $value) {
        if ($key == 'deleteAll') continue;
        $adjustedKey = str_replace('__', '.', (string)$key);
        $conditions[] = array((string)$adjustedKey => (string)$value);
      }
    $status = $this->{$this->modelClass}->deleteAll($conditions);
  } else {
    if ($id = $request->data('id')) {
      $dependents = $request->data('dependents') ? true : false ;
      $status = $this->{$this->modelClass}->delete($id, $dependents);
    } else {
      $status = false;
    }
  }
  return $this->_printJsonResponse($status, $request, 'id:'.$id.' was deleted.', 'item not deleted.');
}