Laravel で Ruby on Rails チュートリアル【13章】

## 環境
- Laravel 5.5
- PHP 7.2

# この章でやること
- [13.1 Micropostモデル](#13-1-)
 - [13.1.1 基本的なモデル](#13-1-1-)
 - [13.1.2 Micropostのバリデーション](#13-1-2-)
 - [13.1.3 User/Micropostの関連付け](#13-1-3-)
 - [13.1.4 マイクロポストを改良する](#13-1-4-)
- [13.2 マイクロポストを表示する](#13-2-)
 - [13.2.1 マイクロポストの描画](#13-2-1-)
 - [13.2.2 マイクロポストのサンプル](#13-2-2-)
 - [13.2.3 プロフィール画面のマイクロポストをテスト](#13-2-3-)
- [13.3 マイクロポストを操作する](#13-3-)
 - [13.3.1 マイクロポストのアクセス制御](#13-3-1-)
 - [13.3.2 マイクロポストを作成する](#13-3-2-)
 - [13.3.3 フィードの原型](#13-3-3-)
 - [13.3.4 マイクロポストを削除する](#13-3-4-)
 - [13.3.5 フィード画面のマイクロポストをテストする](#13-3-5-)
- [13.4 マイクロポストの画像投稿](#13-4-)
 - [13.4.1 基本的な画像のアップロード](#13-4-1-)
 - [13.4.2 画像の検証](#13-4-2-)
 - [13.4.3 画像のリサイズ](#13-4-3-)
 - [13.4.4 本番環境での画像アップロード](#13-4-4-)
- [13.5 最後に](#13-5-)

# 13.1 Micropostモデル

# 13.1.1 基本的なモデル

(/app/Micropost.php)
```php
class Micropost extends Model
{
    public function user()
    {
        return $this->belongsTo("App\User");
    }
}
```
(/database/migrations/[timestamp]_create_microposts_table.php)
```php
class CreateMicropostsTable extends Migration
{
    public function up()
    {
        Schema::create('microposts', function (Blueprint $table) {
            $table->increments('id');
            $table->text('content');
            $table->integer('user_id');
            $table->timestamps();

            $table->foreign('user_id')->references('id')->on('users');
        });
    }

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

# 13.1.2 Micropostのバリデーション

(/app/Http/Controllers/MicropostsController.php)
```php
    public function store(Request $request)
    {
        $request->validate([
            'content' => 'required|max:140'
        ]);
    }
```

# 13.1.3 User/Micropostの関連付け

(/app/Microposts.php)
```php
class Micropost extends Model
{
    protected $guarded = ['id'];

    public function user()
    {
        return $this->belongsTo("User");
    }
}
```
(/app/User.php)
```php
class User extends Authenticatable
{
    public function microposts()
    {
        return $this->hasMany("App\Micropost");
    }
}
```

# 13.1.4 マイクロポストを改良する

(/tests/Unit/MicropostTest.php)
```php
    public function testMicropostOrder()
    {
        $this->assertEquals(Micropost::orderBy("created_at", "desc")->first(), Micropost::first());
    }
```
(/database/seeds/tests/TestSeeder.php)
```php
class TestSeeder extends Seeder
{
    public function run()
    {
        Micropost::create([
            "content" => "I just ate an orange!",
            "created_at" => Carbon::now()->subminutes(10)
        ]);
        Micropost::create([
            "content" => "Check out the @tauday site by @mhartl: http://tauday.com",
            "created_at" => Carbon::now()->subYears(3)
        ]);
        Micropost::create([
            "content" => "Sad cats are sad: http://youtu.be/PKffm2uI4dk",
            "created_at" => Carbon::now()->subHours(2)
        ]);
        Micropost::create([
            "content" => "Writing a short test",
            "created_at" => Carbon::now()
        ]);
    }
}
```
(/app/Micropost.php)
```php
    protected static function boot()
    {
        parent::boot();
        static::addGlobalScope('created_at', function (Builder $builder) {
            $builder->orderBy('created_at', 'desc');
        });
    }
```
(/database/migrations/[timestamp]_create_microposts_table.php)
```php
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
```

# 13.2 マイクロポストを表示する

# 13.2.1 マイクロポストの描画

(/resources/views/microposts/micropost.blade.php)
```html
<li id="micropost-{{ $micropost->id }}">
    <a href="{{ route("users.show", $micropost->user_id) }}">{!! gravatar_for($micropost->user, ["size" => 50]) !!}</a>
    <span class="user">{{ Html::linkRoute("users.show", $micropost->user->name, $micropost->user_id) }}</span>
    <span class="content">{{ $micropost->content }}</span>
    <span class="timestamp">
        Posted {{ time_ago_in_words($micropost->created_at) }} ago.
    </span>
</li>
```
(/app/Http/Controller/UsersController)
```php
            $microposts = $user->microposts()->paginate(30);
            return view('users.show')->with(['user' => $user, 'microposts' => $microposts]);
```
(/resouces/Views/users/show.blade.php)
```html
    <div class="col-md-8">
        @if ($user->microposts())
            <h3>Microposts ({{ $user->microposts()->count() }})</h3>
            <ol class="microposts">
                @foreach ($microposts as $micropost)
                    @include("microposts.micropost")
                @endforeach
            </ol>
            {{ $microposts->links() }}
        @endif
    </div>
```
(/app/helper.php)
```php
if (! function_exists('time_ago_in_words')) {
    function time_ago_in_words($date)
    {
        return \Carbon\Carbon::parse($date)->diffForHumans();
    }
}
```

# 13.2.2 マイクロポストのサンプル

(/database/Factories/MicropostFactory.php)
```php
$factory->define(Micropost::class, function (Faker $faker) {
    return [
        'content' => $faker->text,
        'created_at' => $faker->dateTimeThisYear,
    ];
});
```
(/database/seeds/DatabaseSeeder.php)
```php
        User::take(6)->get()->each(function ($u) {
            $u->microposts()->saveMany(Factory(Micropost::class, 50)->make(["user_id" => $u["id"]]));
        });
```

# 13.2.3 プロフィール画面のマイクロポストをテスト

(/database/seeds/test/TestSeeder.php)
```php
        Micropost::create([
            "content" => "I just ate an orange!",
            "created_at" => Carbon::now()->subminutes(10),
            "user_id" => 1
        ]);
        Micropost::create([
            "content" => "Check out the @tauday site by @mhartl: http://tauday.com",
            "created_at" => Carbon::now()->subYears(3),
            "user_id" => 1
        ]);
        Micropost::create([
            "content" => "Sad cats are sad: http://youtu.be/PKffm2uI4dk",
            "created_at" => Carbon::now()->subHours(2),
            "user_id" => 1
        ]);
        Micropost::create([
            "content" => "Writing a short test",
            "created_at" => Carbon::now(),
            "user_id" => 1
        ]);

        factory(Micropost::class, 30)->create(["user_id" => 1, "created_at" => Carbon::now()->subDays(42)]);
```

(/tests/Feature/UsersProfileTest.php)
```php
class UsersProfileTest extends TestCase
{
    private $user;

    protected function setUp()
    {
        parent::setUp();
        Artisan::call('migrate:fresh');
        $this->seed('TestSeeder');
        $this->user = User::find(1);
    }

    public function testProfileDisplay()
    {
        $response = $this->get(route("users.show", $this->user->id));
        $response->assertViewIs("users.show");
        $dom = $this->dom($response->content());
        $this->assertSame(full_title($this->user->name), $dom->filter("title")->text());
        $this->assertRegExp("/".$this->user->name."/", $dom->filter('h1')->text());
        $this->assertSame(1, $dom->filter("h1>img.gravatar")->count());
        $response->assertSeeText((string) $this->user->microposts()->count());
        $this->assertSame(1, $dom->filter("ul.pagination")->count());
        foreach ($this->user->microposts()->paginate(30) as $micropost) {
            $response->assertSeeText($micropost->content);
        }
    }
}
```

# 13.3 マイクロポストを操作する

(/routes/web.php)
```php
Route::resource('microposts', "MicropostsController", ["only"=> ['store', 'destroy']]);
```

# 13.3.1 マイクロポストのアクセス制御

(/tests/Unit/MicropostsControllerTest.php)
```php
class MicropostsControllerTest extends TestCase
{
    private $micropost;

    protected function setUp()
    {
        parent::setUp();
        Artisan::call('migrate:fresh');
        $this->seed('TestSeeder');
        $this->micropost = Micropost::find(1);
    }

    public function testRedirectCreate()
    {
        $count = Micropost::all()->count();
        $response = $this->post(route("microposts.store", ["content" => "Lorem ipsum"]));
        $this->assertEquals($count, Micropost::all()->count());
        $response->assertRedirect(route("login"));
    }

    public function testRedirectDestroy()
    {
        $count = Micropost::all()->count();
        $response = $this->delete(route("microposts.destroy", $this->micropost->id));
        $this->assertEquals($count, Micropost::all()->count());
        $response->assertRedirect(route("login"));
    }
}
```
(/app/Http/Controllers/MicropostsController.php)
```php
class MicropostsController extends Controller
{
    public function __construct()
    {
        $this->middleware('authenticate')->only(["store", "destroy"]);
    }
}
```

# 13.3.2 マイクロポストを作成する

(/app/Http/Controllers/MicropostsController.php)
```php
    public function store(Request $request)
    {
        $request->validate([
            'content' => 'required|max:140'
        ]);
        $micropost = new Micropost(["content" => $request->content]);
        Auth::user()->microposts()->save($micropost);
        session()->flash('message', ['success' => 'Micropost created!']);
        return redirect("/");
    }
```

(/resources/views/static_pages/home.blade.php)
```html
@extends('layouts.application')

@section('content')
@if (Auth::check())
    <div class="row">
        <aside class="col-md-4">
            <section class="user_info">
                @include("shared.user_info")
            </section>
            <section class="micropost_form">
                @include("shared.micropost_form")
            </section>
        </aside>
    </div>
@else
    <div class="center jumbotron">
        <h1>Welcome to the Sample App</h1>

        <h2>
            This is the home page for the
            <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
            sample application.
        </h2>

        {{ Html::linkRoute('signup', "Sign up now!", [], ["class" => "btn btn-lg btn-primary"]) }}
    </div>

    <a href="http://rubyonrails.org/">{{ Html::image("img/rails.png", "Rails logo") }}</a>
@endif
@endsection
```
(/resources/views/shared/user_info.blade.php)
```html
<a href="{{ route("users.show", Auth::id()) }}">{!! gravatar_for(Auth::user(), ["size" => 50]) !!}</a>
<h1>{{ Auth::user()->name }}</h1>
<span>{{ Html::linkRoute("users.show", "view my profile", Auth::id()) }}</span>
<span>{{ Auth::user()->microposts()->count() . " " . str_plural('micropost', Auth::user()->microposts()->count()) }}</span>
```
(/resources/views/shared/micropost_form.blade.php)
```html
{{ Form::open(["route" => "microposts.store"]) }}
@include('shared.error_messages')
<div class="field">
    {{ Form::textarea('content', null, ["placeholder" => "Compose new micropost..."]) }}
</div>
{{ Form::submit("Post", ["class" => "btn btn-primary"]) }}
{{ Form::close() }}
```

# 13.3.3 フィードの原型

(/app/Http/Controllers/StaticPagesController.php)
```php
    public function home()
    {
        $feed_items = null;
        if (Auth::check()) {
            $feed_items = Auth::user()->microposts()->paginate(30);
        }
        return view('static_pages/home')->with("feed_items", $feed_items);
    }
```
(/resources/views/shared/feed.blade.php)
```html
<ol class="microposts">
    @foreach ($feed_items as $micropost)
        @include("microposts.micropost")
    @endforeach
</ol>
{{ $feed_items->links() }}
```
(/resources/views/static_pages/home.blade.php)
```html
@extends('layouts.application')

@section('content')
@if (Auth::check())
    <div class="row">
        <aside class="col-md-4">
            <section class="user_info">
                @include("shared.user_info")
            </section>
            <section class="micropost_form">
                @include("shared.micropost_form")
            </section>
        </aside>
        <div class="col-md-8">
            <h3>Micropost Feed</h3>
            @includeWhen($feed_items, "shared.feed")
        </div>
    </div>
@else
    <div class="center jumbotron">
        <h1>Welcome to the Sample App</h1>

        <h2>
            This is the home page for the
            <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
            sample application.
        </h2>

        {{ Html::linkRoute('signup', "Sign up now!", [], ["class" => "btn btn-lg btn-primary"]) }}
    </div>

    <a href="http://rubyonrails.org/">{{ Html::image("img/rails.png", "Rails logo") }}</a>
@endif
@endsection
```

# 13.3.4 マイクロポストを削除する

(/resources/views/microposts/micropost.blade.php)
```html
<li id="micropost-{{ $micropost->id }}">

    <a href="{{ route("users.show", $micropost->user_id) }}">{!! gravatar_for($micropost->user, ["size" => 50]) !!}</a>
    <span class="user">{{ Html::linkRoute("users.show", $micropost->user->name, $micropost->user_id) }}</span>
    <span class="content">{{ $micropost->content }}</span>
    <span class="timestamp">
        Posted {{ time_ago_in_words($micropost->created_at) }} ago.
        @if (Auth::id() == $micropost->user_id)
            <a href="javascript:if(window.confirm('Yes Sure?')){document.deleteform{{ $micropost->id }}.submit()}">delete</a>
            {{ Form::open(["route" => ["microposts.destroy", $micropost->id], "method" => "delete", "name" => "deleteform{$micropost->id}"]) }}
            {{ Form::close() }}
        @endif
    </span>
</li>
```
(/app/Http/Controllers/MicropostsController.php)
```php
    public function __construct()
    {
        $this->middleware(function ($request, $next) {

            $micropost = Auth::user()->microposts()->where("id", $request->micropost);
            if ($micropost->count() === 0) {
                return redirect("/");
            }
            return $next($request);
        })->only(["destroy"]);
    }

    public function destroy($id)
    {
        Micropost::find($id)->delete();
        session()->flash('message', ['success' => 'Micropost deleted']);
        return back();
    }
```

# 13.3.5 フィード画面のマイクロポストをテストする

(/database/seeds/test/TestSeeder.php)
```php
        Micropost::create([
            "content" => "Oh, is that what you want? Because that's how you get ants!",
            "created_at" => Carbon::now()->subYears(2),
            "user_id" => 2
        ]);
        Micropost::create([
            "content" => "Danger zone!",
            "created_at" => Carbon::now()->subDays(3),
            "user_id" => 2
        ]);
        Micropost::create([
            "content" => "I'm sorry. Your words made sense, but your sarcastic tone did not.",
            "created_at" => Carbon::now()->subMinutes(10),
            "user_id" => 3
        ]);
        Micropost::create([
            "content" => "Dude, this van's, like, rolling probable cause.",
            "created_at" => Carbon::now()->subHours(4),
            "user_id" => 3
        ]);
```
(/tests/Unit/MicropostsControllerTest.php)
```php
    public function testRedirectDestroyWrongMicropost()
    {
        $this->be(User::find(1));
        $micropost = Micropost::find(5);
        $count = Micropost::all()->count();
        $response = $this->delete(route("microposts.destroy", $micropost->id));
        $this->assertEquals($count, Micropost::all()->count());
        $response->assertRedirect("/");
    }
```
(/tests/Feature/MicropostsInterfaceTest.php)
```php
class MicropostsInterfaceTest extends TestCase
{
    private $user;

    protected function setUp()
    {
        parent::setUp();
        Artisan::call('migrate:fresh');
        $this->seed('TestSeeder');
        $this->user = User::find(1);
    }

    public function testMicropostInterface()
    {
        $this->be($this->user);
        $response = $this->get("/");
        $this->assertSame(1, $this->dom($response->content())->filter("ul.pagination")->count());
        $count = Micropost::all()->count();
        $response = $this->followingRedirects()->post(route("microposts.store"), ["content" => " "]);
        $this->assertEquals($count, Micropost::all()->count());
        $this->assertSame(1, $this->dom($response->content())->filter("div#error_explanation")->count());
        $content = "This micropost really ties the room together";
        $count = Micropost::all()->count();
        $response = $this->followingRedirects()->post(route("microposts.store"), ["content" => $content]);
        $this->assertEquals($count + 1, Micropost::all()->count());
        $response->assertViewIs("static_pages.home");
        $response->assertSeeText($content);
        $this->assertGreaterThan(1, $this->dom($response->content())->filter("a:contains(\"delete\")")->count());
        $first_micropost = $this->user->microposts->first();
        $count = Micropost::all()->count();
        $response = $this->followingRedirects()->delete(route("microposts.destroy", $first_micropost->id));
        $this->assertEquals($count - 1, Micropost::all()->count());
        $response = $this->get(route("users.show", 2));
        $this->assertSame(0, $this->dom($response->content())->filter("a:contains(\"delete\")")->count());
    }
}
```

#### 課題

(/tests/Feature/MicropostsInterfaceTest.php)
```php
    public function testSidebarCount()
    {
        $this->be($this->user);
        $response = $this->get("/");
        $response->assertSeeText("{$this->user->microposts()->count()} microposts");
        $other_user = User::find(4);
        $this->be($other_user);
        $response = $this->get("/");
        $response->assertSeeText("0 micropost");
        $other_user->microposts()->create(["content" => "A micropost"]);
        $response = $this->get("/");
        $response->assertSeeText("A micropost");
    }
```

# 13.4 マイクロポストの画像投稿

# 13.4.1 基本的な画像のアップロード

```
composer require intervention/image
```
(.env)
```
FILESYSTEM_DRIVER=public
```
(/database/migrations/[timestamp]_add_picture_to_microposts.php)
```php
class AddPictureToMicroposts extends Migration
{
    public function up()
    {
        Schema::table('microposts', function (Blueprint $table) {
            $table->string('picture')->nullable();
        });
    }

    public function down()
    {
        Schema::table('microposts', function (Blueprint $table) {
            $table->dropColumn('picture');
        });
    }
}
```
(/app/Http/Controllers/MicropostsController.php)
```php
    public function store(Request $request)
    {
        $request->validate([
            'content' => 'required|max:140'
        ]);
        $micropost = new Micropost;
        $micropost->content = $request->content;
        if ($request->hasFile('picture')) {
            $file = $request->picture;
            $path = "micropost_proto/" . $file->hashName();
            $encode_file = Image::make($file)->encode();
            Storage::put($path, (string) $encode_file, "public");
            $micropost->picture = $path;
        }
        Auth::user()->microposts()->save($micropost);
        session()->flash('message', ['success' => 'Micropost created!']);
        return redirect("/");
    }
```
(/resources/views/shared/micropost_form.blade.php)
```html
{{ Form::open(["route" => "microposts.store", 'files' => true]) }}
@include('shared.error_messages')
<div class="field">
    {{ Form::textarea('content', null, ["placeholder" => "Compose new micropost..."]) }}
</div>
{{ Form::submit("Post", ["class" => "btn btn-primary"]) }}
<span class="picture">
    {{ Form::file("picture") }}
</span>
{{ Form::close() }}
```
(/resources/views/microposts/micropost.blade.php)
```html
    <span class="content">
        {{ $micropost->content }}
        @if ($micropost->picture)
            <img src="{{ Storage::url($micropost->picture) }}">
        @endif
    </span>
```
ストレージフォルダにaliasを貼る
```
php artisan storage:link
```

#### 課題

(/tests/Feature/MicropostsInterfaceTest.php)
```php
    public function testMicropostInterface()
    {
        Storage::fake('design');

        $this->be($this->user);
        $response = $this->get("/");
        $this->assertSame(1, $this->dom($response->content())->filter("ul.pagination")->count());
        $this->assertSame(1, $this->dom($response->content())->filter("input[type=file]")->count());
        $count = Micropost::all()->count();
        $response = $this->followingRedirects()->post(route("microposts.store"), ["content" => " "]);
        $this->assertEquals($count, Micropost::all()->count());
        $this->assertSame(1, $this->dom($response->content())->filter("div#error_explanation")->count());
        $content = "This micropost really ties the room together";
        $picture = UploadedFile::fake()->image('design.jpg');
        $count = Micropost::all()->count();
        $response = $this->followingRedirects()
                        ->post(route("microposts.store"), [
                            "content" => $content,
                            "picture" => $picture
                        ]);
        $this->assertEquals($count + 1, Micropost::all()->count());
        $response->assertViewIs("static_pages.home");
        $response->assertSeeText($content);
        $this->assertGreaterThan(0, $this->dom($response->content())->filter("a:contains(\"delete\")")->count());
        $first_micropost = $this->user->microposts->first();
        $count = Micropost::all()->count();
        $response = $this->followingRedirects()->delete(route("microposts.destroy", $first_micropost->id));
        $this->assertEquals($count - 1, Micropost::all()->count());
        $response = $this->get(route("users.show", 2));
        $this->assertSame(0, $this->dom($response->content())->filter("a:contains(\"delete\")")->count());
    }
```

# 13.4.2 画像の検証

(/app/Http/Controllers/MicropostController.php)
```php
        $request->validate([
            'content' => 'required|max:140',
            'picture' => 'nullable|mimes:jpeg,gif,png|image|max:5120'
        ]);
```
(/resources/views/shared/micropost_form.blade.php)
```html
<script type="text/javascript">
    $('#micropost_picture').bind('change', function() {
      var size_in_megabytes = this.files[0].size/1024/1024;
      if (size_in_megabytes > 5) {
        alert('Maximum file size is 5MB. Please choose a smaller file.');
      }
    });
</script>
```

# 13.4.3 画像のリサイズ

(/app/Http/Controllers/MicropostsController.php)
```php
    public function store(Request $request)
    {
        $request->validate([
            'content' => 'required|max:140',
            'picture' => 'nullable|mimes:jpeg,gif,png|image|max:5120'
        ]);
        $micropost = new Micropost;
        $micropost->content = $request->content;
        if ($request->hasFile('picture')) {
            $file = $request->picture;
            $path = "micropost_proto/" . $file->hashName();
            $encode_file = Image::make($file)->resize(400, 400, function ($constraint) {
                $constraint->aspectRatio();
                $constraint->upsize();
            })->encode();
            Storage::put($path, (string) $encode_file, "public");
            $micropost->picture = $path;
        }
        Auth::user()->microposts()->save($micropost);
        session()->flash('message', ['success' => 'Micropost created!']);
        return redirect("/");
    }
```

# 13.4.4 本番環境での画像アップロード

本番環境ではs3を使用する
```
composer require league/flysystem-aws-s3-v3
```
(本番環境の.env)
```
AWS_ACCESS_KEY_ID="作成したアクセスキー"
AWS_SECRET_ACCESS_KEY="作成したシークレットキー"
AWS_DEFAULT_REGION=ap-northeast-1(東京の場合)
AWS_BUCKET="作成したバケット"

FILESYSTEM_DRIVER=s3
```
```
heroku config:set AWS_ACCESS_KEY_ID="作成したアクセスキー"
heroku config:set AWS_SECRET_ACCESS_KEY="作成したシークレットキー"
heroku config:set AWS_DEFAULT_REGION=ap-northeast-1(東京の場合)
heroku config:set AWS_BUCKET="作成したバケット"

heroku config:set FILESYSTEM_DRIVER=s3
```
s3を用意する
https://qiita.com/tiwu_official/items/ecb115a92ebfebf6a92f

# 13.5 最後に

herokuにgdをインストールする
(composer.json)
```json
    "require": {
        "ext-gd": "*",
```
```
composer update
```
herokuのnginxとphp.iniでファイルのアップロードサイズにかかっている制限を修正
(/Procfile)
```
web: vendor/bin/heroku-php-nginx -C heroku_nginx.conf public/
```
(/heroku_nginx.conf)
```
client_max_body_size 20M;
```
(/public/.user.ini)
```
post_max_size = 20M
upload_max_filesize = 5M
```
# 参考

https://qiita.com/Yorinton/items/0ca1f2802244581afc83
http://kayakuguri.github.io/blog/2017/06/16/larave-std-error/