Source

This package can be installed separately with composer require phplrt/source

Everything phplrt reads - a grammar file, an expression typed by a user, a template - is wrapped in a source object. It is a thin thing: it knows how to give up its content, and it knows what to call itself when an error points at it.

use Phplrt\Source\File;
use Phplrt\Source\Source;

$fromDisk   = new File(__DIR__ . '/example.txt');
$fromString = new Source('2 + 2');

echo $fromString->content; // "2 + 2"

Why Not Just A String?

Two reasons.

The first is error messages. A parser that receives a bare string can only say "syntax error at offset 42". A parser that receives a File can say:

 --> /app/config/routes.txt:7:12

The second is laziness. A File does not read the disk until somebody asks for its content, and once it has, it remembers - until the file changes on disk, at which point it reads again.

The Kinds of Source

File

A real file on disk.

use Phplrt\Source\File;

$source = new File(__DIR__ . '/grammar.pp3');

echo $source->pathname;   // "/app/grammar.pp3"
echo $source->content;    // the contents of the file
echo $source->size;       // size in bytes
echo $source->modifiedAt; // unix timestamp

if ($source->isExists && $source->isReadable) {
    // ...
}

Source

A string you already have in memory.

use Phplrt\Source\Source;

$source = new Source('2 + 2');

VirtualFile

A string that pretends to be a file. Nothing is read from disk, but errors can still point at a name - handy for code that came from a database, an HTTP request, or a test.

use Phplrt\Source\VirtualFile;

$source = new VirtualFile('user-input.txt', '2 + 2');

echo $source->pathname; // "user-input.txt"
echo $source->content;  // "2 + 2"

Stream

An open resource.

use Phplrt\Source\Stream;
use Phplrt\Source\VirtualFileStream;

$source = new Stream(\fopen('php://input', 'rb'));

// ...or with a name attached
$named = new VirtualFileStream('request.json', \fopen('php://input', 'rb'));

The Factory

If you do not know in advance what you are given, let the factory decide:

use Phplrt\Source\SourceFactory;

$factory = new SourceFactory();

$factory->create('2 + 2');                        // Source
$factory->create(new \SplFileInfo('/app/x.txt')); // File
$factory->create(\fopen('php://memory', 'rb+'));  // Stream

There are explicit methods too, when you do want to be specific:

$factory->createFromFile('/app/x.txt');
$factory->createFromString('2 + 2');
$factory->createFromString('2 + 2', 'virtual.txt'); // VirtualFile
$factory->createFromStream($resource);
$factory->createEmpty();

The Interfaces

Type-hint against these rather than the concrete classes:

use Phplrt\Contracts\Source\FileInterface;
use Phplrt\Contracts\Source\ReadableInterface;

// Anything readable: File, Source, VirtualFile, Stream...
function parse(ReadableInterface $source): mixed { /* ... */ }

// Only the ones that have a name: File, VirtualFile, VirtualFileStream
function report(FileInterface $source): string
{
    return $source->pathname;
}

ReadableInterface gives you two properties:

$source->content; // string
$source->stream;  // resource

Both may throw SourceExceptionInterface - a file can disappear between the moment you name it and the moment you read it.

use Phplrt\Contracts\Source\Exception\SourceExceptionInterface;
use Phplrt\Source\File;

try {
    echo new File('/no/such/file')
        ->content;
} catch (SourceExceptionInterface $e) {
    echo $e->getMessage(); // File "/no/such/file" not found
}

Bring Your Own

ReadableInterface is small on purpose. If your source code lives somewhere unusual - a zip archive, a remote service, a database row - implement it yourself and every other phplrt component will accept it:

use Phplrt\Contracts\Source\FileInterface;

final class DatabaseSource implements FileInterface
{
    public function __construct(
        public readonly string $pathname,
        private readonly \PDO $pdo,
        private readonly int $id,
    ) {}

    public string $content {
        get => $this->pdo
            ->query("SELECT body FROM templates WHERE id = {$this->id}")
            ->fetchColumn();
    }

    public mixed $stream {
        get {
            $stream = \fopen('php://memory', 'rb+');
            \fwrite($stream, $this->content);
            \rewind($stream);

            return $stream;
        }
    }
}