Skip to content

IO

Input and output abstractions for PHP applications.


The IO package provides Input/Output abstractions to enable interoperability between multiple sources of bytes like streams, buffers, files and sockets.

Installation

Add the Castor repository to your composer.json.

{
"repositories": [{
"type": "composer",
"url": "https://castor-labs.github.io/php-packages"
}]
}

And then install with composer.

Terminal window
composer require castor/io

Motivation

PHP lacks of an proper abstractions to enable Input and Output interoperability. While PSR-7 StreamInterface was a step ahead, it massively violates the Interface Segregation principle by piling up too much behavior into the interface.

This library defines a set of abstractions over Input and Output operations. There are 9 interfaces defined, along with an standard implementation over native PHP resources.

The Stream API

The Stream API implemented by this library functions as a wrapper over the native PHP streams functions. The Castor\Io\Stream class is capable of performing all the operations you can normally perform on a PHP resource. It doesn’t implement every operation possible; just the most common ones. Current operations and their equivalent methods are:

  1. \fread: Castor\Io\Stream::read
  2. \fwrite: Castor\Io\Stream::write
  3. \fclose: Castor\Io\Stream::close
  4. \fseek: Castor\Io\Stream::seek
  5. \fflush: Castor\Io\Stream::flush

Creating a Stream

Creating a Castor\Io\Stream is really easy. There are only two ways of doing it.

Castor\Io\Stream\open is a proxy over \fopen. It takes the path of the file to open and the mode.

use Castor\Io\Stream;
$stream = Stream\open('https://example.com', 'rb');
$stream->read(10, $buff);
echo $buff; // The first 10 bytes of the source of of that website

Castor\Io\Stream\attach takes an already created resource and puts it into the stream class.

use Castor\Io\Stream;
$stream = Stream\attach(STDOUT);
$stream->write('Write something to the standard output');

Working with Castor\Io\Stream

Although the Castor\Io\Stream class implements all the I/O operations possible, it doesn’t implement all the I/O interfaces available in this library. This is because one of the design goals of this library is that the possible operations on a stream are defined at a type-level.

This is the main difference with the approach that other abstractions over streams have in PHP. For instance, in the PSR StreamInterface we have all the possible I/O operations in a single interface, plus methods for checking if those operations are supported on the stream. This breaks a lot of good object oriented design principles.

In order to allow for defining possible operations at a type level, while avoiding writing the logic multiple times, there are 6 internal sub-types of Castor\Io\Stream that implement the actual I/O interfaces. Those types are selected at-runtime according to the capabilities found in the stream by the Castor\Io\Stream\attach function.

For instance, if you call Castor\Io\Stream\attach on STDIN, you will get an instance of Castor\Io\Stream\Read, which extends Castor\Io\Stream but also implements Castor\Io\Reader. You can now use this stream to be a source of bytes on any method or function that takes a Castor\Io\Reader.

In the same fashion, if you call Castor\Io\Stream\attach on the result of \fopen('/some/file.txt', 'w+b'), you will get an instance of Castor\Io\Stream\ReadWriteSeek, which extends Castor\Io\Stream, but also implements Castor\Io\Writer, Castor\Io\Reader and Castor\Io\Seeker. This is because the file was opened in read and write mode, and also because files on the filesystem can be sought.

This capability checking over your streams at runtime makes your application safer by giving you the assurance that the source of data you are dealing with can be read, or written or sought. Also, makes your code easier to test since you are not coupled to an implementation. Finally, it makes your code easier to reason about and work with because the abstractions are simple, and not massive interfaces with 10+ methods.

The Castor\Io\Stream\inMemory function

Tremendously useful in tests, this function returns a Castor\Io\Stream\ReadWriteSeek that can be used to do pretty much any kind of I/O operation. It takes an optional argument called $data to initialize the stream with that data. The stream is automatically rewinded when that initial data is written. The underlying resource follows the rules of the php://memory stream.

use Castor\Io\Stream;
$stream = Stream\inMemory('data');
echo $n = $stream->read(4, $buff); // 4
echo $buff; // "data"

The Castor\Io interfaces

I/O operations have been abstracted in 6 main separate interfaces. It’s important you study them, because these abstractions are used pretty much everywhere in the Castor Packages.

The Castor\Io\Reader interface

The Castor\Io\Reader interface is implemented by all types that can read bytes from a source. The read method takes the number of bytes you want to read, and then the buffer where to allocate those bytes (passed by reference). The method returns the number of bytes read.

use Castor\Io\Stream;
$stream = Stream\inMemory('Hello World');
echo $stream->read(5, $read); // 5
echo $read; // "Hello"

When there are no more bytes to read, Castor\Io\Reader::read will throw a Castor\Io\EndOfFile exception.

use Castor\Io\Stream;
$stream = Stream\inMemory('Hello World');
echo $stream->read(11, $read); // 11
echo $read; // "Hello World"
// This call will throw a Castor\Io\EndOfFile exception.
$stream->read(1, $read);

Any other error in reading (the source is closed or it cannot be read) will throw a Castor\Io\Error exception.

Reading files in this manner is of course very time consuming when you just want to get all the data in a stream. For this we provide an special utility function.

The Castor\Io\Writer interface

TODO

The Castor\Io\Closer interface

TODO

The Castor\Io\Seeker interface

TODO

The Castor\Io\Flusher interface

TODO

The Castor\Io\ReadAt interface

TODO

The Castor\Io\WriteFrom interface

TODO

Functions and Utilities

This library provides a series of functions and utilities to ease some of the most common tasks when performing some I/O operations.

The Castor\Io\readAll function

This function takes a Castor\Io\Reader and reads all of it’s contents into memory until Castor\Io\EndOfFile is thrown.

use Castor\Io\Stream;
use Castor\Io;
$stream = Stream\open('https://example.com', 'rb');
$contents = Io\readAll($stream); // Puts all the contents in memory