phplrt 4.0

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.

1use Phplrt\Source\File;
2use Phplrt\Source\Source;
3
4$fromDisk   = new File(__DIR__ . '/example.txt');
5$fromString = new Source('2 + 2');
6
7echo $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.

 1use Phplrt\Source\File;
 2
 3$source = new File(__DIR__ . '/grammar.pp3');
 4
 5echo $source->pathname;   // "/app/grammar.pp3"
 6echo $source->content;    // the contents of the file
 7echo $source->size;       // size in bytes
 8echo $source->modifiedAt; // unix timestamp
 9
10if ($source->isExists && $source->isReadable) {
11    // ...
12}

Source

A string you already have in memory.

1use Phplrt\Source\Source;
2
3$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.

1use Phplrt\Source\VirtualFile;
2
3$source = new VirtualFile('user-input.txt', '2 + 2');
4
5echo $source->pathname; // "user-input.txt"
6echo $source->content;  // "2 + 2"

Stream

An open resource.

1use Phplrt\Source\Stream;
2use Phplrt\Source\VirtualFileStream;
3
4$source = new Stream(\fopen('php://input', 'rb'));
5
6// ...or with a name attached
7$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:

1use Phplrt\Source\SourceFactory;
2
3$factory = SourceFactory::createDefault();
4
5$factory->create('2 + 2');                        // Source
6$factory->create(new \SplFileInfo('/app/x.txt')); // File
7$factory->create(\fopen('php://memory', 'rb+'));  // Stream
8$factory->create(new Source('2 + 2'));            // the very same object back

A string is always the source code itself, never a pathname: there is no way to tell one from the other, so a file is referenced by an SplFileInfo.

When you do want to be specific, construct the source yourself - that is what the constructors are for, and it is the only way to reach the named kinds:

1new File('/app/x.txt');
2new Source('2 + 2');
3new VirtualFile('virtual.txt', '2 + 2');
4new Stream($resource);

Drivers

The factory itself knows nothing about the kinds of source; each of them is a driver, and create() hands the argument to every driver in turn until one of them recognizes it. SourceFactory::createDefault() is simply this list:

1use Phplrt\Source\Driver;
2use Phplrt\Source\SourceFactory;
3
4new SourceFactory([
5    new Driver\StringSourceDriver(),      // string       -> Source
6    new Driver\SplFileInfoSourceDriver(), // \SplFileInfo -> File
7    new Driver\StreamSourceDriver(),      // resource     -> Stream
8]);

An argument that already is a source is returned as it is, whatever the drivers are - so create() is safe to call on a value that may or may not have been converted yet.

The first driver that recognizes the argument wins, so prepending your own is how you override a built-in one. A driver returns null for what it does not recognize, and throws for what it recognizes but cannot turn into a source:

 1use Phplrt\Contracts\Source\ReadableInterface;
 2use Phplrt\Source\Driver\SourceDriverInterface;
 3use Phplrt\Source\Stream;
 4use Psr\Http\Message\StreamInterface;
 5
 6final class PsrStreamSourceDriver implements SourceDriverInterface
 7{
 8    public function tryCreate(mixed $source): ?ReadableInterface
 9    {
10        if (!$source instanceof StreamInterface) {
11            return null;
12        }
13
14        return new Stream($source->detach());
15    }
16}

When no driver recognizes the argument, create() throws a NotCreatableException.

The Interfaces

Type-hint against these rather than the concrete classes:

 1use Phplrt\Contracts\Source\FileInterface;
 2use Phplrt\Contracts\Source\ReadableInterface;
 3
 4// Anything readable: File, Source, VirtualFile, Stream...
 5function parse(ReadableInterface $source): mixed { /* ... */ }
 6
 7// Only the ones that have a name: File, VirtualFile, VirtualFileStream
 8function report(FileInterface $source): string
 9{
10    return $source->pathname;
11}

ReadableInterface gives you two properties:

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

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

1use Phplrt\Contracts\Source\Exception\SourceExceptionInterface;
2use Phplrt\Source\File;
3
4try {
5    echo new File('/no/such/file')
6        ->content;
7} catch (SourceExceptionInterface $e) {
8    echo $e->getMessage(); // File "/no/such/file" not found
9}

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:

 1use Phplrt\Contracts\Source\FileInterface;
 2
 3final class DatabaseSource implements FileInterface
 4{
 5    public function __construct(
 6        public readonly string $pathname,
 7        private readonly \PDO $pdo,
 8        private readonly int $id,
 9    ) {}
10
11    public string $content {
12        get => $this->pdo
13            ->query("SELECT body FROM templates WHERE id = {$this->id}")
14            ->fetchColumn();
15    }
16
17    public mixed $stream {
18        get {
19            $stream = \fopen('php://memory', 'rb+');
20            \fwrite($stream, $this->content);
21            \rewind($stream);
22
23            return $stream;
24        }
25    }
26}