cxxi / ftp-client
Pure PHP FTP/FTPS/SFTP client (framework agnostic).
Requires
- php: >=8.2
- psr/log: ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.94
- phpstan/phpstan: ^2.1
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^11.0
Suggests
- ext-ftp: Required for FTP/FTPS operations.
- ext-ssh2: Required for SFTP operations.
README
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?
- Installation
- Requirements
- Quick Start
- URL Format
- Using Connection Options
- Supported Options
- Retry Policy
- Common Operations
- Authentication
- Logging
- Connection Lifecycle
- Architecture
- Quality & Tests
- Troubleshooting
- Contributing
- Security
- Roadmap
- License
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 / FTPSext-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 modefalse→ active modeauto→ 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():
MD5SHA1
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:andSHA1: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:orSHA1:). - 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:
- Ensure all unit tests pass.
- Ensure all integration tests pass.
- Run PHPStan (level max (9) must remain clean).
- 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_hostsintegration - 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.