SpringBoot x Spring Shell で簡易バッチ処理を作る

エキサイト株式会社エンジニアの佐々木です。2021年エキサイトホーディングスアドベントカレンダー7日目を担当させていただきます。

以前、SpringBootで対話的インターフェースをSpring Shellで実装するで対話的なアプリケーションについては記載しており、今回は簡易的なバッチ処理の方を記載します。

はじめに

前回の対話的なアプリケーションのコードを再掲します。(一部修正していますが、メソッド名部分のみになります。)

@ShellComponent(value = "demo")
public class DemoShellController {

    @ShellMethod(value = "足し算をする", key = "addCalc" , group = "calc")
    public Integer add(int a, @Max(10) int b, @ShellOption(value = "--optional_c" , defaultValue = "0") int c){
        return a + b + c;
    }
}

こちらになりますが、引数を渡されたものを足し算する簡単なものになります。前回のままだと対話的なアプリケーションになってしまい、Cron等で動作させられないので、これを改造していきます。

SpringShell内の実装方法

SpringShellではどうやって対話的アプリケーションにしているかの実装をみていきます。

InteractiveShellApplicationRunner.java

@Component
@Order(InteractiveShellApplicationRunner.PRECEDENCE)
public class InteractiveShellApplicationRunner implements ApplicationRunner {

        // 省略

    @Override
    public void run(ApplicationArguments args) throws Exception {
        boolean interactive = isEnabled();
        if (interactive) {
            InputProvider inputProvider = new JLineInputProvider(lineReader, promptProvider);
            shell.run(inputProvider);
        }
    }

        // 省略
}

ApplicationRunnerのインターフェースを用いて実装されているのがわかります。@Orderを用いて、実行順序を制御しています。簡易バッチの方の処理は、ApplicationRunnerインタフェースを用いて、且つ@Orderで対話型アプリケーションよりも早く動かないといけません。

簡易バッチの実装

下記のような実装コードになります。

NonInteractiveRunner.java

@Component
@Order(InteractiveShellApplicationRunner.PRECEDENCE - 1)    //  ① InteractiveShellApplicationRunnerより1つ優先順位を上げる
public class NonInteractiveRunner implements ApplicationRunner {

    private Shell shell;
    private ConfigurableEnvironment configurableEnvironment;

    public NonInteractiveRunner(Shell shell, ConfigurableEnvironment configurableEnvironment) {
        this.shell = shell;
        this.configurableEnvironment = configurableEnvironment;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        if (args.getNonOptionArgs().isEmpty() || args.getNonOptionArgs().stream().anyMatch(s -> s.startsWith("@"))) {
            // ②ここに入った場合は、通常の対話型アプリケーションを実行する
            return;
        }

        InteractiveShellApplicationRunner.disable((configurableEnvironment));  // ③ ここで対話型アプリケーションを無効にする
        final Object evaluate = shell.evaluate(() -> String.join(" ", args.getSourceArgs()));  // ④ここで簡易バッチアプリケーションを実行する
        if (evaluate != null) {
            System.out.println(evaluate);    // ⑤出力するものがあればここで出力する
        }

    }
}

対話型アプリケーションの機能も残したい為、引数がなかったら or 引数に@があれば対話型アプリケーションの動きをするようにしています。(※対話型アプリケーションでは@があると、ファイルに記載されているコマンドを対話型アプリケーションの中で展開する仕様になっています。)

それ以外の場合は、対話型アプリケーションを無効にし、渡された引数でメソッド等を呼び出しバッチとして処理するようなものにします。

 $ java -jar build/libs/batch-0.0.1-SNAPSHOT.jar addCalc 1 2 4

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.0)

2021-12-05 17:46:45.958  INFO 76623 --- [           main] com.example.batch.BatchApplication       : Starting BatchApplication using Java 11.0.13 on 61-17616.local with PID 76623 (/Users/kohei.sasaki/git/sample/batch/build/libs/batch-0.0.1-SNAPSHOT.jar started by kohei.sasaki in /Users/kohei.sasaki/git/sample/batch)
2021-12-05 17:46:45.963  INFO 76623 --- [           main] com.example.batch.BatchApplication       : No active profile set, falling back to default profiles: default
2021-12-05 17:46:46.911  INFO 76623 --- [           main] com.example.batch.BatchApplication       : Started BatchApplication in 6.416 seconds (JVM running for 6.881)

7

答えは7になっています。 これで、SpringShellで簡易的なバッチ処理を作ることができました。対話型アプリケーションもコマンドアプリケーションもできるので、お得な実装になっているかと思います。

おまけ

上記のログにでている、起動時のSpringBootのバナーが邪魔ですね。これはメインメソッド内で消せるのでその実装を行います。

BatchApplication.java

@SpringBootApplication
public class BatchApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(BatchApplication.class)
                .bannerMode(Banner.Mode.OFF)  // ここでバナーをオフにする
                .run(args);
    }
}

上記のように、bannerMode()OFFにすれば表示されません。これで邪魔ではなくなりました。 下記のように、起動してもバナーがでないようになります。下記が実行結果です。

$ ./gradlew assemble && java -jar build/libs/batch-0.0.1-SNAPSHOT.jar addCalc 1 2 4

2021-12-06 09:33:25.406  INFO 85570 --- [           main] com.example.batch.BatchApplication       : Starting BatchApplication using Java 11.0.13 on 61-17616.local with PID 85570 (/Users/kohei.sasaki/git/sample/batch/build/libs/batch-0.0.1-SNAPSHOT.jar started by kohei.sasaki in /Users/kohei.sasaki/git/sample/batch)
2021-12-06 09:33:25.411  INFO 85570 --- [           main] com.example.batch.BatchApplication       : No active profile set, falling back to default profiles: default
2021-12-06 09:33:26.228  INFO 85570 --- [           main] com.example.batch.BatchApplication       : Started BatchApplication in 6.197 seconds (JVM running for 6.6)
7

まとめ

重要データに関しては、SpringBatch等をつかった方が、動作履歴やレジューム(途中から再開)機能があるのでそちらを使ったほうがいいですが、ちょっとしたバッチ処理であれば使ってみるのもいいと思います。対話型のアプリケーションと簡易型のコマンドラインアプリケーションがついてくるので、お得かもしれません。

最後に

2021年エキサイトホーディングスアドベントカレンダー7日目でした。引き続きエキサイトホーディングスのアドベントカレンダーをお楽しみいただけると幸いです。 qiita.com

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

カジュアル面談はこちらになります! meety.net

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

【タイポグラフィ・ポスター製作に】簡単計算!Illustratorでグリッドを作る方法

f:id:excite_ny:20211201131504p:plain

はじめに

エキサイト21卒デザイナーの山﨑です😊

こちらはエキサイトホールディングス Advent Calendar 2021の6日目の記事になります!

qiita.com

今回はIllustratorでのポスターデザインに必須なグリッドシステム(グリッドレイアウト)を解説していきたいと思います。

グリッドシステム・制作方法について

はじめに、グリッドシステムとはどんな物なのか説明します。

グリッドシステムとは?

色々意味はありますが、画面やページを格子状に分割して、規則的な直線を基準にデザイン要素を配置していくデザイン手法です。

ポスターをデザインする前にグリッドレイアウトを作っておくと、デザインが圧倒的に作りやすくなります。

今回エキサイト社内に掲示するバリューポスターをグリッドレイアウトを活用してデザイン制作したので、例として出してみます。

f:id:excite_ny:20211204234145p:plain

右がグリッドレイアウトです。このグリッドがある程度のデザインの配置の目安になっています。

一見適当な正方形を並べて配置しただけに見えますが、実は計算して配置しています。

では、早速解説して行きましょう。

①フォントサイズ・行送り・行間・グリッドに入れる行を決める

まずはこの四つの数字をきめましょう。

製作する文字のフォントサイズを元に決めると作りやすいです。

正直数字はなんでも良いのですが、あまりに数字が大きすぎるとグリッドの正方形が大きくなって組みづらいグリッドレイアウトになりがちなので、注意が必要です。

ひとまず今回は【フォントサイズ21pt、行送り25pt・行間4pt・グリッドに入れる行数は3行】に設定しました。 (グリッドに入れる行は5行・4行・3行から選ぶのが基本的です)

②その数字をもとに計算してグリッドサイズ・グリッド間を割り出す

そうすると、ざっくりこんな感じの図になります。

f:id:excite_ny:20211205005126j:plain

グリッドの正方形の中に3行文が入るとなると、

21pt+4pt+21pt+4pt+21pt=71pt (行送り25pt+行送り25pt+フォント21pt=71ptでも可)

f:id:excite_ny:20211206102305p:plain

3行目の行間4ptがはみ出る形になるので、グリッド間のサイズは

4pt+21pt+4pt=29pt

f:id:excite_ny:20211206103205p:plain

というわけで、正方形のサイズは71pt・正方形と正方形の間は29ptになりました。

③グリッドを作ってみる

f:id:excite_ny:20211205010752p:plain

71ptの正方形を一つ作ります。

f:id:excite_ny:20211205010812p:plain

環境設定>一般>キー入力で「71pt(グリッドサイズ)+29pt(グリッド間)」を入力してください。

このときに数字だけではなくpt (ポイント)という単位も入れましょう。

f:id:excite_ny:20211205010933p:plain

最初の正方形をクリックしてaltを押しながら→を押すと間隔が29ptに保たれた正方形が量産されます。

f:id:excite_ny:20211205011044p:plain

→を押し続けるとすぐに横一列のグリッドが完成しました。次はこの横一列のグリッドを全て選択して、今度はaltを押しながら↓を押します。

f:id:excite_ny:20211205011220p:plain

グリッドが完成しました。

このグリッドをアートボードの中心に合わせます。

この正方形を全て選択し、Command+5を推してグリッド化させれば完成です。

f:id:excite_ny:20211205011311p:plain

終わりに

いかがだったでしょうか?

慣れるまで少し複雑なのですが、仕組みを覚えれば意外と簡単に組めると思います😊

グリッドに合わせすぎるとデザインが窮屈な印象になってしまうので、ご注意ください。

グリッドレイアウトはあくまで目安なので、グリッドから外れてしまっても気にせずデザインするのが良いと思いました😊

それでは、ありがとうございました!

quarkusを使う(バリデーション編)

こんばんは、エキサイト株式会社の中尾です。

前回の続きです。 必要そうなツールを入れていきます。 まず、バリデーションを追加します。

  • gradleに以下を追加
implementation 'io.quarkus:quarkus-hibernate-validator'

java17にしているのでもちろんRecordを使います。

package org.my.hobby;

import javax.inject.Inject;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.constraints.NotBlank;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.Set;
import java.util.stream.Collectors;

record BookSearchRequest(
        @NotBlank(message = "Title may not be blank") String title) {
}

record BookSearchResponse(
        boolean success,
        String message,
        Book book) {
    record Book(String title, String author) {
    }
}

@Path("/book/search")
public class BookController {

    @Inject
    Validator validator;

    /**
     * 書籍をタイトルで検索
     *
     * @param bookSearchRequest
     * @return
     */
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    public BookSearchResponse search(BookSearchRequest bookSearchRequest) {
        Set<ConstraintViolation<BookSearchRequest>> violations = validator.validate(bookSearchRequest);
        if (!violations.isEmpty()) {
            final String errorMessage = violations
                    .stream()
                    .map(e -> e.getMessage())
                    .collect(Collectors.joining(","));
            final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book("", "");
            return new BookSearchResponse(false, errorMessage, hobbyBook);
        }

        if (!"hobby".equals(bookSearchRequest.title())) {
            final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book("", "");
            return new BookSearchResponse(false, "not found book", hobbyBook);
        }

        final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book("hobby", "s-nakao");
        return new BookSearchResponse(true, "", hobbyBook);
    }
}
  • postmanでアクセス
shogo.nakao@localhost: $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
        "title": "hobby"
}'
{"success":true,"message":"","book":{"title":"hobby","author":"s-nakao"}}%
shogo.nakao@localhost: $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
        "title": "s-nakao"
}'
{"success":false,"message":"not found book","book":{"title":"","author":""}}%
shogo.nakao@localhost: $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
}'
{"success":false,"message":"Title may not be blank","book":{"title":"","author":""}}%
shogo.nakao@localhost: $

ちゃんとバリデーションできていますね!嬉しい嬉しい!

springbootと違ってbindingResultは使えないですが、、、(もしかしたら使える方法があるかも?今後調べます)

次回、さらに必要そうなツールを入れるに移動します。

最後に、弊社では採用もバシバシ実施しているので興味のあるかたがいましたらご応募ください。

www.wantedly.com



Advent Calendar 2021を引き続き楽しんでいただけると嬉しいです。

qiita.com

JavaとLombokの@Withでイミュータブルなデータ型を生成する

エキサイト株式会社エンジニアの佐々木です。2021年アドベントカレンダー5日目を担当します。 サービスを開発している上で、データをなるべくイミュータブルにするというのは、バグを作り込まない上で重要だと思います。Java15でRecord型が導入されていますが、まだ未対応なライブラリがあると思います。今回はLombokでは@Withを使って簡単に実装できます。

ミュータブルなデータ型

ミュータブルなデータ型は、Lombokでは@Dataを使って簡単にできます。

@Data
class Form {
    private final Long id;
    private final String name;
}

使用時:
Form form = new Form();
form.setId(1L);
form.setName("sample");

form.setId(2L);   // form.id が 2にかわる
form.setName("sample2"); // form.name が 2に変わってしまう

コード見れば明らかですが、途中でオブジェクトの状態が変わっています。メソッドの引数で渡して、再代入等を特にしていない場合でも変わってしまうので、バグの1つの原因になります。

イミュータブルなデータ型(@Value)

@Value
class Form {
    private final Long id;
    private final String name;
}

使用時:
Form form = new Form(1L, "sample");

form.setId(2L);  // メソッドが存在しないので、エラーになる
form.setName("sample2"); // メソッドが存在しないので、エラーになる

Form form2 = new Form(2L, "sample2"); // 代入 or 再代入をする

コンストラクタを使用したときだけ値を設定できるようにすると、1度定義したオブジェクトの状態は途中で変えることができません。途中で変えるには、再度コンストラクタ呼び出しを行い、再代入等が必要です。しかし、引数が少ないときはいいですが、多くなってくると辛くなってきます。そこで@Withが登場します。

イミュータブルなデータ型(@With)

@With
@Value
class Form {
    private final Long id;
    private final String name;
}

使用時
Form form = new Form(1L, "sample");
form.withId(2L);
form.withName("sample");

log.info("{}", form);  // Form(id=1, name=sample);

Form form2 = form.withId(2L).withName("sample2");
log.info("{}", form);  // Form(id=2, name=sample2);   

withId(), withName() を使用して値変更しても、formオブジェクトの値は変更されていません。setterとは違い、新規オブジェクトが返ってきます。では、@Withがどういう処理をしているのかをdelombokをして見ていきます。

public Form withId(Long id) {
    return this.id == id ? this : new Form(id, this.name);
}

public Form withName(String name) {
    return this.name == name ? this : new Form(this.id, name);
}

このようなコードが裏側で実行されています。値が同じ時は自分の参照を返し、値が変更されている場合は、新規のオブジェクトを生成して返しています。これでイミュータブルなオブジェクトになって安全です。

まとめ

Java15でRecord型が導入され、イミュータブルなオブジェクト専用の型が追加されています。状態が変更される副作用を抑えるべく導入されているかと思います。周辺ライブラリが対応するまではLombokを駆使して、ミュータブル、イミュータブルなオブジェクト生成の使い分けをする感じになるかと思います。

最後に

引き続きエキサイトホールディングスのアドベントカレンダーをお楽しみいただければ幸いです。 Calendar for エキサイトホールディングス Advent Calendar 2021 | Advent Calendar 2021 - Qiita

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

カジュアル面談はmeetyを公開していますので、よろしくお願いいたします! meety.net

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

quarkusを使う(環境構築編)

初めまして、Red Hat大好きエキサイト株式会社の中尾です。

趣味でquarkusを使っていこうと思うので、その記録を残そうと思います。

quarkus cliは使いません。

まずは環境作りです。

quarkusでbuildツールgradleを使おうと思います。 mvnよりもgradleのほうが柔軟なので、、、(宗教戦争ありそうですが。)

  • mvnインストール

インストールするんかい!!って疑問あるかもしれないですが、quarkus自体を入れる最初の儀式です。

brewで入れてください。

brew install maven
  • 初期構築

mvn使って構築します。 buildToolにgradleを指定すると、gradle使えます。

mvn io.quarkus.platform:quarkus-maven-plugin:2.5.1.Final:create \
    -DprojectGroupId=quarkus \
    -DprojectArtifactId=hobby \
    -DprojectVersion=v1.0.0 \
    -DclassName="org.my.hobby.Resource" \
    -Dextensions="resteasy,resteasy-jackson" \
    -DbuildTool=gradle
  • 起動
./gradlew --console=plain quarkusDev

開きました!嬉しい嬉しい!

http://localhost:8080/

http://localhost:8080/hello

次回、必要そうなツールを入れるに移動します。

最後に、弊社では採用もバシバシ実施しているので興味のあるかたがいましたらご応募ください。
www.wantedly.com

Advent Calendar 2021を引き続き楽しんでいただけると嬉しいです。

qiita.com

リモートワークを快適に行うためにデスクまわりを整備した

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 普段は既存サービスのリビルド(PHP / BEAR.Sunday → Java / SpringBoot)を担当しています。

エキサイトホールディングス Advent Calendar 2021の3日目の記事です! qiita.com

現在エキサイトでは、月8回は出社する必要があり、その他の曜日は自宅から作業をしています。 本記事では、自分がリモートワークを快適に行うために購入してよかったものを紹介します。

LG 4Kモニター

f:id:excite-kazuki:20211202190018j:plain

このLGの4Kモニター(32UN500-W)を購入する前は、DellのWQHDモニター(P2720DC)を使っていました。 Dellのモニターでも十分に満足していましたが、不慮の事故によりディスプレイを壊してしまい、新しく購入することになりました。 下記条件を満たすモニターがないか調べていたところ、LGの4Kモニター(32UN500-W)がこれらを満たしていました。

  • 4Kの解像度
  • 枠が細い
  • スピーカー内臓
  • モニターアームが取り付けられる(VESA規格対応)
  • 5万円以内

現状では大画面で作業することができてとても満足していますが、チームメンバーから「EIZOのモニターはいいぞ!」とおすすめされることも多く、今後買い換えるかもしれないです🤣

Anker PowerExpand 9-in-1

f:id:excite-kazuki:20211202191001j:plain

このドッキングステーションは「MacBookとモニターをケーブル1本で繋いで配線を整理したいな」と考えて、購入しました。 現在使用しているMacBookにはType-Cのポートしかないため、給電、ディスプレイ出力、Webカメラ接続などをハブで行うと配線がゴチャゴチャしてしまい、スッキリしていませんでした。PowerExpand 9-in-1を購入してからは、配線がスッキリしたことや、私用のMacBookと社用のMacBookを簡単に切り替えることができることなど、とても満足しています。

エルゴトロン LXデスクマウントアーム

f:id:excite-kazuki:20211202185627j:plain

このモニターアームは「モニターアームがカッコイイ!欲しい!」という単純な理由で購入しました💪 実際に使ってみると、モニターを最大まで低く配置することができたり、モニターの下にキーボードを置くことができたりしてよかったです。あとは見た目がカッコイイのが最高!

AfterShokz OpenMove

f:id:excite-kazuki:20211202185543j:plain

この骨伝導イヤホンは、AirPods Proを使ってミーティングをしていたときに、耳の中を傷つけてしまったため、耳をふさがないイヤホンが欲しいと考えて、購入しました。 「骨伝導イヤホンってしっかり音が聞こえるのかな...?」と懐疑的でしたが、ミーティングは問題なく行えています。 OpenMoveは外部の音をあまり拾わないことや、Type-Cで充電できることなど魅力がたくさんあります。 たまに、大きな音が流れてくると、イヤホンが震えるため、ゾワッ としてしまうのがつらいところ...😵 好きな音楽を流しながら集中して作業したい場合は、ヘッドホンに切り替えるとよさそうです。

オカムラ コンテッサセコンダ

f:id:excite-kazuki:20211202193621j:plain

このオフィスチェアを購入するまでは、IKEAの3000円ほどで購入した椅子を使っていました。 ほぼ毎日がリモートワークということもあり、「腰は大事にしたほうがよい」「よい椅子を買うと幸せになれる」といった記事を多く見たため、「そこまで言うなら買うぞ!」と思いオフィスチェアを購入しました。

長時間作業しても腰が痛くなることはなく、座り心地も最高なため、ずっと座って作業できます! ただ、座りっぱなしは身体によくなたいめ、定期的に立ったり身体を動かすように気をつけていきたいです。

IKEA デスク

f:id:excite-kazuki:20211202184723j:plain

このデスクは天板と2本の脚と引き出しユニットを組み合わせて構築しています。 当初、昇降式デスクも視野に入れていましたが、「結局使うことないかな〜」と思い、それなら引き出しを置こうと思ってこの構成にしています。 横幅が186cmもあるため、一般的な横幅120cmほどのデスク比べてもかなり広いため、余裕をもって色々なものを置くことができたり、引き出しに収納できたりして満足しています。

おわりに

リモートワークを快適に行うために、色々なものを購入してきました。 購入金額の合計を出すのが怖いですね...💸 自宅では職場と違って色々とカスタマイズできるのが魅力的かなと思います。

ここまで読んでいただきありがとうございました!

AWS SDK for PHP を利用してSESで添付ファイル付きのメールを送信する

こんにちは 👋

エキサイトホールディングス Advent Calendar 2021の3日目は、エキサイト株式会社の伊藤(🐦 @motokiito2)が担当させていただきます!

今回は、AWS SDK for PHP を利用して、SESで添付ファイル付きのメールを送信する方法について書かせていただきたいと思います!

なお、SDKの導入や SesClient() の利用方法については本記事では割愛させていただきます。

添付ファイルつきメールを送信する方法

AWS公式のチュートリアルで利用されている sendEmail() は宛先、送信元、本文、件名、文字コード等を渡すだけでメールを送信することができます。

しかし、 sendEmail() には添付ファイルを指定するパラメータが存在しないため、メールのheaderとbodyを自分で成形して送信する sendRawEmail() を利用する必要があります。

docs.aws.amazon.com

sendRawEmail() の利用

sendRawEmail()sendEmail() とは異なり、メールヘッダおよびコンテンツを自前で整形して渡す必要があります。

この例では、送信元 $from , 宛先 $to を渡していますが、必須パラメータは RawMessageのみなので、RawMessageのヘッダ内に含めても問題ないです。

<?php

$this->sesClient->sendRawEmail([
    'Destinations' => [
        $values['to'],
    ],
    'Source'     => $values['from'],
    'RawMessage' => [
        'Data' => $rawMessage,
    ],
]);

sendRawEmail() の詳細は下記ドキュメントを参照してください。

https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-email-2010-12-01.html#sendrawemail

RawMessage

RawMessageを用意する方法はいろいろありますが、この例ではシンプルに文字列で用意します。

SESの設定セットを利用している場合は、ヘッダー部に X-SES-CONFIGURATION-SET: を含める必要があります。

また、sendEmail() がよしなにやってくれている部分を自前で用意する必要があるので、注意してください。

  • SubjectのMIMEエンコードを忘れない

  • 添付ファイルを入れる場合にはメッセージの Content-type を multipart/mixed にしておく必要がある

など..

<?php

private function generateRawMessage(
    string $from,
    string $to,
    string $subject,
    string $body,
    string $fileName,
    string $fileStringBase64Encoded
) : string {
    $boundaryString = '__BOUNDARY__';

    return 'To: ' . $to . "\n"
         . 'From: ' . $from . "\n"
         . 'X-SES-CONFIGURATION-SET: ' . $this->config['aws']['ses']['configuration_set'] . "\n"
         . 'Subject: ' . mb_encode_mimeheader($subject, 'UTF-8') . "\n"
         . "MIME-Version: 1.0\n"
         . 'Content-Type: multipart/mixed; boundary="' . $boundaryString . '"' . "\n\n"
         . '--' . $boundaryString . "\n"
         . "Content-Type: text/html; charset=\"UTF-8\"\n\n"
         . nl2br($body) . "\n\n"
         . '--' . $boundaryString . "\n"
         . 'Content-Type: application/octet-stream; name="' . $fileName . '"' . "\n"
         . "Content-Transfer-Encoding: base64\n"
         . 'Content-Disposition: attachment; filename="' . $fileName . '"' . "\n\n"
         . $fileStringBase64Encoded
         . '--' . $boundaryString . "\n";
}

添付ファイル

添付ファイルは、ファイルをbase64エンコードした文字列を用意します。

<?php

private function generateFileStringBase64Encoded(
    string $fileAbsolutePath
) : string {
    if (!file_exists($fileAbsolutePath)) {
        return false;
    }

    return chunk_split(base64_encode(file_get_contents($fileAbsolutePath)));
}

メール送信メソッドサンプル

これまでの実装を利用した送信メソッドのサンプルコードをのせます。 (SesClientとLoggerはクラス内でコンストラクタインジェクションしています)

<?php

private function sendEmailWithAttachment(
    string $from,
    string $to,
    string $subject,
    string $messageBody,
    string $fileAbsolutePath,
    string $fileName
) : void {
    $fileStringBase64Encoded = $this->generateFileStringBase64Encoded($fileAbsolutePath);

    if (!$fileStringBase64Encoded) {
        throw MailSendFailedException::factoryMailSendFailedException('attachment file is invalid');
    }

    $rawMessage = $this->generateRawMessage(
        $from,
        $to,
        $subject,
        $messageBody,
        $fileName,
        $fileStringBase64Encoded
    );

    try {
        $result = $this->sesClient->sendRawEmail([
            'Destinations' => [
                $to,
            ],
            'Source'     => $from,
            'RawMessage' => [
                'Data' => $rawMessage,
            ],
        ]);
        $messageId = $result['MessageId'];
        $this->logger->info("Completed to sent, messageId: $messageId");
    } catch (AwsException $e) {
        $this->logger->error('[MailSendError] ErrorMessage: ' . $e->getMessage());

        throw MailSendFailedException::factoryMailSendFailedException('mail send error');
    }
}

最後に

AWS SDK for PHPの SesClientは、シンプルなメールを送信する場合は実装が楽なのですが、メールヘッダを弄る必要があったり、今回のようにファイルを添付したい場合などはRawMessageを扱う必要があり、実装が少し複雑になります。

同じようなケースで sendRawEmail() を利用しなければいけない場合に、この記事が参考になってくれればとても嬉しいです!


弊社アドベントカレンダーの他の記事も是非!

qiita.com