狛ログ

2022年2月24日木曜日

【JavaScript】メールテンプレート内のプレースホルダーを独自に式展開(置換)する方法。

オフィス狛 技術部のyuckieee(ゆっきー)です。

■はじめに

今回は、私が関わったプロジェクトで「ユーザがメールテンプレートを自由に編集できるようにしたい!」という要望を受けて、その実現方法を検討・実装した際のお話です。
要件実現にあたって使用したのがJavaScriptの「replace」という関数なのですが、この関数が非常に使える子で...ふるえました(,,゚Д゚)。 ←初心者感丸出しなのと語彙力(笑)

今後も活用する場面がありそうなので、自分の理解向上と備忘録としてコチラにまとめます。

■今回の要件整理

最初に話したとおり、今回の要望はメールテンプレートをユーザが自由に編集・管理したいというものです。また、この要件には「メールの宛先によって一部の内容(名前とか、契約内容とか)はシステムで可変となるようにしたい」という要望も含まれています。
なので、ざっくりこんな感じの要件です。

[要件箇条書き]
     ①メールテンプレートは外部に保存可能とし、ユーザが自由に編集できるようにして欲しい
       ②ユーザが可変指定した箇所は、システムで自動的に置き換えて欲しい

      可変にしたいと言うのは、メールテンプレート内にプレースホルダーを埋め込みたいというイメージです。 そのため、イメージとしては、以下のようにユーザがメールテンプレートを定義したらプレースホルダー(${})部分は、該当する情報に置き換えられて欲しいという感じです。
      (まんま、JavaScriptのテンプレートリテラル内で式展開させる感じと同じです。)

      ■メールテンプレート
      ${last_name} {first_name} 様
      この度は、${company_name}の${plan_name}にご登録いただき、ありがとうございました。
       ↓
      ■メール送信内容
      オフ狛 ゆっきー 様
      この度は、株式会社オフ狛のスペシャルプランにご登録いただき、ありがとうございました。
      

      ■検討結果と実装内容

      テンプレートリテラレルを使えば解決じゃん?と思うのですが、外部から読み込んだ文字列にプレースホルダーが書いてあったとしても式展開されず、単なる文字列になっちゃうんですよね....(´・ω・)
      まぁ、そうですよね、、、ということで、結果、独自にプレースホルダー(${})を置換する関数を作ることにしました。それがコチラ↓

      /**
       * テンプレート置換処理
       * @param  {string} template // 外部から読み込んだメールテンプレート文字列
       * @param  {object} jsonObj  // メールテンプレート内のプレースホルダー置換対象情報
       */
       const templateRepalcer = (template, jsonObj) 
                               => template.replace(/\${([^}]+)}/g, (_, prop) => jsonObj[prop]);
      

      以上です!

      ■実装関数の使用例

      つづいて、実装した関数を使用して簡単な使い方例を以下に紹介します。

      // メールテンプレートのテンプレートリテラレルを顧客情報に置き換える関数を定義
       const templateRepalcer = (template, jsonObj) 
                               => template.replace(/\${([^}]+)}/g, (_, prop) => jsonObj[prop]);
      
      // メールテンプレート(!!実際は外部(DBなど)から読み込む!!)
      const mailTempalte = "${last_name} ${first_name} 様\nこの度は、株式会社オフ狛の${plan_name}にご登録いただきありがとうございました。"
      
      // 顧客情報(!!実際は外部(DBなど)から読み込む!!)
      const userInfo = {
          "user_id": "ID00001",
          "last_name": "オフ狛",
          "first_name": "ゆっきー",
          "plan_name": "スペシャルプラン",
          "mail_address": "yuckieee@officekoma.co.jp"
      }
      
      // メールテンプレートのプレースホルダーを顧客情報に入れ替え
      const mailContents = templateRepalcer(mailTempalte, userInfo);
      
      // 出来上がった変数を本文に編集すれば出来上がり!(この例ではコンソール出力のみ)
      console.log(mailContents);
      

      上記実行の結果、コンソールに出力される内容は以下のとおりです。

      オフ狛 ゆっきー 様
      この度は、株式会社オフ狛のスペシャルプランにご登録いただきありがとうございました。
      

      以降、実装関数について詳細を解説していこうと思います。

      ■関数の詳細説明

      この関数を説明する前にreplace関数についての知識が必要ですが、こちらの詳細は公式(?)ページを参照された方が分かりやすいですし、ここでの説明を割愛します。

      [replace関数の説明サイト]
      MDN Web Docs:String.prototype.replace()
      JavaScript Primer:文字列の置換/削除

      作成した関数は2つあり、replace関数を呼ぶ大元のtemplateReplacerという関数と、replace関数の第二引数で使用する無名関数です。 templateReplacer自体は、テンプレートと差し替えデータを引数に、replace関数を呼び出しているだけですので説明は省略します。
      以下より、replace関数の第2引数で指定している無名関数に焦点をあてて説明したいと思います。

      まず、replace関数全体を眺めてると、以下のとおり、第2引数は、第1引数の結果を引き継いで処理を行う作りとなっていることが分かります。

      template.replace(/\${([^}]+)}/g, (_, prop) => jsonObj[prop]);
      [置換の母体となる文字列].replace(第1引数:[置換対象を正規表現で指定], 第2引数:[第一引数の結果を元に置換処理]);

      無名関数を説明する上で、第1引数も重要となってくるため、第1引数と第2引数の両方を順に説明します。

      第1引数: 置換対象の文字列又は、正規表現を指定

      replace関数の第1引数には、置換対象にしたい文字列を固定で指定するか、正規表現を使って指定する事ができます。
      今回は、汎用性を高めるために正規表現を使用しています。
      なお、/\${([^}]+)}/の箇所は、書き方は色々あると思うので、正規表現をググってみてください。
      .?*とかでも行けると思います。
      ()表記については、第2引数内で説明します。正規表現による抽出とは直接関わりません。

      また、/gを指定することで置換の母体となる文字列全体での抽出が可能になります。
      この指定を行わない場合は、対象が複数あった場合でも、最初に正規表現にひっかかった1件だけが置換される結果となるので注意が必要です。

      第2引数: 置換する文字列又は、置換処理を指定

      第2引数には、置換後の文字列を固定で指定するか、処理自体を指定することが出来ます。今回紹介した関数では、第2引数に無名関数を定義して、置換処理を行う形をとっています。
      なお、第1引数での結果をもとに、以下の引数を無名関数に引き渡すことが可能です。
      ※ブラウザによっては、5つ引数があるようですが、ここでは説明対象外とします。

      (match, p1, p2..., offset, string )

      それぞれの具体的な値について説明します。
      ※実装例で上げたメールテンプレート中の${last_name}を例に上げて説明します
      引数 説明
      match 第1引数の正規表現にマッチした文字列が設定されます。
      今回の例で言うところの${last_name}が該当箇所です。なお、今回使用しないため、関数では_と表記しています。
      p1,p2... 第1引数でキャプチャした文字列が引数として渡されます。
      キャプチャは、第1引数の正規表現内でキャプチャしたい箇所を()で囲むことで指定が可能です。
      複数指定した場合は、指定した分引数が増えていきます。今回は1対1で置換したかったので1つだけ指定しています。
      今回の例ではlast_nameという箇所が抜き出されることになります。
      offset 第1引数の正規表現でヒットした文字列の最初の位置を返します。${last_name}であれば、開始位置の0がセットされる感じです。今回は使用していません。
      string 検索対象全体がセットされます。今回でいうと"${last_name} ${first_name} 様\nこの度は、株式会社狛の"${plan_name}にご登録いただきありがとうございました。"です。今回は使用していません。

      上記の引数を元に置換対象を指定していますが、やっていることはJSONのキーを指定して、キーに紐づく値を取得しているだけです。

      (_, prop) => jsonObj[prop]

      今回の例では、以下のようなJSONが与えられていますので、第一引数でキャプチャしたキー(last_name)を元に、JSONのキーに紐づく値(オフ狛)を取得し、それを置換対象としています。

      {
          "user_id": "ID00001",
          "last_name": "オフ狛",
          "first_name": "ゆっきー",
          "plan_name": "狛スタンダードプラン",
          "mail_address": "yuckieee@officekoma.co.jp"
      }
      

      ■まとめ

      私は、今までreplace関数を単純に置換してくれるだけの関数ででしょ?って思っていました。
      ですが、こんなに汎用性の高い関数であるということを知って、目からウロコ...本当にreplace関数さん申し訳ありません...!!!ってなりました^^;
      特に引数として関数を指定するというスタイルは、自分の作る関数にも活かして行けると面白いなと感じます。

カラム内の区切り文字でデータを分割して取得する方法(SQLServer)。

オフィス狛 技術部のHammarです。

ある開発で、テーブルのカラムにカンマ区切りで値が入っているデータがあるのですが、ここからカンマ区切りで1つのレコードとして分割してデータ抽出したいと要望がありました。
取得イメージ的には、例えば以下のようなfruits_boxテーブルがあったとします。

+----+-------+-----------------------+
| id | name  | fruits                |
+----+-------+-----------------------+
|  1 | boxA  | apple,grape           |
|  2 | boxB  | apple,orange,banana   |
|  3 | boxC  | banana                |
+----+-------+-----------------------+
こちらから

+----+-------+-----------------------+
| id | name  | fruits                |
+----+-------+-----------------------+
|  1 | boxA  | apple                 |
|  1 | boxA  | grape                 |
|  2 | boxB  | apple                 |
|  2 | boxB  | orange                |
|  2 | boxB  | banana                |
|  3 | boxC  | banana                |
+----+-------+-----------------------+
のように取得する感じです。

カラム内にカンマ区切りでデータを入れることは結構あると思います。ただ、そのデータをレコードとして分割して出力することがなかったのですが、今回調べてみて意外と簡単な方法があったのでちょっと書いてみたいと思います。 RDBMSによってそれぞれ書き方は異なるようですが、開発でSQLServerを使っていたので、今回はそのSQLServerで上記のような構成でデータを取得する方法となります。

■STRING_SPLIT関数

まずSQLserverで、区切り文字で分割して結果を取得する方法としては、STRING_SPLIT関数を使う方法があります。
STRING_SPLIT関数は、
STRING_SPLIT(文字列, 区切り文字)
の方法で利用することができます。
使い方としてはこんな感じです。

> SELECT * FROM STRING_SPLIT('apple,orange,banana', ',');
+-----------+
| value     |
+-----------+
| apple     |
| orange    |
| banana    |
+-----------+

これはこれで使えそうなんですが、そもそもこれを使うには1レコード対象にしか使えません。
なので、今回のような複数レコードが存在する場合にはちょっと無理そうです。

■CROSS APPLY句

ということで、もう1つの方法として、「CROSS APPLY」という結合演算子と合体技で取得する方法です。
APPLY句については今回細かく記載は省きますが、テーブルから取得したデータをテーブル値関数の結果として組み合わせて使いたい場合にこのAPPLYという演算子は有効です。

これを使ってSQLを書くと以下のように書くことが出来ます。
> SELECT * 
> FROM fruits_box AS FB
> CROSS APPLY STRING_SPLIT(fruits, ','); 
+----+-------+-----------------------+
| id | name  | fruits                |
+----+-------+-----------------------+
|  1 | boxA  | apple                 |
|  1 | boxA  | grape                 |
|  2 | boxB  | apple                 |
|  2 | boxB  | orange                |
|  2 | boxB  | banana                |
|  3 | boxC  | banana                |
+----+-------+-----------------------+
という感じで、ほしいデータの結果を得ることが出来ました。
結構簡単な記述でいけますね!

1点注意点としては、上記テーブルのfruitsにNULLがあった場合、その結果は返ってこないので、もしNULLも結果として返したい場合は、「OUTER APPLY」を使うことでNULLレコードも結果として得ることができるようになります。

もし上記のような、カラム内の区切り文字で、それぞれレコードとしてデータ取得が必要な場合にはご参考にしてみてください。

SQL Serverで複数カラム(NULL許可のカラムを含む)のユニーク制約を設定する方法。


オフィス狛 技術部のJoeです。

データベースで複数カラムを組み合わせたユニーク制約の設定が可能ですが、データベースによって、NULL値を重複として扱うかどうかが違っています。

例えばMySQLは、NULL値を重複として扱いませんが、SQL Serverは、NULL値を重複した値として扱います。

今回は、SQL ServerでNULL値を含む複数カラムの組み合わせで、ユニーク制約を設定する方法をご紹介したいと思います。
※下記の内容はSQL Server 2017で確認しています

まず、下記のようなテーブルを作成、データを追加します。

【テーブル作成】
CREATE TABLE fish
(
  id bigint IDENTITY(1,1) NOT NULL,
  name nvarchar(10) NOT NULL,
  code varchar(2) NULL,
  PRIMARY KEY CLUSTERED(id)
);

+--------+--------+------+
| id(PK) | name   | code |
+--------+--------+------+
|      1 | かつお |   11 |
|      2 | かつお | NULL |
|      3 | かつお | NULL |
|      4 | まぐろ |   11 |
|      5 | まぐろ |   22 |
+--------+--------+------+

このテーブルに「name」と「code」の組み合わせの複数のカラムに対して、ユニーク制約の設定しようとすると、エラーが発生します。

【ユニーク制約の設定SQL】
ALTER TABLE fish ADD CONSTRAINT uq_fish UNIQUE ( name, code );

【エラー内容】
The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.fish' and the index name 'uq_fish'. The duplicate key value is (かつお, ).

エラー内容のとおり、「かつお, NULL」の組み合わせで、既に重複しているデータがあるので、ユニーク制約を設定することが出来ませんでした。
(SQL Serverは、NULL値を重複した値として扱うため)

もし、NULL値を重複した値として扱いたくない場合、下記の方法で設定が可能です。

【設定方法1】スカラー関数とチェック制約

スカラー関数とチェック制約を組み合わせて、NULL値を含む複数カラムでのユニーク制約を設定可能です。
-- スカラー関数作成
CREATE FUNCTION fishCheck (
  @id bigint,
  @name nvarchar(10),
  @code varchar(20)
)  
RETURNS BIT
WITH RETURNS NULL ON NULL INPUT -- ※1
AS
BEGIN
  RETURN
    CASE WHEN EXISTS
      (SELECT 1
      FROM fish
      WHERE id <> @id -- ※2
      AND name = @name
      AND code = @code)
    THEN 1 ELSE 0 END
END;

-- チェック制約の設定
ALTER TABLE fish ADD CONSTRAINT ck_fish
  CHECK (dbo.fishCheck(id, name, code) <> 1); -- ※3
※1 引数のいずれかが NULL の場合に関数の本体を呼び出すことなく NULL を返します
そのため、code(NULL許可のカラム)がNULLであれば、重複のチェックは行いません
※2 重複レコードを探すSQLでは、チェックする(挿入、更新しようとする)データは除きます
※3 スカラー関数の結果が「1(重複あり)」でなければ、チェックOKとします

と、上記のように設定することができますが、2つの定義が必要ですし、少し解り難いかなという印象です。
もう一つの下記方法ですと、1つの定義で設定が可能です。

【設定方法2】ユニークインデックス

ユニークインデックスでは、WHERE句で条件を指定できます。
「code(NULL許可のカラム)がNULL以外」の条件を指定することで、NULLを除外したユニーク制約の設定が可能です。
CREATE UNIQUE INDEX uq_idx_fish ON fish(name, code) WHERE code IS NOT NULL;



もしSQL Serverで、NULL許可のカラムにユニーク制約を設定する場合、上記を試してみてください。

ただ、MSDNに「一意制約の列を選択する場合は、NOT NULL と定義された列を選択します。」との記載があるので、
ユニーク制約に設定するカラムには、NULL値が入らない前提での設計が必要ですね。
一意インデックスの作成

2022年2月22日火曜日

【Angular】Reactive formを使用して、要素が動的なフォームを作成する方法。


オフィス狛 技術部のmmm(むー)です。

先日Angularにて、Reactive Formを使用し、要素が動的に増えるフォームを作成したかったのですが、公式のページを読んでもなかなかイメージがわかず、苦労したので備忘録として残します。

今回、記事のためにAngularのバージョンは13.2.0を使用して確認しました。
業務で使用した際、Angular8系で動くことも確認しています。

対応方法

作成したかったフォームの構造は下記のようなイメージです。「ID、名前、メモ」で1セットのデータが動的に増減します。
早速ですが、コンポーネントのコードを記載します。
 // app.component.ts
import { Component, VERSION } from '@angular/core';
import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  constructor(private formBuilder: FormBuilder) {}

  // フォーム(枠) = formタグに該当します
  formGroup: FormGroup = this.formBuilder.group({
    features: this.formBuilder.array([]),
  });

  // 実際はAPIからの戻り値を使用すると思いますが、今回は確認用に以下の値を定義します
  list = [
    { id: 1, name: 'Haru', memo: '春' },
    { id: 2, name: 'Natsu', memo: '夏' },
  ];

  ngOnInit() {
    // 空のフォーム(中身) = formタグの中に入れる項目をまとめたものになります
    // 今回の例ですと、「ID、名前、メモ」で1つの単位となります。
    let testForm = this.formBuilder.group({});

    // 初期値とバリデーションを設定します。
    this.list.forEach((obj) => {
      testForm = this.formBuilder.group({
        name: [obj.name, Validators.required],
        memo: [obj.memo],
      });

      // フォーム(中身)をフォーム(枠)に追加します
      this.features.push(testForm);
    });
  }

  // featuresはAbstractControlを返すので、型をFormArrayにします
  get features(): FormArray {
    return this.formGroup.get('features') as FormArray;
  }
}

FormArrayを使用し、各FormControlの値をまとめることができます。
  ・FormControlは1つの入力項目(例えば、inputタグ単位)となります。
・更に、FormGroupを使用し、各FormArrayの値をまとめることができます。
  ・FormArrayが不要な場合は、FormGroupとFormControlだけで問題ありません。

HTMLは下記となります。(バリデーション等は記載していません)
 // app.component.html
<form [formGroup]="formGroup">
  <ng-container formArrayName="nestForm">
  <!-- グループ名(formGroupName)はインデックスになります -->
    <ng-container *ngFor="let val of nestForm.controls; let i = index">
      <ng-container [formGroupName]="i">
        <p>ID:{{ list[i].id }}</p>
        <p>
          名前:<input
            type="text"
            class="form-control"
            formControlName="name"
          />
        </p>
        <p>
          メモ:<input
            type="text"
            class="form-control"
            formControlName="memo"
          />
        </p>
        <hr />
      </ng-container>
    </ng-container>
  </ng-container>
  <button type="submit" [disabled]="formGroup.invalid ">更新</button>
</form>


このコードにバリデーションをチェックし、エラーメッセージを表示する処理を追加したい場合、以下のように作成することができます。
FormControlの名前を固定できるので(下記の例では、「name」)、バリデーションチェック用の処理を重複して書く必要がありません。
  // app.component.ts
// 名前が入力されているかチェックする
errorNameRequired(i: number): boolean {
    return (this.formArray.get('features')).controls[i].get('name').hasError('required');
  }

 // app.component.html
  <p>
    名前:<input
      type="text"
      class="form-control"
      formControlName="name"
    />
	 <!-- エラーメッセージを表示するためのコードを追加↓ -->
	 <ng-container *ngIf="errorNameRequired(i)">名前を入力してください。</ng-container>
  </p>


ちなみにフォームの値に直接アクセスしたい場合は、以下のように確認することができます。
console.log(this.nestForm.value[0].name) // Haru

以上となります。参考になりましたら幸いです。

参考:
https://angular.io/api/forms/FormGroup (公式)
https://angular.io/api/forms/FormArray (公式)

【Laravel】ミドルウェアでクライアントからのリクエスト内容を書き換える。


オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。

今回もLaravelネタになります。
前回、「Jetstream + Fortify」を画面無し(APIとして)で運用する為の方法を記載しました。
その中で、
if ($request->expectsJson()) {
    // クライアントからJSONレスポンスを要求されている場合、リダイレクトさせず、JSON形式メッセージをレスポンスする。
    return response()->json(['message' => 'Already authenticated.'], 200);
}
のような、記載をしたかと思います。
これは、とりあえず、「API化したものの、画面としても使える手段を残す為」となります。

前回は、特に説明しなかったのですが、$request->expectsJson()で、「APIとして呼ばれている」事を判断していました。
これ、正確にはクライアントに依存しているのですが、要は、httpヘッダに「Accept:application/json」を設定している、と言うことになります。
ただ、必ずしもクライアント側が設定してくるとは限らない・・・

そのような場合、クライアントからのリクエストを書き換えて、ヘッダに常に「Accept:application/json」を設定している、と言う状態にすることが出来ます。

まずは、ミドルウェアにクラスを追加します。
[app/Http/Middleware/RequireJson.php](新規追加)
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class RequireJson
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        // クライアントからのAccept要求(クライアントがResponseで欲しいデータ形式)は全てJSONとする。
        $request->headers->set('Accept','application/json');
    }
}
これでヘッダの書き換え処理は出来ました。
あとは、この処理が、クライアントからのリクエストの度に必ず呼ばれるようにしないといけません。

そこで、「Kernel.php」に上記のクラスを読み込む処理を追加します。
[app/Http/Kernel.php]
    protected $middleware = [
        \App\Http\Middleware\TrustProxies::class,
        \Fruitcake\Cors\HandleCors::class,
        \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \App\Http\Middleware\RequireJson::class, // ←追加
    ];
これで完了となります。
※ただ、この設定をした時点で、完全に画面としては使用出来なくなるので、注意が必要です。

ミドルウェアは、色々使い道があるので、もっと使っていきたいですね。