[laravel]라라벨 서비스컨테이너

  1. 컨테이너 바인딩
    • 기본바인딩
      • 서비스컨테이너 바인딩
      • 서비스프로바이더 register() 에서 등록함
      • 일반클래스는 등록할 필요가 없으며 인터페이스 구현객체인 경우 등록해서 사용함
      • 인터페이스와 클로저를 파라미터로 전달하여 바인딩
      • 컨트롤러와 같은 클래스에서 바인딩한 인터페이스를 타입힌트로 지정하면 클로저에 정의한 구현체가 자동으로 의존성 주입됨
      • 예) 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;
}

[laravel] This cache store does not support tagging 해결방법

태그가 지정된 캐시를 저장하고자 할때 ‘This cache store does not support tagging.’ 라는 에러가 발생했다.

코드 :

$articles = Cache::tags('article')->remember.env($cacheKey, now()->addMinutes(30), function () {
            return Article::latest()->paginate(10);
        });

문제해결:

캐시드라이버가 file 또는 database 에서는 태그기능을 지원하지 않는다.
.env 에서 CACHE_DRIVER값을 redis 또는 memcache또는 array로 변경한다.

#CACHE_DRIVER=file
CACHE_DRIVER=array

설정캐시를 날려 수정한 사항이 반영되도록 한다.

php artisan config:cache

만약 드라이버를 수정하지 않고 특정 위치에서만 사용하고 싶을때는 Cache파사드의 store메서드에 드라이버를 인자로 전달하면 된다.

        $articles = Cache::store('array')->tags('article')->remember($cacheKey, now()->addMinutes(30), function () {
            return Article::latest()->paginate(10);
        });

[laravel] 다형성관계 시더작성하기

  • User모델은 Article 모델과 일대다 관계를 가진다.
  • Comment 모델은 Article 모델과 일대다 다형성 관계를 가진다.
  • Article은 제한이 없는 계층적인 댓글을 가진다.

0.테이블 스키마생성

Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
});

Schema::create('articles', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('user_id');
            $table->string('title');
            $table->text('content');
            $table->timestamps();
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
        });

Schema::create('comments', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('user_id');
            $table->unsignedBigInteger('parent_id')->nullable();
            $table->text('content');
            $table->string('commentable_type');
            $table->unsignedBigInteger('commentable_id');
            $table->timestamps();

            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
            $table->foreign('parent_id')->references('id')->on('comments');
        });

1. 모델간 관계 정의

User 모델 :

    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’를 전달해서 외래키를 지정했다.

2.시더에 사용할 팩토리 작성

UserFactory :

$factory->define(User::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'email_verified_at' => now(),
        'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
        'remember_token' => Str::random(10),
    ];
});

ArticleFactory :

$factory->define(Article::class, function (Faker $faker) {
    return [
        'title' => $faker->sentence,
        'content' => $faker->paragraph,
    ];
});

Article은 User와 관계가 정의 되어있기 때문에 user_id를 따로 지정하지 않아도 자동으로 시더를 통해 입력된다.

CommentFactory :

$factory->define(Comment::class, function (Faker $faker) {
    $userIds = App\User::all()->pluck('id')->toArray();
    return [
        'content' => $faker->paragraph(),
        'user_id' => $faker->randomElement($userIds),
    ];
});

마찬가지로 ‘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’값을 전달해 줌으로써 재귀적인 댓글 구조를 가질수 있게 한다.

[laravel] 데이터베이스 시딩

시딩이란

라라벨은 시더클래스를 통해서 데이터베이스에 더미 데이터를 넣을수 있는 기능을 제공한다.

1. 시더클래스

아티즌 명령어를 통해 시더를 생성한다. 생성된 클래스는 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');
    }
}

팩토리 파일을 생성해준다. 옵션을 붙여 모델을 연결해주면 편리하다.

php artisan make:factory ArticleFactory --model=Article

생성된 파일을 열어 내용을 수정하자.

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개의 레코드가 생성된 것을 확인할 수 있다.


References

[laravel]인증기능 커스텀

라라벨은 기본적으로 인증기능을 지원하지만 상황에 따라 커스텀을 해야하는 경우가 있다.

목표

  • 라라벨인증 기능은 사용자가 이용하고, 커스텀한 인증은 관리자가 이용할 수 있게 한다.

목차

  1. 프로젝트 생성
  2. 라라벨인증 기능 설치
  3. 모델 및 마이그레이션 생성
  4. 모델 migrate
  5. 가드, 프로바이더, 패스워드 지정
  6. 미들웨어 생성 및 등록
  7. 라우트 생성
  8. 컨트롤러, 뷰 생성

1. 프로젝트 생성

laravel new blog
cd ./blog
php artisan key:generate
composer install

2. 라라벨 인증기능 설치

라라벨인증을 설치하는 이유

  1. 사용자는 기본인증을 사용하기로 함
  2. 커스텀인증은 처음부터 구현하는것 보다 기본인증을 복사해서 사용하는것이 시간을 절약할 수 있다.
composer require laravel/ui --dev
npm install
npm run dev

3. 모델 및 마이그레이션 생성

기본인증에 사용되는 User모델이 있지만 관리자는 새로운 테이블로 관리하려고하기 때문에 새로운 모델과 마이그레이션을 생성해야 한다.

하지만 메일인증에 사용하는 password_resets 테이블은 분리하지 않고 공통으로 사용하기로 한다.

php artisan make:model Admin -m

‘-m’ 옵션을 사용하면 마이그레이션도 같이 생성된다.

Admin 모델 정의

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class Admin extends Authenticatable
{
    use Notifiable;

    protected $fillable = [
        'name', 'email', 'password',
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

마이그레이션 파일을 열어 테이블 스키마를 생성한다.

    public function up()
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

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

4. 모델 migrate

migrate 명령어를 통해 테이블을 생성해주자

php artisan migrate

5. 가드, 프로바이더, 패스워드 지정

config/auth.php 파일을 열어 생성한 Admin 모델에 대해 가드, 프로바이더, 패스워드를 추가해준다.

    'guards' => [

        ...

        'admin' => [
            'driver' => 'session',
            'provider' => 'admins',
        ],
    ],

    'providers' => [
 
        ...

        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Admin::class,
        ],
    ],

    'passwords' => [

        ...

        'admin' => [
            'provider' => 'admins',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],
  • guards : 매요청마다 어떻게 인증할 것인가
  • providers : 저장소에서 사용자를 어떻게 찾을 것인가
  • passwords : 사용자 테이블에 대한 암호를 재설정하는데 사용하는 브로커

6. 미들웨어 생성과 등록

라우트를 보호할 목적으로 미들웨어를 생성한다. 기본인증에 사용되는 미들웨어 클래스 Authenticate , RedirectIfAuthenticated 를 동일하게 구현할 예정이다.

미들웨어 생성

php artisan make:middleware AdminAuthenticate
php artisan make:middleware AdminRedirectIfAuthenticated
  • AdminAuthenticate : 인증된 사용자가 아니면 로그인 페이지로 리다이렉트
  • AdminRedirectIfAuthenticated : 인증되지 않은 요청만 허용, 로그인된 사용자라면 홈으로 리다이렉트

AdminAuthenticate 미들웨어는 상속받은Illuminate\Auth\Middleware\Authenticate 클래스의 redirectTo메서드를 오버라이드 해서 리다이렉트 경로를 지정해준다.

#AdminAuthenticate.php

    protected function redirectTo($request)
    {
        if (! $request->expectsJson()) {
            return route('admin.login');
        }
    }

AdminRedirectIfAuthenticated 는 클래스를 상속받기 않기 때문에 handle매서드를 지정해주면 된다. Auth파사드를 사용하기 위해 Illuminate\Support\Facades\Auth 네임스페이스 사용한다.

#AdminRedirectIfAuthenticated.php

use Illuminate\Support\Facades\Auth;

    public function handle($request, Closure $next, $guard = null)
    {
        if (Auth::guard($guard)->check()) {
            return redirect('/admin/home');
        }

        return $next($request);
    }

정의한 미들웨어를 app/http/kernel.php 에 등록한다.

    protected $routeMiddleware = [
        
        ...

        'admin.auth' => \App\Http\Middleware\AdminAuthenticate::class,
        'admin.guest' =>\App\Http\Middleware\AdminRedirectIfAuthenticated::class,
    ];

7. 라우트 생성

라라벨 인증에 사용되는 라우트는 아래와 같다. 아래의 기본인증 라우트는 그대로 두고 새로 만든 admin전용 라우트를 생성한다.

  • 기본인증 라우트
MethodURINameActionMiddleware
GET|HEADloginloginApp\Http\Controllers\Auth\LoginController@showLoginFormweb,guest
POSTlogin App\Http\Controllers\Auth\LoginController@loginweb,guest
POSTlogoutlogoutApp\Http\Controllers\Auth\LoginController@logoutweb
GET|HEADregisterregisterApp\Http\Controllers\Auth\RegisterController@showRegistrationFormweb,guest
POSTregister App\Http\Controllers\Auth\RegisterController@registerweb,guest
GET|HEADpassword/confirmpassword.confirmApp\Http\Controllers\Auth\ConfirmPasswordController@showConfirmFormweb,auth
POSTpassword/conform App\Http\Controllers\Auth\ConfirmPasswordController@confirmweb,auth
GET|HEADpassword/resetpassword.requestApp\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestFormweb
POSTpassword/emailpassword.emailApp\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmailweb
GET|HEADpassword/reset/{token}password.resetApp\Http\Controllers\Auth\ResetPasswordController@showResetFormweb
POSTpassword/resetpassword.updateApp\Http\Controllers\Auth\ResetPasswordController@resetweb
GET|HEADemail/verifyverification.noticeApp\Http\Controllers\Auth\VerificationController@showweb,auth
POSTemail/resendverification.resendApp\Http\Controllers\Auth\VerificationController@resendweb,auth,throttle:6,1
GET|HEADemail/verify/{id}/{hash}verification.verifyApp\Http\Controllers\Auth\VerificationController@verifyweb,auth,signed,throttle:6,1
GET|HEADhomehomeApp\Http\Controllers\HomeController@indexweb,auth

위 라우트는 web.php에 정의되어 있지 않고 Auth::routes(); 로 등록되어 있다. 기본인증 라우트를 확인 하려면 /vendor/laravel/ui/src/AuthRouteMethods.php를 확인하면 된다.

  • admin 라우트
MethodURINameActionMiddleware
GET|HEADadmin/loginadmin.loginApp\Http\Controllers\Admin\LoginController@showLoginFormweb,admin.guest
POSTadmin/login App\Http\Controllers\Admin\ LoginController@loginweb,admin.guest
POSTadmin/logoutadmin.logoutApp\Http\Controllers\Admin\ LoginController@logoutweb
GET|HEADadmin/registeradmin.registerApp\Http\Controllers\Admin\ RegisterController@showRegistrationFormweb,admin.guest
POSTadmin/register App\Http\Controllers\Admin\ RegisterController@registerweb,admin.guest
GET|HEADadmin/password/confirmadmin.password.confirmApp\Http\Controllers\Admin\ ConfirmPasswordController@showConfirmFormweb,admin.auth
POSTadmin/password/conform App\Http\Controllers\Admin\ ConfirmPasswordController@confirmweb,admin.auth
GET|HEADadmin/password/resetadmin.password.requestApp\Http\Controllers\Admin\ ForgotPasswordController@showLinkRequestFormweb
POSTadmin/password/emailadmin.password.emailApp\Http\Controllers\Admin\ ForgotPasswordController@sendResetLinkEmailweb
GET|HEADadmin/password/reset/{token}admin.password.resetApp\Http\Controllers\Admin\ ResetPasswordController@showResetFormweb
POSTadmin/password/resetadmin.password.updateApp\Http\Controllers\Admin\ResetPasswordController@resetweb
GET|HEADadmin/email/verifyadmin.verification.noticeApp\Http\Controllers\Admin\VerificationController@showweb,admin.auth
POSTadmin/email/resendadmin.verification.resendApp\Http\Controllers\Admin\VerificationController@resendweb,admin.auth,throttle:6,1
GET|HEADadmin/email/verify/{id}/{hash}admin.verification.verifyApp\Http\Controllers\Admin\VerificationController@verifyweb,admin.auth,signed,throttle:6,1
GET|HEADadmin/homeadmin.homeApp\Http\Controllers\Admin\HomeController@indexweb,admin.auth
#routes/web.php

Route::namespace('Admin')->group(function(){
    Route::prefix('admin')->group(function(){
        //로그인
        Route::get('login', 'LoginController@showLoginForm')->name('admin.login');
        Route::post('login', 'LoginController@login');
        Route::post('logout', 'LoginController@logout')->name('admin.logout');
        //회원가입
        Route::get('register', 'RegisterController@showRegistrationForm')->name('admin.register');
        Route::post('register', 'RegisterController@register');
Route::get('home', 'HomeController@index')->name('admin.home');
        //패스워드확인
        Route::get('password/confirm', 'ConfirmPasswordController@showConfirmForm')->name('admin.password.confirm');
        Route::post('password/confirm', 'ConfirmPasswordController@confirm');
        //패스워드리셋
        Route::get('password/reset', 'ForgotPasswordController@showLinkRequestForm')->name('admin.password.request');
        Route::post('password/email', 'ForgotPasswordController@sendResetLinkEmail')->name('admin.password.email');
        Route::get('password/reset/{token}', 'ResetPasswordController@showResetForm')->name('admin.password.reset');
        Route::post('password/reset', 'ResetPasswordController@reset')->name('admin.password.update');
        //이메일인증 확인
        Route::get('email/verify', 'VerificationController@show')->name('admin.verification.notice');
        Route::post('email/resend', 'VerificationController@resend')->name('admin.verification.resend');
        Route::get('email/verify/{id}/{hash}', 'VerificationController@verify')->name('admin.verification.verify');
    });
});

8. 컨트롤러 및 뷰생성

(1) 컨트롤러 뼈대 만들기

app/Http/Controller 아래에 Admin 폴더를 생성
app/Http/Controller/Auth 하위 파일을 Admin 폴더로 복사한다.
app/Http/Controller/HomeController를 Admin 폴더로 복사한다.

이제 모델 Admin에 대한 컨트롤러는 아래와 같은 디렉토리 구조를 같는다.

  • app/Http/Controller/Admin/LoginController.php
  • app/Http/Controller/Admin/RegisterController.php
  • app/Http/Controller/Admin/ConfirmPasswordController.php
  • app/Http/Controller/Admin/ForgotPasswordController.php
  • app/Http/Controller/Admin/ResetPasswordController.php
  • app/Http/Controller/Admin/VerificationController.php
  • app/Http/Controller/Admin/HomeController.php

(2) 뷰 뼈대 만들기

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');
    }
admin/auth/password/confirm.blade.php
<form method="POST" action="{{ route('admin.password.confirm') }}">

특정라우트를 보호하고 싶을때는 아래와 같이 미들웨어를 지정한다. admin.auth에는 admin 가드를 전달, password.cofirm에는 리다이렉트 라우트를 전달한다.

        Route::get('test', function(){
            return '이페이지가 보인다면 비밀번호 확인을 하신겁니다.';
        })->middleware(['admin.auth:admin', 'password.confirm:admin.password.confirm']);
  • 비밀번호 리셋

라라벨은 기본적으로 이메일로 인증주소를 받아 비밀번호를 초기화 시킨다. 이메일 설정이 되어있지 않으면 메일형식을 로그로 변경해주자.

# .env
MAIL_MAILER=log

비밀번호를 초기화하는 과정은 아래와 같다.

  1. admin/password/reset 페이지 접근
  2. 사용자 정보가 있으면 비밀번호 초기화페이지로 이동 없으면 로그인
  3. 이메일을 제출하면 해당 주소로 인증주소를 보낸다.
  4. 인증주소로 접속하여 비밀번호를 변경

이제 컨트롤러와 뷰를 수정하자

#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전용 리셋주소로 정보를 받아야 한다.

패스워드 재설정 이메일을 커스텀하려면 몇가지 작업이 필요하다.

  1. artisan 명령어를 이용해 notification 생성
  2. Admin모델에서 sendPasswordResetNotification 메서드 오버라이드

패스워드 재설정 참조링크

php artisan make:notification AdminPasswordResetMailToken
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);
    }

[laravel] 관례적인 네이밍 규칙

  • 모델은 단수형
    예)Post
  • 테이블은 복수형
    예)posts
  • 테이블간 관계를 표현할 때
    -테이블 이름_열이름_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을 사용한다.
idpost_idtag_id
111
212
  • 마이그레이션 생성시
    – create_, make_, add_, drop_, change_ 등으로 시작하고, _table로 끝난다.
    예) create_posts_table
  • 컨트롤러
    – 파스칼 표기법을 사용하여 복수형에 Controller접미사를 붙인다.
    예)PostsController

[laravel] 나만보는 라라벨 튜토리얼

라라벨코리아, 라라캐스트를 보고 간단한 프로젝트를 만들면서 정리하는 글

  1. 프로젝트생성
  2. composer 의존성 설치
  3. 프론트앤드 스케폴딩
  4. npm 의존성 설치
  5. laravel-mix 사용하기
  6. model, controller, migration 생성하기
  7. restful한 route 만들기
  8. view 생성하기
  9. 기능구현하기
    • 글쓰기 폼작성
    • 저장하기
    • 글보기
    • 글 수정하기 폼작성
    • 글 업데이트하기
    • 글목록 페이지
    • 삭제하기
  10. 모델을 라우트에 바인딩 시키자
  11. 중복 제거

1.프로젝트 생성

laravel new blog
cd ./blog

2.composer 의존성 설치

composer install

3.프론트앤드 스케폴딩
필요한 부분만 선택하여 설치하면 된다. (본 예제에서는 bootstrap만 스캐폴딩함)

composer require laravel/ui --dev
// Generate basic scaffolding...
php artisan ui bootstrap
php artisan ui vue
php artisan ui react
// Generate login / registration scaffolding...
php artisan ui bootstrap --auth
php artisan ui vue --auth
php artisan ui react --auth

4.npm 의존성 설치
프론트 앤드 스캐폴딩을 하게 되면 package.json에 의존성 목록의 추가된다. 프로젝트에 사용하기 위해서 의존성을 설치해준다.

npm install

5.laravel-mix 사용하기
루트디렉토리의 webpack.mix.js 파일을 열어보면 아래와 같이 기본설정이 되어있다.

mix.js('resources/js/app.js', 'public/js')
    .sass('resources/sass/app.scss', 'public/css');

특별히 추가 할게 없다면 npm run dev 명령어를 실행해서 컴파일 하자. 앞에서 부트스트랩을 스캐폴딩했기 때문에 컴파일 하면 부트스트랩도 같이 컴파일 되어 public/js/app.js, public/css/app.css 로 위치하게 된다. (laravel-mix 문서 보기)

추가로 browsersyc를 webpack.mix.js에 추가해서 브라우저 변경시 새로고침없이 자동으로 리로드되게 해주면 개발할때 편리하다.

//mix.browserSync('mydomain.com');
mix.browserSync('localhost:8000');

6.모델, 컨트롤러, 마이그레이션 생성하기
-r 옵션은 리소스컨트롤러로 컨트롤러에 기본적인 메서드가 자동으로 추가된다.

* 모델과 컨트롤러는 단수로, 마이그레이션은 복수로 표현한다.
모델          Article
컨트롤러      ArticleController
마이그레이션  create_articles_table
php artisan make:model Article
php artisan make:controller ArticleController
php artisan make:migration create_aticles_table
// 한번에 생성하기 -c: controller -r:resource controller -m:migration
php artisan make:model Article -c -m
또는
php artisan make:model Article -r -m

생성된 마이그레이션 파일을 열어 테이블 스키마를 지정해준다.

    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 파일을 생성한다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="{{ asset('css/app.css')}}">
    <script src="{{ asset('js/app.js') }}"></script>
    <title>Document</title>
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <div class="container">
            <a class="navbar-brand" href="{{ route('article.index') }}">Navbar</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav">
                <li class="nav-item {{Request::is('article*')? 'active' : ''}}">
                    <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
                </li>
                </ul>
            </div>
        </div>
    </nav>
    <div class="container">
        @yield('content')
    </div>
    <div class="footer container mt-3 pt-3 text-center">
        <div class="text-muted">@laravel</div>
    </div>
</body>
</html>

/resources/views/article 디렉토리를 생성하고 앞에서 지정한 라우트에 필요한 뷰파일을 생성해준다.

//index.blade.php
@extends('layout')
@section('content')
리스트페이지
@endsection
//create.blade.php
@extends('layout')
@section('content')
글쓰기 폼
@endsection
//show.blade.php
@extends('layout')
@section('content')
글보기
@endsection
//edit.blade.php
@extends('layout')
@section('content')
글쓰기 수정 폼
@endsection
뷰파일은 /resources/views 내에 어디서든 위치해도 상관없는데 일관성을 위해서 article 디렉토리를 추가했다. 

9.기능구현하기

1)글쓰기 폼 (create.blade.php) 작성

  • ArticleController@create
    public function create()
    {
        return view('article.create');
    }
  • create.blade.php
@extends('layout')
@section('content')
    <form action="{{ route('article.store') }}" method="post">
        @csrf
        <div class="form-group">
          <label for="title">제목</label>
          <input 
          type="text"
          name="title"
          id="title"
          class="form-control @error('title') is-invalid @enderror"
          placeholder=""
          aria-describedby="title"
          value="{{ old('title') }}">
          @error('title')
          <p class="invalid-feedback">제목을 입력하세요</p>
          @enderror
        </div>
        <div class="form-group">
          <label for="content">내용</label>
          <textarea class="form-control @error('content') is-invalid  @enderror" name="content" id="content" rows="3">{{ old('content') }}</textarea>
          @error('content')
          <p class="invalid-feedback">내용을 입력하세요</p>
          @enderror
        </div>
        <button type="submit" class="btn btn-primary">저장</button>
    </form>
@endsection

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'));
    }
  • show.blade.php 작성
@extends('layout')
@section('content')
<h1>{{ $article->title }}</h1>
<small class="small text-muted">{{ $article->updated_at }}</small>
<div class="mt-3 pt-3 border-top">{{ $article->content }}</div>
@endsection

4)글 수정하기폼 ArticleController@edit

  • ArticleController@edit
   public function edit($id)
    {   
       $article = Article::find($id); 
        return view('article.edit', compact('article'));
    }
  • edit.blade.php
@extends('layout')
@section('content')
    <form action="{{ route('article.update', $article->id) }}" method="POST">
        @csrf
        @method('PUT')
        <div class="form-group">
          <label for="title">제목</label>
          <input 
          type="text"
          name="title"
          id="title"
          class="form-control @error('title') is-invalid @enderror"
          placeholder=""
          aria-describedby="title"
          value="{{ $erros->has('title') ? old('title') : $article->title }}">
          @error('title')
          <p class="invalid-feedback">제목을 입력하세요</p>
          @enderror
        </div>
        <div class="form-group">
          <label for="content">내용</label>
          <textarea 
          class="form-control @error('content') is-invalid  @enderror" 
          name="content" 
          id="content" 
          rows="3">{{ $erros->has('content') ? old('content') : $article->content}}</textarea>
          @error('content')
          <p class="invalid-feedback">내용을 입력하세요</p>
          @enderror
        </div>
        <button type="submit" class="btn btn-primary">저장</button>
    </form>
    <form action="{{ route('article.destroy', $article->id) }}" method="POST">
      @csrf
      @method('DELETE')
      <button type="submit" class="btn btn-danger">삭제</button>
    </form>
@endsection

@method 지시자를 이용해서 PUT메서드를 스푸핑하는것을 확인하자.

5)글 업데이트 ArticleController@update

public function update($id)
    {
        request()->validate([
            'title' => 'required',
            'content' => 'required'
        ]);
        $article = Article::find($id);
        $article->title = request('title');
        $article->content= request('content');
        $article->save();
        return redirect()->route('article.show', $article->id);
    }

5)글목록 페이지

  • ArticleController@index
public function index(){
        $articles = Article::orderBy('created_at', 'desc')->paginate(15);
        return view('article.index', compact('articles'));
    }
  • inedx.blade.php
@extends('layout')
@section('content')
@foreach($articles as $article)
<div class="card mb-3">
  <div class="row no-gutters">
    <div class="col-md-3">
      <img src="..." class="card-img" alt="">
    </div>
    <div class="col-md-9">
      <div class="card-body">
        <h5 class="card-title">
          <a href="{{ route('article.show', $article->id) }}">{{ $article->title }}</a>
        </h5>
        <p class="card-text">{{ $article->content }}</p>
        <p class="card-text"><small class="text-muted">{{ $article->updated_at }}</small></p>
      </div>
    </div>
  </div>
</div>
@endforeach
@endsection

6)해당글 삭제하기 ArticleController@destroy

    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'];
}

[laravel/storage] 업로드된 파일 저장하기

파일스토리지 설정하기

  • 파일 시스템 설정파일위치 config/filesystem.php
  • 설정파일에서 이미지 저장 path, url등을 수정할 수 있다.

Public 디스크

  • public 디스크는 local드라이버를 사용하고 storage/app/public에 저장함.
  • 웹에서 파일을 서빙하려면 public/storage를 storage/app/public으로 심볼릭 링크를 생성해야한다. 아티즌명령으로 심볼릭링크를 생성할 수 있다.
php artisan storage:link

명령을 실행하고 public/storage를 확인하면 심볼링 링크가 생성된걸 확인 할 수 있다. 이제 파일을 업로드하고 처리하는 코드를 작성해보자.

#write.blade.php

<form enctype="multipart/form-data">
    <input type="file" name="file">
</form>
#controller

$request->file('file')->store('images', 'public');

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)를 지정한다.

뷰에서 값을 확인하면 객체를 포함한 배열을 볼수 있다. 아래와 같이 배열의 길이를 확인하고 처리하면된다.

@if(count($post->file))
    {{$post->file[0]->ori_fname}}
@endif

참고

파일스토리지
업로드된 파일 저장하기