0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

環境

  • Laravel 5.5
  • PHP 7.2

この章でやること

13.1 Micropostモデル

13.1.1 基本的なモデル

(/app/Micropost.php)

class Micropost extends Model
{
    public function user()
    {
        return $this->belongsTo("App\User");
    }
}

(/database/migrations/[timestamp]_create_microposts_table.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)

    public function store(Request $request)
    {
        $request->validate([
            'content' => 'required|max:140'
        ]);
    }

13.1.3 User/Micropostの関連付け

(/app/Microposts.php)

class Micropost extends Model
{
    protected $guarded = ['id'];

    public function user()
    {
        return $this->belongsTo("User");
    }
}

(/app/User.php)

class User extends Authenticatable
{
    public function microposts()
    {
        return $this->hasMany("App\Micropost");
    }
}

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

(/tests/Unit/MicropostTest.php)

    public function testMicropostOrder()
    {
        $this->assertEquals(Micropost::orderBy("created_at", "desc")->first(), Micropost::first());
    }

(/database/seeds/tests/TestSeeder.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)

    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)

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

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

13.2.1 マイクロポストの描画

(/resources/views/microposts/micropost.blade.php)

<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)

            $microposts = $user->microposts()->paginate(30);
            return view('users.show')->with(['user' => $user, 'microposts' => $microposts]);

(/resouces/Views/users/show.blade.php)

    <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)

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)

$factory->define(Micropost::class, function (Faker $faker) {
    return [
        'content' => $faker->text,
        'created_at' => $faker->dateTimeThisYear,
    ];
});

(/database/seeds/DatabaseSeeder.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)

        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)

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)

Route::resource('microposts', "MicropostsController", ["only"=> ['store', 'destroy']]);

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

(/tests/Unit/MicropostsControllerTest.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)

class MicropostsController extends Controller
{
    public function __construct()
    {
        $this->middleware('authenticate')->only(["store", "destroy"]);
    }
}

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

(/app/Http/Controllers/MicropostsController.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)

@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)

<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)

{{ 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)

    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)

<ol class="microposts">
    @foreach ($feed_items as $micropost)
        @include("microposts.micropost")
    @endforeach
</ol>
{{ $feed_items->links() }}

(/resources/views/static_pages/home.blade.php)

@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)

<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)

    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)

        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)

    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)

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)

    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)

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)

    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)

{{ 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)

    <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)

    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)

        $request->validate([
            'content' => 'required|max:140',
            'picture' => 'nullable|mimes:jpeg,gif,png|image|max:5120'
        ]);

(/resources/views/shared/micropost_form.blade.php)

<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)

    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)

    "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/

0
0
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?