QUARTETCOM TECH BLOG https://tech.quartetcom.co.jp/ カルテットコミュニケーションズ開発部ブログ Mon, 16 Mar 2026 09:12:44 +0900 Symfonyの.envファイルをうっかり git プッシュから守る方法 | QUARTETCOM TECH BLOG Symfony https://tech.quartetcom.co.jp/2026/03/16/protect-symfony-credential/ https://tech.quartetcom.co.jp/2026/03/16/protect-symfony-credential/ <p>Symfony でアプリ開発をしていると <code class="language-plaintext highlighter-rouge">.env.local</code> にクレデンシャルをハードコードすることがありますが、センシティブな情報をプレーンテキストで扱うことに若干の怖さを感じています。かんたんにリネームもファイルコピーもできますし GitHub リポジトリに間違えてプッシュしちゃったら怖いですよね。</p> <p>そんなうっかり操作のヒヤリハットを何らかの仕組みで防止できないか調べてみました。</p> <h2 id="はじめに">はじめに</h2> <p>この記事の「クレデンシャル」とは Symfony アプリ内でクローズドに利用する接続情報やトークンのことを示しています。パラメータを経由してサービスクラスに注入しサードパーティベンダーの認証などに使用するイメージです。</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .env.local</span> <span class="s">APY_KEY=1234abcd</span> <span class="s">SLACK_TOKEN=xoxb-000000000000-EXAMPLE-TOKEN</span> </code></pre></div></div> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/services.yaml</span> <span class="na">parameters</span><span class="pi">:</span> <span class="na">apiKey</span><span class="pi">:</span> <span class="s1">'</span><span class="s">%env(APY_KEY)%'</span> <span class="na">slackToken</span><span class="pi">:</span> <span class="s1">'</span><span class="s">%env(SLACK_TOKEN)%'</span> <span class="na">App\Command\SomeService</span><span class="pi">:</span> <span class="na">arguments</span><span class="pi">:</span> <span class="na">$apiKey</span><span class="pi">:</span> <span class="s1">'</span><span class="s">%apiKey%'</span> <span class="na">$slackToken</span><span class="pi">:</span> <span class="s1">'</span><span class="s">%slackToken%'</span> </code></pre></div></div> <p>クレデンシャルは API キー / OAuth 認証トークン / デベロッパートークンなどさまざまな種類があり、ハードコード先も <code class="language-plaintext highlighter-rouge">.env.*</code> だけでなく <code class="language-plaintext highlighter-rouge">.key</code> <code class="language-plaintext highlighter-rouge">.yaml</code> などバリエーションがあります。広範囲に散らばっているクレデンシャルを守るには、いくつかの方法を組み合わせて多層防御するのがベターと言えそうです。</p> <h2 id="envlocal-を-git-で追跡しないgitignore">.env.local を git で追跡しない:.gitignore</h2> <p><a href="https://git-scm.com/docs/gitignore">gitignore</a> は基本的でシンプルな方法です。<code class="language-plaintext highlighter-rouge">.gitignore</code> に書いたパターンにマッチすると git で追跡されず、ステージもコミットもされません。<a href="https://symfony.com/doc/current/setup/symfony_cli.html">Symfony CLI</a> でプロジェクトを作成するとプロジェクトに最適な <code class="language-plaintext highlighter-rouge">.gitignore</code> が作成されます。</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .gitignore</span> <span class="c1">###&gt; symfony/framework-bundle ###</span> <span class="s">/.env.local</span> <span class="s">/.env.local.php</span> <span class="s">/.env.*.local</span> <span class="nn">...</span> </code></pre></div></div> <p>けれどもプロジェクトは常に Symfony CLI で構築されるとは限りません。別プロジェクトからソースコードを流用したり、ゼロベースからディレクトリを構成することもあるでしょう。</p> <p><a href="https://github.com/github/gitignore">https://github.com/github/gitignore</a> のようなテンプレートを使って <code class="language-plaintext highlighter-rouge">.gitignore</code> を作成した場合 <code class="language-plaintext highlighter-rouge">.env.*</code> はその対象に含まれていないかもしれません。記事執筆時点の Symfony 最新バージョンは <a href="https://symfony.com/blog/symfony-8-0-6-released">v8.0.6</a> ですがテンプレートは v4 以降のリリースに追随していないように見えます。</p> <p><a href="https://github.com/github/gitignore/blob/b4105e73e493bb7a20b5d7ea35efd5780ca44938/Symfony.gitignore">gitignore /Symfony.gitignore</a></p> <p>グローバル設定の <code class="language-plaintext highlighter-rouge">~/.config/git/ignore</code> に <code class="language-plaintext highlighter-rouge">.env.*</code> と書いてあるから安全!という思い込みもうっかりミスを誘発します。新しいデバイスをセットアップした時、このディレクトリの同期作業をうっかり忘れてしまうかもしれないからです。</p> <p><code class="language-plaintext highlighter-rouge">.gitignore</code> による追跡防止は <strong>開発の基本であって過信しないこと</strong> というのが私の考えです。</p> <h2 id="envlocal-をコミットしない1password-local-env-files">.env.local をコミットしない:1Password Local .env files</h2> <p>1Password は有償のパスワードマネージャーです。<br /> 2025/10 に「Local .env files」というベータ版機能がリリースされました。この機能は <code class="language-plaintext highlighter-rouge">.env</code> ファイルを安全な場所に退避し、プロセスからの読み取りに開発者の許可を必要とします。</p> <p><a href="https://1password.com/blog/1password-environments-env-files-public-beta">Introducing new .env file support in 1Password environments | 1Password</a></p> <p>1Password アプリで「環境」を追加しプロジェクトを判別する任意の名前を付与します。</p> <p><img src="/assets/img/2026-03-protect-symfony-credential/image-7.png" width="800" /></p> <p>追加した環境に <code class="language-plaintext highlighter-rouge">.env.local</code> をインポートします。</p> <p><img src="/assets/img/2026-03-protect-symfony-credential/image-1.png" width="800" /></p> <p>プロジェクトの既存の <code class="language-plaintext highlighter-rouge">.env.local</code> を削除し、替わりに 1Password のファイルを保存(マウント)します。</p> <p><img src="/assets/img/2026-03-protect-symfony-credential/image-3.png" width="800" /> <img src="/assets/img/2026-03-protect-symfony-credential/image-6.png" width="800" /></p> <p>マウントした <code class="language-plaintext highlighter-rouge">.env.local</code> はファイルの実体ではなく UNIX の <a href="https://ja.wikipedia.org/wiki/%E5%90%8D%E5%89%8D%E4%BB%98%E3%81%8D%E3%83%91%E3%82%A4%E3%83%97">名前付きパイプ</a> です。その内容は 1Password によって保護されているため、プロセスが読み取ろうとすると認証ダイアログが表示されます。許可すると 1Password アプリのロックがかかるまでアクセスが可能になります。</p> <p><img src="/assets/img/2026-03-protect-symfony-credential/image-4.png" width="800" /> <img src="/assets/img/2026-03-protect-symfony-credential/image-5.png" width="800" /></p> <p>この仕組みは「うっかりプッシュ」の観点で良い副作用をもたらします。<strong>ファイルの実体ではないため git でコミットできない</strong> のです。<code class="language-plaintext highlighter-rouge">.gitignore</code> にうっかり書き忘れても、<code class="language-plaintext highlighter-rouge">.env.locall</code> とファイル名をタイポしても、コミット時点でブロックされます。</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add .env.local <span class="o">&gt;</span> error: .env.local: can only add regular files, symbolic links or git-directories <span class="o">&gt;</span> fatal: updating files failed </code></pre></div></div> <p>そして <code class="language-plaintext highlighter-rouge">.env.local</code> を一元管理できるメリットもあります。自宅とオフィスで PC を使い分けているような場合、どのデバイスでも 1Password からマウントしておけば「自宅の PC で変更した内容をオフィスの PC に反映し忘れた」なんてことがなくなります。</p> <p>ただし落とし穴もあります。一時的なバックアップのために <code class="language-plaintext highlighter-rouge">cp .env.local .env.local.bak</code> を実行すると <code class="language-plaintext highlighter-rouge">.env.local.bak</code> はファイルの実体として作成され、うっかりコミットが可能になります。</p> <h2 id="envlocal-を作らないsymfonys-secrets-management-system">.env.local を作らない:Symfony’s secrets management system</h2> <p>そもそもクレデンシャルを <code class="language-plaintext highlighter-rouge">.env.local</code> にハードコードしない手段についても検討の余地があります。Symfony 公式で紹介されている「Symfony’s secrets management system」はそのひとつです。</p> <p><a href="https://symfony.com/doc/current/configuration/secrets.html">How to Keep Sensitive Information Secret | Symfony docs</a></p> <p>この方法は PHP 7.2+ でバンドルされているモジュール <a href="https://www.php.net/manual/ja/book.sodium.php">sodium</a> を使ってクレデンシャルを暗号化し <code class="language-plaintext highlighter-rouge">config/secrets/*</code> に格納します。</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># モジュールが含まれているか調べる</span> php <span class="nt">-m</span> | <span class="nb">grep </span>sodium <span class="o">&gt;</span> sodium </code></pre></div></div> <p>クレデンシャルの登録や削除はコマンドで行います。</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 暗号化・復号化のためのキーペアを作成(初回のみ)</span> php bin/console secrets:generate-keys <span class="c"># クレデンシャルを登録</span> php bin/console secrets:set API_KEY </code></pre></div></div> <p>プロダクション環境用にはプレフィクス <code class="language-plaintext highlighter-rouge">APP_RUNTIME_ENV=prod</code> を付与してコマンドを実行します。</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 暗号化・復号化のためのキーペアを作成(初回のみ)</span> <span class="nv">APP_RUNTIME_ENV</span><span class="o">=</span>prod php bin/console secrets:generate-keys <span class="c"># クレデンシャルを登録</span> <span class="nv">APP_RUNTIME_ENV</span><span class="o">=</span>prod php bin/console secrets:set API_KEY </code></pre></div></div> <p>一部のクレデンシャルをローカル開発用にカスタマイズする時は <code class="language-plaintext highlighter-rouge">--local</code> オプションを使用します。</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bin/console secrets:set API_KEY <span class="nt">--local</span> </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">dev</code> <code class="language-plaintext highlighter-rouge">prod</code> <code class="language-plaintext highlighter-rouge">local</code> それぞれの環境用に生成されたファイル群は以下のように格納されます。</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>config/secrets ├── dev │ ├── dev.API_KEY.b58958.php <span class="c"># 個別のクレデンシャル</span> │ ├── dev.decrypt.private.php <span class="c"># 復号化の秘密鍵</span> │ ├── dev.encrypt.public.php <span class="c"># 暗号化の公開鍵</span> │ ├── dev.list.php <span class="c"># クレデンシャルのリスト</span> └── prod ├── prod.API_KEY.b58958.php <span class="c"># 個別のクレデンシャル</span> ├── prod.decrypt.private.php <span class="c"># 復号化の秘密鍵</span> ├── prod.encrypt.public.php <span class="c"># 暗号化の公開鍵</span> └── prod.list.php <span class="c"># クレデンシャルのリスト</span> .env.dev.local <span class="c"># ローカル開発用にカスタマイズした値</span> </code></pre></div></div> <p>これらのファイルのうち、プロダクション環境の秘密鍵 <code class="language-plaintext highlighter-rouge">prod.decrypt.private.php</code> だけはコミットしてはいけません。秘密鍵を直接サーバーに置くか、サーバーの環境変数 <code class="language-plaintext highlighter-rouge">SYMFONY_DECRYPTION_SECRET</code> に登録します。</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># (A) サーバーに直接配置</span> config/secrets/prod/prod.decrypt.private.php <span class="c"># (B) 環境変数に登録</span> <span class="nb">export </span><span class="nv">SYMFONY_DECRYPTION_SECRET</span><span class="o">={</span>Base64 エンコードした鍵<span class="o">}</span> </code></pre></div></div> <p>登録したクレデンシャルはコマンドでリストアップできます。</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># --reveal オプションで値の表示ができる</span> php bin/console secrets:list <span class="nt">--reveal</span> <span class="o">&gt;</span> <span class="nt">------------</span> <span class="nt">----------</span> <span class="nt">-------------</span> <span class="o">&gt;</span> Secret Value Local Value <span class="o">&gt;</span> <span class="nt">------------</span> <span class="nt">----------</span> <span class="nt">-------------</span> <span class="o">&gt;</span> API_KEY <span class="s2">"123abc"</span> <span class="s2">"111aaa"</span> <span class="o">&gt;</span> DB_PASSWORD <span class="s2">"123def"</span> <span class="s2">"222bbb"</span> <span class="o">&gt;</span> <span class="nt">------------</span> <span class="nt">----------</span> <span class="nt">-------------</span> </code></pre></div></div> <p>この仕組みは <code class="language-plaintext highlighter-rouge">.env.*</code> を使用せず <strong>「うっかりプッシュ」対象を「プッシュしても良い情報」</strong> に置き換えます。ただしローカル開発用にカスタマイズした <code class="language-plaintext highlighter-rouge">.env.dev.local</code> は依然としてうっかりコミットの可能性が残ります。</p> <h2 id="プッシュをブロックgithub-push-protection">プッシュをブロック:GitHub Push protection</h2> <p>クレデンシャルの格納先は <code class="language-plaintext highlighter-rouge">.env.*</code> とは限りません。<code class="language-plaintext highlighter-rouge">.key</code> <code class="language-plaintext highlighter-rouge">.yaml</code> にハードコードした内容を無害なファイルパスとして <code class="language-plaintext highlighter-rouge">.env.local</code> に渡すことがあります。GitHub の Push protection はそのようなケースで有効です。</p> <p><a href="https://docs.github.com/ja/code-security/how-tos/secure-your-secrets/prevent-future-leaks/push-protection-for-users">ユーザーのプッシュ保護 | GitHub ドキュメント</a></p> <p>Organization に属さない個人アカウント配下でパブリックなリポジトリを作成したところ、デフォルトで Push protection が有効になっていました。</p> <p><img src="/assets/img/2026-03-protect-symfony-credential/image-8.png" width="800" /></p> <p>試しに AWS アカウントなどダミーのクレデンシャルを書いたファイルをプッシュすると Push protection によってデータ転送がブロックされました。</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/keys/sample.key</span> <span class="s">DUMMY_GITHUB_TOKEN=ghp_dummy12341234nopqrstuvwxyz....</span> <span class="s">DUMMY_AWS_KEY=AKIAQA2IFJE3MPUDUMMY</span> <span class="s">DUMMY_AWS_SECRET=gsDummyyGtzLXigRMVcDHeuS7Neg6vd....</span> </code></pre></div></div> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 主要な部分のみ抜粋</span> git push origin main Enumerating objects: 8, <span class="k">done</span><span class="nb">.</span> Counting objects: 100% <span class="o">(</span>8/8<span class="o">)</span>, <span class="k">done</span><span class="nb">.</span> remote: Resolving deltas: 100% <span class="o">(</span>2/2<span class="o">)</span>, completed with 2 <span class="nb">local </span>objects. remote: error: GH013: Repository rule violations found <span class="k">for </span>refs/heads/main. remote: remote: - GITHUB PUSH PROTECTION remote: ————————————————————————————————————————— remote: Resolve the following violations before pushing again remote: - Push cannot contain secrets remote: remote: —— Amazon AWS Access Key ID —————————————————————————— remote: locations: remote: - commit: 0a000111222... remote: path: config/keys/sample.key:4 remote: remote: —— Amazon AWS Secret Access Key —————————————————————— remote: locations: remote: - commit: 0a000111222... remote: path: config/keys/sample.key:5 remote: <span class="o">!</span> <span class="o">[</span>remote rejected] main -&gt; main <span class="o">(</span>push declined due to repository rule violations<span class="o">)</span> <span class="c"># ブロックしてくれた ☺️</span> </code></pre></div></div> <p>ダミーの RSA 秘密鍵も試したところ、こちらは Push protection が反応せずプッシュが完了しました。</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/keys/sample.key(実在しないクレデンシャル)</span> <span class="s">-----BEGIN RSA PRIVATE KEY-----</span> <span class="s">MIIEowIBAAKCAQEArandomrandomrandomrandomrandomrandomrandomrandom</span> <span class="s">....</span> <span class="s">-----END RSA PRIVATE KEY-----</span> </code></pre></div></div> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git push origin main <span class="o">&gt;</span> Total 0 <span class="o">(</span>delta 0<span class="o">)</span>, reused 0 <span class="o">(</span>delta 0<span class="o">)</span>, pack-reused 0 <span class="o">(</span>from 0<span class="o">)</span> <span class="o">&gt;</span> + 1234c7c...4567ccc main -&gt; main <span class="c"># プッシュ完了...🙄</span> </code></pre></div></div> <p>GitHub ドキュメントの <a href="https://docs.github.com/ja/code-security/reference/secret-security/supported-secret-scanning-patterns#supported-secrets">Supported secrets</a> を読むと Push protection は単純なパターンマッチングのようなチェックではないことが分かります。</p> <ul> <li>トークン提供元のプロバイダ(AWS / Google など)によるチェックと、非プロバイダ(Generic)に分かれる</li> <li>プロバイダにクレデンシャルを送信してトークンの有効性をチェックすることがある</li> <li>トークンやプロバイダによってチェック内容の網羅度合いが異なる</li> <li>個人アカウントのパブリックリポジトリに適用されるチェックと GitHub Team / Enterprise の GitHub Secret Protection で適用されるチェックに違いがある</li> </ul> <p>偽物のクレデンシャルとほんの数回のプッシュ確認では、ブロック可能なパターンを帰納的に洗い出すことはできませんでした。</p> <p>しかしながら、あらゆるクレデンシャルを対象に GitHub がスキャンしてくれる機能はとても強力です。ブロックが成功した AWS アクセスキーについてはファイルから削除するだけではダメで、コミットログも完全に消し去らないとプッシュができませんでした。</p> <p>GitHub Push protection は <code class="language-plaintext highlighter-rouge">.env.local</code> の <strong>外に漏れ出てしまった情報の監視役</strong> として有効に機能してくれそうです。</p> <h2 id="まとめ">まとめ</h2> <p>Symfony や GitHub など、それぞれがうっかりプッシュの防止策を提供してくれています。どれもすべてのクレデンシャルやユースケースを網羅できる仕組みではありませんが、組み合わせれば高い効果が期待できそうです。</p> <p>これさえやっておけば大丈夫!と慢心せず、ローカルでもクラウドでも電子的に保存したデータは常に漏洩リスクを孕んでいることを念頭に開発していきたいと思っています。</p> Mon, 16 Mar 2026 00:00:00 +0900 Claude Code 入門: 導入から基本操作・コスト管理まで | QUARTETCOM TECH BLOG ClaudeCode https://tech.quartetcom.co.jp/2026/02/24/claude-code-guide/ https://tech.quartetcom.co.jp/2026/02/24/claude-code-guide/ <p>開発部では最近、開発効率を向上させる施策の一環として、Anthropic 社の CLI 型 AI エージェント「<strong>Claude Code</strong>」を導入しました。</p> <p>本記事は、これから Claude Code を触ってみたいエンジニアや、AI ツールによる開発効率化に関心のある方に向けた実践ガイドです。</p> <p>今回は、「基本概念」から、コストを抑える運用まで共有します。</p> <h2 id="1-まず押さえたい基本概念-セッションとコンテキスト">1. まず押さえたい基本概念: 「セッション」と「コンテキスト」</h2> <p>Claude Code を使いこなす上で、以下の 2 つの概念を理解しておくとスムーズです。</p> <h3 id="セッション作業のまとまり">セッション(作業のまとまり)</h3> <p>Claude Code は、起動してから終了するまでの一連のやり取りを「セッション」として管理します。</p> <p>最も重要な仕様は、<strong>セッションは「起動ディレクトリ(フォルダ)」に紐付いて管理される</strong>という点です。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[プロジェクトAのディレクトリ] ⇔ [セッションA(Aの記憶)] | (ディレクトリ移動) ↓ [プロジェクトBのディレクトリ] ⇔ [セッションB(Bの記憶)] </code></pre></div></div> <p>つまり、ディレクトリを移動して<strong>新しく <code class="language-plaintext highlighter-rouge">claude</code> を起動すれば、別のセッションとして扱われます</strong>(前のディレクトリのコンテキストを引きずりません)。</p> <p>履歴は保存されているため、完全に消失するわけではありません。</p> <p>後述の <code class="language-plaintext highlighter-rouge">/resume</code> コマンドを使えば、過去のセッション一覧から再開したい作業を選んで元の状態を復元できます。</p> <h3 id="コンテキストai-の短期記憶">コンテキスト(AI の短期記憶)</h3> <p>AI が一度に覚えられる情報量のことです。 会話が長くなると、この「コンテキスト」が溜まっていきます。</p> <p>コンテキストが溜まると、AI が処理すべき情報量が増えるため、結果として利用料(コスト)も上がってしまいます。</p> <h2 id="2-初期設定-init-と-claudemd">2. 初期設定: <code class="language-plaintext highlighter-rouge">/init</code> と <code class="language-plaintext highlighter-rouge">CLAUDE.md</code></h2> <h3 id="プロジェクトの記憶を作る-init">プロジェクトの「記憶」を作る <code class="language-plaintext highlighter-rouge">/init</code></h3> <p>初めて Claude Code を使うディレクトリでは、最初に以下のコマンドを実行します。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; /init </code></pre></div></div> <p>これを実行すると <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> というファイルが生成されます。 ここにはビルドコマンドやコーディング規約などを記述します。 Claude Code は起動時にこのファイルを読み込み、プロジェクトのルールを理解します。</p> <p><strong>チーム開発での運用ルール</strong></p> <ul> <li><strong>共有設定(Git 管理する)</strong>: プロジェクトルートの <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> は<strong>Git にコミット</strong>します。これにより、チーム全員が同じルール(ビルド手順や規約)を共有でき、通常は他のメンバーが <code class="language-plaintext highlighter-rouge">/init</code> を実行する必要がなくなります(既に設定ファイルが存在するため)。</li> </ul> <h3 id="要件定義ファイルなどの扱い">要件定義ファイルなどの扱い</h3> <p><code class="language-plaintext highlighter-rouge">CLAUDE.md</code> はあくまで「ルールブック」です。機能ごとの仕様書などは <code class="language-plaintext highlighter-rouge">docs/spec.md</code> などのファイルに分け、指示出しの際に <code class="language-plaintext highlighter-rouge">@docs/spec.md</code> のように参照させます。</p> <h2 id="3-うまく伝わらない時は-コンテキスト管理のコツ">3. うまく伝わらない時は? コンテキスト管理のコツ</h2> <p>使っていると、「最近、指示の意図を汲んでくれなくなったな……」と感じることがあります。 これはコンテキストが溜まりすぎて、AI が混乱しているサインかもしれません。</p> <h3 id="基本は-clear-でリセット">基本は <code class="language-plaintext highlighter-rouge">/clear</code> でリセット</h3> <p>タスクの区切りや、話が噛み合わなくなったら、以下のコマンドを打ちます。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; /clear </code></pre></div></div> <p>これにより、これまでのコンテキストが消去され、まっさらな状態で再スタートできます。 <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> の内容は保持されます。</p> <h3 id="コンテキストを残したいなら-compact">コンテキストを残したいなら <code class="language-plaintext highlighter-rouge">/compact</code></h3> <p>「今の文脈は維持したいけど、少し記憶を整理したい」という場合は <code class="language-plaintext highlighter-rouge">/compact</code> が有効です。</p> <p>これは<strong>会話の要約のみを残し、個別のやり取りの詳細な履歴を削除する</strong>ことで、コンテキストを圧縮します。</p> <h2 id="4-利用状況の確認とモデルの使い分け">4. 利用状況の確認とモデルの使い分け</h2> <p>Claude Code の利用状況や制限は、以下のコマンドで確認できます。 契約形態(サブスクリプションまたは API 課金)によって使い分けます。</p> <h3 id="利用状況の確認">利用状況の確認</h3> <ul> <li><strong>サブスクリプションプランの場合</strong>: <code class="language-plaintext highlighter-rouge">/usage</code> コマンド <ul> <li>Claude Pro などのサブスクリプションプランで利用している場合、ここから<strong>レートリミット(残り使用回数)</strong>やプランの使用状況を確認できます。</li> </ul> </li> <li><strong>API キーを利用(従量課金)の場合</strong>: <code class="language-plaintext highlighter-rouge">/cost</code> コマンド <ul> <li>組織で API キーを利用(従量課金)している場合、現在のセッション費用を確認できます。</li> </ul> </li> </ul> <h3 id="モデルの使い分け">モデルの使い分け</h3> <ul> <li><strong>Sonnet 系モデル</strong>: 日常的なコーディングや修正。速度とコストのバランス型です。</li> <li><strong>Opus 系モデル</strong>: 複雑な設計や難解なバグ調査。高精度で、深い推論を必要とする作業向けです。</li> </ul> <p>※モデル性能はアップデートにより変更される場合があります。</p> <h2 id="5-急な割り込み対応セッション管理">5. 急な割り込み対応(セッション管理)</h2> <p>開発中に緊急対応が入った場合も、セッション機能を使えば安全に中断・再開できます。</p> <ul> <li><strong>中断</strong>: 作業中のセッションに <code class="language-plaintext highlighter-rouge">/rename [タスク名]</code> で名前を付けて保存します。</li> <li><strong>再開</strong>: <code class="language-plaintext highlighter-rouge">/resume</code> で過去のセッションを選択し、元の状態(コンテキスト含む)から再開できます。</li> <li><strong>並行作業</strong>: Claude Code のセッションはディレクトリに紐付いています。そのため、<code class="language-plaintext highlighter-rouge">git worktree</code> でディレクトリを分け、セッションを分離して並行作業できます。片方で「バグ調査」をさせつつ、もう片方で「新機能実装」を進めるなど、互いのコンテキストを混ぜずに並行作業が行えます。</li> </ul> <h2 id="まとめ">まとめ</h2> <ul> <li><strong>基本</strong>: セッションはディレクトリに紐付くため、場所を変えて起動すれば記憶も変わる。</li> <li><strong>設定</strong>: チームルールはルートの <code class="language-plaintext highlighter-rouge">CLAUDE.md</code>(Git 管理)</li> <li><strong>維持</strong>: 話が噛み合わなくなったら <code class="language-plaintext highlighter-rouge">/clear</code>。コスト節約にもなる。</li> <li><strong>運用</strong>: モデルの使い分けと <code class="language-plaintext highlighter-rouge">/resume</code> で柔軟に対応。</li> </ul> <p>ぜひ、皆さんの開発フローにも取り入れてみてください。</p> Tue, 24 Feb 2026 00:00:00 +0900 GitHubのマージ(Merge Commit/Squash Merging/Rebase Merging)の違い | QUARTETCOM TECH BLOG https://tech.quartetcom.co.jp/2025/12/05/github-pull-requests-merge/ https://tech.quartetcom.co.jp/2025/12/05/github-pull-requests-merge/ <p>GitHub でプルリクエストをマージする時、マージボタンのプルダウンに「Squash and merge」「Rebase and merge」ってありますよね。これらの違いをよく知らなかったので手元で試してみました。</p> <p><a href="https://docs.github.com/ja/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges">プルリクエストのマージについて | GitHub ドキュメント</a></p> <p>動作確認用に使ったプルリクエストはタイトルを <code class="language-plaintext highlighter-rouge">PR-A</code> に、コミットメッセージを <code class="language-plaintext highlighter-rouge">A1 {コミット時刻}</code> と統一しています。</p> <p><img src="/assets/img/2025-12-05/pull-request.png" width="600" /></p> <h1 id="1-create-a-merge-commit">1) Create a merge commit</h1> <p>プルダウンを展開せずにマージボタンを押したデフォルトの動作です。</p> <p><img src="/assets/img/2025-12-05/merge-type-default.png" width="600" /></p> <p>同じブランチから切った複数ブランチをマージすると、時系列でコミットが積み上がります。</p> <p>メインブランチに見立てた <code class="language-plaintext highlighter-rouge">merge</code> にブランチ <code class="language-plaintext highlighter-rouge">merge-A</code> <code class="language-plaintext highlighter-rouge">merge-B</code> をマージすると次の図のようになります。</p> <p><img src="/assets/img/2025-12-05/merge-tree.png" width="700" alt="デフォルトマージの図" /></p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log merge-A <span class="o">&gt;</span> 44b8478 A2 21:25 <span class="o">&gt;</span> fba0ad2 A1 21:23 git log merge-B <span class="o">&gt;</span> 6bfb8c4 B2 21:26 <span class="o">&gt;</span> 74b8961 B1 21:24 git log merge <span class="o">&gt;</span> 881e805 Merge pull request <span class="c">#18 from ringtail003/merge-B</span> <span class="o">&gt;</span> d2bef64 Merge pull request <span class="c">#17 from ringtail003/merge-A</span> <span class="o">&gt;</span> 6bfb8c4 B2 21:26 &lt;<span class="o">==</span> プルリクエスト<span class="o">(</span>2<span class="o">)</span>のコミット <span class="o">&gt;</span> 44b8478 A2 21:25 &lt;<span class="o">==</span> プルリクエスト<span class="o">(</span>1<span class="o">)</span>のコミット <span class="o">&gt;</span> 74b8961 B1 21:24 &lt;<span class="o">==</span> プルリクエスト<span class="o">(</span>2<span class="o">)</span>のコミット <span class="o">&gt;</span> fba0ad2 A1 21:23 &lt;<span class="o">==</span> プルリクエスト<span class="o">(</span>1<span class="o">)</span>のコミット </code></pre></div></div> <p><a href="https://github.com/ringtail003/blog-github-pull-requests-merge/commits/merge/">GitHub でサンプルを見る</a><br /> <img src="/assets/img/2025-12-05/merge-commit.png" width="800" /></p> <h1 id="2-squash-and-merge">2) Squash and merge</h1> <p><img src="/assets/img/2025-12-05/merge-type-squash.png" width="600" /></p> <p>Squash はプルリクエストのコミット群をひとつにまとめた新しいコミットを作成します。マージ先のブランチでは、プルリクエストとコミットが 1 対 1 になります。</p> <p>メインブランチに見立てた <code class="language-plaintext highlighter-rouge">squash</code> にブランチ <code class="language-plaintext highlighter-rouge">squash-A</code> <code class="language-plaintext highlighter-rouge">squash-B</code> をマージすると次の図のようになります。</p> <p><img src="/assets/img/2025-12-05/squash-tree.png" width="700" alt="squashの図" /></p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log squash-A <span class="o">&gt;</span> 36001a5 A2 21:33 &lt;<span class="o">==</span> マージ元のコミット<span class="o">(</span>2個目<span class="o">)</span> <span class="o">&gt;</span> 709eb44 A1 21:30 &lt;<span class="o">==</span> マージ元のコミット<span class="o">(</span>1個目<span class="o">)</span> git log squash-B <span class="o">&gt;</span> 2d087cb B2 21:34 <span class="o">&gt;</span> aea1a38 B1 21:32 git log squash <span class="o">&gt;</span> 7d65cc7 PR-B <span class="o">(</span><span class="c">#20)</span> <span class="o">&gt;</span> 6d63afc PR-A <span class="o">(</span><span class="c">#19) &lt;== マージ先のコミット(1個にまとまる)</span> </code></pre></div></div> <p><a href="https://github.com/ringtail003/blog-github-pull-requests-merge/commits/squash/">GitHub でサンプルを見る</a><br /> <img src="/assets/img/2025-12-05/squash-commit.png" width="800" /></p> <h2 id="squash-and-merge-の-author">Squash and merge の Author</h2> <p>いくつかプルリクエストを作って試したところ、プルリクエストに一番最初にコミットした人がマージコミットの Author として採用されるようです。</p> <p><img src="/assets/img/2025-12-05/squash-author.png" width="600" /></p> <p>プルリクエストに 2 番目以降にコミットした人は「共作者」という扱いになります。</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log <span class="nt">--pretty</span><span class="o">=</span>full <span class="o">&gt;</span> commit 1234abc <span class="o">&gt;</span> Author: user1 &lt;[email protected]&gt; <span class="o">&gt;</span> Commit: GitHub &lt;[email protected]&gt; <span class="o">&gt;</span> PR-A <span class="o">(</span><span class="c">#100)</span> <span class="o">&gt;</span> <span class="o">&gt;</span> <span class="k">*</span> A1 <span class="o">&gt;</span> <span class="k">*</span> A2 <span class="o">&gt;</span> <span class="nt">---------</span> <span class="o">&gt;</span> Co-authored-by: user2 &lt;[email protected]&gt; &lt;<span class="o">==</span> 共作者 </code></pre></div></div> <p><a href="https://docs.github.com/ja/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/creating-a-commit-with-multiple-authors">複数の作者を持つコミットを作成する | GitHub ドキュメント</a></p> <h1 id="3-rebase-and-merge">3) Rebase and merge</h1> <p><img src="/assets/img/2025-12-05/merge-type-rebase.png" width="600" /></p> <p>Rebase はプルリクエストをマージ先ブランチでリベースしてからコミットを積みます。そのためコミットにそれぞれ新しいハッシュが付与されます。</p> <p>メインブランチに見立てた <code class="language-plaintext highlighter-rouge">rebase</code> にブランチ <code class="language-plaintext highlighter-rouge">rebase-A</code> <code class="language-plaintext highlighter-rouge">rebase-B</code> をマージすると次の図のようになります。</p> <p><img src="/assets/img/2025-12-05/rebase-tree.png" width="700" alt="rebaseの図" /></p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git log rebase-A <span class="o">&gt;</span> 7258210 A2 21:39 <span class="o">&gt;</span> 7fcedd0 A1 21:37 &lt;<span class="o">==</span> マージ元のコミット git log rebase-B <span class="o">&gt;</span> 6ecf78f B2 21:40 <span class="o">&gt;</span> 5bd574d B1 21:38 git log rebase <span class="o">&gt;</span> 3acd2d8 B2 21:40 <span class="o">&gt;</span> f4e83f7 B1 21:38 <span class="o">&gt;</span> e733a59 A2 21:39 <span class="o">&gt;</span> def1981 A1 21:37 &lt;<span class="o">==</span> マージ先のコミット<span class="o">(</span>ハッシュが変わる<span class="o">)</span> </code></pre></div></div> <p><a href="https://github.com/ringtail003/blog-github-pull-requests-merge/commits/rebase/">GitHub でサンプルを見る</a><br /> <img src="/assets/img/2025-12-05/rebase-commit.png" width="800" /></p> <h1 id="マージメッセージ">マージメッセージ</h1> <p>マージコミットのメッセージは Settings ページで変更できます。</p> <p><img src="/assets/img/2025-12-05/message-setting.png" width="700" /></p> <p>選択肢がたくさんあるので表にまとめました。動作確認用に使ったプルリクエストはタイトルを <code class="language-plaintext highlighter-rouge">PR-A {メッセージの種類}</code> と統一しています。</p> <table> <thead> <tr> <th>マージの動作</th> <th>メッセージの種類</th> <th>Commits ページの表示</th> </tr> </thead> <tbody> <tr> <td>Merge</td> <td><code class="language-plaintext highlighter-rouge">Default message</code></td> <td><img src="/assets/img/2025-12-05/message-merge-default.png" width="500" /></td> </tr> <tr> <td>Merge</td> <td><code class="language-plaintext highlighter-rouge">Pull request title</code></td> <td><img src="/assets/img/2025-12-05/message-merge-pr-title.png" width="500" /></td> </tr> <tr> <td>Merge</td> <td><code class="language-plaintext highlighter-rouge">Pull request title and description</code></td> <td><img src="/assets/img/2025-12-05/message-merge-pr-title-and-desc.png" width="500" /></td> </tr> <tr> <td>Squash</td> <td><code class="language-plaintext highlighter-rouge">Default message</code></td> <td><img src="/assets/img/2025-12-05/message-squash-default.png" width="500" /></td> </tr> <tr> <td>Squash</td> <td><code class="language-plaintext highlighter-rouge">Pull request title</code></td> <td><img src="/assets/img/2025-12-05/message-squash-pr-title.png" width="500" /></td> </tr> <tr> <td>Squash</td> <td><code class="language-plaintext highlighter-rouge">Pull request title and commit details</code></td> <td><img src="/assets/img/2025-12-05/message-squash-pr-title-and-detail.png" width="500" /></td> </tr> <tr> <td>Squash</td> <td><code class="language-plaintext highlighter-rouge">Pull request title and description</code></td> <td><img src="/assets/img/2025-12-05/message-squash-pr-title-and-desc.png" width="500" /></td> </tr> </tbody> </table> <p><a href="https://github.com/ringtail003/blog-github-pull-requests-merge/commits/message-merge">GitHub でサンプルを見る | Merge</a><br /> <a href="https://github.com/ringtail003/blog-github-pull-requests-merge/commits/message-squash">GitHub でサンプルを見る | Squash</a></p> <p>一見すると同じに見えるマージメッセージですが、マージコミットのボディ部分が異なります。</p> <h2 id="default-message">Default message</h2> <p><img src="/assets/img/2025-12-05/message-squash-default-2.png" width="700" /></p> <h2 id="pull-request-title">Pull request title</h2> <p><img src="/assets/img/2025-12-05/message-squash-pr-title-2.png" width="700" /></p> <h2 id="pull-request-title-and-commit-details">Pull request title and commit details</h2> <p><img src="/assets/img/2025-12-05/message-squash-pr-title-and-detail-2.png" width="700" /></p> <h2 id="pull-request-title-and-description">Pull request title and description</h2> <p><img src="/assets/img/2025-12-05/message-squash-pr-title-and-desc-2.png" width="700" /></p> <h1 id="考察">考察</h1> <p>マージについて調べようと思ったきっかけは、どのプルリクエストをどの順番でマージしたのかサマリを見たいと思ったことでした。</p> <p>この用途だと、プルリクエストごとにマージコミットが作成される「Squash and merge」が良さそうだということになります。ですが 1 コミットに集約されることで、本来のコミットごとの作業履歴が見られなくなる、単一のコミットでリバートできなくなる、などデメリットもあるようです。たとえば release ブランチにマージする時だけ「Squash and merge」を適用できたら嬉しいのですが記事執筆時点(2025/12)ではブランチごとのコントロールはできないようでした。</p> <p>せっせとキャプチャをとってまとめましたが結論はデフォルトのまま現状維持、いずれまた検討する時がきたらこのブログを見に来ます。</p> Fri, 05 Dec 2025 00:00:00 +0900 暗黙知が支える技術の本質 | QUARTETCOM TECH BLOG チーム https://tech.quartetcom.co.jp/2025/10/10/the-essence-of-technology/ https://tech.quartetcom.co.jp/2025/10/10/the-essence-of-technology/ <p>ソフトウェア開発の現場では、属人化を避けるために「なんでもドキュメント化しよう」という文化がよく見られます。<br /> ナレッジを共有し、誰がいなくなっても困らないチームを作るという考え方自体は、合理的で正しいものだと思います。</p> <p>しかし、「なんでもドキュメント化しよう」という文化は、捉え方次第では技術の習得という観点で思わぬ弊害を生むことがあります。</p> <h2 id="属人化とドキュメントのジレンマ">属人化とドキュメントのジレンマ</h2> <p>例えば、あるプロジェクトの環境構築の手順をまとめたドキュメントがあるとします。<br /> そのドキュメントの内容は、何のために、どこまで詳細に書くべきでしょうか。<br /> その線引きは、チームの方針やメンバーに求めるスキルレベルなどによって大きく異なります。</p> <p>手順を細かく書けば、誰でも手順通りに作業でき、効率的に環境構築を進められます。<br /> もし途中で問題が発生したとしても、そういったケースのトラブルシューティングの手順も記載しておけば問題ないでしょう。</p> <p>しかし、そのようなドキュメントに完全に頼って環境構築を終えたメンバーは、「自走して環境構築ができた」と言えるでしょうか。<br /> 多くの場合、それは「再現できた」にすぎず、「理解して環境構築できた」とは異なります。</p> <p>ドキュメントが過度に充実してマニュアルのようになると、「なぜそうするのか」を考える機会が失われます。<br /> チーム全体がそれを当然のように良しとし、盲目的にドキュメント化を推進していると、将来的にメンバーの技術習得が課題になったり、評価と自己認識のズレや、学習意欲の低下といった問題が生じるかもしれません。</p> <h2 id="技術の習得には思考の身体化が必要">技術の習得には、思考の身体化が必要</h2> <p>ドキュメントは形式知を伝えるための手段としてとても有効ですが、それだけでは技術を習得することはできません。</p> <p>技術を習得するということは、単に知識を得ることではありません。<br /> 知識や経験をうまく繋げ、状況に応じて適切に判断するための思考を身体化することが必要です。</p> <p>開発者としてある程度経験を積むと、「この実装はなぜ気持ち悪いのか」「なぜ今はこの構成のほうが良いのか」といった、言語化しづらい感覚的な知識(暗黙知)を得て、それに基づいた判断ができるようになります。</p> <p>この暗黙知は、どれだけ充実したドキュメントを読んでも得ることはできません。<br /> 仮に「なぜそうするのか」まで書かれていても、それを形式的に知るだけでは、思考の再現には至りません。</p> <p>暗黙知についての有名な例えである「自転車の乗り方」(自転車の乗り方は実際に自転車に乗ることでしか知ることができず、どのように操作すれば良いかを言語化して説明することが難しい)と同じように、技術もまた、手と頭を動かし、試行錯誤するプロセスを通じてしか身につきません。<br /> うまくいったり、うまくいかない経験のなかで生まれる感覚が、技術者としての「地力」となっていきます。</p> <h2 id="暗黙知は文化として残す">暗黙知は、文化として残す</h2> <p>「属人化を避けよう」というスローガンの背景には、「個人への依存を減らそう」という意図があると思います。<br /> しかし、開発をうまく進めるための本質的な知識の多くは、個人の経験からでしか獲得できない暗黙知にあります。</p> <p>本当に強いチームとは、単に手順が共有されているチームではなく、暗黙知を文化として共有できるチームです。<br /> ドキュメントは形式知を必要な分だけ残すことに留め、設計思想、デバッグの勘所、コードの美学といった暗黙知は、ペアプロ・コードレビュー・日常的な対話などを通して受け継いでいくことが大切です。</p> <p>一緒に手を動かし、考え方や価値観を自然に共有できる時間を増やすことが、結果的にチームを強くします。</p> <h2 id="終わりに">終わりに</h2> <p>「できる」や「できるようになった」という感覚や手応えがあると、プロセスを楽しめたり、より創造的になれたりします。</p> <p>今回の記事では、ドキュメント化の具体的な方法までは踏み込めませんでしたが、今後は「強いチームをつくるためのドキュメントとは何か」を意識して考えていきたいと思います!</p> Fri, 10 Oct 2025 00:00:00 +0900 独自のドメインモデルを捨てて、外部のドメインモデルに従う提案 | QUARTETCOM TECH BLOG 逆DDD https://tech.quartetcom.co.jp/2025/10/01/discard-own-domain-model-follow-external-domain-model/ https://tech.quartetcom.co.jp/2025/10/01/discard-own-domain-model-follow-external-domain-model/ <h2 id="はじめに">はじめに</h2> <p>担当しているプロダクトには <strong>「広告を入稿する」機能</strong> があり、これは社内のユーザ部門の業務効率化が主な目的です。</p> <p>今までのフローとして、お客様に「広告設定確認書」という書類を提出して、<br /> その「広告設定確認書内の広告設定」を元に「当該の広告媒体へ入稿を行う」業務フローがありました。</p> <p>このフローをシステム化することで、業務効率を上げていました。 システムの内部の処理は、 <strong>「独自ドメインモデル(広告設定確認書内の広告設定)」を定義し、それを「外部ドメインモデル(媒体API・SDK)」に変換して入稿 API を叩く、という二段構えのフローを採用</strong> していました。</p> <p>このプロダクトを運用していくうちに、運用においても開発においても課題が見えてきました。</p> <h2 id="プロダクトの課題">プロダクトの課題</h2> <ul> <li><strong>「独自ドメインモデル(広告設定確認書内の広告設定)」</strong> を <strong>「外部ドメインモデル (媒体API・SDK)」</strong> に変換する際に、スキーマの不一致によってデータの互換性が取れず、API エラーが起きやすい</li> <li><strong>「独自ドメインモデル」、「外部ドメインモデル」と理解するモデルが2つあり、認知負荷が高い</strong></li> <li>データ互換性維持のための差分対応が属人化し、特定の人しか直しづらい状態になっている</li> </ul> <p>まとめると<strong>「認知負荷が高い」と「データ互換性維持が困難」、「独自ドメインモデルに属人性がある」という課題</strong> がありました。<br /> また、広告媒体 API はアップデートされる機会が多く、このような問題に陥るケースが多くありました。</p> <p>そのことから、現状の設計を根本から見直して、課題を解決する方法を考えました。</p> <h2 id="実装した設計">実装した設計</h2> <p><img src="/assets/img/2025-10-01-discard-own-domain-model-follow-external-domain-model/architect.png" width="800" alt="外部ドメインモデルをシステムに浸透させる設計" /></p> <hr /> <p><img src="/assets/img/2025-10-01-discard-own-domain-model-follow-external-domain-model/driven-pattern.png" width="800" alt="「DDD」と「逆DDD」の差" /></p> <h3 id="コンセプト">コンセプト</h3> <p><strong>「外部ドメインモデル駆動」</strong> とする設計です。<br /> <strong>外部ドメインモデルの変化をシステムがしっかりと影響を受けて、システムが変化する</strong> ことを狙っています。</p> <p>つまり外部のドメインモデルに主体性があります。</p> <p>API 更新頻度が高いため、ACL 層を維持して独自ドメインモデルを管理するよりも、外部ドメインモデルに直接追従する方が、全体のコストを下げて安定運用できると判断して、 ACL 層を外す判断をしました。</p> <p>これは一般的な設計の方針から外れる大胆な判断ですが、<strong>「API に追従することが最優先の領域」</strong> では合理的だと考えました。</p> <p>その他のコンセプトを箇条書きすると以下となります。</p> <ul> <li>特定のオブジェクトのスキーマ(今回で言うと入稿 API を実行する際に必要なオブジェクト)を外部ドメインモデルに寄せている</li> <li><strong>SDK のスキーマをミラー(写像)した Interface</strong> を定義している <ul> <li>その Interface を持つオブジェクトを<strong>各層</strong>に実装する <ul> <li><strong>Infrastructure 層</strong> <ul> <li>API や SDK とやり取りする層</li> </ul> </li> <li><strong>Presentation 層</strong> <ul> <li>フロントエンドの関心を持つ層 <ul> <li>バリデーションの定義を持つ</li> <li>API スキーマとして表に出す層</li> </ul> </li> </ul> </li> <li><strong>Entity 層</strong> <ul> <li>DB に保存する関心を持つ層</li> </ul> </li> </ul> </li> <li>いずれの層は同じインターフェース(スキーマ)が実装されているので「オブジェクト to オブジェクト」のような変換が容易。 <ul> <li>フロントエンドで来たものを Entity 層に変換したり、Infrastructure 層に変換して API の実行ができたりと、レイヤーの意味を考えながら実装ができる</li> </ul> </li> </ul> </li> <li>本来あるはずの Anti-Corruption Layer (ACL) 層 を外して、外部ドメインモデルのスキーマを自システムに影響を反映させる</li> </ul> <p>また「SDK のスキーマをミラー(写像)した Interface」は以下のように「getter」を定義して、 スキーマの互換性を表現しています。</p> <div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">CampaignInterface</span> <span class="p">{</span> <span class="k">public</span> <span class="k">function</span> <span class="n">getName</span><span class="p">():</span> <span class="kt">?string</span><span class="p">;</span> <span class="c1">// SDK スキーマと同じフィールド</span> <span class="p">}</span> </code></pre></div></div> <h4 id="気をつけていること">気をつけていること</h4> <ul> <li>SDK のオブジェクトを「我々のコード」に注入せず、<strong>SDK のスキーマをミラー(写像)した Interface</strong> に依存している <ul> <li>SDK のオブジェクトは基本的に使いづらかったり、実装が汚れがちであるため、Infrastructure 層だけ SDK のオブジェクトを扱うようにして、実装の汚れを閉じ込めている</li> <li>いわば「依存関係逆転の原則(DIP)」を取り入れて、「SDK -&gt; Interface -&gt; Domain」の関係性を作っています。</li> </ul> </li> </ul> <h4 id="この設計を取り入れるメリット">この設計を取り入れるメリット</h4> <ul> <li><strong>API 更新への追従が容易になる</strong> <ul> <li>API の更新内容を見て、変更があれば「我々のシステムの外部ドメインモデルを変更する」、という一対一の関係ができる</li> </ul> </li> <li><strong>属人化を排除できる</strong> <ul> <li>モデルの説明を媒体ドキュメントに委譲できる。</li> </ul> </li> <li><strong>データの互換性の維持が容易になる</strong> <ul> <li>抽象層(独自ドメインモデル)を介さず、媒体スキーマに準拠しているため、適合性が高くできる</li> </ul> </li> </ul> <h4 id="この設計を取り入れるデメリット">この設計を取り入れるデメリット</h4> <ul> <li><strong>「外部ドメインモデル(媒体API・SDK)」の理解が必要</strong> <ul> <li>ただし、これは広告運用プロダクトを扱う以上、避けられない前提知識でもある</li> </ul> </li> <li><strong>「外部ドメインモデル(媒体API・SDK)」が廃止した時、プロダクトの改修範囲が大きくなる</strong></li> <li><strong>外部ドメインモデルをそのままコピーするのでファイル数が膨大になる</strong></li> <li><strong>SDK 側の仕様変更に強く依存するため「ドメイン固有の最適化」がやりにくい</strong> <ul> <li>例:「API のマイクロバジェット値(マイクロ単位の通貨値)」をシステムでは「円」として保存しておきたいなど</li> </ul> </li> <li><strong>元々存在した「独自ドメインモデル(広告設定確認書内の広告設定)」を使わなくなったため、ユーザ部門のコントロール性が下がった。(ただし、今回の設計変更は、ユーザ部門の負うこととなるこの「デメリット」を許容してもらうことをまず合意したうえでとりかかった)</strong> <ul> <li>ユビキタス言語もロストしているため、業務の影響は多少なりとも出ている</li> <li>正規化したと考えても良いが、業務をシステム都合で変えてしまっているのは変わりがない</li> </ul> </li> </ul> <h2 id="まとめ">まとめ</h2> <p><strong>抽象化は一見便利ですが、常に媒体 API に追従しなければならないシステムにおいては、むしろ負債</strong> になりがちです。</p> <p>一方で、抽象化することで 1 つのモデルで他媒体の関心を共通化できるメリットもあるので、<br /> どの選択をしてもリスクとリターンがあると、総括して感じました。</p> <p>今回の設計は独自のドメインモデルを捨てて外部ドメインモデルに従う、いわば<strong>「逆DDD」</strong>ですが、<br /> 認知負荷の低減・属人化の排除・最新 API への追従を考えると、やはりメリットが多いと感じます。</p> <p>そのことから<strong>「むしろ API にしっかりと追従する必要があるシステムには 逆DDD が良い」</strong>という提案をしました。</p> <p>実際に、この設計を今のプロダクトに適応している最中ですが、 API の追従がしやすかったり、スキーマに互換性があるため、API を叩きやすかったりと効果的だと感じています。</p> <p>とはいえ、知れ渡っている理論を逆行する設計なので、心理的にも思い切りが必要です。<br /> そのため <strong>「逆DDD」を採用する時は、しっかりと「そのシステムで何をしたいのか?」を明確化して判断する</strong> のをオススメします。</p> <h2 id="所感">所感</h2> <p>自分でモデリングする機会が減りました。<br /> 面白いところだと思いますが、少し残念です。</p> <p>ひたすら外部ドメインモデルのリファレンスを読んで、<br /> インターフェースやクラスを書いているので、作業感がとてもあります。<br /> (かなり膨大なので疲れます)</p> <p>それでもシステムは安定してきているので、作っているシステムにとって良い設計だろうと判断しています。</p> Wed, 01 Oct 2025 00:00:00 +0900 Symfonyのretry_failed optionsを使って、APIリクエスト失敗時に再試行するようにする | QUARTETCOM TECH BLOG PHP Symfony ExponentialBackoff API https://tech.quartetcom.co.jp/2025/09/25/symfony-retry-failed-option/ https://tech.quartetcom.co.jp/2025/09/25/symfony-retry-failed-option/ <p>Symfonyのretry_failed optionsを活用すると、APIリクエストが失敗した際に指数バックオフアルゴリズムによる再試行が簡単に実装できます。</p> <p>本記事では、その設定方法と検証結果を紹介します :saluting_face:</p> <h3 id="指数バックオフアルゴリズム-exponential-backoff-algorithmとは">指数バックオフアルゴリズム (Exponential Backoff Algorithm)とは</h3> <p>ネットワークやAPIリトライなどで使われる「待ち時間を指数的に増やして再試行する仕組み」です。</p> <p>例えば初回 1秒 → 2秒 → 4秒 → 8秒 … のように、待機時間を指数関数的に増加させてリトライ処理をします。</p> <p>過去記事「リトライ処理時の指数バックオフアルゴリズムを AWS Step Functions で実装」も参考になるので読んでみてください。<br /> <a href="https://tech.quartetcom.co.jp/2024/10/29/exponential-backoff-step-functions/">https://tech.quartetcom.co.jp/2024/10/29/exponential-backoff-step-functions/</a></p> <h2 id="symfonyのretry_failed-optionsの設定方法">Symfonyのretry_failed optionsの設定方法</h2> <p>config/packages/framework.yamlを以下のように設定します。<br /> ※Symfony 5.2以降で利用可能です。</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">// config/packages/framework.yaml</span> <span class="na">framework</span><span class="pi">:</span> <span class="na">http_client</span><span class="pi">:</span> <span class="na">default_options</span><span class="pi">:</span> <span class="na">retry_failed</span><span class="pi">:</span> <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span> <span class="na">max_retries</span><span class="pi">:</span> <span class="m">5</span> <span class="c1"># 最大リトライ回数</span> <span class="na">delay</span><span class="pi">:</span> <span class="m">1000</span> <span class="c1"># 初期遅延 (ミリ秒) → 1s</span> <span class="na">multiplier</span><span class="pi">:</span> <span class="m">2</span> <span class="c1"># 指数の倍率(2 なら 1s,2s,4s,8s...)</span> <span class="na">max_delay</span><span class="pi">:</span> <span class="m">64000</span> <span class="c1"># 最大待機(ミリ秒)→ Memorystoreの推奨に合わせ64s上限など</span> <span class="na">jitter</span><span class="pi">:</span> <span class="m">0.1</span> <span class="c1"># ジッター(0.0〜1.0、ここでは10%の揺らし)。同じタイミングで大量のリトライが発生しないようにランダムなばらつきをもたせる</span> <span class="na">http_codes</span><span class="pi">:</span> <span class="pi">[</span> <span class="nv">423</span><span class="pi">,</span> <span class="nv">425</span><span class="pi">,</span> <span class="nv">429</span><span class="pi">,</span> <span class="nv">500</span><span class="pi">,</span> <span class="nv">502</span><span class="pi">,</span> <span class="nv">503</span><span class="pi">,</span> <span class="nv">504</span><span class="pi">,</span> <span class="nv">507</span><span class="pi">,</span> <span class="nv">510</span> <span class="pi">]</span> <span class="c1"># リトライ対象のHTTPコード</span> </code></pre></div></div> <p>特定API使用時のみリトライする場合は、scoped_clientsを使います。</p> <div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">framework: </span> http_client: <span class="gd">- default_options: </span><span class="gi">+ scoped_clients: + api_time.client: + base_uri: 'https://127.0.0.1:8000/api/time' </span> retry_failed: enabled: true max_retries: 5 ... </code></pre></div></div> <div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//src/Controller/RequestController.php</span> <span class="mf">...</span> <span class="kn">use</span> <span class="nc">Symfony\Contracts\HttpClient\HttpClientInterface</span><span class="o">:</span> <span class="k">final</span> <span class="kd">class</span> <span class="nc">RequestController</span> <span class="kd">extends</span> <span class="nc">AbstractController</span> <span class="p">{</span> <span class="c1">#[Route('/request', name: 'app_request')]</span> <span class="k">public</span> <span class="k">function</span> <span class="n">index</span><span class="p">(</span> <span class="c1">#[Autowire(service: 'api_time.client')]</span> <span class="kt">HttpClientInterface</span> <span class="nv">$httpClient</span> <span class="p">):</span> <span class="kt">Response</span> <span class="p">{</span> <span class="c1">//APIにアクセスする</span> <span class="nv">$response</span> <span class="o">=</span> <span class="nv">$httpClient</span><span class="o">-&gt;</span><span class="nf">request</span><span class="p">(</span><span class="s1">'GET'</span><span class="p">,</span> <span class="s1">'https://127.0.0.1:8000/api/time'</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$response</span><span class="o">-&gt;</span><span class="nf">getStatusCode</span><span class="p">()</span> <span class="o">!==</span> <span class="mi">200</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="nf">render</span><span class="p">(</span><span class="s1">'request/error.html.twig'</span><span class="p">,</span> <span class="p">[</span> <span class="s1">'message'</span> <span class="o">=&gt;</span> <span class="s1">'API request failed with status code: '</span> <span class="mf">.</span> <span class="nv">$response</span><span class="o">-&gt;</span><span class="nf">getStatusCode</span><span class="p">(),</span> <span class="p">]);</span> <span class="p">}</span> <span class="nv">$content</span> <span class="o">=</span> <span class="nv">$response</span><span class="o">-&gt;</span><span class="nf">toArray</span><span class="p">();</span> <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="nf">render</span><span class="p">(</span><span class="s1">'request/index.html.twig'</span><span class="p">,</span> <span class="p">[</span> <span class="s1">'time'</span> <span class="o">=&gt;</span> <span class="nv">$content</span><span class="p">[</span><span class="s1">'time'</span><span class="p">],</span> <span class="p">]);</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>参考: <a href="https://symfony.com/doc/current/reference/configuration/framework.html#http-client">https://symfony.com/doc/current/reference/configuration/framework.html#http-client</a></p> <h2 id="本当に指数関数的にリトライしているか検証">本当に指数関数的にリトライしているか検証</h2> <p>以下の手順で検証を行いました。</p> <ol> <li><a href="#1-http-429エラー2リクエスト10秒を意図的に発生させるapiを作成">HTTP 429エラー(2リクエスト/10秒)を意図的に発生させるAPIを作成</a></li> <li><a href="#2-1で作成したapiを使用するクライアントにsymfonyのretry_failedオプションを設定">1で作成したAPIにリクエストをするクライアントにSymfonyのretry_failedオプションを設定</a></li> <li><a href="#3-リトライ処理が実際に行われているか確認">リトライ処理が実際に行われているか確認</a></li> </ol> <h3 id="1-http-429エラー2リクエスト10秒を意図的に発生させるapiを作成">1. HTTP 429エラー(2リクエスト/10秒)を意図的に発生させるAPIを作成</h3> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>composer create-project symfony/skeleton:<span class="s2">"7.3.x"</span> rate-limitter-example <span class="nb">cd </span>rate-limitter-example/ composer require webapp composer require symfony/rate-limiter bin/console make:controlle ApiTimeController </code></pre></div></div> <div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// src/Controller/ShowTimeController.php <span class="cp">&lt;?php</span> <span class="kn">namespace</span> <span class="nn">App\Controller</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Symfony\Bundle\FrameworkBundle\Controller\AbstractController</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Symfony\Component\HttpFoundation\Request</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Symfony\Component\HttpFoundation\Response</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Symfony\Component\RateLimiter\RateLimiterFactoryInterface</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Symfony\Component\Routing\Attribute\Route</span><span class="p">;</span> <span class="k">final</span> <span class="kd">class</span> <span class="nc">ApiTimeController</span> <span class="kd">extends</span> <span class="nc">AbstractController</span> <span class="p">{</span> <span class="c1">#[Route('/api/time', name: 'app_api_time')]</span> <span class="k">public</span> <span class="k">function</span> <span class="n">index</span><span class="p">(</span><span class="kt">Request</span> <span class="nv">$request</span><span class="p">,</span> <span class="kt">RateLimiterFactoryInterface</span> <span class="nv">$apiTimeLimiter</span><span class="p">):</span> <span class="kt">Response</span> <span class="p">{</span> <span class="nv">$limiter</span> <span class="o">=</span> <span class="nv">$apiTimeLimiter</span><span class="o">-&gt;</span><span class="nf">create</span><span class="p">(</span><span class="nv">$request</span><span class="o">-&gt;</span><span class="nf">getClientIp</span><span class="p">());</span> <span class="c1">// consume(1)の1は消費するトークン数</span> <span class="k">if</span> <span class="p">(</span><span class="kc">false</span> <span class="o">===</span> <span class="nv">$limiter</span><span class="o">-&gt;</span><span class="nf">consume</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="o">-&gt;</span><span class="nf">isAccepted</span><span class="p">())</span> <span class="p">{</span> <span class="k">throw</span> <span class="k">new</span> <span class="nc">TooManyRequestsHttpException</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="s1">'Too Many Requests'</span><span class="p">);</span> <span class="p">}</span> <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="nf">json</span><span class="p">([</span> <span class="s1">'time'</span> <span class="o">=&gt;</span> <span class="p">(</span><span class="k">new</span> <span class="err">\</span><span class="nf">DateTime</span><span class="p">())</span><span class="o">-&gt;</span><span class="nf">format</span><span class="p">(</span><span class="s1">'Y-m-d H:i:s'</span><span class="p">),</span> <span class="p">]);</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">// config/packages/rate_limiter.yaml</span> <span class="na">framework</span><span class="pi">:</span> <span class="na">rate_limiter</span><span class="pi">:</span> <span class="na">api_time</span><span class="pi">:</span> <span class="c1"># use 'sliding_window' if you prefer that policy</span> <span class="na">policy</span><span class="pi">:</span> <span class="s1">'</span><span class="s">fixed_window'</span> <span class="na">limit</span><span class="pi">:</span> <span class="m">2</span> <span class="na">interval</span><span class="pi">:</span> <span class="s1">'</span><span class="s">10</span><span class="nv"> </span><span class="s">second'</span> </code></pre></div></div> <h2 id="2-1で作成したapiを使用するクライアントにsymfonyのretry_failedオプションを設定">2. 1で作成したAPIを使用するクライアントにSymfonyのretry_failedオプションを設定</h2> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>composer create-project symfony/skeleton:<span class="s2">"7.3.x"</span> clear-late-limiter <span class="nb">cd </span>clear-late-limiter/ composer require webapp bin/console make:controlle RequestController </code></pre></div></div> <div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?php</span> <span class="kn">namespace</span> <span class="nn">App\Controller</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Symfony\Bundle\FrameworkBundle\Controller\AbstractController</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Symfony\Component\HttpFoundation\Response</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Symfony\Component\Routing\Attribute\Route</span><span class="p">;</span> <span class="kn">use</span> <span class="nc">Symfony\Contracts\HttpClient\HttpClientInterface</span><span class="p">;</span> <span class="k">final</span> <span class="kd">class</span> <span class="nc">RequestController</span> <span class="kd">extends</span> <span class="nc">AbstractController</span> <span class="p">{</span> <span class="c1">#[Route('/request', name: 'app_request')]</span> <span class="k">public</span> <span class="k">function</span> <span class="n">index</span><span class="p">(</span> <span class="kt">HttpClientInterface</span> <span class="nv">$httpClient</span> <span class="p">):</span> <span class="kt">Response</span> <span class="p">{</span> <span class="c1">//APIにアクセスする</span> <span class="nv">$response</span> <span class="o">=</span> <span class="nv">$httpClient</span><span class="o">-&gt;</span><span class="nf">request</span><span class="p">(</span><span class="s1">'GET'</span><span class="p">,</span> <span class="s1">'https://127.0.0.1:8000/api/time'</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$response</span><span class="o">-&gt;</span><span class="nf">getStatusCode</span><span class="p">()</span> <span class="o">!==</span> <span class="mi">200</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="nf">render</span><span class="p">(</span><span class="s1">'request/error.html.twig'</span><span class="p">,</span> <span class="p">[</span> <span class="s1">'message'</span> <span class="o">=&gt;</span> <span class="s1">'API request failed with status code: '</span> <span class="mf">.</span> <span class="nv">$response</span><span class="o">-&gt;</span><span class="nf">getStatusCode</span><span class="p">(),</span> <span class="p">]);</span> <span class="p">}</span> <span class="nv">$content</span> <span class="o">=</span> <span class="nv">$response</span><span class="o">-&gt;</span><span class="nf">toArray</span><span class="p">();</span> <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="nf">render</span><span class="p">(</span><span class="s1">'request/index.html.twig'</span><span class="p">,</span> <span class="p">[</span> <span class="s1">'time'</span> <span class="o">=&gt;</span> <span class="nv">$content</span><span class="p">[</span><span class="s1">'time'</span><span class="p">],</span> <span class="p">]);</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">// config/packages/framework.yaml</span> <span class="na">framework</span><span class="pi">:</span> <span class="na">http_client</span><span class="pi">:</span> <span class="na">default_options</span><span class="pi">:</span> <span class="na">retry_failed</span><span class="pi">:</span> <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span> <span class="na">max_retries</span><span class="pi">:</span> <span class="m">5</span> <span class="c1"># 最大リトライ回数</span> <span class="na">delay</span><span class="pi">:</span> <span class="m">1000</span> <span class="c1"># 初期遅延 (ミリ秒) → 1s</span> <span class="na">multiplier</span><span class="pi">:</span> <span class="m">2</span> <span class="c1"># 指数の倍率(2 なら 1s,2s,4s,8s...)</span> <span class="na">max_delay</span><span class="pi">:</span> <span class="m">64000</span> <span class="c1"># 最大待機(ミリ秒)→ Memorystoreの推奨に合わせ64s上限など</span> <span class="na">jitter</span><span class="pi">:</span> <span class="m">0.0</span> <span class="c1"># ジッター(0.0〜1.0、ここでは10%の揺らし)。同じタイミングで大量のリトライが発生しないようにランダムなばらつきをもたせる</span> <span class="na">http_codes</span><span class="pi">:</span> <span class="pi">[</span> <span class="nv">423</span><span class="pi">,</span> <span class="nv">425</span><span class="pi">,</span> <span class="nv">429</span><span class="pi">,</span> <span class="nv">500</span><span class="pi">,</span> <span class="nv">502</span><span class="pi">,</span> <span class="nv">503</span><span class="pi">,</span> <span class="nv">504</span><span class="pi">,</span> <span class="nv">507</span><span class="pi">,</span> <span class="nv">510</span> <span class="pi">]</span> <span class="c1"># リトライ対象のHTTPコード</span> </code></pre></div></div> <p>上記の設定で指数関数的にリトライするように設定します。 今回は検証結果がわかりやすいように、<code class="language-plaintext highlighter-rouge">jitter: 0.0</code>とします。</p> <h3 id="3-リトライ処理が実際に行われているか確認">3. リトライ処理が実際に行われているか確認</h3> <p><code class="language-plaintext highlighter-rouge">symfony server:start</code>で各サーバを立てます。以下</p> <ul> <li><a href="https://127.0.0.1:8000">https://127.0.0.1:8000</a> API側</li> <li><a href="https://127.0.0.1:8001">https://127.0.0.1:8001</a> クライアント側</li> </ul> <p>とします。</p> <p><a href="https://127.0.0.1:8001/request">https://127.0.0.1:8001/request</a> に短時間に連続してアクセスしてみます。</p> <p>その後、クライアント側のProfiler &gt; HTTP Client &gt; Responseを確認すると以下のことがわかります。</p> <ul> <li><code class="language-plaintext highlighter-rouge">"retry_count" =&gt; 4</code>より、4回リトライし、5回目で成功している</li> <li><code class="language-plaintext highlighter-rouge">"retries"</code>より、各リトライが1秒後 → 2秒後 → 4秒後 → 8秒後と指数関数的に試行されている</li> </ul> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>▼ <span class="o">[</span>▼ <span class="s2">"info"</span> <span class="o">=&gt;</span> <span class="o">[</span>▼ ... <span class="s2">"debug"</span> <span class="o">=&gt;</span> <span class="s2">""" ... * Request completely sent off &lt; HTTP/2 200 &lt; cache-control: no-cache, private &lt; content-type: application/json &lt; date: Mon, 22 Sep 2025 01:37:56 GMT ... ] "</span>retry_count<span class="s2">" =&gt; 4 "</span>response_headers<span class="s2">" =&gt; [▶] "</span>response_json<span class="s2">" =&gt; [▶] "</span>retries<span class="s2">" =&gt; [▼ [▼ ... "</span>debug<span class="s2">" =&gt; """</span> ... <span class="k">*</span> Request completely sent off &lt; HTTP/2 429 &lt; cache-control: no-cache, private &lt; content-type: text/html<span class="p">;</span> <span class="nv">charset</span><span class="o">=</span>UTF-8 &lt; <span class="nb">date</span>: Mon, 22 Sep 2025 01:37:41 GMT ... <span class="o">]</span> <span class="o">[</span>▼ ... <span class="s2">"debug"</span> <span class="o">=&gt;</span> <span class="s2">""" ... * Request completely sent off &lt; HTTP/2 429 &lt; cache-control: no-cache, private &lt; content-type: text/html; charset=UTF-8 &lt; date: Mon, 22 Sep 2025 01:37:42 GMT ... ] [▼ ... "</span>debug<span class="s2">" =&gt; """</span> ... <span class="k">*</span> Request completely sent off &lt; HTTP/2 429 &lt; cache-control: no-cache, private &lt; content-type: text/html<span class="p">;</span> <span class="nv">charset</span><span class="o">=</span>UTF-8 &lt; <span class="nb">date</span>: Mon, 22 Sep 2025 01:37:44 GMT ... <span class="o">]</span> <span class="o">[</span>▼ ... <span class="s2">"debug"</span> <span class="o">=&gt;</span> <span class="s2">""" ... * Request completely sent off &lt; HTTP/2 429 &lt; cache-control: no-cache, private &lt; content-type: text/html; charset=UTF-8 &lt; date: Mon, 22 Sep 2025 01:37:48 GMT ... ] ] ] </span></code></pre></div></div> <h2 id="まとめ">まとめ</h2> <p>今回の検証では、2リクエスト/10秒という制限のあるAPIに対して、delay: 1000(=1秒)・multiplier: 2に設定したところ、リトライが頻繁に発生し、設定した最大リトライ回数(max_retries)に達してしまうケースがありました。</p> <p>このため、API側のレート制限に合わせて、delayやmultiplier、max_retriesなどのパラメータを適切に調整することが重要です :warning:</p> <p>Symfonyのretry_failed optionsを活用すれば、指数バックオフアルゴリズムによるリトライ機能を簡単に設定できます!ぜひ皆さんのプロジェクトでも取り入れてみてください :heart_hands:</p> Thu, 25 Sep 2025 00:00:00 +0900 PHPでGraphQLサーバー構築入門:Symfony+API Platformを活用した実践 | QUARTETCOM TECH BLOG PHP Symfony GraphQL API https://tech.quartetcom.co.jp/2025/08/15/symfony-api-platform-graphql/ https://tech.quartetcom.co.jp/2025/08/15/symfony-api-platform-graphql/ <p>近年のWebアプリケーションでは、画面構成やUXの多様化にともない<strong>「より柔軟で効率的なデータ取得」</strong>の重要性が高まっています。</p> <p>特にフロントエンドでは、<strong>不要なデータ取得</strong>や<strong>複数回のAPIリクエスト</strong>が開発体験やパフォーマンスに影響を与えるケースも増えてきました。</p> <p>よく使われている<strong>REST API</strong>では、エンドポイントごとに決まったレスポンスが返ってくるため、 フロント側の要件に合わせて<strong>「不要なデータを受け取る」</strong>や<strong>「複数のエンドポイントを呼び分ける」</strong> といった非効率が発生しがちです。</p> <p>これらの課題を解決する手段として注目されているのが<code class="language-plaintext highlighter-rouge">GraphQL</code>です。</p> <p><strong>GraphQL</strong>を使えば、クライアントが必要なデータだけを明確に指定して取得できるため、 通信の無駄が減り開発効率やパフォーマンスの向上につながります。</p> <p>本記事では、<strong>Symfony と API Platform</strong>を使って、<strong>GraphQL API</strong>を簡単に作成する方法を紹介します。</p> <h2 id="️-今回作成するもの">✅️ 今回作成するもの</h2> <p>今回は「書籍(Book)」と「レビュー(Review)」のデータを扱う<strong>GraphQL API</strong>を作成します。</p> <ul> <li>書籍一覧の取得</li> <li>各書籍に紐づくレビューの平均スコアの取得</li> <li>React + Apollo Clientを使ったフロントエンドでのデータ取得</li> </ul> <p>フロント以外のコードはGitHubに公開していますので、ぜひ参考にしてください。</p> <p>🔗<a href="https://github.com/mako5656/symfony-api-platform-graphql">GitHub:symfony-api-platform-graphql</a></p> <h2 id="-目次">📝 目次</h2> <ul> <li><a href="#1-symfonyプロジェクトのセットアップ">1. Symfonyプロジェクトのセットアップ</a></li> <li><a href="#2-api-platformの導入">2. API Platformの導入</a></li> <li><a href="#3-データベース接続postgresql">3. データベース接続(PostgreSQL)</a></li> <li><a href="#4-book--reviewエンティティの作成とマイグレーション">4. Book / Reviewエンティティの作成とマイグレーション</a></li> <li><a href="#5-データ登録-rest-api">5. データ登録 (REST API)</a></li> <li><a href="#6-graphqlの有効化">6. GraphQLの有効化</a></li> <li><a href="#7-graphqlで書籍一覧を取得">7. GraphQLで書籍一覧を取得</a></li> <li><a href="#8-カスタムフィールドレビューの平均スコアを追加">8. カスタムフィールド(レビューの平均スコア)を追加</a></li> <li><a href="#9-フロント側からデータを取得する">9. フロント側からデータを取得する</a></li> <li><a href="#10-まとめ">10. まとめ</a></li> </ul> <h2 id="1-symfonyプロジェクトのセットアップ">1. Symfonyプロジェクトのセットアップ</h2> <p>使用バージョン:</p> <ul> <li>PHP:<code class="language-plaintext highlighter-rouge">v8.2.28</code></li> <li>Symfony:<code class="language-plaintext highlighter-rouge">v7.3.1</code></li> </ul> <p>まずは新規プロジェクトを作成します。</p> <div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>symfony new symfony-api-platform-graphql </code></pre></div></div> <h2 id="2-api-platformの導入">2. API Platformの導入</h2> <p>API Platformは、PHP製のWeb APIフレームワークです。 導入するだけで自動的にRESTとGraphQLのAPIエンドポイントが作成されます。</p> <div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>composer require api </code></pre></div></div> <p>これだけで<code class="language-plaintext highlighter-rouge">/api</code>にアクセスすると、<strong>Swagger UIでREST APIのドキュメント</strong>が確認できます。</p> <blockquote> <p>💡 API Platform では、REST API と GraphQL API の両方を併用できます。 エンティティを定義するだけで、自動的に REST のエンドポイントが生成され、Swagger UI から確認や操作も可能です。 本記事では GraphQL をメインに扱いますが、動作確認も兼ねてデータ登録には REST API を使って進めていきます。</p> </blockquote> <p><img src="/assets/img/2025-08-15/01.png" width="800" alt="APIドキュメント-データ0" /></p> <h2 id="3-データベース接続postgresql">3. データベース接続(PostgreSQL)</h2> <p><code class="language-plaintext highlighter-rouge">.env</code>ファイルを編集し、PostgreSQLに接続できるように設定します。</p> <div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- DATABASE_URL="postgresql://app:[email protected]:5432/app?serverVersion=16&amp;charset=utf8" </span><span class="gi">+ DATABASE_URL="postgresql://app:[email protected]:5432/symfony-api-platform-graphql?serverVersion=16&amp;charset=utf8" </span></code></pre></div></div> <ul> <li>ユーザー名やパスワードは自分の環境に合わせて変更してください。</li> <li>事前にPostgreSQLをインストールしておく必要があります。</li> <li>PostgreSQL の代わりに SQLite や MySQL を使っても大丈夫です。</li> </ul> <p>その後、以下のコマンドでデータベースを作成します。</p> <div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>php bin/console doctrine:database:create </code></pre></div></div> <h2 id="4-book--reviewエンティティの作成とマイグレーション">4. Book / Reviewエンティティの作成とマイグレーション</h2> <p>まずは開発用に<strong>MakerBundle</strong>を追加します。</p> <div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>composer require symfony/maker-bundle <span class="nt">--dev</span> </code></pre></div></div> <p>次に、<code class="language-plaintext highlighter-rouge">書籍(Book)</code>と<code class="language-plaintext highlighter-rouge">レビュー(Review)</code>のエンティティを作成し、マイグレーションを実行します。</p> <div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>php bin/console make:entity <span class="nv">$ </span>php bin/console make:migration <span class="nv">$ </span>php bin/console doctrine:migrations:migrate </code></pre></div></div> <p>👇️️ エンティティ定義のコード</p> <ul> <li><a href="https://github.com/mako5656/symfony-api-platform-graphql/blob/master/src/Entity/Book.php">Book.php</a></li> <li><a href="https://github.com/mako5656/symfony-api-platform-graphql/blob/master/src/Entity/Review.php">Review.php</a></li> </ul> <p>反映後、<code class="language-plaintext highlighter-rouge">/api</code>にアクセスすると、<code class="language-plaintext highlighter-rouge">Book</code>と<code class="language-plaintext highlighter-rouge">Review</code>のエンドポイントが自動生成されていることが確認できます。</p> <p><img src="/assets/img/2025-08-15/02.png" width="800" alt="REST APIドキュメント-エンティティ反映" /></p> <p><img src="/assets/img/2025-08-15/03.png" width="800" alt="REST APIドキュメント-スキーマ" /></p> <p>次に、実際にBookとReviewのデータをAPI経由で登録してみます。</p> <h2 id="5-データ登録-rest-api">5. データ登録 (REST API)</h2> <p>生成された<code class="language-plaintext highlighter-rouge">/api</code>エンドポイントに対して、<code class="language-plaintext highlighter-rouge">Postman</code>などを使ってデータを登録します。</p> <p>Bookデータを登録するために、以下のように<code class="language-plaintext highlighter-rouge">/api/books</code>に<code class="language-plaintext highlighter-rouge">POST</code>リクエストを送信します。</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"isbn"</span><span class="p">:</span><span class="w"> </span><span class="s2">"978-4-7741-8411-1"</span><span class="p">,</span><span class="w"> </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ドメイン駆動設計入門"</span><span class="p">,</span><span class="w"> </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"DDDの基礎を学べる書籍です。"</span><span class="p">,</span><span class="w"> </span><span class="nl">"author"</span><span class="p">:</span><span class="w"> </span><span class="s2">"山田太郎"</span><span class="p">,</span><span class="w"> </span><span class="nl">"publicationDate"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-05-01T00:00:00+09:00"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>Reviewデータの登録も同様に<code class="language-plaintext highlighter-rouge">/api/reviews</code>に<code class="language-plaintext highlighter-rouge">POST</code>リクエストを行います。</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"rating"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w"> </span><span class="nl">"body"</span><span class="p">:</span><span class="w"> </span><span class="s2">"とても分かりやすい内容でDDDが理解できました!"</span><span class="p">,</span><span class="w"> </span><span class="nl">"author"</span><span class="p">:</span><span class="w"> </span><span class="s2">"読者A"</span><span class="p">,</span><span class="w"> </span><span class="nl">"publicationDate"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-05-10T00:00:00+09:00"</span><span class="p">,</span><span class="w"> </span><span class="nl">"book"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/api/books/1"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p><img src="/assets/img/2025-08-15/04.png" width="800" alt="Postmanでのデータ登録" /></p> <p><img src="/assets/img/2025-08-15/05.png" width="800" alt="PostgreSQLでBookデータ確認" /></p> <p>データベースにデータが入っていることが確認できます。</p> <p>サンプルデータとして<code class="language-plaintext highlighter-rouge">Book</code>と<code class="language-plaintext highlighter-rouge">Review</code>データをいくつか登録しておきます。</p> <p>サンプルデータは<a href="https://github.com/mako5656/symfony-api-platform-graphql/blob/master/README.md">README.md</a>に記載しておきます。</p> <p><img src="/assets/img/2025-08-15/06.png" width="800" alt="PostgreSQLでBookとReviewのデータ確認" /></p> <p>REST APIが問題なく動作することを確認できたので、 ここからは本題であるGraphQLの利用方法を見ていきます。</p> <h2 id="6-graphqlの有効化">6. GraphQLの有効化</h2> <p>GraphQLを有効化します。</p> <div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>composer require api-platform/graphql </code></pre></div></div> <p>これで<code class="language-plaintext highlighter-rouge">/graphql</code>にアクセスすれば、<strong>GraphiQL(GraphQL IDE)</strong>が利用可能になります。</p> <p><img src="/assets/img/2025-08-15/07.png" width="800" alt="GraphQL IDE" /></p> <h2 id="7-graphqlで書籍一覧を取得">7. GraphQLで書籍一覧を取得</h2> <p>API Platformでは、GraphQLエンドポイント(<code class="language-plaintext highlighter-rouge">/graphql</code>)が自動で用意されており、ブラウザから<strong>GraphiQL</strong>を使ってクエリを試すことができます。</p> <p>まずは、シンプルに書籍の一覧を取得してみます。</p> <div class="language-graphql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">query</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">books</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">edges</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">node</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="n">title</span><span class="w"> </span><span class="n">author</span><span class="w"> </span><span class="n">isbn</span><span class="w"> </span><span class="n">description</span><span class="w"> </span><span class="n">publicationDate</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>このように、GraphQLでは<strong>必要なフィールドだけを指定して取得できる</strong>のが大きなメリットです。</p> <p><code class="language-plaintext highlighter-rouge">edges &gt; node</code>という構造は、<a href="https://relay.dev/">Relay仕様</a>に沿ったページネーション形式です。</p> <p>👇️️ 結果画面はこんな感じ</p> <p><img src="/assets/img/2025-08-15/08.png" width="800" alt="GraphQLのクエリ実行" /></p> <h2 id="8-カスタムフィールドレビューの平均スコアを追加">8. カスタムフィールド(レビューの平均スコア)を追加</h2> <p>クライアントでレビューを集計するのは手間なので、バックエンドで平均スコアを計算して返すようにします。</p> <h3 id="-bookエンティティに集計ロジックを追加">📌 Bookエンティティに集計ロジックを追加</h3> <div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#[Groups(['book:read'])]</span> <span class="k">public</span> <span class="k">function</span> <span class="n">getAverageRating</span><span class="p">():</span> <span class="kt">?float</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">is_iterable</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="n">reviews</span><span class="p">)</span> <span class="o">||</span> <span class="nb">count</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="n">reviews</span><span class="p">)</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">null</span><span class="p">;</span> <span class="p">}</span> <span class="nv">$sum</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nv">$count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="k">foreach</span> <span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="n">reviews</span> <span class="k">as</span> <span class="nv">$review</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$sum</span> <span class="o">+=</span> <span class="nv">$review</span><span class="o">-&gt;</span><span class="n">rating</span><span class="p">;</span> <span class="nv">$count</span><span class="o">++</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="nv">$count</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">?</span> <span class="nb">round</span><span class="p">(</span><span class="nv">$sum</span> <span class="o">/</span> <span class="nv">$count</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="o">:</span> <span class="kc">null</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>レビューが存在する場合だけ平均を計算し、小数第2位で丸めて返すというシンプルな実装です。</p> <h3 id="-graphqlでフィールドを有効化する">📌 GraphQLでフィールドを有効化する</h3> <p>この<code class="language-plaintext highlighter-rouge">averageRating</code>をGraphQLのレスポンスに含めるためには、<code class="language-plaintext highlighter-rouge">ApiResource</code>にグループ指定を追加します。</p> <div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#[ApiResource(normalizationContext: ['groups' =&gt; ['book:read']])]</span> </code></pre></div></div> <p>これでGraphQL側でも以下のようなクエリで取得可能になります!</p> <div class="language-graphql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">query</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">books</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">edges</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">node</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">title</span><span class="w"> </span><span class="n">averageRating</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>👇️️ 結果画面はこんな感じ</p> <p><img src="/assets/img/2025-08-15/09.png" width="800" alt="平均スコア追加後のGraphQLのクエリ実行" /></p> <p>バックエンドで集計処理を済ませておけば、フロント側は<code class="language-plaintext highlighter-rouge">averageRating</code>をクエリに追加するだけで、すぐに表示できます。</p> <p>REST APIでも集約済みのデータを1リクエストで取得することは可能ですが、 事前に専用のエンドポイントを用意する必要があり画面ごとのデータ要件に応じて設計が増えていきがちです。</p> <p>その点GraphQLでは、クライアントが欲しい形で柔軟にクエリを定義できるため、バックエンド側は汎用的なスキーマを用意するだけで済みます。</p> <p>その結果、<strong>クライアントは欲しいデータを、欲しい形で取得</strong>できるようになり、 バックエンド側も汎用的なスキーマ設計だけで済むため、<strong>API設計の手間を大きく減らす</strong>ことができます。</p> <p>GraphQL のこうした柔軟さは、複雑な画面構成や多様なデータ要件を持つアプリ開発において、特に効果を発揮します。</p> <h2 id="9-フロント側からデータを取得する">9. フロント側からデータを取得する</h2> <p>GraphQLの大きな利点のひとつが、「<strong>フロントエンドから必要なデータだけを効率的に取得できる</strong>」点です。</p> <p>ここでは<code class="language-plaintext highlighter-rouge">React + Apollo Client</code>を使って、書籍のタイトルと平均スコアを表示するシンプルなUIを作ってみます。</p> <h3 id="graphqlクエリの定義">GraphQLクエリの定義</h3> <div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">GET_BOOKS</span> <span class="o">=</span> <span class="nx">gql</span><span class="s2">` query { books { edges { node { id title averageRating } } } } `</span><span class="p">;</span> </code></pre></div></div> <h3 id="コンポーネント側の実装">コンポーネント側の実装</h3> <div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">gql</span><span class="p">,</span> <span class="nx">useQuery</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@apollo/client</span><span class="dl">'</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">BookList</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">loading</span><span class="p">,</span> <span class="nx">error</span><span class="p">,</span> <span class="nx">data</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">useQuery</span><span class="p">(</span><span class="nx">GET_BOOKS</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">loading</span><span class="p">)</span> <span class="k">return</span> <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>読み込み中...<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;;</span> <span class="k">if</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="k">return</span> <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>エラー: <span class="si">{</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;;</span> <span class="k">return</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span> <span class="si">{</span><span class="nx">data</span><span class="p">.</span><span class="nx">books</span><span class="p">.</span><span class="nx">edges</span><span class="p">.</span><span class="nx">map</span><span class="p">(({</span> <span class="nx">node</span> <span class="p">}:</span> <span class="kr">any</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span> <span class="p">&lt;</span><span class="nt">li</span> <span class="na">key</span><span class="p">=</span><span class="si">{</span><span class="nx">node</span><span class="p">.</span><span class="nx">id</span><span class="si">}</span><span class="p">&gt;</span> <span class="si">{</span><span class="nx">node</span><span class="p">.</span><span class="nx">title</span><span class="si">}</span> - 平均スコア: <span class="si">{</span><span class="nx">node</span><span class="p">.</span><span class="nx">averageRating</span> <span class="o">??</span> <span class="dl">'</span><span class="s1">評価なし</span><span class="dl">'</span><span class="si">}</span> <span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span> <span class="p">))</span><span class="si">}</span> <span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span> <span class="p">);</span> <span class="p">};</span> <span class="k">export</span> <span class="k">default</span> <span class="nx">BookList</span><span class="p">;</span> </code></pre></div></div> <p>このように、GraphQLクエリを使ってフロント側で必要なフィールドだけを柔軟に取得できます。</p> <p>バックエンド側で<code class="language-plaintext highlighter-rouge">Groups</code>属性によってフィールド公開範囲を調整できるのも、API Platform の便利な点です。</p> <h2 id="10-まとめ">10. まとめ</h2> <p>今回の構成(Symfony + API Platform + GraphQL)を振り返ってみると、こんな強みがありました。</p> <ul> <li>✅ <code class="language-plaintext highlighter-rouge">自動生成</code>:エンティティ定義だけでREST/GraphQL APIを即構築可能</li> <li>✅ <code class="language-plaintext highlighter-rouge">柔軟性</code>:Groups属性で公開フィールドを制御したり、フロントエンドから必要なデータを自由に取得できる</li> <li>✅ <code class="language-plaintext highlighter-rouge">効率性</code>:GraphQL により、必要な情報だけを最小限で取得できるため、通信や処理の無駄がない</li> </ul> <p>GraphQLが初めての方でも、API Platformを使えば簡単に構築できます。</p> <p>特に、<strong>データの粒度を調整したい場面</strong>や<strong>複雑な画面構成を持つSPA開発</strong>においては、GraphQLのメリットを最大限に活かせます。</p> <p>今回は実装中心の紹介でしたが、GraphQLはUIの多様化に柔軟に対応できる強力な選択肢です。</p> <p>ぜひ一度、試してみてください!</p> <h2 id="️-参考リンク">📚️ 参考リンク</h2> <ul> <li><a href="https://api-platform.com/docs/">API Platform 公式ドキュメント</a></li> <li><a href="https://zenn.dev/ttskch/articles/89a4a6420313bb">Zenn 記事:Symfony + API Platform で GraphQL API を実装</a></li> </ul> Fri, 15 Aug 2025 00:00:00 +0900 Angularリアクティブフォームの型安全な使い方 ─ 罠を避ける実践ポイント | QUARTETCOM TECH BLOG Angular TypeScript Form https://tech.quartetcom.co.jp/2025/08/01/angular-form/ https://tech.quartetcom.co.jp/2025/08/01/angular-form/ <h1 id="はじめに">はじめに</h1> <p>今年に入りチーム編成が変わり、バックエンドエンジニアからフロントエンドやインフラも触ることになりました。 詳しくは<a href="https://tech.quartetcom.co.jp/2025/06/27/rebuild-team/">こちら</a>をご覧ください。</p> <p>その中でAngular v18環境でのフォーム実装に関わる機会があり、特にリアクティブフォームの「型の付け方」や「構成パターン」で混乱があったので、 本記事ではTyped Forms(型付きフォーム)におけるフォームオブジェクトの生成、型推論、および <code class="language-plaintext highlighter-rouge">nonNullable</code> の扱いなどを体系的に整理し、より型が安全なフォーム実装に必要な観点をまとめます。</p> <h1 id="angularのフォーム構成の方法">Angularのフォーム構成の方法</h1> <p>Angularにはフォームを作成するための2つの主要な構成方法があります。</p> <p><a href="https://angular.jp/guide/forms#choosing-an-approach">公式ドキュメント</a>を見ていただければわかりますがデータフローや入力フィールド数の規模感の違いなどがあり、 フォームの構造やロジックの定義箇所、型推論の取り扱いが選定時に特に重要になると感じました。</p> <ul> <li>リアクティブフォーム:フォーム構造・ロジックをTypeScript側で定義する。</li> <li>テンプレート駆動フォーム:フォーム構造・ロジックをHTMLテンプレート側に記述する。</li> </ul> <p>本記事では、Typed Forms(型付きフォーム)を活かせるリアクティブフォームに焦点を当てて解説します。</p> <h1 id="リアクティブフォームの構成要素">リアクティブフォームの構成要素</h1> <p>フォームの実装をする上で良いとされる構成パターンはよく見かけますがなぜいいのかがわからない状態だったので、 以下の3つの観点に分けて順に理解していくとわかりやすいと感じたので構成方法や型指定方法の違いによって起きうる危険性も含めて解説していきます。</p> <ol> <li><a href="#フォームオブジェクトの生成方法">フォームオブジェクトの作成方法</a></li> <li><a href="#フォームの型指定方法">フォームの型の指定方法</a></li> <li><a href="#オプション設定">オプション設定</a></li> </ol> <h2 id="フォームオブジェクトの生成方法">フォームオブジェクトの生成方法</h2> <p>オブジェクトの作成には、主に以下の方法があります。</p> <ul> <li>フォームの基盤となるクラスを<code class="language-plaintext highlighter-rouge">new</code>(インスタンス化)して作成する方法(以下、「ローレベルAPI」)</li> <li>Angularが提供するユーティリティサービスのFormBuilderを使用して作成する方法</li> </ul> <p>以下にそれぞれのサンプルコードを示し違いを説明します。</p> <p>他にも深いところで違いがあるんだとは思いますが、個人的にはDIでのテストのしやすさや可読性の点からFormBuilderの方が好みでした。</p> <h3 id="ローレベルapiのサンプルコード">ローレベルAPIのサンプルコード</h3> <p>フォームの基盤クラスである<code class="language-plaintext highlighter-rouge">FormGroup</code>や<code class="language-plaintext highlighter-rouge">FormControl</code>を明示的にインスタンス化してフォームを作成します。</p> <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">class</span> <span class="nx">UserFormComponent</span> <span class="p">{</span> <span class="nx">userForm</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FormGroup</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="k">new</span> <span class="nx">FormControl</span><span class="p">(</span><span class="dl">''</span><span class="p">),</span> <span class="p">});</span> <span class="p">...</span> <span class="p">}</span> </code></pre></div></div> <h3 id="formbuilderのサンプルコード">FormBuilderのサンプルコード</h3> <p>FormBuilderを使用してフォームを作成します。</p> <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">class</span> <span class="nx">UserFormComponent</span> <span class="p">{</span> <span class="kd">constructor</span><span class="p">(</span><span class="k">private</span> <span class="nx">fb</span><span class="p">:</span> <span class="nx">FormBuilder</span><span class="p">)</span> <span class="p">{}</span> <span class="nx">userForm</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">fb</span><span class="p">.</span><span class="nx">group</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">fb</span><span class="p">.</span><span class="nx">control</span><span class="p">(</span><span class="dl">''</span><span class="p">),</span> <span class="p">});</span> <span class="p">...</span> <span class="p">}</span> </code></pre></div></div> <h2 id="フォームの型指定方法">フォームの型指定方法</h2> <p>上記で作成したフォームに型を指定する方法として、v14以降に導入されたTyped Forms(型付きフォーム)を使用することでより型安全なフォームの実装が可能になります。</p> <p>Typed Forms(型付きフォーム)を使っていて最も混乱したのが、<strong>開発者が指定した型と型推論の違い</strong>でした。 型パラメータやNonNullableFormBuilderを使うことでより開発者が明示的に扱う型を宣言することができ、より型安全なフォームの実装が可能になります。ですが、フォームのFormControlが<code class="language-plaintext highlighter-rouge">null</code>を許容するかどうかによって意図しない型推論が行われることがあるので注意が必要です。</p> <p>以下に分けて解説します。</p> <ul> <li>nullを許容しない場合</li> <li>nullを意図して許容する場合</li> <li>nullを意図せず許容してしまっている場合</li> </ul> <h3 id="nullを許容しない場合">nullを許容しない場合</h3> <p>nullを許容しない場合は、<code class="language-plaintext highlighter-rouge">NonNullableFormBuilder</code>と型パラメータ<code class="language-plaintext highlighter-rouge">&lt;T&gt;</code>使うことでシンプルにより型安全なフォームを作成できます。</p> <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">userForm</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">fb</span><span class="p">.</span><span class="nx">group</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">fb</span><span class="p">.</span><span class="nx">control</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span><span class="p">(</span><span class="dl">""</span><span class="p">),</span> <span class="p">});</span> <span class="kd">constructor</span><span class="p">(</span><span class="k">private</span> <span class="nx">fb</span><span class="p">:</span> <span class="nx">NonNullableFormBuilder</span><span class="p">)</span> <span class="p">{}</span> </code></pre></div></div> <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">FormGroup</span><span class="o">&lt;</span><span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">FormControl</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span> <span class="p">}</span><span class="o">&gt;</span> </code></pre></div></div> <p>明示した通り<code class="language-plaintext highlighter-rouge">&lt;string&gt;</code>の型パラメータを指定することで、<code class="language-plaintext highlighter-rouge">null</code>を許容しない型(<code class="language-plaintext highlighter-rouge">FormControl&lt;string&gt;</code>)として推論されます。</p> <h3 id="nullを許容する場合">nullを許容する場合</h3> <p><code class="language-plaintext highlighter-rouge">null</code>を許容する場合は、以下のようにFormBuilderと型パラメータ<code class="language-plaintext highlighter-rouge">&lt;T&gt;</code>を使ってフォームを作成できます。</p> <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">userForm</span><span class="p">:</span> <span class="nx">FormGroup</span><span class="o">&lt;</span><span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">FormControl</span><span class="o">&lt;</span><span class="kr">string</span> <span class="o">|</span> <span class="kc">null</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span><span class="o">&gt;</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">fb</span><span class="p">.</span><span class="nx">group</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">fb</span><span class="p">.</span><span class="nx">control</span><span class="o">&lt;</span><span class="kr">string</span> <span class="o">|</span> <span class="kc">null</span><span class="o">&gt;</span><span class="p">(</span><span class="dl">""</span><span class="p">),</span> <span class="p">});</span> <span class="kd">constructor</span><span class="p">(</span><span class="k">private</span> <span class="nx">fb</span><span class="p">:</span> <span class="nx">FormBuilder</span><span class="p">)</span> <span class="p">{}</span> </code></pre></div></div> <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">FormGroup</span><span class="o">&lt;</span><span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">FormControl</span><span class="o">&lt;</span><span class="kr">string</span> <span class="o">|</span> <span class="kc">null</span><span class="o">&gt;</span> <span class="p">}</span><span class="o">&gt;</span> </code></pre></div></div> <p>これは記述通りの型推論になっており、<code class="language-plaintext highlighter-rouge">FormControl&lt;string | null&gt;</code>のように<code class="language-plaintext highlighter-rouge">null</code>を許容する型として推論されます。</p> <h3 id="nullを意図せず許容してしまっている場合">nullを意図せず許容してしまっている場合</h3> <p><code class="language-plaintext highlighter-rouge">null</code>を意図せず許容してしまっている場合は、以下のようにFormBuilderで明示的に<code class="language-plaintext highlighter-rouge">&lt;string&gt;</code>の型パラメータを与えているように見えて、内部的なFormControlの仕様によって<code class="language-plaintext highlighter-rouge">null</code>が許容されてしまいます。</p> <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">userForm</span><span class="p">:</span> <span class="nx">FormGroup</span><span class="o">&lt;</span><span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">FormControl</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span><span class="o">&gt;</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">fb</span><span class="p">.</span><span class="nx">group</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">fb</span><span class="p">.</span><span class="nx">control</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span><span class="p">(</span><span class="dl">""</span><span class="p">),</span> <span class="p">});</span> <span class="kd">constructor</span><span class="p">(</span><span class="k">private</span> <span class="nx">fb</span><span class="p">:</span> <span class="nx">FormBuilder</span><span class="p">)</span> <span class="p">{}</span> </code></pre></div></div> <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">FormGroup</span><span class="o">&lt;</span><span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">FormControl</span><span class="o">&lt;</span><span class="kr">string</span> <span class="o">|</span> <span class="kc">null</span><span class="o">&gt;</span> <span class="p">}</span><span class="o">&gt;</span> </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">fb.control&lt;string&gt;</code>で型パラメータを<code class="language-plaintext highlighter-rouge">&lt;string&gt;</code>で与えているので<code class="language-plaintext highlighter-rouge">null</code>は許容されていないように見えますが、Angulerの<code class="language-plaintext highlighter-rouge">FormControl</code>の仕様によって、<code class="language-plaintext highlighter-rouge">null</code>が許容されてしまいます。</p> <p>以下はFormControlの該当する<a href="https://github.com/angular/angular/blob/5b25d93f27b259098ed968a8a355e9c16867b37b/goldens/public-api/forms/index.api.md?plain=1#L310">ソースコード</a>です。</p> <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">control</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span> <span class="nx">formState</span><span class="p">:</span> <span class="nx">T</span> <span class="o">|</span> <span class="nx">FormControlState</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">,</span> <span class="nx">validatorOrOpts</span><span class="p">?:</span> <span class="nx">ValidatorFn</span> <span class="o">|</span> <span class="nx">ValidatorFn</span><span class="p">[]</span> <span class="o">|</span> <span class="nx">FormControlOptions</span> <span class="o">|</span> <span class="kc">null</span><span class="p">,</span> <span class="nx">asyncValidator</span><span class="p">?:</span> <span class="nx">AsyncValidatorFn</span> <span class="o">|</span> <span class="nx">AsyncValidatorFn</span><span class="p">[]</span> <span class="o">|</span> <span class="kc">null</span> <span class="p">):</span> <span class="nx">FormControl</span><span class="o">&lt;</span><span class="nx">T</span> <span class="o">|</span> <span class="kc">null</span><span class="o">&gt;</span><span class="p">;</span> </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">control&lt;T&gt;</code>で<code class="language-plaintext highlighter-rouge">&lt;T&gt;</code>を型パラメータとして与えても返り値は<code class="language-plaintext highlighter-rouge">&lt;T | null&gt;</code>になっていることがわかります。 上記のように型パラメータだけでは<code class="language-plaintext highlighter-rouge">null</code>が許容されている状態となりコンパイルエラーやバグの原因になります。</p> <p>これを回避するためにオプション設定を理解しておくことが重要です。</p> <h2 id="オプション設定">オプション設定</h2> <p><code class="language-plaintext highlighter-rouge">FormControl()</code> や <code class="language-plaintext highlighter-rouge">fb.control()</code> では、第2引数として構成オプションを渡すことができます。 nullを許容しないようにするためには、<code class="language-plaintext highlighter-rouge">nonNullable: true</code>オプションを指定します。</p> <p><a href="#nullを意図せず許容してしまっている場合">nullを意図せず許容してしまっている場合</a>にオプションを追記すると以下のようになります。</p> <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">userForm</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">fb</span><span class="p">.</span><span class="nx">group</span><span class="o">&lt;</span><span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">FormControl</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span><span class="o">&gt;</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">fb</span><span class="p">.</span><span class="nx">control</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span><span class="p">(</span><span class="dl">""</span><span class="p">,</span> <span class="p">{</span> <span class="na">nonNullable</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}),</span> <span class="p">});</span> <span class="kd">constructor</span><span class="p">(</span><span class="k">private</span> <span class="nx">fb</span><span class="p">:</span> <span class="nx">FormBuilder</span><span class="p">)</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nx">userForm</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">fb</span><span class="p">.</span><span class="nx">group</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">fb</span><span class="p">.</span><span class="nx">control</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span><span class="p">(</span><span class="dl">""</span><span class="p">,</span> <span class="p">{</span> <span class="na">nonNullable</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}),</span> <span class="p">});</span> <span class="p">}</span> </code></pre></div></div> <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">FormGroup</span><span class="o">&lt;</span><span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">FormControl</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span> <span class="p">}</span><span class="o">&gt;</span> </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">nonNullable: true</code>を指定することで、返り値の<code class="language-plaintext highlighter-rouge">FormControl&lt;T|null&gt;</code>の型が<code class="language-plaintext highlighter-rouge">FormControl&lt;T&gt;</code>に変わり、<code class="language-plaintext highlighter-rouge">null</code>を許容しないことで型安全性を高めることができます。</p> <p>以下は<code class="language-plaintext highlighter-rouge">nunNullable: true</code>を設定した場合の<a href="https://github.com/angular/angular/blob/5b25d93f27b259098ed968a8a355e9c16867b37b/goldens/public-api/forms/index.api.md?plain=1#L304">ソースコード</a>です。 先ほどのnonNullableのオプション設定のないソースコードと比べると、返り値が<code class="language-plaintext highlighter-rouge">FormControl&lt;T|null&gt;</code>ではなく<code class="language-plaintext highlighter-rouge">FormControl&lt;T&gt;</code>になっていることがわかります。</p> <div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">control</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span> <span class="nx">formState</span><span class="p">:</span> <span class="nx">T</span> <span class="o">|</span> <span class="nx">FormControlState</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">,</span> <span class="nx">opts</span><span class="p">:</span> <span class="nx">FormControlOptions</span> <span class="o">&amp;</span> <span class="p">{</span> <span class="na">nonNullable</span><span class="p">:</span> <span class="kc">true</span><span class="p">;</span> <span class="p">}</span> <span class="p">):</span> <span class="nx">FormControl</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">;</span> </code></pre></div></div> <p>オプションを使うことで<code class="language-plaintext highlighter-rouge">null</code>を許容しないようにすることはできますが、より型安全なフォームを実装したい場合は<a href="#nullを許容しない場合">nullを許容しない場合</a>でフォーム作成をすることで型安全性を高めることができます。</p> <h1 id="まとめ">まとめ</h1> <p>リアクティブフォームは柔軟性が高いため設計を曖昧にしたまま進めると、型の不一致や型推論のnullによるバグが発生しやすくなる印象を受けました。 そのため最初にフォームオブジェクト+型パラメータ+オプション設定を実装の意図に合わせて組むことが、型安全性とテスト容易性を高めるために重要だと感じました。</p> <p>本記事では触れませんでしたが、<code class="language-plaintext highlighter-rouge">interface</code> で値の型を明示したり、<code class="language-plaintext highlighter-rouge">FormBuilder.nonNullable.group()</code> を使ったりすることでも型の安全性を担保できます。 またリアクティブフォームに触れる中で、RxJSによる非同期処理やストリームの考え方(<code class="language-plaintext highlighter-rouge">Observable</code>の<code class="language-plaintext highlighter-rouge">pipe()</code>を使ったオペレーター連結など)も非常に新鮮に感じたのでまた紹介できればと思います。</p> <h1 id="おまけ">おまけ</h1> <h2 id="v13以前での型の戻り値">v13以前での型の戻り値</h2> <p>ブログを書く中でわかったことですがv13以前では<code class="language-plaintext highlighter-rouge">FormGroup</code>や<code class="language-plaintext highlighter-rouge">FormControl</code>に対して型パラメータ<code class="language-plaintext highlighter-rouge">&lt;T&gt;</code>を与えることができず、戻り値は<code class="language-plaintext highlighter-rouge">FormGroup&lt;any&gt;</code>と返ってきていたようで完全に型安全にしたい場合は、<a href="https://github.com/Quramy/ngx-typed-forms">ngx-typed-forms</a>などのライブラリを使う必要があったようです。 この問題はv14でTypedFormsが公式に導入されたことで解消され、今は<code class="language-plaintext highlighter-rouge">FormGroup&lt;T&gt;</code>や<code class="language-plaintext highlighter-rouge">FormControl&lt;T&gt;</code>による型安全なフォームが標準機能として扱えるようになっています。 詳しくは<a href="https://github.com/angular/angular/discussions/44513">こちら</a>を参照してください。</p> Fri, 01 Aug 2025 00:00:00 +0900 プロダクトチーム制への移行とユーザー部門との連携強化に取り組みました! | QUARTETCOM TECH BLOG チーム 組織 https://tech.quartetcom.co.jp/2025/06/27/rebuild-team/ https://tech.quartetcom.co.jp/2025/06/27/rebuild-team/ <p>今回は、システム開発部が現在取り組んだ「チーム体制の見直し」と「他部署との連携強化」についてご紹介します。</p> <h1 id="前提">前提</h1> <p>私たちは大きさの異なるプロダクトや担当業務を合計で約20ほどを開発・運用しています。そのほとんどがいわゆる Web+RDBMS で構成されるアプリケーションですが一部は Node.JS on AWS Lambda だったり Google Apps Script だったりします。</p> <h1 id="旧体制における課題職能別チームの状況">旧体制における課題:職能別チームの状況</h1> <p>これまでのシステム開発部は、PHP、UI、インフラといった<strong>職能別の専門チーム</strong>で構成されていました。各チームは、それぞれの専門分野を担当する数名のメンバーで構成されていました。この体制には、いくつかの課題がありました。</p> <p><img src="/assets/img/2025-06-27/skill-teams.png" /></p> <h2 id="課題例1-チーム間での業務量のばらつき">課題例1: チーム間での業務量のばらつき</h2> <p>チームによって担当するタスクの質と量にばらつきが見られました。これにより、メンバー個々の<strong>価値提供の機会に偏り</strong>が生じ、必ずしも健全とは言えない状況でした。</p> <h2 id="課題例2-プロダクトオーナーシップの不明確さ">課題例2: プロダクトオーナーシップの不明確さ</h2> <p>特定のプロダクトに対し、複数の職能チームのメンバーがその都度対応する形となっていました。そのため、各プロダクトを主体的に担当するメンバーやチームが明確ではなく、結果として<strong>責任の所在が曖昧になる傾向</strong>が見られました。</p> <h1 id="新体制プロダクトチーム制への移行">新体制:プロダクトチーム制への移行</h1> <p>これらの課題に対応するため、私たちは組織体制を「<strong>プロダクトチーム制</strong>」へと変更しました。</p> <p><img src="/assets/img/2025-06-27/product-teams.png" /></p> <p><strong>プロダクトチーム</strong>にはリーダーが置かれ、会社の役職上のマネージャがその職責を担います。(ちなみに各チーム名はシステム開発部の業務で欠かせない Linux のイメージキャラクタであるペンギンの種別から着想を得たものです)<br /> 各プロダクトチームには、担当するプロダクトや業務範囲が設定されています。<br /> さらに、各プロダクトや業務範囲に対し、<strong>一人の PdM(プロダクトマネージャ) と一人以上のメンバー</strong>を明確に割り当てます。</p> <p>新体制への移行により、各メンバーの<strong>権限と責任がより明確に</strong>なりました。これにより、各プロダクトにおけるメンバーの<strong>オーナーシップ意識の醸成</strong>にも寄与すると考えています。一方で、従来の職能チーム体制では求められなかった、より幅広いスキルセットが各メンバーに求められるようになっています。これは、メンバー一人ひとりが専門分野を超えて成長していくことを前提としており、個人のスキル向上とキャリア形成の一助となると考えています。</p> <h2 id="新体制への過程部門内での検討">新体制への過程:部門内での検討</h2> <p>今回のプロダクトチーム制への移行は、慎重かつ綿密な検討を重ねて行われました。私たちは、どのようなチーム構成が適切か、また各プロダクトをどのチームが担当すべきかについて、適宜さまざまな座組で複数回にわたり検討を行いました。<br /> 最終段階ではメンバー全員による複数回の協議を実施し、結論を出すための最終協議はリモートワークメンバーも含め、全員が出社して対面で意見を交換しました。忌憚のない、お互いを尊重するコミュニケーションを通じて、結論を導き出しました。 検討過程でのコミュニケーションは、「変更されたチーム体制」という成果物以上に、メンバーの相互理解を深め、関係を醸成するという価値を提供してくれるものでした。 この変革は、部長である私から部のメンバーに提起したものですが、各メンバーの建設的で主体的な姿勢があったことで実現できたものと認識しています。大変感謝しています!</p> <h1 id="ユーザー部門との連携強化システム開発における共創">ユーザー部門との連携強化:システム開発における共創</h1> <p>私たちが開発するプロダクトやタスクの大部分は、他部署の業務効率化やサポートを目的としています。そこで、ユーザー部門の従業員にもより明確なオーナーシップを持ってもらえるよう、新たな連携の枠組みを構築し始めています。<br /> その端緒として、私からシステム開発におけるユーザー部門の従業員の関与の重要性を共有し、<strong>各プロダクトに一人の PO(プロダクトオーナー) の設定</strong>を依頼しました。<br /> システム開発部の PdM とユーザー部門の PO が一般的に呼称される役割に限定せず、それぞれの役割を適切に協議し設定することで、プロダクトがユーザーの課題を的確に解決し、さらに効果的に利用されることを目指します。</p> <h1 id="今後の展望">今後の展望</h1> <p><strong>プロダクトチーム制への移行</strong>と<strong>ユーザー部門との連携強化</strong>は、システム開発部がより柔軟で、かつ責任を持った組織へと発展するための重要な一歩となります。この変革を通じて、私たちは今後も事業が高品質なサービスを提供するためのシステムを支えていきます!</p> <hr /> Fri, 27 Jun 2025 00:00:00 +0900 JavaScript 非同期処理を Event Loop で理解する | QUARTETCOM TECH BLOG JavaScript https://tech.quartetcom.co.jp/2025/05/30/javascript-asynchronous-processing/ https://tech.quartetcom.co.jp/2025/05/30/javascript-asynchronous-processing/ <h1 id="はじめに">はじめに</h1> <p>久しぶりに JavaScript / TypeScript に触れる機会がありました。<br /> JavaScript の非同期処理は思い出すのにいつも時間がかかります。今後のために非同期処理について少し詳しくまとめます。</p> <p>本記事では、サンプルコードとともに Event Loop の仕組み、Microtasks / Macrotasks の実行タイミングを整理しながら非同期処理についての理解を深めることを目的にしています。</p> <p>本記事はおもに <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth">In depth: Microtasks and the JavaScript runtime environment</a> を参考にしています。</p> <h2 id="event-loop-とは">Event Loop とは</h2> <p>JavaScript の Event Loop は、非同期処理の「実行タイミング」を制御する仕組みです。<br /> JavaScript は(同じ Event Loop のサイクルにおいて)同期処理をすべて実行した後に非同期処理を実行します。言い換えると、非同期処理の準備が整っていても(解決済みでも)同期処理に割り込むことはありません。</p> <ul> <li>同期処理 -&gt; 即時実行される</li> <li>非同期処理 -&gt; キューに登録され、順番に実行される</li> </ul> <h2 id="非同期処理の種類タスクの分類">非同期処理の種類(タスクの分類)</h2> <p><a href="https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth">In depth: Microtasks and the JavaScript runtime environment</a> では非同期処理を 2 種類の「キュー(待ち行列)」に分類して説明しています。</p> <table> <thead> <tr> <th>種類</th> <th>名前</th> <th>実行タイミング</th> <th>例</th> </tr> </thead> <tbody> <tr> <td>Microtasks</td> <td>マイクロタスク</td> <td>現在のタスクの後すぐ実行</td> <td><code class="language-plaintext highlighter-rouge">Promise.then</code>, <code class="language-plaintext highlighter-rouge">queueMicrotask</code></td> </tr> <tr> <td>Macrotasks</td> <td>タスク</td> <td>次の Event Loop のタイミングで実行</td> <td><code class="language-plaintext highlighter-rouge">setTimeout</code>, <code class="language-plaintext highlighter-rouge">setInterval</code></td> </tr> </tbody> </table> <h3 id="注意点">注意点</h3> <ol> <li>「現在のタスク(current task)」という表現が MDN にもありますが、これは setTimeout のような「Macrotasks の 1 件」だけを指すわけではありません。文脈によっては、「今 JavaScript が同期的に実行している 1 サイクル全体(=現在の Event Loop のサイクル)」を指している場合もあります。文脈に注意して解釈する必要があります。</li> <li>Macrotasks は Microtasks との対比を強調する意味で使用されることが多いようです。[In depth: Microtasks and the JavaScript runtime environment] は単に <code class="language-plaintext highlighter-rouge">Macrotasks</code> を<code class="language-plaintext highlighter-rouge">Tasks</code> と表記しています。本記事は分かりやすさのために Microtasks / Macrotasks で統一します。</li> </ol> <h2 id="サンプル">サンプル</h2> <p>今までの説明をサンプルコードを使って見ていきます。</p> <h3 id="サンプル1基本的な実行タイミングを確認">サンプル1:基本的な実行タイミングを確認</h3> <h4 id="コード">コード</h4> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// (1)..(5) は実行順序を表します</span> <span class="kd">function</span> <span class="nx">sampleAsynchronous</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">(</span><span class="nx">resolve</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">task 1.</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// (1)</span> <span class="nx">resolve</span><span class="p">(</span><span class="dl">'</span><span class="s1">task 4.</span><span class="dl">'</span><span class="p">);</span> <span class="p">});</span> <span class="p">}</span> <span class="kd">const</span> <span class="nx">promiseObj</span> <span class="o">=</span> <span class="nx">sampleAsynchronous</span><span class="p">();</span> <span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">task 5.</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// (5)</span> <span class="p">},</span> <span class="mi">0</span><span class="p">);</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">task 2.</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// (2)</span> <span class="nx">promiseObj</span><span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">data</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span> <span class="c1">// (4)</span> <span class="p">});</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">task 3.</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// (3)</span> </code></pre></div></div> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// 出力結果 task 1. task 2. task 3. task 4. task 5. </code></pre></div></div> <h4 id="解説">解説</h4> <p>以下の (1)…(5) はコードのコメントに記載 (1)…(5) に対応します</p> <ol> <li>同期処理 (1)〜(3):すぐに実行される</li> <li>Microtasks (4):Promise.then は Microtasks として登録され、同期処理が終わった後に即実行される</li> <li>Macrotasks (5):setTimeout は次の Event Loop サイクルで実行される</li> </ol> <h4 id="event-loop-のサイクル構造簡略図">Event Loop のサイクル構造(簡略図)</h4> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> ┌────────────┐ │ Macrotasks │ &lt;- (1回目: 同期処理) └────┬───────┘ ↓ ┌────────────┐ │ Microtasks │ &lt;- (Promise.thenなど) └────┬───────┘ ↓ 次の Macrotasks(setTimeout など) </code></pre></div></div> <h3 id="サンプル2複数サイクル実行タイミングを確認">サンプル2:複数サイクル実行タイミングを確認</h3> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// (1)..(10) は実行順序を表します</span> <span class="kd">function</span> <span class="nx">createPromise</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">(</span><span class="nx">resolve</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">task 2.</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// (2)</span> <span class="nx">resolve</span><span class="p">(</span><span class="dl">'</span><span class="s1">task 4.</span><span class="dl">'</span><span class="p">);</span> <span class="p">});</span> <span class="p">}</span> <span class="kd">function</span> <span class="nx">createOtherPromise</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">(</span><span class="nx">resolve</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">task 6.</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// (6)</span> <span class="nx">resolve</span><span class="p">(</span><span class="dl">'</span><span class="s1">task 8.</span><span class="dl">'</span><span class="p">);</span> <span class="p">});</span> <span class="p">}</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">task 1.</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// (1)</span> <span class="kd">const</span> <span class="nx">promiseObj</span> <span class="o">=</span> <span class="nx">createPromise</span><span class="p">();</span> <span class="nx">promiseObj</span><span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">data</span> <span class="o">=&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">data</span><span class="p">));</span> <span class="c1">// (4)</span> <span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">task 5.</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// (5)</span> <span class="kd">const</span> <span class="nx">other</span> <span class="o">=</span> <span class="nx">createOtherPromise</span><span class="p">();</span> <span class="nx">other</span><span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">data</span> <span class="o">=&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">data</span><span class="p">));</span> <span class="c1">// (8)</span> <span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">task 9.</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// (9)</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">task 10.</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// (10)</span> <span class="p">},</span> <span class="mi">2000</span><span class="p">);</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">task 7.</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// (7)</span> <span class="p">},</span> <span class="mi">1000</span><span class="p">);</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">task 3.</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// (3)</span> </code></pre></div></div> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// 出力結果(順序) task 1. task 2. task 3. task 4. task 5. task 6. task 7. task 8. task 9. task 10. </code></pre></div></div> <h4 id="解説-1">解説</h4> <ul> <li>1回目の Event Loop <ul> <li>Macrotasks:task 1. -&gt; task 2. -&gt; task 3.</li> <li>Microtasks:task 4.(Promise.then)</li> </ul> </li> <li>2回目の Event Loop( 1000ms 後の setTimeout ) <ul> <li>Macrotasks:task 5. -&gt; task 6. -&gt; task 7.</li> <li>Microtasks:task 8.( Promise.then )</li> </ul> </li> <li>3回目の Event Loop( さらに 2000ms 後の setTimeout ) <ul> <li>Macrotasks:task 9. -&gt; task 10.</li> <li>Microtasks:なし</li> </ul> </li> </ul> <h1 id="まとめ">まとめ</h1> <p>まとめとしてに本記事の要点を記載します。</p> <ul> <li>Promise.then は同期処理が終わった直後に実行される( Microtasks )</li> <li>setTimeout は 次の Event Loop サイクルで実行される( Macrotasks )</li> </ul> <p>Event Loop は Macrotasks( current task ) -&gt; すべての Microtasks → 次の Macrotasks( Tasks ) という流れで進行します。</p> <p>非同期処理は JavaScript の基本的な機能ですが、文脈による言葉の使い分け(特に「task」)も含め、分かりづらい面もあります。 本記事が非同期処理の理解の一助になれば幸いです。</p> Fri, 30 May 2025 00:00:00 +0900 GitHubでYahoo!広告スクリプトのための開発環境をつくる | QUARTETCOM TECH BLOG Node.js https://tech.quartetcom.co.jp/2025/03/05/yas-scaffold/ https://tech.quartetcom.co.jp/2025/03/05/yas-scaffold/ <h1 id="はじめに">はじめに</h1> <p>みなさん「Yahoo!広告スクリプト」をご存知でしょうか。<br /> Yahoo!広告スクリプトとは、Yahoo! JAPANにおけるWeb広告プラットフォームにアクセスするためのスクリプトです。</p> <p><a href="https://www.lycbiz.com/jp/column/yahoo-ads/service-information/2023021530408670/">Yahoo!広告 スクリプトとは?特徴や利用手順を解説 | LINEヤフー for business</a></p> <p>弊社にはWeb広告の運用代行事業があり、とある集計作業でのスクリプト制作を試みました。</p> <p>Yahoo!広告スクリプトは専用の編集画面でJavaScriptのコードを登録します。<br /> リアルタイム実行またはタイマー実行で、ログを見てトライ&エラーを繰り返すのが基本の使い方です。CI、テスト、リビジョン管理といったなじみの道具はなく、使用できるAPIや構文はECMAScriptのバージョンとは一致しません。</p> <p>そんなランタイムのための開発環境をどう作るか…🤔<br /> 試行錯誤の結果できあがった開発環境について紹介します。</p> <p>※ Yahoo!広告ランタイムは定期的にアップデートされます。当記事はランタイム「202501」を前提としています。</p> <h1 id="目指したもの">目指したもの</h1> <p>できるだけ普段に近づけたいと思い、以下を開発環境の要件としました。</p> <ul> <li>GitHubでリビジョン管理する事</li> <li>TypeScriptで開発する事</li> <li>ユニットテストを実行する事</li> <li>ランタイムのビルトインサービスを表現できる事</li> <li>ランタイムのシンタックスエラーをローカルで再現する事</li> <li>1回限りでなく既存プロジェクトへの展開ができる事</li> </ul> <h1 id="完成したもの">完成したもの</h1> <p><a href="https://github.com/ringtail003/yas-scaffold">https://github.com/ringtail003/yas-scaffold</a></p> <p>完成した開発環境をGitHubリポジトリにアップしました。<br /> このリポジトリは以下のように使用します。</p> <p>空のディレクトリを作成し、npxでセットアップ用スクリプトを叩くと各種設定ファイルがコピーされます。</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>node <span class="nt">--version</span> <span class="o">&gt;</span> v22.9.0 <span class="nv">$ </span><span class="nb">mkdir </span>my-project <span class="nv">$ </span><span class="nb">cd </span>my-project <span class="nv">$ </span>npx ringtail003/yas-scaffold yas <span class="nt">--init</span> <span class="o">&gt;</span> 設定ファイルをコピーしました(package.json) <span class="o">&gt;</span> 設定ファイルをコピーしました(eslint.config.json) <span class="o">&gt;</span> ... </code></pre></div></div> <p>プロジェクト名を書き換えてパッケージをインストールします。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// package.json</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">my-project</span><span class="dl">"</span> <span class="p">}</span> </code></pre></div></div> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm <span class="nb">install</span> </code></pre></div></div> <p>これでセットアップは完了です。<br /> サンプルコードに対してユニットテストとLintが実行できます。</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm <span class="nb">test</span> <span class="o">&gt;</span> ✔ src/greet.ts <span class="o">(</span>72.979375ms<span class="o">)</span> <span class="o">&gt;</span> ✔ プリミティブ <span class="o">(</span>0.4155ms<span class="o">)</span> <span class="o">&gt;</span> ✔ 配列 <span class="o">(</span>0.474125ms<span class="o">)</span> <span class="o">&gt;</span> ... <span class="o">&gt;</span> ℹ tests 13 <span class="o">&gt;</span> ℹ pass 13 </code></pre></div></div> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm run lint <span class="o">&gt;</span> eslint <span class="nt">--fix</span> ./src </code></pre></div></div> <p>ビルドコマンドを叩くと、JavaScriptにトランスパイルしたビルドファイルが生成されます。</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm run publish <span class="o">&gt;</span> created dist/bundle.js <span class="k">in </span>343ms <span class="o">&gt;</span> created dist/publish.js </code></pre></div></div> <p>ビルドファイルの中身をスクリプト編集画面に貼り付けます。<br /> 保存してリアルタイム実行し、ログやリファレンスを見ながら実装を詰めていきます。</p> <hr /> <p>リポジトリはYahoo!広告スクリプトのプロジェクトを構成する目的として、yas-scaffold(Yahoo Ads Script Scaffold)と名付けました。以降はこのリポジトリを「YAS開発環境」と表記します。</p> <h1 id="githubでリビジョン管理する">GitHubでリビジョン管理する</h1> <p>ビルドファイルの冒頭にはgitのコミットが記載されます。<br /> Yahoo!広告スクリプトの編集画面から辿ってビルド時点のコミットが特定できる、という仕組みです。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// publish.js</span> <span class="cm">/** * @file ... * @see https://github.com/{SAMPLE_ORGANIZATION}/my-project/commit/123abcd */</span> <span class="kd">function</span> <span class="nx">main</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span> </code></pre></div></div> <p>コメントはビルド後に <a href="https://github.com/ringtail003/yas-scaffold/blob/0348f7d276ca51fef996c4558ac17fa103a12947/shells/publish.sh">publish.sh</a> で埋め込んでいます。<code class="language-plaintext highlighter-rouge">{SAMPLE_ORGANIZATION}</code> は実際には自社のOrganizationをベタ書きしていて、 <code class="language-plaintext highlighter-rouge">my-project</code> はビルド時にpackage.jsonから読み込みます。</p> <p>Yahoo!広告スクリプトをたくさん登録しても(上限100個)、どのリポジトリで開発しているのか判別ができます。</p> <h1 id="typescriptで開発する">TypeScriptで開発する</h1> <p>TypeScriptからJavaScriptへのトランスパイルは <a href="https://Rollupjs.org/">Rollup</a> を採用しました。</p> <p>最初は <a href="https://babeljs.io/">Babel</a> を使っていたのですが、設定が圧倒的にシンプルであること、複数ファイルを結合するバンドル機能を標準で備えていること、などの優位性からRollupに乗り換えることにしました。</p> <p>セットアップ用スクリプトで作成されるサンプルコードは <a href="https://github.com/ringtail003/yas-scaffold/blob/0348f7d276ca51fef996c4558ac17fa103a12947/src/index.ts">index.ts</a> で、開発言語はTypeScriptです。<a href="https://github.com/ringtail003/yas-scaffold/blob/0348f7d276ca51fef996c4558ac17fa103a12947/package.json#L21">ビルドコマンド</a> を叩くとJavaScriptのファイルが出力されます。</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm run build ├── dist │   ├── bundle.js <span class="c"># ビルドファイル</span> ├── src │   ├── index.ts <span class="c"># ソースコード</span> │   └── greet.ts <span class="c"># ソースコード</span> ├── rollup.config.js </code></pre></div></div> <p>設定ファイル <a href="https://github.com/ringtail003/yas-scaffold/blob/0348f7d276ca51fef996c4558ac17fa103a12947/rollup.config.js">rollup.config.js</a> はたったの14行です。巷ではゼロコンフィグのトランスパイラも登場しているようですが、今のところRollupに満足しています。</p> <h1 id="ユニットテストを実行する">ユニットテストを実行する</h1> <p>テストはNode.jsの <a href="https://nodejs.org/docs/latest-v22.x/api/test.html">ビルトインテストランナー</a> を採用しました。YAS開発環境では関数のIN/OUTを検証するテストが主体で、必要最低限のものが揃えば十分だと思ったからです。</p> <p><a href="https://github.com/ringtail003/yas-scaffold/blob/0348f7d276ca51fef996c4558ac17fa103a12947/package.json#L23-L25">テストコマンド</a> は基本的に <code class="language-plaintext highlighter-rouge">node --test</code> を実行しているだけです。ビルトインテストランナーは <code class="language-plaintext highlighter-rouge">*.test.ts</code> を探してテストスイートを実行します。</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>node <span class="nt">--test</span> <span class="se">\</span> <span class="nt">--experimental-strip-types</span> <span class="se">\ </span><span class="c"># 型宣言を削除する</span> <span class="nt">--experimental-transform-types</span> <span class="se">\ </span><span class="c"># enumなどTypeScript独自のシンタックスを変換する</span> <span class="nt">--no-warnings</span><span class="o">=</span>ExperimentalWarning <span class="c"># experimentalの警告表示をオフにする</span> </code></pre></div></div> <p>テストのサンプルコード <a href="https://github.com/ringtail003/yas-scaffold/blob/0348f7d276ca51fef996c4558ac17fa103a12947/src/index.test.ts#L12">index.test.ts</a> では、インポート文に <code class="language-plaintext highlighter-rouge">.ts</code> が付いています。Node.jsの <code class="language-plaintext highlighter-rouge">--experimental-strip-types</code> フラグは型宣言を削除するだけでTypeScriptのように拡張子を補完しないため、拡張子付きで宣言する必要があります。また型のインポートが削除対象となるように <code class="language-plaintext highlighter-rouge">type</code> キーワードも必要になります。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">greet</span><span class="p">,</span> <span class="nx">type</span> <span class="nx">Greeting</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./greet.ts</span><span class="dl">"</span><span class="p">;</span> </code></pre></div></div> <p>インポートだけ気をつければ後は普通にテストを書くことができます。一般的なアサーションも揃っています。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">test</span><span class="p">,</span> <span class="nx">type</span> <span class="nx">TestContext</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:test</span><span class="dl">"</span><span class="p">;</span> <span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">...</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">test</span><span class="p">(</span><span class="dl">"</span><span class="s2">...</span><span class="dl">"</span><span class="p">,</span> <span class="p">(</span><span class="na">t</span><span class="p">:</span> <span class="nx">TestContext</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">t</span><span class="p">.</span><span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello World</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Hello </span><span class="dl">"</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">World</span><span class="dl">"</span><span class="p">);</span> <span class="nx">t</span><span class="p">.</span><span class="nx">assert</span><span class="p">.</span><span class="nx">deepEqual</span><span class="p">([</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">],</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">]);</span> <span class="kd">const</span> <span class="nx">a</span> <span class="o">=</span> <span class="p">{};</span> <span class="kd">const</span> <span class="nx">b</span> <span class="o">=</span> <span class="nx">a</span><span class="p">;</span> <span class="nx">t</span><span class="p">.</span><span class="nx">assert</span><span class="p">.</span><span class="nx">strictEqual</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">);</span> <span class="p">});</span> <span class="p">});</span> </code></pre></div></div> <p>テスト環境のためのパッケージのインストールやアップデートメンテも不要ですし、ユニットテストならこれで十分かなと思っています。</p> <p>※ <code class="language-plaintext highlighter-rouge">--experimental-strip-types</code> フラグの挙動は <a href="https://nodejs.org/ja/blog/release/v23.6.0">Node.js v23.6</a> でデフォルトになるため、Node.jsのアップデートとともに必要がなくなります。</p> <h1 id="ランタイムのビルトインサービス">ランタイムのビルトインサービス</h1> <p><a href="https://ads-developers.yahoo.co.jp/ja/ads-script/product-guide/reference/">リファレンス | Yahoo! JAPAN広告</a></p> <p>Yahoo!広告スクリプトにはビルトインサービスが存在し、Googleスプレッドシートの書き込みやログ出力ができます。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">ss</span> <span class="o">=</span> <span class="nx">SpreadsheetApp</span><span class="p">.</span><span class="nx">openById</span><span class="p">(</span><span class="dl">"</span><span class="s2">テスト</span><span class="dl">"</span><span class="p">);</span> <span class="nx">Logger</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">完了</span><span class="dl">"</span><span class="p">);</span> </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">SpreadsheetApp</code> や <code class="language-plaintext highlighter-rouge">Logger</code> はYahoo!広告のランタイムで実体が得られるオブジェクトで、そのまま書くとTypeScriptの世界ではコンパイルエラーが発生します。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">ss</span> <span class="o">=</span> <span class="nx">SpreadsheetApp</span><span class="p">.</span><span class="nx">openById</span><span class="p">(</span><span class="dl">"</span><span class="s2">テスト</span><span class="dl">"</span><span class="p">);</span> <span class="c1">// ~~~~~~~~~~~~~~</span> <span class="c1">// ERROR: Cannot find name 'SpreadsheetApp'.ts(2304)</span> </code></pre></div></div> <p>YAS開発環境では、ビルトインサービスをグローバルオブジェクトとして <a href="https://github.com/ringtail003/yas-scaffold/blob/0348f7d276ca51fef996c4558ac17fa103a12947/types/builtin.d.ts">builtin.d.ts</a> で型宣言しています。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">declare</span> <span class="nb">global</span> <span class="p">{</span> <span class="nx">type</span> <span class="nx">SpreadsheetApp</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">openById</span> <span class="p">(</span><span class="na">id</span><span class="p">:</span> <span class="nx">string</span><span class="p">):</span> <span class="nx">Spreadsheet</span><span class="p">;</span> <span class="p">};</span> <span class="c1">// グローバルオブジェクトの存在を宣言する</span> <span class="kd">var</span> <span class="nx">SpreadsheetApp</span><span class="p">:</span> <span class="nx">SpreadsheetApp</span><span class="p">;</span> </code></pre></div></div> <p><a href="https://github.com/ringtail003/yas-scaffold/blob/0348f7d276ca51fef996c4558ac17fa103a12947/tsconfig.json#L7">tsconfig.json</a> でデフォルトで読み込むようにしておけば、どのファイルからもインポート文なしで使用できます。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">"</span><span class="s2">compilerOptions</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">typeRoots</span><span class="dl">"</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">./types</span><span class="dl">"</span><span class="p">]</span> <span class="p">}</span> </code></pre></div></div> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">SpreadsheetApp</span><span class="p">.</span><span class="nx">openById</span><span class="p">(</span><span class="nx">spreadsheetId</span><span class="p">);</span> <span class="c1">// 👍</span> </code></pre></div></div> <p>テスト環境では実体が得られないため、モックをセットしておきましょう。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">globalThis</span><span class="p">.</span><span class="nx">SpreadsheetApp</span> <span class="o">=</span> <span class="p">{</span> <span class="na">openById</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="dl">"</span><span class="s2">dummy file</span><span class="dl">"</span><span class="p">,</span> <span class="p">};</span> <span class="p">});</span> <span class="nx">test</span><span class="p">(</span><span class="dl">"</span><span class="s2">...</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">SpreadsheetApp</span><span class="p">.</span><span class="nx">openById</span><span class="p">(</span><span class="nx">spreadsheetId</span><span class="p">);</span> <span class="c1">// 👍</span> <span class="p">});</span> </code></pre></div></div> <h1 id="シンタックスエラーをローカルで再現する">シンタックスエラーをローカルで再現する</h1> <p>Yahoo!広告のランタイムとECMAScriptのバージョンは一致しません。<code class="language-plaintext highlighter-rouge">Array.flatMap(ES2019)</code> は使用できて、それよりも古い <code class="language-plaintext highlighter-rouge">class(ES6)</code> は使用できない、といった具合です。</p> <p>このような環境の場合、以下のような選択肢が考えられます。</p> <ul> <li>新しいバージョンをベースにしてシンタックスを個別に禁止する</li> <li>古いバージョンをベースにしてシンタックスを個別に許容する</li> </ul> <p>YAS開発環境では「古いバージョン」をベースにしました。新しいシンタックスをひとつずつ試して禁止していくよりも、ランタイムで大半のシンタックスが使える古いバージョンをベースにするほうが楽だからです。</p> <p><a href="https://github.com/ringtail003/yas-scaffold/blob/0348f7d276ca51fef996c4558ac17fa103a12947/tsconfig.json#L6">tsconfig.json</a> には、やや古めの <code class="language-plaintext highlighter-rouge">ES6</code> を指定しています。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">"</span><span class="s2">compilerOptions</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">lib</span><span class="dl">"</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">ES6</span><span class="dl">"</span><span class="p">],</span> <span class="c1">// ES6 === ES2015</span> </code></pre></div></div> <h2 id="シンタックスを禁止するeslint">シンタックスを禁止する(ESLint)</h2> <p><code class="language-plaintext highlighter-rouge">class(ES6)</code> はTypeScriptではコンパイルエラーになりませんが、Yahoo!広告スクリプトのランタイムでシンタックスエラーが発生します。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">Foo</span> <span class="p">{}</span> <span class="c1">// コンパイルエラーにならない</span> </code></pre></div></div> <p>このようなケースでは、うっかりコードを書かないように <a href="https://github.com/ringtail003/yas-scaffold/blob/0348f7d276ca51fef996c4558ac17fa103a12947/eslint.config.js">eslint.config.js</a> のルールで使用を禁止します。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">esPlugin</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">eslint-plugin-es</span><span class="dl">"</span><span class="p">;</span> <span class="k">export</span> <span class="k">default</span> <span class="p">[</span> <span class="p">{</span> <span class="na">files</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">**/*.ts</span><span class="dl">"</span><span class="p">],</span> <span class="na">plugins</span><span class="p">:</span> <span class="p">{</span> <span class="na">es</span><span class="p">:</span> <span class="nx">esPlugin</span><span class="p">,</span> <span class="c1">// `es/` プレフィクスを有効にする</span> <span class="p">...</span> <span class="p">},</span> <span class="na">rules</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">es/no-classes</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">error</span><span class="dl">"</span><span class="p">,</span> <span class="c1">// classの使用を禁止する</span> <span class="p">},</span> <span class="p">}</span> <span class="p">];</span> </code></pre></div></div> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">Foo</span> <span class="p">{}</span> <span class="c1">// ~~~~~~~~~</span> <span class="c1">// ERROR: ES2015 class declarations are forbidden.</span> </code></pre></div></div> <h2 id="シンタックスを許容するdeclare宣言">シンタックスを許容する(declare宣言)</h2> <p>開発中に <code class="language-plaintext highlighter-rouge">Object.groupBy(ES2024)</code> が使いたくなることがありました。YAS開発環境はES6のため、ES2024のAPIの型は存在しません。そのまま書くと型の解決ができずコンパイルエラーが発生します。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">Object</span><span class="p">.</span><span class="nx">groupBy</span><span class="p">([],</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{});</span> <span class="c1">// ~~~~~~~~</span> <span class="c1">// ERROR: Property 'groupBy' does not exist on type 'ObjectConstructor'.</span> </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">Object.groupBy</code> はYahoo!広告スクリプトのランタイムではシンタックスエラーにならず、期待通りの結果が返ることが分かりました。このようなケースでは、declare宣言で型を解決します。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">declare</span> <span class="nb">global</span> <span class="p">{</span> <span class="kr">interface</span> <span class="nx">ObjectConstructor</span> <span class="p">{</span> <span class="nx">groupBy</span><span class="o">&lt;</span><span class="nx">K</span><span class="p">,</span> <span class="nx">T</span><span class="o">&gt;</span><span class="p">(...):</span> <span class="nx">Partial</span><span class="o">&lt;</span><span class="nx">Record</span><span class="o">&lt;</span><span class="nx">K</span><span class="p">,</span> <span class="nx">T</span><span class="p">[]</span><span class="o">&gt;&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">groupBy</span><span class="p">([],</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{});</span> <span class="c1">// 👍</span> </code></pre></div></div> <h2 id="シンタックスを許容するrollup">シンタックスを許容する(Rollup)</h2> <p>YAS開発環境はES6のため <code class="language-plaintext highlighter-rouge">Optional chaining(ES2020)</code> を使うとコンパイルエラーが発生します。この構文はYahoo!広告ランタイムでもシンタックスエラーが発生し、使うことができません。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">v</span> <span class="o">=</span> <span class="nx">foo</span><span class="p">?.</span><span class="nx">bar</span><span class="p">?.</span><span class="nx">value</span><span class="p">;</span> <span class="c1">// ~~ ~~</span> <span class="c1">// ERROR: ES2020 optional chaining is forbidden.</span> </code></pre></div></div> <p>このようなケースでは、Rollupでポリフィルできないか探しましょう。</p> <p>Rollupでは <a href="https://rollupjs.org/configuration-options/#output-generatedcode">generatedCode</a> で指定したECMAScriptのバージョンにより、一部のコードがポリフィルの対象になります。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="p">{</span> <span class="na">output</span><span class="p">:</span> <span class="p">{</span> <span class="na">generatedCode</span><span class="p">:</span> <span class="dl">"</span><span class="s2">es5</span><span class="dl">"</span><span class="p">,</span> <span class="c1">// ES5で出力する</span> <span class="p">...</span> <span class="p">},</span> </code></pre></div></div> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Before: index.ts</span> <span class="kd">const</span> <span class="nx">v</span> <span class="o">=</span> <span class="nx">foo</span><span class="p">?.</span><span class="nx">bar</span><span class="p">?.</span><span class="nx">value</span> <span class="c1">// After: publish.js</span> <span class="p">(</span><span class="nx">_v</span> <span class="o">=</span> <span class="nx">foo</span> <span class="o">===</span> <span class="kc">null</span> <span class="o">||</span> <span class="nx">foo</span> <span class="o">===</span> <span class="k">void</span> <span class="mi">0</span> <span class="p">?</span> <span class="k">void</span> <span class="mi">0</span> <span class="p">:</span> <span class="nx">foo</span><span class="p">.</span><span class="nx">bar</span><span class="p">)</span> <span class="o">===</span> <span class="kc">null</span> <span class="o">||</span> <span class="nx">_v</span> <span class="o">===</span> <span class="k">void</span> <span class="mi">0</span> <span class="p">?</span> <span class="k">void</span> <span class="mi">0</span> <span class="p">:</span> <span class="nx">_v</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span> </code></pre></div></div> <p>ポリフィルできることが分かれば <a href="https://github.com/ringtail003/yas-scaffold/blob/0348f7d276ca51fef996c4558ac17fa103a12947/eslint.config.js">eslint.config.js</a> で許容しておきましょう。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="p">[</span> <span class="p">{</span> <span class="p">...</span> <span class="na">rules</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">es/no-optional-chaining</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">off</span><span class="dl">"</span><span class="p">,</span> <span class="c1">// ルールをオフにする</span> <span class="p">},</span> <span class="p">}</span> <span class="p">];</span> </code></pre></div></div> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">v</span> <span class="o">=</span> <span class="nx">foo</span><span class="p">?.</span><span class="nx">bar</span><span class="p">?.</span><span class="nx">value</span><span class="p">;</span> <span class="c1">// 👍</span> </code></pre></div></div> <h2 id="シンタックスを禁止するeslintカスタムルール">シンタックスを禁止する(ESLintカスタムルール)</h2> <p>Yahoo!広告ランタイムでは <code class="language-plaintext highlighter-rouge">〜</code> という文字が使用できませんでした。コメントアウトの中に存在していても「使用できない文字が含まれている」と保存時にエラーになる事がありました。</p> <p>Yahoo!広告ランタイムの独自エラーのため、TypeScriptのコンパイルエラーでは検出できません。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">Logger</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">1〜100件のデータ取得完了</span><span class="dl">"</span><span class="p">);</span> <span class="c1">// コンパイルエラーにならない</span> </code></pre></div></div> <p>このようなケースでは <a href="https://eslint.org/docs/latest/extend/custom-rule-tutorial">Custom Rule Tutorial</a> を参考にしてESLintのカスタムルールを追加します。YAS開発環境では <a href="https://github.com/ringtail003/yas-scaffold/blob/0348f7d276ca51fef996c4558ac17fa103a12947/lint/plugin.js">plugin.js</a> にカスタムルール置き場を用意しています。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">plugin</span> <span class="o">=</span> <span class="p">{</span> <span class="na">meta</span><span class="p">:</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">eslint-plugin-no-wave-dash</span><span class="dl">"</span><span class="p">,</span> <span class="c1">// ルール名</span> <span class="na">version</span><span class="p">:</span> <span class="dl">"</span><span class="s2">1.0.0</span><span class="dl">"</span><span class="p">,</span> <span class="p">},</span> <span class="na">rules</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">no-wave-dash</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span> <span class="c1">// ルールの実装</span> <span class="nx">create</span><span class="p">(</span><span class="nx">context</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="p">{</span> <span class="nx">Literal</span><span class="p">(</span><span class="nx">node</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nx">node</span><span class="p">.</span><span class="nx">value</span> <span class="o">&amp;&amp;</span> <span class="k">typeof</span> <span class="nx">node</span><span class="p">.</span><span class="nx">value</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="nx">node</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">"</span><span class="s2">〜</span><span class="dl">"</span><span class="p">))</span> <span class="p">{</span> <span class="nx">context</span><span class="p">.</span><span class="nx">report</span><span class="p">({</span> <span class="nx">node</span><span class="p">,</span> <span class="na">message</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Yahoo!広告スクリプトで'〜'は使えません。</span><span class="dl">"</span><span class="p">,</span> <span class="p">});</span> <span class="p">}</span> <span class="p">...</span> </code></pre></div></div> <p>追加したカスタムルールは <a href="https://github.com/ringtail003/yas-scaffold/blob/0348f7d276ca51fef996c4558ac17fa103a12947/eslint.config.js#L48">eslint.config.js</a> で有効にします。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">plugin</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./lint/plugin.js</span><span class="dl">"</span><span class="p">;</span> <span class="k">export</span> <span class="k">default</span> <span class="p">[</span> <span class="p">{</span> <span class="na">plugins</span><span class="p">:</span> <span class="p">{</span> <span class="na">custom</span><span class="p">:</span> <span class="nx">plugin</span><span class="p">,</span> <span class="c1">// `custom/` プレフィクスを有効にする</span> <span class="p">...</span> <span class="p">},</span> <span class="na">rules</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">custom/no-wave-dash</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">error</span><span class="dl">"</span><span class="p">,</span> <span class="c1">// 禁止する</span> <span class="p">...</span> <span class="p">},</span> </code></pre></div></div> <p>このようにすると、Yahoo!広告スクリプトの独自エラーを検出できるようになります。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">Logger</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">1〜100件のデータ取得完了</span><span class="dl">"</span><span class="p">);</span> <span class="c1">// ~~~~~~~~~~~~~~~~~~~~~</span> <span class="c1">// ERROR: Yahoo!広告スクリプトで'〜'は使えません。eslint(custom/no-wave-dash)</span> </code></pre></div></div> <h1 id="既存プロジェクトへの展開">既存プロジェクトへの展開</h1> <p>セットアップ用スクリプトを使うことで、同じYAS開発環境のプロジェクトをいくつも作ることができます。</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npx ringtail003/yas-scaffold yas <span class="nt">--init</span> </code></pre></div></div> <p>Yahoo!広告ランタイムのアップデートでそれまで使えなかったシンタックスが使えるようになったり、Node.jsのアップデートで便利な機能が使えるようになるかもしれません。YAS開発環境もそれに合わせて進化していくでしょう。</p> <p>そんな時のために更新用のスクリプトも用意しています。設定ファイルを再びリポジトリからコピーして、パッケージ更新を促すだけのスクリプトです。</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npx quartetcom/yas-scaffold yas <span class="nt">--update</span> <span class="o">&gt;</span> 設定ファイルをコピーしました(eslint.config.js) <span class="o">&gt;</span> ... <span class="o">&gt;</span> <span class="o">&gt;</span> package.json <span class="s1">'devDependencies'</span> をインストールしてください <span class="o">&gt;</span> ┌─────────┬─────────────┬─────────┬──────────┐ <span class="o">&gt;</span> │ <span class="o">(</span>index<span class="o">)</span> │ name │ current │ required │ <span class="o">&gt;</span> ├─────────┼─────────────┼─────────┼──────────┤ <span class="o">&gt;</span> │ 0 │ <span class="s1">'package-A'</span> │ <span class="s1">'1.0.0'</span> │ <span class="s1">'3.0.0'</span> │ <span class="o">&gt;</span> └─────────┴─────────────┴─────────┴──────────┘ <span class="o">&gt;</span> npm <span class="nb">install</span> <span class="nt">-D</span> <span class="se">\</span> <span class="o">&gt;</span> [email protected] </code></pre></div></div> <p>実装は <a href="https://github.com/ringtail003/yas-scaffold/blob/0348f7d276ca51fef996c4558ac17fa103a12947/bin/yas-cli.js">yas-cli.js</a> に存在します。<a href="https://github.com/ringtail003/yas-scaffold/blob/0348f7d276ca51fef996c4558ac17fa103a12947/package.json#L29">package.json</a> に登録することでnpxでの実行が可能になります。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">"</span><span class="s2">bin</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">yas</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">./bin/yas-cli.js</span><span class="dl">"</span> <span class="p">}</span> </code></pre></div></div> <p>セットアップ用スクリプトで作成したプロジェクトでは、テストランナーの変更など個別のカスタマイズを想定していません。ベースとなるYAS開発環境のリポジトリに変更を加え更新用スクリプトで配布します。</p> <p>このような仕組みにすることで、プロジェクトの数が増えてもYahoo!広告ランタイムやNode.jsのアップデートにかんたんに追随できると考えました。</p> <h1 id="おわりに">おわりに</h1> <p>Yahoo!広告スクリプトに限らず、他のランタイムでも同じ仕組みで開発環境が構築できるのではないかと思います。</p> <p>今回紹介した内容がどなたかのお役に立てば幸いです。</p> Wed, 05 Mar 2025 00:00:00 +0900 PHPカンファレンス名古屋2025 プラチナスポンサー参加レポート | QUARTETCOM TECH BLOG PHP PHPカンファレンス イベント https://tech.quartetcom.co.jp/2025/03/03/phpcon-nagoya-2025-report/ https://tech.quartetcom.co.jp/2025/03/03/phpcon-nagoya-2025-report/ <p><a href="https://phpcon.nagoya/2025/" target="_blank">PHPカンファレンス名古屋2025</a>にプラチナスポンサーとして協賛しブース出展、スタッフとして参加しました!<br /> 初ブース出展で当日は運営スタッフとしての参加だったのでセッションは見ることができませんでしたが、PHPを扱う会社としてPHP業界に貢献できよかったです!<br /> 当日はイベント来場者がアンケートや企画参加のためにブースに来ていただきとても盛り上がったかなと思っています、ありがとうございました!<br /></p> <p><img src="/assets/img/2025-02-27/width840.jpg" /></p> <h2 id="ブースアンケート集計結果">ブースアンケート集計結果</h2> <h3 id="ソフトウェア開発においてai技術の活用は全体の何を占めていますか">ソフトウェア開発においてAI技術の活用は全体の何%を占めていますか?</h3> <p>アンケート内容の定義は以下の通りです。<br /></p> <ul> <li>縦軸:自分が所属しているチーム単位の人数<br /></li> <li>横軸:その業務でAI技術を活用しているパーセンテージ<br /></li> </ul> <p><img src="/assets/img/2025-02-27/width840_2.jpg" /></p> <p>開発規模は1~10人、AI利用率は0%~50%に多く分布していたものの、その中では大きな偏りがない結果となりました。<br /> 回答者の中には、AIの利用率が低いことで謙遜する方や、逆に利用率が高いことで謙遜する方が見受けられました。また、全体としてAIの活用が思ったより進んでいるという声が多くありました。</p> <h3 id="あなたの勤務スタイルは">あなたの勤務スタイルは?</h3> <p>最近ではフルリモート勤務からハイブリット勤務や出社勤務に戻す企業も増えている印象がありましたが、<br /> 結果を見ると、フルリモート勤務が多いという結果でした。</p> <p><img src="/assets/img/2025-02-27/width840_3.jpg" /></p> <h2 id="初ブース出展の感想">初ブース出展の感想</h2> <p>初ブース出展でしたが、PHP業界の方々と交流できてとても楽しかったです。<br /> WebサイトをPHPを使って構築・運用されている方などには、弊社で開発しているWeb広告レポートツール(<a href="https://lisket.jp/">Lisket</a>)やGoogleアナリティクスレポートツール(<a href="https://mugenreport.com/">無限GAレポートメーカー</a>) に関心を寄せてくださる参加者もいらっしゃいました。 そのような方々と直接話すことで、ブース出展を行う企業向けのスポンサープランならではの価値を実感する場面もありました。<br /> 個人的には勉強会への企業からのスポンサーは、企業PRとPHPコミュニティーへの恩返しの意味が強いのかなと思っています。<br /> PHPは古いと言われることも多々ありますが、多くのエンジニアに愛されている言語なんだなと実感しました。<br /> 恩返しという意味では、他にもOSSへの貢献(バグ報告やドキュメント改善など)やPHP Foundationへの寄付などがあると思いますが、PHPを使う企業に属する者としてコミュニティーへの貢献は継続的にしていけたらいいなと思います。</p> <h2 id="カルテット開発部について">カルテット開発部について</h2> <p>カルテット開発部は、インターネット広告専門の広告代理店内の開発部門です。<br /> 少人数の部署ですが、広告運用を効率化する <a href="https://lisket.jp" target="_blank">Lisket</a> とウェブサイト運営に欠かせないGoogleAnalyticsのレポートをエクセル形式で出力できる <a href="https://mugenreport.com" target="_blank">無限GAレポートメーカー</a> という2つのWebサービスを開発〜運営まで行っています。既存システムの保守だけでなく社内システムや新規事業などの新規プロダクトの開発も進行中で、直近プロダクトではスクラム開発も実施しておりました。<br /> フルリモート勤務も可能でライフワークバランスも大切にしているので、PHPを使うエンジニアの方々にも働きやすい環境を提供できると思います。<br /> オンラインカジュアル面談も随時受け付けているので、少しでも興味を持っていただいた方はぜひ<a href="https://quartetcom.co.jp/recruit/engineer/">お気軽にお問い合わせください</a>。</p> Mon, 03 Mar 2025 00:00:00 +0900 PHPカンファレンス名古屋2025にプラチナスポンサーとして協賛します | QUARTETCOM TECH BLOG PHP PHPカンファレンス イベント https://tech.quartetcom.co.jp/2025/02/20/phpcon-nagoya-2025/ https://tech.quartetcom.co.jp/2025/02/20/phpcon-nagoya-2025/ <p><a href="https://phpcon.nagoya/2025/" target="_blank">PHPカンファレンス名古屋2025</a>にプラチナスポンサーとして協賛いたします。<br /> 当日はブースも構えて会場に参加しているので、ぜひ立ち寄っていただければと思います。<br /></p> <p>名古屋でのPHPカンファレンス開催は今回が初めてでカルテットコミュニケーション開発部からは運営スタッフとしても参加します!<br /></p> <h2 id="phpカンファレンス名古屋について">PHPカンファレンス名古屋について</h2> <p>都市規模に比して不活発と言われているエンジニアコミュニティ活動を活発にしたい!<br /> 地理的な理由でこれまで各地のカンファレンスに参加できなかった地方在住のエンジニアの方の参加を通して、名古屋のエンジニアと全国のエンジニアの交流が促進され名古屋のエンジニアコミュニティにも新たな風を吹き込みたい!<br /> そんな思いから開催されるカンファレンスです。<br /></p> <p>【概要】<br /></p> <ul> <li>日時:2025年2月22日(土)<br /> (開場 9:30、本編 10:00〜18:00、懇親会 19:00〜21:00)<br /></li> <li>会場:ウインクあいち 11F<br /></li> <li>公式サイト:<a href="https://phpcon.nagoya/2025/">https://phpcon.nagoya/2025/</a> <br /></li> <li>公式Twitter:<a href="https://x.com/phpcon_nagoya">https://x.com/phpcon_nagoya</a></li> </ul> <h2 id="カルテット開発部について">カルテット開発部について</h2> <p>カルテット開発部は、インターネット広告専門の広告代理店内の開発部門です。<br /> 少人数の部署ですが、広告運用を効率化する <a href="https://lisket.jp" target="_blank">Lisket</a> とウェブサイト運営に欠かせないGoogleAnalyticsのレポートをエクセル形式で出力できる <a href="https://mugenreport.com" target="_blank">無限GAレポートメーカー</a> という2つのWebサービスを開発〜運営まで行っています。既存システムの保守だけでなく社内システムや新規事業などの新規プロダクトの開発も進行中で、直近プロダクトではスクラム開発も実施しておりました。<br /> PHPer絶賛募集中です!少しでも興味を持っていただいた方はぜひオフライン・オンライン両会場で弊社エンジニアにお声掛けください。</p> Thu, 20 Feb 2025 00:00:00 +0900 同一 MySQL サーバ上の複数データベースに対する権限設定 - Ansible mysql_user モジュールで実装する際の注意点 | QUARTETCOM TECH BLOG Ansible MySQL 構成管理 Docker https://tech.quartetcom.co.jp/2025/02/14/ansible-mysql_user-test-tool/ https://tech.quartetcom.co.jp/2025/02/14/ansible-mysql_user-test-tool/ <h2 id="結論">結論</h2> <p>同一 MySQL サーバに共存させた複数データベースに存在するユーザーに対して任意の権限を付与するには、<strong><code class="language-plaintext highlighter-rouge">/</code> を使う必要があります。</strong> <br /> Ansible の <code class="language-plaintext highlighter-rouge">mysql_user</code> モジュールでは、同じユーザーに対して複数回 <code class="language-plaintext highlighter-rouge">priv</code> を指定すると、後から指定した値が優先されるからです。</p> <p>この挙動を整理した時の経緯を紹介します。</p> <h2 id="何で困っていたか">何で困っていたか</h2> <p>同一サーバに複数のデータベースを作成し、 Ansible を使ったユーザー作成と権限管理を行なっていましたが、この際の挙動を捉えるのに苦労しました。</p> <p>例えば、user1 に対する以下のような設定を想定しているとします。</p> <ul> <li><code class="language-plaintext highlighter-rouge">database_a</code> の全てのテーブルに対する <code class="language-plaintext highlighter-rouge">SELECT</code> 権限</li> <li><code class="language-plaintext highlighter-rouge">database_b</code> の全てのテーブルに対する <code class="language-plaintext highlighter-rouge">ALL</code> 権限</li> </ul> <p>この想定で playbook.yml を次のように記述した場合</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="pi">-</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">user1</span><span class="pi">,</span> <span class="nv">state</span><span class="pi">:</span> <span class="nv">present</span><span class="pi">,</span> <span class="nv">host</span><span class="pi">:</span> <span class="s1">'</span><span class="s">localhost'</span><span class="pi">,</span> <span class="nv">priv</span><span class="pi">:</span> <span class="s1">'</span><span class="s">database_a.*:SELECT'</span><span class="pi">,</span> <span class="nv">password</span><span class="pi">:</span> <span class="nv">password1</span> <span class="pi">}</span> <span class="pi">-</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">user1</span><span class="pi">,</span> <span class="nv">state</span><span class="pi">:</span> <span class="nv">present</span><span class="pi">,</span> <span class="nv">host</span><span class="pi">:</span> <span class="s1">'</span><span class="s">localhost'</span><span class="pi">,</span> <span class="nv">priv</span><span class="pi">:</span> <span class="s1">'</span><span class="s">database_b.*:ALL'</span><span class="pi">,</span> <span class="nv">password</span><span class="pi">:</span> <span class="nv">password1</span> <span class="pi">}</span> </code></pre></div></div> <p>Ansible による実行結果は以下のようになり、<code class="language-plaintext highlighter-rouge">user1</code> に関する <code class="language-plaintext highlighter-rouge">database_a</code> への権限を付与できませんでした。</p> <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mysql</span><span class="o">&gt;</span> <span class="k">SELECT</span> <span class="k">Host</span><span class="p">,</span> <span class="k">User</span><span class="p">,</span> <span class="n">Db</span><span class="p">,</span> <span class="n">Select_priv</span><span class="p">,</span> <span class="n">Update_priv</span> <span class="k">FROM</span> <span class="n">mysql</span><span class="p">.</span><span class="n">db</span><span class="p">;</span> <span class="o">+</span><span class="c1">-----------+---------------+--------------------+-------------+-------------+</span> <span class="o">|</span> <span class="k">Host</span> <span class="o">|</span> <span class="k">User</span> <span class="o">|</span> <span class="n">Db</span> <span class="o">|</span> <span class="n">Select_priv</span> <span class="o">|</span> <span class="n">Update_priv</span> <span class="o">|</span> <span class="o">+</span><span class="c1">-----------+---------------+--------------------+-------------+-------------+</span> <span class="o">|</span> <span class="n">localhost</span> <span class="o">|</span> <span class="n">mysql</span><span class="p">.</span><span class="k">session</span> <span class="o">|</span> <span class="n">performance_schema</span> <span class="o">|</span> <span class="n">Y</span> <span class="o">|</span> <span class="n">N</span> <span class="o">|</span> <span class="o">|</span> <span class="n">localhost</span> <span class="o">|</span> <span class="n">mysql</span><span class="p">.</span><span class="n">sys</span> <span class="o">|</span> <span class="n">sys</span> <span class="o">|</span> <span class="n">N</span> <span class="o">|</span> <span class="n">N</span> <span class="o">|</span> <span class="o">|</span> <span class="n">localhost</span> <span class="o">|</span> <span class="n">user1</span> <span class="o">|</span> <span class="n">database_b</span> <span class="o">|</span> <span class="n">Y</span> <span class="o">|</span> <span class="n">Y</span> <span class="o">|</span> <span class="o">+</span><span class="c1">-----------+---------------+--------------------+-------------+-------------+</span> </code></pre></div></div> <p>実行順序を入れ替えても</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="pi">-</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">user1</span><span class="pi">,</span> <span class="nv">state</span><span class="pi">:</span> <span class="nv">present</span><span class="pi">,</span> <span class="nv">host</span><span class="pi">:</span> <span class="s1">'</span><span class="s">localhost'</span><span class="pi">,</span> <span class="nv">priv</span><span class="pi">:</span> <span class="s1">'</span><span class="s">database_b.*:ALL'</span><span class="pi">,</span> <span class="nv">password</span><span class="pi">:</span> <span class="nv">password1</span> <span class="pi">}</span> <span class="pi">-</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">user1</span><span class="pi">,</span> <span class="nv">state</span><span class="pi">:</span> <span class="nv">present</span><span class="pi">,</span> <span class="nv">host</span><span class="pi">:</span> <span class="s1">'</span><span class="s">localhost'</span><span class="pi">,</span> <span class="nv">priv</span><span class="pi">:</span> <span class="s1">'</span><span class="s">database_a.*:SELECT'</span><span class="pi">,</span> <span class="nv">password</span><span class="pi">:</span> <span class="nv">password1</span> <span class="pi">}</span> </code></pre></div></div> <p>以下のような結果となり、やはり <code class="language-plaintext highlighter-rouge">user1</code> に関してデータベースごとに異なる権限を付与できませんでした。</p> <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mysql</span><span class="o">&gt;</span> <span class="k">SELECT</span> <span class="k">Host</span><span class="p">,</span> <span class="k">User</span><span class="p">,</span> <span class="n">Db</span><span class="p">,</span> <span class="n">Select_priv</span><span class="p">,</span> <span class="n">Update_priv</span> <span class="k">FROM</span> <span class="n">mysql</span><span class="p">.</span><span class="n">db</span><span class="p">;</span> <span class="o">+</span><span class="c1">-----------+---------------+--------------------+-------------+-------------+</span> <span class="o">|</span> <span class="k">Host</span> <span class="o">|</span> <span class="k">User</span> <span class="o">|</span> <span class="n">Db</span> <span class="o">|</span> <span class="n">Select_priv</span> <span class="o">|</span> <span class="n">Update_priv</span> <span class="o">|</span> <span class="o">+</span><span class="c1">-----------+---------------+--------------------+-------------+-------------+</span> <span class="o">|</span> <span class="n">localhost</span> <span class="o">|</span> <span class="n">mysql</span><span class="p">.</span><span class="k">session</span> <span class="o">|</span> <span class="n">performance_schema</span> <span class="o">|</span> <span class="n">Y</span> <span class="o">|</span> <span class="n">N</span> <span class="o">|</span> <span class="o">|</span> <span class="n">localhost</span> <span class="o">|</span> <span class="n">mysql</span><span class="p">.</span><span class="n">sys</span> <span class="o">|</span> <span class="n">sys</span> <span class="o">|</span> <span class="n">N</span> <span class="o">|</span> <span class="n">N</span> <span class="o">|</span> <span class="o">|</span> <span class="n">localhost</span> <span class="o">|</span> <span class="n">user1</span> <span class="o">|</span> <span class="n">database_a</span> <span class="o">|</span> <span class="n">Y</span> <span class="o">|</span> <span class="n">N</span> <span class="o">|</span> <span class="o">+</span><span class="c1">-----------+---------------+--------------------+-------------+-------------+</span> </code></pre></div></div> <p>上記以外にも、カンマやリストを使った記述方法も試しましたが、いずれも望んだ結果を得られませんでした。</p> <h2 id="どうやって解決したか">どうやって解決したか</h2> <p>公式情報をちゃんと読み、簡単な検証ツールを作って検証しました。 最終的に以下のような記述により、期待した結果を得ました。</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="pi">-</span> <span class="pi">{</span> <span class="nv">name</span><span class="pi">:</span> <span class="nv">user1</span><span class="pi">,</span> <span class="nv">state</span><span class="pi">:</span> <span class="nv">present</span><span class="pi">,</span> <span class="nv">host</span><span class="pi">:</span> <span class="s1">'</span><span class="s">localhost'</span><span class="pi">,</span> <span class="nv">priv</span><span class="pi">:</span> <span class="s1">'</span><span class="s">database_a.*:SELECT/database_b.*:ALL'</span><span class="pi">,</span> <span class="nv">password</span><span class="pi">:</span> <span class="nv">password1</span> <span class="pi">}</span> </code></pre></div></div> <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mysql</span><span class="o">&gt;</span> <span class="k">SELECT</span> <span class="k">Host</span><span class="p">,</span> <span class="k">User</span><span class="p">,</span> <span class="n">Db</span><span class="p">,</span> <span class="n">Select_priv</span><span class="p">,</span> <span class="n">Update_priv</span> <span class="k">FROM</span> <span class="n">mysql</span><span class="p">.</span><span class="n">db</span><span class="p">;</span> <span class="o">+</span><span class="c1">-----------+---------------+--------------------+-------------+-------------+</span> <span class="o">|</span> <span class="k">Host</span> <span class="o">|</span> <span class="k">User</span> <span class="o">|</span> <span class="n">Db</span> <span class="o">|</span> <span class="n">Select_priv</span> <span class="o">|</span> <span class="n">Update_priv</span> <span class="o">|</span> <span class="o">+</span><span class="c1">-----------+---------------+--------------------+-------------+-------------+</span> <span class="o">|</span> <span class="n">localhost</span> <span class="o">|</span> <span class="n">mysql</span><span class="p">.</span><span class="k">session</span> <span class="o">|</span> <span class="n">performance_schema</span> <span class="o">|</span> <span class="n">Y</span> <span class="o">|</span> <span class="n">N</span> <span class="o">|</span> <span class="o">|</span> <span class="n">localhost</span> <span class="o">|</span> <span class="n">mysql</span><span class="p">.</span><span class="n">sys</span> <span class="o">|</span> <span class="n">sys</span> <span class="o">|</span> <span class="n">N</span> <span class="o">|</span> <span class="n">N</span> <span class="o">|</span> <span class="o">|</span> <span class="n">localhost</span> <span class="o">|</span> <span class="n">user1</span> <span class="o">|</span> <span class="n">database_a</span> <span class="o">|</span> <span class="n">Y</span> <span class="o">|</span> <span class="n">N</span> <span class="o">|</span> <span class="o">|</span> <span class="n">localhost</span> <span class="o">|</span> <span class="n">user1</span> <span class="o">|</span> <span class="n">database_b</span> <span class="o">|</span> <span class="n">Y</span> <span class="o">|</span> <span class="n">Y</span> <span class="o">|</span> <span class="o">+</span><span class="c1">-----------+---------------+--------------------+-------------+-------------+</span> </code></pre></div></div> <h3 id="公式ドキュメント">公式ドキュメント</h3> <blockquote> <p>Multiple privileges can be specified by separating each one using a forward slash: db.table1:priv/db.table2:priv.</p> <p><a href="https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_user_module.html#parameter-priv">https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_user_module.html#parameter-priv</a></p> </blockquote> <h3 id="検証ツール">検証ツール</h3> <p>こちらは GitHub に公開しましたので、興味のある方はご活用ください。 このツールは MySQL に対する Ansible の挙動を Docker を使って確認するのに役立ちます。</p> <p><a href="https://github.com/Tatsumi-I/ansible-mysql_user-test-tool">https://github.com/Tatsumi-I/ansible-mysql_user-test-tool</a></p> <h2 id="まとめ">まとめ</h2> <ul> <li>同一ユーザーへ複数回 <code class="language-plaintext highlighter-rouge">priv</code> を指定した際の挙動を理解していないと、想定した権限を付与できないことがあります。</li> <li>同一サーバに共存する複数データベースにおけるユーザーへ任意の権限を付与するには、<code class="language-plaintext highlighter-rouge">/</code> を使う必要があります。</li> </ul> <p>また、Ansible における変数の扱いやその優先度も併せて把握することが重要です。</p> <blockquote> <p>In general, Ansible gives precedence to variables that were defined more recently, more actively, and with more explicit scope. Variables in the defaults folder inside a role are easily overridden. Anything in the vars directory of the role overrides previous versions of that variable in the namespace.</p> <p><a href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html#understanding-variable-precedence">https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html#understanding-variable-precedence</a></p> </blockquote> <p>どんなツールでもとっかかりは難しいですが、使えると開発者体験が大きく向上しますね。 これがその一助となりましたら幸いです。</p> <h2 id="参考検証環境">(参考)検証環境</h2> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker <span class="nt">--version</span> Docker version 27.4.0, build bde2b89 </code></pre></div></div> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># mysql --version</span> mysql Ver 8.0.37 <span class="c"># ansible --version</span> ansible <span class="o">[</span>core 2.13.13] </code></pre></div></div> Fri, 14 Feb 2025 00:00:00 +0900 意思決定を未来へ届ける | QUARTETCOM TECH BLOG 意思決定 https://tech.quartetcom.co.jp/2025/01/07/delivering-system-design-documents-to-the-future/ https://tech.quartetcom.co.jp/2025/01/07/delivering-system-design-documents-to-the-future/ <h2 id="はじめに">はじめに</h2> <p>開発部の 有澤 です。</p> <p>数ヶ月ほど前に、社内アプリケーションの開発責任者(いわゆるプロダクトマネージャー)になりました。 役割を通じて、ステークホルダーと関わることが増え、既存システムに機能を加える機会が増えてきました。</p> <p>私は新たな要望に応えるため、既存システムと新規機能を比較し、「どのような設計で実現できるか」を検討する機会が増えましたが、 その過程で一つの懸念が浮かびました。</p> <h2 id="システム設計書は時間と共に風化する">システム設計書は時間と共に風化する</h2> <p>私がこれまでに触れてきたシステム設計書には、さまざまな形式がありました。</p> <ul> <li>Excel</li> <li>PowerPoint</li> <li>ナレッジベース</li> <li>コードベース</li> <li>人の頭の中</li> <li>GitHub Issue</li> <li>Pull Request</li> <li>Commit メッセージ</li> <li>Slack メッセージ</li> <li>紙</li> </ul> <p>これらはチームや組織の状況に応じて形が変わるもので、どれが優れているかは一概に言えません。</p> <p>しかし、共通する課題があります。それは、時間とともに「リンク切れ」「検索不可」「消失」などの問題が発生し、情報が風化する点です。 また、これらの情報が分散しているため、仕様を再確認する作業がまるで「地層から化石を掘り起こし、全体像を再構築する」ような作業が必要だったりします。</p> <p>私はこういった情報の復元に慣れていますが、面倒であるのは確かです。<br /> このような状況を自分も生み出してしまうのかと思うと、どうにか解決したいと考えるようになりました。</p> <h2 id="未来人はシステム設計書で何を知りたいのか">未来人はシステム設計書で何を知りたいのか</h2> <p>未来の開発者が何を知りたいかを完全に予測することは難しいです。</p> <p>ですが、自分やチームが「何が必要か?」を考えたり、話し合ったりはできます。<br /> 自分やチームが必要なものが書かれていれば、少なからず未来人は意思決定の参考になるはずです。</p> <p>考えるに、以下のようなことだと定義しました。</p> <ul> <li>なぜこの設計を採用したか?</li> <li>他に代替案はなかったか?</li> <li>この設計はシステムへ適応済みか?</li> <li>適応したかった設計はあるか?</li> <li>大小限らず、発見した技術的負債はあるか?</li> </ul> <p>これらをまとめると、<code class="language-plaintext highlighter-rouge">「意思決定の判断材料」として機能する情報</code> が必要だと考えました。</p> <p>この情報があれば、未来の開発者がシステム変更時にスムーズな判断を下せるようになるはずです。</p> <h2 id="adr-を採用しました">ADR を採用しました</h2> <p>ADR は比較的軽量なフレームワークだったのもあり、こちらを採用することにしました。</p> <p>とはいえ ADR のテンプレートにしっかり準拠するのではなくて、 必要な情報を箇条書きで書ける独自テンプレートを使用することにしました。</p> <p>ドキュメントを書くためのフレームワークを調べたところ、以下がありました。</p> <ul> <li>Design Doc <ul> <li>システムや機能の詳細な設計を記録したドキュメントで、特定の技術的なソリューションや実装について深く掘り下げているのが特徴。</li> <li><a href="https://www.industrialempathy.com/posts/design-docs-at-google/">https://www.industrialempathy.com/posts/design-docs-at-google/</a></li> </ul> </li> <li>Architecture Decision Records (ADR) <ul> <li>システム設計における重要な意思決定を記録するための簡潔なドキュメント。</li> <li><a href="https://adr.github.io/">https://adr.github.io/</a></li> </ul> </li> <li>Lightweight Architecture Decision Records (LADR) <ul> <li>ADRの簡易版で、より軽量な記録形式。小規模なプロジェクトや頻繁な意思決定の記録に適してそう。</li> <li><a href="https://github.com/peter-evans/lightweight-architecture-decision-records?tab=readme-ov-file">https://github.com/peter-evans/lightweight-architecture-decision-records?tab=readme-ov-file</a></li> </ul> </li> </ul> <h2 id="adr-の配置">ADR の配置</h2> <p>PHP アプリケーションのルート直下に doc/adr/ を作成して、その中にマークダウンファイルを格納しています。</p> <p>エディタ上で閲覧・編集が簡潔するので開発者的には嬉しいですし、 バージョン管理されるため、ドキュメントがロストしにくくなります。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PHP Application (root) └── doc └── adr ├── 2024-08-23-依存ライブラリの見直し.md ├── 2024-11-29-◯◯機能の縮小.md ├── 2024-12-17-ADRの導入.md ├── 2024-12-18-◯◯機能の再設計.md └── README.md └── public └── config └── .dockerignore └── etc... </code></pre></div></div> <p>アプリケーションデプロイ時にドキュメントが増えることを避けたい場合は、.dockerignore で doc/ を無視することを推奨します。</p> <h2 id="adr-のテンプレート">ADR のテンプレート</h2> <p>必要な情報を最小限に集めたいので、以下のテンプレートを定義しました。</p> <div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gu">## 背景(Background)</span> <span class="p">-</span> 問題の概要と目的。 <span class="p">-</span> システムが解決しようとする課題。 <span class="gu">## 目標と非目標(Goals and Non-Goals)</span> <span class="p">-</span> 目標: このプロジェクトで達成すべきことを具体的に記述。 <span class="p">-</span> 非目標: このプロジェクトでは対応しないことを明確にしてスコープを限定。 <span class="gu">## 提案内容(Proposal)</span> <span class="p">-</span> 問題を解決するアプローチ。 <span class="p">-</span> 技術的な選択肢や、それぞれのメリット・デメリット。 <span class="gu">## 技術設計(Technical Design)</span> <span class="p">-</span> システムのアーキテクチャ。 <span class="p">-</span> データベース設計やAPI仕様の詳細。 <span class="p">-</span> PHPフレームワークや主要ライブラリの選定理由。 <span class="gu">## 代替案(Alternatives Considered)</span> <span class="p">-</span> 他に検討したアプローチと採用しなかった理由。 <span class="p">-</span> リスクとトレードオフ(Risks and Tradeoffs) <span class="p">-</span> 現状では想定しづらいリスクや、トレードオフについて明記。 <span class="gh"># マイルストーンとスケジュール(Milestones and Timeline)</span> <span class="p">-</span> プロジェクトのスケジュールと各フェーズのゴール。 <span class="p">-</span> 未解決の問題(Unresolved Questions) <span class="p">-</span> 現段階では結論に至っていない内容。 </code></pre></div></div> <p>そもそも、ドキュメントを書くことは開発者のオーバーヘッドです。 見出しに対して文字を埋めることを強要せず、必要な情報だけを書ける柔軟な形式を理想としています。</p> <h2 id="adr-の内容">ADR の内容</h2> <p>まずは「ADRを導入した際の意思決定」を書くのがスタートに丁度よいです。 実際の物よりもボカシていますが、以下のように書きました。</p> <div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># 2024-12-17 ADRの導入</span> <span class="gu">## 背景(Background)</span> <span class="p">-</span> ◯◯ の設計や機能を変更する上で、変更箇所や理由、意思決定が継承されづらい状況であることが懸念だった。 <span class="gu">## 目標と非目標(Goals and Non-Goals)</span> <span class="gu">### 目標</span> <span class="p">-</span> ADRを導入することで、意思決定を継承する。 <span class="p"> -</span> 初期段階で完璧なADRは目指さないで、まずは簡単な形で導入を目指す。 <span class="gu">### 非目標</span> <span class="p">-</span> なし <span class="gu">## 提案内容(Proposal)</span> <span class="p">-</span> ADRを導入 <span class="p"> -</span> コードと一緒に保管される形容なら、プロダクトが生き続く限り残るのでベストと判断した。 <span class="p"> -</span> エディタ内で完結することもDX的に良い。 <span class="gu">## 代替案(Alternatives Considered)</span> <span class="p">-</span> Notionやその他のナレッジベースで管理すること <span class="p"> -</span> リンク切れを起こしたり、そもそも形容として形骸化しやすいので避けた。 </code></pre></div></div> <h2 id="adr-のメンテナンス">ADR のメンテナンス</h2> <p>「必要だと思ったり、便利だと感じる人が増えたら必然的に良いADRが残るはず」と期待しているので、 メンテナンスに関するルールは定義していません。</p> <p>また、ドキュメントをバージョン管理することのデメリットの一つとして「Pull Requestをレビューする必要」がありますが、 doc/ ディレクトリ配下はレビュー不要とすることで、ドキュメントのメンテナンスのハードルを下げても良いと感じています。</p> <h2 id="所感">所感</h2> <p>書いた自分でも後々思うのは、代替案が書いてあるのは凄く便利です。</p> <p>新たに機能を開発した後「なぜこうしなかったのか?」と話が上がることは、プロダクト開発においてよくあります。 その際に自分の記憶が曖昧になっていたり、そもそも自分が退職してしまった時に説明できないのは問題だと感じていたので、 ADR を通してプロダクトの持続可能性を高められたら嬉しいです。</p> Tue, 07 Jan 2025 00:00:00 +0900