Azure Functions: `Azure Functions Core Tools` で TypeScript/QueueTrigger の関数を `func start` 時に `Value cannot be null. (Parameter 'provider')` エラー

バージョン

  • OS: macOS Catalina 10.15.7
  • Azure Functions Core Tools: 3.0.3785
  • node: 14.17.5

遭遇したエラー

Value cannot be null. (Parameter 'provider')

$ func start

Azure Functions Core Tools
Core Tools Version:       3.0.3785 Commit hash: db6fe71b2f05d09757179d5618a07bba4b28826f  (64-bit)
Function Runtime Version: 3.2.0.0

[2021-10-01T13:16:19.680Z] Cannot create directory for shared memory usage: /dev/shm/AzureFunctions
[2021-10-01T13:16:19.680Z] System.IO.FileSystem: Access to the path '/dev/shm/AzureFunctions' is denied. Operation not permitted.
[2021-10-01T13:16:20.590Z] A host error has occurred during startup operation '974818a2-159a-4925-a571-4d4330134e2b'.
[2021-10-01T13:16:20.590Z] Microsoft.Azure.WebJobs.Script: Did not find functions with language [node].
Value cannot be null. (Parameter 'provider')

エラー原因

  • yarn build せずに func start していた

  • function.json を下記のように設定し func start
//function.json
{
  //...
  "scriptFile": "../QueueTrigger/index.ts"
}

  • function.json を下記のように設定し、yarn build 後に func start
{
  //...
  "scriptFile": "../dist/QueueTrigger/index.js"
}

ハマった事

  • Value cannot be null. (Parameter 'provider') についてはいくつも issue が上がっており、総じて ExtensionBundle 起因するエラー、解消方法は、host.json から extensionBundle を削除、というものだった。

  • 今回のケースでは ExtensionBundle とは関連はなかった にも関わらず、上記の issue で示されている work around で試行錯誤してしまった。

解決のきっかけ

  • JavaScript/QueueTrigger ではエラーにならなかった為、index.ts に起因するエラーではないか、とあたりがついた。

CakePHP 4.x: .env ファイルを有効化する (留意点あり)

留意点をメモ

ドキュメント

有効化方法

// if (!env('APP_NAME') && file_exists(CONFIG . '.env')) {
//     $dotenv = new \josegonzalez\Dotenv\Loader([CONFIG . '.env']);
//     $dotenv->parse()
//         ->putenv()
//         ->toEnv()
//         ->toServer();
// }
FOO=bar
BAR=baz
  • config/app.php 内で env('FOO') のように環境変数を取得して利用する
    'Hoge' => [
        'foo' => env('FOO', null), 
    ],

留意点

$ composer remove josegonzalez/dotenv
$ composer require josegonzalez/dotenv:^3.2

CakePHP 4.x: Crud\Action\EditAction のレスポンスに data を含める

class UsersController extends AppController
{
    public function beforeFilter(EventInterface $event): void
    {
        parent::beforeFilter($event);
    }

    public function edit($id = null)
    {
        $this->Crud->on('afterSave', function (Event $event) {
            /** @var \App\Model\Entity\User $entity */
            $entity = $event->getSubject()->entity;
            $this->Crud->action()->setConfig('api.success.data.raw', $entity->toArray());
        });

        return $this->Crud->execute();
    }
}
  • 下記のようなレスポンスが返る
{
  "success": true,
  "data": {
    "id": 1,
    "name": "hoge",
    "created": "2021-08-17T08:30:09+00:00",
    "modified": "2021-08-30T10:43:49+00:00"
  }
}

CakePHP 4.x: FriendsOfCake/crud の コアイベント と カスタムリスナー の追加方法

前提知識

Crud プラグインのコアイベント

コアイベントの利用方法

サンプル

use Crud\Action\EditAction

class UsersController extends ApiAppController
{
    public function beforeFilter(EventInterface $event)
    {
        parent::beforeFilter($event);

        $this->Crud->mapAction('edit', [
            'className' => EditAction::class,
            ],
        ]);
    }

    public function edit(?string $id = null)
    {
        // ...

        //  Crud の EventManager に 無名リスナー を登録し、
        //  Crud.beforeHandle(=Crudコアイベント)に、無名関数(=Closure) を callback としてフックする
        $this->Crud->on('beforeHandle', function (Event $event) { //第1引数: イベント名、第2引数: callback
          // callback は EventInterface を実装したクラスのインスタンスを受け取る
        });

        // 同様に Crud.beforeSave に Closure をフック
        $this->Crud->on('beforeSave', function (Event $event) {

          // Closure 内で、Crud Subject から取得可能な key は下記参照
          //  https://crud.readthedocs.io/en/latest/events.html#crud-beforesave

          // 例: Crud Subject から UserEntity を取得
          $entity = $event->getSubject()->entity;

          // ...
        });

        // 同様に Crud.afterSave に Closure をフック
        $this->Crud->on('afterSave', function (Event $event) {

          $entity = $event->getSubject()->entity;

          // ...
        });

        return $this->Crud->execute();
    }
}

カスタムイベントリスナーの追加

  • \Crud\Listener\BaseListener を継承した Listerクラスを追加し、Controller の initialize() 内で $this->Crud->addListener() する
  • ドキュメント

サンプル

use Cake\Event\EventInterface;
use Crud\Listener\BaseListener;

// カスタムイベントリスナー
class CustomListener extends BaseListener
{
    // CustomListener が処理するイベントを定義
    public function implementedEvents(): array
    {
        return [
            'Crud.beforeRender' => [
              'callable' => 'beforeRender',
              'priority' => 80, //優先順位の設定 See: https://book.cakephp.org/4/ja/core-libraries/events.html#event-priorities
            ],
        ];
    }

    // Crud.beforeRenderイベント を 受信時に実行する処理
    public function beforeRender(EventInterface $event)
    {
      // ...
    }
class ApiAppController extends Controller
{
    public function initialize(): void
    {
        $this->loadComponent('Crud.Crud', [
            'actions' => [
                'Crud.Index',
                'Crud.Add',
                'Crud.Edit',
                'Crud.View',
                'Crud.Delete',

            ]
        ]);

        $this->Crud->addListener('Crud.Api');
        $this->Crud->addListener('Crud.ApiPagination');

        // 追加
        $this->Crud->addListener('CustomListener', CustomListener::class);
    }
}

CakePHP 4: イベントシステムの概要

概要

  • CakePHPのイベントシステム

    • モデル・ビヘイビアー・コントローラー・ビュー・ヘルパーのコールバックの心臓部
    • why
      • クラスの結合度を下げる
      • コードの関心事を明確に分離させる
    • how
      • Observer パターン
        • オブジェクトがイベントを発行し、リスナーに対して、状態変化を通知するパターン
  • イベント

    • Cake\Event\EventInterface を実装したクラスのインスタンス
    • 全てのイベントリスナーに行き渡る
      • イベントに関する情報を持ち、任意のポイントでイベントの伝播を止めることができる
  • コアイベント

  • イベントリスナー

    • Cake\Event\EventListenerInterface を実装したクラスのインスタンス
    • イベント毎の処理を実装する
    • イベントを受信し、イベントに応じた処理を実行する

サンプル

リスナーの実装

use Cake\Event\EventListenerInterface;

// イベント毎のcallbackを登録したいクラスに対し、EventListenerInterface インターフェイスを実装
class HogeLister implements EventListenerInterface
{
    // HogeLister が処理するイベント名を定義
    public function implementedEvents()
    {
        return [
            'Model.Order.afterPlace' => 'doHoge',
        ];
    }

    // Model.Order.afterPlace イベント受信時に実行する処理
    public function doHoge($event, $order)
    {
        // ...
    }
}

リスナーの登録

// イベントリスナーを生成
$hogeListener = new HogeListener();

// Orderモデルの EventManager にリスナーを登録
$this->Orders->getEventManager()->on($hogeListener);

イベントのディスパッチ

use Cake\Event\Event;

// カートシステム の 注文モデル
class OrdersTable extends Table
{
    // 注文の作成
    public function place($order)
    {
        // 注文の作成
        if ($this->save($order)) {
            // カートを空にする
            $this->Cart->remove($order);

            /**
             * イベントを発行し、リスナーに通知
             */
            // イベントオブジェクト(afterPlaceイベント)を生成
            $event = new Event('Model.Order.afterPlace', $this, [
                'order' => $order
            ]);
            // リスナーにイベントを通知
            //   この例では、Orderモデルの EventManager に HogeLister が登録されているので、
            //   Model.Order.afterPlaceイベントに対応する処理(=HogeLister::doHoge)が実行される
            $this->getEventManager()->dispatch($event);
            return true;
        }
        return false;
    }
}

CakePHP 4.x: hasMany, belongsToMany でアソシエーションしたテーブルのレコードを contain で取得する際は、JOIN でなく、別のSQLで取得している

アソシエーションしたテーブルのレコードを contain して取得する際に実行される SQL は下記のように異なる

  • hasOne の場合は JOIN で取得
  • hasMany は別SQLで取得

追記

ドキュメント

hasOne の場合

class UsersTable extends Table
{
    public function initialize(array $config)
    {
        $this->hasOne('Addresses');
    }
}

$UsersTable->find('all')->contain(['Addresses']); を実行した場合は、JOIN で取得される。

SELECT * FROM users INNER JOIN addresses ON addresses.user_id = users.id;

hasMany の場合

class ArticlesTable extends Table
{
    public function initialize(array $config)
    {
        $this->hasMany('Comments');
    }
}

$ArticlesTable->find('all')->contain(['Comments']); を実行した場合は、別SQLで取得される。

SELECT * FROM articles;
SELECT * FROM comments WHERE article_id IN (1, 2, 3, 4, 5);

(※アソシエーションの設定で、'strategy' => 'subquery' を設定した場合は下記)

SELECT * FROM articles;
SELECT * FROM comments WHERE article_id IN (SELECT id FROM articles);

CakePHP 4.x: FriendsOfCake/crud の Config の api.success.data キー

api.success.data.row を指定する場合の例

下記のような感じで

// \Crud\Action\BaseAction を継承したカスタムアクション
class HogeAction extends BaseAction
{
    protected $_defaultConfig = [
        // ...
        'api' => [
            'methods' => ['get'],
            'success' => [
                'code' => 200,
            ],
           // ...
        ],
    ];

    protected function _get()
    {
        //...

        /** @var HogesTable $HogesTable */
        $HogesTable = $this->_table();

        /** @var \Cake\Database\Query $hoges */
        $hoges = $HogesTable()->find()->where(...);

        $fuga = ['a', 'b', 'c'];

        $row = [
          'hoges' => $hoges,
          'fuga' => $fuga,
        ];

        $this->setConfig('api.success.data.raw', $row);

        $subject = $this->_subject();
        $subject->set([
            'success' => true,
        ]);

        $this->_trigger('beforeRender', $subject);
    }

こんな感じのレスポンスを返せる

string(xxx) "{
    "success": true,
    "data": {
        "hoges": [ // HogeEntity の配列
            // ...
        ],
        "fuga": [
            "a",
            "b",
            "c"
        ]
    }
}"

備考: api.success.data.raw に 「文字列の数字をキーにした配列」 を設定する事は出来ない

こんな感じにしてしまうと、

        $row = [
          '1' => $hoges,
          '2' => $fuga,
        ];

        $this->setConfig('api.success.data.raw', $row);

Crud\Listener\ApiListener::_expandPath の第二引数に int が渡り、TypeError になる。

[TypeError] Crud\Listener\ApiListener::_expandPath(): Argument #2 ($path) must be of type string, int given, called in /srv/app/vendor/friendsofcake/crud/src/Listener/ApiListener.php

これはPHPの配列の仕様によるものなので、 api.success.data.raw には「文字列(文字列の数字以外)をキーにした配列」を設定する必要がある

https://www.php.net/manual/ja/language.types.array.php より抜粋

> key は、整数 または 文字列です。 value には任意の型を指定できます。
>
> さらに、次のような key のキャストが発生します。
> 
> 10 進数の int として妥当な形式の String は、 数値の前に + 記号がついていない限り、 int 型にキャストされます。
> つまり、キーに "8" を指定すると、実際には 8 として格納されるということです。

2023/05/30 追記: data に 「文字列の数字をキーにした配列」を設定したい場合 を参照

api.success.data.subject を指定する場合の例

    protected function _get()
    {
        //...

        /** @var \Cake\Database\Query $hoges */
        $hoges = $HogesTable()->find()->where(...);

        $fuga = ['a', 'b', 'c'];

        $this->setConfig('api.success.data.subject', 'hoges');

        $subject = $this->_subject();
        $subject->set([
            'hoges' => $hoges,
            'success' => true,
        ]);

        $this->_trigger('beforeRender', $subject);
    }
string(xxx) "{
    "success": true,
    "data": {
        "hoges": [ // HogeEntity の配列
          // ...
        ]
    }
}"
    protected function _get()
    {
        //...

        /** @var \Cake\Database\Query $hoges */
        $hoges = $HogesTable()->find()->where(...);

        $this->setConfig('api.success.data.subject', ['hoges', 'fuga']);

        $subject = $this->_subject();
        $subject->set([
            'hoges' => $hoges,
            'huga' => $huga,
            'success' => true,
        ]);

        $this->_trigger('beforeRender', $subject);
    }
string(xxx) "{
    "success": true,
    "data": {
        "hoges": [ // HogeEntity の配列
          // ...
        ],
        "fuga": [
            "a",
            "b",
            "c"
        ]
    }
}"

api.success.data.entity を指定する場合の例

    protected function _get()
    {
        //...

        /** @var HogeEntity $hogeEntity */
        $hogeEntity = $HogesTable->get(1);

        // property名を指定
        $this->setConfig('api.success.data.entity', ['id', 'name', 'email']);

        $subject = $this->_subject();
        $subject->set([
            'entity' => $hogeEntity,
            'success' => true,
        ]);

        $this->_trigger('beforeRender', $subject);
    }
string(xxx) "{
    "success": true,
    "data": {
        "id": xxx,
        "name": xxx,
        "email": xxx
    }
}"
    protected function _get()
    {
        //...

        /** @var HogeEntity $hogeEntity */
        $hogeEntity = $HogesTable->get(1);

        // HogeEntity のメソッド名を指定
        $this->setConfig('api.success.data.entity', ['toArray', 'isNew']);

        $subject = $this->_subject();
        $subject->set([
            'entity' => $hogeEntity,
            'success' => true,
        ]);

        $this->_trigger('beforeRender', $subject);
    }
string(xxx) "{
    "success": true,
    "data": {
        "toArray": {
            "id": xxx,
            "name": xxx,
            "email": xxx,
            // ...
        },
        "isNew": false
    }
}"

2023/05/30 追記: data に 「文字列の数字をキーにした配列」を設定したい場合

use ViewVarTrait;$_defaultConfig'scope' => 'table' を忘れない事

class HogeAction extends BaseAction
{
    use ViewVarTrait;

    protected $_defaultConfig = [
        'scope' => 'table',
        'api' => [
            'methods' => ['get'],
            'success' => [
                'code' => 200,
            ],
        ],
    ];

    protected function _get(): void
    {
        //...

        $array = //「文字列の数字をキーにした配列」

        $subject = $this->_subject();
        $subject->set([
            'success' => true,
            'entities' => $array,
        ]);

        $this->_trigger('beforeRender', $subject);
    }
}