laravel6 パッケージ開発ハンズオン(パッケージを作ったことない人向け)
本記事を執筆した経緯
- パッケージに関する日本語記事の情報が散在しているため
成果物
簡素なユーザー編集機能、ユーザー退会機能を実装するパッケージを開発する
対象読者
laravelで個人開発をしたことがある方、gitの最低限の知識がある方
本記事で説明すること
packagist.orgを使用しパッケージを作成し公開する
本記事で説明しないこと
サービスプロパイダー ,サービスコンテナ ,PSR-4 ,githubとgitのコマンド jsonファイルについて
参考にした記事
おなじみ非公式日本語訳 laravel/uiのソースも参考にしました
前準備
laravel6系のプロジェクトを作成後、以下の記事に沿ってlaravel/uiをインストールし動作確認
*パッケージ名には必ずスラッシュを記述する必要がある
例: hogehoge/test
No packages found.の文字が出現したらpackagistに登録できます
登録するパッケージのディレクトリを作成する
vendor配下に以下通りにディレクトリを作成します
プロジェクト名\
├ app
├ bootstrap
├ config
├ database
......
├ vendor
├ bin
├ composer
.....
├ hogehoge(任意のパッケージ名)
└test(任意のパッケージ名)
├src
├Auth
└auth
└Request/
作成したディレクトリvendor/hogehoge/test配下でcomposer initを実行
compose init
色々きかれるので言われた通りに入力
Package name (<vendor>/<name>) [root/test]: hogehoge/test
Description []:
Author [ユーザー名 <メールアドレス>, n to skip]: name <mail@exmple.com>
Minimum Stability []:
Package Type (e.g. library, project, metapackage, composer-plugin) []: project
License []:
Define your dependencies.
Would you like to define your dependencies (require) interactively [yes]? yes
Search for a package:
Would you like to define your dev dependencies (require-dev) interactively [yes]? no
{
"name": "hogehoge/test",
"type": "project",
"authors": [
{
"name": "name",
"email": "mail@exmple.com"
}
],
"require": {}
}
Do you confirm generation [yes]? yes
composer.jsonファイルが作成されます。
compser.jsonファイルにパッケージをロードさせる手順を記述する
#から始まるコメントは消してご使用下さい
{
"name": "hogehoge/test",
"type": "project",
"authors": [
{
"name": "name",
"email": "mail@exmple.com"
}
],
"require": {},
"autoload": {
"psr-4": {
"App\\": "app/",
"hogehoge\\test\\": "src"#パッケージをロードする
},
"classmap": [
.....
"src/UserEdit_Operation_DB.php"#パッケージ内で作成した独自クラスをclassmapに追加する
]
},
"extra": {
"laravel": {
"providers": [
"hogehoge\\test\\UserEditServiceProvider"#config/app.phpにサービスプロパイダーを登録する
]
}
}
}
packagistに登録する前にデバックしたい場合
プロジェクト配下のcomposer.jsonに以下を記述
"autoload": {
"psr-4": {
"App\\": "app/",
"hogehoge\\test\\": "vendor/hogehoge/test/src"#パッケージをロードする
},
"classmap": [
"database/seeds",
"database/factories",
"vendor/hogehoge/test/src/UserEdit_Operation_DB.php" #UserEdit_Operation_DB.phpを作成した際に追加する※作成していない状態で追加するととエラーが発生する
]
},
その後コマンドでcomposer dump-autoloadを実行
composer dump-autoload
config/app.phpにサービスプロパイダーを追加する
return [
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
hogehoge\test\UserEditServiceProvider::class,
]
※うまくいかなかったらキャッシュクリアするとと改善します
パッケージ用のサービスプロバイダーを追加する。
namespace hogehoge\test;
use Illuminate\Support\ServiceProvider;
class UserEditServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function boot()
{
if ($this->app->runningInConsole()) {
#パッケージのArtisanコマンドをLaravelへ登録するには、commandsメソッドを利用する必要がある
$this->commands([
UserEditCommand::class,
]);
}
}
}
UserEditCommand.php を追加
<?php
namespace hogehoge\test;
use Illuminate\Console\Command;
class UserEditCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'useredit';
/**
* The console command description.
*
* @var string
*/
protected $description = 'CreateUserEdit&Withdrawal';
protected $views = [
'auth/UserEdit.stub' =>'auth/UserEdit.blade.php',
'auth/WithdrawalForm.stub' =>'auth/WithdrawalForm.blade.php',
];
protected $requests = [
'ChangePasswordRequest.stub' =>'ChangePasswordRequest.php',
'UpdateEmailRequest.stub' =>'UpdateEmailRequest.php',
'WithdrawalRequest.stub' =>'WithdrawalRequest.php',
];
/**
* Create a new command instance.
*
* @return void
*/
public function handle()
{
$this->exportRequests();
$this->exportViews();
$this->exportBackend();
$this->info('sucsess!');
}
protected function exportRequests(){
if(!file_exists(app_path('Http/Requests'))){
mkdir(app_path('Http/Requests'));
}
foreach ($this->requests as $key => $value) {
file_put_contents(
app_path('Http/Requests/'.$value),
$this->compileRequestStub($key)
);
}
}
protected function exportViews()
{
foreach ($this->views as $key => $value) {
$view = $this->getViewPath($value);
copy(
__DIR__.'/Auth/'.$key,
$view
);
}
}
protected function exportBackend()
{
file_put_contents(
app_path('Http/Controllers/Auth/UserEditController.php'),
$this->compileControllerStub()
);
file_put_contents(
base_path('routes/web.php'),
file_get_contents(__DIR__.'/Auth/auth/routes.stub'),
FILE_APPEND
);
}
protected function compileRequestStub($key)
{
return str_replace(
'{{namespace}}',
$this->laravel->getNamespace(),
file_get_contents(__DIR__.'/Auth/auth/Request/'.$key)
);
}
protected function compileControllerStub()
{
return str_replace(
'{{namespace}}',
$this->laravel->getNamespace(),
file_get_contents(__DIR__.'/Auth/auth/UserEditController.stub')
);
}
protected function getViewPath($path)
{
return implode(DIRECTORY_SEPARATOR, [
config('view.paths')[0] ?? resource_path('views'), $path,
]);
}
}
デバックできる様ににした場合は php artisan list でコマンドを確認できます
commandで生成するcontroller,request,bladeのstabファイルを作成する
@extends('layouts.app')
@section('content')
<body>
@if (session('status'))
{{ session('status') }}
@endif
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">ユーザー設定</div>
<div class="card-body">
<form method="POST" action="/user/edit/email">
@csrf
<div class="form-group row">
<label for="email" class="col-md-4 col-form-label text-md-right">email変更</label>
<div class="col-md-6">
<input id="email" name="Email" value="{{$auth["email"]}}" class="form-control @error('email') is-invalid @enderror">
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
<input type="hidden" name="UserId" value={{$auth["id"]}}>
<button dusk="view-button" class="btn btn-primary">更新</button>
</div>
</form>
<form method="POST" action="/user/edit/password">
@csrf
<div class="form-group row">
<label for="password" class="col-md-4 col-form-label text-md-right">現在のパスワードを入力</label>
<div class="col-md-6">
<input type="password" id="password" name="CurrentPassword" class="form-control @error('CurrentPassword') is-invalid @enderror">
@error('CurrentPassword')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="password" class="col-md-4 col-form-label text-md-right">新規パスワードを入力</label>
<div class="col-md-6">
<input type="password" id="password" name="newPassword" class="form-control @error('password') is-invalid @enderror" name="newPassword">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="password" class="col-md-4 col-form-label text-md-right">新規パスワードを再入力</label>
<div class="col-md-6">
<input type="password" id="password" name="newPassword_confirmation" class="form-control @error('newPassword') is-invalid @enderror" name="newPassword">
@error('newPassword')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
<input type="hidden" name="UserId" value={{$auth["id"]}}>
<button dusk="view-button" class="btn btn-primary">更新</button>
</div>
</form>
<form method="GET" action="/user/edit/delete">
<br>
<button class="button_font_variable_length_withdrawal">退会</button>
</form>
</div>
</div>
</div>
</div>
</body>
@endsection
namespace App\Http\Controllers\Auth;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\ChangePasswordRequest;
use App\Http\Requests\UpdateEmailRequest;
use App\Http\Requests\WithdrawalRequest;
use UserEdit_Operation_DB;
class UserEditController extends Controller
{
private function checkLogin(){
//ログインの有無をチェック
if (!Auth::check()) {
return \App::abort(404);
}
}
public function UserEditForm(Request $request){
//ユーザー編集画面を表示させるメソッド
$auth = auth::user();
$this->checkLogin();
return view('auth.UserEdit',['auth'=>$auth]);
}
public function EmailUpdate(UpdateEmailRequest $request){
//登録メールアドレスを更新するメソッド
$this->checkLogin();
$UserEdit_Operation_DB = new UserEdit_Operation_DB();
return $UserEdit_Operation_DB->EmailUpdate($request);
}
public function PasswordChange(ChangePasswordRequest $request){
//パスワードを変更するメソッド
$this->checkLogin();
$user = Auth::user();
$UserEdit_Operation_DB = new UserEdit_Operation_DB();
return $UserEdit_Operation_DB->PasswordChange($request,$user);
}
public function WithdrawalForm(){
//退会フォームを表示させるメソッド
$auth = auth::user();
return view('auth.WithdrawalForm',['auth'=>$auth]);
}
public function Withdrawal(WithdrawalRequest $request){
//退会処理を追加するメソッド
$id = auth::id();
$UserEdit_Operation_DB = new UserEdit_Operation_DB();
return $UserEdit_Operation_DB->Withdrawal($request,$id);
}
}
Route::group(['middleware' => ['auth']], function() {
Route::get('/user', 'Auth\UserEditController@UserEditForm');
Route::post('/user/edit/email','Auth\UserEditController@EmailUpdate');
Route::post('/user/edit/password','Auth\UserEditController@PasswordChange');
Route::get('/user/edit/delete','Auth\UserEditController@WithdrawalForm');
Route::post('/user/edit/Withdrawal','Auth\UserEditController@Withdrawal');
});
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class ChangePasswordRequest 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 [
'CurrentPassword' => ['required', 'string', 'min:8'],
'newPassword' => ['required', 'string', 'min:8', 'confirmed'],
];
}
public function withValidator(Validator $validator) {
$validator->after(function ($validator) {
$auth = Auth::user();
//現在のパスワードと新しいパスワードが合わなければエラー
if (!(Hash::check($this->input('CurrentPassword'), $auth->password))) {
$validator->errors()->add('CurrentPassword', __('Password does not match'));
}
if(!$this->input('newPassword') === $this->input('newPassword_confirmation') ){
$validator->errors()->add('newPassword', __());
}
});
}
}
```stub:vendor/hogehoge/test/src/Auth/auth/Request/UpdateEmailRequest.stub
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateEmailRequest 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 [
'Email' => 'required|email|max:100',
];
}
}
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class WithdrawalRequest 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 [
'CurrentPassword' => ['required', 'string', 'min:8'],
];
}
public function withValidator(Validator $validator) {
$validator->after(function ($validator) {
$auth = Auth::user();
//現在のパスワードと新しいパスワードが合わなければエラーを出力
if (!(Hash::check($this->input('CurrentPassword'), $auth->password))) {
$validator->errors()->add('CurrentPassword', __('Password does not match'));
}
});
}
}
ここまででコマンドを実行すればファイルが生成されますが、Userテーブルを操作するUserEdit_Operation_DB.phpを実装していないので次で実装します
userテーブルを操作する独自のクラスを実装
<?php
use App\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class UserEdit_Operation_DB
{
public function EmailUpdate($request){
//メール情報更新
return DB::transaction(function () use($request){
User::where('id',$request->UserId)
->lockForUpdate()
//専有ロック
->update(['email'=> $request->Email,]);
User::where('id',$request->UserId)
->update(['email_verified_at' =>NULL]);
//再度メール認証
return redirect('user')->with('status', __('メールアドレスの変更に成功しました'));
});
}
public function PasswordChange($request,$user){
//パスワード変更
return DB::transaction(function () use($request,$user){
User::where('id',$request->UserId)
->lockForUpdate();
$user->password = bcrypt($request->newPassword);
$user->save();
return redirect('user')->with('status', __('パスワードの変更に成功しました'));
});
}
public function Withdrawal($request,$id){
//退会
return DB::transaction(function () use($request,$id){
User::where('id',$id)
->lockForUpdate()
->delete();
Auth::logout();
return redirect('/')->with('status', __('退会できました。ご利用ありがとうございます'));
});
}
}
これでパッケージは完成です
パッケージをgithubのリモートリポジトリにpushする
packagist.orgとgithubを連携させてリモートリポジトリにpush時、更新する様にする
packagistのユーザー名からsettingsを選択
profileに show API Token ボタンがあるので押下後、トークンをコピーする
githubで対象のリモートレポジトリにアクセスしsettingsをクリック
-
Payload URL: https://packagist.org/api/github?username= packagist.orgで登録したユーザー名
-
Content Type: application/json
-
Secret: your 先程のAPIトークンをペースト
-
Which events would you like to trigger this webhook?
-- Just the push event is enough. を選択
アクセスしsubmitをクリック
Repository URLを入力しcheckボタンをクリック後、checkボタンがsubmitに変わるのでクリックすると登録に成功します
成功したら composer require (composer.jsonで登録したname)が表示されますので完成です
お疲れ様でした👏
Discussion