20200530のPHPに関する記事は15件です。

Pimpleの仕組みメモ

概要

以下のDIコンテナを利用してみようと思いましたが、
仕組みが分からなかったので調べた際のメモとなります。
https://github.com/silexphp/Pimple

公式で紹介されていた使い方

use Pimple\Container;
$container = new Container();

$container['session_storage'] = function ($c) {
    return new SessionStorage('SESSION_ID');
};

$container['session'] = function ($c) {
    return new Session($c['session_storage']);
};

$session = $container['session'];

分かっていなかったこと

$cにPimple\Containerオブジェクトが設定される仕組み

実装を確認して見て分かったこと

Containerクラスが実装しているArrayAccessの
offsetGet($id)内で$thisが渡されていたためでした。
使い方を確認しただけでは分かりませんでしたが、実装を見て納得できました。

offsetGet($id)メソッドの該当箇所抜粋
if (isset($this->factories[$this->values[$id]])) { |
    return $this->values[$id]($this); < この箇所
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPテストフレームワークPestのAssertionsの書き方例

環境構築等の参考はこちら

PHPテストフレームワークPestを試してみる - Qiita

こちらも公式がとてもわかりやすい。

参考サイト

Pest - An elegant PHP Testing Framework | Assertions

テストの説明を日本語にしていて若干読みやすい例。

tests/Unit/sampleTest.php
<?php

test('足し算のテスト', function () {
    $sum = 1 + 1;
    assertEquals(2, $sum);
});

test('引き算のテスト', function () {
    $sum = 2 - 1;
    assertEquals(1, $sum);
});

test('真テスト', function () {
    assertTrue(true);
});

test('偽テスト', function () {
    assertFalse(false);
});

test('同じ数か?', function () {
    $array = [1, 2, 3, 4, 5];

    assertCount(5, $array);
});

test('同じものかどうか', function () {
    $var = 'hoge';

    $this->assertEquals('hoge', $var);
});

test('空かかどうか', function () {
    $empty_array = [];

    $this->assertEmpty($empty_array);
});

test('特定の文字列を含むか', function () {
    $hoge = 'hogetarou';

    assertStringContainsString('hoge', $hoge);
});

# 結果表示がよい。
./vendor/bin/pest
   PASS  Tests\Unit\sampleTest
   足し算のテスト
   引き算のテスト
   ($this)と下は同じ
   真テスト
   偽テスト
   カウント
   同じものかどうか
   空かかどうか
   特定の文字列を含むか

  Tests:  9 passed
  Time:   0.65s

$thisをつけて呼んでもよい。ないほうが好み。

test('上($this)と下は同じ', function () {
    $this->assertTrue(true);

    // 上と下は同じ
    assertTrue(true);
});

PHPUnitのAssertionsメソッドたちがそのまま使えるらしい。
1. Assertions — PHPUnit 9.0 Manual

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPテストフレームワークPestのテストの書き方例

環境構築等の参考はこちら

PHPテストフレームワークPestを試してみる - Qiita

こちらも公式がとてもわかりやすい。

参考サイト

Pest - An elegant PHP Testing Framework | Assertions

テストの説明を日本語にしていて若干読みやすい例。

ちなみに例は、"test"を"it"に書き変えてもそのまま動くはずです

tests/Unit/sampleTest.php
<?php

test('足し算のテスト', function () {
    $sum = 1 + 1;
    assertEquals(2, $sum);
});

test('引き算のテスト', function () {
    $sum = 2 - 1;
    assertEquals(1, $sum);
});

test('真テスト', function () {
    assertTrue(true);
});

test('偽テスト', function () {
    assertFalse(false);
});

test('同じ数か?', function () {
    $array = [1, 2, 3, 4, 5];

    assertCount(5, $array);
});

test('同じものかどうか', function () {
    $var = 'hoge';

    $this->assertEquals('hoge', $var);
});

test('空かかどうか', function () {
    $empty_array = [];

    $this->assertEmpty($empty_array);
});

test('特定の文字列を含むか', function () {
    $hoge = 'hogetarou';

    assertStringContainsString('hoge', $hoge);
});

# 結果表示がよい。
./vendor/bin/pest
   PASS  Tests\Unit\sampleTest
   足し算のテスト
   引き算のテスト
   ($this)と下は同じ
   真テスト
   偽テスト
   カウント
   同じものかどうか
   空かかどうか
   特定の文字列を含むか

  Tests:  9 passed
  Time:   0.65s

$thisをつけて呼んでもよい。ないほうが好み。

test('上($this)と下は同じ', function () {
    $this->assertTrue(true);

    // 上と下は同じ
    assertTrue(true);
});

PHPUnitのAssertionsメソッドたちがそのまま使えるらしい。
1. Assertions — PHPUnit 9.0 Manual

グループ毎に実行できる

<?php

test('足し算のテスト', function () {
    $sum = 1 + 1;
    assertEquals(2, $sum);
})->group('calc');

test('引き算のテスト', function () {
    $sum = 2 - 1;
    assertEquals(1, $sum);
})->group('calc');

test('かけ算のテスト', function () {
    $result = 2 * 2;
    assertEquals(4, $result);
})->group('calc');

test('わり算のテスト', function () {
    $result = 2 / 1;
    assertEquals(2, $result);
})->group('calc');

// グループを指定してテスト実行
./vendor/bin/pest --group=calc

   PASS  Tests\Unit\sampleTest
   足し算のテスト
   引き算のテスト
   かけ算のテスト
   わり算のテスト

  Tests:  4 passed

参考サイト
Pest - An elegant PHP Testing Framework | Groups Of Tests

任意のテストをskipする

<?php
// skip
test('足し算のテスト', function () {
    $sum = 1 + 1;
    assertEquals(2, $sum);
})->group('calc')->skip();

// コメントつきskip
test('引き算のテスト', function () {
    $sum = 2 - 1;
    assertEquals(1, $sum);
})->group('calc')->skip('testでskip');

// 条件付きskip
// 使いどころがありそう
test('かけ算のテスト', function () {
    $num = 10;

    $result = 2 * 2;
    assertEquals(4, $result);
})->group('calc')->skip($num < 100, 'numが100より小さければskip');

   WARN  Tests\Unit\sampleTest
  s 足し算のテスト
  s 引き算のテスト  testでskip
  s かけ算のテスト  numが100より小さければskip

  Tests:  3 skipped
  Time:   0.38s

デバックしたいとき

// ひとつだけテスト実行
test('わり算のテスト', function () {
    $result = 2 / 1;
    assertEquals(2, $result);
})->group('calc')->only();

  PASS  Tests\Unit\sampleTest
   わり算のテスト

  Tests:  1 passed

あとでテストを書きたいとき

test('あとで複雑な計算のテストを書く');
or
it('あとで複雑な計算のテストを書く');

   WARN  Tests\Unit\sampleTest
  r あとで複雑な計算のテストを書く  This test did not perform any assertions  /app/vendor/pestphp/pest/src/Factories/TestCaseFactory.php(185) : eval()'d code:4

  Tests:  1 risked
  Time:   0.26s

参考サイト
Pest - An elegant PHP Testing Framework | Skipping Tests

感想

便利だし、書きやすいのではないでしょうか。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPと Vue.jsで簡易掲示板を作ろう〜その1(PHP編)〜

初めに

3つのパートに分けて
1.PHPでとりあえず動く掲示板を作る←今回はここ
2.SCSSで簡単なデザインを作る
3.Vue.jsで少しリッチな動きをつける

目次

・誰向けですか?
・このパートでの完成イメージとデモサイト
・PHPの記述と解説
・HTMLの記述と解説
・*の解説
・まとめ

誰向けですか?

・PHP初心者
・Vue.js初心者
・PHPで特に作りたいものがない人

このパートでの完成イメージとデモサイト

デモサイト
簡易掲示板へようこそ(XSS対策済み)

〜完成イメージ〜
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
スクリーンショット 2020-05-30 17.54.32.png
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
このようにクロスサイトスクリプティング(XSS)の対策をしています。

PHPの記述と解説

index.php
<?php

// htmlspecialchars()でHTMLで本来直接書くことのできない<>などの記号を書けるようにする
// メッセージ欄にscriptを記述する輩への対策(XSSの防止)*1
function h($v) {
    return htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
}

$FILE = 'all_data.text'; // DBを使わずファイルに保存する
$user_id = uniqid(rand().'_'); // IDを作る(ランダムな数値_13文字の文字列)が出力される*2

date_default_timezone_set('Asia/Tokyo'); // ↓date()でどの地域の時間を使用するか決める
$date = date('Y年/m月/d日/H:i'); // date(Y,m,d,H,i)に↑で設定した時間が入る*3

$input_text = ''; // 入力するテキストを変数として代入

$post_data = []; // 一回の投稿のデータを入れる,($user_id, $date, $input_text)を入れる
$all_data = []; // 全ての投稿のデータを入れる

// file_exists(ファイル名)でこのファイルが存在するか調べる
if(file_exists($FILE)) {
    //json_decode()でJSON形式のデータを配列に戻す
    //file_get_contents()で$FILEを読み込む
    $all_data = json_decode(file_get_contents($FILE));
}

// $_SERVERにはHTTPサーバやHTTP通信に関わる情報が格納されている
// $_SERVER['REQUEST_METHOD']でブラウザからのリクエストがGETかPOSTか判別する*4
// もしPOSTのリクエストがあったら
if($_SERVER['REQUEST_METHOD'] === 'POST') {

    // リクエストを受け取った時textが空ではなかったら(送信ボタンが押された時)
    if(!empty($_POST['text'])) {

        // $post_textに投稿されたテキストを代入
        $input_text = $_POST['text'];

        // 新しく投稿されたデータ(データを配列にして入れる)HTMLで表示する時に使う
        $post_data = [$user_id, $date, $input_text];

        // 投稿をファイルに保存する
        $all_data[] = $post_data;

        // ファイルに保存する
        file_put_contents($FILE, json_encode($all_data));
    }

    //header()
    //$_SERVER['SCRIPT_NAME']は現在のスクリプトのパス(簡単に言うと今いる場所のURL)
    header('Location:' . $_SERVER['SCRIPT_NAME']);
    //プログラム終了(exitなかったら後に書かれた処理も実行してしまうから)
    exit;
}
?>



HTMLの記述と解説

HTMLの記述は同じファイルでも分けても好みでOK(今回は続けて同じファイルに記述します)

index.php
<div class="container">
        <h1>簡易掲示板へようこそ(XSS対策済み)</h1>

        <!-- 投稿ボタン -->
        <form method="post"><!-- 中の投稿ボタンが押されたらPOSTのリクエストを送る -->
            <input type="text" name="text">
            <input type="submit" value="投稿する">
        </form>

        <table>
            <?php foreach((array)$all_data as $post_data) : ?>
            <tr>
                <form method="post">
                    <td> <!-- $post_data[2]は$post_text(テキストデータ) -->
                        <?php echo h($post_data[2]); ?>
                    </td>
                    <td> <!-- $post_data[1]は$date(日付) -->
                        <?php echo $post_data[1]; ?>
                    </td>
                </form>
            </tr>
            <?php endforeach; ?>
        </table>
    </div>

~*の解説~

<!--
    *1ENT_QUOTES:["],['],[<],[>]が普通の文字として出力される
    *2uniqid()だけでは同じタイミングで実行すると同じ値が生成されるのでrand()を入れることで回避
        ↑マイクロ秒で同じでないとかぶらないので念のためである
    *3公式マニュアルにdate()の日付文字列の書式が解説されている(分かりやすい)https://www.php.net/manual/ja/function.date.php
    *4 GET:何か情報を検索したり取得するために使うためのメソッド
       POST:登録処理や更新処理などの、書き込みがありリソースが更新される可能性のある処理に対して使うメソッド
       もっと詳しく分かりやすい解説記事:https://qiita.com/kanataxa/items/522efb74421255f0e0a1
-->

まとめ

今回のパートではとりあえず動く掲示板を作成しました!
PHP学習してまだ日が浅いので間違いがあるかもしれません。その時はお教えください?‍♂️
メソッドやプロパティを一つ一つ調べながら学習するといいことが今回分かりました!
この記事を見てくださったあなたの成長を応援します!!
ーーーー
次のパート:PHPと Vue.jsで簡易掲示板を作ろう〜その2( SCSS編)〜

この記事もいかがですか?

初心者に捧げるハンバーガーメニューの作り方
初心者に捧げるヘッダーの作り方
初心者に捧げる〜PHPを使って九九の表を作ろう〜

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[PHP] mysqliでデータをSELECTした際に数値型がすべて文字列になってしまう件

やりたかったこと

phpから、mysqliでもってSQL実行し取得したときに、データベースでINT型またはFLOAT型になっているのにすべてSTRING型になってしまうので、数値のものは数値として取得したい。なぜかと言うと、最終的にjavascriptに持ってきてid順とかで並べ替えたりするときに、1と2と10が文字列だと1→10→2と並んでしまったりする。いちいちparseIntしなければならない。

最終的なコード(PHP)

// SQL実行
$result = $this->mysqli->query($sql); //何かしらSELECTしてくる

// 連想配列に格納
$data = array(); //格納先
while ($row = $result->fetch_assoc()) { //一行ずつ配列で取得して$dataに入れていく。これはよくある手法

    // --- ここから数値変換処理 ---
    foreach ( $row as $key => &$value ) {
        if(is_numeric($value)) $value *= 1; //すべての要素に対して、数値であれば1を掛けることで自動で型変換してもらう(PHPの仕様)
    }
    unset($value); //誤作動防止
    // --- ここまで ---

    $data[] = $row;
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WordPressホストのphpを5から7にアップグレードする

概要

Debian GNU/Linux 9.12 (stretch) のホストで、php 5とapache 2の組み合わせでWordPressを動かしていたのだが、WordPress 5.4.1にはサイトのヘルスチェックを行う機能が追加されており、このヘルスチェックが「php 5は古すぎる」と警告を出してくる。
ごもっともなので、phpを7.xにアップデートする。
ついでにOS/ディストリビューションもDebian GNU/Linux 10.4 (buster)にアップデートする。

ポイント

そこそこ以前から運用してきたWordPressサイトの場合、charsetがlatin1に設定されたままのMySQLデータベーステーブルにutf8文字列が無理やり格納されているため、これを素直にアップデートしていくと、データベースの内容、つまりWordPressのコンテンツが無残に文字化けしてしまう。これを回避する必要がある。

データベースをダンプする

mysqldump -u root --default-character-set=latin1 データベース名 -h localhost -p > dumpdata

WordPressで用いているデータベース名は、Debian GNU/Linuxの場合、/etc/wordpress/config-WordPressホスト名.phpを見ればわかる。WordPressで用いているデータベースへの接続ユーザ名とパスワードもここを見ればわかる。
ただ、そのユーザ名義でmysqldumpしようとしても、データベース全体をロックできないと拒まれるので、-u rootを付けてスーパーユーザ名義で実行している。あるいはapacheをいったん止めてからmysqldumpすればいいだろう。

mysqldumpする際--default-character-set=latin1をつけるのがポイント。charset=latin1なテーブルに(無神経に)utf8文字列が格納されてしまっている場合、それらを文字化けさせず、素直に、(ある意味無神経に)ダンプするには、こうする。ここで下手にutf8を指定してしまうと、歪んでいる現状を下手に正そうとする処理の結果、ダンプ結果が却って(人間にとっては)文字化けしてしまう。ダンプ結果をlv -Ou8 dumpdataなどして確認すると、utf8文字列がそのまま無事ダンプされていることがわかるだろう。

データベースのダンプをutf8にする

ファイルに含まれる文字列自体はすでにutf8なので、あとはtableのcharset指定などをutf8にしてやればよい。
いちおう念のためgrep latin1 dumpdata | lvなどして、文字列「latin1」の出現状況を確認のうえ、

sed 's/latin1/utf8/g' dumpdata > dumpdata.utf8

で置換してやればよい。
ブログ文中などコンテンツ自体にもlatin1という文字列が含まれる場合は、よきに取り計らってください。
ここではutf8mb4を指定しないほうがよい。ブログポスト本文(wp_posts)などは正常に見えたりするものの、例えばサードパーティーのthemeなど周辺に誤動作・文字化けが発生したりする。

Debian GNU/Linux 10.4にupgradeする

vi /etc/apt/sources.list (stretchをbusterに置換)
apt update
apt dist-upgrade

ついでに

apt autoremove
deborphan | xargs dpkg -P

などして不要なパッケージを掃除しておくのもよいだろう。

phpを7にする

php5系のパッケージをphp7.3系にアップグレードする。

ついでに、phpを稼働させるフレームワークもphp-fpmにした。mpm_worker + php-fpm にするを参考にさせていただいた。

dpkg -P libapache2-mod-php5 php5-cli php5-common php5-curl php5-gd php5-json php5-mysql php5-readline
apt install libapache2-mod-php7.3 php7.3-cli php7.3-common php7.3-curl php7.3-gd php7.3-json php7.3-mysql php7.3-readline
a2dismod mpm_prefork
apt install php-fpm
a2enmod proxy_fcgi setenvif
a2enconf php7.3-fpm
a2enmod mpm_worker
a2enmod php7.3

このあたり(このあたりに限らないが)your mileage may varies.

データベースを復旧する

この状態では、MySQLのlatin1テーブルに納められたutf8文字列が、php 5の時代は何も考えず無神経にutf8のまま扱われていたものが、きちんと? 文字化けするようになってしまっている。
なので、上の手順できちんとutf8にしておいたdumpdata.utf8を食べさせる。

mysql -u root データベース名 -h localhost -p < dumpdata.utf8

おしまい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

チャットアプリで、1グループのメッセージ数上限を50件にする(Laravel)

はじめに

初心者による初心者向けの記事です。
陥りやすい様々な苦手分野も交えて書いてみました。
「collectionと配列の違い」「モデルインスタンスってなんやねん!」「クエリビルダとかメソッドとかわからん!」「ネットでcount()関数について調べてから使ったのに、エラーになる!」など。

このように実装しようと思った背景

実用的なチャットアプリを作りたいため、今回の実装をしました。
テーブル関係はgroups hasmany messagesの1対多です。
私はAjax通信のリアルタイムチャットアプリを作成しています。LINEのグループチャット的なものです。
トークルームのAjaxは、では5秒おき(setTimeout("get_messageC()",5000);)にそのグループの全メッセージを取得し、表示してあるメッセージを全部消して(.remove();)全部書き足す(.append(html);)という実装をしています。
しかしこれでは、メッセージが1000,2000と増えるごとに動作が重くなってしまいます。よって、1グループメッセージは50件までしか保存されないような設計にしました。そうすれば最大でも50件しか書き換えと表示が行われません。

やること

★メッセージ送信アクション時に、そのgroup_idと関連づいたmessageの数を数える(ここでcollectionと配列の違いを理解しとく必要がある)→
★50件以上あるなら、上位50件以外のメッセージは削除する(ここでモデルインスタンスなのかDBインスタンスなのか把握しておく必要がある)

下図messageテーブルです。ターミナルにてmysqlで出しました。見づらいですが、messagesテーブルのカラムにgroup_idがあります。user_idはグループ作成者です。いざという時の拡張性を考えて入れときました。
スクリーンショット 2020-05-30 11.50.52.png

collectionと配列の違い

collectionはLaravelが提供する高機能な配列です。一方で配列はみなさんがよく見かけるphpの「the・普通の配列」です。
データをインスタンス化した時はcollectionであることが多いです。例えば下記のようにモデルをインスタンス化した時。
例:$apple = new Apple;
下記のように自分で配列を作った時は、紛れもなくphpの配列です。
例:$apple = array('iphone','macbook' ,'ipad');

違いを理解すべき瞬間

厄介なのは、「配列の要素数を数えるcount関数」1つとっても、collectionと配列で文法が異なることです。先ほどの$appleを例に取りましょう。
collectionの場合$count = $apple->count();
配列の場合$count = count($apple);
ちなみに配列関数count()をcollectionに適用すると以下のようなエラーが出ます。
ErrorException (E_WARNING)
count(): Parameter must be an array or an object that implements Countable

「count()関数は引数は配列かオブジェクトじゃないとダメですよ〜」ということですね。collectionを配列とみなしていないようです。
ちなみに僕もなんのインスタンスがcollectionになるのか、というのは把握していません。エラー文見てその都度判断してます。

書いたコード

先にクラスをuseしちゃいましょう。自分のコントローラーにあるものをコピペします。

今回使用するクラス

注目すべき箇所は「★」をつけてます。今回は★をuseしておけば問題ないと思います。

GroupController.php
<?php

namespace App\Http\Controllers\User;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB; //★
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Auth;
use Illuminate\Support\Facades\Hash;
use App\User;
use App\Follow;
use Abraham\TwitterOAuth\TwitterOAuth;
use App\Group;
use App\Message; //★
use Illuminate\Support\Facades\Log;
use Storage; 
GroupController.php@send
///まず現在チャットしているgroupのメッセージ数を全て取得します。
    $count = $message->where('group_id',$_POST['group_id'])->count();
///その数が50を超えているなら、
    if ($count > 50) {
///現在チャットしてるグループがhasするmessageのidを取得するためにやはりまずメッセージを取得します
      $deleteid = DB::table('messages')
      ->where('group_id', $_POST['group_id'])
///それの並び順を古い順から新しい順に変えます。
      ->orderBy('id','desc')
///messageデータを50件のみ取得します(新しい順なので最新50件)。
      ->take(50)
///idのみ抽出します(必要ないけど、動作軽くなるかなーと思って)
      ->pluck('id')
//最新50件のなかで最も古いメッセージのidを取得。これで$deleteidに最も古いメッセージのidを入れました。
      ->min();

      Message::where('group_id',$_POST['group_id'])
//$deleteidより古いメッセージは全て削除。
      ->where('id','<',$deleteid)->delete();
    }

このコードより前で$message = new Message;しました。つまり$messageは、Messageモデルのインスタンスです。group_idはAjaxのtype: "POST",で送信しているので、$_POST['group_id']で受け取ります。type: "GET",の場合やそもそもtype指定してない場合は$_GET['group_id']で受け取ってください。type指定しないと自動でtype: "GET",になるので。

tips

なぜ$messageがあるにもかかわらずif(){}内でわざわざDB::table('messages');しているのか。なぜ$messageを使用しないのか。
それは、$messageはcollectionだからです。Messageモデルのインスタンスなのでcollectionになります。
クエリビルダのメソッドを使いたいです。->orderBy()->take()->pluck()を使いたいです。DBのインスタンスじゃないとクエリビルダメソッドは使えません。
ちなみにuse Illuminate\Support\Facades\DB; //★クラスをインスタンス化してます。

また、$messageにクエリメソッドをしようすると、Object of class Illuminate\Database\Eloquent\Builder could not be converted to stringエラーが出ます。
一番下でまたクラス呼んでいますが、なぜそうしたか自分でもわかりません。昨日の自分に問い質したいです。
Message::where('group_id',$_POST['group_id'])
->where('id','<',$deleteid)->delete();

最後に

よく「ドキュメント読め」とか「オブジェクト指向を理解しろ」と言われますが、それが全てだと僕は思いません。
ドキュメントが理解できるようになったのは学習開始5ヶ月目とかですし、平均1日6時間とか勉強した挙句のそれです。
僕が要領悪いだけかもしれませんが、是非僕のような初心者の方の役に立てたら幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

朝飯前にMacのローカルにLaravel環境を構築する

Mac ローカルにPHPをインストールする

サクッとLaravel環境を作りたい時ってありますよね?
カップ麺を作る時間すらも惜しい...そんな時...

MacのローカルにLaravel環境を構築する手順をご紹介します。

Mac標準のPHP

$ which php
/usr/bin/php

$ php -v
PHP 7.3.11 (cli) (built: Feb 29 2020 02:50:36) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.11, Copyright (c) 1998-2018 Zend Technologies

これでもいいんですけど、バージョンが古かったりzip拡張機能がデフォルトで入ってなかったりするので少し面倒なので、HomebrewでPHPをインストールします。

複数のPHPバージョンを使い分けたい訳ではなく最新版のPHPが使えればいいので、今回はanyenvやphpenvは使いません。

Homebrew からPHPをインストールする

$ brew install php

~/.zshrc に環境変数のPATHにPHPのパスを追加します。

$ echo export PATH='/usr/local/opt/php/bin/php:$PATH' >> ~/.zshrc

シェルを再起動して設定ファイルを読み直します。

$ exec $SHELL -l

PHPのバージョンが更新されたらokです。

$ php -v
PHP 7.4.6 (cli) (built: May 29 2020 01:44:57) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Zend OPcache v7.4.6, Copyright (c), by Zend Technologies

HomebrewからインストールしたPHPに入っているPHP拡張機能です。

$ php -m
[PHP Modules]
bcmath
bz2
calendar
Core
ctype
curl
date
dba
dom
exif
FFI
fileinfo
filter
ftp
gd
gettext
gmp
hash
iconv
intl
json
ldap
libxml
mbstring
mysqli
mysqlnd
odbc
openssl
pcntl
pcre
PDO
pdo_dblib
pdo_mysql
PDO_ODBC
pdo_pgsql
pdo_sqlite
pgsql
Phar
phpdbg_webhelper
posix
pspell
readline
Reflection
session
shmop
SimpleXML
soap
sockets
sodium
SPL
sqlite3
standard
sysvmsg
sysvsem
sysvshm
tidy
tokenizer
xml
xmlreader
xmlrpc
xmlwriter
xsl
Zend OPcache
zip
zlib

[Zend Modules]
Zend OPcache

これだけ入っていれば十分でしょう。

Homebrew からComposerをインストールする

$ brew install composer
$ composer -V
Composer version 1.10.6 2020-05-06 10:28:10

SQLite

データベースは何でもいいですが、今回は導入が手軽なSQLiteにします。

$ brew install sqlite
$ sqlite3 --version
3.28.0 2019-04-15 14:49:49 378230ae7f4b721c8b8d83c8ceb891449685cd23b1702a57841f1be40b5daapl

Laravelインストーラ

Laravelのバージョンの指定は必要なく最新版をインストールして使いたいのでLaravelインストーラを入れておくと便利でしょう。

$ composer global require laravel/installer

グローバルにlaravel/installer が追加されていたらokです。

$ composer global show | grep laravel
Changed current directory to /Users/ucan/.composer
laravel/installer         v3.1.0  Laravel application installer.

~/.zshrc に環境変数のPATHにPHPのパスを追加します。

$ echo export PATH='$HOME/.composer/vendor/bin:$PATH' >> ~/.zshrc

シェルを再起動して設定ファイルを読み直します。

$ exec $SHELL -l

Laravelのインスール

上記の作業は寝る前に事前に済ませておきましょう。
Laravelのインストールはもはや朝飯前です。

$ laravel new blog
$ cd blog
$ php artisan --version
Laravel Framework 7.13.0

Laravelビルトインサーバを起動する

$ php artisan serve

http://127.0.0.1:8000

control + c でサーバーを停止できます。

LaravelとSQLiteを接続する

.env を修正する

DB_CONNECTION=sqlite
#DB_CONNECTION=mysql
#DB_HOST=127.0.0.1
#DB_PORT=3306
#DB_DATABASE=laravel
#DB_USERNAME=root
#DB_PASSWORD=

データベースファイルを作成する

$ touch database/database.sqlite
$ php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (0 seconds)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (0 seconds)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (0 seconds)

最後に

Laravelの環境構築の記事をいくつか書いているのでよかったら読んでもらえたら嬉しいです!
(情報が古くなってるものもありますが...)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPテストフレームワークPestを試してみる

参考

Pest - An elegant PHP Testing Framework | Installation

やり方

記事を書きながら公式の方が絶対いいなと思いながら書き残します。
個人的につまったところは、composer.jsonの"minimum-stability"など書くところが分からず、ずっと怒られてました。

Dockerでやるならこちらをcloneしてもらったほうがいいかもしれません。
yoshinyan/pest_test

phpunit.xmlは次からコピペする。
pest/phpunit.xml at master · pestphp/pest

# 前提composerが入っていること

composer require phpunit/phpunit:"^9.0" --dev --update-with-dependencies

composer require pestphp/pest --dev
composer.json
{
    "require-dev": {
        "phpunit/phpunit": "^9.0",
        "pestphp/pest": "^0.1.5"
    },
    "minimum-stability": "dev", // <= エラーが起こったらこれを追加
    "prefer-stable": true // <= エラーが起こったらこれを追加
}

ファイル名はXXXTest.phpに固定っぽい。

./tests/Unit/CalcTest.php
<?php
# シンプルでとてもよい。
test('足し算のテスト', function () {
    $sum = 1 + 1;
    assertEquals(2, $sum);
});

// もしくは
it('引き算のテスト', function () {
    $sum = 2 - 1;
    assertEquals(1, $sum);
});
# テスト実行
./vendor/bin/pest

# result
   PASS  Tests\Unit\CalcTest
  ✓ 足し算のテスト
  ✓ it 引き算のテスト

  Tests:  2 passed
  Time:   0.43s

感想

  • PestはJestに結果出力のところが似ていて、かっこいいなと思いました
  • PHPUnitと共存できるらしい。
  • Laravelと相性が良さそう。
  • Pestの英訳が「害虫」と面白い
  • Jestと名前が似ている

よければこちらも見てください

PHPテストフレームワークPestのAssertionsの書き方例 - Qiita

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel Authで実装した認証機能の初回登録時にオリジナルカラムデータの保存に対応する。

目的

  • ユーザの初回登録時にデフォルトでは存在していないカラムへ自動的に値を格納する方法をまとめる

実施環境

  • ハードウェア環境
項目 情報
OS macOS Catalina(10.15.3)
ハードウェア MacBook Pro (16-inch ,2019)
プロセッサ 2.6 GHz 6コアIntel Core i7
メモリ 16 GB 2667 MHz DDR4
グラフィックス AMD Radeon Pro 5300M 4 GB Intel UHD Graphics 630 1536 MB
  • ソフトウェア環境
項目 情報 備考
PHP バージョン 7.4.3 Homwbrewを用いて導入
Laravel バージョン 7.0.8 commposerを用いて導入
MySQLバージョン 8.0.19 for osx10.13 on x86_64 Homwbrewを用いて導入

前提情報

実施したい事

  • Authの認証に使用されるusersテーブルは初期状態で下記のカラムで構成されている。

    mysql> show columns from users;
    +-------------------+-----------------+------+-----+---------+----------------+
    | Field             | Type            | Null | Key | Default | Extra          |
    +-------------------+-----------------+------+-----+---------+----------------+
    | id                | bigint unsigned | NO   | PRI | NULL    | auto_increment |
    | name              | varchar(255)    | NO   |     | NULL    |                |
    | email             | varchar(255)    | NO   | UNI | NULL    |                |
    | email_verified_at | timestamp       | YES  |     | NULL    |                |
    | password          | varchar(255)    | NO   |     | NULL    |                |
    | remember_token    | varchar(100)    | YES  |     | NULL    |                |
    | created_at        | timestamp       | YES  |     | NULL    |                |
    | updated_at        | timestamp       | YES  |     | NULL    |                |
    +-------------------+-----------------+------+-----+---------+----------------+
    
  • この情報にプラスしてサービスを使用するユーザが結婚しているかどうかのデータを格納する「marriage_flag」カラムを追加する。

  • 「marriage_flag」カラムは次の条件で作成する。(カラム名: marriage_flag データ型: int NULLの許容: しない 初期値:0)

    +-------------------+-----------------+------+-----+---------+----------------+
    | Field             | Type            | Null | Key | Default | Extra          |
    +-------------------+-----------------+------+-----+---------+----------------+
    | marriage_flag     | int             | NO   |     | 0       |                |
    +-------------------+-----------------+------+-----+---------+----------------+
    
  • カラム設定でも初期値を指定するが、ユーザ登録シーケンス内でもアプリ側で値を格納するように処理を行う。

  • 「marriage_flag」は0が格納されている時は未婚、1が格納されている時は既婚、2が格納されている時はその他をお見する。

概要

  1. カラムの追加
  2. 登録処理の値追加
  3. 確認

概要

  1. カラムの追加

    1. 下記コマンドを実行してマイグレーションファイルを作成する。

      $ php artisan make:migration add_marriage_flag_column_to_users_table --table=users
      
    2. 下記コマンドを実行して先に作成したマイグレーションファイルを開く(yyyy_mm_ddはマイグレーションファイルの作成日)

      $ vi database/migrations/yyyy_mm_dd_XXXXXX_add_marriage_flag_column_to_users_table.php
      
    3. 開いたマイグレーションファイルを下記の様に修正しする。

      アプリ名ディレクトリ/database/migrations/yyyy_mm_dd_XXXXXX_add_marriage_flag_column_to_users_table.php
      <?php
      
      use Illuminate\Database\Migrations\Migration;
      use Illuminate\Database\Schema\Blueprint;
      use Illuminate\Support\Facades\Schema;
      
      class AddMarriageFlagColumnToUsersTable extends Migration
      {
          /**
           * Run the migrations.
           *
           * @return void
           */
          public function up()
          {
              Schema::table('users', function (Blueprint $table) {
                  //下記を追記する
                  $table->integer('marriage_flag')->default(0);
              });
          }
      
          /**
           * Reverse the migrations.
           *
           * @return void
           */
          public function down()
          {
              Schema::table('users', function (Blueprint $table) {
                  //下記を追記する
                  $table->dropColumn('marriage_flag')->default(0);
              });
          }
      }
      
    4. 下記を実行してマイグレーションを実行する。

      $ php artisan migrate
      
  2. 登録処理の値追加

    1. 下記コマンドを実行してユーザ登録時に実行されるコントローラファイルを開く。

      $ vi app/Http/Controllers/Auth/RegisterController.php
      
    2. 開いたコントローラファイルを下記の様に修正して初期値を指定する。

      アプリ名ディレクトリ/app/Http/Controllers/Auth/RegisterController.php
      <?php
      
      namespace App\Http\Controllers\Auth;
      
      use App\Http\Controllers\Controller;
      use App\Providers\RouteServiceProvider;
      use App\User;
      use Illuminate\Foundation\Auth\RegistersUsers;
      use Illuminate\Support\Facades\Hash;
      use Illuminate\Support\Facades\Validator;
      
      class RegisterController extends Controller
      {
          /*
          |--------------------------------------------------------------------------
          | Register Controller
          |--------------------------------------------------------------------------
          |
          | This controller handles the registration of new users as well as their
          | validation and creation. By default this controller uses a trait to
          | provide this functionality without requiring any additional code.
          |
          */
      
          use RegistersUsers;
      
          /**
           * Where to redirect users after registration.
           *
           * @var string
           */
          protected $redirectTo = RouteServiceProvider::HOME;
      
          /**
           * Create a new controller instance.
           *
           * @return void
           */
          public function __construct()
          {
              $this->middleware('guest');
          }
      
          /**
           * Get a validator for an incoming registration request.
           *
           * @param  array  $data
           * @return \Illuminate\Contracts\Validation\Validator
           */
          protected function validator(array $data)
          {
              return Validator::make($data, [
                  'name' => ['required', 'string', 'max:255'],
                  'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
                  'password' => ['required', 'string', 'min:8', 'confirmed'],
              ]);
          }
      
          /**
           * Create a new user instance after a valid registration.
           *
           * @param  array  $data
           * @return \App\User
           */
          protected function create(array $data)
          {
              return User::create([
                  'name' => $data['name'],
                  'email' => $data['email'],
                  'password' => Hash::make($data['password']),
                  //下記を追加する
                  'marriage_flag' => 0,
              ]);
          }
      }
      
  3. 確認

    1. アプリ名ディレクトリで下記コマンドを実行してローカルサーバを起動する。

      $ php artisan serve
      
    2. 下記にアクセスする。

    3. ユーザの新規登録を行う。

    4. 下記コマンドを実行してターミナルからMySQLにログインする。

      $ mysql -u root -p
      
    5. 下記を実行してデータベース名の一覧を出力する。

      mysql> show databases;
      
    6. 下記を実行してLaravelアプリで使用しているDBを指定する。

      mysql> use データベース名;
      
    7. 下記を実行してusersテーブルのデータを出力する。

      mysql> select * from users;
      
    8. 新規登録したレコードのmarriage_flagカラムに0が格納されている事を確認する。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

51歳(現52)からのプログラミング 備忘 false null php

var_dump(false==null);
var_dump(false===null);

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

51歳(現52)からのプログラミング 備忘Code

var_dump(false==null);
var_dump(false===null);

ファイル読み書き

access.log
aaa bbb
aaa bbb
aaa bbb
書き込みcode
$ary[]='aaa';
$ary[]='bbb';

$file=fopen('access.log','ab');
flock($file,LOCK_EX);
fwrite($file,implode("\t",$ary)."\n"); //\nは必須と覚える
flock($file,LOCK_UN);
fclose($file);
読み出しcode
<?php
function fopn(){
  $file=fopen('access.log','rb');
  flock($file,LOCK_EX);
  return $file;
}

function fcls($file){
  flock($file,LOCK_UN);
  fclose($file);
}

// --------------------------------------
<?php
function fopn(){
  $file=fopen('access.log','rb');
  flock($file,LOCK_EX);
  return $file;
}

function fcls($file){
  flock($file,LOCK_UN);
  fclose($file);
}

//-----------------------------
function fgcsv(){
  $file=fopn();
  print_r(fgetcsv($file));
  fcls($file);
}

function fg(){
  $file=fopn();
  print_r(fgets($file));
  fcls($file);
}

function f(){
  print_r(file('access.log'));
}

function fgc(){
  print_r(file_get_contents('access.log'));
}

function rf(){
  readfile('access.log');
}

fgcsv();echo '<br>';
fg()   ;echo '<br>';
f()    ;echo '<br>';
fgc()  ;echo '<br>';

// Array ( [0] => aaa bbb )
// aaa bbb
// Array ( [0] => aaa bbb [1] => aaa bbb [2] => aaa bbb )
// aaa bbb aaa bbb aaa bbb
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascriptソースコードを隠蔽してみた。

はじめに

javascriptはいやおうなしに見られてしまうので、
APIキーとかソースとか隠せないかな、と考えて色々調べてみた結果、ここに行き着いた。

現状のままではPHPにJsソースを書くことになる欠点がある……ので、これの対策verもそのうち記事にします。

どうやって実装するの?

まずもって、ユーザーがJavascriptのソースコードをのぞき見する主な手段は以下の2パターンある。

  • URLバーに該当するJsのURLを直接打ち込んで、ページとして見る
  • 右クリック→検証→左メニューの「Sources」から閲覧する

逆に言えば、今回やるべきはこの2つの手段を両方とも潰すことである。
それを踏まえて、本システムの要件は以下のとおりとする。

要件

  • ① 直接アクセスした場合にはエラーorニセのコードを出力し、ソースコードを見られないようにすること
  • ② 右クリック→検証→左メニューの「Sources」を使ってもソースコードが見られないようにすること
  • ③ あらかじめ決めておいたサイト以外からは呼び出せないように設定する(場合によっては必要になる。理由は後述)

Step1 直接アクセスの阻止

まず直接URLを指定してページを覗かれた場合だが、これは単純なJavascriptだけでは阻止しきれない。
なぜならJavascriptがクライアントサイドで動く言語だからだ。

pic1-1.png
(※拾い画)

この図はウェブサイトが開かれ、見られるようになるまでの一連の流れ。

javascriptのようなクライアントサイド言語はまずコード全体が閲覧者の端末に送信され、閲覧者の端末の中(図の⑤)で処理が実行される。見せたい見せたくないに関わらずページを開いた瞬間にコードを渡してしまうので、そのあとから隠蔽するのって難しいのだ。

ならばどうするかというと、「サーバーサイド言語」というやつを経由させる。
こいつは図で言うと③で実行される言語で、サーバーマシンの中で処理を行ってからその結果だけを返すようになっている。
ということは、このサーバーサイド言語で直接アクセスかどうかを判定してやればよいわけだ。

今回はPHPを利用してこの処理を実装する。
サーバー内で処理できればそれでいいので、処理自体はPythonでもRubyでもNode.jsでも実装できる……はず。

とりあえず、以下のようにソースを書いてみた。

source.php
<?php

/* 直接アクセス禁止設定 */
if($_SERVER["REQUEST_METHOD"] === 'POST'){
    /* 本命のソース
       ※見せたくないソースをここに記載します */
    echo "本命のソース!";

}else{
    /* 直接アクセスされた場合のダミー記述 */
    echo 'アクセス……拒否しますっ!!(>_<)';
}

?>

直接アクセスかどうかのチェックには、リクエストメソッドの状態を取得する$_SERVER["REQUEST_METHOD"]を用いよう。
この値は通常GETになっていて、後述するような特殊なアクセスを行った場合のみPOSTになる。
直接アクセスの場合はもちろんGETになっているので見れないというわけだ。

Step1 隠したいJavascriptコードの用意

隠したいJsソースを用意する。

javascript
function HideSource(){

    /* ここから処理を記載する */
    let block = document.createElement('div');
    block.style.height = '40px';
    block.style.width = '40px';
    block.style.background = '#00b5ad';
    block.style.borderRadius = '5px';
    block.style.position = 'fixed';
    block.style.top = '50%';
    block.style.left = '50%';
    document.body.appendChild(block);
    /* ここまで */
};

このコードは後述のXMLHttpRequestで読み込み、そのあとに削除する。
そのため、ここに記載するコードはオブジェクト化しておく必要がある。
平たく言うと一つのfunction内にまとめておかなくてはならない。

で、これをStep1のPHPと組み合わせた結果が以下のとおり。

source.php
<?php

/* 直接アクセス禁止設定 */
if($_SERVER["REQUEST_METHOD"] === 'POST'){
    /* 本命のソース
       ※見せたくないソースをここに記載します */
    echo "function HideSource(){

    /* ここから処理を記載する */
    let block = document.createElement('div');
    block.style.height = '40px';
    block.style.width = '40px';
    block.style.background = '#00b5ad';
    block.style.borderRadius = '5px';
    block.style.position = 'fixed';
    block.style.top = '50%';
    block.style.left = '50%';
    document.body.appendChild(block);
    /* ここまで */
};";

}else{
    /* 直接アクセスされた場合のダミー記述 */
    echo 'アクセス……拒否しますっ!!(>_<)';
}

?>

これで、要件①はクリア。
直接URLからページを見てもコードは読めなくなった。

Step3 XMLHttpRequestでの呼び出し

次に呼び出し処理を作成する。
呼び出しには、javascriptのXMLHttpRequestを用いる。

そのままコピペできる内容なので、とりあえずはコードを記載する。

javascript
// 実行
ReadSource(document.body, './source.php');

function ReadSource(target, urls){

    // 宣言
    let request = new XMLHttpRequest();

    // リクエストURLを設定(リクエストタイプはPOSTにすること)
    request.open('POST', urls);

    // リクエスト先からレスポンス(返答)があった場合の処理
    request.onreadystatechange = function(){
        if ( (request.readyState == 4) && (request.status == 200) ){

            let scrpt = document.createElement("script");
            scrpt.type = 'text/javascript';
            scrpt.innerHTML = request.responseText;
            target.appendChild(scrpt);

            // 消去
            target.removeChild(scrpt);

            // 処理 
            HideSource();
        }
    }
    request.send();
}

<script>リンクでファイルを読むのではなく、
外部から読み込んだテキストをhtml上に貼り付け、それをJsとして解釈させているから検証メニューにも載ることはない。
代わりにHTMLソース上に関数HideSource全文が載ってしまうので、それは削除する必要がある。

挙動を見る限り、ソースコードをappendChildした時点で関数全体がどこかに登録される模様。
なので、一度appendChildでhtml上に追加してしまえば消しても大丈夫。

このコードではHideSourceの実行が行われるより先にソースコードを消してしまっているが、
その場合でもReadSource内でHideSourceを実行しておけば問題なく動作する。

で、上記のコードを実際にページに記述してみよう。

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"/>
    <title>たいとる</title>
</head>
<body>

<div id="point"></div>

<script type="text/javascript">
function ReadSource(target, urls) {
    // 宣言
    let request = new XMLHttpRequest();

    // リクエストURLを設定(リクエストタイプはPOSTにすること)
    request.open('POST', urls);

    // リクエスト先からレスポンス(返答)があった場合の処理
    request.onreadystatechange = function(){
        if ( (request.readyState == 4) && (request.status == 200) ){

            let scrpt = document.createElement("script");
            scrpt.type = 'text/javascript';
            scrpt.innerHTML = request.responseText;
            target.appendChild(scrpt);

            // 消去
            target.removeChild(scrpt);

            // 処理 
            HideSource();
        }
    }
    request.send();
}
// 実行
ReadSource(document.getElementById('point'), './source.php');
</script>
</body>
</html>

要件②もクリア。
これで、ソースであるsource.phpを見せないようにしつつ、秘匿したJsを実行できる。やったね。

※完成形のコードはGithubに置いています。
「Clone or download」→「Download ZIP」でダウンロードできます。
PHPを含むので、PHPが動く実行環境が必要というのは言わずもがな。

おまけ XMLHttpRequestとCORS

残るは要件③「あらかじめ決めておいたサイト以外からは呼び出せないように設定する」だが、
これ実は自動的にクリアしている。

今回使ったXMLHttpRequestだが、こいつにはCORSというルールで制限がかけられている。
詳しい説明はこちらの記事に譲るが……すごーーく雑な説明をすると、

XMLHttpRequestで別のファイルを呼び出すときに呼び出し元と呼び出し先のサイトが異なった場合、呼び出される側に『このサイトはオレを呼び出していいぞ!』という許可証をつけておけよ?でなけりゃ呼び出しを拒否するぞ1

ということ。
今回は呼び出し元(index.html)と呼び出し先(source.php)を同じ階層に同梱しているのでこの制限にはあたらない。

が、もし仮に別サイトから呼び出すことを考慮したい場合、source.phpの文頭にheader()を追記して以下のように変更しよう。

source.php
<?php

header('Access-Control-Allow-Origin: https://example.com'); // この部分を追記。 https://example.comのところは呼び出し元のドメインを指定。

/* 直接アクセス禁止設定 */
if($_SERVER["REQUEST_METHOD"] === 'POST'){
    /* 本命のソース
       ※見せたくないソースをここに記載します */
    echo "function HideSource(){

    /* ここから処理を記載する */
    let block = document.createElement('div');
    block.style.height = '40px';
    block.style.width = '40px';
    block.style.background = '#00b5ad';
    block.style.borderRadius = '5px';
    block.style.position = 'fixed';
    block.style.top = '50%';
    block.style.left = '50%';
    document.body.appendChild(block);
    /* ここまで */
};";

}else{
    /* 直接アクセスされた場合のダミー記述 */
    echo 'アクセス……拒否しますっ!!(>_<)';
}

?>

参考文献

http://pc110.club/php/%EF%BD%8Aavascript-hidden
http://labs.vividworks.jp/javascript-%E3%81%AE%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AF%E9%9A%A0%E8%94%BD%E3%81%99%E3%82%8B%E3%81%B9%E3%81%8D%E3%81%8B%EF%BC%9F-vol-4/
https://tenderfeel.xsrv.jp/mootools/705/


  1. 正確には「オリジン」というやつをを比較する。わからなければ、ドメインと同じものだと思っておけばいい。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascriptを隠蔽化してみた。

はじめに

javascriptはいやおうなしに見られてしまうので、
APIキーとかソースとか隠せないかな、と考えて色々調べてみた結果、ここに行き着いた。

現状のままではPHPにJsソースを書くことになる欠点がある……ので、これの対策verもそのうち記事にします。

どうやって実装するの?

まずもって、ユーザーがJavascriptのソースコードをのぞき見する主な手段は以下の2パターンある。

  • URLバーに該当するJsのURLを直接打ち込んで、ページとして見る
  • 右クリック→検証→左メニューの「Sources」から閲覧する

逆に言えば、今回やるべきはこの2つの手段を両方とも潰すことである。
それを踏まえて、本システムの要件は以下のとおりとする。

要件

  • ① 直接アクセスした場合にはエラーorニセのコードを出力し、ソースコードを見られないようにすること
  • ② 右クリック→検証→左メニューの「Sources」を使ってもソースコードが見られないようにすること
  • ③ あらかじめ決めておいたサイト以外からは呼び出せないように設定する(場合によっては必要になる。理由は後述)

Step1 直接アクセスの阻止

まず直接URLを指定してページを覗かれた場合だが、これは単純なJavascriptだけでは阻止しきれない。
なぜならJavascriptがクライアントサイドで動く言語だからだ。

pic1-1.png
(※拾い画)

この図はウェブサイトが開かれ、見られるようになるまでの一連の流れ。

javascriptのようなクライアントサイド言語はまずコード全体が閲覧者の端末に送信され、閲覧者の端末の中(図の⑤)で処理が実行される。見せたい見せたくないに関わらずページを開いた瞬間にコードを渡してしまうので、そのあとから隠蔽するのって難しいのだ。

ならばどうするかというと、「サーバーサイド言語」というやつを経由させる。
こいつは図で言うと③で実行される言語で、サーバーマシンの中で処理を行ってからその結果だけを返すようになっている。
ということは、このサーバーサイド言語で直接アクセスかどうかを判定してやればよいわけだ。

今回はPHPを利用してこの処理を実装する。
サーバー内で処理できればそれでいいので、処理自体はPythonでもRubyでもNode.jsでも実装できる……はず。

とりあえず、以下のようにソースを書いてみた。

source.php
<?php

/* 直接アクセス禁止設定 */
if($_SERVER["REQUEST_METHOD"] === 'POST'){
    /* 本命のソース
       ※見せたくないソースをここに記載します */
    echo "本命のソース!";

}else{
    /* 直接アクセスされた場合のダミー記述 */
    echo 'アクセス……拒否しますっ!!(>_<)';
}

?>

直接アクセスかどうかのチェックには、リクエストメソッドの状態を取得する$_SERVER["REQUEST_METHOD"]を使う。
この値は通常GETになっていて、後述するような特殊なアクセスを行った場合のみPOSTになる。
直接アクセスの場合はもちろんGET。

Step1 隠したいJavascriptコードの用意

隠したいJsソースを用意する。

javascript
function HideSource(){

    /* ここから処理を記載する */
    let block = document.createElement('div');
    block.style.height = '40px';
    block.style.width = '40px';
    block.style.background = '#00b5ad';
    block.style.borderRadius = '5px';
    block.style.position = 'fixed';
    block.style.top = '50%';
    block.style.left = '50%';
    document.body.appendChild(block);
    /* ここまで */
};

このコードは後述のXMLHttpRequestで読み込み、そのあとに削除する。
そのため、ここに記載するコードはオブジェクト化しておく必要がある。
平たく言うと一つのfunction内にまとめておかなくてはならない。

で、これをStep1のPHPと組み合わせた結果が以下のとおり。

source.php
<?php

/* 直接アクセス禁止設定 */
if($_SERVER["REQUEST_METHOD"] === 'POST'){
    /* 本命のソース
       ※見せたくないソースをここに記載します */
    echo "function HideSource(){

    /* ここから処理を記載する */
    let block = document.createElement('div');
    block.style.height = '40px';
    block.style.width = '40px';
    block.style.background = '#00b5ad';
    block.style.borderRadius = '5px';
    block.style.position = 'fixed';
    block.style.top = '50%';
    block.style.left = '50%';
    document.body.appendChild(block);
    /* ここまで */
};";

}else{
    /* 直接アクセスされた場合のダミー記述 */
    echo 'アクセス……拒否しますっ!!(>_<)';
}

?>

これで、要件①はクリア。
直接URLからページを見てもコードは読めなくなった。

Step3 XMLHttpRequestでの呼び出し

次に呼び出し処理を作成する。
呼び出しには、javascriptのXMLHttpRequestを用いる。

そのままコピペできる内容なので、とりあえずはコードを記載する。

javascript
// 実行
ReadSource(document.body, './source.php');

function ReadSource(target, urls){

    // 宣言
    let request = new XMLHttpRequest();

    // リクエストURLを設定(リクエストタイプはPOSTにすること)
    request.open('POST', urls);

    // リクエスト先からレスポンス(返答)があった場合の処理
    request.onreadystatechange = function(){
        if ( (request.readyState == 4) && (request.status == 200) ){

            let scrpt = document.createElement("script");
            scrpt.type = 'text/javascript';
            scrpt.innerHTML = request.responseText;
            target.appendChild(scrpt);

            // 消去
            target.removeChild(scrpt);

            // 処理 
            HideSource();
        }
    }
    request.send();
}

<script>リンクでファイルを読むのではなく、
外部から読み込んだテキストをhtml上に貼り付け、それをJsとして解釈させているから検証メニューにも載ることはない。
代わりにHTMLソース上に関数HideSource全文が載ってしまうので、それは削除する必要がある。

挙動を見る限り、ソースコードをappendChildした時点で関数全体がどこかに登録される模様。
なので、一度appendChildでhtml上に追加してしまえば消しても大丈夫。

このコードではHideSourceの実行が行われるより先にソースコードを消してしまっているが、
その場合でもReadSource内でHideSourceを実行しておけば問題なく動作する。

で、上記のコードを実際にページに記述してみよう。

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"/>
    <title>たいとる</title>
</head>
<body>

<div id="point"></div>

<script type="text/javascript">
function ReadSource(target, urls) {
    // 宣言
    let request = new XMLHttpRequest();

    // リクエストURLを設定(リクエストタイプはPOSTにすること)
    request.open('POST', urls);

    // リクエスト先からレスポンス(返答)があった場合の処理
    request.onreadystatechange = function(){
        if ( (request.readyState == 4) && (request.status == 200) ){

            let scrpt = document.createElement("script");
            scrpt.type = 'text/javascript';
            scrpt.innerHTML = request.responseText;
            target.appendChild(scrpt);

            // 消去
            target.removeChild(scrpt);

            // 処理 
            HideSource();
        }
    }
    request.send();
}
// 実行
ReadSource(document.getElementById('point'), './source.php');
</script>
</body>
</html>

要件②もクリア。
これで、ソースであるsource.phpを見せないようにしつつ、秘匿したJsを実行できる。やったね。

※完成形のコードはGithubに置いています。
「Clone or download」→「Download ZIP」でダウンロードできます。
PHPを含むので、PHPが動く実行環境が必要というのは言わずもがな。

おまけ XMLHttpRequestとCORS

残るは要件③「あらかじめ決めておいたサイト以外からは呼び出せないように設定する」だが、
これ実は自動的にクリアしている。

今回使ったXMLHttpRequestだが、こいつにはCORSというルールで制限がかけられている。
詳しい説明はこちらの記事に譲るが……すごーーく雑な説明をすると、

XMLHttpRequestで別のファイルを呼び出すときに呼び出し元と呼び出し先のサイトが異なった場合、呼び出される側に『このサイトはオレを呼び出していいぞ!』という許可証をつけておけよ?でなけりゃ呼び出しを拒否するぞ1

ということ。
今回は呼び出し元(index.html)と呼び出し先(source.php)を同じ階層に同梱しているのでこの制限にはあたらない。

が、もし仮に別サイトから呼び出すことを考慮したい場合、source.phpの文頭にheader()を追記して以下のように変更しよう。

source.php
<?php

header('Access-Control-Allow-Origin: https://example.com'); // この部分を追記。 https://example.comのところは呼び出し元のドメインを指定。

/* 直接アクセス禁止設定 */
if($_SERVER["REQUEST_METHOD"] === 'POST'){
    /* 本命のソース
       ※見せたくないソースをここに記載します */
    echo "function HideSource(){

    /* ここから処理を記載する */
    let block = document.createElement('div');
    block.style.height = '40px';
    block.style.width = '40px';
    block.style.background = '#00b5ad';
    block.style.borderRadius = '5px';
    block.style.position = 'fixed';
    block.style.top = '50%';
    block.style.left = '50%';
    document.body.appendChild(block);
    /* ここまで */
};";

}else{
    /* 直接アクセスされた場合のダミー記述 */
    echo 'アクセス……拒否しますっ!!(>_<)';
}

?>

参考文献

http://pc110.club/php/%EF%BD%8Aavascript-hidden
http://labs.vividworks.jp/javascript-%E3%81%AE%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AF%E9%9A%A0%E8%94%BD%E3%81%99%E3%82%8B%E3%81%B9%E3%81%8D%E3%81%8B%EF%BC%9F-vol-4/
https://tenderfeel.xsrv.jp/mootools/705/


  1. 正確には「オリジン」というやつをを比較する。わからなければ、ドメインと同じものだと思っておけばいい。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascriptをユーザーから見られないようにしてみた。

はじめに

javascriptはいやおうなしに見られてしまうので、
APIキーとかソースとか隠せないかな、と考えて色々調べてみた結果、ここに行き着いた。

現状のままではPHPにJsソースを書くことになる欠点がある……ので、これの対策verもそのうち記事にします。

※完成形のコードはGithubに置いています。
「Clone or download」→「Download ZIP」でダウンロードできます。
PHPを含むので、PHPが動く実行環境が必要というのは言わずもがな。

どうやって実装するの?

まずもって、ユーザーがJavascriptのソースコードをのぞき見する主な手段は以下の2パターンある。

  • URLバーに該当するJsのURLを直接打ち込んで、ページとして見る
  • 右クリック→検証→左メニューの「Sources」から閲覧する

逆に言えば、今回やるべきはこの2つの手段を両方とも潰すことである。
それを踏まえて、本システムの要件は以下のとおりとする。

要件

  • ① 直接アクセスした場合にはエラーorニセのコードを出力し、ソースコードを見られないようにすること
  • ② 右クリック→検証→左メニューの「Sources」を使ってもソースコードが見られないようにすること
  • ③ あらかじめ決めておいたサイト以外からは呼び出せないように設定する(場合によっては必要になる。理由は後述)

Step1 直接アクセスの阻止

まず直接URLを指定してページを覗かれた場合だが、これは単純なJavascriptだけでは阻止しきれない。
なぜならJavascriptがクライアントサイドで動く言語だからだ。

pic1-1.png
(※拾い画)

この図はウェブサイトが開かれ、見られるようになるまでの一連の流れ。

javascriptのようなクライアントサイド言語はまずコード全体が閲覧者の端末に送信され、閲覧者の端末の中(図の⑤)で処理が実行される。見せたい見せたくないに関わらずページを開いた瞬間にコードを渡してしまうので、そのあとから隠蔽するのって難しいのだ。

ならばどうするかというと、「サーバーサイド言語」というやつを経由させる。
こいつは図で言うと③で実行される言語で、サーバーマシンの中で処理を行ってからその結果だけを返すようになっている。
ということは、このサーバーサイド言語で直接アクセスかどうかを判定してやればよいわけだ。

今回はPHPを利用してこの処理を実装する。
サーバー内で処理できればそれでいいので、処理自体はPythonでもRubyでもNode.jsでも実装できる……はず。

とりあえず、以下のようにソースを書いてみた。

source.php
<?php

/* 直接アクセス禁止設定 */
if($_SERVER["REQUEST_METHOD"] === 'POST'){
    /* 本命のソース
       ※見せたくないソースをここに記載します */
    echo "本命のソース!";

}else{
    /* 直接アクセスされた場合のダミー記述 */
    echo 'アクセス……拒否しますっ!!(>_<)';
}

?>

直接アクセスかどうかのチェックには、リクエストメソッドの状態を取得する$_SERVER["REQUEST_METHOD"]を使う。
この値は通常GETになっていて、後述するような特殊なアクセスを行った場合のみPOSTになる。
直接アクセスの場合はもちろんGET。

Step1 隠したいJavascriptコードの用意

隠したいJsソースを用意する。

javascript
function HideSource(){

    /* ここから処理を記載する */
    let block = document.createElement('div');
    block.style.height = '40px';
    block.style.width = '40px';
    block.style.background = '#00b5ad';
    block.style.borderRadius = '5px';
    block.style.position = 'fixed';
    block.style.top = '50%';
    block.style.left = '50%';
    document.body.appendChild(block);
    /* ここまで */
};

このコードは後述のXMLHttpRequestで読み込み、そのあとに削除する。
そのため、ここに記載するコードはオブジェクト化しておく必要がある。
平たく言うと一つのfunction内にまとめておかなくてはならない。

で、これをStep1のPHPと組み合わせた結果が以下のとおり。

source.php
<?php

/* 直接アクセス禁止設定 */
if($_SERVER["REQUEST_METHOD"] === 'POST'){
    /* 本命のソース
       ※見せたくないソースをここに記載します */
    echo "function HideSource(){

    /* ここから処理を記載する */
    let block = document.createElement('div');
    block.style.height = '40px';
    block.style.width = '40px';
    block.style.background = '#00b5ad';
    block.style.borderRadius = '5px';
    block.style.position = 'fixed';
    block.style.top = '50%';
    block.style.left = '50%';
    document.body.appendChild(block);
    /* ここまで */
};";

}else{
    /* 直接アクセスされた場合のダミー記述 */
    echo 'アクセス……拒否しますっ!!(>_<)';
}

?>

これで、要件①はクリア。
直接URLからページを見てもコードは読めなくなった。

Step3 XMLHttpRequestでの呼び出し

次に呼び出し処理を作成する。
呼び出しには、javascriptのXMLHttpRequestを用いる。

そのままコピペできる内容なので、とりあえずはコードを記載する。

javascript
// 実行
ReadSource(document.body, './source.php');

function ReadSource(target, urls){

    // 宣言
    let request = new XMLHttpRequest();

    // リクエストURLを設定(リクエストタイプはPOSTにすること)
    request.open('POST', urls);

    // リクエスト先からレスポンス(返答)があった場合の処理
    request.onreadystatechange = function(){
        if ( (request.readyState == 4) && (request.status == 200) ){

            let scrpt = document.createElement("script");
            scrpt.type = 'text/javascript';
            scrpt.innerHTML = request.responseText;
            target.appendChild(scrpt);

            // 消去
            target.removeChild(scrpt);

            // 処理 
            HideSource();
        }
    }
    request.send();
}

<script>リンクでファイルを読むのではなく、
外部から読み込んだテキストをhtml上に貼り付け、それをJsとして解釈させているから検証メニューにも載ることはない。
代わりにHTMLソース上に関数HideSource全文が載ってしまうので、それは削除する必要がある。

挙動を見る限り、ソースコードをappendChildした時点で関数全体がどこかに登録される模様。
なので、一度appendChildでhtml上に追加してしまえば消しても大丈夫。

このコードではHideSourceの実行が行われるより先にソースコードを消してしまっているが、
その場合でもReadSource内でHideSourceを実行しておけば問題なく動作する。

で、上記のコードを実際にページに記述してみよう。

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"/>
    <title>たいとる</title>
</head>
<body>

<div id="point"></div>

<script type="text/javascript">
function ReadSource(target, urls) {
    // 宣言
    let request = new XMLHttpRequest();

    // リクエストURLを設定(リクエストタイプはPOSTにすること)
    request.open('POST', urls);

    // リクエスト先からレスポンス(返答)があった場合の処理
    request.onreadystatechange = function(){
        if ( (request.readyState == 4) && (request.status == 200) ){

            let scrpt = document.createElement("script");
            scrpt.type = 'text/javascript';
            scrpt.innerHTML = request.responseText;
            target.appendChild(scrpt);

            // 消去
            target.removeChild(scrpt);

            // 処理 
            HideSource();
        }
    }
    request.send();
}
// 実行
ReadSource(document.getElementById('point'), './source.php');
</script>
</body>
</html>

要件②もクリア。
これで、ソースであるsource.phpを見せないようにしつつ、秘匿したJsを実行できる。やったね。

※完成形のコードはGithubに置いています。
「Clone or download」→「Download ZIP」でダウンロードできます。
PHPを含むので、PHPが動く実行環境が必要というのは言わずもがな。

おまけ XMLHttpRequestとCORS

残るは要件③「あらかじめ決めておいたサイト以外からは呼び出せないように設定する」だが、
これ実は自動的にクリアしている。

今回使ったXMLHttpRequestだが、こいつにはCORSというルールで制限がかけられている。
詳しい説明はこちらの記事に譲るが……すごーーく雑な説明をすると、

呼び出し元と呼び出し先のサイトが別々だったときは呼び出される側に『このサイトはオレを呼び出していいぞ!』という許可証をつけておいてくださいね。許可証なしで他人のサイトからファイル持ってこようとするなら接続させませんよ1

ということ。
今回は呼び出し元(index.html)と呼び出し先(source.php)を同じウェブサイトに同梱しているのでこの制限にはあたらない。

が、もし仮に別サイトから呼び出すことを考慮する2場合、source.phpの文頭にheader()を追記して以下のように変更しよう。

source.php
<?php

header('Access-Control-Allow-Origin: https://example.com'); // この部分を追記。 https://example.comのところは呼び出し元のドメインを指定。

/* 直接アクセス禁止設定 */
if($_SERVER["REQUEST_METHOD"] === 'POST'){
    /* 本命のソース
       ※見せたくないソースをここに記載します */
    echo "function HideSource(){

    /* ここから処理を記載する */
    let block = document.createElement('div');
    block.style.height = '40px';
    block.style.width = '40px';
    block.style.background = '#00b5ad';
    block.style.borderRadius = '5px';
    block.style.position = 'fixed';
    block.style.top = '50%';
    block.style.left = '50%';
    document.body.appendChild(block);
    /* ここまで */
};";

}else{
    /* 直接アクセスされた場合のダミー記述 */
    echo 'アクセス……拒否しますっ!!(>_<)';
}

?>

参考文献

http://pc110.club/php/%EF%BD%8Aavascript-hidden
http://labs.vividworks.jp/javascript-%E3%81%AE%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AF%E9%9A%A0%E8%94%BD%E3%81%99%E3%82%8B%E3%81%B9%E3%81%8D%E3%81%8B%EF%BC%9F-vol-4/
https://tenderfeel.xsrv.jp/mootools/705/


  1. 正確には「オリジン」というやつをを比較する。わからなければ、ドメインと同じものだと思っておけばいい。 

  2. 同じサーバーであっても、別のサブドメインから呼び出したいときには必要だ 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む