tanaka's Programming Memo

プログラミングについてのメモ。

macからPHPでメール送信する(mailtrap.io の設定を追加)

mailtrap.io を利用する場合(2017/2/28 追記)

実際にメールを宛先に届けずに、デバッグ用の mailtrap.io に受信させる場合です。

  • https://mailtrap.io に接続
  • Sign in する。 GitHub アカウントがあれば、ログインした状態でログインできる
  • Inbox から使いたい設定をクリックして選択

f:id:am1tanaka:20170228145001p:plain

  • Integrations 欄の下のコンボボックスをクリックして一覧を表示して、[Postfix]を選択

f:id:am1tanaka:20170228144919p:plain

最後の postfix のリスタートコマンドは実行できないかもしれません。その場合は、以下で一度停止してから、再起動してください。

sudo postfix stop
sudo postfix start

以上で、設定の仕方が表示されます。それに従って設定を進めれば、メールが mailtrap.io へ送信されるようになります。


テストメールの送信

以下で、テストメールを送信します。

mail -s 'test' 宛先のメールアドレス
body
.
  • 登録しているGmailアドレスに「ログイン試行をブロックしました」というメールが届く
  • 詳細のところにあるリンクをクリック
  • 「安全性の低いアプリからのアカウントへのアクセスを変更する」の指示に従って、安全性の低いアプリのアクセスを[ON]にする

以上が設定できたら、改めてテストメールを送信します。今度は送信が成功します。

不要になったら、安全性の低いアプリのアクセスを[OFF]にしておくことをお勧めします。


PHPから送信

以下で、日本語のメールを送信します。
mb_send_mailの引数を、適宜、送りたい内容に書き換えます。

<?php
mb_language("Japanese");
mb_internal_encoding("UTF-8");
if (mb_send_mail(
    "TEST_TO_ADDR@domain.com",  // 送信先メールアドレス
    "件名",
    "本文",
    "From :".mb_encode_mimeheader("テスト送信元")."<TEST_FROM_ADDR@domain.com>"
))
{
    echo "成功\n";
}
else {
    echo "失敗\n";
}

PHPUnit+Seleniumで、ページに要素があるかを確認する方法

(サンプルで " が抜けていたのを修正 2017/2/27)

以下に、画面に要素があるかどうかを判断するサンプルコードを示します。

<?php
class EntryTest extends PHPUnit_Extensions_Selenium2TestCase {
    protected function setUp()
    {
        $this->setBrowser('firefox');
        $this->setBrowserUrl("http://www.google.co.jp");
    }

    public function testValidElement() {
        // ページ開始
        $this->url("http://www.google.co.jp");

        // IDがlst-ibの要素があるかを確認
        if (count($this->elements($this->using("id")->value("lst-ib"))) > 0) {
            echo "lst-ibはある\n";
        }
        // IDがnashiの要素がないかを確認
        if (count($this->elements($this->using("id")->value("nashi"))) === 0) {
            echo "nashiはない\n";
        }
    }
}
?>

usingで様々な要素を検索するときの指示の仕方は http://am1tanaka.hatenablog.com/entry/2015/04/03/000126

PHP DOMDocumentのloadHTML() ShiftJISでエラー

①などの文字が含まれるHTMLページをDOMDocumentのloadHTML()でDOMにしようとしたところ、以下のようなエラーが発生してしまいました。

DOMDocument::loadHTML(): input conversion failed due to input error, bytes 0x81 0xAE 0xE3 0x81

やりたいことは、ShiftJISのページをUTF-8にして、SimpleXMLElementに変換することです。

[PHP] DOMDocument#loadHTML()で、特定の文字が含まれていると正しいDOMを作ってくれない を拝読することで解決しました。

原因1

根本的な原因は、機種依存文字を利用したことです。しかし、①などはUTF-8にも定義されているのでエンコードの指定が合っていれば変換できます。

①などのWindows機種依存文字まで含めたエンコードは、 CP932 や sjis_win などと呼び、ShiftJISとは別に指定することができます。

解決策

$urlに読み込みたいWebページのURLが入っているとして、以下のようにして変換します。

<?php
// URLからHTMLの取得
$html = file_get_contents($url);

// windows用のSJISからUTF8に変換
$html = mb_convert_encoding($html, "UTF-8", "SJIS-win");
// HTMLのcharsetを変換に合わせて修正する
$html = str_replace("charset=shift_jis", "charset=utf-8", $html);

// HTMLをDOMに変換
$domDocument = new domDocument();
$domDocument->loadHTML($html);

// XMLに変換
$xmlstr = $domDocument->saveXML();

// SimpleXMLElementに読み込み
$xml = simplexml_load_string($xmlstr);

原因2

&記号が本文中などにあるとエラーになります。 & でエスケープが必要です。

おまけ

loadHTML()は、読み込ませるHTMLファイルのcharsetに合わせて文字コードを自動的にUTF-8に変換してくれます。そのため、特殊な文字が使われていないShiftJISのページなら、何も考えずにloadHTML()に突っ込んで大丈夫です。

JavaScriptとjQueryで様々な画面サイズを取得する

Webアプリでレイアウトを制御しようとすると様々な画面サイズを把握する必要があります。

iPhoneのシミュレーター上のSafariでの動作結果をまとめました。

取得方法

Webブラウザーのクライアント領域の論理ピクセル数

Webブラウザーが1画面で表示できる範囲です。
縦画面、横画面の状態に対応しています。

// 幅
window.innerWidth
// or
document.documentElement.clientWidth

// 高さ
window.innerHeight
// or
document.documentElement.clientHeight

Webページ全体の論理ピクセル数

Webブラウザーに表示しているページ全体の論理ピクセル数です。クライアントサイズより大きい場合は1画面に収まっていないのでスクロールするようになります。
縦画面、横画面の状態に対応しています。

// 幅
document.width

// 高さ
document.height

Webページのbodyの論理ピクセル数

Webページのbodyのサイズです。ほぼdocument.widthやdocument.heightと同じですが、positionスタイルがabsoluteの要素は含まれません。
縦画面、横画面の状態に対応しています。

// 幅
document.body.clientWidth
// or(jQueryの時)
$('body').width()

// 高さ
document.body.clientHeight
// or(jQueryの時)
$('body').height()

モバイル端末の画面の論理ピクセル数

Webアプリ開発では不要な情報だと思います。常に縦画面の状態での値を返します。

// 幅
screen.width
// or
window.parent.screen.width

// 高さ
screen.height
// or
window.parent.screen.height

モバイル端末の描画可能な論理ピクセル数

Webアプリ開発では不要な情報だと思います。常に縦画面の状態での値を返します。

// 幅
screen.availWidth

// 高さ
screen.availHeight

論理ピクセル数とは

スマートフォンなどのモバイル端末では、画面の解像度が短期間に激変します。例えば、最初のiPhoneでは3.5インチの画面で320x480ピクセルだったものが、iPhone6S Plusでは、5.5インチの画面で1,125x2,001ピクセルになりました(iPhone/iPad解像度(画面サイズ)早見表 - Qiita より)。画面サイズの拡大が1.6倍なのに対して、解像度は4倍近く増えています。1ピクセルの大きさが半分以下になったということです。

実際に画面を構成しているピクセル数を「物理ピクセル数」などと呼びます。物理ピクセル数で画面を設計すると、ボタンや文字などが以前の半分以下の大きさになってしまい、見づらく、操作も難しくなります。解像度の違いに合わせて個別のレイアウトを作成するのも手間がかかって仕方ありません。

そこでモバイル端末では、古い端末と同じようなピクセル数を持った仮想の画面を定義して、その上でレイアウトすることができます。仮想の画面のピクセル数を「論理ピクセル数」などと呼びます。

仮想の画面をどのようなサイズにするかはHTMLの「<meta name="viewport"・・・」で始まる行で調整します。

以下のようなHTMLにすると、解像度が違う種端末間でも同じような画面を設計することができます。shrink-to-fit=noは、iOS9.2で導入された設定です。これを指定しないと、ページの内容に応じて自動的にサイズが調整されてしまい、意図通りの制御が難しくなります。

AndroidiOSのどちらでもそのまま利用できます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no,shrink-to-fit=no" />
    <title></title>
</head>
<body>
<p>本文</p>
</body>
</html>

[user-scalable=no]は、ユーザーのピンチ操作などで、ブラウザーの拡大率が変更できないようにする設定です。これを設定するかは、どのようなアプリケーションを作成するかによります。Web地図のようにアプリ側でピンチ操作を利用する場合は、Webページが拡大するのか、Web地図が拡大するのかが混乱するので、この設定をします。

通常のWebページでは、[user-scalable=no]と[shrink-to-fit=no]はなくても良いかもしれません。状況に合わせて調整してください。

PHPの認証ライブラリ Sentinel

認証に関する様々な機能を提供してくれるPHPのライブラリ、CARTALYSTの「SENTINEL」をmacのローカル環境で使ってみます。

概要

以下、公式サイトのINTRODUCTIONの意訳。

モダンでフレームワークを選ばないAuthorization(認可)やAuthentication(認証)のパッケージです。権限設定、独自のハッシュアルゴリズム、そして追加のセキュリティ機能を持ちます。

パッケージは、FIG(Framework Interop Group)によるPSR-4(PHP Standard Recommendations - Autoloading Standard)に対応しており、PHPコード間で柔軟な制御ができます。

動作環境はPHP 5.4以上が必要で、オプションとしてフレームワーク統合を単純化するために、Laravel 5 FacadeとサービスProviderがバンドルされています。

インストール

Composerを利用するのが最も簡単です。

macなど、Composerがグローバルにインストールされている場合は、ターミナルで組み込みたいプロジェクトフォルダーに移動して、以下でインストールできます。

composer require cartalyst/sentinel "^2.0"

レンタルサーバー上など、composer.pharをダウンロードして利用する場合は以下の通りです。すでにcomposer.pharをダウンロード済みであれば、2行目だけで大丈夫です。

curl -sS https://getcomposer.org/installer | php
php composer.phar require cartalyst/sentinel "^2.0"

vendor/autoload.php ファイルを require することで、パッケージが自動的に読み込まれます(自動的に読み込む機能がPSR-4によるものです)

Nativeで利用する

今回はSlimPHPと組み合わせたいので、Nativeでセットアップします。

Sentinelは illuminate/database など、幾つかのライブラリを利用します。それらをインストールします。

- vendor/cartalyst/sentinel/composer.json に開発用の設定があるので、それを参考にしました

{
    "require": {
        "cartalyst/sentinel": "^2.0",
        "cartalyst/support": "~1.0",
        "illuminate/database": "~5.0",
        "illuminate/events": "^5.2",
        "illuminate/http": "~5.0",
        "illuminate/support": "~5.0",
        "php": ">=5.4.0",
        "paragonie/random_compat": "~1.1"
    }
}
  • 上書き保存する
  • ターミナルのプロジェクトフォルダーから以下を実行して必要なコンポーネントをインストール
composer update

データベースの準備

Sentinel用のデータベースとテーブル、操作するユーザーを作成しましょう。

  • MySQLサーバーが起動していなければ以下で開始する
mysql.server start
  • ターミナルで、プロジェクトフォルダーから以下を実行
mysql -u データベースの作成権限のあるユーザー -p
  • パスワードの要求があるので入力
  • MySQLにログインしたら、以下でデータベースを作成
CREATE DATABASE IF NOT EXISTS sentinel CHARACTER SET=utf8;
  • 作成したデータベースを利用
use sentinel;
  • 設定を読み込む。以下は、MySQLのバージョンが5.6以上の場合。それ以前の場合は「mysql.sql」に差し替える
SOURCE ./vendor/cartalyst/sentinel/schema/mysql-5.6+.sql;
  • Sentinelを操作するためのユーザーを作成する。以下で、localhostのみ有効なsentineluserという名前のユーザーを作成する
CREATE USER 'sentineluser'@'localhost' IDENTIFIED BY '設定したいパスワード';
  • アクセス用のユーザーにデータベース上でテーブルの操作権限を与える
GRANT SELECT,DELETE,INSERT,UPDATE,CREATE ON sentinel.* TO 'sentineluser'@'localhost';
quit

テストコードを作成

動作テストをするためのコードを用意します。プロジェクト用のフォルダーにsrcフォルダーを作成して、その中に tuto.php というファイルを作成して、以下のコードを入力してください。

<?php
// Import the necessary classes
use Cartalyst\Sentinel\Native\Facades\Sentinel;
use Illuminate\Database\Capsule\Manager as Capsule;

// Include the composer autoload file
require __DIR__.'/../vendor/autoload.php';

// Setup a new Eloquent Capsule instance
$capsule = new Capsule;

$capsule->addConnection([
    'driver'    => 'mysql',
    'host'      => 'localhost',
    'database'  => 'sentinel',      // データベース名を変更していたら合わせて変更
    'username'  => 'xxxx',          // xxxxを、データベースのユーザー名に変更
    'password'  => 'xxxxxx',        // xxxxxxを、データベースのパスワードに変更
    'charset'   => 'utf8',
    'collation' => 'utf8_unicode_ci',
]);

$capsule->bootEloquent();

// ユーザー情報
$credentials = [
    'email' => 'email@example.com',
    'password' => 'pass'
];

// 登録済みかを確認
$user = Sentinel::getUserRepository()->findByCredentials($credentials);
if (is_null($user)) {
    // 存在しない場合は、新規登録
    $user = Sentinel::register($credentials);
    print_r($user);
}
else {
    // 存在する場合は削除
    $user->delete();
    echo "user deleted".PHP_EOL;
}

?>
  • 16行目のusernameの'xxxx'と、17行目のpasswordの後ろの'xxxxxx'を、実際に作成したMySQLのユーザー名(今回は sentineluser )とパスワードに書き換える
  • メールのテストをする場合は、26行目と27行目のユーザー情報も変更する。今回のチュートリアルは変更しなくても動作する

以上が出来たら上書き保存をして、ターミナルから以下で実行します。

php ./src/tuto.php

初めて実行すると、ユーザーが未登録なので、指定のユーザーを登録して、登録情報を画面に表示します。

もう一度実行すると、ユーザーが存在するので、ユーザーを削除して、画面に「user deleted」と表示します。

何も表示されない場合は、データベースのユーザー名やパスワードが間違えている可能性があるので見直してください。

まとめ

以下をやりました。

  • Sentinelのインストール
  • Sentinel用のデータベースの準備
  • Sentinelを使ったユーザー登録と削除の動作チェック

あとは、公式ページの USAGE に掲載されているリファレンスやExampleを参照して、使いたい機能を利用しましょう。

参考URL

iPhone/iPadでタップが反応しないこととviewportの不具合対応

散歩リンクにおいて、iPhone/iPadだけ、以下の不具合が発生しました。

  • 操作説明で、画面をタップして先に進む操作ができなかった
  • 画面レイアウトが崩れていた

その対処でやったことです。

タップが効かない

原因は、 iOS で click イベントがわけのわからない動作をする件について - Qiita を読んで判明しました。まさに、bodyにクリックイベントを設定していました。

解決策として、クリックに反応するための透明のdiv要素で全画面を覆って、そこに先に進むためのクリックイベントを設定しました。

レイアウト崩れへの対応

インターフェースが画面内に収まらないという症状が出ました。多少の画面の狭さならば1画面に収まるレイアウトにしていたのですが、それが機能していませんでした。

iOS 9(iPhone)のSafariでviewportが効かない?ページが縮小表示されたり、挙動がおかしい場合の対策 で、原因と対処方法を発見しました。

iOS9.2からviewportに導入された「shrink-to-fit」項目が悪さをしていました。名前的に「画面にフィットするように縮小する」機能でしょう。これによって、意図した画面設定にならなかったようです。HTMLファイルのHEAD要素内のviewport設定の最後に、以下のように「shrink-to-fit=no」を追加することで治りました。

<meta name="viewport" content="initial-scale=1.0, width=device-width, user-scalable=no, shrink-to-fit=no" />

持ち越しの項目

最小の画面の高さを320pxで設計していたため、iPhone5以前のスマホでは、横画面で見るときにレイアウトがはみ出す可能性があります。シミュレーター上でははみ出しました。以下、iPhone4のブラウザの描画可能範囲です。

  • 横画面の時 480px, 232px
  • 縦画面の時 320px, 372px

修正にはレイアウトの設計の見直しが必要になるため、今回は持ち越しとしました。以降は、上記のサイズで最小サイズを設計しようと思います。

実機なしでのテスト方法

今回のバグチェックには実機は使わず(持ってないので)、Xcodeに付属のシミュレーターを利用してmac上で行いました。 - XcodeのiOSシミュレータを単体で起動させる方法

以下、起動方法です。

  • [アプリケーション]>[Xcode]右クリック>[パッケージの内容を表示]>[Contents]>[Developer]>[Applications]>[Simulator]で起動

あるいは

  • [Xcode]を起動>[Xcode]メニュー>[Open Developer Tool]>[Simulator]で起動

画面サイズが11インチのmac book airでは足りないので、[Window]>[Scale]で75%や50%など、適度なサイズに縮小すると便利です。

SlimPHP:Slim マイクロフレームワークで REST アプリケーションを作成する(5)複数のレスポンス・フォーマットのサポート

前へ


IBM developerWorks Slim マイクロフレームワークで REST アプリケーションを作成するを参考に、SlimPHPのスケルトンアプリから作成します。コードはSlimPHP3用に書き換えています。


SlimPHP:Slim マイクロフレームワークで REST アプリケーションを作成する(4)認証 - tanaka's Programming Memo からの続きです。

複数のレスポンス・フォーマットのサポート

リクエストされているContent-Typeに応じて、返すデータの書式を変更することができます。

以下、参考元のソースをSlim3ように書き換えたものです。 src/routes.php を開いて、「$app->get('/articles', ・・・」から始まるコードを以下のように変更してください。

// articlesへのルートを設定する
$app->get('/articles', function($request, $response, $args) {
    try {
        // 一覧を取得
        $articles = R::find('articles');

        // リクエストされたContent-Typeを取得
        $mediaType = $request->getHeader('Content-Type')[0];
        if ($mediaType == 'application/xml') {
            // XMLで結果を返す
            $xml = new SimpleXMLElement('<root/>');
            $result = R::exportAll($articles);
            foreach($result as $r) {
                $item = $xml->addChild('item');
                $item->addChild('id', $r['id']);
                $item->addChild('title', $r['title']);
                $item->addChild('url', $r['url']);
                $item->addChild('date', $r['date']);
            }
            echo $xml->asXml();
            // XMLを返すためのヘッダを設定
            return $response->withHeader('Content-Type', 'application/xml');
        } else if (($mediaType == 'application/json')) {
            // JSONでエンコードした結果を返す
            echo json_encode(R::exportAll($articles));
            // JSONを返すためのヘッダを設定
            return $response->withHeader('Content-Type', 'application/json');
        }
    }
    catch (Exception $e) {
        return $response
            ->withStatus(400)
            ->withHeader('X-Status-Reason', $e->getMessage());
    }
})->add(new CAuthentication());

動作テスト

動作を確認しています。

  • FirefoxでHTTP Resource Testを起動
  • URI欄に http://0.0.0.0:8080/articles と入力
  • Method欄を GET にする
  • Client Request欄から[Header]を選択
  • [Add]を押す
  • コンボボックスを[Content-Type]、[application/xml]に設定
  • 以上ができたら、[Submit]を押す

成功すると、一番下のBody欄にXMLで結果が表示されます。続けて、JSONもテストしましょう。

  • [application/xml]を[application/json]に変更
  • [Submit]を押す

結果がJSONを文字列にエンコードしたものがBody欄に表示されれば成功です。

POSTも対応させる

新規記事の追加時にも、XMLに対応させましょう。Content-Typeがapplication/xmlの場合は、simplexml_load_string()で送信されるデータをXMLで解釈して処理します。

データの返信も同様にXMLの機能を追加します。

src/routes.php を開いて、「$app->post('/articles', ・・・」から始まる行を以下のように修正してください。

// /articles フォルダーへのPOSTリクエストを登録
$app->post('/articles', function ($request, $response, $args) {
  try {
    $mediaType = $request->getHeader('Content-Type')[0];
    $body = $request->getBody();

    if (strpos($mediaType, 'application/xml') === 0){
      $input = simplexml_load_string($body);
    } elseif (strpos($mediaType, 'application/json') === 0) {
      $input = json_decode($body);
    }

    // articleレコードの作成と記録(ここはそのまま)
    $article = R::dispense('articles');
    $article->title = (string)$input->title;
    $article->url = (string)$input->url;
    $article->date = (string)$input->date;
    $id = R::store($article);

    // 結果を返す
    if (strpos($mediaType, 'application/xml') === 0) {
      $xml = new SimpleXMLElement('<root/>');
      $result = R::exportAll($article);
      foreach($result as $r) {
        $item = $xml->addChild('item');
        $item->addChild('id', $r['id']);
        $item->addChild('title', $r['title']);
        $item->addChild('url', $r['url']);
        $item->addChild('date', $r['date']);
      }
      echo $xml->asXml();
      return $response->withHeader('Content-Type', 'application/xml');
    } elseif (strpos($mediaType, 'application/json') === 0) {
      echo json_encode(R::exportAll($article));
      return $response->withHeader('Content-Type', 'application/json');
    }
  } catch (Exception $e) {
    return $response
        ->withStatus(400)
        ->withHeader('X-Status-Reason', $e->getMessage());
  }
});

動作テスト

FirefoxのHTTP Request Testを起動して、以下の通りテストしましょう。

<?xml version="1.0"?>
<root>
  <title>XML Add Test</title><url>http://0.0.0.0:8080/articles</url><date>2016-03-2</date>
</root>
  • [Headers]を押す
  • コンボボックスを[Content-Type]と[application/xml]にする
  • [Submit]を押す

以上で、成功すると、Bodyに新規に登録されたデータがXML形式で表示されます。itemタグとidが新しく追加されているのが確認できます。

引き続き、JSONのテストです。

  • Client Request欄の[Representation]を押す
  • テキストエリアを以下に書き換える
{"title":"JSON Add Test","url":"http:\/\/0.0.0.0:8080\/articles","date":"2016-03-2"}
  • [Headers]を押す
  • [application/xml]を[application/json]に変更
  • [Submit]を押す

以上で、JSONのデータが登録されます。成功すると、一番下のBody欄にJSONで新しく追加されたデータが表示されます。id要素が追加されているのが確認できます。

まとめ

以上で、簡単なSlimPHPを使ったREST APIチュートリアルは完了です。参考にした
Slim マイクロフレームワークで REST アプリケーションを作成する のまとめなども読んでみてください。

認証は省略しましたが、自前で全ての機能を実装するのは大変な上に、セキュリティ的にリスクを抱える可能性が高いので、
Sentinel Manual :: Cartalyst などのライブラリを利用するのが良さそうです。


前へ