XOOPS Cubeのプリロードとデリゲートの解説


# 0. はじめに

本稿はXOOPS Cubeのデリゲートとプリロードについて解説するものです。デリゲートは処理の一部を別のプログラムに委譲するための仕組みです。プリロードはモジュール実行前に前処理を行えるようにするための仕組みです。デリゲートとプリロードは本来別の仕組みで、それぞれ独立して利用することができます。実際の利用では、プリロードプログラムでデリゲート機構を利用し、XOOPSやモジュールの処理や振る舞いを変更するワンファイルハックが多いでしょう。

解説にあたって、本稿は次のような流れで進めていきます。まず最初にデリゲートについて解説します。デリゲートについて初めて触れる方や、デリゲートの実装方法について知りたければ、「1.デリゲート」を読んで下さい。次にプリロードについて解説します。プリロードの仕組みや実装方法については「2.プリロード」をご覧になってください。最後に、ワンファイルハックについて解説します。デリゲートとプリロードを組み合わせた、より発展的で実践的な手法についての情報は「3.ワンファイルハック」が詳しいです。

# 1. デリゲート

## 1.1 デリゲートとは

デリゲート(delegate)とは、あるプログラムの処理の一部を他のプログラムに委譲する仕組みです。デリゲートの仕組みを言語自体が持っている言語がありますが、PHPにはその仕組がないため、XOOPS Cubeが独自にその仕組を提供しています。厳密には他の言語のデリゲートと異なり、XOOPS Cubeのデリゲートは「委譲」だけではなく、イベントの「フック」や「ファイア」の仕組みとしても利用されています。

## 1.2 XOOPS Cubeでのデリゲート機構の仕組み

XOOPS Cubeで「デリゲート」と言うと、委譲やイベントの仕組みや概念を指す場合と、具体的なイベントのフックポイントそのものを指す場合があります。このセクションでは分かりやすさのために、仕組みのことを「デリゲート機構」、フックポイントを「デリゲート」として区別して呼びます。

デリゲート機構は3つの要素で構成されています: デリゲート、コールバック関数、デリゲートマネージャです。デリゲートとは、イベントのフックポイントです。XOOPS Cube Legacyでは多数のデリゲートが定義されています。デリゲートには任意の処理(コールバック関数)を追加(add)することができます。デリゲートがコール(call)された際に、登録した処理が順次実行されます。デリゲートの定義・コールは、`XCube_Delegate` で行います。

```php
<?php
$delegate = new XCube_Delegate(); // デリゲートの作成
$delegate->add($callbackFunction); // 処理の登録
$delegate->call(); // デリゲートをコールし、登録された処理を順次実行
```

コールバック関数とは、デリゲートがコールされた際に呼び出される関数です。コールバック関数をデリゲートがコールされる前に、デリゲートに登録する必要があります。コールバック関数はデリゲートから引数を受け取る場合があります。引数があるかどうかはデリゲートごとに異なります。コールバック関数は、PHPの `callable` 型または `callback` 型であればよく、XOOPS Cubeではコールバック関数用にクラスを定義していません。

```php
<?php

$callbackFunction = function() {
	// 何かの処理
};

$delegate = new XCube_Delegate(); // デリゲートの作成
$delegate->add($callbackFunction); // コールバック関数の登録
$delegate->call(); // コールバック関数が実行される
```

デリゲートマネージャは、デリゲートとコールバック関数を仲介する役割があります。デリゲートは自分自身をデリゲートマネージャに登録(register)します。登録する際にデリゲートは自分自身のデリゲート名をデリゲートマネージャに渡します。コールバック関数は、デリゲート名を指定して自分自身の追加(add)をデリゲートマネージャに要求します。デリゲートマネージャは追加されたコールバック関数をデリゲートに渡します。

```txt
      +--------------------+            +-------------------+
      |   DelegateManager  | <--------- | Callback Function |
      +--------------------+   (2)add   +-------------------+
              ^      |
 (1)register  |      | (3)pass callback function
              |      v
      +--------------------+
      |       Delegate     |
      +--------------------+
```

デリゲートマネージャは `XCube_DelegateManager` オブジェクトが担います。このオブジェクトはXOOPS Cubeが起動したときに生成されます。デリゲートマネージャはデリゲートに名前を与えます。名前さえわかっていれば`XCube_DelegateManager ` オブジェクトを仲介すればどこからでもデリゲートにアクセスすることができるわけです。

```
<?php

$delegate = new XCube_Delegate(); // デリゲートの作成
$delegate->register('MyDelegate.Foo.Bar'); // デリゲートマネージャにデリゲート名を登録

$callbackFunction = function() {
	// 何かの処理
};

$root = XCube_Root::getSingleton();
// 'MyDelegate.Foo.Bar'デリゲートへのコールバック関数追加要求
$root->mDelegateManager->add('MyDelegate.Foo.Bar', $callbackFunction); 

$delegate->call(); // コールバック関数が実行される
```

## 1.3 無名デリゲートと登録済みデリゲート

デリゲートには、無名デリゲートと登録済みデリゲートの2種類があります。ここでは、この2つのデリゲートについて解説します。

### 1.3.1 無名デリゲート

無名デリゲート(unregistered delegate)とは、デリゲートマネージャに登録していないデリゲートのことです。無名デリゲートの特徴は、デリゲートのインスタンスを参照できなければ、デリゲートにコールバック関数を登録できない点です。デリゲートマネージャにデリゲートを登録してしまうと、XOOPSのどこからでもコールバック関数が追加できるようになりますが、スコープが開きすぎてしまうため意図しないコールバックが追加されてしまう危険性があります。無名デリゲートを使うことで、スコープを制限しつつ、安全にデリゲートをコールすることができます。

```php
<?php

class MyScope
{
	protected $secretDelegate;

	public function __construct()
	{
		// 無名デリゲートの定義。
		$this->secretDelegate = new XCube_Delegate();
	}

	public function run()
	{
		$this->secretDelegate->call();
	}
}

class Foo extends MyScope
{
	public function prepare()
	{
		// ここではデリゲートにタッチできる
		$this->secretDelegate->add(array($this, 'callme'));
	}

	public function callme()
	{
		// 何らかの処理
	}
}

$foo = new Foo();
$foo->prepare();

// このスコープでは$secretDelegateにタッチできない

$foo->run();
```

### 1.3.2 登録済みデリゲート

登録済みデリゲート(registered delegate)とは、デリゲートマネージャに登録されたデリゲートのことを言います。デリゲートマネージャに登録する際に、デリゲートの識別子（デリゲート名）をセットするので、デリゲートマネージャを介せばスコープに制限なくデリゲートにコールバック関数を登録することができます。いわば、グローバル空間に存在するデリゲートということになります。クラスやモジュールを超えて委譲したい処理は登録済みデリゲートにするといいでしょう。

```php
<?php

class MyScope
{
	protected $publicDelegate;

	public function __construct()
	{
		$this->publicDelegate = new XCube_Delegate();
		$this->publicDelegate->register('MyDelegate.Foo.Bar');
	}

	public function prepare()
	{
	}

	public function run()
	{
		$this->publicDelegate->call();
	}
}

$foo = new Foo();
$foo->prepare();

// このスコープでは$publicDelegateにタッチできる
$root = XCube_Root::getSingleton();
$root->mDelegateManager->add('MyDelegate.Foo.Bar', function() {
	// 何らかの処理
});

$foo->run();
```

デリゲート名は決まった命名規則があるわけではありません。しかし、デリゲート名はグローバル空間に登録されるため、デリゲート名の資源は有限です。他のデリゲート名と重複しない名前を付ける必要があります。筆者としては、`{モジュール名}.{クラス名}.{メソッド名}` の書式をおすすめします。例えば、site_mapモジュールの `SiteMap_ViewAction` クラスの `executeViewSuccess` メソッドに登録済みデリゲートを定義する場合は、`SiteMap.ViewAction.executeViewSucess` という名前にします。

XOOPS Cube Legacyのデリゲートには多くの登録済みデリゲートがあります。しかしながら、公式にはデリゲート名の目録が文書化されていません。非公式のドキュメント中では [XoopsCubeLegacy2.xDelegateCheatSheet](https://github.com/suin/xoopscube-cheatsheet) が多くのデリゲート名を収録しています。デリゲートはXCLのアップデートで追加・削除・名前変更される可能性がありますので、実際のデリゲートについてはソースコードを参照する他ありません。

## 1.4 デリゲートの遅延登録

登録済みデリゲートへのコールバック関数追加は、デリゲート名をデリゲートマネージャに登録して後、デリゲート名を指定してコールバック関数を追加するというのが基本的な順序です。この順序を逆に行うことを、デリゲートの遅延登録(lazy registering)と言います。

デリゲートマネージャは、未登録のデリゲート名についてもコールバック関数を受け付けることができます。未登録のデリゲート名に対してコールバック関数追加要求があると、デリゲートマネージャはコールバック関数を保持しておき、デリゲート名が登録された際に保持していたコールバック関数をデリゲートに渡すような振る舞いをします。

```php
<?php
$root = XCube_Root::getSingleton();

// 未登録のデリゲート名にコールバック関数を追加
$root->mDelegateManager->add('MyDelegate', function() {
	// 何らかの処理
});

$myDelegate = new XCube_Delegate();
$myDelegate->register('MyDelegate'); // 遅延登録
$myDelegate->call(); // 追加されたコールバック関数が実行される
```

## 1.5 コールバック関数の追加

### 1.5.1 コールバック関数として有効な型

コールバック関数はPHPの `callable` 型または `callback` 型である必要があります。主に次の形式で渡します。

* 関数名の文字列
* オブジェクト・メソッド名からなる配列
* クラス名・静的メソッド名からなる配列
* クラス名・静的メソッド名からなる文字列
* 無名関数やクロージャー
* `__invoke()` を実装したオブジェクト

```php
<?php

$delegate->add('my_func'); // 関数名の文字列 
$delegate->add(array($object, 'doSomething')); // オブジェクト・メソッド名からなる配列
$delegate->add(array('MyClass', 'staticMethod')); // クラス名・静的メソッド名からなる配列
$delegate->add('MyClass::staticMethod'); // クラス名・静的メソッド名からなる文字列
$delegate->add(function(){ /* do something */ }); // 無名関数やクロージャー
$delegate->add($object); // __invoke() を実装したオブジェクト
```

### 1.5.2 基本的な追加方法

コールバック関数をデリゲートに追加するには、 `XCube_Delegate` オブジェクトまたは `XCube_DelegateManager` オブジェクトの `add()` メソッドにコールバック関数を渡します。デリゲートマネージャは第1引数がデリゲート名になり、第2引数以降は、デリゲートの `add()` メソッドと用法は同じです。

```php
<?php
// 無名デリゲート
$delegate = new XCube_Delegate();
$delegate->add(function(){
	// code...
});

// 登録済みデリゲート
$delegateManager = XCube_Root::getSingleton()->mDelegateManager;
$delegateManager->add('FooBarDelegate', function() {
	// code ...
});
```

ひとつのデリゲートに対して複数のコールバック関数を追加することができます。

```php
<?php
$delegate = new XCube_Delegate();
$delegate->add($callbackFunction1);
$delegate->add($callbackFunction2);
```

### 1.5.3 実行順序と優先度

コールバック関数は登録された順序で実行されますが、優先度を2番目の引数にセットすることで、他の関数よりも早く/遅く実行されるようにすることができます。

```php
<?php
$delegate = new XCube_Delegate();
$delegate->add($callbackFunction1);

// これは $callbackFunction1 よりも先に実行されます
$delegate->add($callbackFunction2, XCUBE_DELEGATE_PRIORITY_FIRST);
```

優先度指定は `XCUBE_DELEGATE_PRIORITY_1` から `XCUBE_DELEGATE_PRIORITY_10` の間で設定することができます。`XCUBE_DELEGATE_PRIORITY_FIRST` は `XCUBE_DELEGATE_PRIORITY_1` のエイリアスで最も優先されます。`XCUBE_DELEGATE_PRIORITY_FINAL` は `XCUBE_DELEGATE_PRIORITY_10` のエイリアスで最も遅く実行されます。同じ優先度を指定した関数が複数ある場合は登録された順番で順位が決まります。

```
XCUBE_DELEGATE_PRIORITY_1 XCUBE_DELEGATE_PRIORITY_FIRST
XCUBE_DELEGATE_PRIORITY_2
XCUBE_DELEGATE_PRIORITY_3
XCUBE_DELEGATE_PRIORITY_4
XCUBE_DELEGATE_PRIORITY_5 XCUBE_DELEGATE_PRIORITY_NORMAL
XCUBE_DELEGATE_PRIORITY_6
XCUBE_DELEGATE_PRIORITY_7
XCUBE_DELEGATE_PRIORITY_8
XCUBE_DELEGATE_PRIORITY_9
XCUBE_DELEGATE_PRIORITY_10 XCUBE_DELEGATE_PRIORITY_FINAL
```

### 1.5.4 実行時のファイル読み込み

`add()`メソッドの最後の引数にファイル名を渡すと、デリゲートがコールされる前にそのファイルを読みこませることができます。この仕組は、デリゲート関数が別ファイルにある場合に有効な手段です。デリゲートがコールされなければ、デリゲート関数が定義されているファイルも読み込まれないので、余分なI/Oがなく実行時のリソースを節約することができます。

```php
<?php
$classFile = XOOPS_MODULE_PATH.'/mymodule/class/MyClass.class.php';

$delegate = new XCube_Delegate();
$delegate->add('MyClass::callme()', $classFile);

// 優先度指定と併用する場合
$delegate->add('MyClass::callme()', XCUBE_DELEGATE_PRIORITY_FINAL, $classFile);
```

### 1.5.5 デリゲートからコールバックに変数を渡す

デリゲートからコールバック関数に変数を渡すことができます。 `call()` メソッドの引数に与えられた変数は、その順序でコールバック関数に渡ります。引数の数に制限はありません。

```php
<?php
$delegate = new XCube_Delegate();

// コールバック関数
$delegate->add(function($xoopsUser, $moduleConfig) {
	// $xoopsUserや$moduleConfigが利用可能
});

// コールバック関数に変数を渡す
$delegate->call($xoopsUser, $moduleConfig);
```

プリミティブ型を参照渡しする場合は、`XCube_Ref` オブジェクトを使います。オブジェクト型はPHP5からデフォルトで参照渡しになるので、 `XCube_Ref` を使う必要がなくなりました。

```php
<?php
$delegate = new XCube_Delegate();

// コールバック関数
$delegate->add(function(&$int, &$string) {
	// コールバック関数は参照を受け取るために、&を使う
});

// 参照渡し
$int = 1;
$string = 'string';
$delegate->call(new XCube_Ref($int), new XCube_Ref($string));
```

## 1.6 デリゲートユーティリティ

XOOPS Cubeでは登録済みデリゲートのコールを手軽に行うためのユーティリティクラスを提供しています。登録済みデリゲートをコールするには通常、`XCube_Delegate` のインスタンスを作り、`register` したあと `call` します。

```php
<?php
$delegate = new XCube_Delegate();
$delegate->register('MyDelegate.Foo.Bar');
$delegate->call();
```

`XCube_DelegateUtils` は上記の手続きを1行で行うためのメソッド `call()` を提供します。第1引数に、デリゲート名を指定します。第2引数以降は、コールバック関数に渡されます。

```php
<?php
XCube_DelegateUtils::call('MyDelegate.Foo.Bar');

// 変数をコールバック関数に渡す
XCube_DelegateUtils::call('MyDelegate.Foo.Bar', $xoopsUser, $moduleConfig);
```

## 1.7 デリゲートコールのトレース

デリゲート機構は処理を実行する場所（デリゲート）と実際に処理がある場所（コールバック関数）が分離して存在しているため、しばしばコードが追いにくいことや、どのような処理が実行されているか把握しにくいことがあります。デリゲートがどのような順番で実行されているかなどを知りたいと考えるでしょう。その場合は、`XCube_Delegate::call()` にブレークポイントをしこむと、すべてのデリゲートを追跡することができます。ブレークポイントをしこむには、`Xdebug`が必要になります。`Xdebug`導入が難しいときは、`var_dump()`でデリゲートコールをダンプするだけでも十分実用的です。

# 2. プリロード

## 2.1 プリロードとは

プリロード(preload)とは、モジュール実行前に事前処理を行う仕組みのことです。その処理を書いたプログラムをプリロードと呼ぶこともあります。本書で「プリロード機構」と書いてあるものは仕組みのことを、「プリロード」と書いたものはプリロードプログラムのことを指します。

プリロード機構は、モジュール実行前に設定や下準備をするためのものです。「デリゲート機構」とは独立した仕組みです。しかし、実際のところ多くのプリロードは、「ワンファイルハック」を行うためにデリゲート機構を活用しています。このセクションでは専らプリロードについて扱い、デリゲートについては扱いません。デリゲートについて知りたければ「デリゲート」のセクションを、プリロードでデリゲートを利用する方法については「ワンファイルハック」のセクションを参照してください。

## 2.2 サイトプリロードとモジュールプリロード

プリロードには2種類あります。サイトプリロードとモジュールプリロードです。サイトプリロードはサイトで所有するプリロードです。プリロード単体で配布されているものは、大抵サイトプリロードになります。

モジュールプリロードは、各モジュールが所有するプリロードです。モジュール作者がモジュールに同梱して配布します。モジュールプリロードは、アクティブなモジュールのプリロードに限り読み込まれます。


## 2.3 プリロードの置き場とファイル命名規則

サイトプリロードは、 `/html/preload` に置きます。また、管理画面でのみ読み込むサイトプリロードは `/html/preload/admin` に置きます。モジュールプリロードは、各モジュールの `preload` フォルダに置きます。

プリロードファイルの名前は、`{任意の名前}.class.php` にします。このパターンにマッチしないファイルは `preload` フォルダに置いても読み込まれません。

## 2.4 2つの読み込みタイミング

サイトプリロードとモジュールプリロードは読み込まれるタイミングが異なります。サイトプリロードはXOOPSが起動した直後に読み込まれます。一方のモジュールプリロードは、ある程度コントローラの処理が進んだ時点で読み込まれます。コントローラとプリロードの読み込み順序関係は次のようになります。

1. ★ サイトプリロードの読み込み
1. 環境設定ファイルの読み込み
1. ロガーの初期化
1. データベースへの接続
1. 多言語対応の初期化
1. テキストフィルタの初期化
1. 一般設定の初期化
1. ★ モジュールプリロードの読み込み
1. セッションの初期化
1. ユーザの初期化
1. モジュールの初期化

このリストから分かるように、ロガーやデータベースの初期化に影響を与えられるのはサイトプリロードのみに限られます。

### 2.4.1 モジュールプリロードのロード条件

モジュールプリロードの読み込みは、アクティブな全モジュールに対して行われます。よくある誤解として、アクセス中のモジュールのプリロードのみが読み込まれると思われがちですが、この認識は誤りです。あるモジュールのプリロードで、別のモジュールの動作を変えることが可能です。

### 2.4.2 サイトプリロードの読み込み順

プリロードが読み込まれる順番は、ファイル名のアルファベット順になります。他のプリロードよりいち早く(あるいは最も遅く)読み込ませたい場合は、ファイル名を工夫します。Aで始まるプリロードは最初に、Zで始まるプリロードは最も遅く読み込まれるでしょう。

### 2.4.3 モジュールプリロードの読み込み順

モジュールプリロードの読み込まれる順番は、モジュールの「並び順」の昇順になります。「並び順」は管理画面のモジュール管理で設定される数値です。「並び順」が同値のモジュールが複数ある場合は、モジュールID(mid)の小さいものが先にロードされます。次に、ひとつのモジュールに複数のプリロードがある場合は、サイトプリロードと同様にファイル名の昇順で読み込まれます。

## 2.5 3つの実行タイミング

プリロードが実行されるタイミングは3つあります。1つ目は `preFilter` です。このタイミングはXOOPSが起動した直後です。サイトプリロードの読み込み後に実行されます。ですので、モジュールプリロードは `preFilter` を利用できません。`preFilter` ではデータベースや多言語対応、ロガーなどがセットアップされていませんので、これらを参照することができません。

2つ目は `preBlockFilter` です。モジュールプリロードが読み込まれた後に実行されます。この時点では、データベースの接続が確立していますので、データベースを参照することができます。ユーザの初期化は完了していませんので、ユーザ情報を参照することはできません。

3つ目は `postFilter` です。モジュールが実行される直前に実行されます。この時点では、ほぼすべての初期化が完了していますので、利用できるオブジェクトは3つのフィルタの中で最も多いです。モジュールの初期化も完了しているので、モジュールの情報を利用することもできます。

以上の3つのタイミングをコントローラの処理順序とあわせてみると次のリストのようになります。

1. サイトプリロードの読み込み
1. ★ `preFilter` の実行
1. 環境設定ファイルの読み込み
1. ロガーの初期化
1. データベースへの接続
1. 多言語対応の初期化
1. テキストフィルタの初期化
1. 一般設定の初期化
1. モジュールプリロードの読み込み
1. ★ `preBlockFilter` の実行
1. セッションの初期化
1. ユーザの初期化
1. モジュールの初期化
1. ★ `postFilter` の実行

ちなみに、ひとつのタイミングに複数のプリロードが実行される場合は、先に読み込まれたプリロードが先に実行されます。

## 2.6 実装方法

プリロードは、`XCube_ActionFilter` クラスを継承して実装します。クラス名は任意の名前をつけることができます。任意の名前はファイル名と一致する必要があります。例えば、`FooBar` という名前のプリロードは `FooBar.class.php` にします。大文字小文字は区別します。

FooBar.class.phpの例

```php
<?php
class FooBar extends XCube_ActionFilter
{
}
```

モジュールプリロードは、クラス名の頭にモジュール名を付けます。例えば、`news` モジュールの `FooBar` プリロードは `News_FooBar` という具合にアンダースコアでつなげます。モジュール名の1文字目は大文字にします。`news_letter` のようにアンダースコアを含む場合は、`News_letter_FooBar` になります。ファイル名には、モジュール名を含めないので注意してください。

/html/modules/news/preload/FooBar.class.phpの例

```php
<?php
class News_FooBar extends XCube_ActionFilter
{
}
```

/html/modules/news_letter/preload/FooBar.class.phpの例

```php
<?php
class News_letter_FooBar extends XCube_ActionFilter
{
}
```

プリロードのクラスでは、`preFilter()`, `preBlockFilter()`, `postFilter()` メソッドを1つ以上実装します。これらのメソッドは、実行されるタイミングがことなります。`preFilter()` はXOOPS起動直後に、`preBlockFilter()` はデータベース接続後に、 `postFilter()` はモジュール実行直前に実行されます。実行タイミングの詳細は、「3つの実行タイミング」を御覧ください。

メソッドを実装した FooBar.class.php の例

```php
<?php
class FooBar extends XCube_ActionFilter
{
	public function preFilter()
	{
		var_dump("preFilterが呼ばれました");
	}

	public function preBlockFilter()
	{
		var_dump("preBlockFilterが呼ばれました");
	}

	public function postFilter()
	{
		var_dump("postFilterが呼ばれました");
	}
}
```

## 2.7 プライマリプリロード

モジュールプリロードは `preBlockFilter` のタイミングの直前で読み込まれるため、 原則として `preFilter` のポイントを使うことができません。しかし、プライマリプリロード(primary preloads)にモジュールプリロードを指定することで、 `preFilter` を使えるようになります。プライマリプリロードとは、どのプリロードよりも優先してロードされるプリロードです。

プライマリプリロードは設定ファイル `site_custom.ini` の `Legacy.PrimaryPreloads` の項目に明示的に記述します。XOOPS Cube Legacy 2.1であれば、`/html/settings/site_custom.ini` を作ります。

```
$ touch html/settings/site_custom.ini
```

XOOPS Cube Legacy 2.2では、`settings` フォルダが `xoops_trust_path` に移動されました。`xoops_trust_path` は複数のサイトで共有される可能性があります。すべてのサイトで共通して使う設定は `/xoops_trust_path/settings/site_custom.ini` に記述します。特定のサイトでのみ使う設定は `/xoops_trust_path/settings/site_custom_{XOOPS_SALT}.ini` を作成します。 `XOOPS_SALT` は各サイトごとに異なります。各サイトの `mainfile.php` の `XOOPS_SALT` の定義とあわせます。

```
$ touch xoops_trust_path/settings/site_custom.ini # 共有
$ touch xoops_trust_path/settings/site_custom_a73dfbe9.ini # サイト固有
```

`site_custom.ini` には `site_default.ini` の差分だけ記述します。つまり、`site_custom.ini` をまるごとコピーする必要はありません。`Legacy.PrimaryPreloads` の項目を設定する場合は、 `site_default.ini` から `Legacy.PrimaryPreloads` の項目だけをコピーし `site_custom.ini` に記述します。

`Legacy.PrimaryPreloads` の項目は `キー: 値` の書式で記述します。キーはプリロードクラス名を指定します。値には、プリロードクラスが定義されているファイル名を指定します。ファイル名に指定できるのは `XOOPS_ROOT_PATH` にあるファイルのみです。現時点では、トラストパスのファイルを指定することができませんが、今後のアップデートで改善されるかもしれません。プライマリプリロードは各モジュールの `preload/Primary` フォルダに置きます。

```ini:/xoops_trust_path/settings/site_custom.ini
;                  ;
; Primary Preloads ;
;                  ;

[Legacy.PrimaryPreloads]
protectorLE_Filter=/modules/legacy/preload/protectorLE/protectorLE.class.php
Legacy_SystemModuleInstall=/modules/legacy/preload/Primary/SystemModuleInstall.class.php
Legacy_SiteClose=/modules/legacy/preload/Primary/SiteClose.class.php
User_PrimaryFilter=/modules/user/preload/Primary/Primary.class.php
Legacy_NuSoapLoader=/modules/legacy/preload/Primary/NuSoapLoader.class.php
Legacy_SessionCallback=/modules/legacy/preload/Primary/SessionCallback.class.php
```

## 2.8 プリロードの有効化と無効化

プリロードはモジュールと異なり、管理画面からインストールする必要がありません。`preload` フォルダに配置するだけで実行されるようになります。プリロードを無効化する方法としては、`preload` フォルダから削除するでけですが、`preload/disabled` フォルダを作り、無効化したいプリロードをそこに移動する方法がベストプラクティスです。

# 3. ワンファイルハック

## 3.1 ワンファイルハックとは

ワンファイルハック(one file hack)は、プリロードとデリゲート機構を組み合わせて、コアやモジュールの振る舞いを変える（ハックする）手法を言います。ここでの「ハック」という言葉は、クラッキングのような不正さを表す意味合いはありません。XOOPSでは慣習的に、XOOPS本体やモジュールの機能に手を加えることをハックと表現します。XOOPS Cube Legacy以前のXOOPSでは、デリゲート機構がありませんでしたので、やりたいことを実現するためにしばしばコアのプラグラムを変更しなければならないことがありました。XOOPS Cube Legacyではコアのプログラムを直接変更せずに、プリロードの1ファイルだけでコアの振る舞いをハックできるという点からワンファイルハックと言うようになりました。

現在、配布されているプリロードの多くはワンファイルハックで、例えば次のようなものがあります。アイディア次第で様々なワンファイルハックが可能になります。

* メールアドレスでログインできるようにする。
* 非登録ユーザがユーザプロフィールを閲覧できなくする。
* iPhoneからアクセスが合った場合に専用のテーマに切り替える。
* AdelieDebugのようなデバッグツール。

## 3.2 ワンファイルハックの実装手順

ワンファイルハックは基本的に、プリロードとデリゲート機構を応用して実装しますので、それぞれの基礎的な知識が必要となります。デリゲートについては、「1. デリゲート」のセクションを、プリロードの詳細については、「2. プリロード」を参照してください。ここでは、それぞれの詳細には触れず、「非登録ユーザがユーザプロフィールを閲覧できなくする」ワンファイルハックを例に具体的な実装手順を解説します。

まず、ワンファイルハックを実装するにあたって、プリロードを作ることになります。ここではファイル名は `UserInfoProtector.class.php` としておきます。ファイルの内容は `XCube_ActionFilter` クラスを継承した `UserInfoProtector` クラスを定義することが第一のステップとなります。

```php:UserInfoProtector.class.php
<?php
class UserInfoProtector extends XCube_ActionFilter
{
}
```

次に、プロフィールページのデリゲートを探します。「デリゲート」の章でも述べましたが、デリゲートの公式的な一覧や簡単に探す方法は現在のところありませんので、コアのコードを確認する必要があります。今回の例では `Legacypage.Userinfo.Access` デリゲートが最適です。

デリゲートが特定できれば、今度はデリゲートにコールバックを追加する処理を追加します。この例でのコールバック追加の処理はどのプリロード実行タイミングでもかまいませんが、ここでは `preFilter` を利用することにします。`XCube_ActionFilter` クラスは `XCube_Root` クラスのオブジェクトを `mRoot` メンバとして持っています。なので、コールバックの追加は `mRoot` をつたって `XCube_DelegateManager` を利用します。

```php:UserInfoProtector.class.php
<?php
class UserInfoProtector extends XCube_ActionFilter
{
	public function preFilter()
	{
		$this->mRoot->mDelegateManager->add('Legacypage.Userinfo.Access', function(){
			// コールバック処理
			var_dump('UserInfoProtectorがコールされました'); die;
		});
	}

}
```

この状態で実際に、ユーザプロフィールの画面にアクセスしてコールバック関数が呼ばれているか確認します。

最後に、コールバック関数内で、ゲストユーザかどうかを判定し、ゲストユーザであればサイトトップページにリダイレクトする処理を実装します。

```php:UserInfoProtector.class.php
<?php
class UserInfoProtector extends XCube_ActionFilter
{
	public function preFilter()
	{
		$this->mRoot->mDelegateManager->add('Legacypage.Userinfo.Access', function() {
			$root = XCube_Root::getSingleton();
			if ( $root->mContext->mUser->mIdentity->isAuthenticated() === false ) {
				$root->mController->executeForward(XOOPS_URL);
			}
		});
	}
}
```

## 3.3 ワンファイルハックの問題点

コアやモジュールの振る舞いを変えることができるワンファイルハックは非常に強力ですが問題点もあります。プリロードはXOOPS Cubeの仕組み上、実行時にすべてのプリロードファイルを `require_once` します。しかし、読み込まれたワンファイルハックがページやモジュールによっては必ずしも実行されません。ワンファイルハックはデリゲートがコールされてはじめて実行されるためです。そのため、一部のワンファイルハックはオーバヘッドになってしまい、サイト全体のパフォーマンスに少なからず影響があります。

# 4. おわりに

本稿ではXOOPS Cubeのデリゲート、プリロード、そしてそれらを組み合わせたワンファイルハックについて解説しました。どれも最初はなじみにくい仕組みかもしれませんが、とても強力で便利な機能です。この機会に試してみてはいかがでしょうか。