見出し画像

CakePHP4でEntity作成時にUUIDを自動生成

UUIDカラムを持つモデルはEntityの作成時に必ずUUIDの生成が必要になるが、毎回Controller内に処理を書くとControllerの本流の処理がわかりにくくなったり、場所によって微妙にUUID生成処理の書き方が違ったりしてしまうので、Tableの方にUUIDの処理を移し、さらにTraitを新しく作成して使いまわせるようにした。

src/Model/Table/UsersTable.php

<?php
declare(strict_types=1);

namespace App\Model\Table;

use App\ORM\SoftDelete\SoftDeleteTrait;
use App\ORM\UuidTrait;
use ArrayObject;
use Cake\Datasource\EntityInterface;
use Cake\Event\EventInterface;
use Cake\ORM\Table;

class UsersTable extends Table {
  use SoftDeleteTrait;

  use UuidTrait {
    afterMarshal as protected afterMarshalOfUuidTrait;
  }

  public function initialize(array $config): void {
    parent::initialize($config);
  }

  public function afterMarshal(EventInterface $event, EntityInterface $entity, ArrayObject $data, ArrayObject $options) {
    $this->afterMarshalOfUuidTrait($event, $entity, $data, $options);
  }
}

src/ORM/UuidTrait.php

<?php
declare(strict_types=1);

namespace App\ORM;

use Cake\Datasource\EntityInterface;
use Cake\Event\EventInterface;
use Cake\Utility\Text;
use ArrayObject;

trait UuidTrait {
  public function afterMarshal(EventInterface $event, EntityInterface $entity, ArrayObject $data, ArrayObject $options) {
    if ($entity->isNew()) {
      if (empty($entity['uuid'])) {
        $isEntityExistsWithUuid = true;

        while ($isEntityExistsWithUuid) {
          $entity->set([
            'uuid' => Text::uuid(),
          ]);

          $entityWithUuid = $this->find()
              ->where([
                [$this->aliasField('uuid') => $entity['uuid']],
              ])
              ->first();

          $isEntityExistsWithUuid = !empty($entityWithUuid);
        }
      }
    }
  }
}

ぼやき

Tableのライフサイクルによって呼び出されるメソッド(ここではafterMarshal)をTraitに移すのは初めての経験。Table内での呼び出しが苦しい感じになってしまった。この書き方でデメリットがありそうなら是非ご教授いただきたい。

本筋からずれるが、$data変数のデータ型はarrayではなくArrayObjectになので、afterMarshalの引数を明示的に参照渡し(&$data)にしなくても勝手に参照渡し(っぽい振る舞い)になっている。
オーバーライドしたafterMarshal関数内部で$data変数のプロパティを変更するとafterMarshal関数の呼び出し元にある$data変数のプロパティにも変更が反映される。
PHPではスカラ型及びarray型の変数は値渡し、その他オブジェクト型の変数は参照渡し(っぽい振る舞い)になるようだ。言われてみると「あー、そうだよね。」となるが。

実験してみるとどうやら参照渡しっぽい雰囲気を出しながらどうも参照渡しとは挙動が異なるようだ。以下実験内容を残すが、ArrayObject型であっても、変数本体への代入($arrayObject = 'hoge';)は参照渡しでないと行えないようだ。

<?php
$array = ['hoge' => 'hoge'];

$f1_1 = function (array $array) {
  $array['hoge'] = 'fuga';
};

$f1_1($array);

// array (
//   'result_of' => 'f1_1',
//   'array' => 
//   array (
//     'hoge' => 'hoge',
//   ),
// )

$f1_2 = function (array $array) {
  $array = 'hoge';
};

$f1_2($array);

// array (
//   'result_of' => 'f1_2',
//   'array' => 
//   array (
//     'hoge' => 'hoge',
//   ),
// )

$f1_3 = function (array &$array) {
  $array = 'hoge';
};

$f1_3($array);

// array (
//   'result_of' => 'f1_3',
//   'array' => 'hoge',
// )

$arrayObject = new ArrayObject([
  'hoge' => 'hoge',
]);

$f2_1 = function (ArrayObject $arrayObject) {
  $arrayObject['hoge'] = 'fuga';
};

$f2_1($arrayObject);

// array (
//   'result_of' => 'f2_1',
//   'array_object' => 
//   ArrayObject::__set_state(array(
//      'hoge' => 'fuga',
//   )),
// )

$f2_2 = function (ArrayObject $arrayObject) {
  $arrayObject = 'fuga';
};

$f2_2($arrayObject);

// array (
//   'result_of' => 'f2_2',
//   'array_object' => 
//   ArrayObject::__set_state(array(
//      'hoge' => 'fuga',
//   )),
// )

$f2_3 = function (ArrayObject &$arrayObject) {
  $arrayObject = 'fuga';
};

$f2_3($arrayObject);

// array (
//   'result_of' => 'f2_3',
//   'array_object' => 'fuga',
// )

PHPの変数の振る舞いについてはzval構造体(https://www.php.net/manual/ja/internals2.variables.intro.php)を掘り下げて理解する必要があるがまだその時ではないと自分に言い聞かせている。

この記事が気に入ったらサポートをしてみませんか?