1. Introduction

Modern applications generate and manage large amounts of files PDF invoices, reports, user uploads, images, exports, backups, and more. One of the most common architectural mistakes is tightly coupling business logic directly to the filesystem.

When application code contains functions such as file_put_contents(), fopen(), or hardcoded storage paths, migrating from local disk storage to cloud object storage becomes painful and expensive.

To solve this problem, we implemented a storage abstraction layer built on top of Flysystem that allows the application to seamlessly switch between local storage and Amazon S3-compatible storage without changing business logic.

In this article, we’ll explore the architecture behind the filesystem integration, how storage disks are registered, and how generated PDF documents are stored using a unified API.

2. The File Management Challenge

Consider a typical PDF generation workflow:

$pdfContent = $pdf->Output('', 'S');

file_put_contents('/var/www/files/proforma.pdf', $pdfContent);

At first glance this looks simple.

However, several issues quickly emerge:

  • Storage paths become hardcoded, any slight change in file structure requires changing in all places .
  • Migrating to S3 requires rewriting application code.
  • Permission handling becomes inconsistent.
  • Testing becomes difficult.
  • Different environments require different implementations.

As the application grows, storage concerns begin leaking into business logic.

Ideally, the file generation service should only care about one thing:

“Store this file.”

It should not care where the file is stored.

3. Introducing a Filesystem Abstraction Layer

To separate storage concerns from application logic, we built a wrapper around Flysystem.

The architecture looks like this:

Business code communicates only with the LMVC_FileSystem class.

The filesystem layer determines whether files should be stored locally or in cloud storages, many of the indexing challenges often associated with client-side rendered applications.

4. Why FlySystem?

Flysystem provides a unified filesystem API.

Regardless of the underlying storage implementation, operations remain identical:

$filesystem->write();

$filesystem->read();

$filesystem->delete();

$filesystem->fileExists();

This abstraction allows applications to switch storage providers without modifying business logic.

For example:

$filesystem->write('invoice.pdf', $pdfContent);

The same code works whether the file is stored on:

  • Local disk
  • Amazon S3
  • Google Cloud Storage (GCS)
  • Microsoft Azure Blob Storage
  • Cloudflare R2
  • Wasabi
  • DigitalOcean Spaces
  • Oracle Cloud Object Storage
  • IBM Cloud Object Storage
  • Ceph Object Gateway
  • OpenStack Swift

This portability makes Flysystem an excellent foundation for scalable applications. of content and SEO data, while Next.js is responsible for presenting that data to search engines.

5. The Implementation

Metadata remains one of the most important SEO factors.

5.1 Registering Storage Disks

The system supports multiple storage disks.

A disk represents a storage destination with its own configuration.

Examples include:

  • private-resources
  • public-resources
  • user-uploads
  • backups
  • exports

Disks are registered centrally:

LMVC_FileSystem::registerDisk('private-resources', $config);

Internally, configurations are stored inside:

private static $storageDisks = [];

This creates a centralized registry of available storage destinations.

5.2 Creating FileSystem Instances

Filesystem instances are retrieved through:

$fileSystem = LMVC_FileSystem::getInstance('private-resources', 'files');

The implementation follows a singleton-per-disk strategy.

$key = $diskName . ":" . $subDirectoryName;

This ensures:

Configuration remains consistent.

Adapter initialization occurs once.

S3 clients are reused.

Memory consumption remains predictable.

5.3 Environment-Driven Storage Selection

One of the most powerful aspects of the implementation is runtime adapter selection.

$adapterType = strtolower(FILE_STORAGE_TYPE);

Depending on the environment:

FILE_STORAGE_TYPE=local

or

FILE_STORAGE_TYPE=s3

the system initializes the appropriate adapter.

if ($adapterType === 's3'){
   $this->initS3Adapter();
}
else {
    $this->initLocalAdapter();
}

This means the application code never changes.

Only configuration changes.

5.4 Local Storage Adapter

For local environments, files are stored directly on disk.

private function initLocalAdapter($config){
    $adapter = new LocalFilesystemAdapter($directoryPath);
    $this->filesystem = new Filesystem($adapter);
}

Before initialization, the directory is automatically created:

mkdir($directoryPath, 0777, true);

This guarantees that storage paths always exist.

5.5 S3 Storage Adapter

For production environments, files can be stored in S3-compatible object storage.

$s3Client = new S3Client([
    'credentials' => [
        'key' => $accessKey,
        'secret' => $secret
    ]
]);

The adapter is then initialized:

$adapter = new AwsS3V3Adapter($s3Client, $bucket, $this->subDirectoryName);

And finally wrapped by Flysystem:

$this->filesystem = new Filesystem($adapter);

At this point, the application has no idea whether files are being written to:

/storage/files/

or

s3://private-bucket/files/

The API remains identical.

6. Core Filesystem Operations

The abstraction layer exposes a small set of operations.

Writing Files

$fileSystem->write($filename, $contents);

Internally:

$this->filesystem->write($filename, $contents);

Visibility can also be specified:

$fileSystem->write('report.pdf', $pdf, 'private');

Reading Files

$content = $fileSystem->read('report.pdf');

Any storage exceptions are captured and logged.

Deleting Files

$fileSystem->delete('report.pdf');

The implementation first verifies existence:

if ($this->filesystem->fileExists($filename)){
    $this->filesystem->delete($filename);
}

This avoids unnecessary exceptions.

Renaming Files

Flysystem adapters do not always support native renaming operations.

To maintain compatibility, the implementation performs:

Read → Write → Delete

$contents = $this->read($old);

$this->write($new, $contents);

$this->delete($old);

This approach works consistently across storage providers.

Managing File Metadata

The filesystem layer also exposes metadata retrieval.

$fileSystem->getFileMetaInfo('invoice.pdf');

Returns: [
    'mtime' => 1719982345,
    'size'  => 124518
]

This enables:

  • Download managers
  • Cache validation
  • Audit systems
  • Storage reporting

without exposing adapter-specific logic.

Listing Files

Directories can be queried using:

$fileSystem->list();

Results are normalized:

[
    [
        'type' => 'file',
        'path' => 'invoice.pdf',
        'size' => 24518,
        'mtime' => 1719982345
    ]
]

Again, the response structure remains identical regardless of storage backend.

7. Integrating with PDF

Let’s examine a real-world workflow.

The application generates proforma invoices using TCPDF.

After rendering:

$pdfContent = $pdf->Output('', 'S');

TCPDF returns the PDF as a string instead of writing directly to disk.

This is important because it keeps storage responsibilities outside the PDF library.

The generated file is then saved through the filesystem abstraction.

$fileSystem = LMVC_FileSystem::getInstance('private-resources', 'files');

A destination path is constructed:

$proformaFile = 'proforma_' . $proforma->proformaNumber . '.pdf';

And stored:

$fileSystem->write($filePath, $pdfContent);

At this stage:

  • Development environments are saved locally.
  • Production environments save to S3.
  • The PDF service remains unchanged.

This is the primary benefit of abstraction.

8. Error Handling Strategy

Every filesystem operation is wrapped in exception handling.

Example:

try {
    $this->filesystem->write($filename, $contents);
}

catch (\Exception $e){
    $this->addError($e->getMessage());
}

Errors are accumulated in:

private $errors = [];

This allows callers to inspect failures without crashing the application.

9. Benefits of This Architecture

The filesystem layer provides several advantages:

Storage Independence

Switch from local storage to S3 without modifying business code.

Centralized Configuration

All storage definitions exist in one place.

Improved Testability

Filesystem interactions can be mocked easily.

Consistent API

Developers interact with one interface regardless of storage provider.

Cloud Readiness

Applications can scale beyond a single server without redesigning file handling.

Cleaner Business Logic

Services focus on domain concerns rather than infrastructure concerns.

10. Conclusion

Storage is infrastructure, not business logic.

By introducing a dedicated filesystem abstraction built on Flysystem, the application gains flexibility, portability, and long-term maintainability.

The PDF generation service shown in this example never needs to know whether files are stored on a local server, Amazon S3, MinIO, or any future storage platform.

It simply generates documents and delegates persistence to a dedicated filesystem layer.

This separation of concerns is a foundational principle of scalable software architecture and becomes increasingly valuable as systems evolve from single-server deployments into distributed, cloud-native applications.