해리의 데브로그

Laravel 5.7 From Scratch 11 - Service Provider

|

1. GET STARTED

App\Providers를 보면 라라벨에서 제공하는 기본 providers를 볼 수 있음. App\config\app.php를 보면 Autoloaded Service Providers 목록을 볼 수 있으며, 이 목록을 어플리케이션에 요청이 올때마다 자동적으로 불러옴.

2. boot & register

서비스 프로바이더는 boot()register() 두 가지의 메서드를 갖고 있음.

  • register : 서비스 컨테이너에 바인딩. 라라벨은 App\config\app.php - providers에 있는 서비스 프로바이더들을 돌면서 register 메서드를 호출함.
  • boot : register이 끝난 이후, 라라벨은 다시한번 pp\config\app.php - providers에 있는 서비스 프로바이더들을 돌면서 boot 메서드를 호출함.
class AppServiceProvider extends ServiceProvider
{

    public function register()
    {
      $this->app->singleton('foo', function() {
        return 'bar';
      });
    }

    public function boot()
    {

    }
}
Route::get('/', function() {
    dd(app('foo'));

    return view('welcome');
});

2-1. 응용

//AppServiceProvider.php
public function register()
{
  $this->app->singleton(Twitter::class, function() {
    return new Twitter('api-key');
  });
}

//web.php
use App\Services\Twitter;

Route::get('/', function(Twitter $twitter) {
    dd($twitter);

    return view('welcome');
});

3. Create new service provider

새로운 서비스 프로바이더 생성하여 위의 로직을 새로운 프로바이더에 연결시킬 수 있음

  • php artisan make:provider SocialServiceProvider
  • app\config\app.php에 서비스 프로바이더 추가
// SocialServiceProvider
public function register()
{
  $this->app->singleton(Twitter::class, function() {
    return new Twitter('api-key');
  });
}

// app\config\app.php
    'providers' => [
        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
        App\Providers\SocialServiceProvider::class,

    ],

4. Service Provider 활용

AppServiceProvider@register에서 새로운 클래스 바인딩 코드 구현

public function register()
{
  $this->app->bind(
    \App\Repositories\UserRepository::class,
    \App\Repositories\DbUserRepository::class
  );
}

\App\Repositories 내에서 UserRepository & DbUserRepository 파일 생성

// UserRepository.php
<?php

namespace App\Repositories;

interface UserRepository
{
    public function create($attributes);
}

// DbUserRepository.php

<?php

namespace App\Repositories;

class DbUserRepository implements UserRepository
{
    public function create($attributes)
    {
        User::create();
        dd('creating the user');
    }
}

이후, web.php에서 클래스를 호출하면 됨.

use App\Repositories\UserRepository;

Route::get('/', function(UserRepository $users) {
    dd($users);

    return view('welcome');
});

AppServiceProvider.php에서 register 메서드의 내용을 주석처리할 경우 아래처럼 인스턴스 에러가 발생함.

Illuminate\Contracts\Container\BindingResolutionException
Target [App\Repositories\UserRepository] is not instantiable.

Laravel 5.7 From Scratch 10 - Service Container

|

1. GET STARTED

ProjectsController.php - show 메서드는 Project Model을 파라미터로 받음 이는 Route Model Binding을 이용한 것으로 유사한 개념으로 Service Container Component가 있음

public function show(Project $project)
{
  return view('projects.show', compact('project'));
}

2. Service Container and Auto-Resolution

예를 들어, 라라벨은 Filesystem 이라는 클래스를 갖고 있음. 클래스를 파라미터로 받아 dd($file) 을 입력하면 인스턴스가 출력 결과를 볼 수 있음. 이처럼, 클래스를 파라미터로 받으면 라라벨이 자동적으로 클래스를 사용하게 해줌. behind scene에는 2가지의 컴포넌트가 존재함.

  1. Auto-Resolution: “Okay.. you typed in Filesystem here, it seems like you want instance of that..”
  2. Service Container: Imagine there’s key-value pairs in a container. Laravel is gonna look into container and check “Do we have Filesystem here?” “Oh there it is! That’s probably what the user wants. I’m going to resolve fetch, or get them from the container and give it to user!”
use Illuminate\Filesystem\Filesystem; // import to us Filesystem

// ... skip

public function show(Filesystem, $file)
{
  dd($file)
}

3. 예시 (1)

web.php에서 다른 예시를 들어보자. Laravel application 자체가 사실상 서비스 컨테이너임(we can get things out of here by using helpful function)

  • app() 또는 resolve() 으로 호출 가능.
  • Filesystem 을 기본페이지로 호출하면, 위의 show 메소드에서 출력한 값과 동일한 값이 출력되는 것을 알 수 있음.
use Illuminate\Filesystem\Filesystem;

Route::get('/', function(){
  dd(app(Filesystem::class));
})

4. 예시 (2) - put something into container

app()->bind() 으로 서비스 컨테이너에 등록이 가능함.

  • key( example )으로 value(Example 클래스의 인스턴스 생성)을 호출 할 수 있음.
  • 이후 out of container에서 app('eaxmple') 을 호출하면 동일하게 Example 인스턴스를 출력한 결과를 볼 수 있음.
// fetch 'example' out of container, what is value of accicated key.
app()->bind('example', function() {
  return new \App\Example;
})

Route::get('/', function(){
  dd(app('example'));
})

// 두번 호출할 경우 두개의 별도의 인스턴스가 생성됨.
Route::get('/', function(){
  dd(app('example'), app('example'));
})

5. singleton

위의 마지막 예시처럼 두번 호출할 경우 두개의 별도의 인스턴스가 생성됨. 만약 단 하나의 인스턴스만 생성되길 원할 경우, singleton 메서드를 사용함.

singleton 메소드로 클래스나 인터페이스를 바인딩 하면 컨테이너는 한 번만 해당 의존성을 해결합니다. 싱글톤 바인딩으로 의존성이 해결되면, 컨테이너의 다른 부분에서 호출될 때 동일한 객체 인스턴스가 반환될 것입니다.

app()->singleton('example', function() {
  return new \App\Example;
}

6. Summary

컨테이너 외부에서 요청이 들어왔을 경우,

  1. 서비스 컨테이너에 존재하는지를 확인
  2. 어플리케이션 내에 존재하는 클래스인지 확인 (Full relative address를 입력하면 됨)
Route::get('/', function() {
    dd(app('App\Example'));
});

7. Service container with constructor

서비스 컨테이너가 호출하는 클래스가 고유한 __construct 를 갖고 있는 경우, 그안의 값까지 같이 호출함

<?php

namespace App;

class Example
{
    protected $foo;

    public function __construct(Foo $foo)
    {
        $this->foo = $foo;
    }
}

<?php

namespace App;

class Foo
{

}

아래와 같이 응용이 가능함.

//web.php
app()->singleton('Twitter', function() {
   return new \App\Services\Twitter('abdsfsfdf');
});

// ProjectsController.php
public function show(Project $project)
{
  $twitter = app('twitter');
  dd($twitter);
}

// \App\Services\Twitter.php
<?php

namespace App\Services;

class Twitter
{
    protected $apiKey;

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

8. Service Container as parameter of methods

key를 Full Relative path로 변경해준 후 클래스를 호출하면, 서비스 컨테이너에 등록된 클래스를 메서드의 파라미터로 받아 올 수 있음.

app()->singleton('App\Services\Twitter', function() {
   return new \App\Services\Twitter('abdsfsfdf');
});


use App\Services\Twitter;

public function show(Project $project, Twitter $twitter)
{
  dd($twitter);
}

Laravel 5.7 From Scratch 09 - Form Action Consideration

|

1. Form Action Considerations

태스크를 완료 시킬 수 있는 폼태그 양식을 만들고 그 내용을 업데이트하는 로직을 구성하도록 할 수 있음.

  • 태스크 완료 유무를 체크박스로 표시
  • submit은 자바스크립트 코드로 구현(onChange = "this.form.submit())
  • 삼항 연산자로 checked 속성 적용 유무 확인
  • 폼 태그 메서드 및 액션, csrf 적용
<?php foreach ($project->tasks as $task) : ?>
  <div>
    <form method="POST" action="/tasks/{{ $task->id }}">
    @method('PATCH')
    @csrf

      <label class="checkbox" form="completed">
        <input type="checkbox" name="completed" onChange="this.form.submit()"
          {{ $task->completed ? 'checked' : '' }}>
        {{ $task->description }}
      </label>
    </form>
  </div>
<?php endforeach ?>

2. 관계 & MVC 설정

web.php에서 업데이트 로직에 대한 라우터 설정을 하며 ProjectTasksController.php에서 로직 구현. 또한 모델(Task.php)에서 mass assignment에 대한 설정도 진행

  • return back(); : 바로 이전 페이지로 리턴
  • request()->has('completed') : 해당 키의 값이 존재하는지를 확인함.
// web.php
Route:patch('/tasks/{task}', 'ProjectTasksController@update')

// ProjectTasksController.php
class ProjectTasksController extends Controller
{
    public function update(Task $task)
    {
        $task->update([
            'completed' => request()->has('completed')
        ]);

        return back();
    }
}

// Task.php
class Task extends Model
{
    protected $guarded = [];
    public function project()
    {
        return $this->belongsTo(Project::class);
    }
}

3. Create New Project Tasks

새로운 태스크를 작성하는 코드를 show.blade.php에서 작성 & web.php에서 store에 해당하는 라우트 설정.

// add a new task form

<form method="POST" action="/projects/{{ $project->id }}/tasks">
    @csrf

    <div>
        <label class="label" for="description">New Task</label>
        <input type="text" class="input" name="description" placeholder="New Task">
    </div>
    <div>
        <button type="submit">Add Task</button>
    </div>

</form>


// option 1
Route::post('/projects/{project}/tasks', 'ProjectTasksController@store');
// option 2: hidden input으로 어떤 프로젝트에 대한 것인지를 함께 넘겨줘야함.
Route::patch('/tasks', 'ProjectTasksController@store');

ProjectTasksController@store구현.

첫번째 옵션 처럼 태스크를 생성해도 되나, 해당 로직을 실행하는 메서드를 프로젝트 모델에서 작성하여 클린코드를 지향할 수 도 있음(두번째 옵션 권장). 프로젝트 모델에서도 두가지 방법으로 구현을 할 수 있음.

// ProjectTasksController.php
use App\Project;

class ProjectTasksController extends Controller

public function store(Project $project)
  {

    //option 1
    Task::create([
      'project_id' => $project->id,
      'description' => request('description')
    ]);

    //option 2
    $project->AddTask(request('description'))

      return back();
  }

// Project.php
    public function addTask($task)
    {
				// option 1
        return Task::create([
            'project_id' => $this->id,
            'description' => $description
        ]);

      	// option 2 - 유효성 검증을 통해 array가 반환될 예정이므로
      	// compact()를 사용하지 않고 바로 $task를 넘김.
      	$this->tasks()->create($task);
    }

4. 유효성 검증

서버 사이드에서 유효성 검증을 구현하며, 에러 메세지도 뷰에서 보이도록 하자. 에러메세지의 코드는 사실상 동일하므로 해당 코드가 담긴 별도의 파일을 작성한 후, @include로 상속 받아옴.

// ProjectTasksController.php

public function store(Project $project)
{
  $attributes = request()->validate(['description' => 'required']);

  $project->AddTask($attributes);

  return back();
}

// errors.blade.php
<?php if ($errors->any()) : ?>
<div>
    <ul>
        <?php foreach($errors->all() as $error) : ?>
        <li> {{ $error }}</li>
        <?php endforeach ?>
    </ul>
</div>
<?php endif; ?>

// show.blade.php
@include ('errors')

5. Better Encapsulation

Encapsulation: Hide internal state and values inside a class

컨트롤러에서 직접 코드를 작성해도 되는데 굳이 모델에서 새로운 메서드를 만드는 이유?

much more readable ($project->AddTask vs $project->tasks()->create(request) ). Project model can encapsule, what it means to addTask. All we need to know the outside is that “hey, here is the attribute. I want to addTask” then Project model can be fully responsible for what it means to actually do that.

ProjectTasksController.php - update 메서드 코드에 encapsulation을 적용 + Task.php에 complete 메서드 정의

// ProjectTasksController
public function update(Task $task)
{

  // before
  $task->update([
    'completed' => request()->has('completed')
  ]);

  // after
  $task->complete(request()->has('completed'));

  return back();
}

// Task.php
public function complete($completed=true)
{
  // $this->update(['completed'=>$completed]);
  $this->update(compact('completed'));
}

위 코드를 한번 더 발전시켜서, 완료되지 않았을 때에 대한 메서드도 모델에서 구현 가능

public function incomplete()
{
  $this->complete(false);
}

모델(Task.php)에서 구현한 completeincomplete을 활용하여 ProjectTasksController.php의 update 메서드를 더욱 클린하게(encapulation을 적용하여) 구현할 수 있음. 구현 방법은 아래와 같이 분기문, 삼항 연산자 등을 활용 하면 됨.

public function update(Task $task)
{
  //option 1
  if (request()->has('completed')) {
    $task->complete();
  } else {
    $task->incomplete();
  }

  // option 2
  request()->has('completed') ? $task->complete() : $task->incomplete();

  // option 3
  $method = request()->has('completed') ? 'complete' : 'incomplete';
  $task->$method();

  return back();
}

6. When in Doubt

이전 강의를 통해 태스크 완료 유무의 로직을 ProjectdTasksController@store에서 구현하였음. 현재 구현된 핵심 코드는 아래와 같음. 현재 코드로도 큰 문제는 없으나, 여전히 request()에 종속되어 분기문을 통해 Task 모델의 메서드를 호출하고 있음. 하기 로직은 테스크 완료 유무 로직이 동작하는 별도의 컨트롤러를 생성하여 발전 시킬 수 있음.

$method = request()->has('completed') ? 'complete' : 'incomplete';
$task->$method();

CompletedTasksController.php 생성 후, 태스크 완료, 태스크 완료 해제에 대한 메서드 store , destroy 구현. 기존에 ProjectTasksController.php에 구현되었던 update 메서드는 삭제할 수 있음. 아래와 같이 컨트롤러를 구분함으로써, 동일한 엔드포인트에 대해 다른 메서드를 호출하도록 routes를 설정할 수 있으며, request()에도 종속되지 않으며 분기문도 사용하지 않는 매우 클린한 코드가 완성됨.

// CompletedTasksController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Task; // Task 메서드 사용을 위해 export

class CompletedTasksController extends Controller
{
    public function store(Task $task)
    {
        $task->complete();

        return back();
    }

    public function destroy(Task $task)
    {
        $task->incomplete();

        return back();
    }
}

// web.php
Route::post('/completed-tasks/{task}', 'CompletedTasksController@store');
Route::delete('/completed-tasks/{task}', 'CompletedTasksController@destroy');

show.blade.php - 폼태그의 action 속성 값(url 경로)를 새롭게 지정한 endpoint로 변경시킴. 이때, 동일한 엔드포인트를 기반으로 “테스크 완료” & “테스크 완료 해제”를 적용시키기 위해 @method 값을 변경해주는 분기문 작성

<?php if($project->tasks->count()) : ?>
  <div>
    <?php foreach ($project->tasks as $task) : ?>
      <div>
        <form method="POST" action="/tasks/{{ $task->id }}">
          @if ($task->completed)
          	@method('DELETE')
          @endif
          @csrf

        <label class="checkbox" form="completed">
          <input type="checkbox" name="completed" onChange="this.form.submit()"
              {{ $task->completed ? 'checked' : '' }}>
            {{ $task->description }}
    		</label>
      </form>
    </div>
    <?php endforeach ?>
  </div>
<?php endif ?>

Laravel 5.7 From Scratch 08 - Validation / Eloquent Relationship(1:N)

|

1. Two Layers of Validation

폼 태그로 입력받은 데이터에 대한 유효성 검증(예. 입력하지 않은채 제출할 경우) 은 크게 2가지 방법으로 클라이언트 사이드와 서버 사이드에서 각각 활용이 가능하다.

클라이언트단에서 간단하게 입력 태그에 required를 입력하면 된다. 하지만 개발자 도구로 required를 삭제할 수 도 있다. 이런 경우, 서버사이드 단에서 추가로 유효성 검증을 아래와 같이 진행한다.

2. request()->validate([])

request()->validate([])는 유효성 검증의 attribute를 반환한다.

request()->validate([
  'title' => 'required', // 반드시 필요한 경우
  'description' => ['required', 'min:3'] // min string length: 3
  'password' => ['required', 'confirmed'] // 패스워드에 대한 유효성 검증
]);

request(['title', 'description']) 으로 대체 시킬 수 있음.

public function store()
{
  $attribute = request()->validate([
    'title' => ['required', 'min:3'],
    'description' => ['required','min:3'],
  ]);

  Project::create($attribute);

  return redirect('/projects');
}

3. $errors & old()

이 후, 에러메세지를 브라우저에서 보여주고 싶다면, 라라벨에서 기본적으로 렌더하는 $errors 를 활용할 수 있음.

<?php if ($errors->any()) : ?>
  <div>
  	<ul>
  		<?php foreach($errors->all() as $error) : ?>
    		<li> {{ $error }}</li>
    	<?php endforeach ?>
    </ul>
  </div>
<?php endif; ?>

마지막으로 유효성 검증이 실패했을 경우, 라라벨은 자동적으로 해당 페이지를 리다이렉트 시키는데, 이때 기존에 입력된 값을 브라우저에 남기고 싶을 경우에는 {{old('title')}} 을 입력하면된다.

<div>
  <input type="text" name="title" placeholder="Project title"
         required value="{{ old('title') }}">
</div>

<div>
  <textarea name="description" placeholder="Project description" required>
    {{ old('description') }}
  </textarea>
</div>

4. Your First Eloquent Relationships

php artisan help make:model : 모델 생성에 대한 도움말 확인 가능

Description:
  Create a new Eloquent model class

Usage:
  make:model [options] [--] <name>

Arguments:
  name                  The name of the class

Options:
  -a, --all             Generate a migration, factory, and resource controller for the model
  -c, --controller      Create a new controller for the model
  -f, --factory         Create a new factory for the model
      --force           Create the class even if the model already exists
  -m, --migration       Create a new migration file for the model
  -p, --pivot           Indicates if the generated model should be a custom intermediate table model
  -r, --resource        Indicates if the generated controller should be a resource controller
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
      --env[=ENV]       The environment the command should run under
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

5. Model, Migration, Factory 생성

project cupist$ php artisan make:model Task -m -f : Task 모델, 마이그레이션, 팩토리 생성

\database\migrations\2019_12_07_090008_create_tasks_table.php에서 스키마 재 구성한 후 php artisan migrate 명령어를 통해 마이그레이션을 데이터베이스에 적용.

class CreateTasksTable extends Migration
{
    public function up()
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('project_id'); // 외래키 설정
            $table->string('description'); // 일의 설명
            $table->boolean('completed')->default(false); // 완료 유무
            $table->timestamps();
        });
    }

6. 관계 설정

Project.php & Task.php 모델에서 관계설정을 진행

  • $this->hasMany(Task::class) - 1:N 관계 설정
  • $this->belongsTo(Project::class) - N에서 1로 접근할 때, 부모의 모델 설정
// Project.php
class Project extends Model
{
    protected $guarded = [];

    public function tasks()
    {
        // 1:N관계설정 시 hasMany 사용
        return $this->hasMany(Task::class);
    }
}

// Task.php

class Task extends Model
{
  	// 1:N 관계설정 후, N에서 1로 접근시에는 belongsTo 사용
    public function project()
    {
        return $this->belongsTo(Project::class);
    }
}

MySQL에 접속하여 tasks 테이블에 샘플 데이터를 입력

INSERT INTO tasks(project_id, description, created_at, updated_at) VALUES(2, 'purchase map', NOW(), NOW());
INSERT INTO tasks(project_id, description, created_at, updated_at) VALUES(2, 'Inform school', NOW(), NOW());

7. php artisan tinker 연습

php artisan tinker를 통해 관계 설정된 데이터들의 형태를 볼 수 있음.

  • 1에서 N으로 접근시, App\Project::first()->tasks; 모델의 메서드를 호출하는 것이지만 ()은 입력하지 않음.
  • 마찬가지로, N에서 1로 접근 시에도 APP\Task::first()->project; 으로 접근
>>> App\Project::first();
=> App\Project {#3015
     id: 2,
     title: "My Second Project22",
     description: "Lorem ipsum",
     created_at: "2019-12-05 12:48:01",
     updated_at: "2019-12-07 06:46:38",
   }

>>> App\Project::first()->tasks;
=> Illuminate\Database\Eloquent\Collection {#3004
     all: [
       App\Task {#3015
         id: 2,
         project_id: 2,
         description: "purchase map",
         completed: 0,
         created_at: "2019-12-07 18:15:55",
         updated_at: "2019-12-07 18:15:55",
       },
       App\Task {#3027
         id: 3,
         project_id: 2,
         description: "Inform school",
         completed: 0,
         created_at: "2019-12-07 18:16:39",
         updated_at: "2019-12-07 18:16:39",
       },
     ],
   }
>>> APP\Task::first()
=> App\Task {#3020
     id: 2,
     project_id: 2,
     description: "purchase map",
     completed: 0,
     created_at: "2019-12-07 18:15:55",
     updated_at: "2019-12-07 18:15:55",
   }
>>> APP\Task::first()->project;
=> App\Project {#3000
     id: 2,
     title: "My Second Project22",
     description: "Lorem ipsum",
     created_at: "2019-12-05 12:48:01",
     updated_at: "2019-12-07 06:46:38",
   }

8. Views에 관계 나타내기

Show.blade.php에서도 마찬가지로 메서드이지만 프로퍼티를 호출하는 방식으로 프로젝트에 대한 태스크를 보여 줄 수 있다. 추가로, 태스크가 존재할 경우에만 div 태그가 보이도록 cout()로 분기를 적용함.

<?php if($project->tasks->count()) : ?>
  <div>
  	<?php foreach ($project->tasks as $task) : ?>
    	<li> {{ $task->description }}</li>
    <?php endforeach ?>
  </div>
<?php endif ?>

Laravel 5.7 From Scratch 07 - Route Model Binding / Mass Assignment

|

1. Route Model Binding

Laravel route model binding provides a convenient way to automatically inject the model instances directly into your routes. For example, instead of injecting a user’s ID, you can inject the entire User model instance that matches the given ID.

route model binding을 이용하여, $id 를 파라미터로 받아, $project = Project::find($id); 를 입력하지 않아도 오브젝트를 찾는 것이 가능함.

  • 함수(모델명, 변수명)
public function edit(Project $project)

또한, ProjectController@show 에도 route model binding을 적용할 수 있음.

public function show(Project $project)
{
  return view('projects.show', compact('project'));
}
@extends('layout')

@section('content')
    <h1></h1>
    <div></div>
    <p>
        <a href="/projects//edit">edit </a>
    </p>
@endsection

2. Mass Assignment

모델인 Project.php에서 massive assignment에 대한 설정을 할 수 있음. massive assignment를 통해 HTML으로 입력받을 수 있는 데이터베이스의 필드를 설정 할 수 있음. 이러한 설정을 하지 않을 경우, 브라우저의 개발자 도구를 악용하여 hidden으로 요청을 보내 데이터베이스를 조작하는 것이 가능해짐.

massive assignment를 설정하는 방법은 크게 아래와 같이 2가지 방법이 있음.

  • protected $fillable = [] : 입력이 가능한 필드명을 배열안에 저장
  • protected $guarded = [] : 입력이 불가능한 필드명을 배열에 저장. 빈 배열일 경우 모든 필드를 허용함
class Project extends Model
{
    protected $fillable = [
        'title', 'description'
    ];

//    protected $guarded = [
//    ];
}

3. Clean up the codes

마지막으로 ProjectController.php - 메서드 (store , update ) 내 짜여진 코드를 좀 더 clean하게 변경 시킬 수 있음

// before
public function store()
{
  $project = new Project();
  $project->title = request('title');
  $project->description = request('description');
  $project->save();

  return redirect('/projects');
}

public function update($id)
{
  $project = Project::find($id);
  $project->title = request('title');
  $project->description = request('description');
  $project->save();
  return redirect('/projects');
}

// after
public function store()
{
  Project::create(request(['title','description']));

  return redirect('/projects');
}

public function update(Project $project)
{
  $project->update(request(['title', 'description']));

  return redirect('/projects');
}