ijanki/ftp-bundle
ijanki/ftp-bundle offers a lightweight FTP integration for Laravel/PHP applications, making it easy to connect to remote servers and perform common file operations. Configure your connection once, then upload, download, list, and manage files through a clean, simple API.
Architecture fit. Unchanged by v4.0.0: this is still not a Laravel package. ijanki/ftp-bundle is a Symfony2 Bundle — Bundle class, DependencyInjection/ extension, Resources/config service definitions aimed at the Symfony 2.x container and event system. None of that machinery is consumed by Laravel's container. What is reusable remains the thin domain layer: a connection object and a client wrapping ext-ftp calls (ftp_connect, ftp_login, ftp_put, ftp_nlist, ftp_mkdir, ftp_delete) with PHP's warning-based failures converted to exceptions — roughly 200–400 lines. The 4.0.0 release note describes a runtime semantics change in PHP itself, not a restructuring of the bundle, which reinforces that the valuable surface area is the wrapper and the scaffolding is inert.
Integration feasibility. Still manual wiring — no ServiceProvider, no publishable config, no facade. The v4.0.0 note is a single line and is verbatim from the PHP 8.1 changelog: ftp_connect now returns an FTP\Connection instance instead of a resource. What that tells us is that the package bumped a major version to accommodate PHP 8.1's return-type change. What it does not tell us — because the changelog does not say — is what actually changed inside: whether public method signatures moved (does an accessor now return FTP\Connection where it previously returned a resource?), whether internal is_resource() guards were replaced, whether the Symfony wiring was touched, or whether there is a CI matrix. Integration is feasible in the same way as before (bind behind our own interface in AppServiceProvider, config from env), but the diff must be read before the spike is scheduled. The release note is a red flag on changelog hygiene, not on capability.
Technical risk. The abandonment profile is unchanged: low star count, no dependents, single maintainer, no downstream population exercising edge cases. v4.0.0 slightly reduces one risk and introduces another.
FTP\Connection change is precisely the kind of edit that breaks guards silently. is_resource() returns false for an FTP\Connection object. Any surviving if (!is_resource($conn)) — in the vendor code or in our own adapter — becomes a permanent false negative on PHP 8.1+. On the connect path that fails loudly (acceptable). On delete/mkdir/list paths it can fail as a silent no-op, which is the exact failure mode already flagged as the dominant operational hazard.^4.0 line. If any part of the fleet is on 8.0, the constraint blocks us, and the fix is not "use 3.x" but "use 3.x and own a divergent fork."Key questions. Updated for the release:
FTP\Connection in their signature, and would that leak into our interface?is_resource() checks actually replaced, or only the ftp_connect call site? Grep the vendor tree for is_resource before adoption.The recommendation does not change on the strength of this release, but the risk on the fork path is now sharper and more specific.
Stack fit. Poor as a drop-in, acceptable as a vendored utility — unchanged. Laravel's filesystem abstraction expects a Flysystem adapter; this package implements nothing of the sort and cannot be registered as a disk driver without a custom adapter. Two paths remain: (a) preferred — league/flysystem-ftp behind Laravel's existing ftp disk driver, same ext-ftp behaviour, maintained dependency, stable API; or (b) justified only if we specifically need this bundle's exception semantics or connection-reuse behaviour that Flysystem does not offer.
Migration path. The sequence now has a mandatory diff-review step before any code is written. Read the 3.x→4.0.0 diff; confirm whether the FTP\Connection change was applied comprehensively or only at the connect call site; then install, confirm autoload and dependency tree in isolation (composer why symfony/*), instantiate manually in tinker against a sandbox FTP endpoint, and wrap in an FtpServiceProvider binding a RemoteFileTransfer interface to a concrete adapter. Critically: the interface must not expose the connection handle. FTP\Connection is a PHP-version-specific type that changed once already; normalizing it at the adapter boundary is what keeps an exit to Flysystem to a one-line rebind and keeps a future PHP version change from rippling through application code. If we take path (a), there is no migration — it is configuration.
Compatibility. Two layers now. The PHP layer: ^4.0 requires PHP ≥ 8.1, and 8.1 also deprecates passing null to non-nullable internal parameters — worth a static-analysis pass over any adapter code. The server layer, which still matters more: passive vs. active mode, chrooted roots, UTF-8 filename handling, inconsistent MLSD/LIST support. Validate against the actual target server, not a local mock. Any Laravel queue worker or Octane process holding a long-lived FTP handle will hit server-side idle timeouts; reconnect-per-job rather than cache.
Sequencing.
FTP\Connection; config from environment variables, credentials never committed.vsftpd or pure-ftpd in CI) covering connect, upload, download, list, delete, and failure paths — including a test that runs on the specific PHP minor we deploy.Maintenance. We own this code, not consume it. v4.0.0 is mildly encouraging — a maintainer alive enough to track a PHP 8.1 breaking change — but a whole major version for a one-line runtime semantic change is evidence of no deprecation policy and no compatibility discipline, which means future PHP releases will likely produce more of these bumps, not fewer. Budget accordingly: a few hours a year for PHP version bumps, plus a mandatory review on each major. Pin to a tagged ^4.0 with a committed lockfile, and keep a UPGRADING note in our repo stating the package is vendored and upstream fixes are not expected. MIT licensing keeps the fork option clean.
Support. No commercial support, no active issue triage. Internal engineers are the support tier. Acceptable for low-criticality batch transfer, unacceptable on a revenue path — which argues for confining it to integration/batch workflows.
Scaling. FTP is the bottleneck, not the PHP code. One TCP connection per client instance, no pooling worth the complexity, throughput dominated by server bandwidth and per-file round trips. Passive mode mandatory behind NAT/load balancers. Queue workers open and explicitly close a connection per job — this is now doubly true, because a cached handle also carries a PHP-version-dependent type that makes any accidental serialization or cross-version debugging harder. Horizontal worker scaling is fine, but watch the FTP server's total and per-IP connection limits. For volume, batch into one session (connect once, transfer many, disconnect).
Failure modes. The important ones, with the release-specific addition:
is_resource() returns false for FTP\Connection. Any surviving resource check, ours or vendored, silently disables an operation on PHP 8.1+. Audit every guard; prefer instanceof or a strict null check.ftp_put can return success on a truncated upload if the control connection dies mid-transfer. Verify size post-transfer via ftp_size; treat mismatch as failure.false with a warning; if exception conversion has gaps, deletes and mkdirs fail quietly. Every call site needs explicit success assertions.Ramp-up. Low for a PHP developer — the API is essentially the ext-ftp function set with exceptions, so a competent engineer is productive in under a day. Add one line to the onboarding: on PHP 8.1+, the connection is an object, not a resource, and every is_resource reflex is wrong. The real ramp-up remains operational — the target server's mode, permissions, root layout, and timeout behaviour, plus the discipline of verifying transfers rather than trusting return codes. That belongs in a runbook, and it is the same runbook the Flysystem path needs — which remains the strongest argument for choosing the maintained dependency and saving the fork for the genuinely unavoidable cases.
How can I help you explore Laravel packages today?