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.





