Skip to content

Application · Adapter

File Transfer

Move bytes through a remote FTP or SFTP session while keeping credentials and protocol behavior outside application code.

The application chooses the remote operation; the adapter owns connection state, protocol errors, and resource translation.

File Transfer is for stateful remote protocol sessions. It is not the FileStorage object-store contract and it is not the local Filesystem path API. Depend on FileTransport directly or register named transports in FileTransferService when one use case must address more than one endpoint.

A transport-abstraction layer for remote file operations. The FileTransport port covers sending, retrieving, and listing files on a remote host. FileTransferService acts as a named registry when an application needs to talk to multiple remote endpoints.

Application\FileTransfer
├── FileTransferService                 — Named registry of FileTransport instances
├── Transport\
│   └── FileTransport (interface)       — sendFile(), retrieveFileContents(),
│                                          retrieveFileResource(), readDirectory()
├── Resource\
│   ├── Resource                        — Immutable file/directory descriptor
│   └── ResourceType (enum: string)     — FILE, DIR, LINK, FIFO, CHAR, BLOCK, SOCKET, UNKNOWN
└── Exception\
    └── FileTransferException           — extends SystemException

Adapter\FileTransfer
├── Sftp\
│   └── SftpFileTransport               — FileTransport → phpseclib3 SFTP
├── Ftp\
│   └── FtpFileTransport                — FileTransport → PHP FTP extension
├── Logging\
│   └── LoggingFileTransport            — Decorator: logs operations then delegates
└── Null\
    └── NullFileTransport               — No-op (tests / dev)

Table of Contents

  1. FileTransport
  2. Resource
  3. ResourceType
  4. FileTransferService
  5. Adapters
  6. Symfony Configuration
  7. Usage Examples

FileTransport

Fight\Common\Application\FileTransfer\Transport\FileTransport

interface FileTransport
{
    /** @throws FileTransferException */
    public function sendFile(string $path, mixed $contents): void;

    /** @throws FileTransferException */
    public function retrieveFileContents(string $path): string;

    /** @throws FileTransferException */
    public function retrieveFileResource(string $path): mixed;   // returns resource

    /**
     * @return iterable<Resource>
     * @throws FileTransferException
     */
    public function readDirectory(string $directory): iterable;
}

$contents in sendFile() accepts a string or a stream resource. retrieveFileResource() returns an open stream resource positioned at the start of the file.


Resource

Fight\Common\Application\FileTransfer\Resource\Resource

An immutable value object describing a single remote entry returned by readDirectory().

final readonly class Resource implements Stringable
{
    public function __construct(
        string $path,
        int $size,
        int $userId,
        int $groupId,
        int $mode,
        DateTimeImmutable $accessTime,
        DateTimeImmutable $modifyTime,
        ResourceType $type
    ) {}
}

Accessors

Method Returns Description
path() string Full remote path (trimmed)
size() int Size in bytes
userId() int Owning user ID
groupId() int Owning group ID
mode() int Octal permission mode as integer
permissions() string 4-character octal string e.g. '0644'
accessTime() DateTimeImmutable Last access time
modifyTime() DateTimeImmutable Last modification time
type() ResourceType Entry type
__toString() string Alias for path()

ResourceType

Fight\Common\Application\FileTransfer\Resource\ResourceType

enum ResourceType: string
{
    case FILE    = 'file';
    case DIR     = 'dir';
    case LINK    = 'link';
    case FIFO    = 'fifo';
    case CHAR    = 'char';
    case BLOCK   = 'block';
    case SOCKET  = 'socket';
    case UNKNOWN = 'unknown';
}

FileTransferService

Fight\Common\Application\FileTransfer\FileTransferService

A named registry for multiple FileTransport instances. Useful when an application connects to more than one remote host or protocol simultaneously.

$service = new FileTransferService();
$service->addTransport('sftp-primary', $sftpTransport);
$service->addTransport('ftp-backup',   $ftpTransport);

$transport = $service->getTransport('sftp-primary');
$transport->sendFile('/uploads/report.csv', $csvData);

Throws FileTransferException if a duplicate key is registered. Throws KeyException if the requested key is not found.


Adapters

FTP and SFTP are explicit protocol choices. Prefer SFTP when both endpoints support it, verify the remote host key outside this package, keep credentials out of committed configuration, and set connection timeouts in the configured client. The adapters translate provider failures to FileTransferException; they do not retry, roll back, checksum, or make multi-step operations atomic.

SftpFileTransport

Fight\Common\Adapter\FileTransfer\Sftp\SftpFileTransport

Wraps a phpseclib3\Net\SFTP connection. The SFTP object is constructed and authenticated externally then injected:

use phpseclib3\Net\SFTP;
use Fight\Common\Adapter\FileTransfer\Sftp\SftpFileTransport;

$sftp = new SFTP('sftp.example.com');
$sftp->login('user', 'password');

$transport = new SftpFileTransport($sftp);

readDirectory() uses SFTP::rawlist() and maps phpseclib's numeric type codes to ResourceType values (1 → FILE, 2 → DIR, 3 → LINK, anything else → UNKNOWN).

FtpFileTransport

Fight\Common\Adapter\FileTransfer\Ftp\FtpFileTransport

Uses PHP's built-in FTP extension. Manages the connection lifecycle internally — connects on first use and disconnects after each operation.

use Fight\Common\Adapter\FileTransfer\Ftp\FtpFileTransport;

$transport = new FtpFileTransport(
    host:     'ftp.example.com',
    port:     21,
    username: 'ftpuser',
    password: 'secret',
    ssl:      true,    // use ftp_ssl_connect (default false)
    timeout:  90,      // seconds (default 90)
    passive:  true     // PASV mode (default false)
);

sendFile() creates missing parent directories automatically. readDirectory() uses ftp_mlsd() for structured directory listings. Requires the PHP ftp extension and libssl for SSL connections.

LoggingFileTransport

Fight\Common\Adapter\FileTransfer\Logging\LoggingFileTransport

Decorator that logs the path of each operation via PSR-3 before delegating:

$transport = new LoggingFileTransport(
    new SftpFileTransport($sftp),
    $logger,
    LogLevel::INFO   // default DEBUG
);

Log channels: - sendFile[FileTransfer]: Sending file with path - retrieveFileContents / retrieveFileResource[FileTransfer]: Retrieving file contents/resource with path - readDirectory[FileTransfer]: Reading directory with path

NullFileTransport

Fight\Common\Adapter\FileTransfer\Null\NullFileTransport

Silent no-op adapter. sendFile() does nothing. retrieveFileContents() returns ''. retrieveFileResource() returns an empty php://memory stream. readDirectory() returns an empty array. Useful in tests and local development.

Because it records nothing, NullFileTransport proves only that transfer was suppressed. Use a consumer-owned spy when a test must assert the path or contents sent.


Symfony Configuration

# config/packages/common_file_transfer.yaml

services:
    _defaults:
        autowire: true
        autoconfigure: true

    # --- SFTP ---
    phpseclib3\Net\SFTP:
        factory: ['phpseclib3\Net\SFTP', 'new']
        arguments: ['%env(SFTP_HOST)%']
        calls:
            - [login, ['%env(SFTP_USER)%', '%env(SFTP_PASSWORD)%']]

    Fight\Common\Adapter\FileTransfer\Sftp\SftpFileTransport:
        arguments:
            - '@phpseclib3\Net\SFTP'

    Fight\Common\Adapter\FileTransfer\Logging\LoggingFileTransport:
        decorates: Fight\Common\Adapter\FileTransfer\Sftp\SftpFileTransport
        arguments:
            - '@.inner'
            - '@logger'
            - 'info'

    # --- Registry ---
    Fight\Common\Application\FileTransfer\FileTransferService:
        calls:
            - [addTransport, ['sftp', '@Fight\Common\Adapter\FileTransfer\Sftp\SftpFileTransport']]

    # --- Interface alias ---
    Fight\Common\Application\FileTransfer\Transport\FileTransport:
        alias: Fight\Common\Adapter\FileTransfer\Sftp\SftpFileTransport

Usage Examples

Uploading a File

use Fight\Common\Application\FileTransfer\Transport\FileTransport;

class ReportExporter
{
    public function __construct(private FileTransport $transport) {}

    public function export(Report $report): void
    {
        $this->transport->sendFile(
            '/exports/reports/'.$report->filename(),
            $report->toCsv()
        );
    }
}

Downloading a File

$contents = $transport->retrieveFileContents('/reports/2026-06.csv');

// As a stream resource
$stream = $transport->retrieveFileResource('/reports/2026-06.csv');

Listing a Directory

foreach ($transport->readDirectory('/uploads') as $resource) {
    if ($resource->type() === ResourceType::FILE) {
        echo sprintf(
            '%s  %s  %d bytes',
            $resource->permissions(),
            $resource->path(),
            $resource->size()
        );
    }
}

Using Multiple Transports

$service = new FileTransferService();
$service->addTransport('sftp',   $sftpTransport);
$service->addTransport('backup', $ftpTransport);

// Route by business logic
$service->getTransport($isPrimary ? 'sftp' : 'backup')
    ->sendFile('/data/'.$filename, $contents);

Testing with NullFileTransport

use Fight\Common\Adapter\FileTransfer\Null\NullFileTransport;

$transport = new NullFileTransport();
$transport->sendFile('/any/path', 'data');      // no-op
$contents = $transport->retrieveFileContents('/any/path');  // ''