みなさんこんにちは。
昔clamav使ったファイルのウイルススキャンをやったことがあって、それを思い出そうと思って適当なライブラリを作ったのですが、本当に適当すぎる作りになっちゃって、いや、これこのままだとさすがに恥ずかしすぎるやろ、って思いまして、ちょいと手直しすることにしたのです。
手直ししているときに、どんなふうに直すか、だけでなく、どんな風に使ってもらえるかを考えていたら、やっぱりinterface用意しておくのが都合がいいね、って感じになりました。
というわけで、interface を使ってよりユーザフレンドりーなライブラリを目指してみましょう。
interface
はじめにinterfaceの解説をするので、不要な人は飛ばしてください。
interfaceとは
PHPにおいてinterfaceとは、あるクラスを定義するときに必要な公開メソッドをまとめたもの、です。
こんな風に書きます。
<?php
interface Sequence
{
public function factorial(int $num): int;
public function permutation(int $num, int $factors): int;
}
ここには、単純な階乗と順列の計算をするメソッドが定義されていますが、動作の中身はありません。中身は別に実装する必要があります。
<?php
require_once './Sequence.php';
class RecursiveSequence implements Sequence
{
public function factorial(int $num): int
{
if ($num === 0) {
return 1;
}
return $num * $this->factorial($num - 1);
}
public function permutation(int $num, int $factors): int
{
return $this->factorial($num) / $this->factorial($num - $factors);
}
}
interfaceの中身を実装したクラスに対しては、implements Sequence
のように書かきます。
このような class <class_name> implements <interface_name>
と書いたとき、<class_name> は <interface_name> の実装であるといいます。
あとは、これを使うだけ。
<?php
require './Sequence.php';
require './RecursiveSequence.php';
$obj = new RecursiveSequence;
echo $obj->factorial(1), "\n";
echo $obj->factorial(3), "\n";
echo $obj->factorial(10), "\n";
echo $obj->permutation(3, 2), "\n";
echo $obj->permutation(5, 3), "\n";
echo $obj->permutation(10, 5), "\n";
動作させると
# php test.php
1
6
3628800
6
60
30240
となります。
interfaceの特徴
大雑把には以下のような特徴があります。
- 型宣言に使える
- interfaceをimplementsしたクラスは、interfaceで宣言したメソッドをすべて実装していないとエラーが出る
- interfaceも継承できる
- implementsに複数のinterfaceを指定できる
interfaceを型宣言に使える
普通のクラス名と同じく、interfaceも型宣言に使えます。
メソッドの返り値
<?php
require_once './Sequence.php';
require_once './RecursiveSequence.php';
class Factory
{
public function create(): Sequence
{
return new RecursiveSequence;
}
}
一方で、引数でも使えて
<?php
require './Sequence.php';
require './Factory.php';
$obj = (new Factory)->create();
test($obj);
function test(Sequence $obj)
{
echo $obj->factorial(1), "\n";
echo $obj->factorial(3), "\n";
echo $obj->factorial(10), "\n";
echo $obj->permutation(3, 2), "\n";
echo $obj->permutation(5, 3), "\n";
echo $obj->permutation(10, 5), "\n";
}
こんな風になります。
PHP7.4以降で追加されるプロパティの型宣言でも、interfaceを使うことができます。
<?php
require_once './Sequence.php';
require_once './RecursiveSequence.php';
class Some
{
private Sequence $obj;
public function __construct()
{
$this->obj = new RecursiveSequence;
}
public function getObj()
{
return $this->obj;
}
}
$some = new Some;
echo get_class($some->getObj()), "\n";
// RecursiveSequence
宣言されたメソッドがそろっていないとエラー
interfaceをimplementsしたクラスは、すべてのメソッドを不足なく実装する必要があります。
以下のような場合はコンソールで実行するとエラーが出ます。
<?php
require_once './Sequence.php';
/**
* Sequence の permutationが実装されていないパターンん
*/
class Dame1 implements Sequence
{
public function factorial(int $num): int
{
return 1;
}
}
$obj = new Dame1;
// Fatal error: Class Dame1 contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Sequence::permutation) in /var/www/dame1.php on line 5
<?php
require_once './Sequence.php';
/**
* Sequence の permutationが実装されていないパターンん
*/
class Dame2 implements Sequence
{
public function factorial(int $num): int
{
return 1;
}
public function permutation(int $num, int $factors): string
{
return "1";
}
}
$obj = new Dame2;
// Fatal error: Declaration of Dame2::permutation(int $num, int $factors): string must be compatible with Sequence::permutation(int $num, int $factors): int in /var/www/dame2.php on line 15
継承できる
interfaceを継承することもできます。
別のクラスを継承しつつ、implementsを指定することもできます。
<?php
require_once './Sequence.php';
require_once './RecursiveSequence.php';
interface OtherSequence extends Sequence
{
}
class Test2 extends RecursiveSequence implements OtherSequence
{
}
$obj = new Test2;
echo $obj->permutation(10, 3), "\n";
// 720
implementsに複数のinterfaceを指定できる
クラス定義のimplementsには、複数のinterfaceを指定することができます。
<?php
interface Factorial
{
public function factorial(int $num): int;
}
interface Permutation
{
public function permutation(int $num, int $factors): int;
}
// 複数のinterfaceを実装できる
class Some implements Factorial, Permutation
{
public function factorial(int $num): int
{
return 1;
}
public function permutation(int $num, int $factors): int
{
return 1;
}
}
$some = new Some;
test($some);// Factorial
test2($some);// Permutation
function test(Factorial $obj)
{
echo 'Factorial', "\n";
}
function test2(Permutation $obj)
{
echo 'Permutation', "\n";
}
ここでSome
は、Factorial
であってPermutation
でもあるということになります。
自作ライブラリにinterfaceを!
clamAVを使ったウイルススキャン
これですね。
https://github.com/niisan-tokyo/web-clamav-php/
前回の記事用に作ったリモートのclamdサーバにウイルススキャンしてもらうライブラリです。
https://qiita.com/niisan-tokyo/items/798c945ab4da26c31d16
旧実装の問題点
何が問題だったかって、ここですよね
$manager = new Niisan\ClamAV\Manager(['url' => 'clamav']);
このManager
の中身はリモートのclamdサーバにファイルを分割して投げるという、実装になっています。しかし、ウイルススキャンの方法は前回の記事でみたように、少なくとも2つはあります。
つまるところ、このようなManager
の実装をそのまま使うコードだと、別実装を使おうとしても、すべてのコードの中で、このManagerを使っている部分を探し出し、置き換え、さらに動作も初めから見直す必要があります。
さらに自作せにゃならんとなれば、その実装まではいるので、ことさら面倒です。
interfaceを使って実装の陰を消す
とりあえず、実装を直に使っている状況はよろしくないということで、interfaceを通して使うようにしましょう。interfaceを通しておけば、そのinterfaceでやりたいこと、つまり、存在するメソッドとその使い方がわかります。
今回はウイルススキャンなので、こんな感じに作ってみます。
<?php
namespace Niisan\ClamAV;
interface Scanner
{
/**
* Ping to scanner server or socket
*
* When the scan server is not connected,
* throw RuntimeException.
*
* @return boolean
* @throws \RuntimeException
*/
public function ping() :bool;
/**
* Scan a file.
*
* When the file have some virus, this method return false,
* otherwise return true.
*
* @param string $file_path
* @return boolean
*/
public function scan(string $file_path): bool;
}
接続確認のping
と、ファイルスキャンするscan
があればいいといった感じですね。
次にこれを生成するクラスとしてFactory
を実装します。
<?php
namespace Niisan\ClamAV;
use Niisan\ClamAV\Scanners\RemoteScanner;
class ScannerFactory
{
public static function create(array $config): Scanner
{
return new RemoteScanner($config);
}
}
createの返り値をScannerにしておきます。
さらに、前回作成したManager
とやらを、Scanner
を実装したRemoteScanner
として定義しなおします。
<?php
namespace Niisan\ClamAV\Scanners;
use Generator;
use Niisan\ClamAV\Scanner;
class RemoteScanner implements Scanner
{
private $port = 3310;
private $url;
public function __construct(array $option)
{
if (empty($option['url'])) {
throw new \RuntimeException('ClamAV server host is not input.');
}
$this->url = $option['url'];
$this->port = $option['port'] ?? $this->port;
}
/**
* @inheritDoc
*/
public function ping(): bool
{
// 何かの処理
return true;
}
/**
* @inheritDoc
*/
public function scan(string $file_path): bool
{
// 何かの処理
return $this->checkMessage($message, 'OK');
}
}
そして、先にManager
をnewしていた部分をFactoryで置き換えます。
$scanner = \Niisan\ClamAV\ScannerFactory::create([
'host' => 'example.com'
]);
一見して、newしていたところをScannerFactory::create()
に変えただけですが、この関数の返り値の型がScanner
となっていますので、このコード中の$scanner
は、とりあえずping
とscan
が実装された何か、という状態になります。
実装を追加して好きなほうを選べるようにする
さて、このライブラリはリモートのclamdサーバを使うことを前提にしていましたが、サーバ一台しかないとかで同じサーバにclamdを動作させている場合もあると思い、それ用の実装を用意してみましょう。
<?php
namespace Niisan\ClamAV\Scanners;
use Niisan\ClamAV\Scanner;
class LocalScanner implements Scanner
{
private $path;
public function __construct(array $options = [])
{
if (! isset($options['path'])) {
throw new \RuntimeException("Socket path not given, i.e. ['path' => /var/run/clamav/clamd.ctl");
}
$this->path = $options['path'];
}
/**
* @inheritDoc
*/
public function ping(): bool
{
// いろいろな実装
return true;
}
/**
* @inheritDoc
*/
public function scan(string $file_path): bool
{
// いろいろな実装
return $this->checkMessage($message, 'OK');
}
この実装もScanner
の実装になっているので、もともとあったRemoteScanner
と同じように使えるはずです。
新しくできた実装を使うために、Factoryを改造します。
<?php
namespace Niisan\ClamAV;
use Niisan\ClamAV\Scanners\RemoteScanner;
use Niisan\ClamAV\Scanners\LocalScanner;
class ScannerFactory
{
public static function create(array $config): Scanner
{
return ($config['driver'] === 'local') ? new LocalScanner($config): new RemoteScanner($config);
}
}
あとは、Factoryを使っているところで
$scanner = \Niisan\ClamAV\ScannerFactory::create([
'driver' => 'remote',
'host' => 'example.com'
]);
こんな感じで使えます。
このFactoryからはLocalScanner
とRemoteScanner
の二つをとりうるわけですが、同じinterface Scanner
の実装になっているので、少なくとも ping
とscan
のみを使っている限りは、使い方は保証されています。
最終的な使用法
この前とあまり変わらないですが、こんな感じです。
composer require niisan-tokyo/web-clamav-php:v0.2.1
でライブラリをインストールするなり更新するなりしてから
<!DOCTYPE html>
<head>
<meta charset="utf-8" />
</head>
<body>
<h1>ファイルを送信</h1>
<form action="file.php" enctype="multipart/form-data" method="POST">
<input type="file" name="upfile" /><br>
<button type="submit" name="go">送信する</button>
</form>
</body>
<?php
require 'vendor/autoload.php';
use Niisan\ClamAV\ScannerFactory;
$file = $_FILES['upfile'];
$scanner = ScannerFactory::create([
'driver' => 'remote',
'url' => 'clamav'
]);
$result = true;
if ($file) {
$start = microtime(true);
$result = $scanner->scan($file['tmp_name']);
$resTime = microtime(true) - $start;
}
?>
<!DOCTYPE html>
<head>
<meta charset="utf-8" />
</head>
<body>
<h1>問題<?php if ($result) {?> なし <?php } else {?> あり <?php } ?>!</h1>
時間: <?php echo $resTime ?><br>
<a href="index.html">戻る</a>
</body>
でいけます。
もしも、ローカルのclamdサーバで使うときは
$scanner = ScannerFactory::create([
'driver' => 'local',
'path' => '/var/run/clamav/clamd.ctl'
]);
としてやればよいです。
まとめ
こんな感じで、interfaceを使ってライブラリの使い勝手を少しだけ上げてみました。
ここまでくればDIとかもできるので、そっちへの応用も記事化しようかなって思います。
今回はこんなところです。