diff --git a/.gitignore b/.gitignore
index be84e34068bb9670f050015236af1a43541532b7..ac1c8a85dfcc5e9c20388b08d394924bbb65fa17 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 ac0b76556e639b12349b9b2ef786c2b4d2cc2bee..0000000000000000000000000000000000000000
--- 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 90aa3bc92e323653b9ef5c828f07c987d84dd494..0000000000000000000000000000000000000000
--- 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 7385faba6784ea2621503dbab703463619c7b9a5..0000000000000000000000000000000000000000
--- 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 7db58a021fe0dcc74706941ce1a5ad992b1b2adb..0000000000000000000000000000000000000000
--- 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 0000000000000000000000000000000000000000..1e4072b39fd5d2e10fb71ac60349c53695e067ab
--- /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 8e8d98e886d77cb8dc138c812d29fe6fb2d9dae3..0000000000000000000000000000000000000000
--- 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 0000000000000000000000000000000000000000..16d058a388443b5bc7d54508aa1c8321d6314561
--- /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 864f7f7890b1ac293fb5de9ae2bf1c90824915d5..0000000000000000000000000000000000000000
--- 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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/src/HAB/Pica/Record/AuthorityRecord.php b/src/HAB/Pica/Record/AuthorityRecord.php
new file mode 100644
index 0000000000000000000000000000000000000000..a62980c12cf13ae2f897c11e18ea2985d7a75724
--- /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 0000000000000000000000000000000000000000..d90bbd18fe585c8ea6676ffbcb7d8ee3be2bcb5f
--- /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 0000000000000000000000000000000000000000..4e3f4b9e8ed5edd26dff52cee8dc4cffcb486d1b
--- /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 0000000000000000000000000000000000000000..4b9dcc3298f4b95ffc2303d4f4380af047989e78
--- /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 0000000000000000000000000000000000000000..ca94ecd479033d01a42ea19f9fb1c2fa870cfdde
--- /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 0000000000000000000000000000000000000000..d7ea6b2db2e3650de75fad77a8577e183ad0a922
--- /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 0000000000000000000000000000000000000000..bd44ed878bc210844226eaa3fd50421e573d81c3
--- /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 0000000000000000000000000000000000000000..9edc7a0f85c8f842d225f86fa30af143def5ee13
--- /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 0000000000000000000000000000000000000000..94660b1ff0d546aaedad8f7bac9c5d6679e02bce
--- /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 4074f38738f3847625a1a7e521e392a80fde11cd..0000000000000000000000000000000000000000
--- 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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/src/data/.empty b/src/data/.empty
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/src/docs/.empty b/src/docs/.empty
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/src/php/.empty b/src/php/.empty
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/src/php/HAB/Pica/Record/AuthorityRecord.php b/src/php/HAB/Pica/Record/AuthorityRecord.php
deleted file mode 100644
index 89e94e1e80b335da58202fcbafac3e019fcf18d9..0000000000000000000000000000000000000000
--- 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 b3ac3fd9dda1fed45716cef25854455e6d13d079..0000000000000000000000000000000000000000
--- 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 eb0037fb5725fbbcfd7dcf2afc2592289fd9e276..0000000000000000000000000000000000000000
--- 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 c1349b56d823fa0378056f62b5bfc0620d111a29..0000000000000000000000000000000000000000
--- 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 48c4fed6f6479eef5ecb64f56208f8712d39a911..0000000000000000000000000000000000000000
--- 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 5a8f1a5529f3552d72fee1ebb6880602ee3bc55b..0000000000000000000000000000000000000000
--- 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 ab64f66d411519434384b02a6b8e1d245ec54e2c..0000000000000000000000000000000000000000
--- 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 1d2889e41eaa4543070a051ed9f57a2f3e1df621..0000000000000000000000000000000000000000
--- 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 6dd7fe58b0c05f39f94143544bc6d48016658887..0000000000000000000000000000000000000000
--- 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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/src/tests/functional-tests/.empty b/src/tests/functional-tests/.empty
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/src/tests/integration-tests/.empty b/src/tests/integration-tests/.empty
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/src/tests/unit-tests/.empty b/src/tests/unit-tests/.empty
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/src/tests/unit-tests/bin/.empty b/src/tests/unit-tests/bin/.empty
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/src/tests/unit-tests/bootstrap.php b/src/tests/unit-tests/bootstrap.php
deleted file mode 100644
index f13e5c87315bd5b1ffd466c78b48faf0fc6d4326..0000000000000000000000000000000000000000
--- 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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
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 c93c1534ecfbd9b432129635398ae4410999479e..0000000000000000000000000000000000000000
--- 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 86cf24aed3122bff58fc7dc8b3632daafae00832..0000000000000000000000000000000000000000
--- 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 fa3bad8fb1c88e89f43e3b5cb818f25cb8587672..0000000000000000000000000000000000000000
--- 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 3e864474e93e8eff86e5b8a76bb96b766744137f..0000000000000000000000000000000000000000
--- 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 4c7d03842a8c7d7ecba725142ed52114a9300830..0000000000000000000000000000000000000000
--- 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 461eb09b2c91af84f257c0f42f695f491afb8e97..0000000000000000000000000000000000000000
--- 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 10159be24d02ca5028994c44e4c5481cd047cf0f..0000000000000000000000000000000000000000
--- 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 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/src/www/.empty b/src/www/.empty
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000000000000000000000000000000000000..ba07c30f3cfa27bd999d0d5bcdda38cd3ef0dcd7
--- /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 0000000000000000000000000000000000000000..4d1f732885913130069086a4871f2761007f59c3
--- /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 0000000000000000000000000000000000000000..7f2ad1e5180f59752cc278a0d58e32c4e21b48eb
--- /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 0000000000000000000000000000000000000000..ab9fde887251d09c3c34228ee5b6c498c3c1b9a9
--- /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 0000000000000000000000000000000000000000..c26199a3e661b8693fdd0a106f6a374aa4ba9fb2
--- /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 0000000000000000000000000000000000000000..a1d0db3833fd34773d165d872a998cd7407ce0b9
--- /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 0000000000000000000000000000000000000000..d398b391639a07bd4288f38190025d79569474a1
--- /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 0000000000000000000000000000000000000000..9753e11996a94f59186db8584335629468a6a22c
--- /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));
+    }
+}