- 投稿日:2020-04-21T23:45:20+09:00
【WordPress】クライアントワークにおけるブロックエディタ対応テーマ構築時の小ネタ集
はじめに
WordPress5.0にブロックエディタが搭載されてから、一年半経ちました。
エンジニアやブロガーの間では「ブロックエディタすげえ!」「もうクラシックエディタには戻れない。。」といった声を良く聞きますが、実際のクライアントワークにおいては、まだまだ浸透していないように思います。
これまで多数のWordPressサイト構築案件を担当してきましたが、特にデザイン優先のサイトやコーポレートサイト構築ではブロックエディタの話が出る事も無く、「ブロックエディタ?何それ?」というクライアントさんや元請け会社さんばかりでした。
ですが先日、ブロックエディタで再現しやすいデザインであった事、かつクライアントさんがゴリゴリ記事を投入していきたいという熱量を感じられた案件があったため、「ブロックエディタにフル対応したコーポレートサイト構築」という方針を取りました。
その開発の中で、ブロックエディタの魅力を十分に引き出しつつ、不慣れなクライアントさんでも使いやすいように取り入れたノウハウをまとめて共有したいと思います。
対象とする方
- HTML5/CSS3に沿ったコーディングが出来る。
- 一からオリジナルテーマを作成でき、かつアクションフック・フィルターフック・テンプレートタグを適切に使用出来る。
- ブロックエディタにおいて、バックエンド、フロントエンド側それぞれに適用するCSS/JSをどのようにenqueueすればよいか何となく理解している。
- エディタ側、フロントエンド側のブロックでどのようなclass名が付与されるかを知っている(探す事が出来る)
- npm、babel、webpackというワードは知らなくてもとりあえずおk
- カスタムブロックは作った事がなくてもおk
備考
- 記事執筆にあたり検証した環境は、WordPress 5.4です。
- フロント側でブロック用のデフォルトスタイルが読み込まれている事を前提としています。 (
wp_dequeue_style( 'wp-block-library' );
をfunctions.phpに記述していなければ自動で読み込まれます)色設定編
カラーピッカーを非表示にする
デフォルトで用意されているカラーパレットの他にカラーピッカーがあり、直観的にで好きな色を選ぶことが出来ます。
便利そうな機能ですが、同じ色でもブレが生じたり、またクライアントさんの趣向によっては何ともカラフルなコンテンツが出来上がってしまう可能性がありますので、無効にします。
functions.phpadd_theme_support( 'disable-custom-colors' );またインラインスタイルで色が指定されるため、次項に述べるような一括での色調整が難しくなります。
カラーパレットを変更する
デフォルトでは12色用意されていますが、ベースとなるテーマのカラーに合わせて、パレットのバリエーションを変更します。
デザインに疎いためどのような配色が最適かはっきり言えませんが、テーマで使われている色のうち、
- メインカラー
- サブカラー
- フォントカラー
- アクセントカラー
あたりをピックアップして組み合わせれば良いと思います。
Qiitaを例にして、実際のコードを記載します。
functions.phpadd_theme_support( 'editor-color-palette', array( // array( // 'name' => 'カラーパレットをマウスオーバーした際のタイトル', // 'slug' => '付与されるclass名', // 'color' => 'カラーコード', // ), array( 'name' => 'メインカラー', 'slug' => 'main', 'color' => '#55c500', ), array( 'name' => 'サブカラー', 'slug' => 'sub', 'color' => '#3f9200', ), array( 'name' => 'フォントカラー', 'slug' => 'font', 'color' => '#333333', ), array( 'name' => 'アクセントカラー', 'slug' => 'accent', 'color' => '#cd2e22', ), ) );アクセントカラーは、以下のサイトからピックアップしました。
ベースカラーを指定するだけで、いい感じのカラーバリエーションを生成してくれます。ここまでの設定で、サイドバーの色設定は以下のようになります。
ちょっと地味過ぎましたね。。
なお、WordPress5.4から文章中で部分的に色を設定できるインラインカラーが追加され、ブロックの文字色・背景色とあわせると、カラーパレットを適用した対象に以下のようなclassが付与されます。
カラーパレット適用の対象 付与されるclass名 インライン文字色 has-inline-color,
has-{slug}-colorブロック文字色 has-text-color
has-{slug}-colorブロック背景色 has-background
has-{slug}-background-colorなので、文字色であれば
.has-{slug}-color
に、背景色であれば.has-{slug}-background-color
にスタイルを当てる事になります。インラインで指定しているわけではないので、例えば「全部のアクセントカラーをもう少し濃くしたいなあ」と思った時は、
.has-accent-color
と.has-accent-background-color
のプロパティ値を変えるだけで済みます。また、slugに
red
やyellow
などの色名ではなく、main
やsub
といったclass名としたのには理由があります。例えば、メインカラーを
red
からblue
にしたいとなった場合。
メインカラーのセレクタを.has-red-color
としていた場合、「class名にredが含まれてるのに、なぜか表示される色が青」と混乱が起こってしまいます。色設定自体を無効にする
実際の案件で発生する事はあまりないと思いますが、色設定自体を無効にする事もできます。
インライン文字色、ブロック文字色、ブロック背景色すべてが無効になります。functions.phpadd_theme_support( 'editor-color-palette' ); add_theme_support( 'disable-custom-colors' );グラデーション設定を無効化する
WordPress5.4では、ボタンブロックとカバーブロックでグラデーションを利用出来るようになり、カラーピッカーに加えてタイプ(円形/線形)角度を調整する事が出来ます。
ここも「色設定のカラーピッカーを非表示にする」と同様の理由で、カスタム設定を無効化します。
functions.phpadd_theme_support( 'disable-custom-gradients' );グラデーションのカラーパレットを変更する
配色の決め方は「色設定のカラーパレットを変更する」に準ずるとして、コードの一例を記載します。
functions.phpadd_theme_support( 'editor-gradient-presets', array( array( 'name' => 'メイングラデーション', 'gradient' => 'linear-gradient(to right, #55c500, #3f9200)', 'slug' => 'main', ), ) );グラデーションを無効化する
グラデーション自体使わないという場合は、無効にすることも出来ます。
functions.phpadd_theme_support( 'disable-custom-gradients' ); add_theme_support( 'editor-gradient-presets' );すっきりしましたね!
テキスト設定編
テキスト設定でフォントサイズの数値指定を無効化する
プリセットサイズとして用意されているバリエーションに加え、数値入力(px)でフォントサイズを変更出来ます。
これも、「色設定のカラーピッカーを非表示にする」「グラデーション設定を無効化する」と同様の理由で無効化します。
またpx指定なので、非レスポンシブとなってしまいます。
functions.phpadd_theme_support( 'disable-custom-font-sizes' );テキスト設定でフォントサイズのバリエーションを変更する
デフォルト(class無し)に加えて5サイズ用意されており、それぞれ付与されるclass名と適用されるフォントサイズは以下の通りです。
プリセットサイズ 付与されるclass名 フォントサイズ(エディタ側) フォントサイズ(フロント側) 小 has-small-font-size 13px 13px 標準 has-normal-font-size 16px 16px 中 has-medium-font-size 20px 20px 大 has-large-font-size 36px 36px 特大 has-huge-font-size 48px 42px ※上記に加えて、
has-regular-font-size
、has-larger-font-size
というclass名も定義されていますが、現在は使われていないようです。
gutenberg/style.scss at c19d2d908cba695960cf8407bd0b0afc181aa657 · WordPress/gutenberg · GitHub]前述の通り、フォントサイズはpx指定のため、例えばPC/スマホでベースのフォントサイズが変わった場合にバランスが崩れてしまいます。
そのため、emまたはremでのフォントサイズ指定に変更するか、テーマにあわせてメディアクエリ等で調整します。上記セレクタをそのまま使っても良いのですが、バリエーションを変えたい場合は以下のように記述します。
functions.phpadd_theme_support( 'editor-font-sizes', array( // array( // 'name' => 'プルダウンの表示名', // 'size' => 'フォントサイズ(px)', // 'slug' => '付与されるclass名', // ), array( 'name' => '小', 'size' => 14, 'slug' => 'small', ), array( 'name' => '中', 'size' => 20, 'slug' => 'medium', ), array( 'name' => '大', 'size' => 24, 'slug' => 'large', ), ) );ここでは、ベースのフォントサイズを16pxとして、3パターンを定義しました。
また、エディタ側ではフォントサイズをpxでしか指定できないため、16pxをベースにフォントサイズを決定します。付与されるclass名は「色設定」の場合と同様で、
has-{slug}-font-size
というクラスが付与されます。レスポンシブ対応のため、フォントサイズをemで指定する場合の例は以下。
style.css.has-xs-font-size { font-size: 0.875em; } .has-md-font-size { font-size: 1.25em; } .has-lg-font-size { font-size: 1.5em; }ここまでの設定で、サイドバーのテキスト設定は以下のようになります。
エディタ編
以下項目は、全てCSSで調整を行いますが、エディタ側に独自のCSSを適用する方法は3種類あります。
add_theme_support( 'editor-styles' )
、add_editor_style( 'editor-style.css' )
で読み込む
→エディタのみに適用される。また、各セレクターの直前にブロックエディタ全体を囲っている.editor-styles-wrapper
というクラスが自動的に挿入される。- アクションフック(
enqueue_block_editor_assets
)で読み込む
→エディタのみに適用される。- アクションフック(
enqueue_block_assets
)で読み込む
→フロント側・エディタ側双方で読み込まれる。理解があやふやな方は、手前味噌ですがこちらの記事をご覧ください。
【WordPress】 Gutenberg関連のCSSまとめ
今回は、エディタ側でのみ読み込まれ、かつ書いたCSSがそのまま出力される2番の方法(
enqueue_block_editor_assets
)での具体例を書きます。フォント・文字サイズ
クライアントさんの記事投稿のストレスを減らすために、エディタの見た目をフロント側に出来るだけ近づける事は必須です。
まずは、デフォルトの明朝体(Noto Serif JP)からゴシック体に変更し、同時にフォントサイズ・文字色等も含めてスタイリングします。
block-editor-style.scss.editor-styles-wrapper { > * { color: #333; font-size: 18px; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Yu Gothic", YuGothic, Verdana, Meiryo, "M+ 1p", sans-serif; line-height: 2; } }
.editor-styles-wrapper
に書きたい所ですが、ノーマライズ用のスタイルがbody閉じタグ直前にインラインで出力されるため、優先度の関係で適用されないプロパティがあります。そのため、既に定義されているプロパティについては
.editor-styles-wrapper
直下の要素に定義します。※
add_theme_support( 'editor-styles' )
、add_editor_style( 'editor-style.css' )
でスタイルで読み込んでいる場合は、ノーマライズ用のインラインスタイルの後にインラインで出力されるので、、.editor-styles-wrapper
(bodyタグ)に全て書いてしまってOKです。背景色をスタイリングする
フロント側でbodyに背景色があるデザインの場合、エディタ側にも同じ背景色があたっていると一気に雰囲気が出ます(僕だけ?)
背景色はノーマライズ用CSSに無いので、.editor-styles-wrapper
書けば適用されます。block-editor-style.scss.editor-styles-wrapper { background: #eee; }タイトルをスタイリングする
タイトル自体に各種スタイルがデフォルトで当たっているので、これもフロントにあわせて調整します。
特に、デフォルトのフォントサイズはデカすぎると思う。。同時に、タイトルの
textarea
にfocusされた時の文字色もデフォルトを上書きします。block-editor-style.scss.editor-post-title__block { .editor-post-title__input { padding: 10px 14px; color: #333; font-size: 26px; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Yu Gothic", YuGothic, Verdana, Meiryo, "M+ 1p", sans-serif; line-height: 1.4; &:focus { color: #333; } } }ブロックの間隔をあける
特にテキストメインのコンテンツの場合に顕著ですが、あるブロックにフォーカスした時に、前(上)のブロックが一部隠れて文字が見えない、という状態が起こります。
テキストメインの記事の場合、たとえ一文でも隠れるのは編集時にストレスになる思うので、ブロック間の間隔を広げます。
ここでは、デフォルトの28pxから56pxに広げます。block-editor-style.scss.editor-styles-wrapper { .block-editor-block-list__block { margin-top: 56px; margin-bottom: 56px; } }ただし、ブロックの間隔がフロント側と違う事になるケースが多いので、記事執筆の快適さを優先するのか、フロント・エディタの見た目の整合性を優先するのか、クライアントさんの意向も含めて検討すべきです。
ブロックの幅を広げる
デフォルトではブロックの幅が580pxと狭いので、フロント側に合わせて広げます。
一般的にはコンテナ幅に合わせる事になりますが、管理画面側は左右にサイドバーがあるため、幅を大きくし過ぎるとエディタ領域一杯にブロックが広がってしまい、幅広・全幅を表現出来なくなる場合があります。
フロント側のコンテナ幅、幅広・全幅対応の有無、クライアントさんが記事投稿する時のPC環境等を考慮しながら、最適な値を探りましょう。
block-editor-style.scss.wp-block { max-width: 720px; } // 幅広 .wp-block[data-align="wide"] { max-width: 1080px; } // 全幅 .wp-block[data-align="full"] { max-width: none; }ブロック編
使わないブロックを非表示にする
調べた所、WordPress5.4で用意されているコアブロックは68種類。
汎用的なテーマでない限り、全てのブロックをスタイリングするのは現実的ではありません。設計の話も関係しますが、デザインありきの構築の場合、どのパーツを、どのブロックを使って、どのように表現するかを考えると思います。
そこで、そこから漏れたブロックは一旦スタイリングの対象外とし、無効化します。その後、クライアントさんが使いたい(使いそうな)ブロックを都度解放・スタイリングしていけば良いと思います。
方法は3パターン。
- ブロックマネージャーを使う
→エディタから設定出来るので一番手軽ですが、設定箇所を知っている人なら再表示出来てしまいます。- プラグインを使う
→Disable Gutenberg Blocksという専用のプラグインがありますが、最近のメジャーリリースでテストされていません。 もしくは、カスタムブロック用のプラグインに機能として内包されているものもあります。(Advanced Gutenbergなど)- functions.phpに記述する
→ブロックを無効化するのではなく、ゼロから使うブロックを登録していく方法です。 投稿タイプ別に使うブロックを出しわける事が出来るので、例えば「お知らせはシンプルに投稿したい」といったクライアントさんにはお勧めの方法です。以下は、3番の方法で投稿の場合のみ使えるブロックを見出し・段落に制限する例です。
functions.phpfunction wpdocs_allowed_block_types( $allowed_block_types, $post ) { if ( $post->post_type !== 'post' ) { return $allowed_block_types; } return array( 'core/heading', 'core/paragraph', ); } add_filter( 'allowed_block_types', 'wpdocs_allowed_block_types', 10, 2 );ブロックスタイルを無効化する
「使わないブロックを非表示にする」と同様の理由で、コアブロックで使わないスタイルは一旦無効にします。
コードはJavaScriptで記述するので、エディタ側で読み込まれるフックを使ってjsファイルを読み込みます。
functions.phpfunction mytheme_enqueue_block_editor() { wp_enqueue_script( 'mytheme-block-editor-script', get_theme_file_uri( '/editor.js' ), array( 'wp-blocks', 'wp-dom' ), wp_get_theme()->get( 'Version' ), true ); } add_action( 'enqueue_block_editor_assets', 'mytheme_enqueue_block_editor' );以下は、区切り(幅広線)、区切り(ドット)画像(角丸)、引用(大)を無効化する例。
editor.jswp.domReady( function() { // サンプル // wp.blocks.unregisterBlockStyle( 'ブロック名', 'スタイル名' ); // 区切り(幅広線) wp.blocks.unregisterBlockStyle( 'core/separator', 'wide' ); // 区切り(ドット) wp.blocks.unregisterBlockStyle( 'core/separator', 'dots' ); // 画像(角丸) wp.blocks.unregisterBlockStyle( 'core/image', 'rounded' ); // 引用(大) wp.blocks.unregisterBlockStyle( 'core/quote', 'large' ); });DOMが構築された後に実行させたいため、必ずwp.domReady()で囲います。
また、カスタムスタイルの登録(
registerBlockStyle
)も併用する場合は、競合を避けるためにregisterBlockStyle()のあとに追加します。スタイル名の調べ方ですが、ブロックにそのスタイルを適用すると、サイドバーの「高度な設定 > 追加CSSクラス」にclass名が反映されるので、「
is-style-{slug}
」の{slug}
の部分をスタイル名にします。スペーサーブロックを見えるようにする
特定のブロック間の余白をスペーサーブロックですが、エディタ側ではクリックしない限り、そこには何も表示されません。
スペースなので見えなくて当たり前ですが、エディタ側では後から微調整しやすくするために、クリックしなくても視認出来るようにスタイリングします。
block-editor-style.scss.block-library-spacer__resize-container { display: flex; align-items: center; justify-content: center; color: #666; background: #ddd; &::before { content: "スペース"; } }区切りブロックをクリック出来ない!
区切りブロックを使う場合は深刻な問題だと思っているのですが、区切り線の高さが2pxしかないため、ブロックを選択状態にすることが困難です。
そこで、疑似的にクリック領域を広げつつ、
cursor: pointer
でクリック出来る領域である事を知らせます。block-editor-style.scss.wp-block-separator { padding: 10px 0; cursor: pointer; }あとがき
メジャーなものからマイナーなものまでありますが、案件によって正解は様々だと思います。
せっかくブロックエディタを導入したのに、クライアントさんに「何だか使いにくいなあ。。」と思われないよう、色々工夫していきたい所です。
- 投稿日:2020-04-21T23:20:42+09:00
学習日記 #3
OAuthの概念
「認可情報の委譲」のための仕様である。
・予め信頼関係を構築したサービス間で、ユーザーの同意のもと、セキュアにユーザーの権限を受け渡しする。final修飾子
オーバライドを明示的に禁止することができる。
ボリモーフィズム(Polymorphism)
多様性と訳され、日本語でも抽象的である。要約すると、「同名のメソッドで異なる挙動を実現する」ことである。
トレイト(Trait)
トレイトとは、再利用可能なコード(メソッド、プロパティ)をまとめて切り出す仕組みである。「断片的なクラス」
trait トレイト名 { ....プロパティ/メソッドの定義.... }traitブロックの構文は以下の制約がある。
・定数は持てない
・クラスの継承、インターフェイスの実装は出来ない型を継承するインターフェイスと、実装を継承するトレイトの違いをきちんと理解する。
現在のクラス/親クラスと衝突した場合、
1、現在のクラスのメンバ
2、トレイトのメンバ
3、親クラスのメンバその他
*as句を利用することで、メソッドのアクセス権限を変更することが可能である。
オブジェクトをコピーしたい場合には参照渡しがデフォルトである。
オブジェクトの比較
== : 同じクラスのインスタンスであること、同じプロパティと値をもつこと。
=== : 同じクラスの同じインスタンスを参照すること。
- 投稿日:2020-04-21T22:43:45+09:00
Laravelを知らない中級(中年)プログラマーがマイグレーションファイルの仕組みを調べてみたけど全くわからない!その4:「Larabelアプリケーションの初期化の流れ」
INDEX
Laravelを知らない中級(中年)プログラマーがマイグレーションファイルの仕組みを調べてみたけど全くわからない!
- その1「マイグレーションファイルを見てみよう」
- その2「$app['db']って何者?」
- その3「Repositoryクラス」
- その4「Larabelアプリケーションの初期化の流れ」
- その5「リポジトリの読込」
Larabelアプリケーションの初期化の流れ
Laravelをマイグレーションファイルから少しずつ読み始めて、どうやらお作法であろう存在にいくつか出会いました。そろそろ初期化の流れを追ってみようと思います。
artisan
が叩かれると、PROJECT_ROOT/bootstrap/app.php
が呼び出されます。その最初で Illuminate\Foundation\Application が生成されます。PROJECT_ROOT/bootstrap/app.php抜粋$app = new Illuminate\Foundation\Application( $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) );
Application
クラスを見てみましょう。実体はPROJECT_ROOT/vendor/laravel/framework/src/Illuminate/Foundation/Application.php
です。このクラスのコンストラクタと関連する処理の一部は以下です。Illuminate/Foundation/Application/** * Create a new Illuminate application instance. * * @param string|null $basePath * @return void */ public function __construct($basePath = null) { if ($basePath) { $this->setBasePath($basePath); } $this->registerBaseBindings(); $this->registerBaseServiceProviders(); $this->registerCoreContainerAliases(); } /** * Register the basic bindings into the container. * * @return void */ protected function registerBaseBindings() { static::setInstance($this); $this->instance('app', $this); $this->instance(Container::class, $this); $this->singleton(Mix::class); $this->instance(PackageManifest::class, new PackageManifest( new Filesystem, $this->basePath(), $this->getCachedPackagesPath() )); } /** * Set the base path for the application. * * @param string $basePath * @return $this */ public function setBasePath($basePath) { $this->basePath = rtrim($basePath, '\/'); $this->bindPathsInContainer(); return $this; } /** * Bind all of the application paths in the container. * * @return void */ protected function bindPathsInContainer() { $this->instance('path', $this->path()); $this->instance('path.base', $this->basePath()); $this->instance('path.lang', $this->langPath()); $this->instance('path.config', $this->configPath()); $this->instance('path.public', $this->publicPath()); $this->instance('path.storage', $this->storagePath()); $this->instance('path.database', $this->databasePath()); $this->instance('path.resources', $this->resourcePath()); $this->instance('path.bootstrap', $this->bootstrapPath()); } /** * Get the path to the application "app" directory. * * @param string $path * @return string */ public function path($path = '') { $appPath = $this->appPath ?: $this->basePath.DIRECTORY_SEPARATOR.'app'; return $appPath.($path ? DIRECTORY_SEPARATOR.$path : $path); } /** * Get the base path of the Laravel installation. * * @param string $path Optionally, a path to append to the base path * @return string */ public function basePath($path = '') { return $this->basePath.($path ? DIRECTORY_SEPARATOR.$path : $path); } /** * Get the base path of the Laravel installation. * * @param string $path Optionally, a path to append to the base path * @return string */ public function basePath($path = '') { return $this->basePath.($path ? DIRECTORY_SEPARATOR.$path : $path); } /** * Get the path to the language files. * * @return string */ public function langPath() { return $this->resourcePath().DIRECTORY_SEPARATOR.'lang'; } /** * Get the path to the application configuration files. * * @param string $path Optionally, a path to append to the config path * @return string */ public function configPath($path = '') { return $this->basePath.DIRECTORY_SEPARATOR.'config'.($path ? DIRECTORY_SEPARATOR.$path : $path); } /* ========== 略 ========== */コンストラクタではまず、パス情報の初期化を行っています。アプリケーションコンテナが生成された時に渡された引数は
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
でした。それを引数としてsetBasePath()
メソッドを呼び出しています。setBasePath()
メソッドは渡されたパス情報を$this->basePath
に格納し、bindPathsInContainer()
メソッドを呼び出しています。このメソッドではパスの情報をバインドしているようです。$this->instance()
が何をやっているのか、現段階ではまだ良くわかりません。おそらく後々追うことになるでしょう。今追っているconfig
はpath.config
としてPROJECT_ROOT/config
が設定されているようです。次に
registerBaseBindings()
メソッドが呼び出されています。registerBaseBindings()
メソッドでは以下の処理が行われています。Illuminate/Foundation/Application$this->instance(PackageManifest::class, new PackageManifest( new Filesystem, $this->basePath(), $this->getCachedPackagesPath() ));
PackageManifest
という名前で第二引数で生成したPackageManifest
クラスをアプリケーションコンテナにバインドしているようです。PackageManifest
クラスに渡している三つの引数を追ってみましょう。まずFilesystem
です。実体はPROJECT_ROOT/vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php
です。ファイルを管理するクラスのようです。次に上で初期化された$basePath
が渡されます。そして最後にキャッシュパスが渡されます。キャッシュパスについては後ほど追ってみます。PackageManifest
クラスを見てみましょう。コンストラクタに以下のような定義がされています。Illuminate/Foundation/PackageManifest/** * Create a new package manifest instance. * * @param \Illuminate\Filesystem\Filesystem $files * @param string $basePath * @param string $manifestPath * @return void */ public function __construct(Filesystem $files, $basePath, $manifestPath) { $this->files = $files; $this->basePath = $basePath; $this->manifestPath = $manifestPath; $this->vendorPath = $basePath.'/vendor'; }引数として渡された情報を変数に格納し、追加で
vendorPath
もハードコーディングで定義しています。このへんで
Application
クラスはひとまず置いておいて、一番最初に叩かれるartisan
ファイルを見てみましょう。 次の処理に$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class)
とあります。これは、$app = require_once __DIR__.'/bootstrap/app.php'
の中で以下のようにアプリケーションコンテナにシングルトンとしてバインドしているようです。生成の流れの詳細はまだわかりません。PROJECT_ROOT/bootstrap/app.php抜粋$app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class );
App\Console\Kernel
はcomposer/autoload_classmap.php
で'App\\Console\\Kernel' => $baseDir . '/app/Console/Kernel.php'
と定義されています。
つまり実体はPROJECT_ROOT/app/Console/Kernel.php
のようです。このクラスはIlluminate\Foundation\Console\Kernel
を継承しています。Illuminate\Foundation\Console\Kernel
に以下のような定義がされています。Illuminate\Foundation\Console\Kernel/** * The bootstrap classes for the application. * * @var array */ 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\SetRequestForConsole::class, \Illuminate\Foundation\Bootstrap\RegisterProviders::class, \Illuminate\Foundation\Bootstrap\BootProviders::class, ]; /** * Run the console application. * * @param \Symfony\Component\Console\Input\InputInterface $input * @param \Symfony\Component\Console\Output\OutputInterface|null $output * @return int */ public function handle($input, $output = null) { try { $this->bootstrap(); return $this->getArtisan()->run($input, $output); } catch (Throwable $e) { $this->reportException($e); $this->renderException($output, $e); return 1; } } /** * Bootstrap the application for artisan commands. * * @return void */ public function bootstrap() { if (! $this->app->hasBeenBootstrapped()) { $this->app->bootstrapWith($this->bootstrappers()); } $this->app->loadDeferredProviders(); if (! $this->commandsLoaded) { $this->commands(); $this->commandsLoaded = true; } }
$bootstrappers
に初期設定用のクラスが配列としてセットされています。LoadConfiguration
とそれっぽい記述がありますね。
そして handle() メソッドですが、これは artisan ファイルに make() メソッドの直後に以下のように書かれています。PROJECT_ROOT/artisan抜粋$status = $kernel->handle( $input = new Symfony\Component\Console\Input\ArgvInput, new Symfony\Component\Console\Output\ConsoleOutput );コマンド処理開始のトリガのようです。ここから先程の
handle()
メソッドが呼ばれます。引数はおそらくコマンドラインから渡されたものと標準出力でしょう。そこは後で追うことになるので今は飛ばします。メイン処理のtry
の中に$this->bootstrap()
が記述されています。bootstrap()
メソッドのはじめで、初期化がされていない場合はアプリケーションコンテナのbootstrapWith()
に引数として 先程定義されているのを確認した初期設定用クラスの配列$bootstrappers
を渡しています。アプリケーションコンテナのbootstrapWith()
メソッドを見てみましょう。Illuminate/Foundation/Application::bootstrapWith()/** * Run the given array of bootstrap classes. * * @param string[] $bootstrappers * @return void */ public function bootstrapWith(array $bootstrappers) { $this->hasBeenBootstrapped = true; foreach ($bootstrappers as $bootstrapper) { $this['events']->dispatch('bootstrapping: '.$bootstrapper, [$this]); $this->make($bootstrapper)->bootstrap($this); $this['events']->dispatch('bootstrapped: '.$bootstrapper, [$this]); } }まず、初期化フラグを立てています。先程、初期化がされているか確認するロジックがありましたがそれの判定はここを通過したか否かということのようです。その後に引数で渡された
$bootstrappers
配列をforeach
で回します。配列の中身をevents
サービスのdispatch()
メソッドに内容を渡しているようです。events
サービスはApplication
クラスのregisterCoreContainerAliases()
メソッドでエイリアス登録されていましたね。実体はPROJECT_ROOT/vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php
です。呼び出されているdispatch
と関係するメソッドを見てみましょう。Illuminate/Events/Dispatcher::dispatch|関連メソッド/** * Fire an event and call the listeners. * * @param string|object $event * @param mixed $payload * @param bool $halt * @return array|null */ public function dispatch($event, $payload = [], $halt = false) { // When the given "event" is actually an object we will assume it is an event // object and use the class as the event name and this event itself as the // payload to the handler, which makes object based events quite simple. [$event, $payload] = $this->parseEventAndPayload( $event, $payload ); if ($this->shouldBroadcast($payload)) { $this->broadcastEvent($payload[0]); } $responses = []; foreach ($this->getListeners($event) as $listener) { $response = $listener($event, $payload); // If a response is returned from the listener and event halting is enabled // we will just return this response, and not call the rest of the event // listeners. Otherwise we will add the response on the response list. if ($halt && ! is_null($response)) { return $response; } // If a boolean false is returned from a listener, we will stop propagating // the event to any further listeners down in the chain, else we keep on // looping through the listeners and firing every one in our sequence. if ($response === false) { break; } $responses[] = $response; } return $halt ? null : $responses; } /** * Parse the given event and payload and prepare them for dispatching. * * @param mixed $event * @param mixed $payload * @return array */ protected function parseEventAndPayload($event, $payload) { if (is_object($event)) { [$payload, $event] = [[$event], get_class($event)]; } return [$event, Arr::wrap($payload)]; } /** * Determine if the payload has a broadcastable event. * * @param array $payload * @return bool */ protected function shouldBroadcast(array $payload) { return isset($payload[0]) && $payload[0] instanceof ShouldBroadcast && $this->broadcastWhen($payload[0]); } /** * Check if event should be broadcasted by condition. * * @param mixed $event * @return bool */ protected function broadcastWhen($event) { return method_exists($event, 'broadcastWhen') ? $event->broadcastWhen() : true; } /** * Get all of the listeners for a given event name. * * @param string $eventName * @return array */ public function getListeners($eventName) { $listeners = $this->listeners[$eventName] ?? []; $listeners = array_merge( $listeners, $this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName) ); return class_exists($eventName, false) ? $this->addInterfaceListeners($eventName, $listeners) : $listeners; }まず、受け取った第一引数の初期化クラス名とイベント名(まずは「
bootstrapping:
」)を連結した文字列と第二引数のアプリケーションコンテナをparseEventAndPayload()
メソッドに通します。parseEventAndPayload()
メソッドは、渡されたものがオブジェクトだった場合の処理や配列でない場合の処理をしています。Arr::wrap()
は渡された引数が配列でない場合やnull
の場合配列にして返すstatic
メソッドです。次に
shouldBroadcast()
メソッドでdispatch()
メソッドに渡された第二引数の判定をしています。今回の場合はアプリケーションコンテナが対象になります。shouldBroadcast()
メソッドは引数として受け取ったものを配列アクセスし、0番目がセットされていてそれがShouldBroadcast
クラスのインスタンスで且つ、broadcastWhen()
メソッドが存在しない、若しくは戻り値がtrue
の場合true
に、そうでない場合false
になります。
判定がtrue
の場合、broadcastEvent()
メソッドに渡します。 今回処理するのはアプリケーションコンテナなのでこの処理は通りません。次のforeach ($this->getListeners($event) as $listener)
の処理はイベントリスナとして登録されているかどうかを実装されたインターフェイスも含め調べそれらをforeach
で回して処理をしているようです。イベント登録されている場合はトリガが叩かれる感じでしょうか。今回は主題からそれるので別の機会に追いたいと思います。コール元で戻り値を受け取っていないので次に行きましょう。アプリケーションコンテナの
$this->make($bootstrapper)->bootstrap($this)
です。$bootstrappers
配列を回して一つずつインスタンスを生成し、bootstrap($this)
しているようです。サービスを生成し必ずbootstrap()
メソッドが実行されるという仕様はこの部分で実現されているのでしょう。その後にbootstrapped
イベントトリガを叩く手順ですね。LoadConfiguration
Kernel
クラスの初期化で$bootstrappers
配列に含まれていたIlluminate\Foundation\Bootstrap\LoadConfiguration
がサービスとして初期化されました。このクラスのbootstrap()
メソッドが叩かれているはずです。見てみましょう。Illuminate/Foundation/Bootstrap/LoadConfiguration::bootstrap()/** * Bootstrap the given application. * * @param \Illuminate\Contracts\Foundation\Application $app * @return void */ public function bootstrap(Application $app) { $items = []; // First we will see if we have a cache configuration file. If we do, we'll load // the configuration items from that file so that it is very quick. Otherwise // we will need to spin through every configuration file and load them all. if (file_exists($cached = $app->getCachedConfigPath())) { $items = require $cached; $loadedFromCache = true; } // Next we will spin through all of the configuration files in the configuration // directory and load each one into the repository. This will make all of the // options available to the developer for use in various parts of this app. $app->instance('config', $config = new Repository($items)); if (! isset($loadedFromCache)) { $this->loadConfigurationFiles($app, $config); } // Finally, we will set the application's environment based on the configuration // values that were loaded. We will pass a callback which will be used to get // the environment in a web context where an "--env" switch is not present. $app->detectEnvironment(function () use ($config) { return $config->get('app.env', 'production'); }); date_default_timezone_set($config->get('app.timezone', 'UTC')); mb_internal_encoding('UTF-8'); }
bootstrap()
メソッドはアプリケーションコンテナを引数として受け取ります。まず、アプリケーションコンテナのgetCachedConfigPath()
メソッドを叩き戻り値のファイルが存在確認をしています。Application::getCachedConfigPath()
と関連するメソッドは以下のようになっています。Illuminate\Foundation\Application::Application/** * Get the path to the configuration cache file. * * @return string */ public function getCachedConfigPath() { return $this->normalizeCachePath('APP_CONFIG_CACHE', 'cache/config.php'); } /** * Normalize a relative or absolute path to a cache file. * * @param string $key * @param string $default * @return string */ protected function normalizeCachePath($key, $default) { if (is_null($env = Env::get($key))) { return $this->bootstrapPath($default); } return Str::startsWith($env, '/') ? $env : $this->basePath($env); }
Application::getCachedConfigPath()
はAPP_CONFIG_CACHE
をキーに、デフォルト値にcache/config.php
を引数指定してnormalizeCachePath()
メソッドをコールしています。normalizeCachePath()
はEnv::get($key)
がnull
であるか確認しています。
Env
クラスはPROJECT_ROOT/vendor/laravel/framework/src/Illuminate/Support/Env.php
です。get()
メソッドを見てみましょう。Illuminate\Support\Env::get()|getRepository()/** * Get the environment repository instance. * * @return \Dotenv\Repository\RepositoryInterface */ public static function getRepository() { if (static::$repository === null) { $adapters = array_merge( [new EnvConstAdapter, new ServerConstAdapter], static::$putenv ? [new PutenvAdapter] : [] ); static::$repository = RepositoryBuilder::create() ->withReaders($adapters) ->withWriters($adapters) ->immutable() ->make(); } return static::$repository; } /** * Gets the value of an environment variable. * * @param string $key * @param mixed $default * @return mixed */ public static function get($key, $default = null) { return Option::fromValue(static::getRepository()->get($key)) ->map(function ($value) { switch (strtolower($value)) { case 'true': case '(true)': return true; case 'false': case '(false)': return false; case 'empty': case '(empty)': return ''; case 'null': case '(null)': return; } if (preg_match('/\A([\'"])(.*)\1\z/', $value, $matches)) { return $matches[2]; } return $value; }) ->getOrCall(function () use ($default) { return value($default); }); }
Option::fromValue()
に引数として渡しているstatic::getRepository()->get($key)
から見てみます。getRepository()
メソッドではstatic::$repository
がnull
でない場合はそれを返し、null
だった場合はstatic::$repository
を構築する流れのようです。static::$repository
自体はDotenv/Repository/RepositoryInterface
インターフェイスを実装したインスタンスのようです。処理の中で以下のようなクラスが書かれています。
- EnvConstAdapter
- ServerConstAdapter
- PutenvAdapter
- RepositoryBuilder
これらは、vlucas/phpdotenvを利用したもののようです。Laravelの環境設定を
.env
に記述するのはこの仕組を利用するためのようです。Illuminate\Foundation\Console\Kernel
で定義した$bootstrappers
の一番最初にIlluminate\Foundation\Bootstrap\LoadEnvironmentVariables
がありました。これをKernel::handle()
実行時に読み込んでいます。このLoadEnvironmentVariables::bootstrap()
メソッドからcreateDotenv()
メソッドが呼ばれ、Dotenv::create
が実行されています。このメソッドに引数で渡される、環境設定ファイル名 「.env
」 とパス情報は アプリケーションコンテナでprotected $environmentFile = '.env'
と設定されています。変更したい場合はloadEnvironmentFrom()
メソッドで変えられるようです。このphpdotenv
でリポジトリを構築することで、Laravel設定ファイル、Apache設定、サーバ環境設定等をにアクセスするインターフェイスを整えてくれるようです。
getRepository()
メソッドからRepositoryInterface
インタフェースを実装した環境設定情報が返されます。そこからget()
メソッドが呼ばれ、PhpOption\Option
インターフェイスが実装されたインスタンスが返されます。PhpOption\Option
PhpOption\Option
とは何でしょうか。これはschmittjoh/php-optionのようです。autoload_classmap.php
で'PhpOption\\Option' => $vendorDir . '/phpoption/phpoption/src/PhpOption/Option.php'
と定義されています。Option::fromValue()
と関連するクラスは以下のように定義されています。PhpOption\Option::fromValue()abstract class Option implements IteratorAggregate { /** * Creates an option given a return value. * * This is intended for consuming existing APIs and allows you to easily * convert them to an option. By default, we treat ``null`` as the None * case, and everything else as Some. * * @template S * * @param S $value The actual return value. * @param S $noneValue The value which should be considered "None"; null by * default. * * @return Option<S> */ public static function fromValue($value, $noneValue = null) { if ($value === $noneValue) { return None::create(); } return new Some($value); } /* ========== 略 ========== */ }PhpOption\None::create()final class None extends Option { /** * @return None */ public static function create() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } /* ========== 略 ========== */ }PhpOption\Some::__construct()final class Some extends Option { /** @var T */ private $value; /** * @param T $value */ public function __construct($value) { $this->value = $value; } public function map($callable) { return new self($callable($this->value)); } /* ========== 略 ========== */ }
Option
クラスは抽象クラスで、これを静的メソッド呼び出しをしています。Option
クラス自体はIteratorAggregate
インターフェイスを実装しています。イテレーターとしてアクセスが可能のようですね。渡された第一引数が第二引数と同じ場合は空のオブジェクトであるNone
インスタンスを、同じでなければ第一引数を内部に持ったSome
インスタンスを返すようです。値はSome->value
の形で格納され、この値に各種コールできるメソッドを実装しています。今回アクセスしたいのはAPP_CONFIG_CACHE
なので、$value
にAPP_CONFIG_CACHE
がセットされたSome
インスタンスが返ってくるはずです。Some::map()
メソッドの内容はreturn new self($callable($this->value))
とあります。map()
に渡された引数にはクロージャーが入っていました。Some::$value
を引数にしたクロージャーの結果を$value
に格納したSome
インスタンスが返ってくる流れです。そして返されたSome
インスタンスのgetOrCall()
メソッドを引数としてクロージャーを入れて呼び出します。ただ、Some::getOrCall()
はreturn $this->value
しているだけですので、$value
がそのまま返されます。つまり、アプリケーションコンテナのnormalizeCachePath
にあるif (is_null($env = Env::get($key)))
は各環境設定をまとめたリポジトリからAPP_CONFIG_CACHE
が存在するかを判定しています。もし、存在していな場合は第二引数で指定された$default
つまりcache/config.php
を引数にbootstrapPath()
が呼び出されます。このメソッドはbasePath
にbootstrap
ディレクトリを追加して引数の文字列を連結したものを返します。結果、PROJECT_ROOT/bootstrap/cache/config.php
という文字列が返されます。APP_CONFIG_CACHE
が存在していた場合は、Str::startsWith($env, '/')
が判定され文字列加工をします。startsWith
はJava
などで使われる関数でPHP
関数にないものを独自に定義したもののようです。文字列が引数で指定された文字列で始まるかを判定してtrue
かfalse
を返します。LoadConfiguration::bootstrap()
にそのパスが返され、file_exists
でそのファイルが存在するか判定され、存在した場合はそのキャッシュファイルを読み込み、読み込みフラグ$loadedFromCache
にtrue
がセットされます。存在していた場合は読み込んだキャッシュを、存在していない場合は空の配列をアプリケーションコンテナにconfig
の名前でバインドします。もし、存在しなかった場合はloadConfigurationFiles()
メソッドを第一引数にアプリケーションコンテナ、第二引数に空の配列で初期化したRepository
インスタンスを渡してコールします。Illuminate/Foundation/Bootstrap/LoadConfiguration::loadConfigurationFiles()|関連メソッド/** * Load the configuration items from all of the files. * * @param \Illuminate\Contracts\Foundation\Application $app * @param \Illuminate\Contracts\Config\Repository $repository * @return void * * @throws \Exception */ protected function loadConfigurationFiles(Application $app, RepositoryContract $repository) { $files = $this->getConfigurationFiles($app); if (! isset($files['app'])) { throw new Exception('Unable to load the "app" configuration file.'); } foreach ($files as $key => $path) { $repository->set($key, require $path); } } /** * Get all of the configuration files for the application. * * @param \Illuminate\Contracts\Foundation\Application $app * @return array */ protected function getConfigurationFiles(Application $app) { $files = []; $configPath = realpath($app->configPath()); foreach (Finder::create()->files()->name('*.php')->in($configPath) as $file) { $directory = $this->getNestedDirectory($file, $configPath); $files[$directory.basename($file->getRealPath(), '.php')] = $file->getRealPath(); } ksort($files, SORT_NATURAL); return $files; } /** * Get the configuration file nesting path. * * @param \SplFileInfo $file * @param string $configPath * @return string */ protected function getNestedDirectory(SplFileInfo $file, $configPath) { $directory = $file->getPath(); if ($nested = trim(str_replace($configPath, '', $directory), DIRECTORY_SEPARATOR)) { $nested = str_replace(DIRECTORY_SEPARATOR, '.', $nested).'.'; } return $nested; }
loadConfigurationFiles()
メソッドのはじめでgetConfigurationFiles()
がコールされます。$configPath = realpath($app->configPath())
で設定ファイルのパスをセットしています。これはApplication::configPath()
でreturn $this->basePath.DIRECTORY_SEPARATOR.'config'.($path ? DIRECTORY_SEPARATOR.$path : $path)
と定義されていますので、PROJECT_ROOT/config
が代入されます。次にforeach
でFinder
クラスで色々したものを回しています。Finder
クラスはautoload_classmap で /symfony/finder/Finder.php
と定義されています。symfony/finder
symfony/finder/Finder
を見てみましょう。
Symfony
のディレクトリやファイルの一覧を取得する便利機能が詰め込まれたコンポーネントのようです。見てみましょう。symfony/finder/Finder抜粋/** * Finder allows to build rules to find files and directories. * * It is a thin wrapper around several specialized iterator classes. * * All rules may be invoked several times. * * All methods return the current Finder object to allow chaining: * * $finder = Finder::create()->files()->name('*.php')->in(__DIR__); * * @author Fabien Potencier <fabien@symfony.com> */ class Finder implements \IteratorAggregate, \Countable { /* ========== 中略 ========== */ private $names = []; /* ========== 中略 ========== */ /** * Creates a new Finder. * * @return static */ public static function create() { return new static(); } /** * Restricts the matching to files only. * * @return $this */ public function files() { $this->mode = Iterator\FileTypeFilterIterator::ONLY_FILES; return $this; } /** * Adds rules that files must match. * * You can use patterns (delimited with / sign), globs or simple strings. * * $finder->name('*.php') * $finder->name('/\.php$/') // same as above * $finder->name('test.php') * $finder->name(['test.py', 'test.php']) * * @param string|string[] $patterns A pattern (a regexp, a glob, or a string) or an array of patterns * * @return $this * * @see FilenameFilterIterator */ public function name($patterns) { $this->names = array_merge($this->names, (array) $patterns); return $this; } /** * Searches files and directories which match defined rules. * * @param string|string[] $dirs A directory path or an array of directories * * @return $this * * @throws DirectoryNotFoundException if one of the directories does not exist */ public function in($dirs) { $resolvedDirs = []; foreach ((array) $dirs as $dir) { if (is_dir($dir)) { $resolvedDirs[] = $this->normalizeDir($dir); } elseif ($glob = glob($dir, (\defined('GLOB_BRACE') ? GLOB_BRACE : 0) | GLOB_ONLYDIR | GLOB_NOSORT)) { sort($glob); $resolvedDirs = array_merge($resolvedDirs, array_map([$this, 'normalizeDir'], $glob)); } else { throw new DirectoryNotFoundException(sprintf('The "%s" directory does not exist.', $dir)); } } $this->dirs = array_merge($this->dirs, $resolvedDirs); return $this; } }symfony/finder/Iterator/FileTypeFilterIterator抜粋/** * FileTypeFilterIterator only keeps files, directories, or both. * * @author Fabien Potencier <fabien@symfony.com> */ class FileTypeFilterIterator extends \FilterIterator { const ONLY_FILES = 1; const ONLY_DIRECTORIES = 2; private $mode; /** * @param \Iterator $iterator The Iterator to filter * @param int $mode The mode (self::ONLY_FILES or self::ONLY_DIRECTORIES) */ public function __construct(\Iterator $iterator, int $mode) { $this->mode = $mode; parent::__construct($iterator); } /* ========== 略 ========== */ }クラスドキュメントを見ると、「ファイルとディレクトリを検索するルールを構築できます」とあります。使用例に
$finder = Finder::create()->files()->name('*.php')->in(__DIR__);とあります。
LoadConfiguration:: getConfigurationFiles()
の記述とほぼ同じなので、典型的な利用方法のようです。
まず、create()
でstatic
として自分自身を生成して返します。その後使われるメソッドは基本的に戻り値が$this
になっていて、メソッドチェーンで処理をすすめる前提になっているようです。次にfiles()
メソッドがコールされます。$this->mode
にIterator\FileTypeFilterIterator::ONLY_FILES
を代入しています。ONLY_FILES
は 1 が定義されています。ファイルだけ一覧にするモード定数なのでしょう。次にname()
メソッドに'*.php'
が引数として渡されています。name()
メソッドではFinder::namesに
渡された引数をarray_marge
しています。検索条件を配列で蓄える機能と思われます。最後にin()
メソッドが設定ファイルのパスを引数としてコールされます。渡された引数をforeach
で回し、正規化したパスをFinder::dirs
にセットして自身を返します。Finder
インスタンスはイテレーターインターフェイスを持っていますので、foreach
で回すことができます。つまり、create
で生成した後、モードやディレクトリ、その他条件をセットし結果をイテレーターとして提供するコンポーネントですね。ではloadConfigurationFiles
に戻りましょう。PROJECT_ROOT/config/*.php 設定ファイルの読み込み
symfony/finder
から受け取るものはPROJECT_ROOT/config/
の配下にある.php
拡張子のついたファイル一覧だということが先程わかりました。一覧を受け取った後に以下の処理をしています。Illuminate/Foundation/Bootstrap/LoadConfiguration::getConfigurationFiles()抜粋$directory = $this->getNestedDirectory($file, $configPath); $files[$directory.basename($file->getRealPath(), '.php')] = $file->getRealPath();パスの表記を整えてソートした配列を作り変えしています。
loadConfigurationFiles
の続きに戻りましょう。Illuminate/Foundation/Bootstrap/LoadConfiguration::loadConfigurationFiles()抜粋if (! isset($files['app'])) { throw new Exception('Unable to load the "app" configuration file.'); } foreach ($files as $key => $path) { $repository->set($key, require $path); }受け取った配列に
app
をキーとした情報が存在しなければ 「Unable to load the “app” configuration file.
」というメッセージを添えた例外を投げています。存在すれば第二引数でうけとったリポジトリインスタンスに設定ファイル名をキーとして、設定ファイルを読み込み、その戻り値の配列をセットしてloadConfigurationFiles()
メソッドの処理は終了です。LoadConfiguration::bootstrap()
メソッドの続きに戻りましょう。Illuminate/Foundation/Bootstrap/LoadConfiguration::bootstrap()抜粋// Finally, we will set the application's environment based on the configuration // values that were loaded. We will pass a callback which will be used to get // the environment in a web context where an "--env" switch is not present. $app->detectEnvironment(function () use ($config) { return $config->get('app.env', 'production'); });Illuminate/Foundation/Application::detectEnvironment()/** * Detect the application's current environment. * * @param \Closure $callback * @return string */ public function detectEnvironment(Closure $callback) { $args = $_SERVER['argv'] ?? null; return $this['env'] = (new EnvironmentDetector)->detect($callback, $args); }
detectEnvironment()
メソッドにクロージャーを引数として渡しています。クロージャー自体の引数は設定リポジトリがuse
で指定されています。クロージャーの戻り値は$config->get('app.env', 'production')
とあります。ちょうど先程読んだところですね。PROJECT_ROOT/config/app.php
に記述されている配列の 「env
」 キーを探し、なければproduction
を返します。PRODUCT_ROOT/config/app.php
には'env' => env('APP_ENV', 'production')
とあるので、.env
ファイルなどでAPP_ENV
が設定されていればそれを、なければproduction
を返します。
detectEnvironment()
メソッドを読んでみましょう。EnvironmentDetector
インスタンスを生成して、先程のコールバックとスクリプトへ渡された引数の配列をEnvironmentDetector::detect()
メソッドに渡しています。EnvironmentDetector
クラスを読んでみましょう。実体はIlluminate/Foundation/EnvironmentDetector
です。Illuminate/Foundation/EnvironmentDetector::detect()/** * Detect the application's current environment. * * @param \Closure $callback * @param array|null $consoleArgs * @return string */ public function detect(Closure $callback, $consoleArgs = null) { if ($consoleArgs) { return $this->detectConsoleEnvironment($callback, $consoleArgs); } return $this->detectWebEnvironment($callback); } /** * Set the application environment for a web request. * * @param \Closure $callback * @return string */ protected function detectWebEnvironment(Closure $callback) { return $callback(); } /** * Set the application environment from command-line arguments. * * @param \Closure $callback * @param array $args * @return string */ protected function detectConsoleEnvironment(Closure $callback, array $args) { // First we will check if an environment argument was passed via console arguments // and if it was that automatically overrides as the environment. Otherwise, we // will check the environment as a "web" request like a typical HTTP request. if (! is_null($value = $this->getEnvironmentArgument($args))) { return $value; } return $this->detectWebEnvironment($callback); } /** * Get the environment argument from the console. * * @param array $args * @return string|null */ protected function getEnvironmentArgument(array $args) { foreach ($args as $i => $value) { if ($value === '--env') { return $args[$i + 1] ?? null; } if (Str::startsWith($value, '--env')) { return head(array_slice(explode('=', $value), 1)); } } }
detect()
は受け取った第二引数、スクリプトへ渡された引数、つまりコマンドラインからartisan
を実行した時の引数の配列の存在を判定し、あった場合は、detectConsoleEnvironment()
メソッドの、なかった場合はdetectWebEnvironment()
メソッドの戻り値を返します。detectConsoleEnvironment()
メソッドはgetEnvironmentArgument()
メソッドにコマンド引数の配列を引数として渡します。getEnvironmentArgument()
メソッドはコマンド引数をforeach
で回し、--env
で指定した値が存在する場合はそれを返します。つまりdetect()
はartisan
コマンドの引数の中に--env
があった場合はenv
を上書します。その結果をアプリケーションコンテナのenv
に代入します。LoadConfiguration::bootstrap()
の残りはタイムゾーンをセットしてエンコードをUTF-8
に設定して完了です。
最初の目的より少し読みすぎましたが、LoadConfiguration::loadConfigurationFiles()
でリポジトリに設定ファイルを読み込みセットしているところを確認しました。Illuminate/Config/Repository::get()
で返されるArr::get($this->items, $key, $default)
の$this->items
の正体が明確になりました。次回
初期化の流れを読んでみてなんとなく、ふんわりやってることがわかってきましたが、まだオブジェクトの生成やイベント周りなどはっきりとつかめていない部分がありますね。徐々に理解していけると良いですが、どうなるでしょうか。次回はこの流れでリポジトリの読込を探っていきたいと思います。
続く☆
- 投稿日:2020-04-21T21:02:09+09:00
phpでのcsrf対策
csrf
csrfとはcookiesにログイン情報が残っている状態の時、全くアプリと関係のないリンクを押した時に、そのリンクからアプリのDBアクセスをして変更をしようとする事。これを防ぐためにsessionを利用する。sessionはURLをまたいでファイルが変わっても、値が残るような配列。
アプリのページが立ち上がれば、SESSIONの中にトークンを入れるようにする。
↓
それをHTMLのformに以下を挟み込む<input type="hidden" id="token" name="token" value"<?= h($_SESSION['token']);?>"> #h()はエスケープ↓
サーバ側で$_SESSION['token']と送られてきたtokenに入っている値が同じかどうかを調べる。同じならば全ての送られてきた値が、正常の送信と判断する。
- 投稿日:2020-04-21T21:01:27+09:00
phpでのcsrf対策
csrf
csrfとはcookiesにログイン情報が残っている状態の時、全くアプリと関係のないリンクを押した時に、そのリンクからアプリのDBアクセスをして変更をしようとする事。これを防ぐためにsessionを利用する。sessionはURLをまたいでファイルが変わっても、値が残るような配列。
アプリのページが立ち上がれば、SESSIONの中にトークンを入れるようにする。
↓
それをHTMLのformに以下を挟み込む<input type="hidden" id="token" name="token" value"<?= h($_SESSION['token']);?>"> #h()はエスケープ↓
サーバ側で$_SESSION['token']と送られてきたtokenに入っている値が同じかどうかを調べる。同じならば全ての送られてきた値が、正常の送信と判断する。
- 投稿日:2020-04-21T18:10:33+09:00
PHPのライブラリ「qr-code」でQRコード生成を実装した【CakePHP】
今回はCakePHPでPHPのライブラリ「qr-code」を活用して、QRコード生成を実装しました。「qr-code」はPHPのライブラリなので、CakePHPでなくても実装できます。
Composerでqr-codeをインストール
ターミナルでqr-codeをインストールします。
$ composer require endroid/qr-code問題なくインストールできるとvendorディレクトリ直下にchillerlanディレクトリがダウンロードされます。
QRコード生成
今回は、ビューのヘルパーを使って生成する関数を定義しました。関数はコントローラでもモデルで定義しても問題ありません。
src/View/Helper/CommonHelper.phppublic function qrcode($url) { return (new \chillerlan\QRCode\QRCode())->render($url); }ビューでQRコードを表示
ビューで先ほど定義した関数を呼び出し、任意のURLを引数に渡します。そして、CSSを調整してあげればうまく表示されるはずです。
index.ctp<div style="background:url(<?= $this->Common->qrcode('https://www.google.com/') ?>); height:80px; width:80px; background-size:cover;"></div>参考
- 投稿日:2020-04-21T17:30:28+09:00
俺のLaravelがこんなに遅いわけがない
環境(ベース)
- PHP 7.4.5
- Laravel 7.5.1
Docker for Macでnginxとphp-fpmコンテナをunixソケットで繋いだ環境で試してます。
環境の差異
- OPcache なし
- OPcache あり、プリロードなし
- OPcache なし、プリロードあり
比較方法
- Laravelのwelcome画面をabコマンドの結果で比較します。
Requests per second(1秒間に捌けるリクエスト数)、Time per request(1リクエストあたりの処理時間)に着目します。
以前こんな記事も書いてます。
Apache Bench を初めて使ってみたOPcacheなし
php.ini(設定例)[opcache] opcache.enable = 0$ ab -n 1000 -c 100 http://127.0.0.1/ This is ApacheBench, Version 2.3 <$Revision: 1843412 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 127.0.0.1 (be patient) Completed 100 requests Completed 200 requests Completed 300 requests Completed 400 requests Completed 500 requests Completed 600 requests Completed 700 requests Completed 800 requests Completed 900 requests Completed 1000 requests Finished 1000 requests Server Software: nginx/1.17.8 Server Hostname: 127.0.0.1 Server Port: 80 Document Path: / Document Length: 2426 bytes Concurrency Level: 100 Time taken for tests: 18.432 seconds Complete requests: 1000 Failed requests: 0 Total transferred: 3446000 bytes HTML transferred: 2426000 bytes Requests per second: 54.25 [#/sec] (mean) Time per request: 1843.175 [ms] (mean) Time per request: 18.432 [ms] (mean, across all concurrent requests) Transfer rate: 182.58 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 1 2.5 0 22 Processing: 79 1744 326.5 1836 2038 Waiting: 69 1743 326.6 1834 2038 Total: 83 1745 324.6 1836 2039 Percentage of the requests served within a certain time (ms) 50% 1836 66% 1865 75% 1883 80% 1894 90% 1926 95% 1948 98% 1988 99% 2009 100% 2039 (longest request)
- 1秒間に捌けるリクエスト数:
54.25
- 1リクエストあたりの処理時間:
18.432 (ms)
俺のLaravelがこんなに遅いわけがない...
OPcacheあり、プリロードなし
php.ini(設定例)[opcache] opcache.enable = 1 opcache.memory_consumption = 128 opcache.interned_strings_buffer = 8 opcache.max_accelerated_files = 4000 opcache.validate_timestamps = 0 opcache.huge_code_pages = 0$ ab -n 1000 -c 100 http://127.0.0.1/ This is ApacheBench, Version 2.3 <$Revision: 1843412 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 127.0.0.1 (be patient) Completed 100 requests Completed 200 requests Completed 300 requests Completed 400 requests Completed 500 requests Completed 600 requests Completed 700 requests Completed 800 requests Completed 900 requests Completed 1000 requests Finished 1000 requests Server Software: nginx/1.17.8 Server Hostname: 127.0.0.1 Server Port: 80 Document Path: / Document Length: 2426 bytes Concurrency Level: 100 Time taken for tests: 3.318 seconds Complete requests: 1000 Failed requests: 0 Total transferred: 3446000 bytes HTML transferred: 2426000 bytes Requests per second: 301.41 [#/sec] (mean) Time per request: 331.772 [ms] (mean) Time per request: 3.318 [ms] (mean, across all concurrent requests) Transfer rate: 1014.32 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 2 4.4 1 24 Processing: 31 316 60.1 319 481 Waiting: 9 311 59.3 316 461 Total: 34 318 57.9 320 483 Percentage of the requests served within a certain time (ms) 50% 320 66% 335 75% 346 80% 354 90% 381 95% 404 98% 413 99% 421 100% 483 (longest request)
- 1秒間に捌けるリクエスト数:
301.41
- 1リクエストあたりの処理時間:
3.318 (ms)
54.25
から301.41
へおよそ5.55倍の高速化されました!!
圧倒的すぎる速さ!!キャッシュ効果恐るべし!!OPcacheあり、プリロードあり
php.ini(設定例)[opcache] opcache.enable = 1 opcache.memory_consumption = 128 opcache.interned_strings_buffer = 8 opcache.max_accelerated_files = 4000 opcache.validate_timestamps = 0 opcache.huge_code_pages = 0 opcache.preload = /var/www/preload.php opcache.preload_user = www-datahttps://github.com/brendt/laravel-preload/blob/master/preload.php
このpreload.phpを参考にしてます。ignoreにいくつか追加してます。
ただお試し的に使ってるので、また内容まとまったら別記事にしたいと思います。$ ab -n 1000 -c 100 http://127.0.0.1/ This is ApacheBench, Version 2.3 <$Revision: 1843412 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 127.0.0.1 (be patient) Completed 100 requests Completed 200 requests Completed 300 requests Completed 400 requests Completed 500 requests Completed 600 requests Completed 700 requests Completed 800 requests Completed 900 requests Completed 1000 requests Finished 1000 requests Server Software: nginx/1.17.8 Server Hostname: 127.0.0.1 Server Port: 80 Document Path: / Document Length: 2426 bytes Concurrency Level: 100 Time taken for tests: 2.878 seconds Complete requests: 1000 Failed requests: 0 Total transferred: 3446000 bytes HTML transferred: 2426000 bytes Requests per second: 347.40 [#/sec] (mean) Time per request: 287.850 [ms] (mean) Time per request: 2.878 [ms] (mean, across all concurrent requests) Transfer rate: 1169.09 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 1 1.3 0 7 Processing: 49 268 46.5 275 344 Waiting: 35 267 46.5 274 342 Total: 54 269 45.5 276 344 Percentage of the requests served within a certain time (ms) 50% 276 66% 287 75% 294 80% 298 90% 312 95% 322 98% 331 99% 335 100% 344 (longest request)
- 1秒間に捌けるリクエスト数:
347.40
- 1リクエストあたりの処理時間:
2.878 (ms)
301.41
から347.40
へおよそ1.15倍の高速化されました!!もう俺のLaravelが遅いなんて言わせない...!!
まとめ
# OPcacheなし Requests per second: 54.25 [#/sec] (mean) Time per request: 18.432 [ms] (mean, across all concurrent requests) # OPcacheあり、プリロードなし Requests per second: 301.41 [#/sec] (mean) Time per request: 3.318 [ms] (mean, across all concurrent requests) # OPcacheあり、プリロードあり Requests per second: 347.40 [#/sec] (mean) Time per request: 2.878 [ms] (mean, across all concurrent requests)何も設定してない
54.25
の状態から347.40
へおよそ6.4倍と劇的な高速化を遂げました!!!
PHPのポテンシャル半端ないですね!!!プリロード自体は初めてなので諸々問題出てくるかもしれませんが、何か問題あったらまた記事にしていきたいと思います?
- 投稿日:2020-04-21T15:46:04+09:00
Composerを更新しようとしたら「SHA384 is not supported by your openssl extension, could not verify the phar file integrity」で失敗する
事象
コマンド
composer selfupdate
を実行してComposerの更新を試みたところ、以下のエラーが発生する[UserName: laravel]$ composer selfupdate Updating to version 1.10.5 (stable channel). Downloading (100%) [RuntimeException] SHA384 is not supported by your openssl extension, could not verify the phar file integrity self-update [-r|--rollback] [--clean-backups] [--no-progress] [--update-keys] [--stable] [--preview] [--snapshot] [--set-channel-only] [--] [<version>] [UserName: laravel]$原因
利用しているComposerのバージョンが古く、正常に更新が行われない。
【 同一事象の GitHub Issue 】
SHA384 is not supported by your openssl extension, #7802対応
公式ページ Download Composer の手順に従い、composer.pharをダウンロードする。
以下はバージョン1.10.5の例。php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" php -r "if (hash_file('sha384', 'composer-setup.php') === 'e0012edf3e80b6978849f5eff0d4b4e4c79ff1609dd1e613307e16318854d24ae64f26d17af3ef0bf7cfb710ca74755a') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" php composer-setup.php php -r "unlink('composer-setup.php');"公式ページ Installation - Linux / Unix / macOS の手順に従い、インストールする。
以下はグローバルインストールの例。mv composer.phar /usr/local/bin/composerComposerのバージョンを表示させ、正常にインストールされたかを確認する。
[UserName: bin]$ composer --version Composer version 1.10.5 2020-04-10 11:44:22 [UserName: bin]$
- 投稿日:2020-04-21T14:27:23+09:00
monologを使ってロギング
monologを使用して、ログの取得・記録の機能を追加することになったので、備忘録としてまとめます。
monologとは
phpのロギングのためのライブラリ。
ハンドラーによって、ファイル・メール受信・ソケット・データベースといった様々なWebサービスに記録することが可能。
PSR-3のインターフェースを実装している。
そのため、PSR-3を使用するフレームワークでは利用が容易。Symfony、Laravelでは標準で実装されている。monologの導入
基本的な使用方法のまとめ
インストール
composerを使用してインストール
$ composer require monolog/monolog上記を実行
または
composer.json{ "require": { "monolog/monolog": "^2.02" } }composer.jsonに記述し、インストール
$ composer inastall使いかた
基本的な使い方は以下の通り
(monologのドキュメントより引用)testlog.php<?php use Monolog\Logger; use Monolog\Handler\StreamHandler; // create a log channel $log = new Logger('name'); $log->pushHandler(new StreamHandler('path/to/your.log', Logger::WARNING)); // add records to the log $log->warning('Foo'); $log->error('Bar');まず、チャネル(Loggerインスタンス)を作成する。
インスタンス作成時にチャネル名を引数として渡す(上記の場合'name')。チャネル名はログの場所に合わせて付けることで、複数のLoggerがある場合に抽出するなどの操作が容易になる。作成したチャネルにpushHandlerでログの目的に合わせてハンドラーをセット。
StreamHandlerはmonologでファイル記録用の基本的なハンドラー。
上記の場合、第1引数はログを記録するファイルのパス、第2引数はログレベル(ログレベルの初期値はLogger::DEBUG)
ハンドラは複数持たせることができるので、複数の記録の機能を持たせることができる。ログの記録の際は、レベルを指定できる。上記ではwarningレベルでFooを、errorレベルでBarを書き込みしている。
やってみた
今回やりたかったこと、「APIからのレスポンス結果($result)を一定期間分ファイルに記録する」を試してみた。(レスポンス結果の取得については割愛)
ハンドラーはStreamHandlerを継承したRotatingFileHandlerを使用。
RotatingFileHandlerは1日に1つのログファイルを作成、古いファイルは削除することができるので、一定期間のログを残す場合に便利。response.phpuse Monolog\Logger; use Monolog\Handler\RotatingFileHandler; ----------- // Create a handler $handler = new RotatingFileHandler('/logs/response', 30); // create a log channel $log = new Logger('response'); $log->pushHandler($handler); // add records to the log $logger->info(json_encode($result,JSON_UNESCAPED_UNICODE));logsフォルダにresponseというファイル名で、30日分の記録を保存するようにハンドラーを設定。
infoレベルでログを記録(レスポンスがjson形式で返ってくるので、json_encodeしている)。結果、logsフォルダにはlogs\response-2020-03-31といった日付ごとのファイルが作成された。
ファイルの中には日時、チャネル名、ログレベル、ログ内容 の順で記録される。↓ファイル内はこんな感じで記録されます
[2020-03-31 14:55:18] response.INFO: {"response":"{\"records\":[{\"date............. [2020-03-31 14:55:20] response.INFO: {"response":"{\"records\":[{\"date.............monologを試してみて
今回は簡易的な内容で試したが、もともとmonologはかなり高機能なロギングのライブラリなので、
ハンドラーに持たせるフォーマッターで出力形式もいろいろなものが選択でき、プロセッサーでログレコードに追加などの操作もできる。
使いこなすことができれば、かなり便利そうだなという印象でした。
- 投稿日:2020-04-21T13:35:32+09:00
NASのバックアップ対象ディレクトリをinotifyで監視して更新されたディレクトリのみをrsyncでミラーリング
前回こちらで書いたミラーリングと世代管理バックアップの2系統のスクリプトのうち、ミラーリングのほうのスクリプトについてパフォーマンスアップの修正を行ないました。
前回のスクリプトでは大まかな流れとしてduコマンドの結果を元に更新対象ディレクトリを絞り込んでrsyncに渡すという動作を行なっていましたが、監視対象内のサブディレクトリ数が多くなるとduコマンドの実行時間もそれなりにかかるようになるので、この部分をinotifywaitコマンドでの監視に置き換えています。
inotifywaitを使えるよう準備
inotify-toolsがインストールされていない場合inotifywaitコマンドも使えませんので、その場合はまずinotify-toolsをインストールします。
inotify-toolsをインストール(Debian系の場合)
$ sudo apt install inotify-toolsinotifywaitで監視可能な対象数はデフォルトで8192になっているかと思いますが、NAS全体を監視対象にしようとするとサブディレクトリ数も多くなり8192では不足する場合もままありますので、環境に合わせてこの設定を増やしておきます。
設定値の確認
$ cat /proc/sys/fs/inotify/max_user_watches例として262144に増やす場合
$ sudo sysctl fs.inotify.max_user_watches=262144この設定は再起動すると初期値に戻ってしまいますので、起動時に自動で設定されるようにしておきます。
いくつか方法はありますが、私はrc.localに追記しました。$ sudo nano /etc/rc.local以下を追記
sysctl fs.inotify.max_user_watches=262144以降は前回と同じ方法でmirroring.phpの実行設定をすれば完了です。
スクリプト
mirroring.php<?php /** * rsync ミラーリング * inotifiwait監視版 */ // ミラーリング元ディレクトリ define('SOURCE_DIR', '/home/nas/data/'); // ミラーリング先ディレクトリ define('BACKUP_DIR', '/home/nas_backup/data/'); // その他のrsyncオプション 例: '--exclude=/temp/ --exclude=/*.bak'; define('OTHER_OPTIONS', ''); /** * */ set_time_limit(0); date_default_timezone_set('Asia/Tokyo'); // 一時ファイル保存用ディレクトリ define('TEMP_DIR', (file_exists('/dev/shm/') ? '/dev/shm/.' : '/var/tmp/.'). md5(__DIR__)); if(!file_exists(TEMP_DIR)) { mkdir(TEMP_DIR); chmod(TEMP_DIR, 0700); } // 各ディレクトリ名のデリミタ補正 $sourceDir = preg_replace('|/+$|', '/', SOURCE_DIR. '/'); $backupDir = preg_replace('|/+$|', '/', BACKUP_DIR. '/'); // バックアップ元・バックアップ先が無かったら終了 if(!file_exists($sourceDir) || !preg_match('/:/', $backupDir) && !file_exists($backupDir)) { print "The source '{$sourceDir}' or backup '{$backupDir}' destination directory does not exist.\n"; exit; } // inotifywaitログファイルパス $inotifyLog = TEMP_DIR. '/inotify.log'; // inotifywaitプロセス管理 $res = inotifywaitProcessManage($sourceDir, $inotifyLog); // inotifywaitログが空か最終更新からの経過時間が2秒未満なら終了 if(file_exists($inotifyLog) && (filesize($inotifyLog) == 0 || time() - filemtime($inotifyLog) < 2) && !$res) { exit; } // ロックファイル名 $lockFilename = TEMP_DIR. '/backup.lock'; // ロックファイルが存在していたら同名のプロセス実行中とみなし終了 if(file_exists($lockFilename)) { print "A process with the same name is running.\n"; exit; } else { // ロックファイル作成 if(!@file_put_contents($lockFilename, 'Process is running.')) { print "Could not create `$lockFilename`.\nSet the permissions of the directory `". TEMP_DIR. "` to 0700.\n"; exit; } chmod($lockFilename, 0600); } // 更新対象ディレクトリ取得 $updateDirList = getUpdataDirList($inotifyLog); if(!$updateDirList) { $updateDirList[] = $sourceDir; } // 更新対象ディレクトリに対してrsync実行 foreach($updateDirList as $dir) { if(!file_exists($dir)) continue; $path = str_replace($sourceDir, '', $dir); // rsyncコマンド $command = implode(" ", [ 'rsync -avH', '--delete', OTHER_OPTIONS, '"'. preg_replace('|/+$|', '/', ($sourceDir. $path. '/')). '"', '"'. preg_replace('|/+$|', '/', ($backupDir. $path. '/')). '"', ]); print "$command\n"; exec($command); } // ロックファイル削除 unlink($lockFilename); exit; /** * */ // inotifywaitログから更新対象ディレクトリ取得 function getUpdataDirList($inotifyLog) { $retArr = []; if(file_exists($inotifyLog)) { $fp = fopen($inotifyLog, 'r+'); $tmpArr = []; while(($l = fgets($fp)) !== false) { $l = trim($l); if(!$l) continue; $l = preg_replace('|/[^/]+$|', '/', $l); $tmpArr[$l] = true; } if($tmpArr) ftruncate($fp, 0); fclose($fp); $retArr = $tmpArr; foreach($tmpArr as $k => $v) { foreach($tmpArr as $k_ => $v_) { if($k == $k_) continue; if(isset($retArr[$k]) && strpos($k_, $k) === 0) unset($retArr[$k_]); } } } return array_keys($retArr); } // inotifywaitプロセス管理 function inotifywaitProcessManage($sourceDir, $inotifyLog) { $command = "inotifywait -mr -o {$inotifyLog} -e create,delete,modify,moved_to,moved_from --format %w {$sourceDir}"; // 既存inotifywaitプロセス存在チェック exec('ps x', $res); $pf = 0; foreach($res as $tmp) { if(strpos($tmp, $command) !== false) $pf++; } // ログが無いかログ最終更新から1時間以上経過していたら // 既存inotifywaitプロセスをkillした上で再度inotifywaitをバックグラウンド実行 if(!file_exists($inotifyLog) || time() - filemtime($inotifyLog) >= 3600 * 1 || !$pf) { // inotifywaitコマンドが見つからなかったら終了 if(!preg_match('|/inotifywait|', exec('type inotifywait'))) { print "Cannot find `inotifywait` command.\n"; exit; } exec("pkill -f \"{$command}\""); $command .= " &"; print "$command\n"; if(file_exists($inotifyLog)) unlink($inotifyLog); exec($command); chmod($inotifyLog, 0600); return true; } return false; }ディレクトリの更新監視にinotifywaitを利用してはいますが、更新があった場合に自動的にmirroring.phpが実行されるわけではなくmirroring.phpはこれまで通りcronて定期的に実行し、スクリプト内でinotifywaitのログを参照することでその間に更新のあった監視対象内のサブディレクトリを取得してrsyncに渡すという動作になっています。
ここまで書いておいて何ですが、inotifyで監視してrsyncで同期という流れはLsyncdと似たようなことをしているわけなので、人によっては素直にLsyncdを導入したほうが楽かもしれません。
- 投稿日:2020-04-21T11:46:25+09:00
PHPスコープについて
スコープについて
スコープとは、ある場所で定義された変数や関数使える範囲のことをスコープという。
PHPの変数には、グローバル変数とローカル変数がある。ローカル変数
ローカル変数は、決められた範囲内でしか使えない変数。例えば、関数の中で変数が定義されたら、その関数内でした使うことができない。
<?php $hoge = 1; function fuga() { echo $hoge; } fuga(); //エラーになる。$hogeは関数の中で定義されているので、関数の外では使えない。 ?>グローバル変数
グローバル変数とは、ローカル変数とは違い、関数の外でも使える変数のこと。
先ほどのローカル変数を外で使えるようにするには<?php $hoge = 1; function fuga() { global $hoge; echo $hoge; } fuga(); //結果1と表示される$globalをつけるだけで、関数の外でも使えるようになった。
しかし、関数の外では使えるが、別の関数内では使えない。あくまでもグローバル範囲内で使えるようになる。<?php //グローバル範囲 function fuga() { //ローカル範囲 } //グローバル範囲 function hoge() { //ローカル範囲 } //グローバル範囲 ?>今回はグローバル変数とローカル変数についてまとめました。
static変数などもあるので、勉強しときます。
- 投稿日:2020-04-21T11:11:09+09:00
[nginx]https接続のnginxでPHPを動かす[PHP]
nginxでPHPを動かす
httpではPHPが動くのにhttpsで動かない状態を改善する方法をメモとして残します。
nginx/conf.d/default.conf設定に以下を追加します。
rootフォルダは任意に変更してください。default.confserver { listen 443 ssl http2; server_name sample.com; if ($request_uri ~ ^.*/index.html$){ rewrite ^(.*)/index.html$ $1/ permanent; } location / { root /html; index index.html index.htm index.php; } ssl_protocols TLSv1.2; ssl_ciphers EECDH+AESGCM:EECDH+AES; ssl_ecdh_curve prime256v1; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_certificate /etc/letsencrypt/live/sample.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/sample.com/privkey.pem; error_page 500 502 503 504 /50x.html; location = /50x.html { root /html; } location ~ \.php$ { root /html; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }以上
- 投稿日:2020-04-21T11:06:51+09:00
【PHP】Yahoo!広告APIでレポートデータを取得したい!
はじめに
GoogleAPIについて書いたので、ついでにYahoo!もメモ。
Yahoo!も従来の「Yahoo!プロモーション広告API」から「Yahoo!広告 API」と名称を一新し、
新APIが2月20日に正式リリースされた。これに合わせてサービスも名称を変更し、
「Yahooスポンサードサーチ」は「検索広告」に、「Yahooディスプレイ広告」は「ディスプレイ広告」に。Googleと同じくoAuth2認証になった。ほかもいろいろ変わってるぽい。
個人的には認証以外にはそれほど大きく変わったわけではなさそう?という印象。旧APIはスポンサードサーチが2020年9月30日(水)、
ディスプレイ広告が2020年12月16日(水)にサービス終了とのこと。
#コロナの影響で延びる可能性あるかも・・・?これもGoogleと同じく旧APIでCRONエラーが出たので調べてみたら
新API正式リリースとか言われて「えっうそ」といった感じで慌てて直した。。
リリースノートはちゃんと確認しないとイカンです・・・
#っていうか運用担当者さんにメール来てただろうから教えてくれよと。。リリース詳細はこちら
従来のAPIを利用していた場合でも新たに申し込みが必要とのこと。
申込みが完了すると、検索・ディスプレイ両機能のテストアカウントIDももらえる。
(全然使ってないけど)移行に関しては親切なドキュメントがあったのでスタートアップを参照のこと。
データ取得:oAuth2認証
さて、Yahoo新APIは旧よりも若干不親切で、サンプルプログラムがJavaしか用意されていない。
旧APIはYahoo!のほうがGoogleより親切だと感じたんだけどな。
新はGoogleのがサンプル多くて使いやすそう。認証はGoogleと同じ方式ということなので、横並びでやってみたらあっさり出来た。
※スタートアップに沿って設定あれこれは済ませてから!以下、Yahooのアクセストークン取得処理。
//認証用URL $version = "v1"; $oAuth2_url = "https://biz-oauth.yahoo.co.jp/oauth/$version/token"; //リフレッシュトークン $refresh_token = "REFRESH_TOKEN"; //クライアントID $client_id = "CLIENT_ID"; //クライアントシークレット $client_secret = "CLIENT_SECRET"; //curl START $curl = curl_init(); //OPTIONをセット curl_setopt_array($curl, [ CURLOPT_URL => $oAuth2_url, CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_POSTFIELDS => http_build_query([ "refresh_token" => $refresh_token, "client_id" => $client_id, "client_secret" => $client_secret, "grant_type" => "refresh_token", ]), ]); //curl EXEC(文字列で取得) $resp = curl_exec($curl); //エラーハンドリング用 $errno = curl_errno($curl); //curl END curl_close($curl); //エラーハンドリング if ($errno !== CURLE_OK) { //エラー処理 } //エラーでなければjsonを連想配列化 $jsonresp = json_decode($resp, true); //アクセストークンを取得 $access_token = $jsonresp["access_token"];認証はYahoo検索/Yahooディスプレイ共通。
レポート取得となるとエンドポイントや処理を分ける必要がある。
#ほとんど似た仕様なのだが、それぞれがびっっみょーに違うのでイラッとする。統一してくれ・・・データ取得:レポートデータ取得
続いてレポートデータ取得処理。
利用するリソースはReportDefinitionServiceというやつなのだが、
まず設定をaddして、getして、downloadして最後にremoveという手順が必要。非常にめんどい。。※リソースの使用に関してはリファレンス参照のこと
あと旧から仕様はそれほど変わらないらしく、ステータスがOKになるまで数秒waitする必要がある。
ここはGoogleと大きく違う部分。前述のようにYahoo検索/ディスプレイでびっみょーに処理が違うけど、
基本的に同じなので、Yahoo検索の方を紹介していく。Yahoo検索の場合
//エンドポイント $endpoint = "https://ads-search.yahooapis.jp/api" //アカウントID $account_id = "ACCOUNT_ID"; /** * SELECT fields */ $YSSfields = [ "DAY", "CAMPAIGN_NAME", "ADGROUP_NAME", "CLICKS", "COST", "AVG_CPC", "IMPS", "ADGROUP_ID", "CAMPAIGN_ID", ]; //ヘッダー情報をセット $header = [ "Content-Type: application/json", "Accept: application/json", "Authorization: Bearer ".$access_token, ]; //URLをセット $url = $endpoint."/".$version."/ReportDefinitionService/"; //**************************** // [1]add:reportJobIdを取得 //**************************** //add用URL $url_add = $url."add"; //API用パラメータ配列[※ここでは日時を本日に指定] $param_add = [ "accountId" => $account_id, "operand" => [ [ "reportName" => "test", "reportType" => "ADGROUP", "reportDateRangeType" => "TODAY", "fields" => $YSSfields, ] ] ]; //curl START $curl = curl_init(); //OPTIONをセット curl_setopt_array($curl, [ CURLOPT_URL => $url_add, CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $header, CURLOPT_POSTFIELDS => json_encode($param_add), ]); //curl EXEC(json文字列で取得) $resp_add = curl_exec($curl); $result_add = json_decode($resp_add, true); //json文字列を連想配列化 //curl END curl_close($curl); //エラーハンドリング $errno = ($result_add["errors"] !== NULL) ? $result_add["errors"]["code"] : NULL; if ($errno !== NULL) { //エラーハンドリング } //reportJobIdを取得 $jobid = $result_add["rval"]["values"][0]["reportDefinition"]["reportJobId"]; //**************************** // [2]get:reportデータを取得 // JobStatusがCOMPLETEになるまでWAIT //**************************** $statusflg = false; for ($i = 0; $i < 30; $i++) { //30秒待機 sleep(30); //get用URL $url_get = $url."get"; //API用パラメータ配列 $param_get = [ "accountId" => $account_id, "reportJobIds" => [$jobid], ]; //curl START $curl = curl_init(); //OPTIONをセット curl_setopt_array($curl, [ CURLOPT_URL => $url_get, CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $header, CURLOPT_POSTFIELDS => json_encode($param_get), ]); //curl EXEC(文字列で取得) $resp_get = curl_exec($curl); $result_get = json_decode($resp_get, true); //json文字列を連想配列化 //curl END curl_close($curl); //エラーハンドリング $errno = ($result_get["errors"] !== NULL) ? $result_get["errors"]["code"] : NULL; if ($errno !== NULL) { //エラーハンドリング } //ステータスコードを取得 $status = $result_get["rval"]["values"][0]["reportDefinition"]["reportJobStatus"]; //ステータスコードで処理を判別 if ($status === "IN_PROGRESS" || $status === "WAIT") { continue; } else if ($status === "COMPLETED") { $statusflg = true; break; } else { break; } } //**************************** // [3]download:reportデータをダウンロード //**************************** if ($statusflg) { //ステータスがCOMPLETEDの場合のみ処理 //add用URL $url_dl = $url."download"; //API用パラメータ配列 $param_dl = [ "accountId" => $account_id, "reportJobId" => $jobid, ]; //curl START $curl = curl_init(); //OPTIONをセット curl_setopt_array($curl, [ CURLOPT_URL => $url_dl, CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $header, CURLOPT_POSTFIELDS => json_encode($param_dl), ]); //curl EXEC(文字列で取得)******************************←ここにレポートデータ入ってる $resp_dl = curl_exec($curl); //curl END curl_close($curl); //エラーハンドリング $errno = ($result_dl["errors"] !== NULL) ? $result_dl["errors"]["code"] : NULL; if ($errno !== NULL) { //エラーハンドリング } } //**************************** // [4]remove:reportデータを削除 //**************************** //add用URL $url_rm = $url."remove"; //API用パラメータ配列 $param_rm = [ "accountId" => $account_id, "operand" => [ [ "reportJobId" => $jobid, ] ] ]; //curl START $curl = curl_init(); //OPTIONをセット curl_setopt_array($curl, [ CURLOPT_URL => $url_rm, CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120, CURLOPT_HTTPHEADER => $header, CURLOPT_POSTFIELDS => json_encode($param_rm), ]); //curl EXEC(文字列で取得) $resp_rm = curl_exec($curl); $result_rm = json_decode($resp_rm, true); //curl END curl_close($curl); //エラーハンドリング $errno = ($result_rm["errors"] !== NULL) ? $result_rm["errors"]["code"] : NULL; if ($errno !== NULL) { //エラーハンドリング }長っ!
コード長っ!!
たかだか本日のレポート取るだけでそんなに行数かかるかい?って思うんですけど、
現状で自分が解決できる方法がこれしかない。。Yahooディスプレイ広告の場合
基本的には同じなんですが、違う部分はまず
1.エンドポイント(そりゃそうだよ)
//エンドポイント $endpoint = "https://ads-display.yahooapis.jp/api"2.POSTで渡すパラメータのKEY名や形式が微妙に違う。大文字小文字とか、ほんと微妙に。
//add用パラメータ配列 $param_add = [ "accountId" => $account_id, "operand" => [ [ "reportName" => "test", "dateRangeType" => "TODAY", "fields" => $YDNfields, ] ] ];3.あとgetでステータス取得する際のステータス名が微妙に違う。
WAITの代わりにACCEPTED//ステータスコードで処理を判別 if ($status === "IN_PROGRESS" || $status === "ACCEPTED") { continue; } else if ($status === "COMPLETED") { $statusflg = true; break; } else { break; }他にももしかしたら違いあったかも。微妙すぎてメモってないけど。
横並びで使ってみて動かなかったら一つずつ確認するって感じ。おわりに
Yahoo!の旧APIのサンプルプログラムを部分的に抜粋しながら作ったので、
それほど離れたことはやってない・・・はず。今後、リファレンスやサンプルが充実してきたら、もっとスマートなやり方が分かるのかもしれない。
上記はあくまで我流だということをご承知おきいただければと。。これからAPI導入する方が、少しでも参考にしてくれれば嬉しい。
今度はレポートデータではなくキーワードの更新などに取り掛かる予定。うまくいくといいなー
- 投稿日:2020-04-21T09:57:57+09:00
PHPインストールしたら必須!パス+version確認+php.iniの初期設定をしよう!
PHPをインストールしよう
php5.6を公式サイトからインストール
ちなみに公式サイトでのインストールはntc,src以外の.zip形式をDLしました。例:バージョン7.3の場合
1/21/2020 3:36 PM 28326466 php-7.3.14-src.zip
1/21/2020 3:36 PM 25729058 php-7.3.14-Win32-VC15-x64.zip //ここ
1/21/2020 3:36 PM 23972430 php-7.3.14-Win32-VC15-x86.zip //ここ
1/21/2020 10:49 PM 25951614 php-7.4.2-nts-Win32-vc15-x64.zip
1/21/2020 10:49 PM 24180393 php-7.4.2-nts-Win32-vc15-x86.zipこのDLしたファイルはCドライブのPHPフォルダの中に展開する
※環境変数でパスを通すC;/PHP
PHPファイル初期設定(必須)
PHPのインストールしたフォルダを開いて、「php.ini-development」と「php.ini-production」があることを確認しましょう。
「php.ini-development」をリネームして「php.ini」に変更する
リネームした「php.ini」をテキストエディタで開いて以下の行のコメントアウトをはずす
一ヶ所目
; extension_dir = "ext"
→extension_dir = "ext"
に変更する二ヶ所目
;extension=openssl
→extension=openssl
に変更する当方、これを行わなかったのでcomposerコマンドで下記エラーが出たので初期設定は行うように
C:\Users\TOSHI>composer global require "Laravel/installer=~1.1" Changed current directory to C:/Users/TOSHI/AppData/Roaming/Composer [Composer\Exception\NoSslException] The openssl extension is required for SSL/TLS protection but is not available. If you can not enable the openssl ex tension, you can disable this error, at your own risk, by setting the 'disable-tls' option to true. require [--dev] [--prefer-source] [--prefer-dist] [--no-progress] [--no-suggest] [--no-update] [--no-scripts] [--update-no-dev] [--update-with-dependencies] [--update-with-all-dependencies] [--ignore-platform-reqs] [--prefer-stable] [--prefer-lowest] [--sort-packages] [-o|--optimize-autoloader] [-a|--classmap-authoritative] [--apcu-autoloader] [--] [<packages>]...php artisan -vでエラー
PHP Warning: require(C:\Users\TOSHI\Desktop\app/vendor/autoload.php): failed to open stream: No such file or directory in C:\Users\TOSHI\Desktop\app\artisan on line 18 Warning: require(C:\Users\TOSHI\Desktop\app/vendor/autoload.php): failed to open stream: No such file or directory in C:\Users\TOSHI\Desktop\app\artisan on line 18 PHP Fatal error: require(): Failed opening required 'C:\Users\TOSHI\Desktop\app/vendor/autoload.php' (include_path='.;C:\php\pear') in C:\Users\TOSHI\Desktop\app\artisan on line 18 Fatal error: require(): Failed opening required 'C:\Users\TOSHI\Desktop\app/vendor/autoload.php' (include_path='.;C:\php\pear') in C:\Users\TOSHI\Desktop\app\artisan on line 18このようなエラーが表示されてしまいました。
調べて見るとcomposer install
をするといいらしいしかし・・・エラーが発生
Loading composer repositories with package information Installing dependencies (including require-dev) from lock file Your requirements could not be resolved to an installable set of packages. Problem 1 - Installation request for league/flysystem 1.0.63 -> satisfiable by league/flysystem[1.0.63]. - league/flysystem 1.0.63 requires ext-fileinfo * -> the requested PHP extension fileinfo is missing from your system. Problem 2 - league/flysystem 1.0.63 requires ext-fileinfo * -> the requested PHP extension fileinfo is missing from your system. - laravel/framework v6.13.1 requires league/flysystem ^1.0.8 -> satisfiable by league/flysystem[1.0.63]. - Installation request for laravel/framework v6.13.1 -> satisfiable by laravel/framework[v6.13.1].このエラーはfileinfoが必要ですと言われるので
php.ini
のfileinfoの部分をコメントアウトしてあげます。
;extension=fileinfo
→extension=fileinfo
すると、
composer install
が正常に実行されてphp artisan -v
でlaravelのバージョンも確認できました!補足
php artisan -v
を行う時はlaravelプロジェクトに移動してから!
例:C:\Users\ユーザー名\Desktop\app>cd
C:\Users\ユーザー名\Desktop\app
- 投稿日:2020-04-21T09:00:20+09:00
PHP7.4 ぼくのかんがえたさいきょうのphp.ini
ストーリー
PHPをインストールしたら必ず行う
php.ini
の設定ですが、
ネット上ではPHP5系の情報がたくさん出回っており、非推奨または削除された設定例が数多く困り果てていました。良い感じにまとめてくれてるサイトが見つからなかったので、最強でベストプラクティスな
php.ini
推奨設定を考えました。
異論は受け付けますので、ぜひコメントください。参考設定
PHPでは、開発用と本番用の設定例を用意してくれています。
なんと素晴らしいことなんでしょうか。これをベースに設定します。
- https://github.com/php/php-src/blob/master/php.ini-development
- https://github.com/php/php-src/blob/master/php.ini-production
予め以前の記事で設定の差分を調べておきましたので、よかったらご覧ください。
環境
- PHP 7.4.5 (執筆時のバージョンです。)
※バージョンが異なる場合は公式サイトで有効な設定か確認してください。
開発用 php.ini
php.inizend.exception_ignore_args = off expose_php = on max_execution_time = 30 max_input_vars = 1000 upload_max_filesize = 64M post_max_size = 128M memory_limit = 256M error_reporting = E_ALL display_errors = on display_startup_errors = on log_errors = on error_log = /var/log/php/php-error.log default_charset = UTF-8 [Date] date.timezone = Asia/Tokyo [mysqlnd] mysqlnd.collect_memory_statistics = on [Assertion] zend.assertions = 1 [mbstring] mbstring.language = Japanese本番用 php.ini
php.inizend.exception_ignore_args = on expose_php = off max_execution_time = 30 max_input_vars = 1000 upload_max_filesize = 64M post_max_size = 128M memory_limit = 256M error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT display_errors = off display_startup_errors = off log_errors = on error_log = /var/log/php/php-error.log default_charset = UTF-8 [Date] date.timezone = Asia/Tokyo [mysqlnd] mysqlnd.collect_memory_statistics = off [Assertion] zend.assertions = -1 [mbstring] mbstring.language = Japanese [opcache] opcache.enable = 1 opcache.memory_consumption = 128 opcache.interned_strings_buffer = 8 opcache.max_accelerated_files = 4000 opcache.validate_timestamps = 0 opcache.huge_code_pages = 0 opcache.preload = /var/www/preload.php opcache.preload_user = www-dataオプションの補足
設定値だけだと何を設定しているかわからないので、各項目の補足を付け加えました。
zend.exception_ignore_args
https://www.php.net/manual/ja/migration74.other-changes.php
- 開発 は
off
、本番 はon
- 有効にすると例外のスタックトレースに引数情報が出なくなる
- PHP7.4以降の設定
expose_php
https://www.php.net/manual/ja/ini.core.php#ini.expose-php
- 開発 は
on
、本番 はoff
- 有効にするとHTTPヘッダに
X-Powered-By: PHP/7.4.5
とPHPのバージョン情報が表示されます。max_execution_time
https://www.php.net/manual/ja/info.configuration.php#ini.max-execution-time
- 設定値: 30(秒) デフォルト: 30(秒)
- 1リクエストあたりの最大実行時間(秒)
- コマンドラインから実行する場合のデフォルト設定は 0 です。
- サーバーの負荷を上げることを防止するのに役立ちます。
重たい処理を実行するとこの設定で引っかかるので、その場合はそのコードだけ特別にset_time_limitを呼んであげると良いかもです。
max_input_vars
https://www.php.net/manual/ja/info.configuration.php#ini.max-input-vars
- 設定値: 1000(個) デフォルト: 1000(個)
- 1リクエストで受け付ける最大の入力変数の数
$_GET
,$_POST
,$_COOKIE
それぞれ個別に適用されます。- 設定値を超える場合は
E_WARNING
が発生し、以降の入力変数はリクエストから削除されます。入力フォームが気が狂ったように多い画面とかは1000超えるかも?
upload_max_filesize
https://www.php.net/manual/ja/ini.core.php#ini.upload-max-filesize
- 設定値: 20M デフォルト: 2M
- 設定する単位に短縮表記を使えます
- アップロードされるファイルの最大サイズ。
スマホの写真サイズも大きいので多めにした方が良き
post_max_size
https://www.php.net/manual/ja/ini.core.php#ini.post-max-size
- 設定値: 128M デフォルト: 8M
- upload_max_filesize の設定値より大きくする必要がある。
- POSTデータに許可される最大サイズを設定します。
- ファイルアップロードにも影響します。
memory_limit
https://www.php.net/manual/ja/ini.core.php#ini.memory-limit
- 設定値: 256M デフォルト: 128M
- post_max_size の設定値より大きくする必要がある。
- memory_limit > post_max_size > upload_max_filesize
- 1リクエストあたりの最大メモリ使用量
メモリ設定はサーバーやプロジェクトによるかと思います。
最初から大量に確保するのではなく、必要に応じて上げていくのが良いのかなと思います。error_reporting
https://www.php.net/manual/ja/errorfunc.configuration.php#ini.error-reporting
- 開発 は
E_ALL
、本番 はE_ALL & ~E_DEPRECATED & ~E_STRICT
E_ALL
は 全ての PHP エラーを表示するE_ALL & ~E_DEPRECATED & ~E_STRICT
は 非推奨の警告エラーを除く PHP エラーを表示する。
E_DEPRECATED
は コードの相互運用性や互換性を維持するために PHP がコードの変更を提案する。E_STRICT
は 実行時の注意、将来のバージョンで動作しなくなるコードについて警告する。display_errors
- 開発 は
on
、本番 はoff
- エラーをHTML出力の一部として画面に出力するかどうかを定義します。
- セキュリティ上、本番では
off
推奨display_startup_errors
http://php.net/display-startup-errors
- 開発 は
on
、本番 はoff
display_errors
をon
にした場合でも、PHPの起動シーケンスにおいて発生したエラーは表示されません。- セキュリティ上、本番では
off
推奨log_errors
https://www.php.net/manual/ja/errorfunc.configuration.php#ini.log-errors
- エラーメッセージを、サーバーのエラーログまたはerror_logに記録するかどうかを指します。
- このオプションはサーバーに依存します。
error_log
https://www.php.net/manual/ja/errorfunc.configuration.php#ini.error-log
- スクリプトエラーが記録されるファイル名です。
default_charset = UTF-8
https://www.php.net/manual/ja/ini.core.php#ini.default-charset
- 設定値: UTF-8 デフォルト: UTF-8
- デフォルト文字コード設定
PHP 5.6.0 以降は "UTF-8" がデフォルトになりますが、念のため明示的に指定します。
date.timezone
https://www.php.net/manual/ja/datetime.configuration.php#ini.date.timezone
- 設定値: Asia/Tokyo デフォルト: GMT
- 全ての日付/時刻関数で使用されるデフォルトのタイムゾーン。
mysqlnd.collect_memory_statistics
https://www.php.net/manual/ja/mysqlnd.config.php#ini.mysqlnd.collect-memory-statistics
- 開発 は
on
、本番 はoff
- さまざまなメモリ統計情報の収集を有効にします。
phpinfo()
でmysqli
の統計情報を出力するかどうかzend.assertions
https://www.php.net/manual/ja/ini.core.php#ini.zend.assertions
- 開発 は
1
、本番 は-1
- アサーションのコードを生成して実行します
- 1 アサーションのコードを生成して実行します (開発モード)
- 0 アサーションのコードは生成しますが実行時にはスキップします (実行しません)
- -1 アサーションのコードを生成せず、アサーションのコストがゼロになります (実運用モード)
mbstring.language
https://www.php.net/manual/ja/mbstring.configuration.php#ini.mbstring.language
- 設定値: Japanese デフォルト: neutral
- mbstring で使用される言語設定のデフォルト値。
opcache の設定
https://www.php.net/manual/ja/opcache.configuration.php
本番のみ有効にします。
opcacheするとソースコードのキャッシュ、最適化して高速化が見込めます。ソースコードを変更してもサーバーを再起動しないと変更が反映されなくなるため開発時は使用しません。
opcache.enable
- オペコード・キャッシュを有効にします。
opcache.memory_consumption
- OPcache によって使用される共有メモリ・ストレージのサイズ(MB単位)
opcache.interned_strings_buffer
- インターン (intern) された文字列を格納するために使用されるメモリ量。(MB単位)
opcache.max_accelerated_files
- OPcache ハッシュテーブルのキー(すなわちスクリプト)の最大数
opcache.validate_timestamps
- 有効にすると、OPcache は、スクリプトが更新されたか opcache.revalidate_freq 秒ごとにチェックします。
- 無効にすると、スクリプトの更新をチェックしません。
opcache.huge_code_pages
- PHPコード(textセグメント)を HUGE PAGE にコピーする機能を有効にしたり、無効にしたりできます。
- これにより、パフォーマンスは向上するはずですが、適切なOSの設定が必要です。
※適切なOS設定がいまいちわからなかったので、この設定は無効化しています。
opcache.preload
- サーバが起動した際にコンパイルされ、実行されるPHPスクリプトを指定します。
- PHP7.4以降の設定
※ここはプロジェクトに合わせて自前で用意する必要があります。これは無理に設定しなくてもokと思います。
※Laravelの場合はこちらのコードを参考にしています。 https://github.com/brendt/laravel-preload/blob/master/preload.phpopcache.preload_user
- root ユーザでコードをあらかじめロードすることは、セキュリティ上の理由から禁止されています。
- PHPの実行ユーザーを指定します。
- PHP7.4以降の設定
- Fatal Error "opcache.preload_user" has not been defined
その他
論理値
設定で使用される論理値(true, false, on, off, yes, no)は大文字・小文字は区別しないようなので、True, On等でも認識されます。
とても柔軟で素敵だと思いました???私のphp.iniはどこ?
ここです。
$ php -i | grep php.ini環境変数を使いたい
普通に環境変数読み込めます。
php.inidate.timezone = $TZさいきょうのツール爆誕!
記事と照らし合わせて手元の環境のphp.iniをチェックできるツールができました。
— suin❄️TypeScript入門書執筆中 (@suin) April 21, 2020
## 本番環境としてチェック<br>curl -sS <a href="https://t.co/264zxsg83a">https://t.co/264zxsg83a</a> | php<br>
## 開発環境としてチェック<br>curl -sS <a href="https://t.co/264zxsg83a">https://t.co/264zxsg83a</a> | INIENV=dev php<br>
pic.twitter.com/MigmzN3W1Wお手元の環境と本記事の推奨設定を照らし合わせて、差分を表示する神ツールを @suin 氏が作成してくれました。
開発用の差分チェック
$ curl -sS https://raw.githubusercontent.com/suin/php-playground/master/UcanIniAdvisory/ucan-ini-advisory.php | INIENV=dev php本番用の差分チェック
$ curl -sS https://raw.githubusercontent.com/suin/php-playground/master/UcanIniAdvisory/ucan-ini-advisory.php | php参考
- 投稿日:2020-04-21T08:51:12+09:00
学習日記 #2
インターフェイス
PHPでは多重継承が認められていないため、一度に継承できるクラスは常に1つである。複数のクラスを同時に継承することは出来ない。そこで利用するのがインターフェイスである。
<?php interface インターフェイス名 { 抽象メソッド、定数の定義 }インターフェイスはクラスに似ているが、構文の違いがある。
・中身をもつメソッドやプロパティは定義出来ない(配置できるのは、抽象メソッドと定数だけである。)
・配下のメソッドが抽象メソッドであることは明らかなので、abstract修飾子は必要ないし、指定してはいけない。
・アクセス修飾子も指定できない。public修飾子を指定しても構わないが、意味がないので通常は割愛する。
・インターフェイスであることがわかるように、IFigure,FigureInterFaceのように接頭辞・接尾辞を指定する。インターフェイスの機能を継承することを実装するという。また、実装したクラスのことを実装クラスと呼ぶ。
インターフェイスの実装
class 実装クラス名 implements インターフェイス名, { // クラスの本体 }無名クラス
無名クラスは、名前を持たないクラスである。名前がないため、特定の文の中でしか利用することができない。
しかし、以下のようなメリットがある。
・コードをシンプルに表現することが出来て、見た目に関連性を把握しやすい。
・式が許されている場所であれば、どこにでも記述できる。
・名前がないので、そもそも競合する恐れがない。その性質から、定義したクラスを後から利用しないことがわかっているクラスを定義する際に利用する。
new class {....プロパティ/メソッドの定義...} new class extends 親クラス名 { ....プロパティ/メソッドの定義.... } new class implements インターフェイス名 { ....プロパティ/メソッドの定義... }
- 投稿日:2020-04-21T01:05:24+09:00
PHP+MySQLで作る2ちゃんねる風掲示板機能を解説
記事概要
タイトルの通り、PHPとMySQLを利用して、かつて2ちゃんねると呼ばれていた掲示板のようなものの作り方を解説したものです。はじめにプログラムのソースコード全体を公開します。その後に要所毎に解説を加えていきます。
Recruit
唐突ですが、私は現在、地方の大学に通う、農学部の4年生です。プログラマーとして現在就職活動を行なっております。居住地域の関係上、IT企業でのアルバイト等の経験はなく、1年以上前から独学でプログラミングの学習をしつつ、個人開発を続けていました。
勤務地は問いません。雇用形態はアルバイトやインターンからでも構いません。
ご連絡はTwitterのDM→@Ren_s_off または、こちらのメールアドレスまで(oaihgop4@yahoo.co.jp)お願いいたします。お気軽にお問い合わせください。
Portfolio & Skills
本記事を含め、Qiitaに作成した作品についての記事を投稿していこうと考えています。今は記事が執筆中の段階ですので、随時こちらにURLを更新していきます。
・HTML5,CSS,JavaScript
これらを利用したWebサイトの制作経験があります。レスポンシブに対応しています。JavaScriptはモバイル版のハンバーガーメニューの挙動制御やフッターをメインコンテンツの高さに関わらず常にブラウザの最下部に表示させるために利用しています。
・岩手県のグルメ情報サイトhttps://iwategourmet.com/iwategourmet/・PHP,MySQL
本記事のようにデータベースを利用した掲示板の作成の他、会員制サイトの会員登録およびログイン機能の実装ができます。そちらについてはQiitaにて記事を随時更新していきます。機能
・ハンドルネームと本文を入力して送信すると内容がその下の部分にどんどん追加されていきます。
・それと同時に送信した時刻が記録されていきます。
・ハンドルネームを入力せずに本文を送信すると、ハンドルネームが自動的に「名無し」になります。
・本文を入力せずに送信すると、「本文を入力してください」という警告が出ます。PHPと言えばまあやるよねって感じで、今日は掲示板を作りました。#PHP #MySQL pic.twitter.com/dQDOZ1SE7Y
— K.Takashiro (@Ren_s_off) March 8, 2020データベース
まずデータベースを用意します。bbsという名前のデータベースを作成し、その中にtestというテーブルを作成しました。いい名前を思い付かなかったのでとりあえずtestと名付けました。このtestテーブルに「id」「name」「comment」「date」の4つのカラムを作ります。
bbs.txtCREATE DATABASE bbs DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; CREATE USER 'XXXX'@'YYYY' IDENTIFIED BY 'ZZZZ'; GRANT ALL ON bbs.* TO 'XXXX'@'YYYY'; USE bbs; CREATE TABLE `test` ( `id` INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY , `name` VARCHAR(20) NOT NULL , `comment` VARCHAR(300) NOT NULL , `date` VARCHAR(100) NOT NULL );「txt」として外部ファイルにSQLコマンドを保存して、SOURCEコマンドを利用すれば、外部ファイルで用意したSQLコマンドを一度に読み込めます。
SOURCE (bbs.txtが保存してあるディレクトリまでのパス)/bbs.txt解説
CREATE DATABASE bbs DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;・「CREATE DATABASE XXXX」でXXXXという名前のデータベースを作成します。
・「DEFAULT CHARACTER SET XXXX」で文字コードをXXXXに指定します。マルチバイト文字をプログラムで利用する場合は、文字コードの指定が重要になります。
・「COLLATE (文字コード) _ (言語名) _ (比較法)」 COLLATE以下でデータベースの照合順序を指定します。CREATE USER 'XXXX'@'YYYY' IDENTIFIED BY 'ZZZZ'; GRANT ALL ON bbs.* TO 'XXXX'@'YYYY';・「CREATE USER 〜」でデータベースを操作する際のユーザーを作成します。XXXXにはユーザー名、YYYYにはホスト名が入ります。ホスト名にはIPアドレスやlocalhostも指定できます。ZZZZにパスワードを指定します。
・「GRANT 〜」 GRANTステートメントはMySQLユーザーに権限を付与したり、セキュア接続の使用やサーバーリソースへのアクセスに関する制限などの、その他のアカウント特性を指定する機能もあります。ここではbbsというデータベースに置いて全ての権限をYYYY上でXXXXに与えるという意味です。CREATE TABLE `test` ( `id` INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY , `name` VARCHAR(20) NOT NULL , `comment` VARCHAR(300) NOT NULL , `date` VARCHAR(100) NOT NULL );・「CREATE TABLE
@@@@
〜」 @@@@に作成するTABLE名を設定します。
INT の後に、UNSIGNEDを指定することで、通常、INT型は-2147483648 ~ 2147483647の数値を扱うのに対して、0 ~ 4294967295 の負の数以外の範囲の数値を扱います。負の数が入らないことがわかっているデータに対してはUNSIGNEDを指定すると良いでしょう。ソースコード
・view.php
掲示板の情報の入出力を行う画面です。view.php<?php require_once('model.php'); ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <form action="<?php print $_SERVER['PHP_SELF']; ?>" method="post"> <p>名前<input type="text" name="name"><?php echo $err_msg1; ?></p> <p>本文<textarea name="comment" rows="10" cols="70"></textarea><?php echo $err_msg2; ?></p> <input type="submit" value="送信" name="send"> <?php echo $message; ?> </form> <?php foreach ($res as $value) { echo $value; } ?> </body> </html>・model.php
view.phpで受け取ったデータをデータベースとやりとりするためのプログラムです。データベースの接続に必要な情報や処理は「DBManager.php」という外部ファイルに記述しました。DBManager.php<?php function getDB() { $dsn = "mysql:host=YYYY; dbname=bbs; charset=utf8"; $db_user = "XXXX"; $db_pass = "ZZZZ"; $db = new PDO($dsn, $db_user, $db_pass); return $db; } ?>model.php<?php require_once('DbManager.php'); function h( $str ){ return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); } //名前,タイトル,本文の内容を取得 $name = ( isset( $_POST["name"] ) === true ) ? h( $_POST["name"] ) : "名無し"; $comment = ( isset( $_POST["comment"] ) === true ) ? h( $_POST["comment"] ) : ""; //エラーメッセージ $err_msg1 = ""; //名前が長過ぎる時に呼び出されるエラーメッセージ $err_msg2 = ""; //本文が入力されていない時に呼び出されるエラーメッセージ $message = ""; //書き込みに成功した時に呼び出されるメッセージ if ( isset($_POST["send"] ) === true ) { if ( $name === "") $name = "名無し"; if ( $comment === "" ) $err_msg2 = "本文を入力してください"; $name = trim( $name ); $comment = trim( $comment ); if ( mb_strlen($name, "UTF-8") > 20 ) $err_msg1 = "名前は20文字以内にしてください"; if ( mb_strlen($comment, "UTF-8") > 300 ) $err_msg2 = "本文は300文字以内にしてください"; if ( $err_msg1 === "" && $err_msg2 === "" ) { try { $date = date("Y-m-d H:i:s"); $pdo = getDB(); $pdo->setAttribute(PDO::ATTR_ORACLE_NULLS, PDO::NULL_EMPTY_STRING); $stt = $pdo->prepare('INSERT INTO test(name, comment, date) VALUES(:name, :comment, :date)'); $stt->bindValue(':name', $name); $stt->bindValue(':comment', $comment); $stt->bindValue(':date', $date); $stt->execute(); } catch (PDOException $e) { $message = "<p>接続エラー: " . $e->getMessage() . "</p>"; die(); } finally { $pdo = null; } } } //コメントを格納する変数 $res = array(); try { $pdo = getDB(); $stt = $pdo->query( 'SELECT name, comment, date FROM test' ); //内容を出力 $i = 1; while ( $row = $stt->fetch(PDO::FETCH_ASSOC) ) { $res[$i] = "<p>{$i}:{$row["name"]} {$row["date"]}<br>{$row["comment"]}</p>"; $i++; } } catch (PDOException $e) { $message = "<p>接続エラー: " . $e->getMessage() . "</p>"; die(); } finally { $pdo = null; } ?>解説
function h( $str ){ return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); }XSS対策として、htmlspecialchars()を利用しました。htmlspecialchars()はソースコードが冗長になることを防ぐために、あらかじめ処理をユーザー定義関数h()にまとめました。また、今回のプログラムでは、マルチバイト文字列の長さを判定するif文の直前で、スペースを除去するtrim()を利用していますが、h()の中でtrim()を利用して、文字列を整形するというのも考えられます。
データベースにコメントを追加する
if ( $err_msg1 === "" && $err_msg2 === "" ) { try { $date = date("Y-m-d H:i:s"); $pdo = getDB(); $pdo->setAttribute(PDO::ATTR_ORACLE_NULLS, PDO::NULL_EMPTY_STRING); $stt = $pdo->prepare('INSERT INTO test(name, comment, date) VALUES(:name, :comment, :date)'); $stt->bindValue(':name', $name); $stt->bindValue(':comment', $comment); $stt->bindValue(':date', $date); $stt->execute(); } catch (PDOException $e) { $message = "<p>接続エラー: " . $e->getMessage() . "</p>"; die(); } finally { $pdo = null; } }ここではユーザーから受け取った名前と本文のデータをデータベースに登録しています。名前や本文が入力されているかのチェックや文字数制限のチェックをクリアすると、日付のデータと共に名前と本文がデータベースに記録されます。
$pdo = getDB(); $pdo->setAttribute(PDO::ATTR_ORACLE_NULLS, PDO::NULL_EMPTY_STRING); $stt = $pdo->prepare('INSERT INTO test(name, comment, date) VALUES(:name, :comment, :date)'); $stt->bindValue(':name', $name); $stt->bindValue(':comment', $comment); $stt->bindValue(':date', $date); $stt->execute();・getDB()はPDOオブジェクトを利用してMySQLに接続しています。DBManager.phpファイル内に記述がまとめてあります。PHPでは「mysql_connect()」といったMySQLに接続するための専用の組み込み関数がありますが、PHP5.5.0で非推奨になった後、PHP7.0.0以降では削除されました。したがって、PDOオブジェクトを利用するのが一般的です。
・「$pdo->setAttribute(PDO::ATTR_ORACLE_NULLS, PDO::NULL_EMPTY_STRING);」では空文字列をNULLに変換しています。
・SQL文を実行する方法として、PDO::query,PDO::exec,PDO::prepareを利用する場合の3つがありますが、それぞれ役割があります。ユーザー入力を伴わないクエリに対しては、単純にPDO::queryを実行します。返り値はPDOStatementです。
例$stt = $pdo->query( "SELECT * FROM users" );ユーザー入力を伴わないクエリで、INSERTやUPDATE文を利用した際の件数を直接返り値として欲しい場合や、特に結果を必要としない場合では、PDO::execを利用します。
例$stt = $pdo->exec( "DELETE FROM fruit" );最後にユーザーから入力を受け取ってSQL文を実行する場合には、PDO::prepareを利用します。大まかにはprepareでクエリ文を用意し、bindValueで変数を結び付け、executeで実行するという3段階を踏みます。
「INSERT INTO test(name, comment, date) VALUES(:name, :comment, :date)」の中の「:name」や「:comment」のことをプレースホルダと呼び、ユーザー入力した値を当てはめる場所としてあらかじめ確保しておくものになります。bindValueでプレースホルダと変数を結び付けています。
プレースホルダには「名前なしプレースホルダ」と「名前付きプレースホルダ」の2種類があります。今回利用した「:name」や「:comment」は名前付きプレースホルダです。疑問符プレースホルダと名前付きプレースホルダは混在させて利用してはいけません。以下に名前なしプレースホルダで記述した場合の例を提示します。名前なしプレースホルダで記述した場合$stt = $pdo->prepare( 'INSERT INTO test(name, comment, date) VALUES( ?, ?, ? )' ); $stt->bindValue( 1, $name ); $stt->bindValue( 2, $comment ); $stt->bindValue( 3, $date ); $stt->execute();?は1番目から順番に1,2,3・・・と対応します。
データベースからコメントを読み込む
//コメントを格納する変数 $res = array(); try { $pdo = getDB(); $stt = $pdo->query( 'SELECT name, comment, date FROM test' ); //内容を出力 $i = 1; while ( $row = $stt->fetch(PDO::FETCH_ASSOC) ) { $res[$i] = "<p>{$i}:{$row["name"]} {$row["date"]}<br>{$row["comment"]}</p>"; $i++; } } catch (PDOException $e) { $message = "<p>接続エラー: " . $e->getMessage() . "</p>"; die(); } finally { $pdo = null; } ?>この部分の処理でデータベースからコメントを取得し、出力する準備を行います。
・「\$row = \$stt->fetch(PDO::FETCH_ASSOC)」 fetchメソッドで検索結果に該当するデータを1行ずつ取得します。引数にPDO::FETCH_ASSOCを指定することで、結果セットに返された際のカラム名で添字を付けた配列を返します。これで変数$rowはそれぞれid,name,comment,dateをキーに持つ配列になりました。
$res[$i] = "<p>{$i}:{$row["name"]} {$row["date"]}<br>{$row["comment"]}</p>";あらかじめ用意した配列\$resに、データベースから取得した投稿の情報を整形して、格納していきます。最後にview.phpでforeach文で取り出しています。
改善点などアドバイスありましたら、是非コメント欄の方へ投稿していただけると幸いです。