この記事は Drupal Advent Calendar 2021 の11日目です。
ちょっとしたコードを CLI で動かしたい時に便利な環境が REPL です。Drupal 9 では、Drush の php:cli コマンドで PsySh という PHP の REPL が利用できます。
インストールと実行
Drush の依存関係に PsySh が含まれているので、Composer で Drush を入れるだけで使えます。
ただし、2021年12月5日にリリースされた最新の PsySh v0.11 が導入されると、現状の Drush ではエラーが発生します。このため、いま現在 Drush の新規インストールやアップデートを行うと php:cli コマンドが動作しない状態に陥る可能性があります。
修正のプルリクが出ていますが、反映されるまでの回避策として、PsySh の旧 0.10 版を手動で入れる方法が考えられます。PsySh v0.10.12 を上書きインストールする例を示します。
$ cd <プロジェクトのルート> $ curl -L https://github.com/bobthecow/psysh/archive/refs/tags/v0.10.12.tar.gz | tar xzf - --strip=1 -C vendor/psy/psysh
追記(2021-12-17)
Drush 10.6.2 で psysh への依存関係が次のように変更されて解決しました。
"psy/psysh": ">=0.6 <0.11",
Drush をアップデートしてみたところ、psysh がダウングレードされ、drush php:cli コマンドで正常に対話シェルが起動することを確認しました。
$ composer update drush/drush -w
Loading composer repositories with package information
Updating dependencies
Lock file operations: 0 installs, 2 updates, 0 removals
- Upgrading drush/drush (10.6.1 => 10.6.2)
- Downgrading psy/psysh (v0.11.0 => v0.10.12)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 0 installs, 2 updates, 0 removals
- Downgrading psy/psysh (v0.11.0 => v0.10.12): Extracting archive
- Upgrading drush/drush (10.6.1 => 10.6.2): Extracting archive
Package doctrine/reflection is abandoned, you should avoid using it. Use roave/better-reflection instead.
Package webmozart/path-util is abandoned, you should avoid using it. Use symfony/filesystem instead.
Generating autoload files
45 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
$
導入されている PsySh のバージョンが 0.10.x であれば正常に起動します。
$ drush php:cli
Psy Shell v0.10.12 (PHP 7.4.3 — cli) by Justin Hileman
DEMO site (Drupal 9.3.0)
>>>
起動したら、プロンプト(>>>)に PHP コードを入力して、変数、関数、クラスなどを定義できます。
>>> $msg = 'Hello!';
=> "Hello!"
>>> function greet($peer) {
... echo "Hi, $peer!";
... return $peer;
... }
>>> class Person {
... private $name = '';
...
... public function __construct($name) {
... $this->name = $name;
... }
...
... public function greet($peer) {
... echo "Hi $peer, I'm $this->name!";
... return $this->name;
... }
... }
>>>
式を入力すると評価結果が表示され、ステートメントを入力すると実行されます。
>>> $msg
=> "Hello!"
>>> greet("Jiro");
Hi, Jiro!⏎
=> "Jiro"
>>> $taro = new Person("Taro");
=> Person {#4524}
>>> $taro->greet("Jiro");
Hi Jiro, I'm Taro!⏎
=> "Taro"
>>>
PsySh の機能
PsySh manual に詳しい説明があります。以下、一部を紹介します。
マジック変数
最後に使用した値が情報の種類別に保持されており、これをマジック変数($ で始まる定義済みの変数)で取得することができます。
>>> $_ // 最後の結果を保持するマジック変数
=> "Taro"
>>> greet('Hanako');
Hi, Hanako!⏎
=> "Hanako"
>>> $_
=> "Hanako"
>>>
PsySh コマンド
PsySh は種々の組み込みコマンドを備えています。たとえば、ls コマンドを実行すると、ローカル変数やインスタンス等の一覧が表示されます。
>>> ls
Variables: $container, $msg, $taro
>>> ls -al
Variables:
$container Drupal\Core\DependencyInjection\Container {#768 …620}
$msg "Hello!"
$taro Person {#4524}
$_ "Hanako"
$__out "Hi, Hanako!"
各コマンドの詳しい情報は、help コマンドで確認できます。
>>> help ls
Usage:
ls [--vars] [-c|--constants] [-f|--functions] [-k|--classes] [-I|--interfaces] [-t|--traits] [--no-inherit] [-p|--properties] [-m|--methods] [-G|--grep GREP] [-i|--insensitive] [-v|--invert] [-g|--glob>
Aliases: dir
Arguments:
target A target class or object to list.
Options:
--vars Display variables.
--constants (-c) Display defined constants.
--functions (-f) Display defined functions.
--classes (-k) Display declared classes.
--interfaces (-I) Display declared interfaces.
--traits (-t) Display declared traits.
--no-inherit Exclude inherited methods, properties and constants.
--properties (-p) Display class or object properties (public properties by default).
--methods (-m) Display class or object methods (public methods by default).
--grep (-G) Limit to items matching the given pattern (string or regex).
--insensitive (-i) Case-insensitive search (requires --grep).
--invert (-v) Inverted search (requires --grep).
--globals (-g) Include global variables.
--internal (-n) Limit to internal functions and classes.
--user (-u) Limit to user-defined constants, functions and classes.
--category (-C) Limit to constants in a specific category (e.g. "date").
--all (-a) Include private and protected methods and properties.
--long (-l) List in long format: includes class names and method signatures.
--help (-h) Display this help message.
Help:
List variables, constants, classes, interfaces, traits, functions, methods,
and properties.
Called without options, this will return a list of variables currently in scope.
If a target object is provided, list properties, constants and methods of that
target. If a class, interface or trait name is passed instead, list constants
and methods on that class.
e.g.
>>> ls
>>> ls $foo
>>> ls -k --grep mongo -i
>>> ls -al ReflectionClass
>>> ls --constants --category date
>>> ls -l --functions --grep /^array_.*/
>>> ls -l --properties new DateTime()
dump コマンドは、オブジェクトや基本型の値をダンプします。
>>> dump -a $taro
Person {#4524
-name: "Taro",
}
>>>
doc コマンドを使用すると、PHP のオブジェクト、クラス、定数、メソッド、プロパティのドキュメントを閲覧できます。ただし、事前に PHP マニュアルをダウンロードしてインストールしておく必要があります。日本語版をインストールする例を示します。
$ wget http://psysh.org/manual/ja/php_manual.sqlite -P ~/.local/share/psysh
以後は、調べたいものを引数に指定して doc コマンドを実行すると、そのマニュアルが表示されます。
>>> doc implode
function implode($glue, $pieces)
Description:
配列要素を文字列により連結する
配列の要素を $glue 文字列で連結します。
implode()は、歴史的な理由により、引数をどちら の順番でも受けつけることが可能です。しかし、
explode() との統一性の観点からは、 ドキュメントに記述された引数の順番を使用しないことは推奨されま
せん。
Param:
string $glue デフォルトは空文字列です。
array $pieces 連結したい文字列の配列。
Return:
string すべての配列要素の順序を変えずに、各要素間に $glue 文字列をはさんで 1 つの文字列にして返し
ます。
See Also:
* explode()
* preg_split()
* http_build_query()
>>>
シェルコマンドの実行
PsySh の中からシェルのコマンドを実行することもできます。実行するコマンドラインをバッククォートで囲みます。コマンドの実行結果は、文字列のリテラル("〜")として表示されます。
>>> `pwd`
=> "/var/www/hoge/web\n"
>>> `ls -l`
=> """
total 84\n
-rw-rw-r-- 1 vagrant vagrant 315 Dec 8 12:05 autoload.php\n
drwxrwxr-x 1 vagrant vagrant 4096 Nov 24 23:27 core\n
-rw-rw-r-- 1 vagrant vagrant 1507 Nov 27 17:27 example.gitignore\n
-rw-rw-r-- 1 vagrant vagrant 549 Nov 27 17:27 index.php\n
-rw-rw-r-- 1 vagrant vagrant 94 Nov 27 17:27 INSTALL.txt\n
drwxrwxr-x 1 vagrant vagrant 4096 Dec 8 12:05 modules\n
drwxrwxr-x 1 vagrant vagrant 4096 Nov 27 17:27 profiles\n
-rw-r--r-- 1 vagrant vagrant 12484 Dec 8 21:14 q\n
-rw-rw-r-- 1 vagrant vagrant 3205 Nov 27 17:27 README.md\n
-rw-rw-r-- 1 vagrant vagrant 1586 Nov 27 17:27 robots.txt\n
drwxrwxr-x 1 vagrant vagrant 4096 Dec 8 11:59 sites\n
drwxrwxr-x 1 vagrant vagrant 4096 Nov 27 17:27 themes\n
-rw-r--r-- 1 vagrant vagrant 12484 Dec 8 14:28 t q\n
-rw-rw-r-- 1 vagrant vagrant 804 Nov 27 17:27 update.php\n
-rw-rw-r-- 1 vagrant vagrant 4016 Nov 27 17:27 web.config\n
"""
>>>
Drupal における利用
Drush から起動した PsySh のプロンプトは、Drupal で必要なファイルがすべて読み込まれた状態になっており、モジュール内のコードと同じように Drupal API を呼び出してサイトの操作や情報取得ができます。
// Drupal の構成設定 API でサイト名を取得する例
>>> \Drupal::config('system.site')->get('name');
=> "DEMO site"
// settings.php で設定された $settings['hash_salt'] の値を取得する例
>>> \Drupal\Core\Site\Settings::get('hash_salt');
=> "7bb8S5Y7pZ4tvwciwBR8jsk0Ol2_Piy38vErPATD1TEPwjX0CpZLWkzgaXdALjEcP_ohmiJzkw"
ls コマンドを実行すると、Drupal\Core\DependencyInjection\Container 型の $container という変数が利用できることがわかります。
>>> ls -l
Variables:
$container Drupal\Core\DependencyInjection\Container {#768 …604}
>>>
このオブジェクトの get() メソッドを利用して Drupal のコアサービスのインスタンスを取得できます。entity_type.manager サービスの例を示します。
// entity_type.manager サービスを取得する例
>>> $manager = $container->get('entity_type.manager');
=> Drupal\Core\Entity\EntityTypeManager {#943
+"_serviceId": "entity_type.manager",
}
取得したインスタンスの型の詳細は doc コマンドで閲覧できます。
>>> doc $manager
class Drupal\Core\Entity\EntityTypeManager extends Drupal\Core\Plugin\DefaultPluginManager implements Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface, Drupal\Component\Plugin\Discovery\DiscoveryInterface, Drupal\Component\Plugin\Factory\FactoryInterface, Drupal\Component\Plugin\Mapper\MapperInterface, Drupal\Component\Plugin\PluginManagerInterface, Drupal\Core\Cache\CacheableDependencyInterface, Drupal\Core\Entity\EntityTypeManagerInterface, Symfony\Component\DependencyInjection\ContainerAwareInterface
Description:
Manages entity type plugin definitions.
Each entity type definition array is set in the entity type's annotation and
altered by hook_entity_type_alter().
Do not use hook_entity_type_alter() hook to add information to entity types,
unless one of the following is true:
- You are filling in default values.
- You need to dynamically add information only in certain circumstances.
- Your hook needs to run after hook_entity_type_build() implementations.
Use hook_entity_type_build() instead in all other cases.
See: \Drupal\Core\Entity\Annotation\EntityType
See: \Drupal\Core\Entity\EntityInterface
See: \Drupal\Core\Entity\EntityTypeInterface
See: hook_entity_type_alter()
See: hook_entity_type_build()
ls コマンドを使用すると、クラスのメンバを調べることができます。
>>> ls -al $manager;
Class Properties:
$additionalAnnotationNamespaces [] >
$alterHook "entity_type" >
$cacheBackend Drupal\Core\Cache\ChainedFastBackend >
$cacheKey "entity_type" >
$cacheTags [ …1] >
$classResolver Drupal\Core\DependencyInjection\Class>
$container Drupal\Core\DependencyInjection\Conta>
…
Class Methods:
alterDefinitions protected function alterDefinitions(&$defini>
alterInfo protected function alterInfo($alter_hook) >
cacheGet protected function cacheGet($cid) >
cacheSet protected function cacheSet($cid, $data, $ex>
clearCachedDefinitions public function clearCachedDefinitions() >
createHandlerInstance public function createHandlerInstance($class>
createInstance public function createInstance($plugin_id, a>
doGetDefinition protected function doGetDefinition(array $de>
extractProviderFromDefinition protected function extractProviderFromDefini>
findDefinitions protected function findDefinitions() >
getAccessControlHandler public function getAccessControlHandler($ent>
getActiveDefinition public function getActiveDefinition($entity_>
getCacheContexts public function getCacheContexts() >
getCachedDefinitions protected function getCachedDefinitions() >
getCacheMaxAge public function getCacheMaxAge() >
getCacheTags public function getCacheTags() >
getDefinition public function getDefinition($entity_type_i>
getDefinitions public function getDefinitions() >
getDiscovery protected function getDiscovery() >
getFactory protected function getFactory() >
getFormObject public function getFormObject($entity_type_i>
getHandler public function getHandler($entity_type_id, >
getInstance public function getInstance(array $options) >
getListBuilder public function getListBuilder($entity_type_>
getRouteProviders public function getRouteProviders($entity_ty>
getStorage public function getStorage($entity_type_id) >
…
doc コマンドは、メソッドの詳細の確認にも利用できます。
>>> doc $manager->getStorage
class Drupal\Core\Entity\EntityTypeManager extends Drupal\Core\Plugin\DefaultPluginManager implements Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface, Drupal\Component\Plugin\Discovery\DiscoveryInterface, Drupal\Component\Plugin\Factory\FactoryInterface, Drupal\Component\Plugin\Mapper\MapperInterface, Drupal\Component\Plugin\PluginManagerInterface, Drupal\Core\Cache\CacheableDependencyInterface, Drupal\Core\Entity\EntityTypeManagerInterface, Symfony\Component\DependencyInjection\ContainerAwareInterface
public function getStorage($entity_type_id)
Description:
{@inheritdoc}
---
interface Drupal\Core\Entity\EntityTypeManagerInterface extends Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface, Drupal\Component\Plugin\Discovery\DiscoveryInterface, Drupal\Component\Plugin\Factory\FactoryInterface, Drupal\Component\Plugin\Mapper\MapperInterface, Drupal\Component\Plugin\PluginManagerInterface
abstract public function getStorage($entity_type_id)
Description:
Creates a new storage instance.
Throws:
\Drupal\Component\Plugin\Exception\PluginNotFoundException Thrown if the entity type doesn't exist.
\Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException Thrown if the storage handler couldn't be loaded.
Param:
string $entity_type_id The entity type ID for this storage.
Return:
\Drupal\Core\Entity\EntityStorageInterface A storage instance.
こうした情報を確認しながら、種々の API を呼び出していくことができます。
// uid=1 のユーザーを読み込む例
>>> $user = $manager->getStorage('user')->load(1);
=> Drupal\user\Entity\User {#5435
#uid: Drupal\Core\Field\FieldItemList {#5370},
#uuid: Drupal\Core\Field\FieldItemList {#5573},
#langcode: Drupal\Core\Field\FieldItemList {#5579},
#preferred_langcode: Drupal\Core\Field\FieldItemList {#5593},
#preferred_admin_langcode: Drupal\Core\Field\FieldItemList {#5607},
#name: Drupal\Core\Field\FieldItemList {#5609},
#pass: Drupal\Core\Field\FieldItemList {#5615},
#mail: Drupal\Core\Field\FieldItemList {#5625},
#timezone: Drupal\Core\Field\FieldItemList {#5631},
#status: Drupal\Core\Field\FieldItemList {#5637},
#created: Drupal\Core\Field\FieldItemList {#5643},
#changed: Drupal\Core\Field\ChangedFieldItemList {#5649},
#access: Drupal\Core\Field\FieldItemList {#5655},
#login: Drupal\Core\Field\FieldItemList {#5661},
#init: Drupal\Core\Field\FieldItemList {#5667},
#roles: Drupal\Core\Field\EntityReferenceFieldItemList {#5673},
#default_langcode: Drupal\Core\Field\FieldItemList {#5685},
#user_picture: Drupal\file\Plugin\Field\FieldType\FileFieldItemList {#5690},
}
// インスタンスの内容をダンプする例
>>> dump -a $user;
Drupal\user\Entity\User {#5435
#values: [
"uid" => [
"x-default" => "1",
],
"uuid" => [
"x-default" => "fa02f5db-396d-48af-b8f4-256ab018db5d",
],
"langcode" => [
"x-default" => "ja",
],
"preferred_langcode" => [
"x-default" => "ja",
],
"preferred_admin_langcode" => [
"x-default" => null,
],
"name" => [
"x-default" => "admin",
],
"pass" => [
"x-default" => "$S$Ee85r8WTkOUqZHp1oIxIlig5fOntFC8DrfyOoq.93TE9ezcWD9jO",
],
"mail" => [
"x-default" => "admin@example.com",
],
"timezone" => [
"x-default" => "Asia/Tokyo",
],
"status" => [
"x-default" => "1",
],
"created" => [
"x-default" => "1639116396",
],
:
名前空間を指定すると、クラス等の指定を簡略化できます。
>>> namespace Drupal\user\Entity
>>> User::load(1)
=> Drupal\user\Entity\User {#5435
…
show コマンドを利用して、特定のメソッドのコードを閲覧することもできます。
>>> show User::load
481: /**
482: * {@inheritdoc}
483: */
484: public static function load($id) {
485: $entity_type_repository = \Drupal::service('entity_type.repository');
486: $entity_type_manager = \Drupal::entityTypeManager();
487: $storage = $entity_type_manager->getStorage($entity_type_repository->>
488: return $storage->load($id);
489: }
プロジェクトを常に PhpStorm 等の IDE で開いて xdebug している場合は必要ないかもしれませんが、ターミナルでサイトを調査する時などは便利に活用できそうです。
参考資料
- psysh
- Drepl
- drush php:cli コマンド
- bobthecow/psysh
- PsySH manual
- phpのREPL psysh が便利そうだった
- Interactive PHP Debugging with PsySH
追記:
PsySh のブレークポイントってこれかな。eval(\Psy\sh()) の先で \Psy\debug() が呼ばれて、そこで Shell のインスタンスの run() を呼ぶんですね。https://t.co/XF0ySUBaj9
— Kenji Shirane (@bkenro) January 13, 2022