CakePHPの勉強会で、英語に負けずコードをばんばん読んでいこう!という話があったので、Bakeryの中を覗いて見つけたソースを動かすというのをやってみました。
今回はCSVファイルをfind一発で読んでしまおうというヤツです。
元はSiegfriedHirschさんが作成されました。
CSVファイルをデータベースっぽくする為に、いくつか準備をします。
CakePHPの勉強会で、英語に負けずコードをばんばん読んでいこう!という話があったので、Bakeryの中を覗いて見つけたソースを動かすというのをやってみました。
今回はCSVファイルをfind一発で読んでしまおうというヤツです。
元はSiegfriedHirschさんが作成されました。
CSVファイルをデータベースっぽくする為に、いくつか準備をします。
1./models/datasources/csv_source.phpを設置する。
下記のファイルになります。
<?php
/**
* CSV class
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @author Siegfried Hirsch <siegfried.hirsch@gmail.com>
* @copyright Copyright 2008-2009, Siegfried Hirsch
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
* @created Januar 21, 2009
* @version 1.0
**/
class CsvSource extends DataSource {
/**
* Description string for this Data Source.
*
* @var unknown_type
*/
var $description = "CSV Data Source";
var $delimiter = ';'; // delimiter between the columns
var $maxCol = 0;
var $fields = null; // fieldnames
var $handle = false; // handle of the open csv file
var $page = 1; // start always on the first page
var $limit = 99999; // just to make the chunks not too big
/**
* Default configuration.
*
* @var unknown_type
*/
var $__baseConfig = array(
'datasource' => 'csv',
'path' => '.', // local path on the server relative to WWW_ROOT
'extension' => 'csv', // file extension of CSV Files
'readonly' => true, // only for reading
'recursive' => false, // only false is supported at the moment
);
/**
* Constructor
*/
function __construct( $config = null, $autoConnect = true ){
// Configure::write('debug', 1);
$this->debug = Configure::read( 'debug' ) > 0;
$this->fullDebug = Configure::read( 'debug' ) > 1;
// debug($config);
parent::__construct( $config );
if( $autoConnect ){
return $this->connect();
} else {
return true;
}
}
/**
* Connects to the mailbox using options in the given configuration array.
*
* @return boolean True if the mailbox could be connected, else false
*/
function connect() {
$config = $this->config;
$this->connected = false;
uses( 'Folder' );
if( $config['readonly'] ){
$create = false;
$mode = 0;
} else {
$create = true;
$mode = 0777;
}
//$config['path'] = WWW_ROOT . $config['path'];
$config['path'] = TMP . $config['path']; // Added by Imaoka on 26-Jun-2009
// debug($config['path']);
$this->connection = &new Folder( $path = $config['path'], $create, $mode );
if( $this->connection ){
$this->handle = false;
$this->connected = true;
}
return $this->connected;
}
/**
* listSources
*
* @author: SHirsch
* @created: 21.01.2009
* @return array of available CSV files
*/
function listSources() {
$config = $this->config;
if( $this->_sources !== null ){
return $this->_sources;
}
if( $config['recursive'] ){
// not supported yet -> has to use Folder::findRecursive()
} else {
// すべてにCSVファイル名を取得し、拡張子を省いてテーブル名にしたものを$listに格納する
// list all .csv files and remove the extension to get only "tablenames"
$list = $this->connection->find( '.*' . $config['extension'], false );
foreach( $list as &$l ){
if( stripos( $l, '.' . $config['extension'] ) > 0 ){
$l = str_ireplace( '.' . $config['extension'], '', $l );
}
}
$this->_sources = $list;
}
//debug( $list );
return $list;
}
/**
* Convenience method for DboSource::listSources(). Returns source names in lowercase.
*
* @return array
*/
function sources( $reset = false ){
if( $reset === true ){
$this->_sources = null;
}
return array_map( 'strtolower', $this->listSources() );
}
/**
* Returns a Model description (metadata) or null if none found.
* デリミッタ、列数、各列名をセット。各列名を返す。
*
* @return mixed
**/
function describe( $model ){
//debug( $model->table );
$this->__getDescriptionFromFirstLine( $model );
//debug( $this->fields );
return $this->fields;
}
/**
* __getDescriptionFromFirstLine and store into class variables
*
* @author: SHirsch
* @created: 21.01.2009
*
* @param $model
* @set CsvSource::fields array with fieldnames from the first line
* @set CsvSource::delimiter char the delimiter of this CSV file
*
* @return true
*/
private function __getDescriptionFromFirstLine( $model ){
$config = $this->config;
//$config['path'] = WWW_ROOT . $config['path']; // Added by Imaoka on 26-Jun-2009
$config['path'] = TMP . $config['path']; // Modified by Imaoka on 26-Jun-2009
$filename = $model->table . "." . $config['extension'];
$handle = fopen( $config['path'] . DS . $filename, "r" );
$line = rtrim( fgets( $handle ) ); // remove \n\r
$data_comma = explode( ",", $line );
$data_semicolon = explode( ";", $line );
if( count( $data_comma ) > count( $data_semicolon ) ){
$this->delimiter = ',';
$this->fields = $data_comma;
$this->maxCol = count( $data_comma );
} else {
$this->delimiter = ";";
$this->fields = $data_semicolon;
$this->maxCol = count( $data_semicolon );
}
fclose( $handle );
return true;
}
/* close
**
** @created: 21.01.2009 14:59:08
**
*/
function close(){
if( $this->connected ){
if( $this->handle ){
@fclose( $this->handle );
$this->handle = false;
}
$this->connected = false;
}
}
/**
* The "R" in CRUD
*
* @param Model $model
* @param array $queryData
* @param integer $recursive Number of levels of association
* @return unknown
*/
function read( &$model, $queryData = array(), $recursive = null ){
$config = $this->config;
//$config['path'] = WWW_ROOT . $config['path']; // Added by Imaoka on 26-Jun-2009
$config['path'] = TMP . $config['path']; // Modified by Imaoka on 26-Jun-2009
// デリミッタ、列情報の取得
$this->fields = $this->describe( $model ); // Added by Imaoka on 26-Jun-2009
$filename = $config['path'] . DS . $model->table . "." . $config['extension'];
if( $this->handle === false ){
$this->handle = fopen( $filename, "r" );
}
$queryData = $this->__scrubQueryData( $queryData );
// get the limit
if( isset( $queryData['limit'] ) && !empty( $queryData['limit'] ) ){
$this->limit = $queryData['limit'];
//debug( $this->limit );
}
// get the page#
if( isset( $queryData['page'] ) && !empty( $queryData['page'] ) ){
$this->page = $queryData['page'];
//debug( $this->page );
}
if( empty( $queryData['fields'] ) ){
$fields = $this->fields;
$allFields = true;
} else {
$fields = $queryData['fields'];
$allFields = false;
$_fieldIndex = array();
$index = 0;
// generate an index array of all wanted fields
foreach( $this->fields as $field ){
if( in_array( $field, $fields ) ){
$_fieldIndex[] = $index;
}
$index++;
}
}
$lineCount = 0;
$recordCount = 0;
$resultSet = array();
// Daten werden aus der Datei in ein Array $data gelesen
// Modified by Imaoka on 26-Jun-2009
//while( ( $data = fgetcsv( $this->handle, 8192, $this->delimiter ) ) !== FALSE ){
while( ( $data = fgetcsv_reg( $this->handle, 8192, $this->delimiter ) ) !== FALSE ){
if( $lineCount == 0 ) {
// throw away the first line
$lineCount++;
continue;
// $_page = 1;
} else {
// compute the virtual pagenumber
// Modified by Imaoka on 26-Jun-2009
//$_page = floor( $lineCount / $this->limit ) + 1;
$_page = floor( $lineCount / $this->limit );
if( $lineCount % $this->limit > 0 )
$_page++;
// do have have reached our requested page ?
if( $this->page > $_page ){
$lineCount++;
continue;
}
// skip over records, that are not complete
if( count( $data ) < $this->maxCol ){
$lineCount++;
continue;
}
$record = array();
if( $allFields ){
$i = 0;
$record['id'] = $lineCount;
foreach( $fields as $field ){
$record[$field] = $data[$i++];
}
$resultSet[] = $record;
} else {
$record['id'] = $lineCount;
if( count( $_fieldIndex ) > 0 ){
foreach( $_fieldIndex as $i ){
$record[$this->fields[$i]] = $data[$i];
}
}
$resultSet[] = $record;
}
unset( $record );
// now count every record
$recordCount++;
$lineCount++;
// is our page filled with records, then stop
if( $recordCount >= $this->limit ){
break;
}
}
}
$result[$model->table] = $resultSet;
return $result;
}
/**
* Private helper method to remove query metadata in given data array.
*
* @param array $data
* @return array
*/
function __scrubQueryData( $data ) {
foreach( array( 'conditions', 'fields', 'joins', 'order', 'limit', 'offset', 'group') as $key ){
if( !isset( $data[$key] ) || empty( $data[$key] ) ){
$data[$key] = array ();
}
}
return $data;
}
}
?>
2./app/config/database.phpに追記する。
CSV用のデータソースを定義します。
var $csv = array(
'datasource' => 'csv',
'path' => 'csv', // local path on the server
'extension' => 'csv', // file extension of CSV Files
'readonly' => true, // only for reading
'recursive' => false, // only false is supported at the moment
);
datasource…CSVファイル用なので、’csv’としてください。
path…これは、読み込むCSVファイルを置くフォルダ名です。ソースコードも参照して、適宜変更可です。
extension…CSVファイルの拡張子なので、’csv’です。
readonly…読み込むだけなので、trueです。
recursive…リレーショナルではないので、falseです。
3.CSVファイルを読み込むモデルを用意します。
読み込むCSVファイルは、それ自体をDBテーブルのように扱いますので、ファイル名をテーブル名のように命名し、普通のモデルファイルを用意します。
そのモデルを使用するコントローラ、モデルを使用して取得したデータを表示するビューも用意しました。
それぞれのサンプルコード・ファイル名は下記になります。
モデル名:Price
モデルファイル名:/models/price.php
コントローラ名:prices_controller.php
ビューファイル名:/views/prices/read.php
CSVファイル名:/app/tmp/csv/prices.csv (複数形です。テーブル名と同じ)
CSVファイルの中身は下記です。
1行目は列名、デリミッタはカンマ、ファイルの文字コードはUTF-8、改行コードはLFです。
スクリプトでファイルをアップロードすることを想定し、CSVファイルの置き場所は/tmpの中に/csvというフォルダを作成してやることにしました。
id,name,dep,device,favourite,price
1,山田太郎,代表,自転車,サボテン,1000
2,鈴木一郎,技術,電車,タバコ,1200
3,福山雅治,技術,電車,じゃがりこ,600
4,木村拓哉,営業,電車,デニム,750
5,岡田准一,技術,自転車,お酒,850
4.モデルを書きます。
モデルでは、’csv’というデータソースを使用することを宣言します。
コードは下記。
<?php
class Price extends AppModel {
var $name = 'Price';
var $useDbConfig = 'csv';
var $useTable = false;
}
?>
5.コントローラを書きます。
Priceモデルの使用を宣言し、findでデータを読み込みます。
<?php
class PricesController extends AppController
{
var $name = 'Prices';
var $uses = array( 'Price' );
function read(){
$list = $this->Price->find( 'all' );
$this->set( 'list', $list['prices'] );
}
?>
6.ビューを書きます。
取得したデータの確認に、テーブルに吐き出すように書いています。
<h2>CSVデータ一覧</h2>
<table>
<tr>
<th>ID</th>
<th>名前</th>
<th>部署・役職</th>
<th>通勤手段</th>
<th>好きなもの</th>
<th>価格</th>
</tr>
<?php
foreach( $list as $data )
{
print <<<EOD
<tr>
<td>{$data['id']}</td>
<td>{$data['name']}</td>
<td>{$data['dep']}</td>
<td>{$data['device']}</td>
<td>{$data['favourite']}</td>
<td>{$data['price']}</td>
</tr>
EOD;
}
?>
</table>
さて、findした結果、データがどのように入ってきたのかを確認してみましょう。
そして、テーブルに表示すると下記のような感じです。
読み込む際のパラメータとしては、読み込み件数(limit)、件数指定が指定されている場合にどのページを読み込むか(page)、どの列を読み込むか(fields)の三つが使用可能です。
$list = $this->Price->find( 'all', array( 'limit' => 4, // read only 100 records
'page' => 1, // start at page 2, limit is needed to make sense
'fields' => array( 'id', 'name', 'price' )
)
);
CSVファイルの場所など少し気をつけるポイントはありますが、find一発で読み込めるのは便利です。
是非お試しください。