Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
72.41% covered (warning)
72.41%
21 / 29
CRAP
93.65% covered (success)
93.65%
118 / 126
RdfWriterBase
0.00% covered (danger)
0.00%
0 / 1
72.41% covered (warning)
72.41%
21 / 29
65.05
93.65% covered (success)
93.65%
118 / 126
 __construct
0.00% covered (danger)
0.00%
0 / 1
3.02
87.50% covered (warning)
87.50%
7 / 8
 newSubWriter
n/a
0 / 0
1
n/a
0 / 0
 registerShorthand
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 prefix
0.00% covered (danger)
0.00%
0 / 1
2.06
75.00% covered (warning)
75.00%
3 / 4
 isShorthand
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 isPrefix
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getPrefixes
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 isValidLanguageCode
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
1 / 1
 sub
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
5 / 5
 getRole
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 write
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 expandShorthand
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
3 / 3
 expandQName
0.00% covered (danger)
0.00%
0 / 1
4.07
83.33% covered (warning)
83.33%
5 / 6
 blank
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 start
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 finish
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
5 / 5
 drain
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
7 / 7
 flattenBuffer
0.00% covered (danger)
0.00%
0 / 1
4.07
83.33% covered (warning)
83.33%
5 / 6
 drainSubs
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 about
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
12 / 12
 a
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 say
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
10 / 10
 is
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
4 / 4
 text
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 value
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
100.00%
22 / 22
 state
0.00% covered (danger)
0.00%
0 / 1
4.02
88.89% covered (warning)
88.89%
8 / 9
 writeSubject
n/a
0 / 0
1
n/a
0 / 0
 writePredicate
n/a
0 / 0
1
n/a
0 / 0
 writeResource
n/a
0 / 0
1
n/a
0 / 0
 writeText
n/a
0 / 0
1
n/a
0 / 0
 writeValue
n/a
0 / 0
1
n/a
0 / 0
 expandSubject
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 expandPredicate
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 expandResource
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 expandType
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
<?php
namespace Wikimedia\Purtle;
use Closure;
use InvalidArgumentException;
use LogicException;
/**
 * Base class for RdfWriter implementations.
 *
 * Subclasses have to implement at least the writeXXX() methods to generate the desired output
 * for the respective RDF constructs. Subclasses may override the startXXX() and finishXXX()
 * methods to generate structural output, and override expandXXX() to transform identifiers.
 *
 * @license GPL-2.0-or-later
 * @author Daniel Kinzler
 */
abstract class RdfWriterBase implements RdfWriter {
    /**
     * @var array An array of strings, RdfWriters, or closures.
     */
    private $buffer = [];
    /**
     * @var RdfWriter[] sub-writers.
     */
    private $subs = [];
    protected const STATE_START = 0;
    protected const STATE_DOCUMENT = 5;
    protected const STATE_SUBJECT = 10;
    protected const STATE_PREDICATE = 11;
    protected const STATE_OBJECT = 12;
    protected const STATE_FINISH = 666;
    /**
     * @var string the current state
     */
    private $state = self::STATE_START;
    /**
     * Shorthands that can be used in place of IRIs, e.g. ("a" to mean rdf:type).
     *
     * @var string[] a map of shorthand names to [ $base, $local ] pairs.
     * @todo Handle "a" as a special case directly. Use for custom "variables" like %currentValue
     *  instead.
     */
    private $shorthands = [];
    /**
     * @var string[] a map of prefixes to base IRIs
     */
    protected $prefixes = [];
    /**
     * @var array pair to store the current subject.
     * Holds the $base and $local parameters passed to about().
     */
    protected $currentSubject = [ null, null ];
    /**
     * @var array pair to store the current predicate.
     * Holds the $base and $local parameters passed to say().
     */
    protected $currentPredicate = [ null, null ];
    /**
     * @var BNodeLabeler
     */
    private $labeler;
    /**
     * Role ID for writers that will generate a full RDF document.
     */
    public const DOCUMENT_ROLE = 'document';
    public const SUBDOCUMENT_ROLE = 'sub';
    /**
     * Role ID for writers that will generate a single inline blank node.
     */
    private const BNODE_ROLE = 'bnode';
    /**
     * Role ID for writers that will generate a single inline RDR statement.
     */
    private const STATEMENT_ROLE = 'statement';
    /**
     * @var string The writer's role, see the XXX_ROLE constants.
     */
    protected $role;
    /**
     * Are prefixed locked against modification?
     * @var bool
     */
    private $prefixesLocked = false;
    /**
     * @param string $role The writer's role, use the XXX_ROLE constants.
     * @param BNodeLabeler|null $labeler
     *
     * @throws InvalidArgumentException
     */
    public function __construct( $role, BNodeLabeler $labeler = null ) {
        if ( !is_string( $role ) ) {
            throw new InvalidArgumentException( '$role must be a string' );
        }
        $this->role = $role;
        $this->labeler = $labeler ?: new BNodeLabeler();
        $this->registerShorthand( 'a', 'rdf', 'type' );
        $this->prefix( 'rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' );
        $this->prefix( 'xsd', 'http://www.w3.org/2001/XMLSchema#' );
    }
    /**
     * @param string $role
     * @param BNodeLabeler $labeler
     *
     * @return RdfWriterBase
     */
    abstract protected function newSubWriter( $role, BNodeLabeler $labeler );
    /**
     * Registers a shorthand that can be used instead of a qname,
     * like 'a' can be used instead of 'rdf:type'.
     *
     * @param string $shorthand
     * @param string $prefix
     * @param string $local
     */
    protected function registerShorthand( $shorthand, $prefix, $local ) {
        $this->shorthands[$shorthand] = [ $prefix, $local ];
    }
    /**
     * Registers a prefix
     *
     * @param string $prefix
     * @param string $iri The base IRI
     *
     * @throws LogicException
     */
    public function prefix( $prefix, $iri ) {
        if ( $this->prefixesLocked ) {
            throw new LogicException( 'Prefixes can not be added after start()' );
        }
        $this->prefixes[$prefix] = $iri;
    }
    /**
     * Determines whether $shorthand can be used as a shorthand.
     *
     * @param string $shorthand
     *
     * @return bool
     */
    protected function isShorthand( $shorthand ) {
        return isset( $this->shorthands[$shorthand] );
    }
    /**
     * Determines whether $shorthand can legally be used as a prefix.
     *
     * @param string $prefix
     *
     * @return bool
     */
    protected function isPrefix( $prefix ) {
        return isset( $this->prefixes[$prefix] );
    }
    /**
     * Returns the prefix map.
     *
     * @return string[] An associative array mapping prefixes to base IRIs.
     */
    public function getPrefixes() {
        return $this->prefixes;
    }
    /**
     * @param string|null $languageCode
     *
     * @return bool
     */
    protected function isValidLanguageCode( $languageCode ) {
        // preg_match is somewhat (12%) slower than strspn but more readable
        return $languageCode !== null && preg_match( '/^[\da-z-]{2,}$/i', $languageCode );
    }
    /**
     * @return RdfWriter
     */
    final public function sub() {
        $writer = $this->newSubWriter( self::SUBDOCUMENT_ROLE, $this->labeler );
        $writer->state = self::STATE_DOCUMENT;
        // share registered prefixes
        $writer->prefixes =& $this->prefixes;
        $this->subs[] = $writer;
        return $writer;
    }
    /**
     * Returns the writers role. The role determines the behavior of the writer with respect
     * to which states and transitions are possible: a BNODE_ROLE writer would for instance
     * not accept a call to about(), since it can only process triples about a single subject
     * (the blank node it represents).
     *
     * @return string A string corresponding to one of the the XXX_ROLE constants.
     */
    final public function getRole() {
        return $this->role;
    }
    /**
     * Appends string to the output buffer.
     * @param string $w
     */
    final protected function write( $w ) {
        $this->buffer[] = $w;
    }
    /**
     * If $base is a shorthand, $base and $local are updated to hold whatever qname
     * the shorthand was associated with.
     *
     * Otherwise, $base and $local remain unchanged.
     *
     * @param string &$base
     * @param string|null &$local
     */
    protected function expandShorthand( &$base, &$local ) {
        if ( $local === null && isset( $this->shorthands[$base] ) ) {
            list( $base, $local ) = $this->shorthands[$base];
        }
    }
    /**
     * If $base is a registered prefix, $base will be replaced by the base IRI associated with
     * that prefix, with $local appended. $local will be set to null.
     *
     * Otherwise, $base and $local remain unchanged.
     *
     * @param string &$base
     * @param string|null &$local
     *
     * @throws LogicException
     */
    protected function expandQName( &$base, &$local ) {
        if ( $local !== null && $base !== '_' ) {
            if ( isset( $this->prefixes[$base] ) ) {
                $base = $this->prefixes[$base] . $local; // XXX: can we avoid this concat?
                $local = null;
            } else {
                throw new LogicException( 'Unknown prefix: ' . $base );
            }
        }
    }
    /**
     * @see RdfWriter::blank()
     *
     * @param string|null $label node label, will be generated if not given.
     *
     * @return string
     */
    final public function blank( $label = null ) {
        return $this->labeler->getLabel( $label );
    }
    /**
     * @see RdfWriter::start()
     */
    final public function start() {
        $this->state( self::STATE_DOCUMENT );
        $this->prefixesLocked = true;
    }
    /**
     * @see RdfWriter::finish()
     */
    final public function finish() {
        // close all unclosed states
        $this->state( self::STATE_DOCUMENT );
        // ...then insert output of sub-writers into the buffer,
        // so it gets placed before the footer...
        $this->drainSubs();
        // and then finalize
        $this->state( self::STATE_FINISH );
        // Detaches all subs.
        $this->subs = [];
    }
    /**
     * @see RdfWriter::drain()
     *
     * @return string RDF
     */
    final public function drain() {
        // we can drain after finish, but finish state is sticky
        if ( $this->state !== self::STATE_FINISH ) {
            $this->state( self::STATE_DOCUMENT );
        }
        $this->drainSubs();
        $this->flattenBuffer();
        $rdf = implode( '', $this->buffer );
        $this->buffer = [];
        return $rdf;
    }
    /**
     * Calls drain() an any RdfWriter instances in $this->buffer, and replaces them
     * in $this->buffer with the string returned by the drain() call. Any closures
     * present in the $this->buffer will be called, and replaced by their return value.
     */
    private function flattenBuffer() {
        foreach ( $this->buffer as &$b ) {
            if ( $b instanceof Closure ) {
                $b = $b();
            }
            if ( $b instanceof RdfWriter ) {
                $b = $b->drain();
            }
        }
    }
    /**
     * Drains all subwriters, and appends their output to this writer's buffer.
     * Subwriters remain usable.
     */
    private function drainSubs() {
        foreach ( $this->subs as $sub ) {
            $rdf = $sub->drain();
            $this->write( $rdf );
        }
    }
    /**
     * @see RdfWriter::about()
     *
     * @param string $base A QName prefix if $local is given, or an IRI if $local is null.
     * @param string|null $local A QName suffix, or null if $base is an IRI.
     *
     * @return RdfWriter $this
     */
    final public function about( $base, $local = null ) {
        $this->expandSubject( $base, $local );
        if ( $this->state === self::STATE_OBJECT
            && $base === $this->currentSubject[0]
            && $local === $this->currentSubject[1]
        ) {
            return $this; // redundant about() call
        }
        $this->state( self::STATE_SUBJECT );
        $this->currentSubject[0] = $base;
        $this->currentSubject[1] = $local;
        $this->currentPredicate[0] = null;
        $this->currentPredicate[1] = null;
        $this->writeSubject( $base, $local );
        return $this;
    }
    /**
     * @see RdfWriter::a()
     * Shorthand for say( 'a' )->is( $type ).
     *
     * @param string $typeBase The data type's QName prefix if $typeLocal is given,
     *        or an IRI or shorthand if $typeLocal is null.
     * @param string|null $typeLocal The data type's  QName suffix,
     *        or null if $typeBase is an IRI or shorthand.
     *
     * @return RdfWriter $this
     */
    final public function a( $typeBase, $typeLocal = null ) {
        return $this->say( 'a' )->is( $typeBase, $typeLocal );
    }
    /**
     * @see RdfWriter::say()
     *
     * @param string $base A QName prefix.
     * @param string|null $local A QName suffix.
     *
     * @return RdfWriter $this
     */
    final public function say( $base, $local = null ) {
        $this->expandPredicate( $base, $local );
        if ( $this->state === self::STATE_OBJECT
            && $base === $this->currentPredicate[0]
            && $local === $this->currentPredicate[1]
        ) {
            return $this; // redundant about() call
        }
        $this->state( self::STATE_PREDICATE );
        $this->currentPredicate[0] = $base;
        $this->currentPredicate[1] = $local;
        $this->writePredicate( $base, $local );
        return $this;
    }
    /**
     * @see RdfWriter::is()
     *
     * @param string $base A QName prefix if $local is given, or an IRI if $local is null.
     * @param string|null $local A QName suffix, or null if $base is an IRI.
     *
     * @return RdfWriter $this
     */
    final public function is( $base, $local = null ) {
        $this->state( self::STATE_OBJECT );
        $this->expandResource( $base, $local );
        $this->writeResource( $base, $local );
        return $this;
    }
    /**
     * @see RdfWriter::text()
     *
     * @param string $text the text to be placed in the output
     * @param string|null $language the language the text is in
     *
     * @return $this
     */
    final public function text( $text, $language = null ) {
        $this->state( self::STATE_OBJECT );
        $this->writeText( $text, $language );
        return $this;
    }
    /**
     * @see RdfWriter::value()
     *
     * @param string $value the value encoded as a string
     * @param string|null $typeBase The data type's QName prefix if $typeLocal is given,
     *        or an IRI or shorthand if $typeLocal is null.
     * @param string|null $typeLocal The data type's  QName suffix,
     *        or null if $typeBase is an IRI or shorthand.
     *
     * @return $this
     */
    final public function value( $value, $typeBase = null, $typeLocal = null ) {
        $this->state( self::STATE_OBJECT );
        if ( $typeBase === null && !is_string( $value ) ) {
            $vtype = gettype( $value );
            switch ( $vtype ) {
                case 'integer':
                    $typeBase = 'xsd';
                    $typeLocal = 'integer';
                    $value = "$value";
                    break;
                case 'double':
                    $typeBase = 'xsd';
                    $typeLocal = 'double';
                    $value = "$value";
                    break;
                case 'boolean':
                    $typeBase = 'xsd';
                    $typeLocal = 'boolean';
                    $value = $value ? 'true' : 'false';
                    break;
            }
        }
        $this->expandType( $typeBase, $typeLocal );
        $this->writeValue( $value, $typeBase, $typeLocal );
        return $this;
    }
    /**
     * State transition table
     * First state is "from", second is "to"
     * @var array
     */
    protected $transitionTable = [
            self::STATE_START => [
                    self::STATE_DOCUMENT => true,
            ],
            self::STATE_DOCUMENT => [
                    self::STATE_DOCUMENT => true,
                    self::STATE_SUBJECT => true,
                    self::STATE_FINISH => true,
            ],
            self::STATE_SUBJECT => [
                    self::STATE_PREDICATE => true,
            ],
            self::STATE_PREDICATE => [
                    self::STATE_OBJECT => true,
            ],
            self::STATE_OBJECT => [
                    self::STATE_DOCUMENT => true,
                    self::STATE_SUBJECT => true,
                    self::STATE_PREDICATE => true,
                    self::STATE_OBJECT => true,
            ],
    ];
    /**
     * Perform a state transition. Writer states roughly correspond to states in a naive
     * regular parser for the respective syntax. State transitions may generate output,
     * particularly of structural elements which correspond to terminals in a respective
     * parser.
     *
     * @param int $newState one of the self::STATE_... constants
     *
     * @throws LogicException
     */
    final protected function state( $newState ) {
        if ( !isset( $this->transitionTable[$this->state][$newState] ) ) {
            throw new LogicException( 'Bad transition: ' . $this->state . ' -> ' . $newState );
        }
        $action = $this->transitionTable[$this->state][$newState];
        if ( $action !== true ) {
            if ( is_string( $action ) ) {
                $this->write( $action );
            } else {
                $action();
            }
        }
        $this->state = $newState;
    }
    /**
     * Must be implemented to generate output that starts a statement (or set of statements)
     * about a subject. Depending on the requirements of the output format, the implementation
     * may be empty.
     *
     * @note $base and $local are given as passed to about() and processed by expandSubject().
     *
     * @param string $base
     * @param string|null $local
     */
    abstract protected function writeSubject( $base, $local = null );
    /**
     * Must be implemented to generate output that represents the association of a predicate
     * with a subject that was previously defined by a call to writeSubject().
     *
     * @note $base and $local are given as passed to say() and processed by expandPredicate().
     *
     * @param string $base
     * @param string|null $local
     */
    abstract protected function writePredicate( $base, $local = null );
    /**
     * Must be implemented to generate output that represents a resource used as the object
     * of a statement.
     *
     * @note $base and $local are given as passed to is() and processed by expandObject().
     *
     * @param string $base
     * @param string|null $local
     */
    abstract protected function writeResource( $base, $local = null );
    /**
     * Must be implemented to generate output that represents a text used as the object
     * of a statement.
     *
     * @param string $text the text to be placed in the output
     * @param string|null $language the language the text is in
     */
    abstract protected function writeText( $text, $language );
    /**
     * Must be implemented to generate output that represents a (typed) literal used as the object
     * of a statement.
     *
     * @note $typeBase and $typeLocal are given as passed to value() and processed by expandType().
     *
     * @param string $value the value encoded as a string
     * @param string|null $typeBase
     * @param string|null $typeLocal
     */
    abstract protected function writeValue( $value, $typeBase, $typeLocal = null );
    /**
     * Perform any expansion (shorthand to qname, qname to IRI) desired
     * for subject identifiers.
     *
     * @param string &$base
     * @param string|null &$local
     */
    protected function expandSubject( &$base, &$local ) {
    }
    /**
     * Perform any expansion (shorthand to qname, qname to IRI) desired
     * for predicate identifiers.
     *
     * @param string &$base
     * @param string|null &$local
     */
    protected function expandPredicate( &$base, &$local ) {
    }
    /**
     * Perform any expansion (shorthand to qname, qname to IRI) desired
     * for resource identifiers.
     *
     * @param string &$base
     * @param string|null &$local
     */
    protected function expandResource( &$base, &$local ) {
    }
    /**
     * Perform any expansion (shorthand to qname, qname to IRI) desired
     * for type identifiers.
     *
     * @param string|null &$base
     * @param string|null &$local
     */
    protected function expandType( &$base, &$local ) {
    }
}