カテゴリーアーカイブ PHP

著者:杉浦

【Laravel】LaravelのソースコードでよくあるArrayAccessを実装するClass設計

 Laravelのソースコード内を

implements[^\n]+ArrayAccess

という正規表現でググるとArrayAccessを満たすClassを軒並み見つけられます。この記事ではこれらのClassで使われているArrayAccessの実装方法とその設計について述べます。
 ArrayAccessというのはPHPの組み込みインタフェースの一つです。
PHP: ArrayAccess – Manual
 このインタフェースを満たしたClassのインスタンスは配列として値を呼び出せるようになります。例えば次です。

<?php
class obj implements ArrayAccess {
    private $container = array();

    public function __construct() {
        $this->container = array(
            "one"   => 1,
            "two"   => 2,
            "three" => 3,
        );
    }

    public function offsetSet($offset, $value) {
        if (is_null($offset)) {
            $this->container[] = $value;
        } else {
            $this->container[$offset] = $value;
        }
    }

    public function offsetExists($offset) {
        return isset($this->container[$offset]);
    }

    public function offsetUnset($offset) {
        unset($this->container[$offset]);
    }

    public function offsetGet($offset) {
        return isset($this->container[$offset]) ? $this->container[$offset] : null;
    }
}

$obj = new obj;

var_dump(isset($obj["two"]));
var_dump($obj["two"]);
unset($obj["two"]);
var_dump(isset($obj["two"]));
$obj["two"] = "A value";
var_dump($obj["two"]);
$obj[] = 'Append 1';
$obj[] = 'Append 2';
$obj[] = 'Append 3';
print_r($obj);

 配列として呼び出し、代入、isset()、empty()、unset()が使えるようになります。注意するのは配列となっているわけではないことです。上のコードの続きで

array_pop($obj);

などとした場合、引数がArray型でないと怒られ、PHPが致命的なエラーを起こします。

 Laravelのソースコード中のいくつかのClassではこのArrayAccessを実装しています。例えばデータベース定義を行うクラスであるFluentでは次の様になっています。

    /**
     * Determine if the given offset exists.
     *
     * @param  string  $offset
     * @return bool
     */
    public function offsetExists($offset)
    {
        return isset($this->attributes[$offset]);
    }

    /**
     * Get the value for a given offset.
     *
     * @param  string  $offset
     * @return mixed
     */
    public function offsetGet($offset)
    {
        return $this->get($offset);
    }

    /**
     * Get an attribute from the fluent instance.
     *
     * @param  string  $key
     * @param  mixed   $default
     * @return mixed
     */
    public function get($key, $default = null)
    {
        if (array_key_exists($key, $this->attributes)) {
            return $this->attributes[$key];
        }

        return value($default);
    }

    /**
     * Set the value at the given offset.
     *
     * @param  string  $offset
     * @param  mixed   $value
     * @return void
     */
    public function offsetSet($offset, $value)
    {
        $this->attributes[$offset] = $value;
    }

    /**
     * Unset the value at the given offset.
     *
     * @param  string  $offset
     * @return void
     */
    public function offsetUnset($offset)
    {
        unset($this->attributes[$offset]);
    }

 いずれもattributesへのアクセス方法です。attributesにはカラムの定義が羅列されます。
 ブラウザに返すためのwebページを定義するクラスViewでは次の様になっています。

    /**
     * Determine if a piece of data is bound.
     *
     * @param  string  $key
     * @return bool
     */
    public function offsetExists($key)
    {
        return array_key_exists($key, $this->data);
    }

    /**
     * Get a piece of bound data to the view.
     *
     * @param  string  $key
     * @return mixed
     */
    public function offsetGet($key)
    {
        return $this->data[$key];
    }

    /**
     * Set a piece of data on the view.
     *
     * @param  string  $key
     * @param  mixed   $value
     * @return void
     */
    public function offsetSet($key, $value)
    {
        $this->with($key, $value);
    }

    /**
     * Add a piece of data to the view.
     *
     * @param  string|array  $key
     * @param  mixed   $value
     * @return $this
     */
    public function with($key, $value = null)
    {
        if (is_array($key)) {
            $this->data = array_merge($this->data, $key);
        } else {
            $this->data[$key] = $value;
        }

        return $this;
    }

    /**
     * Unset a piece of data from the view.
     *
     * @param  string  $key
     * @return void
     */
    public function offsetUnset($key)
    {
        unset($this->data[$key]);
    }

 いずれもdataへのアクセス方法です。dataにはHTMLコードのテンプレートファイルに渡す変数が羅列されます。
 ここで挙げたのは二例ですが、Collection、Modelなど他にも同様の実装はあります。この実装の方針は、Classの中に主目的のデータらを格納した配列を持ち、その配列を直接扱う方法の一つとしてArrayAccessを実装、というものです。このため次の様にClassを作るとLaravel内のコードと違和感なく扱えるままClassを拡張していくことができます。

class MyModel implements ArrayAccess
{
    /** @var array 主データ */
    public $data;

    //
    // $dataを操る色々
    //

    /**
     * {@inheritdoc}
     * @param  mixed $offset
     * @return bool
     */
    public function offsetExists($offset)
    {
        // この実装は疑問。LaravelではoffsetExistsの返り値がarray_key_existsの返り値であることがしばしばある。
        // しかしArrayAccessの実装によってisset($this[$offset])とした時、$this->offsetExists($offset)が評価される。
        // Laravelに従った場合、$this->data[$offset]=nullの時にisset($this->data[$offset])===false、isset($this[$offset])===trueとなる。
        // この実装の場合、isset($this->data[$offset])===isset($this[$offset])が担保できる。
        return isset($this->data[$offset]);
    }

    /**
     * {@inheritdoc}
     * @param  mixed $offset
     * @return mixed
     */
    public function offsetGet($offset)
    {
        return $this->data[$offset];
    }

    /**
     * {@inheritdoc}
     * @param  mixed $offset
     * @param  mixed $value
     * @return void
     */
    public function offsetSet($offset, $value)
    {
        return $this->data[$offset] = $value;
    }

    /**
     * {@inheritdoc}
     * @param  mixed $offset
     * @return void
     */
    public function offsetUnset($offset)
    {
        unset($this->data[$offset]);
    }
}

 このやり方はリレーショナルデータベース以外のデータリソースを参照するModelを作る時などで割と便利です。EloquentをそのままExtendすると不意にクエリビルダが走る事故が起きますが、この書き方なら起きません。また適切に拡張した場合、Controller等のModelを外部から参照する際に他のEloquentModelと記述上の区別なしに扱うことができます。

  • この記事いいね! (1)
takahashi 著者:takahashi

fuelPHP + PDO_sqlsrvでストアドプロシージャ実行時にIMSSPエラーが出てきたときの対処法

FuelPHP上でMSSQLサーバーにあるストアドプロシージャを実行しようとしたところ、こんなエラーが発生。

PDOException [ IMSSP ]:
SQLSTATE[IMSSP]: The active result for the query contains no fields.
エラー

見慣れないエラーコードだったので少し戸惑いましたが、調べてみたところ、解決策が見つかりました。

“The active result contains no fields” using PDO with MS SQL – Stack Overflow

このエラーが出た場合、実行するSQL文の前に

SET NOCOUNT ON;

を実行しておくとエラーを回避できるようです。

このオプションを追加したところ、自分の環境でもエラーが出なくなりました。

同じエラーでお困りの方は、是非試してみてください。

  • この記事いいね! (0)
著者:杉浦

【Laravel】Bladeで直書きJavaScriptのコンポーネントを作る時の勧め

BladeはシンプルながらパワフルなLaravelのテンプレートエンジンです。
Bladeテンプレート 5.7 Laravel

 BladeはLaravelで用いられるテンプレートエンジンであり、PHPの構文を書きやすく、読みやすい形でHTMLに埋め込みます。例えば

<?php if(auth()->guard('admin')->check()): ?>
  <div class="alert alert-danger">
    <?php echo e($slot); ?>

  </div>
<?php endif; ?>

ならばBladeでは

@auth('admin')
<div class="alert alert-danger">
    {{ $slot }}
</div>
@endauth

と記述できます。
 Bladeはコンポーネント指向――多数の部品を組み合わせることで完成品を作る考え――の元にwebページを作ることが出来ます。例えば、

@section('link_button')
    <a class="btn btn-primary font-weight-bold text-white" href="{{$link}}">
        {{$text}}
        <i class="fa fa-angle-right fa-lg right -pull-right"></i>
    </a>
@overwrite
@yield('link_button')

と定義すると

@include('link_button',['text'=>'hoge','link'=>'/hoge'])
@include('link_button',['text'=>'fuga','link'=>'/fuga'])
@include('link_button',['text'=>'foo','link'=>'/foo'])

と呼び出した時に/hoge, /fuga, /fooそれぞれに飛んでいくボタンの見た目のリンクが作られます。この仕組みはJavaScriptに応用できます。
 やり方はscriptタグを書いたコンポートを定義するだけです。例えば、

@section('alert_by_javascript')
<script>
    if (document.readyState !== 'loading') {
      alertHoge()
    } else {
      document.addEventListener('DOMContentLoaded', alertHoge);
    }
    function alertHoge(){
      alert({{$hoge}});
    }
</script>
@overwrite
@yield('alert_by_javascript')

と定義して

@include('alert_by_javascript',['hoge'=>'hoge'])
@include('alert_by_javascript',['hoge'=>'fuga'])
@include('alert_by_javascript',['hoge'=>'foo'])

 と呼び出すと、hoge, fuga, fooと文字列を表示するalertが発生します。パッと使うだけならこの通り簡単なのですが、注意点は素のJavaScriptを書いている点で問題点が発生します。深刻なのはグローバル汚染です。全て同じ階層にあるため他コンポーネントと変数名、関数名が被った時に容易にバグります。
 解決方法の一つは常に即時関数で囲みスコープを作ることです。こうするとコンポーネントの外に漏れることはありません。しかし、外部で使いたい時もあります。グローバル汚染を防ぎながら外部でJavaScriptを適宜呼び出せるようにする方法が次です。

@section('auto_redirect_to_top_script')
  @php
    // グローバル汚染回避のためにランダムな文字列か指定された文字列を名前に持つ関数を作る
    $function_name = $function_name ?? str_random();
  @endphp
  <script type="text/javascript">
    if (document.readyState !== 'loading') {
      {{$function_name}}();
    } else {
      document.addEventListener('DOMContentLoaded', {{$function_name}});
    }
    function {{$function_name}}(){
      alert({{$hoge}});
    }
  </script>
@overwrite
@yield('alert_by_javascript')

 関数の名前を呼び出す際に関数名を決めます。こうするとスコープは閉じたままで、外から任意に呼び出せます。

@include('alert_by_javascript',['hoge'=>'hoge','function_name' => 'hoge_function'])// hoge_function()が走る
@include('alert_by_javascript',['hoge'=>'fuga','function_name' => 'fuga_function'])// hoge_function()が走る
@include('alert_by_javascript',['hoge'=>'foo'])// `${ランダムな文字列}`()が走る

 この手法の注意点はやはり素のJavaScriptを書いている点です。古いIEを代表とするいくらかのブラウザは近年のJavaScriptの記述を読み取れず、実行できません。このコンポーネントに書かれているJavaScriptはほぼそのままwebページに表示されます。なんとなくで記述を行うといくらかのブラウザではJavaScriptを実行してくれません。他にも圧縮、難読化、コメントなど素のJavaScriptそのままならではの問題点が出てきます。

  • この記事いいね! (1)
著者:杉浦

【PHP】traitを用いたinterfaceに対するテストの作り方

 interfaceは実装を定義する記法です。interfaceはimplementsによって任意の数指定され、classはimplements以下で指定したinterface全ての実装が必須になります。

interface A{
  public function hoge();
  public function fuga();
}

interface B{
  public function foo();
  public function bar();
}

class C implemnts A,B{
  public function hoge(){
    // Cに関するhoge()の具体的な処理
  }
  
  public function fuga(){
    // Cに関するfuga()の具体的な処理
  }
  
  public function foo(){
    // Cに関するfoo()の具体的な処理
  }
  
  public function bar(){
    // Cに関するbar()の具体的な処理
  }
}

class D implemnts A{
  public function hoge(){
    // Dに関するhoge()の具体的な処理
  }
  
  public function fuga(){
    // Dに関するfuga()の具体的な処理
  }
}

PHP: オブジェクト インターフェイス – Manual
 interfaceで実装を強要される関数はinterafaceの時点で何かしら達成する目的を持ちます。もし目的を持たないならば、その関数それぞれの中身は乱雑なものになり、インタフェースによってコードの設計を整理することはできないでしょう。
 interfaceで定義されたメソッドは実装は異なれど共通の目的を持ちます。そのため、その目的が達成できているか否かのテストが欲しくなります。このテストは同じinterfaceを持つクラスならば共通化できます。traitを用いて共通化を実現します。
 traitはコードを再利用するための仕組みです。
PHP: トレイト – Manual
 traitの再利用の仕組みの特徴は複数のtraitを一つのclassで使えることです。is-a関係に従わない場合、混乱の元になる継承との最大の違いがこの特徴です。

class TestC extend TestBase{
  use TraitHoge,TraitFuga;
}

 interfaceに関するテストをtraitにまとめることでコードの整理をします。ある一つのinterfaceを実装したクラスをテストするならば、その一つのinterfaceに対応する一つのtraitを呼び出す、という具合です。

trait TestATrait{
  function testHoge(){
    // hoge()共通のテスト
  }

  function testFuga(){
    // fuga()共通のテスト
  }
}

class TestC extend TestBase{
  use TestATrait,TestBTrait;
}

class TestD extend TestBase{
  use TestATrait;
}

 interfaceの呼ばれ方はクラスによってまちまちであり、それら全てをテストする際、継承や参照を用いると謎のクラスや妙な場所を通るといったことが起きやすいです。テストをコピペをするとコードの変更をテストに反映する際の労力が大きくなります。interfaceとtraitは1対1の関係で作ることが容易であり、セットで用いることによってコードをきれいに保てます。

  • この記事いいね! (0)
著者:杉浦

【PHP】コードの複雑さを調べるPHP静的解析ツールPhpMetricsの紹介

 ソースコードを静的に(実行せずに)評価する際には様々な指標があります。大体、条件文の多さ、行数、クラスの数、依存関係、抽象度、意味のあるコメントの量あたりを元にして色々計算するのですが、それらすべてを把握するのは困難です。PhpMetricsはそういった指標を計算してレポートを出力してくれる静的解析ツールです。
PhpMetrics
 Composerで提供されているのでインストールは

composer global require phpmetrics/phpmetrics

で十分です。使い方もお手軽です。例えばHTML形式のレポートが欲しいなら

phpmetrics --report-html=[htmlレポートの出力先] --exclude=[調査しないディレクトリA],[調査しないディレクトリB] [調査対象のソースコードのディレクトリ] 

 これだけです。フレームワークやライブラリを用いている場合、[調査しないディレクトリ]を指定しないとそれらを、神クラス――機能が集中しすぎているクラス――扱いして結構な数の警告が出ます。
 実行結果のHTMLを見ると次の様な画面が出ます。

 PhpMetrics report
 とりあえず特に気にするべきなのが赤丸で囲ったViolationsとAverage cyclomatic complexity by classとMaintainability / complexityです。
 Violationsはバグりそう、読み難そうといった原因がはっきりしているコードをその理由付きでリスト化しています。ここに現れたものを解決するだけでいくらかマシになります。

 Average cyclomatic complexity by classはクラス毎の循環複雑度の平均です。この値が大きいと条件文などによる分岐が多いコードということです。分岐が多いと処理を追う際の組み合わせが膨大になり、テストも困難になってしまいます。
 Maintainability / complexityはソースコード中の各クラス、ファイルを図示したものです。色は危険度、大きさはコード量を表しています。この図が小さい緑丸で染まったコードが分かりやすいコードということです。

  • この記事いいね! (1)
著者:杉浦

【Laravel】DBに依存させずにEloquentモデルをテストする

 EloquentはLaravelに備わっているORM(Object-relational mapping)です。
Eloquent:利用の開始 5.7 Laravel
 オブジェクト関連マッピングとはオブジェクトとDB(データベース)の関連のマッピングのことであり、その都合上オブジェクトはデータベースと密接な関係にあります。

<?php
class Post extends Model
{
    // これだけでpostsテーブル中の各レコードを呼べる。
}


 この密接な関係のため、モデルをテストする際にDBが付きまといがちです。何を呼び出すにしても、DBの中を見に行きます。しかしモデルのメソッドのテストはDBから切り離されるべきです。これはモデルのテストがそれ以上掘り下げるべき最小単位のユニットテストであり、テスト失敗の原因がテストやソースコードのエラーかDBのエラーか考える手間を省くべきでもあるからです。
 次の様なコードを記述することでDBからEloquentモデルを切り離してテストできます。

<?php

namespace Tests\Unit\App\Models;

use App\Models\User;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Collection;
use Tests\TestCase;

class UserTest extends TestCase
{
    public function testScopeFindOrFailPort()
    {
        self::assertInstanceOf(User::class, UserMock::scopeFindOrFailPort(UserMock::query(), '20100'));
        try {
            UserMock::scopeFindOrFailPort(UserMock::query(), 'hoge');
            self::assertTrue(false, 'not foundでなかった');
        } catch (ModelNotFoundException $e) {
            self::assertTrue(true);
        }
    }
}

/**
 * モデルUserのモック。データベースに関わらず一定のものを返すようにしている。
 */
class UserMock extends User
{
    public $mock_data;
    public $mock_return_data;

    /**
     * {@inheritdoc}
     * UserMock constructor.
     * @param array $attributes
     */
    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);
// テストデータ定義
        $mocks = [];
        for ($i = 0; $i < 5; ++$i) {
            $mocks[] = (new User())->forceFill([
                'id' => $i,
                'group_key' => $i % 3,
                'hash' => 'hash_'.$i,
                'port' => ['1234', '12345', '20100'][$i % 2],
                'created_at' => now(),
                'updated_at' => now(),
                'deleted_at' => null,
            ]);
        }
        $this->mock_data = new Collection($mocks);
    }

// DBにアクセスするLaravel内部に隠れている部分を引っ張り出して置き換え

    /**
     * {@inheritdoc}
     * @return User|Builder|UserMock
     */
    public static function query()
    {
        return new self();
    }

    /**
     * {@inheritdoc}
     * @param  array                    $columns
     * @return User|Model|object|null
     */
    /** @noinspection PhpHierarchyChecksInspection */
    public function first($columns = [])
    {
        return $this->mock_data->first();
    }

    /**
     * {@inheritdoc}
     * @param  array|Closure|string $column
     * @param  string|null          $operator
     * @param  null                 $value
     * @param  string               $boolean
     * @return User|UserMock
     */
    /** @noinspection PhpHierarchyChecksInspection */
    public function where($column, $operator = null, $value = null, $boolean = 'and')
    {
        if (func_num_args() === 2) {
            $value = $operator;

            $operator = '=';
        }
        $this->mock_data = $this->mock_data->where($column, $operator, $value);

        return $this;
    }
}

 重要なのは、テスト対象のモデルを継承しているがテスト対象のモデル内で定義されたメソッドを変更していないクラス、をテストコードに記述することです。このUserMockクラスは、UserクラスのDBにアクセスしてデータを取ってくる部分を、テストコード中で定義する値を取ってくる様に上書きしたクラスです。テスト対象そのものを置き換えている様にも見えますが、この置き換えている部分は大過ないことが担保されているLaravel内部のテスト済みコードのModelクラスの部分です。

 テスト用クラスUserMockはテスト対象クラスUserで定義されたコードを変更せず、Userの更に奥のModelから継承したコードを変更しています。これによりUserクラスを破壊することなく、DBのアクセスの代わりにローカル変数へのアクセスを行うUserクラスであるUserMockクラスが作れます。
 このようにするとデータベースに依存することなくEloquentのテストが出来ます。こういったコードはローカルのPHPのみで完結しており、実行速度が速い、他のデータリソースを汚染しない、と便利です。ここまで小さいテストならばモデル全体を通しても10秒とかからずにテストが終わります。おかげでGitにコミットする度にローカルで走らせても苦になりません。

DBへのアクセスメソッドは大体\Illuminate\Database\Eloquent\Builder以下にそろっています。

  • この記事いいね! (0)
著者:杉浦

【Laravel】artisan view:cacheでBladeの中身を読めるようにする

 LarvelはBladeなるテンプレート言語を備えています。
Bladeテンプレート 5.7 Laravel
 便利は便利なのですが独自な上、説明もざっくばらんなので詳細に中身を把握するのは難しいです。
 artisan view:cacheはresources以下のBladeファイルのキャッシュを生成するコマンドです。このコマンドで生成されるPHPファイルはBladeをPHP言語にコンパイルした結果です。コンパイル後のファイルを読むことでBladeが何をやっているのか少しわかりやすくなります。
 例えば次のコードをコンパイルすると

@section('header')
@endsection
@yield('header')

@section('footer')
@show

@section('body')
@overwrite
@yield('body')

次の様になります。

<?php $__env->startSection('header'); ?>// @section
<?php $__env->stopSection(); ?>// @endsection
<?php echo $__env->yieldContent('header'); ?>// @yield

<?php $__env->startSection('footer'); ?>// @section
<?php echo $__env->yieldSection(); ?>// @show

<?php $__env->startSection('body'); ?>// @section
<?php $__env->stopSection(true); ?>// @overwrite
<?php echo $__env->yieldContent('body'); ?>// @yield

 こうやってPHPコードに直すとやっていることを追いかけられるようになり、中身を知ることが出来ます。
 @endsection, @yield, @show, @overwriteはそれぞれ@sectionと密に関わるディレクティブです。
Bladeテンプレート 5.7 Laravel#レイアウト定義
 @endsectionはsectionで定義を始めたコードの終点を示し、@yieldはsectionの出力を示し、@showはsectionで定義を始めたコードの終点であり即時展開を示し、overwriteは同名sectionの上書きを行うsection定義の終点を示します。これだけだとどういうこととなりますがコードを読むと少しわかります。@sectionの中身であるstartSectionの内容が次です。

    /**
     * Start injecting content into a section.
     *
     * @param  string  $section
     * @param  string|null  $content
     * @return void
     */
    public function startSection($section, $content = null)
    {
        if ($content === null) {
            if (ob_start()) {// ここが大事
                $this->sectionStack[] = $section;
            }
        } else {
            $this->extendSection($section, $content instanceof View ? $content : e($content));
        }
    }

 obstart()でこれ以降の出力(ブラウザに映る予定のHTMLコードなど)をバッファにため込みます。これは改めて命令されるまで出力されません。ついで@endsection,@overwriteの中身であるstopSection()が次です。

    /**
     * Stop injecting content into a section.
     *
     * @param  bool  $overwrite
     * @return string
     *
     * @throws \InvalidArgumentException
     */
    public function stopSection($overwrite = false)
    {
        if (empty($this->sectionStack)) {
            throw new InvalidArgumentException('Cannot end a section without first starting one.');
        }

        $last = array_pop($this->sectionStack);

        if ($overwrite) {
            $this->sections[$last] = ob_get_clean();
        } else {
            $this->extendSection($last, ob_get_clean());
        }

        return $last;
    }

 ob_get_clean()でバッファの内容を取得し、出力バッファを削除します。@overwriteはこの$overwrite=trueでstopSectionを呼び出しています。これにより問答無用でプロパティsectionsの末尾にバッファを書き込んでいます。@overwriteに引数がないのは必ず末尾を参照していたせいです。
 $overwrite=falseの時はextendSection()へ回って、もし以前に定義していればそちらを呼び出すようにしています。
 @showの中身であるyieldSection()、@yieldの中身であるyieldContent()が次です。

    /**
     * Stop injecting content into a section and return its contents.
     *
     * @return string
     */
    public function yieldSection()
    {
        if (empty($this->sectionStack)) {
            return '';
        }

        return $this->yieldContent($this->stopSection());
    }
    
    /**
     * Get the string contents of a section.
     *
     * @param  string  $section
     * @param  string  $default
     * @return string
     */
    public function yieldContent($section, $default = '')
    {
        $sectionContent = $default instanceof View ? $default : e($default);

        if (isset($this->sections[$section])) {
            $sectionContent = $this->sections[$section];
        }

        $sectionContent = str_replace('@@parent', '--parent--holder--', $sectionContent);

        return str_replace(
            '--parent--holder--', '@parent', str_replace(static::parentPlaceholder($section), '', $sectionContent)
        );
    }

 yieldSectionは短縮形の様なものです。stopSection、yieldContentの順に呼び出し、最後に宣言したsectionを閉じ、出力しています。
 yieldContentはプロパティ$this->sectionsの中から目当てのsectionを選び、整形して返しているのみです。
 @endsection, @yield, @show, @overwriteはBlade内部では糖衣構文として役割に応じた大きく違う名前を与えられていましたが、実際にはちょっとした違う引数で呼び出される同じ関数らであることがわかります。

  • この記事いいね! (0)
takahashi 著者:takahashi

LaravelをNginxで動作させる際の設定

LaravelではどのURLを指定されても、(自分の担当するパス配下へのリクエストであれば)Laravel自身を起動させるために必ずpublic/index.phpを経由させる必要があります。

この設定はPHPだけでは限界があるため、Webサーバー側の設定で対応する必要があります。

Laravelのパッケージにはこの”index.phpを必ず経由させる”という設定が書かれた設定ファイルがデフォルトで含まれています。
具体的には

public/.htaccess

上記のファイルがこの設定ファイルにあたります。
ところが、この.htaccessファイルは基本的にApacheなどのごく一部のWebサーバーでしか認識しないので、たとえは先日の記事のようにnginxにPHPをインストールした環境で動作させようとしても、そのままではうまく動いてくれません。

この場合、.htaccessに書かれている設定と同様の働きになる設定をWebサーバーの設定ファイルに指定する必要が出てきます。
ただし、その記述の仕方もApacheとは異なってきますので、各Webサーバー用の書き方に書き換えないといけません。

nginxの場合、.htaccessの中身をnginx用の設定に書き換えてくれるWebサービスがあり、まず最初にこのサービスを使って変換した設定ファイルを使って設定を行ってみました。

元の.htaccess

<IfModule mod_rewrite.c>
    <IfModule mod_negotiation.c>
        Options -MultiViews -Indexes
    </IfModule>

    RewriteEngine On

    # Handle Authorization Header
    RewriteCond %{HTTP:Authorization} .
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

    # Redirect Trailing Slashes If Not A Folder...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_URI} (.+)/$
    RewriteRule ^ %1 [L,R=301]

    # Handle Front Controller...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [L]
</IfModule>

上記サイトで変換後のnginxコンフィグ

# nginx configuration

location / {
  if (!-e $request_filename){
    rewrite ^(.*)$ /%1 redirect;
  }
  if (!-e $request_filename){
    rewrite ^(.*)$ /index.php break;
  }
}

実際に入れ込むと以下のようになります。

server {

  listen        80;
  listen        443 ssl;

  server_name  example.com;

  ssl_certificate       /path/to/ssl/fullchain.pem;
  ssl_certificate_key   /path/to/ssl/privkey.pem;

  root /path/to/docroot;
  client_max_body_size 1g;
  location / {
        if (!-e $request_filename){
                rewrite ^(.*)$ /%1 redirect;
        }
        if (!-e $request_filename){
                rewrite ^(.*)$ /index.php break;
        }      
        index index.php index.htm index.html;
  }
  location ~ \.php$ {
    if (!-e $request_filename){
        rewrite ^(.*)$ /%1 redirect;
    }
    if (!-e $request_filename){
        rewrite ^(.*)$ /index.php break;
    }
    fastcgi_pass   unix:/var/run/php-fpm.sock;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    include        fastcgi_params;
    allow all;
  }
}

ところが、上記のような設定を行ったところ、リダイレクトループに陥ってしまいます。

いろいろ調べたところ、Nginx向けのLaravel用の設定が公開されていました。

nginxをLaravel5.4用に設定する – Qiita

上記の記事を基に今回の場合の設定ファイルを書き換えてみます。

server{
   listen        80;
   listen        443 ssl;

   server_name  example.com;

   ssl_certificate       /path/to/ssl/fullchain.pem;
   ssl_certificate_key   /path/to/ssl/privkey.pem;

   root /path/to/docroot;

   location / {     
        index  index.php index.html index.htm;
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass   unix:/var/run/php-fpm.sock;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
}

全然違うじゃん…

やはり、ただ設定を変換しただけではうまく行かないみたいですね。

参考元の記事ではLaravel5.4となっていましたが、5.6の環境でも同様の設定で問題なく動作しました。

Laravel + Nginxの構築でお困りの方の参考になれば幸いです。

  • この記事いいね! (0)
takahashi 著者:takahashi

AmazonLinux 2 + Nginx でPHPを使えるようにする方法

Nginxはリバースプロキシとしてかなり優秀なサーバーアプリケーションで、自分も愛用しているのですが、あくまでリバースプロキシとしてしか使っていませんでした。

しかし、Nginxの本来の役割はWebサーバーですので当然Nginx自身がアプリの動作環境になることも可能です。

今回機会があってAWS EC2上のNginxで直接アプリケーションを動かす必要が出てきたので、実際にセットアップしてみました。

NginxにはApacheのようにモジュールを組み込んでPHPを動作させるような仕組みがない為、CGIを利用して動作できるようにします。
ただ、通常のCGI版PHPだと重いので、Fast CGI版のPHPが利用できるphp-fpmを利用します。

今回はPHP7.1の環境をインストールします。

まずは必要なパッケージをインストールします。

#epelリポジトリをインストール
sudo yum -y install epel-release

#remi リポジトリをインストール
sudo yum -y install http://rpms.famillecollet.com/enterprise/remi-release-7.rpm

#nginxをインストール
sudo yum -y install nginx

#php-fpm(php7.1)をインストール
sudo yum -y install --enablerepo=remi,remi-php71 --disablerepo=amzn2-core  php-fpm
#※--disablerepo=amzn2-coreしておかないと古いphp-fpmがインストールされます。

インストールが完了したら設定していきます。

sudo vi /etc/php-fpm.d/www.conf

次の項目を編集します。

;localから参照するのでUNIX SOCKETを設定。
- ;listen = 127.0.0.1:9000;
+ listen = /var/run/php-fpm.sock

;UNIX SOCKETファイルのユーザーとグループの指定(指定しないとrootとなるが、nginxからアクセスできなくなる。)
- ;listen.owner = nobody
- ;listen.group = nobody
+ listen.owner = nginx
+ listen.group = nginx

Nginx側も設定を変更します。
Nginxでは各バーチャルホストごとの設定を定義するserverセクションに記述します。

server {

  listen        80;
  listen        443 ssl;

  server_name  example.com;

  ssl_certificate       /path/to/ssl/fullchain.pem;
  ssl_certificate_key   /path/to/ssl/privkey.pem;

  client_max_body_size 1g;
  location / {
        root /path/to/docroot;
        index index.php index.html index.htm;
        allow all;
  }
  location ~ \.php$ {
    root           /path/to/docroot;
    fastcgi_pass   unix:/var/run/php-fpm.sock;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    include        fastcgi_params;
    allow all;
  }
}

基本的には上記のような設定でOKです。

設定が完了したらphp-fpmとnginxを再起動します。

sudo systemctl start php-fpm nginx
sudo systemctl enable php-fpm nginx

あとはおなじみのphpinfo();をドキュメントルートに置いて、phpが実行されているか確認します。

vi /path/to/docroot/phpinfo.php
<?php
    phpinfo();
?>

あとは

http://example.com/phpinfo.php

のようにして、phpの環境情報が表示されれば成功です。

  • この記事いいね! (0)
著者:杉浦

【Laravel】Collection中のitemが未定義でもnullでもないか検査したい時はisset()でなくget()!==nullを使うべき

 CollectionはLaravelの持つ配列をラッピングしたクラスです。Collectionを用いることによってPHPの素の配列操作よりも多彩な配列操作を一貫性を保ったまま容易に実現できます。

Illuminate\Support\Collectionクラスは配列データを操作するための、書きやすく使いやすいラッパーです。以下の例をご覧ください。配列から新しいコレクションインスタンスを作成するためにcollectヘルパを使用し、各要素に対しstrtoupperを実行し、それから空の要素を削除しています。
コレクション 5.7 Laravel

 Collectionは素晴らしいですが、題名にあるisset()の関連部分では特殊な挙動を見せます。

$arr = ['hoge' => null];
dump(isset($arr['hoge']));// false
dump(isset($arr['fuga']));// false

$col = new \Illuminate\Support\Collection(['hoge' => null]);
dump(isset($col['hoge']));// true
dump(isset($col['fuga']));// false

 Collectionを介したせいで結果が素の配列と異なってしまいました。これはCollectionのArrayAccessインタフェースの実装の仕方に原因があります。
 ArrayAccessインタフェースはオブジェクトを配列として呼び出すためのインタフェースです。これを実装すると次の画像の様にオブジェクトを配列として呼び出せます。
 
PHP: ArrayAccess – Manual
 ArrayAccessの中でissetと直接的に絡んでいるメソッドがoffsetExistsです。

このメソッドが実行されるのは、ArrayAccess を実装したオブジェクト上で isset() あるいは empty() を使用した場合です。
PHP: ArrayAccess::offsetExists – Manual

 \Illuminate\Support\CollectionのArrayAccess::offsetExistsは次の様に実装されています。

    /**
     * Determine if an item exists at an offset.
     *
     * @param  mixed  $key
     * @return bool
     */
    public function offsetExists($key)
    {
        return array_key_exists($key, $this->items);
    }

 $this->itemsがCollection内で扱われている配列です。これはkeyが存在すれば値がnullであってもissetでtrueを返すということです。このため

$col = new \Illuminate\Support\Collection(['hoge' => null]);
dump(isset($col['hoge']));// true

となります。
 一見バグの様に見えますが、Determine if an item exists at an offset.とコメントされている上、実装内容を

    public function offsetExists($key)
    {
        return isset($this->items[$key]);
    }

と変えるとこの変更が原因でLaravel自体のテストで失敗が起きます。つまり仕様です。

 このアンチパターンと化したisset()の代わりになる判定方法が次のコードです。

$col = new \Illuminate\Support\Collection(['hoge' => null]);
dump($col->get('hoge') !== null);// false
dump($col->get('fuga') !== null);// false

https://implode.io/5yDeL4
 Collection中のgetメソッドを用います。getメソッドはキーの値を返すメソッドです。次の引用の様にキーが存在しない場合、例外でなくnullを返します。

getメソッドは指定されたキーのアイテムを返します。キーが存在していない場合はnullを返します。

 これによりgetメソッドの返り値はキーの値がnullであっても、キーが存在しなくともnullになります。これでissetと同じく、変数がセットされていること、そしてNULLでないことを検査することができます。

  • この記事いいね! (0)