popphp / pop-storage
Pop Storage Component for Pop PHP Framework
Requires
- php: >=8.4.0
- aws/aws-sdk-php: ^3.339.12
- popphp/pop-dir: ^5.0.0
- popphp/pop-http: ^6.0.0
- popphp/pop-utils: ^3.0.0
Requires (Dev)
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^12.5.0
README
- Overview
- Install
- Upgrading to 3.0
- Quickstart
- Adapters
- Error Handling
- Working with Files
- Directories
- Helper Methods
- Accessing the Adapter Directly
Overview
pop-storage is a storage component that provides interchangeable adapters to easily manage and switch
between different storage resources. Supported storage adapters are:
- AWS S3
- Microsoft Azure
- Local Disk
NOTE: The use of enterprise storage solutions like AWS S3 and Microsoft Azure require credentials and permissions to be created in their respective administration portals. Please refer to the online documentation, guidelines and polices for whichever storage platform to which you are attempting to connect your application using this component. Please take care in granting access and assigning permissions to your application instance. Always follow the recommended security policies and guidelines of your chosen storage platform.
pop-storage is a component of the Pop PHP Framework.
Install
Install pop-storage using Composer.
composer require popphp/pop-storage
Or, require it in your composer.json file
"require": {
"popphp/pop-storage" : "^3.0.0"
}
Upgrading to 3.0
Version 3.0 is a deliberate breaking release that removes silent failures from the storage API. If you are upgrading from 2.x, three changes need attention. See Error Handling below for the full exception model this introduces; this section covers only what to change in existing code.
1. Failures throw exceptions instead of returning false or doing nothing.
Previously, a failed write could silently do nothing and a missing file could quietly return false.
Now every operation either succeeds or throws a typed exception. The old per-layer
Pop\Storage\Adapter\Exception and Pop\Storage\Adapter\Azure\Exception classes have been removed;
catch the new Pop\Storage\Exception\* types instead (or Pop\Storage\Exception itself as a
catch-all).
2. getFileSize(), getFileType(), getFileMTime() and md5File() no longer return false.
They now return int, string, int|string and string respectively. Code that tested the return
value for false needs to catch an exception instead:
// 2.x $size = $storage->getFileSize('test.pdf'); if ($size === false) { // not found } // 3.0 try { $size = $storage->getFileSize('test.pdf'); } catch (Pop\Storage\Exception\FileNotFoundException $e) { // not found }
3. Path traversal is rejected.
Any path containing a .. segment now throws PathTraversalException rather than being resolved.
This applies to every path-taking method, including the name value of an uploaded file array — which
comes directly from $_FILES and is attacker-controlled. If any existing code relied on a ..
segment being silently resolved, it will now throw instead.
Quickstart
A storage object can be created using one of the factories:
use Pop\Storage\Storage; $storage = Storage::createAzure('ACCOUNT_NAME', 'ACCOUNT_KEY', 'CONTAINER');
Then a local file can be uploaded to the storage platform:
$storage->putFile('test.pdf');
Or, a remote file can be downloaded from the storage platform, which will return the file contents to be utilized within the application:
$fileContents = $storage->fetchFile('test.pdf');
Adapters
By default, there are 3 available adapters. All of the adapters share the same interface and
are interchangeable. Other adapters can be created, as long as they implement the same
Pop\Storage\StorageInterface.
AWS S3
The Amazon AWS S3 adapter interfaces with AWS S3 and requires the following credentials and access information to be obtained from the AWS administration console:
- AWS Key
- AWS Secret
- AWS Region
- AWS Version (usually
latest) - The AWS S3 bucket to access (in the format
s3://bucket)
use Pop\Storage\Storage; $storage = Storage::createS3('AWS_BUCKET', new S3\S3Client([ 'credentials' => [ 'key' => 'AWS_KEY', 'secret' => 'AWS_SECRET', ], 'region' => 'AWS_REGION', 'version' => 'AWS_VERSION' ]));
Microsoft Azure
The Microsoft Azure adapter interfaces with Microsoft Azure Storage and requires the following credentials and access information to be obtained from the Azure administration console:
- Account Name
- Account Key
- The Azure container to access (in the format
container)
Note: The container should be configured to have "hierarchical namespace" support turned on for better support with filenames and folders.
use Pop\Storage\Storage; $storage = Storage::createAzure('ACCOUNT_NAME', 'ACCOUNT_KEY', 'CONTAINER');
Local Disk
The local disk adapter allows simple management of files and folders on the local disk of the application using the same interface as the other adapters. This can be useful for local development and testing, before switching to one of the enterprise adapters for production.
It only needs the main directory to serve as the base location:
use Pop\Storage\Storage; $storage = Storage::createLocal(__DIR__ . '/tmp/');
Error Handling
An operation on pop-storage either succeeds or throws a typed exception — there are no false
return values or silent no-ops to check for. Every exception lives under Pop\Storage\Exception\*
and extends Pop\Storage\Exception, so a single catch-all always works, with specific types
available when you need to branch on the failure:
try { $contents = $storage->fetchFile('test.pdf'); } catch (Pop\Storage\Exception\FileNotFoundException $e) { // Handle the missing file specifically } catch (Pop\Storage\Exception $e) { // Or catch any storage failure }
The available exception types are FileNotFoundException, DirectoryNotFoundException,
UnableToWriteFileException, UnableToReadFileException, UnableToDeleteFileException,
UnableToCopyFileException, UnableToMoveFileException, UnableToCreateDirectoryException,
UnableToDeleteDirectoryException, UnableToGenerateTemporaryUrlException,
UnsupportedOperationException and PathTraversalException.
A few methods are deliberate exceptions to the "always throw" rule:
fileExists(),isDir()andisFile()always returnbool. They're questions, not operations, so a path that simply isn't there is a normalfalse, not an error. (They can still throwPathTraversalException— see below — since that signals invalid input, not a negative answer.)getFileSize(),getFileType(),getFileMTime()andmd5File()never returnfalse. They returnint,string,int|stringandstringrespectively. A missing file throwsFileNotFoundException; metadata that can't be read from an otherwise successful response throwsUnableToReadFileException.
Path traversal is rejected. Any path containing a .. segment throws PathTraversalException
rather than being resolved, on every path-taking method across every adapter — including the name
value of an uploaded file array, which comes directly from $_FILES and is attacker-controlled. A
single leading /, \, ./ or .\ is normalized away rather than rejected, so ordinary paths are
unaffected.
Working with Files
There are a number of available methods to assist in the uploading and downloading of files to and from the storage platform, as well as obtaining general data and information about them.
Put a local file on the remote location
Use a file on disk:
$storage->putFile('test.pdf');
Use a stream of file contents:
$storage->putFileContents('test.pdf', $fileContents);
putFileContents() writes the file whether or not it already existed. If you specifically want to
replace the contents of a file that must already be there — and get a FileNotFoundException if it
isn't — use replaceFileContents() instead:
$storage->replaceFileContents('test.pdf', $newFileContents);
Fetch file contents
This method returns the file contents to be utilized within the application:
$fileContents = $storage->fetchFile('test.pdf');
Fetch file info
This method uses a custom request (i.e, a HEAD request) to return general information
about a file without downloading the file's contents:
// Returns an array of file info: $info = $storage->fetchFileInfo('test.pdf');
Streaming files
For large files, the streaming methods move contents through a PHP stream resource instead of
buffering the whole file in memory. putFileStream() writes from a readable resource:
$resource = fopen('/path/to/large-video.mp4', 'r'); $storage->putFileStream('large-video.mp4', $resource); fclose($resource);
And fetchFileStream() returns a readable resource, which the caller is responsible for closing:
$resource = $storage->fetchFileStream('large-video.mp4'); while (!feof($resource)) { echo fread($resource, 8192); } fclose($resource);
Temporary (presigned) URLs
This method returns a time-limited URL that grants read access to a file without exposing your credentials or making the file public — an S3 presigned URL, or an Azure SAS-token URL:
// Valid for 15 minutes (900 seconds) by default $url = $storage->getTemporaryUrl('test.pdf');
// Or, set the expiration explicitly $url = $storage->getTemporaryUrl('test.pdf', 3600);
The local disk adapter has no equivalent concept and throws
Pop\Storage\Exception\UnsupportedOperationException.
Upload files from a server request ($_FILES format)
$storage->uploadFiles($_FILES);
// Where $file follows the $_FILES array format specified in PHP: // $file = ['tmp_name' => '/tmp/Hs87jdk', 'name' => 'test.pdf', 'size' => 8574, 'error' => 0] $storage->uploadFile($file);
List Files
You can list or search the files in the current location:
$files = $storage->listFiles();
$files = $storage->listFiles('test*');
$files = $storage->listFiles('*.pdf');
List all or search all directories and files together:
$all = $storage->listAll();
All three listing methods accept a second $recursive parameter (default false, which lists only the
current location, one level deep). Passing true walks every level below the current location:
// ['test.pdf', 'foo/test2.pdf', 'foo/bar/test3.pdf'] $files = $storage->listFiles(null, true);
$dirs = $storage->listDirs(null, true); $all = $storage->listAll(null, true);
Recursive results are paths relative to the current location, including their intermediate directories,
so each one can be passed straight back into fetchFile(), deleteFile(), and friends:
foreach ($storage->listFiles(null, true) as $file) { $contents = $storage->fetchFile($file); }
Copy or move file from one remote location to another
// The source file remains $storage->copyFile('test.pdf', 'foo/test2.pdf');
// The source file no longer exists $storage->renameFile('test.pdf', 'foo/test2.pdf');
Copy of move file from/to an external location on the same remote storage resource
This allows you to copy or move files between different AWS buckets or Azure containers that are outside the currently referenced bucket or container.
To External
// AWS example. The source file remains $storage->copyFileToExternal('test.pdf', 's3://other-bucket/test.pdf'); // Azure example. The source file remains $storage->copyFileToExternal('test.pdf', '/other-container/test.pdf');
// AWS example. The source file no longer exists $storage->moveFileToExternal('test.pdf', 's3://other-bucket/test.pdf'); // Azure example. The source file no longer exists $storage->moveFileToExternal('test.pdf', '/other-container/test.pdf');
From External
// AWS example. The source file remains $storage->copyFileFromExternal('s3://other-bucket/test.pdf', 'test.pdf'); // Azure example. The source file remains $storage->copyFileFromExternal('/other-container/test.pdf', 'test.pdf');
// AWS example. The source file no longer exists $storage->moveFileFromExternal('s3://other-bucket/test.pdf', 'test.pdf'); // Azure example. The source file no longer exists $storage->moveFileFromExternal('/other-container/test.pdf', 'test.pdf');
Delete file
$storage->deleteFile('test.pdf');
Directories
The AWS and Azure storage resources don't explicitly support "directories" or "folders." However, they
do still allow for a "directory-like" structure in the form of "prefixes." The pop-storage component
normalizes that functionality into a more "directory-like" interface that allows the ability to change
directories, make directories and remove directories.
NOTE: The creation or removal of empty directories is only allowed with the S3 and local adapters. The Azure storage resource doesn't allow the explicit creation or removal of empty directories. Instead, a new "directory" (prefix) is created automatically created with an uploaded file that utilizes a prefix. Conversely, a "directory" (prefix) is automatically removed when the last file that utilizes the prefix is deleted.
$storage = Storage::createS3('s3://my-bucket', new S3\S3Client([ 'credentials' => [ 'key' => 'AWS_KEY', 'secret' => 'AWS_SECRET', ], 'region' => 'AWS_REGION', 'version' => 'AWS_VERSION' ])); // Create the bucket 's3://my-bucket/foo' $storage->mkdir('foo'); // Point the adapter at 's3://my-bucket/foo' // Any files pushed will store here // Any delete calls will delete files from here $storage->chdir('foo'); // Removes the bucket and its content $storage->rmdir('foo');
Base directory vs. current directory
setBaseDir()/getBaseDir() control the fixed root the storage object operates against (the bucket
or container itself); getCurrentDir() reports wherever chdir() last pointed it:
$storage->setBaseDir('s3://my-bucket'); $storage->getBaseDir(); // 's3://my-bucket' $storage->chdir('foo'); $storage->getCurrentDir(); // 's3://my-bucket/foo'
chdir() always resolves from the base directory, not from wherever you last chdir'd — so it is
not cumulative. Calling chdir('foo') and then chdir('bar') points at foo's sibling bar, not
at foo/bar. Call chdir() with no argument to return to the base directory.
List Directories
You can list or search the directories in the current location:
$dirs = $storage->listDirs();
$dirs = $storage->listDirs('foo*');
$dirs = $storage->listDirs('*foo/');
List all or search all directories and files together:
$all = $storage->listAll();
And, as with the file listings, a second $recursive parameter walks every level below the current location:
$dirs = $storage->listDirs(null, true);
Helper Methods
There are a number of helper methods to provide information on file status or things like whether or not the file exists.
var_dump($storage->fileExists('test.pdf')) // Returns bool var_dump($storage->isDir('foo')); // Returns bool var_dump($storage->isFile('test.pdf')); // Returns bool var_dump($storage->getFileSize('test.pdf')); // Returns filesize value as an integer var_dump($storage->getFileType('test.pdf')); // Return either 'file' or 'dir' var_dump($storage->getFileMTime('test.pdf')); // Returns date/time value var_dump($storage->md5File('test.pdf')); // Returns MD5 hash of file
fileExists(), isDir() and isFile() always return a bool. The four metadata methods below them
never return false — if the file isn't there they throw Pop\Storage\Exception\FileNotFoundException,
and if the metadata can't be read they throw Pop\Storage\Exception\UnableToReadFileException.
Accessing the Adapter Directly
Storage delegates every method it exposes to whichever adapter it was created with, always
matching the shared Pop\Storage\StorageInterface contract. A handful of capabilities are specific
to one storage platform and only exist on that adapter's own class, not on the shared interface —
reach them with getAdapter() (or its shorter alias, adapter()):
$adapter = $storage->adapter(); // same as $storage->getAdapter()
The Microsoft Azure adapter is currently the only one with extras beyond the interface, all related to Azure-specific blob behavior:
// fetchFile()'s second argument: pass false to get the raw Pop\Http\Client\Response back // instead of the file contents as a string (useful for inspecting response headers). $response = $storage->adapter()->fetchFile('test.pdf', false); // deleteFile() and moveFileFromExternal() take an optional trailing $snapshots argument // controlling Azure's x-ms-delete-snapshots header when the blob has snapshots: // 'include' (default) deletes the blob and its snapshots, 'only' deletes just the // snapshots and leaves the blob, and null omits the header entirely. $storage->adapter()->deleteFile('test.pdf', 'only'); $storage->adapter()->moveFileFromExternal('/other-container/test.pdf', 'test.pdf', 'only');