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

File Laravel Package

hoa/file

Hoa\File is a PHP library for robust filesystem access, offering object-oriented abstractions for files, directories, links, sockets, and streams. It supports reading, writing, locking, permissions, iterators, and exceptions, making file operations safer and more consistent.

View on GitHub
Deep Wiki
Context7

Getting Started

hoa/file is a standalone OOP wrapper around PHP's stream functions. There is no service provider, facade, config file, or publishable asset — it is plain PHP objects you new up. That is the whole integration story, and it is also the main reason to think twice before adopting it.

Install

composer require hoa/file

This pulls in the Hoa dependency chain (hoa/stream, hoa/consistency, hoa/exception, hoa/event, hoa/protocol). All are abandoned; pin the resolved versions in composer.lock deliberately.

Look here first

  1. Hoa\File\Generic — the method surface every other class inherits: open(), close(), read(), readLine(), readAll(), write(), writeLine(), seek(), tell(), eof(), lock(), unlock(), copy(), move(), delete(), getSize(), getMTime().
  2. The concrete subclasses: Read, Write, ReadWrite, Temporary, Directory, Link, Finder.
  3. Hoa\File\Finder only if you don't already have symfony/finder (you do — it ships with Laravel).

First use case: stream a file instead of file_get_contents()

use Hoa\File\Read;

$file = new Read(storage_path('app/imports/customers.csv'));

try {
    while (!$file->eof()) {
        $line = rtrim($file->readLine(), "\r\n");
        // process one row, constant memory
    }
} finally {
    $file->close();
}

Writing

use Hoa\File\Write;

$out = new Write(storage_path('app/reports/summary.txt'));
$out->writeLine('rows: ' . $count);
$out->close();

Verify from php artisan tinker before wiring it into a command or job. If that two-minute smoke test fails on your PHP version, stop there — see Gotchas.


Implementation Patterns

1. Wrap it in a tiny service, bind it once

Don't scatter new Read(...) through controllers. One factory class gives you a single place to swap in native PHP later when you drop the package.

namespace App\Services;

use Hoa\File\Read;
use Hoa\File\Write;
use Hoa\File\ReadWrite;

class HoaFile
{
    public function read(string $absolutePath): Read
    {
        return new Read($absolutePath);
    }

    public function write(string $absolutePath): Write
    {
        return new Write($absolutePath); // truncates on construct — see Gotchas
    }

    public function append(string $absolutePath): ReadWrite
    {
        return new ReadWrite($absolutePath, null, ReadWrite::MODE_APPEND);
    }
}

Bind it in AppServiceProvider::register() as a singleton if it is stateless (it is). Do not build a Facade for it; injection is clearer and keeps the dependency visible.

2. Bridge from Laravel's filesystem to a real path

Hoa only understands local streams. Use Laravel to resolve the path, Hoa to do the I/O:

$path = Storage::disk('local')->path("imports/{$id}.csv");
$file = (new HoaFile)->read($path);

This does not work for s3, ftp, or any non-local disk — those need Storage::readStream(). Keep the boundary explicit.

3. Chunked import in a queued job

The one genuinely useful pattern: constant-memory reads plus resumability via seek()/tell().

public function handle(HoaFile $files): void
{
    $file = $files->read(Storage::disk('local')->path($this->relativePath));
    $offset = $this->resumeOffset;

    try {
        if ($offset > 0) {
            $file->seek($offset);
        }

        while (!$file->eof()) {
            $this->ingest(rtrim($file->readLine(), "\r\n"));
            $this->resumeOffset = $file->tell();
        }
    } finally {
        $file->close();
    }
}

Persist resumeOffset every N rows so a retried job skips already-processed data.

4. Locked append for cross-process logs/audit trails

Hoa's lock()/unlock() are the reason some teams reach for this package at all:

$fh = (new HoaFile)->append(storage_path('logs/audit.log'));

try {
    $fh->lock(\LOCK_EX);
    $fh->writeLine(json_encode($event));
} finally {
    $fh->unlock();
    $fh->close();
}

LOCK_EX + finally guarantees workers don't write interleaved lines. Only trustworthy on a local filesystem — see Gotchas.

5. Finder inside an Artisan command

If you're already committed to Hoa, using its Finder keeps one style in the command:

$finder = (new \Hoa\File\Finder())
    ->in(storage_path('app/inbox'))
    ->name('*.csv');

foreach ($finder as $file) {
    $this->line('queued: ' . $file->getRealPath());
    ImportJob::dispatch($file->getRealPath());
}

If you are not already committed, use symfony/finder — identical API surface, actively maintained.

6. Testing

Hoa hits the real filesystem, so Storage::fake('local') is your friend; combine with Storage::path() and write to that directory. Close every handle in tearDown() — a leaked handle causes "too many open files" in the test process and masks the real failure.


Gotchas and Tips

The elephant: it is archived and untested on modern PHP

Last release 2017-07-11, zero dependents, no maintainer. The code targets PHP 5.5–7.x. Before committing any of the above, run your suite on your actual runtime (PHP 8.1/8.2/8.3): expect deprecation notices and possibly fatals inside hoa/consistency and hoa/event before hoa/file is even reached. If the smoke test fails, the correct decision is SplFileObject or Illuminate\Filesystem\Filesystem — they cover readLine, seek, tell, eof, and flock with zero dependencies.

Exceptions don't fit Laravel

Hoa\File\Exception extends Hoa\Exception\Exception, not RuntimeException. It will not be classified by your Handler the way Laravel exceptions are. Catch at your service boundary and rethrow:

try {
    $file->readAll();
} catch (\Hoa\Exception\Exception $e) {
    throw new \RuntimeException("Cannot read {$path}", 0, $e);
}

Hoa's exceptions also carry rich internal attributes — log (string) $e rather than $e->getMessage() if you need the full trace context.

new Write($path) truncates on construction

The file is opened in wb mode in the constructor, before you write a single byte. Never use Write for read-modify-write: if the process dies between construct and write you have an empty file. Use ReadWrite with MODE_APPEND, or write to Hoa\File\Temporary and move() into place.

Paths resolve against CWD and the hoa:// protocol

Relative paths are not resolved against base_path(). Under php artisan serve the CWD is public/; under a queue worker it's the project root. Always pass absolute paths from base_path(), storage_path(), or Storage::path(). If hoa:// entries appear in stack traces, someone is relying on Hoa\Protocol resolution — a singleton that will persist across requests under Octane. Don't mutate it at runtime.

Locking is advisory and local-only

flock()-based locks are fine on ext4/APFS, unreliable-to-broken on NFS and meaningless on object storage. lock() without a matching unlock() in finally will wedge a queue worker on the handle. Treat the pair as strictly bracketed.

Newlines are platform-dependent

writeLine() appends the value of Generic::getNewLine() (\n on Unix, \r\n on Windows), and readLine() keeps the terminator. On a mixed dev/prod fleet this silently changes file bytes between environments. rtrim() on read; pass explicit "\n" when you control the format. Windows CI will catch this, nothing else will.

readAll() is file_get_contents() with more ceremony

It materialises the whole file in memory. For anything that might exceed memory_limit, loop eof()/readLine() or read() with a fixed-length buffer (setStreamBucketSize() tunes the underlying stream bucket). Also note every operation dispatches an event through hoa/event — benchmark against raw fgets before putting this in a hot loop over millions of rows; it is measurably slower.

Handles leak in long-running processes

Generic::__destruct() closes the stream, but destructors are lazy in long-running workers and a throw during shutdown is ugly. Close explicitly in finally, every time. The underlying resource is reachable through the stream layer (getStream()) if you need stream_get_meta_data() to debug a stubborn handle.

Debugging checklist

When Hoa throws, check in this order: (1) resolved absolute path is what you think — dd($path); (2) is_file() / is_readable() / parent dir is_writable(); (3) the file isn't already open elsewhere with LOCK_EX; (4) PHP's error_get_last() — Hoa sometimes lets a native warning fire before wrapping it. Hoa\File\Directory and Hoa\File\Link exist but add nothing over RecursiveDirectoryIterator/readlink().

Extension points, if you must

Subclass Hoa\File\Generic (or Read/Write) to attach domain behaviour — e.g. CsvWrite extends Write with a typed writeRow(array $row). Custom stream_wrapper_register() wrappers are picked up by Hoa's stream layer. These are real extension points; just remember you're extending abandoned code, so keep the subclass thin enough to port to SplFileObject in an afternoon.

Composer hygiene

Five abandoned transitive packages, no security process, composer audit will be quiet only because nobody is looking. If you keep this, consider vendoring/forking under a private namespace so a future composer update doesn't silently break — and add a // TODO: replace with SplFileObject on the service class the day you write it.

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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource