From 447806cc6f799f888e4088519b44b3753e940255 Mon Sep 17 00:00:00 2001
From: David Maus <maus@hab.de>
Date: Sun, 13 Jan 2013 09:50:56 +0100
Subject: [PATCH] Adjust directory structure for package delivery via Composer

Also includes cosmetic fixes wrt coding-style.
---
 .gitignore                                    |  15 +-
 .hgignore                                     |  11 -
 build.local.xml                               |   6 -
 build.properties                              |   9 -
 build.xml                                     | 500 ------------------
 composer.json                                 |  16 +
 package.xml                                   | 154 ------
 phpunit.xml                                   |  21 +
 phpunit.xml.dist                              |  28 -
 src/.empty                                    |   0
 src/HAB/Pica/Record/AuthorityRecord.php       | 147 +++++
 src/HAB/Pica/Record/CopyRecord.php            | 170 ++++++
 src/HAB/Pica/Record/Field.php                 | 337 ++++++++++++
 src/HAB/Pica/Record/Helper.php                | 131 +++++
 src/HAB/Pica/Record/LocalRecord.php           | 196 +++++++
 src/HAB/Pica/Record/NestedRecord.php          | 192 +++++++
 src/HAB/Pica/Record/Record.php                | 286 ++++++++++
 src/HAB/Pica/Record/Subfield.php              | 146 +++++
 src/HAB/Pica/Record/TitleRecord.php           | 214 ++++++++
 src/README.txt                                |  68 ---
 src/bin/.empty                                |   0
 src/data/.empty                               |   0
 src/docs/.empty                               |   0
 src/php/.empty                                |   0
 src/php/HAB/Pica/Record/AuthorityRecord.php   | 145 -----
 src/php/HAB/Pica/Record/CopyRecord.php        | 166 ------
 src/php/HAB/Pica/Record/Field.php             | 326 ------------
 src/php/HAB/Pica/Record/Helper.php            | 132 -----
 src/php/HAB/Pica/Record/LocalRecord.php       | 189 -------
 src/php/HAB/Pica/Record/NestedRecord.php      | 186 -------
 src/php/HAB/Pica/Record/Record.php            | 277 ----------
 src/php/HAB/Pica/Record/Subfield.php          | 141 -----
 src/php/HAB/Pica/Record/TitleRecord.php       | 208 --------
 src/tests/.empty                              |   0
 src/tests/functional-tests/.empty             |   0
 src/tests/integration-tests/.empty            |   0
 src/tests/unit-tests/.empty                   |   0
 src/tests/unit-tests/bin/.empty               |   0
 src/tests/unit-tests/bootstrap.php            |  36 --
 src/tests/unit-tests/php/.empty               |   0
 .../HAB/Pica/Record/AuthorityRecordTest.php   | 147 -----
 .../php/HAB/Pica/Record/CopyRecordTest.php    |  75 ---
 .../php/HAB/Pica/Record/FieldTest.php         | 225 --------
 .../php/HAB/Pica/Record/LocalRecordTest.php   | 170 ------
 .../php/HAB/Pica/Record/RecordTest.php        |  73 ---
 .../php/HAB/Pica/Record/SubfieldTest.php      |  96 ----
 .../php/HAB/Pica/Record/TitleRecordTest.php   | 112 ----
 src/tests/unit-tests/www/.empty               |   0
 src/www/.empty                                |   0
 tests/bootstrap.php                           |  31 ++
 .../HAB/Pica/Record/AuthorityRecordTest.php   | 153 ++++++
 tests/src/HAB/Pica/Record/CopyRecordTest.php  |  82 +++
 tests/src/HAB/Pica/Record/FieldTest.php       | 231 ++++++++
 tests/src/HAB/Pica/Record/LocalRecordTest.php | 183 +++++++
 tests/src/HAB/Pica/Record/RecordTest.php      |  81 +++
 tests/src/HAB/Pica/Record/SubfieldTest.php    |  97 ++++
 tests/src/HAB/Pica/Record/TitleRecordTest.php | 124 +++++
 57 files changed, 2845 insertions(+), 3488 deletions(-)
 delete mode 100644 .hgignore
 delete mode 100644 build.local.xml
 delete mode 100644 build.properties
 delete mode 100644 build.xml
 create mode 100644 composer.json
 delete mode 100644 package.xml
 create mode 100644 phpunit.xml
 delete mode 100644 phpunit.xml.dist
 delete mode 100644 src/.empty
 create mode 100644 src/HAB/Pica/Record/AuthorityRecord.php
 create mode 100644 src/HAB/Pica/Record/CopyRecord.php
 create mode 100644 src/HAB/Pica/Record/Field.php
 create mode 100644 src/HAB/Pica/Record/Helper.php
 create mode 100644 src/HAB/Pica/Record/LocalRecord.php
 create mode 100644 src/HAB/Pica/Record/NestedRecord.php
 create mode 100644 src/HAB/Pica/Record/Record.php
 create mode 100644 src/HAB/Pica/Record/Subfield.php
 create mode 100644 src/HAB/Pica/Record/TitleRecord.php
 delete mode 100644 src/README.txt
 delete mode 100644 src/bin/.empty
 delete mode 100644 src/data/.empty
 delete mode 100644 src/docs/.empty
 delete mode 100644 src/php/.empty
 delete mode 100644 src/php/HAB/Pica/Record/AuthorityRecord.php
 delete mode 100644 src/php/HAB/Pica/Record/CopyRecord.php
 delete mode 100644 src/php/HAB/Pica/Record/Field.php
 delete mode 100644 src/php/HAB/Pica/Record/Helper.php
 delete mode 100644 src/php/HAB/Pica/Record/LocalRecord.php
 delete mode 100644 src/php/HAB/Pica/Record/NestedRecord.php
 delete mode 100644 src/php/HAB/Pica/Record/Record.php
 delete mode 100644 src/php/HAB/Pica/Record/Subfield.php
 delete mode 100644 src/php/HAB/Pica/Record/TitleRecord.php
 delete mode 100644 src/tests/.empty
 delete mode 100644 src/tests/functional-tests/.empty
 delete mode 100644 src/tests/integration-tests/.empty
 delete mode 100644 src/tests/unit-tests/.empty
 delete mode 100644 src/tests/unit-tests/bin/.empty
 delete mode 100644 src/tests/unit-tests/bootstrap.php
 delete mode 100644 src/tests/unit-tests/php/.empty
 delete mode 100644 src/tests/unit-tests/php/HAB/Pica/Record/AuthorityRecordTest.php
 delete mode 100644 src/tests/unit-tests/php/HAB/Pica/Record/CopyRecordTest.php
 delete mode 100644 src/tests/unit-tests/php/HAB/Pica/Record/FieldTest.php
 delete mode 100644 src/tests/unit-tests/php/HAB/Pica/Record/LocalRecordTest.php
 delete mode 100644 src/tests/unit-tests/php/HAB/Pica/Record/RecordTest.php
 delete mode 100644 src/tests/unit-tests/php/HAB/Pica/Record/SubfieldTest.php
 delete mode 100644 src/tests/unit-tests/php/HAB/Pica/Record/TitleRecordTest.php
 delete mode 100644 src/tests/unit-tests/www/.empty
 delete mode 100644 src/www/.empty
 create mode 100644 tests/bootstrap.php
 create mode 100644 tests/src/HAB/Pica/Record/AuthorityRecordTest.php
 create mode 100644 tests/src/HAB/Pica/Record/CopyRecordTest.php
 create mode 100644 tests/src/HAB/Pica/Record/FieldTest.php
 create mode 100644 tests/src/HAB/Pica/Record/LocalRecordTest.php
 create mode 100644 tests/src/HAB/Pica/Record/RecordTest.php
 create mode 100644 tests/src/HAB/Pica/Record/SubfieldTest.php
 create mode 100644 tests/src/HAB/Pica/Record/TitleRecordTest.php

diff --git a/.gitignore b/.gitignore
index be84e34..ac1c8a8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,8 @@
-.build
-dist
-.tmp
-nbproject
-review
-vendor
-\#*#
-.#*#
+*~
+\#*
+.\#*
 TAGS
+ChangeLog
+vendor
+review
+build
diff --git a/.hgignore b/.hgignore
deleted file mode 100644
index ac0b765..0000000
--- a/.hgignore
+++ /dev/null
@@ -1,11 +0,0 @@
-syntax: glob
-
-.build
-.dist
-nbproject
-review
-tmp
-vendor
-\#*#
-.#*#
-TAGS
diff --git a/build.local.xml b/build.local.xml
deleted file mode 100644
index 90aa3bc..0000000
--- a/build.local.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<project name="local" default="help">
-  <target name="help">
-    <echo message="This component has no local build targets." />
-  </target>
-</project>
-<!-- vim: set tabstop=2 shiftwidth=2 expandtab: -->
diff --git a/build.properties b/build.properties
deleted file mode 100644
index 7385fab..0000000
--- a/build.properties
+++ /dev/null
@@ -1,9 +0,0 @@
-project.name=PicaRecord
-project.channel=hab20.hab.de/service/pear
-project.majorVersion=0
-project.minorVersion=4
-project.patchLevel=0
-project.snapshot=false
-
-component.type=php-library
-component.version=11
diff --git a/build.xml b/build.xml
deleted file mode 100644
index 7db58a0..0000000
--- a/build.xml
+++ /dev/null
@@ -1,500 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- build file for phing -->
-<project default="help" basedir="." name="${project.name}">
-  <!-- Human-readable info about our component -->
-  <property file="build.properties" />
-  <taskdef name="now" classname="Phix_Project.ComponentManager.Phing.NowTask" />
-  <now name="date.now"/>
-  <if>
-    <and>
-      <isset property="project.snapshot"/>
-      <istrue value="${project.snapshot}"/>
-    </and>
-    <then>
-      <property name="project.version" value="${project.majorVersion}.${project.minorVersion}.${project.patchLevel}snapshot${date.now}" />
-      <property name="project.stability" value="snapshot" />
-    </then>
-    <else>
-      <property name="project.version" value="${project.majorVersion}.${project.minorVersion}.${project.patchLevel}" />
-      <property name="project.stability" value="stable" />
-    </else>
-  </if>
-  <property name="project.apiversion"      value="${project.majorVersion}.${project.minorVersion}" />
-
-  <!-- Paths to the directories that we work with -->
-  <property name="project.srcdir"          value="${project.basedir}/src" override="true" />
-  <property name="project.src.phpdir"      value="${project.srcdir}/php" override="true" />
-  <property name="project.src.bindir"      value="${project.srcdir}/bin" override="true" />
-  <property name="project.src.datadir"     value="${project.srcdir}/data" override="true" />
-  <property name="project.src.docdir"      value="${project.srcdir}/docs" override="true" />
-  <property name="project.src.testdir"     value="${project.srcdir}/tests" override="true" />
-  <property name="project.src.wwwdir"      value="${project.srcdir}/www" override="true" />
-  <property name="project.src.testunitdir" value="${project.src.testdir}/unit-tests" override="true" />
-  <property name="project.src.testintdir"  value="${project.src.testdir}/integration-tests" override="true" />
-  <property name="project.src.testfuncdir" value="${project.src.testdir}/functional-tests" override="true" />
-
-  <property name="project.reviewdir"       value="${project.basedir}/review" override="true" />
-  <property name="project.review.logsdir"  value="${project.basedir}/review/logs" override="true" />
-  <property name="project.review.docsdir"  value="${project.reviewdir}/docs" override="true" />
-  <property name="project.review.codebrowserdir" value="${project.reviewdir}/code-browser" override="true" />
-  <property name="project.review.codecoveragedir" value="${project.reviewdir}/code-coverage" override="true" />
-
-  <property name="project.builddir"        value="${project.basedir}/.build"  override="true" />
-  <property name="project.pkgdir"          value="${project.builddir}/${project.name}-${project.version}" override="true" />
-  <property name="project.tmpdir"          value="${project.basedir}/.tmp" override="true" />
-
-  <property name="project.vendordir"       value="${project.basedir}/vendor" override="true" />
-  <property name="project.vendor.bindir"   value="${project.vendordir}/bin" override="true" />
-  <property name="project.vendor.datadir"  value="${project.vendordir}/data" override="true" />
-  <property name="project.vendor.phpdir"   value="${project.vendordir}/php" override="true" />
-  <property name="project.vendor.testdir"  value="${project.vendordir}/tests" override="true" />
-  <property name="project.vendor.docdir"   value="${project.vendordir}/docs" override="true" />
-  <property name="project.vendor.wwwdir"   value="${project.vendordir}/www" override="true" />
-
-  <property name="project.distdir"         value="${project.basedir}/dist" />
-  <property name="project.distdir.lastBuilt" value="${project.basedir}/dist/lastBuilt" />
-  <property name="project.tarfilename"     value="${project.name}-${project.version}.tgz" />
-  <property name="project.tarfile"         value="${project.distdir}/${project.tarfilename}" />
-
-  <!-- what was the last PEAR package we created, if any? -->
-  <if>
-    <available file="${project.distdir.lastBuilt}"/>
-    <then>
-      <property file="${project.distdir.lastBuilt}"/>
-    </then>
-    <else>
-      <property name="project.lastBuiltTarfile" value="false"/>
-    </else>
-  </if>
-
-  <!-- override this if you want to run additional PEAR commands -->
-  <property name="pear.cmd" value="" override="true" />
-
-  <!-- lists of the files that make up our package -->
-  <fileset dir="${project.src.bindir}" id="binfiles">
-    <include name="**/**"/>
-    <exclude name="**/.DS_Store"/>
-    <exclude name="**/.empty"/>
-    <exclude name="**/.svn"/>
-  </fileset>
-  <fileset dir="${project.src.datadir}" id="datafiles">
-    <include name="**/**"/>
-    <exclude name="**/.DS_Store"/>
-    <exclude name="**/.empty"/>
-    <exclude name="**/.svn"/>
-  </fileset>
-  <fileset dir="${project.src.phpdir}" id="phpfiles">
-    <include name="**/**"/>
-    <exclude name="**/.DS_Store"/>
-    <exclude name="**/.empty"/>
-    <exclude name="**/.svn"/>
-  </fileset>
-  <fileset dir="${project.src.testunitdir}/php" id="testfiles">
-    <include name="**/**"/>
-    <exclude name="**/.DS_Store"/>
-    <exclude name="**/.empty"/>
-    <exclude name="**/.svn"/>
-  </fileset>
-  <fileset dir="${project.src.wwwdir}" id="wwwfiles">
-    <include name="**/**" />
-    <exclude name="**/.DS_Store"/>
-    <exclude name="**/.empty"/>
-    <exclude name="**/.svn"/>
-  </fileset>
-  <fileset dir="${project.src.docdir}" id="docfiles">
-    <include name="**/**" />
-    <exclude name="**/.DS_Store"/>
-    <exclude name="**/.empty"/>
-    <exclude name="**/.svn"/>
-  </fileset>
-  <fileset dir="${project.basedir}" id="topleveldocfiles">
-    <include name="*.txt" />
-    <include name="*.md" />
-  </fileset>
-
-  <taskdef name="phingcallifexists" classname="Phix_Project.ComponentManager.Phing.PhingCallIfExistsTask" />
-  <import file="build.local.xml"/>
-
-  <!-- Tell the user what this build file supports -->
-  <target name="help">
-    <echo message="${project.name} ${project.version}: build.xml targets:" />
-    <echo message="" />
-    <echo message="Setup your dev environment:" />
-    <echo message="" />
-    <echo message="  build-vendor" />
-    <echo message="    Populate vendor/ with this package's dependencies" />
-    <echo message="  vendor-pear" />
-    <echo message="    Run additional PEAR commands inside the vendor folder" />
-    <echo message="" />
-    <echo message="Develop your code:" />
-    <echo message="" />
-    <echo message="  lint" />
-    <echo message="    Check the PHP files for syntax errors" />
-    <echo message="  test" />
-    <echo message="    Run the component's PHPUnit tests" />
-    <echo message="  code-review" />
-    <echo message="    Run all of the code quality targets:" />
-    <echo message="" />
-    <echo message="    code-browser" />
-    <echo message="      Run code quality tests for PHP_CodeBrowser" />
-    <echo message="    phpcpd" />
-    <echo message="      Check for cut and paste problems" />
-    <echo message="    phploc" />
-    <echo message="      Calculate the size of your PHP project" />
-    <echo message="    phpdoc" />
-    <echo message="      Create the PHP docs from source code" />
-    <echo message="" />
-    <echo message="Publish your component:" />
-    <echo message="" />
-    <echo message="  pear-package" />
-    <echo message="    Create a PEAR-compatible package" />
-    <echo message="  publish-local" />
-    <echo message="    Publish your PEAR-compatible package into a local copy" />
-    <echo message="    of your PEAR channel" />
-    <echo message="  install-vendor" />
-    <echo message="    Install this component from source into vendor/" />
-    <echo message="  install-system" />
-    <echo message="    Install this component from source for all local users" />
-    <echo message="    You must be root to run this target on Linux!!" />
-    <echo message=""/>
-    <echo message="Maintain your component:"/>
-    <echo message=""/>
-    <echo message="  upgrade-skeleton"/>
-    <echo message="    Upgrade the skeleton files for this component"/>
-    <echo message=""/>
-    <echo message="Additional targets:" />
-    <echo message=""/>
-    <echo message="  clean" />
-    <echo message="    Remove all temporary folders created by this build file" />
-    <echo message="  version" />
-    <echo message="    Show this component's version from build.properties" />
-    <echo message="" />
-    <phingcallifexists target="local.help" />
-  </target>
-
-  <!-- Show the current version, as set in build.properties -->
-  <!-- This is just to be a time-saver -->
-  <target name="version">
-    <echo message="${project.version}" />
-  </target>
-
-  <!-- Run PHP lint on all of the source code -->
-  <target name="lint">
-    <phplint>
-      <fileset dir="${project.src.phpdir}">
-        <include name="**/*.php" />
-      </fileset>
-    </phplint>
-    <phingcallifexists target="local.lint" />
-  </target>
-
-  <!-- Run the unit tests for this module -->
-  <target name="run-unittests" depends="lint">
-    <!-- Make sure vendor/ folder exists -->
-    <if>
-      <not>
-        <available file="${project.vendordir}" type="dir"/>
-      </not>
-      <then>
-        <phingcall target="build-vendor"/>
-      </then>
-    </if>
-
-    <!-- do we have any tests? -->
-    <!-- currently cannot think of a reliable way to test this in phing -->
-
-    <!-- run the tests -->
-    <delete dir="${project.review.codecoveragedir}" />
-    <mkdir dir="${project.review.codecoveragedir}" />
-    <mkdir dir="${project.review.logsdir}" />
-    <exec command="phpunit" checkreturn="true" logoutput="true"/>
-    <echo/>
-    <echo>The code coverage report is in file://${project.review.codecoveragedir}</echo>
-    <echo/>
-  </target>
-
-  <!-- Run all the tests for this module -->
-  <target name="test" depends="run-unittests">
-    <phingcallifexists target="local.test"/>
-  </target>
-
-  <!-- Run the code review quality tests -->
-  <target name="code-review" depends="run-unittests, code-browser, phpcpd, pdepend, phploc">
-    <phingcallifexists target="local.code-review"/>
-  </target>
-
-  <!-- Run all of the targets for setting up the code browser -->
-  <target name="code-browser" depends="phpmd, phpcs, phpcb">
-    <phingcallifexists target="local.code-browser"/>
-  </target>
-
-  <target name="pdepend">
-    <mkdir dir="${project.review.logsdir}" />
-    <exec command="pdepend --phpunit-xml=${project.review.logsdir}/pdepend.xml --jdepend-xml=${project.review.logsdir}/jdepend.xml --jdepend-chart=${project.review.logsdir}/dependencies.svg --overview-pyramid=${project.review.logsdir}/overview-pyramid.svg ${project.src.phpdir}" logoutput="true"/>
-  </target>
-
-  <!-- Generate package docs -->
-  <target name="phpdoc">
-    <mkdir dir="${project.review.logsdir}" />
-    <exec command="phpdoc -d ${project.src.phpdir} -t ${project.review.docsdir}" logoutput="true"/>
-    <echo message="You will find the PHPDoc for your project at: ${project.review.docsdir}/index.html"/>
-    <phingcallifexists target="local.phpdoc"/>
-  </target>
-
-  <!-- Check code for code smells -->
-  <target name="phpmd">
-    <mkdir dir="${project.review.logsdir}" />
-    <exec command="phpmd ${project.src.phpdir} xml codesize,design,naming,unusedcode --reportfile ${project.review.logsdir}/phpmd.xml" logoutput="true" />
-  </target>
-
-  <target name="phpcpd">
-    <mkdir dir="${project.review.logsdir}"/>
-    <exec command="phpcpd --log-pmd ${project.review.logsdir}/pmd-cpd.xml ${project.src.phpdir}" logoutput="true" />
-  </target>
-
-  <!-- Check the code for style violations -->
-  <target name="phpcs">
-    <mkdir dir="${project.review.logsdir}" />
-    <exec command="phpcs --report=xml --report-file=${project.review.logsdir}/checkstyle.xml --standard=${checkstyle.standard} --extensions=php ${project.src.phpdir}" logoutput="true"/>
-  </target>
-
-  <!-- Build the code-browser files -->
-  <target name="phpcb" depends="phpmd">
-    <delete dir="${project.review.codebrowserdir}" />
-    <mkdir dir="${project.review.codebrowserdir}" />
-    <exec command="phpcb --log ${project.review.logsdir} --source ${project.src.phpdir} --output ${project.review.codebrowserdir}" logoutput="true" />
-  </target>
-
-  <!-- Work out the size of the project -->
-  <target name="phploc">
-    <mkdir dir="${project.review.logsdir}" />
-    <exec command="phploc --log-xml ${project.review.logsdir}/phploc.xml --log-csv ${project.review.logsdir}/phploc.csv ${project.src.phpdir}" logoutput="true" />
-  </target>
-
-  <!-- Populate vendor with the dependencies for this component -->
-  <target name="build-vendor" depends="pear-package,setup-vendor">
-    <echo>Populating vendor/ with dependencies</echo>
-    <exec command="phix pear:register-channels" checkreturn="true" logoutput="true" />
-    <exec command="pear -c ${project.tmpdir}/pear-config install --alldeps ${project.tarfile}" logoutput="true" checkreturn="true"/>
-    <echo/>
-    <echo>Your vendor/ folder has been built.</echo>
-    <echo>You only need to run 'phing build-vendor' again if you change the</echo>
-    <echo>dependencies listed in your package.xml file.</echo>
-    <echo/>
-    <phingcallifexists target="local.buildvendor"/>
-  </target>
-
-  <!-- Setup the vendor folder -->
-  <target name="setup-vendor">
-    <echo>Creating vendor/ as a sandboxed PEAR install folder</echo>
-    <delete dir="${project.vendordir}" />
-    <mkdir dir="${project.vendordir}" />
-    <delete dir="${project.tmpdir}" />
-    <mkdir dir="${project.tmpdir}" />
-    <exec command="pear config-create ${project.tmpdir} ${project.tmpdir}/pear-config" checkreturn="true" logoutput="true" />
-    <exec command="pear -c ${project.tmpdir}/pear-config config-set preferred_state alpha" checkreturn="true" logoutput="true" />
-    <exec command="pear -c ${project.tmpdir}/pear-config config-set php_dir ${project.vendor.phpdir}" checkreturn="true" logoutput="true" />
-    <exec command="pear -c ${project.tmpdir}/pear-config config-set bin_dir ${project.vendor.bindir}" checkreturn="true" logoutput="true" />
-    <exec command="pear -c ${project.tmpdir}/pear-config config-set data_dir ${project.vendor.datadir}" checkreturn="true" logoutput="true" />
-    <exec command="pear -c ${project.tmpdir}/pear-config config-set doc_dir ${project.vendor.docdir}" checkreturn="true" logoutput="true" />
-    <exec command="pear -c ${project.tmpdir}/pear-config config-set test_dir ${project.vendor.testdir}" checkreturn="true" logoutput="true" />
-    <exec command="pear -c ${project.tmpdir}/pear-config config-set www_dir ${project.vendor.wwwdir}" checkreturn="true" logoutput="true" />
-  </target>
-
-  <!-- Create the PEAR package, ready for release -->
-  <target name="pear-package">
-    <echo>Building release directory</echo>
-    <delete dir="${project.builddir}" />
-    <mkdir dir="${project.pkgdir}" />
-    <if>
-      <available file="${project.src.bindir}"/>
-      <then>
-        <copy todir="${project.pkgdir}">
-          <fileset refid="binfiles"/>
-        </copy>
-      </then>
-    </if>
-    <if>
-      <available file="${project.src.datadir}"/>
-      <then>
-        <copy todir="${project.pkgdir}">
-          <fileset refid="datafiles"/>
-        </copy>
-      </then>
-    </if>
-    <if>
-      <available file="${project.src.docdir}"/>
-      <then>
-        <copy todir="${project.pkgdir}">
-          <fileset refid="docfiles"/>
-        </copy>
-      </then>
-    </if>
-    <if>
-      <available file="${project.src.phpdir}"/>
-      <then>
-        <copy todir="${project.pkgdir}">
-          <fileset refid="phpfiles"/>
-        </copy>
-      </then>
-    </if>
-    <if>
-      <available file="${project.src.testunitdir}"/>
-      <then>
-        <copy todir="${project.pkgdir}">
-          <fileset refid="testfiles"/>
-        </copy>
-      </then>
-    </if>
-    <if>
-      <available file="${project.src.wwwdir}"/>
-      <then>
-        <copy todir="${project.pkgdir}">
-          <fileset refid="wwwfiles"/>
-        </copy>
-      </then>
-    </if>
-    <copy todir="${project.pkgdir}">
-      <fileset refid="topleveldocfiles"/>
-    </copy>
-    <copy todir="${project.builddir}">
-      <fileset dir=".">
-        <include name="package.xml" />
-      </fileset>
-    </copy>
-
-    <exec command="phix pear:expand-package-xml" checkreturn="yes" logoutput="yes"/>
-
-    <echo>Creating ${project.tarfile} PEAR package</echo>
-
-    <mkdir dir="${project.distdir}" />
-    <delete file="${project.tarfile}" />
-    <tar destfile="${project.tarfile}" compression="gzip">
-      <fileset dir="${project.builddir}">
-        <include name="**/**" />
-      </fileset>
-    </tar>
-
-    <!-- remember the tarball we have just build -->
-    <property name="project.lastBuiltTarfile" value="${project.tarfile}" override="true"/>
-    <echo file="${project.distdir.lastBuilt}" append="false">project.lastBuiltTarfile=${project.tarfile}</echo>
-
-    <!-- write a message to say which file we built last -->
-    <echo>Your PEAR package is in ${project.tarfile}</echo>
-    <phingcallifexists target="local.pear-package"/>
-  </target>
-
-  <!-- Install the code -->
-  <target name="install-vendor">
-    <if>
-      <not>
-        <contains string="${project.lastBuiltTarfile}" substring="${project.name}"/>
-      </not>
-      <then>
-        <fail message="Please run 'phing pear-package' first, then try again."/>
-      </then>
-    </if>
-
-    <if>
-      <not>
-        <available file="${project.vendordir"/>
-      </not>
-      <then>
-        <phingcall target="build-vendor" />
-      </then>
-    </if>
-
-    <if>
-      <available file="${project.lastBuiltTarfile}"/>
-      <then>
-        <exec command="pear -c ${project.tmpdir}/pear-config install --alldeps -f ${project.lastBuiltTarfile}" logoutput="true" checkreturn="true"/>
-        <phingcallifexists target="local.install-vendor"/>
-      </then>
-      <else>
-        <echo>Cannot find PEAR package file ${project.lastBuiltTarfile}</echo>
-        <fail message="Run 'phing pear-package' to create a new PEAR package, then try again."/>
-      </else>
-    </if>
-  </target>
-
-  <!-- install a package system-wide -->
-  <target name="install-system">
-    <if>
-      <not>
-        <contains string="${project.lastBuiltTarfile}" substring="${project.name}"/>
-      </not>
-      <then>
-        <echo>Please run 'phing pear-package' first, then try again.</echo>
-      </then>
-      <elseif>
-        <available file="${project.lastBuiltTarfile}"/>
-        <then>
-          <exec command="pear install -f -a ${project.lastBuiltTarfile}" checkreturn="true" logoutput="true" />
-          <phingcallifexists target="local.install-system"/>
-        </then>
-      </elseif>
-      <else>
-        <echo>Cannot find PEAR package file ${project.lastBuiltTarfile}</echo>
-        <echo>Run 'phing pear-package' to create a new PEAR package, then try again</echo>
-      </else>
-    </if>
-  </target>
-
-  <!-- Publish to local copy of PEAR channel -->
-  <target name="publish-local" depends="pear-package">
-    <if>
-      <not>
-        <contains string="${project.lastBuiltTarfile}" substring="${project.name}"/>
-      </not>
-      <then>
-        <echo>Please run 'phing pear-package' first, then try again.</echo>
-      </then>
-      <elseif>
-        <available file="${project.lastBuiltTarfile}"/>
-        <then>
-          <!-- get rid of any existing snapshots we may have published -->
-          <foreach param="packagefile" absparam="abspackagefile" target="pirum-remove-package">
-            <fileset dir="${pear.local}/get">
-              <include name="${project.name}*snapshot*.tgz" />
-            </fileset>
-          </foreach>
-
-          <!-- publish the new PEAR package -->
-          <exec command="pirum add ${pear.local} ${project.lastBuiltTarfile}" checkreturn="true" logoutput="true" />
-          <phingcallifexists target="local.publish-local"/>
-        </then>
-      </elseif>
-      <else>
-        <echo>Cannot find PEAR package file ${project.lastBuiltTarfile}</echo>
-        <echo>Run 'phing pear-package' to create a new PEAR package, then try again</echo>
-      </else>
-    </if>
-  </target>
-
-  <target name="pirum-remove-package">
-    <exec command="pirum remove ${pear.local} ${packagefile}" logoutput="true" checkreturn="true" />
-  </target>
-
-  <!-- Run additional PEAR commands in the vendor folder -->
-  <target name="vendor-pear">
-    <exec command="pear -c ${project.tmpdir}/pear-config ${pear.cmd}" logoutput="true" checkreturn="true" />
-  </target>
-
-  <!-- Upgrade the skeleton files here and now -->
-  <target name="upgrade-skeleton">
-    <exec command="phix ${component.type}:upgrade ." logoutput="true" checkreturn="true" />
-    <phingcallifexists target="local.upgrade-skeleton"/>
-  </target>
-
-  <!-- Clean up the mess -->
-  <target name="clean">
-    <delete dir="${project.builddir}" />
-    <delete dir="${project.distdir}" />
-    <delete dir="${project.reviewdir}" />
-    <delete dir="${project.pkgdir}" />
-    <delete dir="${project.distdir}" />
-    <delete dir="${project.tmpdir}" />
-    <phingcallifexists target="local.clean"/>
-  </target>
-</project>
-<!-- vim: set tabstop=2 shiftwidth=2 expandtab: -->
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..1e4072b
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,16 @@
+{
+    "name": "hab/pica-record",
+    "description": "Object oriented interface to Pica+ records, fields, and subfields",
+    "type": "library",
+    "license": "GPL-3.0+",
+    "authors": [
+	{
+	    "name": "David Maus",
+	    "email": "maus@hab.de",
+	    "role": "Developer"
+	}
+    ],
+    "support": {
+	"email": "maus@hab.de"
+    }
+}
diff --git a/package.xml b/package.xml
deleted file mode 100644
index 8e8d98e..0000000
--- a/package.xml
+++ /dev/null
@@ -1,154 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<package packagerversion="1.9.1" version="2.0"
-	 xmlns="http://pear.php.net/dtd/package-2.0"
-	 xmlns:tasks="http://pear.php.net/dtd/tasks-1.0"
-	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	 xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
-			     http://pear.php.net/dtd/tasks-1.0.xsd
-			     http://pear.php.net/dtd/package-2.0
-			     http://pear.php.net/dtd/package-2.0.xsd">
-  <name>${project.name}</name>
-  <channel>${project.channel}</channel>
-  <summary>Object oriented interface to Pica+ records</summary>
-  <description>
-    PicaRecord provides an object oriented interface to Pica+ records, fields, and subfields.
-  </description>
-  <lead>
-    <name>David Maus</name>
-    <user>dmaus</user>
-    <email>maus@hab.de</email>
-    <active>yes</active>
-  </lead>
-  <date>${build.date}</date>
-  <time>${build.time}</time>
-  <version>
-    <release>${project.version}</release>
-    <api>${project.majorVersion}.${project.minorVersion}</api>
-  </version>
-  <stability>
-    <release>${project.stability}</release>
-    <api>stable</api>
-  </stability>
-  <license>GNU General Public License v3</license>
-  <notes>
-    The PicaRecord package does not provide the means to read or write Pica+ records. In order to do so you need to install the packages PicaReader and PicaWriter that are available via this PEAR channel, too.
-  </notes>
-  <contents>
-    <dir baseinstalldir="/" name="/">
-      ${contents}
-    </dir>
-  </contents>
-  <dependencies>
-    <required>
-      <php>
-	<min>5.3.0</min>
-      </php>
-      <pearinstaller>
-	<min>1.9.4</min>
-      </pearinstaller>
-      <package>
-	<name>Autoloader</name>
-	<channel>pear.phix-project.org</channel>
-	<min>3.0.0</min>
-	<max>3.999.9999</max>
-      </package>
-    </required>
-  </dependencies>
-  <phprelease />
-  <changelog>
-    <release>
-      <version>
-	<release>0.4.0</release>
-	<api>0.4</api>
-      </version>
-      <stability>
-	<release>stable</release>
-	<api>stable</api>
-      </stability>
-      <date>2013-01-10</date>
-      <license>GNU General Public License v3</license>
-      <notes>
-	* allow empty subfield values (031N $6)
-      </notes>
-    </release>
-    <release>
-      <version>
-	<release>0.3.2</release>
-	<api>0.3</api>
-      </version>
-      <stability>
-	<release>stable</release>
-	<api>stable</api>
-      </stability>
-      <date>2013-01-10</date>
-      <license>GNU General Public License v3</license>
-      <notes>
-	* remove hard dependency to Autoloader
-      </notes>
-    </release>
-    <release>
-      <version>
-	<release>0.3.1</release>
-	<api>0.3</api>
-      </version>
-      <stability>
-	<release>stable</release>
-	<api>stable</api>
-      </stability>
-      <date>2012-04-17</date>
-      <license>GNU General Public License v3</license>
-      <notes>
-	* fix getFields() with selector in NestedRecord
-      </notes>
-    </release>
-    <release>
-      <version>
-	<release>0.3.0</release>
-	<api>0.3</api>
-      </version>
-      <stability>
-	<release>stable</release>
-	<api>stable</api>
-      </stability>
-      <date>2012-03-05</date>
-      <license>GNU General Public License v3</license>
-      <notes>
-	* PicaRecord is now E_STRICT compliant
-	* add Record::getFirstMatchingField(), return first field matching a selector
-	* add Field::getNthSubfield(), return the nth subfield with a specified code
-      </notes>
-    </release>
-    <release>
-      <version>
-	<release>0.2.0</release>
-	<api>0.2</api>
-      </version>
-      <stability>
-	<release>stable</release>
-	<api>stable</api>
-      </stability>
-      <date>2012-02-17</date>
-      <license>GNU General Public License v3</license>
-      <notes>
-	* add Record::getMaximumOccurrenceOf(), return maximum occurrence value of field identified by tag
-	* fix the inline documentation
-      </notes>
-    </release>
-    <release>
-      <version>
-	<release>0.1.0</release>
-	<api>0.1</api>
-      </version>
-      <stability>
-	<release>stable</release>
-	<api>stable</api>
-      </stability>
-      <date>2012-02-15</date>
-      <license>GNU General Public License v3</license>
-      <notes>
-	Initial release.
-      </notes>
-    </release>
-  </changelog>
-</package>
-<!-- vim: set tabstop=2 shiftwidth=2 expandtab: -->
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..16d058a
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<phpunit bootstrap="tests/bootstrap.php" strict="true">
+  <testsuites>
+    <testsuite name="Unit Tests">
+      <directory suffix="Test.php">tests</directory>
+    </testsuite>
+  </testsuites>
+  <filter>
+    <blacklist>
+      <directory suffix=".php">vendor</directory>
+      <directory suffix=".php">tests</directory>
+    </blacklist>
+    <whitelist addUncoveredFilesFromWhitelist="true">
+      <directory suffix=".php">bin</directory>
+      <directory suffix=".php">src</directory>
+    </whitelist>
+  </filter>
+  <logging>
+    <log type="coverage-html" target="review/code-coverage"/>
+  </logging>
+</phpunit>
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
deleted file mode 100644
index 864f7f7..0000000
--- a/phpunit.xml.dist
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0"?>
-<phpunit bootstrap="src/tests/unit-tests/bootstrap.php">
-    <testsuites>
-        <testsuite name="Unit Tests">
-            <directory suffix="Test.php">src/tests/unit-tests</directory>
-        </testsuite>
-    </testsuites>
-    <filter>
-        <blacklist>
-            <directory suffix=".php">vendor</directory>
-            <directory suffix=".php">src/tests</directory>
-        </blacklist>
-        <whitelist addUncoveredFilesFromWhitelist="true">
-            <directory suffix=".php">src/bin</directory>
-            <directory suffix=".php">src/php</directory>
-        </whitelist>
-    </filter>
-    <logging>
-        <log type="coverage-html" target="review/code-coverage"/>
-        <log type="coverage-clover" target="review/logs/phpunit.xml"/>
-        <log type="json" target="review/logs/phpunit.json"/>
-        <log type="tap" target="review/logs/phpunit.tap"/>
-        <log type="junit" target="review/logs/phpunit-junit.xml"/>
-        <log type="testdox-html" target="review/testdox.html"/>
-        <log type="testdox-text" target="review/testdox.txt"/>
-    </logging>
-</phpunit>
-<!-- vim: set tabstop=4 shiftwidth=4 expandtab: -->
diff --git a/src/.empty b/src/.empty
deleted file mode 100644
index e69de29..0000000
diff --git a/src/HAB/Pica/Record/AuthorityRecord.php b/src/HAB/Pica/Record/AuthorityRecord.php
new file mode 100644
index 0000000..a62980c
--- /dev/null
+++ b/src/HAB/Pica/Record/AuthorityRecord.php
@@ -0,0 +1,147 @@
+<?php
+
+/**
+ * Pica+ AuthorityRecord.
+ *
+ * This file is part of PicaRecord.
+ *
+ * PicaRecord is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PicaRecord is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   PicaRecord
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2012, 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
+ */
+
+namespace HAB\Pica\Record;
+
+use InvalidArgumentException;
+
+class AuthorityRecord extends Record
+{
+
+    /**
+     * Append a field to the record.
+     *
+     * @see Record::append()
+     *
+     * @throws InvalidArgumentException Field level other than 0
+     * @throws InvalidArgumentException Field already in record
+     *
+     * @param  Field $field Field to append
+     * @return void
+     */
+    public function append (Field $field)
+    {
+        if ($field->getLevel() !== 0) {
+            throw new InvalidArgumentException("Invalid field level {$field->getLevel()}");
+        }
+        return parent::append($field);
+    }
+
+    /**
+     * Return the Pica production number (record identifier).
+     *
+     * @return string|null Pica production number or NULL if none exists
+     */
+    public function getPPN ()
+    {
+        $ppnField = $this->getFirstMatchingField('003@/00');
+        if ($ppnField) {
+            $ppnSubfield = $ppnField->getNthSubfield('0', 0);
+            if ($ppnSubfield) {
+                return $ppnSubfield->getValue();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Set the Pica production number.
+     *
+     * Create a field 003@/00 if necessary.
+     *
+     * @param  string $ppn Pica production number
+     * @return void
+     */
+    public function setPPN ($ppn)
+    {
+        $ppnField = $this->getFirstMatchingField('003@/00');
+        if ($ppnField) {
+            $ppnSubfield = $ppnField->getNthSubfield('0', 0);
+            if ($ppnSubfield) {
+                $ppnSubfield->setValue($ppn);
+            } else {
+                $ppnField->append(new Subfield('0', $ppn));
+            }
+        } else {
+            $this->append(new Field('003@', 0, array(new Subfield('0', $ppn))));
+        }
+    }
+
+    /**
+     * Return TRUE if the record is valid.
+     *
+     * A valid authority record MUST have exactly one valid PPN field
+     * (003@/00$0) and exactly one type field (002@/0$0) with a type indicator
+     * `T'.
+     *
+     * @see AuthorityRecord::checkPPN();
+     * @see AuthorityRecord::checkType();
+     *
+     * @return boolean TRUE if the record is valid
+     */
+    public function isValid ()
+    {
+        return parent::isValid() && $this->checkPPN() && $this->checkType();
+    }
+
+    /**
+     * Return true if the record has exactly one PPN field (003@/00) with a
+     * subfield $0.
+     *
+     * @return boolean True if the record has a valid PPN field
+     */
+    protected function checkPPN ()
+    {
+        $ppnField = $this->getFields('003@/00');
+        if (count($ppnField) === 1) {
+            $ppnSubfield = reset($ppnField)->getNthSubfield('0', 0);
+            if ($ppnSubfield) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Return true if the record has exactly one type field (002@/00) with a
+     * subfield $0 whose first character is `T'.
+     *
+     * @return boolean True if the record has a valid type field
+     */
+    protected function checkType ()
+    {
+        $typeField = $this->getFields('002@/00');
+        if (count($typeField) === 1) {
+            $typeSubfield = reset($typeField)->getNthSubfield('0', 0);
+            if ($typeSubfield) {
+                $typeCode = $typeSubfield->getValue();
+                if ($typeCode === 'T') {
+                    return true;
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/HAB/Pica/Record/CopyRecord.php b/src/HAB/Pica/Record/CopyRecord.php
new file mode 100644
index 0000000..d90bbd1
--- /dev/null
+++ b/src/HAB/Pica/Record/CopyRecord.php
@@ -0,0 +1,170 @@
+<?php
+
+/**
+ * Pica+ CopyRecord.
+ *
+ * This file is part of PicaRecord.
+ *
+ * PicaRecord is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PicaRecord is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   PicaRecord
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2012, 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
+ */
+
+namespace HAB\Pica\Record;
+
+use InvalidArgumentException;
+
+class CopyRecord extends Record
+{
+
+    /**
+     * The item number.
+     *
+     * @var integer
+     */
+    protected $_itemNumber;
+
+    /**
+     * Append a field to the copy record.
+     *
+     * You can only append field of level 2 to a copy record.
+     *
+     * @see Record::append()
+     *
+     * @throws InvalidArgumentException Field level other than 2
+     * @throws InvalidArgumentException Item number mismatch
+     * @throws InvalidArgumentException Field already in record
+     *
+     * @param  Field $field Field to append
+     * @return void
+     */
+    public function append (Field $field)
+    {
+        if ($field->getLevel() !== 2) {
+            throw new InvalidArgumentException("Invalid field level: {$field->getLevel()}");
+        }
+        if ($this->getItemNumber() === null) {
+            $this->setItemNumber($field->getOccurrence());
+        }
+        If ($field->getOccurrence() != $this->getItemNumber()) {
+            throw new InvalidArgumentException("Item number mismatch: {$this->getItemNumber()}, {$field->getOccurrence()}");
+        }
+        return parent::append($field);
+    }
+
+    /**
+     * Return the item number.
+     *
+     * @return integer|null Item number
+     */
+    public function getItemNumber ()
+    {
+        return $this->_itemNumber;
+    }
+
+    /**
+     * Set the item number.
+     *
+     * @param  integer $itemNumber Item number
+     * @return void
+     */
+    protected function setItemNumber ($itemNumber)
+    {
+        $this->_itemNumber = (int)$itemNumber;
+    }
+
+    /**
+     * Return the exemplar production number (EPN).
+     *
+     * @return string Exemplar production number
+     */
+    public function getEPN ()
+    {
+        $epnField = $this->getFirstMatchingField('203@');
+        if ($epnField) {
+            $epnSubfield = $epnField->getNthSubfield('0', 0);
+            if ($epnSubfield) {
+                return $epnSubfield->getValue();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Set the exemplar production number (EPN).
+     *
+     * Create a field 203@ if necessary.
+     *
+     * @param  string $epn Exemplar production number
+     * @return void
+     */
+    public function setEPN ($epn)
+    {
+        $epnField = $this->getFirstMatchingField('203@');
+        if ($epnField) {
+            $epnSubfield = $epnField->getNthSubfield('0', 0);
+            if ($epnSubfield) {
+                $epnSubfield->setValue($epn);
+            } else {
+                $epnField->append(new Subfield('0', $epn));
+            }
+        } else {
+            $this->append(new Field('203@', $this->getItemNumber(), array(new Subfield('0', $epn))));
+        }
+    }
+
+    /**
+     * Set the containing local record.
+     *
+     * @param  LocalRecord $record Local record
+     * @return void
+     */
+    public function setLocalRecord (LocalRecord $record)
+    {
+        $this->unsetLocalRecord();
+        if (!$record->containsCopyRecord($this)) {
+            $record->addCopyRecord($this);
+        }
+        $this->_parent = $record;
+    }
+
+    /**
+     * Unset the containing local record.
+     *
+     * @return void
+     */
+    public function unsetLocalRecord ()
+    {
+        if ($this->_parent) {
+            if ($this->_parent->containsCopyRecord($this)) {
+                $this->_parent->removeCopyRecord($this);
+            }
+            $this->_parent = null;
+        }
+    }
+
+    /**
+     * Return the containing local record.
+     *
+     * @return LocalRecord|null
+     */
+    public function getLocalRecord ()
+    {
+        return $this->_parent;
+    }
+
+}
\ No newline at end of file
diff --git a/src/HAB/Pica/Record/Field.php b/src/HAB/Pica/Record/Field.php
new file mode 100644
index 0000000..4e3f4b9
--- /dev/null
+++ b/src/HAB/Pica/Record/Field.php
@@ -0,0 +1,337 @@
+<?php
+
+/**
+ * Pica+ Field.
+ *
+ * This file is part of PicaRecord.
+ *
+ * PicaRecord is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PicaRecord is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   PicaRecord
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2012, 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
+ */
+
+namespace HAB\Pica\Record;
+
+use InvalidArgumentException;
+
+class Field 
+{
+
+    /**
+     * Regular expression matching a valid Pica+ field tag.
+     *
+     * @var string
+     */
+    const TAG_RE = '|^[012][0-9]{2}[A-Z@]$|D';
+
+    /**
+     * Return TRUE if argument is a valid field tag.
+     *
+     * @param  mixed $arg Variable to check
+     * @return boolean TRUE if argument is a valid field tag
+     */
+    public static function isValidFieldTag ($arg) 
+    {
+        return (bool)preg_match(self::TAG_RE, $arg);
+    }
+
+    /**
+     * Return TRUE if argument is a valid field occurrence.
+     *
+     * Argument is casted to int iff it is either null or a numeric string.
+     *
+     * @param  mixed $arg Variable to check
+     * @return boolean TRUE if argument is a valid field occurrence
+     */
+    public static function isValidFieldOccurrence ($arg) 
+    {
+        if ($arg === null || ctype_digit($arg)) {
+            $arg = (int)$arg;
+        }
+        return is_int($arg) && $arg >= 0 && $arg < 100;
+    }
+
+    /**
+     * Return predicate that matches a field shorthand against a regular
+     * expression.
+     *
+     * @param  string $reBody Body of regular expression
+     * @return callback Predicate
+     */
+    public static function match ($reBody) 
+    {
+        if (strpos($reBody, '#') !== false) {
+            $reBody = str_replace('#', '\#', $reBody);
+        }
+        $regexp = "#{$reBody}#D";
+        return function (Field $field) use ($regexp) {
+            return (bool)preg_match($regexp, $field->getShorthand());
+        };
+    }
+
+    /**
+     * Return a new field based on its array representation.
+     *
+     * @throws InvalidArgumentException Missing `tag', `occurrene', or `subfields' index
+     * @param  array $field Array representation of a field
+     * @return \HAB\Pica\Record\Field A shiny new field
+     */
+    public static function factory (array $field) 
+    {
+        foreach (array('tag', 'occurrence', 'subfields') as $index) {
+            if (!array_key_exists($index, $field)) {
+                throw new InvalidArgumentException("Missing '{$index}' index in field array");
+            }
+        }
+        return new Field($field['tag'],
+                         $field['occurrence'],
+                         array_map(array('HAB\Pica\Record\Subfield', 'factory'), $field['subfields']));
+    }
+
+    ///
+
+    /**
+     * The field tag.
+     *
+     * @var string
+     */
+    protected $_tag;
+
+    /**
+     * The field level.
+     *
+     * @var integer
+     */
+    protected $_level;
+
+    /**
+     * The field occurrence.
+     *
+     * @var integer
+     */
+    protected $_occurrence;
+
+    /**
+     * The field shorthand.
+     *
+     * @var string
+     */
+    protected $_shorthand;
+
+    /**
+     * Constructor.
+     *
+     * @throws InvalidArgumentException Invalid field tag or occurrence
+     * @param  string $tag Field tag
+     * @param  integer $occurrence Field occurrence
+     * @param  array $subfields Initial set of subfields
+     * @return void
+     */
+    public function __construct ($tag, $occurrence, array $subfields = array()) 
+    {
+        if (!self::isValidFieldTag($tag)) {
+            throw new InvalidArgumentException("Invalid field tag: $tag");
+        }
+        if (!self::isValidFieldOccurrence($occurrence)) {
+            throw new InvalidArgumentException("Invalid field occurrence: $occurrence");
+        }
+        $this->_tag = $tag;
+        $this->_occurrence = (int)$occurrence;
+        $this->_shorthand = sprintf('%4s/%02d', $tag, $occurrence);
+        $this->_level = (int)$tag[0];
+        $this->setSubfields($subfields);
+    }
+
+    /**
+     * Set the field subfields.
+     *
+     * Replaces the subfield list with subfields in argument.
+     *
+     * @param  array $subfields Subfields
+     * @return void
+     */
+    public function setSubfields (array $subfields) 
+    {
+        $this->_subfields = array();
+        foreach ($subfields as $subfield) {
+            $this->addSubfield($subfield);
+        }
+    }
+
+    /**
+     * Add a subfield to the end of the subfield list.
+     *
+     * @throws InvalidArgumentException Subfield already present in subfield list
+     * @param  \HAB\Pica\Record\Subfield $subfield Subfield to add
+     * @return void
+     */
+    public function addSubfield (\HAB\Pica\Record\Subfield $subfield) 
+    {
+        if (in_array($subfield, $this->getSubfields(), true)) {
+            throw new InvalidArgumentException("Cannot add subfield: Subfield already part of the subfield list");
+        }
+        $this->_subfields []= $subfield;
+    }
+
+    /**
+     * Remove a subfield.
+     *
+     * @throws InvalidArgumentException Subfield is not part of the subfield list
+     * @param  \HAB\Pica\Record\Subfield $subfield Subfield to delete
+     * @return void
+     */
+    public function removeSubfield (\HAB\Pica\Record\Subfield $subfield) 
+    {
+        $index = array_search($subfield, $this->_subfields, true);
+        if ($index === false) {
+            throw new InvalidArgumentException("Cannot remove subfield: Subfield not part of the subfield list");
+        }
+        unset($this->_subfields[$index]);
+    }
+
+    /**
+     * Return the field's subfields.
+     *
+     * Returns all subfields when called with no arguments.
+     *
+     * Otherwise the returned array is constructed as follows:
+     *
+     * Each argument is interpreted as a subfield code. The nth element of the
+     * returned array maps to the nth argument in the function call and contains
+     * NULL if the field does not have a subfield with the selected code, or the
+     * subfield if it exists. In order to retrieve multiple subfields with an
+     * identical code you repeat the subfield code in the argument list.
+     *
+     * @return array Subfields
+     */
+    public function getSubfields () 
+    {
+        if (func_num_args() === 0) {
+            return $this->_subfields;
+        } else {
+            $selected = array();
+            $codes = array();
+            $subfields = $this->getSubfields();
+            array_walk($subfields, function ($value, $index) use (&$codes) { $codes[$index] = $value->getCode(); });
+            foreach (func_get_args() as $arg) {
+                $index = array_search($arg, $codes, true);
+                if ($index === false) {
+                    $selected []= null;
+                } else {
+                    $selected []= $subfields[$index];
+                    unset($codes[$index]);
+                }
+            }
+            return $selected;
+        }
+    }
+
+    /**
+     * Return the nth occurrence of a subfield with specified code.
+     *
+     * @param  string $code Subfield code
+     * @param  integer $n Zero-based subfield index
+     * @return \HAB\Pica\record\Subfield|null The requested subfield or NULL if
+     *         none exists
+     */
+    public function getNthSubfield ($code, $n) 
+    {
+        $count = 0;
+        foreach ($this->getSubfields() as $subfield) {
+            if ($subfield->getCode() == $code) {
+                if ($count == $n) {
+                    return $subfield;
+                }
+                $count++;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return the field tag.
+     *
+     * @return string Field tag
+     */
+    public function getTag () 
+    {
+        return $this->_tag;
+    }
+
+    /**
+     * Return the field occurrence.
+     *
+     * @return integer Field occurrence
+     */
+    public function getOccurrence () 
+    {
+        return $this->_occurrence;
+    }
+
+    /**
+     * Return the field level.
+     *
+     * @return integer Field level
+     */
+    public function getLevel () {
+        return $this->_level;
+    }
+
+    /**
+     * Return the field shorthand.
+     *
+     * @return string Field shorthand
+     */
+    public function getShorthand () 
+    {
+        return $this->_shorthand;
+    }
+
+    /**
+     * Return TRUE if the field is empty.
+     *
+     * A field is empty if it contains no subfields.
+     *
+     * @return boolean TRUE if the field is empty
+     */
+    public function isEmpty () 
+    {
+        return empty($this->_subfields);
+    }
+
+    /**
+     * Finalize the clone() operation.
+     *
+     * @return void
+     */
+    public function __clone () 
+    {
+        $this->_subfields = Helper::mapClone($this->_subfields);
+    }
+
+    /**
+     * Return a printable representation of the field.
+     *
+     * The printable representation of a field is its shorthand.
+     *
+     * @return string Printable representation of the field
+     */
+    public function __toString () 
+    {
+        return $this->getShorthand();
+    }
+}
\ No newline at end of file
diff --git a/src/HAB/Pica/Record/Helper.php b/src/HAB/Pica/Record/Helper.php
new file mode 100644
index 0000000..4b9dcc3
--- /dev/null
+++ b/src/HAB/Pica/Record/Helper.php
@@ -0,0 +1,131 @@
+<?php
+
+/**
+ * Helper functions.
+ *
+ * This file is part of PicaRecord.
+ *
+ * PicaRecord is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PicaRecord is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   PicaRecord
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2012, 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
+ */
+
+namespace HAB\Pica\Record;
+
+abstract class Helper
+{
+
+    /**
+     * Return the complement of a function.
+     *
+     * The complement of a function is a function that takes the same arguments
+     * as the original function but returns a boolean with the opposite truth
+     * value.
+     *
+     * @param  callback $function Original function
+     * @return callback Complement function
+     */
+    public static function complement ($function)
+    {
+        return function () use ($function) { return !call_user_func_array($function, func_get_args()); };
+    }
+
+    /**
+     * Return an array of the results of calling a method for each element of a
+     * sequence.
+     *
+     * @param  array $sequence Sequence of objects
+     * @param  string $method Name of the method
+     * @param  array $arguments Optional array of method arguments
+     * @return array Result of calling method on each element of sequence
+     */
+    public static function mapMethod (array $sequence, $method, array $arguments = array())
+    {
+        if (empty($arguments)) {
+            $f = function ($element) use ($method) {
+                return $element->$method();
+            };
+        } else {
+            $f = function ($element) use ($method, $arguments) {
+                return call_user_func_array(array($element, $method), $arguments);
+            };
+        }
+        return array_map($f, $sequence);
+    }
+
+    /**
+     * Return an array with clones of each element in sequence.
+     *
+     * @param  array $sequence Sequence of objects
+     * @return array Sequence of clones
+     */
+    public static function mapClone (array $sequence)
+    {
+        return array_map(function ($element) { return clone($element); }, $sequence);
+    }
+
+    /**
+     * Return TRUE if at leat one element of sequence matches predicate.
+     *
+     * @todo   Make FALSE and TRUE self-evaluating, maybe
+     *
+     * @param  array $sequence Sequence
+     * @param  callback $predicate Predicate
+     * @return boolean TRUE if at least one element matches predicate
+     */
+    public static function some (array $sequence, $predicate)
+    {
+        foreach ($sequence as $element) {
+            if (call_user_func($predicate, $element)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Return TRUE if every element of sequence fullfills predicate.
+     *
+     * @todo   Make FALSE and TRUE self-evaluating, maybe
+     *
+     * @param  array $sequence Sequence
+     * @param  callback $predicate Predicate
+     * @return boolean TRUE if every element fullfills predicate
+     */
+    public static function every (array $sequence, $predicate)
+    {
+        foreach ($sequence as $element) {
+            if (!call_user_func($predicate, $element)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Flatten sequence.
+     *
+     * @param  array $sequence Sequence
+     * @return array Flattend sequence
+     */
+    public static function flatten (array $sequence)
+    {
+        $flat = array();
+        array_walk_recursive($sequence, function ($element) use (&$flat) { $flat []= $element; });
+        return $flat;
+    }
+}
\ No newline at end of file
diff --git a/src/HAB/Pica/Record/LocalRecord.php b/src/HAB/Pica/Record/LocalRecord.php
new file mode 100644
index 0000000..ca94ecd
--- /dev/null
+++ b/src/HAB/Pica/Record/LocalRecord.php
@@ -0,0 +1,196 @@
+<?php
+
+/**
+ * Pica+ LocalRecord.
+ *
+ * This file is part of PicaRecord.
+ *
+ * PicaRecord is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PicaRecord is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   PicaRecord
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2012, 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
+ */
+
+namespace HAB\Pica\Record;
+
+use InvalidArgumentException;
+
+class LocalRecord extends NestedRecord
+{
+
+    /**
+     * Append a field to the local record.
+     *
+     * You can only append field with a level of 0 to a local record.
+     *
+     * @see Record::append()
+     *
+     * @throws InvalidArgumentException Field level invalid
+     * @throws InvalidArgumentException Field already in record
+     *
+     * @param  Field $field Field to add
+     * @return void
+     */
+    public function append (Field $field)
+    {
+        if ($field->getLevel() !== 1) {
+            throw new InvalidArgumentException("Invalid field level: {$field->getLevel()}");
+        }
+        parent::append($field);
+    }
+
+    /**
+     * Add a copy record.
+     *
+     * @throws InvalidArgumentException Record already contains the copy record
+     * @throws InvalidArgumentException Record already contains a copy record with the same item number
+     *
+     * @param  CopyRecord $record Copy record to add
+     * @return void
+     */
+    public function addCopyRecord (CopyRecord $record)
+    {
+        if ($this->getCopyRecordByItemNumber($record->getItemNumber())) {
+            throw new InvalidArgumentException("Cannot add copy record: Copy record with item number {$record->getItemNumber()} already present");
+        }
+        $this->addRecord($record);
+        $record->setLocalRecord($this);
+    }
+
+    /**
+     * Remove a copy record.
+     *
+     * @param  CopyRecord $record Record to remove
+     * @return void
+     */
+    public function removeCopyRecord (CopyRecord $record)
+    {
+        $this->removeRecord($record);
+        $record->unsetLocalRecord();
+    }
+
+    /**
+     * Return copy record by item number.
+     *
+     * @param  integer $itemNumber Item number
+     * @return CopyRecord|null The copy record or null if none exists
+     */
+    public function getCopyRecordByItemNumber ($itemNumber)
+    {
+        foreach ($this->_records as $record) {
+            if ($record->getItemNumber() === $itemNumber) {
+                return $record;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return all copy records.
+     *
+     * @return array Copy records
+     */
+    public function getCopyRecords ()
+    {
+        return $this->_records;
+    }
+
+    /**
+     * Return the ILN (internal library number) of the local record.
+     *
+     * @return integer|null ILN of the local record or NULL if none set
+     */
+    public function getILN ()
+    {
+        $ilnField = $this->getFirstMatchingField('101@/00');
+        if ($ilnField) {
+            $ilnSubfield = $ilnField->getNthSubfield('a', 0);
+            if ($ilnSubfield) {
+                return $ilnSubfield->getValue();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return true if local record contains the copy record.
+     *
+     * @param  CopyRecord $record Copy record
+     * @return boolean
+     */
+    public function containsCopyRecord (CopyRecord $record)
+    {
+        return $this->containsRecord($record);
+    }
+
+    /**
+     * Set the containing title record.
+     *
+     * @param  TitleRecord $record Title record
+     * @return void
+     */
+    public function setTitleRecord (TitleRecord $record)
+    {
+        $this->unsetTitleRecord();
+        if (!$record->containsLocalRecord($this)) {
+            $record->addLocalRecord($this);
+        }
+        $this->_parent = $record;
+    }
+
+    /**
+     * Unset the containing title record.
+     *
+     * @return void
+     */
+    public function unsetTitleRecord ()
+    {
+        if ($this->_parent) {
+            if ($this->_parent->containsLocalRecord($this)) {
+                $this->_parent->removeLocalRecord($this);
+            }
+            $this->_parent = null;
+        }
+    }
+
+    /**
+     * Return the containing local record.
+     *
+     * @return TitleRecord|null
+     */
+    public function getTitleRecord ()
+    {
+        return $this->_parent;
+    }
+
+    /**
+     * Compare two copy records.
+     *
+     * Copyrecords are compared by their item number.
+     *
+     * @see CopyRecord::getItemNumber()
+     * @see NestedRecord::compareRecords()
+     *
+     * @param  Record $a First copy record
+     * @param  Record $b Second copy record
+     * @return integer Comparism value
+     */
+    protected function compareRecords (Record $a, Record $b)
+    {
+        return $a->getItemNumber() - $b->getItemNumber();
+    }
+
+}
\ No newline at end of file
diff --git a/src/HAB/Pica/Record/NestedRecord.php b/src/HAB/Pica/Record/NestedRecord.php
new file mode 100644
index 0000000..d7ea6b2
--- /dev/null
+++ b/src/HAB/Pica/Record/NestedRecord.php
@@ -0,0 +1,192 @@
+<?php
+
+/**
+ * Abstract base class of nested records.
+ *
+ * A nested record is a record that contains zero or more other records. It is
+ * the base class of {@link TitleRecord title} and {@link LocalRecord local}
+ * records and implements internal accessors for the contained records and the
+ * propagation of field getters to the contained records.
+ *
+ * This file is part of PicaRecord.
+ *
+ * PicaRecord is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PicaRecord is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   PicaRecord
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2012, 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
+ */
+
+namespace HAB\Pica\Record;
+
+use InvalidArgumentException;
+
+abstract class NestedRecord extends Record 
+{
+
+    /**
+     * Contained records.
+     *
+     * @var array
+     */
+    protected $_records = array();
+
+    /**
+     * Delete fields matching predicate.
+     *
+     * The delete() is propagated down to all contained records.
+     *
+     * @see Record::delete()
+     *
+     * @param  callback $where Predicate
+     * @return void
+     */
+    public function delete ($where) 
+    {
+        parent::delete($where);
+        Helper::mapMethod($this->_records, 'delete', array($where));
+    }
+
+    /**
+     * Sort fields and contained records.
+     *
+     * The sort() is propagated down to all contained records. In addition the
+     * nested records are sorted themselves using the implementing class'
+     * compareNestedRecords() function.
+     *
+     * @see Record::sort()
+     * @see NestedRecord::compareNestedRecords()
+     *
+     * @return void
+     */
+    public function sort () 
+    {
+        parent::sort();
+        Helper::mapMethod($this->_records, 'sort');
+        usort($this->_records, array($this, 'compareRecords'));
+    }
+
+    /**
+     * Return true if the record is empty.
+     *
+     * A nested record is empty iff it contains no fields and no non-empty
+     * contained record.
+     *
+     * @return boolean
+     */
+    public function isEmpty () 
+    {
+        return parent::isEmpty() && Helper::every($this->_records, function (Record $record) { return $record->isEmpty(); });
+    }
+
+    /**
+     * Return true if the record is valid.
+     *
+     * A nested record is valid iff it and all contained records are valid.
+     *
+     * @see Record::isValid()
+     *
+     * @return boolean
+     */
+    public function isValid () 
+    {
+        return parent::isValid() && !Helper::every($this->_records, function (Record $record) { return $record->isValid(); });
+    }
+
+    /**
+     * Return fields of the record.
+     *
+     * @see Record::getFields()
+     *
+     * @param  string $selector Body of regular expression
+     * @return array Fields
+     */
+    public function getFields ($selector = null) 
+    {
+        if ($selector === null) {
+            return array_merge($this->_fields, Helper::flatten(Helper::mapMethod($this->_records, 'getFields')));
+        } else {
+            return $this->select(Field::match($selector));
+        }
+    }
+
+    /**
+     * Compare two contained records and return a comparism value suitable for
+     * usort().
+     *
+     * @see http://www.php.net/manual/en/function.usort.php
+     *
+     * @param  Record $a First record
+     * @param  Record $b Second record
+     * @return integer Comparism value
+     */
+    abstract protected function compareRecords (Record $a, Record $b);
+
+    /**
+     * Add a record as a contained record.
+     *
+     * @throws InvalidArgumentException Record already contains the record
+     *
+     * @param  Record $record Record to add
+     * @return void
+     */
+    protected function addRecord (Record $record) 
+    {
+        if ($this->containsRecord($record)) {
+            throw new InvalidArgumentException("{$this} already contains {$record}");
+        }
+        $this->_records []= $record;
+    }
+
+    /**
+     * Remove a contained record.
+     *
+     * @throws InvalidArgumentException Record does not contain the record
+     *
+     * @param  Record $record Record to remove
+     * @return void
+     */
+    protected function removeRecord (Record $record) 
+    {
+        $index = array_search($record, $this->_records, true);
+        if ($index === false) {
+            throw new InvalidArgumentException("{$this} does not contain {$record}");
+        }
+        unset($this->_records[$index]);
+    }
+
+    /**
+     * Return true if this record contains the requested record.
+     *
+     * @param  \HAB\Pica\Record\Record Record to check
+     * @return boolean
+     */
+    protected function containsRecord (Record $record) 
+    {
+        return in_array($record, $this->_records, true);
+    }
+
+    /**
+     * Finalize the clone() operation.
+     *
+     * Clone all contained records.
+     *
+     * @return void
+     */
+    public function __clone () 
+    {
+        $this->_records = Helper::mapClone($this->_records);
+    }
+}
\ No newline at end of file
diff --git a/src/HAB/Pica/Record/Record.php b/src/HAB/Pica/Record/Record.php
new file mode 100644
index 0000000..bd44ed8
--- /dev/null
+++ b/src/HAB/Pica/Record/Record.php
@@ -0,0 +1,286 @@
+<?php
+
+/**
+ * Abstract base class of all record structures.
+ *
+ * The abstract base class defines and partially implements the interface to
+ * all record structures. This class is the direct parent of records that do
+ * not contain other records, i.e. {@link AuthorityRecord authority} and
+ * {@link CopyRecord copy} records.
+ *
+ * This file is part of PicaRecord.
+ *
+ * PicaRecord is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PicaRecord is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   PicaRecord
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2012, 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
+ */
+
+namespace HAB\Pica\Record;
+
+use InvalidArgumentException;
+
+abstract class Record
+{
+
+    /**
+     * Return a new record based on its array representation.
+     *
+     * Returns either a {@link TitleRecord} or a {@link AuthorityRecord}
+     * depending on the field 002@ which encodes the record type.
+     *
+     * @throws InvalidArgumentException Missing type field
+     * @throws InvalidArgumentException Missing `fields' index
+     *
+     * @param  array $record Array representation of a record
+     * @return TitleRecord|AuthorityRecord New record instance
+     */
+    public static function factory (array $record)
+    {
+        if (!array_key_exists('fields', $record)) {
+            throw new InvalidArgumentException("Missing 'fields' index in record array");
+        }
+        $fields = array_map(array('HAB\Pica\Record\Field', 'factory'), $record['fields']);
+        $type = null;
+        $typePredicate = Field::match('002@/00');
+        foreach ($fields as $field) {
+            if ($typePredicate($field)) {
+                $typeSubfield = $field->getNthSubfield('0', 0);
+                if ($typeSubfield) {
+                    $type = $typeSubfield->getValue();
+                    break;
+                }
+            }
+        }
+        if ($type === null) {
+            throw new InvalidArgumentException("Missing type field (002@/00$0)");
+        }
+        if ($type[0] === 'T') {
+            return new AuthorityRecord($fields);
+        } else {
+            return new TitleRecord($fields);
+        }
+    }
+
+    ///
+
+    /**
+     * The record fields.
+     *
+     * @var array
+     */
+    protected $_fields = array();
+
+    /**
+     * The containing parent record, if any.
+     *
+     * @var \HAB\Pica\Record
+     */
+    protected $_parent;
+
+    /**
+     * Constructor.
+     *
+     * @param  array $fields Initial set of fields
+     * @return void
+     */
+    public function __construct (array $fields = array())
+    {
+        $this->setFields($fields);
+    }
+
+    /**
+     * Return array of fields matching predicate.
+     *
+     * @param  callback $where Predicate
+     * @return array Matching fields
+     */
+    public function select ($where)
+    {
+        return array_filter($this->getFields(), $where);
+    }
+
+    /**
+     * Delete fields matching predicate.
+     *
+     * @param  callback $where Predicate
+     * @return void
+     */
+    public function delete ($where)
+    {
+        $complement = Helper::complement($where);
+        $this->_fields = array_filter($this->_fields, $complement);
+    }
+
+    /**
+     * Append a field to the record.
+     *
+     * @throws InvalidArgumentException Field already in record
+     *
+     * @param  \HAB\Pica\Record\Field $field Field to append
+     * @return void
+     */
+    public function append (Field $field)
+    {
+        if (in_array($field, $this->_fields, true)) {
+            throw new InvalidArgumentException("{$this} already contains {$field}");
+        }
+        $this->_fields []= $field;
+    }
+
+    /**
+     * Sort the fields of the record.
+     *
+     * Fields are sorted by their shorthand.
+     *
+     * @see \HAB\Pica\Record\Field::getShorthand()
+     *
+     * @return void
+     */
+    public function sort ()
+    {
+        usort($this->_fields,
+              function (Field $fieldA, Field $fieldB) {
+              return strcmp($fieldA->getShorthand(), $fieldB->getShorthand());
+          });
+    }
+
+    /**
+     * Set the record fields.
+     *
+     * Removes the current set of fields and replaces it with the fields in
+     * argument.
+     *
+     * @param  array $fields Fields
+     * @return void
+     */
+    public function setFields (array $fields)
+    {
+        $this->_fields = array();
+        foreach ($fields as $field) {
+            $this->append($field);
+        }
+    }
+
+    /**
+     * Return the maximum occurrence value of a field.
+     *
+     * @throws InvalidArgumentException Invalid field tag
+     *
+     * @param  string $tag Field tag
+     * @return int|null Maximum occurrence of field or NULL if field does not
+     *         exist
+     */
+    public function getMaximumOccurrenceOf ($tag)
+    {
+        if (!preg_match(Field::TAG_RE, $tag)) {
+            throw new InvalidArgumentException("Invalid field tag: {$tag}");
+        }
+        return array_reduce($this->getFields($tag),
+                            function ($maxOccurrence, Field $field) {
+                            if ($field->getOccurrence() > $maxOccurrence || $maxOccurrence === null) {
+                                return $field->getOccurrence();
+                            } else {
+                                return $maxOccurrence;
+                            }
+                        }, null);
+    }
+
+    /**
+     * Return TRUE if the record is empty.
+     *
+     * A record is empty if it contains no fields.
+     *
+     * @return boolean TRUE if record is empty
+     */
+    public function isEmpty ()
+    {
+        return empty($this->_fields);
+    }
+
+    /**
+     * Return true if the record is valid.
+     *
+     * The base implementation checks that record is not empty and does not
+     * contain an empty field.
+     *
+     * @return boolean
+     */
+    public function isValid ()
+    {
+        return !$this->isEmpty() && !Helper::some($this->getFields(), function (Field $field) { return $field->isEmpty(); });
+    }
+
+    /**
+     * Return fields of the record.
+     *
+     * Optional argument $selector is the body of a regular expression. If set,
+     * this function returns only fields whose shorthand is matched by the
+     * regular expression.
+     *
+     * @see Field::match()
+     *
+     * @param  string $selector Body of regular expression
+     * @return array Fields
+     */
+    public function getFields ($selector = null)
+    {
+        if ($selector === null) {
+            return $this->_fields;
+        } else {
+            return $this->select(Field::match($selector));
+        }
+    }
+
+    /**
+     * Return the first field that matches a selector.
+     *
+     * @param  string $selector Body of regular expression
+     * @return Field|null The first matching field or NULL if no match
+     */
+    public function getFirstMatchingField ($selector)
+    {
+        $fields = $this->getFields($selector);
+        if (empty($fields)) {
+            return null;
+        } else {
+            return reset($fields);
+        }
+    }
+
+    /**
+     * Finalize the clone() operation.
+     *
+     * @return void
+     */
+    public function __clone ()
+    {
+        $this->_fields = Helper::mapClone($this->_fields);
+    }
+
+    /**
+     * Return a printable representation of the record.
+     *
+     * The printable representation of a record is the object hash prefixed by
+     * the class name.
+     *
+     * @return string Printable representation of the record
+     */
+    public function __toString ()
+    {
+        return get_class($this) . ':' . spl_object_hash($this);
+    }
+}
\ No newline at end of file
diff --git a/src/HAB/Pica/Record/Subfield.php b/src/HAB/Pica/Record/Subfield.php
new file mode 100644
index 0000000..9edc7a0
--- /dev/null
+++ b/src/HAB/Pica/Record/Subfield.php
@@ -0,0 +1,146 @@
+<?php
+
+/**
+ * Pica+ subfield.
+ *
+ * A subfield is a cons of an alphanumeric character and a possibly empty
+ * string value.
+ *
+ * This file is part of PicaRecord.
+ *
+ * PicaRecord is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PicaRecord is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   PicaRecord
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2012, 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
+ */
+
+namespace HAB\Pica\Record;
+
+use InvalidArgumentException;
+
+class Subfield
+{
+
+    /**
+     * Return true if argument is a valid subfield code.
+     *
+     * @param  mixed $arg Variable to check
+     * @return boolean
+     */
+    public static function isValidSubfieldCode ($arg)
+    {
+        return (bool)preg_match('/^[a-z0-9]$/Di', $arg);
+    }
+
+    /**
+     * Return a new subfield based on its array representation.
+     *
+     * The array representation of a subfield is an associative array with the
+     * keys `code' and `value', holding the subfield code and value.
+     *
+     * @throws InvalidArgumentException Missing code or value index
+     *
+     * @param  array $subfield Array representation of a subfield
+     * @return Subfield New subfield
+     */
+    public static function factory (array $subfield)
+    {
+        if (!array_key_exists('code', $subfield)) {
+            throw new InvalidArgumentException("Missing 'code' index in subfield array");
+        }
+        if (!array_key_exists('value', $subfield)) {
+            throw new InvalidArgumentException("Missing 'value' index in subfield array");
+        }
+        return new Subfield($subfield['code'], $subfield['value']);
+    }
+
+    ///
+
+    /**
+     * The subfield code.
+     *
+     * @var string
+     */
+    protected $_code;
+
+    /**
+     * The subfield value.
+     *
+     * @var string Value
+     */
+    protected $_value;
+
+    /**
+     * Constructor.
+     *
+     * @throws InvalidArgumentException Invalid subfield code
+     *
+     * @param  string $code Subfield code
+     * @param  string $value Subfield value
+     * @return void
+     */
+    public function __construct ($code, $value)
+    {
+        if (!self::isValidSubfieldCode($code)) {
+            throw new InvalidArgumentException("Invalid subfield code: {$code}");
+        }
+        $this->_code = $code;
+        $this->setValue($value);
+    }
+
+    /**
+     * Set the subfield value.
+     *
+     * @param  string $value Subfield value
+     * @return void
+     */
+    public function setValue ($value)
+    {
+        $this->_value = $value;
+    }
+
+    /**
+     * Return the subfield value.
+     *
+     * @return string Subfield value
+     */
+    public function getValue ()
+    {
+        return $this->_value;
+    }
+
+    /**
+     * Return the subfield code.
+     *
+     * @return string Subfield code
+     */
+    public function getCode ()
+    {
+        return $this->_code;
+    }
+
+    /**
+     * Return printable representation of the subfield.
+     *
+     * The printable representation of a subfield is its value.
+     *
+     * @return string Subfield value
+     */
+    public function __toString ()
+    {
+        return $this->getValue();
+    }
+}
\ No newline at end of file
diff --git a/src/HAB/Pica/Record/TitleRecord.php b/src/HAB/Pica/Record/TitleRecord.php
new file mode 100644
index 0000000..94660b1
--- /dev/null
+++ b/src/HAB/Pica/Record/TitleRecord.php
@@ -0,0 +1,214 @@
+<?php
+
+/**
+ * Pica+ title record.
+ *
+ * This file is part of PicaRecord.
+ *
+ * PicaRecord is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PicaRecord is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   PicaRecord
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2012, 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
+ */
+
+namespace HAB\Pica\Record;
+
+use InvalidArgumentException;
+
+class TitleRecord extends NestedRecord
+{
+
+    /**
+     * Append a field to the title record.
+     *
+     * @see Record::append()
+     *
+     * You can only directly add fields with a level of 0.
+     *
+     * @throws InvalidArgumentException Field level invalid
+     * @throws InvalidArgumentException Field already in record
+     *
+     * @param  Field $field Field to append
+     * @return void
+     */
+    public function append (Field $field)
+    {
+        if ($field->getLevel() !== 0) {
+            throw new InvalidArgumentException("Invalid field level: {$field->getLevel()}");
+        }
+        parent::append($field);
+    }
+
+    /**
+     * Set the record's fields.
+     *
+     * @todo   Relocate to \HAB\Pica\Record\Record::factory(), maybe
+     *
+     * @param  array $fields Field
+     * @return void
+     */
+    public function setFields (array $fields)
+    {
+        $this->_fields = array();
+        $this->_records = array();
+        $prevLevel = null;
+        foreach ($fields as $field) {
+            $level = $field->getLevel();
+            if ($level === 0) {
+                $this->append($field);
+            } else {
+                if ($level === 1 && $prevLevel !== 1) {
+                    $localRecord = new LocalRecord(array($field));
+                    $this->addLocalRecord($localRecord);
+                } else {
+                    $records = $this->getLocalRecords();
+                    $localRecord = end($records);
+                    if ($level === 1) {
+                        $localRecord->append($field);
+                    } else {
+                        $copyRecord = $localRecord->getCopyRecordByItemNumber($field->getOccurrence());
+                        if ($copyRecord) {
+                            $copyRecord->append($field);
+                        } else {
+                            $localRecord->addCopyRecord(new CopyRecord(array($field)));
+                        }
+                    }
+                }
+            }
+            $prevLevel = $level;
+        }
+    }
+
+    /**
+     * Add a local record.
+     *
+     * @throws InvalidArgumentException Record already contains the local record
+     *
+     * @param  LocalRecord $record Local record
+     * @return void
+     */
+    public function addLocalRecord (LocalRecord $record)
+    {
+        $this->addRecord($record);
+        $record->setTitleRecord($this);
+    }
+
+    /**
+     * Remove a local record.
+     *
+     * @param  LocalRecord $record Local record to remove
+     * @return void
+     */
+    public function removeLocalRecord (LocalRecord $record)
+    {
+        $this->removeRecord($record);
+        $record->unsetTitleRecord();
+    }
+
+    /**
+     * Return array of all local records.
+     *
+     * @return array Local records
+     */
+    public function getLocalRecords ()
+    {
+        return $this->_records;
+    }
+
+    /**
+     * Return a local record identified by its ILN.
+     *
+     * @param  integer $iln Intenal library number
+     * @return LocalRecord|null
+     */
+    public function getLocalRecordByILN ($iln)
+    {
+        foreach ($this->getLocalRecords() as $localRecord) {
+            if ($localRecord->getILN() == $iln) {
+                return $localRecord;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return the Pica production number (record identifier).
+     *
+     * @return string|null
+     */
+    public function getPPN ()
+    {
+        $ppnField = $this->getFirstMatchingField('003@/00');
+        if ($ppnField) {
+            $ppnSubfield = $ppnField->getNthSubfield('0', 0);
+            if ($ppnSubfield) {
+                return $ppnSubfield->getValue();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Set the Pica production number.
+     *
+     * Create a field 003@/00 if necessary.
+     *
+     * @param  string $ppn Pica production number
+     * @return void
+     */
+    public function setPPN ($ppn)
+    {
+        $ppnField = $this->getFirstMatchingField('003@/00');
+        if ($ppnField) {
+            $ppnSubfield = $ppnField->getNthSubfield('0', 0);
+            if ($ppnSubfield) {
+                $ppnSubfield->setValue($ppn);
+            } else {
+                $ppnField->append(new Subfield('0', $ppn));
+            }
+        } else {
+            $this->append(new Field('003@', 0, array(new Subfield('0', $ppn))));
+        }
+    }
+
+    /**
+     * Return true if title record contains the local record.
+     *
+     * @param  LocalRecord $record Local record
+     * @return boolean
+     */
+    public function containsLocalRecord (LocalRecord $record)
+    {
+        return $this->containsRecord($record);
+    }
+
+    /**
+     * Compare two local records.
+     *
+     * @see NestedRecord::compareRecords()
+     *
+     * Local records are compared by their ILN.
+     *
+     * @param  Record $a First record
+     * @param  Record $b Second record
+     * @return Comparism value
+     */
+    protected function compareRecords (Record $a, Record $b)
+    {
+        return $a->getILN() - $b->getILN();
+    }
+
+}
\ No newline at end of file
diff --git a/src/README.txt b/src/README.txt
deleted file mode 100644
index 4074f38..0000000
--- a/src/README.txt
+++ /dev/null
@@ -1,68 +0,0 @@
-Your src/ folder
-================
-
-This src/ folder is where you put all of your code for release.  There's
-a folder for each type of file that the PEAR Installer supports.  You can
-find out more about these file types online at:
-
-http://blog.stuartherbert.com/php/2011/04/04/explaining-file-roles/
-
-  * bin/
-
-    If you're creating any command-line tools, this is where you'd put
-    them.  Files in here get installed into /usr/bin on Linux et al.
-
-    There is more information available here: http://blog.stuartherbert.com/php/2011/04/06/php-components-shipping-a-command-line-program/
-
-    You can find an example here: https://github.com/stuartherbert/phix/tree/master/src/bin
-
-  * data/
-
-    If you have any data files (any files that aren't PHP code, and which
-    don't belong in the www/ folder), this is the folder to put them in.
-
-    There is more information available here: http://blog.stuartherbert.com/php/2011/04/11/php-components-shipping-data-files-with-your-components/
-
-    You can find an example here: https://github.com/stuartherbert/ComponentManagerPhpLibrary/tree/master/src/data
-
-  * php/
-
-    This is where your component's PHP code belongs.  Everything that goes
-    into this folder must be PSR0-compliant, so that it works with the
-    supplied autoloader.
-
-    There is more information available here: http://blog.stuartherbert.com/php/2011/04/05/php-components-shipping-reusable-php-code/
-
-    You can find an example here: https://github.com/stuartherbert/ContractLib/tree/master/src/php
-
-  * tests/functional-tests/
-
-    Right now, this folder is just a placeholder for future functionality.
-    You're welcome to make use of it yourself.
-
-  * tests/integration-tests/
-
-    Right now, this folder is just a placeholder for future functionality.
-    You're welcome to make use of it yourself.
-
-  * tests/unit-tests/
-
-    This is where all of your PHPUnit tests go.
-
-    It needs to contain _exactly_ the same folder structure as the src/php/
-    folder.  For each of your PHP classes in src/php/, there should be a
-    corresponding test file in test/unit-tests.
-
-    There is more information available here: http://blog.stuartherbert.com/php/2011/08/15/php-components-shipping-unit-tests-with-your-component/
-
-    You can find an example here: https://github.com/stuartherbert/ContractLib/tree/master/test/unit-tests
-
-  * www/
-
-    This folder is for any files that should be published in a web server's
-    DocRoot folder.
-
-    It's quite unusual for components to put anything in this folder, but
-    it is there just in case.
-
-    There is more information available here: http://blog.stuartherbert.com/php/2011/08/16/php-components-shipping-web-pages-with-your-components/
diff --git a/src/bin/.empty b/src/bin/.empty
deleted file mode 100644
index e69de29..0000000
diff --git a/src/data/.empty b/src/data/.empty
deleted file mode 100644
index e69de29..0000000
diff --git a/src/docs/.empty b/src/docs/.empty
deleted file mode 100644
index e69de29..0000000
diff --git a/src/php/.empty b/src/php/.empty
deleted file mode 100644
index e69de29..0000000
diff --git a/src/php/HAB/Pica/Record/AuthorityRecord.php b/src/php/HAB/Pica/Record/AuthorityRecord.php
deleted file mode 100644
index 89e94e1..0000000
--- a/src/php/HAB/Pica/Record/AuthorityRecord.php
+++ /dev/null
@@ -1,145 +0,0 @@
-<?php
-
-/**
- * The AuthorityRecord class file.
- *
- * This file is part of PicaRecord.
- *
- * PicaRecord is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PicaRecord is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-
-namespace HAB\Pica\Record;
-
-/**
- * The Pica+ authority record.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-class AuthorityRecord extends Record {
-
-  /**
-   * Append a field to the record.
-   *
-   * @see Record::append()
-   *
-   * @throws \InvalidArgumentException Field level other than 0
-   * @throws \InvalidArgumentException Field already in record
-   * @param  Field $field Field to append
-   * @return void
-   */
-  public function append (Field $field) {
-    if ($field->getLevel() !== 0) {
-      throw new \InvalidArgumentException("Invalid field level {$field->getLevel()}");
-    }
-    return parent::append($field);
-  }
-
-  /**
-   * Return the Pica production number (record identifier).
-   *
-   * @return string|null Pica production number or NULL if none exists
-   */
-  public function getPPN () {
-    $ppnField = $this->getFirstMatchingField('003@/00');
-    if ($ppnField) {
-      $ppnSubfield = $ppnField->getNthSubfield('0', 0);
-      if ($ppnSubfield) {
-        return $ppnSubfield->getValue();
-      }
-    }
-    return null;
-  }
-
-  /**
-   * Set the Pica production number.
-   *
-   * Create a field 003@/00 if necessary.
-   *
-   * @param  string $ppn Pica production number
-   * @return void
-   */
-  public function setPPN ($ppn) {
-    $ppnField = $this->getFirstMatchingField('003@/00');
-    if ($ppnField) {
-      $ppnSubfield = $ppnField->getNthSubfield('0', 0);
-      if ($ppnSubfield) {
-        $ppnSubfield->setValue($ppn);
-      } else {
-        $ppnField->append(new Subfield('0', $ppn));
-      }
-    } else {
-      $this->append(new Field('003@', 0, array(new Subfield('0', $ppn))));
-    }
-  }
-
-  /**
-   * Return TRUE if the record is valid.
-   *
-   * A valid authority record MUST have exactly one valid PPN field
-   * (003@/00$0) and exactly one type field (002@/0$0) with a type indicator
-   * `T'.
-   *
-   * @see \HAB\Pica\Record\AuthorityRecord::checkPPN();
-   * @see \HAB\Pica\Record\AuthorityRecord::checkType();
-   *
-   * @return boolean TRUE if the record is valid
-   */
-  public function isValid () {
-    return parent::isValid() && $this->checkPPN() && $this->checkType();
-  }
-
-  /**
-   * Return TRUE if the record has exactly one PPN field (003@/00) with a
-   * subfield $0.
-   *
-   * @return boolean TRUE if the record has a valid PPN field
-   */
-  protected function checkPPN () {
-    $ppnField = $this->getFields('003@/00');
-    if (count($ppnField) === 1) {
-      $ppnSubfield = reset($ppnField)->getNthSubfield('0', 0);
-      if ($ppnSubfield) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  /**
-   * Return TRUE if the record has exactly one type field (002@/00) with a
-   * subfield $0 whose first character is `T'.
-   *
-   * @return boolean TRUE if the record has a valid type field
-   */
-  protected function checkType () {
-    $typeField = $this->getFields('002@/00');
-    if (count($typeField) === 1) {
-      $typeSubfield = reset($typeField)->getNthSubfield('0', 0);
-      if ($typeSubfield) {
-        $typeCode = $typeSubfield->getValue();
-        if ($typeCode === 'T') {
-          return true;
-        }
-      }
-    }
-  }
-}
\ No newline at end of file
diff --git a/src/php/HAB/Pica/Record/CopyRecord.php b/src/php/HAB/Pica/Record/CopyRecord.php
deleted file mode 100644
index b3ac3fd..0000000
--- a/src/php/HAB/Pica/Record/CopyRecord.php
+++ /dev/null
@@ -1,166 +0,0 @@
-<?php
-
-/**
- * The CopyRecord class file.
- *
- * This file is part of PicaRecord.
- *
- * PicaRecord is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PicaRecord is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-
-namespace HAB\Pica\Record;
-
-/**
- * The Pica+ copy record.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-class CopyRecord extends Record {
-
-  /**
-   * The item number.
-   *
-   * @var integer
-   */
-  protected $_itemNumber;
-
-  /**
-   * Append a field to the copy record.
-   *
-   * You can only append field of level 2 to a copy record.
-   *
-   * @see Record::append()
-   *
-   * @throws \InvalidArgumentException Field level other than 2
-   * @throws \InvalidArgumentException Item number mismatch
-   * @throws \InvalidArgumentException Field already in record
-   * @param  \HAB\Pica\Record\Field $field Field to append
-   * @return void
-   */
-  public function append (\HAB\Pica\Record\Field $field) {
-    if ($field->getLevel() !== 2) {
-      throw new \InvalidArgumentException("Invalid field level: {$field->getLevel()}");
-    }
-    if ($this->getItemNumber() === null) {
-      $this->setItemNumber($field->getOccurrence());
-    }
-    If ($field->getOccurrence() != $this->getItemNumber()) {
-      throw new \InvalidArgumentException("Item number mismatch: {$this->getItemNumber()}, {$field->getOccurrence()}");
-    }
-    return parent::append($field);
-  }
-
-  /**
-   * Return the item number.
-   *
-   * @return integer|null Item number
-   */
-  public function getItemNumber () {
-    return $this->_itemNumber;
-  }
-
-  /**
-   * Set the item number.
-   *
-   * @param  integer $itemNumber Item number
-   * @return void
-   */
-  protected function setItemNumber ($itemNumber) {
-    $this->_itemNumber = (int)$itemNumber;
-  }
-
-  /**
-   * Return the exemplar production number (EPN).
-   *
-   * @return string Exemplar production number
-   */
-  public function getEPN () {
-    $epnField = $this->getFirstMatchingField('203@');
-    if ($epnField) {
-      $epnSubfield = $epnField->getNthSubfield('0', 0);
-      if ($epnSubfield) {
-        return $epnSubfield->getValue();
-      }
-    }
-    return null;
-  }
-
-  /**
-   * Set the exemplar production number (EPN).
-   *
-   * Create a field 203@ if necessary.
-   *
-   * @param  string $epn Exemplar production number
-   * @return void
-   */
-  public function setEPN ($epn) {
-    $epnField = $this->getFirstMatchingField('203@');
-    if ($epnField) {
-      $epnSubfield = $epnField->getNthSubfield('0', 0);
-      if ($epnSubfield) {
-        $epnSubfield->setValue($epn);
-      } else {
-        $epnField->append(new Subfield('0', $epn));
-      }
-    } else {
-      $this->append(new Field('203@', $this->getItemNumber(), array(new Subfield('0', $epn))));
-    }
-  }
-
-  /**
-   * Set the containing local record.
-   *
-   * @param  \HAB\Pica\Record\LocalRecord $record Local record
-   * @return void
-   */
-  public function setLocalRecord (\HAB\Pica\Record\LocalRecord $record) {
-      $this->unsetLocalRecord();
-      if (!$record->containsCopyRecord($this)) {
-          $record->addCopyRecord($this);
-      }
-      $this->_parent = $record;
-  }
-
-  /**
-   * Unset the containing local record.
-   *
-   * @return void
-   */
-  public function unsetLocalRecord () {
-      if ($this->_parent) {
-          if ($this->_parent->containsCopyRecord($this)) {
-              $this->_parent->removeCopyRecord($this);
-          }
-          $this->_parent = null;
-      }
-  }
-
-  /**
-   * Return the containing local record.
-   *
-   * @return \HAB\Pica\Record\LocalRecord|null
-   */
-  public function getLocalRecord () {
-      return $this->_parent;
-  }
-
-}
\ No newline at end of file
diff --git a/src/php/HAB/Pica/Record/Field.php b/src/php/HAB/Pica/Record/Field.php
deleted file mode 100644
index eb0037f..0000000
--- a/src/php/HAB/Pica/Record/Field.php
+++ /dev/null
@@ -1,326 +0,0 @@
-<?php
-
-/**
- * The Field class file.
- *
- * This file is part of PicaRecord.
- *
- * PicaRecord is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PicaRecord is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-
-namespace HAB\Pica\Record;
-
-/**
- * The Pica+ field.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-class Field {
-
-  /**
-   * Regular expression matching a valid Pica+ field tag.
-   *
-   * @var string
-   */
-  const TAG_RE = '|^[012][0-9]{2}[A-Z@]$|D';
-
-  /**
-   * Return TRUE if argument is a valid field tag.
-   *
-   * @param  mixed $arg Variable to check
-   * @return boolean TRUE if argument is a valid field tag
-   */
-  public static function isValidFieldTag ($arg) {
-    return (bool)preg_match(self::TAG_RE, $arg);
-  }
-
-  /**
-   * Return TRUE if argument is a valid field occurrence.
-   *
-   * Argument is casted to int iff it is either null or a numeric string.
-   *
-   * @param  mixed $arg Variable to check
-   * @return boolean TRUE if argument is a valid field occurrence
-   */
-  public static function isValidFieldOccurrence ($arg) {
-    if ($arg === null || ctype_digit($arg)) {
-      $arg = (int)$arg;
-    }
-    return is_int($arg) && $arg >= 0 && $arg < 100;
-  }
-
-  /**
-   * Return predicate that matches a field shorthand against a regular
-   * expression.
-   *
-   * @param  string $reBody Body of regular expression
-   * @return callback Predicate
-   */
-  public static function match ($reBody) {
-    if (strpos($reBody, '#') !== false) {
-      $reBody = str_replace('#', '\#', $reBody);
-    }
-    $regexp = "#{$reBody}#D";
-    return function (Field $field) use ($regexp) {
-      return (bool)preg_match($regexp, $field->getShorthand());
-    };
-  }
-
-  /**
-   * Return a new field based on its array representation.
-   *
-   * @throws \InvalidArgumentException Missing `tag', `occurrene', or `subfields' index
-   * @param  array $field Array representation of a field
-   * @return \HAB\Pica\Record\Field A shiny new field
-   */
-  public static function factory (array $field) {
-    foreach (array('tag', 'occurrence', 'subfields') as $index) {
-      if (!array_key_exists($index, $field)) {
-        throw new \InvalidArgumentException("Missing '{$index}' index in field array");
-      }
-    }
-    return new Field($field['tag'],
-                     $field['occurrence'],
-                     array_map(array('HAB\Pica\Record\Subfield', 'factory'), $field['subfields']));
-  }
-
-  ///
-
-  /**
-   * The field tag.
-   *
-   * @var string
-   */
-  protected $_tag;
-
-  /**
-   * The field level.
-   *
-   * @var integer
-   */
-  protected $_level;
-
-  /**
-   * The field occurrence.
-   *
-   * @var integer
-   */
-  protected $_occurrence;
-
-  /**
-   * The field shorthand.
-   *
-   * @var string
-   */
-  protected $_shorthand;
-
-  /**
-   * Constructor.
-   *
-   * @throws \InvalidArgumentException Invalid field tag or occurrence
-   * @param  string $tag Field tag
-   * @param  integer $occurrence Field occurrence
-   * @param  array $subfields Initial set of subfields
-   * @return void
-   */
-  public function __construct ($tag, $occurrence, array $subfields = array()) {
-    if (!self::isValidFieldTag($tag)) {
-      throw new \InvalidArgumentException("Invalid field tag: $tag");
-    }
-    if (!self::isValidFieldOccurrence($occurrence)) {
-      throw new \InvalidArgumentException("Invalid field occurrence: $occurrence");
-    }
-    $this->_tag = $tag;
-    $this->_occurrence = (int)$occurrence;
-    $this->_shorthand = sprintf('%4s/%02d', $tag, $occurrence);
-    $this->_level = (int)$tag[0];
-    $this->setSubfields($subfields);
-  }
-
-  /**
-   * Set the field subfields.
-   *
-   * Replaces the subfield list with subfields in argument.
-   *
-   * @param  array $subfields Subfields
-   * @return void
-   */
-  public function setSubfields (array $subfields) {
-    $this->_subfields = array();
-    foreach ($subfields as $subfield) {
-      $this->addSubfield($subfield);
-    }
-  }
-
-  /**
-   * Add a subfield to the end of the subfield list.
-   *
-   * @throws \InvalidArgumentException Subfield already present in subfield list
-   * @param  \HAB\Pica\Record\Subfield $subfield Subfield to add
-   * @return void
-   */
-  public function addSubfield (\HAB\Pica\Record\Subfield $subfield) {
-    if (in_array($subfield, $this->getSubfields(), true)) {
-      throw new \InvalidArgumentException("Cannot add subfield: Subfield already part of the subfield list");
-    }
-    $this->_subfields []= $subfield;
-  }
-
-  /**
-   * Remove a subfield.
-   *
-   * @throws \InvalidArgumentException Subfield is not part of the subfield list
-   * @param  \HAB\Pica\Record\Subfield $subfield Subfield to delete
-   * @return void
-   */
-  public function removeSubfield (\HAB\Pica\Record\Subfield $subfield) {
-    $index = array_search($subfield, $this->_subfields, true);
-    if ($index === false) {
-      throw new \InvalidArgumentException("Cannot remove subfield: Subfield not part of the subfield list");
-    }
-    unset($this->_subfields[$index]);
-  }
-
-  /**
-   * Return the field's subfields.
-   *
-   * Returns all subfields when called with no arguments.
-   *
-   * Otherwise the returned array is constructed as follows:
-   *
-   * Each argument is interpreted as a subfield code. The nth element of the
-   * returned array maps to the nth argument in the function call and contains
-   * NULL if the field does not have a subfield with the selected code, or the
-   * subfield if it exists. In order to retrieve multiple subfields with an
-   * identical code you repeat the subfield code in the argument list.
-   *
-   * @return array Subfields
-   */
-  public function getSubfields () {
-    if (func_num_args() === 0) {
-      return $this->_subfields;
-    } else {
-      $selected = array();
-      $codes = array();
-      $subfields = $this->getSubfields();
-      array_walk($subfields, function ($value, $index) use (&$codes) { $codes[$index] = $value->getCode(); });
-      foreach (func_get_args() as $arg) {
-        $index = array_search($arg, $codes, true);
-        if ($index === false) {
-          $selected []= null;
-        } else {
-          $selected []= $subfields[$index];
-          unset($codes[$index]);
-        }
-      }
-      return $selected;
-    }
-  }
-
-  /**
-   * Return the nth occurrence of a subfield with specified code.
-   *
-   * @param  string $code Subfield code
-   * @param  integer $n Zero-based subfield index
-   * @return \HAB\Pica\record\Subfield|null The requested subfield or NULL if
-   *         none exists
-   */
-  public function getNthSubfield ($code, $n) {
-    $count = 0;
-    foreach ($this->getSubfields() as $subfield) {
-      if ($subfield->getCode() == $code) {
-        if ($count == $n) {
-          return $subfield;
-        }
-        $count++;
-      }
-    }
-    return null;
-  }
-
-  /**
-   * Return the field tag.
-   *
-   * @return string Field tag
-   */
-  public function getTag () {
-    return $this->_tag;
-  }
-
-  /**
-   * Return the field occurrence.
-   *
-   * @return integer Field occurrence
-   */
-  public function getOccurrence () {
-    return $this->_occurrence;
-  }
-
-  /**
-   * Return the field level.
-   *
-   * @return integer Field level
-   */
-  public function getLevel () {
-    return $this->_level;
-  }
-
-  /**
-   * Return the field shorthand.
-   *
-   * @return string Field shorthand
-   */
-  public function getShorthand () {
-    return $this->_shorthand;
-  }
-
-  /**
-   * Return TRUE if the field is empty.
-   *
-   * A field is empty if it contains no subfields.
-   *
-   * @return boolean TRUE if the field is empty
-   */
-  public function isEmpty () {
-    return empty($this->_subfields);
-  }
-
-  /**
-   * Finalize the clone() operation.
-   *
-   * @return void
-   */
-  public function __clone () {
-    $this->_subfields = Helper::mapClone($this->_subfields);
-  }
-
-  /**
-   * Return a printable representation of the field.
-   *
-   * The printable representation of a field is its shorthand.
-   *
-   * @return string Printable representation of the field
-   */
-  public function __toString () {
-    return $this->getShorthand();
-  }
-}
\ No newline at end of file
diff --git a/src/php/HAB/Pica/Record/Helper.php b/src/php/HAB/Pica/Record/Helper.php
deleted file mode 100644
index c1349b5..0000000
--- a/src/php/HAB/Pica/Record/Helper.php
+++ /dev/null
@@ -1,132 +0,0 @@
-<?php
-
-/**
- * The Helper class file.
- *
- * This file is part of PicaRecord.
- *
- * PicaRecord is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PicaRecord is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-
-namespace HAB\Pica\Record;
-
-/**
- * Abstract class to anchor helper functions.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-abstract class Helper {
-
-  /**
-   * Return the complement of a function.
-   *
-   * The complement of a function is a function that takes the same arguments
-   * as the original function but returns a boolean with the opposite truth
-   * value.
-   *
-   * @param  callback $function Original function
-   * @return callback Complement function
-   */
-  public static function complement ($function) {
-    return function () use ($function) { return !call_user_func_array($function, func_get_args()); };
-  }
-
-  /**
-   * Return an array of the results of calling a method for each element of a
-   * sequence.
-   *
-   * @param  array $sequence Sequence of objects
-   * @param  string $method Name of the method
-   * @param  array $arguments Optional array of method arguments
-   * @return array Result of calling method on each element of sequence
-   */
-  public static function mapMethod (array $sequence, $method, array $arguments = array()) {
-    if (empty($arguments)) {
-      $f = function ($element) use ($method) {
-        return $element->$method();
-      };
-    } else {
-      $f = function ($element) use ($method, $arguments) {
-        return call_user_func_array(array($element, $method), $arguments);
-      };
-    }
-    return array_map($f, $sequence);
-  }
-
-  /**
-   * Return an array with clones of each element in sequence.
-   *
-   * @param  array $sequence Sequence of objects
-   * @return array Sequence of clones
-   */
-  public static function mapClone (array $sequence) {
-    return array_map(function ($element) { return clone($element); }, $sequence);
-  }
-
-  /**
-   * Return TRUE if at leat one element of sequence matches predicate.
-   *
-   * @todo   Make FALSE and TRUE self-evaluating, maybe
-   *
-   * @param  array $sequence Sequence
-   * @param  callback $predicate Predicate
-   * @return boolean TRUE if at least one element matches predicate
-   */
-  public static function some (array $sequence, $predicate) {
-    foreach ($sequence as $element) {
-      if (call_user_func($predicate, $element)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  /**
-   * Return TRUE if every element of sequence fullfills predicate.
-   *
-   * @todo   Make FALSE and TRUE self-evaluating, maybe
-   *
-   * @param  array $sequence Sequence
-   * @param  callback $predicate Predicate
-   * @return boolean TRUE if every element fullfills predicate
-   */
-  public static function every (array $sequence, $predicate) {
-    foreach ($sequence as $element) {
-      if (!call_user_func($predicate, $element)) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  /**
-   * Flatten sequence.
-   *
-   * @param  array $sequence Sequence
-   * @return array Flattend sequence
-   */
-  public static function flatten (array $sequence) {
-    $flat = array();
-    array_walk_recursive($sequence, function ($element) use (&$flat) { $flat []= $element; });
-    return $flat;
-  }
-}
\ No newline at end of file
diff --git a/src/php/HAB/Pica/Record/LocalRecord.php b/src/php/HAB/Pica/Record/LocalRecord.php
deleted file mode 100644
index 48c4fed..0000000
--- a/src/php/HAB/Pica/Record/LocalRecord.php
+++ /dev/null
@@ -1,189 +0,0 @@
-<?php
-
-/**
- * The LocalRecord class file.
- *
- * This file is part of PicaRecord.
- *
- * PicaRecord is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PicaRecord is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-
-namespace HAB\Pica\Record;
-
-/**
- * The Pica+ local record.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-class LocalRecord extends NestedRecord {
-
-  /**
-   * Append a field to the local record.
-   *
-   * You can only append field with a level of 0 to a local record.
-   *
-   * @see \HAB\Pica\Record\Record::append()
-   *
-   * @throws \InvalidArgumentException Field level invalid
-   * @throws \InvalidArgumentException Field already in record
-   * @param  \HAB\Pica\Record\Field $field Field to add
-   * @return void
-   */
-  public function append (Field $field) {
-    if ($field->getLevel() !== 1) {
-      throw new \InvalidArgumentException("Invalid field level: {$field->getLevel()}");
-    }
-    parent::append($field);
-  }
-
-  /**
-   * Add a copy record.
-   *
-   * @throws \InvalidArgumentException Record already contains the copy record
-   * @throws \InvalidArgumentException Record already contains a copy record with the same item number
-   * @param  \HAB\Pica\Record\CopyRecord $record Copy record to add
-   * @return void
-   */
-  public function addCopyRecord (\HAB\Pica\Record\CopyRecord $record) {
-    if ($this->getCopyRecordByItemNumber($record->getItemNumber())) {
-      throw new \InvalidArgumentException("Cannot add copy record: Copy record with item number {$record->getItemNumber()} already present");
-    }
-    $this->addRecord($record);
-    $record->setLocalRecord($this);
-  }
-
-  /**
-   * Remove a copy record.
-   *
-   * @throws \HAB\Pica\Record\Exception Record does not contain the specified copy record
-   * @param  \HAB\Pica\Record\CopyRecord $record Record to remove
-   * @return void
-   */
-  public function removeCopyRecord (\HAB\Pica\Record\CopyRecord $record) {
-    $this->removeRecord($record);
-    $record->unsetLocalRecord();
-  }
-
-  /**
-   * Return copy record by item number.
-   *
-   * @param  integer $itemNumber Item number
-   * @return \HAB\Pica\Record\CopyRecord|null The copy record or null if none exists
-   */
-  public function getCopyRecordByItemNumber ($itemNumber) {
-    foreach ($this->_records as $record) {
-      if ($record->getItemNumber() === $itemNumber) {
-        return $record;
-      }
-    }
-    return null;
-  }
-
-  /**
-   * Return all copy records.
-   *
-   * @return array Copy records
-   */
-  public function getCopyRecords () {
-    return $this->_records;
-  }
-
-  /**
-   * Return the ILN (internal library number) of the local record.
-   *
-   * @return integer|null ILN of the local record or NULL if none set
-   */
-  public function getILN () {
-    $ilnField = $this->getFirstMatchingField('101@/00');
-    if ($ilnField) {
-      $ilnSubfield = $ilnField->getNthSubfield('a', 0);
-      if ($ilnSubfield) {
-        return $ilnSubfield->getValue();
-      }
-    }
-    return null;
-  }
-
-  /**
-   * Return true if local record contains the copy record.
-   *
-   * @param  \HAB\Pica\Record\CopyRecord $record Copy record
-   * @return boolean
-   */
-  public function containsCopyRecord (\HAB\Pica\Record\CopyRecord $record) {
-    return $this->containsRecord($record);
-  }
-
-  /**
-   * Set the containing title record.
-   *
-   * @param  \HAB\Pica\Record\TitleRecord $record Title record
-   * @return void
-   */
-  public function setTitleRecord (\HAB\Pica\Record\TitleRecord $record) {
-      $this->unsetTitleRecord();
-      if (!$record->containsLocalRecord($this)) {
-          $record->addLocalRecord($this);
-      }
-      $this->_parent = $record;
-  }
-
-  /**
-   * Unset the containing title record.
-   *
-   * @return void
-   */
-  public function unsetTitleRecord () {
-      if ($this->_parent) {
-          if ($this->_parent->containsLocalRecord($this)) {
-              $this->_parent->removeLocalRecord($this);
-          }
-          $this->_parent = null;
-      }
-  }
-
-  /**
-   * Return the containing local record.
-   *
-   * @return \HAB\Pica\Record\TitleRecord|null
-   */
-  public function getTitleRecord () {
-      return $this->_parent;
-  }
-
-  /**
-   * Compare two copy records.
-   *
-   * Copyrecords are compared by their item number.
-   *
-   * @see \HAB\Pica\Record\CopyRecord::getItemNumber()
-   * @see \HAB\Pica\Record\NestedRecord::compareRecords()
-   *
-   * @param  \HAB\Pica\Record\Record $a First copy record
-   * @param  \HAB\Pica\Record\Record $b Second copy record
-   * @return integer Comparism value
-   */
-  protected function compareRecords (\HAB\Pica\Record\Record $a, \HAB\Pica\Record\Record $b) {
-    return $a->getItemNumber() - $b->getItemNumber();
-  }
-
-}
\ No newline at end of file
diff --git a/src/php/HAB/Pica/Record/NestedRecord.php b/src/php/HAB/Pica/Record/NestedRecord.php
deleted file mode 100644
index 5a8f1a5..0000000
--- a/src/php/HAB/Pica/Record/NestedRecord.php
+++ /dev/null
@@ -1,186 +0,0 @@
-<?php
-
-/**
- * The NestedRecord class file.
- *
- * This file is part of PicaRecord.
- *
- * PicaRecord is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PicaRecord is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-
-namespace HAB\Pica\Record;
-
-/**
- * Abstract base class of nested records.
- *
- * A nested record is a record that contains zero or more other records. It is
- * the base class of {@link TitleRecord title} and {@link LocalRecord local}
- * records and implements internal accessors for the contained records and the
- * propagation of field getters to the contained records.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-abstract class NestedRecord extends Record {
-
-  /**
-   * Contained records.
-   *
-   * @var array
-   */
-  protected $_records = array();
-
-  /**
-   * Delete fields matching predicate.
-   *
-   * The delete() is propagated down to all contained records.
-   *
-   * @see \HAB\Pica\Record\Record::delete()
-   *
-   * @param  callback $where Predicate
-   * @return void
-   */
-  public function delete ($where) {
-    parent::delete($where);
-    Helper::mapMethod($this->_records, 'delete', array($where));
-  }
-
-  /**
-   * Sort fields and contained records.
-   *
-   * The sort() is propagated down to all contained records. In addition the
-   * nested records are sorted themselves using the implementing class'
-   * compareNestedRecords() function.
-   *
-   * @see \HAB\Pica\Record\Record::sort()
-   * @see \HAB\Pica\Record\NestedRecord::compareNestedRecords()
-   *
-   * @return void
-   */
-  public function sort () {
-    parent::sort();
-    Helper::mapMethod($this->_records, 'sort');
-    usort($this->_records, array($this, 'compareRecords'));
-  }
-
-  /**
-   * Return TRUE if the record is empty.
-   *
-   * A nested record is empty iff it contains no fields and no non-empty
-   * contained record.
-   *
-   * @return boolean TRUE if the record is empty
-   */
-  public function isEmpty () {
-    return parent::isEmpty() && Helper::every($this->_records, function (Record $record) { return $record->isEmpty(); });
-  }
-
-  /**
-   * Return TRUE if the record is valid.
-   *
-   * A nested record is valid iff it and all contained records are valid.
-   *
-   * @see \HAB\Pica\Record\Record::isValid()
-   *
-   * @return boolean True if the record is valid
-   */
-  public function isValid () {
-    return parent::isValid() && !Helper::every($this->_records, function (Record $record) { return $record->isValid(); });
-  }
-
-  /**
-   * Return fields of the record.
-   *
-   * @see \HAB\Pica\Record\Record::getFields()
-   *
-   * @param  string $selector Body of regular expression
-   * @return array Fields
-   */
-  public function getFields ($selector = null) {
-    if ($selector === null) {
-      return array_merge($this->_fields, Helper::flatten(Helper::mapMethod($this->_records, 'getFields')));
-    } else {
-      return $this->select(Field::match($selector));
-    }
-  }
-
-  /**
-   * Compare two contained records and return a comparism value suitable for
-   * usort().
-   *
-   * @see http://www.php.net/manual/en/function.usort.php
-   *
-   * @param  \HAB\Pica\Record\Record $a First record
-   * @param  \HAB\Pica\Record\Record $b Second record
-   * @return integer Comparism value
-   */
-  abstract protected function compareRecords (\HAB\Pica\Record\Record $a, \HAB\Pica\Record\Record $b);
-
-  /**
-   * Add a record as a contained record.
-   *
-   * @throws \InvalidArgumentException Record already contains the record
-   * @param  \HAB\Pica\Record\Record $record Record to add
-   * @return void
-   */
-  protected function addRecord (\HAB\Pica\Record\Record $record) {
-    if ($this->containsRecord($record)) {
-      throw new \InvalidArgumentException("{$this} already contains {$record}");
-    }
-    $this->_records []= $record;
-  }
-
-  /**
-   * Remove a contained record.
-   *
-   * @throws \InvalidArgumentException Record does not contain the record
-   * @param  \HAB\Pica\Record\Record $record Record to remove
-   * @return void
-   */
-  protected function removeRecord (\HAB\Pica\Record\Record $record) {
-    $index = array_search($record, $this->_records, true);
-    if ($index === false) {
-      throw new \InvalidArgumentException("{$this} does not contain {$record}");
-    }
-    unset($this->_records[$index]);
-  }
-
-  /**
-   * Return true if this record contains the requested record.
-   *
-   * @param  \HAB\Pica\Record\Record Record to check
-   * @return boolean
-   */
-  protected function containsRecord (\HAB\Pica\Record\Record $record) {
-    return in_array($record, $this->_records, true);
-  }
-
- /**
-   * Finalize the clone() operation.
-   *
-   * Clone all contained records.
-   *
-   * @return void
-   */
-  public function __clone () {
-    $this->_records = Helper::mapClone($this->_records);
-  }
-}
\ No newline at end of file
diff --git a/src/php/HAB/Pica/Record/Record.php b/src/php/HAB/Pica/Record/Record.php
deleted file mode 100644
index ab64f66..0000000
--- a/src/php/HAB/Pica/Record/Record.php
+++ /dev/null
@@ -1,277 +0,0 @@
-<?php
-
-/**
- * The Record class file.
- *
- * This file is part of PicaRecord.
- *
- * PicaRecord is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PicaRecord is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-
-namespace HAB\Pica\Record;
-
-/**
- * Abstract base class of all record structures.
- *
- * The abstract base class defines and partially implements the interface to
- * all record structures. This class is the direct parent of records that do
- * not contain other records, i.e. {@link AuthorityRecord authority} and
- * {@link CopyRecord copy} records.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-abstract class Record {
-
-  /**
-   * Return a new record based on its array representation.
-   *
-   * Returns either a {@link TitleRecord} or a {@link AuthorityRecord}
-   * depending on the field 002@ which encodes the record type.
-   *
-   * @throws \InvalidArgumentException Missing type field
-   * @throws \InvalidArgumentException Missing `fields' index
-   * @param  array $record Array representation of a record
-   * @return \HAB\Pica\Record\TitleRecord|\HAB\Pica\Record\AuthorityRecord New record instance
-   */
-  public static function factory (array $record) {
-    if (!array_key_exists('fields', $record)) {
-      throw new \InvalidArgumentException("Missing 'fields' index in record array");
-    }
-    $fields = array_map(array('HAB\Pica\Record\Field', 'factory'), $record['fields']);
-    $type = null;
-    $typePredicate = Field::match('002@/00');
-    foreach ($fields as $field) {
-      if ($typePredicate($field)) {
-        $typeSubfield = $field->getNthSubfield('0', 0);
-        if ($typeSubfield) {
-          $type = $typeSubfield->getValue();
-          break;
-        }
-      }
-    }
-    if ($type === null) {
-      throw new \InvalidArgumentException("Missing type field (002@/00$0)");
-    }
-    if ($type[0] === 'T') {
-      return new AuthorityRecord($fields);
-    } else {
-      return new TitleRecord($fields);
-    }
-  }
-
-  ///
-
-  /**
-   * The record fields.
-   *
-   * @var array
-   */
-  protected $_fields = array();
-
-  /**
-   * The containing parent record, if any.
-   *
-   * @var \HAB\Pica\Record
-   */
-  protected $_parent;
-
-  /**
-   * Constructor.
-   *
-   * @param  array $fields Initial set of fields
-   * @return void
-   */
-  public function __construct (array $fields = array()) {
-    $this->setFields($fields);
-  }
-
-  /**
-   * Return array of fields matching predicate.
-   *
-   * @param  callback $where Predicate
-   * @return array Matching fields
-   */
-  public function select ($where) {
-    return array_filter($this->getFields(), $where);
-  }
-
-  /**
-   * Delete fields matching predicate.
-   *
-   * @param  callback $where Predicate
-   * @return void
-   */
-  public function delete ($where) {
-    $complement = Helper::complement($where);
-    $this->_fields = array_filter($this->_fields, $complement);
-  }
-
-  /**
-   * Append a field to the record.
-   *
-   * @throws \InvalidArgumentException Field already in record
-   * @param  \HAB\Pica\Record\Field $field Field to append
-   * @return void
-   */
-  public function append (\HAB\Pica\Record\Field $field) {
-    if (in_array($field, $this->_fields, true)) {
-      throw new \InvalidArgumentException("{$this} already contains {$field}");
-    }
-    $this->_fields []= $field;
-  }
-
-  /**
-   * Sort the fields of the record.
-   *
-   * Fields are sorted by their shorthand.
-   *
-   * @see \HAB\Pica\Record\Field::getShorthand()
-   *
-   * @return void
-   */
-  public function sort () {
-    usort($this->_fields,
-          function (Field $fieldA, Field $fieldB) {
-            return strcmp($fieldA->getShorthand(), $fieldB->getShorthand());
-          });
-  }
-
-  /**
-   * Set the record fields.
-   *
-   * Removes the current set of fields and replaces it with the fields in
-   * argument.
-   *
-   * @param  array $fields Fields
-   * @return void
-   */
-  public function setFields (array $fields) {
-    $this->_fields = array();
-    foreach ($fields as $field) {
-      $this->append($field);
-    }
-  }
-
-  /**
-   * Return the maximum occurrence value of a field.
-   *
-   * @throws \InvalidArgumentException Invalid field tag
-   * @param  string $tag Field tag
-   * @return int|null Maximum occurrence of field or NULL if field does not
-   *         exist
-   */
-  public function getMaximumOccurrenceOf ($tag) {
-    if (!preg_match(Field::TAG_RE, $tag)) {
-      throw new \InvalidArgumentException("Invalid field tag: {$tag}");
-    }
-    return array_reduce($this->getFields($tag),
-                        function ($maxOccurrence, Field $field) {
-                          if ($field->getOccurrence() > $maxOccurrence || $maxOccurrence === null) {
-                            return $field->getOccurrence();
-                          } else {
-                            return $maxOccurrence;
-                          }
-                        }, null);
-  }
-
-  /**
-   * Return TRUE if the record is empty.
-   *
-   * A record is empty if it contains no fields.
-   *
-   * @return boolean TRUE if record is empty
-   */
-  public function isEmpty () {
-    return empty($this->_fields);
-  }
-
-  /**
-   * Return TRUE if the record is valid.
-   *
-   * The base implementation checks that record is not empty and does not
-   * contain an empty field.
-   *
-   * @return boolean TRUE if the record is valid
-   */
-  public function isValid () {
-    return !$this->isEmpty() && !Helper::some($this->getFields(), function (Field $field) { return $field->isEmpty(); });
-  }
-
-  /**
-   * Return fields of the record.
-   *
-   * Optional argument $selector is the body of a regular expression. If set,
-   * this function returns only fields whose shorthand is matched by the
-   * regular expression.
-   *
-   * @see \HAB\Pica\Record\Field::match()
-   *
-   * @param  string $selector Body of regular expression
-   * @return array Fields
-   */
-  public function getFields ($selector = null) {
-    if ($selector === null) {
-      return $this->_fields;
-    } else {
-      return $this->select(Field::match($selector));
-    }
-  }
-
-  /**
-   * Return the first field that matches a selector.
-   *
-   * @see \HAB\Pica\Record\getFields()
-   *
-   * @param  string $selector Body of regular expression
-   * @return \HAB\Pica\Record\Field|null The first matching field or NULL if
-   *         no match
-   */
-  public function getFirstMatchingField ($selector) {
-    $fields = $this->getFields($selector);
-    if (empty($fields)) {
-      return null;
-    } else {
-      return reset($fields);
-    }
-  }
-
- /**
-   * Finalize the clone() operation.
-   *
-   * @return void
-   */
-  public function __clone () {
-    $this->_fields = Helper::mapClone($this->_fields);
-  }
-
-  /**
-   * Return a printable representation of the record.
-   *
-   * The printable representation of a record is the object hash prefixed by
-   * the class name.
-   *
-   * @return string Printable representation of the record
-   */
-  public function __toString () {
-    return get_class($this) . ':' . spl_object_hash($this);
-  }
-}
\ No newline at end of file
diff --git a/src/php/HAB/Pica/Record/Subfield.php b/src/php/HAB/Pica/Record/Subfield.php
deleted file mode 100644
index 1d2889e..0000000
--- a/src/php/HAB/Pica/Record/Subfield.php
+++ /dev/null
@@ -1,141 +0,0 @@
-<?php
-
-/**
- * The Subfield class file.
- *
- * This file is part of PicaRecord.
- *
- * PicaRecord is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PicaRecord is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-
-namespace HAB\Pica\Record;
-
-/**
- * The Pica+ subfield.
- *
- * A subfield is a cons of a alphanumeric character and a non-empty value.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-class Subfield {
-
-  /**
-   * Return TRUE if argument is a valid subfield code.
-   *
-   * @param  mixed $arg Variable to check
-   * @return boolean TRUE if argument is a valid subfield code
-   */
-  public static function isValidSubfieldCode ($arg) {
-    return (bool)preg_match('/^[a-z0-9]$/Di', $arg);
-  }
-
-  /**
-   * Return a new subfield based on its array representation.
-   *
-   * The array representation of a subfield is an associative array with the
-   * keys `code' and `value', holding the subfield code and value.
-   *
-   * @throws \InvalidArgumentException Missing code or value index
-   * @param  array $subfield Array representation of a subfield
-   * @return \HAB\Pica\Record\Subfield New subfield
-   */
-  public static function factory (array $subfield) {
-    if (!array_key_exists('code', $subfield)) {
-      throw new \InvalidArgumentException("Missing 'code' index in subfield array");
-    }
-    if (!array_key_exists('value', $subfield)) {
-      throw new \InvalidArgumentException("Missing 'value' index in subfield array");
-    }
-    return new Subfield($subfield['code'], $subfield['value']);
-  }
-
-  ///
-
-  /**
-   * The subfield code.
-   *
-   * @var string
-   */
-  protected $_code;
-
-  /**
-   * The subfield value.
-   *
-   * @var string Value
-   */
-  protected $_value;
-
-  /**
-   * Constructor.
-   *
-   * @throws \InvalidArgumentException Invalid subfield code
-   * @param  string $code Subfield code
-   * @param  string $value Subfield value
-   * @return void
-   */
-  public function __construct ($code, $value) {
-    if (!self::isValidSubfieldCode($code)) {
-      throw new \InvalidArgumentException("Invalid subfield code: {$code}");
-    }
-    $this->_code = $code;
-    $this->setValue($value);
-  }
-
-  /**
-   * Set the subfield value.
-   *
-   * @param  string $value Subfield value
-   * @return void
-   */
-  public function setValue ($value) {
-    $this->_value = $value;
-  }
-
-  /**
-   * Return the subfield value.
-   *
-   * @return string Subfield value
-   */
-  public function getValue () {
-    return $this->_value;
-  }
-
-  /**
-   * Return the subfield code.
-   *
-   * @return string Subfield code
-   */
-  public function getCode () {
-    return $this->_code;
-  }
-
-  /**
-   * Return printable representation of the subfield.
-   *
-   * The printable representation of a subfield is its value.
-   *
-   * @return string Subfield value
-   */
-  public function __toString () {
-    return $this->getValue();
-  }
-}
\ No newline at end of file
diff --git a/src/php/HAB/Pica/Record/TitleRecord.php b/src/php/HAB/Pica/Record/TitleRecord.php
deleted file mode 100644
index 6dd7fe5..0000000
--- a/src/php/HAB/Pica/Record/TitleRecord.php
+++ /dev/null
@@ -1,208 +0,0 @@
-<?php
-
-/**
- * The TitleRecord class file.
- *
- * This file is part of PicaRecord.
- *
- * PicaRecord is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PicaRecord is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-
-namespace HAB\Pica\Record;
-
-/**
- * A Pica+ title record.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-class TitleRecord extends NestedRecord {
-
-  /**
-   * Append a field to the title record.
-   *
-   * @see \HAB\Pica\Record\Record::append()
-   *
-   * You can only directly add fields with a level of 0.
-   *
-   * @throws \InvalidArgumentException Field level invalid
-   * @throws \InvalidArgumentException Field already in record
-   * @param  \HAB\Pica\Record\Field $field Field to append
-   * @return void
-   */
-  public function append (\HAB\Pica\Record\Field $field) {
-    if ($field->getLevel() !== 0) {
-      throw new \InvalidArgumentException("Invalid field level: {$field->getLevel()}");
-    }
-    parent::append($field);
-  }
-
-  /**
-   * Set the record's fields.
-   *
-   * @todo   Relocate to \HAB\Pica\Record\Record::factory(), maybe
-   *
-   * @param  array $fields Field
-   * @return void
-   */
-  public function setFields (array $fields) {
-    $this->_fields = array();
-    $this->_records = array();
-    $prevLevel = null;
-    foreach ($fields as $field) {
-      $level = $field->getLevel();
-      if ($level === 0) {
-        $this->append($field);
-      } else {
-        if ($level === 1 && $prevLevel !== 1) {
-          $localRecord = new LocalRecord(array($field));
-          $this->addLocalRecord($localRecord);
-        } else {
-          $records = $this->getLocalRecords();
-          $localRecord = end($records);
-          if ($level === 1) {
-            $localRecord->append($field);
-          } else {
-            $copyRecord = $localRecord->getCopyRecordByItemNumber($field->getOccurrence());
-            if ($copyRecord) {
-              $copyRecord->append($field);
-            } else {
-              $localRecord->addCopyRecord(new CopyRecord(array($field)));
-            }
-          }
-        }
-      }
-      $prevLevel = $level;
-    }
-  }
-
-  /**
-   * Add a local record.
-   *
-   * @throws \InvalidArgumentException Record already contains the local record
-   * @param  \HAB\Pica\Record\LocalRecord $record Local record
-   * @return void
-   */
-  public function addLocalRecord (\HAB\Pica\Record\LocalRecord $record) {
-    $this->addRecord($record);
-    $record->setTitleRecord($this);
-  }
-
-  /**
-   * Remove a local record.
-   *
-   * @throws \HAB\Pica\Record\Exception Record does not contain the local record
-   * @param  \HAB\Pica\Record\LocalRecord $record Local record to remove
-   * @return void
-   */
-  public function removeLocalRecord (\HAB\Pica\Record\LocalRecord $record) {
-    $this->removeRecord($record);
-    $record->unsetTitleRecord();
-  }
-
-  /**
-   * Return array of all local records.
-   *
-   * @return array Local records
-   */
-  public function getLocalRecords () {
-    return $this->_records;
-  }
-
-  /**
-   * Return a local record identified by its ILN.
-   *
-   * @param  integer $iln Intenal library number
-   * @return \HAB\Pica\Record\LocalRecord|null The local record or NULL if none exists
-   */
-  public function getLocalRecordByILN ($iln) {
-    foreach ($this->getLocalRecords() as $localRecord) {
-      if ($localRecord->getILN() == $iln) {
-        return $localRecord;
-      }
-    }
-    return null;
-  }
-
-  /**
-   * Return the Pica production number (record identifier).
-   *
-   * @return string|null Pica production number or NULL if none exists
-   */
-  public function getPPN () {
-    $ppnField = $this->getFirstMatchingField('003@/00');
-    if ($ppnField) {
-      $ppnSubfield = $ppnField->getNthSubfield('0', 0);
-      if ($ppnSubfield) {
-        return $ppnSubfield->getValue();
-      }
-    }
-    return null;
-  }
-
-  /**
-   * Set the Pica production number.
-   *
-   * Create a field 003@/00 if necessary.
-   *
-   * @param  string $ppn Pica production number
-   * @return void
-   */
-  public function setPPN ($ppn) {
-    $ppnField = $this->getFirstMatchingField('003@/00');
-    if ($ppnField) {
-      $ppnSubfield = $ppnField->getNthSubfield('0', 0);
-      if ($ppnSubfield) {
-        $ppnSubfield->setValue($ppn);
-      } else {
-        $ppnField->append(new Subfield('0', $ppn));
-      }
-    } else {
-      $this->append(new Field('003@', 0, array(new Subfield('0', $ppn))));
-    }
-  }
-
-  /**
-   * Return true if title record contains the local record.
-   *
-   * @param  \HAB\Pica\Record\LocalRecord $record Local record
-   * @return boolean
-   */
-  public function containsLocalRecord (\HAB\Pica\Record\LocalRecord $record) {
-      return $this->containsRecord($record);
-  }
-
-  /**
-   * Compare two local records.
-   *
-   * @see \HAB\Pica\Record\NestedRecord::compareRecords()
-   *
-   * Local records are compared by their ILN.
-   *
-   * @param  \HAB\Pica\Record\Record $a First record
-   * @param  \HAB\Pica\Record\Record $b Second record
-   * @return Comparism value
-   */
-  protected function compareRecords (\HAB\Pica\Record\Record $a, \HAB\Pica\Record\Record $b) {
-    return $a->getILN() - $b->getILN();
-  }
-
-}
\ No newline at end of file
diff --git a/src/tests/.empty b/src/tests/.empty
deleted file mode 100644
index e69de29..0000000
diff --git a/src/tests/functional-tests/.empty b/src/tests/functional-tests/.empty
deleted file mode 100644
index e69de29..0000000
diff --git a/src/tests/integration-tests/.empty b/src/tests/integration-tests/.empty
deleted file mode 100644
index e69de29..0000000
diff --git a/src/tests/unit-tests/.empty b/src/tests/unit-tests/.empty
deleted file mode 100644
index e69de29..0000000
diff --git a/src/tests/unit-tests/bin/.empty b/src/tests/unit-tests/bin/.empty
deleted file mode 100644
index e69de29..0000000
diff --git a/src/tests/unit-tests/bootstrap.php b/src/tests/unit-tests/bootstrap.php
deleted file mode 100644
index f13e5c8..0000000
--- a/src/tests/unit-tests/bootstrap.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-// =========================================================================
-//
-// tests/bootstrap.php
-//		A helping hand for running our unit tests
-//
-// Author	Stuart Herbert
-//		(stuart@stuartherbert.com)
-//
-// Copyright	(c) 2011 Stuart Herbert
-//		Released under the New BSD license
-//
-// =========================================================================
-
-// step 1: create the APP_TOPDIR constant that all components require
-define('APP_TOPDIR', realpath(__DIR__ . '/../../php'));
-define('APP_LIBDIR', realpath(__DIR__ . '/../../../vendor/php'));
-define('APP_TESTDIR', realpath(__DIR__ . '/php'));
-
-// step 2: find the autoloader, and install it
-require_once(APP_LIBDIR . '/psr0.autoloader.php');
-
-// step 3: add the additional paths to the include path
-psr0_autoloader_searchFirst(APP_LIBDIR);
-psr0_autoloader_searchFirst(APP_TESTDIR);
-psr0_autoloader_searchFirst(APP_TOPDIR);
-
-// step 4: enable ContractLib if it is available
-if (class_exists('Phix_Project\ContractLib\Contract'))
-{
-        \Phix_Project\ContractLib\Contract::EnforceWrappedContracts();
-}
-
-// step 5: Set error level to include E_STRICT
-\error_reporting(\E_ALL | \E_STRICT);
\ No newline at end of file
diff --git a/src/tests/unit-tests/php/.empty b/src/tests/unit-tests/php/.empty
deleted file mode 100644
index e69de29..0000000
diff --git a/src/tests/unit-tests/php/HAB/Pica/Record/AuthorityRecordTest.php b/src/tests/unit-tests/php/HAB/Pica/Record/AuthorityRecordTest.php
deleted file mode 100644
index c93c153..0000000
--- a/src/tests/unit-tests/php/HAB/Pica/Record/AuthorityRecordTest.php
+++ /dev/null
@@ -1,147 +0,0 @@
-<?php
-
-/**
- * Unit test for the AuthorityRecord class.
- *
- * This file is part of PicaRecord.
- *
- * PicaRecord is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PicaRecord is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-
-namespace HAB\Pica\Record;
-
-class AuthorityRecordTest extends \PHPUnit_FrameWork_TestCase {
-
-  ///
-
-  public function testConstructor () {
-    return new AuthorityRecord();
-  }
-
-  /**
-   * @depends testConstructor
-   */
-  public function testIsEmpty (AuthorityRecord $r) {
-    $this->assertTrue($r->isEmpty());
-  }
-
-  /**
-   * @depends testConstructor
-   */
-  public function testAppend (AuthorityRecord $r) {
-    $r->append(new Field('000@', 0, array(new Subfield('0', 'valid'))));
-    $this->assertFalse($r->isEmpty());
-  }
-
-  /**
-   * @depends testConstructor
-   */
-  public function testGetPPN (AuthorityRecord $r) {
-    $this->assertNull($r->getPPN());
-    $r->append(new Field('003@', 0, array(new Subfield('0', 'valid'))));
-    $this->assertEquals('valid', $r->getPPN());
-  }
-
-  /**
-   * @depends testConstructor
-   */
-  public function testDelete (AuthorityRecord $r) {
-    $this->assertFalse($r->isEmpty());
-    $r->delete(Field::match('..../..'));
-    $this->assertTrue($r->isEmpty());
-  }
-
-  ///
-
-  public function testSetPPN () {
-    $r = new AuthorityRecord();
-    $this->assertNull($r->getPPN());
-    $r->setPPN('something');
-    $this->assertEquals('something', $r->getPPN());
-    $r->setPPN('else');
-    $this->assertEquals('else', $r->getPPN());
-    $this->assertEquals(1, count($r->getFields('003@/00')));
-  }
-
-  public function testClone () {
-    $r = new AuthorityRecord();
-    $f = new Field('003@', 0);
-    $r->append($f);
-    $c = clone($r);
-    $this->assertNotSame($r, $c);
-    $fields = $c->getFields();
-    $this->assertNotSame($f, reset($fields));
-  }
-
-  public function testIsInvalidEmptyField () {
-    $r = new AuthorityRecord(array(new Field('003@', 0)));
-    $this->assertFalse($r->isValid());
-  }
-
-  public function testIsInvalidMissingPPN () {
-    $r = new AuthorityRecord(array(new Field('002@', 0, array(new Subfield('0', 'T')))));
-    $this->assertFalse($r->isValid());
-  }
-
-  public function testIsInvalidMissingType () {
-    $r = new AuthorityRecord(array(new Field('003@', 0, array(new Subfield('0', 'something')))));
-    $this->assertFalse($r->isValid());
-  }
-
-  public function testIsInvalidWrongType () {
-    $r = new AuthorityRecord(array(new Field('002@', 0, array(new Subfield('0', 'A')))));
-    $this->assertFalse($r->isValid());
-  }
-
-  public function testIsValid () {
-    $r = new AuthorityRecord(array(new Field('002@', 0, array(new Subfield('0', 'T'))),
-                                   new Field('003@', 0, array(new Subfield('0', 'valid')))));
-    $this->assertTrue($r->isValid());
-  }
-
-  public function testSort () {
-    $r = new AuthorityRecord(array(new Field('003@', 99, array(new Subfield('0', 'valid'))),
-                                   new Field('003@', 0, array(new Subfield('0', 'valid')))));
-    $r->sort();
-    $fields = $r->getFields('003@');
-    $this->assertEquals('003@/00', reset($fields)->getShorthand());
-    $this->assertEquals('003@/99', end($fields)->getShorthand());
-  }
-
-  ///
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testAppendThrowsExceptionOnDuplicateField () {
-    $r = new AuthorityRecord();
-    $f = new Field('003@', 0);
-    $r->append($f);
-    $r->append($f);
-  }
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testAppendThrowsExceptionOnInvalidLevel () {
-    $r = new AuthorityRecord();
-    $r->append(new Field('101@', 0));
-  }
-
-}
\ No newline at end of file
diff --git a/src/tests/unit-tests/php/HAB/Pica/Record/CopyRecordTest.php b/src/tests/unit-tests/php/HAB/Pica/Record/CopyRecordTest.php
deleted file mode 100644
index 86cf24a..0000000
--- a/src/tests/unit-tests/php/HAB/Pica/Record/CopyRecordTest.php
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-
-/**
- * Unit test for the CopyRecord class.
- *
- * This file is part of PicaRecord.
- *
- * PicaRecord is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PicaRecord is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-
-namespace HAB\Pica\Record;
-
-class CopyRecordTest extends \PHPUnit_FrameWork_TestCase {
-
-  public function testGetEPN () {
-    $r = new CopyRecord();
-    $this->assertNull($r->getEPN());
-    $r->append(new Field('203@', 0, array(new Subfield('0', 'something'))));
-    $this->assertEquals('something', $r->getEPN());
-  }
-
-  public function testSetEPN () {
-    $r = new CopyRecord(array(new Field('203@', 0, array(new Subfield('0', 'something')))));
-    $this->assertEquals('something', $r->getEPN());
-    $r->setEPN('epn');
-    $this->assertEquals('epn', $r->getEPN());
-  }
-
-  public function testLocalRecordReference () {
-    $l = new LocalRecord();
-    $c = new CopyRecord();
-    $this->assertNull($c->getLocalRecord());
-    $l->addCopyRecord($c);
-    $this->assertSame($l, $c->getLocalRecord());
-    $c->unsetLocalRecord();
-    $this->assertNull($c->getLocalRecord());
-    $this->assertFalse($l->containsCopyRecord($c));
-  }
-
-  ///
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testAppendThrowsExceptionOnInvalidFieldLevel () {
-    $r = new CopyRecord();
-    $r->append(new Field('003@', 0));
-  }
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testAppendThrowsExceptionOnNumberMismatch () {
-    $r = new CopyRecord();
-    $r->append(new Field('201@', 0));
-    $r->append(new Field('202@', 1));
-  }
-
-}
diff --git a/src/tests/unit-tests/php/HAB/Pica/Record/FieldTest.php b/src/tests/unit-tests/php/HAB/Pica/Record/FieldTest.php
deleted file mode 100644
index fa3bad8..0000000
--- a/src/tests/unit-tests/php/HAB/Pica/Record/FieldTest.php
+++ /dev/null
@@ -1,225 +0,0 @@
-<?php
-
-/**
- * Unit test for the Field class.
- *
- * This file is part of PicaRecord.
- *
- * PicaRecord is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PicaRecord is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-
-namespace HAB\Pica\Record;
-
-class FieldTest extends \PHPUnit_FrameWork_TestCase {
-
-  public function testValidFieldOccurrenceCastNull () {
-    $this->assertTrue(Field::isValidFieldOccurrence(null));
-  }
-
-  public function testValidFieldOccurrenceCastString () {
-    $this->assertTrue(Field::isValidFieldOccurrence('10'));
-  }
-
-  public function testInvalidFieldOccurrenceCastString () {
-    $this->assertFalse(Field::isValidFieldOccurrence("10\n"));
-  }
-
-  public function testInvalidFieldOccurrenceLowerBound () {
-    $this->assertFalse(Field::isValidFieldOccurrence(-1));
-  }
-
-  public function testInvalidFieldOccurrenceUpperBound () {
-    $this->assertFalse(Field::isValidFieldOccurrence(100));
-  }
-
-  public function testInvalidFieldTagTrailingNewline () {
-    $this->assertFalse(Field::isValidFieldTag("003@\n"));
-  }
-
-  public function testMatch () {
-    $this->assertTrue(call_user_func(Field::match('003./..'), new Field('003@', 0)));
-    $this->assertTrue(call_user_func(Field::match('003./..'), new Field('003Z', 0)));
-    $this->assertTrue(call_user_func(Field::match('003./..'), new Field('003Z', 99)));
-  }
-
-  public function testFactory () {
-    $f = Field::factory(array('tag' => '003@', 'occurrence' => 10, 'subfields' => array()));
-    $this->assertInstanceOf('HAB\Pica\Record\Field', $f);
-    $this->assertEquals('003@/10', $f->getShorthand());
-  }
-
-  ///
-
-  public function testConstructor () {
-    return new Field('003@', 0);
-  }
-
-  /**
-   * @depends testConstructor
-   */
-  public function testIsEmpty (Field $f) {
-    $this->assertTrue($f->isEmpty());
-    $s = new Subfield('a', 'valid');
-    $f->addSubfield($s);
-    $this->assertFalse($f->isEmpty());
-    $f->removeSubfield($s);
-  }
-
-  /**
-   * @depends testConstructor
-   */
-  public function testGetTag (Field $f) {
-    $this->assertEquals('003@', $f->getTag());
-  }
-
-  /**
-   * @depends testConstructor
-   */
-  public function testGetOccurrence (Field $f) {
-    $this->assertEquals(0, $f->getOccurrence());
-  }
-
-  /**
-   * @depends testConstructor
-   */
-  public function testGetLevel (Field $f) {
-    $this->assertEquals(0, $f->getLevel());
-  }
-
-  /**
-   * @depends testConstructor
-   */
-  public function testGetShorthand (Field $f) {
-    $this->assertEquals('003@/00', $f->getShorthand());
-  }
-
-  ///
-
-  public function testSetSubfields () {
-    $f = new Field('003@', 0);
-    $f->setSubfields(array(new Subfield('a', 'first a'),
-                           new Subfield('d', 'first d'),
-                           new Subfield('a', 'second a')));
-    $this->assertFalse($f->isEmpty());
-    return $f;
-  }
-
-  public function testGetNthSubfield () {
-    $f = new Field('003@', 0, array(new Subfield('a', 'first a'),
-                                    new Subfield('b', 'first b'),
-                                    new Subfield('a', 'second a')));
-    $s = $f->getNthSubfield('a', 0);
-    $this->assertInstanceOf('HAB\Pica\Record\Subfield', $s);
-    $this->assertEquals('first a', $s->getValue());
-    $s = $f->getNthSubfield('a', 1);
-    $this->assertInstanceOf('HAB\Pica\Record\Subfield', $s);
-    $this->assertEquals('second a', $s->getValue());   
-    $s = $f->getNthSubfield('a', 2);
-    $this->assertNull($s);
-  }
-
-  /**
-   * @depends testSetSubfields
-   */
-  public function testGetSubfields (Field $f) {
-    $this->assertEquals(3, count($f->getSubfields()));
-    return $f;
-  }
-
-  /**
-   * @depends testGetSubfields
-   */
-  public function testGetSubfieldsWithCode (Field $f) {
-    $this->assertEquals(5, count($f->getSubfields('x', 'x', 'x', 'x', 'x')));
-    $s = $f->getSubfields('d');
-    $this->assertEquals('first d', reset($s));
-    $s = $f->getSubfields('a');
-    $this->assertEquals('first a', reset($s));
-    $s = $f->getSubfields('a', 'd', 'a');
-    $this->assertEquals('second a', end($s));;
-    return $f;
-  }
-
-  ///
-
-  public function testClone () {
-    $f = new Field('003@', 0);
-    $s = new Subfield('a', 'valid');
-    $f->addSubfield($s);
-    $c = clone($f);
-    $this->assertNotSame($c, $f);
-    $this->assertNotSame($s, $c->getNthSubfield('a', 0));
-  }
-
-  ///
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testFactoryThrowsExceptionOnMissingTagIndex () {
-    Field::factory(array('occurrence' => 10, 'subfields' => array()));
-  }
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testFactoryThrowsExceptionOnMissingOccurrenceIndex () {
-    Field::factory(array('tag' => '003@', 'subfields' => array()));
-  }
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testFactoryThrowsExceptionOnMissingSubfieldIndex () {
-    Field::factory(array('tag' => '003@', 'occurrence' => 10));
-  }
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testContructorThrowsExceptionOnInvalidTag () {
-    new Field('invalid', 0);
-  }
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testConstructorThrowsExceptionOnInvalidOccurrence () {
-    new Field('003@', 1000);
-  }
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testAddSubfieldThrowsExceptionOnDuplicateSubfield () {
-    $f = new Field('003@', 0);
-    $s = new Subfield('a', 'valid');
-    $f->addSubfield($s);
-    $f->addSubfield($s);
-  }
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testRemoveSubfieldThrowsExceptionOnNonExistentField () {
-    $f = new Field('003@', 0);
-    $s = new Subfield('a', 'valid');
-    $f->removeSubfield($s);
-  }
-}
\ No newline at end of file
diff --git a/src/tests/unit-tests/php/HAB/Pica/Record/LocalRecordTest.php b/src/tests/unit-tests/php/HAB/Pica/Record/LocalRecordTest.php
deleted file mode 100644
index 3e86447..0000000
--- a/src/tests/unit-tests/php/HAB/Pica/Record/LocalRecordTest.php
+++ /dev/null
@@ -1,170 +0,0 @@
-<?php
-
-/**
- * Unit test for the LocalRecord class.
- *
- * This file is part of PicaRecord.
- *
- * PicaRecord is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PicaRecord is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-
-namespace HAB\Pica\Record;
-
-class LocalRecordTest extends \PHPUnit_FrameWork_TestCase {
-
-  public function testAddCopyRecord () {
-    $r = new LocalRecord();
-    $r->addCopyRecord(new CopyRecord());
-  }
-
-  public function testClone () {
-    $r = new LocalRecord();
-    $c = new CopyRecord(array(new Field('200@', 11)));
-    $r->addCopyRecord($c);
-    $clone = clone($r);
-    $this->assertNotSame($clone, $r);
-    $this->assertNotSame($c, $clone->getCopyRecordByItemNumber(11));
-  }
-
-  public function testRemoveCopyRecord () {
-    $r = new LocalRecord();
-    $r->addCopyRecord(new CopyRecord(array(new Field('200@', 11))));
-    $this->assertEquals(1, count($r->getCopyRecords()));
-    $r->removeCopyRecord($r->getCopyRecordByItemNumber(11));
-  }
-
-  public function testSort () {
-    $r = new LocalRecord();
-    $a = new CopyRecord(array(new Field('200@', 11)));
-    $b = new CopyRecord(array(new Field('200@', 99)));
-    $r->addCopyRecord($b);
-    $r->addCopyRecord($a);
-    $c = $r->getCopyRecords();
-    $this->assertSame($b, reset($c));
-    $r->sort();
-    $c = $r->getCopyRecords();
-    $this->assertSame($a, reset($c));
-  }
-
-  public function testGetILN () {
-    $r = new LocalRecord();
-    $this->assertNull($r->getILN());
-    $r->append(new Field('101@', 0, array(new Subfield('a', '50'))));
-    $this->assertEquals(50, $r->getILN());
-  }
-
-  public function testSelectPropagatesDown () {
-    $r = new LocalRecord();
-    $c = new CopyRecord(array(new Field('200@', 11)));
-    $r->addCopyRecord($c);
-    $this->assertEquals(1, count($r->select(Field::match('200@/11'))));
-  }
-
-  public function testDeletePropagatesDown () {
-    $r = new LocalRecord();
-    $c = new CopyRecord(array(new Field('200@', 11)));
-    $r->addCopyRecord($c);
-    $this->assertFalse($c->isEmpty());
-    $r->delete(Field::match('200@/11'));
-    $this->assertTrue($c->isEmpty());
-  }
-
-  public function testIsEmpty () {
-    $r = new LocalRecord();
-    $this->assertTrue($r->isEmpty());
-    $r->addCopyRecord(new CopyRecord());
-    $this->assertTrue($r->isEmpty());
-    $r->addCopyRecord(new CopyRecord(array(new Field('200@', 11))));
-    $this->assertFalse($r->isEmpty());
-  }
-
-  public function testGetMaximumOccurrenceOf () {
-    $r = new LocalRecord();
-    $this->assertNull($r->getMaximumOccurrenceOf('144Z'));
-    $r->append(new Field('144Z', 0));
-    $this->assertEquals(0, $r->getMaximumOccurrenceOf('144Z'));
-    $r->append(new Field('144Z', 10));
-    $this->assertEquals(10, $r->getMaximumOccurrenceOf('144Z'));
-  }
-
-  public function testContainsCopyRecord () {
-    $r = new LocalRecord();
-    $c = new CopyRecord();
-    $this->assertFalse($r->containsCopyRecord($c));
-    $r->addCopyRecord($c);
-    $this->assertTrue($r->containsCopyRecord($c));
-  }
-
-  public function testTitleRecordReference () {
-    $t = new TitleRecord();
-    $l = new LocalRecord();
-    $this->assertNull($l->getTitleRecord());
-    $t->addLocalRecord($l);
-    $this->assertSame($t, $l->getTitleRecord());
-    $l->unsetTitleRecord();
-    $this->assertNull($l->getTitleRecord());
-    $this->assertFalse($t->containsLocalRecord($l));
-  }
-
-  ///
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testAddCopyRecordThrowsExceptionOnItemNumberCollision () {
-    $r = new LocalRecord();
-    $r->addCopyRecord(new CopyRecord(array(new Field('200@', 11))));
-    $r->addCopyRecord(new CopyRecord(array(new Field('200@', 11))));
-  }
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testAddCopyRecordThrowsExceptionOnDuplicateCopyRecord () {
-    $r = new LocalRecord();
-    $c = new CopyRecord();
-    $r->addCopyRecord($c);
-    $r->addCopyRecord($c);
-  }
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testRemoveCopyRecordThrowsExceptionOnCopyRecordNotContainedInRecord () {
-    $r = new LocalRecord();
-    $c = new CopyRecord();
-    $r->removeCopyRecord($c);
-  }
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testAppendThrowsExceptionOnInvalidLevel () {
-    $r = new LocalRecord();
-    $r->append(new Field('003@', 0));
-  }
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testGetMaximumOccurrenceOfThrowsExceptionOnInvalidFieldTag () {
-    $r = new LocalRecord();
-    $r->getMaximumOccurrenceOf('@@@@');
-  }
-}
\ No newline at end of file
diff --git a/src/tests/unit-tests/php/HAB/Pica/Record/RecordTest.php b/src/tests/unit-tests/php/HAB/Pica/Record/RecordTest.php
deleted file mode 100644
index 4c7d038..0000000
--- a/src/tests/unit-tests/php/HAB/Pica/Record/RecordTest.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-
-/**
- * Unit test for the AuthorityRecord class.
- *
- * This file is part of PicaRecord.
- *
- * PicaRecord is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PicaRecord is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-
-namespace HAB\Pica\Record;
-
-class RecordTest extends \PHPUnit_FrameWork_TestCase {
-
-  public function testFactoryCreatesAuthorityRecord () {
-    $record = Record::factory(array('fields' => array(
-                                      array('tag' => '002@',
-                                            'occurrence' => 0,
-                                            'subfields' => array(
-                                              array('code' => '0',
-                                                    'value' => 'T'))))));
-    $this->assertInstanceOf('HAB\Pica\Record\AuthorityRecord', $record);
-  }
-
-  public function testGetFirstMatchingField () {
-    $record = new AuthorityRecord(array(new Field('001@', 0),
-                                        new Field('001@', 1)));
-    $this->assertNull($record->getFirstMatchingField('002@/00'));
-    $this->assertInstanceOf('HAB\Pica\Record\Field', $record->getFirstMatchingField('001@'));
-  }
-
-  ///
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testFactoryThrowsExceptionOnMissingFieldsIndex () {
-    Record::factory(array());
-  }
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testFactoryThrowsExceptionOnMissingTypeField () {
-    $record = Record::factory(array('fields' => array()));
-  }
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testFactoryThrowsExceptionOnMissingTypeSubfield () {
-    $record = Record::factory(array('fields' => array(
-                                      array('tag' => '002@',
-                                            'occurrence' => 0,
-                                            'subfields' => array()))));
-  }
-}
\ No newline at end of file
diff --git a/src/tests/unit-tests/php/HAB/Pica/Record/SubfieldTest.php b/src/tests/unit-tests/php/HAB/Pica/Record/SubfieldTest.php
deleted file mode 100644
index 461eb09..0000000
--- a/src/tests/unit-tests/php/HAB/Pica/Record/SubfieldTest.php
+++ /dev/null
@@ -1,96 +0,0 @@
-<?php
-
-/**
- * Unit test for the Subfield class.
- *
- * This file is part of PicaRecord.
- *
- * PicaRecord is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PicaRecord is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-
-namespace HAB\Pica\Record;
-
-class SubfieldTest extends \PHPUnit_FrameWork_TestCase {
-
-  public function testValidSubfieldCodeZero () {
-    $this->assertTrue(Subfield::isValidSubfieldCode('0'));
-  }
-
-  public function testInvalidSubfieldCodeTrailingNewline () {
-    $this->assertFalse(Subfield::isValidSubfieldCode("a\n"));
-  }
-
-  public function testFactory () {
-    $s = Subfield::factory(array('code' => 'a', 'value' => 'valid'));
-    $this->assertInstanceOf('HAB\Pica\Record\Subfield', $s);
-    $this->assertEquals('a', $s->getCode());
-    $this->assertEquals('valid', $s->getValue());
-  }
-
-  ///
-
-  public function testConstructor () {
-    return new Subfield('a', 'valid');
-  }
-
-  /**
-   * @depends testConstructor
-   */
-  public function testGetValue (Subfield $s) {
-    $this->assertEquals('valid', $s->getValue());
-  }
-
-  /**
-   * @depends testConstructor
-   */
-  public function testGetCode (Subfield $s) {
-    $this->assertEquals('a', $s->getCode());
-  }
-
-  /**
-   * @depends testConstructor
-   */
-  public function testToString (Subfield $s) {
-    $this->assertEquals('valid', (string)$s);
-  }
-
-  ///
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testConstructorThrowsExceptionOnInvalidCode () {
-    new Subfield(null, 'valid');
-  }
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testFactoryThrowsExceptionOnMissingCodeIndex () {
-    Subfield::factory(array('value' => 'valid'));
-  }
-
-  /**
-   * @expectedException \InvalidArgumentException
-   */
-  public function testFactoryThrowsExceptionOnMissingValueIndex () {
-    Subfield::factory(array('code' => 'a'));
-  }
-
-}
\ No newline at end of file
diff --git a/src/tests/unit-tests/php/HAB/Pica/Record/TitleRecordTest.php b/src/tests/unit-tests/php/HAB/Pica/Record/TitleRecordTest.php
deleted file mode 100644
index 10159be..0000000
--- a/src/tests/unit-tests/php/HAB/Pica/Record/TitleRecordTest.php
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-
-/**
- * Unit test for the TitleRecord class.
- *
- * This file is part of PicaRecord.
- *
- * PicaRecord is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PicaRecord is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
- *
- * @package   PicaRecord
- * @author    David Maus <maus@hab.de>
- * @copyright Copyright (c) 2012 by Herzog August Bibliothek Wolfenbüttel
- * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
- */
-
-namespace HAB\Pica\Record;
-
-class TitleRecordTest extends \PHPUnit_FrameWork_TestCase {
-
-  public function testAppend () {
-    $r = new TitleRecord();
-    $r->append(new Field('003@', 0));
-    $this->assertEquals(1, count($r->getFields()));
-  }
-
-  public function testAddLocalRecord () {
-    $r = new TitleRecord();
-    $l = new LocalRecord();
-    $this->assertEquals(0, count($r->getLocalRecords()));
-    $r->addLocalRecord($l);
-    $this->assertEquals(1, count($r->getLocalRecords()));
-    return $r;
-  }
-
-  /**
-   * @depends testAddLocalRecord
-   */
-  public function testRemoveLocalRecord (TitleRecord $r) {
-    $l = $r->getLocalRecords();
-    $l = end($l);
-    $r->removeLocalRecord($l);
-    $this->assertEquals(0, count($r->getLocalRecords()));
-  }
-
-  public function testSetFields () {
-    $r = new TitleRecord();
-    $r->append(new Field('003@', 0));
-    $this->assertEquals(1, count($r->getFields()));
-  }
-
-  public function testSetFieldsCreatesNewLocalRecord () {
-    $r = new TitleRecord();
-    $fields = array();
-    $fields []= new Field('003@', 0);
-    $r->setFields($fields);
-    $this->assertEquals(0, count($r->getLocalRecords()));
-    $fields []= new Field('101@', 0, array(new Subfield('a', 1)));
-    $r->setFields($fields);
-    $this->assertEquals(1, count($r->getLocalRecords()));
-    $fields [] = new Field('200@', 0);
-    $r->setFields($fields);
-    $this->assertEquals(1, count($r->getLocalRecords()));
-    $fields []= new Field('101@', 0, array(new Subfield('a', 2)));
-    $r->setFields($fields);
-    $this->assertEquals(2, count($r->getLocalRecords()));
-  }
-
-  public function testGetLocalRecordByILN () {
-    $r = new TitleRecord();
-    $r->addLocalRecord(new LocalRecord(array(new Field('101@', 0, array(new Subfield('a', 11))))));
-    $r->addLocalRecord(new LocalRecord(array(new Field('101@', 0, array(new Subfield('a', 99))))));
-    $l = $r->getLocalRecordByILN(11);
-    $this->assertInstanceOf('HAB\\Pica\\Record\\LocalRecord', $l);
-    $this->assertEquals(11, $l->getILN());
-    $l = $r->getLocalRecordByILN(33);
-    $this->assertNull($l);
-  }
-
-  public function testGetPPN () {
-    $r = new TitleRecord();
-    $this->assertNull($r->getPPN());
-    $r->append(new Field('003@', 0, array(new Subfield('0', 'something'))));
-    $this->assertEquals('something', $r->getPPN());
-  }
-
-  public function testSetPPN () {
-    $r = new TitleRecord();
-    $r->setPPN('something');
-    $this->assertEquals(1, count($r->getFields('003@/00')));
-    $r->setPPN('something else');
-    $this->assertEquals('something else', $r->getPPN());
-  }
-
-  public function testContainsLocalRecord () {
-      $r = new TitleRecord();
-      $l = new LocalRecord();
-      $this->assertFalse($r->containsLocalRecord($l));
-      $r->addLocalRecord($l);
-      $this->assertTrue($r->containsLocalRecord($l));
-  }
-}
diff --git a/src/tests/unit-tests/www/.empty b/src/tests/unit-tests/www/.empty
deleted file mode 100644
index e69de29..0000000
diff --git a/src/www/.empty b/src/www/.empty
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000..ba07c30
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * This file is part of PicaRecord.
+ *
+ * OAI-PMH-Server is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * OAI-PMH-Server is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with OAI-PMH-Server.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.txt GNU General Public License v3
+ */
+
+require_once realpath(__DIR__ . '/../vendor/autoload.php');
+
+define('PHPUNIT_FIXTURES', realpath(__DIR__ . '/fixtures'));
+
+$loader = new Composer\Autoload\ClassLoader();
+$loader->add('HAB', realpath(__DIR__ . '/../src'));
+$loader->add('HAB', realpath(__DIR__ . '/src'));
+$loader->register();
diff --git a/tests/src/HAB/Pica/Record/AuthorityRecordTest.php b/tests/src/HAB/Pica/Record/AuthorityRecordTest.php
new file mode 100644
index 0000000..4d1f732
--- /dev/null
+++ b/tests/src/HAB/Pica/Record/AuthorityRecordTest.php
@@ -0,0 +1,153 @@
+<?php
+
+/**
+ * Unit test for the AuthorityRecord class.
+ *
+ * This file is part of PicaRecord.
+ *
+ * PicaRecord is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PicaRecord is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   PicaRecord
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2012, 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
+ */
+
+namespace HAB\Pica\Record;
+
+use PHPUnit_FrameWork_TestCase;
+
+class AuthorityRecordTest extends PHPUnit_FrameWork_TestCase 
+{
+
+    ///
+
+    public function testIsEmpty () 
+    {
+        $r = new AuthorityRecord();
+        $this->assertTrue($r->isEmpty());
+    }
+
+    public function testAppend () 
+    {
+        $r = new AuthorityRecord();
+        $r->append(new Field('000@', 0, array(new Subfield('0', 'valid'))));
+        $this->assertFalse($r->isEmpty());
+    }
+
+    public function testGetPPN () 
+    {
+        $r = new AuthorityRecord();
+        $this->assertNull($r->getPPN());
+        $r->append(new Field('003@', 0, array(new Subfield('0', 'valid'))));
+        $this->assertEquals('valid', $r->getPPN());
+    }
+
+    public function testDelete () 
+    {
+        $r = new AuthorityRecord();
+        $r->append(new Field('003@', 0, array(new Subfield('0', 'valid'))));
+        $this->assertFalse($r->isEmpty());
+        $r->delete(Field::match('..../..'));
+        $this->assertTrue($r->isEmpty());
+    }
+
+    ///
+
+    public function testSetPPN () 
+    {
+        $r = new AuthorityRecord();
+        $this->assertNull($r->getPPN());
+        $r->setPPN('something');
+        $this->assertEquals('something', $r->getPPN());
+        $r->setPPN('else');
+        $this->assertEquals('else', $r->getPPN());
+        $this->assertEquals(1, count($r->getFields('003@/00')));
+    }
+
+    public function testClone () 
+    {
+        $r = new AuthorityRecord();
+        $f = new Field('003@', 0);
+        $r->append($f);
+        $c = clone($r);
+        $this->assertNotSame($r, $c);
+        $fields = $c->getFields();
+        $this->assertNotSame($f, reset($fields));
+    }
+
+    public function testIsInvalidEmptyField () 
+    {
+        $r = new AuthorityRecord(array(new Field('003@', 0)));
+        $this->assertFalse($r->isValid());
+    }
+
+    public function testIsInvalidMissingPPN () 
+    {
+        $r = new AuthorityRecord(array(new Field('002@', 0, array(new Subfield('0', 'T')))));
+        $this->assertFalse($r->isValid());
+    }
+
+    public function testIsInvalidMissingType () 
+    {
+        $r = new AuthorityRecord(array(new Field('003@', 0, array(new Subfield('0', 'something')))));
+        $this->assertFalse($r->isValid());
+    }
+
+    public function testIsInvalidWrongType () 
+    {
+        $r = new AuthorityRecord(array(new Field('002@', 0, array(new Subfield('0', 'A')))));
+        $this->assertFalse($r->isValid());
+    }
+
+    public function testIsValid () 
+    {
+        $r = new AuthorityRecord(array(new Field('002@', 0, array(new Subfield('0', 'T'))),
+                                       new Field('003@', 0, array(new Subfield('0', 'valid')))));
+        $this->assertTrue($r->isValid());
+    }
+
+    public function testSort () 
+    {
+        $r = new AuthorityRecord(array(new Field('003@', 99, array(new Subfield('0', 'valid'))),
+                                       new Field('003@', 0, array(new Subfield('0', 'valid')))));
+        $r->sort();
+        $fields = $r->getFields('003@');
+        $this->assertEquals('003@/00', reset($fields)->getShorthand());
+        $this->assertEquals('003@/99', end($fields)->getShorthand());
+    }
+
+    ///
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testAppendThrowsExceptionOnDuplicateField () 
+    {
+        $r = new AuthorityRecord();
+        $f = new Field('003@', 0);
+        $r->append($f);
+        $r->append($f);
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testAppendThrowsExceptionOnInvalidLevel () 
+    {
+        $r = new AuthorityRecord();
+        $r->append(new Field('101@', 0));
+    }
+
+}
\ No newline at end of file
diff --git a/tests/src/HAB/Pica/Record/CopyRecordTest.php b/tests/src/HAB/Pica/Record/CopyRecordTest.php
new file mode 100644
index 0000000..7f2ad1e
--- /dev/null
+++ b/tests/src/HAB/Pica/Record/CopyRecordTest.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * Unit test for the CopyRecord class.
+ *
+ * This file is part of PicaRecord.
+ *
+ * PicaRecord is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PicaRecord is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   PicaRecord
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2012, 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
+ */
+
+namespace HAB\Pica\Record;
+
+use PHPUnit_FrameWork_TestCase;
+
+class CopyRecordTest extends PHPUnit_FrameWork_TestCase {
+
+    public function testGetEPN ()
+    {
+        $r = new CopyRecord();
+        $this->assertNull($r->getEPN());
+        $r->append(new Field('203@', 0, array(new Subfield('0', 'something'))));
+        $this->assertEquals('something', $r->getEPN());
+    }
+
+    public function testSetEPN ()
+    {
+        $r = new CopyRecord(array(new Field('203@', 0, array(new Subfield('0', 'something')))));
+        $this->assertEquals('something', $r->getEPN());
+        $r->setEPN('epn');
+        $this->assertEquals('epn', $r->getEPN());
+    }
+
+    public function testLocalRecordReference ()
+    {
+        $l = new LocalRecord();
+        $c = new CopyRecord();
+        $this->assertNull($c->getLocalRecord());
+        $l->addCopyRecord($c);
+        $this->assertSame($l, $c->getLocalRecord());
+        $c->unsetLocalRecord();
+        $this->assertNull($c->getLocalRecord());
+        $this->assertFalse($l->containsCopyRecord($c));
+    }
+
+    ///
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testAppendThrowsExceptionOnInvalidFieldLevel ()
+    {
+        $r = new CopyRecord();
+        $r->append(new Field('003@', 0));
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testAppendThrowsExceptionOnNumberMismatch ()
+    {
+        $r = new CopyRecord();
+        $r->append(new Field('201@', 0));
+        $r->append(new Field('202@', 1));
+    }
+
+}
diff --git a/tests/src/HAB/Pica/Record/FieldTest.php b/tests/src/HAB/Pica/Record/FieldTest.php
new file mode 100644
index 0000000..ab9fde8
--- /dev/null
+++ b/tests/src/HAB/Pica/Record/FieldTest.php
@@ -0,0 +1,231 @@
+<?php
+
+/**
+ * Unit test for the Field class.
+ *
+ * This file is part of PicaRecord.
+ *
+ * PicaRecord is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PicaRecord is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   PicaRecord
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2012, 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
+ */
+
+namespace HAB\Pica\Record;
+
+use PHPUnit_FrameWork_TestCase;
+
+class FieldTest extends PHPUnit_FrameWork_TestCase
+{
+
+    public function testValidFieldOccurrenceCastNull () {
+        $this->assertTrue(Field::isValidFieldOccurrence(null));
+    }
+
+    public function testValidFieldOccurrenceCastString () {
+        $this->assertTrue(Field::isValidFieldOccurrence('10'));
+    }
+
+    public function testInvalidFieldOccurrenceCastString () {
+        $this->assertFalse(Field::isValidFieldOccurrence("10\n"));
+    }
+
+    public function testInvalidFieldOccurrenceLowerBound () {
+        $this->assertFalse(Field::isValidFieldOccurrence(-1));
+    }
+
+    public function testInvalidFieldOccurrenceUpperBound () {
+        $this->assertFalse(Field::isValidFieldOccurrence(100));
+    }
+
+    public function testInvalidFieldTagTrailingNewline () {
+        $this->assertFalse(Field::isValidFieldTag("003@\n"));
+    }
+
+    public function testMatch () {
+        $this->assertTrue(call_user_func(Field::match('003./..'), new Field('003@', 0)));
+        $this->assertTrue(call_user_func(Field::match('003./..'), new Field('003Z', 0)));
+        $this->assertTrue(call_user_func(Field::match('003./..'), new Field('003Z', 99)));
+    }
+
+    public function testFactory () {
+        $f = Field::factory(array('tag' => '003@', 'occurrence' => 10, 'subfields' => array()));
+        $this->assertInstanceOf('HAB\Pica\Record\Field', $f);
+        $this->assertEquals('003@/10', $f->getShorthand());
+    }
+
+    ///
+
+    public function testIsEmpty ()
+    {
+        $f = new Field('003@', 0);
+        $this->assertTrue($f->isEmpty());
+        $s = new Subfield('a', 'valid');
+        $f->addSubfield($s);
+        $this->assertFalse($f->isEmpty());
+        $f->removeSubfield($s);
+    }
+
+    public function testGetTag ()
+    {
+        $f = new Field('003@', 0);
+        $this->assertEquals('003@', $f->getTag());
+    }
+
+    public function testGetOccurrence ()
+    {
+        $f = new Field('003@', 0);
+        $this->assertEquals(0, $f->getOccurrence());
+    }
+
+    public function testGetLevel ()
+    {
+        $f = new Field('003@', 0);
+        $this->assertEquals(0, $f->getLevel());
+    }
+
+    public function testGetShorthand ()
+    {
+        $f = new Field('003@', 0);
+        $this->assertEquals('003@/00', $f->getShorthand());
+    }
+
+    ///
+
+    public function testSetSubfields ()
+    {
+        $f = new Field('003@', 0);
+        $f->setSubfields(array(new Subfield('a', 'first a'),
+                               new Subfield('d', 'first d'),
+                               new Subfield('a', 'second a')));
+        $this->assertFalse($f->isEmpty());
+        return $f;
+    }
+
+    public function testGetNthSubfield ()
+    {
+        $f = new Field('003@', 0, array(new Subfield('a', 'first a'),
+                                        new Subfield('b', 'first b'),
+                                        new Subfield('a', 'second a')));
+        $s = $f->getNthSubfield('a', 0);
+        $this->assertInstanceOf('HAB\Pica\Record\Subfield', $s);
+        $this->assertEquals('first a', $s->getValue());
+        $s = $f->getNthSubfield('a', 1);
+        $this->assertInstanceOf('HAB\Pica\Record\Subfield', $s);
+        $this->assertEquals('second a', $s->getValue());
+        $s = $f->getNthSubfield('a', 2);
+        $this->assertNull($s);
+    }
+
+    /**
+     * @depends testSetSubfields
+     */
+    public function testGetSubfields (Field $f)
+    {
+        $this->assertEquals(3, count($f->getSubfields()));
+        return $f;
+    }
+
+    /**
+     * @depends testGetSubfields
+     */
+    public function testGetSubfieldsWithCode (Field $f)
+    {
+        $this->assertEquals(5, count($f->getSubfields('x', 'x', 'x', 'x', 'x')));
+        $s = $f->getSubfields('d');
+        $this->assertEquals('first d', reset($s));
+        $s = $f->getSubfields('a');
+        $this->assertEquals('first a', reset($s));
+        $s = $f->getSubfields('a', 'd', 'a');
+        $this->assertEquals('second a', end($s));;
+        return $f;
+    }
+
+    ///
+
+    public function testClone ()
+    {
+        $f = new Field('003@', 0);
+        $s = new Subfield('a', 'valid');
+        $f->addSubfield($s);
+        $c = clone($f);
+        $this->assertNotSame($c, $f);
+        $this->assertNotSame($s, $c->getNthSubfield('a', 0));
+    }
+
+    ///
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testFactoryThrowsExceptionOnMissingTagIndex ()
+    {
+        Field::factory(array('occurrence' => 10, 'subfields' => array()));
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testFactoryThrowsExceptionOnMissingOccurrenceIndex ()
+    {
+        Field::factory(array('tag' => '003@', 'subfields' => array()));
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testFactoryThrowsExceptionOnMissingSubfieldIndex ()
+    {
+        Field::factory(array('tag' => '003@', 'occurrence' => 10));
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testContructorThrowsExceptionOnInvalidTag ()
+    {
+        new Field('invalid', 0);
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testConstructorThrowsExceptionOnInvalidOccurrence ()
+    {
+        new Field('003@', 1000);
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testAddSubfieldThrowsExceptionOnDuplicateSubfield ()
+    {
+        $f = new Field('003@', 0);
+        $s = new Subfield('a', 'valid');
+        $f->addSubfield($s);
+        $f->addSubfield($s);
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testRemoveSubfieldThrowsExceptionOnNonExistentField ()
+    {
+        $f = new Field('003@', 0);
+        $s = new Subfield('a', 'valid');
+        $f->removeSubfield($s);
+    }
+}
\ No newline at end of file
diff --git a/tests/src/HAB/Pica/Record/LocalRecordTest.php b/tests/src/HAB/Pica/Record/LocalRecordTest.php
new file mode 100644
index 0000000..c26199a
--- /dev/null
+++ b/tests/src/HAB/Pica/Record/LocalRecordTest.php
@@ -0,0 +1,183 @@
+<?php
+
+/**
+ * Unit test for the LocalRecord class.
+ *
+ * This file is part of PicaRecord.
+ *
+ * PicaRecord is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PicaRecord is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   PicaRecord
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2012, 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
+ */
+
+namespace HAB\Pica\Record;
+
+use PHPUnit_FrameWork_TestCase;
+
+class LocalRecordTest extends PHPUnit_FrameWork_TestCase
+{
+
+    public function testClone ()
+    {
+        $r = new LocalRecord();
+        $c = new CopyRecord(array(new Field('200@', 11)));
+        $r->addCopyRecord($c);
+        $clone = clone($r);
+        $this->assertNotSame($clone, $r);
+        $this->assertNotSame($c, $clone->getCopyRecordByItemNumber(11));
+    }
+
+    public function testRemoveCopyRecord ()
+    {
+        $r = new LocalRecord();
+        $r->addCopyRecord(new CopyRecord(array(new Field('200@', 11))));
+        $this->assertEquals(1, count($r->getCopyRecords()));
+        $r->removeCopyRecord($r->getCopyRecordByItemNumber(11));
+    }
+
+    public function testSort ()
+    {
+        $r = new LocalRecord();
+        $a = new CopyRecord(array(new Field('200@', 11)));
+        $b = new CopyRecord(array(new Field('200@', 99)));
+        $r->addCopyRecord($b);
+        $r->addCopyRecord($a);
+        $c = $r->getCopyRecords();
+        $this->assertSame($b, reset($c));
+        $r->sort();
+        $c = $r->getCopyRecords();
+        $this->assertSame($a, reset($c));
+    }
+
+    public function testGetILN ()
+    {
+        $r = new LocalRecord();
+        $this->assertNull($r->getILN());
+        $r->append(new Field('101@', 0, array(new Subfield('a', '50'))));
+        $this->assertEquals(50, $r->getILN());
+    }
+
+    public function testSelectPropagatesDown ()
+    {
+        $r = new LocalRecord();
+        $c = new CopyRecord(array(new Field('200@', 11)));
+        $r->addCopyRecord($c);
+        $this->assertEquals(1, count($r->select(Field::match('200@/11'))));
+    }
+
+    public function testDeletePropagatesDown ()
+    {
+        $r = new LocalRecord();
+        $c = new CopyRecord(array(new Field('200@', 11)));
+        $r->addCopyRecord($c);
+        $this->assertFalse($c->isEmpty());
+        $r->delete(Field::match('200@/11'));
+        $this->assertTrue($c->isEmpty());
+    }
+
+    public function testIsEmpty ()
+    {
+        $r = new LocalRecord();
+        $this->assertTrue($r->isEmpty());
+        $r->addCopyRecord(new CopyRecord());
+        $this->assertTrue($r->isEmpty());
+        $r->addCopyRecord(new CopyRecord(array(new Field('200@', 11))));
+        $this->assertFalse($r->isEmpty());
+    }
+
+    public function testGetMaximumOccurrenceOf ()
+    {
+        $r = new LocalRecord();
+        $this->assertNull($r->getMaximumOccurrenceOf('144Z'));
+        $r->append(new Field('144Z', 0));
+        $this->assertEquals(0, $r->getMaximumOccurrenceOf('144Z'));
+        $r->append(new Field('144Z', 10));
+        $this->assertEquals(10, $r->getMaximumOccurrenceOf('144Z'));
+    }
+
+    public function testContainsCopyRecord ()
+    {
+        $r = new LocalRecord();
+        $c = new CopyRecord();
+        $this->assertFalse($r->containsCopyRecord($c));
+        $r->addCopyRecord($c);
+        $this->assertTrue($r->containsCopyRecord($c));
+    }
+
+    public function testTitleRecordReference ()
+    {
+        $t = new TitleRecord();
+        $l = new LocalRecord();
+        $this->assertNull($l->getTitleRecord());
+        $t->addLocalRecord($l);
+        $this->assertSame($t, $l->getTitleRecord());
+        $l->unsetTitleRecord();
+        $this->assertNull($l->getTitleRecord());
+        $this->assertFalse($t->containsLocalRecord($l));
+    }
+
+    ///
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testAddCopyRecordThrowsExceptionOnItemNumberCollision ()
+    {
+        $r = new LocalRecord();
+        $r->addCopyRecord(new CopyRecord(array(new Field('200@', 11))));
+        $r->addCopyRecord(new CopyRecord(array(new Field('200@', 11))));
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testAddCopyRecordThrowsExceptionOnDuplicateCopyRecord ()
+    {
+        $r = new LocalRecord();
+        $c = new CopyRecord();
+        $r->addCopyRecord($c);
+        $r->addCopyRecord($c);
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testRemoveCopyRecordThrowsExceptionOnCopyRecordNotContainedInRecord ()
+    {
+        $r = new LocalRecord();
+        $c = new CopyRecord();
+        $r->removeCopyRecord($c);
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testAppendThrowsExceptionOnInvalidLevel ()
+    {
+        $r = new LocalRecord();
+        $r->append(new Field('003@', 0));
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testGetMaximumOccurrenceOfThrowsExceptionOnInvalidFieldTag ()
+    {
+        $r = new LocalRecord();
+        $r->getMaximumOccurrenceOf('@@@@');
+    }
+}
\ No newline at end of file
diff --git a/tests/src/HAB/Pica/Record/RecordTest.php b/tests/src/HAB/Pica/Record/RecordTest.php
new file mode 100644
index 0000000..a1d0db3
--- /dev/null
+++ b/tests/src/HAB/Pica/Record/RecordTest.php
@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * Unit test for the AuthorityRecord class.
+ *
+ * This file is part of PicaRecord.
+ *
+ * PicaRecord is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PicaRecord is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   PicaRecord
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2012, 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
+ */
+
+namespace HAB\Pica\Record;
+
+use PHPUnit_FrameWork_TestCase;
+
+class RecordTest extends PHPUnit_FrameWork_TestCase
+{
+
+    public function testFactoryCreatesAuthorityRecord ()
+    {
+        $record = Record::factory(array('fields' => array(
+                                            array('tag' => '002@',
+                                                  'occurrence' => 0,
+                                                  'subfields' => array(
+                                                      array('code' => '0',
+                                                            'value' => 'T'))))));
+        $this->assertInstanceOf('HAB\Pica\Record\AuthorityRecord', $record);
+    }
+
+    public function testGetFirstMatchingField ()
+    {
+        $record = new AuthorityRecord(array(new Field('001@', 0),
+                                            new Field('001@', 1)));
+        $this->assertNull($record->getFirstMatchingField('002@/00'));
+        $this->assertInstanceOf('HAB\Pica\Record\Field', $record->getFirstMatchingField('001@'));
+    }
+
+    ///
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testFactoryThrowsExceptionOnMissingFieldsIndex ()
+    {
+        Record::factory(array());
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testFactoryThrowsExceptionOnMissingTypeField ()
+    {
+        $record = Record::factory(array('fields' => array()));
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testFactoryThrowsExceptionOnMissingTypeSubfield ()
+    {
+        $record = Record::factory(array('fields' => array(
+                                            array('tag' => '002@',
+                                                  'occurrence' => 0,
+                                                  'subfields' => array()))));
+    }
+}
\ No newline at end of file
diff --git a/tests/src/HAB/Pica/Record/SubfieldTest.php b/tests/src/HAB/Pica/Record/SubfieldTest.php
new file mode 100644
index 0000000..d398b39
--- /dev/null
+++ b/tests/src/HAB/Pica/Record/SubfieldTest.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * Unit test for the Subfield class.
+ *
+ * This file is part of PicaRecord.
+ *
+ * PicaRecord is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PicaRecord is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   PicaRecord
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2012, 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
+ */
+
+namespace HAB\Pica\Record;
+
+use PHPUnit_FrameWork_TestCase;
+
+class SubfieldTest extends PHPUnit_FrameWork_TestCase
+{
+
+    public function testValidSubfieldCodeZero ()
+    {
+        $this->assertTrue(Subfield::isValidSubfieldCode('0'));
+    }
+
+    public function testInvalidSubfieldCodeTrailingNewline ()
+    {
+        $this->assertFalse(Subfield::isValidSubfieldCode("a\n"));
+    }
+
+    public function testFactory ()
+    {
+        $s = Subfield::factory(array('code' => 'a', 'value' => 'valid'));
+        $this->assertInstanceOf('HAB\Pica\Record\Subfield', $s);
+        $this->assertEquals('a', $s->getCode());
+        $this->assertEquals('valid', $s->getValue());
+    }
+
+    ///
+
+    public function testGetValue ()
+    {
+        $s = new Subfield('a', 'valid');
+        $this->assertEquals('valid', $s->getValue());
+    }
+
+    public function testGetCode ()
+    {
+        $s = new Subfield('a', 'valid');
+        $this->assertEquals('a', $s->getCode());
+    }
+
+    public function testToString () {
+        $s = new Subfield('a', 'valid');
+        $this->assertEquals('valid', (string)$s);
+    }
+
+    ///
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testConstructorThrowsExceptionOnInvalidCode ()
+    {
+        new Subfield(null, 'valid');
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testFactoryThrowsExceptionOnMissingCodeIndex ()
+    {
+        Subfield::factory(array('value' => 'valid'));
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testFactoryThrowsExceptionOnMissingValueIndex ()
+    {
+        Subfield::factory(array('code' => 'a'));
+    }
+
+}
\ No newline at end of file
diff --git a/tests/src/HAB/Pica/Record/TitleRecordTest.php b/tests/src/HAB/Pica/Record/TitleRecordTest.php
new file mode 100644
index 0000000..9753e11
--- /dev/null
+++ b/tests/src/HAB/Pica/Record/TitleRecordTest.php
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * Unit test for the TitleRecord class.
+ *
+ * This file is part of PicaRecord.
+ *
+ * PicaRecord is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PicaRecord is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PicaRecord.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package   PicaRecord
+ * @author    David Maus <maus@hab.de>
+ * @copyright Copyright (c) 2012, 2013 by Herzog August Bibliothek Wolfenbüttel
+ * @license   http://www.gnu.org/licenses/gpl.html GNU General Public License v3
+ */
+
+namespace HAB\Pica\Record;
+
+use PHPUnit_FrameWork_TestCase;
+
+class TitleRecordTest extends PHPUnit_FrameWork_TestCase
+{
+
+    public function testAppend ()
+    {
+        $r = new TitleRecord();
+        $r->append(new Field('003@', 0));
+        $this->assertEquals(1, count($r->getFields()));
+    }
+
+    public function testAddLocalRecord ()
+    {
+        $r = new TitleRecord();
+        $l = new LocalRecord();
+        $this->assertEquals(0, count($r->getLocalRecords()));
+        $r->addLocalRecord($l);
+        $this->assertEquals(1, count($r->getLocalRecords()));
+        return $r;
+    }
+
+    /**
+     * @depends testAddLocalRecord
+     */
+    public function testRemoveLocalRecord (TitleRecord $r)
+    {
+        $l = $r->getLocalRecords();
+        $l = end($l);
+        $r->removeLocalRecord($l);
+        $this->assertEquals(0, count($r->getLocalRecords()));
+    }
+
+    public function testSetFields ()
+    {
+        $r = new TitleRecord();
+        $r->append(new Field('003@', 0));
+        $this->assertEquals(1, count($r->getFields()));
+    }
+
+    public function testSetFieldsCreatesNewLocalRecord ()
+    {
+        $r = new TitleRecord();
+        $fields = array();
+        $fields []= new Field('003@', 0);
+        $r->setFields($fields);
+        $this->assertEquals(0, count($r->getLocalRecords()));
+        $fields []= new Field('101@', 0, array(new Subfield('a', 1)));
+        $r->setFields($fields);
+        $this->assertEquals(1, count($r->getLocalRecords()));
+        $fields [] = new Field('200@', 0);
+        $r->setFields($fields);
+        $this->assertEquals(1, count($r->getLocalRecords()));
+        $fields []= new Field('101@', 0, array(new Subfield('a', 2)));
+        $r->setFields($fields);
+        $this->assertEquals(2, count($r->getLocalRecords()));
+    }
+
+    public function testGetLocalRecordByILN ()
+    {
+        $r = new TitleRecord();
+        $r->addLocalRecord(new LocalRecord(array(new Field('101@', 0, array(new Subfield('a', 11))))));
+        $r->addLocalRecord(new LocalRecord(array(new Field('101@', 0, array(new Subfield('a', 99))))));
+        $l = $r->getLocalRecordByILN(11);
+        $this->assertInstanceOf('HAB\\Pica\\Record\\LocalRecord', $l);
+        $this->assertEquals(11, $l->getILN());
+        $l = $r->getLocalRecordByILN(33);
+        $this->assertNull($l);
+    }
+
+    public function testGetPPN ()
+    {
+        $r = new TitleRecord();
+        $this->assertNull($r->getPPN());
+        $r->append(new Field('003@', 0, array(new Subfield('0', 'something'))));
+        $this->assertEquals('something', $r->getPPN());
+    }
+
+    public function testSetPPN ()
+    {
+        $r = new TitleRecord();
+        $r->setPPN('something');
+        $this->assertEquals(1, count($r->getFields('003@/00')));
+        $r->setPPN('something else');
+        $this->assertEquals('something else', $r->getPPN());
+    }
+
+    public function testContainsLocalRecord ()
+    {
+        $r = new TitleRecord();
+        $l = new LocalRecord();
+        $this->assertFalse($r->containsLocalRecord($l));
+        $r->addLocalRecord($l);
+        $this->assertTrue($r->containsLocalRecord($l));
+    }
+}
-- 
GitLab