CakePHPのシェル機能で各種コントローラーやコンポーネントを読み込む

CakePHPのシェル機能の使い方って情報が少ない気がする。
ということでちょっとしたまとめ。

<?php
App::import('Core', 'Controller'); // コントローラーのベースクラス
App::import('Controller', 'App'); // AppController
App::import('Controller', 'UsersController'); // UsersController
App::import('Component', 'TestComp'); // コンポーネント
App::import('Component', 'MyPlugin.PluginComp'); // プラグインのコンポーネント

class SampleShell extends Shell {
    var $uses = array('User'); // モデルは普通にこれでOK

    function startup() {
        $this->Controller = new Controller();
        $this->AppController = new AppController();
        $this->UsersController = new UsersController();
        $this->TestComp = new TestCompComponent();
        $this->PluginComp = new PluginCompComponent();
    }

    function test() {
        // 後は下のようにメンバ変数から各コントローラーやコンポーネントにアクセス可能
        $this->TestComp->test();
        $this->UsersController->getTest();
        // モデルも普通に使えます
        $data = $this->User->find('all');
    }
}
?>

CakePHP1.2 画像のリサイズ・アップロード Imageコンポーネント

CakePHPで使える画像リサイズ用のImageコンポーネントを作ったので公開してみます。
コンポーネントを作成するに当たって下記のサイトを参考にさせていただきました。
CakePHP1.2 画像のアップロード(#02)
画像アップロード処理のセキュリティホールをふさぐ(CakePHP修行 #37) | IDEA*IDEA

それではここで、作成したImageコンポーネントを用いて画像のアップロード機能を作成していこうと思います。

まず仕様は以下のものとします。

・アップロード可能な画像はJPEG/GIF/PNG
・アップロード可能なファイルのサイズは500KByte以下
・画像の形式チェック、ファイルサイズの制限はバリデーションによって行う
・アップロードした画像は指定した幅、高さで縮小される
・リサイズした画像は"webroot/img/users"以下に保存する


画像をアップロードするためのフォームを作成
views/users/upload.ctp

<?=$form->create('User', aa('url',aa('controller','users', 'action','upload'), 'type','file'))?>
<input type="hidden" name="MAX_FILE_SIZE" value="500000" />
画像:<?=$form->file('User.image_name')?>
<?=$form->error('User.image_name', null, array('escape' => false))?>
<?=$form->submit('アップロード')?>
<?=$form->end()?>


コンポーネントの作成
controllers/components/image.php

<?php
class ImageComponent extends Object {

    /**
     * 画像のリサイズ・保存
     *
     * @param string $srcImagePath 絶対パス
     * @param string $saveDirPath 絶対パスかwebroot/img/からのパス '/'と指定した場合webroot/img/となる
     * @param string $newImageName 保存画像名
     * @param int $newWidth
     * @param int $newHeight
     * @param boolean $aspectRatio 画像横縦比の維持フラグ true:維持する false:維持しない
     * @param boolean $del ソース画像の削除フラグ
     * @return string $newImageName 画像名
     */
    function resize($srcImagePath, $saveDirPath, $newImageName, $newWidth, $newHeight, $aspectRatio = false, $del = false) {
        $imageInfo = getimagesize($srcImagePath);
        $exts = array(1 => 'gif', 2 => 'jpeg', 3 => 'png');
        $srcWidth  = $imageInfo[0];
        $srcHeight = $imageInfo[1];
        $imageType = $exts[$imageInfo[2]];

        $saveDirPath = $this->_getPath($saveDirPath);

        if ($aspectRatio) {
            if ($srcWidth < $srcHeight) {
                // 縦に長い場合
                $newWidth = ($newHeight * $srcWidth) / $srcHeight;
            } else {
                // 横に長い場合
                $newHeight = ($newWidth * $srcHeight) / $srcWidth;
            }
        }

        $createFunc = 'imagecreatefrom'.$imageType;
        $saveFunc = 'image'.$imageType;

        // 画像のリサイズ
        $srcImage = $createFunc($srcImagePath);
        $newImage = imagecreatetruecolor($newWidth, $newHeight);
        imagecopyresampled($newImage, $srcImage, 0, 0, 0, 0, $newWidth, $newHeight, $srcWidth, $srcHeight);
        if ($imageType == 'jpeg') {
            $newImageName .= ".jpg";
            $saveFunc($newImage, $saveDirPath.$newImageName, 100);
        } else {
            $newImageName .= ".{$imageType}";
            $saveFunc($newImage, $saveDirPath.$newImageName);
        }

        // ソース画像の削除
        if ($del) {
            $this->del($srcImagePath);
        }

        return $newImageName;
    }

    /**
     * 画像ファイルの削除
     * unlinkのラッパーメソッド
     *
     * @param string $imagePath 画像ファイルのパス 絶対パスかwebroot/img/からのパス
     * @return boolean
     */
    function del($imagePath) {
        $iamgePath = $this->_getPath($imagePath);
        if (file_exists($iamgePath)) {
            return unlink($iamgePath);
        } else {
            return false;
        }
    }

    /**
     * パスの取得
     *
     * @param $path
     * @return string $path
     */
    function _getPath($path) {
        // フルパスの場合はそのまま返す
        if (strpos($path, ':'.DS) === false && strpos($path, ':/') === false) {
            if (empty($path) || $path == '/') {
                $path = IMAGES;
            } else {
                $path = preg_replace("/^\//", '', $path);
                $path = IMAGES.$path;
                if (!preg_match('/\.[a-zA-Z]+$/', $path)) {
                    // ディレクトリパスの場合
                    $path .= !preg_match('/\/$/', $path) ? '/' : '';
                }
            }
        }
        $path = str_replace(DS, '/', $path);

        return $path;
    }
}
?>


コントローラーの作成
controllers/users_controller.php

<?php
class UsersController extends AppController {

    var $components = array('Image');

    function upload() {
        if (empty($this->data)) {
            // バリデーションの際に誤動作するため、画像が指定されていない場合はnullを入れておく
            if(empty($this->data['User']['image_name']['name'])) {
                $this->data['User']['image_name'] = null;
            }

            // 入力エラーがある場合は画面を再表示する
            $this->User->set($this->data);
            if ($this->User->validates() === false) {
                return;
            }

            // 画像のリサイズ・アップロード
            $newImageName = 'uploadTest';
            $this->data['User']['image_name'] = $this->Image->resize(
                $this->data['User']['image_name']['tmp_name'], // ソース画像
                '/users', // 画像の保存先 この場合は"webroot/img/users"に保存される
                $newImageName, // 保存名
                100, // リサイズ横幅
                100, // リサイズ縦幅
                true // 画像横縦比の維持フラグ
            );
            $this->flash('画像のアップロードが完了しました。');
        }
    }
?>


ここで、$this->data['User']['image_name']の中身は以下のようになっています。

Array
(
    [name] => 画像名
    [type] => 画像形式(image/pjpegなど)
    [tmp_name] => サーバーにアップされたテンポラリファイルのパス
    [error] => アップロードに関するエラーコード(0:成功 それ以外:失敗)
    [size] => アップロードされたファイルのバイトサイズ
)

画像形式やエラーコードの詳細に関しては下のサイトをご覧ください。
離れPHP島 - POSTのファイルアップロードによりPHPに渡されるスーパーグローバル変数


バリデーション用メソッドの追加
app_model.php

<?php
class AppModel extends Model {

    /**
     * 画像のMIME型のチェック
     *
     * @param array $data
     * @param array $exts
     * @return boolean
     */
    function imageMime($data, $exts) {
        if ($data['image_name']['error'] == 0) {
            foreach ($exts as $ext) {
                if (strpos($data['image_name']['type'], $ext) !== false) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * ファイルの有無のチェック
     *
     * @param array $data
     * @return boolean
     */
    function existsFile($data) {
        if ($data['image_name']['error'] == 0 && $data['image_name']['size'] != 0) {
            return true;
        }
        return false;
    }

    /**
     * ファイルサイズのチェック
     *
     * @param array $data
     * @param int $fileSize
     * @return boolean
     */
    function maxFileSize($data, $fileSize) {
        if ($data['image_name']['error'] == 0 && $data['image_name']['size'] < $fileSize) {
            return true;
        }
        return false;
    }
?>


バリデーションの設定
model/user.php

<?php
class User extends AppModel {

    var $name = 'User';

    var $validate = array(
        'image_name' => array(
            'imageMime' => array(
                'rule' => array('imageMime', array('jpeg', 'gif', 'png')),
                'message' => '適切な画像ファイルを指定してください',
                'last' => true,
            ),
            'existsFile' => array(
                'rule' => array('existsFile'),
                'message' => "画像ファイルが見つかりません<br>\nファイルを指定し直してください",
                'last' => true,
            ),
            'maxFileSize' => array(
                'rule' => array('maxFileSize', 500000),
                'message' => "画像ファイルのサイズが大きすぎます<br>\nファイルサイズは500KByteまでに抑えてください",
                'last' => true,
            ),
        ),
    );
}
?>

以上です。

コンポーネントのリサイズ処理の部分ですが、その部分だけ抜き出して別メソッドにすれば、別形式への画像の変換などにも対応できると思います。
改変や再利用などは完全に自由ですので、好きに使ってやってください〜

TOPページで表示しているタワーレコード音楽ニュースについて

「うたちゃく!」のトップページにて表示しているタワーレコードの音楽ニュースについて少し話しておこうと思います。
トップのニュースは、タワーレコードオンラインにて配信されているRSSを取得して表示しており、もちろんタワーレコードさんにはメールで表示の許可を貰った上で表示しています。
その際に表示を許可するために提示された条件を抜粋して載せさせていただきます。

タワーレコードRSS表示条件(問い合わせメールの返信から抜粋)
担当部署へ確認したところ個人のサイトであればタワーのニュースであることを
明示した上で、RSSのタワーサイトへのリンクのみを表示することを条件として
ご利用いただけます。

同じようにタワーレコードRSS表示を考えられている方は、上記の条件を参考に利用されるよう、お願いいたします。

うたちゃく!のサービスを開始しました

本日「うたちゃく!」のサービスを開始しました。
いや〜長かった!
制作期間自体は2ヶ月ほどだったんですが、その前の設計やらフレームワークの勉強やらをのんびりやってたせいで思いのほか時間がかかってしまった。
でもその分しっかりしたシステムを構築できたと自負してます。

とりあえず「うたちゃく!」のサービスの概要を説明しますと、

・新着チェック登録したアーティストの新着CDを表示
・ユーザーの所有しているCDの管理(所有管理)
・他ユーザーのプロフィールや所有CDを見る、などのSNS的なサービス


という風になっております。

「うたちゃく!」の利用手順としましては以下のようになってます。

1.ユーザー登録を行う
2.登録したアカウントでログインする
3.アーティスト一覧から新着チェックアーティストの登録を行う
4.所有しているCDを所有登録する
5.ユーザーページにて新着CDのチェックや所有CDの管理を行う


以上、「うたちゃく!」のサービスをざっと説明いたしました。
うたちゃく!‐音楽CDの新着チェック、音楽CDの所有管理サービス

CakePHPのソース内で見かけたPHPのテクニック

CakePHPのソースをトレースしていて気になったテクニックをメモ。

配列の末尾に値を加える

いわずもがな、array_pushで実現できることですが以下のコードでも可能。

<?php
$ary[] = "aaa";
?>

もしかしてよく知られてる?
この方が個人的には可読性が高いと感じたため、これからはarray_pushは使わずにこちらをつかっていこうと思います。

クラス内でメソッドを呼び出す際のテクニック

クラス内で自身のメソッドを呼び出す場合、通常は$this->sampleMethod($str)のようにして$this->の後にメソッド名と引数を指定してやれば該当するメソッドを呼び出すことが出来ます。
ですが、このメソッド名の部分に文字列を指定することも可能です。

<?php
class sample {
    function test() {
        // 通常のやり方
        $this->sampleMethod("aaa");
        // メソッド名に文字列を指定するやり方
        $methodName = "sampleMethod";
        $this->{$metodName}("bbb");
    }
    function sampleMethod($str) {
        print $str;
    }
}
?>

上記ソースのように{}(大かっこ)で文字列の入った変数を囲ってやれば、それがメソッド名と認識され、該当するメソッドを呼び出すことができます。
また、メンバ変数も同じように{}で囲ってやることによって呼び出すことができます。
ライブラリを作成する際に役に立ちそうですね。
これを使えば文字列でswtch文を分岐させそれぞれの分岐で違うメソッドを実行、といったような愚行を行わなくてよさそうです(苦笑)

MySQLの照合順序指定・UTF-8

utf8_general_ci と utf8_unicode_ci の違い - Ceekz Logs (Move to y.ceek.jp)
MySQLでのUTF-8の照合順序の種類を調べてみた。

まずMySQLでよく使われるUTF-8の照合順序の種類には大きくわけて3種類あり、それぞれの特性は以下のようになっています。

  • utf8_bin
    • LIKEなどの使用時に英字の大文字小文字が区別される
  • utf8_general_ci
    • LIKEなどの使用時に英字の大文字小文字が区別されない
    • 照合の速度に重きが置かれている
  • utf8_unicode_ci

ざっと調べた限りではutf8_binかutf8_general_ciを用途(大文字小文字のヒットのさせ方)によって使い分けてる方が多いみたいです。

CakePHPのValidate設定いろいろ

CakePHPのValidate関連の設定で分からない所が多々あったため調べてみた。

Validateのエラーメッセージ優先度を調整する

http://cakephp.blog16.jp/index.php/2008/07/24/p41:TITLE

<?php
'mail' => array(
    array(
        'rule' => VALID_NOT_EMPTY,
        'required' => true,
        'last' => true,
        'message' => '入力してください'
    ),
    array(
        'rule' => array('email'),
        'last' => true,
        'message' => 'メールアドレスが正しくありません'
    ),
),
?>

赤字で示したlastにtrueを指定すると、処理中のフィールドのエラー処理がその場で終了するため、他のエラーメッセージに上書きされることなく意図したエラーメッセージを表示することができます。

エラーメッセージを好きな場所に表示する

http://www.syuhari.jp/blog/archives/394:TITLE

<?php
echo $form->error('User.email'); 
?>

ViewにてFormヘルパーのerrorメソッドを使用すると、引数として指定したフィールドの
エラーメッセージを表示することが出来ます。
ちなみにデフォルトでのエラー表示をさせないようにするには、

<?php
echo $form->input('User.email', array('type'=>'text', 'error'=>false));
?>

のようにinputメソッドのオプションで'error'=>falseを指定してやればOKです。

全てのエラーメッセージを配列の形で取得する

上記のようにエラーメッセージを一つずつ指定して表示するのではなく、全てのエラーメッセージを配列で取得して、ループで回して表示したいという場合には、以下の方法でエラーメッセージを取得することができます。

<?php
// View内で全てのエラーメッセージを配列の形で取得する
$errorMessages = $this->validationErrors['モデル名']
?>

エラーメッセージはthisのvalidationErrorsの該当モデルのkeyに連想配列の形で格納されているため、上記の方法によって取得することが出来ます。