20200421のPHPに関する記事は17件です。

【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.php
add_theme_support( 'disable-custom-colors' );

またインラインスタイルで色が指定されるため、次項に述べるような一括での色調整が難しくなります。

カラーパレットを変更する

デフォルトでは12色用意されていますが、ベースとなるテーマのカラーに合わせて、パレットのバリエーションを変更します。
デザインに疎いためどのような配色が最適かはっきり言えませんが、テーマで使われている色のうち、

  • メインカラー
  • サブカラー
  • フォントカラー
  • アクセントカラー

あたりをピックアップして組み合わせれば良いと思います。

Qiitaを例にして、実際のコードを記載します。

functions.php
add_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',
        ),
    )
);

アクセントカラーは、以下のサイトからピックアップしました。
ベースカラーを指定するだけで、いい感じのカラーバリエーションを生成してくれます。

https://palx.jxnblk.com/

ここまでの設定で、サイドバーの色設定は以下のようになります。

カラーピッカー.png

ちょっと地味過ぎましたね。。

なお、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にredyellowなどの色名ではなく、mainsubといったclass名としたのには理由があります。

例えば、メインカラーをredからblueにしたいとなった場合。
メインカラーのセレクタを.has-red-colorとしていた場合、「class名にredが含まれてるのに、なぜか表示される色が青」と混乱が起こってしまいます。

色設定自体を無効にする

実際の案件で発生する事はあまりないと思いますが、色設定自体を無効にする事もできます。
インライン文字色、ブロック文字色、ブロック背景色すべてが無効になります。

functions.php
add_theme_support( 'editor-color-palette' );
add_theme_support( 'disable-custom-colors' );

グラデーション設定を無効化する

WordPress5.4では、ボタンブロックとカバーブロックでグラデーションを利用出来るようになり、カラーピッカーに加えてタイプ(円形/線形)角度を調整する事が出来ます。

ここも「色設定のカラーピッカーを非表示にする」と同様の理由で、カスタム設定を無効化します。

functions.php
add_theme_support( 'disable-custom-gradients' );

グラデーションのカラーパレットを変更する

配色の決め方は「色設定のカラーパレットを変更する」に準ずるとして、コードの一例を記載します。

functions.php
add_theme_support(
    'editor-gradient-presets',
    array(
        array(
        'name'     => 'メイングラデーション',
        'gradient' => 'linear-gradient(to right, #55c500, #3f9200)',
        'slug'     => 'main',
        ),
    )
);

グラデーションを無効化する

グラデーション自体使わないという場合は、無効にすることも出来ます。

functions.php
add_theme_support( 'disable-custom-gradients' );
add_theme_support( 'editor-gradient-presets' );

無効にした場合、デフォルトの状態からこのように変わります。
グラデーション.png

すっきりしましたね!

テキスト設定編

テキスト設定でフォントサイズの数値指定を無効化する

プリセットサイズとして用意されているバリエーションに加え、数値入力(px)でフォントサイズを変更出来ます。

これも、「色設定のカラーピッカーを非表示にする」「グラデーション設定を無効化する」と同様の理由で無効化します。

またpx指定なので、非レスポンシブとなってしまいます。

functions.php
add_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-sizehas-larger-font-sizeというclass名も定義されていますが、現在は使われていないようです。
gutenberg/style.scss at c19d2d908cba695960cf8407bd0b0afc181aa657 · WordPress/gutenberg · GitHub]

前述の通り、フォントサイズはpx指定のため、例えばPC/スマホでベースのフォントサイズが変わった場合にバランスが崩れてしまいます。
そのため、emまたはremでのフォントサイズ指定に変更するか、テーマにあわせてメディアクエリ等で調整します。

上記セレクタをそのまま使っても良いのですが、バリエーションを変えたい場合は以下のように記述します。

functions.php
add_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;
}

ここまでの設定で、サイドバーのテキスト設定は以下のようになります。

テキスト設定.png

エディタ編

以下項目は、全てCSSで調整を行いますが、エディタ側に独自のCSSを適用する方法は3種類あります。

  1. add_theme_support( 'editor-styles' )add_editor_style( 'editor-style.css' )で読み込む
    →エディタのみに適用される。また、各セレクターの直前にブロックエディタ全体を囲っている .editor-styles-wrapper というクラスが自動的に挿入される。
  2. アクションフック(enqueue_block_editor_assets)で読み込む
    →エディタのみに適用される。
  3. アクションフック(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閉じタグ直前にインラインで出力されるため、優先度の関係で適用されないプロパティがあります。

エディタ.png

そのため、既に定義されているプロパティについては.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;
        }
    }
}

ブロックの間隔をあける

特にテキストメインのコンテンツの場合に顕著ですが、あるブロックにフォーカスした時に、前(上)のブロックが一部隠れて文字が見えない、という状態が起こります。
ブロックの間隔をあける1.png

テキストメインの記事の場合、たとえ一文でも隠れるのは編集時にストレスになる思うので、ブロック間の間隔を広げます。
ここでは、デフォルトの28pxから56pxに広げます。

block-editor-style.scss
.editor-styles-wrapper {

    .block-editor-block-list__block {
        margin-top: 56px;
        margin-bottom: 56px;
    }
}

ブロックの間隔をあける2.png

ただし、ブロックの間隔がフロント側と違う事になるケースが多いので、記事執筆の快適さを優先するのか、フロント・エディタの見た目の整合性を優先するのか、クライアントさんの意向も含めて検討すべきです。

ブロックの幅を広げる

デフォルトではブロックの幅が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パターン。

  1. ブロックマネージャーを使う
    →エディタから設定出来るので一番手軽ですが、設定箇所を知っている人なら再表示出来てしまいます。
  2. プラグインを使う
    Disable Gutenberg Blocksという専用のプラグインがありますが、最近のメジャーリリースでテストされていません。 もしくは、カスタムブロック用のプラグインに機能として内包されているものもあります。(Advanced Gutenbergなど)
  3. functions.phpに記述する
    →ブロックを無効化するのではなく、ゼロから使うブロックを登録していく方法です。 投稿タイプ別に使うブロックを出しわける事が出来るので、例えば「お知らせはシンプルに投稿したい」といったクライアントさんにはお勧めの方法です。

以下は、3番の方法で投稿の場合のみ使えるブロックを見出し・段落に制限する例です。

functions.php
function 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.php
function 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.js
wp.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: "スペース";
    }
}

スペーサーブロック.png

区切りブロックをクリック出来ない!

区切りブロックを使う場合は深刻な問題だと思っているのですが、区切り線の高さが2pxしかないため、ブロックを選択状態にすることが困難です。

そこで、疑似的にクリック領域を広げつつ、cursor: pointerでクリック出来る領域である事を知らせます。

block-editor-style.scss
.wp-block-separator {
    padding: 10px 0;
    cursor: pointer;
}

区切りブロック1.png

あとがき

メジャーなものからマイナーなものまでありますが、案件によって正解は様々だと思います。

せっかくブロックエディタを導入したのに、クライアントさんに「何だか使いにくいなあ。。」と思われないよう、色々工夫していきたい所です。

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

学習日記 #3

OAuthの概念

「認可情報の委譲」のための仕様である。
・予め信頼関係を構築したサービス間で、ユーザーの同意のもと、セキュアにユーザーの権限を受け渡しする。

final修飾子

オーバライドを明示的に禁止することができる。

ボリモーフィズム(Polymorphism)

多様性と訳され、日本語でも抽象的である。要約すると、「同名のメソッドで異なる挙動を実現する」ことである。

トレイト(Trait)

トレイトとは、再利用可能なコード(メソッド、プロパティ)をまとめて切り出す仕組みである。「断片的なクラス」

trait トレイト名 {
 ....プロパティ/メソッドの定義....
}

traitブロックの構文は以下の制約がある。
・定数は持てない
・クラスの継承、インターフェイスの実装は出来ない

型を継承するインターフェイスと、実装を継承するトレイトの違いをきちんと理解する。

現在のクラス/親クラスと衝突した場合、
1、現在のクラスのメンバ
2、トレイトのメンバ
3、親クラスのメンバ

その他

*as句を利用することで、メソッドのアクセス権限を変更することが可能である。

オブジェクトをコピーしたい場合には参照渡しがデフォルトである。

オブジェクトの比較

== : 同じクラスのインスタンスであること、同じプロパティと値をもつこと。
=== : 同じクラスの同じインスタンスを参照すること。

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

Laravelを知らない中級(中年)プログラマーがマイグレーションファイルの仕組みを調べてみたけど全くわからない!その4:「Larabelアプリケーションの初期化の流れ」

INDEX

Laravelを知らない中級(中年)プログラマーがマイグレーションファイルの仕組みを調べてみたけど全くわからない!

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()が何をやっているのか、現段階ではまだ良くわかりません。おそらく後々追うことになるでしょう。今追っているconfigpath.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\Kernelcomposer/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::$repositorynullでない場合はそれを返し、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なので、$valueAPP_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()が呼び出されます。このメソッドはbasePathbootstrapディレクトリを追加して引数の文字列を連結したものを返します。結果、PROJECT_ROOT/bootstrap/cache/config.phpという文字列が返されます。APP_CONFIG_CACHEが存在していた場合は、Str::startsWith($env, '/')が判定され文字列加工をします。startsWithJavaなどで使われる関数でPHP関数にないものを独自に定義したもののようです。文字列が引数で指定された文字列で始まるかを判定してtruefalseを返します。LoadConfiguration::bootstrap()にそのパスが返され、file_existsでそのファイルが存在するか判定され、存在した場合はそのキャッシュファイルを読み込み、読み込みフラグ$loadedFromCachetrueがセットされます。存在していた場合は読み込んだキャッシュを、存在していない場合は空の配列をアプリケーションコンテナに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が代入されます。次にforeachFinderクラスで色々したものを回しています。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->modeIterator\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の正体が明確になりました。

次回

初期化の流れを読んでみてなんとなく、ふんわりやってることがわかってきましたが、まだオブジェクトの生成やイベント周りなどはっきりとつかめていない部分がありますね。徐々に理解していけると良いですが、どうなるでしょうか。次回はこの流れでリポジトリの読込を探っていきたいと思います。

続く☆

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

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に入っている値が同じかどうかを調べる。同じならば全ての送られてきた値が、正常の送信と判断する。

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

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に入っている値が同じかどうかを調べる。同じならば全ての送られてきた値が、正常の送信と判断する。

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

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ディレクトリがダウンロードされます。

スクリーンショット 2020-04-21 17.50.52.png

QRコード生成

今回は、ビューのヘルパーを使って生成する関数を定義しました。関数はコントローラでもモデルで定義しても問題ありません。

src/View/Helper/CommonHelper.php
public 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-21 18.21.48.png

参考

5分で出来る!PHPでQRコードを生成する方法

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

俺のLaravelがこんなに遅いわけがない

Laravel

環境(ベース)

  • PHP 7.4.5
  • Laravel 7.5.1

Docker for Macでnginxとphp-fpmコンテナをunixソケットで繋いだ環境で試してます。

環境の差異

  1. OPcache なし
  2. OPcache あり、プリロードなし
  3. 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-data

https://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のポテンシャル半端ないですね!!!

プリロード自体は初めてなので諸々問題出てくるかもしれませんが、何か問題あったらまた記事にしていきたいと思います?

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

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/composer

Composerのバージョンを表示させ、正常にインストールされたかを確認する。

[UserName: bin]$ composer --version
Composer version 1.10.5 2020-04-10 11:44:22
[UserName: bin]$
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.php
use 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はかなり高機能なロギングのライブラリなので、
ハンドラーに持たせるフォーマッターで出力形式もいろいろなものが選択でき、プロセッサーでログレコードに追加などの操作もできる。
使いこなすことができれば、かなり便利そうだなという印象でした。

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

NASのバックアップ対象ディレクトリをinotifyで監視して更新されたディレクトリのみをrsyncでミラーリング

前回こちらで書いたミラーリングと世代管理バックアップの2系統のスクリプトのうち、ミラーリングのほうのスクリプトについてパフォーマンスアップの修正を行ないました。

前回のスクリプトでは大まかな流れとしてduコマンドの結果を元に更新対象ディレクトリを絞り込んでrsyncに渡すという動作を行なっていましたが、監視対象内のサブディレクトリ数が多くなるとduコマンドの実行時間もそれなりにかかるようになるので、この部分をinotifywaitコマンドでの監視に置き換えています。

inotifywaitを使えるよう準備

inotify-toolsがインストールされていない場合inotifywaitコマンドも使えませんので、その場合はまずinotify-toolsをインストールします。

inotify-toolsをインストール(Debian系の場合)

$ sudo apt install inotify-tools

inotifywaitで監視可能な対象数はデフォルトで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を導入したほうが楽かもしれません。

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

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変数などもあるので、勉強しときます。

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

[nginx]https接続のnginxでPHPを動かす[PHP]

nginxでPHPを動かす

httpではPHPが動くのにhttpsで動かない状態を改善する方法をメモとして残します。

nginx/conf.d/default.conf設定に以下を追加します。
rootフォルダは任意に変更してください。

default.conf
server {
        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;
        }
}

以上

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

【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導入する方が、少しでも参考にしてくれれば嬉しい。

今度はレポートデータではなくキーワードの更新などに取り掛かる予定。うまくいくといいなー

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

PHPインストールしたら必須!パス+version確認+php.iniの初期設定をしよう!

PHPをインストールしよう

php5.6を公式サイトからインストール
コメント.png
ちなみに公式サイトでのインストールは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=opensslextension=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=fileinfoextension=fileinfo

すると、composer installが正常に実行されてphp artisan -vでlaravelのバージョンも確認できました!

補足

php artisan -vを行う時はlaravelプロジェクトに移動してから!
例:C:\Users\ユーザー名\Desktop\app>cd
C:\Users\ユーザー名\Desktop\app

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

PHP7.4 ぼくのかんがえたさいきょうのphp.ini

ストーリー

PHPをインストールしたら必ず行う php.ini の設定ですが、
ネット上ではPHP5系の情報がたくさん出回っており、非推奨または削除された設定例が数多く困り果てていました。

良い感じにまとめてくれてるサイトが見つからなかったので、最強でベストプラクティスな php.ini 推奨設定を考えました。
異論は受け付けますので、ぜひコメントください。

参考設定

PHPでは、開発用と本番用の設定例を用意してくれています。
なんと素晴らしいことなんでしょうか。これをベースに設定します。

予め以前の記事で設定の差分を調べておきましたので、よかったらご覧ください。

環境

  • PHP 7.4.5 (執筆時のバージョンです。)

※バージョンが異なる場合は公式サイトで有効な設定か確認してください。

開発用 php.ini

php.ini
zend.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.ini
zend.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

http://php.net/display-errors

  • 開発 は on、本番 は off
  • エラーをHTML出力の一部として画面に出力するかどうかを定義します。
  • セキュリティ上、本番では off 推奨

display_startup_errors

http://php.net/display-startup-errors

  • 開発 は on、本番 は off
  • display_errorson にした場合でも、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.php

opcache.preload_user

その他

論理値

設定で使用される論理値(true, false, on, off, yes, no)は大文字・小文字は区別しないようなので、True, On等でも認識されます。
とても柔軟で素敵だと思いました???

私のphp.iniはどこ?

ここです。

$ php -i | grep php.ini

環境変数を使いたい

普通に環境変数読み込めます。

php.ini
date.timezone = $TZ

さいきょうのツール爆誕!

お手元の環境と本記事の推奨設定を照らし合わせて、差分を表示する神ツールを @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

参考

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

学習日記 #2

インターフェイス

PHPでは多重継承が認められていないため、一度に継承できるクラスは常に1つである。複数のクラスを同時に継承することは出来ない。そこで利用するのがインターフェイスである。

<?php
interface インターフェイス名 {
  抽象メソッド、定数の定義
}

インターフェイスはクラスに似ているが、構文の違いがある。
・中身をもつメソッドやプロパティは定義出来ない(配置できるのは、抽象メソッドと定数だけである。)
・配下のメソッドが抽象メソッドであることは明らかなので、abstract修飾子は必要ないし、指定してはいけない。
・アクセス修飾子も指定できない。public修飾子を指定しても構わないが、意味がないので通常は割愛する。
・インターフェイスであることがわかるように、IFigure,FigureInterFaceのように接頭辞・接尾辞を指定する。

インターフェイスの機能を継承することを実装するという。また、実装したクラスのことを実装クラスと呼ぶ。

インターフェイスの実装

class 実装クラス名 implements インターフェイス名, {
   // クラスの本体
}

無名クラス

無名クラスは、名前を持たないクラスである。名前がないため、特定の文の中でしか利用することができない。
しかし、以下のようなメリットがある。
・コードをシンプルに表現することが出来て、見た目に関連性を把握しやすい。
・式が許されている場所であれば、どこにでも記述できる。
・名前がないので、そもそも競合する恐れがない。

その性質から、定義したクラスを後から利用しないことがわかっているクラスを定義する際に利用する。

new class {....プロパティ/メソッドの定義...}
new class extends 親クラス名 { ....プロパティ/メソッドの定義.... }
new class implements インターフェイス名 { ....プロパティ/メソッドの定義... }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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にて記事を随時更新していきます。

機能

・ハンドルネームと本文を入力して送信すると内容がその下の部分にどんどん追加されていきます。
・それと同時に送信した時刻が記録されていきます。
・ハンドルネームを入力せずに本文を送信すると、ハンドルネームが自動的に「名無し」になります。
・本文を入力せずに送信すると、「本文を入力してください」という警告が出ます。

データベース

 まずデータベースを用意します。bbsという名前のデータベースを作成し、その中にtestというテーブルを作成しました。いい名前を思い付かなかったのでとりあえずtestと名付けました。このtestテーブルに「id」「name」「comment」「date」の4つのカラムを作ります。

bbs.txt
CREATE 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文で取り出しています。

改善点などアドバイスありましたら、是非コメント欄の方へ投稿していただけると幸いです。

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