20191124のlaravelに関する記事は11件です。

【テストおじさんへの道】テストをやると幸せになれる

what this is?

これは僕がテストおじさんになるまでの軌跡を書いた記事です。
この記事を社内で共有してテストの文化を浸透させるために書いています。
執筆当時はまだ「テスト赤ちゃん」です。優しく指摘してください。

テストをなぜ行う?

答えは簡単です。
「人間は愚かで、プログラムを操ることは難しいから」
です。

最近では「誰でもプログラマーになれる!」なんて、さもプログラマーの敷居が低いかのような言葉がインターネッツで飛び交っていますが
甘いです。
プログラムは簡単に壊れます。そして人に牙をむきます。
そんな事が当たり前の職業でどうやってプログラムを正常にコントロールするのか…
それが「テスト」です。

テストを書かないとどうなるのか

いくつかあります。
・品質が保証されない
・デバッグのコストが莫大になる
・改修のコストとリスクが莫大になる
・引き継ぎがのリスクが高くなる
などなど…

僕が一番推したいのは
「不安で寝れない」
ということです。
いつ、どこで、どんなときにバグが起こるか、書いた本人ですらわからないのです。
そんな不安をテストは解消してくれます。

テストを書くとどうなるのか

もうメリットがたくさんです。
・品質が保証されているので、安心して運用出来る。
・デバッグのコストが一気に下がる。手動でのフォームテストなどをしなくてもよくなる。
・改修が簡単になる。どこに副作用があるのか簡単に把握出来る。
・引き継ぎが楽になる。とりあえずテストを読めば使用がふんわりわかる。

様々な恩恵を受けることができますが、一つだけデメリットがあります。
それは
「開発コストが上がる」
ということです。
当然今までの開発にプラスしてテストも書くわけですから、仕方ありません。
ですがメリットを考えるとプラマイゼロ、むしろメリットのほうが大きいと思います。

テストをするにはどうする?

僕は主にphpを使うので、テストではphpunitを利用します。
他の言語ではjsunitやrubyunitなど、○○unitでググればなんか出ると思います。

そんなphpunitですが、みんな使っているLaravelに基本搭載されています。
vendor\phpunit\phpunit\phpunitにバイナリデータがあります。コレが正体です。
ですが、結構奥のディレクトリに眠っているので
普段はvendor\phpunit\bin\phpunitというシンボリックリンクがあるので、それを叩きます。

コマンドで
sudo php ./vendor/bin/phpunit
と打つと

PHPUnit 8.4.2 by Sebastian Bergmann and contributors.

と表示されてテストが走ります。

テストを書くにはどうする?

テストはLaravelのtests\Featureの中に置きます。
デフォルトで入っているExampleTest.phpには以下のように書かれています。

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testBasicTest()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

基本的に書き方としては
・test○○○というpublic functionを書く
・$this->get('○○○')でレスポンスを得る
・aeert○○○()でテストする
です。

test○○○というpublic functionを書く

基本的にクラスの上から下へ実行されます。
関数名はtestというプレフィックスを付けなければ自動で実行されません。

$this->get('○○○')でレスポンスを得る

getの他にもpostやputなどがあります。

aeert○○○()でテストする

assert○○○()をアサーションといいます。
assertStatus()やassertSee()などたくさんあります。
このアサーションを組み合わせてプログラムをテストします。

アサーションには、phpunitが用意しているものと
https://phpunit.readthedocs.io/ja/latest/assertions.html

Laravelのヘルパとして用意されているものがあります。
https://readouble.com/laravel/5.7/ja/http-tests.html

まずは書いてみよう

簡単なものを書いてみて動きを自分で確認するのがテストへの第一歩だと思います。
とりあえず触ってみましょう、テストの素晴らしさがわかると思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelの処理の流れコードリーディング(投げたエラーはどこで処理されている???)

概要

Laravelではthrowしたエラーを明示的にcatchしなくてもよしなにしてくれます。

エラーをカスタムしたいなと思ったときにどこでエラーをキャッチしているのか知りたかったのでLaravelの流れを読むことにしました。

start

public/index.php
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

Kernel::handle()のtry句の中身を見ていきます。

vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php
$request->enableHttpMethodParameterOverride();

$response = $this->sendRequestThroughRouter($request);

enableHttpMethodParameterOverride()はプロパティをtrueにセットしているだけなのでスルーします。

sendRequestThroughRouter()は同じクラスに実装されています。

vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php
protected function sendRequestThroughRouter($request)
{
    $this->app->instance('request', $request);

    Facade::clearResolvedInstance('request');

    $this->bootstrap();

    return (new Pipeline($this->app))
                ->send($request)
                ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                ->then($this->dispatchToRouter());
}

$this->app->instance('request', $request)では$requestをサービスコンテナに登録しています。

Facade::clearResolvedInstance('request')はファサードにキャッシュされているrequestをクリアしています。(キャッシュされるタイミングはあるのだろうか?)

$this->bootstrap()でいろいろ準備をしてるみたいです。

vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php
protected $bootstrappers = [
    \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
    \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
    \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
    \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
    \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
    \Illuminate\Foundation\Bootstrap\BootProviders::class,
];

public function bootstrap()
{
    if (! $this->app->hasBeenBootstrapped()) {
        $this->app->bootstrapWith($this->bootstrappers());
    }
}

$bootstrappersプロパティの配列をbootstrapWith()の引数にぶちこんでいきます。

vendor/laravel/framework/src/Illuminate/Foundation/Application.php
public function bootstrapWith(array $bootstrappers)
{
    foreach ($bootstrappers as $bootstrapper) {

        $this->make($bootstrapper)->bootstrap($this);

    }
}

サービスコンテナで順番にインスタンス化していって、その中のbootstrap()を呼んでいます。

bootstrap()が終わったら、sendRequestThroughRouter()に戻ります。

vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php
protected function sendRequestThroughRouter($request)
{
    $this->app->instance('request', $request);

    Facade::clearResolvedInstance('request');

    $this->bootstrap();

    return (new Pipeline($this->app))
                ->send($request)
                ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                ->then($this->dispatchToRouter());
}

ここでreturnしているので、Pipelineクラスがresponseを生成しているみたいですね。

vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php
public function send($passable)
{
    $this->passable = $passable;

    return $this;
}

public function through($pipes)
{
    $this->pipes = is_array($pipes) ? $pipes : func_get_args();

    return $this;
}

send()はrequestをプロパティにセットして、through()はmiddlewareをプロパティにセットしています。

Pipelinethen()はmiddlewareを適用してrouterにdispatchを行います。(then()については複雑なのでこちらの記事を読んでください。LaravelのPipeline::thenでmiddlewareを処理している流れをまとめてみた)

return $pipeline($this->passable)は最終的にprepareDestination()を呼んで、引数のClosureを呼び出しています。

vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php
protected function prepareDestination(Closure $destination)
{
    return function ($passable) use ($destination) {
        try {
            // ここが呼び出される
            return $destination($passable);
        } catch (Exception $e) {
            return $this->handleException($passable, $e);
        } catch (Throwable $e) {
            return $this->handleException($passable, new FatalThrowableError($e));
        }
    };
}

$destinationthen()の引数に渡されたものなので、$this->dispatchToRouter()で生成したClosureが呼び出されます。

\Illuminate\Foundation\Http\Kernel.php
protected function dispatchToRouter()
{
    return function ($request) {
        $this->app->instance('request', $request);

        return $this->router->dispatch($request);
    };
}

routerのdispatch()を呼び出します。

vendor/laravel/framework/src/Illuminate/Routing/Router.php
public function dispatch(Request $request)
{
    $this->currentRequest = $request;

    return $this->dispatchToRoute($request);
}

public function dispatchToRoute(Request $request)
{
    return $this->runRoute($request, $this->findRoute($request));
}

protected function runRoute(Request $request, Route $route)
{
    //...

    return $this->prepareResponse($request,
        $this->runRouteWithinStack($route, $request)
    );
}

$this->findRoute()でリクエストされたルートやメソッド(get, postなど)で対象のルーティングを見つけてきます。

runRoute()でレスポンスを返却しています。

vendor/laravel/framework/src/Illuminate/Routing/Router.php
protected function runRouteWithinStack(Route $route, Request $request)
{
    $shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
                            $this->container->make('middleware.disable') === true;

    $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);

    return (new Pipeline($this->container))
                    ->send($request)
                    ->through($middleware)
                    ->then(function ($request) use ($route) {
                        return $this->prepareResponse(
                            $request, $route->run()
                        );
                    });
}

Pipelineでmiddlewareを適用しつつ、$route->run()でいよいよControllerに処理を委譲していきます。

vendor/laravel/framework/src/Illuminate/Routing/Route.php
public function run()
{
    $this->container = $this->container ?: new Container;

    try {
        if ($this->isControllerAction()) {
            return $this->runController();
        }

        return $this->runCallable();
    } catch (HttpResponseException $e) {
        return $e->getResponse();
    }
}

protected function runController()
{
    return $this->controllerDispatcher()->dispatch(
        $this, $this->getController(), $this->getControllerMethod()
    );
}
vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php
public function dispatch(Route $route, $controller, $method)
{
    $parameters = $this->resolveClassMethodDependencies(
        $route->parametersWithoutNulls(), $controller, $method
    );

    if (method_exists($controller, 'callAction')) {
        return $controller->callAction($method, $parameters);
    }

    return $controller->{$method}(...array_values($parameters));
}

ここまできてやっと$controller->{$method}とコントローラーのメソッドが呼ばれました!

コントローラーで投げられた例外は、HttpResponseExceptionクラスなら、Route::run()ないでcatchされレスポンスに変換されています。

vendor/laravel/framework/src/Illuminate/Routing/Route.php
public function run()
{
    try {
        if ($this->isControllerAction()) {
            return $this->runController();
        }
    } catch (HttpResponseException $e) {
        return $e->getResponse();
    }
}

それ以外の例外は、Pipeline::prepareDestination()でキャッチされ、handleException()が呼ばれます。

vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php
protected function prepareDestination(Closure $destination)
{
    return function ($passable) use ($destination) {
        try {
            return $destination($passable);
        } catch (Exception $e) {
            return $this->handleException($passable, $e);
        } catch (Throwable $e) {
            return $this->handleException($passable, new FatalThrowableError($e));
        }
    };
}
vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php
protected function handleException($passable, Exception $e)
{
    if (! $this->container->bound(ExceptionHandler::class) ||
        ! $passable instanceof Request) {
        throw $e;
    }

    $handler = $this->container->make(ExceptionHandler::class);

    $handler->report($e);

    $response = $handler->render($passable, $e);

    if (method_exists($response, 'withException')) {
        $response->withException($e);
    }

    return $response;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelでSNSの雛形デザイン作成してみた(user_tableまで)

今回SNSの雛形デザインを作成してみましたので、オープンソースとして共有いたします。

目的

・汎用的データ作成
|- デザイン
|- ソース

実装した機能

|- user側のテーブルの編集機能

作成物

スクリーンショット 2019-11-24 19.32.10.png
スクリーンショット 2019-11-24 19.32.30.png

環境構築

Mac High sierra
Laravel 5.8
VirtialBox+Vagrant+Homestead環境

※こちらはチートシートにまとめてありますので
このURLを参照してください。
 |- Laravel環境構築 チートシート

チートシートの「vagrant ssh」まで完了すれば大丈夫です。

1.データクローン

まずどこでもいいので下記コマンドをターミナルに入力してデータをクローンしてください

ターミナル
$ git clone https://dai570415@bitbucket.org/dai570415/laravel-sns-master.git

2.クローンデータを解凍して環境に入れる

先ほど環境構築した際に「code」フォルダを作成しているかと思いますので、その中にクローンしてきたファイル(中身のみ)をごそっと入れてください。

下記アドレスにアクセス
192.168.10.10

Topページが表示されるはずです。

3.DB関係を設定する

マイグレート

ターミナル
$ php artisan migrate

* こちらでDB周りが構築できました

最後に

汎用性を重視した雛形作成を目的にしているので
投稿側はあえて実装していません。以下URLから実装してください。

Laravelで投稿アプリの機能を作成 ~CRUD編~
Laravelで投稿アプリの機能を作成 ~リレーション編~

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelでクロージャを使った自作バリデーションの作成

Laravelのバリデーションでチェックボックスの特定の組み合わせを不可にするために、
自作バリデーションを作成してみました。
自作バリデーションはRulesを作る方法がありますが、
調べた所、バリデーションでもクロージャが使えるようでした。

簡単なサンプルを作ってみました。
まずフォームを作ります。

<h1>果物を選んで下さい。</h1>
@if ($errors->any())
    <ul>
        @foreach ($errors->all() as $error)
            <li>{{ $error }}</li>
        @endforeach
    </ul>
@endif
<form action="{{ route('check') }}" method="POST">
    @csrf
    <input type="checkbox" name="fruits[]" value="apple">りんご
    <input type="checkbox" name="fruits[]" value="orange">みかん
    <input type="checkbox" name="fruits[]" value="banana">バナナ
    <input type="submit">
</form>

りんごとバナナに同時にチェックがある時にエラーにするために、
コントローラーに以下のバリデーションを追加します。
fruitsのバリデーションの1つとしてクロージャを組み込んでます。
$attributeにフォーム名、$valueにフォームの値が入り、
$failにエラーメッセージ入れて返すという形になります。

public function check(Request $request)
{
    $request->validate([
        'fruits' => [
            'required',
            function ($attribute, $value, $fail) {
                if (in_array('apple', $value) && in_array('banana', $value)) {
                    return $fail('りんごとバナナは同時に選べません。');
                }
            }
        ],
    ]);
    return view('input');
}

少し可読性は悪いですが、別クラスを作らなくてすむのは便利です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Twigで拡張子が2つある理由

Twigのファイル名は、「index.html.twig」と「html」+「twig」と拡張子が2つついています。

これは「twig」というのが、Twigのテンプレートファイルということを示し、「html」は、twigファイルをレンダリングして生成されるものが、「HTML」ファイルであることを示している。

これは、Template Namingに記載されていました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelで日付をYYYY年MM月DD日 (曜日) hh: mmと表示させる

日付と時間だけだと簡単ですが、間に曜日を挟んだ場合の情報が少なくて面倒だったので、メモ
LaravelにCarbonのv2以降をインストールすれば以下のように1行でかけます。
今回は参考にdirectiveを使ってますが、viewHelperとかの方がいいかも。

AppServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        \Blade::directive('datetime_and_week', function ($expression) {
            return "<?php echo Carbon\Carbon::parse($expression)->isoFormat('YYYY年MM月DD日 (ddd) LT '); ?>";
        });
    }
}

directiveなのでView clearコマンドを実行後に
viewなどで下記のように呼び出せばOK

Sample.blade.php
<?php

@datetime_and_week('2019-11-24 00:00:00')

下記のように表示されます!
2019年11月24日 (日) 00:00

他に良い方法あれば教えてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelのファサードについて簡単にまとめる

概要

Laravelの代表的な機能としてファサードがある。これを使うと、staticなメソッドを呼ぶのと同じ要領で、どこからでも簡単にフレームワークの機能を呼び出すことができる。

use Illuminate\Support\Facades\Cache;

Route::get('/cache', function () {
    return Cache::get('key');
});

下の例のように、useで名前空間を指定せずとも、グローバル名前空間でアクセスするように書くことも可能。(ちなみにこれが可能なのはconfig/app.phpaliasesに記述されている対応関係をclass_alias関数によって結びつけているから。要はエイリアス(別名)をつけて省略的に書けるようにしていると思っておけば良いと思う。たぶん。)

Route::get('/config', function () {
    return \Config::get('key');
});

これを一見すると、Illuminate\Support\Facades\Configクラスに実装されたgetメソッドを呼んでいるように見えるが、実はgetメソッドはIlluminate\Support\Facades\Configクラスにも、継承元のIlluminate\Support\Facades\Facadeクラスにも存在していない。実際には、getFacadeAccessor()が実装されているのみである。

Illuminate\Support\Facades\Config.php
<?php

namespace Illuminate\Support\Facades;

class Config extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'config';
    }
}

この時何が起こっているか、簡単に辿ってみる。

マジックメソッドと__callStatic()

Illuminate\Support\Facades\Configクラスはget()を持っていなかった。
実装されていないstaticなメソッドを呼ぼうとするとどうなるかというと、PHPでは__callStatic()というマジックメソッドが呼ばれることになる。

public static __callStatic ( string $name , array $arguments ) : mixed

__callStatic() は、 アクセス不能メソッドを静的コンテキストで実行したときに起動します。
引数 $name は、 コールしようとしたメソッドの名前です。 引数 $argumentsは配列で、メソッド $name に渡そうとしたパラメータが格納されます。

マジックメソッドはPHPにおける特殊な関数であり、開発者が直接指定して呼び出すことは基本的になく、特定のタイミングで勝手に呼び出される。代表的なものが馴染み深い__construct()であり、新たにインスタンスが生成されるタイミングで勝手に呼び出されるマジックメソッドといえる。

中身

継承元のIlluminate\Support\Facades\Facadeクラスには、__callStatic()が実装されている。(コメントなどは省略)

Illuminate\Support\Facades\Facade.php
<?php

namespace Illuminate\Support\Facades;

...

abstract class Facade
{
    ...

    public static function getFacadeRoot()
    {
        return static::resolveFacadeInstance(static::getFacadeAccessor());
    }

    protected static function resolveFacadeInstance($name)
    {
        if (is_object($name)) {
            return $name;
        }

        if (isset(static::$resolvedInstance[$name])) {
            return static::$resolvedInstance[$name];
        }

        if (static::$app) {
            return static::$resolvedInstance[$name] = static::$app[$name];
        }
    }

    public static function __callStatic($method, $args)
    {
        $instance = static::getFacadeRoot();

        if (! $instance) {
            throw new RuntimeException('A facade root has not been set.');
        }

        return $instance->$method(...$args);
    }
}

上記のPHPマニュアルからの引用の通り、__callStatic()の引数$methodには呼び出そうとしたメソッドの名前、$argsにはその時に使用した引数が配列の形式で入る。

getFacadeRoot()でサービスコンテナ($app)から対象となるインスタンスを取得し、そのインスタンスのメソッドを引数の$methodによって実行する。

つまり、staticなメソッドを使用しているように見えて、実際には実体となるインスタンスがサービスコンテナから取り出され、指定したメソッドが実行される、という流れになっていることがわかる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ファサードの内部における動作を簡単にまとめる【Laravel】

概要

Laravelの代表的な機能としてファサードがある。これを使うと、staticなメソッドを呼ぶのと同じ要領で、どこからでも簡単にフレームワークの機能を呼び出すことができる。

use Illuminate\Support\Facades\Cache;

Route::get('/cache', function () {
    return Cache::get('key');
});

下の例のように、useで名前空間を指定せずとも、グローバル名前空間でアクセスするように書くことも可能。(ちなみにこれが可能なのはconfig/app.phpaliasesに記述されている対応関係をclass_alias関数によって結びつけているから。要はエイリアス(別名)をつけて省略的に書けるようにしていると思っておけば良いと思う。たぶん。)

Route::get('/config', function () {
    return \Config::get('key');
});

これを一見すると、Illuminate\Support\Facades\Configクラスに実装されたgetメソッドを呼んでいるように見えるが、実はgetメソッドはIlluminate\Support\Facades\Configクラスにも、継承元のIlluminate\Support\Facades\Facadeクラスにも存在していない。実際には、getFacadeAccessor()が実装されているのみである。

Config.php
<?php

namespace Illuminate\Support\Facades;

class Config extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'config';
    }
}

この時何が起こっているか、簡単に辿ってみる。

マジックメソッドと__callStatic()

Illuminate\Support\Facades\Configクラスはget()を持っていなかった。
実装されていないstaticなメソッドを呼ぼうとするとどうなるかというと、PHPでは__callStatic()というマジックメソッドが呼ばれることになる。

public static __callStatic ( string $name , array $arguments ) : mixed

__callStatic() は、 アクセス不能メソッドを静的コンテキストで実行したときに起動します。
引数 $name は、 コールしようとしたメソッドの名前です。 引数 $argumentsは配列で、メソッド $name に渡そうとしたパラメータが格納されます。

マジックメソッドはPHPにおける特殊な関数であり、開発者が直接指定して呼び出すことは基本的になく、特定のタイミングで勝手に呼び出される。代表的なものが馴染み深い__construct()であり、新たにインスタンスが生成されるタイミングで勝手に呼び出されるマジックメソッドといえる。

中身

継承元のIlluminate\Support\Facades\Facadeクラスには、__callStatic()が実装されている。(コメントなどは省略)

Facade.php
<?php

namespace Illuminate\Support\Facades;

...

abstract class Facade
{
    ...

    public static function getFacadeRoot()
    {
        return static::resolveFacadeInstance(static::getFacadeAccessor());
    }

    protected static function resolveFacadeInstance($name)
    {
        if (is_object($name)) {
            return $name;
        }

        if (isset(static::$resolvedInstance[$name])) {
            return static::$resolvedInstance[$name];
        }

        if (static::$app) {
            return static::$resolvedInstance[$name] = static::$app[$name];
        }
    }

    public static function __callStatic($method, $args)
    {
        $instance = static::getFacadeRoot();

        if (! $instance) {
            throw new RuntimeException('A facade root has not been set.');
        }

        return $instance->$method(...$args);
    }
}

上記のPHPマニュアルからの引用の通り、__callStatic()の引数$methodには呼び出そうとしたメソッドの名前、$argsにはその時に使用した引数が配列の形式で入る。

getFacadeRoot()でサービスコンテナ($app)から対象となるインスタンスを取得し、そのインスタンスのメソッドを引数の$methodによって実行する。

つまり、staticなメソッドを使用しているように見えて、実際には実体となるインスタンスがサービスコンテナから取り出され、指定したメソッドが実行される、という流れになっていることがわかる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP7.4のpreloadいれたらLaravelは早くなるのだろうかと思って検証した

皆さんこんにちは

PHP7.4でpreloadが来るっていうので、ワクテカしているわけですが、実際どれくらい早くなるのかしらって思いまして、ちょうど色々検証していたので、Laravelでも軽くやっちゃおうぜって思ったので、やってみます。

あと4日ですね!!

三行で

  • preloadが来るよ
  • preloadで読み込み済みの状態を作れるよ
  • Laravelに導入すると、結構早くなるよ

Preload

PHP7.4でopcacheにpreloadという機能が入ります。これの概要は

  • opcacheの設定にpreloadを実行するヘルパーファイルを指定できる
  • webサーバを起動するとopcacheがヘルパーファイルを実行する
  • ヘルパーファイル内でキャッシュ化したものは、通常のアクセスにおいて、すでに読み込まれたものとしてみなされる

そう、読み込み済みですよ!
簡単に読み込み済みの効果を見るために、以下のような例を考えてみましょう。

preload利用の例

まずは、こんなDockerfileで、動かせるコンテナを作ってみます。

FROM php:7.4.0RC4-cli

RUN apt update && apt install -y vim sudo && docker-php-ext-install opcache &&\
    echo 'opcache.preload=/var/www/preload.php' >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini &&\
    useradd nginx

WORKDIR /var/www

ベースコンテナはちょいと古いのですが、まあ、検証当時のものなんで、許してくださいな。
そして、以下のようなディレクトリ構成のファイル群を用意します。

classes
 - A.php
 - B.php
index.php
preload.php

内容としては

A.php
<?php 
class A
{
    public function name(): string
    {
        return return static::class;
    }
}
B.php
<?php
class B extends A {}
index.php
<?php
$b = new B;

echo $b->name() . '<BR>';

さて、サーバのエンドポイントとしてはindex.phpを使うわけですが、この中ではrequireを一回も使っていないし、当然autoloadの設定もしていないので、

sudo -u nginx php -S 0.0.0.0:8080

としてサーバを起動しても、いざアクセスすると「Bなんてクラス知らんのだけど」っていうエラーが出るだけです。

ここで、preloadを使ってみましょう。
先程のDockerfileには予めopcache.preloadの設定が挿入されるようになっています。実際に書かれている設定はこんな感じになっています。

/usr/local/etc/php/conf.d/docker-php-ext-opcache.ini
zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20190902/opcache.so
opcache.preload=/var/www/preload.php

このopcache.preloadがヘルパーファイルの場所になっているので、そこに以下のようなヘルパーをおいておきましょう。

preload.php
<?php

$files = glob(__DIR__ . '/classes/**.php');

foreach ($files as $file) {
    opcache_compile_file($file);
}

この状態でサーバを起動します。

sudo -u nginx php -S 0.0.0.0:8080
[Sun Nov 24 04:54:36 2019] PHP 7.4.0RC4 Development Server (http://0.0.0.0:8080) started

ちゃんと起動したようです。
それではアクセスしてみましょう。

$ curl http://localhost:8080
B<BR>

普通に動きますね。

preloadは性能に絡めるのか

多くのWebフレームワークは大量のクラスが定義されていますが、PHPでそれを読むにはいちいちファイルを見に行かなければなりません。しかも、PHPでは、基本的に各アクセスごとにプロセスが独立しているため、ファイルの読み込みもアクセスごとに発生します。
opcacheでこの動きを緩和することができますが、その動きは割と複雑です。

  1. クラスが呼ばれる (静的・インスタンス生成など)
  2. クラスの定義がない場合、autoloadで設定したルールに従い、ファイルをrequireする
  3. requireしようとしたファイルのキャッシュがあった場合、ファイルのタイムスタンプよりも新しければ、そのままキャッシュを使う
  4. キャッシュが古ければ、ファイルを読みに行く(設定によってスキップ可能)
  5. ファイルを読んだ場合はキャッシュを更新する

preloadしてあれば、1ですむので、それなりに早くなるんじゃないかって思います。

Laravelに入れてみる

たくさんのファイルを読み込むといえばLaravelですね!
そこで、適当にLaravelを入れて、検証してみましょう。

includeしているファイルを見てみる

とりあえず、どのくらいのファイルを読み込んでいるのか見てみます。

public/index.php
$kernel->terminate($request, $response);

$includes = get_included_files();

file_put_contents('/var/www/includes.txt', implode("\n", $includes));

こんな感じのを仕込むと、ファイルのリストが出てきます。350ファイル以上ありますね。

preloadに入れてみる

適当にpreloadに入れるわけですが、viewのキャッシュファイルとかは読んでもしょうがないかなとか思いながら、以下のようなloaderを作ります。

preload.php
<?php
$includes = file_get_contents('/var/www/includes.txt');
$includes = explode("\n", $includes);

foreach ($includes as $include) {
    if (strpos($include, '/storage/framework') !== false) {
        echo $include, " - storage \n";
        continue;
    }

    if (strpos($include, 'index.php') !== false) {
        echo $include, " - index \n";
        continue;
    }

    if (strpos($include, 'src/config') !== false) {
        echo $include, " - config\n";
        continue;
    }

    opcache_compile_file($include);
}

こいつをpreloadした状態でサーバを起動します。
index.phpにerror_log(count($includes));みたいなコードを突っ込んでやると、読み込みファイル数が363 -> 46 になりました。せっかくなら1とかになってくれるとありがたかったんですが、そううまくは行かないようですね。

検証する

例によってab使って一分間のアクセスレートを見てみましょう。
abはubuntu 18.04 (wsl2のやつ) を使ってます。

preloadなし

$ ab -t 60 -c 1 http://localhost:8080/

...

Concurrency Level:      1
Time taken for tests:   60.004 seconds
Complete requests:      23
Failed requests:        0
Total transferred:      77747 bytes
HTML transferred:       55798 bytes
Requests per second:    0.38 [#/sec] (mean)
Time per request:       2608.884 [ms] (mean)
Time per request:       2608.884 [ms] (mean, across all concurrent requests)
Transfer rate:          1.27 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       0
Processing:   177 2609 1619.2   2894    7586
Waiting:      176 2608 1619.1   2893    7585
Total:        177 2609 1619.2   2894    7586

Percentage of the requests served within a certain time (ms)
  50%   2844
  66%   3064
  75%   3230
  80%   3256
  90%   3323
  95%   3658
  98%   7586
  99%   7586
 100%   7586 (longest request)

2.5秒超えるのか。。。
一応、opecache使ってファイルはキャッシュしているはずなんですが。

preloadあり

$ ab -t 60 -c 1 http://localhost:8080/

...

Concurrency Level:      1
Time taken for tests:   60.160 seconds
Complete requests:      301
Failed requests:        0
Total transferred:      1017565 bytes
HTML transferred:       730226 bytes
Requests per second:    5.00 [#/sec] (mean)
Time per request:       199.867 [ms] (mean)
Time per request:       199.867 [ms] (mean, across all concurrent requests)
Transfer rate:          16.52 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:   148  200 140.9    179    1575
Waiting:      148  199 140.9    179    1574
Total:        148  200 140.9    179    1575

Percentage of the requests served within a certain time (ms)
  50%    179
  66%    187
  75%    193
  80%    199
  90%    209
  95%    224
  98%    260
  99%    852
 100%   1575 (longest request)

ほんまか?
10倍強の速度になっているんだが。

まとめ

速度強化が劇的すぎて、流石にコンテナ特有のボリュームアクセス遅い問題に関係しちゃっているのかもしれないなぁとか思いながらも、ファイルアクセスが減って早くなっているのは事実だなぁって思いました。
将来的にはフレームワークごとにpreloadのヘルパーを用意してくれると、こっちとしては脳死で実装できて楽だなぁって他力本願に考えちゃったりしています。

今回はこんなところです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelでControllerの名前を変更する方法

概要

php artisan make:controller 

で作成したControllerを後に改名したくなった場合、知らずにやると
file does not exit とかで困ったので記録。

手順

1.Controllerのファイル名を変更する。
2.Controllerファイルを開いて、Class名を変更する。
3.web.phpを開いて、第二引数の@の前の部分を手順2のClass名に差し替える。
4.以下をターミナルで実行する。

composer dump-autoload

参考にしたページ

Laravel 5.7でControllerのファイル名を少し変更(大文字)しただけなのに、Controllerファイルが見つからないエラーが出るようになった

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

herokuデプロイで10分であなたのアプリを世界に公開する手順【Laravel5.8】

今回はherokuを使った、Laravelプロジェクトを世界に手っ取り早く公開する手順をご紹介します。

実は今回僕も初の公開作業をしながら同時にこのqiitaを書いているので、これが公開されているということは動作は保証されます笑

herokuとはPaaS(Platform as a Service)と呼ばれるサービスで、アプリケーションを実行するためのプラットフォームです。

もう少しわかりやすく説明すると、サーバやOS、データベースなどの「プラットフォーム」と呼ばれる部分を、インターネット越しに使えるようにしてくれるサービスの一つです。

レンタルサーバーと似ていると思うかもしれませんが、レンタルサーバーとPaaSは「環境を貸してくれる」という意味では一緒なので、ほぼ同じと考えても問題はありません

herokuとは?初心者でも5分で分かる基本や特徴をまるっと紹介

大まかな流れか以下の通りです。

herokuに登録

heroku CLIダウンロード

herokuでアプリ作成

PostgreSQLへの接続設定

herokuでDBマイグレーション

heroku openで世界に公開!

それではいきましょう!

環境

MacOS Mojave 10.14.5(windowsのみなさんごめんなさい🙇‍♂️)
Laravel5.8.x
MAMP5.3で作成したLaravelプロジェクト

herokuに登録

こちらより各自登録しておいてください。

https://signup.heroku.com/jp

heroku CLI

まずはherokuコマンドを入れます。

$ brew tap heroku/brew && brew install heroku

念の為バージョン確認します。

$ heroku -v
heroku/7.35.0 darwin-x64 node-v12.13.0

okですね。

ターミナルからherokuログイン

cdコマンドでlaravelプロジェクトに移動してください。

$ cd /Applications/MAMP/htdocs/kanban

kanbanは僕のプロジェクト名で、MAMPなのでhtdocs以下になっています

さあheroku CLIでログインします。

$ heroku login

以下のようなメッセージが出たらq以外を押す

heroku: Press any key to open up the browser to login or q to exit: 

すると?

スクリーンショット 2019-11-24 9.29.02.png

こんなブラウザが出てくるので、Log in押します。

Logging in... done
Logged in as xxx@xxx.xxx

ログインできた!

HerokuのHTTPサーバー用コンフィグファイル作成

$ echo "web: vendor/bin/heroku-php-apache2 public" > /Applications/MAMP/htdocs/kanban/Procfile

ここは正直自分も行う理由があやふやです(^◇^;)
ProcfileはHerokuアプリの起動時に実行するプロセスを定義するためのファイルみたいです。

herokuでアプリを作成する

$ heroku create

スクリーンショット 2019-11-24 10.09.13.png

前者がアプリのURL、後者がgitのプッシュ先になります。
これから使う大切なものなので、控えておきます。

ここからgitでherokuのリモートリポジトリにプッシュしていきます。

$ git init

これでリポジトリを作成します。
そして実はLaravelプロジェクト直下に.gitという隠しディレクトリができています。
これ超重要です。

$ git add .

git add .コマンドはリポジトリにコミットする前に行うステージングを全ファイル分やってくれます。

$ git commit -m "initial commit"

ステージングしたファイルをコミットしています。

そしたら、ここで一回.git/configを開きます。

そして、先ほどターミナルに出たgitのリポジトリアドレスも確認してください。

[remote "heroku"]
  url = https://git.heroku.com/lit-plateau-44850.git

これを追記しますが、
ここのアドレスは人によって違ってきます。

これがないとリモートリポジトリがどれかわからないので、fatal errorが出るんですよね(再送2回)

では、

$ git push heroku master

herokuのmasterリポジトリにプッシュせしめるで候。

remote: Verifying deploy... done.
To https://git.heroku.com/lit-plateau-44850.git
 * [new branch]      master -> master

このようなメッセージが出たらherokuデプロイできています。

ここからは細々した設定をしていきます。

Herokuの環境変数

$ heroku plugins:install heroku-config
$ heroku config:set DB_DATABASE=laravel
$ heroku config:push

DB名Laravelは、僕はDBローカルのMySQLでDB名がLaravelだったので、herokuでも合わせました。

Herokuにpostgresqlを作成、接続

herokuが無料で提供しているのはPostgreSQLということなので、herokuでHeroku Postgresを作成し、接続します。

$ heroku addons:create heroku-postgresql:hobby-dev
$ php -r 'preg_match("/^postgres:\/\/(.+?):(.+?)@(.+?):(.+?)\/(.*?)$/", `heroku config:get DATABASE_URL`, $matches); `heroku config:set DB_CONNECTION=pgsql DB_HOST=$matches[3] DB_PORT=$matches[4] DB_DATABASE=$matches[5] DB_USERNAME=$matches[1] DB_PASSWORD=$matches[2]`;'

herokuでマイグレーション

それでは、herokuでマイグレーションを実行します。

$ heroku run "php artisan migrate"

その後、

Migration table created successfully.

と出たら、終了です。

先ほどターミナルに出てきたアプリのURL覚えてますか?

あれが公開URLなので、緊張の入力です...

スクリーンショット 2019-11-24 10.49.50.png

公開できた😭

DBも機能してる。

ちなみにtechpitで作ったtrello風タスク管理アプリになります(https化し忘れた...)

http://lit-plateau-44850.herokuapp.com/

画像投稿機能とかtwitter Authとか入れたかったなぁ...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む