BEAR.Sunday で Twitter OAuth

2015/01/08追記 ココカラ-->

本エントリ内で紹介している ```src/Module/OAuth/Twitter{Module|Provider}.php``` は外部モジュールとして切り出し、 [ray/oauth-module](https://github.com/Ray-Di/Ray.OAuthModule) として公開しています。

<--ココマデ

----

つい昨日(2014/12/22)、[BEAR.Sunday v1.0.0-alpha がリリース](https://www.facebook.com/permalink.php?story_fbid=295995643923923&id=110116939178462)されました！

## 最初に

今ちょうどプライベートでBEAR.Sundayを使ったサービス開発を進めていて、TwitterへのOAuthという大きめの実装に着手しようとしていたので、この機会にAdventCalendarのエントリとして実装例を紹介します。

### BEAR.Sunday と PHPoAuthLib

- [BEAR.Sunday](http://bearsunday.github.io/)
	+ リソース指向のPHPフレームワーク
	+ DIとAOPをサポート
	+ [BEAR.Package](https://github.com/koriym/BEAR.Package)、[BEAR.Resource](https://github.com/koriym/BEAR.Resource)、[BEAR.Sunday](https://github.com/koriym/BEAR.Sunday)の3つのパッケージから構成される
	+ 2014/12/22に```v1.0.0-alpha```が[リリース](https://github.com/koriym/BEAR.Sunday/releases/tag/1.0.0-alpha)されましたが、自分のプロジェクトで使用しているバージョンが```v0.10.5```のため、本エントリではv0.10系を使用する前提で書いています

- [PHPoAuthLib](https://github.com/Lusitanian/PHPoAuthLib)
	+ PHP製のOAuthライブラリ
	+ Twitter以外にも[多くのサービス](https://github.com/Lusitanian/PHPoAuthLib/tree/master/examples)をサポートしている
	+ いくつか存在するPHPのOAuthライブラリの中で１番オブジェクト指向でしっかり設計されている(と思う(小並感))

### 本エントリで詳しく説明しないこと

- BEAR.Sunday(v0.10系)のプロジェクト作成方法

	```bash
	$ composer create-project bear/skeleton:~0.10 My.Project
	$ cd My.Project
	$ composer install
	```
+ BEAR.SundayのDI(Dependency Injection)の仕組み
+ OAuthの仕組み
+ Twitterアプリの作成方法（＝APIキーの取得方法）


## Twitterアプリの設定

Twitterアプリの[管理ページ](https://apps.twitter.com/)で **Website** と **Callback URL** を設定します。

![Pentan_Local___Twitter_Application_Management.png](https://qiita-image-store.s3.amazonaws.com/0/3899/111c5d7f-4fd0-f5ba-067f-134d3048d0b7.png "Pentan_Local___Twitter_Application_Management.png")


## プロジェクト構成

 編集/追加した部分以外は、基本的にComposerコマンドで作成したそのままのプロジェクト構成です。

![TwitterProvider_php_-__pentan__-_pentan_-____workspace_pentan_.png](https://qiita-image-store.s3.amazonaws.com/0/3899/680d0bf5-390a-1870-c34f-13071772ba8b.png "TwitterProvider_php_-__pentan__-_pentan_-____workspace_pentan_.png")

### PHPoAuthLibのインストール

**src/composer.json (編集)**

```diff
	"require": {
		"php": "5.5.*",
		"bear/package": "0.13.0",
+		"lusitanian/oauth": "~0.3",
		"ext-apcu": "*",
		"ext-mbstring": "*"
	},
```

### TwitterのAPIキーを定数として定義し、DIにより使用する

**var/conf/constants.php (編集)**

Twitter APIの```Consumer Key```,```Consumer Secret```と```Callback URL```を定数として定義し、以降のロジックからDIの仕組みにより使用します。

```diff
	'prod' => [
		// DB
		'master_db' => $masterDb,
		'slave_db'  => $slaveDb,
+		// Twitter API
+		'twitter_api' => [
+			'consumer_key'    => '{YOUR_CONSUMER_KEY}',
+			'consumer_secret' => '{YOUR_CONSUMER_SECRET}',
+			// OAuth時のコールバックURLパス
+			'oauth_callback_path' => '/oauth/twitter/callback',
+		],
	],
```
### PHPoAuthLibをDIする

**src/Module/OAuth/TwitterProvider.php (追加)**

PHPoAuthLibをBEAR.SundayのDIの仕組みから使うための初期化処理を記述します。


```php
<?php

namespace My\Project\Module\OAuth;

use OAuth\Common\Consumer\Credentials;
use OAuth\Common\Http\Uri\UriFactory;
use OAuth\Common\Storage\Session;
use OAuth\OAuth1\Service\Twitter;
use OAuth\ServiceFactory;
use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
use Ray\Di\ProviderInterface;

class TwitterProvider implements ProviderInterface
{
	/**
	 * @var string
	 */
	private $consumerKey;

	/**
	 * @var string
	 */
	private $consumerSecret;

	/**
	 * @var string
	 */
	private $oauthCallbackPath;

	/**
	 * @param array $apiConfig
	 *
	 * @Inject
	 * @Named("twitter_api")
	 */
	public function __construct($apiConfig)
	{
		$this->consumerKey    = $apiConfig['consumer_key'];
		$this->consumerSecret = $apiConfig['consumer_secret'];
		$this->oauthCallbackPath = $apiConfig['oauth_callback_path'];
	}

	/**
	 * @return \OAuth\OAuth1\Service\Twitter
	 */
	public function get()
	{
		// コールバックURLの生成
		$uri = (new UriFactory())->createFromSuperGlobalArray($_SERVER);
		$uri->setPath($this->oauthCallbackPath);
		$uri->setQuery('');
		$callbackUrl = $uri->getAbsoluteUri();

		$credentials = new Credentials(
			$this->consumerKey,
			$this->consumerSecret,
			$callbackUrl
		);

		// Twitter用のOAuthサービスインスタンスを生成
		return (new ServiceFactory())->createService('twitter', $credentials, new Session());
	}
}

```

**src/Module/OAuth/TwitterModule.php (追加)**

上で作成した```TwitterProvider```(のgetメソッドの戻り値)を```\OAuth\OAuth1\Service\Twitter```に注入(束縛)するための設定を記述します。

```php
<?php

namespace My\Project\Module\OAuth;

use OAuth\OAuth1\Service\Twitter;
use Ray\Di\AbstractModule;
use Ray\Di\Scope;

class TwitterModule extends AbstractModule
{

	protected function configure()
	{
		$this->bind(Twitter::class)->toProvider(TwitterProvider::class)->in(Scope::SINGLETON);
	}
}

```

**src/Module/App/Dependency.php (編集)**

```diff
    protected function configure()
    {
+		$this->install(new TwitterModule());
    }
```

**src/Module/AppModule.php (編集)**

依存をアプリにインストールします。

```diff
	protected function configure()
	{
		$this->install(new StandardPackageModule('Kawanamiyuu\Pentan', $this->context, dirname(dirname(__DIR__))));
		
		// override module
		// $this->install(new SmartyModule($this));
		
		// $this->install(new AuraViewModule($this));
		
		// install application dependency
-		// $this->install(new App\Dependency);
+		$this->install(new App\Dependency);
		
		// install application aspect
		 $this->install(new App\Aspect($this));
	}
}

```

**src/Inject/TwitterOAuthServiceInject.php (追加)**

プログラムからはこの```trait```をuseすることでOAuthロジックを使用します。```setTwitterOAuthService```で依存が注入されます。

```php
<?php

namespace My\Project\Inject;


use OAuth\OAuth1\Service\Twitter;
use Ray\Di\Di\Inject;

trait TwitterOAuthServiceInject
{
	/**
	 * @var Twitter
	 */
	protected $twitterOAuthService;

	/**
	 * @param Twitter $twitter
	 *
	 * @Inject
	 */
	public function setTwitterOAuthService(Twitter $twitter)
	{
		$this->twitterOAuthService = $twitter;
	}
} 

```


### Twitterの認証画面へのリダイレクト

**src/Resource/Page/Oauth/Twitter/Redirect.php (追加)**

Twitterの認証画面 **にリダイレクトする** ためのPageリソースです。
次のURLパスに対応します→ ```/oauth/twitter/redirect```

```php
<?php

namespace My\Project\Resource\Page\Oauth\Twitter;

use BEAR\Resource\ResourceObject;
use My\Project\Inject\TwitterOAuthServiceInject;

class Redirect extends ResourceObject
{
	use TwitterOAuthServiceInject;

	public function onGet()
	{
		// リクエストトークンを取得
		$requestToken = $this->twitterOAuthService->requestRequestToken()->getRequestToken();

		$url = $this->twitterOAuthService->getAuthorizationUri([
			'oauth_token' => $requestToken,
			'force_login' => 'true',
		]);

		// Twitterの認証画面へリダイレクト
		header('Location:' . $url);
		exit;
	}
} 
```

### Twitterの認証画面からのコールバック

**src/Resource/Page/Oauth/Twitter/Callback.php (追加)**

Twitterの認証画面 **からリダイレクトされる** Pageリソースです。
次のURLパスに対応します→ ```/oauth/twitter/callback```

```php
<?php

namespace My\Project\Resource\Page\Oauth\Twitter;

use BEAR\Resource\ResourceObject;
use My\Project\Inject\TwitterOAuthServiceInject;

class Callback extends ResourceObject
{
	use TwitterOAuthServiceInject;

        /**
         * @param string $oauth_token
         * @param string $oauth_verifier
         * @param string $denied
         */
	public function onGet($oauth_token = null, $oauth_verifier = null, $denied = null)
	{
		// Twitterの認証画面で[キャンセル]ボタンを押された場合
		if ($denied) {
			$this['is_authorized'] = false;
			return $this;
		}

		// アクセストークンを取得
		$accessToken = $this->twitterOAuthService->requestAccessToken(
			$oauth_token,
			$oauth_verifier
		);

		// 認証成功時
		$this['is_authorized'] = true;
		$this['token']         = $accessToken->getAccessToken();
		$this['token_secret']  = $accessToken->getAccessTokenSecret();
		$this['user_id']       = $accessToken->getExtraParams()['user_id'];
		$this['screen_name']   = $accessToken->getExtraParams()['screen_name'];
		return $this;
	}
} 
```

**src/Resource/Page/Oauth/Twitter/Callback.twig (追加)**

※注意※
ここではサンプルコードとしての便宜上、取得したトークンを画面に表示します。プロダクションコードではDBに保存後リダイレクトするなどして適切にハンドリングします。

```html
<html lang="ja">
<head>
<meta charset="utf-8" />
<style type="text/css">
	dt { font-weight: bold; }
	dt:after { content: ':'; }
</style>
</head>
<body>
{% if is_authorized %}
	<dl>
		<dt>Token</dt><dd>{{ token }}</dd>
		<dt>Token Secret</dt><dd>{{ token_secret }}</dd>
	</dl>
	<dl>
		<dt>user_id</dt><dd>{{ user_id }}</dd>
		<dt>screen_name</dt><dd>{{ screen_name }}</dd>
	</dl>
{% else %}
	<p>
		<a href="/oauth/twitter/redirect">リトライ</a>
	</p>
{% endif %}
</body>
</html>
```

## ローカルでの動作確認

１. ビルドインサーバを起動する

```bash
$ cd /path/to/My.Project
# DI、AOPのためのコンパイルキャシュを削除
$ php bin/clear.php
$ php -S localhost:8080 -t var/www
```

２. ```http://localhost:8080/oauth/twitter/redirect``` にアクセスする

３. Twitterの認証画面にリダイレクトされる

![Twitter___アプリケーション認証 2.png](https://qiita-image-store.s3.amazonaws.com/0/3899/886cee58-736c-ef39-46d2-6bb0dc6a1b03.png "Twitter___アプリケーション認証 2.png")

４. [連携アプリを認証]ボタンを押す

５. ```http://localhost:8080/oauth/twitter/callback``` にリダイレクトされる

アクセストークンが表示されればOK

![Twitter___アプリケーション認証.png](https://qiita-image-store.s3.amazonaws.com/0/3899/a6a088e6-1792-35d1-2e4f-2669059a54b1.png "Twitter___アプリケーション認証.png")

## 最後に

今回作成したコードの大半がDIの仕組みに関する部分であったように、BEAR.Sundayを使った開発ではDIが実装の肝になってきます。
自分も今回初めて本格的に外部ライブラリをDIして使用するコードを書いてみてその仕組みの理解が進みました。

今後もBEAR.Sundayを使った開発を通してその設計思想を学んでいきたいと思います。
とりあえず次は今のプロジェクトのコードベースを```v1.0.0-alpha```に移行しないと(*´ڡ`●)