38
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

基礎的なLaravelスキルを確かめるサンプルプロジェクト

Last updated at Posted at 2018-06-25

「戸田奈津子訳」タグ付与に対する説明と対応

「戸田奈津子訳」のTwitter検索結果を確認しました。Qiita記事執筆時の和訳/翻訳を他者の名を借りて行うことに対する問題提起だと私は理解しました。
私自身が本記事に当該タグを付与した理由は下記の通りです。
@rana_kualuさんの有益な和訳/翻訳記事に刺激を受けたこと
→これは各記事のいいね数を一目瞭然だと思いますが、@rana_kualuさんがプログラマ、エンジニアにとって有益な英語情報をコンスタントに日本語訳してくれており、大変勉強になっております。contribution数の通り、「戸田奈津子訳」タグを用いて精力的に情報発信をされている方です。また、私が記事を読む限り戸田奈津子さんを侮辱するような表現はなく、「良質な英語情報を日本に広める役割の一端を担っている」という意味では映画、技術という分野を超えた志を感じました。
②私自身が映画好きであること
→何よりも私自身が映画好きです。海外映画はよほどの事情がなければ字幕で見ており、エンドロールが流れ終わった後に誰が字幕を担当したのか判明する瞬間は、映画鑑賞における一種の楽しみとなっています。字幕担当者によって映画の感想が変わるということはなく、松浦美奈さんであってもアンゼたかしさんであっても戸田奈津子さんであっても仕事ぶりに感謝するのみです。
③「和訳」「意訳」「翻訳」タグよりクリックしてもらいやすいタグだと思ったこと
→字幕翻訳家としての戸田奈津子さんは熱烈な映画ファンでなくとも有名人だと認識しています。上記②のような背景があった上で、知名度にあやかろうという下心や閲覧者にとってフックになるだろうという気持ちが正直ありました。私自身が戸田さんの翻訳に対して違和感を持ったことはなく(そもそもの英語リスニング力や映画特有の背景知識が不足していることもありますが、、、)、本記事においても嘲笑する意図も表現もないものの、同業の方から厳しい評判があるのも知った上で利用しようとした意図は間違いなくあります。

上記のようなタグ運用をした結果、本当の戸田奈津子さんの訳だと誤認する戸田さんに対する厳しい批評を想起して不愉快になる、といった意見をTwitter上でうかがいました。
これらの意見に関してはまさしくご指摘の通りでした。また、少なからずそういった方がいるかもしれないと思いながらもPV目当てであえて付与していました。そういった経緯も含めてご迷惑をおかけした方々、戸田奈津子さん含めて不愉快な思いをした方々に謝罪をしたいと思います。申し訳ありませんでした。私の配慮が欠けておりました。

対応として本記事より「戸田奈津子訳」タグを外して「和訳」「翻訳」「日本語訳」というタグを付与しました。
改めて本記事を読んでくださると大変ありがたく思います。

選手よりも速く走るペースメーカー

Laravelの学習が2周目に入った。知識が曖昧な部分や学んだ記憶が無い分野、新しい概念などあまりにもヌケモレだらけだったので、会社の新卒向けLaravel研修に潜り込み、新卒の人たちをペースメーカーにして学び直している。

POVILAS KOROP氏との出会い

ある程度Laravel用語が頭に入ってきたので、英語の記事も少しずつ読めるようになってきた。やはり英語記事の方が情報量は多いんだなと思いつつググっていたら「How to Test Junior Laravel Developer Skills: Sample Project」という記事を見つけた。今の自分の状況にちょうど良さそうなタイトルと中身だったので、知識整理やチートシート作成がてら取り組んでみることにした。POVILAS KOROP氏のことは発音が良くわからなかったので、私はリスペクトの意味を込めて**「ポッコロさん」**と呼んでいる。記事自体は全文英語なので、適宜私の和訳センスが発揮される。

サンプルプロジェクトを通してひよっこLaravelプログラマーのスキルを確かめる

ポッコロさん曰く、このSample Projectを通じてひよっこLaravelプログラマーの下記の知識、実装能力をテストできるらしい。

  • MVC
  • Auth
  • CRUD and Resource Controllers
  • Eloquent and Relationships
  • Database migrations and seeds
  • Form Validation and Requests
  • File management
  • Basic Bootstrap front-end
  • Pagination

サンプルプロジェクトの概要

会社と従業員の管理アプリケーション(Adminpanel to manage companies)を作る。要件は下記の通り。

  • Basic Laravel Auth: ability to log in as administrator
    管理者としてログインできるようにする。
  • Use database seeds to create the first user with email admin@admin.com and password “password”
    管理者をdatabase seederを使って指定の通りに作成する
  • CRUD functionality (Create / Read / Update / Delete) for two menu items: Companies and Employees.
    Companies(会社)とEmployees(従業員)の2つのメニューを作り、CRUD機能を実装する
  • Companies DB table consists of these fields: Name (required), email, logo (minimum 100×100), website
    Companiesテーブルは4つのカラムで構成される
  • Employees DB table consists of these fields: First name (required), last name (required), Company (foreign key to Companies), email, phone
    Employeesテーブルは5つのカラムで構成される。後から気づいたのだが、emailとphoneカラムを作成し忘れていた。。。
  • Use database migrations to create those schemas above
    上記のテーブルを作るためにマイグレーションを実行する
  • Store companies logos in storage/app/public folder and make them accessible from public
    companyのロゴ画像を指定のフォルダに保存して表示できるようにする
  • Use basic Laravel resource controllers with default methods – index, create, store etc.
    コントローラーはちゃんと使うこと
  • Use Laravel’s validation function, using Request classes
    バリデーション機能を実装すること
  • Use Laravel’s pagination for showing Companies/Employees list, 10 entries per page
    ページネーションを実装すること
  • Use Laravel make:auth as default Bootstrap-based design theme, but remove ability to register
    ユーザー登録機能は使えないようにする(make:authすると自動で作成されてしまう)。Bootstrapを使う。

実装手順

プロジェクトの準備

プロジェクトを作成する

composer create-project --prefer-dist laravel/laravel pokkoro02 "5.5.*"

表示を確認する

php artisan serve
実行後は下記ににアクセスして表示を確認する
http://127.0.0.1:8000

DBを作成する

各々の環境で今回のためのDBを作成する

.envでDBとの接続の設定をする
 DB_CONNECTION=mysql
 DB_HOST=127.0.0.1
 DB_PORT=3306
 DB_DATABASE=pokkoro02
 DB_USERNAME=root
 DB_PASSWORD=
app.phpでタイムゾーンを日本にする
app.php
 'timezone' => 'UTC',
 
 'timezone' => 'Asia/Tokyo',

model, migration, seeder, factory, authの定義や作成

モデルファイルとマイグレーションファイルの作成。

「-m」オプションをつければ2つのファイルを同時に作れる。今回はCompany.phpとEmployee.phpの2つを作る。
php artisan make:model Company -m
php artisan make:model Employee -m

マイグレーションファイルでカラムの定義をする
companiesのマイグレーションファイル.php
 public function up()
 {
     Schema::create('companies', function (Blueprint $table) {
         $table->increments('id');
         $table->string('name');
         $table->string('email')->nullable();
         $table->binary('logo')->nullable();
         $table->string('website')->nullable();
         $table->timestamps();
     });
 }
employeesのマイグレーションファイル.php
 public function up()
 {
     Schema::create('employees', function (Blueprint $table) {
         $table->increments('id');
         $table->string('first_name');
         $table->string('last_name');
         $table->integer('company_id')->unsigned();
         $table->timestamps();
 		// 外部キーの設定
         $table->foreign('company_id')->references('id')->on('companies');
     });
 }
モデル間のリレーションの定義をする
Company.php
 class Company extends Model
 {
     protected $table = 'companies';
 
     protected $guarded = array('id');
 
     public $timestamps = true;
 
     protected $fillable = [
     	'name', 'email', 'website', 'created_at', 'updated_at'
     ];
 
     public function employees()
     {
         return $this->hasMany('App\Employee');
     }
 }
Employee.php
 class Employee extends Model
 {
     protected $table = 'employees';
 
     protected $guarded = array('id');
 
     public $timestamps = true;
 
     protected $fillable = [
     	'first_name', 'last_name', 'company_id', 'email', 'website', 'created_at', 'updated_at'
     ];
 
     public function company()
     {
     	return $this->belongsTo('App\Company');
     }
 }
各テーブルのSeeder, Factoryファイルを作成してダミーデータ作成の準備をする

Seederファイルの作成
php artisan make:seeder UsersTableSeeder
php artisan make:seeder CompaniesTableSeeder
php artisan make:seeder EmployeesTableSeeder

DatabaseSeeder.phpで使用するSeederファイルを指定する

DatabaseSeeder.php
 $this->call([
 	UsersTableSeeder::class,
 	CompaniesTableSeeder::class,
 	EmployeesTableSeeder::class,
 ]);

UsersTableSeeder.phpで管理権限を有するユーザーを1名定義する

UsersTableSeeder.php
 public function run()
 {
     // adminユーザーを定義
     App\User::create([
     	'name' => 'admin',
     	'email' => 'admin@admin.com',
     	'password' => Hash::make('password'),
     	'remember_token' => str_random(10),
     ]);
 }

CompaniesTableSeeder.phpでEmployeesTableSeederとリレーション含めた作成コードを記述する

CompaniesTableSeeder.php
 public function run()
 {
     factory(App\Company::class, 35)->create()->each(function ($company) {
     	factory(App\Employee::class, 10)->create(['company_id' => $company->id]);
     });
 }

Factoryファイルの作成
php artisan make:factory CompanyFactory
php artisan make:factory EmployeeFactory

CompanyFactory.phpでダミーデータの定義をする

CompanyFactory.php
 $factory->define(App\Company::class, function (Faker $faker) {
     return [
         'name' => $faker->company,
         'email' => $faker->unique()->safeEmail,
         'website' => $faker->url,
     ];
 });

EmployeeFactory.phpは、下記の通り定義する

EmployeeFactory.php
 $factory->define(App\Employee::class, function (Faker $faker) {
     return [
         'first_name' => $faker->firstNameFemale,
         'last_name'  => $faker->lastName,
         // company_idは外部キー
         'company_id' => function() {
         	return factory(App\Company::class)->create()->id;
         }
     ];
 });

一通り定義が完了したので--seedオプションを指定して実行する。
php artisan migrate:refresh --seed
コマンド実行後にデータを確認する。

認証機能を作成するためmake:authコマンドを実行する
php artisan make:auth

コントローラーの作成とルーティングの定義

コントローラーの作成

--resourceオプションを使う
php artisan make:controller CompanyController --resource
php artisan make:controller EmployeeController --resource

ルーティングの定義
web.php
 Route::resource('companies', 'CompanyController');
 Route::resource('employees', 'EmployeeController');

想定通りのルーティングがされたかどうか下記コマンドで確認する
php artisan route:list

CRUD処理の実装とビューファイルの作成

Bootstrap4を使えるようにする

この記事を参考にして進めた。

app.blade.phpの編集

コメント部分のみ修正を加えた。header部分にcompanies/indexとemployees/indexのリンクを張り、ユーザー登録機能部分を削除した。

layouts/app.blade.php
 <div class="collapse navbar-collapse" id="app-navbar-collapse">
     <!-- Left Side Of Navbar -->
     <ul class="nav navbar-nav navbar-left">
     	// ログイン中のユーザーにはcompaniesとemployeesのindex画面が表示されるようにする
         @guest
         @else
             <li><a href="{{ route('companies.index') }}">Company</a></li>
             <li><a href="{{ route('employees.index') }}">Employee</a></li>
         @endguest
     </ul>
 
     <!-- Right Side Of Navbar -->
     <ul class="nav navbar-nav navbar-right">
         <!-- Authentication Links -->
         @guest
             <li><a href="{{ route('login') }}">Login</a></li>
             // 登録機能を削除(今回の要件)
             {{-- <li><a href="{{ route('register') }}">Register</a></li> --}}
         @else
             <li class="dropdown">
                 <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" aria-haspopup="true" v-pre>
                     {{ Auth::user()->name }} <span class="caret"></span>
                 </a>
 
                 <ul class="dropdown-menu">
                     <li>
                         <a href="{{ route('logout') }}"
                             onclick="event.preventDefault();
                                      document.getElementById('logout-form').submit();">
                             Logout
                         </a>
 
                         <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                             {{ csrf_field() }}
                         </form>
                     </li>
                 </ul>
             </li>
         @endguest
     </ul>
 </div>
companyの一覧機能を作る
CompanyController.php
 public function index()
 {
     $companies = Company::all();
 
     return view('companies.index', [
         'companies' => $companies,
     ]);
 }
companies/index.blade.php
 @extends('layouts.app')
 
 @section('content')
 <div class="container">
     <div class="row">
         <div class="col-md-8 col-md-offset-2">
         	// 新規作成ボタンを追加する箇所
             <div class="panel panel-default">
                 <div class="panel-heading">Companies一覧</div>
                 <table class="table table-striped panel-body">
                     <thead>
                     <tr>
                         <th>FirstName</th>
                         <th>LastName</th>
                         <th>CompanyID</th>
                         <th width="100">&nbsp;</th>
                         <th width="100">&nbsp;</th>
                     </tr>
                     </thead>
                     <tbody>
                     @foreach ($employees as $employee)	
 	                    <tr>
 		                        <td>{{ $employee->first_name }}</td>
 		                        <td>{{ $employee->last_name }}</td>
 		                        <td>{{ $employee->company_id }}</td>
 		                        <td>編集ボタンを記述する場所</td>
 		                        <td>削除ボタンを記述する場所</td>
 	                    </tr>
                     @endforeach
                     </tbody>
                 </table>
             </div>
         </div>
     </div>
 </div>
 @endsection
companyの追加機能を作る
CompanyController.php
 public function create()
 {
     return view('/companies/create');
 }
companies/create.blade.php
 @extends('layouts.app')
 
 @section('content')
 <div class="container">
 	<div class="row">
 		<div class="col-md-8 col-md-offset-2">
 			@if ($errors->any())
 				<div class="alert alert-danger">
 					<ul>
      					// エラーの表示箇所
 						@foreach ($errors->all() as $error)
 							<li>{{ $error }}</li>
 						@endforeach
 					</ul>
 				</div>
 			@endif
 			<h2>Company登録</h2>
    			// データの保存処理をするformを作成する。
 			<form action="{{ route('companies.store') }}" method="POST">
 				{{ csrf_field() }}
 
 				// バリデーションの結果弾かれてきた値を表示するoldメソッド
 				<div class="form-group">
 					<label for="company-name">CompanyName</label>
 					<input type="text" name="name" class="form-control" value="{{ old('name') }}">
 				</div>
 
 				<div class="form-group">
 					<label for="company-email">CompanyEmail</label>
 					<input type="text" name="email" class="form-control" value="{{ old('email') }}">
 				</div>
 
 				<div class="form-group">
 					<label for="company-website">CompanyWebsite</label>
 					<input type="text" name="website" class="form-control" value="{{ old('website') }}">
 				</div>
 
 				<button type="submit" class="btn btn-primary">Submit</button>
 			</form>
 		</div>
 	</div>
 </div>
 @endsection
CompanyController.php
 public function store(Request $request)
 {
 	// バリデーション処理
     $request->validate([
         'name'    => 'required',
         'email'   => 'required',
         'website' => 'required',
     ]);
 
     $company = new Company;
     $company->name    = $request->name;
     $company->email   = $request->email;
     $company->website = $request->website;
 
     $company->save();
 
     return redirect('companies');
 }
companyの編集機能を作る

<編集ボタンを記述する場所>の箇所を下記の通り変更する。

companies/index.blade.php
 <td>
 	<form action="{{ route('companies.edit', ['id' => $company->id]) }}" method="GET">
 		{{ csrf_field() }}
 		<button type="submit" class="btn btn-sm btn-success">
 			<i class="fa fa-edit"></i> 編集
 		</button>
 	</form>
 </td>
CompanyController.php
 public function edit($id)
 {
 	// 編集対象のcompany情報をformに表示させておくためにデータを取得する
     $company = Company::findOrFail($id);
     return view('companies.edit', ['company' => $company]);
 }
companiex/edit.blade.php
 @extends('layouts.app')
 
 @section('content')
 <div class="container">
 	<div class="row">
 		<div class="col-md-8 col-md-offset-2">
 			@if ($errors->any())
 				<div class="alert alert-danger">
 					<ul>
 						@foreach ($errors->all() as $error)
 							<li>{{ $error }}</li>
 						@endforeach
 					</ul>
 				</div>
 			@endif
 			<h2>Company登録</h2>
    			// 現在のcompany_idをupdateアクションに送るformを作成。update処理なのでPATCHメソッドを追記する
 			<form action="{{ route('companies.update', ['id' => $company->id]) }}" method="POST">
 				{{ csrf_field() }}
 				{{ method_field('PATCH') }}
 
 				// CompanyControllerのeditアクションで送られてきたcompanyデータを表示する
 				<div class="form-group">
 					<label for="company-name">CompanyName</label>
 					<input type="text" name="name" class="form-control" value="{{ $company->name }}">
 				</div>
 
 				<div class="form-group">
 					<label for="company-email">CompanyEmail</label>
 					<input type="text" name="email" class="form-control" value="{{ $company->email }}">
 				</div>
 
 				<div class="form-group">
 					<label for="company-website">CompanyWebsite</label>
 					<input type="text" name="website" class="form-control" value="{{ $company->website }}">
 				</div>
 
 				<button type="submit" class="btn btn-primary">Submit</button>
 			</form>
 		</div>
 	</div>
 </div>
 @endsection
CompanyController.php
 public function update(Request $request, $id)
 {
     $request->validate([
         'name'    => 'required',
         'email'   => 'required',
         'website' => 'required',
     ]);
 
     $company = Company::findOrFail($id);
     
     $company->name    = $request->name;
     $company->email   = $request->email;
     $company->website = $request->website;
 
     $company->save();
 
     return redirect('companies');
 }
companyの削除機能を作る

<削除ボタンを記述する場所>の箇所を下記の通り変更する。

companies/index.blade.php
 <td>
 	// method_filedでDELETEを指定する
 	<form action="{{ route('companies.destroy', ['id' => $company->id]) }}" method="POST">
  		{{ csrf_field() }}
    		{{ method_field('DELETE') }}
     	
      	<button type="submit" class="btn btn-sm btn-danger">
       		<i class="fa fa-trash"></i> 削除
         </button>
      </form>
 </td>
CompanyController.php
 public function destroy($id)
 {
     $company = Company::findOrFail($id);
     $company->delete();
 
     return redirect('companies');
 }

これで削除ができると思ったらリレーションのエラー(親レコードを削除する際に子レコードの扱いをどうするか)が発生したため、マイグレーションファイルで外部キーの削除に関して指定する。Laravel 5.5 データベース:マイグレーションを参照。

子モデル(Employees)のマイグレーションファイル.php
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');

定義を変更したら再度migrateを実行する
php artisan migrate:refresh --seed

以上でcompanyの一通りのCRUD処理は完了。Employeesでも同じ流れのため省略するが、外部キーの保存(store)と更新(update)処理の箇所だけ追加の処理が必要だったため下記で追記する。
Laravel 5.5 Eloquent:リレーション参照。

EmployeeController.php

 public function store(Request $request)
 {
     $request->validate([
         'first_name' => 'required',
         'last_name'  => 'required',
         'company_id' => 'required',
     ]);
 
     $employee = new Employee;
     $employee->first_name = $request->first_name;
     $employee->last_name = $request->last_name;
 
 	// 保存対象の$employeeが所属する$companyを定義する
     $company = Company::findOrFail($request->company_id);
     $company->employees()->save($employee);
 
     return redirect('employees');
 }

このままだとログインしていなくてもCRUDできてしまうので、authのmiddlewareをかける。

app.php
 Route::middleware(['auth'])->group(function() {
 	Route::resource('companies', 'CompanyController');
 	Route::resource('employees', 'EmployeeController');
 });

よりAdvancedなひよっこLaravelプログラマーになるために

ポッコロさん曰く、下記の機能を実装できるとさらにイケてるプログラマーになれるらしい。今後、本記事のサンプルプロジェクトをベースに機能を随時追加していく。

  • Use Datatables.net library to show table – with our without server-side rendering
    ちょっと何言ってるかわからない(サンドウィッチマン富澤風)
  • Use more complicated front-end theme like AdminLTE
    AdminLTEのようなフロントエンドテーマを使ってみる
  • Email notification: send email whenever new company is entered (use Mailgun or Mailtrap)
    メール通知機能を作る
  • Make the project multi-language (using resources/lang folder)
    多言語対応する
  • Basic testing with phpunit (I know some would argue it should be the basics, but I disagree)
    phpunitを使った基本的なテスト機能を作る

また、下記の2店の実装を今回できなかったので、引き続き取り組む。

  • accessible from public
    companyのロゴ画像を指定のフォルダに保存して表示できるようにする
  • Use Laravel’s pagination for showing Companies/Employees list, 10 entries per page
    ページネーションを実装すること
38
52
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?