Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Flysystem Gcs Laravel Package

cedricziel/flysystem-gcs

Flysystem adapter for Google Cloud Storage that lets Laravel and PHP applications use GCS buckets as filesystem disks for uploading, reading, listing, and deleting objects through a familiar Flysystem API.

View on GitHub
Deep Wiki
Context7

Getting Started

First, check your Flysystem generation. This adapter implements League\Flysystem\AdapterInterface (Flysystem 1.x). That means it works with Laravel ≤ 8 only. If you're on Laravel 9+, stop here and use league/flysystem-google-cloud-storage — Laravel ships a built-in gcs driver for it — because Composer will refuse to install this package alongside league/flysystem ^2|^3.

Minimal path on Laravel 8:

composer require cedricziel/flysystem-gcs

It pulls in google/cloud-storage (the "gcloud PHP library"), which is what actually talks to GCS. Authenticate the same way you would for any Google client:

export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json

Where to look first: open vendor/cedricziel/flysystem-gcs/src/. There is essentially one adapter class in the CedricZiel\FlysystemGcs namespace — read its constructor signature before writing config, since that's the source of truth for what options (bucket handle, optional path prefix, visibility) it accepts. The README is thin; the code is ~a few hundred lines and is faster to read than to search for docs.

Register the disk in App\Providers\AppServiceProvider::boot() (the package ships no Laravel service provider):

use Google\Cloud\Storage\StorageClient;
use League\Flysystem\Filesystem;
use Illuminate\Support\Facades\Storage;

Storage::extend('gcs', function ($app, $config) {
    $client = new StorageClient([
        'projectId'   => $config['project_id'],
        'keyFilePath' => $config['key_file'] ?? null, // omit to use ADC
    ]);

    $adapter = new \CedricZiel\FlysystemGcs\GoogleCloudStorageAdapter(
        $client->bucket($config['bucket_name']),
        $config['path_prefix'] ?? null
    );

    return new Filesystem($adapter);
});

First use case: offloading user uploads out of the local disk.

// config/filesystems.php
'gcs' => [
    'driver'      => 'gcs',
    'project_id'  => env('GOOGLE_CLOUD_PROJECT_ID'),
    'bucket_name' => env('GOOGLE_CLOUD_STORAGE_BUCKET'),
    'key_file'    => env('GOOGLE_APPLICATION_CREDENTIALS'),
    'path_prefix' => env('GOOGLE_CLOUD_STORAGE_PATH_PREFIX'),
],
Storage::disk('gcs')->putFileAs('avatars', $request->file('avatar'), "{$user->id}.jpg");

Implementation Patterns

Namespace a shared bucket per environment. GCS buckets are often shared staging/production; the adapter's path_prefix option is the cheapest isolation you get. Set it from APP_ENV and every key becomes production/avatars/.... Do this on day one — retrofitting prefixes onto existing objects is a copy-and-delete job.

Wrap in a caching adapter for any listing UI. Flysystem 1 has League\Flysystem\Cached\CachedAdapter, and GCS charges per list operation while being slow on large prefixes. A media library that calls listContents() on every page load will be noticeably better with a cache store attached. Cache invalidation is manual, so scope it to read-mostly prefixes.

Stream large objects; don't get() them. Storage::disk('gcs')->readStream($path) and writeStream($path, $resource) map onto the gcloud library's streaming API and avoid holding whole files in PHP memory. For uploads, putFile()/putFileAs() already stream from the uploaded temp file. For downloads, return a streamed response:

return response()->streamDownload(function () use ($path) {
    fpassthru(Storage::disk('gcs')->readStream($path));
}, basename($path));

Signed URLs need a subclass. The adapter does not implement getUrl, so Storage::url() and Storage::temporaryUrl() will throw or return nothing useful. The clean fix is a thin subclass that exposes signed URLs through the underlying bucket handle:

class SignedUrlGcsAdapter extends GoogleCloudStorageAdapter
{
    public function getTemporaryUrl(string $path, $expiration, array $options = []): string
    {
        return $this->bucket->object($this->applyPathPrefix($path))
            ->signedUrl(new \DateTime("+{$expiration} minutes"), $options);
    }
}

Bind the subclass in Storage::extend and call Storage::disk('gcs')->getDriver()->getTemporaryUrl(...). Note this requires the service account to have roles/iam.serviceAccountTokenCreator on itself, or signing will fail — a common 403.

Reach through the adapter for bucket-level operations. Anything the Flysystem surface doesn't cover (object lifecycle, predefinedAcl, compose, resumable multi-chunk uploads, signed policy documents) is one hop away:

$bucket = Storage::disk('gcs')->getAdapter()->getBucket();
$bucket->object('avatars/1.jpg')->update(['metadata' => ['cacheControl' => 'public, max-age=3600']]);

Keep the disk name stable across the migration. Name the disk gcs and keep it that way. When you eventually move to Laravel 9+ and league/flysystem-google-cloud-storage, only the Storage::extend block and config keys change — every Storage::disk('gcs')->... call in your app stays put. That is the single highest-leverage decision you can make here.

Test against a local disk, never a real bucket. Storage::fake('gcs') in Laravel swaps in a local driver rooted under storage/framework/testing/disks/gcs, even for custom disks. Reserve real-bucket tests for a single smoke test behind a separate test suite.

Gotchas and Tips

This package is archived (last release 2021-02-02, 13 stars). Treat it as frozen: pin it ("cedricziel/flysystem-gcs": "~1.0") and commit composer.lock, because the google/cloud-storage transitive dependency will keep moving and is where any security fixes actually land. Budget for the migration rather than expecting upstream fixes.

GCS has no real directories. createDirectory() writes a zero-byte placeholder object (or is a no-op); deleteDirectory() lists and deletes objects one by one, and on a prefix with thousands of objects it will be slow and bill per operation. Prefer lifecycle rules on the bucket for bulk expiry, and never deleteDirectory('') from a web request.

No rename — it's copy-then-delete. Storage::move() on a 2 GB object is a full server-side copy plus a delete, and it is not atomic. Concurrent readers can see both or neither. Design for immutable keys instead of moving things around.

Visibility is ACL-based, and Uniform Bucket-Level Access breaks it. If the bucket has UBLA enabled, object ACLs are off and any visibility: public / setVisibility() path fails with a 400-level ServiceException. With UBLA you serve files through signed URLs or a CDN, full stop.

Errors arrive as Google exceptions, not Laravel ones. The adapter doesn't consistently translate Google\Cloud\Core\Exception\ServiceException into Flysystem's typed exceptions, so a missing object may surface as a raw 404 rather than UnableToReadFile. Wrap reads defensively and check Storage::disk('gcs')->exists($path) first; catch ServiceException at the boundary and map to your domain errors.

Credential gotchas, in order of frequency:

  • GOOGLE_APPLICATION_CREDENTIALS pointing at a file the PHP-FPM user can't read → silent fallback to metadata server, then a confusing timeout.
  • Passing JSON via env: StorageClient wants an array under keyFile, not a string. json_decode(env('GCP_CREDENTIALS'), true).
  • On GKE/Cloud Run, omit key_file entirely and rely on Workload Identity — don't bake keys into images.
  • 403 on write usually means the SA lacks roles/storage.objectCreator/objectAdmin on the bucket, not on the project.

URL generation is not implemented. Storage::url() will not work. Anything you build around public links must go through signed URLs (see the subclass pattern above) or a public bucket + CDN. This is the most common surprise for people porting from superbalist/flysystem-google-storage.

Metadata is partly synthetic. getSize() and getMimetype() may require an extra object metadata lookup per call — avoid calling them in a loop over listContents(), which is already the slowest operation on the adapter. Fetch the listing once and read size/mimetype from the returned array.

Debugging checklist when something silently fails: (1) Storage::disk('gcs')->getAdapter()->getBucket()->name() to confirm the bucket actually resolved; (2) check whether path_prefix is duplicating (production/production/...) — a classic after copying config between environments; (3) enable Google\Cloud\Core\RequestBuilder logging / LoggerInterface on StorageClient to see the raw HTTP call; (4) verify the object exists in the console before assuming an SDK bug, since the flat namespace means a trailing slash in a prefix silently changes the key.

Extension point worth knowing: because the whole surface is one adapter class, subclassing is the intended way to add anything missing (signed URLs, getUrl, cache-control headers, custom metadata on write). Subclass, don't fork — the package is archived and you will not be pushing fixes back upstream.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums