tezu memo blog

日々行った作業をメモしていきます

Laravel Validation Test Requestクラスのみを対象

はじめに

バリデーションのテストをしたいのですが、 テスト 5.2 Laravel だと、bladeファイルも必要なので ちょっと違うんですよね

テスト対象

一からコードを書くのが面倒なので、以下を購入してイジって遊んでます

codecanyon.net

BlogRequestはjosh adminのコード

<?php

namespace App\Http\Requests;

use App\Http\Requests\Request;

class BlogRequest extends Request {

    /**
    * Determine if the user is authorized to make this request.
    *
    * @return bool
    */
    public function authorize()
    {
        return true;
    }

    /**
    * Get the validation rules that apply to the request.
    *
    * @return array
    */
    public function rules()
    {
        return [
            'title' => 'required|min:3',
            'content' => 'required|min:3',
            'blog_category_id' => 'required',
        ];
    }

}

テストケース

サービスコンテナにMockのリクエストをセットするのがポイント

<?php
use App\Http\Requests\BlogRequest;
use Illuminate\Http\Exception\HttpResponseException;
use Illuminate\Http\Request;

class BlogRequestTest extends TestCase
{
// php artisan make:test App\Http\Requests\BlogRequestTest

    /**
     * @return void
     * @test
     */
    public function 未入力()
    {
        try {
            // App::makeでvalidationが実行されてしまう。。
            App::make(BlogRequest::class);
        } catch (HttpResponseException $e) {
            $messages = $this->getMessages();
            $this->assertEquals('The title field is required.', $messages['title'][0]);
            $this->assertEquals('The content field is required.', $messages['content'][0]);
            $this->assertEquals('The blog category id field is required.', $messages['blog_category_id'][0]);
        } catch (Exception $e) {
            $this->fail("HttpResponseExceptionが発生すべき");
        }
    }

    /**
     * @return void
     * @test
     */
    public function 最小値_直前の値()
    {
        $this->bindRequest([
            'title' => '11',
            'content' => '22',
            'blog_category_id' => 1
        ]);

        try {
            App::make(BlogRequest::class);
        } catch (HttpResponseException $e) {
            $messages = $this->getMessages();
            $this->assertEquals('The title must be at least 3 characters.', $messages['title'][0]);
            $this->assertEquals('The content must be at least 3 characters.', $messages['content'][0]);
            $this->assertFalse(isset($messages['blog_category_id']));
        } catch (Exception $e) {
            $this->fail("HttpResponseExceptionが発生すべき");
        }
    }

    /**
     * @return void
     * @test
     */
    public function 最小値()
    {
        $this->bindRequest([
            'title' => '111',
            'content' => '222',
            'blog_category_id' => 1
        ]);

        try {
            App::make(BlogRequest::class);
        } catch (HttpResponseException $e) {
            $this->fail("全て正常値。HttpResponseExceptionは発生しない");
        } catch (Exception $e) {
            $this->fail("全て正常値。Exceptionは発生しない");
        }
    }

    /**
     * @param $params array
     */
    private function bindRequest($params)
    {
        // vendor/laravel/framework/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php
        // でサービスコンテナからrequestを取り出してRequestクラスにパラメータをセットしている
        //
        // $this->initializeRequest($request, $app['request']);
        //
        // protected function initializeRequest(FormRequest $form, Request $current)
        // $form->initialize(
        //    $current->query->all(), $current->request->all(), $current->attributes->all(),
        //    $current->cookies->all(), $files, $current->server->all(), $current->getContent()
        // );

        // サービスコンテナにMockのリクエストをセット
        App::instance('request', Request::create('', 'POST', $params, [], [], [], []));
    }

    /**
     * @return array
     */
    private function getMessages()
    {
        /**
         * @var \Illuminate\Support\ViewErrorBag $viewErrorBag
         * @var \Illuminate\Support\MessageBag $messageBag
         */
        $viewErrorBag = Session::get('errors');
        $messageBag = $viewErrorBag->getBag('default');
        return $messageBag->getMessages();
    }
}

Laravel Monolog Cascadeを使用してログレベル毎に出力するファイルを変更する

概要

ログレベル毎に出力するファイルを変更したい

Laravel標準ではdailyでログファイルをローテートは出来る程度

.env

APP_LOG=daily

storage/logs

laravel-2016-10-04.log
laravel-2016-10-05.log
laravel-2016-10-06.log

Monolog Cascadeを使用して、ログレベル毎に出力するファイルを変更する github.com

手順

インストール

composer.jsonに以下を追記後、composer updateを実行

"require": {
    "theorchard/monolog-cascade": "^0.4.0"
}

設定

https://github.com/theorchard/monolog-cascade Configuration structureを参考に作成

config/monolog.php

<?php

return [
    'version' => 1,

    'formatters' => [
        'dashed' => [
            'format' => "%datetime%-%channel%.%level_name% - %message% %context% %extra%\n",
            'include_stacktraces' => true
        ],
    ],
    'handlers' => [
        //-------------------------------------------------
        // Admin
        // ------------------------------------------------
        'admin_info' => [
            'class' => 'Monolog\Handler\RotatingFileHandler',
            'level' => 'INFO',
            'filename' => env('LOG_ADMIN_INFO_PATH', storage_path('logs/admin_info.log')),
            'maxFiles' => 5,
            'formatter' => 'dashed',
            'processors' => ['web_processor', 'introspection_processor', 'login_user_processor'],
        ],

        'admin_error' => [
            'class' => 'Monolog\Handler\RotatingFileHandler',
            'level' => 'ERROR',
            'filename' => env('LOG_ADMIN_ERROR_PATH', storage_path('logs/admin_error.log')),
            'maxFiles' => 5,
            'formatter' => 'dashed',
            'processors' => ['web_processor', 'introspection_processor', 'login_user_processor'],
        ],

        // WARNINGのみ出力
        '_admin_warning' => [
            'class' => 'Monolog\Handler\RotatingFileHandler',
            'level' => 'WARNING',
            'filename' => env('LOG_ADMIN_WARNING_PATH', storage_path('logs/admin_warning.log')),
            'maxFiles' => 5,
            'formatter' => 'dashed',
            'processors' => ['web_processor', 'introspection_processor', 'login_user_processor'],
        ],
        'admin_warning' => [
            'class' => 'Monolog\Handler\FilterHandler',
            'handler' => '_admin_warning',
            'minLevelOrList' => ['WARNING'],
        ],

        //-------------------------------------------------
        // Front
        // ------------------------------------------------
        'front_info' => [
            'class' => 'Monolog\Handler\RotatingFileHandler',
            'level' => 'INFO',
            'filename' => env('LOG_FRONT_INFO_PATH', storage_path('logs/front_info.log')),
            'maxFiles' => 5,
            'formatter' => 'dashed',
            'processors' => ['web_processor', 'introspection_processor'],
        ],

        'front_error' => [
            'class' => 'Monolog\Handler\RotatingFileHandler',
            'level' => 'ERROR',
            'filename' => env('LOG_FRONT_ERROR_PATH', storage_path('logs/front_error.log')),
            'maxFiles' => 5,
            'formatter' => 'dashed',
            'processors' => ['web_processor', 'introspection_processor'],
        ],

        // WARNINGのみ出力
        '_front_warning' => [
            'class' => 'Monolog\Handler\RotatingFileHandler',
            'level' => 'WARNING',
            'filename' => env('LOG_FRONT_WARNING_PATH', storage_path('logs/front_warning.log')),
            'maxFiles' => 5,
            'formatter' => 'dashed',
            'processors' => ['web_processor', 'introspection_processor'],
        ],
        'front_warning' => [
            'class' => 'Monolog\Handler\FilterHandler',
            'handler' => '_front_warning',
            'minLevelOrList' => ['WARNING'],
        ],
    ],
    'processors' => [
        // IP,URL,HTTP METHOD,referrer
        'web_processor' => [
            'class' => 'Monolog\Processor\WebProcessor'
        ],
        // 出力ファイル名、出力行
        'introspection_processor' => [
            'class' => 'Monolog\Processor\IntrospectionProcessor',
            'skipClassesPartials' => [
                'Monolog\\',
                'Illuminate\\',
            ],
        ],
        // ログイン情報
        'login_user_processor' => [
            'class' => 'App\Services\Monolog\Processor\LoginUserProcessor'
        ],
    ],
    'loggers' => [
        // このキーをMonologSetting::change()で指定
        'admin' => [
            'handlers' => ['admin_info', 'admin_error', 'admin_warning']
        ],
        'front' => [
            'handlers' => ['front_info', 'front_error', 'front_warning']
        ]
    ]
];

Monolog Cascadeに差し替え

LaravelのMonolog Loggerと差し替える

app/Services/Monolog/MonologSetting.php

<?php
namespace App\Services\Monolog;

use Cascade\Cascade;
use Illuminate\Log\Writer;
use Log;

class MonologSetting
{
    /**
     * ログ設定を変更する
     * @param  string $logName 変更名 config/monolog.phpのhandlersキー
     */
    public static function change($logName)
    {
        // 登録済みインスタンスを削除
        Log::clearResolvedInstances();
        app()->instance('log', new Writer(Cascade::getLogger($logName)));
    }
}

Laravelの登録方法を参考にした

https://github.com/laravel/framework/blob/5.3/src/Illuminate/Foundation/Bootstrap/ConfigureLogging.php

<?php

namespace Illuminate\Foundation\Bootstrap;

use Illuminate\Log\Writer;
use Monolog\Logger as Monolog;
use Illuminate\Contracts\Foundation\Application;

class ConfigureLogging
{
    /**
     * Register the logger instance in the container.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return \Illuminate\Log\Writer
     */
    protected function registerLogger(Application $app)
    {
        $app->instance('log', $log = new Writer(
            new Monolog($app->environment()), $app['events'])
        );

        return $log;
    }
}

Facadeを追加

<?php
namespace App\Facades;

use Illuminate\Support\Facades\Facade;

class MonologSetting extends Facade {

    protected static function getFacadeAccessor() { return 'monolog_setting'; }

}

config\app.php

<?php

return [
    'aliases' => [
        'MonologSetting' => App\Facades\MonologSetting::class, // ここを追加
    ],
];

サービスプロバイダー

MonologSettingのバインドとFront/Admin/コンソールでロガーを切替

artisanコマンドで雛型作成

php artisan make:provider MonologSettingServiceProvider
Provider created successfully.
<?php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use MonologSetting;
use Cascade\Cascade;

class MonologSettingServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        Cascade::fileConfig(config_path('monolog.php'));

        $changeName = null;
        // コマンド実行時とwebアクセス時で設定を変更
        if (php_sapi_name() == 'cli') {
            $changeName = 'console';
        } elseif (str_is('/admin/*', env('REQUEST_URI'))) {
            $changeName = 'admin';
        }
        if (!is_null($changeName)) {
            MonologSetting::change($changeName);
        }
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        // キーはMonologSettingファサードのgetFacadeAccessor()の戻り値と一致
        \App::bind('monolog_setting', function()
        {
            return new \App\MonologSetting;
        });
    }
}

config\app.php

'providers' => [

    /*
     * Application Service Providers...
     */
    App\Providers\AppServiceProvider::class,
    App\Providers\AuthServiceProvider::class,
    App\Providers\EventServiceProvider::class,
    App\Providers\RouteServiceProvider::class,
    App\Providers\MonologSettingServiceProvider::class, // add
],

ログインユーザ名を出力

認証認可はSentinel Manual :: Cartalystを使用

<?php

namespace App\Services\Monolog\Processor;

use Sentinel;

class LoginUserProcessor
{
    /**
     * @param  array $record
     * @return array
     */
    public function __invoke(array $record)
    {
        if (Sentinel::check()) {
            $user = Sentinel::getUser();
            $record['extra']['username'] = $user->getUserLogin();
        }

        return $record;
    }
}

結果

変更前

storage/logs/laravel-2016-10-13.log

[2016-10-13 07:02:52] local.WARNING: array (
  '_method' => 'PUT',
  '_token' => 'E5ola1t7HaX79kfkEcZW3oNxhdYau10SdxDUuBDc',
  'first_name' => '',
  'last_name' => '',
  'email' => 'admin@xxxxx.xxx',
  'password' => 'xxxxx',
  'password_confirm' => '',
)  
[2016-10-13 07:02:52] local.WARNING: array (
  'first_name' => 
  array (
    0 => 'The first name field is required.',
  ),
  'last_name' => 
  array (
    0 => 'The last name field is required.',
  ),
  'email' => 
  array (
    0 => 'The email has already been taken.',
  ),
)  
[2016-10-13 07:05:51] local.ERROR: exception 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException' in \vendor\laravel\framework\src\Illuminate\Routing\RouteCollection.php:161
Stack trace:
#0 \vendor\laravel\framework\src\Illuminate\Routing\Router.php(821): Illuminate\Routing\RouteCollection->match(Object(Illuminate\Http\Request))

変更後

errorレベル

storage/logs/front_error-2016-10-13.log

2016-10-13 07:31:04-front.ERROR - exception 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException' in vendor\laravel\framework\src\Illuminate\Routing\RouteCollection.php:161
Stack trace:
#0 vendor\laravel\framework\src\Illuminate\Routing\Router.php(821): Illuminate\Routing\RouteCollection->match(Object(Illuminate\Http\Request))

warnレベル

storage/logs/front_warning-2016-10-13.log

2016-10-13 07:31:45-front.WARNING - array (
  '_method' => 'PUT',
  '_token' => 'E5ola1t7HaX79kfkEcZW3oNxhdYau10SdxDUuBDc',
  'first_name' => '',
  'last_name' => '',
  'email' => 'admin@xxxxx.xxx',
  'password' => 'xxxxx',
  'password_confirm' => '',
)
2016-10-13 07:31:45-front.WARNING - array (
  'first_name' => 
  array (
    0 => 'The first name field is required.',
  ),
  'last_name' => 
  array (
    0 => 'The last name field is required.',
  ),
  'email' => 
  array (
    0 => 'The email has already been taken.',
  ),
)

WindowsでPhpStormとXdebugを動かす

少しハマったので整理してみました

環境

  • WindowsVista(32bit版)
  • Apache 2.2.11
  • PHP 5.3.25(Thread Safe)
  • PhpStorm 6.0.2


最初からこのサイトを見ていたら、すんなり設定出来たかも知れません
Zero-configuration Web Application Debugging with Xdebug and PhpStorm - PhpStorm - Confluence

Apache - PHP連携は省略します、、、

Xdebug

http://xdebug.org/download.php

Vista32bit版&PHP 5.3.25(Thread Safe)を使用しているので、PHP 5.3 VC9 TS (32 bit)を選択
http://www.xdebug.org/files/php_xdebug-2.2.3-5.3-vc9.dll

f:id:tezu35:20130531140642j:plain


ダウンロードしたdllをPHPのインストールdir(%PHP_HOME%)のextに移動

C:\Program Files\PHP\ext>ls php_xdebug-2.2.3-5.3-vc9.dll
php_xdebug-2.2.3-5.3-vc9.dll


php.ini

設定

以下を追加
xdebug.profiler_output_dirは存在dirであれば何処でも

[xdebug]
zend_extension="C:\Program Files\PHP\ext\php_xdebug-2.2.3-5.3-vc9.dll"
xdebug.remote_enable=1
xdebug.remote_host=127.0.0.1
xdebug.remote_port=9000
xdebug.remote_handler=dbgp
xdebug.profiler_enable=1
xdebug.profiler_output_dir=C:\tmp\xdebug

確認

phpinfo();でも出来ますが、コマンドでもいけます

>php -v
PHP 5.3.25 (cli) (built: May  8 2013 19:16:49)
Copyright (c) 1997-2013 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2013 Zend Technologies
    with Xdebug v2.2.3, Copyright (c) 2002-2013, by Derick Rethans

 
  

PhpStorm

File→Settings→PHPを選択し、インストールしたPHPInterpreterを指定
f:id:tezu35:20130531140632j:plain
 
File→Settings→PHP→Debugを選択し、XdebugのDebug portがphp.iniのxdebug.remote_portと一致することを確認後、
画面下部の"Use debugger bookmarklets to initiate debugger from your favorite browser"のリンクを選択
f:id:tezu35:20130531140636j:plain
 
選択後、http://www.jetbrains.com/phpstorm/marklets/に遷移
画面下部のStart debuggerとStop debuggerをブックマーク保存
f:id:tezu35:20130531140639j:plain
 
 

実行

1. Run→Start Listen PHP Debug Connectionsを選択
ToolbarでもOK
f:id:tezu35:20130531141646j:plain
2. ブレイクポイントを設定 
3. (対象画面を表示した状態で)ブックマーク保存したStart debuggerを選択
4. 画面操作
止まった!
f:id:tezu35:20130531142835j:plain

データベースをDiffするツール liquibase

設計書の突き合せが面倒なので探していたら良いツールがありました

Liquibase | Database Refactoring | Liquibase
 

ダウンロード

現時点の最新2.0.5を取得
http://github.com/downloads/liquibase/liquibase/liquibase-2.0.5-bin.zip

今回はAntを使って実行しています
こちらからDL出来ます → Apache Ant - Binary Distributions
 

インストール

%ANT_HOME%\lib下にliquibase.jarとJDBCドライバをコピー(PostgreSQLを使ってます)

$ ls -l
-rwx------+ 1 Administrators mkpasswd    920 Apr  3 13:59 build.xml
-rwx------+ 1 Administrators mkpasswd 926426 May  2  2012 liquibase.jar
-rwx------+ 1 Administrators mkpasswd 579785 Apr  2 10:16 postgresql-9.2-1002.jdbc4.jar

 

パラメータ(build.xml

こんな感じです
property要素のvalue属性でDatabaseを指定しています

<?xml version="1.0" encoding="UTF-8"?>
<project name="database-diff" default="default" basedir=".">

  <!-- ver3.3.0とver3.3.2の比較 -->
  <property name="url"value="jdbc:postgresql://localhost:5434/ver332"/>
  <property name="referenceeUrl" value="jdbc:postgresql://localhost:5434/ver330"/>

  <echo message="url is ${url}"/>
  <echo message="referenceeUrl is ${referenceeUrl}"/>

  <path id="classpath">
    <fileset dir="." includes="*.jar"/>
  </path>

  <target name="default">
    <taskdef resource="liquibasetasks.properties">
      <classpath refid="classpath"/>
    </taskdef>
    <diffDatabase
        driver="org.postgresql.Driver"
        url="${url}"
        username="postgres"
        password="postgres"
        referenceUrl="${referenceeUrl}"
        referenceUsername="postgres"
        referencePassword="postgres"
        outputFile="outputfile.txt"
        classpathref="classpath">
    </diffDatabase>
  </target>

</project>

 

実行

build.xmlで指定したoutputfile.txtが作成されてます

$ ant
Buildfile: build.xml
     [echo] url is jdbc:postgresql://localhost:5434/ver332
     [echo] referenceeUrl is jdbc:postgresql://localhost:5434/ver314

default:

BUILD SUCCESSFUL

$ ls -l outputfile.txt
-rwx------+ 1 Administrators mkpasswd 29746 4月   4 17:34 outputfile.txt
Total time: 20 seconds

 

結果

Reference Database: postgres @ jdbc:postgresql://localhost:5434/ver314
Target Database: postgres @ jdbc:postgresql://localhost:5434/ver332
Product Name: EQUAL
Product Version: EQUAL
Missing Tables: 
     AAAAATbl
     BBBBBTbl
     CCCCCTbl
Unexpected Tables: 
     DDDDDTbl
     EEEEETbl
     FFFFFTbl
Missing Views: NONE
Unexpected Views: NONE
Changed Views: NONE
Missing Columns: 
     ZZZZZTbl.name
     ZZZZZTbl.tel
Unexpected Columns: 
     YYYYYTbl.zipcode
     YYYYYTbl.address

 

おまけ

上記build.xmlは修正済みですが、マニュアル(http://www.liquibase.org/ja/manual/diffdatabase_ant_task)通りに作成するとエラーが発生します

$ ant
BUILD FAILED
build.xml:27: diffDatabase doesn't support the "baseUrl" attribute

Javadochttp://www.jarvana.com/jarvana/view/org/liquibase/liquibase-core/2.0-rc5/liquibase-core-2.0-rc5-javadoc.jar!/index.html?liquibase/integration/ant/DiffDatabaseTask.html)を確認したところ、プロパティの名称がbaseXXXからreferenceXXXに変更されてますね、、

Antのマニュアルの修正が漏れているみたいです

Flexible Renamer ファイル名変更 正規表現で

ググっても期待する結果がヒットしなかったので

先頭の文字を変更

例)CT001_00_mlt.jpgをSAMPLE01_00_mlt.jpgに変更

検索 CT001(.*)
置換 SAMPLE02\1

画面
f:id:tezu35:20130305135253j:plain

 

中間の文字を変更

例 SAMPLE02_ct001_01_mlt.jpgをSAMPLE02_BLU_01_mlt.jpgに変更

検索 (.*)ct001(.*)
置換 \1BLU\2

画面
f:id:tezu35:20130305135258j:plain