interprism's blog

インタープリズム株式会社の開発者ブログです。

Laravel9で環境構築1(環境構築)

Laravel9の環境構築

こんにちは、だーさんです。
Laravel(5.6)とMySQLを使用してWebサイトを作成する仕事をしています。

個人的に環境構築というものが苦手なのですが、 Laravel9を使ってwebアプリを開発する環境構築が簡単にできたのでご紹介です。 若干長いので何話かに分けてご紹介していきますね。

環境

  • M1mac

Dockerをインストール

  • Docker version 20.10.14

Laravel9.0をインストール

curl -s "https://laravel.build/example-app?php=81" | bash

example-appの名前でdirectoryが作成されます。この名前は自由に変更可能です。

開発環境を構築

cd example-app
./vendor/bin/sail up

作成されたdirectoryに移動してLaravel Sailを起動します。
./vendor/bin/sail upを起動するとDockerのアプリケーションコンテナがローカルマシンに構築されます。
初回はDockerのイメージファイルなどがダウンロードされますが、2回目以降はコンテナを立ち上げるだけなので高速に起動できるようになります。

起動

http://localhost

ブラウザを開いて上記URLにアクセスします。
exampleとしてあらかじめ用意されているresources/views/welcome.blade.phpで作成された静的ページが表示されます。
この時点でアプリケーション開発が行える状態になりました!

welcome.blade.phpで作成された静的ページ

一旦sailを終了

ターミナルで「control + cキー」で終了

sailコマンドの省略化

./vendor/bin/sailコマンドを網回呼び出すのは面倒なのでエイリアスを設定します。

vim ~/.zshrc

シェルの設定ファイルを開きます。

alias sail='[ -f sail ] && bash sail || bash vendor/bin/sail'

上記を入力します。

source ~/.zshrc

シェルファイルを反映させます。

次回からsailコマンドは以下のように実行でいます。

sail up

続きはこちら

以上になります。とっても簡単なのでちょっとしたwebサイトを作りたい人は試してみてくださいね(>v<)

Laravel9で環境構築2(sailコマンド) - interprism's blog

Laravel9で環境構築3(DockerFileでカスタマイズ) - interprism's blog

Laravel9で環境構築4(複数人で開発する): 作成予定です。

参考文献

  • プロフェッショナルWebプログラミング Laravel〈最新Laravel 9対応〉

【Laravel】select句にsubQueryを構築したい

【Laravel】select句にsubQueryを構築したい

初めまして、だーさんです。
Laravel(PHP)とMySQLを使用してWebサイトを作成する仕事をしています。

Laravelのクエリビルダーを使用してMySQLのコードを書いていてちょっと行き詰まったので記事を書いてみようかなと思いました。 探してもうまい方法が見つからなかったので誰かの助けになればいいなと思います。 (スマートなやり方を知っている方がいたらぜひ教えてほしいですっっ)

環境

mac
PHP: 7.3.33
Laravel Framework: 5.7.28
MySQL: 5.7.36

やりたいこと

Laravelのクエリビルダでselect句内にサブクエリを書きたい。 MySQLでは以下のような書き方になります。

SELECT
    t.team_id,
    (SELECT
         COUNT(DISTINCT a.member_id) AS member_count
     FROM
         table_member AS a
     WHERE
         a.team_id = t.team_id
     GROUP BY a.team_id
    ) AS member_count
FROM
    table_team AS t

上記は業務で使用しているコードを基に必要な部分だけ抜き出したものです。
従って、サブクエリにしなくても目的を達成できるコードになってしまいましたが、趣旨がずれてしまうので今回はご了承ください。(^^;)

抽出したいデータはチーム(team)に対するそれぞれのメンバー数(member)です。
今回行き詰まったポイントとなったのは、基盤となるFROMtable_teamのIDカラムとサブクエリ内のtable_memberのIDカラムを結合するところでした。

a.team_id = t.team_id

成功したコード

結論として以下のコードで目的のコードを構築することになりました。

DB::table('table_team AS t')
    ->select([
        't.team_id'
        , DB::raw("(
            SELECT
                COUNT(DISTINCT a.member_id) AS member_count
            FROM
                table_member AS a
            WHERE
                a.team_id = t.team_id
            GROUP BY a.team_id) AS member_count")
    ])
    ->get();

select()DB::raw()で生のMySQL文を埋め込むやり方です。

残念なポイントはサブクエリ部分が生のMySQL文になってしまったことです。 DB::raw()を使用すると部分的に生のMySQL文を埋め込むことができます。 これを使用し、select句内に無理やり埋め込む形で構築しました。

本当はサブクエリ内もクエリビルダーで構築したかったのですが、うまくいかず上記の形で落ち着きました。 以下でここに辿り着くまでの失敗例を載せて行きます。 お時間ある方はお付き合いください。m(_ _)m

失敗例1(構文エラー)

select句内にMySQL文入れ込むので初めは単純に以下のように無名関数で入れてみました。

DB::table('table_team AS t')
    ->select([
        't.team_id'
        , function (Builder $query) {
            $query->select([
                    DB::raw('COUNT(DISTINCT a.member_id) AS member_count')
                ])
                ->from('table_member AS a')
                ->where('a.team_id', '=', 't.team_id')
                ->groupBy(['a.team_id'])
                ->get();
        }
    ])
    ->get();

こちらは単純に構文エラー。 select()はこのような関数の形を許容していないらしいです。

stripos() expects parameter 1 to be string, object given

失敗例2(カラムが見つからない)

調べてみるとLaravelにselectSub()というサブクエリ用の関数が存在したので使用してみました。

DB::table('table_team AS t')
    ->select([
        't.team_id'
    ])
    ->selectSub(
        function (Builder $query) {
            $query->select([
                    DB::raw('COUNT(DISTINCT a.member_id) AS member_count')
                ])
                ->from('table_member AS a')
                ->where('a.team_id', '=', 't.team_id')
                ->groupBy(['a.team_id'])
                ->get();
        }, 'member_count')
    ->get();

こちらは取得データが何やらおかしい。。。むむ。
構築されるMySQL文を見てみると以下の形に、

SELECT
    t.team_id,
    (SELECT
         COUNT(DISTINCT a.member_id) AS member_count
     FROM
         table_member AS a
     WHERE
         a.team_id = 't.team_id'  #<--- 文字列になっている!!
     GROUP BY a.team_id
    ) AS member_count
FROM
    table_team AS t

where()の使用の仕方が間違っており、't.team_id'という文字列にマッチするものだけ検索してしまっていました。 カラム名を使用できるように以下に書き換え!

DB::table('table_team AS t')
    ->select([
        't.team_id'
    ])
    ->selectSub(
        function (Builder $query) {
            $query->select([
                    DB::raw('COUNT(DISTINCT a.member_id) AS member_count')
                ])
                ->from('table_member AS a')
                ->where('a.team_id', '=', DB::raw('t.team_id'))
                ->groupBy(['a.team_id'])
                ->get();
        }, 'member_count')
    ->get();

こちらはちゃんとカラム名で指定できました! ただ、「t.team_idというカラムは存在しません。」というエラーが出ました。 どうやら、このselectSub()はこれのみで完結するSQL文しか書けない模様。

まとめ

select句内にサブクエリを構築するのに、selectSub()を使用するとjoin句などで使用しているカラムを指定することができません。 従ってDB::raw()で生のMySQL文をselect()に埋め込む方法で構築することになります。

クエリビルダーで完結できないかと結構調べたのですが、結論としては「なんとも惜しい」形での収束となってしまいました。 私自身がそうなのですが、できそうでできないと諦めきれなくなってしまうので、 この記事を見て「できないんだー」と思えるところに落ち着けるタイミングになればいいなと思います。 (理想はこういうSQLにLaravelが対応してくれることですが笑)

最後までお付き合いいただきありがとうございました。 また、どこかで会えますように。

インタープリズムのページ

Push APIでプッシュ通知を実装

プッシュ通知はサーバーからアプリに対して通知を送信できるもので、スマートフォンのアプリ等で日常的に触れていると思います。今回はWebアプリでプッシュ通知を利用する為のPushAPIについて調べたことを紹介したいと思います。

PushAPI

Webアプリがプッシュ通知を利用できるようにPushAPIが用意されています。用意されたAPIを利用することで簡単にプッシュ通知が利用できるようになっています。 developer.mozilla.org

Service Worker

PushAPIの説明に入る前にService Workerについて軽く説明します。

developer.mozilla.org Service WorkerはWebアプリとは別スレッドで動作し、Webアプリ、ブラウザー、ネットワークの間にいるプロキシサーバーのような存在です。WebアプリごとにこのService Workerが登録でき、ここでリソースのアクセスの間に入りキャッシュを返したり、今回説明するプッシュ通知を受け取るような仕事を行います。詳しくはリンク先の説明をお読み下さい。

プッシュ通知の流れ

w3c.github.io

https://w3c.github.io/push-api/images/sequence_diagram.png

W3Cのページのシーケンス図がわかりやすいのでこれを借ります。 上図の

  • web page
  • service worker
  • user agent

はそれぞれのWebアプリのユーザー側にあるものでuser agentはブラウザにあたります。そして

  • push service
  • application server

はそれぞれサーバーです。

シーケンス図でメッセージを送るまでの流れは以下となりますね。

  1. まずService Workerの登録を行う(register())
  2. プッシュ通知の購読処理を行う(subscribe())
  3. push serviceに購読処理が行われpush先のendpoint等が返ってくるので、それをapplication serverに渡す
  4. application serverはpush serviceに、push serviceはブラウザに、ブラウザはservice workerにとpush messageが伝搬する
  5. service workerでpush eventに対する処理を登録しておき、そこから例えば画面への通知などを実行する

push service

push serviceとは各ブラウザのベンダーが用意しているサーバーでこれがブラウザとapplication serverとを仲介しています。ブラウザによって利用しているものは異なります。

f:id:interprism:20211114231628p:plain
chrome,edgeによるpush serviceの違い

動かしてみる

以下のサイトを一通り読むと使い方がわかります。

developers.google.com

ここではどんな雰囲気のコードが必要かをかいつまんで見ていきたいと思います。 まずはserviceWorkerを用意します。 本来install, activate, fetchなど色々なeventについて記述しますが今回は説明に必要なpush eventについてのみ書いています。 これを書くことでプッシュ通知が着た際に中のcallbackが動いてくれます。下のコードではログを表示した後showNotifidationでデスクトップ通知を出しています。

self.addEventListener("push", function (event) {
    console.log("[Service Worker] Push Received.");
    console.log(`[Service Worker] Push had this data: "${event.data.text()}"`);

    const title = "Push Recieved!!!";
    const options = {
        body: event.data.text()
    };
    event.waitUntil(self.registration.showNotification(title, options));
});

次にシーケンス図にあったようにservice workerを登録しなければなりません。 以下のようなコードをページを読み込む際に実行されるようにしてservice workerを登録します。 やっていることの流れは以下です。

  • まずservice workerの登録をする
  • 返ってきたregistrationのpushManagerからプッシュ通知の購読情報を取得
  • もし購読していない場合には同じくpushManagerのsubscribeで購読処理を行う
if ("serviceWorker" in navigator) {
    window.addEventListener("load", async () => {
        // service workerの登録
        const registration = await navigator.serviceWorker
            .register("/sw.js")
            .catch(console.error);
        console.log("sw registered.");

        // push通知のsubscriptrionの入手
        let subscription = await registration.pushManager.getSubscription();
        console.log(JSON.stringify(subscription));

        // subscribeされていない場合にsubscribeを実行
        if (!subscription) {
            subscription = await registration.pushManager.subscribe({
                userVisibleOnly: true,
                applicationServerKey: "<key>"
            });
            console.log(`subscribed! endpoint: ${subscription.endpoint}`);
        }
    });
}

subscribeでは引数としてapplicationServerKeyというものを渡しています。push serviceがapplication serverを認証するのに使用する公開鍵でapplication serverが対応し、事前に生成しておく必要があります。またchromeではこれが必須となっているようです。

developer.mozilla.org

準備OK

Webアプリ側は上記の手順が出来れば準備はできました。application serverがpush serviceにメッセージを送信することでservice workerのpush eventまで伝搬してくれます。簡単!!

chromeのdeveloper toolではプッシュ通知のテストが出来ます。画像のPushボタンを押すことでservice workerのpush eventが発火してくれます。

f:id:interprism:20211115001144p:plain
devtoolによるpush通知のテスト

どういう状態なら受け取れるのか?

プッシュ通知が受け取れることはわかりました。この通知はWebアプリが開いているときのみ受け取れるのでしょうか?実際に試してみました。 上で紹介したcode labの中でプッシュ通知送信を行えるWebアプリがリンクされていたのでそれを用いました。こちらで生成した鍵でsubscribeし、endpointをコピペしてアプリからプッシュ通知を送信します。 web-push-codelab.glitch.me

結果的に一度購読すればブラウザが起動している間はプッシュ通知を受け付けてくれます。当該Webアプリが起動していなくてもプッシュ通知が受け付けてくれますし、Service Worker自体がStopしていてもpush eventによってService Workerが起動してプッシュ通知を受け付けます。またブラウザを閉じ、再度起動した際にもService Workerの登録やプッシュ通知のsubscribe自体は有効なようでプッシュ通知を受け付けてくれました。

まとめ

Push APIを使ってプッシュ通知を実装するのは見てきたとおり結構簡単に出来、しかもWebアプリを起動していなくてもブラウザが動いていれば通知が受けられる強力なものでした。送信するapplication server側については触れていませんのでそちらで何か大変なことがあるかもしれませんが、fcmなどのサービスを利用すればそちらもケースによっては結構簡単に扱えるのではないかと思います。 Progressive Web Appという概念がありますがプッシュ通知ができることは大事な部分の一つだと思うのでぜひ理解しておきたかったもので今回勉強出来て良かったです。

プログラミングコンテストの感想【2021年入社 Y.S】

はじめに [ プログラミングコンテストとは? ]

まず、どのようなことを行ったのか簡単にご説明します。

問題ファイル

  • 1行につき以下の形式で問題が書かれている
    • [行インデックス,列インデックス]+[行インデックス,列インデックス]=
      • 行インデックスは0始まり
      • 列インデックスは0始まり
  • 空行は存在しません。
  • 行インデックスと列インデックスが範囲外の地点を指す可能性は考慮しなくて良い

入力ファイルと問題ファイルと結果の例

入力ファイルが3x3

1,5,3
2,8,1
9,2,6

で問題ファイルが、

[0,1]+[2,2]=
[2,0]+[1,1]=

の場合は、結果は以下のようになります。

11
17

パターン

入力ファイルと問題ファイルの組み合わせで以下の3つのパターンが存在します。

  • SMALLパターン
    • 入力ファイル: 100行100列。各要素の値は1〜100の整数
    • 問題ファイル: 100個の演算式
  • MEDIUMパターン
    • 入力ファイル: 10,000行10,000列。各要素の値は1〜10,000の整数
    • 問題ファイル: 100個の演算式
  • LARGEパターン
    • 入力ファイル: 10,000行10,000列。各要素の値は1〜10,000の整数
    • 問題ファイル: 10,000個の演算式

順位付け

以下のルールでランキングを競います

  • プログラムの開始から終了までの実行時間が短いものが上位
  • コンパイルエラーは失格扱い
  • 実行中の例外は失格扱い
  • 計算結果が誤っている場合は失格扱い

2021年入社 Y.S

ずっと楽しみにしていたプログラミングコンテストが終わりました。結果も自分の中では満足のいく結果が出せたかなと思います。
このプログラミングコンテストでは、アルゴリズムを多少工夫することで、現実的には解決することのできない問題を解くことができるということを再度実感しました。
大学院の頃に研究で、計算時間を意識する問題を扱っていたので、(モノとしては全然違いましたが)とても懐かし気持ちと楽しい気持ちで終えることができました。
プログラミングコンテスト自体は一週間の期間が用意されており、その期間はずっとプログラミングコンテストのことについて考えていました。
普段の研修課題では同期の人たちと相談し合いながら、分からない部分を教え合いながら進めていましたが、このプログラミングコンテスト期間は同期の方とはほとんど相談をすることなく取り組んでいました(コンテストなので、できるだけ自分の力でという気持ちがありました)。

プログラミングコンテストの感想【2021年入社 S.O】

はじめに [ プログラミングコンテストとは? ]

まず、どのようなことを行ったのか簡単にご説明します。

問題ファイル

  • 1行につき以下の形式で問題が書かれている
    • [行インデックス,列インデックス]+[行インデックス,列インデックス]=
      • 行インデックスは0始まり
      • 列インデックスは0始まり
  • 空行は存在しません。
  • 行インデックスと列インデックスが範囲外の地点を指す可能性は考慮しなくて良い

入力ファイルと問題ファイルと結果の例

入力ファイルが3x3

1,5,3
2,8,1
9,2,6

で問題ファイルが、

[0,1]+[2,2]=
[2,0]+[1,1]=

の場合は、結果は以下のようになります。

11
17

パターン

入力ファイルと問題ファイルの組み合わせで以下の3つのパターンが存在します。

  • SMALLパターン
    • 入力ファイル: 100行100列。各要素の値は1〜100の整数
    • 問題ファイル: 100個の演算式
  • MEDIUMパターン
    • 入力ファイル: 10,000行10,000列。各要素の値は1〜10,000の整数
    • 問題ファイル: 100個の演算式
  • LARGEパターン
    • 入力ファイル: 10,000行10,000列。各要素の値は1〜10,000の整数
    • 問題ファイル: 10,000個の演算式

順位付け

以下のルールでランキングを競います

  • プログラムの開始から終了までの実行時間が短いものが上位
  • コンパイルエラーは失格扱い
  • 実行中の例外は失格扱い
  • 計算結果が誤っている場合は失格扱い

感想 [ 2021年入社 S.O ]

普段の研修では可読性を意識したコードを書くことがほとんどで、計算量や実行時間についてはほとんど意識せず(場合によっては計算量を犠牲にしてでも可読性を優先する)に書くため、研修で計算量や実行時間について考える機会が得られて嬉しく思いました。
また、ファイルの入出力について詳しくわかっていないままコンテストを迎えたため、この辺りの調査に多くの時間を取られてしまいましたが、実際には自身のタイムにさほど影響せず徒労に終わってしまった点が印象に残っています。
自身のタイムを更新するためには、ほとんど意味のない小さな改良に思える点でも軽視してはいけないことも学べました。
コードを改良していく際には、ローカル環境と提出先の環境で時間を計測して改良が有効かを判断していましたが、どちらの環境も実行のたびにタイムがぶれるため、判断が難しかったと記憶しています。
また、最終日にはサーバーが不調となり、実行時間が10倍くらいに増えてしまう事態になったのは残念だったと思います。

Redmine プラグイン開発テンプレート

github.com

要点

  1. redmine-plugin-docker ダウンロード
  2. Redmine 初期化

    make init git_tag=4.0.9

  3. プラグインの雛形生成

    make plug-new name=my_plugin

補足

必要なもの

(bash,) make, git, docker, docker-compose, docker-sync

構成と使い方

README に最低限書きました.

make コマンドをいくつか紹介します.

  • init {git_branch | git_tag | git_commit}=:

    初期化. Redmine ソースを git-clone, Docker イメージのビルド, その他インストール処理を含む. Redmine のバージョンを git-commitish で指定.

  • reinit:

    再初期化. 最初の初期化の後, 手動で変更した, Redmine のバージョンや, Docker 関連の構成を反映.

  • up:

    コンテナ起動, docker-sync コンテナ起動を含む.

  • down:

    コンテナ削除, docker-sync コンテナ削除を含む.

  • bundle-exec cmd=:

    Redmine コンテナで bundle exec コマンド実行. コマンドを cmd= で指定, 例えば, make bundle-exec cmd="runner hello.rb"bundle exec runner hello.rb を実行.

  • rake task=:

    Redmine コンテナで Rake タスク実行. タスクを task= で指定, 例えば, make rake task=log:clearbundle exec rake log:clear を実行.

  • rails cmd=:

    Redmine コンテナで Rails コマンド実行. コマンドを cmd= で指定.

  • rails-c:

    RedmineRails コンソールに接続.

  • rails-s:

    Redmine を開発用サーバーで起動.

  • plug-new name=:

    Redmine プラグイン雛形作成. プラグイン名を name= で指定.

  • plug-new-model plug= model=:

    Redmine プラグイン Model の雛形作成. プラグイン名を plug=, Model 名を model= で指定.

  • plug-new-ctrl plug= ctrl=:

    Redmine プラグイン Controller の雛形作成. プラグイン名を plug=, Controller 名を ctrl= で指定.

お気持ち

業務仕様のプラグイン作成など Redmine に関わる機会が多いのですが

開発用に長く使う環境もあれば, 検証用に細かくバージョンやソースコードを調整して使い捨てる環境が欲しいときがあります.

Docker で環境構築の手間を減らし, make でコマンド文字数を減らすことで, 快適に作業に臨めるようになりました.

ちょっと Redmine 改造したいとか, プラグイン試したいときにご利用ください.

画像読み込みでCLSの発生を抑制する

CLSとは?

Googleが2021年から取り入れる予定のウェブページUXの指標、コアウェブバイタルの一つ。「Cumulative Layout Shift」の略でユーザーの意図しないレイアウトのズレがスコアで表され、これが低い程良いです。

f:id:interprism:20210512003646g:plain
CLS例

画像の読み込み

昨今のWebページでは画像がたくさん使われます。その画像の読み込みでもCLSに気を配る必要があります。
一つずつ状況を変えて見ていきます。

サイズに関する情報を何も指定しない

<img src="https://hogehoge.co.jp/hugahuga.jpg" alt="">

画像にwidth, heightの情報を一切指定しないと、画像読み込み前には一切スペースが確保されていないところにいきなり画像が描画され、ズレが生じてしまいます。

width, heightを固定する

<img src="https://hogehoge.co.jp/hugahuga.jpg" width="200px" height="300px" alt="">

width, heightに固定値を入れて指定をすると、事前にそのサイズが必要だとブラウザがわかるので予め領域を確保しておきます。そして画像が読み込まれた段階でそこに画像が表示されるのでズレは発生しません。
ただし固定値で指定を行うと一つ問題が起きます。レスポンシブ対応が出来ません。

cssでwidth=%指定, height=auto

<style>
  img {
    width: 100%;
    height: auto;
  }
</style>

<img src="https://hogehoge.co.jp/hugahuga.jpg" alt="">

例えばwidth=100%, height=autoとしてみます。こうするとwidthは画面幅等から決まり、heightがautoとなっているので画像が読み込まれた後に画像の比率に合わせて高さが調整されるのでレスポンシブに画像が正しく表示出来ます。
ただし高さが比率に合わせて調整出来るということはこのままでは画像は読み込まれるまで高さが決まりません。よって読み込み後にズレが生じてしまいます。

比率を教える

<style>
  img {
    width: 100%;
    height: auto;
    /* aspect-ratio: attr(width) / attr(height);  <- chrome, firefoxはこれが無くてもtag側で設定したwidth, heightの比率が勝手に使用される*/
  }
</style>

<img src="https://hogehoge.co.jp/hugahuga.jpg" width="200px" height="300px" alt="">

aspect-ratioというcssのプロパティがchrome等一部のブラウザで現在使用出来ます。これは名前どおり比率を指定出来るものです。

Chrome 88のここに注目!CSSのaspect-ratioプロパティでアスペクト比が簡単に、別窓はデフォルトでnoopenerに | コリス

css側で固定値で比率を指定することも出来ますが、chrome等ではimgタグのwidth, heightを指定するとaspect-ratioがその比率で設定されます。

  • cssでwidth=100%, height=autoとすると
    • widthの長さが予め分かる
  • imgタグのwidth, heightを設定すると
    • 設定値自体はcssで設定したwidth, heightに置き換えられる
    • その上で画像の比率がimgタグに設定した値で決まる

から上記の2つのwidth, heightを設定すると widthの長さと画像の比率が決まるので高さが計算出来、ズレが発生しなくなります。さらにwidth, heightはcssの設定が効くのでレスポンシブ対応出来ています。

以下は実際ズレが発生する状況、しない状況をそれぞれ再現するコードです。

※PC版のchromeでのみ確認。それ以外だと意図しない動きかもしれません

まとめ

以上からズレを発生させない為にはブラウザに予めどんなサイズの画像がくるのか教えることが必要なのがわかりました。レスポンシブに、さらにズレも発生しないようにするには以下の対応を行いましょう!

  • imgタグでwidth, heightを指定
  • cssでwidth:%指定, height:auto を指定

参考

www.smashingmagazine.com

PAGE TOP