컨트롤러와 같은 클래스에서 바인딩한 인터페이스를 타입힌트로 지정하면 클로저에 정의한 구현체가 자동으로 의존성 주입됨
예) PostController.php 생성자에 Transistor 인터페이스 타입힌트를 지정하면 구현체인 PodcastParser::class 주입
# AppServiceProvider.php
use App\Services\Transistor;
use App\Services\PodcastParser;
...
public function register()
{
//
$this->app->bind(Transistor::class, function ($app) {
return new PodcastParser();
});
}
...
# PostController.php
use App\Services\Transistor;
use App\Services\PodcastParser;
public function __construct(Transistor $transistor)
{
$this->transistor = $transistor;
}
public function articles()
{
return $this->hasMany(Article::class);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
Article 모델 :
protected $fillable = ['user_id', 'title', 'content'];
public function user()
{
return $this->belongsTo(User::class);
}
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
Comment 모델:
protected $fillable = ['content', 'user_id', 'commentable_type', 'commentable_id', 'parent_id'];
public function commentable()
{
return $this->morphTo();
}
public function parent()
{
return $this->belongsTo(Comment::class, 'parent_id');
}
public function replies()
{
return $this->hasMany(Comment::class, 'parent_id');
}
public function article()
{
return $this->belongsTo(Article::class, 'commentable_id');
}
public function user()
{
return $this->belongsTo(User::class);
}
엘로퀀트는 comments 테이블의 외래키를 comment_id로 자동으로 추정하기 때문에 메서드 인자에 ‘parent_id’를 전달해서 외래키를 지정했다.
마찬가지로 ‘commentable_type’, ‘commentabe_id’를 지정하지 않는 이유는 위와 같다.
‘paretn_id’ 는 null을 허용한다. 이 값이 지정되어 있지 않으면 최상위 댓글로 간주한다. ‘CommentFactory’ 에는 이 값을 비워두어 최상위 댓글을 생성하게 한다. 후에 시더에서 make 메서드에 ‘parent_id’ 를 전달해 계층적인 댓글 구조를 만들어 준다.
3.시더작성
UsersTableSeeder :
public function run()
{
factory(App\User::class, 10)->create()->each(function($user){
$user->articles()->createMany(factory(App\Article::class, 3)->make()->toArray());
});
}
CommentsTableSeeder :
public function run()
{
$faker = Faker\Factory::create();
$articles = App\Article::all();
//최상위 댓글 생성
$articles->each(function($article){
$article->comments()->createMany(factory(App\Comment::class, 3)->make()->toArray());
});
//계층적 댓글 생성
$articles->each(function($article) use ($faker){
for($i=0; $i<10; $i++)
{
$commentIds = $article->comments()->pluck('id')->toArray();
$article->comments()->create(factory(App\Comment::class)->make(
['parent_id' => $faker->randomElement($commentIds)]
)->toArray());
}
});
}
계층적 댓글 생성 부분에서 make 메서드를 보면 ‘CommentFactory’에서 지정하지 않은 ‘parent_id’값을 전달해 줌으로써 재귀적인 댓글 구조를 가질수 있게 한다.
아티즌 명령어를 통해 시더를 생성한다. 생성된 클래스는 database/seeds에 생성 된다.
php artisan make:seeder UsersTableSeeder
시더파일을 정의하자
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class UsersTableSeeder extends Seeder
{
public function run()
{
DB::table('users')->insert([
'name' => Str::random(10),
'email' => Str::random(10).'@gmail.com',
'password' => bcrypt('password'),
]);
}
}
이제 시더를 실행하게되면 데이터베이스에 내용이 입력이 될 것이다.
php artisan db:seed --class=UsersTableSeeder
–class 옵션을 붙여주기 귀찮다면 DatabaseSeeder 에 시더를 등록해준다.
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run()
{
$this->call(UsersTableSeeder::class);
}
}
php artisan db:seed 실행해보면 데이터가 잘 들어가는 것을 확인할 수 있다. 마이그레이션에 –seed 옵션을 사용하면 편리하다.
php artisan migrate:refresh --seed
2. Eloquent 모델팩토리 사용하기
시더에 쿼리빌더를 통해 입력하는 방법보다 모델팩토리를 생성해서 시딩하는게 편리하다. 모델팩토리 생성 커맨드를 입력하자.
php artisan make:factory UserFactory
위에서 생성한 UsersTableSeeder, UserFactory는 라라벨에 이미 생성되어 있기때문에 생성할 필요가 없다. 설명을 위해서 설명하는것 뿐이다.
이제 시더클래스에 생성한 팩토리를 사용해보자.
factory(App\User::class, 50)->create();
기존 내용을 삭제하고 시더를 사용해서 새로운 데이터를 넣어보자
php artisan migrate:refresh --seed
3. 관계모델일때 시더정의하기
일대다인 모델을 정의하자.
namespace App;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use Notifiable;
protected $fillable = [
'name', 'email', 'password',
];
protected $hidden = [
'password', 'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
];
public function articles(){
return $this->hasMany('App\Article');
}
}
namespace App;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
protected $fillable = [
'subject', 'content',
];
public function user(){
return $this->belongsTo('App\User');
}
}
use App\Article;
use Faker\Generator as Faker;
$factory->define(Article::class, function (Faker $faker) {
return [
'subject' => $faker->title,
'content' => $faker->paragraph,
'user_id' => factory(App\User::class),
];
});
이제 UsersTableSeeder 클래스를 수정해주자.
public function run()
{
factory(App\User::class, 50)->create()->each(function($user){
$user->articles()->save(factory(App\Article::class)->make());
});
}
마이그레이션을 해보자.
php artisan migrate:refresh --seed
데이터베이스를 확인해보면 user 테이블과 aticles 테이블 관계에 따라 50개의 레코드가 생성된 것을 확인할 수 있다.
resource/views 아래에 admin 폴더를 생성한다. resource/views/auth 폴더를 복사해 resource/views/admin 아래에 붙여 넣는다. resource/views/home.blade.php 복사해 resource/views/admin 아래에 붙여 넣는다.
(3) 구현하기
Admin 컨트롤러 각각의 파일을 열어 상단에 정의 되어있는 네임스페이스 정의를 변경해준다.
namespace App\Http\Controllers\Admin;
네임스페이스를 변경했으면 각 컨트롤러의 메소드 및 사용하고 있는 트레이트 메서드를 목적에 맞게 오버라이드 한다.
로그인 기능 구현
#Admin/LoginController.php
#Auth 파사드 추가
use Illuminate\Support\Facades\Auth;
protected $redirectTo = 'admin/home';
public function __construct()
{
#미들웨어 변경, admin 파라미터 전달
$this->middleware('admin.guest:admin')->except('logout');
}
#AuthenticatesUsers 트레이트 메서드 오버라이드
public function showLoginForm()
{
return view('admin.auth.login');
}
#가드지정
protected function guard()
{
return Auth::guard('admin');
}
#admin/login.blade.php
#라우트 변경
<form method="POST" action="{{ route('admin.login') }}">
회원가입
#Admin/RegisterController.php
#User 모델대신 Admin 모델을 사용
use App\Admin;
#Auth파사드 추가
use Illuminate\Support\Facades\Auth;
protected $redirectTo = '/admin/home';
public function __construct()
{
$this->middleware('admin.guest:admin');
}
public function showRegistrationForm()
{
return view('admin.auth.register');
}
protected function validator(array $data)
{
#unique 속성에 파라미터 admins(테이블명) 전달
return Validator::make($data, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:admins'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
}
protected function create(array $data)
{
return Admin::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
}
protected function guard()
{
return Auth::guard('admin');
}
#admin/register.blade.php
#라우트 변경
<form method="POST" action="{{ route('admin.register') }}">
#Admin/HomeController.php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class HomeController extends Controller
{
public function __construct()
{
$this->middleware('admin.auth:admin');
}
public function index()
{
return view('admin.home');
}
}
비밀번호 확인
라라벨은 특정영역에 접근하기전에 비밀번호를 확인하도록 요청할 수 있는 기능이 있다.
#Admin/ConfirmPasswordController.php
protected $redirectTo = '/admin/home';
public function __construct()
{
#미들웨어에 파라미터를 전달해 가드를 지정
$this->middleware('admin.auth:admin');
}
public function showConfirmForm()
{
return view('admin.auth.passwords.confirm');
}
라라벨은 기본적으로 이메일로 인증주소를 받아 비밀번호를 초기화 시킨다. 이메일 설정이 되어있지 않으면 메일형식을 로그로 변경해주자.
# .env
MAIL_MAILER=log
비밀번호를 초기화하는 과정은 아래와 같다.
admin/password/reset 페이지 접근
사용자 정보가 있으면 비밀번호 초기화페이지로 이동 없으면 로그인
이메일을 제출하면 해당 주소로 인증주소를 보낸다.
인증주소로 접속하여 비밀번호를 변경
이제 컨트롤러와 뷰를 수정하자
#admin/ForgotPasswordController.php
#Password 파사드 추가
use Illuminate\Support\Facades\Password;
public function __construct()
{
$this->middleware('admin.auth:admin');
}
public function showLinkRequestForm()
{
return view('admin.auth.passwords.email');
}
public function broker()
{
return Password::broker('admin');
}
#admin/auth/passwords/email.blade.php
#라우트 변경
<form method="POST" action="{{ route('admin.password.email') }}">
storage/logs/laravel.log 파일을 열어 인증주소를 확인해보면 localhost:8000/password/reset/token?email=your@email.com와 같은 형식으로 나타나는데 이 주소는 라라벨의 리셋주소이다. 따라서 Admin전용 리셋주소로 정보를 받아야 한다.
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Lang;
use Illuminate\Auth\Notifications\ResetPassword as ResetPasswordNotification;
class AdminPasswordResetMailToken extends ResetPasswordNotification
{
use Queueable;
public function toMail($notifiable)
{
if (static::$toMailCallback) {
return call_user_func(static::$toMailCallback, $notifiable, $this->token);
}
if (static::$createUrlCallback) {
$url = call_user_func(static::$createUrlCallback, $notifiable, $this->token);
} else {
$url = url(config('app.url').route('admin.password.reset', [
'token' => $this->token,
'email' => $notifiable->getEmailForPasswordReset(),
], false));
}
return (new MailMessage)
->subject(Lang::get('Reset Password Notification'))
->line(Lang::get('You are receiving this email because we received a password reset request for your account.'))
->action(Lang::get('Reset Password'), $url)
->line(Lang::get('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')]))
->line(Lang::get('If you did not request a password reset, no further action is required.'));
}
}
패스워드 재설정은 Illuminate\Auth\Notifications\ResetPassword의 클래스를 기본으로 사용하기 때문에 ResetPassword 을 상속받아 AdminPasswordResetMailToken를 작성해준다.
#Admin.php
use App\Notifications\AdminPasswordResetMailToken;
public function sendPasswordResetNotification($token)
{
$this->notify(new AdminPasswordResetMailToken($token));
}
모델이 사용하는 Illuminate\Foundation\Auth\User 의 클래스에 지정되어 있는 CanResetPassword 트레이트의 메서드 sendPasswordResetNotification를 오버라이드한다.
이제 이메일주소를 입력하고 인증주소를 확인해보면 원하는 주소로 변경되었음을 확인 할 수 있다.
인증메일부분을 처리했으니 인증주소를 통해 넘어온 사용자에게 비밀번호를 받고 저장하는 부분을 처리해주자.
#Admin/ResetPasswordController.php
#Request, Auth, Password 파사드 추가
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Password;
protected $redirectTo = '/admin/home';
public function showResetForm(Request $request, $token = null)
{
return view('admin.auth.passwords.reset')->with(
['token' => $token, 'email' => $request->email]
);
}
protected function guard()
{
return Auth::guard('admin');
}
public function broker()
{
return Password::broker('admin');
}
#라우트 변경
<form method="POST" action="{{ route('admin.password.update') }}">
이메일 인증하기
웹사이트를 사용하기전에 이메일을 인증해야하는 경우가 있다. 인증정보는 eamil_verified_at 필드에 저장된다.
기본인증기능에서는 라우트에 verify 옵션을 전달하면 사용할 수 있다.
Auth::routes(['verify' => true]);
커스텀한 인증에서는 비밀번호 초기화처럼 추가적으로 개발해야 한다.
#admin/VerificationController.php
use Illuminate\Http\Request;
protected $redirectTo = '/admin/home';
public function __construct()
{
$this->middleware('admin.auth:admin');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
public function show(Request $request)
{
return $request->user()->hasVerifiedEmail()
? redirect($this->redirectPath())
: view('admin.auth.verify');
}
#admin/auth/verify.blade.php
#라우트 변경
<form class="d-inline" method="POST" action="{{ route('admin.verification.resend') }}">
컨트롤러와 뷰쪽은 끝. 이제 인증메일을 발송하는 부분을 수정하자.
notification 클래스 생성
php artisan make:notification AdminVerifyEmail
Illuminate\Auth\Notifications\VerifyEmail 는 기본인증에 사용되는 클래스이다. Illuminate\Auth\Notifications\VerifyEmail 클래스를 상속받아 필요한 부분만 다시 정의해준다.
namespace App\Notifications;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\URL;
use Illuminate\Auth\Notifications\VerifyEmail;
class AdminVerifyEmail extends VerifyEmail
{
protected function verificationUrl($notifiable)
{
return URL::temporarySignedRoute(
'admin.verification.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
'id' => $notifiable->getKey(),
'hash' => sha1($notifiable->getEmailForVerification()),
]
);
}
}
생성한 AdminVerifyEmail 클래스를 Admin 모델에 적용해준다.
#Admin.php
use App\Notifications\AdminVerifyEmail;
public function sendEmailVerificationNotification()
{
$this->notify(new AdminVerifyEmail);
}
[Composer\Downloader\TransportException]
The "https://packagist.org/p/provider-2020-01%24d05e2d9aae910523f4e326c5a73b6cc7b6d498492a6f115892377153193c21bd.js
on" file could not be downloaded (HTTP/1.1 404 Not Found)
테이블간 관계를 표현할 때 -테이블 이름_열이름_foreign 예)posts_user_id_foreign -이 이름은 외래키관계를 삭제할 때 사용한다. $table->dropForeign( posts_user_id_foreign );
외래 키 열 이름 및 메서드 이름 관례 관계가 ‘일(one)’ 이 되는 쪽 모델로 접근하는 메서드는 단수 ‘다(many)’가 되는 쪽은 복수로 사용한다.
app/Post.php
public function author(){
return $this->belongsTo(User::class, 'user_id');
}
app/User.php
public function posts(){
return $this->hasMany(Post::class, 'post_id')
}
피벗 테이블 이름 및 열 이름 관례 – 다대다로 연결하려는 두 테이블의 이름을 단수로 바꾸고 알파벳 순으로 연결한다. 연결자로는 밑줄(_)을 사용한다. – 외래키의 열 이름은 모델이름_id을 사용한다.
id
post_id
tag_id
1
1
1
2
1
2
마이그레이션 생성시 – create_, make_, add_, drop_, change_ 등으로 시작하고, _table로 끝난다. 예) create_posts_table
컨트롤러 – 파스칼 표기법을 사용하여 복수형에 Controller접미사를 붙인다. 예)PostsController
특별히 추가 할게 없다면 npm run dev 명령어를 실행해서 컴파일 하자. 앞에서 부트스트랩을 스캐폴딩했기 때문에 컴파일 하면 부트스트랩도 같이 컴파일 되어 public/js/app.js, public/css/app.css 로 위치하게 된다. (laravel-mix 문서 보기)
추가로 browsersyc를 webpack.mix.js에 추가해서 브라우저 변경시 새로고침없이 자동으로 리로드되게 해주면 개발할때 편리하다.
public function up()
{
Schema::create('articles', function (Blueprint $table) {
$table->bigIncrements('id');
$table->text('title');
$table->longText('content');
$table->timestamps();
$table->boolean('is_published')->nullable()->default(false);
});
}
public function down()
{
Schema::dropIfExists('articles');
}
//up 메서드는 migrate 명령어가 실행될 때 호출되고 down 메서드는 migrate:rollback 될 때 호출된다.
php artisan migrate 명령어를 실행해서 테이블을 생성하면된다. 이때 한번도 마이그레이션하지 않았으면 laravel에서 제공하는 테이블까지 함께 생성된다.
7. restful한 route 만들기
//인덱스 리스트
route::get('/article/', 'ArticleController@index')->name('article.index');
//글쓰기 폼
route::get('/article/create', 'ArticleController@create')->name('article.create');
//새로운글 저장
route::post('/article', 'ArticleController@store')->name('article.store');
//해당글 보기
route::get('/article/{id}', 'ArticleController@show')->name('article.show');
//해당글 수정 폼
route::get('/article/{id}/edit', 'ArticleController@edit')->name('article.edit');
//해당글 업데이트
route::put('/article/{id}', 'ArticleController@update')->name('article.update');
//해당글 삭제
route::delete('/article/{id}/destroy', 'ArticleController@destroy')->name('article.destroy');
route파사드의 name메서드를 사용하는 편이 나중에 url 관리하기가 편하다.
8.view 생성하기 /resources/views 디렉토리에 이번프로젝트에 뼈대가 될 layout.blade.php 파일을 생성한다.
form안에 @csrf 를 사용해서 csrf 토큰을 꼭 생성해야 한다. validation처리를 위해서 @error, 글로벌 헬퍼함수 old() 부분을 잘 확인하자. old()는 넘어온값을 세션에 저장해 두는데 validation에 통과하지 못했을 때 양식에 이전에 입력한 값을 다시 넣어주기 위해 사용한다.
2)저장하기 ArticleController@store
use App\Article;
public function store()
{
request()->validate([
'title' => 'required',
'content' => 'required'
]);
$article = new Article();
$article->title = request('title');
$article->content= request('content');
$article->save();
return redirect()->route('article.index');
}
3)글보기
ArticleController@show 작성
public function show($id)
{
$article = Article::find($id);
return view('article.show', compact('article'));
}
public function destroy($id)
{
$article = Article::find($id);
$article->delete();
return redirect()->route('article.index');
}
10. 모델을 라우트에 바인딩 시키자
라우트의 {id} 부분을 {article}로 변경하고 컨트롤러 메서드에 $id로 전달한 파라미터를 Article $article 타입 힌트로 지정한다.
만약 route의 {article}을 {foo}로 지정했다면 컨트롤러의 타입힌트도 Article $foo로 변경해야 한다.
//web.php
route::get('/article/', 'ArticleController@index')->name('article.index');
route::get('/article/create', 'ArticleController@create')->name('article.create');
route::post('/article', 'ArticleController@store')->name('article.store');
route::get('/article/{article}', 'ArticleController@show')->name('article.show');
route::get('/article/{article}/edit', 'ArticleController@edit')->name('article.edit');
route::put('/article/{article}', 'ArticleController@update')->name('article.update');
route::delete('/article/{article}/destroy', 'ArticleController@destroy')->name('article.destroy');
//ArticleController.php
public function index(){
$articles = Article::orderBy('created_at', 'desc')->paginate(15);
return view('article.index', compact('articles'));
}
public function create()
{
return view('article.create');
}
public function store()
{
request()->validate([
'title' => 'required',
'content' => 'required'
]);
$article = new Article();
$article->title = request('title');
$article->content= request('content');
$article->save();
return redirect()->route('article.index');
}
public function show(Article $article)
{
$article = Article::find(Article $article);
return view('article.show', compact('article'));
}
public function edit(Article $article)
{
$article = Article::find(Article $article);
return view('article.edit', compact('article'));
}
public function update(Article $article)
{
request()->validate([
'title' => 'required',
'content' => 'required'
]);
$article = Article::find(Article $article);
$article->title = request('title');
$article->content= request('content');
$article->save();
return redirect()->route('article.show', $article->id);
}
public function destroy(Article $article)
{
$article = Article::find(Article $article);
$article->delete();
return redirect()->route('article.index');
}
라우트에 모델을 바인딩하면 라라벨은 자동으로 테이블의 id칼럼을 조회하게 된다. id가 아닌 다른 칼럼을 조회하게 하려면 model에 getRouteKeyName메서드를 오버라이드 해준다.
class Article extends Model
{
function getRouteKeyName(){
return 'title';
}
}
11. 중복제거
validate와 게시글 저장, 업데이트 하는부분이 store와 update에서 중복이 발생했다. validation 부분을 메서드로 작성하여 중복을 제거하자.
public function index(){
$articles = Article::orderBy('created_at', 'desc')->paginate(15);
return view('article.index', compact('articles'));
}
public function create()
{
return view('article.create');
}
public function store()
{
Article::create($this->validateArticle());
return redirect()->route('article.index');
}
public function show(Article $article)
{
return view('article.show', compact('article'));
}
public function edit(Article $article)
{
return view('article.edit', compact('article'));
}
public function update(Article $article)
{
$article->update($this->validateArticle());
return redirect()->route('article.show', $article->id);
}
public function destroy(Article $article)
{
$article->delete();
return redirect()->route('article.index');
}
protected function validateArticle(){
return request()->validate([
'title' => 'required',
'content' => 'required'
]);
}
중복을 제거 했지만 글을 작성하거나 업데이트 하면 아래와 같이 에러가 발생할 것이다.
Add [title] to fillable property to allow mass assignment on [App\Article].
기본적으로 모든 Eloquent 모델은 대량 할당-Mass Assignment 으로부터 보호되기 때문에, 모델의 ‘fillable’나 ‘guarded’속성을 지정해야 주어야 한다. fillable은 화이트리스트방식이고, guarded는 블랙리스트 방식이라고 생각하면 된다.
class Article extends Model
{
protected $guarded = [];
또는
protected $fillable = ['title', 'content'];
}
store메소드의 첫번째 인자는 파일이 저장될 폴더명이다. 예제에서는 ‘images’로 지정했기 때문에 /storage/app/public에 images 폴더를 생성한 뒤 파일을 저장한다. 파일명은 자동으로 랜덤한 값이 들어가고 경로와함께 반환된다. 예) images/임의이름.jpg 두번째 인자는 저장에 사용할 디스크를 지정한다.
#view.blade.php // 파일 서빙
{{asset('storage/file.jpg')}}
asset(‘storage/file.jpg’)을 통해서 반환되는 이미지 경로는 mysite.com/storage/images/file.jpg 가 된다.
실제로사용하기
글을저장하는 post테이블, 파일을 저장하는 file테이블이 있다.
post모델에 1:다 관계(hasMany)를 지정한다.
file모델에는 1:1 관계( belongsTo)를 지정한다.
뷰에서 값을 확인하면 객체를 포함한 배열을 볼수 있다. 아래와 같이 배열의 길이를 확인하고 처리하면된다.