20200823のPHPに関する記事は13件です。

Laravel CRUD

はじめに

LaravelでCRUDを作っていきます。

やらないこと

  • 認証関係は扱いません。
  • 削除の前の確認などJavaScriptで実現される機能は今回は作りません。

つくるもの

アクション 画面の有無 内容
index 画面あり 一覧表示画面
create 画面あり 新規入力フォーム
store 画面なし 追加処理(createの登録ボタン)
show 画面あり 詳細表示
edit 画面あり 変更フォーム(既存の値が入っている状態)
update 画面なし 変更処理(editの更新ボタン)
destroy 画面なし 削除処理(showの削除ボタン)
流れ
index(一覧表示) ┳ create(新規作成画面) ━ store(新規保存)
            ┗ show(詳細表示)     ┳ edit(編集画面) ━ update(上書き保存)
                        ┗ destroy(削除)     

準備

テーブルの作成

作成するテーブル

id name telephone email created_at updated_at
ID 名前 電話番号 メールアドレス 作成日時 更新日時

modelファイルとmigrationファイルの作成

terminal
php artisan make:model Models/Member -m

-mオプションでマイグレーションファイルも一緒に作成します。

下記の2つのファイルが作成されます。
app\Models\Member.php
database\migrations\xxxx_xx_xx_xxxxxx_create_members_table.php

migrationファイルの編集

database\migrations\xxxx_xx_xx_xxxxxx_create_members_table.php
public function up()
  {
    Schema::create('members', function (Blueprint $table) {
      $table->id();
      $table->string('name',20);
      $table->string('telephone',13)->nullable()->unique();
      $table->string('email',255)->nullable()->unique();
      $table->timestamps();
    });
  }

public function down()
  {
    Schema::dropIfExists('members');
  }

migrationの実行

terminal
php artisan migrate

membersテーブルが作成されます。

レコードの挿入

表示確認用にmembersテーブルにレコードを入れます。

seederファイルの作成

terminal
php artisan make:seeder MembersTableSeeder

database\seeds\MembersTableSeeder.phpが作成されます。

seederファイルの編集

database\seeds\MembersTableSeeder.php
    public function run()
     {
         DB::table('members')->insert(
             [
               [
                 'name'=>'山田',
                 'telephone'=>'xxxx-xxxxx',
                 'email'=>'yamada@example.com',
                 'created_at'=>now(),
                 'updated_at'=>now(),
               ],

               ]
         );
     }

DatabaseSeederへの登録

作成したMembersTableSeederをDatabaseSeederに登録します。

database\seeds\DatabaseSeeder.php
public function run()
  {
    $this->call(MembersTableSeeder::class);
  }

seederファイルの実行

terminal
php artisan db:seed

membersテーブルにレコードが入ります。

controllerの作成

terminal
php artisan make:controller MemberController --resource

--resourceオプションをつけると、7つのアクションの雛形が予め用意されます。

controllerの編集

7つのアクションの雛形が予め用意されています。

app\Http\Controllers\MemberController.php
//追加
use Illuminate\Support\Facades\DB;
use App\Models\Member;

public function index()
  {
    //
  }

public function create()
   {
     //
   }

public function store(Request $request)
  {
    //
  }

public function show($id)
  {
    //
  }

public function edit($id)
  {
    //
  }

public function update(Request $request, $id)
  {
    //
  }

public function destroy($id)
  {
    //
  }

一覧画面(index)

routingの追加

/member/indexにアクセスした場合のルーティングを追加します。

route/web.php
//追記
Route::group(['prefix'=>'member'], function () {
  Route::get('index', 'MemberController@index')->name('member.index');
});

controllerの編集

membersテーブルからデータを取ってきて、viewに渡します。

app\Http\Controllers\MemberController.php
public function index()
  {
    //memberテーブルからname,telephone,emailを$membersに格納
    $members=DB::table('members')
      ->select('id', 'name', 'telephone', 'email')
      ->get();

    //viewを返す(compactでviewに$membersを渡す)
    return view('member/index', compact('members'));
  }

viewの新規作成

resources\views\member\index.blade.php
<h1>一覧表示</h1>

<table>
<tr>
<th>ID</th>
<th>名前</th>
<th>電話番号</th>
<th>メールアドレス</th>
</tr>
@foreach($members as $member)
<tr>
<td>{{$member->id}}</td>
<td>{{$member->name}}</td>
<td>{{$member->telephone}}</td>
<td>{{$member->email}}</td>
</tr>
@endforeach
</table>

確認

簡易サーバーを起動します。

terminal
php artisan serve

ブラウザで http://127.0.0.1:8000/member/index にアクセスし、一覧表示ができていることを確認します。
Ctrl+Cで簡易サーバーを終了します。

新規登録(create)

routingの追加

/member/createにアクセスした場合のルーティングを追加します。

route/web.php
Route::group(['prefix'=>'member'], function () {
  Route::get('index', 'MemberController@index')->name('member.index');

  //追記
  Route::get('create', 'MemberController@create')->name('member.create');
});

controllerの編集

create.blade.phpを返します。

app\Http\Controllers\MemberController.php
public function create()
  {
    return view('member/create');
  }

viewの新規作成

新規作成画面を作ります。
名前だけ入力必須にしています。

resources\views\member\create.blade.php
<h1>新規作成</h1>

<form method="POST" action="">
  @csrf

  <div>
    <label for="form-name">名前</label>
    <input type="text" name="name" id="form-name" required>
  </div>

  <div>
    <label for="form-tel">電話番号</label>
    <input type="tel" name="telephone" id="form-tel">
  </div>

  <div>
    <label for="form-email">メールアドレス</label>
    <input type="email" name="email" id="form-email">
  </div>

  <button type="submit">登録</button>

</form>

導線(index→create)

一覧表示画面から新規作成画面へのリンクです。

resources\views\member\index.blade.php
<a href="{{ route('member.create') }}">{{ __('新規作成') }}</a>

導線(create→index)

新規作成画面から一覧表示画面へのリンクです。

resources\views\member\create.blade.php
//追加
<a href="{{ route('member.index') }}">{{ __('一覧へ戻る') }}</a>

確認

簡易サーバーを起動します。

terminal
php artisan serve

ブラウザで http://127.0.0.1:8000/member/create にアクセスし、新規入力画面ができていることを確認します。
(この時点では、登録ボタンを押してもエラーになります。)
Ctrl+Cで簡易サーバーを終了します。

新規保存(store)

新規追加画面で入力した値をDBに保存します。

routingの追加

/member/storeにアクセスした場合のルーティングを追加します。

route/web.php
Route::group(['prefix'=>'member'], function () {
  Route::get('index', 'MemberController@index')->name('member.index');
  Route::get('create', 'MemberController@create')->name('member.create');

  //追加
  Route::post('store', 'MemberController@store')->name('member.store');
});

controllerの編集

フォームで入力された値をmembersテーブルに格納します。

app\Http\Controllers\MemberController.php
public function store(Request $request)
  {
    $member=new Member;

    $member->name=$request->input('name');
    $member->telephone=$request->input('telephone');
    $member->email=$request->input('email');

    $member->save();

    //一覧表示画面にリダイレクト
    return redirect('member/index');
  }

viewの新規作成

なし

導線(create→store)

一覧表示画面から新規作成画面へのリンクです。

resources\views\membercreate.blade.php
//<form method="POST" action="">
//↓フォームの送信先の変更
<form method="POST" action="{{route('member.store')}}">

導線(store→index)]

controllerで、リダイレクトを記述済です。

確認

簡易サーバーを起動します。

terminal
php artisan serve

ブラウザで http://127.0.0.1:8000/member/create にアクセスし、値を入力して送信すると、member/indexにリダイレクトされ、membersテーブルに値が入っている事を確認します。
Ctrl+Cで簡易サーバーを終了します。

詳細表示(show)

routingの追加

/member/show/idにアクセスした場合のルーティングを追加します。

route/web.php
Route::group(['prefix'=>'member'], function () {
  Route::get('index', 'MemberController@index')->name('member.index');
  Route::get('create', 'MemberController@create')->name('member.create');
  Route::post('store', 'MemberController@store')->name('member.store');

  //追加
  Route::get('show/{id}', 'MemberController@show')->name('member.show');
});

controllerの編集

指定したIDのメンバーの詳細ページを表示する処理をします。

app\Http\Controllers\MemberController.php
public function show($id)
  {
    $member=Member::find($id);

    return view('member/show', compact('member'));
  }

viewの新規作成

resources\views\member\show.blade.php
<h1>詳細表示</h1>

<div>
名前
{{$member->name}}
</div>

<div>
電話番号
{{$member->telephone}}
</div>

<div>
メールアドレス
{{$member->email}}
</div>

導線(index→show)

一覧表示画面に、1カラム増やして詳細画面へのリンクを追加します。

resources\views\member\index.blade.php
<table>
<tr>
<th>ID</th>
<th>名前</th>
<th>電話番号</th>
<th>メールアドレス</th>
<th>詳細</th>
</tr>
@foreach($members as $member)
<tr>
<td>{{$member->id}}</td>
<td>{{$member->name}}</td>
<td>{{$member->telephone}}</td>
<td>{{$member->email}}</td>
<td><th><a href="{{route('member.show',['id'=>$member->id])}}">詳細</a></th></td>
</tr>
@endforeach
</table>

導線(show→index)

resources\views\member\show.blade.php
<a href="{{ route('member.index') }}">{{ __('一覧に戻る') }}</a>

確認

簡易サーバーを起動します。

terminal
php artisan serve

ブラウザで http://127.0.0.1:8000/member/index にアクセスし、詳細を押すと詳細画面が表示されることを確認します。
Ctrl+Cで簡易サーバーを終了します。

編集(edit)

routingの追加

/member/edit/idにアクセスした場合のルーティングを追加します。

route/web.php
Route::group(['prefix'=>'member'], function () {
  Route::get('index', 'MemberController@index')->name('member.index');
  Route::get('create', 'MemberController@create')->name('member.create');
  Route::post('store', 'MemberController@store')->name('member.store');
  Route::get('show/{id}', 'MemberController@show')->name('member.show');

  //追加
  Route::get('edit/{id}', 'MemberController@edit')->name('member.edit');
});

controllerの編集

app\Http\Controllers\MemberController.php
public function edit($id)
  {
    $member=Member::find($id);

    return view('member/edit', compact('member'));
  }

viewの新規作成

resources\views\member\edit.blade.php
<h1>編集</h1>

 <form method="POST" action="">
  @csrf

 <div>
  名前
  <input type="text" name=name value="{{$member->name}}">
  </div>

  <div>
  電話番号
  <input type="text" name=telephone value="{{$member->telephone}}">
  </div>

  <div>
  メールアドレス
  <input type="text" name=email value="{{$member->email}}">
  </div>


  <input type="submit" value="更新する">

  </form>

導線(show→edit)

resources\views\member\show.blade.php
//追加
<a href="{{route('member.edit',['id'=>$member->id])}}">{{ __('編集') }}</a>

導線(edit→show)

resources\views\member\edit.blade.php
//追加
<a href="{{route('member.show',['id'=>$member->id])}}">{{ __('詳細に戻る') }}</a>

確認

簡易サーバーを起動します。

terminal
php artisan serve

ブラウザで http://127.0.0.1:8000/member/index にアクセスし、詳細→編集と進む編集フォームが表示されることを確認します。
(この時点では、更新ボタンを押してもエラーになります。)

更新(update)

編集画面で「更新」ボタンを押した時の動作を設定していきます。

routingの追加

/member/update/idにアクセスした場合のルーティングを追加します。

route/web.php
Route::group(['prefix'=>'member'], function () {
  Route::get('index', 'MemberController@index')->name('member.index');
  Route::get('create', 'MemberController@create')->name('member.create');
  Route::post('store', 'MemberController@store')->name('member.store');
  Route::get('show/{id}', 'MemberController@show')->name('member.show');
  Route::get('edit/{id}', 'MemberController@edit')->name('member.edit');

  //追加
  Route::post('update/{id}', 'MemberController@update')->name('member.update');
});

controllerの編集

app\Http\Controllers\MemberController.php
public function update(Request $request, $id)
  {
    $member=Member::find($id);

    $member->name=$request->input('name');
    $member->telephone=$request->input('telephone');
    $member->email=$request->input('email');

    //DBに保存
    $member->save();

    //処理が終わったらmember/indexにリダイレクト
    return redirect('member/index');
}

viewの新規作成

なし

導線(edit→update)

editの送信ボタンの送信先を指定します。

resources\views\member\edit.blade.php
//<form method="POST" action="">
//↓
<form method="POST" action="{{route('member.update',['id' =>$member->id])}}">

導線(update→index)

controllerでリダイレクト済

確認

簡易サーバーを起動します。

terminal
php artisan serve

ブラウザで http://127.0.0.1:8000/member/index にアクセスし、詳細画面→編集画面と移動し、値を変更して「更新」ボタンを押し、/member/indexのデータが変更されていることを確認します。
Ctrl+Cで簡易サーバーを終了します。

削除(destroy)

routingの追加

/member/destroy/idにアクセスした場合のルーティングを追加します。

route/web.php
Route::group(['prefix'=>'member'], function () {
  Route::get('index', 'MemberController@index')->name('member.index');
  Route::get('create', 'MemberController@create')->name('member.create');
  Route::post('store', 'MemberController@store')->name('member.store');
  Route::get('show/{id}', 'MemberController@show')->name('member.show');
  Route::get('edit/{id}', 'MemberController@edit')->name('member.edit');
  Route::post('update/{id}', 'MemberController@update')->name('member.update');

  //追加
  Route::post('destroy/{id}', 'MemberController@destroy')->name('member.destroy');
});

controllerの編集

指定のIDを削除する処理を書きます。

app\Http\Controllers\MemberController.php
public function destroy($id)
  {
    $member=Member::find($id);

    $member->delete();

    return redirect('member/index');
  }

viewの新規作成

なし

導線(show→destroy)

resources\views\member\show.blade.php
//追加
<form method="POST" action="{{route('member.destroy',['id'=>$member->id])}}">
  @csrf
  <button type="submit">削除</button>
</form>

導線(destroy→index)

controllerでリダイレクト済

確認

簡易サーバーを起動します。

terminal
php artisan serve

ブラウザで http://127.0.0.1:8000/member/index にアクセスし、詳細画面に移動し、「削除」ボタンを押し、/member/indexのデータが削除されていることを確認します。
Ctrl+Cで簡易サーバーを終了します。

バリデーションの準備

エラーメッセージの日本語化

resources/lang/に下記のjaフォルダをenフォルダと同じ階層に配置します。
https://github.com/minoryorg/laravel-resources-lang-ja
※jaフォルダ内のファイルの編集でさらにメッセージのカスタマイズができます。

resources\lang\ja\validation.php
//'attributes' => [],
//↓
'attributes' => ['email'=>'メールアドレス',
'name'=>'名前'
],

バリデーション(新規保存)

form requestの作成

terminal
php artisan make:request StoreMember

app\Http\Requests\StoreMember.phpが新規作成されます。

form requestの編集

バリデーションのルールを指定します。

app\Http\Requests\StoreMember.php
//追加
use Illuminate\Validation\Rule;

public function authorize()
  {
    //return false;
    //↓falseをtrueに変更
    return true;
  }

public function rules()
  {
    return [

      //追加
      'name' => [
        'string',
        'required',
        'max:20'
         ],

      'telephone' => [
        'string',
        'nullable',
        'max:13',
        'unique:members'
        ],

      'email' => [
        'nullable',
        'max:255',
        'email',
        'unique:members'
        ]
    ];
  }
キーワード 内容
required 必須
max 最大長
unique:テーブル名 指定のテーブルでユニーク
nullable null許容
accepted チェックされている
string 文字列型
email email型
url url型

controller

app\Http\Controllers\MemberController.php
//追加
use App\Http\Requests\StoreMember;


//public function store(Request $request)
//↓変更
public function store(StoreMember $request)

view

エラーメッセージを表示させたい場所にエラーメッセージを配置します。

resources\views\member\create.blade.php
<form method="POST" action="{{route('member.store')}}">
  @csrf

  <div>
    <label for="form-name">名前</label>

    // <input type="text" name="name" id="form-name" required>
    //↓valueを追加して入力値を保持させます。
    <input type="text" name="name" id="form-name" required value="{{old('name')}}">

    //追加
    @error('name')
    {{$message}}
    @enderror

  </div>

  <div>
    <label for="form-tel">電話番号</label>

    //<input type="tel" name="telephone" id="form-tel">
    //↓valueを追加して入力値を保持させます。
    <input type="tel" name="telephone" id="form-tel" value="{{old('telephone')}}">

   //追加
   @error('telephone')
   {{$message}}
   @enderror

  </div>

  <div>

    <label for="form-email">メールアドレス</label>

    //<input type="email" name="email" id="form-email">
    //↓valueを追加して入力値を保持させます。
    <input type="email" name="email" id="form-email" value="{{old('email')}}">

    //追加
    @error('email')
    {{$message}}
    @enderror

  </div>

  <button type="submit">送信</button>

</form>

バリデーション(上書き保存)

新規登録のバリデーションのままでは、ユニークにする電話番号と、メールアドレスを弾いてしまうので、変更が必要です。

form requestの作成

terminal
php artisan make:request UpdateMember

app\Http\Requests\UpdateMember.phpが新規作成されます。

form requestの編集

新規作成のバリデーションとほぼ同じですが、既存の値も許可するように変更します。

app\Http\Requests\UpdateMember.php
//追加
use Illuminate\Validation\Rule;

public function authorize()
  {
    //return false;
    //↓falseをtrueに変更
    return true;
  }

public function rules()
  {
    return [

      //追加
      'name' => [
        'string',
        'required',
        'max:20'
        ],

      'telephone' => [
         'string',
         'nullable',
         'max:13',

         //既存の値も許可
         Rule::unique('members')->ignore($this->id)
       ],

      'email' => [
        'nullable',
        'max:255',
        'email',

         //既存の値も許可
         Rule::unique('members')->ignore($this->id)
         ]

     ];
   }

controller

app\Http\Controllers\MemberController.php
//追加
use App\Http\Requests\UpdateMember;

//public function update(Request $request, $id)
//↓変更
public function update(UpdateMember $request, $id)

view

エラーメッセージを表示させたい場所にエラーメッセージを配置します。

resources\views\member\edit.blade.php
<form method="POST" action="{{route('member.update',['id' =>$member->id])}}">
 @csrf

  <div>
    名前
    <input type="text" name=name value="{{$member->name}}">
    @error('name')
    {{$message}}
    @enderror
  </div>

  <div>
    電話番号
    <input type="text" name=telephone value="{{$member->telephone}}">
    @error('telephone')
    {{$message}}
    @enderror
  </div>

  <div>
    メールアドレス
    <input type="text" name=email value="{{$member->email}}">
    @error('email')
    {{$message}}
    @enderror
  </div>

  <input type="submit" value="更新する">

</form>

ページネーション

controller

一覧表示の件数が増えてきたら、ページを分けます。

app\Http\Controllers\MemberController.php
public function index()
  {
    $members=DB::table('members')
      ->select('id', 'name', 'telephone', 'email')
      //->get();
      //↓ 1ページに表示する件数を指定
      ->paginate(20);

      //viewを返す(compactでviewに$membersを渡す)
      return view('member/index', compact('members'));
  }

view

一覧画面にページ送りのUIを追加します。

resources\views\member\index.blade.php
//追加
{{$members->links()}}

記述としては1行ですが、下記のようにhtmlが生成されますので、CSSで形を整えてください。

html
<nav>
  <ul class="pagination">

    <li class="page-item disabled" aria-disabled="true" aria-label="« Previous"><span class="page-link" aria-hidden="true">‹</span></li>
    <li class="page-item active" aria-current="page">
      <span class="page-link">1</span>
    </li>
    <li class="page-item">
      <a class="page-link" href="http://127.0.0.1:8000/member/index?page=2">2</a>
    </li>
    <li class="page-item">
      <a class="page-link" href="http://127.0.0.1:8000/member/index?page=2" rel="next" aria-label="Next »">›</a>
    </li>

  </ul>
</nav>

検索機能

routingの追加

route/web.php
Route::group(['prefix'=>'member'], function () {
  Route::get('index' 'MemberController@index')->name('member.index');
  Route::get('create', 'MemberController@create')->name('member.create');
  Route::post('store', 'MemberController@store')->name('member.store');
  Route::get('show/{id}', 'MemberController@show')->name('member.show');
  Route::get('edit/{id}', 'MemberController@edit')->name('member.edit');
  Route::post('update/{id}', 'MemberController@update')->name('member.update');
  Route::post('destroy/{id}', 'MemberController@destroy')->name('member.destroy');

  //追加
  Route::get('search', 'MemberController@search')->name('member.search');
});

controllerの編集

app\Http\Controllers\MemberController.php
public function search(Request $request)
  {
    $serach=$request->input('q');

    $query=DB::table('members');

    //検索ワードの全角スペースを半角スペースに変換
    $serach_spaceharf=mb_convert_kana($serach, 's');


    //検索ワードを半角スペースで区切る
    $keyword_array=preg_split('/[\s]+/', $serach_spaceharf, -1, PREG_SPLIT_NO_EMPTY);

    //検索ワードをループで回してマッチするレコードを探す
    foreach ($keyword_array as $keyword) {
        $query->where('name', 'like', '%'.$keyword.'%');
      }

    $query->select('id', 'name', 'telephone', 'email');
    $members=$query->paginate(20);

    return view('member/index', compact('members'));
  }

viewの編集

一覧表示画面に検索フォームを追加します。

resources\views\member\index.blade.php
//追加
<form method="GET" action="{{route('member.search')}}">
  @csrf
  <div>
    <label for="form-search">検索</label>
    <input type="search" name="q" id="form-search">
  </div>

  <button type="submit">検索</button>

</form>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP】URLのリストをまとめてDLする

とりあえず備忘録として

URLのリストをまとめてDLできるようなものを作成していく。
例えば以下のようなテキストファイルにURLをつらつらと書いていってそれらをまとめてDLできる。

:list.txt
https://.....
https://.....
https://.....
https://.....

DL保存先ディレクトリを作成

ディレクトリの名前をURLリストファイルの名前と同じものとした。
コマンドライン引数でURLリストファイルのパスを受け取って、そこから名前を抽出する。

以下は保存用ディレクトリがなかったら作成するコード

$dirName = basename($argv[1], ".txt");
$dir = FILE . 'src/' . $dirName;
if (!file_exists($dir)) {
    mkdir($dir);
}

basename

を使うことでパスからファイル名の抽出が簡単になる。
第1引数はパスで、第2引数へ拡張子を指定してやるとファイル名のみを抽出できる。

コマンドライン引数でURLリストテキストを読み込む

URLが膨大になることを考えて読み込みは1行ずつ行い、URLリストファイルを配列にする

以下はその雛形

$file = fopen($argv[1], "r");
if ($file) {
    while ($line = fgets($file)) {
       //処理
    }
}
fclose($file);

参考:https://webkaru.net/php/function-fgets/

ダウンロード

forで回していくがループの始まりを工夫した

for ($i = $argv[2] ?? 0; $i < count($array); $i++) {
//処理
}

$iの初期化を$i = $argv[2] ?? 0;のようにしている。
これは$argv[2]あればそれを代入して、そうでなければ0を代入するというもの。これによりDLしたい開始の場所を指定できる。

curl

基本的な書き方しかしていないが一応載せます。

    $options = array(
        CURLOPT_URL =>  $url,
        CURLOPT_RETURNTRANSFER => true, 
    );

    $ch = curl_init();

    //オプション
    curl_setopt_array($ch, $options);

    $html =  curl_exec($ch);
    file_put_contents($filepath, $html);
    curl_close($ch); 

CURLOPT_RETURNTRANSFER => trueを指定しないと保存がうまく行かなかったような、、、、

実行

$php dl.php URLリストファイルのパス [オプション]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

phpでOSの環境変数から値を取得する

envtest.php
<?php
echo getenv('REMOTE_ADDR');
?>

動作確認

$ export REMOTE_ADDR=1.1.1.1
$ php envtest.php 
1.1.1.1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CircleCIでPHPUnit

はじめに

本記事はCircleCIを利用して、PHPUnitでCIの自動化を実施することを目的とし、以下の図をワンシーケンス通す備忘録とする。
スクリーンショット 2020-08-08 17.21.37.png

GitHubのリポジトリはこちら

環境

macOS Catalina:10.15.5(19F101)
php 7.4.9
composer 1.10.10
PHPUnit 9.3.7

目次

  • 前提条件
  • CircleCIを登録する
  • GitHubでCI連携用のリポジトリを作る
  • composerを取得する
  • composerでPHPUnitをインストールする
  • PHPUnitで手動実行するテストファイルを作成する
  • PHPUnitを手動実行する
  • CircleCIとGitHubで作成したリポジトリを連携する
  • CircleCIでPHPUnitを動作させるためにGitHubにTestコードを修正してプッシュしてみる
  • おまけ・・・config.yml作成時につまづいた点について

前提条件

本記事の手順については、予めGitHubの登録作業が実施済であることを前提とする。また、上記の図にある通り以下技術についての知識が必要。

  • git
  • docker
  • php
  • composer
  • PHPUnit
  • UNIXコマンド

CircleCIを登録する

  • CircleCIにアクセス スクリーンショット 2020-08-08 22.07.36.png
  • 「Sign up with GitHub」をクリックする
    • GitHubで未認証の場合は、CircleCIとの認証を促されるため、GitHubアカウントで認証する。
  • 以下のような画面になる。 スクリーンショット 2020-08-08 22.17.02.png

GitHubでCI連携用のリポジトリを作る

  • GitHubにログインする(GitHubの登録は予め実施しておく必要がある。)
    スクリーンショット 2020-08-08 22.37.38.png
  • Newをクリックする。
    スクリーンショット 2020-08-08 22.38.04.png
  • 「Repository name」に任意のリポジトリ名を入力する。今回は「circleci-phpunit」を設定。
  • 「Create repository」をクリックすると以下の画面が表示される。 スクリーンショット 2020-08-09 8.03.25.png
  • ベアリポジトリ(GitHub側)とノンベアリポジトリ(ローカル側)の現在の状態毎の、GitHubとの連携方法を提示してくれる。今回は「...or create a new repository on the command line」(ローカルにノンベアリポジトリを作成してからベアリポジトリにプッシュする方法)を採用することとする。 スクリーンショット 2020-08-08 22.50.30.png
  • 上記のコマンドを枠内の右上のアイコンをクリックしてコピーする。
  • git管理するフォルダを作成する。
    今回はリポジトリ名(circleci-phpunit)と同じ名前で作成する。
  • 「terminal.app」を開き、作成した「circleci-phpunit」階層に移動して、GitHubの画面のアイコンでコピーしたものを貼り付けて実行する。
  • GitHubページにコマンドで追加した「README.md」が存在すればリポジトリの操作準備完了。

composerを取得する

composerを取得するには予めphpをmacにインストールしてパスを切っておく必要がある。ちなみにmacには最初からphpが入っているが、macOSのバージョンをあげるとphpの設定がbackupとして変わってしまうことがあるので私の環境ではxamppをダウンロードしてxamppないの「xamppfies/bin」にパスを通して置きphpコマンドをterminalから利用できるようにしている。この状態であれば以下の手順でcomposerのダウンロードが可能となる。

まずはcomposerをダウンロードしたいディレクトリに移動する。

cd プロジェクトのフォルダ

その後以下を参考にcomposerをダウンロードする。

composerの本家サイトから抜粋

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === 'e5325b19b381bfd88ce90a5ddb7823406b2a38cff6bb704b0acc289a09c8128d4a8ce2bbafcd1fcbdc38666422fe2806') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"

composer.pharをダウンロード後、拡張子を除いてダウンロードしたcomposerが動作するか確認する。※pharのままでも動作可能だが、コマンド短縮のため拡張子は省く。

sakamotoyuya:circleci-phpunit sakamotoyuya$ mv composer.phar composer
sakamotoyuya:circleci-phpunit sakamotoyuya$ ./composer -v
   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
Composer version 1.10.10 2020-08-03 11:35:19

Usage:
  command [options] [arguments]

Options:
・・・

composerでPHPUnitをインストールする

  • ダウンロードしたcomposerと同一階層(以後ルートディレクトリという)に「composer.json」ファイルを以下の内容で作成する。
    PHPUnit本家サイトstartupを参考にして実施。
composer.json
{
    "autoload": {
        "classmap": [
            "src/"
        ]
    },
    "require-dev": {
        "phpunit/phpunit": "^9"
    }
}

オートロード(autoload)方法にはpsr-4psr-0classmapfilesとあるが、今回は規定したディレクトリ内を全て読み込むclassmapを対象にすることとする。

オートロードの詳細は以下を参考。
composerドキュメント - オートロード

オートロードの指定先をsrc/として、このフォルダは以下のモジュールを自動読み込み対象ファイルとする。

require-devはパッケージリンクという。
パッケージリンクの詳細は以下を参考。
composerドキュメント - パッケージリンク

パッケージリンクにはrequire(本番用)とrequire-dev(開発用)の二つがある。
これについての使い方の詳細は以下がわかりやすいため参照。
【Laravel】composer require-devの開発、本番での切り分け

$ composer install --no-dev

開発用パッケージは本番環境では不要ですので、--no-devオプションを付けcomposerコマンドを実行する。
そうするとrequire-devオプションに記載されているライブラリ自体はvendorへインストールされません

要するにコンパイルオプションを切り替えることで、パッケージの導入可否を決定する。本番用ではPHPUnitを使わないため、require-devに入れて--no-devをコンパイルオプションを付与することで、導入しない動作となる。

  • ルートディレクトリにsrcフォルダを作成する。
  • 以下のようなフォルダ構造となる。
ルートディレクトリ
 |- .git       ・・・隠しフォルダ
 |- src       ・・・フォルダ
 |- composer    ・・・ファイル
 |- composer.json ・・・ファイル
 |- README.md    ・・・ファイル
  • ルートディレクトに移動する。cd ルートディレクトリ
  • 以下のコマンドでPHPUnitをインストールする。
$ ./composer install

composerのコマンドを実行するには、実行ファイルcomposerとその実行ファイルが読み取る設定ファイルcomposer.jsonが必要となる。そのファイルが置いてある箇所に移動して実行する必要がある。
本手順でvendorフォルダが作成される。
vendorフォルダは以下にパッケージがインストールされていくのですが、このフォルダは大きくなったり可変することが多いので、.gitignoreファイルを作ってgit管理の対象外にする。

  • 以下の内容で.gitignoreファイルを作る。
.gitignore
vendor
  • ディレクトリは以下のようになる。
ルートディレクトリ
 |- .git       ・・・隠しフォルダ
 |- .gitignore   ・・・⭐️ファイル
 |- src       ・・・フォルダ
 |- vendor     ・・・⭐️composerによりインストールされたフォルダ
 |  |- bin
 |  |   |- phpunit・・・⭐️phpunitの実行ファイル
 |  ~
 |- composer    ・・・ファイル
 |- composer.json ・・・ファイル
 |- README.md    ・・・ファイル

PHPUnitで手動実行するテストファイルを作成する

  • srcフォルダに運用するモジュールのHuman.phpを作成する
Human.php
<?php
class Human{    
    public function helloString(){
        return "こんにちわ";
    }
    public function goodnightString(){
        return "おやすみ";
    }
}
  • ルートフォルダにtestsフォルダを作成する。フォルダ構成は以下のようになる。
ルートディレクトリ
 |- .git       ・・・隠しフォルダ
 |- src       ・・・フォルダ
 |  |- Human.php   ・・・⭐️評価対象モジュール
 |
 |- tests      ・・・⭐️フォルダ
 |- vendor     ・・・composerによりインストールされたフォルダ
 |  |- bin
 |  |   |- phpunit・・・phpunitの実行ファイル
 |  ~
 |- composer    ・・・ファイル
 |- composer.json ・・・ファイル
 |- README.md    ・・・ファイル

  • testsフォルダにHuman.phpをテストするための、テストコードHumanTest.phpを作成する
HumanTest.php
<?php
use PHPUnit\Framework\TestCase;
final class SakamotoTest extends TestCase{
    public function testA(){
        $obj = new Human();
        $this->assertSame("こんにちわ",$obj->helloString());
    }
    public function testB(){
        $obj = new Human();
        // $this->assertSame("おーまいっが!",$obj->goodnightString());
        $this->assertSame("おやすみ",$obj->goodnightString());
    }

    public function testD(){
        $obj = new Human();
        $this->assertSame("おやすみ",$obj->goodnightString());
    }
}

テストコードのファイル名には決まりがあり、「テストしたいモジュールのファイル名+Test.php」のようにする必要がある。今回Human.phpをテストしたいので、ファイル名はHumanTest.phpとなる。また、HumanTest.phpの中身のクラス名はファイル名と異なっても動作可能なようです。試しにSakamotoTestクラスにしてもテスト実行可能でした。そうする意味もないので、クラス名はファイル名に合わせてHumanTestにした方が可読性がいいですね。今回は上記の通りお試しのコードとなっているが、実際に作る際はHumanTestクラスとしましょう。

  • このときのフォルダ構成は以下となる
ルートディレクトリ
 |- .git       ・・・隠しフォルダ
 |- src       ・・・フォルダ
 |  |- Human.php   ・・・評価対象モジュール
 |
 |- tests      ・・・フォルダ
 |  |- HumanTest.php・・・⭐️Testファイル
 |
 |- vendor     ・・・composerによりインストールされたフォルダ
 |  |- bin
 |  |   |- phpunit・・・phpunitの実行ファイル
 |  ~
 |- composer    ・・・ファイル
 |- composer.json ・・・ファイル
 |- README.md    ・・・ファイル

PHPUnitを手動実行する

  • ソースコードを更新したので、オートロードを更新する
$ ./composer dump-autoload

プロジェクトのファイルを更新した場合はこのコマンドを実行する必要がある。
これを実施しないと変更前の状態での開始となっている。

  • PHPUnitを手動実行する
$ ./vender/bin/phpunit tests

テストコードのあるフォルダを指定する。今回はtestsフォルダを指定する。
問題なければ以下のような結果になる。

sakamotoyuya:circleci-phpunit sakamotoyuya$ ./vendor/bin/phpunit tests
PHPUnit 9.3.7 by Sebastian Bergmann and contributors.


Warning:       Test case class not matching filename is deprecated
               in /Users/sakamotoyuya/Documents/GitHub/circleci-phpunit/tests/HumanTest.php
               Class name was 'SakamotoTest', expected 'HumanTest'

...                                                                 3 / 3 (100%)

Time: 00:00.016, Memory: 4.00 MB

OK (3 tests, 3 assertions)
...                                                                 3 / 3 (100%)

Time: 29 ms, Memory: 4.00 MB

OK (3 tests, 3 assertions)

このように結果がOKであればテストOKということです。
サンプルコードHumanTest.phpは文字列比較しているだけなので、コメントアウト箇所を入れ替えることでNGの場合も確認できる。手動での確認はここまで。
いよいよ次からこの流れをCircleCIを用いて自動化する。

  • ここまでできたら、コミットしてGitHubにプッシュする。
git add .
git commit -m "phpunitでテスト環境を作成"
git push

プッシュ後のGitHubはこのようになる。
スクリーンショット 2020-08-23 8.20.57.png

CircleCIとGitHubで作成したリポジトリを連携する

  • CircleCIにログイン > Projectsを開く スクリーンショット 2020-08-23 9.10.18.png CircleCI登録時にGitHubと連携しているため、GitHubで作成したリポジトリ一覧が表示される。
  • circleci-phpunitSet Up Projectをクリック スクリーンショット 2020-08-23 9.19.30.png
  • Use Existing Configをクリック スクリーンショット 2020-08-23 9.20.55.png
.翻訳
config.ymlファイルを追加しましたか?  
すでに.circleci / config.ymlを追加している場合は、プロジェクトの構築を開始できる。
そうでない場合は、ビルドを開始する前に、config.ymlファイルをダウンロードして、
リポジトリのルートにある.circleciという名前の新しいフォルダーに追加する。

ルートディレクトリ直下に.circleci/config.ymlの設定ファイルが必要とのことなので、これに従って設定ファイルを作っていく。

  • .circleci/config.ymlを以下の内容で作成する。
.circleci/config.yml
version: 2.1
jobs:
  build:
    environment:
      TZ: "Asia/Tokyo"
      DEBIAN_FRONTEND: noninteractive
    docker: 
      - image: ubuntu:latest # the primary container, where your job's commands are run
        environment:
          TZ: "Asia/Tokyo"
          DEBIAN_FRONTEND: noninteractive
    steps:
      - checkout # check out the code in the project directory
      - run: apt-get update && apt-get install -y tzdata
      - run: apt-get install -y wget sudo gnupg
      - run: sudo apt-get update
      - run: sudo apt-get upgrade -y
      - run: sudo apt install -y php
      - run: sudo apt-get install -y php-mbstring
      - run: sudo apt-get install -y php-xml
      - run: sudo apt-get install -y zip
      - run: sudo apt-get install -y unzip
      - run: echo "Set disable_coredump false" >> /etc/sudo.conf
      - run: php ./composer install && ./vendor/bin/phpunit tests

設定ファイルの詳細は後で記載する。

  • 現在のフォルダ構成は以下の通りとなる
ルートディレクトリ
 |- .git       ・・・隠しフォルダ
 |- .circleci
 |  |- config.yml ・・・⭐️circleciの設定ファイル
 |
 |- src       ・・・フォルダ
 |  |- Human.php   ・・・評価対象モジュール
 |
 |- tests      ・・・フォルダ
 |  |- HumanTest.php・・・Testファイル
 |
 |- vendor     ・・・composerによりインストールされたフォルダ
 |  |- bin
 |  |   |- phpunit・・・phpunitの実行ファイル
 |  ~
 |- composer    ・・・ファイル
 |- composer.json ・・・ファイル
 |- README.md    ・・・ファイル
  • 設定ファイルを作成したら再度コミットしてプッシュする。
git add .
git commit -m "CircleCIの設定ファイル(config.yml)を作成"
git push

※GitHubの画面は省略する。

  • CircleCIへログイン > Projects > phpunit-circleciのSet Up Project > Use Existing Config > Start Buildingをクリックする。 スクリーンショット 2020-08-23 10.16.24.png

このようになるのでcircleci-phpunit #2Actionsの一番左のアイコンをクリックする。
スクリーンショット 2020-08-23 10.20.42.png

セットアップできているか確認できるのでSUCCESSとなっていれば問題ありませんね。
CircleCIとGitHubリポジトリとの連携は完了です。
これでローカルのソースコードを修正してGitHubにプッシュした際にCircleCIが自動的にデバッグphpunitを実行してくれる環境を作成することができました。
実際にプッシュして試す。

CircleCIでPHPUnitを動作させるためにGitHubにTestコードを修正してプッシュしてみる

  • すでに成功する環境が作ってあるので、README.mdにテストを追加してcommitしてプッシュする。
README.md
# circleci-phpunit
テスト

コマンドで以下を実行する。

git add .
git commit -m "README.mdの修正"
git push
  • CircleCI > Projects > phpunit-circleciをクリックするとこのようになっている。 スクリーンショット 2020-08-23 10.30.54.png
    RUNNING状態でconfig.ymlファイルに従ってビルド実行中がわかる。
  • 青色塗り潰し箇所のRUNNINGをクリックすると以下の画面になる。 スクリーンショット 2020-08-23 10.31.10.png
  • buildをクリックすると以下の画面になる。
    スクリーンショット 2020-08-23 10.31.51.png
    この画面は、config.ymlファイルの設定に従って実行中の実行内容がリアルタイムで閲覧可能な画面になりる。

    config.ymlファイルを修正する際は、config.yml修正後にpush操作をして、この画面をみながら問題有無をチェックして問題あれば修正するという流れとする。

    この画像ではすでにSUCCESS表示となっているが、CircleCIの無料版ではビルド時間に制限があるので、ここを確認してビルドが終わらないような問題がある場合は「・・・」からビルドの停止ができるので停止するなどしましょう。ビルドが成功または失敗する場合は、ビルド終了となる。

  • CircleCIでビルドが成功するとGitHub表示も成功表示となる。
    スクリーンショット 2020-08-23 10.42.32.png

    緑色のチェック表示が入り、ビルドに失敗すると×表示となる。

おまけ・・・config.yml作成時につまづいた点について

  • circleciでcheckoutした後にいる、現在の作業ディレクトリはGitHubからcheckoutしたプロジェクトのルートディレクトリとなる
  • phpインストール時にタイムゾーンが聞かれてずっと停止したままとなってしまっていた。
  • runコマンドでUNIXコマンドが叩ける。
  • circleciのWEB画面上で環境変数の設定が実はできる。
  • CircleCIドキュメント上の上部の検索窓も便利
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

みんな大好き「バリデーション」。コロナでどこへも行けませんが,画面にマスク着用すると,開発がちょっとだけ幸せになるかもという余談

概要

台湾しかり,ベトナムしかり。

コロナ対策で成功している国を見ると,早期の歯止めが,共通因数だなぁと感じるおじさんです。

システム開発の現場でも,早期の歯止めが,開発を幸せにしてくれるということを,記事にしてみようと思います。

具体的には,みんな大好き「バリデーション」関連です。

みんな大好き「バリデーション」

システム開発工程が無事完了し,いざ,利用部門ユーザを巻き込んだテストの段になって,意外と多いのが,誤入力防止機能(バリデーション)を,追加開発リクエストされるという展開。

「この欄は,過去日付が入力されたら,エラーメッセージを出して欲しい」

一般的な傾向として,完成したシステム画面を,実際に操作してみると,利用部門の期待(と欲望)は,ぐーんと高まるのですが,これは,人間として,ごく自然な心理特性です(=人間,何かを手に入れたら,もっと良いものが欲しくなる)。

特に,「品質の作り込み」が大好きな日本国内のシステム導入では,テスト工程での「後出しじゃんけん」って,驚くほど,お盛んですよね。

言葉がまったく通用しない地域を含めて,過去,六か国でのシステム導入を経験したおじさんですが,ぶっちゃけ,一番消耗したのは日本導入でした(ヒソヒソ)。

要求を無下にはねのけると,良好な人間関係に亀裂が走ることもありますが,Yes マンになってしまうと,要求がエスカレートし,プロジェクトをうまくコントロールできなくなるリスクもあり,プロマネとして,難しいかじ取りを迫られることもあります。

軽く済ませたい,フロントエンド側での実装作業

バリデーション実装となれば,大きく分けて,二つのアプローチがあります:

  • フロントエンド(例:JavaScript)でのバリデーション実装
  • バックエンド(例:PHP)でのバリデーション実装

システムって,誤入力はもちろんのこと,悪意のこもった入力まで,きちんと想定しなければならないため,DB 書込処理と近い距離にある,バックエンドのバリデーション実装は必須と言って良いと思います。

問題は,フロントエンドでのバリデーション実装。

どうせ,最終的にはバックエンド側でバリデーションを通すことになるのだから,フロントエンドは,できるだけ,手軽な実装方式で済ませたいというのが,多くの開発者での,共通認識ではないでしょうか?

きちんと作り込んだシステムって,データを入れて,入力欄からフォーカスアウトするごとに,律義にエラーチェック(それも,サーバとの通信を要する,高度なもの)がかかったりしますが,あれをすべての入力欄でやったら,死にます(おじさんは)。

定番スタイルとして確立された実装技術

それでは,フロントエンドでのバリデーションを,簡易的に済ませるには,どのようなアプローチがあるでしょうか?

データ型によっては,すでに,コスパよき実装手法が確立されたものもあります:

  • 日付型 カレンダー(例:DatePicker)経由でのみ入力を許可。手入力は禁止
  • 文字型 maxlength 指定によって,上限文字数を制限
  • 論理型 <input type="checkbox"> の利用

入力欄そのものを,正しいパターンしか反応させなくするという,物理的に封じるアプローチ。

また,「性別」や「都道府県」など,リスト化が容易な入力欄は,<select></select> を使えば,予期せぬ値がバックエンドに送られることを,未然防止できます。

これらは,いずれも手軽に実装できるものであり,エンドユーザにとっても,入力ミスを回避できるため,ユーザフレンドリーな UI であると言えます。

もうひとひねり,フロントエンドでの実装作業

一方,数字や電話番号,郵便番号,メールアドレスといった「データ型」の話になると,汎用的な実装手法について,あまり活発に議論されていない印象があります。

こういった「データ型」は,システム仕様によって,求められるチェック条件が変わる(例:郵便番号は,国によって,桁数が異なります)ため,一般論として語りづらいという背景はあるにせよ,システム開発の現場では,頻出する「データ型」であることに変わりありません。

ケースバイケースですが,「テキストマスク」を使うと,幸せになれる案件だって,結構あるかも知れないので,ご紹介します。

画面にも「マスク」着用しよう

バリデーションと比べると,認知度は低くなるのですが,入力欄の書式をチェックする「テキストマスク」を採用するというアプローチもあります。

ブラウザの JavaScript 上で動作するもので,いろいろなライブラリが出回っていますが,参考として 'Vanilla Text Mask' を取り上げます:

公式サイト:vanilla-text-mask

以下,いくつかの事例を取り上げます。

実装事例① 「郵便番号」欄

テキストマスク 'Vanilla Text Mask' は,正規表現と,JavaScript の文法(配列)をミックスさせた書き方が特徴的です。

ここでは,「数値3ケタ ハイフン 数値4ケタ」という,日本の郵便番号を取り上げてみましょう。

実装作業は,JS ファイルで [/\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/] というマスクを指定し,入力欄とバインドするだけ。

第一印象では,正規表現と,JavaScript の文法(配列)が入り混じっているため,頭にクエスチョンマークが浮かびそうになりますが,普通の JavaScript の文法(配列)と解釈すれば,恐れるに足りません

これによって,キーボードからの入力が制約できるようになり,123-4567 と入れれば反応しますが,abc-defg123456 とタイプすれば,何事も起こらなかったかのように,無反応になります(キーボードの入力値をはねのけます)。

index.html
<input type="text" id="postal_code">
postal_code_mask.js
  const postal_code_mask = [/[\d]/, /\d/, /\d/, '-', /[\d]/, /\d/, /\d/, /\d/]

  const postal_code = document.querySelector('#postal_code')

  vanillaTextMask.maskInput({
    inputElement: postal_code,
    mask: postal_code_mask
  })

実装事例② 「年齢」欄

次は,年齢欄を題材にしてみましょう。

たとえば,未成年が利用しないシステム(年齢の幅:20 ~ 99)だと,JS ファイルで [/[2-9]/, /[0-9]/] というマスクを指定し,入力欄とバインドするだけで完了。

これによって,キーボードからの入力が制約できるようになり,23 と入れれば反応しますが,ab12 とタイプすれば,何事も起こらなかったかのように,無反応になります。

index.html
<input type="text" id="age">
age_mask.js
  const age_mask = [/[2-9]/, /[0-9]/]

  const age = document.querySelector('#age')

  vanillaTextMask.maskInput({
    inputElement: age,
    mask: age_mask
  })

まとめ

以上,「テキストマスク」による,フロントエンドの誤入力防止アプローチをご紹介しました。

サーバとの通信が求められず,かつ,書式が固定されているようなケースでは,フロントエンド上で実装が完結するので,つぶしの効きやすい技術であると言えます。

(ほぼ)すべてのブラウザの種類やバージョンで正常動作する,クロスブラウザ対応をしてくれているのも,嬉しいポイントですね。

締めくくる前に,もう一度強調したいと思いますが,バリデーション実装のキモは,やはり,バックエンド側(理想的には DB 書込直前タイミング)での最終チェック。

そして,バリデーションって,足せば足すほど望ましいというわけでもなく,システム開発予算とのバランスもありますし,場合によっては,バッサリ省略するという,思い切ったアプローチを視野に入れることも大切です:

  • 人間の手入力が介在しないデータ入力(例:他システムからデータを受け取るケースなど)
  • 社内ネットワーク内でのみ提供され,利用人数が非常に少ない簡易的システム

フロントエンドとバックエンドで,バリデーション処理の一部を共通化して,開発工数を節約するのも一案。

このトピック周辺って,いくら語っても,語りつくせない,奥深き世界ですが,本記事によって,「テキストマスク」と出会い,幸せになれるエンジニアが一人でもいらっしゃったら,おじさんの苦労がすべて報われます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

駆け出しエンジニア「サーバーサイド言語を勉強したら、何ができるようになるの?」

登場人物

駆け出しエンジニア(以下、後輩? と呼ぶ)
やっとの思いでHTML/CSS/JavaScriptを習得した、駆け出しエンジニア。
JavaScriptのライブラリやフレームワークも経験して、
とりあえずイメージしたWEBアプリを作成できるレベル感に達成した。
使っているブラウザはGoogle Chrome。

駆け出しエンジニアの友人(以下、先輩? と呼ぶ)
駆け出しエンジニアと同じカリキュラムで勉強をした後、
駆け出しエンジニアよりも1年早くサーバー関連の仕事を経験してきた、彼の友人。

あらすじ

後輩? :
サーバーサイド言語を学ぶべきなのはわかるけど、これを学んだら何ができるようになるんだろう?

オンラインサービスが作れるようになる、というのはわかるんだけど、それだけなのかな?
なんかパッとしないな〜。

先輩? :
おっ、後輩。
なにやら勉強のモチベーションに行き詰まっているようだね?

後輩? :
先輩!
そうなんだよ〜。サーバーサイド言語がとっても大事な知識だというのは理解してるんだけど、
JavaScriptでもいろいろ作れるようになって楽しんでるから、
今まで作れなかった、こんなものが作れるようになる!みたいなワクワク感がないと、
モチベーションがあがらないんだよ〜。

先輩? :
ちょうど、そんな時期かな〜と思って、相談にのりにきたよ。
1年前の僕も、おなじ気持ちでモヤモヤしていたから、僕が経験して得た知識を伝授してあげよう。

後輩? :
やったー!

オンラインサービス

先輩? :
サーバーサイド言語を学ぶ一番の目的がオンラインサービスを作ることだというのは、わかるよね。

後輩? :
うん、自分が入力したデータを世界中の人と共有するには、
自分のPCのかわりにデータを配信してくれるPC(=サーバー)が必要なんだよね。

先輩? :
そう。そしてデータ共有方法の発展系として、
ユーザーのひとりが情報を更新したらリアルタイムで全てのユーザーに内容を反映できる「WebSocket」や、
ユーザーからのアクションがなくともサーバーからユーザーにデータを送信できる「Push通信」などがあるね。

ログインシステム(ユーザー情報の保存)

先輩? :
オンライン化の次に大事な目的が、
ユーザーの情報を、こちらがわのPC、つまりサーバーに保存しておけることだよ。

後輩? :
JavaScriptの内容はすべてユーザーの手元に渡ってしまうものだから、
JavaScriptだけでパスワードっぽい機能を実装することは何の意味もないっていう話は聞いたことあるよ。

先輩? :
そう。どんなに難読化などの対策を頑張っても、
ユーザーに見せたくない情報をJavaScriptに持たせている時点で、仕組みとしては欠陥品だね。

後輩? :
うんうん。

ただ、ユーザーの情報を保存しておく目的っていうのはイマイチイメージできないよ。
もちろん、保存する必要なんてない!とは思わないけど、
具体的にどんなことができるようになるかは、いまいちわからない。

先輩? :
いちばんの目的は、ログインシステムを導入することだね。
IDとパスワードが一致するユーザーは同一人物だと判断することで、
たとえ使っているデバイスやブラウザが変わっても、そのユーザーの情報をひきつづき利用することができるよ。

後輩? :
うん。たしかに、それはそうだね。

先輩? :
それに、会員の情報がなければ、「お誕生日おめでとうメールを送る」みたいな、情報をもとに分岐する作業はできないよね。

後輩? :
いやいや、別にサーバーに保存してなくても、誕生日を入力してもらう画面で情報を取得してlocal storageなどに保持しておけば
ユーザーがアクセスしてきたタイミングで「お誕生日おめでとうメッセージの表示」はできるんじゃない?

先輩? :
たしかに。
後輩は、JavaScriptだけでもいろんな事ができるように、頑張って勉強しているよね。
ちなみに、JavaScriptでもメールを送る方法はあるから、「メッセージの表示」じゃなくて、ふつうに「メールを送る」こともできるよ。
気になるなら「JavaScript メール送信」でググってみてね。

後輩? :
そうなのか!
やっぱりJavaScriptって、なんでもできるんだなあ。

先輩? :
まあ、話がそれちゃったけど、
ユーザーの情報をサーバーに保存しておくとどういうメリットがあるのかは、あとで詳しく説明する事にするよ。

閑話 シェル言語について

先輩? :
ところで「シェル言語」については、既に習得済みだったかな?

後輩? :
うん。「コマンドライン」とか「シェルスクリプト」とか「Linuxコマンド」とか呼ばれているものだよね。
Windowsなら「コマンドプロンプト」や「PowerShell」、
Macなら「Terminal」を開いて入力することで、
PC内のファイルを作ったり消したりできる、PCの操作をするための言語だよね?

先輩? :
うん。
ちなみにMacやLinuxで一般的に使われているシェル言語は「bash」という名前の言語だよ。
あ、最近のMacは、これを更に便利にした「zsh」という言語を標準で使うように変更されたね。

後輩? :
「絶対やっておいた方がいい」って言われて、とりあえず勉強したんだよ。
基本は難しくなかったし、
黒い画面でカタカタする感じがプログラマー!!って感じがして楽しかったけど、
ぶっちゃけまだ有効活用できてない。

先輩? :
それもそのはず。
実はシェル言語を使う機会って、サーバーの操作をするときか、
手元のPCにサーバーに関わる設定をするときがほとんどだからね。

後輩? :
あっ、そうなの?

先輩? :
うん、理由は単純で、サーバー用のPCには基本的にデスクトップ画面やマウスがないから。
手元にあるPCからサーバーに「SSH接続」して、シェル言語を使って操作するような方法しかない。
だから必然的に必要になってくるというわけ。

後輩? :
なるほど。学習中、どういう時につかうべきなのかイマイチ想像できなかったのは、
手元のPCでは基本的に使う必要がないからなのか。

先輩? :
ちなみにサーバー用のPCを用意するとき、使われるOSはほとんどの場合「Linux」というOSだよ。なんてったって無料だからね。
「Linux」の標準のシェル言語は「bash」なので、手元のPCがMacであれば練習に使えるけど、
手元のPCがWindowsの場合、ルールやコマンドが色々違うから、「コマンドプロンプト」や「PowerShell」で練習するときは注意しながら進めたほうがいいよ。

それと、これは余計なお世話かもしれないけど、学習サービスによっては、
「パーミッション」という概念について解説されていないところがあるらしい。
シェルでファイルを操作するようになったら必ず必要になる大事な知識だから、
実際にサーバーに触りはじめる時には、勉強しておく事をオススメするよ。

後輩? :
僕が使った学習サービスでは、「パーミッション」というのはやらなかったな…。
その時が来たら、調べてみるね。

プログラミング単体で実行できる

先輩? :
ところでなんで急にシェル言語の話をしたかというと、
次の話の説明をするためなんだ。

JavaScriptは、HTMLで読み込むことで、ページを開いたときに自動で実行されるのが標準の動作だよね。

後輩? :
そうだね。

先輩? :
対して、サーバーサイド言語は基本的に「シェル」でコマンドを叩くことでも実行できるようにしてあるものなんだ。
JavaScriptをサーバーサイド言語として使えるようにした「Node.js」という仕組みも、こういうことをできるようにするためのものなんだよね。

まず、サーバー、もしくは自分のPCに好きなサーバーサイド言語をインストールする。
そうすると「シェル」でサーバーサイド言語を実行するためのコマンドが使えるようになる。

後輩? :
つまり、検証ツールのConsoleと同じようなことを、「コマンドプロンプト」や「Terminal」などの、
いわゆる「シェル」でできるのがサーバーサイド言語っていうこと?

先輩? :
うん、Consoleみたいにひとつひとつ命令を実行していくこともできるけど、
基本的には、JavaScriptと同じようにプログラム言語を書いたファイルをつくっておいて、
そのファイルを「シェル」で一気に実行する、っていう使い方が一般的かな。

後輩? :
そうなんだ。

先輩? :
つまりシェルでプログラミング言語を単体実行できることが、
JavaScriptと大きく違うポイントだっていうこと。

後輩? :
プログラミング言語だけを実行するっていうのが、いまいちイメージできないな。

先輩? :
たとえばさっきの「お誕生日メール」を例にすると、
PCのどこかにユーザーの情報を保存しておけば、
その情報を取得しにいって、誕生日ならメールを送る、という処理を単体で実行できるよね。

後輩? :
ああ、そういう感じか。
JavaScriptのプログラミングは最終的にConsole.logするか、HTMLに出力するかっていうのが基本だったけど、その最終地点をメールにしちゃえば、プログラム単体で目的が達成できるもんね。

先輩? :
そういうこと。
JavaScriptが「WEBページを開いている間しかプログラムを実行できない」ことに対して、
サーバーサイド言語は「プログラム単体で実行できる」ということも、サーバーサイド言語でできるようになるひとつのメリットだよ。

好きな時間にプログラムを実行できる

先輩? :
プログラム単体で実行できることを踏まえて、
サーバーは更に「好きな時間にプログラムを実行できる」というメリットがあるよ。

サーバーに限らず基本的なPCの機能として、決まった時間に「シェル言語」を実行するための「cron」という仕組みがあるんだけど、知ってる?

後輩? :
聞いたことないよ。
あらかじめ実行するコードと、実行する時間をきめておけば、その時間に自動実行してくれるっていうこと?

先輩? :
そのとおりだよ。
だから「cron」の設定をすれば、好きな時間に好きなプログラムの実行ができるというわけ。
もちろん、PCが起動していなかったらcronも動かないから、サーバーは常につけっぱなしにする事がほとんどだよ。

ユーザーがWEBページを開いていなくても、こちらの好きな時間にユーザーに対するアクションを起こすことができる。
ユーザーに対するアクション以外にも、サーバー内のデータの整理や、外部サービスを利用するために必要な情報を送信するみたいな処理を、自動で実行することができる。
これはJavaScriptには出来ないから、サーバーサイド言語ならではのメリットだよね?

後輩? :
たしかに。なんか勉強するぞって気持ちになってきたかも!

データベース

先輩? :
ちなみにユーザーの情報を保存しておいたり、誕生日の人のメールアドレスだけ取得する、みたいな柔軟な情報操作をするために「データベース」っていう機能がよくインストールされるよ。

後輩? :
「データベース」は聞いたことある。そういう時に使うやつなんだね。
っていうか、データベースってインストールするものなの?

先輩? :
うん。サーバーだって普通のPCと同じで、最初はただのPCだからね。
サーバーサイド言語も、データベースも、WEBサーバーという機能ですらも、
ぜんぶシェル言語を使ってインストールすることで使えるようになる、アプリみたいなものなんだよ。

後輩? :
そう聞くと、なんだかサーバーというものが今までより身近に感じるかも。

先輩? :
データベース自体は、シェルから実行して単体で使うものなんだけど、
サーバーサイド言語には、その言語からデータベースにアクセスするための方法が用意されているから、
それを学べばサーバーサイドのプログラムでデータベースの情報を利用できるようになるよ。

WEBサーバー

先輩? :
そういえば、さっきチラっと触れたけど
そもそもWEBサーバーってどういう機能か知ってる?

後輩? :
たぶん、わかってると思う。
HTMLやCSSやJavaScriptのファイルを、ネットを介してユーザーに配信できるんだよね?

先輩? :
まあ、そのとおりだね。
そして配信する際に、特定のIPアドレスからのアクセスをブロックしたり、
アクセスされたアドレスによって表示するファイルを決めるのも、WEBサーバーの機能だよ。

後輩? :
表示するファイルを決めるって…
ユーザーがアクセスするアドレスは、サーバーのディレクトリ構成やファイル名そのままじゃなくても大丈夫ってこと?

先輩? :
うん。◯◯◯というアドレスにアクセスされたら、△△△というファイルを表示するっていうことができる。
仮にサーバー内のディレクトリ構成やファイル名がごちゃごちゃしちゃっても、
ユーザーがアクセスしてくるアドレスは単純なものに変更できてしまうので、これもメリットのひとつかもね。

サーバーサイドレンダリング

先輩? :
WEBサーバーの機能とサーバーサイド言語を組み合わせて利用した、重要な機能があるんだ。
サーバーでは、配信するファイル(主にHTML)の内容を書き換えて配信することができるんだよ。

後輩? :
JavaScriptでもHTMLの内容を書き換えることはできるけど、
サーバーでそれをするメリットは何なの?

先輩? :
JavaScriptでHTMLを書き換えるということは、書き換え前のHTMLの内容も、書き換え後のHTMLの内容も、すべてユーザーの手元にある状態だよね?

後輩? :
えーっと…
書き換え前のHTMLの内容は、ブラウザの「ソースを表示」機能で見ることができるし、
JavaScriptの内容は、検証ツールの「Sources」タブから見ることができるから、
それを読み解けば把握できちゃう状況ではあるね。

先輩? :
うん。それに対して、サーバーでファイルの内容を書き換えてから配信するということは、
ブラウザの「ソースを表示」機能で見れるHTMLが、既に書き換えられた状態になってるということ。
なので、どのように書き換えたかがユーザーに全く伝わらず、よりセキュリティ的に良い(=セキュアな)仕組みになるよ。

それと、配信するファイルの容量も軽くなるから、通信量的にもお得になるね。

後輩? :
なるほどなあ。たしかに、HTMLとJavaScriptの内容を削減して配信できるわけだから、その分データは軽くなりそうだね。

先輩? :
うん。だから、ユーザーがページ内で行った行動(入力やクリックなど)によって表示内容を判断しなきゃいけない部分はJavaScript、
ページ内での行動にかかわらず、ページを表示する時点でどのような表示内容にするか判断できる場面なら、サーバーサイド言語で書き換えるのが合理的だよ。
もちろん、必ずそうしろという事ではないけどね。

このようにサーバーで予めファイルの情報を書き換えて配信することを「サーバーサイドレンダリング」と呼ぶよ。

まとめ

先輩? :
どうかな?
サーバーサイドを学ぶことで、

  1. オンラインサービスを作成できる!
  2. ログインシステムを実装できる!
  3. プログラム単体で実行できる!
  4. 好きな時間にプログラムを実行できる!
  5. データベースを利用したシステムを作れる!
  6. WEBサーバーを作れる!
  7. サーバーサイドレンダリングができる!

少なくともこれだけの事ができるようになるんだから、今までよりもっといろんなシステムが作れるようになる。
学習のモチベーションは、上がったかな?

後輩? :
うん!もちろん!
何のために勉強するのか、具体的に想像することができて、すっごく助かったよ!
ありがとう!!

先輩? :
もちろん、僕の知識以外にも、出来るようになることはもっともっといっぱいあるとおもう。
もしかしたら、コメント欄で教えてもらえるかもしれないから、チェックしてみてね。

後輩? :
みなさま、コメント欄へのコメントお待ちしております!

先輩? :
後輩くん以外の駆け出しエンジニアのみんなにとっても、
この記事が学習のモチベーションを上げるきっかけになったら、とってもうれしいな!
長い話になりましたが、最後まで読んでくれて、どうもありがとうございました!

後輩? :
ありがとうございました!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelでドメイン駆動設計を用いてプロフィール登録を実装してみる

DDDでプロフィール登録を実装する流れ

1、Controllerを作成する
2、Model(Eloquent)を作成する
3、Viewを作成する
4、Eloquentを編集する
5、リポジトリを作成する
6、Providerに登録する
7、UseCaseを作成する
8、Requestを作成する
9、Controllerを編集する
10、ルーティングを編集する

それではやってみよ〜

1、Controllerを作成する

Controller:ProfileController
php artisan make:controller ProfileController

2、モデルを作成する(migrationファイルも作成する)

Model:Profile
php artisan make:model Profile --migration

■migrantionファイル

2020_mm_dd_xxxx_create_profiles_table
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateProfilesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('profiles', function (Blueprint $table) {
            $table->id();
            $table->string('nick_name');
            $table->string('introduce');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('profiles');
    }
}

■DBマイグレーションしてprofileテーブルを作成する

php artisan migrate

3、Viewを作成する

resources/views/profile.blade.php
@extends('layouts.app')

@section('content')
<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">
                    @if (session('status'))
                        <div class="alert alert-success" role="alert">
                            {{ session('status') }}
                        </div>
                    @endif

                </div>
                {{ Form::open() }}
                <div class="form-group row col-12">
                    <p class="d-flex align-items-center col-3">ニックネーム</p>
                    {{Form::input('text','name', '', ['class' => 'form-controller col-5'])}}
                </div>       
                <div class="form-group row col-12">
                    <p class="d-flex align-items-center col-3">自己紹介</p>
                    {{Form::textarea('introduce', '', ['class' => 'form-controller col-5'])}}
                </div>    
                {{ Form::close() }}
            </div>
        </div>
    </div>
</div>
@endsection

スクリーンショット 2020-08-16 0.30.57.png

4、Eloquentを編集する

app/Domain/RepositoryInterface/ProfileRepositoryInterface.blade.php
<?php

namespace App\Eloquent;

use Illuminate\Database\Eloquent\Model;

class EloquentProfile extends Model
{
    protected $table = 'profiles';

}

5、リポジトリを作成する

■RepositoryInterfaceを作成する

app/Domain/RepositoryInterface/ProfileRepositoryInterface.blade.php
<?php

namespace App\Domain\RepositoryInterface;

use App\Eloquent\EloquentProfile;

interface ProfileRepositoryInterface
{
    /**
     * @param $post
     * @return EloquentProfile
     */
    public function store($post): ?EloquentProfile;

}

■Repositoryを作成する

app/Domain/Repository/ProfileRepository.blade.php
<?php

namespace App\Domain\Repository;

use App\Eloquent\EloquentProfile;
use App\Domain\RepositoryInterface\ProfileRepositoryInterface;

class ProfileRepository implements ProfileRepositoryInterface
{

  public function store($post): EloquentProfile
  {
      $eloquentProfile = EloquentProfile::findOrNew($post['id']);
      $eloquentProfile -> nick_name = $post['nick_name'];
      $eloquentProfile -> introduce = $post['introduce'];

      $eloquentProfile -> save();

      return $eloquentProfile;


  }
}

6、Providerに登録する

app/Providers/AppServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(
            \App\Domain\Repository\ProfileRepository::class,
            \App\Domain\RepositoryInterface\ProfileRepositoryInterface::class
        );
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {

    }
}


7、UseCaseを作成する

app/Domain/UseCase/registerProfile.php
<?php

namespace App\Domain\UseCase\Profile;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use App\Domain\Model\Profile;
use App\Domain\Repository\ProfileRepository;
use App\Eloquent\EloquentProfile;

class RegisterProfile{

    private $profileRepo;

    public function __construct(ProfileRepository $profileRepo){
        $this->profileRepo = $profileRepo;
    }

    public function __invoke($post)
    {
        $post['id'] = 0;

        return $this->profileRepo->store($post);
    }

}

8、Requestを作成する

app/Http/Request/ProfileRequest.php
<?php

namespace App\Http\Request;

use App\Models\Profile;
use Illuminate\Foundation\Http\FormRequest;

class ProfileRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'nick_name' => 'required|string|max:255',
            'introduce' =>'required|string|max:255',
        ];

    }

}

9、Controllerを編集する

app/Domain/UseCase/registerProfile.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Domain\UseCase\Profile\registerProfile;
use App\Http\Request\ProfileRequest;

class ProfileController extends Controller
{
    public function register(ProfileRequest $request, registerProfile $useCase){

        $result = $useCase($request->all());

        return redirect()->route('profile.result');

    }

}

10、ルーティングを編集する

web.php
<?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::view('/profile/edit', 'profile.edit')->name('profile.edit');
Route::post('/profile/register', 'ProfileController@register')->name('profile.register');
Route::view('/profile/result', 'profile.result')->name('profile.result');

うごいた!!!

スクリーンショット 2020-08-23 15.06.12.png

参考

Laravel5を使ってドメイン駆動設計で作るサンプルアプリ。
Laravelでドメイン駆動設計(DDD)を実践し、Eloquent Model依存の設計から脱却する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

laravel ルーティングまとめ

ルーティングとは

viewからリクエストされたURLと、Controller内に定義されたActionを結び付ける役割
アクセスしたアドレスに応じて対応するControllerのActionを呼び出す仕組みのことをRoutingという

基本的なRoutingの書き方

Route::get('user/news/create', 'user\NewsController@create');

http://XXXXXX.jp/user/ からはじまるURLを指定することになる。
user/news/create というURLにアクセスしたら NewsController の create Actionに割り当てますという意味

ルーティングのgroup化

ルーティングの設定で Route::group を使用すると一括して定義することができるよ
主に使うものは、
・プレフィックス(URLをまとめるときに使用)
・ミドルウェア(主に認証をかけるときに使用)
だよ

下記のコードは
プレフィックスを使って同じURLを経由するroutingを一括りにして、誰が見てもわかりやすく、変更にも簡単に対処できるように整理しているよ

Route::group(['prefix' => 'user'], function() {
    Route::get('news/create', 'user\NewsController@create');
    Route::get('news/edit', 'user\NewsController@edit');
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ubuntuに最新版のパッケージをインストールする方法まとめ

Ubuntuでパッケージをインストールしようとしたとき
バージョンが古かったりそもそも存在しなかったり
するときの対処法のまとめ

結論としてリポジトリを追加して最新版のパッケージを
インストールするやり方は2点

  • 公式(パッケージの方)のリポジトリをサイトから探す
  • PPAを使う

apt installはそもそも何をしているのか?

ざっくりとした手順は以下

  1. 依存関係のチェック
  2. パッケージをダウンロードし、dpkgに渡す
  3. dpkgがパッケージをインストール

結局のところ2番目のパッケージが古いか
もしくは存在しないかが問題になる

パッケージはどこにあるのか?

パッケージは/etc/apt/sources.listにリポジトリとして存在している

以下一部抜粋

# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to
# newer versions of the distribution.
deb http://us.archive.ubuntu.com/ubuntu/ bionic main restricted
# deb-src http://us.archive.ubuntu.com/ubuntu/ bionic main restricted

重要なのは
deb http://us.archive.ubuntu.com/ubuntu/ bionic main restricted
の部分

書式は

deb uri distribution [component]

となっているが
見てわかる通りなんだかややこしい

distribution、component等にそれぞれ意味があるのだが
結局なにをしているのかというと
この書式でURLを形成している

例えばdeb http://us.archive.ubuntu.com/ubuntu/ bionic-updates universe
ならhttp://us.archive.ubuntu.com/ubuntu/dists/bionic-updates/universe/binary-amd64/
にPackages.gzがあることが確認できる

ubuntu_formal_repository.PNG

つまりパッケージはUbuntu上に存在しているわけではなく
どっかのサーバーからダウンロードしてインストールすることになる

また配布しているサイトによってはdistributionとcomponentの
書き方がまちまちだったりするので書式が複雑ならURLを辿ってみると
どこにパッケージが存在するのか解りやすかったりする

distributionとcomponentの詳しい説明は下記
APT の設定 (/etc/apt/sources.list) をちゃんと理解する

sources.listにリポジトリを加える

sources.listがなんなのかわかったので
実際にリポジトリを加えてみる

基本的に公式(アプリの方)がリポジトリを公開しているのなら
やり方はだいたい公式サイトに書いてあるので参照

今回はNginxを追加してみる

まず現時点で入手できるパッケージを確認

$ apt-cache show nginx
Package: nginx
Architecture: all
Version: 1.14.0-0ubuntu1.7
Priority: optional
Section: web
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Debian Nginx Maintainers <pkg-nginx-maintainers@lists.alioth.debian.org>
…

最新版で1.14.0

sources.list.d下にnginx.listを加える

$ sudo vi /etc/apt/sources.list.d/nginx.list

#以下を追加
deb http://nginx.org/packages/ubuntu bionic nginx

この場合bionic(Ubuntu18.04のコードネーム)がdistribution
nginxがcomponentになる

実際にURLを辿っていくと
http://nginx.org/packages/ubuntu/dists/bionic/nginx/binary-amd64/
にPackagesがある

nginx_package.PNG

たいていパッケージはPGPで暗号化されているので
公開鍵を入手

PGPについては1分でわかるPGP

$ wget https://nginx.org/keys/nginx_signing.key | sudo apt-key add -
OK

apt-key addで公開鍵をインポートして使います

あとはアップデートしてインストールするだけ

$ sudo apt update
$ sudo apt install nginx

最後にバージョンを確認

$ nginx -v
$ nginx version: nginx/1.18.0

PPAを使う

公式がリポジトリを作ってない場合があります
そんな時はPPA

PPAは、「Personal Package Archive」の略で
その名の通り第三者が配布しているリポジトリ

導入の際は自己責任でやりましょう

PPAは安全なのか?

とかいいつつホントは公式が配布しているんでしょ?
と思われるかもしれないが

ホントに第三者が配布している(公式が配布している場合もある)ので
セキュリティを気にするなら下記の三点は抑えておく

  1. PPAを作成した人をキチンと調べる(公式なのかそうでないのか等)
  2. そのPPAの使用ユーザー数を調べる(使っているユーザーはいるのかググる)
  3. 更新頻度を調べる

参考サイト
Are PPAs safe to add to my system and what are some “red flags” to watch out for?

導入する

以上のことを踏まえて実際にPPAでPHP7.2のリポジトリを導入してみる

https://launchpad.net/ubuntu/+ppas
からPHPで検索してみると大量のPPAがヒットするが
この中でも更新頻度が高いものを選択する

PPA_PHP72_edit.png

実際、このPPAは有名でググると大量の記事がヒットがわかる

追加の仕方は

$ sudo add-apt-repository ppa:user/ppa-name

大体はAdding this PPA to your systemの欄に書いてある通り

実際に導入してみる

$ sudo add-apt-repository ppa:ondrej/php

sources.list.dを調べてみるとondrej-ubuntu-php-bionic.list
が追加されているので中身を調べてみる

$ cat /etc/apt/sources.list.d/ondrej-ubuntu-php-bionic.list
deb http://ppa.launchpad.net/ondrej/php/ubuntu bionic main
# deb-src http://ppa.launchpad.net/ondrej/php/ubuntu bionic main

基本的なことは先ほど説明した通り
あとはupdateしてinstallするだけ

$ sudo apt update
$ sudo apt install php7.2

ということで無事インストールできました

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

phpQuery、PhamtomJSを使ってJavascriptで生成されたWebサイトをスクレイピングする

概要

PHPでスクレイピングが簡単に行えるphpQueryと、ヘッドレスブラウザのPhamtoJSを使って、Javascriptで生成されたWebサイトのスクレイピングを行います。スクレイピングはサイトの分析をするときに使える便利な技術です。
今回はニュースサイトから最新記事の情報を取得し、指定のメールアドレスに送信できるようにします。

スクレイピングは、使い方によってはサーバーに負荷がかかったりすることもありますので、利用規約に反していないかなど注意しなければいけない点がいくつかあります。スクレイピングを行う際は、下記サイト等を参考に慎重に行ってください。
Webスクレイピングの注意事項一覧

phpQueryのダウンロード

phpQueryのダウンロードページから最新の「phpQuery-...-onefile.zip」をダウンロードします。
https://code.google.com/archive/p/phpquery/downloads

phpQuery-onefile.phpというファイルが取得できるので、それを読み込みます。

require_once("./phpQuery-onefile.php");

PhamtomJSのインストール

「composer.json」ファイルを作成し以下を記述します。

{
    "config": {
        "bin-dir": "bin"
    },
    "scripts": {
        "post-install-cmd": [
            "PhantomInstaller\\Installer::installPhantomJS"
        ],
        "post-update-cmd": [
            "PhantomInstaller\\Installer::installPhantomJS"
        ]
    }
}

ファイルを作成したら、以下を実行します。

$ composer require "jonnyw/php-phantomjs:4.*"
$ tree bin
bin
└── phantomjs

HTML形式で保存する

まずは、サイトを丸ごとHTML形式で保存します。
PhamtomJSを使ってサイトのjavascriptを実行させてから、phpQueryで情報を取得します。

#phpQueryの読み込み
require_once("/Users/sh/myapps/basic_php_v3-master/work/phpQuery-onefile.php");
#Phantomjsの読み込み
require '/Users/sh/myapps/basic_php_v3-master/work/vendor/autoload.php';
use JonnyW\PhantomJs\Client;
use JonnyW\PhantomJs\DependencyInjection\ServiceContainer;

$client = Client::getInstance();
$request = $client->getMessageFactory()->createRequest();
$response = $client->getMessageFactory()->createResponse();

#URLを指定しHTML情報を取得
$url = 'https://www3.nhk.or.jp/news/catnew.html';
$request->setUrl($url);
$client->send($request,$response);
$htmlstr = $response->getContent();
$dom = new DOMDocument;
@$dom->loadHTML($htmlstr);
$dom = $dom->saveHTML();
$doc = phpQuery::newDocument($dom);

#HTML形式で保存
$fp = fopen('data.html', 'w');
fwrite($fp, $doc);
fclose($fp);

保存したHTMLから必要な情報だけ取得し、HTML形式で保存する

今回はタイトル、投稿日時、詳細リンク、タグを取得します。

$file = fopen("news.html", "w");
#forを使って最新20件のニュース情報を取得
  for($i=0;$i<20;$i++){
#XPathを使って取得したい部分を指定する
    $title[$i] = $doc->find("body #content .content--list li:eq($i) dl dd a em")->text();
    $time[$i] = $doc->find("body #content .content--list li:eq($i) dl dd a time")->text();
    $tag[$i] = $doc->find("body #content .content--list li:eq($i) dl dd span .i-word")->text();
    $link [$i]= "https://www3.nhk.or.jp/".$doc->find("body #content .content--list li:eq($i) dl dd a ")->attr("href");
    fputs($file, $title[$i].$time[$i].$tag[$i].$link [$i]."\n");
  }
  fclose($file);

これで、必要な情報だけが記載されたHTMLファイルが完成しました。

HTMLファイルをテキスト化しメールで送信する。

#HTMLをテキスト化
$html = file_get_contents("news.html");  
  $dom= phpQuery::newDocument($html);
  $news = $dom->text();

#テキスト化されたHTMLを本文として、メールアドレスに送信する
  mb_language("Japanese");
  mb_internal_encoding("UTF-8");
  if(mb_send_mail("メールアドレスを入力", "最新ニュース", $news))
  {
    echo "メール送信成功です";
  }
  else
  {
    echo "メール送信失敗です";
  }

最後に

本記事で紹介したやり方は、スクレイピングの手順としてかなり回りくどいことをしています。
本来なら、サイトをまるごとHTML形式で保存する必要はなく、ハナから必要な部分だけを取得してHTML化する方がスマートです。
今回は、$docをそのままphpQueryでスクレイピングをするとエラーが発生したので仕方なくこのやり方をしました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPSpreadSheetで1万行強書こうとして死んだ話となんとかした話

記憶を頼りに。
帳票出力で1万行を超えるExcelをPHPSpreadSheetで書こうとした。
プロジェクトにはすでにPHPSpreadSheetのヘルパーライブラリがあってそれ使えみたいな空気感。
で、パフォーマンスが悪くて死んだ。
2000行超えたあたりから1行書くのに3秒とかかかる様になって詰んでた。

パフォーマンス問題

問題は記憶が確かならこの関数

$spreadsheet->getActiveSheet()->insertNewRowBefore(7, 2);

insertNewRowBefore。この関数、前の行の様式や書式を引き継いでしてくれるので楽なんですけどパフォーマンスで死ぬ。
PHPSpreadSheet、全体的にパフォーマンス問題抱えているようでGithubとかも死屍累々、Stackoverflowでは別のライブラリ使えって言われるので全体的に負け戦。でもなんとかしなきゃいけない

対応方法

ともかくからのExcelに行番号を保持してインクリメントしながら書き倒して、様式書式全部設定していけば速度もメモリも許容範囲に収まった。
楽な道はパフォーマンスが悪い、という典型でした。

ダメだった対応

  • 適当な大きさのExcelを用意してそこに書く

5万行とかのExcelテンプレート用意したらメモリ使用量が1GBとかいってダメだった。

結果

所要時間

360分(予測)→ 12分
まぁまぁ、劇的な結果。
1時間たっても1/6しか進んでなくてプロセス殺したので死んでたときの経過時間は予測。
実際はどんどん一行書くのに時間がかかっていくようになっていたので360分では終わらなかったと思う。

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPでは==と===の意味が違うらしい

プログラミングの勉強日記

2020年8月23日
PHPを学んでいく上で=====を知り、違いがあることを知ったのでまとめる。

PHPの比較演算子

 条件式で用いる左右の値の大小や等値関係を確認する演算子を比較演算子という。演算子の左右に置いた値や変数の内容の比較をする。

演算子 役割
== 等しい
!= 等しくない
> より多い
>= 以上
< 未満
<= 以下

==と===意味の違い


 ==:値が等しいときにtrueを示す
 ===:値が等しく型も等しいtrueを示す

※PHPでは==演算子を使うと型が違っても等しいとみなす。

 なので、===!==の等号や不等号は値とデータ型まで含めた完全一致もしくは不完全一致かを判定する。データ型は文字や整数、実数などのデータの種類のこと。

$a = 10;
$b = '10';

//trueを返す
$a == $b

//falseを返す
$a === $b
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel7 ユーザ認証時にメールアドレス確認の処理を付与する

目的

  • LaravelのAuth認証ではメールアドレスはただ入力するだけで、入力されたメールアドレスが正しい物であるかどうかの確認がない
  • 入力メールアドレス宛にメールを送信し、初回のログインはメール内のURLからのみ行える様にする

実施環境

  • ハードウェア環境
項目 情報
OS macOS Catalina(10.15.5)
ハードウェア MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)
プロセッサ 2GHzクアッドコアIntel Core i5
メモリ 32 GB 3733 MHz LPDDR4
グラフィックス Intel Iris Plus Graphics 1536 MB
  • ソフトウェア環境
項目 情報 備考
PHPバージョン 7.4.3 Homwbrewを用いて導入
Laravelバージョン 7.0.8 commposerを用いてこちらの方法で導入→Mac Laravelの環境構築を行う
MySQLバージョン 8.0.19 for osx10.13 on x86_64 Homwbrewを用いてこちらの方法で導入→Mac HomebrewでMySQLをインストールする

前提条件

  • 前述した実施環境に準ずる環境が整っていること。
  • Laravelアプリが作成され、アプリの起動、ブラウザからの確認ができる状態になっていること。
  • Authを用いたユーザ認証機能がすでに付与されていること。

前提情報

読後感

  • LaravelのAuth認証にメールアドレスの確認処理を付与することができる。
  • ユーザ認証情報入力→メール受信→メール内のURLから初回ログインをしてもらう。

概要

  1. .envの修正
  2. モデルファイルの修正
  3. ルーティング情報の修正
  4. コントローラファイルの修正
  5. 確認

詳細

  1. .envの修正

    1. アプリ名ディレクトリで下記コマンドを実行して.envファイルを開く。

      $ vi .env
      
    2. MAIL_MAILERの設定を下記の様に修正する。メール送信は行われず、情報がLaravelのログに出力される。

      アプリ名ディレクトリ/.env
      MAIL_MAILER=log
      
    3. 下記に修正後の.envファイルの全体の内容を記載する。

      アプリ名ディレクトリ/.env
      APP_NAME=Laravel
      APP_ENV=local
      APP_KEY=アプリキーの記載は各個人で異なります。
      APP_DEBUG=true
      APP_URL=http://localhost
      
      LOG_CHANNEL=stack
      
      DB_CONNECTION=mysql
      DB_HOST=127.0.0.1
      DB_PORT=3306
      DB_DATABASE=DB名
      DB_USERNAME=root
      DB_PASSWORD=皆さんの環境のMySQLのrootユーザのパスワード
      
      BROADCAST_DRIVER=log
      CACHE_DRIVER=file
      QUEUE_CONNECTION=sync
      SESSION_DRIVER=file
      SESSION_LIFETIME=120
      
      REDIS_HOST=127.0.0.1
      REDIS_PASSWORD=null
      REDIS_PORT=6379
      
      MAIL_MAILER=log
      MAIL_HOST=smtp.mailtrap.io
      MAIL_PORT=2525
      MAIL_USERNAME=null
      MAIL_PASSWORD=null
      MAIL_ENCRYPTION=null
      MAIL_FROM_ADDRESS=null
      MAIL_FROM_NAME="${APP_NAME}"
      
      AWS_ACCESS_KEY_ID=
      AWS_SECRET_ACCESS_KEY=
      AWS_DEFAULT_REGION=us-east-1
      AWS_BUCKET=
      
      PUSHER_APP_ID=
      PUSHER_APP_KEY=
      PUSHER_APP_SECRET=
      PUSHER_APP_CLUSTER=mt1
      
      MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
      MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
      
  2. モデルファイルの修正

    1. アプリ名ディレクトリで下記コマンドを実行してusersテーブルにリンクするモデルファイルを開く。

      $ vi app/User.php
      
    2. 下記の様に修正する。

      アプリ名ディレクトリ/app/User.php
      <?php
      
      namespace App;
      
      // 下記を修正する
      //use Illuminate\Contracts\Auth\MustVerifyEmail;
      use Illuminate\Contracts\Auth\MustVerifyEmail as MustVerifyEmailContract;
      use Illuminate\Auth\MustVerifyEmail;
      // 上記までを修正する
      use Illuminate\Foundation\Auth\User as Authenticatable;
      use Illuminate\Notifications\Notifiable;
      
      // 下記を修正する
      //class User extends Authenticatable
      class User extends Authenticatable implements MustVerifyEmailContract
      {
          // 下記を修正する
          //use Notifiable;
          use MustVerifyEmail, Notifiable;
      
          /**
           * The attributes that are mass assignable.
           *
           * @var array
           */
          protected $fillable = [
              'name', 'email', 'password',
          ];
      
          /**
           * The attributes that should be hidden for arrays.
           *
           * @var array
           */
          protected $hidden = [
              'password', 'remember_token',
          ];
      
          /**
           * The attributes that should be cast to native types.
           *
           * @var array
           */
          protected $casts = [
              'email_verified_at' => 'datetime',
          ];
      }
      
      
  3. ルーティング情報の修正

    1. アプリ名ディレクトリで下記コマンドを実行してルーティングファイルを開く。

      $ vi routes/web.php
      
    2. 下記の様に修正を行う。

      アプリ名ディレクトリ/routes/web.php
      <?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();
      Auth::routes(['verify' => true]);
      
  4. コントローラファイルの修正

    1. アプリ名ディレクトリで下記コマンドを実行してコントローラファイルを開く。

      $ vi app/Http/Controllers/HomeController.php
      
    2. 下記の様に修正を行う。

      アプリ名ディレクトリ/app/Http/Controllers/HomeController.php
      <?php
      
      namespace App\Http\Controllers;
      
      use Illuminate\Http\Request;
      
      class HomeController extends Controller
      {
          /**
           * Create a new controller instance.
           *
           * @return void
           */
          public function __construct()
          {
              // 下記を修正する
              //$this->middleware('auth');
              $this->middleware('verified');
          }
      
          /**
           * Show the application dashboard.
           *
           * @return \Illuminate\Contracts\Support\Renderable
           */
          public function index()
          {
              return view('home');
          }
      }
      
  5. 確認

    1. アプリ名ディレクトリで下記コマンドを実行してローカルサーバを起動する。

      $ php artisan serve
      
    2. 下記リンクにアクセスする。

    3. 下記のページの右上の「REGISTER」をクリックする。

      Laravel-12.png

    4. 任意の情報を入力し「Register」をクリックする。

      Laravel-13.png

    5. 下記画面が表示されることを確認する。

      Laravel-14.png

    6. アプリ名ディレクトリで下記コマンドを実行してLaravelのログファイルを開く。

      $ vi storage/logs/laravel.log
      
    7. 開いたログファイルの最終行付近に下記の様な記載があることを確認する。

      アプリ名ディレクトリ/storage/logs/laravel.log
      [Laravel](http://localhost)
      
      # Hello!
      
      Please click the button below to verify your email address.
      
      Verify Email Address: http://127.0.0.1:8000/email/verify/6/1107c7572edff3d7050bd4c68404ff6d0919d508?expires=1598068938&signature=a52ea1e97b368ac4b68dd2183cd3ff84d121b608556673e01c6a1aa2a5715161
      
      If you did not create an account, no further action is required.
      
      Regards,
      Laravel
      
      If you’re having trouble clicking the "Verify Email Address" button, copy and paste the URL below
      into your web browser: [http://127.0.0.1:8000/email/verify/6/1107c7572edff3d7050bd4c68404ff6d0919d508?expires=1598068938&signature=a52ea1e97b368ac4b68dd2183cd3ff84d121b608556673e01c6a1aa2a5715161](http://127.0.0.1:8000/email/verify/6/1107c7572edff3d7050bd4c68404ff6d0919d508?expires=1598068938&signature=a52ea1e97b368ac4b68dd2183cd3ff84d121b608556673e01c6a1aa2a5715161)
      
      © 2020 Laravel. All rights reserved.
      
    8. みなさんのlogファイルの「Verify Email Address」の後に書かれたURLにアクセスする。(本記事のリンクは筆者の環境の物なのでアクセスしても正常な処理にならない。)

    9. 下記の様なページが表示されたらメールアドレスの確認が完了し初回ログインも完了である。

      Laravel-16.png

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む