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}