Study & Practice

北海道札幌市のプログラマによる技術とか雑記のブログ

Vue.jsのライフサイクルフックを学ぶ

7月からVue.jsを扱うプロジェクトに入ったのでVue.jsをおさらいしています。今回はライフサイクルフックについて学んでいきます。参考は公式ドキュメントです。

jp.vuejs.org

ライフサイクルフックとは

インスタンスの状態が変化したタイミングで自動的に呼ばれる関数です。ライフサイクルフックを理解することはVue.jsで効果的な開発をすることにとって、とても重要です。

beforeCreate

Vueインスタンスが作られる前、インスタンスが初期化されるときに呼ばれる。インスタンスプロパティなんかにアクセスすることができないのであんまり使うことはない。あるとしたらVueインスタンスを作る前に、Vue外部と連携を取る必要がある時かな。経験はないです。

created

インスタンスが作られた後に呼ばれます。このタイミングでdata、computed、methodsなどが利用可能になります。dataやcomputedにアクセスできるので、使い所はありそうですね。

beforeMount

Vueインスタンスが作成された後、DOMが初めて構築される直前に呼ばれます。DOMが構築される「前」なのであまり使い所はないですね。

mounted

DOMが構築されたタイミングで呼ばれます。HTMLとして描画されているわけではないこと、子コンポーネントのDOMも構築されていることが保証されるわけでない点に注意が必要です。

beforeUpdate

dataオプションで保持している値が変更され、DOMが更新される前に呼ばれます。更新される前のDOMにアクセスしたいときに有効です。

updated

dataオプションで保持している値が変更され、DOMが更新された後に呼ばれます。update内でDOMの更新を行うと無限ループに陥ってしまうので注意が必要です。また、子コンポーネントが更新されていることは保証されません。呼ばれる回数も多いことから描画コストもかかる上に、ロジックが複雑になりやすいのでできるだけ使うのを避けた方がいいフックです。

activated

コンポーネントが活性化するときに呼ばれます。このフックはタグを使用している時のみ呼ばれます。

deactivated

コンポーネントが非活性化するときに呼ばれます。このフックはタグを使用している時のみ呼ばれます。

beforeDestroy

destroy()メソッドを呼んだ時など、Vueインスタンスが破棄される直前に呼ばれます。このフック内では、Vueの各オプションはまだ機能しているため、インスタンス内の情報を利用したい場合に有効になるフックです。

destroyed

Vueインスタンスが破棄された後に呼ばれます。このフックが呼ばれるとき、既にVueの各オプションは機能していないので、Vueインスタンスが持っていた情報にはアクセスできないため使い所は少ないフックです。

errorCaputured

コンポーネントからエラーが捕捉されたときに呼ばれます。このフックは返り値としてbooleanが定義されています。falseを返すことで親コンポーネントへのエラーの伝播を止めることができます。

まとめ

さらっとVueもライフサイクルフックを全て紹介してみました。activated、deactivatedなんてフックがあるのは知らなかったし、destroyedなんて使うことあんのかな〜って改めて思いました。使うのはほとんどcreated、mountedあたりなんじゃないでしょうか?ひとまず、頭の片隅ででも覚えておけば、開発中に何かあったときにドキュメントを見返して使ってみることができると思うので、是非覚えてみてください。

Vue.js + TypeScriptの始め方

7月から新しいチームに参画しました。Nuxt.js+TypeScriptを使ったプロジェクトで、技術的な難易度は高くないとのことなんですが、Nuxt.jsは会社が推しているフロントエンド技術でもあるようなので、これから学習していこうと思っています。まずは何事も基本から、ということで今回はNuxt.jsの前にVue.s+TypeScriptのコードを書いていくための下準備を行なっていきます。

手順などは公式ドキュメントに従います。
v3.vuejs.org

Vue CLIのインストール

まずはVue CLIをインストールしていきます。業務ではyarnを使っているみたいなので今回はyarnを使います。

yarn global add @vue/cli

以下のコマンドを実行してバージョンが表示されればOKです

vue --version

プロジェクト作成

次にプロジェクトを作ります。下記のコマンドはhello-world-appという名前のプロジェクトを作るコマンドです。

vue create hello-world-app

コマンドを実行するとターミナルが以下のような表示になります。

? Please pick a preset: (Use arrow keys)
❯ Default ([Vue 2] babel, eslint)
  Default (Vue 3) ([Vue 3] babel, eslint)
  Manually select features

vue.jsをそのまま使うのであればVue2かVue3で問題ありませんが、今回はTypeScriptを使うので「Manually select features」を選択します。

次は以下のような表示になります。

? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to t
oggle all, <i> to invert selection)
❯◉ Choose Vue version
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

TypeScriptを使用するので、TypeScriptを選択します。選択するには矢印キーで移動してスペースキーを押します。

下記のようにTypeScript横の丸が塗りつぶされた状態になっていればOKです。

? Please pick a preset: Manually select features
? Check the features needed for your project:
 ◉ Choose Vue version
 ◉ Babel
❯◉ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

エンターキーを押して次に進みます。

あとはお好みで問題ありませんが、参考に僕が選んだ結果を以下に載せておきます。

? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Linter
? Choose a version of Vue.js that you want to start the project with 2.x
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N)No

正常にプロジェクトが作成されたら以下のような出力がされているはずです。

🎉  Successfully created project hello-world-app.
👉  Get started with the following commands:

 $ cd hello-world-app
 $ yarn serve

次は指示に従ってアプリケーションを動かしてみます。

アプリケーションの実行

以下のコマンドを実行します。

cd hello-world-app
yarn serve

ブラウザからhttp://localhost:8081/にアクセスするとVue.js + TypeScriptのWelcomeページが表示されます。

Vue.js + TypeScriptのWelcomeページ

Hello Worldする

僕はどんな言語や技術でもHello Worldと出力することから始めることにしているので、今回もやっていきます。

./src/App.vueを以下のように編集します。

<template>
  <div id="app">
    <h1>Hello World!</h1>
  </div>
</template>

これでhttp://localhost:8081/にアクセスしたら「Hello World!」と出力されているはずです。

まとめ

おつかれさまでした。とはいっても今回は本当に初歩的なことをしているだけですけどね。次からはVue.jsやTypeScriptの構文についてのポストをしていきたいと思います。

LaravelのValidationを使ってみる

5月から参画したチームがLaravelを使っていまして、ちょいちょい不具合修正のチケットなんかを担当し始めた。しかし、Laravelの機能がまだあんまりわかってないもので既存のコードを読むのに時間がかかってしまっています。ということで公式ドキュメントを読んでいろんな機能を使ってみようと思います。今回はその第一弾、Validation機能を使っていきます。

laravel.com

下準備

まずは下準備です。

以下のコマンドでコントローラーを追加します。

php artisan make:controller ValidationController

コントローラーにview表示用のメソッドを追加します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ValidationController extends Controller
{
    // 追加
    public function index()
    {
        return view('validation');
    }

    // 追加
    public function validation(Request $request)
    {
        echo "name: $request->name<br>";
        echo "email: $request->email<br>";
    }
}

以下のコマンドでviewを追加

touch resources/views/validation.blade.php

resources/views/validation.blade.phpの内容は以下のようにします。

<form method="POST" action="/validation">
    @csrf
    <label>Name</label>
    <input id="name" name="name" type="text">
    <br>
    <label>Email</label>
    <input id="email-address" name="email" type="email">
    <button type="submit">Submit</button>
</form>

最後にweb.phpにValidationController::index()とValidationController::validation()へのルーティング設定を追加します。

<?php

use App\Http\Controllers\ValidationController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

// 追加
Route::get('/validation', [ValidationController::class, 'index']);
Route::post('/validation', [ValidationController::class, 'validation']);

これでhttp://localhost:8000/validationにブラウザからアクセスすると

f:id:carametal:20210623203734p:plain
こんな感じのシンプルなフォームが出来上がります。

そしてNameとEmailに適当な文字列を入れてSubmitを押すと以下のような出力がされるはずです。

name: test name
email: test@example.com

これで下準備が完了です。

基本的なValidationの使い方

それではValidationの機能を使っていきます。

まずはValidationControllerのvalidationメソッドを以下のように更新します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ValidationController extends Controller
{
    public function index()
    {
        return view('validation');
    }

    // 更新
    public function validation(Request $request)
    {
        $request->validate([
            'name' => 'required|max:10',
            'email' => 'required|max:20'
        ]);
        echo "name: $request->name<br>";
        echo "email: $request->email<br>";
    }
}

Requestのvalidateメソッドが公式ドキュメントで一番最初に紹介されるValidation機能です。今回のコードはリクエストのnameとemail両方を必須項目として、nameを最大10文字、emailを最大20文字の制限をつけています。

ただ、実行していただければわかるかと思いますが、validationでエラーになるデータをPOSTしてもFormの画面に戻されるだけでエラーなどが表示されませんので、エラーメッセージの表示機能を追加してみます。

<form method="POST" action="/validation">
    @csrf
    <label>Name</label>
    <input id="name" name="name" type="text">
    <br>
    <label>Email</label>
    <input id="email-address" name="email" type="email">
    <button type="submit">Submit</button>
</form>

{{-- 追加 --}}
@if ($errors->any())
    <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
    </ul>
@endif

validationがエラーになると自動的に元の画面にリダイレクトされます。$errorsという変数にエラーメッセージが格納されているため、上記のように取り出して画面上に表示することができます。

ためしにNameとEmailをどちらも入力せずにSubmitを押してみると以下のようなエラーメッセージが表示されます。

The name field is required.
The email field is required.

どちらの項目も必須項目に設定してあるためfield is requiredのメッセージが出ます。

次はNameを11文字以上、Emailを21文字以上にしてみましょう。

The name must not be greater than 10 characters.
The email must not be greater than 20 characters

今度はmust not be greater thanのメッセージが出ました。意図した通りにValidationが効いていますね。


ちなみに今回のような規定のvalidationを使用している場合、resources/lang/en/validation.phpでエラーメッセージを編集することができます。定義されている連想配列の中からrequired、maxとそれぞれのキーを探して値として設定されているエラーメッセージを編集してください。

※上記のファイルは英語用のファイルとして定義されています。日本語用のファイルを設定したい方は以下を参考にしてください。
laravel.com

Form Requestを使ったValidation

次はForm Request機能を使ったValidationを試してみます。

Form Requestはartisanコマンドで作ることができます。

php artisan make:request ValidationRequest

app/Http/Requests/ValidationRequest.phpを以下のように書き換えます。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ValidationRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        // false -> true
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        // 更新
        return [
            'name' => 'required|max:15',
            'email' => 'required|max:25'
        ];
    }
}

Validationの内容が一緒だと変化したかわかりずらいのでそれぞれmaxを15と25に設定してみました。これでコントローラーのValidationが不要になったので削除します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ValidationController extends Controller
{
    public function index()
    {
        return view('validation');
    }

    // 更新
    public function validation(ValidationRequest $request)
    {
        $request->validated();
        echo "name: $request->name<br>";
        echo "email: $request->email<br>";
    }
}

$request->validated();のでForm Requestで設定したValidationを呼び出しています。早速使ってみましょう。

The name must not be greater than 15 characters.
The email must not be greater than 25 characters.

それぞれ15文字と25文字の制限になりましたね。これでForm RequestのValidationを実装できました。

ちなみにForm Requestではエラーメッセージのカスタムもできます。やってみましょう

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ValidationRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required|max:15',
            'email' => 'required|max:25'
        ];
    }

    // 追加
    public function messages()
    {
        return [
            'name.required' => 'Name is required.',
            'name.max' => 'Name must be less than 15 characters.',
            'email.required' => 'Email is required.',
            'email.max' => 'Email must be less than 25 characters.',
        }
    }
}

messages()メソッドはFormRequestクラスに定義されており、それをオーバーライドする形で定義します。requestのキーとValidationのルール名をドットで繋ぐ記法を使っています。上記のコードでそれぞれを未入力のままSubmitすると

Name is required.
Email is required.

ちゃんと反映されてますね。

Nameを16文字以上、Emailを26文字以上入力すると

Name must be less than 15 characters.
Email must be less than 25 characters.

こちらもちゃんと反映されてますね。

それとattributes()メソッドをオーバーライドすることで汎用的なエラーメッセージを作ることもできます。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ValidationRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required|max:15',
            'email' => 'required|max:25'
        ];
    }

    // 追加
    public function attributes()
    {
        return [
            'name' => 'Name',
            'email' => 'Email'
        ];
    }

    public function messages()
    {
        return [

            '*.required' => ':attribute is required.',
            'name.max' => 'Name must be less than 15 characters.',
            'email.max' => 'Email must be less than 25 characters.',
        ];
    }
}

このようにすることで:attributeの部分がnameのエラーではName、EmailのエラーではEmailという表示をしてくれるようになります。

Validationのリダイレクト先の指定

Validationでエラーになった場合に元の画面ではなく、特定の指定した画面にリダイレクトさせたい場合があると思います。その場合はValidator::make()メソッドを使うことができます。

<?php

namespace App\Http\Controllers;

use App\Http\Requests\ValidationRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class ValidationController extends Controller
{
    public function index()
    {
        return view('validation');
    }

    public function validation(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|max:15',
            'email' => 'required|max:25'
        ]);

        if($validator->fails()) {
            return redirect('/error');
        }
        echo "name: $request->name<br>";
        echo "email: $request->email<br>";
    }
}

fails()メソッドはValiationでエラーが発生した際に、trueを返します。なのでエラーが発生したら/errorにリダイレクトするという処理になります。

リダイレクト先が必要になるので以下のようにルーティングを追加してください。

<?php

use App\Http\Controllers\ValidationController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Route::get('/validation', [ValidationController::class, 'index']);
Route::post('/validation', [ValidationController::class, 'validation']);

// 追加
Route::get('/error', function() {
    return 'ERROR.';
});

これでValidationでエラーが出るデータを入力してsubmitすると

ERROR.

と表示されるはずです。

まとめ

ということでValidation機能を使ってみました。今回はrequiredとmaxしかしようしていませんが、組み込みのValidationルールは数多くあるので是非公式ドキュメントを見て使ってみてください。

laravel.com

サービスコンテナ入門(Laravel)

5月から配属されたチームがLaravelを使ったチームなので、ちゃんと勉強しとこうと思ってLaravelのドキュメントを読み始めました。Laravelを使う上で避けて通れないのが「サービスコンテナ」ということで、本ポストを学んだことの事項とします。

サービスコンテナとは

公式ドキュメントでは「サービスコンテナはクラスの依存関係を管理し依存性の注入(Dependency Injection)を実行するための強力なツールです。」と記載があります。

laravel.com

依存性の注入(Dependency Injection)ってなに?なんのためにあるのっていう方にはこちらの記事がおすすめです。

qiita.com

それでは早速Laravelのコードを書いて学んでいきましょう。

準備

まずはLaravelプロジェクトを作ります。

composer create-project laravel/laravel service-container-example

※最新の公式ドキュメントではLaravel Sailの利用が推奨されているんですが、今開発用に使っているM1 MacBook Airだとエラーが解決できなかったのでcompoerを使ってます。

プロジェクトディレクトリに入ってPHP用のテストサーバを起動します。

cd service-container-example
php artisan serve

ブラウザからhttp://localhost:8000にアクセスしてLaravelのWelcomeページが表示されれば準備完了です。

サービスコンテナの使い方

サービスコンテナを使うためのAPIを作っていきます。まずはコントローラーからです。プロジェクトルートディレクトリで以下のコマンドを実行します。

php artisan make:controller HelloController

./app/Http/Controllers/HelloController.phpができるので、indexメソッドを作成します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class HelloController extends Controller
{
    // 追加
    public function index()
    {
        return "Hello.";
    }
}

./routes/web.phpにHelloController@index用のルーティングを設定します。

<?php

use App\Http\Controllers\HelloController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

// 追加
Route::get('/hello', [HelloController::class, "index"]);

これでhttp://localhost:8000/helloにアクセスしたら「Hello.」と表示されます。

それではこの機能を単純なサービスコンテナを使って実装していきます。

./app/Models/HelloMaker.phpを以下の内容で作成します。

<?php

namespace App\Models;

class HelloMaker
{
    public function text()
    {
        return "Hello from Hello Maker.";
    }
}

以下のコマンドを実行してHelloMakerProvider.phpを作成します。

php artisan make:provider HelloMakerProvider

./app/Providers/HelloMakerProvider.phpが作成されるのでregisterメソッドにサービスコンテナを利用するための処理を追加します。

<?php
namespace App\Providers;

use App\Models\HelloMaker;
use Illuminate\Support\ServiceProvider;

class HelloMakerProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        // 追加
        $this->app->bind(HelloMaker::class, function() {
            return new HelloMaker();
        });
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

そして./config/app.phpで定義されている連想配列のprovidersの項目に「App\Providers\HelloMakerProvider::class」を追加します。
ファイルは結構大きいので全ては載せませんが追加後のprovidersは私の環境では以下のようになっています。

    'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,
        Illuminate\Bus\BusServiceProvider::class,
        Illuminate\Cache\CacheServiceProvider::class,
        Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
        Illuminate\Cookie\CookieServiceProvider::class,
        Illuminate\Database\DatabaseServiceProvider::class,
        Illuminate\Encryption\EncryptionServiceProvider::class,
        Illuminate\Filesystem\FilesystemServiceProvider::class,
        Illuminate\Foundation\Providers\FoundationServiceProvider::class,
        Illuminate\Hashing\HashServiceProvider::class,
        Illuminate\Mail\MailServiceProvider::class,
        Illuminate\Notifications\NotificationServiceProvider::class,
        Illuminate\Pagination\PaginationServiceProvider::class,
        Illuminate\Pipeline\PipelineServiceProvider::class,
        Illuminate\Queue\QueueServiceProvider::class,
        Illuminate\Redis\RedisServiceProvider::class,
        Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
        Illuminate\Session\SessionServiceProvider::class,
        Illuminate\Translation\TranslationServiceProvider::class,
        Illuminate\Validation\ValidationServiceProvider::class,
        Illuminate\View\ViewServiceProvider::class,

        /*
         * Package Service Providers...
         */

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,

        // 追加
        App\Providers\HelloMakerProvider::class,
    ],


最後にHelloControllerの内容を以下のように編集します。

<?php

namespace App\Http\Controllers;

use App\Models\HelloMaker;
use Illuminate\Http\Request;

class HelloController extends Controller
{
    private $hello_maker;

    // 追加
    public function __construct(HelloMaker $hello_maker)
    {
        $this->hello_maker = $hello_maker;
    }

    public function index()
    {
        return $this->hello_maker->text();
    }
}

HelloMakerProvider.phpのregisterメソッドの処理を記載することによって、HelloControllerのコンストラクタでHelloMakerが呼ばれたときに自動的に新しいインスタンスが生成されるようになります。

これでhttp://localhost:8000/helloにアクセスしたときに「Hello from Hello Maker.」と表示されます。

ただこれはサービスコンテナを使ってはいますがHelloMakerがそのまま出てきているので依存性の注入の利点が活かせていません。ということで依存性の注入を活かすためにインターフェースを利用したバージョンにしてみましょう。

インターフェースを使ったバージョン

まずは./app/Models/TextMaker.phpをinterfaceとして定義します。

<?php

namespace App\Models;

interface TextMaker
{
    public function text();
}

HelloMakerをTextMakerの実装にします。

<?php

namespace App\Models;

// implements TextMakerを追加
class HelloMaker implements TextMaker
{
    public function text()
    {
        return "Hello from Hello Maker.";
    }
}

./app/Providers/HelloMakerProvider.phpを以下のように編集します。

<?php

namespace App\Providers;

use App\Models\HelloMaker;
use App\Models\TextMaker;
use Illuminate\Support\ServiceProvider;

class HelloMakerProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        // 変更
        $this->app->bind(TextMaker::class, HelloMaker::class);
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

./app/Http/Controllers/HelloController.phpの内容を以下に編集します。

<?php

namespace App\Http\Controllers;

use App\Models\TextMaker;

class HelloController extends Controller
{
    // $hello_maker から$text_makerに変更
    private $text_maker;

    public function __construct(TextMaker $text_maker)
    {
        $this->text_maker = $text_maker;
    }

    public function index()
    {
        return $this->text_maker->text();
    }
}

これでインターフェースを使ったバージョンの実装ができました。動作は変わっていませんが、HelloControllerではTextMakerしか参照していないのにhttp://localhost:8000/helloにアクセスすると「Hello from Hello Maker.」と表示されるはずです。

ついでにインターフェースの実装を呼び分けるパターンも試してみましょう

実装を呼び分けるパターン

以下のコマンドで./app/Http/Controllers/GoodbyeController.phpを作成します。

php artisan make:controller GoodbyeController

内容は以下のようにします。

<?php

namespace App\Http\Controllers;

use App\Models\TextMaker;
use Illuminate\Http\Request;

class GoodbyeController extends Controller
{
    private $text_maker;

    public function __construct(TextMaker $text_maker)
    {
        $this->text_maker = $text_maker;
    }

    public function index()
    {
        return $this->text_maker->text();
    }
}

./routes/web.phpにGoodbyeController用のルーティング設定を追加します。

use App\Http\Controllers\GoodbyeController;
use App\Http\Controllers\HelloController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Route::get('/hello', [HelloController::class, "index"]);

// 追加
Route::get('/goodbye', [GoodbyeController::class, "index"]);

./app/Models/GoodbyeMaker.phpを作成し、内容を以下のようにします。

<?php

namespace App\Models;

class GoodbyeMaker implements TextMaker
{
    public function text()
    {
        return "Goodbye from Goodbye Maker.";
    }
}

以下のコマンドで./app/Providers/TextMakerProvider.phpを作成します。

php artisan make:provider TextMakerProvider

内容を以下のようにします。

<?php

namespace App\Providers;

use App\Http\Controllers\GoodbyeController;
use App\Http\Controllers\HelloController;
use App\Models\GoodbyeMaker;
use App\Models\HelloMaker;
use App\Models\TextMaker;
use Illuminate\Support\ServiceProvider;

class TextMakerProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->when(HelloController::class)
            ->needs(TextMaker::class)
            ->give(HelloMaker::class);
        
        $this->app->when(GoodbyeController::class)
            ->needs(TextMaker::class)
            ->give(GoodbyeMaker::class);
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

./app/Providers/HelloMakerProvider.phpは不要になるので削除します。

最後に./config/app.phpのprovidersからApp\Providers\HelloMakerProvider::classをApp\Providers\TextMakerProvider::classを変更します。

これでhttp://localhost:8000/helloにアクセスすると「Hello from Hello Maker.」と表示され、http://localhost:8000/goodbyeにアクセスすると「Goodbye from Goodbye Maker.」と表示されます。

まとめ

以上がLaravelのサービスコンテナの使い方になります。今回のようなテキストを返すだけのAPIに対してサービスコンテナを使ってもあまりメリットは感じられません。しかし、コントローラごとにDBの向き先を変えたい場合や、テストでモックを使いたい場合などではインターフェースを利用した依存性の注入は非常に強力です。疎結合な設計をする際にも重要な技術になってくるので使い方を覚えておいて損はないはずです。みなさんも是非使ってみてください。

リーダブルコードから学んだプログラミングにおける命名原則

転職をして5月から新しい職場で働き始めました。オフィスの本棚には色々な本が置いてるわけですが、実はいままで読んだことなかったリーダブルコードを借りて読んでみました。Clean CodeとCode Complete(上巻のみ)を読んだことがあるので目新しい情報はなかったですが、そういえばこんなことも書いてあったなぁ。みたいなすっかり忘れていた項目も多かったので自分用のリファレンスマニュアルも兼ねて、久々のブログポストにしようと思います。

リーダブルコードは15章に分けて構成されていますが、本ポストではいわゆる「命名」について書かれている2章と3章で、もっとも重要だと感じた内容を3つにまとめました。

明確な単語を選ぶ

1つめは明確な単語を選ぶ、違う言い方をすると役割が伝わりやすい名前をつけることです。本書では「2.1 明確な単語を選ぶ」のほか「2.2 tmpやretvalなどの汎用的な名前を避ける」、「2.3 抽象的な名前よりも具体的な名前を使う」など複数のチャプターに分けられ、詳細な具体例が提示されています。それらに書かれているのは関数の動作や変数の役割を、より簡潔で正確に表すことができる言葉選びをする必要があるということです。

本書ではgetが例に挙げられています。getはプログラミングをしていれば頻繁に目にしますが、できる限り使うべきではありません。なぜなら、getは「値を取得する」こと以上の情報が得られないからです。getの代わりにdownloadを使用することでどこかのサーバーからダウンロードしてくるのだということが明確にわかるようになりますし、selectを使用することで複数の対象の中から取得するということが伝わりやすくなります。

このような単語のボキャブラリーを探す際に便利なThesaurusというサイトが紹介されていました。名前を考える際にチェックしてみてください。

名前に情報を追加する

2つ目は明確な名前をつけた上でより詳細な情報を追加することです。本書では単位を名前に含めることが例として挙げられています。たとえば、描画する図形の高さを保持する変数にheightとだけ名付けるのではなく、heightPixelのように単位を含めた変数名にすることで何を入れるべきなのかがひと目でわかるようになります。

他にも単位ではなく属性を追加するという方法もあります。パスワードを保持してる変数に対して、十分に安全な複雑さを持っている場合はsecurePasswordとしたり、逆に単純で短い場合にはinsecurePasswordと名前をつけることでどんな意図で使われる変数なのかがより詳細に伝わるようになります。ただこの例でいえば、セキュリティの担保するための重要な処理の周辺など、追加した情報が重要な役割を果たす場合でなければ不要な情報になってしまい、余計にわかりにくくしてしまうので注意してください。

不要な単語は投げ捨てる

3つ目は不要な単語は投げ捨てる、つまり意図を伝えるために不要な単語を取り除くことです。前項と対立するようにも感じますが、役割をわかりやすくするという目的は同じです。本書ではconvertToStringをtoStringに変えるという例が挙げられていました。toStringだけでも文字列に変換するという動作が十分に伝わるためconvertは不要だという考え方です。

他に例を挙げるとするならuserDataやuserInfoと名付けられている変数はほとんどの場合でuserで十分です。オブジェクト指向を意識したプログラミングが行われていれば、オブジェクトが何らかのデータを持っていることはあきらかです。そのため単にuserという変数であってもユーザのデータであることはひと目でわかります。

まとめ

非常に短い内容になりましたが、本当に重要なことはシンプルなものです。本ポストにわかりやすい具体的なプラクティスは含まれていませんが、日々この3つを意識してコードを書くことで命名の能力が養われていくはずです。是非、みなさんも試してみてください。

MacにHomebrewをインストールする方法

M1 MacBook Airを購入して本日無事届いたので開発環境構築のためにまずHomebrewをインストールした

Homebrewのインストール方法だが

まずHomebrewのページにアクセスしてページ上部にあるインストール用のコマンドをコピーしてターミナルで実行する

コマンドは2021年1月21日現在で以下のようになっている

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

パスワードが要求されるのでMacのログイン時のパスワードを入力する

そうするとインストールが実行される。正常に終了すると「Installation successful!」と出力される

最後にPathの設定をする。
※少し前までは必要なかったのか「Homebew インストール」とGoogle検索して出てくるページにはPathの設定が必要という記事は見当たらなかった。もしかしたら環境によっては不要なのかもしれない。

おそらく「Installation successful!」の少し下に以下のような出力があると思う

==> Next steps:
- Add Homebrew to your PATH in /Users/carametal/.zprofile:
    echo 'eval $(/opt/homebrew/bin/brew shellenv)' >> /Users/carametal/.zprofile
    eval $(/opt/homebrew/bin/brew shellenv)
- Run `brew help` to get started
- Further documentation: 
    https://docs.brew.sh

これは私の場合なので出力は人によって違うと思うが、上記の出力の2行目と3行目のコマンド(echoとeval)を実行するとPathが設定される。

以上の手順が終わればインストールが正常に完了しているはず。以下のコマンドでバージョンが正常に動くかどうかを確認して終了

brew -v

執筆時点での出力は以下

Homebrew 2.7.5
Homebrew/homebrew-core (git revision aac1e; last commit 2021-01-21)

CodeDeployを使ってEC2インスタンスにLaravelプロジェクトをデプロイする

AWSを有効活用するためCI/CD系をとりあえず試している中でCodeDeployの使い方は少しずつ分かってきたので、手順をまとめる。

EC2インスタンスの起動

まずはデプロイするサーバーとしてEC2インスタンスを起動します。
AWSマネジメントコンソールを開き、こちらの記事の「EC2インスタンスの作成」までを行ってください。

carametal.hatenablog.com

ただ、注意点としてステップ 3: インスタンスの詳細の設定で「AmazonEC2RoleforAWSCodeDeploy」ポリシーをもったIAMロールを設定するようにしてください。
なお、本記事ではEC2にKey=Name,Value=CodeDeployTrialというタグをつけたと仮定して進めていきます。

CodeDeployエージェントのインストール

次にCodeDeployを実行に必要なパッケージをEC2インスタンスにインストールします。

sshでEC2インスタンスに入ったら以下のコマンドを実行します。

sudo yum update -y
sudo yum -y install ruby
sudo yum install -y wget
cd /home/ec2-user
wget https://aws-codedeploy-ap-northeast-1.s3.ap-northeast-1.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto

5つ目のwgetコマンドに渡しているURLはEC2がどのリージョンにあるかによって変わります。
詳しくは以下をご覧ください。
CodeDeploy リソースキットリファレンス - AWS CodeDeploy

以上のコマンドが正常に終了したらCodeDeployエージェントのインストールは完了です。

Laravelプロジェクトの作成

ローカルの開発マシンにlaravel newコマンドでLaravelプロジェクトを作成します。Composerを使いますのでもしインストールをされていない方は以下からダウンロードしてください。
https://getcomposer.org/download/

以下のコマンドを実行するとcode-deploy-tiralというディレクトリを作成されます。その中にLaravelに必要なパッケージが揃った状態になっています。

laravel new code-deploy-trial

CodeDeployに必要なファイルを作成

code-deploy-trialディレクトリに入ります。

cd code-deploy-trial

以下の構成になるようにファイル、ディレクトリを作ります。

scripts
└ install_dependencies.sh
└ start_server.sh
└ stop_server.sh
appspec.yml

code-deploy-trialディレクトリ直下にappspec.ymlとscriptsディレクトリ、scriptsディレクトリに.shファイルが3つです。

そして各ファイルは以下の内容になります。

appspec.yml

version: 0.0
os: linux
files:
  - source: /
    destination: /var/www/html
hooks:
  BeforeInstall:
    - location: scripts/install_dependencies.sh
      timeout: 300
      runas: root
  ApplicationStart:
    - location: scripts/start_server.sh
      timeout: 300
      runas: root
  ApplicationStop:
    - location: scripts/stop_server.sh
      timeout: 300
      runas: root

install_dependencies.sh

#!/bin/bash
yum install -y httpd
amazon-linux-extras install php7.3


start_server.sh

#!/bin/bash
systemctl start httpd.service

stop_server.sh

#!/bin/bash
isExistApp=`pgrep httpd`
if [[ -n  $isExistApp ]]; then
	systemctl stop httpd
fi

install_dependencies.shのyum installは-yオプションを付け忘れると後々デプロイを失敗することがあります。注意してください。

これで設定ファイルの準備ができました。

ソースコードをデプロイするためのS3バケットの準備

s3バケットを作ります。
code-deploy-trialというバケット名は既に使われていたのでcarametal-code-deploy-trialとしました。

aws s3 mb s3://carametal-code-deploy-trial

S3バケットにCodeDeploy用のポリシーを設定します。

bucket-policy.jsonというファイルを以下の内容で作成してください。
バケット名、使用しているIAMユーザーのID、EC2を作るときに設定したIAMロールのARNとなっている部分は皆さんの環境に合わせて入れ替えてください。

{
  "Statement": [
      {
          "Action": [
              "s3:PutObject"
          ],
          "Effect": "Allow",
          "Resource": "arn:aws:s3:::バケット名/*",
          "Principal": {
              "AWS": [
                  "使用しているIAMユーザーのID"
              ]
          }
      },
      {
        "Action": [
            "s3:Get*",
            "s3:List*"
        ],
        "Effect": "Allow",
        "Resource": "arn:aws:s3:::バケット名/*",
        "Principal": {
            "AWS": [
                "EC2を作るときに設定したIAMロールのARN"
            ]
        }
    }
  ]
}

ちなみに使用しているIAMユーザーのIDは以下のコマンドでとれます。ちなみにIAMユーザーのARNを設定しても問題ありません。

aws sts get-caller-identity

EC2を作るときに設定したIAMロールのARNは以下のコマンドで取れます。

 aws iam get-role --role-name CodeDeployTrialRole

bucket-policy.jsonの準備ができたらS3バケットにポリシーを紐づけします。

aws s3api put-bucket-policy --bucket carametal-code-deploy-trial --policy file://bucket-policy.json

これでS3バケットの準備は完了です。

CodeDeployのセットアップ

CodeDeployにデプロイするため必要な準備を行っていきます。

まずはCodeDeployで扱うアプリケーションを作成します。
今回はCodeDeployTrialという名前でアプリケーションを作成します。

aws deploy create-application --application-name CodeDeployTrial

デプロイグループを作成します。
「EC2に設定したNameタグの値」と「AWSCodeDeployRoleを持つIAMロールのARN」はみなさんの環境に応じて変更してください。

aws deploy create-deployment-group --application-name CodeDeployTrial \
  --deployment-group-name CodeDeployTrialGroup \
  --deployment-config-name CodeDeployDefault.OneAtATime \
  --ec2-tag-filters Key=Name,Value=EC2に設定したNameタグの値,Type=KEY_AND_VALUE \
  --service-role-arn AWSCodeDeployRoleを持つIAMロールのARN

アプリケーションのデプロイ

そしていよいよアプリ―ケーションのデプロイを行います。
まずはソースコードをS3にアップロードします。

aws deploy push \
  --application-name CodeDeployTrial \
  --s3-location s3://carametal-code-deploy-trial/code-deploy-trial.zip

※以下のコマンドを実行するとLaravelプロジェクトのすべてのファイルがs3にアップロードされます。本来.envファイルやvendorディレクトリなどはサーバーごとで別のものを用意するべきと言われています。注意してください

そしてEC2インスタンスへのデプロイです。

aws deploy create-deployment \
  --application-name CodeDeployTrial\
  --deployment-group-name CodeDeployTrialGroup \
  --deployment-config-name CodeDeployDefault.OneAtATime \
  --s3-location bucket=carametal-code-deploy-trial,bundleType=zip,key=code-deploy-trial.zip,eTag=s3オブジェクトのETag

s3オブジェクトのETagとなっている部分は以下のコマンドで取得できます。

    • bucketバケット名、--keyは取得したいファイルのパスを引数とします。
aws s3api head-object --bucket carametal-code-deploy-trial --key code-deploy-trial.zip

これでデプロイの完了です。ただまだ終わりません。

Apacheの設定をLaravel向けにする

この状態でEC2インスタンスIPアドレスにアクセスしてみてもApacheのテストページが表示されてしまいます。設定ファイルの内容を変更してLaravelに向けるようにします。
/etc/httpd/conf/httpd.confをテキストエディタで開いて編集します。

sudo vim /etc/httpd/conf/httpd.conf

DocumentRootを"/var/www/html"から"/var/www/html/public"に変更します。

# DocumentRoot "/var/www/html"
DocumentRoot "/var/www/html/public"

これでIPへのアクセスがLaravelに向きます。

それともう一つの中にあるAllowOverrideをNoneからAllに変更します。
コメントがたくさんあると思いますが、コメントを除くと以下のようになります。
これはドキュメントルート以外にアクセスするために必要な設定です。

<Directory "/var/www/html">
    Options Indexes FollowSymLinks
    # AllowOverride None
    AllowOverride All
    Require all granted
</Directory>

このままだとLogが書き込めなかったというエラーになるので以下を実行します。
/var/www/html配下のディレクトリとファイルがapacheの所有物になりLogへの書き込みが可能になります。

sudo chown -R apache:apache /var/www/html/*

ようやくEC2のIPアドレスへブラウザからアクセスするとLaravelのwelcomページが表示されます。

これでLaravelプロジェクトをEC2インスタンスにデプロイできました。が、もう少しだけ続きます。

再デプロイ

先ほどAllowOverrideを変更してドキュメントルート以外へのアクセスを有効にしましたが、まだ実際に動くコードがありません。
なのでコードを追加し、EC2インスタンスに再デプロイして確かめてみます。
Laravelプロジェクトのroutes/web.phpの末尾に以下を追加します。

Route::get('info', function() {
    phpinfo();
});

S3へのaws deploy pushとaws deploy create-deploymentを再度実行します。create-deploymentのeTagはpushするたびに更新されるので新しい値を入力してください。

これでIPアドレス/infoにブラウザからアクセスするとPHPの詳細情報が表示されるはずです。

まとめ

なんとかCodeDeployの初歩的な使い方は理解できたかなと思います。ただ、.envとかvendorとかもまとめてデプロイしちゃってるから実際に運用するには向いてない。もっというとCI/CDにはまだほど遠いので、次回はCodePipelineも使ってより実践的な手法を身に着けたい。