Modelで使用しているテーブルをダンプするので書いたコード。

CakePHPで特定のModelが使用しているテーブルをブラウザ経由でサクッとダンプしたい時のひな形です。めったに必要ないと思いますが、更新作業中にツールを切り替えずにダンプファイルを取得したいときなどに。

Core で何か提供されているAPIがないか探したのですが見当たらず、うろうろ検索してて良さげな1.2系のプラグインSQLダンプを生成するCakePHPプラグイン “Sql Dumper” があってこれをちょこっとカスタマイズしようと思ったのですが、触りだすと余計時間が掛かりそうだったので作りました。

いずれ上記のプラグインを使ってみたいのですが、それまでつなぎとしての雑記。

下記に書いたのは、あまりスマートじゃないですが mysqldump をシステムからコールした。対象となるデータベースはMySQL。PostgreSQLは無知なのでパス。MySQL以外だと例外が投げられます。

モデル

肝心な箇所は Behavior に書いてある。

class Post extends AppModel
{
	// 他のモデルでも使うので 下記のBehavior に移動した
	public $actsAs = array('TableDumpable');
}

コントローラ

ダンプファイルを読み込んで、それをそのまま出力。CakeResponse::download() に頑張ってもらった。

class PostsController extends AppController
{
	public function admin_download()
	{
		// ダンプファイルのダウンロードを試みる。
		try {
			$file = $this->Category->getDumpedFile();
		} catch (Exception $e) {
			// 処理するなら
		}
		if ($file && file_exists($file)) {
			$content = file_get_contents($file);
			if (($pos = strrpos($file, DS)) !== false) {
				$file = substr($file, $pos+1);
			}
			// CakeResponse::download() がいい仕事をしてくれます。
			$this->response->download($file);

			// ビューはただ単にコンテンツのみを出力する
			// レイアウトは流用してますが、要するに余分なものは何も出力しない。
			$this->layout = 'ajax';
			$this->set(compact('content'));

		} else {
			throw new NotFoundException();
		}
	}
}

レイアウト

元からある ajax.ctp をそのまま流用するのに気が引けるなら download.ctp などを作成。

<?php
echo $this->fetch('content');

ビューファイル

出力するだけ。

<?php
echo $content;

ビヘイビア

テーブルのスキーマとデータをダンプしてそのファイルパスを返すメソッドを実装。

// 上記の Post のBehavior。 getDumpedFilePath を実装。
class TableDumpableBehavior extends ModelBehavior
{

/**
 * Model 側でテーブルをダンプしてキャッシュディレクトリに保存し、
 * 成功した場合にファイルパスを返します。
 *
 * @param Model $model
 * @param string | null $filePath 一時保存するファイルパス。削除のロジックは書いていません。
 * @param string | boolean $compress 圧縮する場合の指定。 true で 'gzip' 圧縮。
 * @param string $extForCompress 圧縮した場合に追加する拡張子。'.zip' など。
 *                  ピリオドを含む。未指定の場合 'gzip' 圧縮なら '.gz' が、
 *                  それ以外は圧縮オプション名をそのまま使用。
 * @return string
 * @throws CakeException
 */
	public function getDumpedFilePath(Model $model, $filePath = null, $compress = false, $extForCompress = null)
	{
		// DATABASE_CONFIG オブジェクトの作成と、使用するデータの取得
		if (empty(ConnectionManager::$config)) {
			ConnectionManager::create();
		}
		$config = ConnectionManager::$config->{$model->useDbConfig};

		// MySQL 以外は例外を投げる
		if ($config['datasource'] !== 'Database/Mysql') {
			throw new CakeException('Datasource must be Database/Mysql');
		}

		// テーブル名の決定
		$table = $model->useTable;
		if ($model->tablePrefix) {
			$table = $model->tablePrefix . $table;
		} else {
			$table = $config['prefix'] . $table;
		}

		// データベース名の取得
		$db = $config['database'];

		// 保存するファイルパスの決定
		if ($filePath === null) {
			// 保存ファイルはデフォルトでキャッシュの 'models' ディレクトリに
			// DB名とテーブル名を連結させたもの + '.sql'。
			$filePath = CACHE . 'models' . DS . $db . '-' . $table . '.sql';
		}

		// 圧縮オプションの決定
		// 私の環境では gzip を使うのでこんな具合ですが、
		// システムに依存するので詳しくはそれぞれ調査して下さい。
		if ($compress) {
			if ($compress === true || $compress === 'gzip') {
				$ext = $extForCompress ? $extForCompress : '.gz';
				$filePath .= $ext;
				$pipe = ' --opt | gzip';
			} else {
				$ext = $extForCompress ? $extForCompress : $compress;
				$filePath .= $ext;
				$pipe = ' --opt | ' . Sanitize::escape($compress);
			}
		} else {
			$pipe = '';
		}

		App::uses('Sanitize', 'Utility');
		$user = Sanitize::escape($config['login']);
		$password = Sanitize::escape($config['password']);
		$db = Sanitize::escape($db);
		$table = Sanitize::escape($table);
		$filePath = Sanitize::escape($filePath);
		if (DS === '\\') {
			$filePath = str_replace(DS . DS, DS, $filePath);
		}

		// コマンドを組み立ててシステムで実行
		$cmd = sprintf('mysqldump -u "%s" -p"%s" %s %s %s> %s', $user, $password, $db, $table, $pipe, $filePath);

		$output = array(); // たぶん要素が追加されていると何らかのエラーですが、ダンプファイルは生成されているかもしれない
		$maybeError = null; // たぶん1以上の値が入ってるとエラーで、しかもダンプファイルは生成されていない
		exec($cmd, $output, $maybeError);
		if (empty($maybeError) && file_exists($filePath) &&  filesize($filePath)) {
			return $filePath;
		}
		return false;
	}
}