はじめに
今回は、Drupalのエンティティハンドラーについて深ぼってみようと思います。
少し備忘録的な感じになりそうですがどなたかの助けに繋がれば幸いです。
まずは、EntityListBuilderから〜。
ListBuilder とは
定義が少しむずかしいんですが、MVCで言う所のController層に位置するエンティティのハンドラーです。
よくコアやコントリビュートモジュールが用意している一覧画面はこれで定義されていることが多い。
さくっと管理画面を作りたい時に持って来いなハンドラークラスです(リストコンポーネントっていう方がしっくりくるかも)
一覧画面で利用できるコンポーネントも定義できます(されてます)
リーディング
主に、以下3つが実装されています。
- データ取得
- 表示
- ヘルパー
設計
設計場所: core/lib/Drupal/Core/Entity/EntityListBuilderInterface.php
以下、コアソースからInterfaceを抽出しました。
/**
* Gets the entity storage.
*
* @return \Drupal\Core\Entity\EntityStorageInterface
* The storage used by this list builder.
*/
public function getStorage();
/**
* Loads entities of this type from storage for listing.
*
* This allows the implementation to manipulate the listing, like filtering or
* sorting the loaded entities.
*
* @return \Drupal\Core\Entity\EntityInterface[]
* An array of entities implementing \Drupal\Core\Entity\EntityInterface
* indexed by their IDs. Returns an empty array if no matching entities are
* found.
*/
public function load();
/**
* Provides an array of information to build a list of operation links.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity the operations are for.
*
* phpcs:disable Drupal.Commenting
* @todo Uncomment new method parameters before drupal:12.0.0.
* @see https://www.drupal.org/project/drupal/issues/3533078
* @param \Drupal\Core\Cache\CacheableMetadata|null $cacheability
* The cacheable metadata to add to if your operations vary by or depend on
* something.
* phpcs:enable
*
* @return array
* An associative array of operation link data for this list, keyed by
* operation name, containing the following key-value pairs:
* - title: The localized title of the operation.
* - url: An instance of \Drupal\Core\Url for the operation URL.
* - weight: The weight of this operation.
*/
public function getOperations(EntityInterface $entity /* , ?CacheableDependencyInterface $cacheability = NULL */);
/**
* Builds a listing of entities for the given entity type.
*
* @return array
* A render array as expected by
* \Drupal\Core\Render\RendererInterface::render().
*/
public function render();
上x2 と下x2 でデータ取得・表示に分類できる感じですね(インターフェース分けろよ、とも思いましたが...)
実装
実装場所: core/lib/Drupal/Core/Entity/EntityListBuilder.php
コード量が多いので気になった箇所だけ特筆させていただきます。
1. データ取得クエリ
以下の getEntityListQuery()
メソッドで抽象化されているみたい。$limit
はクラスプロパティで定義出来る感じなので拡張して上書きすることもできますね。
フィルターやソート要件を拡張したい時はこのメソッドですね〜。
/**
* Returns a query object for loading entity IDs from the storage.
*
* @return \Drupal\Core\Entity\Query\QueryInterface
* A query object used to load entity IDs.
*/
protected function getEntityListQuery(): QueryInterface {
$query = $this->getStorage()->getQuery()
->accessCheck(TRUE)
->sort($this->entityType->getKey(static::SORT_KEY));
// Only add the pager if a limit is specified.
if ($this->limit) {
$query->pager($this->limit);
}
return $query;
}
2. 管理リンク
「編集・削除」等のドロップダウンの管理リンク、よく使うと思います。
追加したい!削除したい!って時意外と多くないでしょうか?
そんな時はこの getOperations()
メソッドを実装しましょう。
/**
* {@inheritdoc}
*/
public function getOperations(EntityInterface $entity/* , ?CacheableMetadata $cacheability = NULL */) {
$args = func_get_args();
$cacheability = $args[1] ?? new CacheableMetadata();
$operations = $this->getDefaultOperations($entity, $cacheability);
$operations += $this->moduleHandler()->invokeAll('entity_operation', [$entity, $cacheability]);
$this->moduleHandler->alter('entity_operation', $operations, $entity, $cacheability);
uasort($operations, '\Drupal\Component\Utility\SortArray::sortByWeightElement');
return $operations;
}
※ 呼び出しているgetDefaultOperations()
関数を上書きするほうが綺麗なこともあります。
3. 表示
以下の render()
関数で実装されています。
'#type' => 'table',
でTableElementを指定して、各プロパティを渡してる感じですね。
もちろん#cache
プロパティを定義することも可能。
/**
* {@inheritdoc}
*
* Builds the entity listing as renderable array for table.html.twig.
*
* @todo Add a link to add a new item to the #empty text.
*/
public function render() {
$build['table'] = [
'#type' => 'table',
'#header' => $this->buildHeader(),
'#title' => $this->getTitle(),
'#rows' => [],
'#empty' => $this->t('There are no @label yet.', ['@label' => $this->entityType->getPluralLabel()]),
'#cache' => [
'contexts' => $this->entityType->getListCacheContexts(),
'tags' => $this->entityType->getListCacheTags(),
],
];
foreach ($this->load() as $entity) {
if ($row = $this->buildRow($entity)) {
$build['table']['#rows'][$entity->id()] = $row;
}
}
// Only add the pager if a limit is specified.
if ($this->limit) {
$build['pager'] = [
'#type' => 'pager',
];
}
return $build;
}
コアソースでの拡張例
ユーザーはヘッダーや行の項目でカスタマイズが施されてますね。
適用の仕方
パターン①: カスタムエンティティの場合
本記事ではカスタムエンティティの実装方法については言及しませんが、
core/modules/node/src/Entity/Node.php#L61 のエンティティアノテーション内にて、定義することが可能です。
handlers: [
// 省略...
'list_builder' => NodeListBuilder::class,
// 省略...
],
パターン②: 既存のエンティティの場合
エンティティストレージオブジェクトが生成される際に、ハンドラーをセットすることで適用することができます。
function mymodule_entity_type_build(array &$entity_types) {
$entity_types['node']->setListBuilderClass(MyModuleListBuilder::class);
}
まとめ
本記事では、コントローラ層のハンドラである ListBuilder を紹介しました。
Viewsで管理画面を作ると重いですよね。
そういう時、EntityListBuilderだと、簡単に高パフォーマンスな一覧画面を実装できますので、ぜひご活用ください。