Help us understand the problem. What is going on with this article?

自作したウイルススキャンのライブラリがあまりにも残念だったので、interfaceを使って少しでもましにしたい

みなさんこんにちは。

昔clamav使ったファイルのウイルススキャンをやったことがあって、それを思い出そうと思って適当なライブラリを作ったのですが、本当に適当すぎる作りになっちゃって、いや、これこのままだとさすがに恥ずかしすぎるやろ、って思いまして、ちょいと手直しすることにしたのです。
手直ししているときに、どんなふうに直すか、だけでなく、どんな風に使ってもらえるかを考えていたら、やっぱりinterface用意しておくのが都合がいいね、って感じになりました。

というわけで、interface を使ってよりユーザフレンドりーなライブラリを目指してみましょう。

interface

はじめにinterfaceの解説をするので、不要な人は飛ばしてください。

interfaceとは

PHPにおいてinterfaceとは、あるクラスを定義するときに必要な公開メソッドをまとめたもの、です。
こんな風に書きます。

Sequence.php
<?php

interface Sequence
{

    public function factorial(int $num): int;

    public function permutation(int $num, int $factors): int;
}

ここには、単純な階乗と順列の計算をするメソッドが定義されていますが、動作の中身はありません。中身は別に実装する必要があります。

RecursiveSequence.php
<?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> の実装であるといいます。
あとは、これを使うだけ。

test.php
<?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も型宣言に使えます。

メソッドの返り値

Factory.php
<?php
require_once './Sequence.php';
require_once './RecursiveSequence.php';

class Factory
{
    public function create(): Sequence
    {
        return new RecursiveSequence;
    }
}

一方で、引数でも使えて

test.php
<?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したクラスは、すべてのメソッドを不足なく実装する必要があります。
以下のような場合はコンソールで実行するとエラーが出ます。

dame1.php
<?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
dame2.php
<?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を指定することもできます。

test2.php
<?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を指定することができます。

test4.php
<?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でやりたいこと、つまり、存在するメソッドとその使い方がわかります。
今回はウイルススキャンなので、こんな感じに作ってみます。

src/Scanner.php
<?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を実装します。

src/ScannerFactory.php
<?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として定義しなおします。

src/Scanners/RemoteScanner.php
<?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は、とりあえずpingscanが実装された何か、という状態になります。

実装を追加して好きなほうを選べるようにする

さて、このライブラリはリモートのclamdサーバを使うことを前提にしていましたが、サーバ一台しかないとかで同じサーバにclamdを動作させている場合もあると思い、それ用の実装を用意してみましょう。

src/Scanners/LocalScanner.php
<?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を改造します。

src/ScannerFactory.php
<?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からはLocalScannerRemoteScannerの二つをとりうるわけですが、同じinterface Scanner の実装になっているので、少なくとも pingscanのみを使っている限りは、使い方は保証されています。

最終的な使用法

この前とあまり変わらないですが、こんな感じです。

composer require niisan-tokyo/web-clamav-php:v0.2.1

でライブラリをインストールするなり更新するなりしてから

index.html
<!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>
file.php
<?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とかもできるので、そっちへの応用も記事化しようかなって思います。

今回はこんなところです。

niisan-tokyo
流行りに微妙に遅れてついていく、エンジニア9年生です。
roxx
人材紹介業むけプラットフォーム「agent bank」、リファレンスチェックサービス「back check」を運営。
https://roxx.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした