CakePHP3 グループの中の最大値を取得する

PHP

CakePHP3になり、クエリービルダーとかに変わって、ぐぇぇと言いながら取り組んでいます。

ここに、Shipmentというモデルと、1対他の関係にある、Deliveryというモデルがあります。
Shipment has many Deliveries という関係です。
Deliveryには、重量を示すweightというフィールドがあります。
で、Shipmentから、DeliveryをShipmentでまとめた場合、同じShipmentの中のDeliveryのweightの最大値を求める時、どうしたらいいのでしょうか。

データベースアクセス & ORM クエリビルダ
を見ながら、試行錯誤で次のようにできました。

 //ShipmentsControkker内で
  $query = $this->Shipments->find()->contain([
    'Deliveries'=> function ($q) {
 		 return $q
 		->select(['id','offer_id', 'weight'])
 		->select(['max_weight' => $q->func()->max('weight')])
 		->group('offer_id');
             },
    ->select(['id', 'status']);
 debug($query->toArray());
 

CakePHP3 multiCheckboxの使い方

PHP
CakePHP3

複数のチェックボックスを表示して、複数選択させたい場合、CakePHP3でどうすればいいか、すぐわからなかったので、書いておきます。

下記APIには、複数チェックボックスを作るための

http://api.cakephp.org/3.2/class-Cake.View.Helper.FormHelper.html#_multiCheckbox

 multiCheckbox( string $fieldName , array|Traversable $options , array $attributes [] ) 

というフォームヘルパーの関数が記載されています。

次のように利用します。

 $options = [
 	(int) 1 => 'パレット',
 	(int) 2 => '手積み'
 ]
 echo $this->Form->multiCheckbox( 'WorkTypes.work_type_ids' ,$options);

次のようなHTMLが出力されます。

 <div class="checkbox">
   <label for="matchconditionworktypes-work-type-ids-1">
     <input type="checkbox" id="matchconditionworktypes-work-type-ids-1" value="1" name="MatchConditionWorkTypes[work_type_ids][]">
        パレット
   </label>
 </div>
 <div class="checkbox">
  <label for="matchconditionworktypes-work-type-ids-2">
    <input type="checkbox" id="matchconditionworktypes-work-type-ids-2" value="2" name="MatchConditionWorkTypes[work_type_ids][]">
       手積み
   </label>
 </div>

CakePHP3 beforeFilter(Event $event)のエラー

PHP
CakePHP3

コントローラーごとに、認証なしにアクセスできるメソッドを、許可しておくのに下記のようにやると思いますが、

 //AppController内
         $this->loadComponent('Auth', [
            'loginRedirect' => [
                'controller' => 'Offers',
                'action' => 'index'
            ],
            'logoutRedirect' => [
                'controller' => 'Users',
                'action' => 'add',
                'home'
            ]
        ]);
 //個別のController内
 public function beforeFilter(Event $event)
 	{
 		parent::beforeFilter($event);
 		$this->Auth->allow(['cancelFromSp']);
 	}

次のようなエラーが出ちゃうことがあります。

Strict (2048): Declaration of App\Controller\MoresController::beforeFilter() should be compatible with App\Controller\AppController::beforeFilter(Cake\Event\Event $event) [APP/Controller\MoresController.php, line 12]

Warning (4096): Argument 1 passed to App\Controller\MoresController::beforeFilter() must be an instance of App\Controller\Event, instance of Cake\Event\Event given, called in C:\xampp2\htdocs\cake\vendor\cakephp\cakephp\src\Event\EventManager.php on line 386 and defined [APP/Controller\MoresController.php, line 30]

あれれれー、なんか変数の設定の仕方がおかしかったかな??
とか思っちゃいますが、原因は簡単なことで、

 use Cake\Event\Event;

が抜けてるとこうなっちゃうんですねー。

コントローラーの最初に、上記のEventをuseするように追加します。

 <?php
 namespace App\Controller;
 
 use App\Controller\AppController;
 use Cake\Event\Event;
 
 class MoresController extends AppController
 {
 
    
    public function beforeFilter(Event $event)
 	{
 		parent::beforeFilter($event);
 		$this->Auth->allow(['cancelFromSp']);
 	}
 
    

CakePHP3 Plugin DebugKit could not be found.

PHP
CakePHP3.2.11

ことの発端は、簡単なメール送信ができないことでした。

 $email = new Email();
 $email->from(['hogehoge@onlineconsultant.jp'=>'OC']			  ->to('geho@onlineconsultant.jp')
 ->subject('お知らせ');

で、メール送信できないのです。

簡単なメソッドで試すとできるんですけど、DBのアップデート後にメール送信を実行すると、なぜかメール送信できない。

で、

 /logs/error.log

を見てみると

Error: [Cake\Core\Exception\MissingPluginException] Plugin DebugKit could not be found.

ってエラーが出てるんですね。

DebugKitは、/vendor/cakephp の中にdebug_kitという名前であるんですけどね。

下記のサイトに

http://stackoverflow.com/questions/18906869/cakephp-searching-for-debugkit-at-wrong-path

debug_kitのパーミッションを変更するべしと書いてありました。

その通り、パーミッションを変更したら、このエラーもログに出なくなり、メールも送信できるようになりました!

CakePHP3 PHPUnit テーブルが消える

PHP
CakePHP 3.2.3

CakePHP3 PHPUnit テーブルが消える

CakePHPでのユニットテストに取り組んでおります。
まだまだやり始めたばっかりなので、いろいろと間違っているところもあると思いますが、詳しい人はぜひ教えてください。

さて、いきなり全体のテストではなく、まずは試しにモデルのテストを作ってみます。

 //テストする対象のモデル
 //Model/Table/OffersTable.php
 class OffersTable extends Table
 {
  //前略
  public function findApproved(Query $query, array $options){
 
    	$query->where([
            'Offers.status' => 'approved'
        ]);
        $query->select(['id', 'status']);
        return $query;
    }
  //後略
 }
 //上記のメソッドのテストを作ります
 //tests/TestCase/Model/Table/OffersTableTest.php
 <?php
 namespace App\Test\TestCase\Model\Table;
 
 use App\Model\Table\OffersTable;
 use Cake\ORM\TableRegistry;
 use Cake\TestSuite\TestCase;
 
 /**
 * App\Model\Table\OffersTable Test Case
 */
 class OffersTableTest extends TestCase
 {
 	
    public $Offers;
    public $fixtures = [
        'app.offers'
    ];
 
    public function setUp()
    {
        parent::setUp();
        $config = TableRegistry::exists('Offers') ? [] : ['className' => 'App\Model\Table\OffersTable'];
        $this->Offers = TableRegistry::get('Offers', $config);
    }
 
    public function tearDown()
    {
        unset($this->Offers);
 
        parent::tearDown();
    }
 
    public function testInitialize()
    {
        $this->markTestIncomplete('Not implemented yet.');
    }
 
    public function testValidationDefault()
    {
        $this->markTestIncomplete('Not implemented yet.');
    }
 
    public function testBuildRules()
    {
        $this->markTestIncomplete('Not implemented yet.');
    }
    
 
   public function testFindApproved()
    {
        $query = $this->Offers->find('approved');
        $this->assertInstanceOf('Cake\ORM\Query', $query);
        $result = $query->hydrate(false)->toArray();
        $expected = [
            ['id' => 2, 'status' => 'approved'],
        ];
        debug($result);
        $this->assertEquals($expected, $result);
    }
 
 }

設定ファイルに、テスト用のDBの設定を書いておきます。

    'Datasources' => [
 
        /**
         * The test connection is used during the test suite.
         */
        'test' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            'username' => 'my_app',
            'password' => 'secret',
            'database' => 'test_myapp',
            'encoding' => 'utf8',
            'timezone' => 'UTC',
            'cacheMetadata' => true,
            'quoteIdentifiers' => false,
            'log' => false,
        ],
    ],

サーバーで、次のようにphpunitを実行します。

 phpunit --filter testFindApproved tests/TestCase/Model/Table/OffersTableTest

次のように、Failureが返ってきます。

 PHP Warning:  Module 'intl' already loaded in Unknown on line 0
 PHPUnit 5.2.9 by Sebastian Bergmann and contributors.
 
 F 1 / 1 (100%)/tests/TestCase/Model/Table/OffersTableTest.php (line 88)
 ########## DEBUG ##########
 []
 ###########################
 
 
 Time: 149 ms, Memory: 12.00Mb
 
 There was 1 failure:
 
 1) App\Test\TestCase\Model\Table\OffersTableTest::testFindApproved
 Failed asserting that two arrays are equal.
 --- Expected
 +++ Actual
 @@ @@
 Array (
     0 => Array (
 -        'id' => 2
 +        'id' => 24
         'status' => 'approved'
     )
 )
 
 /var/www/html/gildotcom/tests/TestCase/Model/Table/OffersTableTest.php:89
 
 FAILURES!
 Tests: 1, Assertions: 2, Failures: 1.

ま、これは予想されたことなのでよいのですが、問題は、test_myappデータベースから、offers というテーブルが消えてしまったことです。

は??(・A・) Oh My God!!となりますね。私の場合は、別に開発中のサーバーだったので、無問題なのですが…。

犯人は、こ・い・つ!

    public $fixtures = [
        'app.offers'
    ];

Fixtureというやつですね。
これを指定しているので、

 tests/Fixture/OffersFixture.php

にある、次のフィクスチャーにあるデータが実行されていたようです。

 <?php
 namespace App\Test\Fixture;
 
 use Cake\TestSuite\Fixture\TestFixture;
 
 /**
 * OffersFixture
 *
 */
 class OffersFixture extends TestFixture
 {
    // @codingStandardsIgnoreStart
    public $fields = [
        'id' => ['type' => 'string', 'length' => 64, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
        'status' => ['type' => 'string', 'length' => 16, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
        'winner_id' => ['type' => 'string', 'length' => 64, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
        'work_type_id' => ['type' => 'integer', 'length' => 11, 'unsigned' => false, 'null' => false],
   //以下略        
    ];
    // @codingStandardsIgnoreEnd
 
    public $records = [
        [
            'id' => '24a1f938-c898-4376-8a05-b8d6a614dda7',
            'status' => 'approved',
            'winner_id' => 'Lorem ipsum dolor sit amet',
            'work_type_id' => 1,
            'created' => '2016-02-02 11:22:24',
            'modified' => '2016-02-02 11:22:24'
        ],
    ];
 }

そんで、そのあとDBのテーブルがDropされていた。と。

フィクスチャーはテストの際に、実際のデータに影響を与えないように、仮のテスト用データを作れるという機能です。
上記では、class OffersFixtureでデータのフィールドと、実際のデータを作っているわけですね。

下記の公式ページに
http://book.cakephp.org/3.0/en/development/testing.html#creating-a-test-method

CakePHP performs the following during the course of a fixture based test case:
・Creates tables for each of the fixtures needed.
・Populates tables with data, if data is provided in fixture.
・Runs test methods.
・Empties the fixture tables.
・Removes fixture tables from database.

要は
「そのテーブル 消えるよ」
と言ってくれていたのですが、仮のデータが消されるのかと思ってましたよ…(つД`)
本当に、テスト用のデータベースのテーブルを消すんですね!!Cake先輩!!

ちなみに、bake で自動でテストコード作ると、このように自動でFixtureを作ってくれて、Fixtureを使うようになっています。

bakeしたみなさん、ご注意を!!
app.phpのテストDBの設定を、
「本物のデータでテストしてみよー」
とか気軽に考えて、気軽に実行すると、データが全部消えることになりますから。

ということで、public $fixtures をOffersTabteTest.phpから消去し、再び

 phpunit --filter testFindApproved tests/TestCase/Model/Table/OffersTableTest

すると、test_myAppDBからoffersテーブルを読み込み、次のように結果がOKとなります。

 PHP Warning:  Module 'intl' already loaded in Unknown on line 0
 PHPUnit 5.2.9 by Sebastian Bergmann and contributors.
  1 / 1 (100%)/tests/TestCase/Model/Table/OffersTableTest.php (line 87)
 ########## DEBUG ##########
 [
        (int) 0 => [
                'id' => (int) 2,
                'status' => 'approved'
        ]
 ]
 ###########################
 
 Time: 189 ms, Memory: 12.00Mb
 
 OK (1 test, 2 assertions)

テーブルもきえませんでした! ⊂(^-^)⊃