[Laravel]あなたもOSS貢献！GitHubに自作ライブラリを公開してcomposerでインストールできるようになるまで

# はじめに
この記事は[Laravel #2 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/laravel-2)の記事です。
Laravel(というより、PHP)の超絶便利なライブラリ管理ツールとして、「composer」があります。Laravelのインストールや、関連するライブラリのインストールでも使用していますね。
仕事の方で、OSSなWebデータベース[Exment](https://exment.net)を公開した経緯もあり、
今回は、あなたが開発したライブラリを公開し、composerでインストールできるようになるまでの手順を記載します！
調べてもあまり載ってなかったり、分散している様々な記事をまとめる必要があったので、今回まとめることにしました。
もしかすると、全世界のLaravel開発者が、あなたの公開したライブラリをcomposerで使用しちゃうかもしれませんよ。OSS界隈に貢献しましょう！
スキルやポートフォリオ公開にもいいかもしれないですね。
# 前提事項
## 開発するサンプルライブラリ
Laravelで、データベースのjson型の値をset/get/forget/clearする汎用ライブラリ"db-json-common"を開発して公開する
## 最終目標
Laravel用のライブラリを開発し、composerでインストール出来るようにする
## 流れまとめ
- 開発用のLaravelプロジェクトを作成する
- packagesに必要な設定を行う
- ソースコードの作成・動作確認
- githubにリポジトリを作成し、開発したソースコードをpushする
- [Packagist](https://packagist.org/)にリポジトリを登録する
- 別プロジェクトでcomposerを試す
## 環境
- Laravel 5.5以上
- MySQL 5.7以上(json型を使用します)
## 注意事項
- 筆者がネットの記事を見ながら、独自に培ったノウハウです。「もっといいやり方あるよ！」ことがあれば、ぜひコメントお願いします！
# 開発開始
※今回開発するライブラリの目的は、最下部の「今回のライブラリの目的」に記載しておりますので、良かったらご参照ください。
## 開発用のLaravelプロジェクトを作成する
実際にLaravelのプロジェクトを開発・デバッグしながら、ライブラリを開発するのが良いでしょう。
ということで、Laravelプロジェクトをcomposerを使用して作成します。
今回はLaravel5.5以上という前提なので、5.5.*を指定します。
~~~
composer create-project "laravel/laravel=5.5.*" db-json-common
~~~
## packagesに必要な設定を行う
パッケージ化するために必要な設定を行います。
[こちらのサイト](https://laraveldaily.com/how-to-create-a-laravel-5-package-in-10-easy-steps/)を大いに参考にしていきます。 
### packagesフォルダ作成
プロジェクトのルートにpackagesフォルダを作成し、その後ソースコードまでのパスのフォルダ（以後「ライブラリフォルダ」とします）を作成します。  
今回は「hirossyi73/db-json-common」という名称にしたいので、「packages/hirossyi73/db-json-common」フォルダを作成します。
### ライブラリ用のcomposer.json作成
フォルダ「packages/hirossyi73/db-json-common」にコンソールで移動し、その後以下のコマンドを実行します。
すると、composerを作成するための対話式コンソールが実行します。
~~~
$ composer init
  Welcome to the Composer config generator
This command will guide you through creating your composer.json config.
Package name (<vendor>/<name>) [h-sato/db-json-common]: hirossyi73/db-json-common
Description []:
Author [XXX, n to skip]:  Hiroshi Sato <contact@hirossyi.net>
Minimum Stability []:
Package Type (e.g. library, project, metapackage, composer-plugin) []: library
License []: MIT
Define your dependencies.
Would you like to define your dependencies (require) interactively [yes]?
Search for a package:
Would you like to define your dev dependencies (require-dev) interactively [yes]?
Search for a package:
{
    "name": "hirossyi73/db-json-common",
    "type": "library",
    "license": "MIT",
    "authors": [
        {
            "name": "Hiroshi Sato",
            "email": "contact@hirossyi.net"
        }
    ],
    "require": {}
}
}
~~~
requireには、これから開発するライブラリに依存するライブラリがある場合に追加します。
例えば、今回開発するライブラリはLaravel5.5以上が前提なので、以下のようになります。
~~~
    "require": {
        "php": ">=7.0.0",
        "laravel/framework": "~5.5"
    }
~~~
これで、composer.jsonが作成されます。
### プロジェクトのルートに、packagesパスを通す
プロジェクトルートのcomposer.jsonを編集して、今作成したライブラリのパス（hirossyi73/db-json-common）を読み込み出来るようにします。
「repositories」は項目にないので、新たにjsonに追加します。
~~~ json
"repositories": [
    {
        "type": "path",
        "url": "packages/hirossyi73/db-json-common",
        "options": {
            "symlink": true
        }
    }
],
~~~
「require」と「autoload/psr-4」は元々のcomposer.jsonにすでにあるので、値のみ追加します。
※便宜上コメントを追加していますが、jsonファイル内にはコメントを追加できないので、実装時には削除してください
~~~ json
"require": {
    // ...
    "hirossyi73/db-json-common": "dev-master" // Add
},
// ...
"autoload": {
    "psr-4": {
        "App\\": "app/",
        "Hirossyi73\\DbJsonCommon\\": "packages/hirossyi73/db-json-common/src/" // Add
    }
},
~~~
その後、プロジェクトのルートから以下のコマンドを実施します。
~~~~
composer update
~~~~
### サービスプロバイダ作成
サービスプロバイダを作成します。
まず、artisanコマンドでサービスプロバイダを作成します。
~~~~
php artisan make:provider DbJsonCommonServiceProvider
~~~~
すると、フォルダapp/ProvidersにPHPファイル"DbJsonCommonServiceProvider"が作成されるので、そのファイルをプロジェクト内に移動します。
また、namespaceも修正します。
※ファイル移動
app/Providers/DbJsonCommonServiceProvider.php
↓
packages/hirossyi73/db-json-common/src/DbJsonCommonServiceProvider.php(srcフォルダは新規作成)
※namespace修正
~~~ php
<?php
namespace Hirossyi73\DbJsonCommon; // modify
use Illuminate\Support\ServiceProvider;
class DbJsonCommonServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}
~~~
そして、ライブラリフォルダ内のcomposer.jsonを修正します。この内容を行うことで、Laravel 5.5より対応しているauto-discoveryで、自動的にインストールしてくれます。
requireの下あたりにでも入れましょう。
~~~ json
"autoload": {
    "psr-4": {
        "Hirossyi73\\DbJsonCommon\\": "src/"
    }
},
"extra": {
    "laravel": {
        "providers": [
            "Hirossyi73\\DbJsonCommon\\DbJsonCommonServiceProvider"
        ]
    }
}
~~~
## ソースコードの作成・動作確認
ここまでが、どんなLaravelパッケージ作成でも基本的に必須となる内容です。
ここからは、ライブラリ独自の内容を追加します。皆様が独自のライブラリを公開する場合は、以下の内容は皆様独自のものを作成してください。
### ソースコードの作成
今回は、Modelに追加する用のTraitsを、src/Trais/DbJsonTraits.phpとして作成します。
~~~ php
<?php
namespace Hirossyi73\DbJsonCommon\Traits;
use \Closure;
trait DbJsonTraits
{
    private $dbJsonfuncs = [];
  
    public function __construct(){
        if (property_exists($this, 'dbJson') && !is_null($this->dbJson)) {
            $funcName = null;
            if (is_string($this->dbJson)) {
                $this->dbJson = [$this->dbJson];
            }
            $funcPrefix = ['get', 'set', 'forget', 'clear'];
            foreach ($this->dbJson as $key => $json) {
                if (is_numeric($key)) {
                    $funcKey = studly_case($json);
                } else {
                    $funcKey = $key;
                }
                foreach ($funcPrefix as $prefix) {
                    $this->dbJsonfuncs[$prefix.$funcKey] = [
                        'method' => $prefix.'Json',
                        'json' => $json,
                    ];
                }
            }
        }
    }
    public function __call($method, $parameters)
    {
        if(array_key_exists($method, $this->dbJsonfuncs)){
            $callMethod = $this->dbJsonfuncs[$method]['method'];
            if(method_exists($this, $callMethod)){
                $funcparams = array_merge([$this->dbJsonfuncs[$method]['json']], $parameters);
                return $this->{$callMethod}(...$funcparams);
            }
        }
        return parent::__call($method, $parameters);
    }
    /**
     * get value from json
     */
    protected function getJson(...$parameters)
    {
        list($dbcolumnname, $key, $default) = $parameters + [null, null, null];
        $json = $this->{$dbcolumnname};
        if(!isset($json)){return $default;}
        return array_get($json, $key, $default);
    }
    /**
     * set value from json
     * 
     */
    protected function setJson(...$parameters){
        list($dbcolumnname, $key, $val, $forgetIfNull) = $parameters + [null, null, null, null];
        if (!isset($dbcolumnname) && !isset($key)) {
            return $this;
        }
        // if key is array, loop key value
        if (is_array($key)) {
            foreach ($key as $k => $v) {
                $this->setJson($dbcolumnname, $k, $v);
            }
            return $this;
        }
        // if $val is null and $forgetIfNull is true, forget value
        if($forgetIfNull && is_null($val)){
            return $this->forgetJson($dbcolumnname, $key);
        }
        $value = $this->{$dbcolumnname};
        if (is_null($value)) {
            $value = [];
        }
        $value[$key] = $val;
        $this->{$dbcolumnname} = $value;
        return $this;
    }
  
    /**
     * forget value from json
     * 
     */
    protected function forgetJson(...$parameters){
        list($dbcolumnname, $key) = $parameters + [null, null];
        if (!isset($dbcolumnname) && !isset($key)) {
            return $this;
        }
      
        $value = $this->{$dbcolumnname};
        if (is_null($value)) {
            $value = [];
        }
        array_forget($value, $key);
        $this->{$dbcolumnname} = $value;
        return $this;
    }
    /**
     * clear value from json
     * 
     */
    protected function clearJson(...$parameters){
        list($dbcolumnname) = $parameters + [null];
        if (!isset($dbcolumnname)) {
            return $this;
        }
        $this->{$dbcolumnname} = null;
        return $this;
    }
}
~~~
### Readme.md作成
ライブラリのルートフォルダに、Readme.mdファイルを作成します。
作成したReadmeは、GitHubのリポジトリTOPに表示されます。あった方が良いです。
手順としては、今回は割愛します。
### 動作確認
動作確認のためのコードを作成します。
普通のLaravelアプリ開発と同様、プロジェクトのルートでmigrationを作成して、Modelを作成して、Controllerに動作検証用のコードを追加します。
※本来であれば、ここは単体テストコードであった方が親切だし、よりライブラリの質が上がりますね
~~~
php artisan make:model Setting -mc
~~~
~~~ php
// XXXX_XX_XX_XXXXXX_create_settings_table.php
public function up()
    {
        Schema::create('settings', function (Blueprint $table) { 
            $table->increments('id'); 
            $table->json('option')->nullable();  // ここ重要
            $table->timestamps(); 
        });
    }
~~~
~~~ 
php artisan migrate
~~~
~~~ php
<?php
// app\Setting.php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Hirossyi73\DbJsonCommon\Traits\DbJsonTraits; //追加
class Setting extends Model
{
    use DbJsonTraits; //追加
    protected $casts = ['option' => 'json'];//元々必要
    protected $dbJson = 'option'; // 追加。配列、連想配列に対応
}
~~~
~~~ php
<?php
// app\Http\SettingController.php
///// 関数だけ。超ざっくりです。ていうか絶対単体テスト作ったほうがいいな・・・
public function index(){
    return view('index', ['settings' => Setting::all()]);
}
public function store(){
    $setting = new Setting;
    $setting->setOption(['foobar_flg' => true, 'now' => date('YmdHis')]);
    $setting->save();
    return redirect('/');
}
public function update($id){
    $setting = Setting::find($id);
    $setting->setOption(['foobar_flg' => false, 'now' => date('YmdHis')]);
    $setting->save();
    return redirect('/');
}
~~~
~~~ php
<?php
// routes/web.php
///// 追加部分のみ
Route::get('/', 'SettingController@index');
Route::get('/store', 'SettingController@store');
Route::get('/update/{id}', 'SettingController@update');
~~~
上記の内容が完了したら、serveコマンドでサーバー立ち上げて、動作確認してみてください。
万が一エラー、特に作成したライブラリまでのパスが存在しないなどのエラーが発生した場合は、
~~~
composer dump-autoload
~~~
コマンドを試して、autoloadを最新化してみてください。
## GitHub公開
開発が完了したら、GitHubに公開します。
作成したライブラリフォルダにて、ローカルリポジトリを作成、リモートに反映させます。
参考：(https://qiita.com/taigamikami/items/26c29cb0fcdb0c05d396)
### ローカル側
~~~
cd hirossyi73/db-json-common
git init
git add .
git commit -m "initialize"
~~~
### リモート側
- リモートリポジトリに、hirossyi73/db-json-common作成
- リポジトリのhttps(SSHでもいいかも)をコピー
### ローカル側
~~~
git remote add origin [リモートリポジトリのhttps/SSHのcopy]
git merge --allow-unrelated-histories origin/master
git push --set-upstream origin master
~~~
これで、リモートリポジトリにライブラリが公開されます。
## [Packagist](https://packagist.org/)にライブラリ登録
いま公開したライブラリをcomposerから呼び出すには、[Packagist](https://packagist.org/)にライブラリを登録を行う必要があります。
Packagistに会員登録後、以下の手順を行ってください。
- [submit](https://packagist.org/packages/submit)ページにて、リポジトリURLを記入する。 
- これだけでpackagist登録完了・・・？（他にもなにかする必要があった気がしますが、検証ではこれだけで公開できました）
## 別プロジェクトでcomposerを試す
ここまですべて完了したら、別のLaravelプロジェクトを作成して、いまPackagistに登録したライブラリをcomposerでインストールしてみましょう。
~~~
composer require hirossyi73/db-json-common=dev-master
~~~
これで、インストールされるはずです。(GitHubにpushしてから、数十分はかかるみたいです)
新しく作成したプロジェクトでも、動作確認をしてみましょう。今度は正真正銘、composerからインストールしたパッケージなので、前半で書いたような、packagesフォルダ作成、ライブラリフォルダ作成のような手順は必要ないです。
なにか不具合などがあれば、元々のライブラリフォルダ内のソースコードを修正し、再度GitHubにpushしましょう。
### ローカル側(バージョンのタグ付け)
このままだと、ブランチにはmasterしかありません。
masterブランチしかない状況だと、この後登録するcomposerではdev-masterとして扱われ、「composer require hirossyi73/db-json-common=dev-master」と、dev-masterを付けなければならなくなります。
バージョンを追加するには、GitHubにタグを追加する必要があります。動作確認も完了し、正規版になったタイミングで、以下のコマンドを実施してください。
~~~
git tag -a v1.0.0 -m 'Publshed'
git push origin v1.0.0
~~~
これで、今後は以下のコマンドで最新版がインストールされるようになります。
~~~
composer require hirossyi73/db-json-common
~~~
# おわりに
以上で、パッケージ公開が完了しました。
少しでも世の中に貢献したい！と思ったら、どんどんソースコード公開していきましょう！
# [余談]今回のライブラリの目的
本筋からは逸れますが、今回開発するライブラリの機能についてもう少し説明します。（ライブラリ公開手順だけ見たい方は飛ばしてください）
  
MySQLでは、json型というものが用意されており、LaravelのModelでも、$castsに追加することでjson型を扱う事ができます。
ただ、json型の列に値を追加する場合、若干クセがあります。
例えば、settingsテーブルのjson型の列optionに、値「foobar_flg:false」を追加したい場合、Modelを使用して直感的に書くなら以下のようになりそうです。
~~~ php
$setting = \App\Setting::find($id);
$setting->option['foobar_flg'] = false;
$setting->save();
~~~
ところがこのコードは、2行目で「Indirect modification of overloaded property App\Setting::$option has no effect」というエラーが発生してしまいます。
Google翻訳を使用して訳すと「オーバーロードされたプロパティの間接的な変更\App\Setting::optionは効果がありません」ということのようです。
回避しようとする場合、こんな感じなります。（実際に、当初私はこう実装してました）
~~~ php
$setting = \App\Setting::find($id);
$option = array_get($setting, 'option', []);
$option['foobar_flg'] = false;
$setting->option = $option;
$setting->save();
~~~
要するに、「一旦optionを配列変数として、DBの列optionから取り出す」「option変数にfoobar_flg:falseを代入」「setting->option変数に$option配列を代入」と、かなり回りくどいことをする必要があります。
まずコードが非常に冗長ですし、万が一2行目をうっかり「option = [];」なんて書いてしまった日には、それまでoptionに入っていたfoobar_flg以外の値がすべて、吹き飛ぶことになってしまい、非常に危険です。
そのため、「json方の値を安全にセット」「安全に取得」「安全にクリア」などを行う汎用関数を作成することにしました。
以下のように書く方法です。
~~~ php
$setting = \App\Setting::find($id);
$option = array_get($setting, 'option', []);
$setting->setOption('foobar_flg', true); // setその1
// $setting->setOption(['foobar_flg' => true, 'abc' => 1]); // setその2。複数ある場合はこちら
$setting->option = $option;
$setting->save();
///// ついでに紹介
$val = $setting->getOption('foobar_flg', false); // get。第2引数は値がない場合のデフォルト
$setting->forgetOption('foobar_flg'); // 要素の削除
$setting->clearOption(); // 要素の全削除(optionにnullをセット)。引数にfalseを入れると、nullの代わりに空配列をセット
~~~
ModelにTraitと、json型になる列名のプロパティを追加するだけで、上記の関数が使用できるようになるためのライブラリを、今回開発しています。