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

Ftp Laravel Package

anchu/ftp

Adds an FTP filesystem driver to Laravel so you can use the familiar Storage disk API to upload, download, list, and delete files on remote FTP servers. Configure a disk with your host and credentials and work with remote files like local ones.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require anchu/ftp
php artisan vendor:publish --provider="Anchu\Ftp\FtpServiceProvider" --tag=config

Requires ext-ftp (and ext-openssl if you plan to use FTPS). Laravel 7 supports package auto-discovery, so no manual provider/alias registration is needed — the package registers Anchu\Ftp\FtpServiceProvider and the Ftp facade for you. Confirm the file exists at config/ftp.php after publishing.

First thing to look at is the connections array in config/ftp.php. Every connection is a named array:

'connections' => [
    'default' => [
        'host'     => env('FTP_HOST'),
        'port'     => env('FTP_PORT', 21),
        'username' => env('FTP_USERNAME'),
        'password' => env('FTP_PASSWORD'),
        'passive'  => env('FTP_PASSIVE', true),
        'ssl'      => env('FTP_SSL', false),
        'timeout'  => env('FTP_TIMEOUT', 90),
        'root'     => env('FTP_ROOT', ''), // path prefix applied to all remote paths
    ],
    'partner' => [ /* ... */ ],
],

The first real use case is dropping a generated file onto a remote host:

use Anchu\Ftp\Facades\Ftp;

Ftp::connection('default')->put(
    storage_path('app/exports/orders.csv'),
    'exports/orders.csv'
);

Ftp::put(...) (no connection) targets the default connection. Paths are remote paths relative to root, not absolute filesystem paths — don't prepend the root yourself.

Implementation Patterns

Treat the facade as a thin wrapper over a stateful socket. Ftp::connection('name') returns a connection object bound to that config entry; file methods handle connecting lazily. The idiomatic pattern is to wrap it in your own service so the rest of the app doesn't depend on the facade directly:

class RemoteArchive
{
    public function __construct(private string $connection = 'default') {}

    public function upload(string $local, string $remote): void
    {
        $ftp = Ftp::connection($this->connection);
        $this->ensureDirectory($ftp, dirname($remote), $remote);
        if (! $ftp->put($local, $remote)) {
            throw new RuntimeException("Upload failed: {$remote}");
        }
    }
}

Directory creation is not recursive. makeDir() creates one level. Build the remote tree explicitly from dirname() parts, and guard with exists() first, because most FTP servers throw a warning rather than a clean error when the directory already exists:

$parts = explode('/', trim($dir, '/'));
$path  = '';
foreach ($parts as $part) {
    $path = ltrim($path . '/' . $part, '/');
    if (! $ftp->exists($path)) {
        $ftp->makeDir($path);
    }
}

Use named connections for environment isolation. Typical setups give themselves a default (production drops) and a staging/partner connection. Keep credentials in .env per environment and always run php artisan config:cache in production — never call env() inside application code, only inside the config file.

Queue long transfers. FTP is blocking and timeout defaults to 90 seconds. For anything larger than a few MB, dispatch a job (ShouldQueue, $timeout greater than the FTP timeout) and close the connection at the end of handle() so the worker doesn't hold dead sockets across restarts:

try {
    Ftp::connection('default')->put($tmp, $remote);
} finally {
    Ftp::connection('default')->close();
}

Schedule recurring syncs with an Artisan command, not a closure in Kernel::schedule(), so you get retries and log context:

$schedule->command('orders:push')->hourly()->withoutOverlapping();

Listings and downloads. nlist() returns an array of raw names (server-dependent — some include . and ..); rawlist() returns the ls -l-style lines and needs parsing per server, so prefer nlist() when all you need is names. For pull-style ingestion, get() to a temp local path, process, then unlink() locally.

If you want the Storage API instead, this package isn't it — it's a procedural wrapper around ftp_*. Reach for league/flysystem-ftp and register an FTP disk if you need Storage::disk('ftp')->put(...), streaming, or assertExists() in tests. Mixing both is fine; just pick one per workflow.

Gotchas and Tips

  • Maintenance risk. Last release was November 2020 and targets Laravel ≤ 7. The facade and service provider APIs are stable, so it often runs on 8/9, but the composer constraint will block installs on newer Laravel versions unless you fork or use --ignore-platform-reqs. Audit before adding it to a fresh project — for new work, league/flysystem-ftp is the safer long-term choice.
  • No SFTP. This package is plain FTP/FTPS only. If the remote host requires SSH, use league/flysystem-sftp-v3; don't waste time trying to force ssl => true to mean SFTP — it means explicit FTP over TLS.
  • ssl => true needs ext-openssl and server support. Without both, ftp_ssl_connect fails silently in the connect path. Verify with a one-off script before wiring it into a schedule.
  • Passive mode is the #1 "it hangs" cause. passive => true is correct for almost everything behind NAT; flip to false only if the server's control connection succeeds but data transfers stall. Lower timeout to 10–15s while debugging so failures surface fast.
  • root is a prefix, not a chroot. It prepends to remote paths but does not restrict traversal — ../ will still work if the server allows it. Don't rely on it for security.
  • Return values are bool, not exceptions, for file ops. put, get, delete, makeDir return false on failure and may emit a PHP warning. Always check the return value; wrap the call in a small helper that converts false into an exception, otherwise failures disappear into logs. Connection failures (bad host, refused port) are the case that does throw, so guard the first call of a batch with try/catch and short-circuit the job.
  • Facade mocking in tests is awkward because connection() returns a different object. Either Ftp::shouldReceive('connection')->andReturnSelf() followed by shouldReceive('put'), or — much cleaner — inject RemoteArchive-style wrappers and fake those, keeping the facade at the boundary.
  • Long-running processes (queue workers, Octane) leak connections. The connection object caches its socket; after a worker restart or a network blip, subsequent calls on the stale handle fail. Call close() in a finally block, or re-fetch Ftp::connection(...) per job rather than caching it on a singleton.
  • Filenames are byte-oriented. nlist() doesn't transcode UTF-8; non-ASCII names come back mangled on many servers. Stick to ASCII slugged names for anything you upload, and you'll avoid a class of "file uploaded but can't be found" bugs.
  • get()/put() take local file paths, not streams. To upload from memory, write to a temp file (tempnam()), upload, then delete. Don't try passing an fopen() resource.
  • Prefer exists() over catching a failed delete()/get() — servers disagree on error codes for missing files, and a pre-check keeps the failure mode consistent across hosts.
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