カテゴリー
Laravel

Laravelのデフォルトページネータの仕様を教えてクレメンス

まずはこいつを見てくれ。こいつをどう思う?

Laravelはこんなんも何も考えずに実装してくれる。最高や。

せやけども、どう言う仕様か突っ込まれることもあるやろし、なんか変更したいとか言われる可能性がなくもない。実際スルー推奨やけども。

とはいえ、説明せなアカンときはテキストで説明するのも憚れるし、これ見てクレメンスで済むように用意したったで。

と言うわけで、説明するゾー。これはあくまで、何もパラメータいじらずに使ったパターンやからね。なんかエグいやつは、そもそもガッツリページネータカスタムした方がええで。これはデフォルト使ってちゃちゃっとやりたいニキ向けや。

せやけども、デフォルトで唯一あると言っていいパラメータは、「現在のページの左右に表示するのは何個分か」だけや。

onEachSideとかでgrepしてくれたらええねんけども、それが初期値では3なんよね。

それがどう言うことかっちゅうのは先のアレを見てもらいたい。「15ページある場合」のセクションや。ほんで、現在のページが8のパターンに着目してもらうと、8を中心に6,7,8と9,10,11となってて、他が省略されとるね。つまりこう言うことやねん。

で、次に見てもらいたいのは、先頭と最後のところやね。

なんで、1,2と14,15になるか。

これはそう言うものなんや。ここは全くいじれない。ガッハ(ガッツリハードコード)や。なんで2個なのか、理由を知りたい。だいたいデザインとか仕様とずれるのはここや…。

最後にもう一つ、「全部で14ページある場合」から省略の(…)が出てくるけれども、これもそう言う仕様やねん。

明確にロジックがわけられているんよ。

最後のページ < ($onEachSide * 2) + 8 みたいな条件分岐がなされていて、これが適用されると省略のパターンにならないんよね。

つまりや、デフォルトのページネータでやってて、テストするときは、ページ数が14以上にならないと出てこないパターンがあるでっちゅうことや。要注意やで!

ところで、これ、みんなどうやって検索するん?
Pagination?Paginator?ページング?

カテゴリー
Laravel

Laravel が急に動かなくなった Late 2020。Undefined index: name 編

ついこの間まで動いていたLaravelが急に動かなくなった。何もしてないのに動かなくなった。むしろ何もしてないから動かなくなったんやろな。Dockerで環境用意してあるやつだけども、いつものように起動してアクセスしてみたらこんなエラーが。

あ、ホンマ…(絶句)

Undefined index: name

なんやねん、これ…。

検討つかんねんけど、コアでこう言うんが発生する場合は、コードの問題やないな。きっとなんかPHPんとこのコンテナがおかしそうやな…。これは教育やろな。

再ビルドっと…。

ビルド時にcomposerがインストールされて依存ライブラリも自動インストールされるんやけども、なんやパッケージが見つからんとか、依存関係がいろいろおかしいで…?

[InvalidArgumentException] has a PHP requirement incompatible with your PHP version, PHP extensions and Composer version

ん?そんな、さすがにPHPのバージョンは固定しとるで…。

Composer (version 2.0.7) successfully installed

そんなもん、オマエ…

なるほど、どうやらComposerの方が生まれ変わったようや…。

なんかいろいろ影響ありそうで、直すの大変やなっちゅうわけで、Composerの方のバージョンを1系に固定して完了や。

Composer、バージョンあがったでっちゅうのをね、伝え羅れればと。

そらね、同じことの繰り返しよ。

カテゴリー
Laravel

LaravelのCacheのPermissionについて (failed to open stream: Permission denied)

artisanを操作するユーザーとWebサーバーの実行ユーザーが違うのは多々あるはずやねん。

artisanを使うときはahraユーザー、webはapacheとかね。ほんでね、お互いグループは共有してるとするやで。つまり、グループでのパーミッション指定があってればええっちゅうこっちゃね。

せやけども、キャッシュ機構(ファイルにストアするやつね)を使ってる箇所があるとするやろ?

そうすると、storage/framework/cache なディレクトに作られるファイルのパーミッションが、artisanのときは、ahra:ahra になるし、webのときは apache:apache になるわけよ。

せやけども、問題はこのファイルのパーミッションが644になってる場合があるわけよね。

ほんで、ahraユーザーで、artisan clear:cache するとさ、

Application cache cleared!

って出るんやけども、実際ね、クリアできてへん。

ほんで、実際にキャッシュ操作するコマンド作って動かしたらさ、webで作られてるとこけるわけやねん。

failed to open stream: Permission denied

やね。

まあ、つまり、キャッシュをファイルに書き出す時のパーミッションをね、グループも書き込み可能にしたいわけやねん。

調べるとさ、カスタムのファイルストアマネージャ作ったとか、ファイルマスク変更するとか、いろいろあったわけやねんけどね、今のLaravel(7.x)ならもっとこう、なんかあるんやないかなって。

あった。Illuminate/Cache/FileStore.phpやね。

    /**
     * Create an instance of the file cache driver.
     *
     * @param  array  $config
     * @return \Illuminate\Cache\Repository
     */
    protected function createFileDriver(array $config)
    {
        return $this->repository(new FileStore($this->app['files'], $config['path'], $config['permission'] ?? null));
    }

出たわね。

パーミッション指定できるやん。というわけで、config/cache.phpのココを

        'file' => [
            'driver' => 'file',
            'path' => storage_path('framework/cache/data'),
            'permission' => 0664,
        ],

こうかな。

これでwebの方で作られたキャッシュファイルを見てみると、確かに、グループが書き込み可能になっとった。これでキャッシュの更新自体は成功や…!たしかに、ahraユーザーでキャッシュ更新する処理は成功するようになったンゴ。

だがしかし、artisan clear:cache の削除はやっぱり効かへん…。

    /**
     * Create the file cache directory if necessary.
     *
     * @param  string  $path
     * @return void
     */
    protected function ensureCacheDirectoryExists($path)
    {
        if (! $this->files->exists(dirname($path))) {
            $this->files->makeDirectory(dirname($path), 0777, true, true);
        }
    }

ふーむ、おかしいな。ディレクトリのパーミッションは指定してるんやが…。

調べるンゴねぇ。

内部的に、mkdir() で、指定のpermissionを渡しているんだが、それでもそうならないのは、mkdir()はumaskによる制限がかかるんやて。

なるほど。

umask()を、ahraユーザーとwebでそれぞれ実行してみた結果。

ahraは0002、webやと0022や。

そら(0777のディレクトリをmkdir()で作ることは)そう(できない)よ。

これは教育やろなぁ。

というわけであとはumaskの設定をってことになるんやが、手取り早いのは、laravelのAppServiceProvider.phpあたりで

    public function register()
    {
        // overwrite umask
        umask(0002);

かな。悪手な気がするけれども。サーバー設定いじれないときはこうするしかない。

これで、ahraユーザー、webのどちらで作られたキャッシュファイルでも同じように操作できるようになったで。

ええんちゃうか、最高ちゃう。

ついでやけども、ログ(config/loggin.php)も同様やね。

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

        'daily' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'debug',
            'days' => 14,
            'permission' => 0664,
        ],

single と daily で作られるファイルのパーミッションが指定可能や。

アプリケーションが稼働し続けることによって自動で作成されたファイルでややこしいことにならないように、最初にはっきりしておきたいところやね。

追記(2020.11.16)

あかん。あかんで。結局オーナーとグループが違うわけやから、動かん。コマンドが先に実行しちゃった場合は、グループがapacheにならへん。

そこで、SGIDよ。

ディレクトリに設定すれば、そのディレクトリ配下に作成されるやつのグループがそいつと同じものになるっちゅうやつやね。

つまり、Laravelなら、storageとbootstrap/cacheに設定しておけばええと思うで。

例えば、storageディレトリのグループがapacheなら。

chmod g+s storage

で。

そしたら、その中に作られるファイルやディレクトリは基本的にapacheになるっちゅうことやね。

セキュリティ的にあれやけども。しゃーない、切り替えていけ。

カテゴリー
Laravel

hetemlでLaravel。Swift_TransportException with message ‘Expected response code 220 but got an empty response’

hetemlのレンタルサーバーでLaravelを動かすときに、メールが送れない。件のエラーが出たときの対処法。

そら(本来ならSMTPとかね使うんやけども。いろいろ事情とかあるからね。)そう(今回は、そのまま sendmail を使った。)よ。

.env で、MAIL_MAILER=sendmail にしとく。

そうすると、config/mail.phpの

'sendmail' => [
    'transport' => 'sendmail',
    'path' => '/usr/sbin/sendmail -bs',
],

が使われることになるわけなんやけども。何のことはない。このpath部分を、

‘path’ => ‘/usr/sbin/sendmail -t’,

と、こうする。オプションを変えるだけやね。

-t にしとくと、必要な情報をメッセージから抽出してくれるらしい。設定がきっちりできてれば、-bsでも送れると思うンゴ。

メール送れへん、っていう状態をひとまず緊急回避するための対応やね。適当に使ったら、サーバーに負荷かかる可能性もあるし、最終的にはちゃんとしたメーラー、使ったらええねんな。

カテゴリー
Laravel

Mailableのプレビュー時、Markdownのテンプレートで@component()なHTMLがエスケープされてしまうンゴ

Mailableを実装したものは、簡単にプレビューできるようになっとる。例えば、こんな感じにルーティングする。

Route::get('mailable', function () {
    return (new ClassImplementsMailable)
        ->subject('foobar')
        ->markdown('emails.foobar', []);
});

そうすると、メールの中身がどんなふうに組み立てられるかを確認することができる。

今回は、Auth関連のユーザー登録時にメールアドレス検証のために送信されるメールの内容を調整したかった。いろいろいじりたかったので、Mailableを実装したクラスに置き換える。

vendor/laravel/framework/src/Illuminate/Auth/Notifications/VerifyEmail.php
を継承したクラスのtoMail()をこんな感じにする。


    /**
     * Build the mail representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return \Illuminate\Notifications\Messages\MailMessage
     */
    public function toMail($notifiable)
    {
        if (static::$toMailCallback) {
            return call_user_func(static::$toMailCallback, $notifiable);
        }

        return (new ClassImplementsMailable)
           ->markdown(); // ここでいろいろ

returnが\Illuminate\Notifications\Messages\MailMessageとなっているのでtoMail()の呼び出し元を見てみる。

    /**
     * Send the given notification.
     *
     * @param  mixed  $notifiable
     * @param  \Illuminate\Notifications\Notification  $notification
     * @return void
     */
    public function send($notifiable, Notification $notification)
    {
        $message = $notification->toMail($notifiable);

        if (! $notifiable->routeNotificationFor('mail', $notification) &&
            ! $message instanceof Mailable) {
            return;
        }

        if ($message instanceof Mailable) {
            return $message->send($this->mailer);
        }

        $this->mailer->send(
            $this->buildView($message),
            array_merge($message->data(), $this->additionalMessageData($notification)),
            $this->messageBuilder($notifiable, $notification, $message)
        );

インスタンスがMailableだった場合の処理も用意されてるので、問題無い。きっと。

ここで、件のメール内容のプレビューの話に戻る。

というわけでまずは、元々使われていたMarkdownのテンプレートをベースに調整しようとしたのだが…。

出たわね。

テンプレートはこう。

@component('mail::message')
    {{-- Greeting --}}
    @if (! empty($greeting))
        # {{ $greeting }}
    @else
        @if ($level === 'error')
            # @lang('Whoops!')
        @else
            # @lang('Hello!')
        @endif
    @endif

    {{-- Intro Lines --}}
    @foreach ($introLines as $line)
        {{ $line }}

    @endforeach

    {{-- Action Button --}}
    @isset($actionText)
        <?php
        switch ($level) {
            case 'success':
            case 'error':
                $color = $level;
                break;
            default:
                $color = 'primary';
        }
        ?>
        @component('mail::button', ['url' => $actionUrl, 'color' => $color])
            {{ $actionText }}
        @endcomponent
    @endisset

    {{-- Outro Lines --}}
    @foreach ($outroLines as $line)
        {{ $line }}

    @endforeach

    {{-- Salutation --}}
    @if (! empty($salutation))
        {{ $salutation }}
    @else
        @lang('Regards'),<br>{{ config('app.name') }}
    @endif

    {{-- Subcopy --}}
    @isset($actionText)
        @component('mail::subcopy')
            @lang(
                "If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below\n".
                'into your web browser: [:actionURL](:actionURL)',
                [
                    'actionText' => $actionText,
                    'actionURL' => $actionUrl,
                ]
            )
        @endcomponent
    @endisset
@endcomponent

どうやら、@componentの中身がエスケープされて表示されてしまっている模様。

調べたところ、Laravelの特定のバージョンで起こるとかなんとか。みなさん、いろいろ試行錯誤していたようだが、結果、解決策は…。

@component('mail::message')
{{-- Greeting --}}
@if (! empty($greeting))
# {{ $greeting }}
@else
@if ($level === 'error')
# @lang('Whoops!')
@else
# @lang('Hello!')
@endif
@endif

{{-- Intro Lines --}}
@foreach ($introLines as $line)
{{ $line }}

@endforeach

{{-- Action Button --}}
@isset($actionText)
<?php
switch ($level) {
case 'success':
case 'error':
$color = $level;
break;
default:
$color = 'primary';
}
?>
@component('mail::button', ['url' => $actionUrl, 'color' => $color])
{{ $actionText }}
@endcomponent
@endisset

{{-- Outro Lines --}}
@foreach ($outroLines as $line)
{{ $line }}

@endforeach

{{-- Salutation --}}
@if (! empty($salutation))
{{ $salutation }}
@else
@lang('Regards'),<br>{{ config('app.name') }}
@endif

{{-- Subcopy --}}
@isset($actionText)
@component('mail::subcopy')
@lang(
"If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below\n".
'into your web browser: [:actionURL](:actionURL)',
[
'actionText' => $actionText,
'actionURL' => $actionUrl,
]
)
@endcomponent
@endisset
@endcomponent

インデントを消す。

ええやん。

おそらく最新のLaravelやとおこらないと思うんやけども、いつかのために。ちなみにバージョンはv5.7.28やった。

カテゴリー
Laravel

compact(): Undefined variable: operator っていうエラーがでたンゴ

過去のLaravelを新しい環境で動かしてみたら件のエラーが出たンゴ。Laravelは5.4。新しい環境のPHPは7.4。

QueryBuilderの処理で上記例外が発生している模様。

 /**
     * Add an exists clause to the query.
     *
     * @param  \Illuminate\Database\Query\Builder $query
     * @param  string  $boolean
     * @param  bool  $not
     * @return $this
     */
    public function addWhereExistsQuery(self $query, $boolean = 'and', $not = false)
    {
        $type = $not ? 'NotExists' : 'Exists';

        $this->wheres[] = compact('type', 'operator', 'query', 'boolean');

        $this->addBinding($query->getBindings(), 'where');

        return $this;
    }

こ↑こ↓

PHPが7.1にダウングレードするか、Laravelのバージョンを上げる必要がありそう。

なので、このエラーに直面したらPHPのバージョンの問題の可能性が微レ存っていう話。

カテゴリー
Laravel

サブディレクトリにLaravelをインストールしたらTelecsopeが使えないンゴ…

そら(サブディレクトリにLaravelインストールしたら)そう(Telescopeは動かん)よ、とレスされてる人がTelescopeのissueにたくさんおったで…。

のっぴきならない事情でサブディレクトリにLaravelを配置することになったとしても、使いたいやん?
実際、何とかしてくれてる人がおったで。

https://github.com/laravel/telescope/pull/281

   /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {

      if ($this->app->isLocal()) {
        View::composer(
            ['telescope::layout'],
            function ($view) {
                $view->with('telescopeScriptVariables', [
                    'path' => 'subfolder/admin/telescope',
                    'timezone' => config('app.timezone'),
                    'recording' => ! cache('telescope:pause-recording'),
                ]);
            });
      }
    }

こんな感じでViewServiceProviderに追記すればええ。

⊂(^ω^)⊃ セフセフ!!

カテゴリー
Laravel

UnitTestでconfig()が使えないンゴ…

LaravelのEloquentのUnitTestを書いていて、Eloquentの中にconfig()を利用するものがあったんやけども、そこがエラーになった。

 Target class [config] does not exist.

  at vendor/laravel/framework/src/Illuminate/Container/Container.php:807
    803| 
    804|         try {
    805|             $reflector = new ReflectionClass($concrete);
    806|         } catch (ReflectionException $e) {
  > 807|             throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
    808|         }
    809| 
    810|         // If the type is not instantiable, the developer is attempting to resolve
    811|         // an abstract type such as an Interface or Abstract Class and there is

コンテナにconfigが登録されてないでってことらしいで。ということなので、UnitTest実行時にコンテナに登録してみた。

app()->bind('config', function () {
                return new class {
                    public function get(...$args)
                    {
                        return 'なんてすごいんだ……(恍惚)';
                    }
                };
            });

とりあえずテキストが返ればよかったので、こんな感じで解決できた。

やっぱりEloquentの中でconfig()直接使うなってことなんやなと。横着せずに、ちゃんと外から渡して疎結合にしろってはっきりわかんだね。

カテゴリー
Laravel

UnitTestでEloquentの日付情報を操作するとエラーになるンゴ…

LaravelのEloquentをUnitTestするときに、日付の属性をセットしようとすると下記のエラーになった。

 Call to a member function connection() on null

  at vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:1342
    1338|      * @return \Illuminate\Database\Connection
    1339|      */
    1340|     public static function resolveConnection($connection = null)
    1341|     {
  > 1342|         return static::$resolver->connection($connection);
    1343|     }

データベースのコネクションを探しているようや。これは、日付の属性をセットするときは、データベースへのアクセスが必要になるためらしいんやが…。

    /**
     * Get the format for database stored dates.
     *
     * @return string
     */
    public function getDateFormat()
    {
        return $this->dateFormat ?: $this->getConnection()->getQueryGrammar()->getDateFormat();
    }

なるほど、日付のフォーマットをデータベースタイプから参照してくれる機能のおかげみたいや。

    /**
     * Set the date format used by the model.
     *
     * @param  string  $format
     * @return $this
     */
    public function setDateFormat($format)
    {
        $this->dateFormat = $format;

        return $this;
    }

フォーマットは直接指定できるようなので、Eloquentで指定しておけばええみたいや。

これで、データベースのコネクションエラーにならずにEloquentの日付属性を利用するようなUnitTestも動くようになったで。