PHP の Flysystem の使い方まとめ

PHP でファイル操作のためのシンプルな API を提供する Composer パッケージ「 Flysystem 」の使い方をまとめてみました。

PHP Composer package Flysystem

Flysystem は「さまざまなストレージを共通のインタフェースで扱えること」が大きな特徴のひとつですが、ローカルのストレージに対して使うだけでも PHP の組み込みの fopen()fread()fclose()mkdir()rmdir()scandir()SplFileInfoSplFileObject の使用に伴うわずらわしさや思わぬバグの発生を抑えられるため、ローカルのストレージに対して利用することだけでも大きなメリットがあります。

今回のサンプルコードでは、次のバージョンを使って動作確認をしました。 バージョンが異なるとインタフェースが違うこともあるため、参考にされる際はご注意ください。

  • PHP 5.6
  • Flysystem 1.0.41

目次

長くなったので目次をつけました。

  • インストール
  • 基本の考え方
  • サンプルコードの前提
  • ファイル操作
    • 読み込み
    • 書き込み
    • 削除
    • コピー
  • ディレクトリ操作
    • 作成
    • 削除
    • リスト
  • 共通操作
    • 存在チェック
    • 名前の変更 / 移動
    • 権限チェック
    • メタ情報取得

最初にパッケージのインストール方法から見ていきましょう。

インストール

Flysystem は Composer パッケージなので、インストールは composer コマンドを使って行います。 Flysystem のベンダ名を含むパッケージ名は league/flysystem です。

$ composer require league/flysystem

基本の考え方

具体的な使い方を見ていく前に、 Flysystem を使う上で基本となる考え方を押さえておくとよいでしょう。 重要な考え方が 2 つあります。

アダプタパターン

Flysystem はアダプタパターンによりさまざまなストレージをサポートしています。 公式のドキュメントを見ると、公式には次の 3 つのストレージがサポートされていて、

  • ローカルストレージ( Local
  • FTP ( Ftp
  • テスト用 ( NullAdapter

追加パッケージを入れれば以下のストレージなども利用できるようです(動作確認はしていません)。

  • インメモリ
  • Zip ファイル
  • Azure
  • AWS S3
  • Copy.com
  • DigitalOcean Spaces
  • Dropbox

アダプタはこのパッケージの核となる Filesystem クラスのコンストラクタに渡す形で使用します。

use League\Flysystem\Filesystem as FlyFilesystem;
use League\Flysystem\Adapter\Local as FlyLocal;

$fs = new FlyFilesystem(new FlyLocal(__DIR__));

アダプタルートディレクトリ

Flysystem の各アダプタには「ルートディレクトリ」というものが存在します。 ファイルアクセスはそのルートディレクトリからの相対パスで行う形になります。

ローカルストレージの場合、ルートディレクトリはアダプタクラス Local のコンストラクタにパスを渡す形で指定します。

use League\Flysystem\Filesystem as FlyFilesystem;
use League\Flysystem\Adapter\Local as FlyLocal;

$adapter = new FlyLocal('root/directory');
$fs = new FlyFilesystem($adapter);

$content = $fs->read('/sample.txt');
// => ファイル root/directory/sample.txt の中身が取得される

その他 Flysystem を利用する上で重要な概念については公式の「 Core Concepts 」のページが詳しいので、利用する前にそちらもあわせてチェックしておくとよいでしょう。

サンプルコードの前提

以下のサンプルでは次の 2 行の use 文が事前に宣言されているという想定でお読みください。

use League\Flysystem\Filesystem as FlyFilesystem;
use League\Flysystem\Adapter\Local as FlyLocal;

ファイル操作

読み込み

ファイルの読み込みは read() または readStream() で行います。

// 読み込み
$fs = new FlyFilesystem(new FlyLocal(__DIR__));

$content = $fs->read('data/sample-in.txt');
print($content);
// => sample-in.txt の中身の文字列
// 読み込み(ストリーム)
$fs = new FlyFilesystem(new FlyLocal(__DIR__));

$stream = $fs->readStream('data/sample-in.txt');
while (!feof($stream)) {
  $line = fgets($stream);
  print($line);
}
fclose($stream);
// => sample-in.txt の中身を行単位で分割した文字列

ファイルサイズが十分小さいとわかっている場合は read() を、そうでない場合は readStream() を使うとよいかと思います。

書き込み

ファイルの書き込みは少し細かく分かれていて、 write() update() put() という 3 つのメソッドがあります。 ちがいは次のとおりです。

  • write() ファイル新規作成
  • update() ファイル更新
  • put() ファイル新規作成 or 更新

write()update() は思わずファイルが存在した場合や、逆に思わず存在しない場合には例外が上がる安全設計になっています。

// 書き込み(新規作成)
// ファイルがすでに存在すれば例外 League\Flysystem\FileExistsException が上がる
use League\Flysystem\FileExistsException as FlyFileExistsException;

$fs = new FlyFilesystem(new FlyLocal(__DIR__));

try {
  $content = $fs->write('data/sample-out.txt', 'This text is written.');
} catch (FlyFileExistsException $e) {
  throw $e;
}
// 書き込み(更新)
// ファイルが存在しなければ例外 League\Flysystem\FileNotFoundException が上がる
use League\Flysystem\FileNotFoundException as FlyFileNotFoundException;

$fs = new FlyFilesystem(new FlyLocal(__DIR__));

try {
  $content = $fs->update('data/sample-out.txt', 'This text is written.');
} catch (FlyFileNotFoundException $e) {
  throw $e;
}
// 書き込み(新規もしくは更新)
// 書き込み権限がない場合は Warning のみ上がる(内部で使われている file_put_contents() の挙動)
$fs = new FlyFilesystem(new FlyLocal(__DIR__));

$content = $fs->put('data/sample-out.txt', 'This text is written.');

いずれの場合も、第 1 引数はアダプタルートディレクトリからの相対パス、第 2 引数は書き込む内容の文字列を渡します。 ここにはサンプルコードはあげていませんが、第 3 引数には可視性などの引数が指定できたりします。

パス途中のディレクトリが存在しない場合は自動的に新規作成されます。

書き込みの場合も、読み込みの場合と同じくストリームがサポートされています。 write() update() put() にそれぞれ対応した writeStream() updateStream() putStream() というメソッドが用意されており、いずれも、第 2 引数には文字列の代わりにストリームを指定します。

// 書き込み(ストリーム)
// write() update() put() のストリーム版がそれぞれ存在します
$fs = new FlyFilesystem(new FlyLocal(__DIR__));

// 新規作成
$stream = fopen('https://packagist.org/search.json?q=league', 'r');
$content = $fs->writeStream('data/packages.json', $stream);
fclose($stream);

// 更新
$stream = fopen('https://packagist.org/search.json?q=league', 'r');
$content = $fs->updateStream('data/packages.json', $stream);
fclose($stream);

// 新規作成または更新
$stream = fopen('https://packagist.org/search.json?q=league', 'r');
$content = $fs->putStream('data/packages.json', $stream);
fclose($stream);

削除

ファイルの削除は delete() メソッドで行います。 引数は削除対象ファイルのアダプタルートディレクトリからの相対パスです。

// 削除
$fs = new FlyFilesystem(new FlyLocal(__DIR__));

$fs->delete('data/inner/sample.txt');

ファイル読み込みと削除を一括で行える readAndDelete() というメソッドも用意されています。

// 削除(中身を読み込んだ上で削除)
$fs = new FlyFilesystem(new FlyLocal(__DIR__));

$fs->readAndDelete('data/inner/sample.txt');

コピー

ファイルのコピーはそのまま名前のとおりの copy() メソッドで行います。

// コピー
$fs = new FlyFilesystem(new FlyLocal(__DIR__));

$fs->copy('data/original.txt', 'data/copied.txt');

ディレクトリ操作

作成

ディレクトリの作成は createDir() メソッドで行います。

// 作成
// 再帰的にすべてのディレクトリが作られる
// 失敗すると PHP Warning が上がる( mkdir() の挙動)
$fs = new FlyFilesystem(new FlyLocal(__DIR__));

$fs->createDir('data/inner');

パス途中のディレクトリが存在しない場合は自動的に作成されます。

Flysystem でファイルの書き込みをすると途中のディレクトリは自動的に作成されるので、ディレクトリの削除する操作に比べるとディレクトリの作成をする頻度はそれほど高くないかと思います。

削除

ディレクトリの削除は deleteDir() メソッドで行います。

// 削除
// 中にファイルが含まれている場合はファイルもろとも削除される
$fs = new FlyFilesystem(new FlyLocal(__DIR__));

$fs->deleteDir('data/inner');

deleteDir() には注意が必要で、中にファイルが含まれている場合は下層のファイルごとすべて削除されてしまいます。 rmdir() と同じ挙動を期待していると思わぬデータロスに繋がるので使う場合は細心の注意が必要です。

リスト

ディレクトリ内のファイルの一覧取得は listContents() メソッドで行います。

// リスト
use League\Flysystem\NotSupportedException;

$fs = new FlyFilesystem(new FlyLocal(__DIR__));

try {
  $entries = $fs->listContents('/data');
} catch (NotSupportedException $e) {
  throw $e;
  // => Links are not supported, encountered link at ...
}

foreach ($entries as $entry) {
  // ローカルストレージの場合、各 $entry は以下のキーを持つ連想配列
  // - type
  // - path
  // - timestamp
  // - size
  // - dirname
  // - basename
  // - extension
  // - filename
  if ($entry['type'] === 'file') {
    print($entry['path'] . ' is a file.' . PHP_EOL);
  }
  elseif ($entry['type'] === 'dir') {
    print($entry['path'] . ' is a directory.' . PHP_EOL);
  }
}

listContents() の戻り値はファイルのメタ情報を格納した連想配列の配列です。

PHP の組みこみの scandir() 関数との大きなちがいとして、 ... (カレントディレクトリと親ディレクトリ)は listContents() の結果には含まれません。 これは、 scandir() を使って ... に煩わされた経験のある人にはとてもうれしいポイントでしょう。

また、私が確かめたかぎりでは、シンボリックリンクはサポートされていません。 シンボリックリンクが含まれるディレクトリに対して listContents() を実行した場合、デフォルトでは独自の例外 League\Flysystem\NotSupportedException が上がるようになっています。

シンボリックリンクの扱いとしては「例外を上げる」以外に「スキップする」というのも用意されており、これを選ぶには、アダプタのコンストラクタの第 3 引数に定数 Local::SKIP_LINKS を渡せば OK です。

$adapter = new Local($directory, LOCK_EX, Local::SKIP_LINKS);

「例外を上げる」を表すデフォルト値は Local::DISALLOW_LINKS です。

ちなみに、 Local クラスのコンストラクタの宣言部は次のようになっています。

/**
 * Constructor.
 *
 * @param string $root
 * @param int    $writeFlags
 * @param int    $linkHandling
 * @param array  $permissions
 *
 * @throws LogicException
 */
public function __construct($root, $writeFlags = LOCK_EX, $linkHandling = self::DISALLOW_LINKS, array $permissions = [])

上の「アダプタルートディレクトリ」の項のとおり、ファイルはすべてアダプタルートディレクトリからの相対パスで取得されます。 結果、ファイル指定時のパスの先頭の / はあってもなくても同じ挙動になるようです。

// 先頭の / はあってもなくても同じ(ローカルストレージの場合だけ動作確認しています)
$matched = $fs->listContents('/data') === $fs->listContents('data');
// => true

/ ありの場合となしの場合のどちらを使ってもよいかと思いますが、プロジェクト内ではどちらか一方に統一するのがよいでしょう。

listContents() はデフォルトでは対象ディレクトリ直下のファイルだけを返すようになっています。 下層も含めて所属するすべてのファイルの一覧を取得したい場合は、第 2 引数の $recursive オプションに true を渡します。

// リスト(再帰で下層のファイルも取得する)

use League\Flysystem\NotSupportedException;

$fs = new FlyFilesystem(new FlyLocal(__DIR__));

try {
  $entries = $fs->listContents('data', true);
} catch (NotSupportedException $e) {
  throw $e;
  // => Links are not supported, encountered link at ...
}

アダプタルートディレクトリの外にあるファイルにアクセスしようとすると、例外 LogicException があがります。 このあたりも安心して使えるありがたい仕組みになっています。

// リスト(ルートディレクトリ外のファイルへのアクセス)
$fs = new FlyFilesystem(new FlyLocal(__DIR__));

try {
  $entries = $fs->listContents('..');
} catch (LogicException $e) {
  throw $e;
  // => Fatal error: Uncaught exception 'LogicException' with message 'Path is outside of the defined root, ...
}

アダプタルートディレクトリ直下のファイルを走査したい場合は、 listContents() の第 1 引数に文字列 . か空文字列を渡す、あるいは第 1 引数そのものを省略すると OK です。

// リスト
$fs = new FlyFilesystem(new FlyLocal(__DIR__));

// 次の 3 つは同じ結果
$entries = $fs->listContents('');
$entries = $fs->listContents('.');
$entries = $fs->listContents();

共通操作

以下の操作については、ファイルとディレクトリに対して共通の API で操作が行えます。

存在チェック

存在チェックは has() メソッドで行います。 戻り値は booleantrue または false です。

// 存在チェック
$fs = new FlyFilesystem(new FlyLocal(__DIR__));

$is_found = $fs->has('/data/mysterious-file.txt');
// => true または false

名前の変更 / 移動

名前の変更や移動は rename() メソッドで行います。

// 名前の変更 / 移動
// ファイル書き込みの場合と同様、ディレクトリは自動で作成される
$fs = new FlyFilesystem(new FlyLocal(__DIR__));

$fs->rename('data/old.txt', 'data/01/new.txt');

権限チェック / 変更

権限のチェックや変更には、 2 つのメソッド getVisibility()setVisibility() を使用します。

ただし、権限は 8 進数表記の 755 などではなく、権限は複数のプラットフォームに対応できるよう publicprivate の 2 つの値を取りうる「 visibility 」という概念で取り扱う形になっています。

// 権限チェック / 変更
$fs = new FlyFilesystem(new FlyLocal(__DIR__));

$metadata = $fs->getVisibility('/data/secret-file.txt');
// => public
$metadata = $fs->setVisibility('/data/secret-file.txt', 'private');
$metadata = $fs->getVisibility('/data/secret-file.txt');
// => private

publicprivate のデフォルト値は Local クラスの static プロパティで与えられており、具体的な値は次のとおりとなっています。

/**
 * @var array
 */
protected static $permissions = [
    'file' => [
        'public' => 0644,
        'private' => 0600,
    ],
    'dir' => [
        'public' => 0755,
        'private' => 0700,
    ]
];

これらは Local クラスのコンストラクタの第 4 引数でアダプタ単位で指定することもできます。

メタ情報取得

ファイルのメタ情報の取得には getMetadata() getTimestamp() getSize() getMimetype() などのメソッドを使用します。

// メタ情報取得
$fs = new FlyFilesystem(new FlyLocal(__DIR__));

$metadata = $fs->getMetadata('/data/unknown-file.txt');
// => [
//   "type" => "file",
//   "path" => "/data/unknown-file.txt",
//   "timestamp" => 1507358850,
//   "size" => 13,
// ]

$metadata = $fs->getTimestamp('/data/unknown-file.txt');
// => 1507358850

$mimetype = $fs->getSize('data/sample-in.txt');
// => 13

$mimetype = $fs->getMimetype('data/sample-in.txt');
// => "text/plain"

上のとおり getMetadata() は各種メタ情報を格納した連想配列を、 getTimestamp()getSize()getMimetype() はスカラ型の数値または文字列を返します。

ひととおり、使い方については以上です。

PHP の組み込みの関数とクラスを使ってファイルやディレクトリを扱うのはなかなかの苦痛ですが、 Flysystem を使うとこのあたりの無用な苦労はだいぶ回避することができます。

最後にもうひとつ、少し長めのサンプルコードを。 これは特定のディレクトリの中にあるファイルをすべて上の階層に移動して、空になったディレクトリを削除するサンプルです。

// ディレクトリの中身を外に取り出す

use League\Flysystem\Filesystem as FlyFileSystem;
use League\Flysystem\Adapter\Local as FlyLocal;

$filesystem = new FlyFileSystem(new FlyLocal($container));

// $container ディレクトリ内にある $wrapper_dir ディレクトリの中身を $container ディレクトリに移す
// listContents() の戻り値には . や .. は含まれません
foreach ($filesystem->listContents($wrapper_dir) as $file) {
  $filesystem->rename($file['path'], $file['basename']);
}

// すべて移動したのでラッパーディレクトリを削除する
$filesystem->deleteDir($wrapper_dir);

論理的にはシンプルな操作ですが、これを素の PHP で書くとなると思わず落とし穴に引っかかったりするので、「シンプルな操作をシンプルに書けること」というのは言語としてとても重要なことだなぁと改めて思う次第です。

・・・長くなりましたが、以上で Flysystem のまとめは終了です。

リンク先の日本語のページもわかりやすいので、興味のある方はよろしければ。