PHPackages                             ottosmops/ocfl - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [File &amp; Storage](/categories/file-storage)
4. /
5. ottosmops/ocfl

ActiveLibrary[File &amp; Storage](/categories/file-storage)

ottosmops/ocfl
==============

PHP implementation of the Oxford Common File Layout (OCFL) v1.1 storage specification.

v1.0.0(3mo ago)018MITPHPPHP ^8.3CI passing

Since Apr 20Pushed 3mo agoCompare

[ Source](https://github.com/ottosmops/ocfl)[ Packagist](https://packagist.org/packages/ottosmops/ocfl)[ Docs](https://github.com/ottosmops/ocfl)[ RSS](/packages/ottosmops-ocfl/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (1)Dependencies (6)Versions (2)Used By (0)

ottosmops/ocfl
==============

[](#ottosmopsocfl)

[![Latest Version on Packagist](https://camo.githubusercontent.com/d2e99940bae3321e8829a651279eb3fe4cf212f31fc6d35d1e0f2c57bbf4725c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6f74746f736d6f70732f6f63666c2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ottosmops/ocfl)[![Total Downloads](https://camo.githubusercontent.com/0ea415b892ea876acd58d48050aca08c6a149e93a8c8b11977243efd8e14f6be/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6f74746f736d6f70732f6f63666c2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ottosmops/ocfl)[![Tests](https://github.com/ottosmops/ocfl/actions/workflows/tests.yml/badge.svg)](https://github.com/ottosmops/ocfl/actions/workflows/tests.yml)[![PHP Version Require](https://camo.githubusercontent.com/c4015f9b40fd7132c3cb8e5805bff1413b114450057b86f0569c055e4d4fa030/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f6f74746f736d6f70732f6f63666c2f7068703f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ottosmops/ocfl)[![License: MIT](https://camo.githubusercontent.com/1b01ef0024ba0866c115986b895301f657c1b21fc29f05c4844b7f2e8d89204d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e7376673f7374796c653d666c61742d737175617265)](LICENSE)

`ottosmops/ocfl` is a PHP library for working with the [Oxford Common File Layout (OCFL) v1.1](https://ocfl.io/1.1/spec/) — a storage specification for long-term digital preservation.

It reads, writes, and validates OCFL objects against the local filesystem or any [Flysystem](https://flysystem.thephpleague.com) v3 backend (S3, Azure, GCS, in-memory, …).

Features
--------

[](#features)

- **Domain API** — readonly value objects for `Inventory`, `Version`, `User`, `OcflObject`, `StorageRoot` — the whole spec modelled in types.
- **Read** — open an existing object, list versions, resolve logical paths to content paths (respecting forward-delta dedup), stream content, or check out an entire version to a directory.
- **Write** — create an object, commit new versions with content `addFile` / `addContents`, `renameFile`, `removeFile`. Forward-delta dedup and crash-safe staging (`.tmp-XXXX` → `rename`) handled automatically.
- **Storage layouts** — `0002-flat-direct-storage-layout` and `0004-hashed-n-tuple-storage-layout` out of the box, with a pluggable `StorageLayout` interface for custom extensions.
- **Validator** — rejects all 55 OCFL [bad-object fixtures](https://github.com/OCFL/fixtures)with the correct spec-referenced error codes, accepts all 12 good-object fixtures, and emits 13/13 warn-object advisories.
- **Pluggable storage** — `LocalFilesystem` by default; `FlysystemFilesystem`adapter for any `league/flysystem` v3 backend (S3, Azure, GCS, …).
- **CLI** — `ocfl validate|info|list` for quick inspection from the shell, with optional `--json` output for scripting.
- **Framework-agnostic** — zero required Composer runtime dependencies beyond the PHP standard library. Laravel / Symfony wrappers are easy to build on top.

Requirements
------------

[](#requirements)

- PHP 8.3 or later
- `ext-hash`, `ext-json`, `ext-mbstring`

Optional, for cloud storage:

- `league/flysystem` ^3.0 plus the adapter of your choice

Installation
------------

[](#installation)

```
composer require ottosmops/ocfl
```

Tests run against the official [OCFL fixtures](https://github.com/OCFL/fixtures). Clone with submodules:

```
git clone --recurse-submodules https://github.com/ottosmops/ocfl
# or, if already cloned:
git submodule update --init --recursive
```

Quick start
-----------

[](#quick-start)

### Creating an object

[](#creating-an-object)

```
use Ottosmops\Ocfl\OcflObject;

$object = OcflObject::create(
    path: '/path/to/storage/my-object',
    id:   'urn:example:my-object',
);

$object = $object->newVersion()
    ->addContents('README.md', "# Hello, OCFL\n")
    ->addFile('data/report.pdf', '/tmp/report.pdf')
    ->withMessage('Initial import')
    ->withUser('Alice', 'mailto:alice@example.com')
    ->commit();

$object->head();                              // "v1"
$object->logicalPaths('v1');                  // ['README.md', 'data/report.pdf']
$object->readContent('v1', 'README.md');      // "# Hello, OCFL\n"
```

### Committing a new version

[](#committing-a-new-version)

```
$object = OcflObject::open('/path/to/storage/my-object')
    ->newVersion()
    ->addContents('CHANGELOG.md', "## v2\n- Added changelog\n")
    ->renameFile('README.md', 'README.txt')
    ->withMessage('Docs update')
    ->withUser('Alice', 'mailto:alice@example.com')
    ->commit();

$object->head();                              // "v2"
$object->resolveContentPath('v2', 'README.txt');
// → "v1/content/README.md" (dedup: not re-stored in v2)
```

### Using a storage root with an id-to-path layout

[](#using-a-storage-root-with-an-id-to-path-layout)

```
use Ottosmops\Ocfl\Storage\StorageRoot;
use Ottosmops\Ocfl\Storage\HashedNTupleStorageLayout;

$root = StorageRoot::create(
    path:   '/path/to/storage',
    layout: new HashedNTupleStorageLayout(),
);

$root->createObject('urn:example:foo')
    ->newVersion()
    ->addContents('hello.txt', 'hi')
    ->commit();

// Later, in another process:
$root   = StorageRoot::open('/path/to/storage');
$object = $root->getObject('urn:example:foo');
$ids    = $root->listObjects();
```

### Checking out a version

[](#checking-out-a-version)

```
$object->checkout('/tmp/snapshot-v1', 'v1');
// Materialises the logical state of v1 into /tmp/snapshot-v1, verifying
// every content digest during the copy.
```

### Validating an object

[](#validating-an-object)

```
use Ottosmops\Ocfl\Validation\ErrorCode;

$report = OcflObject::open('/path/to/object')->validate();

$report->isValid();                           // bool — no errors
$report->hasWarnings();                       // bool
$report->hasError(ErrorCode::E040);           // bool
foreach ($report->errors() as $issue) {
    echo "[{$issue->code->value}] {$issue->message}\n";
}
```

Cloud storage via Flysystem
---------------------------

[](#cloud-storage-via-flysystem)

Any `league/flysystem` v3 filesystem can host an OCFL storage root.

```
use Aws\S3\S3Client;
use League\Flysystem\Filesystem as LeagueFilesystem;
use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
use Ottosmops\Ocfl\Filesystem\FlysystemFilesystem;
use Ottosmops\Ocfl\Storage\StorageRoot;
use Ottosmops\Ocfl\Storage\HashedNTupleStorageLayout;

$client  = new S3Client([...]);
$league  = new LeagueFilesystem(new AwsS3V3Adapter($client, 'my-bucket'));
$fs      = new FlysystemFilesystem($league);

$root = StorageRoot::create('/archive', new HashedNTupleStorageLayout(), $fs);

$root->createObject('urn:example:foo')
    ->newVersion()
    ->addContents('doc.txt', 'content')
    ->withUser('Alice', 'mailto:alice@example.com')
    ->commit();
```

Content digests are streamed, not buffered — large files never need to be loaded into memory just to hash them.

Command-line usage
------------------

[](#command-line-usage)

A small `ocfl` binary is shipped in `bin/` (Composer installs it into `vendor/bin/ocfl`).

```
# Validate an object; exit 0 if valid, 1 if not
vendor/bin/ocfl validate /path/to/object

# Print metadata
vendor/bin/ocfl info /path/to/object

# List all object ids below a storage root
vendor/bin/ocfl list /path/to/storage-root

# Create, commit, checkout
vendor/bin/ocfl create /path/to/object urn:example:foo --digest=sha512
vendor/bin/ocfl commit /path/to/object \
    --from=/path/to/staging-dir \
    --message='Initial import' \
    --user=Alice \
    --user-address=mailto:alice@example.com
vendor/bin/ocfl checkout /path/to/object /path/to/snapshot --version=v1

# Machine-readable output for any subcommand
vendor/bin/ocfl validate --json /path/to/object
```

`ocfl commit --from=` treats the source directory as the canonical logical state of the next version: every file below `` becomes a logical path, and any file in the previous version that no longer exists in `` is removed. Content dedup is automatic.

Exit codes: `0` success · `1` object invalid · `2` usage error · `3` runtime error. Colours are emitted by default; pipe through `| cat` to strip them.

Laravel integration
-------------------

[](#laravel-integration)

The core package is framework-agnostic. For a Laravel app, wire it into the container yourself — no second package needed:

```
// app/Providers/OcflServiceProvider.php
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Storage;
use League\Flysystem\Filesystem as LeagueFilesystem;
use Ottosmops\Ocfl\Filesystem\FlysystemFilesystem;
use Ottosmops\Ocfl\Filesystem\LocalFilesystem;
use Ottosmops\Ocfl\Storage\HashedNTupleStorageLayout;
use Ottosmops\Ocfl\Storage\StorageRoot;

final class OcflServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(StorageRoot::class, function () {
            $disk = Storage::disk(config('ocfl.disk', 'local'));

            // Laravel's Storage::disk() returns its own Filesystem wrapper;
            // grab the underlying Flysystem operator and adapt it.
            $fs = $disk->getDriver() instanceof LeagueFilesystem
                ? new FlysystemFilesystem($disk->getDriver())
                : new LocalFilesystem();

            return StorageRoot::open(
                path: config('ocfl.root', storage_path('ocfl')),
                fs:   $fs,
            );
        });
    }
}
```

Then anywhere in the app:

```
$root   = app(StorageRoot::class);
$object = $root->getObject('urn:example:foo');
```

Artisan wrappers for `validate` / `list` can shell out to `vendor/bin/ocfl`or call `Application::run()` directly.

Validation status
-----------------

[](#validation-status)

The `ObjectValidator` emits OCFL-spec error and warning codes that link directly to .

CategoryCoverageGood-object fixtures12 / 12 validate with zero errorsBad-object fixtures**55 / 55** rejected with the documented error codeWarn-object fixtures13 / 13 emit the documented advisoryImplemented error codes: `E001 E003 E007 E008 E010 E011 E013 E015 E017 E019 E023 E025 E033 E034 E036 E037 E038 E040 E041 E046 E048 E049 E050 E052 E053 E058 E060 E061 E063 E064 E066 E067 E070 E092 E093 E095 E096 E097 E099 E100 E101 E103 E107`.

Implemented warnings: `W001 W002 W004 W005 W007 W008 W009 W010 W011 W013`.

Development
-----------

[](#development)

```
composer install
composer check                        # pint + phpstan + pest
composer test                         # pest
composer test:coverage                # pest with coverage (needs xdebug or pcov)
composer analyse                      # phpstan level max
composer format                       # laravel pint
composer refactor                     # rector
```

CI runs on Ubuntu and macOS against PHP 8.3 and 8.4. Current test suite: **249 tests · 469 assertions · 94 % line coverage**; most remaining uncovered lines are host-level I/O failure paths (mkdir-refused, hash\_file-on-special-file, etc.) that can't be triggered deterministically in a unit test.

References
----------

[](#references)

- [OCFL 1.1 Specification](https://ocfl.io/1.1/spec/)
- [OCFL Implementation Notes](https://ocfl.io/1.1/implementation-notes/)
- [OCFL Validation Codes](https://ocfl.io/1.1/spec/validation-codes.html)
- [OCFL Community Extensions](https://ocfl.github.io/extensions/)
- [OCFL Fixtures](https://github.com/OCFL/fixtures) (used as test corpus)
- Related implementations: [ocfl-java](https://github.com/OCFL/ocfl-java), [ocflcore](https://github.com/inveniosoftware/ocflcore) (Python), [rocfl](https://github.com/pwinckles/rocfl) (Rust), [gocfl](https://github.com/ocfl-archive/gocfl) (Go).

License
-------

[](#license)

See [LICENSE](LICENSE).

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance82

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

95d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/76f31e7e6772db47a91388ed82840fae1fa57185bb82a64924bb3839697222c2?d=identicon)[ottosmops](/maintainers/ottosmops)

---

Top Contributors

[![ottosmops](https://avatars.githubusercontent.com/u/4144389?v=4)](https://github.com/ottosmops "ottosmops (23 commits)")

---

Tags

storageversioningrepositorydigital-preservationarchivingocflfixityoxford-common-file-layout

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/ottosmops-ocfl/health.svg)

```
[![Health](https://phpackages.com/badges/ottosmops-ocfl/health.svg)](https://phpackages.com/packages/ottosmops-ocfl)
```

###  Alternatives

[league/flysystem

File storage abstraction for PHP

13.6k679.9M2.6k](/packages/league-flysystem)[league/flysystem-aws-s3-v3

AWS S3 filesystem adapter for Flysystem.

1.7k285.7M1.1k](/packages/league-flysystem-aws-s3-v3)[google/cloud

Google Cloud Client Library

1.2k16.7M57](/packages/google-cloud)[sylius/resource-bundle

Resource component for Sylius.

23610.8M236](/packages/sylius-resource-bundle)[microsoft/azure-storage-common

This project provides a set of common code shared by Azure Storage Blob, Table, Queue and File PHP client libraries.

4318.0M6](/packages/microsoft-azure-storage-common)[league/flysystem-async-aws-s3

AsyncAws S3 filesystem adapter for Flysystem.

3012.1M44](/packages/league-flysystem-async-aws-s3)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
