Laravelで「違反を報告」機能をつくる

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、私はこれまで特定の会社や会員さんオンリーの管理システムを多く開発してきたのですが、インターネット上のユーザーが自由にコンテンツを投稿できるシステムもいくつか開発したことがあります。

そして、そんな場合にあった方がいい機能が・・・・・・

違反を報告する機能

です。

残念ながらインターネット上には悪意をもってコンテンツを投稿する人がいるのも事実なので、やはり何か不適切なものを発見したら管理者に通報する機能はあったほうがいいと考えています。

そこで❗

今回は、Laravel + Vueでこの「違反を報告する」機能を実装してみたいと思います。

ぜひ皆さんのお役に立てましたら嬉しいです😊✨
(最後にソースコード一式をダウンロードできますよ👍)

「くせっ毛を治すために
ヘアピンで矯正中です😂」

開発環境: Laravel 7.x

やりたいこと

今回やりたいことの詳細は次のとおりです。

  • 違反報告をメール受信できるようにする
  • 違反報告するカテゴリを選択できるようにする
  • より汎用的に使えるように、どのテーブル(モデル)でもパラメーターで切り替えられるようにする

では、やってみましょう❗

設定ファイルをつくる

まず、「違反を報告」機能の設定ファイルをつくっておき、この中で各種データを登録することにします。

config/report.php

<?php

return [

    // 違反報告を受信する管理者メールアドレス
    'emails' => [
        'admin@example.com',
    ],

    // 報告メールの送信者
    'from' => 'from@example.com',

    // 対象モデル(キーはテーブル名)
    'targets' => [
        'posts' => [
            'model' => \App\Post::class,
            'url' => 'post/{id}'
        ],
        'books' => [
            'model' => \App\Book::class,
            'url' => function($id) { // コールバック関数でもOK

                return url('/book/'. $id);

            }
        ],
    ],

];

この中で、targetsはキーがテーブル名(例:users)で、urlは、報告メールに含めるリンクに使われることになります。

なお、このURLは次の2パターンを想定しています。

  • {id}を置換したものを使用
  • コールバック関数で return されたものを使用

モデル&テーブルをつくる

次に、違反内容のカテゴリを管理するテーブルとモデルを作成します。
以下のコマンドを実行してください。

php artisan make:model ReportCategory -m

するとモデルとマイグレーションが一気に作成されるので、マイグレーションを以下のように変更してください。

database/migrations/****_**_**_******_create_report_categories_table.php

// 省略

public function up()
{
    Schema::create('report_categories', function (Blueprint $table) {
        $table->id();
        $table->string('name')->comment('カテゴリ名');
        $table->timestamps();
    });
}

// 省略

変更が完了したら、以下のコマンドでテーブルを作成しておいてください。

php artisan migrate

実際のテーブルはこのようになります。

カテゴリデータを追加するSeederをつくる

では、先ほどつくったreport_categoriesテーブルにデータを保存させるためにSeederファイルをつくります。

以下のコマンドを実行してください。

php artisan make:seed ReportCategoriesTableSeeder

すると、Seederファイルが作成されるので中身を以下のように変更してください。

database/seeds/ReportCategoriesTableSeeder.php

// 省略

public function run()
{
    // 違反内容
    $names = [
        100 => '暴力的なコンテンツ',
        200 => '差別的なコンテンツ',
        300 => '性的なコンテンツ',
        400 => '有害なコンテンツ',
        500 => 'スパム的なコンテンツ',
        999 => 'その他'
    ];

    foreach ($names as $id => $name) {

        $category = new \App\ReportCategory();
        $category->id = $id;
        $category->name = $name;
        $category->save();

    }

}

// 省略

そして、このReportCategoriesTableSeederLaravelに登録しておきます。

database/seeds/DatabaseSeeder.php

// 省略

public function run()
{
     // $this->call(UsersTableSeeder::class);
     $this->call(ReportCategoriesTableSeeder::class); // 👈 追加
}

// 省略

これで準備は完了です。
以下のコマンドでデータをテーブルに保存しましょう。

php artisan db:seed --class=ReportCategoriesTableSeeder

なお、全てのテーブルをゼロから構築しなおしてもOKです。
その場合はこちら。

php artisan migrate:fresh --seed

実際のテーブルはこうなります。

※なお、なぜIDを連番にしていないかというと、後から別のカテゴリを差し込みたい場合に都合がいいからです。連番だと間に差し込めないので。もしくは、sortなどの項目を追加して並べ替えできるようするのもいいですね👍

ルートをつくる

では、違反報告フォームと送信先のルートをつくりましょう。

routes/web.php

// 省略

Route::get('report/create', 'ReportController@create');
Route::post('report', 'ReportController@store');

上のルートが違反報告フォームで、下のルートはAjaxの送信先になります。

バリデーション・ルールをつくる

次に、コントローラーで使うことになる独自のバリデーション・ルールをつくります。

チェックするのは以下の2点です。

  • 対象のテーブルはコンフィグで設定されているか?
  • そのテーブルに対象IDが存在しているか?

では、以下のコマンドを実行してください。

php artisan make:rule HasReportTarget

するとファイルが作成されるので、中身を次のように変更します。

app/Rules/HasReportTarget.php

<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;

class HasReportTarget implements Rule
{
    public function __construct()
    {
        //
    }

    public function passes($attribute, $id)
    {
        $target = request('target');
        $model = $this->getModel($target);

        return (
            $model instanceof Model && // コンフィグで設定されている
            $model->where('id', $id)->exists() // テーブルにIDが存在している
        );
    }

    public function message()
    {
        return '該当データが見つかりません。';
    }

    private function getModel($table) {

        $targets = config('report.targets');

        if(Arr::exists($targets, $table)) {

            $class = config('report.targets')[$table]['model'];

            if(class_exists($class)) {

                return new $class();

            }

        }

        return null;

    }
}

この中で重要なのが、getModel()の部分です。

ここでは、config/report.phpで設定したモデルを取得し、さらにそのモデルは本当に存在しているのかをチェックしてからインスタンスを返すようにしています。

※なお、バリデーション・エラーは翻訳させていません。詳しくは、インストール直後にやること3点 > バリデーションをご覧ください。

メール送信部分をつくる

では、先に違反報告メールを送信する部分をつくっておきましょう。
以下のコマンドを実行してください。

Mailableをつくる

php artisan make:mail Reported

すると、ファイルが作成されるので中身を次のように変更します。

app/Mail/Reported.php

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;

class Reported extends Mailable
{
    use Queueable, SerializesModels;

    private $data;

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

    public function build()
    {
        $target = $this->data['target'];
        $id = $this->data['id'];
        $comment = $this->data['comment'];
        $category_id = $this->data['report_category_id'];
        $category_name = \App\ReportCategory::where('id', $category_id)->value('name');
        $url_pattern = config('report.targets')[$target]['url'];
        $url = '';

        if(is_string($url_pattern)) { // 文字列の場合

            $url = str_replace('{id}', $id, $url_pattern);

        } else if(is_callable($url_pattern)) { // コールバック関数の場合

            $url = $url_pattern($id);

        }

        $emails = config('report.emails');
        $from = config('report.from');

        return $this->to($emails)
            ->from($from)
            ->subject('違反報告がありました')
            ->view('emails.reported')
            ->with([
                'category_name' => $category_name,
                'comment' => $comment,
                'url' => $url,
            ]);
    }
}

基本的なメール送信なので詳しくは割愛しますが、イレギュラーな部分は$urlを取得する部分です。

ここは、最初にconfig/report.phpで設定したURLを取得し、文字列とコールバック関数で分岐をさせています。

メールのテンプレート(ビュー)をつくる

続いて、メール送信する文面をテンプレートで作っておきましょう。

resources/views/emails/reported.blade.php

違反報告がありました。<br><br>

該当URL: <a href="{{ $url }}">{{ $url }}</a><br>
カテゴリ: {{ $category_name }}<br>
@if(!empty($comment))
メッセージ: <br>{!! nl2br($comment) !!}
@endif

ここで注意が必要なのが、nl2br()を使っている部分です。

nl2br()は、改行コードを<br>に変換してくれるPHP標準関数ですが、その周りの埋め込みコードは{!! ... !!}文字をエスケープ「しない」形式にしています。

これは、通常の{{ .... }}だと、<br>タグがエスケープされてHTMLでは改行にならなくなってしまうからです。

コントローラーをつくる

では、ルートで指定したReportControllerをつくります。
以下のコマンドを実行してください。

php artisan make:controller ReportController

ファイルが作成されるので中身を次のように変更してください。

app/Http/Controllers/ReportController.php

<?php

namespace App\Http\Controllers;

use App\Mail\Reported;
use App\Rules\HasReportTarget;
use Illuminate\Http\Request;
use Illuminate\Validation\Rules\Exists;
use Illuminate\Validation\Rules\In;

class ReportController extends Controller
{
    public function create(Request $request) {

        $report_categories = \App\ReportCategory::get();
        return view('report.create')->with([
            'id' => $request->id,
            'target' => $request->target,
            'report_categories' => $report_categories
        ]);

    }

    public function store(Request $request) {

        $validated_data = $request->validate([
            'id' => ['required', new HasReportTarget()],
            'target' => ['required'],
            'report_category_id' => ['required', 'exists:report_categories,id'],
            'comment' => ['required_if:report_category_id,999']
        ]);

        \Mail::send(new Reported($validated_data));

        return ['result' => true];

    }
}

create()の方が報告フォームを表示するメソッドで、store()Ajaxの送信先になり、実際にメールを送信する部分になります。

なお、このstore()の中では先ほどつくったHasReportTarget()ルールを使い、同じくバリデーションが通過したらReported()Mailableを使っています。

ビューをつくる

最後に送信フォームのビューをつくります。

resources/views/report/create.blade.php

<html>
<head>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
</head>
<body>
    <div id="app" class="p-3">
        <div class="row">
            <div class="col-4">
                <span class="badge badge-info mb-3">テストリンク</span>
                <ul class="pl-3">
                    <li>
                        <a href="?target=posts&id=3">[posts] テーブルの ID:3 を報告する</a>
                    </li>
                    <li>
                        <a href="?target=books&id=5">[books] テーブルの ID:5 を報告する</a>
                    </li>
                    <li>
                        <a href="?target=items&id=4">[items] テーブルの ID:4 を報告する(コンフィグで登録されていない)</a>
                    </li>
                    <li>
                        <a href="./create">パラメータなし</a>
                    </li>
                </ul>
            </div>
            <div class="col-4">
                <h1 class="mb-4">違反報告フォーム</h1>
                <div class="form-group">
                    <div class="alert alert-danger" v-if="errors.id" v-text="errors.id"></div>
                    <div class="alert alert-danger" v-if="errors.target" v-text="errors.target"></div>
                    <label>カテゴリ</label>
                    <select class="form-control" v-model="params.report_category_id">
                        <option value="">▼以下から選択してください</option>
                        <option v-for="c in reportCategories" :value="c.id" v-text="c.name"></option>
                    </select>
                    <div class="alert alert-danger" v-if="errors.report_category_id" v-text="errors.report_category_id"></div>
                </div>
                <div class="form-group">
                    <label>メッセージ</label>
                    <textarea rows="7" class="form-control" v-model="params.comment"></textarea>
                    <div class="alert alert-danger" v-if="errors.comment" v-text="errors.comment"></div>
                </div>
                <div class="text-right">
                    <button type="button" class="btn btn-danger btn-lg" @click="onSubmit">報告する</button>
                </div>
            </div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
    <script>

        new Vue({
            el: '#app',
            data: {
                params: {
                    id: '{{ $id }}',
                    target: '{{ $target }}',
                    report_category_id: '',
                    comment: ''
                },
                errors: {},
                reportCategories: {!! $report_categories !!}
            },
            methods: {
                onSubmit() {

                    if(confirm('違反報告を送信します。よろしいですか?')) {

                        this.errors = {};
                        const url = '/report';
                        axios.post(url, this.params)
                            .then(response => {

                                if(response.data.result === true) {

                                    alert('報告が完了しました');

                                    this.params.report_category_id = '';
                                    this.params.comment = '';

                                }

                            })
                            .catch(error => {

                                const responseErrors = error.response.data.errors;
                                let errors = {};

                                for(const key in responseErrors) {

                                    errors[key] = '⚠ '+ responseErrors[key][0];

                                }

                                this.errors = errors;

                            });

                    }

                }
            }
        });

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

この中で重要なのが、Vue内のparamsreportCategoriesです。

なぜなら、ここのデータはLaravel側からやってきたものをコードとして出力し、その出力したものがVueコードの一部になっているからで、つまりPHPJavaScriptが連携してるわけですね。

テストしてみる

では、postsテーブルをつくり、データを追加して以下のURLにアクセスしてみます。

https://******/report/create

するとフォームが表示されるので以下のように入力して「報告する」ボタンをクリックしてみます。

すると・・・・・・

以下のようなメールが送信されてきました。

成功です😊✨

※なお、メール送信のテストをするには、MailCatcherが便利ですよ👍

ダウンロードする

今回実際に開発したソースコード一式を以下からダウンロードできます。

「違反を報告」機能をつくる

※ただし、マイグレーション等はご自身で行っていただく必要があります。

おわりに

ということで、今回は「違反を報告する」機能をつくってみました。

YouTubeでもTwitterでもそうですが、やはりユーザーが増えてくると予期しない使い方をする人が出てくるものですので、こういった機能があれば少しは対処しやすくなるんじゃないでしょうか。

さらに、不適切なコンテンツを投稿する側からしても「あ!通報される可能性があるぞ😫」となると、めったなことはできなくなるはずですので、予防効果もあるかと思います。

ぜひみなさんもやってみてくださいね。

ではでは〜❗


「SPY x Family っていう漫画、
面白いですね👍」

この記事が役立ちましたらシェアお願いします😊✨ by 九保すこひ
また、わかりにくい部分がありましたらお問い合わせからお気軽にご連絡ください。
(また、個人レッスンも承ってます👍)
このエントリーをはてなブックマークに追加       follow us in feedly