20190821のPHPに関する記事は14件です。

laravelで検索機能を実装する

今回は文字列を入力して部分一致検索、プルダウンメニューから選択して検索を行う機能を実装します。具体的には下記の画面でユーザ名、棋力、好きな戦法による検索を行います。
11fdea620d70330aa57c7896d10a1faa.png

Userモデルは以下のように name(ユーザ名)、stength(棋力)、tactics(好きな戦法)というカラムを定義します。

class User extends Authenticatable
{
    protected $fillable = [
        'name','strength','tactics',
    ];
}

コントローラは以下のようなindexメソッドを定義します。

class SearchController extends Controller
{
    public function index(Request $request){
        $query = User::query();

     //$request->input()で検索時に入力した項目を取得します。
        $search1 = $request->input('strength');
        $search2 = $request->input('tactics');
        $search3 = $request->input('name');

         // プルダウンメニューで指定なし以外を選択した場合、$query->whereで選択した棋力と一致するカラムを取得します
        if ($request->has('strength') && $search1 != ('指定なし')) {
            $query->where('strength', $search1)->get();
        }

         // プルダウンメニューで指定なし以外を選択した場合、$query->whereで選択した好きな戦法と一致するカラムを取得します
        if ($request->has('tactics') && $search2 != ('指定なし')) {
            $query->where('tactics', $search2)->get();
        }

        // ユーザ名入力フォームで入力した文字列を含むカラムを取得します
        if ($request->has('name') && $search3 != '') {
            $query->where('name', 'like', '%'.$search3.'%')->get();
        }

    //ユーザを1ページにつき10件ずつ表示させます
        $data = $query->paginate(10);

        return view('users.search',[
            'data' => $data
        ]);
    }
}

検索ボタンを押したときのルーティングを以下のように記述します。

//検索ボタンを押すとコントローラのindexメソッドを実行します
Route::get('Search','SearchController@index')->name('search');

ユーザ検索ページのビューを以下のように記述します。

    <div class="row">
        <div class="col-sm-4">
            <div class="text-center my-4">
                <h3 class="brown border p-2">ユーザ検索</h3>
            </div>
            {!! Form::open(['route' => 'search', 'method' => 'get']) !!}
                <div class="form-group">
                    {!! Form::label('text', 'ユーザ名:') !!}
                    {!! Form::text('name' ,'', ['class' => 'form-control', 'placeholder' => '指定なし'] ) !!}
                </div>
                <div class="form-group">
                    {!! Form::label('strength', '棋力:') !!}
                    {!! Form::select('strength', ['指定なし' => '指定なし'] + Config::get('strength.kiryoku') ,'指定なし') !!}
                </div>
                <div class="form-group">
                    {!! Form::label('tactics', '好きな戦法:') !!}
                    {!! Form::select('tactics', ['指定なし' => '指定なし'] + Config::get('tactics.senpou') , '指定なし') !!}
                </div>
                {!! Form::submit('検索', ['class' => 'btn btn-primary btn-block']) !!}
            {!! Form::close() !!}
        </div>
        <div class="col-sm-8">
            <div class="text-center my-4">
                <h3 class="brown p-2">ユーザ一覧</h3>
            </div>

            <div class="container">
                <!--検索ボタンが押された時に表示されます-->
                @if(!empty($data))
                    <div class="my-2 p-0">
                        <div class="row  border-bottom text-center">
                            <div class="col-sm-4">
                                <p>ユーザ名</p>
                            </div>
                            <div class="col-sm-4">
                                <p>棋力</p>
                            </div>
                            <div class="col-sm-4">
                                <p>好きな戦法</p>
                            </div>
                        </div>
              //検索条件に一致したユーザを表示します
                        @foreach($data as $item)
                                <div class="row py-2 border-bottom text-center">
                                    <div class="col-sm-4">
                                        <a href="">{{ $item->name }}</a>
                                    </div>
                                    <div class="col-sm-4">
                                        {{ $item->strength }}
                                    </div>
                                    <div class="col-sm-4">
                                        {{ $item->tactics }}
                                    </div>
                                </div>
                        @endforeach
                    </div>
                    {{ $data->appends(request()->input())->render('pagination::bootstrap-4') }}
                @endif
            </div>
        </div>
    </div>

ここでは以下のように、あらかじめconfigディレクトリにstrength.phpを作成して'kiryoku'という配列を定義しています。上記のビューではconfig::get('strength.kiryoku')でその配列を取得して、プルダウンメニューを作成しています。さらに['指定なし' => '指定なし'] + config::get('strength.kiryoku')としてkiryoku配列に「指定なし」という要素を追加しています。

config/strength.php

<?php 
return array(
    'kiryoku' => array(
  '10級' => '10級', 
  '9級' => '9級', 
  '8級' => '8級', 
  '7級' => '7級',
  '6級' => '6級', 
  '5級' => '5級', 
  '4級' => '4級', 
  '3級' => '3級',
  '2級' => '2級', 
  '1級' => '1級', 
  '初段' => '初段', 
  '二段' => '二段',
  '三段' => '三段', 
  '四段' => '四段', 
  '五段' => '五段', 
  '六段' => '六段',
  ),
);
?>

config::get('tactics.senpou')も同様です。
config/tactics.php

<?php 
return array(
    'senpou' => array(
  '角換わり' => '角換わり', 
  '矢倉' => '矢倉', 
  '相掛かり' => '相掛かり', 
  '横歩取り' => '横歩取り',
  '向かい飛車' => '向かい飛車', 
  '三間飛車' => '三間飛車', 
  '四間飛車' => '四間飛車', 
  '中飛車' => '中飛車'
  ),
);
?>

ユーザ名のみで検索すると、以下のように正しく表示されました。
40f910bb147f96bbc5217e4810dd315e.png

また棋力、好きな戦法で検索した場合も、以下のように正しく表示されました。
78290b7b15c518d244dee790a40b7dc0.png

今回の検索機能のポイントは、コントローラの$request->inputメソッドと$query->whereメソッドかなと思いました。あとプルダウンメニューの実装はconfigファイルを使用しましたが、意外と躓きました。プルダウンメニューはほかの方法でより簡単に実装できるのかもしれません。

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

EC2インスタンスを立ち上げて、PHPを動かすまで

はじめに

インフラ初心者のエンジニアにEC2のインスタンスを立てる手順を教えることになりました。
私も専門領域外ですが、初心者がハマりそうなポイントとPHPを入れて動かすところまで書いてみます。

EC2インスタンス立ち上げる

インスタンスを立てる手順は既に分かりやすい記事が多数あるので省略。
こちらの記事がシンプルで分かりやすかったので、オススメです。

AWS EC2でWebサーバーを構築してみる
https://qiita.com/Arashi/items/629aaed33401b8f2265c

上記記事の手順に沿って、無事にApache動作確認できたとします。
もし導入中にハマったら、最後のセクションでハマりやすいポイントを書いたので参考にしてみてください。

PHPを入れる

sudo yum -y install php

実行すると以下の通り。

Loaded plugins: extras_suggestions, langpacks, priorities, update-motd
Resolving Dependencies
--> Running transaction check
---> Package php.x86_64 0:5.4.16-45.amzn2.0.6 will be installed
--> Finished Dependency Resolution

...

Installed:
  php.x86_64 0:5.4.16-45.amzn2.0.6                                                                                                                                                                          

Complete!

yumの場合、デフォルトだとPHPは5.4です。
PHP7はちょっと一工夫しないといけないので、初心者がいきなり7を入れようと頑張るよりは、まず5.4を入れて動くところまで確認したほうが良いかと思います。

index.phpを有効にする

vi /etc/httpd/conf/httpd.conf

vimは使い慣れていないと使いづらいかもしれません。
サーバでサクッと設定いじったりする際に使えると便利なので、この期に覚えてしまいましょう。

...
#
<IfModule dir_module>
    DirectoryIndex index.php index.html
</IfModule>

#
...

こう記述すると、ファイル名を指定せずアクセスした際にindex.phpが存在すれば表示し、存在しなければindex.htmlを表示します。

vimの操作が分からない方

  • vimは開いたときはコマンドモードで、文章の編集ができません。
  • iキーでインサートモード(文字入力ができるモード)に切り替わります。
  • インサートモード中にEscでコマンドモードに戻ります。基本的に、このインサートモードとコマンドモードを切り替えて使います。
  • 最初は超絶使いづらいですが、慣れるとサクサクコーディングできるようになります。

index.phpを追加し保存するには

  • コマンドモードで /index[Enter]
    • スラッシュの後に入力した文字で検索ができる
  • iキーでインサートモードに切り替え、index.phpの文字列をタイピングした後にEscでコマンドモードに戻る
  • コマンドモードで :w[Enter] :q[Enter]と入力
    • :wで保存(write)、:qでファイルを閉じる(quit)
    • :wq[Enter]と入力すると、保存しファイルを閉じる処理を一度に実行できる

設定を反映する

設定ファイルを変更してアクセスしても、まだ反映されません。
基本的にミドルウェアの設定を変更した場合、反映するには再起動が必要です。

sudo service httpd configtest

まずはconfigでsyntax errorがないかテスト。
慣れてくるとついつい即再起動しがちですが、syntax errorが起きているとApache起動に失敗してサービスが止まるので、必ずconfigtestでエラーが起きないことを確認する癖を付けましょう。

sudo service httpd graceful

gracefulで再起動します。
ちなみに再起動はgracefulじゃなくrestartでもできますが、restartはWebサーバにアクセスしているユーザーがいて子プロセスがあってもkillします。
gracefulは子プロセス終了まで待って設定を反映するので安全です。
ただしgracefulだと反映されない設定もあったりするので、その場合はrestartしましょう。

index.phpを配置する

vi /var/www/html/index.php

vimが開くので

<?php echo 'hoge' ?>

で保存し終了。

Apacheの設定がうまくいっていれば、Webブラウザで開いた際のページに「hoge」と書かれたページが表示されます。

ハマりやすいポイント

他にもあれば随時追加します。

EC2インスタンスにsshで入れない

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0666 for 'xxxxx.pem' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key 'xxxxx.pem': bad permissions
ec2-user@xx.xx.xx.xx: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).

ハマることが非常に多いです。
秘密鍵のパーミッションが緩いとエラーになります。

sudo chmod 400 xxxxx.pem

でいけるはず。

Apacheの設定ファイルが見つからない/読み取り専用で編集できない

E45: 'readonly' オプションが設定されています (! を追加で上書き)

ミドルウェアのconfigいじったり起動/停止等を実行する際は一般ユーザーが簡単にいじれちゃうと困るので、基本的にrootユーザーで実行します。

sudo su -

index.phpが真っ白な画面になる

ソースを表示すると、phpファイルがそのままテキストとして表示されていませんか?
その場合はApacheの

...
#
<IfModule dir_module>
    DirectoryIndex index.php index.html
</IfModule>

#
...

が同じように記述されているか確認してください。

index.phpが読み取り専用で保存できない

/var/www/html/ディレクトリのパーミッションがおかしい可能性があります。
雑ですが777にパーミッションを変えてしまいましょう。

sudo chmod 777 /var/www/html/

以上です。

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

file_get_contents('php://stdin') で、標準入力を渡さないとどうなるか。

ハマったこと

PHPの入出力ストリームラッパーを使って、標準入力を受け取る場合。(手元のPHP7.1で動作確認)
https://www.php.net/manual/ja/wrappers.php.php

たとえばこんなスクリプトで

<?php
$str = file_get_contents('php://stdin');
echo $str;

標準入力を渡すと、その文字列をそのまま表示します。

$ echo 'hoge' | php test.php 
hoge

では、このスクリプトに標準入力を渡さなかった場合どうなるでしょう?

$ php test.php 

空の文字列でも帰ってくるのかなと思ってたのですが。。。
実は、 処理が止まってプロンプトが帰ってきません
(正確には、パイプを渡してない時は標準入力が入力待ちになってブロックされている)

対処策

posix_isatty() 関数を利用します。
http://php.net/manual/ja/function.posix-isatty.php

この関数を使って、 STDINがオープンされていて、 かつ端末に接続されているか否かを判定します。
(標準入力が渡っている場合はfalseになる)

<?php
$str = (posix_isatty(STDIN)) ? 'default' : file_get_contents('php://stdin');
echo $str;

動作検証

$ echo 'hoge' | php test.php 
hoge

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

Wordpress 開発でも静的解析したい!

静的解析しよう

Wordpress のプログラムを書いているときにサイトが止まるような実行エラーって怖いですよね。
それらを少しでも減らすために、プログラム公開前に自身の開発環境で、型のチェックや引数のチェックなど、静的解析を正しく行うことで、予め実行エラーに気づける環境を作りましょう。

環境

  • Wordpress プラグイン開発
  • PHP7

ディレクトリ構成

.
├── RAPI_Plugin.php  # plugin main ファイル
├── composer.json
├── composer.lock
├── phpstan.neon     # phpstan 設定ファイル
├── require.php
├── src              # 静的解析したいプログラムのディレクトリ
│   ├── Controllers
│   ├── RAPI.php
│   ├── Models
│   └── Routes
├── bin
├── tests
└── vendor

PHPStan

PHP 向けの静的解析ツールです。
こちらの方の説明が詳しいです。
https://qiita.com/qiita_masaharu/items/24bf34579119628eefe2

WordPress extensions for PHPStan

szepeviktor/phpstan-wordpress を利用します。
https://github.com/szepeviktor/phpstan-wordpress

composer.json
{
    "require-dev": {
        "szepeviktor/phpstan-wordpress": "^0.2.0"
    },
    "scripts": {
        "post-install-cmd": "PHPStan\\WordPress\\Composer\\FixWpStubs::php73Polyfill",
        "post-update-cmd": "PHPStan\\WordPress\\Composer\\FixWpStubs::php73Polyfill"
    }
}

これを composer install

設定ファイル(phpstan.neon)

phpstan.neon
includes:
    - vendor/szepeviktor/phpstan-wordpress/extension.neon
parameters:
    excludes_analyse: ['bin', 'tests', 'vendor']
    autoload_directories:
        - ./src
    bootstrap: null
    level: max
    fileExtensions:
        - php
    inferPrivatePropertyTypeFromConstructor: true

実行準備

composer update --classmap-authoritative 

このコマンド実行する

実行

./vendor/bin/phpstan analyse --memory-limit 256M .

 11/11 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%


 [OK] No errors

良さそうですね。

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

Wordpress でカスタム URL を追加する

Wordpress 環境で開発をする際、新しくページ(URL)を追加したい場合があるかと思います。
ただ、ページ追加に関しては、rewrite_rules_array に URL やパラメータを追加するなどの Wordpress 独特の知識が必要となります。
そういったものを取り払い、Routing としてページ追加を実現することが記事の趣旨です。

環境

・Wordpress プラグイン開発
・PHP 7.2.19
・RAPI という namespace でプラグイン開発

Route Class

このクラスはこちらのサイトから引用させていただいています。
https://firegoby.jp/archives/1213

これは、作りたい URL を wordpress の URL の取り扱いに変換させるためのクラスです。

Route.php
<?php

namespace RAPI\Routes;

class Route {
  private $rule     = null;
  private $query    = null;
  private $callback = null;

  public function __construct($rule, $query, $callback){
    $this->rule     = $rule;
    $this->query    = $query;
    $this->callback = $callback;
    add_filter('query_vars', array(&$this, 'query_vars'));
    add_filter('rewrite_rules_array', array(&$this, 'rewrite_rules_array'));
    add_action('init', array(&$this, 'init'));
    add_action('wp', array(&$this, 'wp'));
  }

  public function init()
  {
    global $wp_rewrite;
    $rules = $wp_rewrite->wp_rewrite_rules();
    if (!isset($rules[$this->rule])) {
      $wp_rewrite->flush_rules();
    }
  }

  public function rewrite_rules_array($rules)
  {
    global $wp_rewrite;
    $new_rules[$this->rule] = $wp_rewrite->index . '?'.$this->query.'=1';
    $rules = array_merge($new_rules, $rules);
    return $rules;
  }

  public function query_vars($vars)
  {
    $vars[] = $this->query;
    return $vars;
  }

  public function wp()
  {
    if (get_query_var($this->query)) {
      call_user_func($this->callback);
    }
  }
}

?>

Router class

これは URL の登録管理のためのクラスです。
Route インスタンスを作ることで Routing を作成します
下の例であれば /rapi/api/article の URL のアクションを article_api_controller の index function にマッピングして登録しています。

Router.php
<?php

namespace RAPI\Routes;

use RAPI\Controllers\Api\ArticleController;

class Router {
  protected $article_api_controller;

  public function __construct() {
    $this->article_api_controller = new ArticleController();
    $this->addRoutes();
  }

  public function addRoutes() {
    new Route('rapi/api/article', 'rapi_api_article_index', [$this->article_api_controller, 'index']);
  }
}

利用方法

App.php
<?php

namespace RAPI;

use RAPI\Routes\Router;

class App {
  protected $router;

  public function __construct() {
    $this->router = new Router()
  }
}

Controller class (参考)

上の例であれば、 /rapi/api/article の URL で、この index() メソッドが実行され、
JSON で ["id":1] が出力されるものとなります。
REST API 作るだけであれば WP REST API で良いですけどね。

ArticleController.php
<?php

namespace RAPI\Controllers\Api;

class ArticleController {
  public function index() {
    wp_send_json(['id' => 1]);
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【WordPress】年別の記事の出し分けとページネーション

Advanced custom fieldを使用

releases.php
<div class="releases_list">
        <?php
        $paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
        $query = new WP_Query(array(
            'posts_per_page' => 4,
            'post_type' => 'releases',
            'orderby' => 'date',
            'paged' => $paged
        )); ?>
        <?php $prev_year = null; ?>
        <?php if ($query -> have_posts()): ?><?php while ($query -> have_posts()) : $query -> the_post(); ?> 
            <?php $this_year = get_the_date('Y'); ?>
            <?php if ($prev_year != $this_year):  ?>
                    <div class="releases_list_year"><p><?php echo $this_year; ?></p></div>
            <?php endif; ?>
            <ul class="releases_list_news">
                <li class="releases_list_news_list">
                    <dl class="releases_list_news_dl">
                        <dt class="releases_list_news_ttl"><p><?php the_time('Y-n-j'); ?></p></dt>
                        <dd class="releases_list_news_read">
                            <a><?php the_field('news'); ?></a>
                        </dd>
                    </dl>
                </li>
            </ul>
        <?php $prev_year = $this_year; ?>
        <?php endwhile; ?><?php endif; ?>
    </div>      
    <div class="pagi">
        <?php custom_pagination('/releases/', $paged, $query->max_num_pages); ?>
    </div>  

参考サイト:
https://stackoverflow.com/questions/3397725/wordpress-how-can-i-display-post-link-group-by-year

functions.php
function custom_pagination($uri, $current, $last, $range = 5){
    $showitems = ($range * 2)+1;
    if(preg_match('/\\?/', $uri)){
        $uri .= '&';
    } else {
        $uri .= '?';
    }
    if(1 !== (int)$last){
        $prev = $current-1;
        $next = $current+1;
        $currentPage = false;
        $beforePage = false;
        $afterNum = 0;
        $path = get_template_directory_uri();
        echo "<div class='page_nsv'>";
        // echo  "<p class=\"prev prne nav_list\"><a href=\"{$uri}paged={$prev}\">&lt;</a></p>";
        echo  "<ul class=\"nav_list nav_con_list dot_left dot_right\">";
        for ($i=1; $i <= $last; $i++){
            if($i === 1 && $current !== 1){
                 echo "<p class=\"prev prne nav_list\"><a href=\"{$uri}paged={$prev}\">&lt;</a></p>";
            }
            if($current === $i){
                echo "<li class=\"nav_con_list_li\"><a href=\"#\" onclick=\"return false;\" class=\"page-link\">{$current}</a></li>";
            } else {
                echo "<li class=\"nav_con_list_li\"><a href=\"{$uri}paged={$i}\" class=\"page-link\">{$i}</a></li>";
            }
            if($i == $last && $current != $last){
                 echo "<li class=\"nav_con_list_li\"><a class=\"page-link\" href=\"{$uri}paged={$next}\"></a></li><p class=\"next prne nav_list\"></p>";
                 echo "<p class=\"next prne nav_list\"><a href=\"{$uri}paged={$next}\">&gt;</a></p>";
            }
        }
        echo "</ul>";
        echo "<p class=\"next prne nav_list\"></p>";
        echo "</div>";  
    }
}

functions.phpでページネーションを設定

スクリーンショット 2019-08-21 14.27.16.png
投稿すると画像のようになります

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

POST送信後と送信前の文字数の違い

概要

ThreeLでwikiのように整形を行っている部分で
うまいこと変換されないことが問題点にあたる。

結論

改行コードの違い
htmlのtextareaの改行コードがCR/LF

htmlのtextareaの改行コードがCRLFであるが
javascriptの場合は、LF。

この違いがあるため \n\n 部分が認識されなかった。

経緯

ajaxのプレビューと本ページが差分が存在
文字列の長さの検証、違うことが確認できる。

調べてみるとtextareaの改行コードがCRLF
jsではLF
違いの確認が出来た。

解決策

$text = str_replace("\r\n", "\n", $post_textarea);

どちらかにそろえてあげるだけで大丈夫です。
もしくはパーサー側で対応するのもありかと思う。

ほかの問題点として文字数が違うため文字数制限をかける際に少なくなったりなどがあるなるので、知っておくといいですね」。

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

Laravelで閉包テーブル(Closure Table) を簡単に実現してくれるライブラリ ClosureTable

Laravelで閉包テーブル(Closure Table) を簡単に実現してくれるライブラリ ClosureTable

DBのツリー構造のテーブルにはいくつかありますが、その中に閉包テーブルというものがあります。
この記事にたどり着く人には説明不要だと思うので説明は省きます!!!!

各ツリー構造についてはこちらの記事がわかりやすい気がします!
https://qiita.com/hirashunshun/items/06adf4f42f03a9f3b63d

実際に閉包テーブルを自前で実現しようとすると、CRUDのクエリ投げるときに整合性とれてるのかこれ。。。って部分があり、孫だけ取得とかそのへんのクエリをゴリゴリ書くのは面倒な部分が多く、そこがデメリットな感じもします

本題

ゴリゴリクエリ書くのが面倒だなーというデメリットを解決してくれるのがこれ!
https://github.com/franzose/ClosureTable

テーブル構造のことなんて何も考えず脳死で閉包テーブルが作れます!
めちゃくちゃ良いライブラリだと思います

使い方(Migrationまで

ライブラリが提供しているコマンドでmigrationを作成

php artisan closuretable:make --entity=trees

すると、migration・Model・Interfaceとかが作られる

create 2019_08_19_105450_create_trees_table_migration
create Tree
create TreesInterface
create TreesClosure
create TreesClosureInterface

2019_08_19_105450_create_trees_table_migration
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTreesTableMigration extends Migration
{
    public function up()
    {
        Schema::create('trees', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('parent_id')->unsigned()->nullable();
            $table->integer('position', false, true);
            $table->integer('real_depth', false, true);
            $table->softDeletes();

            $table->foreign('parent_id')
                ->references('id')
                ->on('trees')
                ->onDelete('set null');
        });

        Schema::create('trees_closure', function (Blueprint $table) {
            $table->increments('closure_id');

            $table->integer('ancestor', false, true);
            $table->integer('descendant', false, true);
            $table->integer('depth', false, true);

            $table->foreign('ancestor')
                ->references('id')
                ->on('trees')
                ->onDelete('cascade');

            $table->foreign('descendant')
                ->references('id')
                ->on('trees')
                ->onDelete('cascade');
        });
    }

    public function down()
    {
        Schema::table('trees_closure', function (Blueprint $table) {
            Schema::dropIfExists('trees_closure');
        });

        Schema::table('trees', function (Blueprint $table) {
            Schema::dropIfExists('trees');
        });
    }
}

最低限の構造を作ってくれます。必要であれば、カラムとか追加可能
基本的にはtrees側に追加すればよいかと

interface抜粋

Treeinterface
use Franzose\ClosureTable\Contracts\EntityInterface;

interface treesInterface extends EntityInterface
{
}

継承しているInterfaceがClosureTable独自のものになっている

モデル

Tree.php
use Franzose\ClosureTable\Models\Entity;

class trees extends Entity implements treesInterface
{
    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'trees';

    /**
     * ClosureTable model instance.
     *
     * @var treesClosure
     */
    protected $closure = 'App\treesClosure';
}

継承しているEntityがClosureTable独自のものになっている

この辺のclass名がloweerCaseなのは気持ち悪いので好みで変える…!

rotected $closure = 'App\treesClosure';

実際の使い方

一番上のモデルを作る時

$rootTree = new Tree();
$rootTree->save();

普通にモデル作るのと同じ

子供追加

// 親作る
$rootTree = new Tree();
$rootTree->save();

// 子供作る
$childTree = new Tree();
$rootTree->addChild($childTree);
$childTree->save();

// 子供2作る
$childTree2 = new Tree();
$rootTree->addChild($childTree2);
$childTree2->save();

これもいい感じにできる

削除

// 親作る
$rootTree = new Tree();
$rootTree->save();

// 子供作る
$childTree = new Tree();
$rootTree->addChild($childTree);
$childTree->save();

// 子供2作る
$childTree2 = new Tree();
$rootTree->addChild($childTree2);
$childTree2->save();

// 子供達だけ削除の場合
$rootTree->deleteSubtree();
// 自分を含む子供達を削除する場合
$rootTree->deleteSubtree(true);

取得

$rootTree = Tree::find(いつもの);

// これで子供達とれる(孫まで取れるわけではないので再帰処理とか別メソッドを使う必要がある
$children = $rootTree->getChildren();

こんなかんじ

書くのが辛くなったのでまとめ

使いやすい!

各メソッドはいい感じに抽象化されてて使いやすい!公式みて!!
https://github.com/franzose/ClosureTable#ancestors

使ってみて!!

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

IntelliJ・WebStrom・PhpStorm等のJetBrains製IDEで、文字列の直前に「language=JSON」と書くと、その文字列にJSONのシンタックスハイライトが効いて便利だった。

IntelliJやWebStrom、PhpStormなどのJetBrains製IDEで、文字列の前に// language=JSONというコメントをつけると、IDEが文字列をJSONとして認識してくれるため、

  • JSONとしてのシンタックスハイライト
  • JSON構文エラーの警告
  • JSONのコード補完
  • コード整形

といった、地の文でJSONを書いたときにIDEがやってくれるような恩恵を享受できるようになる。

language-json.gif

この機能はLanguage Injectionと呼ばれるもの。コメントが書ける大抵の言語なら、JavaでPHPでもJavaScriptでもScalaでも使えるようだ。

この機能はJSONに限ったものでなく、language=SQLlanguage=HTMLなどのlanguage_IDを指定することで他の言語にも対応可能。

// language=<language_ID>

Screenshot_2019_08_21_12_44.png

PHPは@langが使える

PHPではPhpDocの@langでもLanguage Injectionすることができる。

Screenshot_2019_08_21_12_32.png

Javaなどでは@Languageアノテーションが使える

JavaやGroovy、Kotlinではorg.jetbrains:annotationsをMavenなどの依存に追加することで、@Languageアノテーションを使うことができる。Scalaには対応していない様子。

Screenshot_2019_08_21_12_31.png

参考文献

所感

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

SendGrid + CakePHPでWeb API利用時にTemplateを使う

SMTPを使うならいつものsetTemplateでテンプレートを指定できますが、Web APIを使う場合SendGridの純正ライブラリはCakePHP用に書かれたものではないためCakePHPのテンプレートを使うようになっていません。
テンプレートに値を埋めたテキスト(HTML)を取得して、addContentします。

宛先や件名の設定などは省略しているので公式を参照してください。

// 実際はComponentにして使っています。以下のコードはControllerで使うことを想定しています

// Viewを自分でnewするので使えるようにしておきます
use Cake\View\View;

$view = new View($this->request, $this->response);

// $varsにはテンプレートで使う値が入ってます
// ['username' => 'テスト太郎']
// のような連想配列を想定しています
// viewに値を渡すとき使う$this->set()と同じなので設定できればどう書いてもいいです
foreach ($vars as $key => $value) {
    $view->set($key, $value);
}

// $templateにテンプレートを指定します
// Template直下から指定する必要があるので、
// いつものEmailテンプレートを使いたければ Email/text(またはhtml)/テンプレート名 にします
// これで$contentにテンプレートに値を埋め込んだテキストが入ります
$content = $view->render($template, false);

// マルチパートにしたければplain、html両方addContentします
if ($format == 'text') {
    $email->addContent('text/plain', $content);
} else {
    $email->addContent('text/html', $content);
}

$sendgrid = new SendGrid('API-KEY');
try {
    $response = $sendgrid->send($email);
    // ログは好きにしてください
    $this->log($response->statusCode());
    $this->log($response->headers());
    $this->log($response->body());
} catch (Exception $e) {
    $this->log('Caught exception: '. $e->getMessage(), LOG_ERR);
}

SendGridの例を書きましたが、Controller中でTemplateに値を埋めた状態のテキスト取得する方法は汎用的なので知っておくと他でも使えます。

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

ローカルのPHPからsshトンネルを使ってサーバーへmysql接続

ちょっとしたクエリを試したいときなどローカルから直接アクセスできないDBサーバーへ接続したいときがあるかと思います。
mysqlクライアントツールなど使えば簡単にできるのですが、今回はphpの処理も絡めて行いたかったのでローカルのPHPプログラムからサーバのDBにアクセスできるようにしました。

ポートフォワードさせる(sshトンネル)

ターミナルで以下のコマンドを実行させます。

ssh -N -L {任意のポート番号}:{DBエンドポイント} -i {鍵ファイル} -p {踏み台のsshポート番号} {ユーザ名}@{踏み台サーバのホスト}

上記コマンドの波括弧部分を変えます。任意のポート番号は使用されていないものならなんでも良いです。このポート番号にアクセスするとポートフォワードされるようになります。
オプションですが
N...リモートコマンドを実行しない
L...指定されたようにポートフォワードさせる
のようになっています。

PDO

$pdo = new PDO('mysql:host='. "127.0.0.1:{任意のポート番号}" .';dbname=' . "db_name" .';charset=utf8', "user_name", "password");

PDOはいつもの通りですが、hostがサーバーではなくローカルを指すようにします。その際、sshコマンドで入力したポートを利用します。

これでテスト的にプログラムを実行したいときに便利になりました。

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

phpunitで作成したテストケースをもとに試験項目表を作成する-その2

やりたいこと

前回phpunitで作成したテストケースをもとに試験項目表を作成するでテストケースから試験の内容を出力するところまで作成できました。

ただ、以下のような不満があります。

  • コマンドを2回実行しないと試験表が出力されない。
  • テストしたクラス全部の結果が一つにレポートにまとまってしまうので個別のテストクラスごとの結果も保存しておきたい。
    • ※製品のリポジトリとは別のリポジトリに管理しやすい形だと嬉しい

そこで今回はPHPUnitのテストランナーの拡張して試験あとに実行する処理を追加していこうと思います。

処理の全体像

イメージとしてはPHPUnitのテストランナーで実行されたテストクラスを監視して以下のような簡単なしくみを実装しています。
outputPhpunitTest.png

出来上がった試験結果はこんな感じでとりあえず配置しようと思います。
2019-08-21_10h41_05.png

実装方法

テストランナー拡張用のクラスを用意する

phpunitの実行中に処理を挟むためのテストランナー拡張用クラスを用意します。
今回は以下のようなfileを配置します。配置場所はどこでもよいですが今回はtestsフォルダ配下にExtensionフォルダを設けてそちらに格納するようにしました。

BeforeTestHookで実行中のテストクラス名を取得して、処理完了後にHTMLへの変換処理を実行しています。

余談ですがAfterLastTestHookというイベントフックもあったのですが、HTMLへの変換のもとにしているファイル出力がAfterLastTestHookのタイミングではまだ生成されていなかったのでデストラクタでテスト完了後の処理を記述しています。

TestRunnerExtension.php
<?php


namespace Tests\Extension;

use PHPUnit\Runner\BeforeTestHook;

final class TestRunnerExtension implements BeforeTestHook
{
    /**
     * @var string
     */
    private $resultFilePath = 'tests/log/logfile.xml';

    /**
     * @var string
     */
    private $testedFileList = 'tests/log/testedFileList.txt';

    /**
     * @var string
     */
    private $resultDir = 'tests/log/result/';

    /**
     * @var array
     */
    private $classList = [];

    /**
     * PHPUnitのログが出力後にファイル生成を行いたいので
     * __destructで終了処理を記述する。
     */
    public function __destruct()
    {
        if (!file_exists($this->resultFilePath)) {
            return;
        }
        if (count($this->classList) > 1) {
            $this->multiClassTestReport();
        } else {
            $this->singleClassTestReport();
        }
    }

    /**
     * 複数のクラスにまたがって試験した場合は個別のテストクラスも実行する。
     * @throws \ReflectionException
     */
    private function multiClassTestReport()
    {
        exec('xsltproc phpunit.xslt ' . $this->resultFilePath . ' > ' . $this->resultDir . 'output.html');

        if (file_exists($this->testedFileList)) {
            unlink($this->testedFileList);
        }

        foreach ($this->classList as $className) {
            $reflection = new \ReflectionClass($className);
            exec('vendor/bin/phpunit ' . $reflection->getFileName());
        }
    }

    /**
     * 単一のクラスの試験ではクラス名と対応したパスにファイルを生成する。
     */
    private function singleClassTestReport()
    {
        $filePathArr = explode('\\', $this->classList[0]);
        $fileName = array_pop($filePathArr);
        $filePath = implode('/', $filePathArr);

        if (!file_exists($this->resultDir . $filePath)) {
            mkdir($this->resultDir . $filePath, 0777, true);
        }
        exec('xsltproc phpunit.xslt ' . $this->resultFilePath . ' > ' . $this->resultDir . $filePath . '/' . $fileName . '.html');
    }

    /**
     * 実行されたテスト情報からテスト対象のクラス名を取得する。
     * @param string $test
     */
    final public function executeBeforeTest(string $test): void
    {
        $test = substr($test, 0, strpos($test, '::'));
        if (!in_array($test, $this->classList, true)) {
            $this->classList[] = $test;
        }
    }
}

phpunit.xmlに拡張クラスを追記する。

上記で作成した拡張クラスをphpnit.xmlに以下のように追記します。
これでphpunitを実行するだけで拡張クラスに記述した処理を自動で実行してくれます。

phpunit.xml
    <extensions>
        <extension class="Tests\Extension\TestRunnerExtension"/>
    </extensions>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP のエラーログを root が作成してしまってアプリから書けなくなる問題

php のエラーログ、php.ini で次のように設定していると・・

error_log = /var/log/php/php.log
log_errors = On

なにかの拍子で root で php を実行してエラーが出力されると /var/log/php/php.log が root 所有の 0644 とかで作成されてしまい、アプリケーションの実行ユーザーからエラーログが書けなくなってしまいます。

この問題を解決するためのいくつかの案。

案:logrotate で 0666 でファイルを作成する

logrotate で次のように create 0666 root root を指定します。

/var/log/php/*.log {
  missingok
  notifempty
  create 0666 root root
}

こうしておけばローテーション時に 0666 でログファイルができるので、root でログに書き込んでしまっても Web アプリからのエラーログが書き込めなくなったりはしません。

と思ったら次のような問題があるらしいです。

logrotateがrenameして新しいファイルを作るよりはやくPHPのloggerがファイルを作ってしまい、logrotateがfile existsで死んでしまう。どうしてPHPはファイルを開きっぱなしにできないのかしら
https://twitter.com/kazeburo/status/960717004494684161

うーん・・・

案:php.ini ではエラーログを設定しない

php.ini ではエラーログのファイル名を空にして SAPI のデフォの出力先に出るようにします(CLI なら標準出力?か標準エラー?、mod_php なら Apache の error_log、php-fpm なら??)。

php.ini

error_reporting = -1
display_errors = off
display_startup_errors = off
error_log =
log_errors = On

その上で httpd.conf とか php-fpm.conf とかの SAPI に固有の設定ファイルでエラーログの出力先を指定します。

httpd.conf

php_value error_log /var/log/php/apache.log

php-fpm.conf

php_value[error_log] = /var/log/php/apache.log

もしくは php-<sapi>.ini のような特定の SAPI でだけ読まれる php.ini でも良いかもしれません。

cron とかから CLI を実行するときはコマンドラインオプションで指定します。

cron.d/app

SHELL=/bin/bash
MAILTO=root
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
* * * * * apache php -d error_log=/var/log/php/batch.log -d display_errors=stderr /path/to/batch.php | logger -i -t app

logrotate では nocreate を指定します。

/var/log/php/*.log {
  missingok
  notifempty
  nocreate
}

これで不意に root で php -r xxx とかしてしまってもログファイルは作成されません。

さいごに

PHP を使い始めた最初のころから php.ini で↓みたいにするのは鉄板だと思ってたのですが、

error_log = /var/log/php/php.log
log_errors = On

実はそうでもなくて世間一般では SAPI 固有の設定ファイルで指定してたりする? のだろうか。いや、syslog という手もあるか。。。

追記

Web アプリケーションを root で動かすことは一般的にはありえないですが、サーバのオペレーション用のスクリプトやちょっとしたワンライナーを PHP で作って root で実行とかすると、サーバグローバルな php.ini でログを吐く設定していると問題になることがあるということですね。ログ以外でも php.ini で出力先が設定できるものは同様です(なにかあったっけ?思いつかない)。アプリケーションのキャッシュとかは PHP のコアが吐くわけではないので問題ありません(それはアプリケションが吐くものなので)。

もっとも、PHP で Web アプリケーション以外を作ることはまあまずないですけど・・実際のところシステムの運用中にこの問題が起こったことはないと思います。

ただ、たまに PHP が一番得意だという理由で PHP でなんでも書く人はいますしそれをあまり止めたりもしないです。もちろん、Web アプリケーションと同じ言語で書いてしまうと Web アプリケーションの都合で言語のバージョンアップをするときに影響範囲がわかりにくくなるので、サーバオペレーション系のスクリプトはディストリから提供されているものをそのまま使うのが良いだろうと思います(Bash とか Python とか)。Web アプリケーション用の PHP とサーバオペレーション用の PHP を別々に入れる? それはやめておいたほうが良いと思いますね、、、どっちの PHP で実行されるかわからなくて新規の人がとてつもなく混乱しそうです(勝手なイメージですが Ruby だと Web アプリケーションとサーバオペレーションをを同じ Ruby で実行してることが多そう?)。

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

【Laravel】artisanコマンドまとめ

よく使うartisanコマンドのメモ

https://readouble.com/laravel/5.8/ja/

routeの確認

php artisan route:list

マイグレーション

php artisan make:migration create_users_table

マイグレーションを作成

php artisan migrate

マイグレーションを実行する

php artisan migrate:rollback

マイグレーションをロールバック

Seeder

php artisan make:seeder UsersTableSeeder

seederを作成する

シーダクラスを書き上げたら、Composerのオートローダーを再生成する。

composer dump-autoload

php artisna db:seed

DatabaseSeeder クラスに追加したSeedを実行する

public function run()
{
    $this->call([
        UsersTableSeeder::class,
        PostsTableSeeder::class,
        CommentsTableSeeder::class,
    ]);
}

php artisan db:seed --class=UsersTableSeeder

特定のファイルを個別に実行する

php artisan migrate:refresh --seed

php artisan migrate:refresh でテーブルを再構築し、seederの値を初期値として設定。
本番環境でやるとやばい。とてもやばい。

モデルクラスを作成する

php artisan make:model User

マイグレーションも同時に作成できる。
php artisan make:model Flight --migration
php artisan make:model Flight -m

リクエストクラスを生成する

php artisan make:request UserRegistPost

ページネーションビューのカスタマイズ

php artisan vendor:publish --tag=laravel-pagination

resources/views/vendorディレクトリ以下にページネーションのビューファイルが生成される。
デフォルトではbootstrap-4.blade.phpが使用されている。
https://readouble.com/laravel/5.8/ja/pagination.html#customizing-the-pagination-view

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