
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、私はこれまで特定の会社や会員さんオンリーの管理システムを多く開発してきたのですが、インターネット上のユーザーが自由にコンテンツを投稿できるシステムもいくつか開発したことがあります。
そして、そんな場合にあった方がいい機能が・・・・・・
違反を報告する機能
です。
残念ながらインターネット上には悪意をもってコンテンツを投稿する人がいるのも事実なので、やはり何か不適切なものを発見したら管理者に通報する機能はあったほうがいいと考えています。
そこで
今回は、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();
}
}
// 省略
そして、このReportCategoriesTableSeeder
をLaravel
に登録しておきます。
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
内のparams
とreportCategories
です。
なぜなら、ここのデータはLaravel
側からやってきたものをコードとして出力し、その出力したものがVue
コードの一部になっているからで、つまりPHP
とJavaScript
が連携してるわけですね。
テストしてみる
では、posts
テーブルをつくり、データを追加して以下のURLにアクセスしてみます。
https://******/report/create
するとフォームが表示されるので以下のように入力して「報告する」ボタンをクリックしてみます。
すると・・・・・・
以下のようなメールが送信されてきました。
成功です
※なお、メール送信のテストをするには、MailCatcherが便利ですよ
ダウンロードする
今回実際に開発したソースコード一式を以下からダウンロードできます。
「違反を報告」機能をつくる※ただし、マイグレーション等はご自身で行っていただく必要があります。
おわりに
ということで、今回は「違反を報告する」機能をつくってみました。
YouTube
でもTwitter
でもそうですが、やはりユーザーが増えてくると予期しない使い方をする人が出てくるものですので、こういった機能があれば少しは対処しやすくなるんじゃないでしょうか。
さらに、不適切なコンテンツを投稿する側からしても「あ!通報される可能性があるぞ」となると、めったなことはできなくなるはずですので、予防効果もあるかと思います。
ぜひみなさんもやってみてくださいね。
ではでは〜
「SPY x Family っていう漫画、
面白いですね」