[XML]要素・属性の使い分けと命名規則(要素名・属性名の決め方)

プログラミングをする上でデータを格納する標準的な形式として XML はよく使われます。
XML には値を入れておくための要素(Element)と属性(Attribute)があり、どちらを使わなければいけないという明確な決まりはありません。
しかしながら何らかのルールに従って要素に格納するか属性を使うかを決め、ファイル全体に一貫性を持たせなければいけません。

<要素 属性="属性の値">要素の内容</要素>

自分一人でルールを決めるよりも一般論を取り入れたほうが無難です。私自身要素・属性の使い分けに悩むことが多いので次のガイドラインを参考にルールについて考えてみたいと思います。

Google XML Document Format Style Guide
https://google.github.io/styleguide/xmlstyle.html

Principles of XML design: When to use elements versus attributes
http://www.ibm.com/developerworks/library/x-eleatt/


要素名・属性名の大文字・小文字

長い要素名を区切る場合、半角スペースは使えないのでそのまま続けて書くか、単語の頭を大文字にする、アンダースコア(_)を利用することになります。

<sampleelement>
<sampleElement>
<SampleElement>
<sample_element>

全て文法としては正しいです。Google のガイドラインは All names MUST use lowerCamelCase. と定めていますし、他の多くの XML 文章を見てもそうなっていることが多いので、ここは「sampleElement」としておくのが良さそうです。

ちなみに「URL」のような略語は「sourceURL」のようにせず「sourceUrl」としたほうが良いようです。しかし「iPhone」や「iMac」をどう処理すべきかは難しいところです。「sampleiPhone」のようにしてしまっては可読性が下がってしまうので、ガイドラインにはありませんが「sampleIphone」とするか「sampleIPhone」のようにしておくしかなさそうですね。

属性に関しても要素と同じ記法を使うのが良さそうです。XHTML などでは属性のみ http-equiv のようにハイフンで書くようにしている場合も多く見られますが特に理由がなければこちらも lowerCamelCase で良いと思います。

要素・属性の使い分け

一般的には要素より属性のほうが制限が多く、長すぎる内容を入れたり改行を含むものを入れるには向きません。要素であれば CDATA セクションも利用できるのであらゆる値は要素だけでも一応表現できます。

ただし、メモリ消費の観点で見ると、要素は同じ要素を複数含む可能性があるため探索に多くのメモリを消費しますが、属性は2つ以上同じ名前を持てないため、少ないメモリで済みます。
とはいえ一つの要素に多くの属性をもたせるのはあまり一般的ではなく、せいぜい 10 個程度にして相関性の強いものを子要素でグループ化してそちらの属性とするのが良いようです。

方向性としては利用者の目に触れるものは要素、管理のために使われる内部的なデータは属性にするのが良いようです。ただし Google は「このルールは別の理由で破られることが多い」と注釈を添えています。

ガイドライン(12. Elements vs. Attributes)によるとおおまかな基準は次のとおりです。(要約&抜粋)

要素に向いているケース

・データが複数回出現するなら要素を使う。「data1=”..” data2=”..”」のようにはしない。
・一つのオブジェクトとしてまとめることができ、親子関係があるなら要素を使う。
・出現順序に意味がある場合
・複数行のデータ
・データが巨大な場合や BASE64 変換されたバイナリなど。
・内容が自然言語であれば要素に入れ、属性として使用言語を記述するのが良い。

属性に向いているケース

・管理用のIDやコード番号は属性にする。
・別のデータのクラス、役割、処理方法を表すメタデータの場合。
・上書きされない限り子孫要素にも属性の内容が反映される場合(xml:lang や 名前空間宣言など)

値の記述

key-value で表すことのできるデータは要素名としてキー、属性 value として value を記述するのが良いとされています。

<genre value="science" />

また、単位系には 国際単位 を使うのが一般的で、できれば単位を表す属性 unit をつけておくのが理想です。

<weight value="180" unit="kg" />

まとめ

要素名・属性名は lowerCamelCase で記述し、ユーザーに見せる内容は要素に、管理・検索に使う内部的な値は属性に持たせる。
key-value のペアで表せるような単純なものであれば空要素を用いて要素名と value 属性の形で表現する。

ただし Google のガイドラインでは次のように締めくくられています。(拙訳)

“ルールの奴隷となって粗雑で独りよがりのむかむかするような汚らしい設計を作るよりはルールの一部、あるいは全てを破ってしまおう。(たとえそれが MUST とされていたとしても。)特に「シンプルなもの/複雑なもの」「メタデータ/データ」のようにはっきり分けられるなら 子要素・属性を両方使ったほうが良いが、不規則に混在させてしまうと従うのも使うのも難しくなってしまう。”

[PHP]ディレクトリー(フォルダ)の階層構造を維持したまま圧縮する

ZipArchive クラスを使った単純なファイルの圧縮については前回の記事に書きましたが、ディレクトリーを対象に、そのサブディレクトリも含めて圧縮するには再帰的に処理する工夫が必要です。

<?php
// 圧縮するディレクトリー
$dir = dirname(__FILE__) . '/base/';

// Zipファイルの保存先
$file = './test.zip';

zipDirectory($dir, $file);


// ディレクトリを圧縮する
function zipDirectory($dir, $file, $root=""){
	$zip = new ZipArchive();
	$res = $zip->open($file, ZipArchive::CREATE);

	if($res){
		// $rootが指定されていればその名前のフォルダにファイルをまとめる
		if($root != "") {
			$zip->addEmptyDir($root);
			$root .= DIRECTORY_SEPARATOR;
		}

		$baseLen = mb_strlen($dir);
		
		$iterator = new RecursiveIteratorIterator(
			new RecursiveDirectoryIterator(
				$dir,
				FilesystemIterator::SKIP_DOTS
				|FilesystemIterator::KEY_AS_PATHNAME
				|FilesystemIterator::CURRENT_AS_FILEINFO
			), RecursiveIteratorIterator::SELF_FIRST
		);

		$list = array();
		foreach($iterator as $pathname => $info){
			$localpath = $root . mb_substr($pathname, $baseLen);
		
			if( $info->isFile() ){
				$zip->addFile($pathname, $localpath);
			} else {
				$res = $zip->addEmptyDir($localpath);
			}
		}

		$zip->close();
	} else {
		return false;
	}
}

ディレクトリ内のファイル一覧の作り方に関しては過去の記事のものをもとに少し変更してあります。
関数を使う際は引数として (ディレクトリパス, Zipファイル保存先, 親ディレクトリの名前) を指定します。親ディレクトリの名前は省略可で、指定するとその名前のフォルダがZipファイル内に作られ、ファイルは親フォルダ以下にまとめられます。

zip_root

[PHP]ZipArchiveクラスを使ったファイルのZip圧縮と展開(解凍)

PHP5.2 以上で、zip Extension が利用可能な環境であればZIPファイル圧縮や展開は次の方法で簡単にできます。

圧縮

<?php
// 圧縮するファイルの配列
$files = array('file1.txt', 'file2.txt');

$zip = new ZipArchive();
$res = $zip->open('./test2.zip', ZipArchive::CREATE);

if($res === true){
	foreach($files as $file){
		$zip->addFile($file);
	}
	$zip->close();
} else {
	echo 'Error Code: ' . $res;
}

展開

<?php
// 圧縮ファイルのパス
$file = './test.zip';

// 展開先ディレクトリ
$to = './test/';

$zip = new ZipArchive();
$res = $zip->open($file);

if($res === true){
	$zip->extractTo($to);
	$zip->close();
} else {
	echo 'Error Code: ' . $res;
}

ZipArchive オブジェクトにファイルを追加するには addFile() を使います。
ファイルではなく文字列からファイルを作成して追加する場合は addFromString(ファイル名, 内容) で行います。

$zip->addFromString("sample.txt", "Hello, World!");

作成した ZIP ファイルをダウンロードさせる場合は次のようにします。

<?php
$file = './test.zip';
header('Content-Type: application/octet-stream'); 
header(sprintf('Content-Disposition: attachment; filename="%s"', basename($file)) ); 
header(sprintf('Content-Length: %d', filesize($file)) );
readfile($file);

今回はファイルのみを圧縮しましたが、フォルダーをディレクトリ構造を維持したまま圧縮する方法についてはこちらの記事にまとめてあります。