20191209のPHPに関する記事は24件です。

【Laravel】 認証でユーザを取得するとき,特定のスコープに限定する

問題

class User extends Model implements Authenticatable
{
    use SoftDeletes;
}
Auth::user() // 論理削除ユーザは除外される

SoftDeletes を使用している Authenticatable の場合,グローバルスコープで論理削除を除外するものが付与されるので,認証時にも除外される。ところが論理削除とは違う「有効アカウント」「無効アカウント」といった表現を status={0,1} のように行っている場合,素の状態では対応できない。ここでは,できるだけ Laravel 標準の Auth に乗っかる形での実装を目指して対応してみる。

(備考)【Laravel】 認証や認可に関する補足資料 - Qiita

実装

AuthScopable インタフェースの作成

モデルごとに実装は異なるので,共通して使えるようにインタフェースを作成する。命名や引数・返り値の方は Laravel の標準的なスコープの規約に従っている。

<?php

namespace App\Auth;

use Illuminate\Database\Eloquent\Builder;

interface AuthScopable
{
    /**
     * Add a scope for authentication.
     *
     * @param  \Illuminate\Database\Eloquent\Builder      $query
     * @return null|\Illuminate\Database\Eloquent\Builder
     */
    public function scopeForAuthentication(Builder $query): ?Builder;
}

ScopedEloquentUserProvider の作成

EloquentUserProvider は Laravel の標準実装にあり,既定ではこれが使用されている。今回はここの一部を継承して,認証でクエリが発行されるときに AuthScopable で示されるスコープを適用できるようにしてみる。

<?php

namespace App\Auth;

use Illuminate\Auth\EloquentUserProvider;

class ScopedEloquentUserProvider extends EloquentUserProvider
{
    /**
     * @param  null|\Illuminate\Database\Eloquent\Model $model
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function newModelQuery($model = null)
    {
        $query = parent::newModelQuery($model);

        $instance = $query->getModel();

        if ($instance instanceof AuthScopable) {
            $query = $instance->scopeForAuthentication($query) ?? $query;
        }

        return $query;
    }
}

AuthServiceProvider および config/auth.php での登録

<?php

namespace App\Providers;

use App;
use App\Auth\ScopedEloquentUserProvider;
use App\Policies;
use App\User;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        App\User::class => Policies\UserPolicy::class,
    ];

    /**
     * Register any authentication / authorization services.
     */
    public function boot(): void
    {
        $this->registerPolicies();

        // カスタム UserProvider として "scoped-eloquent" という名前で登録する
        Auth::provider('scoped-eloquent', function (Container $app, array $config) {
            return $app->make(ScopedEloquentUserProvider::class, [
                'model' => $config['model'],
            ]);
        });
    }
}
<?php

return [

    /* ... */

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | If you have multiple user tables or models you may configure multiple
    | sources which represent each model / table. These sources may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        'users' => [
            'driver' => 'scoped-eloquent', // ここが "eloquent" になっているので, "scoped-eloquent" に書き換え
            'model' => App\User::class,
        ],
    ],

    /* ... */

];

モデルへの適用

あとは書くだけ!

class User extends Model implements Authenticatable, AuthScopable
{
    public function scopeForAuthentication(Builder $query): Builder
    {
        return $query->where('status', 1);
    }
}
Auth::user() // ステータスが 0 のユーザは除外される

おまけ

標準的なスコープ命名規則に従っているので,一応こんな使い方もできます…
(多分そんなに使わないと思うけど)

User::where('email', 'xxx@example.com')->forAuthentication()->firstOrFail()
User::where('email', 'xxx@example.com')->scopes(['forAuthentication'])->firstOrFail()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CakePHP3.8のCsrfProtectionMiddlewareを特定動作時に無効化する

実際のコード

Application.php
Router::scope('/', function (RouteBuilder $routes) {
    // Register scoped middleware for in scopes.
    $routes->registerMiddleware('csrf', new CsrfProtectionMiddleware([
        'httpOnly' => true
    ]));
    /**
     * Apply a middleware to the current route scope.
     * Requires middleware to be registered via `Application::routes()` with `registerMiddleware()`
     */
    if (empty($_POST["postFlg"])){
        $routes->applyMiddleware('csrf');
    };

解説

Application.php
Router::scope('/', function (RouteBuilder $routes) {
    // Register scoped middleware for in scopes.
    $routes->registerMiddleware('csrf', new CsrfProtectionMiddleware([
        'httpOnly' => true
    ]));
    /**
     * Apply a middleware to the current route scope.
     * Requires middleware to be registered via `Application::routes()` with `registerMiddleware()`
     */
    $routes->applyMiddleware('csrf');

元々はこうなっています。

$routes->applyMiddleware('csrf');

この一行がCsrfProtectionMiddlewareを実際に聞かせるもの。
if文で、postFlgの値がPOSTされた時には働かないようにしています。

多分良い方法じゃない

csrfcomponentならいい感じに無効化出来るものもあったし、
csrfmiddleware自体を全部切る方法は簡単に見つかったのですが...
すぐに見つからなかったです。

最後に

PHP自体業務では2週間くらいしか使ってないですし、
CakePHPについては触る事自体2週間という所。
わかりやすく設計はされているんだろうなあ
となんとなくわかるのですが、今はまだ慣れていないですね。

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

pre-commitのススメ

この記事は Git Advent Calendar 2019 10日目の記事です。

はじめに

最近、gitのhooksでpre-commitを使い始めた
なかなか良いことが多かったので書いてみる

モチベーション

  • PHPという自由な言語なので、他人のコードを読む時に苦しみたくない
  • コードレビューの時にスペースがないとかくだらないことをチェックしたくない
  • シンタックスエラーを事前に検知したい

やったこと

  • php -lでのシンタックスチェック
  • phpmdでのお作法チェック
  • php-cs-fixerでの自動修正

上記3点をcommit時にチェック。違反や引っかかることがあった時に、commitできないという結構厳しめのルール設定

php -lについて

  • phpのシンタックスエラーをチェックしてくれる
<?php

$a = "test"
echo $a;

?>
$ php -l file.php

Parse error: syntax error, unexpected 'echo' (T_ECHO) in test.php on line 5
Errors parsing test.php

このようにあらかじめエラーを見つけてくれる

phpmdについて

公式

インストール方法

$ brew install phpmd

$ phpmd --version
PHPMD 2.7.0snapshot201907302127

このように表示されればOK

使用法

  • 使っていない変数のチェック
  • 長すぎる変数名
  • 複雑すぎるクラス

などなど様々なバグの温床をあらかじめ見つけてくれる

使い方は以下のように2通り

$ phpmd file.php text unusedcode,codesize,naming

or

$ phpmd file.php text phpmd-ruleset.xml

phpmdコマンドの後ろにファイル名またはディレクトリ、レポートフォーマット(text or xml)、使用ルールまたはルール記述xmlファイルの指定という風に書く。

ルールセットについて

上記のようにxmlファイルでルールを設定することができる。

phpmd-ruleset.xml
<?xml version="1.0"?>
<ruleset name="My first PHPMD rule set"
        xmlns="http://pmd.sf.net/ruleset/1.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0
                    http://pmd.sf.net/ruleset_xml_schema.xsd"
        xsi:noNamespaceSchemaLocation="
                    http://pmd.sf.net/ruleset_xml_schema.xsd">
<description>
    My custom rule set that checks my code...
</description>

    <rule ref="rulesets/codesize.xml">
        <exclude name="CyclomaticComplexity" />
    </rule>
    <rule ref="rulesets/controversial.xml" />
    <rule ref="rulesets/unusedcode.xml" />
    <rule ref="rulesets/naming.xml">
        <exclude name="ShortVariable" />
        <exclude name="ShortMethodName" />
    </rule>
    <rule ref="rulesets/design.xml">
        <exclude name="CouplingBetweenObjects" />
    </rule>
</ruleset>

このように、<rule>タグで使用ルールを指定。<exclude>タグでルール内の細かいルールの除外をする。

例えば、

<rule ref="rulesets/naming.xml">
    <exclude name="ShortVariable" />
    <exclude name="ShortMethodName" />
</rule>

この場合、ネーミングに関するルールを使用するが、短すぎる(3文字以内)の変数名は引っかかるというルールを除外している。

php-cs-fixerについて

インストール方法

// composerのあるディレクトリに移動
$ cd path/to/your/project

$ composer install

$ ./vendor/bin/php-cs-fixer --version

PHP CS Fixer 2.16.0 Yellow Bird by Fabien Potencier and Dariusz Ruminski

こうなればOK。

使い方

$ ./vendor/bin/php-cs-fixer fix file.php

このようにすると、これが

<?php

$a="test";
echo $a

?>

こうなる

<?php

$a = "test";
echo $a;

これでコードの統一性がある程度出るし、変な指摘をしないで済む。

ルールについて

.php_cs.distというファイルで設定できる

<?php

return PhpCsFixer\Config::create()
    ->setRiskyAllowed(true)
    ->setRules([
        '@PSR2' => true, //基準となるルールの設定
        'array_syntax' => ['syntax' => 'short'], //配列の書き方について
        'global_namespace_import' => ['import_classes' => true, 'import_constants' => true, 'import_functions' => true],  //namespaceの書き方について
        'concat_space' => ['spacing' => 'one'] //文字列連結の際のスペースについて
    ])
    ->setFinder(PhpCsFixer\Finder::create()
        ->exclude('vendor')
        ->in(__DIR__)
    );

これ以外にもこれを使えば便利に比較することができる。

pre-commitの設定

$ cd path/to/your/project/.git/hooks

$ touch pre-commit

$ vim pre-commit
#!/bin/sh
against=HEAD

IS_ERROR=0

for FILE in `git diff-index --name-status $against -- | grep -E '^[AUM].*\.php$'| cut -c3-`; do
    if php -l $FILE; then
        ./vendor/bin/php-cs-fixer fix $FILE
        git add $FILE

        if ! phpmd $FILE text phpmd-ruleset.xml; then
            IS_ERROR=1
        fi
    else
        IS_ERROR=1
    fi
done
exit $IS_ERROR

.phpファイルのみだけがチェック対象。

導入後

  • コードレビューでクリティカルな指摘に集中できるようになった
  • コーディングスタイルが整うようになり、可読性が上がった
  • 潜在的なバグが減らせた(と思ってる)
  • 一度にcommitするとしんどいからcommit粒度が細かくなり、振り返りやすくなった

良いことだらけ。
ぜひ良いコミットライフを!

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

【PHP】【初心者向け】ぼっちっちゲームを作ってみた

この記事は2019新卒 エンジニア Advent Calendar 2019の10日目の記事です。

19新卒!というわけで参加させていただきました。が,バリバリ初心者ゆえ,記事には期待しないでください…

自分について

  • 2019年,文学部を卒業
  • プログラミングまったくの未経験で,システム開発の部署に
  • HTML・CSS・JS・PHPを学び今に至る

本題

PHP(とMySQLとJS)を用いて,ぼっちっちゲームができるWEBアプリを開発しました。
(※実際に作ったのは10月頃です)

ぼっちっちゲームって?

関ジャニ∞クロニクル という番組で実施された,多人数で遊ぶゲームです。

お題がかかれているカードを1人ずつにランダムに振り分け、かかれている単語について話し合う。5枚の内1枚だけ違うお題がかかれているのだが、1人だけ違うカードを持っている人、つまり「ぼっちちっちが誰なのか」を最も早く見破ったメンバーが勝利するというもの。

引用:https://www.fujitv.co.jp/fujitv/news/20195531.html

ワードウルフと大部分は共通しています。どっちかというとワードウルフのほうが有名かもしれません。
もちろんワードウルフでも遊べます。

イメージを考える

完成イメージ:

遊ぶメンバーで集まる
⇒.phpファイルにアクセスする
⇒スタートボタンを押す(ほかの人は画面を見てはいけない)
⇒ブラウザにお題が表示される
⇒ボタンを押したらお題が非表示になる
⇒確認したら次の人にスマホを渡す
⇒…
⇒人数分スマホを回したらゲームスタート。

人狼ゲームのアプリのイメージです。

できたやつのスクショをどうぞ


1. 人数を設定してスタートボタンを押す

2. スタートボタンを押した直後,一人目のお題表示

3. 隠すボタンを押してお題を隠し,次の人にスマホを回す

4. 次の人は進むボタンを押すと,次のお題が表示される 確認したら隠す これを繰り返す

5. どこかでお題が1度だけ変わる

6. 5回表示したので,ゲームスタート!

プログラムを考えていく

5人で遊ぶ場合を考えて

  • 進むボタンを押すたびに数字が増える変数を設定する これを \$count とする
  • 1~5からランダムな数字を一つ決定する これを \$botti として
  • if (\$count === \$botti) の場合に,違うお題を表示
  • お題は多次元配列で管理, \$data[\$key] とする
    • 進むボタンをただの更新ボタンにし,\$count・\$botti・\$keyをCookieで保持すればできるのでは。

というわけでまず書いてみた

<?php
$data1 = [];
$data1[0] = [];
$data1[1] = [];
$data1[0][0] = '東京';
$data1[0][1] = '富士山';

$data1[1][0] = '横山';
$data1[1][1] = '村上';

$data1[2][0] = '富士山';
$data1[2][1] = '東京';

$data1[3][0] = '村上';
$data1[3][1] = '横山';

//お題を選ぶ
if(isset($_COOKIE['key'])){
    $key = $_COOKIE['key'];
}else{
    $key = rand(0,3);
}

//ぼっちは何番目~
if(isset($_COOKIE['botti'])){
    $botti = $_COOKIE['botti'];
}else{
    $botti = rand(1,5);
}

//カウント情報があるか判定
if(isset($_COOKIE['count'])){
    $count = $_COOKIE['count'];
    $count++;
}else{
    $count = 1;
}

//cookieに保存する
setcookie('count',$count);
setcookie('botti',$botti);
setcookie('key',$key);

//リセットされたときcookie削除
if(isset($_POST['reset'])){
    setcookie('botti',$botti, time()-3600);
    setcookie('count',$count, time()-3600);
    setcookie('key',$key, time()-3600);
}
?>
<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <title>ぼっち</title>
    </head>
    <body>
        <p>あなたのお題は:</p>
        <?php if($count == $botti){ ?>
            <p><?php echo $data1[$key][0]; ?></p>
        <?php }else{ ?>
            <p><?php echo $data1[$key][1]; ?></p>
        <?php } ?>
        <form method='post'>
            <input type='submit' value='実行'>
        </form>
        <form method="post">
            <input type='hidden' name='reset' value='reset'>
            <input type='submit' value='リセット'>
        </form>
        <?php if(isset($_POST['reset'])){ ?>
            <div>リセットされました</div>
        <?php }?>
    </body>
</html>

Q.お題の意味はなんですか
A.番組をみたらわかります

(一番最初に書いたやつをほぼそのまま持ってきたので変数名とかひどいです)
とりあえず期待していた動きはできました。しかし,遊べるレベルではありません

  • 人数設定ができない(ファイルを直接いじる必要がある)
  • 非表示ボタンがない
  • 表示されるお題は2つのうちどちらか,しかも二重登録する必要がある
    • 例えば果物をお題にする場合,現状「りんご」「バナナ」で \$data1[0][0]/[1] とすると,「りんご」が必ず多数派になります。したがって,「バナナ」「りんご」の順でもお題登録をしなければいけません。
    • そもそも,「りんご」「バナナ」「さくらんぼ」「メロン」…など複数のお題から多数派/少数派を選ぶ仕組みのほうが,ランダム性もあり楽しいはずです。

改善してみた

<?php

$data = [];

//テンプレ:$data[] = array();
$data[0] = array('大野智','二宮和也','櫻井翔','松本潤','相葉雅紀');
$data[1] = array('国分太一','松岡昌宏','長瀬智也','城島茂');
$data[2] = array('坂本昌行','長野博','井ノ原快彦','森田剛','三宅健');
$data[3] = array('横山裕','村上信五','丸山隆平','安田章大','大倉忠義');
$data[4] = array('山田涼介','知念侑李','中島裕翔','有岡大貴','高木雄也','伊野尾慧','八乙女光','薮宏太','岡本圭人');
$data[5] = array('中島健人','菊池風磨','佐藤勝利','松島聡','マリウス葉');
$data[6] = array('亀梨和也','上田竜也','中丸雄一');
$data[7] = array('加藤シゲアキ','小山慶一郎','手越祐也','増田貴久');

//人数が送信された場合
if(isset($_POST['num'])){
    $person = intval($_POST['num']);

    //自然数か判定する
    if($person > 0){

        //関数呼び出し
        $cookieArr = setValue($data, $person);

        $group = $cookieArr[0];
        $key1 = $cookieArr[1];
        $key2 = $cookieArr[2];
        $botti = $cookieArr[3];
        $count = $cookieArr[4];

        $cookieArr[] = $person;

        $cookieValue = implode(',',$cookieArr);
        setcookie('cookie',$cookieValue);

    }else{
        $error = '数字は1以上で入力してください。';
    }
}

//Cookieが存在する場合
if(isset($_COOKIE['cookie'])){
    $cookieArr = explode(',',$_COOKIE['cookie']);

    $group = $cookieArr[0];
    $key1 = $cookieArr[1];
    $key2 = $cookieArr[2];
    $botti = $cookieArr[3];
    $count = $cookieArr[4]+1;
    $person = $cookieArr[5];

    $cookieArr[4]++;

    //再びsetcookie
    $cookieValue = implode(',',$cookieArr);
    setcookie('cookie',$cookieValue);

    //リセットされた場合
    if(isset($_POST['reset'])){
        setcookie('cookie', $cookieValue, time()-3600);
    }
}

function setValue($data,$person){ //2回目アクセスしか発生しない。

    //group = $dataのどれを選ぶか
    $group = rand(0,(count($data))-1);

    //keys = $dataの多数派と少数派を決める
    $keys = array_rand($data[$group], 2);
    var_dump($data[$group]);
    shuffle($keys);

    $key1 = $keys[0];
    $key2 = $keys[1];

    $botti = rand(1,$person);

    $count = 1;

    return array($group, $key1, $key2, $botti, $count);
}
?>
<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <title>ぼっちっちゲーム(仮)</title>
        <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
    </head>
    <body>
        <div class='container'>
            <p>☆リセット→人数入力→スタートを押すようにしてください☆</p>
            <form method='post'>
                <p><input type='number' name='num' style='width:50px;'>人でプレイ!
                <input type='submit' value='スタート'></p>
            </form>
            <p>あなたのお題は:</p>
            <?php if(isset($_COOKIE['cookie']) || isset($_POST['num'])){  ?>
                <?php if($count == $botti){ ?>
                    <p id='answer1'><?php echo $data[$group][$key1]; ?></p>
                <?php }elseif(intval($count) > intval($person)){ ?>         
                    <p>人数分表示しました。</p>
                <?php }else{ ?>
                    <p id='answer2'><?php echo $data[$group][$key2]; ?></p>
                <?php } ?>
                <form>
                    <input type='submit' value='進む'>
                </form>
            <?php } ?>
            <input type='button' onclick="hidden1();" value='隠す'>
            <form method="post">
                <input type='hidden' name='reset' value='reset'>
                <input type='submit' value='リセット'>
            </form>
            <?php if(isset($_POST['reset'])){ ?>
                <div>リセットされました</div>
            <?php }?>
            <?php if(isset($error) && empty($_COOKIE['cookie'])){ ?>
                <div>数字は1以上で入力してください。</div>
            <?php }?>
        </div>
        <script>
            function hidden1(){
                if(document.getElementById('answer1') != null){
                    var ans1 = document.getElementById('answer1');
                    ans1.style.display='none';
                }else{
                    var ans2 = document.getElementById('answer2');
                    ans2.style.display='none';
                }
            }
        </script>
    </body>
</html>

Q. ジャニオタなの?
A. ジャニオタではない

これで,3つ以上の要素のあるお題配列からランダムに2つお題として選ばれる要素が決定され,表示させることができました。
しかし,やはりお題をファイルの中に組み入れているのが気になります。プログラムを書いた人が有利になってしまう。参加する人がみんなお題登録できるべきでしょう。

上記では配列でお題を持っていますが,カンマ区切りで文字列としてお題を登録して,そいつをexplodeすればよいのでは!?

お題登録ページを作る

というわけで,データベースと連携してブラウザからもお題登録できるようにします。
お題を登録するデータベースをまず作成します。

ID    int(11)      not null    auto_increment
DATA varchar(1000)   not null

ぶっちゃけカラムは1つあればよいのですが,念のためIDカラムも作成します。

そして,お題登録ページを作成します。遊ぶページとは別にしたかったので,以下のようなページ構成になりました

top.html ―― create.php  …お題登録ページ
     |
     ―― play.php   …今まで作ってきたページ

現在の完成形を以下に載せます(top.htmlは省略)

create.php
<?php
$dsn = 'mysql:dbname=***;host=***;charset=utf8;';
$user = '***';
$pass = '***';

try{
    $pdo = new PDO($dsn,$user,$pass,
        [PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC]
    );

    if(isset($_POST['inputs'])){
        $input = $_POST['inputs'];
        if(preg_match('/.+,.+/',$input)){
            $query = 'INSERT INTO botti(DATA) VALUES(:input);';
            $stmt = $pdo -> prepare($query);
            $stmt -> bindValue(':input', $input, PDO::PARAM_STR);
            $stmt -> execute();
            $message = '登録されました。';
        }else{
            $message = '正しくない入力です!';
        }
    }

}catch(PDOExpection $e){
    exit($e->getMessage());
}
?>

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <title>お題登録</title>
        <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
        <script src='js/bootstrap.bundle.js'></script>
        <link rel='stylesheet' href='css/bootstrap.css'>
    </head>
    <body>
        <div class='container'>
            <p>お題をカンマ区切りで入力してください。カンマは半角でお願いします。</p>
            <p>例:りんご,バナナ,メロン,さくらんぼ</p>
            <form method='post' name="myform">
                <input type='text' name='inputs'>
                <input type="button" class='register' value="登録" onclick='confirmFunc();'>
            </form>
            <?php if(isset($message)){ ?>
                <div class='mt-2'><?php echo $message; ?></div>
            <?php } ?>
            <a href='top.html'>戻る</a>
        </div>
        <script>
            function confirmFunc(){
                var val = confirm('登録します。よろしいですか?');
                if (val == true){
                    document.myform.submit();
                }
            }
        </script>
    </body>
</html>

play.php
<?php

$dsn = 'mysql:dbname=***;host=***;charset=utf8;';
$user = '***';
$pass = '***';

try{
    $pdo = new PDO($dsn,$user,$pass,
        [PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC]
    );

    $stmt = $pdo->query('select DATA from botti');
    $rows = $stmt->fetchAll(PDO::FETCH_COLUMN);

}catch(PDOExpection $e){
    exit($e->getMessage());
}

//人数が送信された場合
if(isset($_POST['num'])){
    $person = intval($_POST['num']);

    //自然数か判定する
    if($person > 0){

        //関数呼び出し
        $cookieArr = setValue($rows, $person);

        $group = $cookieArr[0];
        $key0 = $cookieArr[1];
        $key1 = $cookieArr[2];
        $botti = $cookieArr[3];
        $count = $cookieArr[4];

        $cookieArr[] = $person;

        $data = explode(',', $rows[$group]);

        $cookieValue = implode(',',$cookieArr);
        setcookie('cookie',$cookieValue);

    }else{
        $error = '数字は1以上で入力してください。';
    }
}

//Cookieが存在する場合
if(isset($_COOKIE['cookie'])){
    $cookieArr = explode(',',$_COOKIE['cookie']);

    $group = $cookieArr[0];
    $key0 = $cookieArr[1];
    $key1 = $cookieArr[2];
    $botti = $cookieArr[3];
    $count = $cookieArr[4]+1;
    $person = $cookieArr[5];

    $cookieArr[4]++;

    $data = explode(',', $rows[$group]);

    //再びsetcookie
    $cookieValue = implode(',',$cookieArr);
    setcookie('cookie',$cookieValue);

    //リセットされた場合
    if(isset($_POST['reset'])){
        setcookie('cookie', $cookieValue, time()-3600);
    }
}

function setValue($rows,$person){ //2回目アクセスしか発生しない。

    //group = $dataのどれを選ぶか
    $group = rand(0,(count($rows))-1); //int

    $data = explode(',',$rows[$group]); //str→array

    //keys = $dataの多数派と少数派を決める
    $keys = array_rand($data, 2);
    shuffle($keys);

    $key0 = $keys[0];
    $key1 = $keys[1];

    $botti = rand(1,$person);

    $count = 1;

    return array($group, $key0, $key1, $botti, $count);
}


function h($str)
{
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
?>

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <title>ぼっちっちゲーム(仮)</title>
        <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
        <script src='js/bootstrap.bundle.js'></script>
        <link rel='stylesheet' href='css/bootstrap.css'>
    </head>
    <body>
        <div class='container'>
            <p></p>
            <p>☆リセット→人数入力→スタートを押すようにしてください☆</p>
            <form method='post'>
                <p><input type='number' name='num' style='width:50px;'>人でプレイ!
                <input type='submit' value='スタート'></p>
            </form>
            <p></p>
            <p>あなたのお題は:</p>
            <?php if(isset($_COOKIE['cookie']) || isset($_POST['num'])){  ?>
                <?php if($count == $botti){ ?>
                    <p id='answer1'><?php echo h($data[$key0]); ?></p>
                <?php }elseif(intval($count) > intval($person)){ ?>         
                    <p>人数分表示しました。</p>
                <?php }else{ ?>
                    <p id='answer2'><?php echo h($data[$key1]); ?></p>
                <?php } ?>
                <form>
                    <input type='submit' value='進む'>
                </form>
            <?php } ?>
            <p></p>
            <p></p>
            <input type='button' onclick="hideAns();" value='隠す'>
            <p></p>
            <form method="post">
                <input type='hidden' name='reset' value='reset'>
                <input type='submit' value='リセット'>
            </form>
            <?php if(isset($_POST['reset'])){ ?>
                <div>リセットされました</div>
            <?php }?>
            <?php if(isset($error) && empty($_COOKIE['cookie'])){ ?>
                <div>数字は1以上で入力してください。</div>
            <?php }?>
            <p></p>
            <p></p>
            <a href='top.html'>戻る</a>
        </div>
        <script>
            function hideAns(){
                if(document.getElementById('answer1') != null){
                    var ans1 = document.getElementById('answer1');
                    ans1.style.display='none';
                }else{
                    var ans2 = document.getElementById('answer2');
                    ans2.style.display='none';
                }
            }
        </script>
    </body>
</html>

Q.やっぱり変数名ひどくない?
A.返す言葉もございません。

Q.なんでHTMLタグの属性,シングルクォーテーションなの?
A.この頃の癖です。今はダブルにしてます。

Q.Bootstrap使ってなくない?
A.見た目考えるのめんどくさかっ(ry

それはともかく,誰でもお題が登録できるようになりました!これでプログラム作成者もお題のすべては知りません!!

実際に遊んでみた

このプログラムを使って実際に友達と遊びました。事前にお題登録はお願いしていたのですが,なんと100を超えるお題が登録されておりました!

楽しかったです!!
とても盛り上がりました。

そのほか作ってみた感想

  • PHP,HTML,JS,Cookie,データベースといった色んな要素を組み合わせて作ったので学んだことの復習にもなりました。
  • ソースコードはまだまだ稚拙なものなので,また書き直したいです。とりあえず毎回テーブルを全件取得してるのはよろしくないので,SELECTの段階で1レコードに絞り込みたいですね。あとはオブジェクト指向をマスターしてからかな…
  • そして,この記事を書いてみて,アウトプットの難しさを痛感しました_(:3 」∠)_うへー

ここまでお読みくださりありがとうございました!
初心者のみなさまお互い頑張りましょう。

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

【PHP】【初心者向け】ぼっちっちは誰だゲームを作ってみた

この記事は2019新卒 エンジニア Advent Calendar 2019の10日目の記事です。

19新卒!というわけで参加させていただきました。が,バリバリ初心者ゆえ,記事には期待しないでください…

自分について

  • 2019年,文学部を卒業
  • プログラミングまったくの未経験で,システム開発の部署に
  • HTML・CSS・JS・PHPを学び今に至る

本題

PHP(とMySQLとJS)を用いて,ぼっちっちは誰だゲームができるWEBアプリを開発しました。
(※実際に作ったのは10月頃です)

ぼっちっちは誰だゲームって?

関ジャニ∞クロニクル という番組で実施された,多人数で遊ぶゲームです。

お題がかかれているカードを1人ずつにランダムに振り分け、かかれている単語について話し合う。5枚の内1枚だけ違うお題がかかれているのだが、1人だけ違うカードを持っている人、つまり「ぼっちちっちが誰なのか」を最も早く見破ったメンバーが勝利するというもの。

引用:https://www.fujitv.co.jp/fujitv/news/20195531.html

ワードウルフと大部分は共通しています。どっちかというとワードウルフのほうが有名かもしれません。
もちろんワードウルフでも遊べます。

イメージを考える

完成イメージ:

遊ぶメンバーで集まる
⇒.phpファイルにアクセスする
⇒スタートボタンを押す(ほかの人は画面を見てはいけない)
⇒ブラウザにお題が表示される
⇒ボタンを押したらお題が非表示になる
⇒確認したら次の人にスマホを渡す
⇒…
⇒人数分スマホを回したらゲームスタート。

人狼ゲームのアプリのイメージです。

できたやつのスクショをどうぞ


1. 人数を設定してスタートボタンを押す

2. スタートボタンを押した直後,一人目のお題表示

3. 隠すボタンを押してお題を隠し,次の人にスマホを回す

4. 次の人は進むボタンを押すと,次のお題が表示される 確認したら隠す これを繰り返す

5. どこかでお題が1度だけ変わる

6. 5回表示したので,ゲームスタート!

プログラムを考えていく

5人で遊ぶ場合を考えて

  • 進むボタンを押すたびに数字が増える変数を設定する これを \$count とする
  • 1~5からランダムな数字を一つ決定する これを \$botti として
  • if (\$count === \$botti) の場合に,違うお題を表示
  • お題は多次元配列で管理, \$data[\$key] とする
    • 進むボタンをただの更新ボタンにし,\$count・\$botti・\$keyをCookieで保持すればできるのでは。

というわけでまず書いてみた

<?php
$data1 = [];
$data1[0] = [];
$data1[1] = [];
$data1[0][0] = '東京';
$data1[0][1] = '富士山';

$data1[1][0] = '横山';
$data1[1][1] = '村上';

$data1[2][0] = '富士山';
$data1[2][1] = '東京';

$data1[3][0] = '村上';
$data1[3][1] = '横山';

//お題を選ぶ
if(isset($_COOKIE['key'])){
    $key = $_COOKIE['key'];
}else{
    $key = rand(0,3);
}

//ぼっちは何番目~
if(isset($_COOKIE['botti'])){
    $botti = $_COOKIE['botti'];
}else{
    $botti = rand(1,5);
}

//カウント情報があるか判定
if(isset($_COOKIE['count'])){
    $count = $_COOKIE['count'];
    $count++;
}else{
    $count = 1;
}

//cookieに保存する
setcookie('count',$count);
setcookie('botti',$botti);
setcookie('key',$key);

//リセットされたときcookie削除
if(isset($_POST['reset'])){
    setcookie('botti',$botti, time()-3600);
    setcookie('count',$count, time()-3600);
    setcookie('key',$key, time()-3600);
}
?>
<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <title>ぼっち</title>
    </head>
    <body>
        <p>あなたのお題は:</p>
        <?php if($count == $botti){ ?>
            <p><?php echo $data1[$key][0]; ?></p>
        <?php }else{ ?>
            <p><?php echo $data1[$key][1]; ?></p>
        <?php } ?>
        <form method='post'>
            <input type='submit' value='実行'>
        </form>
        <form method="post">
            <input type='hidden' name='reset' value='reset'>
            <input type='submit' value='リセット'>
        </form>
        <?php if(isset($_POST['reset'])){ ?>
            <div>リセットされました</div>
        <?php }?>
    </body>
</html>

Q.お題の意味はなんですか
A.番組をみたらわかります

(一番最初に書いたやつをほぼそのまま持ってきたので変数名とかひどいです)
とりあえず期待していた動きはできました。しかし,遊べるレベルではありません

  • 人数設定ができない(ファイルを直接いじる必要がある)
  • 非表示ボタンがない
  • 表示されるお題は2つのうちどちらか,しかも二重登録する必要がある

    • 例えば果物をお題にする場合,現状「りんご」「バナナ」で \$data1[0][0]/[1] とすると,「りんご」が必ず多数派になります。したがって,「バナナ」「りんご」の順でもお題登録をしなければいけません。
    • そもそも,「りんご」「バナナ」「さくらんぼ」「メロン」…など複数のお題から多数派/少数派を選ぶ仕組みのほうが,ランダム性もあり楽しいはずです。
  • したがって、「どのお題が選ばれたか」「そのうちの何番目と何番目の要素が多数派/少数派として選ばれたか」「何番目の人間がぼっちか」をCookieに持つ必要があります。また、遊ぶ人数もCookieに持っておいたほうがよいでしょう。

改善してみた

<?php

$data = [];

//テンプレ:$data[] = array();
$data[0] = array('大野智','二宮和也','櫻井翔','松本潤','相葉雅紀');
$data[1] = array('国分太一','松岡昌宏','長瀬智也','城島茂');
$data[2] = array('坂本昌行','長野博','井ノ原快彦','森田剛','三宅健');
$data[3] = array('横山裕','村上信五','丸山隆平','安田章大','大倉忠義');
$data[4] = array('山田涼介','知念侑李','中島裕翔','有岡大貴','高木雄也','伊野尾慧','八乙女光','薮宏太','岡本圭人');
$data[5] = array('中島健人','菊池風磨','佐藤勝利','松島聡','マリウス葉');
$data[6] = array('亀梨和也','上田竜也','中丸雄一');
$data[7] = array('加藤シゲアキ','小山慶一郎','手越祐也','増田貴久');

//人数が送信された場合
if(isset($_POST['num'])){
    $person = intval($_POST['num']);

    //自然数か判定する
    if($person > 0){

        //関数呼び出し
        $cookieArr = setValue($data, $person);

        $group = $cookieArr[0];
        $key1 = $cookieArr[1];
        $key2 = $cookieArr[2];
        $botti = $cookieArr[3];
        $count = $cookieArr[4];

        $cookieArr[] = $person;

        $cookieValue = implode(',',$cookieArr);
        setcookie('cookie',$cookieValue);

    }else{
        $error = '数字は1以上で入力してください。';
    }
}

//Cookieが存在する場合
if(isset($_COOKIE['cookie'])){
    $cookieArr = explode(',',$_COOKIE['cookie']);

    $group = $cookieArr[0];
    $key1 = $cookieArr[1];
    $key2 = $cookieArr[2];
    $botti = $cookieArr[3];
    $count = $cookieArr[4]+1;
    $person = $cookieArr[5];

    $cookieArr[4]++;

    //再びsetcookie
    $cookieValue = implode(',',$cookieArr);
    setcookie('cookie',$cookieValue);

    //リセットされた場合
    if(isset($_POST['reset'])){
        setcookie('cookie', $cookieValue, time()-3600);
    }
}

function setValue($data,$person){ //2回目アクセスしか発生しない。

    //group = $dataのどれを選ぶか
    $group = rand(0,(count($data))-1);

    //keys = $dataの多数派と少数派を決める
    $keys = array_rand($data[$group], 2);
    var_dump($data[$group]);
    shuffle($keys);

    $key1 = $keys[0];
    $key2 = $keys[1];

    $botti = rand(1,$person);

    $count = 1;

    return array($group, $key1, $key2, $botti, $count);
}
?>
<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <title>ぼっちっちゲーム(仮)</title>
        <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
    </head>
    <body>
        <div class='container'>
            <p>☆リセット→人数入力→スタートを押すようにしてください☆</p>
            <form method='post'>
                <p><input type='number' name='num' style='width:50px;'>人でプレイ!
                <input type='submit' value='スタート'></p>
            </form>
            <p>あなたのお題は:</p>
            <?php if(isset($_COOKIE['cookie']) || isset($_POST['num'])){  ?>
                <?php if($count == $botti){ ?>
                    <p id='answer1'><?php echo $data[$group][$key1]; ?></p>
                <?php }elseif(intval($count) > intval($person)){ ?>         
                    <p>人数分表示しました。</p>
                <?php }else{ ?>
                    <p id='answer2'><?php echo $data[$group][$key2]; ?></p>
                <?php } ?>
                <form>
                    <input type='submit' value='進む'>
                </form>
            <?php } ?>
            <input type='button' onclick="hidden1();" value='隠す'>
            <form method="post">
                <input type='hidden' name='reset' value='reset'>
                <input type='submit' value='リセット'>
            </form>
            <?php if(isset($_POST['reset'])){ ?>
                <div>リセットされました</div>
            <?php }?>
            <?php if(isset($error) && empty($_COOKIE['cookie'])){ ?>
                <div>数字は1以上で入力してください。</div>
            <?php }?>
        </div>
        <script>
            function hidden1(){
                if(document.getElementById('answer1') != null){
                    var ans1 = document.getElementById('answer1');
                    ans1.style.display='none';
                }else{
                    var ans2 = document.getElementById('answer2');
                    ans2.style.display='none';
                }
            }
        </script>
    </body>
</html>

Q. ジャニオタなの?
A. ジャニオタではない

これで,3つ以上の要素のあるお題配列からランダムに2つお題として選ばれる要素が決定され,表示させることができました。
しかし,やはりお題をファイルの中に組み入れているのが気になります。プログラムを書いた人が有利になってしまう。参加する人がみんなお題登録できるべきでしょう。

上記では配列でお題を持っていますが,カンマ区切りで文字列としてお題を登録して,そいつをexplodeすればよいのでは!?

お題登録ページを作る

というわけで,データベースと連携してブラウザからもお題登録できるようにします。
お題を登録するデータベースをまず作成します。

ID    int(11)      not null    auto_increment
DATA varchar(1000)   not null

ぶっちゃけカラムは1つあればよいのですが,念のためIDカラムも作成します。

そして,お題登録ページを作成します。遊ぶページとは別にしたかったので,以下のようなページ構成になりました

top.html ―― create.php  …お題登録ページ
     |
     ―― play.php   …今まで作ってきたページ

現在の完成形を以下に載せます(top.htmlは省略)

create.php
<?php
$dsn = 'mysql:dbname=***;host=***;charset=utf8;';
$user = '***';
$pass = '***';

try{
    $pdo = new PDO($dsn,$user,$pass,
        [PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC]
    );

    if(isset($_POST['inputs'])){
        $input = $_POST['inputs'];
        if(preg_match('/.+,.+/',$input)){
            $query = 'INSERT INTO botti(DATA) VALUES(:input);';
            $stmt = $pdo -> prepare($query);
            $stmt -> bindValue(':input', $input, PDO::PARAM_STR);
            $stmt -> execute();
            $message = '登録されました。';
        }else{
            $message = '正しくない入力です!';
        }
    }

}catch(PDOExpection $e){
    exit($e->getMessage());
}
?>

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <title>お題登録</title>
        <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
        <script src='js/bootstrap.bundle.js'></script>
        <link rel='stylesheet' href='css/bootstrap.css'>
    </head>
    <body>
        <div class='container'>
            <p>お題をカンマ区切りで入力してください。カンマは半角でお願いします。</p>
            <p>例:りんご,バナナ,メロン,さくらんぼ</p>
            <form method='post' name="myform">
                <input type='text' name='inputs'>
                <input type="button" class='register' value="登録" onclick='confirmFunc();'>
            </form>
            <?php if(isset($message)){ ?>
                <div class='mt-2'><?php echo $message; ?></div>
            <?php } ?>
            <a href='top.html'>戻る</a>
        </div>
        <script>
            function confirmFunc(){
                var val = confirm('登録します。よろしいですか?');
                if (val == true){
                    document.myform.submit();
                }
            }
        </script>
    </body>
</html>

play.php
<?php

$dsn = 'mysql:dbname=***;host=***;charset=utf8;';
$user = '***';
$pass = '***';

try{
    $pdo = new PDO($dsn,$user,$pass,
        [PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC]
    );

    $stmt = $pdo->query('select DATA from botti');
    $rows = $stmt->fetchAll(PDO::FETCH_COLUMN);

}catch(PDOExpection $e){
    exit($e->getMessage());
}

//人数が送信された場合
if(isset($_POST['num'])){
    $person = intval($_POST['num']);

    //自然数か判定する
    if($person > 0){

        //関数呼び出し
        $cookieArr = setValue($rows, $person);

        $group = $cookieArr[0];
        $key0 = $cookieArr[1];
        $key1 = $cookieArr[2];
        $botti = $cookieArr[3];
        $count = $cookieArr[4];

        $cookieArr[] = $person;

        $data = explode(',', $rows[$group]);

        $cookieValue = implode(',',$cookieArr);
        setcookie('cookie',$cookieValue);

    }else{
        $error = '数字は1以上で入力してください。';
    }
}

//Cookieが存在する場合
if(isset($_COOKIE['cookie'])){
    $cookieArr = explode(',',$_COOKIE['cookie']);

    $group = $cookieArr[0];
    $key0 = $cookieArr[1];
    $key1 = $cookieArr[2];
    $botti = $cookieArr[3];
    $count = $cookieArr[4]+1;
    $person = $cookieArr[5];

    $cookieArr[4]++;

    $data = explode(',', $rows[$group]);

    //再びsetcookie
    $cookieValue = implode(',',$cookieArr);
    setcookie('cookie',$cookieValue);

    //リセットされた場合
    if(isset($_POST['reset'])){
        setcookie('cookie', $cookieValue, time()-3600);
    }
}

function setValue($rows,$person){ //2回目アクセスしか発生しない。

    //group = $dataのどれを選ぶか
    $group = rand(0,(count($rows))-1); //int

    $data = explode(',',$rows[$group]); //str→array

    //keys = $dataの多数派と少数派を決める
    $keys = array_rand($data, 2);
    shuffle($keys);

    $key0 = $keys[0];
    $key1 = $keys[1];

    $botti = rand(1,$person);

    $count = 1;

    return array($group, $key0, $key1, $botti, $count);
}


function h($str)
{
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
?>

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <title>ぼっちっちゲーム(仮)</title>
        <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
        <script src='js/bootstrap.bundle.js'></script>
        <link rel='stylesheet' href='css/bootstrap.css'>
    </head>
    <body>
        <div class='container'>
            <p></p>
            <p>☆リセット→人数入力→スタートを押すようにしてください☆</p>
            <form method='post'>
                <p><input type='number' name='num' style='width:50px;'>人でプレイ!
                <input type='submit' value='スタート'></p>
            </form>
            <p></p>
            <p>あなたのお題は:</p>
            <?php if(isset($_COOKIE['cookie']) || isset($_POST['num'])){  ?>
                <?php if($count == $botti){ ?>
                    <p id='answer1'><?php echo h($data[$key0]); ?></p>
                <?php }elseif(intval($count) > intval($person)){ ?>         
                    <p>人数分表示しました。</p>
                <?php }else{ ?>
                    <p id='answer2'><?php echo h($data[$key1]); ?></p>
                <?php } ?>
                <form>
                    <input type='submit' value='進む'>
                </form>
            <?php } ?>
            <p></p>
            <p></p>
            <input type='button' onclick="hideAns();" value='隠す'>
            <p></p>
            <form method="post">
                <input type='hidden' name='reset' value='reset'>
                <input type='submit' value='リセット'>
            </form>
            <?php if(isset($_POST['reset'])){ ?>
                <div>リセットされました</div>
            <?php }?>
            <?php if(isset($error) && empty($_COOKIE['cookie'])){ ?>
                <div>数字は1以上で入力してください。</div>
            <?php }?>
            <p></p>
            <p></p>
            <a href='top.html'>戻る</a>
        </div>
        <script>
            function hideAns(){
                if(document.getElementById('answer1') != null){
                    var ans1 = document.getElementById('answer1');
                    ans1.style.display='none';
                }else{
                    var ans2 = document.getElementById('answer2');
                    ans2.style.display='none';
                }
            }
        </script>
    </body>
</html>

Q.やっぱり変数名ひどくない?
A.返す言葉もございません。

Q.なんでHTMLタグの属性,シングルクォーテーションなの?
A.この頃の癖です。今はダブルにしてます。

Q.Bootstrap使ってなくない?
A.見た目考えるのめんどくさかっ(ry

それはともかく,誰でもお題が登録できるようになりました!これでプログラム作成者もお題のすべては知りません!!

実際に遊んでみた

このプログラムを使って実際に友達と遊びました。事前にお題登録はお願いしていたのですが,なんと100を超えるお題が登録されておりました!

楽しかったです!!
とても盛り上がりました。

そのほか作ってみた感想

  • PHP,HTML,JS,Cookie,データベースといった色んな要素を組み合わせて作ったので学んだことの復習にもなりました。
  • ソースコードはまだまだ稚拙なものなので,また書き直したいです。とりあえず毎回テーブルを全件取得してるのはよろしくないので,SELECTの段階で1レコードに絞り込みたいですね。また、idの値で自分がぼっちか分かってしまいます。Cookieはもう仕方ない。あとはオブジェクト指向をマスターしてからかな…
  • そして,この記事を書いてみて,アウトプットの難しさを痛感しました_(:3 」∠)_うへー

ここまでお読みくださりありがとうございました!
初心者のみなさまお互い頑張りましょう。

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

”パソコンを付ける”から”なんとか開発できる”になるまで

始めまして!
新卒1年目でエンジニアをしているマンです。
この度Advent Calendarの企画に混ぜて頂き、初めての投稿をしてみます
至らぬ点もあるかと思いますが是非色々ご指摘頂ければ幸いです。
今回は初めて開発していく中で理解に苦しんだ点を共有してみたいと思います

開発環境

  • PHP
  • phpMyAdmin
  • phpStorm
  • Git

開発を始める前の私の状態

  • とりあえずインターネットで検索はできる
  • PHP?環境?え、何それは…
  • 簡単なゲームを作りたいと思ってC++をやっていた時期が私にもありました

私に課せられた課題

WEBアプリケーションのソースコードをローカル上で確認し、
あるページの文言○○を××に変える

私『勝ったなガハハ』

はい、地獄のスタート

その壱

”まずツールの使い方が分からない”

phpStormを使って開発をしていくというのは辛うじて理解できたものの
どうやれば開発できるのか?そもそもここに表示されている謎のコードは何なのか?
からスタートしたため全てが手探りでした

とりあえず文言を変えればよいと考えていたので、なんとなく検索を使って探し始めましたが
これが更なる混乱を招きました

その弐

”周りへの聞き方が分からない”

処理がどのように呼び出されて、どのように表示されているのか分からないまま
課題の目的である、変更対象の文言を探し始めてしまいました

すると、周りの方に質問をしてみようと思っても
過程がグチャグチャなので必然的に質問もうまくできません
これにより見事に泥沼化しました

その参

”そもそもなんで動いているのかが分からない”

与えられた課題は文言変更でしたが、
実際に動いているWEBアプリケーションの文言変更なので
対象の文字列は必ずどこかにあるはず

探し方が悪いというところまで半端に掴んでいた為
どのようにその画面や文言が表示されているか考えるのを二の次
にしてしまっていました

ここらへんで文言を探すことに躍起になっているので
開発において重要なそもそもなぜ動くのかという疑問が
完全に蚊帳の外になります

その肆

”調べ方が絶望的に分からない”

大分時間が経って初めてGoogleなどで調べようとしますが
そもそもどういう検索キーワードで調べればよいかが謎でした
出てきたエラー文などをそのまま検索してみても
アプリケーション独自のエラーであることが大半だったので
Google先生は無力でした
(自分の調べ方が悪かっただけですが)

その伍

"飛び交っている言語が分からない"

解決策を求め分かる人に話を聞きに行きます。
そしてそこで四苦八苦しながら現状の課題を伝えます。
そして帰ってくる言葉を聞いて
????????
となります

前提知識の差はありますがここまでで正規ルートから外れた調査の仕方をしていた為に
得られたアドバイスが全く分かりませんでした

オワタ(´・ω・`)

どうすれば良かったか?

個人的な見解ですが、周りへの聞き方を改善することで
このカオスな初動はもっと違う姿を見せてくれていたと思います
(後から)当たり前(と分かった事)ですが
プログラムはルールに従って順番に動くわけです

この順番をすっ飛ばして答えを探そうとしたから滅茶苦茶になりました
この後に作成するようになったメモを持って聞くことで大分聞きやすくなったので
形はどうあれこのようにまとめるといいのではないでしょうか

あくまで参考程度になのでもっと良い纏め方があれば教えて頂きたいです!
(例)
➀一番最初に呼び出されるファイル:hoge/test.html
➁最初に通る処理:hoge function
➂その次呼び出されるファイル:hoge/test2.html
:
:

これをまとめようとすると自分が処理のどこを理解できていないかが浮き彫りになり、
周りに聞いたタイミングで自分の思考過程を追って貰いやすくなったので
非常に質問が楽になりました

結論

  • 最初は一つ一つ処理を丁寧に追うことを考えないと詰む
  • 最初にどこが呼び出されるか、なぜそれが呼び出されるかが分からない時は 過程を飛ばさないでそこから聞く、調べる方が早い
  • 質問前に自分の思考過程を書くと整理しやすい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

p5jsでPokémon GOっぽいUIを作る(1)

位置情報と連携するサイトを作っているが、ふとPokémon GOっぽくしたいと思ったもののUnityを覚えるのは大変。というわけでp5jsでどこまで近づけられるかを試してみる1回目。多分、10回ぐらいすれば色々と機能が揃うかも。

まず地図を作る

Pokémon GOはGoogleだと思いますが、ここはYahoo!JapanのYOLPのスタティックマップを使ってみます。

https://developer.yahoo.co.jp/webapi/map/openlocalplatform/v1/static.html

アプリケーションIDを取得して、こんな感じで施設名とか地名が入ってない地図を作ってみる。

<?php

$appid = "YOUR_APP_ID";
$lat = @$_GET["lat"] ?: "35.66521320007564";
$lon = @$_GET["lon"] ?: "139.7300114513391";
$query = http_build_query([
    'appid' => $appid,
    'lat' => $lat,
    'lon' => $lon,
    'z' => '16',
    'style' => 'off:address,landmark,line_name,station_name,symbol,area_name'
]);

$url = 'https://map.yahooapis.jp/map/V1/static?' . $query;
$temp = file_get_contents($url);

$fw = fopen("map.png","w");
fwrite($fw,$temp);
fclose($fw);
?>

p5jsで地図を平面に表示する

次にp5jsをダウンロードし、取得した地図を貼ってみる。

https://p5js.org/

<script src="p5.min.js"></script>
<script>

let img;
function preload() {
  img = loadImage('map.png');
}

function setup() {
  createCanvas(600, 600, WEBGL);
}

function draw() {
    background(0);

    rotateX(1);
    rotateZ(frameCount * 0.001);

    push();
    texture(img);
    plane(1000, 1000);
    pop();
}

</script>

こんな感じになりました。

スクリーンショット 2019-12-09 22.07.49.png

次回は中心に棒人間でも描いてみるか、それとも地図を回転させてみるか。気まぐれに書き足してみます。

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

CMS

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

マイナーなCMS

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

Composerと、Homebrewのまとめ

~はじめに~

僕は、つい最近エンジニアとしてqnoteに就職した21歳のオトコです。
まだまだ技術として自慢出来るものや、人間として誇れるものも何もありませんがこれから少しずつでも前に進んで行こうと思っています。
元々、自動車販売の営業や自販機の補充をする仕事などをやっていて、そこでは自分の好きな事をやれている実感がなかったので転職しました。(ほんとはもっといろんな理由がありますが、、、笑)
最初はRubyとrailsを学習していて、PHPやLaravelは触った事もありませんでした。
(ゆうてもRubyも使いこなしていた訳ではありませんが。笑)
そこで、Laravelの開発環境を整えている最中、『何やねんこれ』って思った"Composer"と
"Homebrew"について初心者なりに調べて自分なりにまとめてみました。出来るだけ分かりやすい説明を心掛けます
(間違っていたら指摘してくださいっっっm(_ _)m)

そもそもComposerとはなんぞや??

Laravel開発環境とかで調べてると、「とりあえずComposerインストールしてな( ^∀^)」くらいの感じで言われる事が多く、Composerが何なのかという説明がない事が多かったです。
「じゃあ調べよう!!!!!!!!!!!!!!!!」
と思って調べました?
一言で言うとComposerは、「PHPのパッケージ依存性管理システムである。」
「ん☺️???何それ☺️」って初心者は思う訳ですよ。

じゃあ噛み砕いていきましょう。パッケージの依存管理とは何か??

まず最初にパッケージの概念について。
例えですが、一つ一つの部品(モジュール)を集めて箱に入れるとその箱の名前は(パッケージ)になります。
要するに、様々な部品が入った塊がいろんな機能を持った的な感じかと僕は思ってます。
※パッケージをいくつか集めてインストール出来る様にすると『ライブラリ』になります。

じゃあ依存とは??
こんな男性も少なくないと思いますが、彼女に「お前がいないと俺もう無理だ。生きていけないよ」っていうのが人間の依存だと思うのですが、それと一緒で、パッケージをインストールしても、他のパッケージ(彼女)がないと動けないパッケージがあります。

~だからなんやねん~

って思ったそこのあなた。ここでComposerの出番です。

例えば、欲しいパッケージがあるとします。
「このパッケージほしいなああ〜〜。でも、このパッケージをインストールしたらあのパッケージもインストールしないとだし。あれもこれも。。。。(泣)」
あ。おワタ。。。依存地獄や。。。
でも、Composerを使うとコマンド一発で必要なパッケージを全てインストールできちゃうんです。
※packagistという所からパッケージを取得します。packagistはインターネット上に公開されているメインリポジトリです。

Composerのいい所

  • リポジトリを汚さなくて済む。(Composerでインストールしたパッケージは「Vender」というディレクトリに一括で保存される。)
  • ライブラリの依存性はcomposer.jsonに書き出される為、分かりやすい。自分で作成するか、対話形式で作成することも出来る。
  • 一回のコマンドで依存するパッケージを全て持ってこれる。
  • composer.lockのおかげでチームに共有しやすい。

共有するには

同じチームのメンバーが、Composerで管理されたライブラリを落とすにはcomposer.jsonを共有すればいい。
composer.jsonがあるディレクトリ上で$ composer installを実行するだけで依存しているライブラリをインストールする事ができる。

ここでのcomposerの挙動
1 各ライブラリを見に行く
2 見に行ったライブラリに依存性があるか確認。
3 1,2を繰り返し確認を行っていく。

そこで思うのは、毎回これを共有した皆んながやると思うと時間かかってしょうがなくね??

でも大丈夫!「composer.lock」

composer.lockというファイルが生成されている。
これは、composer.jsonの中にあるライブラリを取得するのに実際にどのファイルを落としたのかをひとまとめにしたもの。
つまり、composer.lockがあるからいちいち依存性を確認しに行かなくてもいい。初めてライブラリをインストールした人と同じファイル、バージョンのものをインストールできる。

composerでよく使用するコマンド

ここではよく使うComposerのコマンドを紹介していきます。

  • $composer init
    composerを使ったプロジェクトを自作するときに使う。ちゃちゃっとライブラリを書きたいときなど。対話式でいくつか設定を加えることもできるが、全部スキップしても問題はない。

  • $composer createーproject
    git cloneして、composer installするのと全く同じ結果になる。

  • $composer require <package> [:tag]
    composerを使ってパッケージや、ライブラリをインストールする。
    ライブラリの情報は「composer.json」に記載され、実際に何をインストールしたかの情報が「composer.lock」に記載される。オプションとして、「--dev」をつけると開発環境専用のパッケージとしてインストールが出来、composer.jsonの中の「require-dev」の項目に追加される。

  • $composer install
    composer.jsonに記載されれている内容、もしくはcomposer.lockに従ってパッケージをインストールする。パッケージが更新されたら再度このコマンドを実行する。
    このコマンドを打つことにより、チームメンバーはパッケージの依存性を解決することができる。

  • $composer update
    composer.lockを無視してパッケージをcomposer.jsonを元にインストールする。
    現在の依存性を更新して全部最新化したいときなどに使う。

  • $composer remove <package>
    requierの逆バージョン。
    必要のないパッケージを取り除く。

Homebrewとは?

Homebrewとは、macOS用パッケージマネージャーのことです。

パッケージマネージャーって何??

ターミナルからコマンドを実行することにより、簡単にパッケージをイントールしたりアンインストールできるツール。

Homebrewの使い方

Homebrewでは、「brew」コマンドを使ってパッケージの管理を行う。
代表的なbrewコマンド↓

  • $brew install パッケージ名
    パッケージのインストールを行う。「brew」コマンドで一番よく使うコマンドになる。

  • $brew uninstall パッケージ名
    パッケージのアンインストールを行う。

  • $brew update
    Homebrew本体のアップデートを行う。
    パッケージの最新バージョンが使えるようになる、使えるパッケージが増えたりする。

  • $brew upgrade
    パッケージの更新。インストールしてあるパッケージは全て更新される。

  • $brew upgrade パッケージ名
    インストールしている特定のパッケージを更新する。

  • $brew list
    インストール済みのパッケージ一覧表示。

  • $brew search パッケージ名
    パッケージの検索。

Homebrew豆知識

Homebrewの意味を皆さんご存知でしょうか??
直訳すると、「自家醸造」になります。
ロゴから見てもわかりますが、ビールの自家醸造というなんとも酒好きにはたまらないネーミングセンスです。
僕は、ビールよりウィスキーの方が好きですが酒好きには変わりありません( ´∀`)homebrew.png
このHomebrewでは「自家醸造」にまつわる事が使う上でのヒントとなるようです。
参考記事
https://qiita.com/omega999/items/6f65217b81ad3fffe7e6

「brew」
これは「醸造する」という意味になりますが、「makeする、作る」という意味になる。

「homebrew」
これは「自家醸造する」という意味ですがこれは「userが自分で作り、使う」という意味になる。

「celler」
ワインセラーとかの'セラー'と一緒で「保管しておく、保存先」という意味になる。

「keg」
樽や醸成用、意味としては「作るための材料」になる。

「formula」
直訳すると、「式」になるがそれまでの過程、方法、手順という意味になる。

例えがとてもわかりやすかったので引用させていただきます。。。

homebrewとは「ユーザが自らパッケージをビルドして使用する」ことのメタファーで「ビールを自家醸造して保存する・飲む」ことを意味しているのです。
手順(調理法formula)通りにパッケージをビルド(醸造)して保存(/usr/local/cellerに格納)して、使う(/usr/loca/binにリンク)ってことのようです。

まとめ

ComposerやHomebrewの動きが全然わからなかった僕ですが、一つ一つの役割や特徴が分かると頭に入って来やすかったです。
まだまだ勉強不足で知らないことばっかりですが、分からない事が分かる様になる楽しさがたまらなく好きなので、
これからも勉強して、少しでも会社に貢献できればなと思っております。
これを機に、勉強したことを少しずつ記事にして投稿していければなと思いますのでよろしくお願いします。
(正しいことを覚えたいので、間違っている事があればガンガン指摘していただいて構いませんので、、、、)

最後に

qnoteの皆さん。まだまだこんなへなちょこりんで迷惑ばっかりかけていますが、皆さんに少しでも追いつけるように頑張りますのでこれからもよろしくお願いします!!!

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

おれがコンピュータに足し算を教えてやろうと思ったらもう知ってた

echo gmp_add(
    '2345760293460295683704558703456102394871348756109347501936410934756102913874187344541930193470934875',
    '7947618927365019364019257691823461928734634570193248703473987562093471093576293478628949398873429857'
); // 10293379220825315047723816395279564323605983326302596205410398496849574007450480823170879592344364732

はい。

まあ、これだけだとアレなので、GMPを使わずaddBigIntをもう少し簡単に書けないか検討してみることにします。

function addBigInt(string $a, string $b): string{
    $a = strrev($a);
    $b = strrev($b);

    $ret = '';
    for ($i = $c = 0;isset($a[$i]) || isset($b[$i]); $i++) {
        $tmp = ($a[$i] ?? 0) + ($b[$i] ?? 0) + $c;
        $c = $tmp > 9;
        $ret[$i] = $tmp % 10;
    }

    if ($c) {
        $ret[$i] = 1;
    }
    return strrev($ret);
}

とりあえず何も考えずに書いたらこんなかんじになりました。
単に1桁毎に順番に計算しているだけですね。

strrevが3回も入ってたりisset判定が残念な感じだったりif($c)とかわざわざ書いてたり微妙なのでどうにかしたいところです。

function addBigInt(string $a, string $b): string
{
    $i = max(strlen($a), strlen($b));
    $a = str_pad($a, $i, 0, STR_PAD_LEFT);
    $b = str_pad($b, $i, 0, STR_PAD_LEFT);
    $ret = '';

    for ($c = 0, $i--; $i >= 0; $i--) {
        $tmp = ($a[$i] ?? 0) + ($b[$i] ?? 0) + $c;
        $c = $tmp > 9;
        $ret[$i] = $tmp % 10;
    }

    if ($c) {
        return '1' . $ret;
    }
    return $ret;
}

strrevを消して逆順に計算することとし、また桁数を揃えることで毎回issetで条件判定せずに済むようにしました。
しかしstr_padのもっさり感と、if($c)がやっぱり消せないのが惜しいところです。

最終式

function addBigInt(string $a, string $b): string
{
    for($a=str_pad($a,$i=max(strlen($a),strlen($b),$c=0),0,0),$b=$r=str_pad($b,$i--,0,0);$i>=0;$c=($t=$a[$i]+$b[$i]+$c)>9,$r[$i--]=$t%10);return$c?'1'.$r:$r;
}

単に詰めただけです。
エラーを出さない条件では、このあたりが限界でした。

ちなみにエラーを許容するなら、strrevを使ったほうが短くなります。

エラー出る
function addBigInt(string $a, string $b): string
{
    for($f=strrev,$l=max(strlen($a=$f($a)),strlen($b=$r=$f($b)));$i<$l;$c=($t=$a[$i]+$b[$i]+$c)>9,$r[$i++]=$t%10);return($c?1:'').$f($r);
}

最終式の問題点

max(strlen($a))と入力値を元にした数値を使っているので、PHP_INT_MAX桁を超える文字列だと正常に動かなくなります。
その前にメモリとかが死にそうですが。

あと0未満の値は正しく計算できません。
これは元の関数からしてそうなので仕様としておきます。

感想

残った2カ所の,を消せないものか。
あれがあると結局単に複数の式を並べてるだけになるから、なんというかこう面白くないんだよね。

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

LINEMessagingApiを使って簡単なBOTを作成する

SDKのインストール

https://github.com/line/line-bot-sdk-php
composer require linecorp/line-bot-sdk

WebhookをLINEDevelopersの管理画面から登録する

スクリーンショット 2019-12-09 18.53.46.png

ページ真ん中にある「Webhook settings」にwebhookのURLを設定(httpsじゃないとダメです)して、「Use webhook」のトグルをONにします。
スクリーンショット 2019-12-09 18.55.44.png

メッセージを受け取ったときに、送信されたメッセージを返すBOTを作る

$signature = $request->headers->get(HTTPHeader::LINE_SIGNATURE);

$httpClient = new CurlHTTPClient('管理画面から取得できる Channel access token');
$bot = new LINEBot($httpClient, ['channelSecret' => '管理画面から取得できる Channel secret']);
$events = $bot->parseEventRequest($request->getContent(), $signature);
foreach($events as $event) {
    if($event instanceof MessageEvent) {
        $bot->replyText($event->getReplyToken(), '「' . $event->getText() . '」と発言しましたね。');
    }
}

このBOTに対して何か発言するとこんな感じになります。
ZKToOKlTueo67AVSXTYL1575886195-1575886200.gif

ちょっと改造する

先ほどのコードを少し改造して、確認ダイアログ的なものを出してみたいと思います。
先ほどは、replyTextメソッドを呼んでただのテキストメッセージでしたが、replyMessageメソッドにはMessageBuilderのインスタンスを渡せるのでちょっとリッチなメッセージを送れたりします。
https://github.com/line/line-bot-sdk-php/blob/4e16fb07379a9cab76b9136ff3057fbf40ef8360/src/LINEBot.php#L123-L129

if ($event instanceof MessageEvent) {
    $confirmBuilder = new ConfirmTemplateBuilder('「' . $event->getText() . '」と発言しましたね。', [
        new MessageTemplateActionBuilder('はい', 'はい'),
        new MessageTemplateActionBuilder('いいえ', 'いいえ'),
    ]);
    $builder = new TemplateMessageBuilder('test', $confirmBuilder);
    $bot->replyMessage($event->getReplyToken(), $builder);
}

PDf9C8oRuOmz7NadEVbS1575888539-1575888551.gif

終わりに

https://developers.line.biz/ja/docs/messaging-api/
MessagingAPIでできることはまだまだたくさんあるので、相手の発言によってアクションを変える等すればもっとBOTっぽくなりますね。
相手からのメッセージを正規表現で判定等しないといけなさそうではありますが。。。

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

Gmail(SMTP)経由でメール送信する

記事の主旨

GmailのSMTP利用する際には、

  • Googleアカウントのセキュリティ設定を忘れず、済ましておいてくださいね
  • 「安全性の低いアプリの許可」に逃げず、「二段階認証/アプリパスワード」を使いましょう

というだけの記事です。

SMTP Error: Could not authenticate.
エラーにつまった人向けの備忘録です。

メール送信のサンプルコード

https://github.com/kasssy/php_sendmail

Googleアカウント作成〜メール送信に成功するまで

Googleセキュリティ設定を意識せずに、作業をした場合に起こること・対処方法です。

Googleアカウントを作成する。

  • ID : サンプルコード内のfrom@gmail.com
  • PW : アカウントのマスタパスワード
(point)作成段階のデフォルト設定では、セキュリティ上、SMTPエラー(アクセス不可)になってます。

サンプルコードで、テスト送信してみる。が、あえなくエラー発生。

以下を実行しても

## ID / PW を作成したアカウント情報に書き換えて、以下を実行
$ php test_sendmail.php

エラーが発生する。

2019-12-09 08:30:35     SERVER -> CLIENT: 535-5.7.8 Username and Password not accepted. Learn more at
                                          535 5.7.8  https://support.google.com/mail/?p=BadCredentials o15sm24713268pgf.2 - gsmtp
2019-12-09 08:30:35     SMTP ERROR: Password command failed: 535-5.7.8 Username and Password not accepted. Learn more at
                                          535 5.7.8  https://support.google.com/mail/?p=BadCredentials o15sm24713268pgf.2 - gsmtp
2019-12-09 08:30:35     SMTP Error: Could not authenticate.
2019-12-09 08:30:35     CLIENT -> SERVER: QUIT
2019-12-09 08:30:35     SERVER -> CLIENT: 221 2.0.0 closing connection o15sm24713268pgf.2 - gsmtp
2019-12-09 08:30:35     SMTP Error: Could not authenticate.
PHP Fatal error:  Uncaught PHPMailer\PHPMailer\Exception: SMTP Error: Could not authenticate. in /home/ec2-user/tests/vendor/phpmailer/phpmailer/src/PHPMailer.php:2019

https://support.google.com/mail/?p=BadCredentialsを見て頓挫する。。人は続きを読んでください。

通信が届いていることを確認する。

問題なし。(まぁ、ログ上でも到達できているので、当たり前ですが一応。)
```
$ telnet smtp.gmail.com 587

Trying 74.125.23.108...
Connected to smtp.gmail.com.
Escape character is '^]'.
220 smtp.gmail.com ESMTP g191sm26005183pfb.19 - gsmtp
```

Googleアカウント側(Gmail)を確認する。

以下のようなメールが届く。
(メール届いてない方は、純粋に ID/PW間違いを疑ってください。)
¥スクリーンショット 2019-12-09 17.40.12.png

*非推奨*Google側のアクセス許可を"有効"にする。

https://support.google.com/mail/?p=BadCredentials
に習って、

スクリーンショット 2019-12-09 17.44.21.png

を"有効"にして、再度テスト送信する。
→無事にメールが受信できた。けど!!この方法は、非推奨なので、次の項までやってくださいね。

Google側の二段階認証を有効にし、アプリパスワードを発行する。

注意) 安全性の低いアプリの許可を"無効"に戻すことは忘れないように。

https://blog.saboh.net/smtpgmailcom/
上記サイトが大変分かりやすいので、参考にしてください。

## PWを発行した「アプリパスワード」に書き換えて、以下を実行
$ php test_sendmail.php

→無事にメールが受信できた。お疲れ様でした。

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

Laravelでドメイン駆動設計を実践し、Eloquent Model依存の設計から脱却する

この記事はドメイン駆動設計#1 Advent Calendar 2019の10日目の記事です。

やったこと

自社サイトのバックエンドをLaravelで実装して半年間が経ち、初期に考えた設計にいろいろと綻びが出てきたと感じていました。
そんな中、ちょうど実践ドメイン駆動設計やWeb+DB Pressで特集された体験DDDを読むことができたので、さっそくいくつかの機能をDDDで実装してみました。
本記事では「もともとLaravelで実践していたEloquent Model依存の設計」の問題点を提起し、「DDDを取り入れて実装した結果」のソースコードや考え方、そのメリットを記載しています。

結論

  • LaravelのModelをあらゆるレイヤーで使うと改修が難しくなる
  • 開発する機能のユースケースを主語と述語で文章に表現し、そのままUseCase層の実装として表現する
  • EntityやValueObjectに制約条件をまとめ、適切に例外を吐く
  • LaravelのModelはORMとしてのみ利用する
  • PHPの言語自体の限界はあるので、命名の工夫などで適宜我慢する
  • 実運用の際はどの機能から、どこまで完璧主義でDDDをやるか考える

初期の設計

まずは「もともとLaravelで実践していたEloquent Model依存の設計」についてお話します。

簡単に自己紹介をさせていただきますと、今年の3月まではLIFULLという会社でWebエンジニアをしていたのですが、転職して、以降はNoSchoolという教育ベンチャーでCTOをやっています。
学生が質問し家庭教師が回答する勉強Q&Aサイトを運営しており、開発する上で意識すべきデータは「質問者・回答者」「質問」「回答」などです。

Laravelの設計は、おおまかに下記の方針で行いました。

  • テーブルごとにLaravelのModelを作成する(User、Question、Answer・・・)
  • それぞれのModelを使ってデータをCRUDするRepositoryを作成する(UserRepository、QuestionRepository、AnswerRepository・・・)
  • Repositoryからデータを取ってきたあと、サービスの仕様に合わせて整形する目的で、Serviceを作成する(UserService、QuestionService、AnswerService・・・)
  • 同様にControllerを作成する(UserController、QuestionController、AnswerController・・・)

このように、あくまでテーブル構成を思い浮かべ、テーブル構成に対応したModelを作成し、以降Repository、Service、Controllerと、レイヤー化しているように見えるけど、実際はただ単にデータをリレーしているだけのアーキテクチャを組んでいました。

Modelの扱い

例えば「質問を1件取得する」場合、下記の手順で実装します。

① QuestionRepositoryに返り値としてQuestionModelを返す質問取得メソッドを実装

public function findQuestion(int $id): Question
{
    return Question::findOrFail($id);
}

② 次にQuestionServiceにfindQuestion(int $id)をQuestionRepositoryのfindQuestionを読んで返すだけの内容で実装します。

public function findQuestion(int $id): Question
{
    return $this->repository->findQuestion($id);
}

③ 最後にQuestionControllerを実装します。

public function findQuestion(int $id)
{
    return view('question.find', [
        'question' => $this->service->findQuestion($id)
    ]);
}

④ bladeファイルの中では$questionを起点にデータをリレーションで取得して表示します。

    <h1 class="title-question">
      {{ $question->title }}
    </h1>
    <span class="author-name">{{ $question->user->name }}</span>
    <span>さんの質問</span>
    @foreach ($answers as $question->answer)
    <section class="answer-item">
        ... 以降、質問の各回答が並ぶ ...

このように、【Eloquent ModelをViewまで返す】方法で実装しました。

ModelをViewまで取り回すメリット

何にせよ開発が非常に速いです。

{{ $question->user->name }}というのは、Question.phpに

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class)->withDefault([
            // ...
        ]);
    }

のようにリレーションが設定されていれば、View層でUserがQuestionのプロパティかのように繋げてアクセスできるということを示しています。

Userにリレーションが設定されていれば、さらにそこから繋げてデータにアクセスできます。

これはつまり、【View層で新しいデータが欲しいときは、Modelだけ変更すればRepository、Service、Controllerの変更が不要である】ことを示します。
もちろんRepositoryやServiceの変更を伴う改修も多いですが、新しいデータをViewに出すという要件に対しては、何も考えなければ、Questionモデルから順番にリレーションをたどってほとんどのデータにアクセスできるため改修スピードを速めることが可能です。

例えばあるとき、「質問一覧画面には質問本文の最初の20文字だけ表示して欲しい」という要件があったとしましょう。下記のようにbladeファイルに直接PHPのプログラムを書くのはちょっと憚られますよね(※str_limit_jaは全角文字ベースで文字列をトリミングする関数とします)。

{{ str_limit_ja($question->body, 20) }}

このとき、RepositoryやServiceを変更しなくても、Question.phpにAccessorを増やせば実装が終わります。

Question.php
    public function getShortBodyAttribute($value): string
    {
        return str_limit_ja($this->body, 20);
    }

このようにAccessorを書けば、Viewでは

{{ $question->short_body }}

で20文字にトリミングされた質問文が表示できます。

この魅力にハマった我々は、次から次へと改修案件をこの方法で捌いていきました。
我々も「Viewにロジックを載せるのは悪手」ということくらいは知っていたので、Viewにロジックを避けつつQuestionに関する処理をまとめて書けるAccessorは優秀だ!と考えました。
弊社はスタートアップということも有り、次から次へと副業のエンジニアさんも入ってきました。入ってきて既存実装を見て、「なるほどModelにAccessorを生やすのか」と真似していきました。こうしてEloquent Modelはあらゆるページに、様々なリレーションとAccessorを伴って広まっていったのです。

後々やってくる苦難の道など窺い知る余地もなく・・・

発生しうる問題

このようにテーブルを起点にクラス設計を固めてしまったある日、こんな要件が発生したとします。

【質問一覧では20文字まで表示だけど、ユーザーのプロフィールで見れるユーザーの質問一覧には質問本文を40文字まで表示してね】

いつもどおりAccessorを増やそうとすると、すでにこんな実装が・・・

Question.php
    public function getShortBodyAttribute($value): string
    {
        return str_limit_ja($this->body, 20);
    }

こうなると、諦めてbladeにstr_limit_jaを実装するか、medium_bodyのようなヤバいネーミングのAccessorを増やすか・・・みたいな選択肢になってきますね。

逆もまた然りです。

【こないだの質問一覧の20文字さ、ちょっと短すぎるから一覧→詳細の遷移率上げるために30文字に増やしてよ】

よしきた、と変更したところ

Question.php
    public function getShortBodyAttribute($value): string
    {
+        return str_limit_ja($this->body, 30);
-        return str_limit_ja($this->body, 20);
    }

なんと、全く関係なかったはずのユーザーのプロフィールで見れる質問一覧の文字数も30文字に増えてしまいました

いつの間にか誰かがprofile.blade.phpでも$question->short_bodyを使っていたようです。

※以上の内容はフィクションですが、実際に似たような事案が何度も発生しました。

なぜこのような問題が起こったのか?

一言で言えば、

【質問データがユーザーからどう見えるかは、ユースケースによって異なるはずなのに、全てのケースにおいて同じClassのオブジェクトを返して実現していたから】

だと考えています。

  • 質問個別ページで見る質問
  • 質問一覧ページで見る質問
  • プロフィールページで見る質問
  • 質問投稿者自身が質問個別ページで見る質問
  • ログアウトユーザーが見る質問、ログインユーザーが見る質問
  • 質問を投稿するときに入力する質問

といった、サービス内でも様々なユースケースに応じて姿を変える「質問」を、そもそも全て単一の「Question.php」のインスタンスで実現しようとしたことに無理があったはずです。

これはUserでも近いことが発生してしまっていました。NoSchoolの勉強Q&Aサイトでは、現役の家庭教師や塾講師が学生ユーザーの質問に回答します。それぞれのユーザーはメールアドレスやパスワードなどの認証情報を同じusersテーブルに所有していたことからUser.phpでModelを作成し、取り回していました。

これにより、たとえば生徒しか使えない機能を実現する場合でも、理論上全てのタイプのユーザーが入りうる$userがbladeにやってくるから余分にif文を書かないといけない。といった問題が起こっていました。

補足

LaravelをAPIサーバーメインとして活用する場合、APIリソース(https://readouble.com/laravel/6.x/ja/eloquent-resources.html) という機能を活用すれば、APIから返る値を仕様に応じたパラメータに整形する役目を担うレイヤーを作成できます。

NoSchoolでもNuxtでSPAとして開発を始めたときや、iOSアプリを開発したときにAPIを組みましたが、そのときはAPIリソースを使うことで、リレーションが盛り込まれたModelを最小限にしてクライアントに返すように実装できました。対処療法ではありますが、APIリソースによってModelの取り回しによる問題のいくつかは解消できますので、個人的にはLaravelの機能で一番好きな機能です。

補足2

一方、ルートモデルバインディング(https://readouble.com/laravel/6.x/ja/routing.html) はLaravelの中でも指折りのヤバい機能だと思っています。これはControllerで直接Action Methodの引数にModelインスタンスを受け取れるというものです。

ControllerがModelをいきなり扱えるためレイヤードアーキテクチャの根本から覆してしまいますし、直接Modelを受け取るために、いわゆるN+1問題を解消するためのeager loadを行うためのwithメソッドを噛ますことができません。

しかし、これも開発時間短縮を実現できるため一時期弊社内で流行し、かなり多用された結果、Question.phpモデル自身にwithプロパティが設定され、質問に関連する全クエリに強制的にwithが走るという状況が発生してしまいました。ヤバいです。

設計の問題点を受けての考察

ここまで考えて、当時の僕が考えたのは下記のようなことでした。

  • Repository→Serviceの時点で、LaravelのEloquent Modelを返さず、用途に応じた独自のオブジェクトを返すべきなのではないか?
  • そのオブジェクトはシンプルなPHPのClass(いわゆるPOPO)として実装し、プロパティへの型補完が効くように実装すれば透明性も担保できる
  • Repositoryの役目はEloquent Model→POPOへの変換に特化させる
  • Service〜Viewが扱うオブジェクトは独自オブジェクトだけになるから、影響範囲を絞り込める

このとき僕が考えた【Repositoryが返してくる独自のオブジェクト】が、DDDの文脈で言うところの【Entity】に落ち着いてくると思うのですが、当時の僕はそこまで考えられていませんでした。

ただ、具体的な実装方法が固まりきらなかったためこの考察は考察しただけで終わり、またModelを流用しまくる日々に戻りました。

転機

そんな日々に転機が訪れ、DDD実践へ繋がっていった転機が2つ有りました。

転機① スマートフォンアプリ開発

1つ目はネイティブアプリの開発です。

Webと同等以上の機能を有するiOSアプリをリリースするにあたって、1人でアプリで利用するAPIを全て組みました。初めてスマホアプリ開発に関わって気がついたことは色々ありすぎるので別途記事にまとめられたらと思うのですが、DDD関連で気がついたことといえば、やはり影響範囲についてでした。

ネイティブアプリはWebと違って、リリースするとユーザーがアップデートしない限りこちらから関与することは原則できません(強制アップデートを除く)。

今までWebだけで考えていた影響範囲がアプリにも広がり、さらにアプリもバージョンごとに考えないといけないことを考えると、それら全てのデバイスから質問データにアクセスするときはQuestion.phpを通っているという事実が、恐ろしく思えてきました。

さきほどのshort_bodyのような独自Accessorをアプリ向けのAPIでも利用しようものなら、もう改修が怖くなって改善スピードが低下する未来が想像できました。

結果、Postmanを使ってAPIテストを組むことでお茶を濁したのですが、テストは出口対策なので、設計レベルで改善できることはないかな、と考える時間が増えました。

転機② Web+DB Pressの特集【体験DDD】

まあもう今回の記事はこの特集に関して本当にありがとうございました勉強になりましたって言いたいだけの記事と言っても過言ではないのですが笑

このころいわゆるEric本を買って読んだものの何一つわからず手元でEntityっぽものを組んで、いや違う、こんなの実戦投入できないと頭を悩ませていた中、この特集の話を聞いて速攻買いました。

実際にUseCase層、永続化層、Domain層の解説とともに(Javaではありますが)生のソースコードが添付されており、非常にイメージしやすい内容になっていました。やはりエンジニアはソースコードで会話するのが一番です。

LaravelでDDDを実践する

お待たせしました。実際に実務上の施策でDDDを取り入れた設計・実装をやってみた内容をまとめていきます。

DDDを実践したときの手順

  1. 実現する機能に登場する人物、扱われるデータをUMLに落とし込み、関係性やそれぞれの成立条件を可視化する
  2. 機能で実現するUXを「◯◯が■■する」といった主語と述語で表現される文章にまとめる
  3. UseCaseを実装。EntityとRepositoryはモックで、あくまで2. で作成した文章を実現するように実装する
  4. Entityの中にデータの初期化や変更を実装。
  5. UseCaseとのインターフェース用にValueObjectを実装
  6. 最後にRepositoryを作成するためにテーブル構成を考え、LaravelのModelを作成しORMとして永続化・検索処理を実装

1. UML作成

まずは実現する機能に登場する人物、扱われるデータをUMLに落とし込みます。

書くときはPlantUMLという、YAMLファイルでUMLを記載できるツールを使って書きました。

具体的には、VSCodeにPlantUMLのプラグインを入れてUMLを記述していきます。公式ドキュメントを読めば書き方はすぐにわかります。

UMLといっても色々あると思いますが、僕はクラス図を使って書いています(もちろん、この時点で、このクラス設計で実装しよう、というものを決定できるわけはないので、ここでクラスとして表現したものに設計が引っ張られないように注意します)。

iOS の画像 (3).png

雑にスクリーンショットを貼りましたが、このようにYAMLファイルを左に、UMLのプレビューが右に出た状態で編集でき、最後にPNGなどでエクスポートできます。

業界によってはUMLを書いてから実装なんて当たり前かもしれませんが、スタートアップで働いていて正直そういった仕様を明記しないことに甘えていたので、久々にUMLを書きました。

詳しい書き方は「体験DDD」に書いてありますが、個人的には、各クラスから吹き出しを生やして、制約条件を明記していくところがポイントです。そこで記述した制約条件をできる限り後述するEntity、またはValueObjectに徹底的に閉じ込めていくことが重要です。

2. 「◯◯が■■する」といった文章にまとめる

次に、実現したい機能についてユースケース図を書くか、または「◯◯が■■する」という文章を箇条書きでまとめます。

例えば質問投稿機能を作成するとしましょう。

質問投稿機能を作るとき、エンジニアであれば

【本文やタグといった質問データを受け取り、現在のログインユーザーID777とともにデータベースにInsertする】

と考えるでしょう。しかし、「◯◯が■■する」の形式で考えれば

【学生ユーザーが質問を投稿する】

と記載できます(※学生が質問する勉強質問サイトの場合)。

後述するUseCaseを実装するときに、個人的な考えですが、「◯◯が■■する」の文型で書くように実装することが重要だと思っています。

経験上、前述の【データベースにInsertする】という思考で実装すると、Service層(以下、UseCase層)のところまでデータベースの構成を意識した実装が漏れ出てきます。データベースのことをUseCase層より上位のレイヤーが忘れて実装できるように、あくまで現実世界に即した、極論を言えばエンジニア以外の人にも通じるような表現に、実装する機能を落とし込んで記述できることが大事です。

補足

ここで主語と述語で文章表現することを徹底しなかった僕の失敗談があります。
定額課金機能を実装したときに、決済ベンダーでPay.jpを利用させていただいているのですが、【Pay.jp上に課金履歴を保存する】と考えながら実装したため、アプリ経由の課金(In App Payment)やキャリア決済の実装等を考慮した段階で、本来抽象化されているはずのUseCase層が再利用しにくくなっていることに気が付きました。本当は【ユーザーが課金する】と考えながら実装することで、ユーザーEntityや課金履歴Entity、または独自のHelperなどにPay.jp独自のロジックを閉じ込める工夫ができたはずです。

3. UseCaseを実装

ここまで考えたあと、実装を始めます。最初の実装をEntityなどからはじめる方もいるかもしれませんが、僕はいまのところUseCaseから作り始めるのが好きです。

UseCase層は、ユーザーが自社のサービスを利用する場面を表現する層です。

さきほど2. で落とし込んだ「◯◯が■■する」という粒度の情報を持っている層になります。

では、さきほどから例示している【学生ユーザーが質問を投稿する】UseCaseを実装してみた例を示します。

QuestionPostUseCase.php
<?php

namespace App\QuestionPost\UseCase;

use App\QuestionPost\Domain\ValueObject\UserAccountId;
use App\QuestionPost\Domain\ValueObject\QuestionBody;
use App\QuestionPost\Domain\ValueObject\QuestionTags;

use App\QuestionPost\Domain\Repository\QuestionerRepositoryInterface;
use App\QuestionPost\Domain\Repository\QuestionRepositoryInterface;

use App\QuestionPost\Domain\Entity\QuestionEntity;
use App\QuestionPost\Domain\Entity\QuestioningUserEntity;
use App\QuestionPost\Domain\Exception\QuestionPostFailedException;

final class QuestionPostUseCase
{
    private $questionerRepository;
    private $questionRepository;

    // ※ここはLaravelのAppServiceProviderでRepositoryの実体をDIします
    public function __construct(
        QuestionerRepositoryInterface $questionerRepository,
        QuestionRepositoryInterface $questionRepository
    ) {
        $this->questionerRepository = $questionerRepository;
        $this->questionRepository = $questionRepository;
    }

    public function execute(
        // ポイント1 UseCaseの引数はValueObjectがGOOD
        UserAccountId $userId,
        QuestionBody $body,
        QuestionTags $tags
    ): QuestionEntity {
        // ポイント2 Repositoryから質問者Entityを取得
        // @var QuestioningUserEntity
        $questioner = $this->questionerRepository->getQuestioner($userId);

        // ポイント3 質問者Entityが質問を投稿
        // $question は QuestionEntityのインスタンス
        $question = $questioner->postQuestion($body, $tags);

        // ポイント4 永続化
        return $this->questionRepository->saveQuestion($question);
    }
}

ポイント1 UseCaseの引数はValueObjectがGOOD

UseCaseはおおむねLaravelでいうとControllerから呼ばれることが多いですが、その際の引数はValueObject(後述します)がおすすめです。

一番いけないと思うのはArrayを渡すパターンです。$request->all()でリクエストの内容を取得し連想配列でレイヤーを超えてデータを渡していくことが僕は多かったのですが、内容が不透明になって、結局あとからRepositoryなどでisset feat. 三項演算子地獄を生むことになります。
例えばintと型を定義すると、どんな数値であっても入ってくることができますが、UserAccountIdといった独自の型を定義すれば、より安全に、ヒューマンエラーを防いで扱うことができます。

ポイント2 Repositoryから質問者Entityを取得

実現したい機能は【学生ユーザーが質問を投稿する】なので、まずは主語となる「学生ユーザー」を用意します。

「学生ユーザー」はすでに登録済みのユーザーなので、データベースに永続化されています。そのためRepository経由でQuestioningUserEntity(1人の質問するユーザーを示すEntity)を取得します。

「学生ユーザー」なのでStudentEntityといった名称でも良いかもしれませんが、僕の見解としては、今後仕様変更で学生以外の種別のユーザーが質問可能になる可能性、などの幅を残すために「質問者Entity」くらい抽象化した命名でもいいのではと考えています。

ポイント3 質問者Entityが質問を投稿

※Entityのソースコードは後述します

次にQuestioningUserEntityに実装されている(この時点ではモックですが)postQuestionメソッドを実行することで質問を投稿します。

引数には質問作成に必要なValueObject(後述します)を受け取り、投稿に成功するとQuestionEntity(質問内容を示すEntity)を返します。

ポイントは、QuestionEntity型のインスタンスは、このQuestioningUserEntityに実装されたpostQuestion経由ではないと生成できないように実装することです。するとソースコード上で「質問は必ずQuestioningUserEntityに該当するユーザーが投稿する」ということを暗黙のうちに示すことができます。

このようにEntityの生成ルートを縛ることで、今回の例だと、「user_idがNULLの質問データをテーブルにInsertしてしまう」といった事故を防ぐことができますし、
Q&Aサイトと一口にいっても掲示板のように匿名ユーザーでも書き込めるサイトもある中で、このサイトは必ずユーザーアカウントが存在する場合のみ質問できるのだ、ということをソースを読む人に伝えられます。

ポイント4 永続化

作成した質問EntityはRepositoryによって永続化(=データベースへの保存)します。

Entityの作成と、永続化はわけるほうがお互いの責務が分離されて望ましいと思います。


以上でUseCaseの解説を終わります。EntityやValueObjectの説明をしないで話を進めるのが辛くなってきたので先に進みますね。

4. Entityの実装

質問投稿機能の例では現在「質問者Entity(QuestioningUserEntity)」と「質問内容Entity(QuestionEntity)」が登場しています。

QuestioningUserEntityだけ、実装をざっくり例で示します。

QuestioningUserEntity.php
final class QuestioningUserEntity
{
    private $userAccountId;
    private $userType;

    // ポイント1 最重要!コンストラクタをプライベートにする
    private function __construct() {}

    // ポイント2 Repositoryが現在のデータを入れる静的メソッドを作る
    public static function reconstructFromDatabase(
        UserAccountId $userAccountId,
        UserType $userType
    ): QuestioningUserEntity {
        // プライベートコンストラクタはクラス内からは呼べます。new self()等でも可
        $questioningUser = new QuestioningUserEntity();
        $questioningUser->userAccountId = $userAccountId;
        $questioningUser->userType = $userType;
        return $questioningUser; // 返すのはインスタンス
    }

    // ポイント3 質問投稿メソッド
    public function postQuestion(
        QuestionBody $body,
        QuestionTags $tags
    ): QuestionEntity {
        // ここで質問を作成できないユーザーの場合は例外をThrow
        if ($this->userType !== UserType::STUDENT) {
            throw QuestionPostFailedException::withMessages(
                [
                    'message' => '質問を作成する権限がありません'
                ]
            );
        }

        // QuestionEntityがインスタンス化されるルートがここだけになる→学生以外のユーザーは決して質問を作成できない
        $question = QuestionEntity::createByQuestioningUser(
            QuestionBody $body,
            QuestionTags $tags
        );
        return $question;
    }
}

ポイント1 最重要!コンストラクタをプライベートにする

まずはコンストラクタを明示的にプライベートにすることが大切です。インスタンスが作られる方法を特定のメソッドのみに絞ることで、絶対に不整合なデータや思わぬデータをデータベースから取得したり、保存できなくなります

ポイント2 静的メソッドでインスタンスを作成させる

プライベートコンストラクタになったら、どうやって外のクラスがインスタンスを作成するかと言うと、PublicかつStaticなメソッドでインスタンスを作って返すようなものを作ります。

今回の例だとシンプルですが、プロパティの多いデータの場合はNULLableなデータはここでクラスメンバ変数にNULLを代入するなどします。

そうすることで、ソースコード上で、「このメンバ変数はNULLableですよ」「このメンバ変数は必ず(ValueObject)型の値が入りますよ」ということが表現できます。

僕は今まで何度も、「この連想配列のこのキーには何が入っているんだ?」と関数の引数を見て困惑することがありましたが、こうやって生成元を縛ったクラスにしておけば、読み解くのが容易になります。

QuestioningUserEntityの場合は生成元がRepository、すなわちデータベースからのデータ読み取り時のみなので、reconstructFromDatabaseという命名にしています。僕の認識が正しければPHPではこのメソッドはRepositoryからしか呼べないといった制限を自然にはかけれないはずなので、仕方なく命名で担保しています。

ポイント3 他のEntityを生成するメソッドで制約条件を明記

(例外設計についてはまだ自分の中で正解が固まっていないので、このほうが良いと思いますといったご意見をお待ちしています!)

QuestioningUserEntityは、postQuestionメソッドで質問作成に失敗した場合、独自で設計した例外「QuestionPostFailedException」をThrowします。

ここでの失敗というのは、例えばQuestioningUserEntityに格納されているユーザーが「学生ユーザー」ではなかった場合や、他にも「1時間に1問しか質問できない」といったサービス独自の制約があったときにその制約に弾かれた場合などです。
こういった制約条件はpostQuestionメソッド内に集約されていて、UseCaseからは条件の詳細を知ることは無いようにします。

QuestionPostFailedExceptionはLaravelで用意されているValidationExceptionを継承して実装するのがいまのところおすすめです。というのも、ValidationExceptionのサブクラスがThrowされるとLaravelの例外ハンドラ(Handler.php)がステータスコード422(APIの場合)でクライアントに返してくれるからです。

例外がAPIのエラーメッセージやステータスコードを管理しているのが少し責務の位置づけが妙な気もしているのですが、Laravelを使ってきた自分としては自然なのでこの方法でやっています。

これらの制約条件をパスしたときのみQuestionEntityが同じような静的公開メソッドによってインスタンス化されて返り値となります。

質問作成時にのみ判断できる制約条件がある場合は、このcreateByQuestioningUser内部で実装するイメージです。

QuestionEntityの例示は省きます。

5. ValueObject(VO)の実装

ValueObjectは、その名の通り値をオブジェクトとしてよりリッチに表現できる余地を残す仕組みです。

さっそく実装内容を示しますが、VOはいたってシンプルではあります。

UserAccountId.php
final class UserAccountId
{
    private $id;

    private function __construct()
    {
    }

    public static function create(int $primitiveId): UserAccountId
    {
        if ($primitiveId <= 0) {
            throw InvalidUserAccountException::withMessages([
                'message' => 'ユーザーIDが不正です'
            ]);
        }

        $instance = new UserAccountId();
        $instance->id = $primitiveId;
        return $instance;
    }

    public function toInt(): int
    {
        return $this->id;
    }
}

僕はEntityと同じ要領で、createメソッドのみからインスタンスを生成できるようにしていて、そこでintはintでも0以下のintは許さないよ、といった数値のValidationを噛ませているイメージです。

また、実際はデータベースの永続化等でintに直さないといけない場面もあるので、toIntメソッドを実装しています。

たったこの程度で実装が終わることが多いので、当初は実装する意味はないかなーと思っていたのですが、結局意義はあるなと思ったので、原則ほとんどの値に対してVOを作っています。

ValueObjectの意義

ValueObject(VO)の意義は主に以下の3つあると思っています。

  • メールアドレスの形式検証のように、一般的な検証ロジックを集約する
  • 個々の値に対して成立する制約条件を表現する
  • 引数をVOにしてレイヤー間のデータをやり取りすることで、引数の順番をミスするなどのケアレスミスを防ぐ

検証ロジックに関しては、LaravelならFormRequestを使ったほうが便利じゃないか、という意見があると思います。

しかし、FormRequestの場合、あのクラスの役目は「このリクエストにはどんなパラメータがあるのか、またはそれは必須か」という存在確認と、「それぞれのパラメータの値は【一般的に妥当か】」(メールアドレス検証など)と、「それぞれのパラメータはサービスを成立させる上で問題ないか」(メールアドレスが他のユーザーと被っていないか)というサービス仕様上の検証の3つほどの観点が混ざってしまっています。

具体的に何が問題かと言うと、弊社のようにWebとiOSアプリにサービスを展開する場合、ほぼ似ているけど微妙に入力パラメータの違うAPIが複数生まれて、それぞれFormRequestを作成していると、上記で言うところの【サービス仕様上の検証】が複数のファイルにまたがって記述されることになります。

すなわち、ある日「ユーザーのメールアドレス重複を許す」という決定が例えば下ったとして(ヤバいけど)、そのときの変更範囲が各プラットフォームごとに発生するということです。それって大袈裟だなというか、Presentation層に業務ロジックによるValidationが漏れ出ているから変更範囲が広がっているのだなと感じます。

なので、僕としてはFormRequestは便利なのですが、あれだけで検証ロジック全て終わりではなくて、サービスの仕様に依存するものはValueObjectとかEntityで検証ロジックを表現しよう、と思うのです。メールアドレスの書式検証のような、ユースケースにあまり依存しないものはVOで、メールアドレスが既存のユーザーに被っているかどうか、というユースケースに寄ったものはEntityやドメインサービス(※ここでは書かないですが詳しくは体験DDDや実践DDDを参照)に書き込みます。

また、関数の引数をVOにすることで、連想配列の中身がわからないとか、引数が全部intだからうっかり順番を間違えてしまう、というようなミスを防ぐことができます。

6. Repositoryの実装

ようやくここまで来て永続化の話ができるようになりました。最初のLaravelのEloquent Model依存の設計手法ではテーブル設計から考えていたので、ここまで進めてようやくRepositoryを考えるというのは、僕にとってはかなり斬新です。もちろんテーブル設計が複雑な場合、結局EntityやUseCaseが引っ張られる可能性はありますが、原則UseCaseやEntityから考えるのが良いと思います。

Repositoryがやることは至ってシンプルです。ここではQuestionerRepositoryの具体実装を示します。忘れた方はUseCaseの説明に戻って、getQuestionerメソッドを使っている箇所を探してみてください。

QuestionerRepository.php
final class QuestionerRepository implements QuestionerRepositoryInterface
{
    public function getQuestioner(UserAccountId $userId): ?QuestioningUserEntity
    {
        $userOrm = new \App\Model\User();
        $userData = $userOrm->find($userId->toInt());

        if ($userData === null) {
            return null; // or throw an Exception
        }

        return QuestioningUserEntity::reconstructFromDatabase(
            $userId,
            UserType::create($userData->user_type)
        );
    }
}

Repositoryの実装のポイントは、なんといってもLaravelのEloquent ModelをORMでのみ利用するというところです。

        $userOrm = new \App\Model\User();
        $userData = $userOrm->find($userId->toInt());

ここで懐かしのUserモデルが利用され、findメソッドによって指定したIDのユーザーインスタンスを取得します。

しかし最終的には先述のreconstructFromDatabaseによってQuestioningUserEntityにWrapされ、IDとユーザータイプだけを持ったインスタンスとしてユースケース層に渡っていくこととなります。

この方法によって、僕が頭を悩ませていた、Modelがどこからでも使われていて影響範囲が読めない問題を防ぎます。ユースケースやドメイン知識ごとに適切なEntityと、そのEntityに必要なデータだけ取得、保存するRepositoryを作成することで、もちろんファイル数やクラス数は爆増しますが影響範囲を絞ることに成功します。テスタビリティも向上することでしょう。

補足

ここまでで一通りの説明は終了します。最後に補足をいくつか。

UseCaseのクラス設計

僕は1ユースケース1クラスで作るのが気に入っています。唯一のメソッドexecuteのみを有するイメージです。

なぜかというと、UserUseCaseのような抽象的な名前にしてしまうと、なんでもかんでもそのファイルに実装が詰め込まれ、可読性の低下、複雑にするだけの再利用といった結果を生むだろうと思ったからです。

Interfaceについて

レイヤ間の抽象化にInterfaceの実装は欠かせません。僕は現状、RepositoryにのみInterfaceを作成し、実装するというルールにしています。UseCaseもInterfaceを作ったほうが良いのかなとは思いますが、単純に手間なのでやっていません。。。

AppServiceProvider.phpでInterfaceと実装をBindさせるように設定しています。

どこまでDDDするのか?

もちろんサービス全体をDDDで作り直すのが理想でしょうが、正直今の自分にその余裕はないです。弊社がスタートアップというのもありますが、技術都合でDDDに変更しなければならない!というのを押し通すのは難しいなと思っています。

とはいえ、現時点で弊社でDDDに挑戦しているのは「サイトに登録している家庭教師への指導依頼=コンバージョン」と、「学生ユーザーが限定機能を開放する定額課金プラン=実際に金銭が動く」というサイト内でもかなり高難度かつミスが許されない部分です。こういった特定の機能であれば、ある意味他機能から独立するのが望ましい上に、経営層へ実装に時間をかけ保守性およびテスタビリティを高める説明が自然にできるため、実践したという流れになります。

テストは書いているのか?

最初テストを書かなかったのですが、DDDで開発しているとEntityやVO、UseCaseの作り直しが開発の過程でしばしば発生するので、テストを書いていないと変更が億劫になりいずれ手抜き実装が爆誕することが想定されました。

現在はPHPUnitを使って、UseCase単位のテストは書くようにしています。また、結合テストとしてHTTPテストも記述しています。

詳しくは別の記事などで書ければと思いますが、LaravelではTestCaseクラスが独自拡張されていて、setUpメソッドをオーバーライドして利用することでEloquent Modelやfactoryメソッドをテストケースで利用、再利用することができます。setUpメソッドをオーバーライドせずに使うとDIなどのLaravelの初期ロード(正式名称なんですかね)が動かないのでテストが書きにくいです。

テストも書くとなると、尚更事業優先度が非常に高いところからチャレンジするのが向いているなと感じているところです。

ディレクトリ構成は?

下記のようなディレクトリ構成でやってみています。既存設計がもうそこそこの規模になっているので、ルートディレクトリごと分けてしまっています。

app/
├── Console
├── Constant
├── Domain // ここのディレクトリ配下はDDDのアプローチで設計・実装している
│   ├── { DomainName } // 扱う事業領域名
│   │   ├── Domain // ドメイン層
│   │   ├── Infrastructure // 永続化層
│   │   └── UseCase // ユースケース層(旧設計がServiceという単語を使っており、意識して分けるためにUseCaseとした)
│   ├── { DomainName }
│   ├── { DomainName }
│   ├── ...
│   ├── Base // DDD全体でベースとなるClass。将来的にはEntityやVOの基底クラスも作りたい
│   │   └── Exception // ValidationExceptionを拡張したclassを配置
   ...
├── Events
├── Exceptions
├── Helpers
...

まとめ

旧質問投稿UseCase

DDDをやる前だったら、質問投稿時のUseCase(Service層)はこんなシンプルな実装になるでしょう。

QuestionService.php
    public function postQuestion(array $params, int $userId)
    {
        $question = $this->questionRepository->storeQuestion($params, $userId);

        return $question;
    }

この実装に比べれば、これまで説明した実装は、ソースコードが仕様を説明し、適切な制約をかけているという観点で非常に情報量が多い実装になっていることがわかると思います。
具体的には投稿内容がarrayにまとめられているよりVOになっているほうがわかりやすい、どんなユーザーが質問投稿できるか理解しやすいなどです。

結論

  • LaravelのModelをあらゆるレイヤーで使うと改修が難しくなる
  • 開発する機能のユースケースを主語と述語で文章に表現し、そのままUseCase層の実装として表現する
  • EntityやValueObjectに制約条件をまとめ、適切に例外を吐く
  • LaravelのModelはORMとしてのみ利用する!!
  • PHPの言語自体の限界はあるので、命名の工夫などで適宜我慢する
  • 実運用の際はどの機能から、どこまで完璧主義でDDDをやるか考える

以上です

思った以上に長文になりましたが、今の自分のDDDの実力はこんなところです。もっと上手に設計できるように経験を積んで、運用を経て痛い目に遭っていこうと思います!

しかしやっぱり型のある言語がいいですね。最近はフロントもバックエンドもTypeScriptで組むのが良いんじゃないかと思えてきています(過激派)。

ぜひTwitterでも繋がっていただけると嬉しいです。
https://twitter.com/Meijin_garden

また、よかったらLaravelの初期設計をやった頃のQiitaの記事もぜひ。
【実録】WordPressサイトをAWS+Laravel+Nuxtにフルリプレイスした話(技術選定編)

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

Google Translation API v3 を Node で使ってみた

はじめに

案件で使う機会があったので忘備録的な感じで記載していこうと思います。
諸々間違い、認識違いがあるかもしれませんが生暖かく見守っていただければと思います。

実装イメージ

  • XServer X10プランを使用します。
  • NodeでWebサーバ起動して云々はXServer上で実装するのは難しいのでphpで受けてコマンド呼び出しで動かします。
  • 翻訳結果はjson形式で返却します。
    • 本来であれば翻訳結果をjavascriptが受け取りうまくゴニョゴニョしてhtml上で表現するが正しいと思いますが、残念ながら自分はPHPerでjavascriptが得意じゃないのでこの部分は割愛させていただきたく。

環境

  • XServer X10プランで契約できるレンタルサーバ
  • php
    • 7.2.17
  • perl
    • 5.16
  • nodebrew
    • 1.0.1
  • Node
    • v12.10.0
  • npm
    • 6.10.3

※php, perlのバージョンはXServerのデフォルト設定(2019/12/04時点)
※nodebrewは自分が試した時点での最新。
※Nodeはnodebrewにて指定する感じ、npmはnodeインストール時に一緒に入るもの。

ディレクトリ構成

  • XServerの基本構造をそのまま使用します。
  • 翻訳機能についてはドキュメントルート直下には作りませんでした。直URLでアクセスされたときのことを考えたくなかったからです。
  • 各ディレクトリ・ファイルについては後述で説明しますが、最終的には下記のような構成になります。
/xxxxx.xsrv.jp/
    public_html/
        index.html
        translation.php
    node/
        json/
        node_modules/
        package-lock.json
        translation.js
        gcpprj-example-xxxxxx-yyyyyyyyyyyy.json

nodebrew, node.js, npmのインストール

$ cd ~/
$ wget git.io/nodebrew
$ perl nodebrew setup
 :
 :
$ vi .bashrc
export PATH=$HOME/.nodebrew/current/bin:$PATH   ※この行を.bashrcの末尾に追加
$ source ~/.bashrc
$ nodebrew help    ※このコマンドを叩くと諸々出てくれば成功
nodebrew 1.0.1
 :
 :
$ nodebrew install v12.10.0  ※これでnode.js v12.10.0がインストールされる
$ nodebrew use v12.10.0      ※これでnode.js v12.10.0を使うよと宣言する感じ
$ node -v
v12.10.0
$ npm -v
6.10.3

Google Cloud Platformの設定及びGoogleTranslateAPIを使用するための準備

  • GCP側でプロジェクトを作成します。
  • 「請求アカウント」を作成し上記で作ったプロジェクトに紐付けてください。
  • 使用APIに「Cloud Translation API」を追加してください。
  • このクイックスタートのページの「プロジェクトをセットアップする」ボタンを押下しプロジェクトを指定すると「JSONとしての秘密鍵」がDownloadされます。 上記の「gcpprj-example-xxxxxx-yyyyyyyyyyyy.json」というのが「JSONとしての秘密鍵」になります。
  • クイックスタートにはその後「環境変数なんちゃら」という記載がありますが、とりあえず今のところはスルー。
  • クイックスタートの次の手順「クライアント ライブラリのインストール」では「NODE.JS」を選択するとnpmのインストールコマンドが出てくるのでそれを上記のnodeディレクトリ配下で実行します。
    • nodeディレクトリはデフォルトでは無いのでmkdirで作っていきながら作業する感じです。
$ cd ~/
$ cd xxxxx.xsrv.jp
$ mkdir node
$ cd node
$ mkdir json
$ npm install --save @google-cloud/translate   <== これがクイックスタート上に出てきたインストールコマンド
$ ls -1F
json/
node_modules/
package-lock.json
  • このnodeディレクトリ配下に先程Downloadされた「JSONとしての秘密鍵」をFTP等でアップロードします。
$ cd ~/xxxxx.xsrv.jp/node
$ ls -1F
json/
node_modules/
package-lock.json
gcpprj-example-xxxxxx-yyyyyyyyyyyy.json
  • クイックスタートの次の手順「テキストの翻訳」で、NODE.JSのサンプルコードをそのまんまコピーして上記で作ったnodeディレクトリにtranslation.jsとして保存。
  • サンプルコードのままだと、翻訳対象文字列がコードに直書き状態なので引数で受け取って可変できるようにします。その時、翻訳対象文字列を直接引数で指定してしまうと「コマンドインジェクション」が発生する可能性が高く怖いので翻訳対象文字列を引数のjsonファイルから取得するような仕様に変更します。

  • translation.jsの中身は下記の通り

const projectId = 'XXXXX-yyyyy-zzzzz';  // GCP上のプロジェクトIDを記載
const location = 'global';

// jsonから読み込むように修正
const jsonPath = process.argv[2];
const json = require(jsonPath);
const text = json.text;

// Imports the Google Cloud Translation library
const {TranslationServiceClient} = require('@google-cloud/translate').v3beta1;

// Instantiates a client
const translationClient = new TranslationServiceClient();
async function translateText() {
  // Construct request
  const request = {
    parent: translationClient.locationPath(projectId, location),
    contents: [text],
    mimeType: 'text/plain', // mime types: text/plain, text/html
    sourceLanguageCode: 'ja',   // 日本語から。
    targetLanguageCode: 'en',   // 英語に。
  };

  // Run request
  const [response] = await translationClient.translateText(request);

  for (const translation of response.translations) {
    console.log(`${translation.translatedText}`);
  }
}

translateText();
  • ディレクトリ内はこんな感じ
$ cd ~/xxxxx.xsrv.jp/node
$ ls -1F
json/  ※翻訳対象の文字列をjson形式にして保存する場所
node_modules/
package-lock.json
gcpprj-example-xxxxxx-yyyyyyyyyyyy.json
translation.js
  • 試しにこの状態で下記のコマンドを叩いて強引に実行してみます。
  • jsonで設定している日本語は「私は本を持っている」です。
$ cd ~/xxxxx.xsrv.jp/node
$ echo "{\"text\":\"\u79c1\u306f\u672c\u3092\u6301\u3063\u3066\u3044\u308b\"}" > json/example.json  ※ダミーjson作成
$ node translation.js /home/xxx/xxxxx.xsrv.jp/node/json/example.json
(node:395938) UnhandledPromiseRejectionWarning: Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
    at GoogleAuth.getApplicationDefaultAsync ...
     :
     :
(node:398472) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:398472) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

$
  • うまく動きません。「環境変数なんちゃら」をスルーしたためです。
  • 「環境変数を設定する」、「翻訳対象の文字列を取得し、jsonファイルを作る」という処理はこの後に記載するphpロジックにて実装することを想定しています。

php, htmlで残りの処理を作成する。

  • ドキュメントルート直下にindex.htmlを設置、翻訳対象文字列を入力するフォームを作ります。
<html>
  <head>
    <meta charset="UTF-8" />
    <title>日本語を英語に翻訳する</title>
  </head>
  <boby>
    <form action="translation.php" method="post">
      日→英、翻訳テキスト:<input type="text" name="target"> <input type="submit" value="翻訳">
    </form>
  </body>
</html>
  • translation.phpで下記機能を実装します。
    • 翻訳対象文字列の取得
    • jsonファイルの生成
    • 環境変数のセット
    • translation.jsの実行、結果取得
    • json形式にして結果を出力
define('HOME_DIR',       '/home/xxx/');  // 各自環境のものを入れる
define('NODE',           HOME_DIR   . '.nodebrew/current/bin/node');  // nodeのインストール状況では違うものになるはず
define('DOMAIN_DIR',     HOME_DIR   . 'xxxxx.xsrv.jp/');
define('NODE_DIR',       DOMAIN_DIR . 'node/');
define('JSON_DIR',       NODE_DIR   . 'json/');
define('GCP_JSON',       NODE_DIR   . 'gcpprj-example-xxxxxx-yyyyyyyyyyyy.json');
define('TRANSLATION_JS', NODE_DIR   . 'translation.js');


// 引数が無い場合は何もしない
if (!isset($_POST['target']) || $_POST['target'] === '') {
    header("Location: https://xxxxx.xsrv.jp/index.html");
    exit();
}

// jsonファイルを生成
define('TEXT_RANGE', implode(array_merge(range(0, 9), range('a', 'z'), range('A', 'Z'))));
$rndtxt   = substr(str_shuffle(TEXT_RANGE), 0, 16);
$jsonfile = sprintf("%s%s.json",JSON_DIR, $rndtxt);
$json_ary['text'] = $_POST['target'];
$jsondata         = json_encode($json_ary);
file_put_contents($jsonfile, $jsondata);

// JSONの秘密鍵を環境変数にセット
putenv('GOOGLE_APPLICATION_CREDENTIALS='.GCP_JSON);

// コマンドの作成・実行
$command = sprintf("%s %s %s", NODE, TRANSLATION_JS, $jsonfile);
$out = $res = '';
exec($command, $out, $res);

// 結果解析
if (!empty($out[0])) {
    header('content-type: application/json; charset=utf-8');
    echo json_encode(['text' => $out[0]]);
} else {
    header('content-type: application/json; charset=utf-8');
    echo json_encode(['error' => 'error']);
}
exit();

やってみる

  • こんな感じで入れてみて…

コメント 2019-12-06 205149.png

  • こんな感じで出る。
  • firefoxだとjson形式を開くと色々解析してくれる(が、今回はシンプル極まりないので特に意味なしですが)

コメント 2019-12-06 205244_2.png

補足

  • Google Translation API v3 はまだβ版なので注意しよう。
    • 早くphp版ライブラリでないかぁと思ったり。
  • Google Translation API v3 には無料枠がある。
    • FAQの「Cloud Translation API へのアクセス」の「無料の割り当てはありますか?」に記載あり。
    • 自分が見た価格表はv3の特別枠の記載があったが消えている(2019/12/04更新したみたいでその時消えたっぽい)
    • こうやって記載しているそばから更新されていく可能性大(なぜならまだβ版だから)
    • 案件で使った経緯はこの「無料枠」があるところが大きかった。(が、今は消えているのでちょっとドキドキしています)

まとめ

  • 自分の担当案件ではよく「日英のHPをCMSで作る」という要件がよくあるのでこれからも多用していくかなと思っています。
  • 正直、ロジックを作るところよりもGCPの設定のほうが難しかった。

:christmas_tree: FORK Advent Calendar 2019
:arrow_left: 9日目 Vuetifyのdatepickerを使って【和暦】+【年度/月】pickerを作ってみた @BigFly
:arrow_right: 11日目 @talow1 さんよろしくおねがいします。

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

[PHP]多次元配列のソートを1行で書く

概要

PHPの多次元配列で、指定したキーを基準にソートしたい場合、次のように処理を書く。

  1. ソートしたいキーの配列を作成して、
  2. 関数 array_multisort() に渡してあげる。

それを1行でやってしまおう。
(PHP5.5.0以降)

よくあるやり方。

track_num の番号でソートしたい場合、track_numだけを抽出した配列 $sort を作って array_multisort() の第一引数に渡す。

array_multisort.php
foreach ((array) $array as $key => $value) {
    $sort[$key] = $value['track_num'];
}
array_multisort($sort, SORT_ASC, $array);
var_dump($array);

引用:PHPの多次元連想配列のソート
https://qiita.com/shy_azusa/items/54dadc55e3e71cde1445

1行でやる

関数 array_column() を使います。
(PHP5.5.0以降で使えます。)

array_multisort_1liner.php
array_multisort (array_column($array, 'track_num'), SORT_ASC, $array);
var_dump($array);

参考:One-liner function to sort multidimensionnal array by key, thank's to array_column
(多次元配列のキーを指定して1ライナーでソートする。感謝するぜ、array_column。 )
https://www.php.net/manual/ja/function.array-multisort.php#119291

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

【決定版】PHP : echoとprintの違い 【まとめ】

序文

echo / print とは

PHP以外でも古くから、LinuxコマンドやC言語など様々な場所で存在する標準出力系の命令。
ブラウザやシステムに対して「とにかく文字列を出力する」ということをしてくれる。
echo / print について各所で調べると解説記事が散在するようなので、自分用に下記でまとめています。

PHPでは echo / print どっちを使うべきか?

結論から言うとどちらでも良いようです。仕様が少しだけ違いますが、自分の狭い観測範囲ではそのために使い分ける場面はほとんどないので、使い分ける場面に遭遇したらまた記事は更新します。
echo / print を使っている人の理由をそれぞれ確認すると「初めて使ったのが echo だったから」「最初に読んだ本のサンプルプログラムでは print だったから」というものが多い印象。
echo と print は、PHPマニュアル上はどちらも「String 関数」のくくりになっていますが、両者ともに厳密には「関数」ではなく「言語構造」(PHPという言語を構成する要素そのもの。他にifやarray()やuseやissetなどがある)ということには注意が必要です。

PHPマニュアル: String 関数
PHPマニュアル: キーワードのリスト

本題

echo の仕様

PHPマニュアル: echo — 1 つ以上の文字列を出力する

echo ( string $arg1 [, string $... ] ) : void

すべてのパラメータを出力します。末尾に改行を付加することはありません。

echo は実際には関数ではありません (言語構造です)。このため、使用する際に括弧は必要ありません。 (いくつかの他の言語構造と異なり) echo は関数のように動作しません。そのため、 関数のコンテキスト中では常に使用することができません。 加えて、複数のパラメータを指定して echo をコールしたい場合、括弧の中にパラメータを記述してはいけません。

echo には、開始タグの直後に等号を付ける短縮構文もあります。 この短縮構文は、PHP 5.4.0 より前のバージョンでは設定オプションshort_open_tag が有効な場合しか使えません。

I have <?=$foo?> foo.

print との主な違いは、 echo がリスト形式の引数を受け付け、返り値を持たないことです。

print の仕様

PHPマニュアル: print — 文字列を出力する

print ( string $arg ) : int

arg を出力します。

printは実際には関数ではありません (言語構造です)。このため、引数を括弧で括る必要はありません。

echo との主な違いは、 print が単一の引数のみ受け付け、常に 1 を返すことです。

というわけで両者の違い

echoのマニュアルには行末に void とあり、printのマニュアルには行末に int とありますが、これはechoが返り値を持たず、printは返り値で常に1を返すことを表します。
このことからechoでは値を持ったり返したりすることはできないため、条件分岐などで式として扱えませんが、printには返り値があるため式として利用することが可能です。
PHPの言語仕様によると、式の定義は、値のあるもの全てだそうです。

PHPマニュアル > 言語リファレンス > 式

下記で、printとechoを式として使えるかどうかの簡単な例を掲載しています。

if (print '') {
    print 'OK';
} else {
    print 'NG';
}
// 出力結果 OK

// if (echo '') {
//     echo 'OK';
// } else {
//     echo 'NG';
// }
// echoは返り値がない=値を持たない=式ではないので、こちらはパースエラーになる

他には、引数として受け付け可能な数も異なり、echoでは複数の文字を引数として与えることができますが、printでは単一の引数のみ受け付けるようになっています。
例えば複数の文字列を扱いたいときに . (ドット) で連結することが多いかと思いますが、 , (カンマ) で区切ることによって第一引数、第二引数を指定するように、echoでは複数の文字列の指定が可能だということです。

echo 'A','B'; // カンマ使用 → 出力結果 AB
echo 'A'.'B'; // ドット使用 → 出力結果 AB

print 'A'.'B'; // ドット使用 → 出力結果 AB
// print 'A','B'; // printでカンマを使って複数の文字列を出力しようとするとエラーになる

echoを単体で使用する場合は、省略して書くやり方もあります。
例えばHTMLファイル上で部分的にechoを使用したいとき、 <?= ?> のタグで囲ってあげて省略記法とすることができます。
これはPHP5.4.0未満のバージョンでは設定オプションshort_open_tag を有効にしないと使えませんが、5.4.0以降であればデフォルトで使えます。

html
key : <?='value'; ?> // 出力結果 key : value

筆者が使うのはというと

echo派です。
printよりもechoの方がタイピングが少なくて済むため。
printは返り値があり関数的な振る舞いをする分、echoより処理速度が遅いとも言いますが、100万回くらいのprintとechoを実行したときに0.01秒の差が出るかどうかというくらいのようです。(ただし出典のリンクが散逸してしまったので話半分に読んでいただければ。)
慣れた方を使うか、現場のコーディング規約に従うかすれば問題ないでしょう。

参考URL

PHPの言語構造とは
PHPのechoとprintは言語構造だというけど、言語構造とは何か
var_export — 変数の文字列表現を出力または返す
var_dumpよりログ作成に便利なvar_export
echoコマンドの詳細まとめました【Linuxコマンド集】
PHPの省略記法
【bash】 echoとprintfの違い
PHP の echo と print のちがいと使いどころ
echo,print,printfの違い
あなたは『echo』派?それとも『print』派?
【PHP】echoとprintfとsprintfの違い
【PHP超入門】式・文・構文・言語構造・制御構造について

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

コーディングの手助けとなる "コメントコーディング" を紹介

これはユアマイスター Advent Calendar 2019の9日目の記事です。

はじめに

みなさんは、コメントコーディングの存在を知ってますか?
コーディングをする上で色々な手助けをしてくれるこの手法を私は利用しています。
なので、今回の記事を通して

・まずはその存在を知って欲しい
・そして良し悪しを理解して欲しい

この2つの観点から、コメントコーディングに関する考えを独自の主観でいくつかお話ししたいと思います。

コメントコーディングとは?

まずコメントコーディングは、コメントとコーディングを掛け合わせた造語であり、コーディングをする上での手助けとなる手法を指します。

ちなみにコメントに関しては、Wikipediaさんで掲載されてましたので、下記にその引用を記載します。

コメント(英: comment)とは、コンピュータ言語(プログラミング言語やデータ記述言語)によって書かれたソースコードのうち、人間のために覚えとして挿入された注釈のことである。この部分はコンピュータが処理を行うときにはないものとして無視されるため、自由に文を挿入することができる。
引用元:Wikipedia

つまりコメントとは、注釈という事ですね。

実際のソースコード上におけるコメントはこちら↓
スクリーンショット_2019-12-08_15_20_20.png
引用元:https://github.com/cakephp/cakephp/blob/master/src/Http/ServerRequest.php

これは、CakePHP3のServerRequestクラスのソースコードを一部抜粋したものです。
赤い枠で囲われている部分、全てがコメントです。
※コメント、というもののイメージを掴んで頂くための実例なのでコメントの量は気にしないでください。

このように、プログラミングをする時には、コメントを記載するケースがあります。

プログラミング言語を利用してプログラムを記述することをコーディングと呼ぶことがありますが、それと近しい意味合いで、コメントを利用して記述する事をコメントコーディングと呼びます。

以下に例を示します。

class UsersController extends AppController
{
    public function input()
    {
        // POSTリクエストかどうかを判定
        // TRUEの場合、POSTデータに対してバリデーションをかける
        // バリデーションに引っかかった場合、前の画面にリダイレクトする
        // バリデーションを通った場合、確認画面を表示する
    }
}

多少粒度は粗めですが、こんな感じでコメントによるコーディングしていきます。
やりたい事を言語化するという言い方もできるかもしれません。

もう少し粒度を細かくする場合、例えば、

・POSTリクエスト以外のリクエストがきた場合どうするのか?
・どんなバリデーションをかけるのか?

などを記述すると良いと思います。

コメントコーディングのメリット

1. コーディング効率が上がる

なんといってもこれが最大のメリットだと私は感じます。

コメントコーディングをする事で

・やりたい事が明確になる
・事前に課題の特定ができる(※潜在的な課題は除く)

そのため、純粋なコーディングの時に無駄な時間がなくなるため、コメントコーディングがコーディングの効率アップに寄与してると私は思います。

効率を考えたコーディングをする場合、まずやる事を明確にすることが重要だと考えます。

そのため、行き当たりばったりのコーディングではとても非効率なコーディングになってしまい、また、トータルで見た時のコーディング時間が多くかかってしまうため、効率的な手法ではないと私は思います。

2. 課題解決の相談がスムーズになる

上記で述べたように、コメントコーディングをする事で、やりたい事を明確にする事ができます。

そのため、課題や疑問を解決するための相談は、やりたい事が明確になっている事でスムーズになります。

コメントコーディングによるメリットなのか?と言われると、やや怪しさが残りますが、
コメントコーディングはやりたい事を明確にする事ができるという要素を持っているため、間接的ではありますが、メリットの1つと言えるでしょう。

コメントコーディングのデメリット

1. コーディング時間が増加する

ここでいうコーディング時間とは、プログラミング言語を利用したコーディングとコメントコーディングを合わせたトータルの時間を指します。

これがデメリットか?というとやや自信はありませんが、時間に厳密な人や実体験としてコメントコーディングの良さを感じていない人は、ただの時間増加と捉えてしまう可能性があるため、デメリットだと感じるかもしれません。

上記で述べたように、純粋なコーディングをする際の無駄な時間は減ります。

しかし、コメントコーディングという1つのフローを追加する事とほぼ同じなので、必然的に時間は増えます。
特にコメントコーディング知りたての当時の私は、コメントコーディングのメリットを体験として感じられていなかったため、早く純粋なコーディングをしたい、という煩わしさに駆られてた事もありました。

まとめ

今回はコメントコーディングについてお話ししてみましたがいかがだったでしょうか?

冒頭でもお話ししたように、この記事を通してコメントコーディングの存在を知り、そして少しでも理解していただけたら嬉しいです。

メリット・デメリットの話は、完全に私の主観でしかないので参考程度に読んでいただければ幸いです。時にメリットはデメリットとなり、デメリットがメリットとなることがあるので、感じ方は人それぞれでしょう。

コメントコーディングに限らず、開発の手助けとなる手法はどんどんチャレンジしてみて、都度紹介できればと思います。

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

nginx-proxy と docker-compose でレガシーなLAMP(LEMP)環境を複数稼働できるようにした

この記事は、Makuake Development Team Advent Calendar 2019 7日めの記事です。

yutakiと申します。
PRIME ORDERというWebシステム開発サービスのエンジニアリングマネージャをやっております。
https://prime-order.jp/

概要

最近、レガシーな案件のプレビューのために
十数年ずっと頑張ってきたオンプレの共有開発サーバが故障してしまい
急いでnginx-proxyとdocker-composeで複数案件の開発環境を復活させた話をします。

やったこと(ざっくり)

※端末はmacです
基本的に下記のコンテナ構成でプロジェクトを作成

  • 新しめ(laravel系): [nginx] + [php] + [mysql] + [node(build専用)] の4台構成
  • 古め(その他): [apache & php] + [mysql] の2台構成

そしてnginx-proxyでローカルドメインを割り当てました。
たとえばhogeプロジェクトであれば http://hoge.localhostで使えるようにした感じです。

1. 案件ごとの構成をdocker-composeで作成

ベースimageの選定について

一般的な構成の時は、app以外はほぼ公式イメージで対応しました。

appについては、docker-php-ext-installを使えるので、エクステンションの管理も楽です。
一部、pecl経由でないと入れられないものもありますがそれはpeclで入れて有効にすればいいだけ。
(imagemagickとか)

FROM php:7.1-fpm-alpine

# lib
RUN apk add --no-cache --virtual build-dependencies gcc make autoconf libc-dev libtool \
 && apk add --no-cache --virtual zlib1g-dev libxml2-dev \
 && apk add --no-cache --virtual libmagickwand-dev libpng-dev imagemagick-dev

# composer
COPY --from=composer /usr/bin/composer /usr/bin/composer

# php extension
RUN docker-php-ext-install zip xml pdo_mysql gd

RUN pecl install imagick \
  && docker-php-ext-enable imagick

RUN pecl install mailparse \
  && docker-php-ext-enable mailparse

COPY php.ini /usr/local/etc/php/

WORKDIR /var/www

こんな感じで書いたものを使いまわしていけました。


apacheが一緒になっている5.4系のイメージなどはこんな感じ。

FROM php:5.4.45-apache

# composer
COPY --from=composer /usr/bin/composer /usr/bin/composer

# lib
RUN apt-get update \
  && apt-get install -y git libmagickwand-dev libmcrypt-dev

# php extension
RUN docker-php-ext-install mcrypt pdo pdo_mysql zip \
  && pecl install imagick \
  && docker-php-ext-enable imagick

# apache module
RUN mv /etc/apache2/mods-available/rewrite.load /etc/apache2/mods-enabled/ \
  && mv /etc/apache2/mods-available/headers.load /etc/apache2/mods-enabled/

# conf, ini
COPY site.conf /etc/apache2/sites-enabled/site.conf
COPY php.ini /usr/local/etc/php/php.ini

WORKDIR /var/www/site

apacheのモジュールがconfファイルを移動するだけで有効になるので楽なのがポイントです。


あとは、ごく少数、PHP5.2みたいな古いバージョンが使われている社内システムもありました。
そちらは公式にはバージョンがないので、DockerHubで該当バージョンを使っているものを探して利用しました。

例えば下記など。
https://github.com/andres-ortiz/php5.2-apache2.2
セキュリティパッチ等当ててくれているので開発環境とはいえありがたいです。
ただこのimageは起動時にrun.shでhtdocs以下にログディレクトリを作成してしまうので
スクリプトを上書きするかDocumentrootを変更するなどすると良いと思います。

2. nginx-proxy

こちらを利用します。
https://github.com/jwilder/nginx-proxy

使い方の要点は下記です。

  • どこでもいいのでnginx-proxyをdocker-compose.yml経由で起動しておく
  • 各案件のdocker-composeの調整
    • 「VIRTUAL_HOST」という環境変数でホスト名を定義する
    • nginx-proxyと同じネットワークに所属させる

nginx-proxyを起動しっぱなしにしておけば特に他に設定が不要という神ツールです。
(dockerのプロセスを監視、各docker-composeのup、downを検知して勝手にプロキシしてくれます)

nginx-proxy本体のdocker-compose.yml

docker-compose.ymlは最小構成ならこれだけ

docker-compose.yml
version: '2'

services:
  nginx-proxy:
    image: jwilder/nginx-proxy
    ports:
      - "80:80"
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
    restart: always

ちなみにネットワーク名は固定してもいいと思います。
nginx-proxyというディレクトリで起動しているなら「nginx-proxy_default」
がネットワーク名になります。

各案件のdocker-compose.yml

たとえばこう

docker-compose.yml
version: '3'

services:
  hoge-web-php:
    image: ${WEB_PHP_IMAGE}
    build: docker/web-php
    ports:
    - 80
    volumes:
      - ./logs/apache2:/usr/local/apache2/logs:cached
      - ./server:/usr/local/apache2/app:cached
    environment:
      VIRTUAL_HOST: hoge.localhost
  hoge-db:
    image: ${MYSQL_IMAGE}
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DBNAME}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      TZ: ${MYSQL_TZ}
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
    - ./logs/mysql:/var/log/mysql:cached
    - ./docker/db/data:/var/lib/mysql:cached
    - ./docker/db/my.cnf:/etc/mysql/conf.d/my.cnf
    - ./docker/db/sql:/docker-entrypoint-initdb.d
    ports:
    - 3306
# 下記はnginx-proxyを利用するときのみ解放
networks:
  default:
    external:
      name: nginx-proxy_default

webサーバのコンテナのポートは80番にしてください。
あと、サービス名(hoge-web-php, hoge-dbなど)はネットワーク内でuniqueである必要があります。
案件名をprefixなどでつけておくといいと思います。

ポイントは、hoge-web-phpの

docker-compose.yml
environment:
  VIRTUAL_HOST=hoge.localost

そして最下部の

docker-compose.yml
networks:
  default:
    external:
      name: nginx-proxy_default

です。
なお、このnetworksセクションをコメントアウトしてしまえば通常通りポートフォワードでの利用もできます。
nginx-proxyにproxyさせたいときだけ記述すれば良いです。

これで、http://hoge.localhostで該当案件を処理することができます。

その他

proxyの設定をいじりたい場合

confを上書きすれば簡単です。

こんなふうに上書き

docker-compose.yml
    volumes:
      - ./proxy.conf:/etc/nginx/proxy.conf

デフォルトの中身はこうです。
(必要なものは揃っている印象です)

proxy.conf
# HTTP 1.1 support
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $proxy_connection;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;

# Mitigate httpoxy attack (see README for details)
proxy_set_header Proxy "";

SSL対応(リンク紹介だけ)

nginx-proxyをssl対応する場合は
https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion
を活用すれば死ぬほど簡単に対応できます。

双方の活用は、素晴らしい参考記事がありましたので貼っておきます
https://tech.quartetcom.co.jp/2017/04/11/multiple-ssl-apps-on-one-docker-host/

Laravel Valetを利用する場合

80/443ポートの食い合いになるので使う時はvalet stopしてください。
Laravel Valetはローカルにnginxを立ち上げていて、dnsmasqと併用して似たようなことをしています。

valetと共存させようと欲張り、nginx-proxyのポートを8080:80
にして運用しようとしてみたところ、プロキシはされるのですが
ローカル -> nginxの間のX-Forwarded-*系を伝搬させることができず
Laravelのrouteメソッドなどで適切なURLが作成できませんでした。
しばらく調べてみたんですが、案件ごとにごちゃごちゃ設定するしか手がなさそうだったので断念。

これTCPのプロキシならMySQLもいけるんじゃ?->いけなかった

MySQLも3306:3306でプロキシしてhoge-db.localhostでSequel Proとかでアクセスできるんじゃないのか?
と思ってやってみましたが、ホストは解決できるものの正常に通信はできませんでした。
超残念。こちらは大人しくサービスごとにport設定するなどしています。
issueでも似たこと話してました。
https://github.com/jwilder/nginx-proxy/issues/318

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

[baserCMS] AddConfig を使って固定ページ専用の入力画面みたいなものを作る

前回記事で書いたAddConfigプラグインは独自のconfigを追加できるものですが、実質的にはカスタムフィールドでオリジナルデータを簡単に登録できるものだったりもします。
これをうまく利用する事でHTMLが苦手な人も入力しやすい固定ページ用の入力画面(会社案内とか求人情報とか)を作ることができます。
今回は、そのやり方について紹介したいと思います。

固定ページのカスタム方法

デフォルトでインストールされた状態の会社案内(about)ページをカスタマイズするイメージで進めていきます。

AddConfigでカスタム項目を作る

カスタム項目をAddConfigで作成していきます。
AddConfigのインストールや使い方そのものに関しては、前回の記事や使い方の記事を書いて頂いたものがあるのでそちらを参考にしてください。

会社データの部分をカスタムフィールド化していきます。

内容的にはCakePHPのformヘルパーのinputとvalidationを混ぜたようなもので設定を書いて4つフィールドを作っただけです。

AddConfig/Config/setting.php
//下の方に設定を追記します
$config['AddConfig']['form'][] = [ 
    'title' => '会社案内', //アコーディオン・見出し
    'fields' => [
        'about_name' => [  //フォームのname
            'required' => true, //必須マークを表示したい場合につける
            'label' => '会社名',
            'parts' => [ //フォームタイプ
                'type' => 'text', 
                'size'=>'55',
            ],
            'validate' => [
                [
                    'rule' => 'notBlank',
                    'message' => '会社名を入力してください'
                ],
            ],
        ],
        'about_organize' => [ 
            'required' => true, 
            'label' => '設立',
            'parts' => [ 
                'type' => 'text', 
                'size'=>'55',
                'placeholder' => '例: 2015年6月',
            ],
            'validate' => [
                [
                    'rule' => 'notBlank',
                    'message' => '設立を入力してください'
                ],
            ],
        ],
        'about_address' => [ 
            'required' => true, 
            'label' => '所在地',
            'parts' => [ 
                'type' => 'textarea', 
                'rows' => '5', 
                'cols' => '60',
                'placeholder' => '例: 渋谷区渋谷1-1-1 ○○ビル5階',
            ],
            'validate' => [
                [
                    'rule' => 'notBlank',
                    'message' => '所在地を入力してください'
                ],
            ],
        ],
        'about_description' => [
            'required' => true, 
            'label' => '会社概要',
            'parts' => [
                'type' => 'textarea',
                'rows' => '5',
                'cols' => '60',
                'counter' => true,
                'maxlength' => 1000,
            ],
            'validate' => [
                [
                    'rule' => 'notBlank',
                    'message' => '会社概要を入力してください'
                ],
                [
                    'rule' => ['maxLength', 1000],
                    'message' => '会社概要は1000文字以内で入力してください'
                ],
            ],
        ],
    ]
];

インストール直後はサンプルが表示されるようになっているので、以下部分もsetting.phpからコメントアウトしてください。

// include "sample-form.php"; //使用時にコメントアウトするか削除してください。

setting.phpの内容を保存して【オリジナル設定】を開くと以下のようなフォームが作成されます。データを登録して保存します。
addconf01.jpg

固定ページにオリジナルテンプレートを設定する

固定ページは /theme/使用テーマ/Pages/templates/ 以下に新たにphpファイルを置くと、
固定ページの編集画面の中で作成したファイルをテンプレートとして選ぶことが出来るようになります。
default.php(ビジュアルエディタの内容を出力するだけのもの)をコピーしてabout.phpを作成します。
addconf00.jpg

会社案内の編集ページで【固定ページテンプレート】のプルダウンをaboutに変更して保存します。
こうすることで会社案内のページではabout.phpが表示されるようになります。

会社案内のテンプレート(about.php)を修正する

先ほどaddConfigで登録した内容をabout.phpで表示できるようにします。
会社案内ページに登録されているのビジュアルエディタのソースをベースに修正します。今回はコピーしたデフォルトの内容は使わないので全部削除して以下の内容に差し替えます。

/Pages/templates/about.php
<h2>会社案内</h2>

<h3>会社データ</h3>

<table>
    <tbody>
        <tr>
            <th>会社名</th>
            <td><?php $this->AddConfig->f('about_name')?></td>
        </tr>
        <tr>
            <th>設立</th>
            <td><?php $this->AddConfig->f('about_organize')?></td>
        </tr>
        <tr>
            <th>所在地</th>
            <td><?php $this->AddConfig->brf('about_address')?></td>
        </tr>
        <tr>
            <th>事業内容</th>
            <td><?php $this->AddConfig->brf('about_description')?></td>
        </tr>
    </tbody>
</table>

<h3>アクセスマップ</h3>
<?php $this->BcBaser->googleMaps(array("width" => 585)) ?>

ヘルパーの内容はこんな感じです。

$this->AddConfig->f('キー名')  テキスト表示
$this->AddConfig->brf('キー名')  改行を<br>にしてテキスト表示

どちらもデフォルトだとhtmlタグはエスケープされます。リンクなどを付けたい場合は第2引数でfalseを入れてください。

addconf02.jpg

会社案内のページを見ると会社データの部分に登録した内容が反映することで来ました。
フォーム項目を追加したい場合でも、configを追加してテンプレートに列を追加するだけなので修正も簡単に出来ると思います。

複数ページ分作る場合は?

AddConfigは、一つの $config['AddConfig']['form'][] グループごとにアコーディオン化される仕組みになっているので、比較的コンパクトに収まるようになっています。
※旧管理画面も1.3からアコーディオン対応しました。
addconf03.jpg

とは言っても、やっぱりページ分けたいよ? って意見が当然あると思うので、ページを分ける方法を紹介します。

入力画面を複数に分ける方法

1.2でも元々ちょっと修正を加えるだけですぐ出来るものだったのですが、1.3対応にする際にデフォルトで使えるようにしました。
configにあるform内容を1階層下げて['ページのURL']の下に追加するだけで簡単に分けることができます。

既存の設定をキー名グループに追加するだけ
//デフォルトのURL /admin/add_config/add_configs/formに入る
$config['AddConfig']['name'] = '共通タイトル'; //h1タイトル
$config['AddConfig']['form'][] = [ // 項目内容

//トップページ用のURL /admin/add_config/add_configs/form/topに入る
$config['AddConfig']['top']['name'] = 'トップページ'; //h1タイトル
$config['AddConfig']['top']['form'][] = [ // 項目内容

//会社案内用のURL /admin/add_config/add_configs/form/aboutに入る
$config['AddConfig']['about']['name'] = '会社案内'; //h1タイトル
$config['AddConfig']['about']['form'][] = [ // 項目1の内容
$config['AddConfig']['about']['form'][] = [ // 項目2の内容
同じ場所に複数追加すればアコーディオングループになります

登録されるデータ自体は共通になります

別画面の登録した内容でも、データ自体はページに紐づけている仕組みではありません。
単純に【name】みたいなみたいな簡単な同名項目を作ってしまうと別ページで上書きされてしまいうので注意してください。
なのでフォーム項目のキー名はページ名のprefix推奨です。 例 about_キー名

登録されたデータの確認

/admin/add_config/add_configs/ から登録内容を確認できます。
※システム管理者のみ編集・削除も出来るようにしています。
addconf04.jpg

追加したページへのリンク

動的に追加する仕組みは今のところありません。お気に入りに追加してしまうのが一番早いと思います。

メニューとして修正したい場合は以下の部分を修正すると追加できます。

  • 旧管理画面
    → View/elements/addd_config.phpのliタグ部分
  • 新管理画面(admin-third)
    → Event/AddConfigControllerEventListener.phpの$contents['AddConfig']['menus']の部分

それぞれ'action'=>'form'の行をコピーして、追加してリンク部分の名称を変更して、URL部分は/form/aboutなら以下のように変更すればOKです。

'action'=>'form'  'action'=>'form','about'  

最後に

今回記事では単純なフォームパーツしか使用しませんでしたが、アップロードなど色々なフォームパーツがconfig設定だけで作成できます。1.3ではマークダウンとカラーピッカーも追加しました。インストール時に【表示内容】&【設定ソース】の確認が出来ますので、触ればすぐ使えると思います。
色々お試しください。

https://github.com/BigFly3/AddConfig

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

Laravelの通知ライブラリでslackボタンアクション機能を実装

Laravel Advent Calendar 2019 - Qiita の 9 日目 の記事です。

Laravelの通知ライブラリを使ってボタン付きのslack通知を実装した記事を
下書きのままにしていたので書き上げることにしました..!

Laravelの通知ライブラリ

laravel5.8以降のslack通知機能に変化

通知クラスが外部ライブラリに変わりました!
5.8以降のslack通知(公式)

composerを使って入れないと使えなくなりました。

composer require laravel/slack-notification-channel

ちなみにv5.8以前はguzzleを入れれば使える形でした..。
5.8以前のslack通知(公式)

(実はv5.7.16以降から通知クラスは外部ライブラリ化されてますが、ドキュメントは5.8以降しか)

Nexmoの通知クラスと一緒にframwork内から削除されています。

gitの差分

slack通知のボタンアクション機能とは?

公式のgifだと以下のような表示。
ボタンが付いていて押された後、元のメッセージを変えて1人しか押せなかったり、誰が押したか表示したり色んな機能が考えられそうです..!
Example_6.gif

slack公式のボタン機能

今までのslack通知クラスでボタン機能使えなかったの?

そうなんです!
当然、slack側のAPIには用意されていたのでguzzleで直接叩いたり、Custom Channelsを使って実装することは出来ましたが、Laravelが用意してくれている通知クラスでは使えませんでした。

環境

Laravelの開発環境をdockerで作るなら、@ucan-labさんのLaravelの開発環境をDockerを使って構築するがおすすめです。常に更新されてる!

slack側の設定は以下を参考に
Slack APIを使用してメッセージを送信する

slackのボタンアクション通知の仕組みはこちらを参考にしました!
slackで単純なボタン付きメッセージを送る

通信ライブラリを使って実装

さて、通信ライブラリを使って実装してみましょう。
slack-notification-channel

通知したいチャンネルにIncoming Webhooksの設定をしてwebhookを取得します

Incoming Webhooksについてはこの記事を参考にしました。
SlackのIncoming Webhooksを使い倒す

slack通知機能をまず作ります。この時、.env に設定したwebhookをconfigに書いておき、routeNotificationForSlack メソッドで指定します。

SlackNotificationService.php
<?php
namespace App\Services;

use App\Notifications\SlackButtonMessage;
use App\Notifications\SlackSend;
use App\Notifications\SlackSendQuestion;
use Illuminate\Notifications\Notifiable;

class SlackNotificationService
{

    use Notifiable;

    /**
     * SlackチャンネルのWebhookURLを返す
     *
     * @return string
     */
    public function routeNotificationForSlack()
    {
        return config('services.slack.button');
    }


    /**
     * 送信メソッド
     * @param $message
     */
    public function send()
    {

        // 通知
        $this->notify(new SlackButtonMessage());

    }
}

上記の送信メソッドで呼ばれるslackボタン通知用のNotificationクラスを作成

SlackButtonMessage.php
<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;

/**
 * Slack通知クラス
 */
class SlackButtonMessage extends Notification
{
    use Queueable;
    private $sendMessage;
    private $title;
    /**
     * Create a new notification instance.
     *
     * @return void
     */
    public function __construct()
    {

    }
    /**
     * Get the notification's delivery channels.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return ['slack'];
    }

    /**
     * Slack通知処理
     *
     * @param  mixed  $notifiable
     * @return \Illuminate\Notifications\Messages\SlackMessage
     */
    public function toSlack($notifiable)
    {
        return (new SlackMessage)
            ->from('Test通知', ':face_vomiting:')
            ->content('これはテストです :face_vomiting:')
            ->attachment(function ($attachment) {
                $attachment->action('googleリンク','https://www.google.com/','primary');
            });
    }
}

出ました!
スクリーンショット 2019-12-09 5.24.12.png

そして積みました...。slack-notification-channelはボタン通知に対応していますが、ボタン押した際のアクションは単純なURLリンクしか設定できないようです!
SlackAttachment.php の所定の位置を見ると確かにそうなってます。

SlackAttachment.php
    /**
     * Add an action (button) under the attachment.
     *
     * @param  string  $title
     * @param  string  $url
     * @param  string  $style
     * @return $this
     */
    public function action($title, $url, $style = '')
    {
        $this->actions[] = [
            'type' => 'button',
            'text' => $title,
            'url' => $url,
            'style' => $style,
        ];

        return $this;
    }

gifのようにボタン押下後に、元のメッセージに色々アクションを実装したい場合は、無理ですね。
あくまでも通知クラスのライブラリということですね。

GuzzleでAPI実装

書いていきます!

SlackButtonService.php
<?php
namespace App\Services;

use GuzzleHttp\Client;

class SlackButtonService
{
    public function postMessage( $message ) {

        $client = new Client([
            'headers' => [ 'Content-Type' => 'application/json' ]
        ]);

        $response = $client->post( config( 'services.slack.button' ),
            ['body' => $message]
        );
        logger($response->getBody());
        $data = json_decode( $response->getBody()->getContents());

        return $data;
    }
}

送信メッセージ出す所

send.php
    private $slack;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct(
                                SlackButtonService $service)
    {
        $this->slack = $service;
    }

    public function send(){
        $message = "こんにちは!";

        $data = array(
            "text" => $message
        );

        $actions =
            [
                "id" => "1",
                "name" => "test",
                'type' => "button",
                'text' => "こんにちは!",
                "value" => "button_1",
            ];

        $data += [
            "attachments" =>
                [
                    [   "callback_id" => "test",
                        "fallback" => "More details...",
                        'actions' => [$actions],
                    ]
                ]
        ];

        $payload = json_encode($data);

        $res = $this->slack->postMessage($payload);
    }

こんな感じで出ますね!
スクリーンショット 2019-12-09 7.37.30.png

slackからのレスポンス処理を設定(Interactive MessagesでここのURLを設定しておくことでPOSTされます)

web.php
Route::group(['prefix' => 'slack'], function()
{
    Route::post('/api/response', 'ApiController@getSlackResponse');
});

slackからのPOSTに対してcsrfトークンチェックを外す

VerifyCsrfToken.php
<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    /**
     * Indicates whether the XSRF-TOKEN cookie should be set on the response.
     *
     * @var bool
     */
    protected $addHttpCookie = true;

    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array
     */
    protected $except = [
        //
        '/slack/api/*',
    ];
}

レスポンス処理

ApiController.php
    public function getSlackResponse(Request $request){

        $temp = json_decode($request['payload'],true);
        $temp['original_message']['text']=$temp['user']['name'] . 'が押しました!';
        return response($temp['original_message']);
    }

こんな感じで通知が来た後に...

スクリーンショット 2019-12-09 7.37.30.png

ボタンを押すと...
スクリーンショット 2019-12-09 7.37.49.png

みたいに元のメッセージを変更したり出来ます!

slackを使っている会社では、slack連携による業務改善やエンゲージメントを高めるような施策が今後も増えてくると思います!
この記事が少しでも実装の助けになればと思います。

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

【PHP】変数のスコープのお話

関数の内部から、グローバルスコープの変数へアクセスすることはできない。

まずは、このコードをご覧ください。

$a = 0;
function hoge () {
    $a++; // Undefined variable: a
}

hoge();
echo $a; // 0

期待した動作としては、関数の中で変数$aの値をインクリメントして、1増えた値を出力してくれるものでした。
どうして期待とは異なった動作をしたのでしょうか。

それは、関数の中と外で変数のスコープが異なることに原因があります。

通常、関数の外側で定義された変数はグローバルスコープとなり、関数の内部で定義された変数は、ローカルスコープとなります。
(ちなみに、PHPにブロックスコープはありません)

// グローバルスコープ
$a = 0; 
function hoge () {
    // ローカルスコープ
    $a++;
}
//グローバルスコープ
hoge();
echo $a;

しかし、特に他の言語を学んだことのある方なら、グローバルスコープとローカルスコープの関係について、このようなイメージをお持ちではないでしょうか?

image.png

グローバルスコープからローカルスコープへのアクセスは禁止されているけれど、ローカルスコープはグローバルスコープの一部なので、グローバル変数も使用可能、といった感じです。

しかし、実際にはこんなだいたいこんなイメージだと思ってください。

image.png

そもそも、お互いに離れているので、通常はアクセスすることができないといった感じです。

通常は、引数として渡してやるのが一般的です。

$a = 0; 
function hoge ($a) {
    $a++;
}

hoge();
echo $a; // 0

エラーはでなくなりましたが、以前出力される値は0のままです。
これは、関数に変数を渡すときは、変数そのものを受け渡しているのではなく、変数の中身をコピーして渡している(値渡し)からです。

変数そのものを渡す(参照渡し)ときには、&をつけてあげます。

$a = 0; 
function hoge (&$a) {
    $a++; // グローバルスコープの変数と同じもの
}

hoge();
echo $a; // 1

また、あまり使われませんが変数の前にglobalと宣言することで、引数として渡さなくても関数の内部からグローバル変数へアクセスすることが可能です。

$a = 0; 
function hoge () {
    global $a; // 明示的にグローバルな変数にアクセスする
    $a++; 
}

hoge();
echo $a; // 1

話は少々横道にそれますが、PHPにはスーパーグローバル変数というものがあります。
スーパーとついているだけあって、この変数はグローバルスコープだろうがローカルスコープだろうが、どんな場所でもアクセスすることが可能です。

$a = 0; 
function hoge () {
    // これはグローバルスコープに現在定義されているすべての変数への参照を含む連想配列
    $GLOBALS['a']++;

    // これらもグローバススコープの一種
    $_GET['address'];
    $_POST['address'];
}

hoge();
echo $a; // 1

無名関数の内部から外の変数を使う

無名関数の内部で、親のスコープから変数を引き継ぐ場合には、useを使います。

<?php

$array = ['APPLE', 'LEMON', 'BANANA'];
$case_insensitive = true;

$red_fruits = array_filter($array, function($value) use($case_insensitive) {
    if ($case_insensitive) {
        $value = mb_strtolower($value);
    }
    return $value === 'apple';
});

print_r($red_fruits);
// Array ( [0] => APPLE )

これは、先程の例のように、グローバルスコープの変数にアクセスする場合とは、違うことに注意しましょう。

<?php
$array = ['APPLE', 'LEMON', 'BANANA'];
$case_insensitive = true;

function func($array) {
    $case_insensitive = false;

    $red_fruits = array_filter($array, function($value) use($case_insensitive) {
        if ($case_insensitive) {
            $value = mb_strtolower($value);
        }
        return $value === 'apple'
    });

    print_r($red_fruits);
}
func($array);
// Array ( )

無名関数を関数の中に閉じ込めてあげると、先程とはことなり、そのスコープ内の変数を取り込んでいることがわかります。
グローバルスコープの変数は使われておりません。

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

Phalconで実装する際に躓いたことのetc

jpgについて その3 でも書こうかと思っていたが、需要無いだろうし、ちょっと書くには準備が必要なので、今回は直近で触って齷齪したphpのフレームワーク「phalcon」のちょっとしたことを書いていこうかと思います。

Phalconでコード書いてて思ったことはまず記事の少なさと、記事があっても古くてリンク切れってのが散見されたこと。あと、各環境でのDIとかの書き方によって、そのまま適用するのが難しいってこともあった・・・
cakeとかに比べてベンチマークで20倍の速度とかあるらしいけど、こうまでしてPHPを使う必要ってあるんかなーってのが正直な感想です_(:3」∠)_

※この記事の執筆者の環境は PHP7.2 Phalcon 3.4.3

Nginx + Phalcon

最初らへんルーティングはされるのにパラメータが取れねぇ(;・∀・)
ってなってました。
ちゃんとNginx側のconfに

        location / {
            try_files $uri $uri/ @rewrite;
        }

        location @rewrite {
            rewrite ^/(.*)$ /index.php?_url=/$1;
        }

みたいな感じで「_url=xxxx」なrewriteを挟んであげる必要がありました。

ルーティングに記載してないアクセスは404にしたい

設定してないURLにアクセスが来た際には問答無用で404を表示したいなーってことで、ControllerBaseあたりに書くのかと思ってたら、DIのdispatcherで握りつぶす感じでした。
diを登録しているであろう service.php あたりで

use Phalcon\Dispatcher;
use Phalcon\Mvc\Dispatcher as MvcDispatcher;
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;
use Phalcon\Mvc\Dispatcher\Exception as DispatchException;

$di->setShared('dispatcher', function () {
        // Create an EventsManager
        $eventsManager = new EventsManager();

        // Attach a listener
        $eventsManager->attach(
            'dispatch:beforeException',
            function (Event $event, $dispatcher, Exception $exception) {
                // Handle 404 exceptions
                if ($exception instanceof DispatchException) {
                    $dispatcher->forward(
                        [
                            'controller' => 'index',
                            'action'     => 'notFound',
                        ]
                    );

                    return false;
                }

                // Alternative way, controller or action doesn't exist
                switch ($exception->getCode()) {
                    case Dispatcher::EXCEPTION_HANDLER_NOT_FOUND:
                    case Dispatcher::EXCEPTION_ACTION_NOT_FOUND:
                        $dispatcher->forward(
                            [
                                'controller' => 'index',
                                'action'     => 'notFound',
                            ]
                        );

                        return false;
                }
            }
        );
        $dispatcher = new MvcDispatcher();
        // Bind the EventsManager to the dispatcher
        $dispatcher->setEventsManager($eventsManager);

        return $dispatcher;
    }
);

とし、あとは IndexController に notFoundAction で 404 を表示するページを設置する感じ。

composer を使いたい

その他のPHPのパッケージを併せて使いたいので、composerを利用する。
phalcon dev-tool でphalconのモジュールを作成すると PSR-4 には則らない形でディレクトリが掘られるが、そこの管理とcomposerの管理は切り離して考えて良いみたい。
通常通り、composer.jsonとcomposer.lockをgit管理下に置き、phalcon側のloaderを読み出しているindex.phpあたりに

    /**
     * Include Autoloader
     */
    include APP_PATH . '/config/loader.php';
    include BASE_PATH . '/vendor/autoload.php';

な感じで phalcon側のloaderと並列して配置します。

formの使い勝手

フロント側のvolt templateと併せてphalconのデフォルトのform機能が用意されており、単純なformとして利用する分には、必要なformのelementの用意もvalidationも記載できるので十分利用できそうではある。
が、Ajax通信やモーダル表示、validationなどをフロントにやらせようとjava scriptをごりごり書き始めると、classやidの管理が分離されてしまうため、とても取り扱いづらいものになってしまった。

フロントでjsをごりごりしたい場合には、フレームワークを入れつつ単純なPHP側からの値渡しぐらいの用途に留めるのが良さそう。

Controllerをネストさせたい

少々ソースコードが大きくなってくるとサブディレクトリを掘って、その中にControllerを配置したいこともあるかと思います。
(元の設計時点で分けるならphalconのmodule使えやってことで良いと思いますが、整理のためにサブディレクトリを切りたい、そんな要望です。)

どうやらphalconでControllerをサブディレクトリ下に置くにはnamespaceをきるしか方法が無い模様。(それ以外の方法があるなら教えてえろい人)
moduleで切り分ける場合にもがっつりnamespaceきるし、そういう思想なのかもしれない。

簡単な例として、ユーザーのパスワードをリセットするControllerをネストさせてみたいとします。
ディレクトリの構造としてはこんな感じ(例だからねw)

controllers/ControllerBase
controllers/UserController
controllers/user/PasswordController
views/user/create.volt
views/user/password/reset.volt

ControllerBaseやUserControllerはそのまま利用できますが、userディレクトリにネストしたPasswordControllerは以下のような形で記述。
(namespaceだから自分が分かりやすいようにきってね)

namespace User\Controllers;

class PasswordController extends \ControllerBase
{
    public function resetAction()
    {
        // 処理
    }
}

その上でphalconのloaderにnamespaceの情報をロードするよう追記。
($config->application->controllersDir の部分は自分の環境用ので置換してね)

$loader->registerNamespaces(
    [
        'User\Controllers' => $config->application->controllersDir.'user/'
    ]
);

で、最後にrouterに設定したnamespaceの情報を含めてmountしまふ。

use Phalcon\Cli\Router\Route;
use Phalcon\Mvc\Router;
use Phalcon\Mvc\Router\Group as RouterGroup;

$user_password = new RouterGroup([
    'namespace' => 'User\Controllers',
    'controller' => 'password'
]);
$user_password->setPrefix('/user/password');
$user_password->addGet('/reset', ['action' => 'reset']);
$router->mount($user_password);

まぁ、なんて面倒くさい(;・∀・)
追加で、views/user/password 配下の reset.volt を自動でactionのviewとして関連付けるには、diのvolt周りを設定しているところに

$di->setShared('view', function () {
    $config = $this->getConfig();

    $view = new View();
    $view->setDI($this);
    $view->setViewsDir([
        $config->application->viewsDir,
        $config->application->viewsDir.'user/'
    ]);

    $view->registerEngines([
        '.volt' => function ($view) {
            $config = $this->getConfig();

            $volt = new VoltEngine($view, $this);

            $volt->setOptions([
                'compiledPath' => $config->cache->cacheDir,
                'compiledSeparator' => '_',
                'compileAlways' => $config->cache->compileAlways
            ]);

            return $volt;
        },
        '.phtml' => PhpEngine::class

    ]);

    return $view;
});

な感じで setViewsDir に userディレクトリ配下を追加してあげる必要がある模様。


なんか他にも結構引っかかったところがあった気がするけど、とりあえずこんなもんで・・・
phalconと戦っていて、最終的に行き着くのは、公式ドキュメントでも記事でもなく、Cのソースコードなのだわ\(^o^)/

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

昔を思い出しながらLaravel、FuelPHP、Codeigniterを比較してみた。

こんにちは!葛山です。
今回会社でアドベントカレンダーをやろうということで僕も慣れないブログを書いてみることにしました。
業務でコードに触れることは少なくなったのですが、最近Laravelを勉強したので昔使っていたFuelPHPやCodeigniterとの比較記事を書こうと思います。

はじめに

パーソンリンクは2011年に僕が立ち上げた会社なのですが、少人数だったので僕がPMを担当しながらプログラマーも兼務しているという状況でした。
最初はCodeigniter、2012年からはFuelPHPをメインに使っていました。

なぜPHPなのか

僕がPMの時にPHPを採用していた理由は「人が集めやすい」からです。
開発案件は急に始まるので、その際一番苦戦するのが人員の確保です。
当時はうちのメンバーの人数も少なく、外部の方と一緒に即興でチームを組むこともありましたので(開発では割とそれがスタンダードなのかも)その際一番募集をかけやすかったのがPHPでした。

PHPはWEBアプリ開発において人気のプログラミング言語なのですが、フレームワークの種類が多すぎて何を使うべきかわかりませんよね。
そこで今回は、数あるフレームワークの中で僕が推しているLaravel、FuelPHP、Codeigniterについて比較と解説をしていきたいと思います。

学習コスト

僕の体感だと
Laravel > FuelPHP > CodeIgniter
です。

CodeIgniterを初めて使った時、それまでZend FrameworkやSymfonyなどを使っていましたが、学習コストの少なさに驚きました。
CodeIgniterで街コンサイトを作ったのですが、PHPさえ知っていればほぼ学習せずに作り上げるところまではいけました。ピュアなPHP+αってイメージです。
FuelPHPとCodeIgniterの差は機能の多さで、フレームワークとしての本質はあまり変わらない印象です。

まだ勉強を始めたばかりなのですが、LaravelはRuby on Railsみたいないイメージでコーディング規約もちゃんとあるので大規模開発にも適応できそうです。
開発環境の構築も簡単でドキュメントも読みやすかったです。
AWSやSNSとの連携等、機能がたくさんあるので使いこなそうと思うと学習コストはかかりますが、これさえあれば大体のwebアプリ開発は完結できそうです。

処理速度

検証していないので後日上げますw
他の記事を読んだところこの中ではCodeIgniterが一番高速らしいですね!

柔軟性と拡張性

CodeIgniter > FuelPHP > Laravel
です。

CodeIgniterが一番コーディング規約などの制限が緩く、自分で書きやすいように書いていくことができます(僕は簡単なシステムを作るときはModelはほぼ書かずに既存ORMのみで書きました)
フレームワークの拡張もしやすくcoreをオーバーライドすることで簡単に拡張できます。
個人的には2人以上が関わるプロジェクトにはオススメしませんが、今はもしかすると良い管理方法があるかもしれません。。

FuelPHPもCodeIgniterの開発者の一部の人たちが作っていることもあり、似ている部分は多いです。
規約が緩いのは変わらないですが、Oilコマンドでクラスの自動生成ができるので、そこである程度型が決まってきます。
ディレクトリ構成もわかりやすく、coreの拡張がし易いです。
下記のようなディレクトリ構成になっていますが、appとcoreの配下がほぼ同じ構成になっているので拡張したいcoreがあればそれをappでオーバーライドして記述すれば良いのです。

ルートディレクトリ/
 ├fuel/
 │  ├app/
 │  ├core/
 │  └packages/
 │
 └public/
    ├assets/
    └index.php

また、CodeIgniterではデフォルトでドキュメントルートにシステムファイルが置かれてしまっていましたが、publicディレクトリに切り離されたのでセキュリティ的にも良くなりました。

Laravelはプラグインが充実していることとフレームワーク自体の完成度が高いため、coreなどを柔軟に拡張することは前提としていないのですが、ベースとなるController等どのプロジェクトでも拡張するようなクラスはデフォルトでapp配下に置いてあるのでそれを拡張していく感じです。
それ以外を拡張する場合は少し面倒で、ただcoreを継承するだけではなくService Providerを書かなければいけなかったり工程が多いので柔軟性は他の二つに比べると一番低いとしました。
ただし、前述したようにcomposerで簡単に高機能なプラグインが導入できるので拡張性は高いとも言えます。

まとめ

ここまで僕の所感を元にPHPのフレームワーク比較をしてきましたが、現在のイチ推しはLaravelです。
理由はフレームワークの性能以上に、メンテナンスの頻度やプラグインの豊富さなどがビジネス要件を多く満たせるかの鍵になってくるので、今トレンドのものを選択するのが一番良いからです。
これからPHPを学ぶ人はLaravelから始めてみましょう!

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