20191009のPHPに関する記事は11件です。

開発環境から始めるdocker環境の浸透

dockerによるCIと、コンテナクラスタリング構築の記録を残していきます。

スマートフォンベースのECサイトのプロジェクトへ配属になりました。
プロジェクトはオーソドックスなLAMP構成+jQueryによるインタラクションが付与されたものですが、中〜大規模のPVを持っており、決済機能も存在する事から
品質管理やリリース手順などは整備されていました。

ライフサイクルとしては成熟期を過ぎた印象があり、プロダクトとしては一定の完成度に達していたので
バックエンドより、ユーザが直接関わるUI部分の改修に比重が置かれ
インフラ部分については、現状動いているのだから、コストを掛けて改修するほど優先度は高くない、といった状況でした。

そんな中で、合間を見てdocker環境を構築し、現場に浸透させて行った記録です。
docker環境の構築の助けになれば幸いです。
(記録になるので、ベストプラクティスから外れている部分も多いかと思いますが、ご容赦下さい)

当時の開発環境

dev.jpg
開発環境は、VirtualBox(Vagrant)でCentOSベースのイメージを一つ起動し
一つのOSの内に
・サービス内で展開している、三種類の2層webアプリケーション(VirtuaHostによる統合)
・上記3アプリケーションから参照するmysql
・DB負荷軽減の為のRedisサーバ
が同居しています。

その他、CDNとしてAWS S3を採用している為、擬似S3となるminoサーバを
ホストマシン上で起動しています。
ホストマシン上のGitHubのリポジトリを、Vagrantのイメージにマウントし、エディタで開発を行なっていました。

開発環境の問題点

前述の通りインフラ構築は逆風の雰囲気でしたが
開発チームからは、開発環境の問題の指摘が挙げられていました。

1.Virtualboxの起動速度

Virtualboxは、仮想環境内でOSを起動する為起動にリソースを消費しますが
dockerはホストOSの機能を共有して、docker仮想環境内で
アプリケーションが格納されたコンテナを起動する為、
「OSを起動する」部分のオーバーヘッドが無く、起動が高速になります。
docker導入のメリットとして訴求しました。

2.サービス環境と開発環境で、構成が異なり、健全ではない

■開発環境は、3つの2層webアプリケーションを1つのEC2インスタンスに集約していましたが
商用環境は、AWS EC2を3台に分け(別途、multi-AZも実施している)
それぞれのアプリケーションを分けて配置していました。
■開発環境はCentOS6(何故かOSのメッセージ全般がドイツ語)でしたが、
商用はAmazonLinuxでした。

dockerでコンテナ毎にサーバを構築する事で、商用と同じ3サーバ構成にする事で、解決を試みました。
OS差分の問題は、AmazonLinuxのDockerイメージを使うことも考えましたが
開発陣からの、「商用と同じ環境」をより納得させる為に、
商用で稼働しているAmazonLinuxを、Dockerイメージとして使うことで、心理的安全性を担保する事にしました。

将来的にAWS ECSによる、コンテナクラスタのデプロイを想定し
その際は、alpineで再構築する予定だったので、この対応は繋ぎという形でした。
(正直、そのままalpineで構築しておけばよかった)

3.Githubリポジトリが2つ存在し、連携しにくい

配属以前は、SASS/JS/HTMLが別リポジトリで管理されていました。
フロントのリポジトリで、gulpを使用したhtml+SASS+JSのモックを作成し
バックエンドのリポジトリに(ZIPか何かで)移植、viewファイルへ反映を行なっていましたが
・2つのリポジトリとのバージョン整合が大変
・移植コストが大きい。大規模なフロントエンド開発がもう無さそう
という事を考慮し、リポジトリをバックエンドに統合する事にしました。
統合により
・バックエンドのJSやSASSの変更をgulpで検知しコンパイル、ブラウザに即時(又はリロード)で反映
・PHPソースとJS/CSSが正しくバージョン管理されているので、将来的には継続的デプロイとして、GitHubへのPUSH時、JS・SASSを自動でコンパイルし、dockerimageの自動デプロイと、JSやCSS,画像ファイルのS3への自動反映
(AWS codebuildを想定)
が可能だと見積りました。

プロトタイプ作成

プロトタイプを見せる事が説得力に繋がると感じたので、業務の合間にdockerの構築を進めていくことになりました。

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

【PHP8.0】オブジェクト初期化子のRFCが却下されそう

いつのまにやらObject InitializerというRFCが投票に入っていました。
ちょっとだけ面白そうと思ったのですが、ただ、ほぼ確実に却下されるので詳しく見てもしょうがないのでざっくり紹介してみます。

Object Initializer

文法

class Customer{
  public $id;
  public $name;
  private DateTimeImmutable $createdAt;
}

$customer = new Customer{
  id = 123,
  name = "John Doe",
};

newするときに中括弧で引数を渡すと、自動的にプロパティにセットされます。

キーが文字列ではないところが、PHPとして物凄い違和感がありますね。

上の例は、下のようなよくある文と同等です。

$customer = new Customer();
$customer->id = 123;
$customer->name = "John Doe";

従って、privateである$createdAtに値を突っ込むことはできません。

制約

オブジェクト初期化子を使う場合、全てのpublicプロパティを指定しなければなりません。

$customer = new Customer{
  id = 123, // RuntimeException class object failed due to missing required properties
};

オブジェクト初期化子自体を使わない場合は、普通にインスタンス化できます。

$customer = new Customer();

未定義プロパティ

未定義のプロパティに値を突っ込めます。

$baz = 'baz';

$obj = new stdClass {
  foo = "bar",
  $baz = true,
};

ええー、と思いますが、そもそもこれPHPの仕様だったわ。

コンストラクタ

オブジェクト初期化子を使う場合、コンストラクタに引数は渡せません。
即ち、以下のような書き方は文法エラーになります。

$customer = new Customer($dateTime){
  id = 123,
  name = "John Doe",
};

マジックメソッド

可視プロパティがなかった場合、普通にマジックメソッド__setが呼ばれます。
RFCに書かれている以下の例では、$nameはpublicなので直接値が入り、protectedである$emailはマジックメソッド__setが呼ばれることになるようです。

class EmailAddress
{
  protected string $email;
  public ?string $name = null;

  public function __set(string $name, $value): void
  {
    if ($name !== "email") {
      return;
    }
    if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
      throw new InvalidArgumentException("Invalid email address");
    }
    $this->email = $value;
  }
}

$email = new EmailAddress {
  email = "john.doe@example.org",
  name = "John Doe",
};

将来の予定

無名クラスstdClassを使うときは、もうクラス名すらも書かなくてよくない?

$obj = new {
  foo = "bar",
  $baz = true,
};

配列展開してpublicプロパティ突っ込めたらよくない?

$array = [
  'a' => 1,
  'b' => 2,
];

$obj = new { ...$array };

投票

2019/10/07投票開始、2019/10/21投票終了、可決には2/3+1の賛成が必要です。
2019/10/09現在は賛成3反対18で、ほぼ確実に却下されます。

この投票期間、RFCには何故か書かれていません。

感想

うん、まあ却下だよね。

特にpublicプロパティは全て指定しなければならないという厳格さと、未定義プロパティを指定できるという緩さが同居してるのは気持ち悪いというかなんというか。
これならまだAutomatic Property Initializationのほうがいいと思います。
Code free constructorよりはいいと思いますが。

ただ無名クラスはすごい便利そうなので、これは欲しいところですね。

$obj = new{
  'id' = 123,
  'name' = 'John Due',
};

まあ、今でもほぼ同じように書けたりはするんですけどね。

$obj = (object)[
  'id' => 123,
  'name' => 'John Due',
];
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

datetimeを配列にしたり、一行で日付だけ受け取ったり

やりたいこと

'2019-10-09 01:20:33'

というmysqlに登録されているdatetime形式のデータを

$datetime = [
    'year' => '2019',
    'month' => '10',
    'day' => '09',
    'hour' => '01',
    'minute' => '20',
    'second' => '33',
];

といった配列形式にしたり

$day = hogehoge('2019-10-09 01:20:33'); // 09

と、ぱっと取得できるようにしたい。

配列形式にする

// 一般的なdatetime
$datetime1 = getDateTimeArray('2019-10-09 01:20:33');
$datetime2 = getDateTimeArray('2019/10/09 01:20:33');

// 特殊な場合はformatオプションを付ける
$datetime3 = getDateTimeArray('2019年10月09日01時20分33秒', 'Y年m月d日H時i分s秒');

function getDateTimeArray($datetime, $format = '')
{
    if (empty$format) {
        $date = new DateTime($datetime);
    } else {
        $date= DateTime::createFromFormat($datetime, $format);        
    }
    $datetime_key = ['year', 'month', 'day', 'hour', 'minute', 'second'];
    return array_combine(
        $datetime_key,
        preg_split('/[-: ]/', $date->format('Y-m-d H:i:s'))
    );
}

日付だけほしい

// 一行で
$day1 = date_create('2019-10-09 01:20:33')->format('d');

$day2 = DateTime::createFromFormat('Y年m月d日H時i分s秒', '2019年10月09日01時20分33秒')->format('d');

関数使ったほうが見やすいかな

// 一般的なdatetime
$day1 = getDateTimeByFormat('2019-10-09 01:20:33', 'd');

// 特殊な場合
$day2 = getDateTimeByFormat('2019年10月09日01時20分33秒', 'd', 'Y年m月d日H時i分s秒');

function getDateTimeByFormat($datetime, $format, $create_format = "")
{
    if (empty($create_format)) {
        return date_create($datetime)->format($format);
    } else {
        return DateTime::createFromFormat($create_format, $datetime)->format($format);
    }
}

日付以外表示しないならこれでもいいがちょっと長い。

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

Twitterにウェルカムメッセージを設定する

Twitterにはウェルカムメッセージを設定することが可能です。
日本語のドキュメントがあまり見当たらなかったので備忘録。

ウェルカムメッセージ

初めてダイレクトメッセージを送る(または会話を始める)ときに自動で表示されます。

ウェルカムメッセージを表示するには2つの方法が存在します。
- ダイレクトメッセージ画面を開いた場合にデフォルトで表示されるメッセージ
- ツイートに付与したボタン(ディープリンク)から遷移する場合にのみ表示される特定のメッセージ

設定してみる

詳しいドキュメントはWelcome Messagesを参照。

Twitterの開発者申請は済んでいるものとして進めていきます。
今回もPHPを使用します。

TwitterOAuth

Twitter APIを使用するためにabraham/twitteroauthを使用します。

$composer require abraham/twitteroauth
Composerでインストールします。

メッセージを登録する

ウェルカムメッセージは表示させるためにはアカウントにメッセージを登録する必要があります。

ドキュメントは下記
direct_messages/welcome_messages/new

create_message.php
<?php
require './vendor/autoload.php';

use Abraham\TwitterOAuth\TwitterOAuth;
use Dotenv\Dotenv;

$dotenv = Dotenv::create($_SERVER['PWD']);
$dotenv->load();

$twitter = new TwitterOAuth(
    $_SERVER['CONSUMER_KEY'],
    $_SERVER['CONSUMER_SECRET_KEY'],
    $_SERVER['OAUTH'],
    $_SERVER['OAUTH_SECRET']
);

$text = "これはWelcomeMessageです。
自動で送信されます。";

$params = [
    "welcome_message" => [
        "message_data" => [
            "text" => $text
        ]
    ]
];

$res = $twitter->post('direct_messages/welcome_messages/new', $params, true);
var_dump($res);

ウェルカムメッセージの基本はダイレクトメッセージと同じです。
message_dataにメッセージで表示させたいテキストを記述します。

リクエストが成功すると下記のレスポンスが返ってきます。

{
  "welcome_message": {
    "id": "1181826364040605700",
    "created_timestamp": "1570593298193",
    "message_data": {
      "text": "これはWelcomeMessageです。
自動で送信されます。",
      "entities": {
        "hashtags": [],
        "symbols": [],
        "user_mentions": [],
        "urls": []
      }
    },
    "source_app_id": "*****"
  },
  "apps": {
    "*****": {
      "id": "*****",
      "name": "AppsName",
      "url": "https:\\/\\/twitter.com\\/good_wall"
    }
  }
}

重要なのはwelcome_message内のidです。
デフォルトで表示する場合にもディープリンクから表示する場合にもこのidが必要なためメモ等しておきます。
忘れたとしてもGET direct_messages/welcome_messages/listで登録しているメッセージの一覧を取得することができます。

メッセージは1つだけではなく複数登録することが可能です。

デフォルトで表示する

メッセージは登録しただけでは表示されることはありません。
まずはデフォルトで表示されるように設定してみます。

ドキュメントは下記
POST direct_messages/welcome_messages/rules/new

create/rules.php
<?php
require './vendor/autoload.php';

use Abraham\TwitterOAuth\TwitterOAuth;
use Dotenv\Dotenv;

$dotenv = Dotenv::create($_SERVER['PWD']);
$dotenv->load();

$twitter = new TwitterOAuth(
    $_SERVER['CONSUMER_KEY'],
    $_SERVER['CONSUMER_SECRET_KEY'],
    $_SERVER['OAUTH'],
    $_SERVER['OAUTH_SECRET']
);

$welcome_message_id = "1181826364040605700";

$params = [
    "welcome_message_rule" =>
    [
        "welcome_message_id" => $welcome_message_id
    ]
];

$res = $twitter->post('direct_messages/welcome_messages/rules/new', $params, true);
var_dump($res);

$welcome_message_idには先程メッセージの登録時に返ってきたidを記述します。

リクエストが成功すると下記のレスポンスが返ってきます。

{
  "welcome_message_rule": {
    "id": "1181817806356172805",
    "created_timestamp": "1570601570303",
    "welcome_message_id": "1181826364040605700"
  }
}

指定したwelcome_message_idが含まれたwelcome_message_ruleが返ってくれば成功です。

実際の挙動を確認します。
ezgif-3-5d981add13c6.gif
DM画面を開くと自動でメッセージが表示されました。(ブラウザだとラグが…)

デフォルトで設定できるメッセージは1アカウントに対して1つです。
メッセージを変更をする場合には現在設定されているルールを削除してから新しいルール登録する必要があります。

特定のツイートから表示する

ディープリンクを使用することでデフォルト以外のメッセージを表示させることも可能です。

ドキュメントは下記
Deeplinking to a Welcome Message

Example Direct Message Deeplink:
https://twitter.com/messages/compose?recipient_id=3805104374&welcome_message_id=12345

ディープリンクはTwitterのuser_idとwelcome_message_idを用いて作成します。
recipient_idにはusr_idを,welcome_message_idにはwelcome_message_idを指定します。

新たにディープリンク用のメッセージを登録しました。

{
  "welcome_message": {
    "id": "1181846303585771525",
    "created_timestamp": "1570604597384",
    "message_data": {
      "text": "これはディープリンクで表示されるメッセージです。",
      "entities": {
        "hashtags": [],
        "symbols": [],
        "user_mentions": [],
        "urls": []
      }
    },
    "source_app_id": "*****"
  },
  "apps": {
    "*****": {
      "id": "*****",
      "name": "AppsName",
      "url": "https:\\/\\/twitter.com\\/good_wall"
    }
  }
}

これを元にディープリンクを作成します。

https://twitter.com/messages/compose?recipient_id=127167709&welcome_message_id=1181846303585771525
※クリックするとTwitterのDM画面が開いてメッセージが飛んできます。

このディープリンクを呟くとダイレクトメッセージボタンが生成されます。
image.png
上記リンク及びボタンからDM画面を開くと先程登録したメッセージが表示されます。

ディープリンクからDM画面に遷移した場合はデフォルトのメッセージは表示されません。

基本がダイレクトメッセージなのでボタンやクイックリプライの設定が可能です。
ezgif-3-0e8390b31192.gif

ボタンやクイックリプライについてはこちらを参考にしてください。

まとめ

Account Activity APIと併用すればチャットボット開発できるな〜とか思いました。

デフォルトのメッセージは停止済みですが,ディープリンクの方は残してあります。
実際に試してみたい方はリンクを押してみてください。
ウェルカムメッセージの履歴は送信者側には残りません。
そのため通知等がこちらに飛ぶことはないので安心してください。

おわり。

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

CSP(Content-Security-Policy) でインラインのCSSは許可したい

Content-Security-Policy

JavaScriptのみをはじきたかった。
インラインCSSは許可したい。

これだとインラインCSSもはじかれてしまう。

header("Content-Security-Policy: default-src 'self';");

https://developer.mozilla.org/ja/docs/Glossary/Fetch_directive

すべてのフェッチディレクティブは default-src で代替されます。

script-src ディレクティブにのみ指定をする。

header("Content-Security-Policy: script-src 'self';");

https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Security-Policy/script-src

script-src ディレクティブは、 JavaScript の情報なソースを指定します。

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

【Laravel】管理画面と一般サイトでログファイルを分けてみる

一般ユーザがアクセスする画面と、管理者がアクセスする画面があるサイトの場合、一つのログに出力していると、両方のログが混在して「これはどっちの処理のログ?」みたいになって分かりづらい。

  • 一般ユーザは、一般ユーザ用のログファイルに出力
  • 管理者は、管理者用のログファイルに出力

という風に完全に分けられれば見やすくなるはず。

さっそく分けてみた。

環境

  • laravel 5.8

前提

一般ユーザ用の画面

「https://example.com/admin」配下以外は全部、一般ユーザ用

管理画面

逆に「https://example.com/admin」配下は全部、管理画面

作ってみた

Larvael5.6からログの設定方法・出力方法が大きく変わった様で、今回は5.8で試しています。(検証はしてないけど、たぶん5.6〜5.7でも動くのではないかと。)

ざっくりとした手順

  1. 一般ユーザ用と管理者用のログチャネルを作成
  2. ログ出力用の自作ファサードを作成し、そこでログチャネルを指定する
  3. 自作ファサードの登録

一般ユーザ用と管理者用のログチャネルを作成

ログ設定用のコンフィグファイルに設定を追加します。
adminチャネルとuserチャネルを追加し、それぞれ出力先ファイルを指定する。

/config/logging.php
    'channels' => [
        /**
         * 
         * 元々書いてある設定は省略
         * 
         */


        //以降を追加
        'admin' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel_admin.log'),
            'level' => env('LOGGING_LEVEL', 'info'),
            'days' => env('LOG_ROTATE_DAYS', 14),
        ],

        'user' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel_user.log'),
            'level' => env('LOGGING_LEVEL', 'info'),
            'days' => env('LOG_ROTATE_DAYS', 14),
        ],
    ],

ログ出力用の自作ファサードを作成し、そこでログチャネルを指定する

ファサードクラス

公式の手順通りに作成

/app/Facades/CustomLogFacade.php
<?php

namespace App\Facades;


use App\Services\Support\CustomLogSupport;
use Illuminate\Support\Facades\Facade;

class CustomLogFacade extends Facade
{

    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return CustomLogSupport::class;
    }
}

ロジッククラス

ファサードの本体を作成します。

/app/Service/Support/CustomLogSupport.php
<?php

namespace App\Services\Support;

use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Str;

class CustomLogSupport
{
    public function __call($name, $args)
    {
        $chanel = 'stack';
        try{
            $domain = config('app.url');
            $url = Request::url();
            if(Str::is($domain.'/admin/*', $url)){
                $chanel = 'admin';
            }else if(Str::is($domain.'/*', $url)){
                $chanel = 'user';
            }else{
                $chanel = 'batch';
            }
        }catch (\Exception $e){
            //握りつぶす
        }

        $collectArgs = collect($args);
        Log::channel($chanel)->{$name}($collectArgs->shift(), $collectArgs->toArray());
    }
}

マジックメソッドの__call()を使って、Logファサードをラップしてます。
その際にchannel($chanel)で使用するチャネルを指定しています。

このチャネル振り分けロジックを変えればサブドメイン毎にログを分ける事もできそうです。

自作ファサードの登録

これもお決まり通りに登録します。

サービスコンテナに登録

/app/Providers/AppServiceProvider.php
    public function register()
    {
        //ログ出力
        $this->app->singleton(CustomLogSupport::class, CustomLogSupport::class);
    }

ファサードの登録

/app/config/app.php
    'aliases' => [
        /**
         * 
         * 元々書いてある設定は省略
         * 
         */


        'CustomLog' =>  \App\Facades\CustomLogFacade::class,
    ],

結果

$ ls
laravel_admin-2019-10-09.log    laravel_user-2019-10-09.log

分割して出力された!

ちなみにファサードの実態クラス(今回でいうとCustomLogSupport)ってどういうクラス名で、どういうディレクトリに格納するのが一般的なんですかね?

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

【Laravel】ログファイルを機能ごとに分けてみる

一般ユーザがアクセスする画面と、管理者がアクセスする画面があるサイトの場合、一つのログに出力していると、両方のログが混在して「これはどっちの処理のログ?」みたいになって分かりづらい。

  • 一般ユーザは、一般ユーザ用のログファイルに出力
  • 管理者は、管理者用のログファイルに出力

という風に完全に分けられれば見やすくなるはず。

さっそく分けてみた。

環境

  • laravel 5.8

前提

一般ユーザ用の画面

「https://example.com/admin」配下以外は全部、一般ユーザ用

管理画面

逆に「https://example.com/admin」配下は全部、管理画面

作ってみた

Larvael5.6からログの設定方法・出力方法が大きく変わった様で、今回は5.8で試しています。(検証はしてないけど、たぶん5.6〜5.7でも動くのではないかと。)

ざっくりとした手順

  1. 一般ユーザ用と管理者用のログチャネルを作成
  2. ログ出力用の自作ファサードを作成し、そこでログチャネルを指定する
  3. 自作ファサードの登録

一般ユーザ用と管理者用のログチャネルを作成

ログ設定用のコンフィグファイルに設定を追加します。
adminチャネルとuserチャネルを追加し、それぞれ出力先ファイルを指定する。

/config/logging.php
    'channels' => [
        /**
         * 
         * 元々書いてある設定は省略
         * 
         */


        //以降を追加
        'admin' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel_admin.log'),
            'level' => env('LOGGING_LEVEL', 'info'),
            'days' => env('LOG_ROTATE_DAYS', 14),
        ],

        'user' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel_user.log'),
            'level' => env('LOGGING_LEVEL', 'info'),
            'days' => env('LOG_ROTATE_DAYS', 14),
        ],
    ],

ログ出力用の自作ファサードを作成し、そこでログチャネルを指定する

ファサードクラス

公式の手順通りに作成

/app/Facades/CustomLogFacade.php
<?php

namespace App\Facades;


use App\Services\Support\CustomLogSupport;
use Illuminate\Support\Facades\Facade;

class CustomLogFacade extends Facade
{

    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return CustomLogSupport::class;
    }
}

ロジッククラス

ファサードの本体を作成します。

/app/Service/Support/CustomLogSupport.php
<?php

namespace App\Services\Support;

use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Str;

class CustomLogSupport
{
    public function __call($name, $args)
    {
        $chanel = 'stack';
        try{
            $domain = config('app.url');
            $url = Request::url();
            if(Str::is($domain.'/admin/*', $url)){
                $chanel = 'admin';
            }else if(Str::is($domain.'/*', $url)){
                $chanel = 'user';
            }else{
                $chanel = 'batch';
            }
        }catch (\Exception $e){
            //握りつぶす
        }

        $collectArgs = collect($args);
        Log::channel($chanel)->{$name}($collectArgs->shift(), $collectArgs->toArray());
    }
}

マジックメソッドの__call()を使って、Logファサードをラップしてます。
その際にchannel($chanel)で使用するチャネルを指定しています。

このチャネル振り分けロジックを変えればサブドメイン毎にログを分ける事もできそうです。

自作ファサードの登録

これもお決まり通りに登録します。

サービスコンテナに登録

/app/Providers/AppServiceProvider.php
    public function register()
    {
        //ログ出力
        $this->app->singleton(CustomLogSupport::class, CustomLogSupport::class);
    }

ファサードの登録

/app/config/app.php
    'aliases' => [
        /**
         * 
         * 元々書いてある設定は省略
         * 
         */


        'CustomLog' =>  \App\Facades\CustomLogFacade::class,
    ],

結果

$ ls
laravel_admin-2019-10-09.log    laravel_user-2019-10-09.log

分割して出力された!

ちなみにファサードの実態クラス(今回でいうとCustomLogSupport)ってどういうクラス名で、どういうディレクトリに格納するのが一般的なんですかね?

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

【Laravel】自作のミドルウェア内でログインユーザー情報が取得できなかった原因

ログインしているユーザー情報(ユーザーID)をログに出力したいなと思い、ミドルウェアを作成したのですがAuth::id()でうまく取得できなかった。

作ってみた

環境

  • Laravel 5.8

ミドルウェアを作成

UserIdLogMiddleware.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;

class UserIdLogMiddleware
{

    /**
     *
     *
     * @param $request
     * @param Closure $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        try{
            Auth::check()
                ?Log::info('ユーザーID:'.Auth::id())
                :Log::info('未ログイン');
        }catch(\Exception $exception){
            //ログ出力できなくてもエラーにしない
            Log::warning('ユーザーIDの取得に失敗');
        }

        return $next($request);
    }
}

ミドルウェアを登録

今回は全部のリクエスト時に適用できればいいから$middlewareに設定しよう。(今思えばこれが間違いの始まりだった・・・)

/app/Http/Kernel.php
    protected $middleware = [
        \App\Http\Middleware\TrustProxies::class,
        \App\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \App\Http\Middleware\UserIdLogMiddleware::class,    //ここに追加
    ];

実行結果

local.INFO: 未ログイン

・・・ログインIDが取得できていない。

原因

ブレークポイント貼って色々と処理を辿ってみたところミドルウェアの実行順番は、

  1. グローバルミドルウェア($middleware)
  2. ルートミドルウェア($routeMiddleware)

となっていた。なるほど。

と、ここで、ふと思いました。

ログインユーザ情報ってなんで取得できるんだろう??

そういえば、ログイン済みユーザだけのルーティングってauthミドルウェアを適用しているし、

Route::get('profile', function() {
    // 認証済みのユーザーのみが入れる
})->middleware('auth');

(Laravel日本語ページから引用)

その中のコントローラとかでAuth::id()ってやるし、、、

もしや、認証情報ってauthミドルウェア(\App\Http\Middleware\Authenticate)を実行した後じゃないと、Authファサードで取れないのでは?
考えてみれば、そりゃそうだ。

と、ここでKernelクラスをもう一度見てみると、

    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
    ];

うん、authミドルウェアはルートミドルウェア($routeMiddleware)に書いてある。

何がダメだったのか

authミドルウェアが動く前に実行されていたから!

修正

  • グローバルミドルウェア → ルートミドルウェアに変更
  • そしてauthミドルウェアの後に動かす

修正結果はこちら

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [
        \App\Http\Middleware\TrustProxies::class,
        \App\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
//        \App\Http\Middleware\UserIdLogMiddleware::class,    //ここに追加
    ];

    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            'user.log',  //グループに入れて全リクエストに適用する
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
        ],
    ];

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        'user.log' => \App\Http\Middleware\UserIdLogMiddleware::class,  //こっちに移動
    ];

    /**
     * The priority-sorted list of middleware.
     *
     * This forces non-global middleware to always be in the given order.
     *
     * @var array
     */
    protected $middlewarePriority = [
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\Authenticate::class,
        \App\Http\Middleware\UserIdLogMiddleware::class,  //実行の順番を指定しておく
        \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \Illuminate\Auth\Middleware\Authorize::class,
    ];
}

修正後の実行結果

local.INFO: ユーザーID:2

出力された!

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

Simple HTML DOM Parserで読み込むと改行が削除されてしまう対応

PHPでhtmlのdom操作はSimple HTML DOM Parserが便利だなと思って使ってみましたが、
file_get_htmlで読み込むと改行コードが勝手に削除されてしまってハマったのでメモです。

試してみたこと

file_get_htmlのオプションを指定する

file_get_htmlで読み込む際のオプション指定で$stripRNをfalseにするといいという記述を見つけたのでやってみた

$html = file_get_html($file, false, null, -1, -1, true, true, DEFAULT_TARGET_CHARSET, false);

str_get_htmlの場合はこのよう

$html = str_get_html($str, true, true, DEFAULT_TARGET_CHARSET, false);


が結果は変わらずだった。

MAX_FILE_SIZEを増やしてみる

読み込むHTMLのデータ量が大きすぎると読み込みに失敗するそうです。
default600KBに設定されてるので、simple_html_dom.phpを直接開いてMAX_FILE_SIZEの値を増やしてみた


が結果は変わらずだった。

file_get_htmlの$stripRNオプションを直接書き換える

simple_html_dom.phpを直接編集してfile_get_html関数の$stripRNのdefault値をfalseにしてみた

スクリーンショット 2019-10-09 10.43.33.png


すると無事改行が削除されていたのが改善された

推奨できる内容かは分かりませんが、最終手段としてのメモとして。。
これで改善されたということはオプションの指定の方法が間違っていただけかも。。

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

bookedの入力画面をカスタマイズする

www.bookedscheduler.com

予約の作成で表示される文字列の改造ができた。(バージョン2.7.4のお話です)

C:\Apache24\htdocs\booked\lang\ja_jp.php

ja_jp.php
/* $strings['ReservationTitle'] = '件名';*/
$strings['ReservationTitle'] = '所属 氏名';

/*$strings['ReservationDescription'] = '説明';*/
$strings['ReservationDescription'] = '説明文 入力者を記入';

こんな感じ

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

composer requireがInstallation failedするけどどうしてもrequireしたいんだ

Composer requireしたらなんか怒られて失敗した

# composer require kalnoy/nestedset
Using version ^5.0 for kalnoy/nestedset
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - illuminate/events v6.2.0 requires php ^7.2 -> your PHP version (7.1.29) does not satisfy that requirement.
    - 省略
    - illuminate/events 6.x-dev requires php ^7.2 -> your PHP version (7.1.29) does not satisfy that requirement.
    - Conclusion: don't install kalnoy/nestedset v5.0.0
    - Conclusion: remove laravel/framework v5.6.38
    - Installation request for kalnoy/nestedset ^5.0 -> satisfiable by kalnoy/nestedset[v5.0.0, v5.x-dev].
    - Conclusion: don't install laravel/framework v5.6.38
    - kalnoy/nestedset v5.x-dev requires illuminate/events ~5.7.0|~5.8.0|^6.0 -> satisfiable by illuminate/events[5.7.17,省略v6.0.4, v6.1.0, v6.2.0].
    - don't install illuminate/events 5.7.17|don't install laravel/framework v5.6.38
    - 省略
    - don't install illuminate/events v5.8.9|don't install laravel/framework v5.6.38
    - Installation request for laravel/framework (locked at v5.6.38, required as 5.6.*) -> satisfiable by laravel/framework[v5.6.38].


Installation failed, reverting ./composer.json to its original content.

原因

今入れようとしたパッケージ(kalnoy/nestedset)とすでに入っているパッケージ(laravel/framework)がそれぞれ同じパッケージ(Illuminate/events)の違うバージョンに依存してしまっている。
本来なら古いバージョンのものをわざわざ入れるのは良くない(と思う。)が、今回はどうしても使いたいし、今後すでに入っている方(laravel/framework)のパッケージのバージョンをあげた際に解決する可能性もあるので強引に突破する。

解決法

このようにバージョン指定にアスタリスクをつけて、依存関係が解決されるバージョンが存在するのを祈る。

# composer require kalnoy/nestedset:4.*

kalnoy/nestedsetは現時点で5系が最新、4系の最新が4.3.5だったが、4.3.4で入った。
これでもしだめだったら:3.*:2.*と順にやっていくしかない。

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