さくらのオブジェクトストレージをAWS SDK for PHP 2で
PHPからさくらのオブジェクトストレージを利用したくなり、AWS SDKを使えるか試したときのメモ。 AWS SDK for PHP v2 を使った。
list objects
composer.json
{ "require": { "aws/aws-sdk-php": "2.*" } }
list_objects.php
<?php require 'vendor/autoload.php'; use Aws\S3\S3Client; $client = S3Client::factory([ 'key' => '<<アクセスキー>>', 'secret' => '<<シークレットキー>>', 'base_url'=> 'https://b.sakurastorage.jp/', //コンパネに表示されるURLからバケット名を除いたもの ]); $iterator = $client->getIterator('ListObjects', [ 'Bucket' => '<<バケット名>>', ]); foreach ($iterator as $object) { echo $object['Key'] . "\n"; }
put object
put_object.php
<?php require 'vendor/autoload.php'; use Aws\S3\S3Client; $client = S3Client::factory([ 'key' => '<<アクセスキー>>', 'secret' => '<<シークレットキー>>', 'base_url'=> 'https://b.sakurastorage.jp/', //コンパネに表示されるURLからバケット名を除いたもの ]); $iterator = $client->getIterator('ListObjects', [ 'Bucket' => '<<バケット名>>', ]); foreach ($iterator as $object) { echo $object['Key'] . "\n"; } </pre>
get object
get_object.php
<?php require 'vendor/autoload.php'; use Aws\S3\S3Client; $client = S3Client::factory([ 'key' => '<<アクセスキー>>', 'secret' => '<<シークレットキー>>', 'base_url'=> 'https://b.sakurastorage.jp/', //コンパネに表示されるURLからバケット名を除いたもの ]); $result = $client->getObject([ 'Bucket' => '<<バケット名>>', 'Key' => 'data.txt' ]); echo get_class($result['Body']) . "\n"; echo $result['Body'] . "\n";
delete object
delete_object.php
<?php require 'vendor/autoload.php'; use Aws\S3\S3Client; $client = S3Client::factory([ 'key' => '<<アクセスキー>>', 'secret' => '<<シークレットキー>>', 'base_url'=> 'https://b.sakurastorage.jp/', //コンパネに表示されるURLからバケット名を除いたもの ]); $result = $client->deleteObject([ 'Bucket' => '<<バケット名>>', 'Key' => 'data.txt', ]); var_dump($result);
参考
http://aws.amazon.com/jp/developers/getting-started/php/ http://docs.aws.amazon.com/aws-sdk-php/v2/api/class-Aws.S3.S3Client.html http://cloud-news.sakura.ad.jp/wp-content/uploads/2015/01/ObjectStorage_API.pdf
Yiiでも中間テーブルにアクセスしたい
またこのネタです。
YiiFrameworkのCManyManyRelationは中間テーブルへのアクセスをサポートしていません。ならばオレオレリレーションによってそれを可能にしてみます。前エントリのまとめと反するのですが、まあ、あそびばってことで。
実現のためにCJoinElementを改造します。まだまだ中途半端な状態ですが載せておきます。気がむいたら更新するかもしれません。
2012.10.25 実験段階 とりあえずレイジーローディング
リレーション
まずオレオレリレーションモデルです。
protected/components/ManyManyPivotRelation.php
<?php class ManyManyPivotRelation extends CManyManyRelation { public $pivotModel = null; public $pivotWith = array(); }
モデル
続いてモデル
<?php class Viewer extends CActiveRecord { public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return 'viewer'; } public function relations() { return array( 'movies_with_pivot' => array("ManyManyPivotRelation", 'Movie', 'viewer_watched_movie(viewer_id, movie_id)', 'pivotModel' => 'ViewerWatchedMovie', // <- 中間テーブルモデル 'pivotWith' => array('id','viewer_id', 'movie_id', 'liked',), // <-中間デーブルのカラム ), ); } }
viewer_watched_movie テーブルのPKはIDです。
<?php class ViewerWatchedMovie extends CActiveRecord { public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return 'viewer_watched_movie'; } }
<?php class Movie extends CActiveRecord { public $pivot; // <- ここに中間テーブルのモデルが入る public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return 'movie'; } }
CJoinElement
次はCJoinElementに手を入れます。hack begin - hack end を挿入してください。
リレーションがManyManyPivotRelationならば、selectするカラムを追加し、select結果からpivotインスタンスを生成し、子モデルの $record->pivot にセットしています。
YiiFramework/framework/db/ar/CActiveFinder.php
<?php class CJoinElement { private function applyLazyCondition($query,$record) { $schema=$this->_builder->getSchema(); $parent=$this->_parent; if($this->relation instanceof CManyManyRelation) { .... if($parentCondition!==array() && $childCondition!==array()) { $join='INNER JOIN '.$joinTable->rawName.' '.$joinAlias.' ON '; $join.='('.implode(') AND (',$parentCondition).') AND ('.implode(') AND (',$childCondition).')'; if(!empty($this->relation->on)) $join.=' AND ('.$this->relation->on.')'; $query->joins[]=$join; foreach($params as $name=>$value) $query->params[$name]=$value; } else throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.', array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name))); // hack begin ---------- if($this->relation instanceof ManyManyPivotRelation) { // add pivot columns // TODO table.column -> t0_c0 foreach ($this->relation->pivotWith as $col) { $query->selects[]=$joinAlias.'.'.$schema->quoteColumnName($col); } } // hack end ---------- } else { .... } } private function populateRecord($query,$row) { .... if(isset($this->records[$pk])) $record=$this->records[$pk]; else { $attributes=array(); $aliases=array_flip($this->_columnAliases); foreach($row as $alias=>$value) { if(isset($aliases[$alias])) $attributes[$aliases[$alias]]=$value; } $record=$this->model->populateRecord($attributes,false); foreach($this->children as $child) { if(!empty($child->relation->select)) { $record->addRelatedRecord($child->relation->name,null,$child->relation instanceof CHasManyRelation); } } $this->records[$pk]=$record; // hack begin ---------- if($this->relation instanceof ManyManyPivotRelation) { // make pivot instance // TODO t0_c0 -> column_name $p = new $this->relation->pivotModel; $record->pivot = $p->populateRecord($row,false); } // hack end ---------- } .... } }
ビュー
<?php $viewers = Viewer::model()->findAll(); foreach($viewers as $viewer) { foreach ($viewer->movies_with_pivot as $movie) { echo $viewer->name . ($movie->pivot->liked ? ' liked ' : ' didn’t like ') . $movie->title; } }
結果
hoge didn’t like foo hoge liked bar piyo liked foo piyo didn’t like bar
Yiiで中間テーブルに属性を持たせたいときはHasMany, BelongsToを使う
YiiFrameworkのCManyManyRelationは中間テーブルへのアクセス手段を用意してくれません。なので、中間テーブルに属性を持たせたいときはHasManyとBelongsToを使うのが、今のところエレガントな解決方法のようです。
http://www.yiiframework.com/wiki/285/accessing-data-in-a-join-table-with-the-related-models/#c6007
http://www.yiiframework.com/wiki/285/accessing-data-in-a-join-table-with-the-related-models/#c7483
この問題のサンプルコードにはtypoがたくさんあるので、検証コードを載せておきます。
テーブル
CREATE TABLE IF NOT EXISTS `viewer` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `movie` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `viewer_watched_movie` ( `id` int(11) NOT NULL AUTO_INCREMENT, `viewer_id` int(11) NOT NULL, `movie_id` int(11) NOT NULL, `liked` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`), KEY `fk_viewer_watched` (`movie_id`), KEY `fk_movie_watched_by` (`viewer_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
データ
INSERT INTO `viewer` (`id`, `name`) VALUES (1, 'v1'), (2, 'v2'); INSERT INTO `movie` (`id`, `title`) VALUES (1, 'm1'), (2, 'm2'); INSERT INTO `viewer_watched_movie` (`id`, `viewer_id`, `movie_id`, `liked`) VALUES (1, 1, 1, 1), (2, 1, 2, 0), (3, 2, 1, 1);
モデル
<?php class Viewer extends CActiveRecord { public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return 'viewer'; } public function relations() { return array( 'watched_movies' => array(self::HAS_MANY, 'ViewerWatchedMovie', 'viewer_id'), ); } }
<?php class Movie extends CActiveRecord { public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return 'movie'; } }
<?php class ViewerWatchedMovie extends CActiveRecord { public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return 'viewer_watched_movie'; } public function relations() { return array( 'movie' => array(self::BELONGS_TO, 'Movie', 'movie_id'), ); } }
レイジーローディング
<?php $viewer = Viewer::model()->findByPk(1); foreach ($viewer->watched_movies as $watched) { echo $viewer->name . ($watched->liked ? ' liked ' : ' didn’t like ') . $watched->movie->title; }
イーガーローディング
<?php $viewers = Viewer::model()->with(array( 'watched_movies'=>array( 'condition'=>'watched_movies.liked=:liked', 'params'=>array(':liked'=>true), ), ))->findAll(); foreach ($viewers as $viewer) { foreach ($viewer->watched_movies as $watched) { echo $viewer->name . ($watched->liked ? ' liked ' : ' didn’t like ') . $watched->movie->title; } }
まとめのような
CActiveRecordの MANY_MANY, HAS_MANY...は単なるクラス名なので、次のようにオレオレリレーションのクラス名「OreOreRelation」でもいいんです。
<?php class Viewer extends CActiveRecord { .... public function relations() { return array( 'movies' => array('OreOreRelation', 'Movie', 'viewer_watched_movie(viewer_id, movie_id)', 'pivot_model'=>'ViewerWatchedMovie', ), ); } }
で、オレオレリレーションでSQL書き換えればいいかと思ったらさにあらず、SELECT..JOIN...生成はリレーションクラスでなく、CActiveFinderというかCJoinElementがやっています。
中間テーブルを取得するリレーションを作ってみたのですが、CJoinElementの 「if (relation instanseof CManyManyRelation) 」している辺りを探りながらパッチ当てることで、レイジーローディングだけどうにかできました。書くまでもなく茨の道です。
そんなわけで、いまのところエレガントな解決方法はHasMany, BelongsToだな、というところに落ち着きました。まえに弱点と書きましたが、今回のような設計で使ってくれと言うことなんだと思います。
laravelのpivotいいなと思ったの巻
yii ActiveRecord の弱点
yii ActiveRecord は、many-manyの中間テーブルに属性を付けにくい弱点があります。たとえばこんな問題とか。
Accessing data in a join table with the related models
いまだにキレイな解決方法がない。と思う。ほんとにないの?
laravel は pivot で解決
一方、laravelならこう書く。
テーブル
CREATE TABLE viewer ( id INT NOT NULL PRIMARY KEY, name VARCHAR(45)); CREATE TABLE movie ( id INT NOT NULL PRIMARY KEY, title VARCHAR(45)); CREATE TABLE viewer_watched_movie ( id INT NOT NULL, -- <--- ここ見逃さない viewer_id INT NOT NULL, movie_id INT NOT NULL, liked TINYINT(1), PRIMARY KEY (id), CONSTRAINT fk_viewer_watched FOREIGN KEY (movie_id) REFERENCES movie (id), CONSTRAINT fk_movie_watched_by FOREIGN KEY (viewer_id) REFERENCES viewer (id));
viewerモデル
<?php class Viewer extends Eloquent { public static $table = 'viewer'; public function movies() { return $this->has_many_and_belongs_to('Movie', 'viewer_movies') ->with('liked'); // <--- この with が大事 } }
movieモデル
<?php class Movie extends Eloquent { public static $table = 'movie'; }
コントローラー
<?php $viewer = Viewer::find(1); foreach ($viewer->movies()->get() as $movie) echo $viewer->name . ($movie->pivot->liked ? ' liked ' : ' didn’t like ') // <--- pivot で with したカラムを参照できる . $movie->title ."<br>" ;
うん、素直に書けた。しかも冒頭挙げたページのproblemコードと酷似したコードで書けてる。
注意
pivot テーブルに created_at, updated_at がないと、こんなエラーがでる。
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'viewer_movies.created_at' in 'field list'
そんなときは、 application/start.php にこれを書いておく。
Laravel\Database\Eloquent\Pivot::$timestamps = false;
pivot テーブルに created_at, updated_at を要求するのはお節介なような気が。
参考
Eloquent ORM - laravel 日本語ドキュメント
Disable timestamps in pivot table ? - github/laravel/issues/543
laravelを使おうかと思った理由の一つがコレ
laravel はじめました
公式ドキュメントを購入したのは先月だったか。ようやくはじめた。
はじめて書いたコード
見よう見まねで書いたログインフォームがこれ。 action_login と action_dologin で同じものをviewに渡そうとゴニョゴニョしていますね。
controllers/auth.php
<?php class Auth_Controller extends Base_Controller { public function action_login() { $input = array('username' => ''); return View::make('auth.login') ->with('input', $input) ->with('errors', new Laravel\Messages()); } public function action_dologin() { $input = Input::all(); $rules = array( 'username' => 'required', 'password' => 'required', ); $validation = Validator::make($input, $rules); if ($validation->fails()) { return View::make('auth.login') ->with('input', $input) ->with('errors', $validation->errors); } return "authed!"; } }
views/auth/login.blade.php
{{ Form::open('auth', 'post') }} {{ Form::text('username', $input['username']) }} @if ($errors->has('username')) {{ $errors->first('username') }} @endif {{ Form::password('password') }} @if ( $errors->has('password')) {{ $errors->first('password') }} @endif {{ Form::submit() }} {{ Form::close() }}
書き直したコード
ドキュメントとかソースとか読み直して、書き直したのがこれ。action_login からバリデーション失敗時のコードがなくなりすっきりしました。
controllers/auth.php
<?php class Auth_Controller extends Base_Controller { public function action_login() { return View::make('auth.login'); } public function action_dologin() { $input = Input::all(); $rules = array( 'username' => 'required', 'password' => 'required', ); $validation = Validator::make($input, $rules); if ($validation->fails()) { return Redirect::back() ->with_input() ->with_errors($validation); } return "authed!"; } }
views/auth/login.blade.php
{{ Form::open('auth', 'post') }} {{ Form::text('username', Input::old('username')) }} {{ $errors->first('username') }} {{ Form::password('password') }} {{ $errors->first('password') }} {{ Form::submit() }} {{ Form::close() }}
わかったこと
バリデーションエラーで前のページに戻るときは、Redirect::back()
あわせて with_input(), with_errors() も使う
Input::old() で前回入力した値を得られる
そのために with_input() しておく
view に空の errors を渡す必要はない
Viewコンストラクタ で $this->date['errors'] = new Messages() している。
エラーメッセージのキーは errors とするのが laravel の流儀っぽい
バリデーションメッセージ表示は {{$errors->first('fieldname')}}
エラーがなければ何もしないから if いらない
PHPTALのカスタムモディファイア if, eq を作ってみた
PHPTALと格闘すること数日。PHPTALにはif文っぽい表現をするにはPHPモディファイアを使わざるをえないことに悩みました。
condとeqがあればいいのに。なぜだろう。これが TAL-way なのか。でも欲しい。PHPモディファイアは使いたくない。でも。でも。。。
んが作った。
if, eq モディファイア
function phptal_tales_if( $src, $nothrow ) { list($if_part, $right) = explode('?', $src, 3); if (strpos($right,'|')) { list($then_part, $else_part) = explode('|', $right, 2); } else { $then_part = $right; $else_part = 'null'; } return '('.phptal_tales($if_part, $nothrow).') ? '.phptal_tales($then_part, $nothrow).' : '.phptal_tales($else_part, $nothrow).''; } function phptal_tales_eq( $src, $nothrow ) { list($left, $right) = explode(' ', $src, 2); return '('.phptal_tales($left, $nothrow).' == '.phptal_tales($right, $nothrow).' )'; }
condでなくifにした。
使い方
コントローラー
$people = array(); $people[] = (object)array("name"=>"foo", "phone"=>"01-344-121-021"); $people[] = (object)array("name"=>"bar", "phone"=>"05-999-165-541"); $people[] = (object)array("name"=>"baz", "phone"=>"01-389-321-024"); $people[] = (object)array("name"=>"quz", "phone"=>"05-321-378-654"); $template->people = $people; echo $template->execute();
テンプレート
<span tal:replace="if: eq:people/0/name people/0/name ? string:then " >default</span> :then <br/> <span tal:replace="if: eq:people/0/name people/1/name ? string:then " >default</span> :null <br/> <span tal:replace="if: eq:people/0/name people/0/name ? string:then | string:else" /> :then <br/> <span tal:replace="if: eq:people/0/name people/1/name ? string:then | string:else"/> :else <br/> <span tal:replace="if: exists:people/1 ? string: people/1 exists | string: people/1 not exists " >default</span> :people/1 exists <br/> <span tal:replace="if: exists:people/100 ? string: people/100 exists | string: people/100 not exists " >default</span> :people/100 not exists <br/>
というわけで
PHPTALの道から外れるかもしれないが作ってすっきりした。これならいろいろ表現できそう。もとからあるexistsも活かせるし、neもすぐ作れるね。
YiiFrameworkでPHPTALしてみた
PHPTALやってみようかと思ったので、YiiFrameworkで動くPHPTALViewRenderer作ってみた。
インストール
PHPTAL
phptalとかgithubからzipをダウンロードし解凍し、classes ディレクトリ下の PHPTALディレクトリと PHPTAL.phpを prorected/vendor/PHPTAL へコピーする。
PHPTALViewRenderer
次をコピペする。ETwigViewRenderer を参考に作成しました。
prorected/extension/PHPTALViewRenderer.php
<?php class PHPTALViewRenderer extends CApplicationComponent implements IViewRenderer { /** * @var string Path alias to PHPTAL.php */ public $PHPTALPathAlias = 'application.vendors.PHPTAL'; /** * @var string Template files extension */ public $fileExtension = '.html'; private $_basePath; private $_basePathLength; function init() { require_once Yii::getPathOfAlias($this->PHPTALPathAlias).'/PHPTAL.php'; Yii::registerAutoloader(array('PHPTAL', 'autoloadRegister'), true); $app = Yii::app(); /** @var $theme CTheme */ $theme = $app->getTheme(); if ($theme === null) { $this->_basePath = $app->getBasePath(); } else { $this->_basePath = $theme->getBasePath(); } $this->_basePathLength = strlen($this->_basePath) ; return parent::init(); } public function renderFile($context, $sourceFile, $data, $return) { $sourceFile = "protected/".substr($sourceFile, $this->_basePathLength); $phptal = new PHPTAL($sourceFile); $phptal->this = $context; foreach($data as $key => $val) { $phptal->$key = $val; } $result = $phptal->execute(); if ($return) { return $result; } echo $result; } }
設定
PHPTALViewRenderer を設定
prorected/config/main.php
'components'=>array( 'viewRenderer'=>array( 'class'=>'ext.PHPTALViewRenderer', 'fileExtension' => '.html', ),
PHPTALのサンプルを試す
コントローラー
PHPTALのサンプルのパクリ。Personクラス作るのが面倒なので(object)で代用。
protected/controllers/TestController.php
<?php class TestController extends Controller { public $layout='column1'; public function actionIndex() { $title = 'The title value'; $people = array(); $people[] = (object)array("name"=>"foo", "phone"=>"01-344-121-021"); $people[] = (object)array("name"=>"bar", "phone"=>"05-999-165-541"); $people[] = (object)array("name"=>"baz", "phone"=>"01-389-321-024"); $people[] = (object)array("name"=>"quz", "phone"=>"05-321-378-654"); $this->render('index', compact('title', 'people')); } }
ビュー
PHPTALのサンプルそのまんま。
protected/views/test/index.html
<?xml version="1.0"?> <html> <head> <title tal:content="title"> Place for the page title </title> </head> <body> <h1 tal:content="title">sample title</h1> <table> <thead> <tr> <th>Name</th> <th>Phone</th> </tr> </thead> <tbody> <tr tal:repeat="person people"> <td tal:content="person/name">person's name</td> <td tal:content="person/phone">person's phone</td> </tr> <tr tal:replace=""> <td>sample name</td> <td>sample phone</td> </tr> <tr tal:replace=""> <td>sample name</td> <td>sample phone</td> </tr> </tbody> </table> </body> </html>
結果
得られたhtmlの一部
layouts/main.php, layouts/column1.php の内部に↓が出力される。
<?xml version="1.0"?> <html> <head> <title>The title value</title> </head> <body> <h1>The title value</h1> <table> <thead> <tr> <th>Name</th> <th>Phone</th> </tr> </thead> <tbody> <tr> <td>foo</td> <td>01-344-121-021</td> </tr><tr> <td>bar</td> <td>05-999-165-541</td> </tr><tr> <td>baz</td> <td>01-389-321-024</td> </tr><tr> <td>quz</td> <td>05-321-378-654</td> </tr> </tbody> </table> </body> </html>
課題
- もっとPHPTALを学ばなきゃ
- CWebUserへのアクセスは $template->user = Yii::app()->user; とすれば可能
- layouts/main.php, layouts/column1.php をどうにかする
- renderPartialは macroを使うのかな
- formatterはモディファイアに組み込むのかな
- yii::t()は PHPTAL_TranslationService に組み込むのかな
- フラグメントキャッシュは PHPTAL_Trigger を使って実現するのかな
次は TAL-way じゃないのでサポートしなくていいよね
- widget
- CHTML