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