Laravel-generatorを使って爆速でtwitterを作るぞ!バックエンド編
※この記事はバックエンドまでです。要望があればフロントエンド編も作るかも?
年末年始にLaravelを使って何か作りたいな〜と思っていた時にこんな記事を見つけました
どうやらコマンドから作るだけでCRUDからテストまで作ってくれるらしい。。。すごい!
という事でLaravel-generatorを使ってtwitter風サイトを作ってみました
実際に作ってみたサイト
2022/7/7 停止中
IInetter
いいねがいっぱいできるtwitter風SNS
テストアカウント
Email:test@example.com
Password:password
ご自由に触ってもらってOKです
管理画面
Swagger API
GitHub
タグはv1参照
簡単な仕様
- ツイートするにはログイン必須
- 各ユーザーのタイムラインは未ログインで閲覧可能
- アカウント名・メールアドレスの変更。アカウントの削除が不可(ツイートは削除可能)
- 出来ること:アカウント登録・ツイート・リツイート・返信・いいね
- 出来ないこと:フォロー・ツイート検索・DM・ブックマーク・etc...(終わりが見えなかったので)
- 管理画面はadmin権限を付与したユーザーのみログイン可
使用環境・ツール
- PostgreSQL 12.5
- PHP 8.0
- Laravel 8.12
- laravel-generator 8.0.x-dev
- adminlte-templates 8.0.x-dev
- Swagger 2.0
- Nuxt 2.14 (Vue 2.6)
- ※Heroku(本番)だとapacheを使用
Dockerを作る
詳細はGitHubを見てください
version: "3"
services:
# Nginx ################################################
nginx:
build: ./nginx
environment:
TZ: Asia/Tokyo
expose:
- "80"
depends_on:
- php-fpm
volumes:
- "./web:/var/www/web:cached"
- "./log/nginx:/var/log/nginx"
ports:
- "80:80"
# php-fpm ################################################
php-fpm:
build: ./php-fpm
expose:
- "9000"
depends_on:
- mysql
volumes:
- "./web:/var/www/web:cached"
postgres:
build: ./postgres
environment:
POSTGRES_DB: db
POSTGRES_USER: root
POSTGRES_PASSWORD: root
volumes:
- "./.data/postgres:/var/lib/postgresql/data"
ports:
- "5432:5432"
バックエンドを作る
①プロジェクト作成&基本設定
dockerのphp-fpmに入ってLaravelプロジェクト作成
composer create-project laravel/laravel iinetter
.env.exampleをコピーして.envを作成&修正しておきます
DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=db
DB_USERNAME=root
DB_PASSWORD=root
基本的にはinfyomのドキュメントに従います
必要なパッケージをインストール&アップデート
composer require infyomlabs/laravel-generator=^8.0.x-dev
composer require laravelcollective/html=^6.2
composer require infyomlabs/adminlte-templates=^2.0
composer update
app.phpエイリアス追記
'Form' => Collective\Html\FormFacade::class,
'Html' => Collective\Html\HtmlFacade::class,
'Flash' => Laracasts\Flash\Flash::class,
コマンド実行(infyomのconfig作成)
php artisan vendor:publish --provider="InfyOm\Generator\InfyOmGeneratorServiceProvider"
APIルート更新
Route::prefix('api')
->middleware('api')
->as('api.')
->namespace($this->app->getNamespace().'Http\Controllers\API')
->group(base_path('routes/api.php'));
コマンド実行(infyomの用のBaseController等作成)
php artisan infyom:publish
たまにGitHubのAPIリミットに引っかかるのでHead toのリンクから「Personal access token」を作成して「Token (hidden): 」に入力してください
GitHub API limit (0 calls/hr) is exhausted, could not fetch https://api.github.com/repos/InfyOmLabs/swaggervel. Create a GitHub OAuth token to go over the API rate limit. You can also wait until ? for the rate limit to reset.
Head to https://github.com/settings/tokens/new?scopes=repo&description=xxxxxxxxxxxxxxxxxxxxxxxx
to retrieve a token. It will be stored in "/root/.composer/auth.json" for future use by Composer.
Token (hidden):
②ログイン画面作成
必要なパッケージをインストール&コマンド実行
composer require laravel/ui:^3.0
# ログイン画面作成
php artisan ui bootstrap --auth
# infyomのフォーマットに上書き
php artisan infyom.publish:layout --localized
エラーが起きるのでadminlte-templatesのバージョンを8.0.x-devに合わせてみる
ErrorException
copy(/var/www/web/iinetter/vendor/infyomlabs/adminlte-templates/templates/scaffold/auth/email_locale.stub): Failed to open stream: No such file or directory
再実行するとviewsファイルがlocalized形式で上書きされます。
composer require infyomlabs/adminlte-templates=^8.0.x-dev
php artisan infyom.publish:layout --localized
# 全部y(yes)
viewファイルが一通り揃ったのでひとまず日本語化
php -r "copy('https://readouble.com/laravel/8.x/ja/install-ja-lang-files.php', 'install-ja-lang.php');"
php -f install-ja-lang.php
php -r "unlink('install-ja-lang.php');"
laravel-generatorで自動生成したviewファイル対応のresourceが無いのでvendorから拾ってくる。日本語フォーマットはないので適宜日本語を設定。
cp ./vendor/infyomlabs/laravel-generator/locale/en/* ./resources/lang/ja
ログイン画面周りは一部日本語化してみた
<?php
return [
/*
|--------------------------------------------------------------------------
| 認証言語行
|--------------------------------------------------------------------------
|
| 以下の言語行は認証時にユーザーに対し表示する必要のある
| 様々なメッセージです。アプリケーションの必要に合わせ
| 自由にこれらの言語行を変更してください。
|
*/
'failed' => 'ログイン情報が登録されていません。',
'password' => '入力されたパスワードが正しくありません。',
'throttle' => 'ログインに続けて失敗しています。:seconds秒後に再度お試しください。',
'full_name' => '氏名',
'email' => 'メールアドレス',
'password' => 'パスワード',
'confirm_password' => 'パスワード確認',
'remember_me' => 'Remember Me',
'sign_in' => 'ログイン',
'sign_out' => 'ログアウト',
'register' => '登録',
'login' => [
'title' => 'ログイン',
'forgot_password' => 'パスワードを忘れた',
'register_membership' => '新規登録',
],
'registration' => [
'title' => '新規登録',
'i_agree' => '同意する',
'terms' => '規約',
'have_membership' => '既に登録済みです',
],
'forgot_password' => [
'title' => 'パスワードを初期化するには、Eメールを入力してください',
'send_pwd_reset' => 'メール送信',
],
'reset_password' => [
'title' => 'パスワードをリセット',
'reset_pwd_btn' => 'パスワードリセット',
],
'emails' => [
'password' => [
'reset_link' => 'クリックしてパスワードをリセット',
],
],
'app' => [
'member_since' => 'Member since',
'messages' => 'Messages',
'settings' => 'Settings',
'lock_account' => 'Lock Account',
'profile' => 'Profile',
'online' => 'Online',
'search' => 'Search',
'create' => 'Create',
'export' => 'Export',
'print' => 'Print',
'reset' => 'Reset',
'reload' => 'Reload',
],
];
configの設定を日本に
- 'timezone' => 'UTC',
+ 'timezone' => 'Asia/Tokyo',
- 'locale' => 'en',
+ 'locale' => 'ja',
- 'faker_locale' => 'en_US',
+ 'faker_locale' => 'ja_JP',
マイグレーションを走らせて
php artisan migrate
ログインしてみるとこんなログイン画面
③Swagger導入
せっかくの自動生成なのでswaggerも導入します
composerにrepositories追記
"repositories": [
{
"type": "vcs",
"url": "git@github.com:InfyOmLabs/swaggervel.git"
}
],
必要なパッケージをインストール&アップデート
composer require appointer/swaggervel=dev-master
composer require infyomlabs/swagger-generator=dev-master
composer update
コマンド実行(swagger用のconfigやjsが作成される)
php artisan vendor:publish --provider="Appointer\Swaggervel\SwaggervelServiceProvider"
swagger設定をonにして
'add_on.swagger' => true
swaggerの画面を確認するとこんな感じ
④auth周り作成
作成するauthは「api」と「web」の二種類作成していきます
④-1.apiのauth
基本的にはLaravelが用意した方式に合わせて作っていきます
最新だとPassportやSanctumを使った方式の方が良さそうです。。。
api_tokenカラム追加
php artisan make:migration add_column_api_token_to_users
Schema::table('users', function ($table) {
$table->string('api_token', 80)->after('password')
->unique()
->nullable()
->default(null);
});
config/auth.phpのhashをtrueに
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => true,
],
ドキュメントの通りにAPIトークン管理用のAPIを作る。V1配下なのはswaggerに合わせるため
php artisan make:controller API/V1/ApiTokenController
<?php
namespace App\Http\Controllers\API\V1;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class ApiTokenController extends Controller
{
/**
* 認証済みのユーザーのAPIトークンを更新する
*
* @param \Illuminate\Http\Request $request
* @return JsonResponse
*
* @SWG\Put(
* path="/api_token",
* summary="Update token",
* tags={"Auth"},
* description="Update token",
* produces={"application/json"},
* security={{"apiToken":{}}},
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="object",
* @SWG\Property(
* property="token",
* description="token",
* type="string"
* )
* )
* )
* )
* @SWG\SecurityScheme(
* securityDefinition="apiToken",
* type="apiKey",
* name="api_token",
* in="query"
* )
*/
public function update(Request $request)
{
return new JsonResponse(self::updateApiToken($request), 200);
}
/**
* update api_token
*
* @param Request $request
* @return array
*/
public static function updateApiToken(Request $request): array
{
$token = Str::random(60);
$request->user()->forceFill([
'api_token' => hash('sha256', $token),
])->save();
return ['token' => $token];
}
}
routeを更新
Route::group(['prefix' => 'v1', 'namespace' => 'V1'], function () {
Route::group(['middleware' => 'auth:api'], function () {
Route::put('api_token', 'ApiTokenController@update');
});
});
このままだとログイン出来ないので、ユーザー登録・ログインAPIを作成していきます。
まずはユーザー登録API
php artisan make:controller API/V1/RegisterController
Laravelのauth周りは大体Overrideすればなんとかなります。
swagger用にdocコメントも追記
<?php
namespace App\Http\Controllers\API\V1;
class RegisterController extends \App\Http\Controllers\Auth\RegisterController
{
/**
* 新規ユーザー作成
*
* @SWG\Post(
* path="/register",
* summary="Store a newly created User in storage",
* tags={"Auth"},
* description="Store User",
* produces={"application/json"},
* @SWG\Parameter(
* name="name",
* description="name of User",
* type="string",
* required=true,
* in="formData"
* ),
* @SWG\Parameter(
* name="email",
* description="email of User",
* type="string",
* required=true,
* in="formData"
* ),
* @SWG\Parameter(
* name="password",
* description="password of User",
* type="string",
* required=true,
* in="formData"
* ),
* @SWG\Parameter(
* name="password_confirmation",
* description="password_confirmation of User",
* type="string",
* required=true,
* in="formData"
* ),
* @SWG\Response(
* response=201,
* description="successful operation"
* )
* )
*/
}
次にログインAPI
php artisan make:controller API/V1/LoginController
app/Http/Controllers/Auth/LoginController.phpをマネして作成。ログイン時にapi_tokenを返すようにしておきます。
<?php
namespace App\Http\Controllers\API\V1;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* apiログイン。api_tokenを返す
*
* @SWG\Post(
* path="/login",
* tags={"Auth"},
* summary="Login",
* description="Login",
* produces={"application/json"},
* @SWG\Parameter(
* name="email",
* description="email of User",
* type="string",
* required=true,
* in="formData"
* ),
* @SWG\Parameter(
* name="password",
* description="password of User",
* type="string",
* required=true,
* in="formData"
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="object",
* @SWG\Property(
* property="token",
* description="token",
* type="string"
* )
* )
* )
* )
*/
/**
* Send the response after the user was authenticated.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
protected function sendLoginResponse(Request $request)
{
$this->clearLoginAttempts($request);
if ($response = $this->authenticated($request, $this->guard()->user())) {
return $response;
}
return new JsonResponse(ApiTokenController::updateApiToken($request), 200);
}
}
routeを更新
Route::group(['prefix' => 'v1', 'namespace' => 'V1'], function () {
Route::post('register', 'RegisterController@register');
Route::post('login', 'LoginController@login');
Route::group(['middleware' => 'auth:api'], function () {
Route::put('api_token', 'ApiTokenController@update');
});
});
swaggerからユーザー登録&ログイン
「Authorize」ボタンからapi_tokenをセット
auth認証できてる!
④-2.webのauth
is_adminのカラムを追加して、trueの場合のみ管理画面(web)にログインできるようにします。
is_adminカラム追加
php artisan make:migration add_column_is_admin_to_users
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(false);
});
ログイン条件を追加して
/**
* Get the needed authorization credentials from the request.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
protected function credentials(Request $request)
{
$credentials = $request->only($this->username(), 'password');
// 管理者ユーザーのみログイン可
$credentials['is_admin'] = true;
return $credentials;
}
ユーザー登録後に必ずログインさせるようにして
/**
* The user has been registered.
*
* @param \Illuminate\Http\Request $request
* @param mixed $user
* @return mixed
*/
protected function registered(Request $request, $user)
{
// ユーザー登録後に必ずログインさせるので、一旦ログアウト
Auth::logout();
}
初期データ投入時に管理者ユーザーを入れておきます
User::create([
'name' => 'admin',
'email' => 'admin@example.com',
'password' => Hash::make('password'),
'is_admin' => 1,
]);
これで管理画面側はadmin@example.comしかログイン出来なくなりました。
他のユーザーにも管理者権限を付与できるようにUserのCRUD画面を作っていきます!
その前に一旦初期化しておきます
php artisan migrate:fresh --seed
⑤CRUD作成
ここからいよいよlaragel-genaratorの出番
基本的には下記コマンドで進めていきます
infyom:api
がapiのCRUDを作成
infyom:scaffold
が画面のCRUDを作成
# とりあえずCRUD作成
php artisan infyom:scaffold Xxxxx
# スキーマからCRUD作成
php artisan infyom:scaffold Xxxxx --fieldsFile=Xxxxx.json
# スキーマからapi作成
php artisan infyom:api Xxxxx --fieldsFile=Xxxxx.json --prefix=v1
# テーブルからCRUD作成
php artisan infyom:scaffold Xxxxx --fromTable --tableName=xxxxx
コマンドを作る際には必ずmodel_schemasから生成するようにしていきます
参考URL
サンプルスキーマも参考になります
vendor/infyomlabs/laravel-generator/samples/fields_sample.json
リレーション周りはソースを参考に
vendor/infyomlabs/laravel-generator/src/Common/GeneratorFieldRelation.php
schema作成時のプロパティはざっくり以下の通り
{
// カラム名
"name": "user_id",
// カラム定義
"dbType": "bigInteger:unsigned:foreign,users,id",
// 入力フォームのタイプ
"htmlType": "",
// バリデーション
"validations": "",
// トップの検索から引っかかるか
"searchable": false,
// 入力許可
"fillable": true,
// プライマリーキーであるか
"primary": false,
// フォーム表示するか
"inForm": false,
// 一覧ページに表示するか
"inIndex": true,
// 詳細ページに表示するか
"inView": true
},
とりあえずざっくりテーブル設計
tweetsテーブルはそのままツイートを保存しておき、tweet_typeでリツイート・リプライ等を判別します。ref_tweet_idで参照元のツイートIDを保存しておきます。
users
------------------
id
name
email
email_verified_at
password
remember_token
created_at
updated_at
tweets
------------------
id
user_id
ref_tweet_id
tweet_type
tweet_text
reply_count
retweet_count
favorite_count
created_at
⑤-1.Userの画面用CRUD作成
usersテーブルのスキーマ定義を作成して
[
{
"name": "id",
"dbType": "bigInteger,true,true",
"htmlType": "",
"validations": "",
"searchable": false,
"fillable": false,
"primary": true,
"inForm": false,
"inIndex": true,
"inView": true
},
{
"name": "name",
"dbType": "string:unique",
"htmlType": "text",
"validations": "required|string|max:255|regex:\/^[0-9a-zA-Z]+$\/|unique:users,name",
"searchable": true,
"fillable": true,
"primary": false,
"inForm": true,
"inIndex": true,
"inView": true
},
{
"name": "email",
"dbType": "string:unique",
"htmlType": "email",
"validations": "required|email|max:255|unique:users,email",
"searchable": true,
"fillable": true,
"primary": false,
"inForm": true,
"inIndex": true,
"inView": true
},
{
"name": "email_verified_at",
"dbType": "timestamp:nullable",
"htmlType": "",
"validations": "",
"searchable": false,
"fillable": false,
"primary": false,
"inForm": false,
"inIndex": false,
"inView": false
},
{
"name": "password",
"dbType": "string",
"htmlType": "password",
"validations": "required|string|confirmed|min:8|regex:\/^[0-9a-zA-Z]+$\/",
"searchable": false,
"fillable": true,
"primary": false,
"inForm": true,
"inIndex": false,
"inView": false
},
{
"name": "api_token",
"dbType": "string,80:nullable:unique:defalut,null",
"htmlType": "",
"validations": "",
"searchable": false,
"fillable": false,
"primary": false,
"inForm": false,
"inIndex": false,
"inView": false
},
{
"name": "remember_token",
"dbType": "string,100:nullable",
"htmlType": "",
"validations": "",
"searchable": false,
"fillable": false,
"primary": false,
"inForm": false,
"inIndex": false,
"inView": false
},
{
"name": "created_at",
"dbType": "timestamp",
"htmlType": "",
"validations": "",
"searchable": false,
"fillable": false,
"primary": false,
"inForm": false,
"inIndex": false,
"inView": false
},
{
"name": "updated_at",
"dbType": "timestamp",
"htmlType": "",
"validations": "",
"searchable": false,
"fillable": false,
"primary": false,
"inForm": false,
"inIndex": false,
"inView": false
},
{
"name": "is_admin",
"dbType": "boolean:default,false",
"htmlType": "checkbox,1",
"validations": "nullable|boolean",
"searchable": false,
"fillable": true,
"primary": false,
"inForm": true,
"inIndex": true,
"inView": true
}
]
コマンド実行
php artisan infyom:scaffold User --fieldsFile=User.json
User.json上書きするか、マイグレーションを実行するか聞かれますが適宜答えます。今回はどちらもno
User.json already exists. Do you want to overwrite it? [y|N] (yes/no) [no]:
>
Do you want to migrate database? [y|N] (yes/no) [no]:
>
これでマイグレーションファイル、モデル、コントローラー、フォームリクエスト、リポジトリー、ファクトリー、ビューがいい感じに作られます。
今回はUserのマイグレーションファイルは要らないので削除。
Userモデルは通常のモデルと違うので修正します
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
/**
* @SWG\Definition(
* definition="User",
* required={"name", "email", "password"},
* @SWG\Property(
* property="id",
* description="id",
* type="integer",
* format="int32"
* ),
* @SWG\Property(
* property="name",
* description="name",
* type="string"
* ),
* @SWG\Property(
* property="email",
* description="email",
* type="string"
* ),
* @SWG\Property(
* property="email_verified_at",
* description="email_verified_at",
* type="string",
* format="date-time"
* ),
* @SWG\Property(
* property="password",
* description="password",
* type="string"
* ),
* @SWG\Property(
* property="api_token",
* description="api_token",
* type="string"
* ),
* @SWG\Property(
* property="remember_token",
* description="remember_token",
* type="string"
* ),
* @SWG\Property(
* property="created_at",
* description="created_at",
* type="string",
* format="date-time"
* ),
* @SWG\Property(
* property="updated_at",
* description="updated_at",
* type="string",
* format="date-time"
* ),
* @SWG\Property(
* property="is_admin",
* description="is_admin",
* type="boolean"
* )
* )
*/
class User extends Authenticatable
{
use HasFactory;
public $table = 'users';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'email',
'password',
'is_admin'
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password',
'email_verified_at',
'api_token',
'remember_token',
];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'id' => 'integer',
'name' => 'string',
'email' => 'string',
'password' => 'string',
'api_token' => 'string',
'remember_token' => 'string',
'is_admin' => 'boolean'
];
/**
* Validation rules
*
* @var array
*/
public static $rules = [
'name' => 'required|string|max:255|regex:/^[0-9a-zA-Z]+$/|unique:users,name',
'email' => 'required|email|max:255|unique:users,email',
'password' => 'required|string|confirmed|min:8|regex:/^[0-9a-zA-Z]+$/',
'is_admin' => 'nullable|boolean'
];
}
コントローラーもpasswordがそのまま登録・更新されてしまうので、Hash化してから保存するように修正
/**
* Store a newly created User in storage.
*
* @param CreateUserRequest $request
*
* @return Response
*/
public function store(CreateUserRequest $request)
{
$input = $request->all();
+ if (isset($input['password'])) {
+ $input['password'] = Hash::make($input['password']);
+ }
$user = $this->userRepository->create($input);
Flash::success('User saved successfully.');
return redirect(route('users.index'));
}
〜〜〜
/**
* Update the specified User in storage.
*
* @param int $id
* @param UpdateUserRequest $request
*
* @return Response
*/
public function update($id, UpdateUserRequest $request)
{
$user = $this->userRepository->find($id);
if (empty($user)) {
Flash::error('User not found');
return redirect(route('users.index'));
}
$input = $request->all();
+ if (isset($input['password'])) {
+ $input['password'] = Hash::make($input['password']);
+ }
$user = $this->userRepository->update($input, $id);
Flash::success('User updated successfully.');
return redirect(route('users.index'));
}
routeもログインユーザーのみ、Userの登録・更新・削除が出来るようにします
<?php
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');
});
Auth::routes();
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
Route::group(['middleware' => ['auth:web']], function () {
Route::resource('users', App\Http\Controllers\UserController::class);
});
passwordの確認フォームも必要なので追記しておきます
<!-- Password Field -->
<div class="form-group col-sm-6">
{!! Form::label('password', 'Password:') !!}
{!! Form::password('password', ['class' => 'form-control','minlength' => 8]) !!}
</div>
<!-- Password Confirmation Field -->
<div class="form-group col-sm-6">
{!! Form::label('password_confirmation', 'Password Confirmation:') !!}
{!! Form::password('password_confirmation', ['class' => 'form-control','minlength' => 8]) !!}
</div>
何故か画像のリンク切れてる。。。今回はadmin-templateを使いましたが、core-uiの方が良いかも?
これで管理画面からuserの登録・更新・削除はできるようになりました。かんたんですね。
現状だと誰でも登録・更新・削除できてしまうので、ロールやポリシーを追加すると良いです。
⑤-2.Tweetの画面用CRUD作成
php artisan infyom:scaffold Tweet --fieldsFile=Tweet.json
routeを更新
Route::group(['middleware' => ['auth:web']], function () {
Route::resource('users', App\Http\Controllers\UserController::class);
Route::resource('tweets', App\Http\Controllers\TweetController::class);
});
ツイート登録時にuser_idが必要なのでログインユーザーのuser_idを登録しておきます
/**
* Store a newly created Tweet in storage.
*
* @param CreateTweetRequest $request
*
* @return Response
*/
public function store(CreateTweetRequest $request)
{
$input = $request->all();
+ $input['user_id'] = auth('web')->id();
$tweet = $this->tweetRepository->create($input);
Flash::success('Tweet saved successfully.');
return redirect(route('tweets.index'));
}
ツイートカウント系をそのまま登録するとエラーになるので、初期値に0を設定しておきます
<!-- Reply Count Field -->
<div class="form-group col-sm-6">
{!! Form::label('reply_count', 'Reply Count:') !!}
{!! Form::number('reply_count', 0, ['class' => 'form-control','min' => 0]) !!}
</div>
<!-- Retweet Count Field -->
<div class="form-group col-sm-6">
{!! Form::label('retweet_count', 'Retweet Count:') !!}
{!! Form::number('retweet_count', 0, ['class' => 'form-control','min' => 0]) !!}
</div>
<!-- Favorite Count Field -->
<div class="form-group col-sm-6">
{!! Form::label('favorite_count', 'Favorite Count:') !!}
{!! Form::number('favorite_count', 0, ['class' => 'form-control','min' => 0]) !!}
</div>
はい、ツイートも登録できました
⑤-3.TweetのAPI用CRUD作成
次はapiを作ります。(prefixを付けているのはswaggerに合わせるためです)
php artisan infyom:api Tweet --fieldsFile=Tweet.json --prefix=v1
これでAPI用のコントローラー、フォームリクエスト、レスポンスリソース、テストコード、swaggerが作成されます。
v1のprefixを付けたのでコントローラーが見ているリポジトリ・モデルなどは前回作ったv1のついていない方を向けるように修正してください。そしてV1/に作成されたモデル、リポジトリは削除します。
自動生成されるAPIの改修が大きい場合はコントローラー名等を変えて別APIとして作った方が良さそうです。
ログインユーザーのみツイートできるようにして
Route::group(['prefix' => 'v1', 'namespace' => 'V1'], function () {
Route::post('register', 'RegisterController@register');
Route::post('login', 'LoginController@login');
Route::group(['middleware' => 'auth:api'], function () {
Route::put('api_token', 'ApiTokenController@update');
Route::resource('tweets', TweetAPIController::class);
});
});
ログインユーザーのuser_idをセットするように修正
public function store(CreateTweetAPIRequest $request)
{
$input = $request->all();
+ $input['user_id'] = auth('api')->id();
$tweet = $this->tweetRepository->create($input);
return $this->sendResponse(new TweetResource($tweet), 'Tweet saved successfully');
}
apiにauth認証が必要な場合は、swaggerのコメントにsecurity={{"apiToken":{}}}
を追記します
/**
* @param Request $request
* @return Response
*
* @SWG\Get(
* path="/tweets",
* summary="Get a listing of the Tweets.",
* tags={"Tweet"},
* description="Get all Tweets",
* produces={"application/json"},
+ * security={{"apiToken":{}}},
〜〜
* )
*/
public function index(Request $request)
swaggerを確認するとこんな感じにAPIが生えます
とりあえずテストを通すようにする
# test用env作成
cp .env .env.testing
# テスト用DBを作成してそっちを向かせる
DB_DATABASE=db_test
# テスト用DBマイグレーション
php artisan migrate --env=testing
このままだとテストが通らないのでfactory修正
<?php
namespace Database\Factories;
use App\Models\Tweet;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class TweetFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Tweet::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'user_id' => function () {
return User::factory()->create()->id;
},
'ref_tweet_id' => null,
'tweet_type' => $this->faker->randomElement(['tweet', 'retweet', 'reply']),
'tweet_text' => $this->faker->word,
'reply_count' => $this->faker->randomDigitNotNull,
'retweet_count' => $this->faker->randomDigitNotNull,
'favorite_count' => $this->faker->randomDigitNotNull,
];
}
}
自動生成したテストコードも走らせるように追記
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
+ <testsuite name="Repositories">
+ <directory suffix="Test.php">./tests/Repositories</directory>
+ </testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
+ <testsuite name="APIs">
+ <directory suffix="Test.php">./tests/APIs</directory>
+ </testsuite>
</testsuites>
ログインユーザーでテストが通るように修正
<?php namespace Tests\APIs;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
use Tests\ApiTestTrait;
use App\Models\Tweet;
use App\Models\User;
class TweetApiTest extends TestCase
{
use ApiTestTrait, WithoutMiddleware, DatabaseTransactions;
/**
* @test
*/
public function test_create_tweet()
{
$user = User::factory()->create();
$tweet = Tweet::factory()->make(['user_id' => $user->id, 'tweet_type' => 'tweet'])->toArray();
$this->response = $this->actingAs($user, 'api')->json(
'POST',
'/api/v1/tweets', $tweet
);
$this->assertApiResponse($tweet);
}
/**
* @test
*/
public function test_read_tweet()
{
$user = User::factory()->create();
$tweet = Tweet::factory()->create(['user_id' => $user->id, 'tweet_type' => 'tweet']);
$this->response = $this->actingAs($user, 'api')->json(
'GET',
'/api/v1/tweets/'.$tweet->id
);
$this->assertApiResponse($tweet->toArray());
}
/**
* @test
*/
public function test_update_tweet()
{
$user = User::factory()->create();
$tweet = Tweet::factory()->create(['user_id' => $user->id]);
$editedTweet = Tweet::factory()->make(['tweet_type' => 'tweet'])->toArray();
$this->response = $this->actingAs($user, 'api')->json(
'PUT',
'/api/v1/tweets/'.$tweet->id,
$editedTweet
);
$this->assertApiResponse($editedTweet);
}
/**
* @test
*/
public function test_delete_tweet()
{
$user = User::factory()->create();
$tweet = Tweet::factory()->create(['user_id' => $user->id, 'tweet_type' => 'tweet']);
$this->response = $this->actingAs($user, 'api')->json(
'DELETE',
'/api/v1/tweets/'.$tweet->id
);
$this->assertApiSuccess();
$this->response = $this->actingAs($user, 'api')->json(
'GET',
'/api/v1/tweets/'.$tweet->id
);
$this->response->assertStatus(404);
}
}
テストを走らせると
php artisan test
OK!
PASS Tests\Unit\ExampleTest
✓ basic test
PASS Tests\Repositories\TweetRepositoryTest
✓ create tweet
✓ read tweet
✓ update tweet
✓ delete tweet
PASS Tests\Feature\ExampleTest
✓ basic test
PASS Tests\APIs\TweetApiTest
✓ create tweet
✓ read tweet
✓ update tweet
✓ delete tweet
Tests: 10 passed
Time: 1.33s
説明がちょっと長くなってしまいましたが、こんな感じで作っていきます!
⑥まとめ
1テーブルに対してAPIとSwaggerとCRUD画面とテストコードを作るとしたら一週間くらいはかかりそうなものですが、Laravel-generatorを使うと慣れれば2〜3時間で作れてしまうのでとても良いですね!
あまり複雑ではないシステムや社内で使うツールなんかを作るのに向いていそうです!
逆に複雑なシステムだと素のLaravelを使った方が早そうです。pivot(中間テーブル)が多い場合や、テーブル更新が1対1では出来ない場合は向いていない感じがしました。(システムに合わせるか、サービスに合わせるかですね!)
GitHubにソース上げてますので、参考にしてみてください
Discussion