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\FileSource; 2use Phplrt\Source\StringSource; 3 4$fromDisk = FileSource::createFromPathname(__DIR__ . '/example.txt'); 5$fromString = StringSource::createFromString('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 FileSource can
say:
--> /app/config/routes.txt:7:12
The second is laziness. A FileSource does not touch the disk until somebody
asks it to, and the file it opens then belongs to it until the source itself
is gone.
The Kinds of Source
Every source is named after what it reads, and every one of them can be read in any order and any number of times.
| Class | Reads |
|---|---|
StringSource |
a string in memory |
FileSource |
a real file on disk |
ResourceSource |
an open resource |
VirtualSource |
another source, under a pathname |
FileSource
A real file on disk.
1use Phplrt\Source\FileSource; 2 3$source = FileSource::createFromPathname(__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}
StringSource
A string you already have in memory.
1use Phplrt\Source\StringSource; 2 3$source = StringSource::createFromString('2 + 2');
VirtualSource
Any other source, pretending 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\StringSource; 2use Phplrt\Source\VirtualSource; 3 4$source = VirtualSource::createFromString('user-input.txt', '2 + 2'); 5 6echo $source->pathname; // "user-input.txt" 7echo $source->content; // "2 + 2"
The pathname is the only thing it adds - everything read comes from the source it wraps, whatever kind that is. A name over a resource is the same class:
$named = VirtualSource::createFromResourceStream('request.json', \fopen('php://input', 'rb'));
There is one for a real file as well, which is how a grammar read from disk gets reported under the name it was included as:
$named = VirtualSource::createFromPathname('/app/grammar.pp3');
ResourceSource
An open resource.
1use Phplrt\Source\ResourceSource; 2 3$source = ResourceSource::createFromResource(\fopen('php://input', 'rb')); 4 5echo $source->isSeekable; // whether the resource can be rewound 6echo $source->uri; // "php://input", or null for a resource without one 7echo $source->mode; // "rb"
The resource has to be open for reading: one opened for writing alone is rejected right away rather than at the moment somebody tries to read it.
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'); // StringSource 6$factory->create(new \SplFileInfo('/app/x.txt')); // FileSource 7$factory->create(\fopen('php://memory', 'rb+')); // ResourceSource 8$factory->create(StringSource::createFromString('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:
1FileSource::createFromPathname('/app/x.txt'); 2StringSource::createFromString('2 + 2'); 3VirtualSource::createFromString('virtual.txt', '2 + 2'); 4ResourceSource::createFromResource($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 -> StringSource 6 new Driver\SplFileInfoSourceDriver(), // \SplFileInfo -> FileSource 7 new Driver\ResourceSourceDriver(), // resource -> ResourceSource 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\ResourceSource; 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 ResourceSource::createFromResource($source->detach()); 15 } 16}
When no driver recognizes the argument, create() throws a
SourceExceptionInterface.
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: FileSource, StringSource, ResourceSource... 5function parse(ReadableInterface $source): mixed { /* ... */ } 6 7// Only the ones that have a name: FileSource, VirtualSource... 8function report(FileInterface $source): string 9{ 10 return $source->pathname; 11}
ReadableInterface is the whole of the source and any fragment of it:
1$source->content; // string - the whole source 2$source->read(0, 4096) // string - at most 4096 bytes located at offset 0
The length of a source is not among them. A pipe has no length until it ends,
so only some of the sources can answer that question, and it is asked of those
alone - FileSource::$size answers it with filesize().
All of them 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\FileSource; 3 4try { 5 echo FileSource::createFromPathname('/no/such/file') 6 ->content; 7} catch (SourceExceptionInterface $e) { 8 echo $e->getMessage(); // File "/no/such/file" not found 9}
Reading In Chunks
$content gives you everything at once, which is fine until the source is
larger than the memory you are willing to spend on it. read() takes the
fragment you name and nothing else:
1for ($offset = 0;; $offset += \strlen($chunk)) { 2 $chunk = $source->read($offset, 4096); 3 4 if ($chunk === '') { 5 break; 6 } 7 8 // ... 9}
read() returns up to the number of bytes you asked for and an empty string
once the offset is at the end of the source, which is what tells you the data
is over.
Reading Twice
Reading a source leaves it as it is, so any fragment can be taken out of it again, in any order:
1$source = StringSource::createFromString('2 + 2'); 2 3$source->read(0, 2); // '2 ' 4$source->read(2, 3); // '+ 2' 5$source->read(0, 2); // '2 ' again 6$source->content; // '2 + 2' - the whole of it, whatever has been read
This holds for a pipe, a socket and php://input as well, none of which can
be rewound: such a source keeps everything it has given away, so the memory it
costs is the memory of the data that has been read out of it.
1$input = ResourceSource::createFromResource(\fopen('php://input', 'rb')); 2 3$input->content; // reads the input 4$input->content; // the very same data
A source built over a resource begins where the resource has been left at the moment it has been given away, so a resource that has already been read in part is the source of what is left in it:
1$stream = \fopen('/app/x.txt', 'rb'); // "2 + 2" 2\fseek($stream, 2); 3 4$source = ResourceSource::createFromResource($stream); 5 6$source->content; // '+ 2' 7$source->read(0, 1); // '+'
Who Closes The Resource
A ResourceSource never closes a resource it did not open - the one who
opened it is the one to close it. Construct it with autoclose: true when you
would rather hand that duty over:
1$owned = new ResourceSource(\fopen('/app/x.txt', 'rb'), autoclose: true); 2 3unset($owned); // the file is closed here
A FileSource opens a file of its own the first time it is read and closes it
along with itself, so a file is held only for as long as the source that names
it is alive.
Bring Your Own
If your source code lives somewhere unusual - a zip archive, a remote service,
a database row - name it with a VirtualSource over the data you already hold,
and every other phplrt component will accept it:
1use Phplrt\Source\VirtualSource; 2 3final class TemplateSources 4{ 5 public function __construct( 6 private readonly \PDO $pdo, 7 ) {} 8 9 public function createFromId(int $id): VirtualSource 10 { 11 $body = $this->pdo 12 ->query("SELECT body FROM templates WHERE id = {$id}") 13 ->fetchColumn(); 14 15 return VirtualSource::createFromString("template://{$id}", $body); 16 } 17}
The pathname is the only thing a VirtualSource adds; everything read comes
from the source it wraps, so a string, an open resource or a real file all
work as the thing underneath.
Implementing ReadableInterface by hand is only worth it when the data
arrives in chunks of its own, such as a paged HTTP response - $content and
read() are what the rest of phplrt will call, and holding on to the pages
that have already arrived is what makes them answerable twice.
The two members are all there is to it, and they live in a package of their own - see Contracts for what an implementation has to promise and for the factory contract next to it.