Laravel7.x、EC2でphp artisan down –allow=${ip}が効かない時

本番環境でメンテナンスモードにして、特定のIPだけ操作を許可したい場合は、

$ php artisan down --allow=${ip}

と書く。EC2で実行したところ、指定したipでもメンテナンスモードの画面に。

EC2 ELB: ELBを使って、SSL対応している

ELBを挟んでいるため、ip取得のclassにHTTP_X_FORWARDED_FORを追加する
$ sudo vi vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/CheckForMaintenanceMode.php

public function handle($request, Closure $next)
    {
        if ($this->app->isDownForMaintenance()) {
            $data = json_decode(file_get_contents($this->app->storagePath().'/framework/down'), true);

            // 追加
            if (isset($data['allowed']) && IpUtils::checkIp($request->server->get('HTTP_X_FORWARDED_FOR'), (array) $data['allowed'])) {
                return $next($request);
            }

            if (isset($data['allowed']) && IpUtils::checkIp($request->ip(), (array) $data['allowed'])) {
                return $next($request);
            }

            if ($this->inExceptArray($request)) {
                return $next($request);
            }

            throw new MaintenanceModeException($data['time'], $data['retry'], $data['message']);
        }

        return $next($request);
    }

※HTTP_X_FORWARDED_FOR: リバースプロキシを挟んだアクセス元のIPを取得する

これで、メンテナンスモード時に、特定IPのみ操作できるようになります。
$ php artisan down –allow=${myIP}
$ php artisan up

Laravel 6.x command(スケジューラ)の使い方

### make:command
$ php artisan make:command TestCommand

app/Console/Commands/TestCommand.php

protected $signature = 'command:testcommand';
public function handle()
    {
        //
        echo "test command execute!";
    }

app/Console/Kernel.php

 protected $commands = [
        //
        \App\Console\Commands\TestCommand::class,
    ];

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        // $schedule->command('inspire')
        //          ->hourly();
        $schedule->command('command:testcommand')->daily();
    }

### command実行
$ php artisan list;
$ php artisan command:testcommand;

$ crontab -e

実用性から考えると、バッチ処理はcronで直書きの方が楽そうですが、Githubなどで全てソースコードで管理したい場合は使えるかもしれません。どちらが良いかは好みやコンセンサスでしょうか。

Laravel Logの設定

### .env
Log channelはdefaultでstackに設定されている

LOG_CHANNEL=stack

### config/logging.php
stackで、channelsは’single’に設定されています。

'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['single'],
            'ignore_exceptions' => false,
        ],

        'single' => [
            'driver' => 'single',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'debug',
        ],
        // 省略
    ],

### ログの出力先
storate/logs/laravel.log

###コマンドラインで出力
less +F storage/logs/laravel.log

### controllerからログ出力
$ php artisan make:controller –resource LogController

Route::get('log', 'LogController@index');
public function index()
    {
        //
        \Log::info('ログ出力test');
        return 'test';
    }

heplerを使う場合

logger()->info('something has happened');

laravel.log

[2020-02-24 14:13:10] local.INFO: ログ出力test 
[2020-02-24 14:22:10] local.INFO: something has happened  

### ログレベル
emergency, alert, critical, error, warning, notice, info, debug

ログの設定は、運用体制と併せて柔軟に検討する必要がある。

laravelでcsv出力

### view
csvダウンロードボタンを作り、postメソッドのhiddenで引数を渡します。

{!! Form::open(['method'=>'POST', 'action'=>['HogeController@download'] ]) !!}
 <input type="hidden" name="period" value="{{ $inputs&#91;'data'&#93; }}">
 {!! Form::submit('CSVダウンロード',['class'=>'btn', 'name'=>'csv']) !!}
{!! Form::close() !!}

### route
csvファイルをreturnするだけなので、設計のコンベンションが無ければディレクトリは然程きにする必要なし。

Route::post('/dir/csv', 'HogeController@download');

### controller

use Symfony\Component\HttpFoundation\StreamedResponse;
public function download(Request $request){
// 省略

// CSV生成
        $response = new StreamedResponse(function() use($data1, $data2){
                $stream = fopen('php://output', 'w');
                stream_filter_prepend($stream, 'convert.iconv.utf-8/cp932//TRANSLIT');
                fputcsv($stream, ['id','raw1','raw2','raw3','raw4','raw5']);
                foreach($data as $value){
                     fputcsv($stream, ['id',$value->raw1,$value->raw2,$value->raw3,$value->raw4,$value->raw5]);
                }
                fclose($stream);
        });
        $response->headers->set('Content-Type', 'application/octet-stream');
        $response->headers->set('Content-Disposition', 'attachment; filename="user.csv"');
        return $response;

特段ストレージやS3などに一時保存しなくて良いので、シンプルだ。
上記はユーザがダウンロードボタンを押した時の処理だが、月次のバッチ処理によるcsv出力なども比較的簡単に実装できそうだ。

LaravelでtinyMCE & filemanagerの使い方

tinyMCEだと、textareaで文字の装飾はもちろん画像の挿入ができる

### viewの作成
views/includes/tinyeditor.blade.php

<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/5/tinymce.min.js" referrerpolicy="origin"></script>
<script>tinymce.init({selector:'textarea'});</script>

posts/create.blade.php

@extends('layouts.admin')

@section('content')

    @include('includes.tinyeditor')
...

### filemanager install
https://unisharp.github.io/laravel-filemanager/installation

$ php composer.phar require unisharp/laravel-filemanager
$ php composer.phar require intervention/image

Unisharp\Laravelfilemanager\LaravelFilemanagerServiceProvider::class,
        Intervention\Image\ImageServiceProvider::class,

'Image' => Intervention\Image\Facades\Image::class,

$ php artisan vendor:publish –tag=lfm_config
$ php artisan vendor:publish –tag=lfm_public

### tinyeditor.blade.php

<script src="//cdn.tinymce.com/4/tinymce.min.js"></script>
<script>
  var editor_config = {
    path_absolute : "/",
    selector: "textarea",
    plugins: [
      "advlist autolink lists link image charmap print preview hr anchor pagebreak",
      "searchreplace wordcount visualblocks visualchars code fullscreen",
      "insertdatetime media nonbreaking save table contextmenu directionality",
      "emoticons template paste textcolor colorpicker textpattern"
    ],
    toolbar: "insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media",
    relative_urls: false,
    file_browser_callback : function(field_name, url, type, win) {
      var x = window.innerWidth || document.documentElement.clientWidth || document.getElementsByTagName('body')[0].clientWidth;
      var y = window.innerHeight|| document.documentElement.clientHeight|| document.getElementsByTagName('body')[0].clientHeight;

      var cmsURL = editor_config.path_absolute + 'laravel-filemanager?field_name=' + field_name;
      if (type == 'image') {
        cmsURL = cmsURL + "&type=Images";
      } else {
        cmsURL = cmsURL + "&type=Files";
      }

      tinyMCE.activeEditor.windowManager.open({
        file : cmsURL,
        title : 'Filemanager',
        width : x * 0.8,
        height : y * 0.8,
        resizable : "yes",
        close_previous : "no"
      });
    }
  };

  tinymce.init(editor_config);
</script>

### config
lfm.php

'base_directory' => 'public',
'images_folder_name' => 'images',

### view
post.blade.php

<p>{!! $post->body !!}</p>

{{$post->body}}だと画像が表示されないので注意が必要
殆ど公式に書いてあるので、公式との闘いになりそう

Gravatarとは?

https://ja.gravatar.com/support/what-is-gravatar/

Model User.php

public function getGravatarAttribute(){
        $hash = md5(strtolower(trim($this->attribute['email'])));
        return "http://www.gravatar.com/avatar/$hash";
    }

Viewのimg srcでAuth::user()->gravatar()と書くと、gravatarに登録した画像が表示される

公式ドキュメント
https://ja.gravatar.com/site/implement/images/
https://www.gravatar.com/avatar/${hashedUrl}

あんまり使ってるの見たことないけど、アバターのマイナンバーみたいな発想は面白い

LaravelでのSessionの使い方

## sessionの表示
Requestメソッドを使う

public function index(Request $request)
    {
        return $request->session()->all();
    }

## Set session
$request->session()->put([‘$key’=>’$value’]);とします

public function index(Request $request)
    {
        $request->session()->put(['peter'=>'artist']);
        return view('home');
    }

## Global function of session
globaの場合は、putを書く必要がない

public function index(Request $request)
    {
        session(['peter'=>'hoge']);
        
        return $request->session()->all();
        // return view('home');
    }

## sessionの読み込み

public function index(Request $request)
    {
        return $request->session()->get('peter');
        // return view('home');
    }

globalの場合

return session('peter');

## session削除

public function index(Request $request)
    {
        $request->session()->forget('peter');
        return $request->session()->all();
    }

## sessionデータを全て削除

$request->session()->flush();

## flush

$request->session()->flush('message', 'Post has been created');
return $request->session()->get('message');

sessionが使えるということは、cookieも使えるのでしょうか?
sessionやcookieを扱えると、アプリケーションの幅が広がりますね。

LaravelのMiddleware Security/Protection

Middlewareを学びます。Middlewareというと何を思い浮かべるでしょうか?
ミドルウェアはその名の通り、OSとアプリケーションの中間にあるソフトウェアという意味です。
フレームワークLaravelでのMiddlewareとはどういう意味でしょうか?そもそもLaravel自体はOSではなく、アプリケーションのソフトウェアです。そのフレームワークにミドルウェアという機能があるのは何か虎に睨まれたようです。では具体的に見ていきましょう。

まず、新規にプロジェクトを立ち上げます
$ php artisan make:auth
$ php artisan migrate

./app/Http/kernel.php

protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'can' => \Illuminate\Foundation\Http\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    ];

./app/Http/Middleware
Authenticate.php
EncryptCookies.php
RedirectIfAutenticated.php
VerifyCsrfToken.php

Authenticate.php

public function handle($request, Closure $next, $guard = null)
    {
        if (Auth::guard($guard)->guest()) {
            if ($request->ajax() || $request->wantsJson()) {
                return response('Unauthorized.', 401);
            } else {
                return redirect()->guest('login');
            }
        }

        return $next($request);
    }

$ php artisan make:middleware RoleMiddleware
Middleware created successfully.

$ php artisan down

Kernel.php

protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'can' => \Illuminate\Foundation\Http\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'role'=>\App\Http\Middleware\RoleMiddleware::class,
    ];

Route

Route::get('/admin/user/roles', ['middleware'=>'role', function(){

	return "Middleware role";
}]);

app/Http/Middleware/RoleMiddleware.php

public function handle($request, Closure $next)
    {
        return redirect('/');

        return $next($request);
    }

/admin/user/roles にアクセスするとリダイレクトされます。

Laravel ファイルアップロード時のコントローラーの書き方

普通にHTMLで書いた場合

<form action="/uploadfile" method="post" enctype="multipart/form-data">
	@csrf
	<div class="form-group">
		<input type="file" class="form-control-file" name="fileToUpload" id="exampleInputFile">
	</div>
	<button type="submit" class="btn btn-primary">Submit</button>
</form>

collectiveのForm::openのthird parameterに’files’=>trueを追加する
Form:file(‘file’)とする

{!! Form::open(['method'=>'POST', 'action'=>'PostsController@store', 'files'=>true]) !!}
		{{ csrf_field()}}

		<div class="form-group">
			{!! Form::file('file', ['class'=>'form-controll']) !!}
		</div>

		<div class="form-group">
			{!! Form::label('title', 'Title') !!}
			{!! Form::text('title', null, ['class'=>'form-controll']) !!}
		</div>

		<div class="form-group">
			{!! Form::submit('Create Post', ['class'=>'btn btn-primary']) !!}

		</div>
	{!! Form::close() !!}

controller

public function store(CreatePostRequest $request)
    {
        return $request->file('file');
    }

オリジナル名、ファイルサイズ

public function store(CreatePostRequest $request)
    {
        $file = $request->file('file');
        
        echo "<br>";
        echo $file->getClientOriginalName();

        echo "<br>";
        echo $file->getClientSize();
    }

$ php artisan make:migration add_path_column_to_posts –table=posts
migration file

public function up()
    {
        Schema::table('posts', function (Blueprint $table) {
            //
            $table->string('path');
        });
    }

public function down()
    {
        Schema::table('posts', function (Blueprint $table) {
            //
            $talbe->dropColumn('path');
        });
    }

$ php artisan migrate

Post.php

protected $fillable = [
		'user_id',
		'title',
		'content',
		'path'
	];

PostsController.php

public function store(CreatePostRequest $request)
    {

        $input = $request->all();

        if($file = $request->file('file')){
            $name = $file->getClientOriginalName();

            $file->move('./images', $name);

            $input['path'] = $name;

        }

        Post::create($input);
    }

view

<ul>
		@foreach($posts as $post)

		<div class-"image-container">
			<img height="100" src="images/{{$post->path}}">
		</div>
		<li><a href="{{ route('posts.show', $post->id) }}">{{$post->title}}</a></li>
		
		@endforeach
	</ul>

images/{{$post->path}} は、accessorsでimages/を省略する

Model:Post.php

public $directory = "/images/";

public function getPathAttribute($value){

		return $this->directory . $value;
	}

View: index.php

<div class-"image-container">
			<img height="100" src="{{$post->path}}">
		</div>

今回はシンプルな書き方で、ファイルに対する拡張子やファイルサイズのバリデーションをつけていませんが、コントローラーもしくはrequestsでバリデーションを付ければ良いでしょう。

ファイルの格納場所はサーバー内ですが、これをS3にする場合はmove()の箇所を変える必要があります。