コードを共通化してスリムにするテクニック(Laravel&Vue)

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、先日レッスンをさせていただいている生徒さんから「重複するコードを共通化する」テクニックが勉強したいという貴重な意見をいただきました。

確かに、ドキュメントではコードの書き方自体は説明していますが、「より効率的な(スリムな)」書き方ととなると、それほど多くはない印象です。

かく言う私も、(まだまだ未熟ですが)思い起こせばいろんな天才プログラマーさんたちのコードからテクニックをパクってきた歴史があるので、この意見は確かに的を射ているなと感じました。

※このあたりは、問題が出たときに自分でフレームワークの中身までチェックしたり、英語版ですが、やはりstack overflowは勉強になりますよ👍

そこで❗

今回は、私がメインで使っている「Laravel(PHP)」「Vue」の中から、「こういう共通化ができますよ」というサンプルをまとめてみたいと思います。

ぜひ学習者さんたちのお役に立てると嬉しいです😊✨

「家では冬の寒さに耐えながら
クラフトビールを飲みます🍺」

共通化するメリット

そもそも「コードを共通化するとどんなメリットがあるの❓」という話ですが、個人的には以下の3つが大きいと考えています。

  • 一度書いたコードは使い回しができる(ラクできる)
  • 変更が必要になっても共通化したコードだけを変更すれば全てに適用される
  • 機能ごとに本流のコードと分割するため、頭の整理がしやすい

⚠ ご注意

とは言え、共通化はやり過ぎないように気をつけています。なぜなら「えっ、これだけのために分割したの・・・💦」ってなると逆に混乱してしまうからです。後でコードを読む人のことを考えながらプログラムするのがヒューマンですよね。

では、ひとつずつ見ていきましょう❗

Laravel:ViewComposerをつかう

ViewComposerとは、簡単に言うと「効率よくパラメータをビューに送る」機能です。

例えば、ログインユーザーのデータをビューに送る基本的なコードを見てみましょう。おそらく以下のようになると思います。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Str;

class HomeController extends Controller
{
    // 省略

    public function user_to_view_1(Request $request) {

        $user = $request->user();

        return view('user_to_view_1')->with([ // 👈 ここでビューに送る
            'user' => $user
        ]);

    }
}

もちろんこれでもいいのですが、例えば別のメソッドでも同じことをする場合どうなるでしょうか。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Str;

class HomeController extends Controller
{
    // 省略

    public function user_to_view_2(Request $request) {

        $user = $request->user();

        return view('user_to_view_2')->with([ // 👈 ここでも同じく送る
            'user' => $user
        ]);

    }
}

やはり、ここでも$userをビューに送らないといけません。

もちろん、数が少ない場合はこれでも問題ありませんが、サイトの規模が大きくなってくるといちいち$userを送るのはめんどうです。

そこで登場するのがViewComposerです。
ViewComposerを使えば先ほどのコードはひとつだけでOKになります。

では、実際に作ってみましょう❗
まず、ファイルをつくり中身を以下のようにします。

app/Providers/ViewServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;

class ViewServiceProvider extends ServiceProvider
{
    public function boot()
    {
        View::composer('*', function($view){

            if(auth()->check()) {

                $user = auth()->user();
                $view->with('user', $user); // 👈 ビューに $user を送る

            }

        });
    }
}

※ コードの意味としては「もしログインしていたら、そのユーザーデータを全てのビューに送る」ということになります。

では、このViewServiceProviderLaravelに登録して有効にしましょう。

config/app.php

<?php

return [

    // 省略

    'providers' => [

        // 省略

        App\Providers\ViewServiceProvider::class, // 👈 ここを追加しました

    ],

これで準備は完了です。

こうすることで、いちいち$userをビューに送るコードを省略してもビューの中で$userにアクセスできるようになります。

public function user_to_view_1() {

    return view('user_to_view_1'); // 👈 この中で $userが 使える!

}

なお、ViewComposerは特定のビューだけ有効にすることもできます。
例えば、「resources/views/auth」フォルダにあるビューだけ有効にするには、以下のようにします。

View::composer('auth/*', function($view){

    // 省略

});

また、ひとつのビューだけで有効にする場合はこうなります。

以下は、「resources/views/user_to_view_1.blade.php」だけで有効になる例です。

View::composer('user_to_view_1', function($view){

    // 省略

});

Laravel:スコープを使う

スコープとは、モデルに自分の好きな条件(WhereOrderByなど)を設定することができる機能です。

例えば、usersテーブルで以下のようなWHERE句を考えてみましょう。

  • 成人している(age >= 20)
  • 東京都に住んでいる(prefecture_id = 13 👈 都道府県コード)
  • 今日登録した人(created_at = 今日)

これをコードで書くと以下のようになります。

$users = \App\User::where('age', '>=', 20)
    ->where('prefecture_id', 13)
    ->whereDate('created_at', today())
    ->get();

もちろんこのコードを1ヶ所だけで使うなら、このままで問題ありません。

しかし、もし何度もこの条件が必要になり、繰り返しコピペして使ってしまうとどうなるでしょうか。

人間のすることですから、どこかが抜けてしまうこともあるでしょうし、もし一部コード変更する必要がでたら、全てのコードを変更しないといけません。

そのため、この部分を共通化して「ラクできるコード」にしてみましょう。
スコープはモデル内にセットします。

app/User.php

class User extends Authenticatable
{
    // 省略

    // スコープ
    public function scopeWhereNewTokyoAdults($query) {

        $query->where('age', '>=', 20)
            ->where('prefecture_id', 13)
            ->whereDate('created_at', today());

    }
}

すると、以下のようにたった1行でこの条件を呼び出せるので、繰り返し使う場合とてもラクですし、さらに保守管理にも有効です。

$users = \App\User::whereNewTokyoAdults()->get(); // 👈 これだけでOK

スコープの詳しい内容については、以下のページをご覧ください。

📝 【Laravel】スコープを使えば「うっかり」が減って「ラク」できる

Laravel:SQLを共通化する

スコープほど大掛かりではありませんが、条件分岐を使ったDBデータの取得でも「ちょっとした共通化」ができます。

例えば、今日と昨日で分岐したデータを取得する例を見てみましょう!

$when = 'today';

if($when === 'today') { // 👈 今日登録した人

    $today = today();
    $users = \App\User::where('id', '<', 10)
        ->where('age', '>=', 20)
        ->where('prefecture_id', 13)
        ->whereDate('created_at', $today)
        ->get();

} else if($when === 'yesterday') { // 👈 昨日登録した人

    $yesterday = today()->subDay();
    $users = \App\User::where('id', '<', 10)
        ->where('age', '>=', 20)
        ->where('prefecture_id', 13)
        ->whereDate('created_at', $yesterday)
        ->get();

}

このコードはifで分岐をしてDBデータを取得していますが、where()のほとんどは重複しています。

もちろんデータは正しく取得できますが、もし将来的に「prefecture_idの部分は削除するように」と仕様変更が出たとすると、2ヵ所修正しないといけなくなります。つまり(1ヵ所だけなら心配はありませんが)このコードには「削除を忘れてしまう」リスクがあるわけです。

そこで、このコードも共通化してみましょう。

$query = \App\User::where('id', '<', 10)
    ->where('age', '>=', 20)
    ->where('prefecture_id', 13); // 👈 まだ get() を実行しない

$when = 'today';

if($when === 'today') {

    $today = today();
    $query->whereDate('created_at', $today);

} else if($when === 'yesterday') {

    $yesterday = today()->subDay();
    $query->whereDate('created_at', $yesterday);

}

$users = $query->get(); // 👈 ここで get() を実行する

このコードでは先ほどの重複部分をひとつにまとめていますが、重要なのは、一番最初の部分でget()を呼び出していないことです。

そして、if文の中でさらに条件を追加し、最終的にget()しています。

また、when()を使うとよりシンプルなコードでデータ取得できます。

$when = 'today';
$users = \App\User::where('id', '<', 10)
    ->where('age', '>=', 20)
    ->where('prefecture_id', 13)
    ->when($when, function($q, $when) {

        if($when === 'today') {

            $today = today();
            $q->whereDate('created_at', $today);

        } else if($when === 'yesterday') {

            $yesterday = today()->subDay();
            $q->whereDate('created_at', $yesterday);

        }

    })->get();

when()は第一引数がtrue(空白でない)なら中身が実行されるようになっています。

※ なお、コード中に出てくるパラメータが固定なら.envconfigファイルの中で一言管理しておく方がいいでしょう。

PHP:Traitをつかう

Traitとは英語で「特徴」という意味の単語で、簡単に言うと「特定の機能だけ分割しておくもの」と考えてください。

では、Laravelのモデルを例にしてTraitを作ってみましょう❗

例えば、「」と「ゲーム」のDBテーブルがあり、どちらもprice(値段)のデータを持っているとします。

そして、例えばこの値段を「3桁カンマ + 円」(例:1,000円)という形式にする場合、以下のようなAccessorを使うことが多いと思います。

app/Book.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    // Accessor
    public function getPriceTextAttribute() { // 👈 重複する部分(その1)

        return number_format($this->price) .'円';

    }
}

app/Game.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Game extends Model
{
    // Accessor
    public function getPriceTextAttribute() { // 👈 重複する部分(その2)

        return number_format($this->price) .'円';

    }
}

しかし、コードを見ていただくとわかるとおりこのgetPriceTextAttribute()というメソッドは全く同じコードが重複しています。

ということで、この部分をTraitで共通化してみましょう。

まずはPriceTrait.phpというファイルを作成します。

app/Traits/PriceTrait.php

<?php

namespace App\Traits;

trait PriceTrait {

    // Accessor
    public function getPriceTextAttribute() { // 👈 共通化するメソッド

        return number_format($this->price) .'円';

    }

}

すると、このTraitを以下のように呼び出すだけでBookモデル、GameモデルどちらからでもgetPriceTextAttribute()を呼び出すことができるようになります。

app/Book.php

<?php

namespace App;

use App\Traits\PriceTrait; // 👈 ここを追加しました
use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    use PriceTrait; // 👈 ここを追加しました
}

app/Game.php

<?php

namespace App;

use App\Traits\PriceTrait; // 👈 ここを追加しました
use Illuminate\Database\Eloquent\Model;

class Game extends Model
{
    use PriceTrait; // 👈 ここを追加しました
}

つまり、本体にはAccessorを定義していませんが、このまま以下のようにして呼び出すことができるというわけですね。

ちなみに、TraitはPHPについている機能なのでAccessorだけではなく、どんなものにも使うことができます。(つまり、Laravelならコントローラーでも使えます)

$book = \App\Book::find(1);
dump($book->price_text); // 👈 TraitからAccessorを呼び出してます

$game = \App\Game::find(1);
dump($game->price_text); // 👈 これも同じです

※ なお、Traitの名前には「Notifiable」や「Billable」のように「****able(〜できる)」という形式が多く使われるようですが、今回はわかりやすくPriceTraitにしています。
Traitは、PHP 5.4以降で使える機能です。

Vue:ミックスインをつかう

Vueには、コードを共通化する「ミックスイン」という機能が備わっています。

では、ここでも値段を「3桁カンマ + 円」で表示するメソッドで見てみましょう❗

book_price.html

<html>
<body>
<div id="app">
    <h1>本の値段を表示するサンプル</h1>
    <span v-text="priceFormat(bookPrice)"></span> です。
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script>

    new Vue({
        el: '#app',
        data: {
            bookPrice: 1000
        },
        methods: {
            priceFormat(price) { // 👈 重複する部分(その1)

                return price.toString()
                    .replace(/\B(?=(\d{3})+(?!\d))/g, ',') + // 3桁カンマにします
                    '円';

            }
        }
    });

</script>
</body>
</html>

game_price.html

<html>
<body>
<div id="app">
    <h1>ゲームの値段を表示するサンプル</h1>
    <span v-text="priceFormat(gamePrice)"></span> です。
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script>

    new Vue({
        el: '#app',
        data: {
            gamePrice: 5000
        },
        methods: {
            priceFormat(price) { // 👈 重複する部分(その2)

                return price.toString()
                    .replace(/\B(?=(\d{3})+(?!\d))/g, ',') + // 3桁カンマにします
                    '円';

            }
        }
    });

</script>
</body>
</html>

こちらも見ていただくとわかるとおり、priceFormat()というメソッドが全く同じコードで重複しています。

ということで、この部分を「ミックスイン」で共通化してスリムにしてみましょう。

まず、ミックスインのファイルを作成します。

js/vue/mixins.js

const priceMixin = {
    methods: {
        priceFormat(price) {

            return price.toString()
                .replace(/\B(?=(\d{3})+(?!\d))/g, ',') + // 3桁カンマにします
                '円';

        }
    }
};

すると、以下のようにするだけでどこからでもpriceFormat()にアクセスすることができるようになります。

<html>
<body>
<div id="app">
    <h1>本の値段を表示するサンプル</h1>
    <span v-text="priceFormat(bookPrice)"></span> です。
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<!-- 👇 ミックスインを読み込み -->
<script src="/js/vue/mixins.js"></script>
<script>

    new Vue({
        el: '#app',
        mixins: [ priceMixin ], // 👈ここでセットします
        data: {
            bookPrice: 1000
        }
    });

</script>
</body>
</html>

さらに言うと、ミックスインはメソッドだけじゃなくdatacomputedmountedなど通常のVueで使うものにも対応しています。

以下の例を見てください。

const priceMixin = {
    data() {
        return {
            currency: 'JPY' // 通貨
        }
    },
    methods: {
        priceFormat(price) { // 3桁カンマ + 円

            return price.toString()
                .replace(/\B(?=(\d{3})+(?!\d))/g, ',') + // 3桁カンマにします
                '円';

        },
        tax(price) { // 消費税がいくらか計算

            return this.priceFormat(
                Math.floor(price * 0.1)
            );

        }
    },
    computed: {
        currencyText() { // 現在の通貨・テキスト

            return `現在の通貨は${this.currency}です。`;

        }
    },
    mounted() {

        this.currency = 'USD'; // 米ドル

    }
};

ミックスインの中にいろいろと追加していますが、これらは全て共通化コードとしてどこからでもアクセスができます。

そのため、Vue本体にメソッドなどを全く書かなくても次のような使い方ができます。

<html>
<body>
<div id="app">
    <h1>本の値段を表示するサンプル</h1>
    <span v-text="priceFormat(bookPrice)"></span> (税:<span v-text="tax(bookPrice)"></span>)です。
    <span v-text="currencyText"></span>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script src="/js/vue/mixins.js"></script>
<script>

    new Vue({
        el: '#app',
        mixins: [ priceMixin ],
        data: {
            bookPrice: 1000
        }
    });

</script>
</body>
</html>

これを実行するとこうなります。(赤枠の部分がミックスインの中から来ています)

⚠ ご注意

なお、ミックスインのdataは関数でreturnすることになっていますのでご注意ください。さらに、Vue 2まではdataをオブジェクトとしてセットできていましたが、Vue 3からは標準でもreturnする形になっているので、このあたりは頭の中を整理しておいた方がいいかもしれませんね👍

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はいつもとは少し違った視点の記事をお届けしてみました。

なお、いくつか思いつくままにまとめましたが、コードの共通化はこれ以外にも大小たくさんのものがあります。

例えば、JavaScriptの外部ファイル化もそうですし、コンポーネントもコード共通化のひとつです。

また、コードの共通化は「工夫」がものをいう分野だと思いますので、ぜひみなさんも「こうやったらもっと可読性があがるんじゃ❓❓」というようにパズルを解くような気持ちでやってみてはいかがでしょうか。

※ 個人的には、人気パズルゲーム「ぷよぷよ」の連鎖をよく授業中に考えてたんですが、その感覚と似てます😂

ではでは〜❗


「ぷよぷよでは、
スキヤポデスが好きでした👍」

このエントリーをはてなブックマークに追加       follow us in feedly