cxxi/ftp-client

Pure PHP FTP/FTPS/SFTP client (framework agnostic).

Maintainers

Package info

github.com/cxxi/ftp-client

pkg:composer/cxxi/ftp-client

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.1 2026-02-17 08:45 UTC

This package is auto-updated.

Last update: 2026-04-17 13:32:37 UTC


README

PHP Version CI Tests codecov PHPStan Level License Packagist Downloads

Pure PHP FTP / FTPS / SFTP client (framework agnostic).

Simple, expressive API:

$clientFtp = FtpClient::fromUrl('ftp://user:pass@example.com:21/path');

$clientFtps = FtpClient::fromUrl('ftps://user:pass@example.com:21/path');

$clientSftp = FtpClient::fromUrl('sftp://user:pass@example.com:22/path');

A clean, testable and production-ready transport layer designed for:

  • Modern PHP applications (8.2+)
  • CLI tools & workers
  • Framework integration (Symfony bundle available)
  • High reliability environments

Supports:

  • Automatic protocol resolution (ftp://, ftps://, sftp://)
  • Safe retry policies with exponential backoff & jitter
  • SFTP host key verification (MD5 / SHA1)
  • PSR-3 logging
  • Clean architecture (Ports & Adapters)

Table of Contents

Why this library?

PHP already provides ext-ftp and ext-ssh2. So why wrap them?

Because raw extensions:

  • Expose global functions
  • Mix transport logic with application logic
  • Lack retry mechanisms
  • Provide no structured error handling
  • Are difficult to unit test
  • Offer no safety model for destructive operations

This library adds:

Unified API

Same interface for FTP, FTPS and SFTP.

Switch protocol by changing the URL scheme.

Built-in Retry Strategy

  • Configurable retry count
  • Exponential backoff
  • Optional jitter
  • Safe vs unsafe operation handling

Designed for unstable networks and production systems.

Secure SFTP Support

  • Host key algorithm control
  • MD5 / SHA1 fingerprint verification (via ext-ssh2)
  • Strict host key checking option

No silent trust of unknown hosts.

Clean Architecture

  • Ports & adapters
  • Fully mockable infrastructure layer
  • Decoupled from PHP global functions
  • Designed for strict static analysis (PHPStan level max (9) + strict rules + bleeding edge)

Framework Agnostic

Works in:

  • Plain PHP scripts
  • Symfony (recommended via the FtpClientBundle)
  • Laravel
  • CLI workers
  • Cron jobs

No framework dependency.

Production Ready

This library is:

  • Fully covered by unit tests (100%)
  • Integration tested against real FTP / FTPS / SFTP servers
  • Static analysis clean (PHPStan level max (9) + strict rules)
  • Free of global state
  • Designed for deterministic error handling

It is built to be used in critical environments where network instability and safe retries matter.

Installation

composer require cxxi/ftp-client

Requirements

  • PHP 8.2+
  • psr/log ^3.0

Optional extensions:

  • ext-ftp → required for FTP / FTPS
  • ext-ssh2 → required for SFTP

Note: FTPS requires ext-ftp with SSL support (ftp_ssl_connect() available).

Quick Start

use Cxxi\FtpClient\FtpClient;

$client = FtpClient::fromUrl('ftp://user:pass@example.com:21/path');

$client
    ->connect()
    ->loginWithPassword()
    ->listFiles('.');

URL Format

ftp://user:pass@host:21/path
ftps://user:pass@host:21/path
sftp://user:pass@host:22/path

Notes:

  • Credentials may be URL-encoded.
  • Transport is resolved automatically from the scheme.
  • Path becomes the working directory after connection.

Using Connection Options

You can pass a ConnectionOptions instance:

use Cxxi\FtpClient\Model\ConnectionOptions;

$options = new ConnectionOptions(
    timeout: 15,
    retryMax: 3,
    retryDelayMs: 500,
    retryBackoff: 2.0,
    retryJitter: true
);

$client = FtpClient::fromUrl(
    'ftps://user:pass@example.com:21/path',
    options: $options
);

Array format (canonical structure)

You can also build options from an array. The canonical structure groups transport-layer options under dedicated keys, so protocol-specific settings stay isolated and easy to extend.

$options = ConnectionOptions::fromArray([
    'timeout' => 15,
    'passive' => 'auto',

    'retry' => [
        'max' => 3,
        'delay_ms' => 500,
        'backoff' => 2.0,
        'jitter' => true,
        'unsafe_operations' => false,
    ],

    'ssh' => [
        'host_key_algo' => 'ssh-ed25519',
        'expected_fingerprint' => 'MD5:aa:bb:cc:dd:...',
        'strict_host_key_checking' => true,
    ],
]);

Protocol-specific keys are ignored when not applicable (e.g. passive is ignored for SFTP).

Supported Options

timeout

Type: int|null Applies to: FTP / FTPS / SFTP

  • FTP/FTPS → connect timeout
  • SFTP → stream timeout for transfers

passive (FTP / FTPS only)

Type: auto | true | false

  • true → force passive mode
  • false → active mode
  • auto → try passive, fallback to active

Ignored for SFTP.

SFTP Host Key Verification

SFTP supports strict host key verification using MD5 or SHA1 fingerprints (as supported by ext-ssh2).

$options = new ConnectionOptions(
    hostKeyAlgo: 'ssh-ed25519',
    expectedFingerprint: 'MD5:aa:bb:cc:dd:...',
    strictHostKeyChecking: true
);

If strictHostKeyChecking is enabled and fingerprint does not match, connection will fail.

SFTP Fingerprint Limitations (ext-ssh2)

When using the ext-ssh2 extension (PECL ssh2), only the following fingerprint algorithms are available via ssh2_fingerprint():

  • MD5
  • SHA1

The extension does not expose SHA256 fingerprints, even though the underlying libssh2 library supports it.

As a consequence:

  • SHA256: fingerprints (OpenSSH default format) cannot be verified when using ext-ssh2.
  • Only MD5: and SHA1: prefixed fingerprints are supported.
  • There is no automatic fallback between algorithms.

If a SHA256: fingerprint is provided, the connection will fail when strict host key checking is enabled.

This limitation comes from the PHP extension API, not from the library itself.

Retry Policy

Retry is disabled by default (retryMax = 0).

When enabled:

Safe operations are retried:

  • connect
  • login
  • listFiles
  • downloadFile
  • getSize
  • getMTime
  • isDirectory

Unsafe operations are not retried unless explicitly allowed:

  • deleteFile
  • rename
  • chmod
  • removeDirectory
  • removeDirectoryRecursive
  • makeDirectory

Enable unsafe retries:

$options = new ConnectionOptions(
    retryMax: 3,
    retryUnsafeOperations: true
);

Common Operations

Upload / Download

$client->putFile('remote.csv', '/local/file.csv');

$client->downloadFile('remote.csv', '/tmp/remote.csv');

Directory Utilities

$client->isDirectory('subdir');

$client->makeDirectory('subdir', recursive: true);

$client->removeDirectory('empty-dir');

$client->removeDirectoryRecursive('dir-to-delete');

File Utilities

$client->deleteFile('old.csv');

$client->rename('old.csv', 'new.csv');

$size = $client->getSize('file.csv');

$mtime = $client->getMTime('file.csv');

$client->chmod('file.csv', 0644);

FTP-only Advanced Listing

Available only on FTP / FTPS:

$raw = $client->rawList('.', recursive: false);

$mlsd = $client->mlsd('.');

Authentication

Password

$client
    ->connect()
    ->loginWithPassword();

Override credentials:

$client->loginWithPassword('user', 'pass');

SFTP Public Key

$client
    ->connect()
    ->loginWithPubkey(
        '/home/me/.ssh/id_rsa.pub',
        '/home/me/.ssh/id_rsa',
        user: 'my-user'
    );

Only valid for SFTP connections.

Logging

Pass a Psr\Log\LoggerInterface to the factory:

use Cxxi\FtpClient\FtpClient;

$client = FtpClient::fromUrl(
    'ftp://user:pass@example.com:21/path',
    logger: $logger
);

Logged events include:

  • connection attempts
  • authentication
  • transfers
  • destructive operations

Credentials are never logged.

Connection Lifecycle

Connections auto-close on destruction.

For long-running scripts:

$client->closeConnection();

Safe to call even if not connected.

Architecture

The library follows a clean architecture approach:

  • Transport contracts (Contracts)
  • Protocol-specific transports (FTP / SFTP)
  • Infrastructure ports (filesystem, streams, ssh2, ftp)
  • Native adapters
  • Retry wrapper with safe/unsafe semantics

This design allows easy mocking and full unit testing.

Quality & Tests

This library is designed for reliability in production environments.

Unit Tests

  • 100% code coverage on the core domain and infrastructure
  • Strict PHPUnit 11 configuration
  • Full mocking of native adapters
  • Error handling and retry semantics fully tested
  • PHPStan level max (9) clean (source + tests)
  • phpstan-strict-rules enabled
  • bleedingEdge rules enabled

Run unit tests:

composer test:unit

Generate coverage report:

composer test:coverage

Integration Tests

All protocols are tested against real Dockerized servers.

Protocol Server Tested Features
FTP pure-ftpd Active / Passive / Auto mode, transfers, directory ops, MLSD
FTPS pure-ftpd (TLS required) Explicit TLS, transfers, error cases
SFTP OpenSSH Password auth, public key auth, fingerprint verification

Each integration stack is fully isolated and reproducible.

Run individual protocol tests:

composer test:integration:ftp
composer test:integration:ftps
composer test:integration:sftp

Run all integration tests:

composer test:integration

Run everything (unit + integration):

composer test:all

Static Analysis

PHPStan level max (9) with strict rules and bleeding edge enabled:

composer phpstan

Configuration includes:

  • level: max (9)
  • phpstan-strict-rules
  • bleedingEdge.neon
  • treatPhpDocTypesAsCertain: true

Code Style

composer cs
composer cs:check

CI

All checks are enforced in CI:

  • Unit tests (100% coverage)
  • FTP integration tests
  • FTPS (TLS) integration tests
  • SFTP integration tests
  • PHPStan level max (9) + strict rules
  • Coding standards

Every protocol feature documented in this README is covered by automated tests.

Troubleshooting

FTPS: ftp_ssl_connect() not available

Make sure ext-ftp is compiled with SSL support. ftp_ssl_connect() must be available.

SFTP: host key verification fails

  • Ensure the fingerprint prefix matches (MD5: or SHA1:).
  • SHA256 fingerprints are not supported by ext-ssh2.

Passive mode issues (FTP/FTPS)

If transfers hang behind NAT/firewalls:

  • Try forcing passive mode (passive: true)
  • Or use passive: auto

Connection timeouts

Adjust the timeout option depending on network conditions.

Contributing

Contributions are welcome.

Before submitting a pull request:

  1. Ensure all unit tests pass.
  2. Ensure all integration tests pass.
  3. Run PHPStan (level max (9) must remain clean).
  4. Follow existing coding standards.

Useful commands:

composer test:all
composer phpstan
composer cs

Please open an issue first for significant changes or architectural discussions.

Security

If you discover a security vulnerability, please open a GitHub Security Advisory or contact the maintainer privately before disclosing it publicly.

Credentials are never logged by design.

see SECURITY.md.

Roadmap

The project roadmap is maintained separately to keep this README focused and concise.

Full roadmap available here: docs/ROADMAP.md

The roadmap covers:

  • SFTP backend improvements (phpseclib support, SHA256 fingerprints)
  • OpenSSH known_hosts integration
  • FTPS security enhancements (TLS controls, certificate validation)
  • Stream-based API & atomic uploads
  • Transfer progress callbacks
  • Resumable transfers
  • Long-running worker stability improvements
  • Observability & structured events
  • Advanced retry safety
  • Performance & scalability explorations

All roadmap items follow the project principles:

  • Clean architecture
  • Deterministic behavior
  • Strong safety guarantees
  • Backward compatibility (unless major version bump)

Community feedback is welcome via GitHub Issues and Discussions.

License

MIT — see LICENSE.