mohamedhabibwork / laravel-oci-driver
Laravel Flysystem driver for Oracle Cloud Infrastructure (OCI) Object Storage
Package info
github.com/mohamedhabibwork/laravel-oci-driver
pkg:composer/mohamedhabibwork/laravel-oci-driver
Fund package maintenance!
Requires
- php: ^8.2
- ext-fileinfo: *
- ext-openssl: *
- guzzlehttp/guzzle: ^7.8
- illuminate/contracts: ^10.0||^11.0||^12.0||^13.0
- illuminate/filesystem: ^10.0||^11.0||^12.0||^13.0
- illuminate/support: ^10.0||^11.0||^12.0||^13.0
- league/flysystem: ^3.25
- nesbot/carbon: ^2.72||^3.0
- ramsey/uuid: ^4.7
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^2.9||^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1.1||^7.10.0
- orchestra/testbench: ^8.0||^9.0||^10.0||^11.0
- pestphp/pest: ^3.0||^4.0
- pestphp/pest-plugin-arch: ^3.0||^4.0
- pestphp/pest-plugin-laravel: ^3.0||^4.0
- phpstan/extension-installer: ^1.3||^2.0
- phpstan/phpstan-deprecation-rules: ^1.1||^2.0
- phpstan/phpstan-phpunit: ^1.3||^2.0
- spatie/laravel-ray: ^1.35
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-09 12:11:19 UTC
README
Laravel Flysystem (v3) driver for Oracle Cloud Infrastructure (OCI) Object Storage.
Requires PHP 8.2+ (8.3+ with Laravel 13) and Laravel 11, 12 or 13 (tested in CI). Laravel 10 is allowed by the package constraints but is not tested in CI.
Installation
composer require mohamedhabibwork/laravel-oci-driver php artisan vendor:publish --provider="LaravelOCI\LaravelOciDriver\LaravelOciDriverServiceProvider" --tag="laravel-oci-driver-config"
The publish tag is optional: it is only needed if you want the laravel-oci-driver.connections config file.
Quick start
Add a disk to config/filesystems.php:
'oci' => [ 'driver' => 'oci', 'tenancy_id' => env('OCI_TENANCY_ID'), 'user_id' => env('OCI_USER_ID'), 'key_fingerprint' => env('OCI_KEY_FINGERPRINT'), 'key_path' => env('OCI_KEY_PATH'), 'namespace' => env('OCI_NAMESPACE'), 'region' => env('OCI_REGION'), 'bucket' => env('OCI_BUCKET'), ],
use Illuminate\Support\Facades\Storage; Storage::disk('oci')->put('documents/hello.txt', 'Hello, Oracle Cloud!'); $content = Storage::disk('oci')->get('documents/hello.txt'); $files = Storage::disk('oci')->files('documents');
Configuration
Each disk is built from its own array. Required keys: tenancy_id, user_id, key_fingerprint, namespace, region, bucket and one key source. A missing value throws an InvalidArgumentException that names the disk.
Private key sources
Checked in this order; the first one that is set is used:
| Key | Meaning |
|---|---|
private_key |
PEM contents as a string |
private_key_base64 |
PEM contents, base64-encoded |
key_path |
Local path to the PEM file (URLs are rejected) |
Use passphrase for an encrypted key.
Options
| Key | Default | Description |
|---|---|---|
storage_tier |
Standard |
Standard, InfrequentAccess or Archive |
url_path_prefix |
'' |
Prefix for every object name |
timeout / connect_timeout |
30 / 10 |
Seconds; timeout is the total request time and does not apply to upload PUTs |
retry_attempts / retry_delay |
3 / 1000 |
Retries on connection errors, 429, 5xx; delay in ms, exponential |
upload.multipart_threshold |
104857600 |
Bytes; larger (or unknown-size) streams use multipart |
upload.chunk_size |
33554432 |
Part size in bytes; minimum 10 MiB (10485760) |
upload.enable_checksum |
true |
Content-MD5 on single-PUT string uploads |
delete_concurrency |
10 |
Parallel deletes in deleteDirectory |
copy_timeout / copy_poll_interval |
300 / 1000 |
Seconds / ms for async copy |
temporary_url.default_expiry / max_expiry |
3600 / 86400 |
Seconds |
visibility |
private |
What visibility() reports when no mapping is set |
visibility_mapping |
null |
Opt-in visibility to tier map |
laravel-oci-driver.global.throw_exceptions (default true) controls whether a failed temporaryUrl() throws or returns ''.
The cache.*, logging.*, content_type.* and most other global.* keys in the published config file are reserved and currently unused. The full reference is in docs/CONFIGURATION.md.
Multiple disks
Define as many oci disks as you need, each with its own bucket and credentials. To share credentials, set 'connection' => 'name' on a disk: values from laravel-oci-driver.connections.name are merged underneath the disk values and the disk wins.
'oci' => ['driver' => 'oci', 'connection' => 'default'], 'oci_backups' => [ 'driver' => 'oci', 'connection' => 'default', 'bucket' => 'backups', 'url_path_prefix' => 'nightly', 'storage_tier' => 'InfrequentAccess', ],
A connection that exists only in laravel-oci-driver.connections is not usable on its own: a filesystems.disks entry with 'driver' => 'oci' is always required. LaravelOciDriver::connection('backups') returns the disk named backups (or oci_backups).
Uploads and multipart
Writes up to upload.multipart_threshold are a single streamed PUT (not buffered in memory). Larger writes, and streams whose size cannot be determined, use OCI native multipart upload: one part of upload.chunk_size bytes is held in memory at a time, and the upload is aborted if a part fails.
Per-write options:
Storage::disk('oci')->put('reports/q1.pdf', $contents, [ 'content_type' => 'application/pdf', 'storage_tier' => 'InfrequentAccess', 'metadata' => ['department' => 'finance'], // stored as opc-meta-* 'checksum' => false, // skip Content-MD5 for this write ]);
Listing, directories, copy and move
- Listing is paginated and lazy; returned paths do not include
url_path_prefix. makeDirectory()writes an emptydir/object;deleteDirectory()deletes every object under the directory withdelete_concurrencyparallel requests and throwsUnableToDeleteDirectorylisting the keys that failed. On a disk withouturl_path_prefix, deleting the root (''or'/') is refused to protect the bucket.copy()is asynchronous on OCI; the driver polls the work request until it completes, fails orcopy_timeoutelapses.move()uses OCIrenameObject(same bucket, atomic).
Temporary URLs
temporaryUrl() creates a pre-authenticated request (PAR):
$url = Storage::disk('oci')->temporaryUrl('invoices/1.pdf', now()->addHour()); // upload URL $url = Storage::disk('oci')->temporaryUrl('inbox/file.bin', now()->addMinutes(10), [ 'access_type' => 'ObjectWrite', // default: ObjectRead ]);
Expiry defaults to temporary_url.default_expiry and is capped at temporary_url.max_expiry. PARs are not deleted when they expire. Remove expired ones that this package created:
php artisan oci:par:prune oci --dry-run # report only
php artisan oci:par:prune oci
Schedule it, for example Schedule::command('oci:par:prune oci')->daily();.
Visibility
OCI has no per-object ACL. By default setVisibility() throws UnableToSetVisibility, and visibility() reports the disk's visibility value (default private). If you want Laravel's visibility API to move objects between storage tiers, opt in:
'visibility_mapping' => [ 'public' => 'Standard', 'private' => 'InfrequentAccess', ],
Tier changes and restores from Archive are available on the adapter:
$adapter = Storage::disk('oci')->getAdapter(); $adapter->updateStorageTier('old/report.pdf', \LaravelOCI\LaravelOciDriver\Enums\StorageTier::ARCHIVE); $adapter->restore('old/report.pdf', hours: 24);
Artisan commands
| Command | Purpose |
|---|---|
oci:setup |
Create the .oci folder, generate/copy key files and write config |
oci:config [--validate] [--disk=oci] |
Interactive configuration, or validate a disk |
oci:connection {list|test|switch|summary} {name?} |
Inspect configured connections |
oci:status [--bucket=] |
Test connectivity and basic file operations on the oci disk |
oci:par:prune {disk=oci} [--dry-run] |
Delete expired pre-authenticated requests created by this package |
Errors
Failures are mapped to the matching Flysystem exception (UnableToReadFile, UnableToWriteFile, UnableToDeleteFile, UnableToDeleteDirectory, UnableToCopyFile, UnableToMoveFile, UnableToListContents, ...). The previous exception is an OciRequestFailed carrying the HTTP status, opc-request-id and OCI error code.
Troubleshooting
php artisan oci:config --validate --disk=<disk>shows which required value is missing.- Names with spaces,
+or%are percent-encoded as a single path segment; do not pre-encode them. - The adapter always applies
url_path_prefix: with prefixlocal,write('local/a')storeslocal/local/a. (OciClient::getPrefixedPath()keeps its idempotent rule for direct client callers.) - See docs/TROUBLESHOOTING.md.
Upgrading
Behaviour changes to be aware of:
- Listed paths no longer include the disk's
url_path_prefix. setVisibility()no longer moves objects between storage tiers unlessvisibility_mappingis set; without it, it throwsUnableToSetVisibility.- Private keys can no longer be loaded from a URL.
OciClient::listObjects()throws on API errors instead of returning[].upload.chunk_sizedefault rose from 8 MiB to 32 MiB; the minimum is 10 MiB.deleteDirectory(), storage tier changes and restores used to report success silently; they now either work or throw.
See CHANGELOG.md.
Documentation
Configuration · Authentication · Examples · Testing · Troubleshooting · API reference
Some of the other guides under docs/ pre-date the transport and configuration rewrite and may describe options that no longer exist; docs/CONFIGURATION.md and this README are authoritative.
Testing
composer test
composer analyse
Contributing, support and license
See CONTRIBUTING.md. Issues: GitHub Issues. Security: security@mohamedhabib.work. Licensed under the MIT License, see LICENSE.md.
Created by Mohamed Habib.