tjtjtjのメモ

自分のためのメモです

さくらのオブジェクトストレージを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 を要求するのはお節介なような気が。

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>

layouts/main.php, layouts/column1.php はとりあえずそのまんま使う。

結果

f:id:tjtjtjofthedead:20120404212858p:image:w640

得られた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 じゃないのでサポートしなくていいよね