laravel6 パッケージ開発ハンズオン(パッケージを作ったことない人向け)

21 min読了の目安(約19700字TECH技術記事

本記事を執筆した経緯

  • パッケージに関する日本語記事の情報が散在しているため

成果物

簡素なユーザー編集機能、ユーザー退会機能を実装するパッケージを開発する

対象読者

laravelで個人開発をしたことがある方、gitの最低限の知識がある方

本記事で説明すること

packagist.orgを使用しパッケージを作成し公開する

本記事で説明しないこと

サービスプロパイダー ,サービスコンテナ ,PSR-4 ,githubとgitのコマンド jsonファイルについて

参考にした記事


おなじみ非公式日本語訳

laravel/uiのソースも参考にしました

前準備

laravel6系のプロジェクトを作成後、以下の記事に沿ってlaravel/uiをインストールし動作確認


今回はPackagistを用いてパッケージ化することが目標なので

Packagistにアクセスし任意のパッケージ名を検索、存在するかどうか確認する
*パッケージ名には必ずスラッシュを記述する必要がある
例: 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を実行

プロジェクト名/vendor/hogehoge/test
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ファイルにパッケージをロードさせる手順を記述する

#から始まるコメントは消してご使用下さい

vendor/hogehoge/test/composer.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に以下を記述

プロジェクト名/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にサービスプロパイダーを追加する

config/app.php
return [

        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
	hogehoge\test\UserEditServiceProvider::class,
	
]

※うまくいかなかったらキャッシュクリアするとと改善します

パッケージ用のサービスプロバイダーを追加する。

vendor/hogehoge/test/src/UserEditServiceProvider.php
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 を追加

vendor/hogehoge/test/src/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ファイルを作成する

vendor/hogehoge/test/src/Auth/auth/UserEdit.stub
@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
vendor/hogehoge/test/src/Auth/auth/UserEditController.stub
 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);
   }
}
vendor/hogehoge/test/src/Auth/auth/routes.stub
    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');

  });
vendor/hogehoge/test/src/Auth/auth/Request/ChangePasswordRequest.stub
<?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',
      ];
  }
}

vendor/hogehoge/test/src/Auth/auth/Request/WithdrawalRequest.stub
<?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テーブルを操作する独自のクラスを実装

vendor/hogehoge/test/src/UserEdit_Operation_DB.php
<?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. を選択

    packagist.orgにgithubを登録


アクセスしsubmitをクリック

Repository URLを入力しcheckボタンをクリック後、checkボタンがsubmitに変わるのでクリックすると登録に成功します

成功したら composer require (composer.jsonで登録したname)が表示されますので完成です

お疲れ様でした👏