• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sweetrdf / quickRdfIo / #112

24 Apr 2025 02:36PM UTC coverage: 90.805%. Remained the same
#112

push

php-coveralls

zozlak
Implement rdf-interface 3.1.0

* Implement $baseUri parameter handling in parse() and parseStream() of
  all parsers.
* Util::parse(): pass document base URI to the parser.

(closes #11)

33 of 34 new or added lines in 7 files covered. (97.06%)

21 existing lines in 4 files now uncovered.

869 of 957 relevant lines covered (90.8%)

6.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.93
/src/quickRdfIo/RdfXmlParser.php
1
<?php
2

3
/*
4
 * The MIT License
5
 *
6
 * Copyright 2022 zozlak.
7
 *
8
 * Permission is hereby granted, free of charge, to any person obtaining a copy
9
 * of this software and associated documentation files (the "Software"), to deal
10
 * in the Software without restriction, including without limitation the rights
11
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
 * copies of the Software, and to permit persons to whom the Software is
13
 * furnished to do so, subject to the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be included in
16
 * all copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
 * THE SOFTWARE.
25
 */
26

27
namespace quickRdfIo;
28

29
use XmlParser;
30
use Psr\Http\Message\StreamInterface;
31
use rdfInterface\QuadIteratorInterface as iQuadIterator;
32
use rdfInterface\ParserInterface as iParser;
33
use rdfInterface\QuadInterface as iQuad;
34
use rdfInterface\DataFactoryInterface as iDataFactory;
35
use rdfInterface\BlankNodeInterface as iBlankNode;
36
use rdfInterface\NamedNodeInterface as iNamedNode;
37
use zozlak\RdfConstants as RDF;
38

39
class RdfXmlParserState {
40

41
    const STATE_ROOT       = 'root';
42
    const STATE_NODE       = 'node';
43
    const STATE_PREDICATE  = 'predicate';
44
    const STATE_VALUE      = 'value';
45
    const STATE_XMLLITERAL = 'xmlliteral'; //https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-XML-literals
46

47
    public string $state             = self::STATE_ROOT;
48
    public ?string $datatype          = null;
49
    public string $lang              = '';
50
    public iNamedNode | iBlankNode $subject;
51
    public iNamedNode $predicate;
52
    public ?string $literalValue      = null;
53
    public int $literalValueDepth = 0;
54
    public ?bool $isCDataPredicate  = null;
55
    public bool $isCollection      = false;
56
    public int $sequenceNo        = 1;
57
    public iBlankNode | iNamedNode | null $reifyAs           = null;
58

59
    public function withState(string $state): self {
60
        $copy        = clone($this);
1✔
61
        $copy->state = $state;
1✔
62
        return $copy;
1✔
63
    }
64

65
    public function withSubject(iNamedNode | iBlankNode $subject): self {
66
        $copy          = clone($this);
1✔
67
        $copy->subject = $subject;
1✔
68
        return $copy;
1✔
69
    }
70
}
71

72
/**
73
 * Streaming RDF-XML parser. Fast and with low memory footprint.
74
 * 
75
 * Known deviations from the RDF-XML specification:
76
 * 
77
 * - Doesn't resolve "/.." in relative IRIs.
78
 * - Doesn't support rdf:Seq shorthand syntax
79
 * - Doesn't support rdf:Collection shorthand syntax
80
 *
81
 * @author zozlak
82
 */
83
class RdfXmlParser implements iParser, iQuadIterator {
84

85
    const RDF_ROOT             = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#RDF';
86
    const RDF_ABOUT            = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#about';
87
    const RDF_DATATYPE         = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#datatype';
88
    const RDF_RESOURCE         = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#resource';
89
    const RDF_NODEID           = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nodeID';
90
    const RDF_ID               = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#ID';
91
    const RDF_DESCRIPTION      = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#Description';
92
    const RDF_PARSETYPE        = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#parseType';
93
    const RDF_ABOUTEACHPREFIX  = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#aboutEachPrefix';
94
    const RDF_ABOUTEACH        = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#aboutEach';
95
    const RDF_LI               = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#li';
96
    const RDF_COLLELPREFIX     = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#_';
97
    const PARSETYPE_RESOURCE   = 'Resource';
98
    const PARSETYPE_LITERAL    = 'Literal';
99
    const PARSETYPE_COLLECTION = 'Collection';
100
    const XML_BASE             = 'http://www.w3.org/XML/1998/namespacebase';
101
    const XML_LANG             = 'http://www.w3.org/XML/1998/namespacelang';
102
    const CHUNK_SIZE           = 1000000;
103

104
    use CreateBlankNodeTrait;
105
    use TmpStreamParserTrait;
106

107
    /**
108
     * 
109
     * @var array<string>
110
     */
111
    private static array $skipAttributes = [
112
        self::RDF_ABOUT,
113
        self::RDF_ID,
114
        self::RDF_NODEID,
115
        self::RDF_RESOURCE,
116
        self::RDF_DATATYPE,
117
        self::RDF_PARSETYPE,
118
        self::XML_LANG,
119
    ];
120

121
    /**
122
     * 
123
     * @var array<string>
124
     */
125
    private static array $literalAttributes = [
126
        self::RDF_ID,
127
        self::RDF_DATATYPE,
128
        self::XML_LANG,
129
    ];
130

131
    private iDataFactory $dataFactory;
132
    private StreamInterface $input;
133
    private XmlParser $parser;
134
    private string $baseUriDefault;
135
    private string $baseUriEmpty;
136
    private ?int $key = null;
137

138
    /**
139
     * 
140
     * @var array<string, iNamedNode>
141
     */
142
    private array $elementIds;
143

144
    /**
145
     * 
146
     * @var array<string, array<string>>
147
     */
148
    private array $nmsp;
149

150
    /**
151
     * 
152
     * @var array<RdfXmlParserState>
153
     */
154
    private array $stack;
155
    private RdfXmlParserState $state;
156

157
    /**
158
     * 
159
     * @var array<iQuad>
160
     */
161
    private array $triples;
162

163
    public function __construct(iDataFactory $dataFactory, string $baseUri = '') {
164
        $this->dataFactory    = $dataFactory;
5✔
165
        $this->baseUriDefault = $baseUri;
5✔
166
    }
167

168
    public function __destruct() {
169
        if (isset($this->parser)) {
3✔
170
            xml_parser_free($this->parser);
2✔
171
        }
172
    }
173

174
    public function setBaseUri(string $baseUri): void {
UNCOV
175
        $this->baseUriDefault = $baseUri;
×
176
    }
177

178
    public function current(): iQuad {
179
        return current($this->triples) ?: throw new \OutOfBoundsException();
4✔
180
    }
181

182
    public function key(): int | null {
UNCOV
183
        return key($this->triples) === null ? null : $this->key;
×
184
    }
185

186
    public function next(): void {
187
        $this->key++;
4✔
188
        next($this->triples);
4✔
189
        while (key($this->triples) === null && !$this->input->eof()) {
4✔
190
            $this->triples = [];
4✔
191
            $ret           = xml_parse($this->parser, $this->input->read(self::CHUNK_SIZE) ?: '', false);
4✔
192
            if ($ret !== 1) {
4✔
UNCOV
193
                $this->reportError();
×
194
            }
195
        }
196
        if ($this->input->eof()) {
4✔
197
            $ret = xml_parse($this->parser, '', true);
4✔
198
        }
199
    }
200

201
    /**
202
     * 
203
     * @param resource | StreamInterface $input
204
     * @return iQuadIterator
205
     */
206
    public function parseStream($input, string $baseUri = ''): iQuadIterator {
207
        if (is_resource($input)) {
4✔
208
            $input = new ResourceWrapper($input);
3✔
209
        }
210
        if (!($input instanceof StreamInterface)) {
4✔
UNCOV
211
            throw new RdfIoException("Input has to be a resource or " . StreamInterface::class . " object");
×
212
        }
213

214
        $this->input = $input;
4✔
215
        if (!empty($baseUri)) {
4✔
216
            $this->baseUriDefault = $baseUri;
2✔
217
        }
218
        return $this;
4✔
219
    }
220

221
    public function rewind(): void {
222
        if ($this->input->tell() !== 0) {
4✔
UNCOV
223
            $this->input->rewind();
×
224
        }
225
        if (isset($this->parser)) {
4✔
226
            xml_parser_free($this->parser);
2✔
227
        }
228
        $this->nmsp       = [];
4✔
229
        $this->elementIds = [];
4✔
230
        $this->state      = new RdfXmlParserState();
4✔
231
        $this->stack      = [$this->state];
4✔
232
        $this->parseBaseUri($this->baseUriDefault);
4✔
233
        $this->parser     = xml_parser_create_ns('UTF-8', '');
4✔
234
        xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING, 0);
4✔
235
        xml_parser_set_option($this->parser, XML_OPTION_SKIP_TAGSTART, 0);
4✔
236
        xml_parser_set_option($this->parser, XML_OPTION_SKIP_WHITE, 0);
4✔
237
        xml_set_element_handler($this->parser, fn($x, $y, $z) => $this->onElementStart($y, $z), fn($x, $y) => $this->onElementEnd($y));
4✔
238
        xml_set_character_data_handler($this->parser, fn($x, $y) => $this->onCData($y));
4✔
239
        xml_set_start_namespace_decl_handler($this->parser, fn($x, $y, $z) => $this->onNamespaceStart($x, $y, $z));
4✔
240
        xml_set_end_namespace_decl_handler($this->parser, fn($x, $y) => $this->onNamespaceEnd($x, $y));
4✔
241
        $this->triples    = [];
4✔
242
        $this->next();
4✔
243
    }
244

245
    public function valid(): bool {
246
        return key($this->triples) !== null;
4✔
247
    }
248

249
    /**
250
     * 
251
     * @param string $name
252
     * @param array<string, string> $attribs
253
     * @return void
254
     * @throws RdfIoException
255
     */
256
    private function onElementStart(string $name, array &$attribs): void {
257
        $oldState      = $this->state;
4✔
258
        $this->state   = clone($oldState);
4✔
259
        $this->stack[] = $this->state;
4✔
260

261
        if (isset($attribs[RdfXmlParser::XML_LANG])) {
4✔
262
            $this->state->lang = $attribs[RdfXmlParser::XML_LANG];
3✔
263
        }
264
        $this->state->datatype = $this->resolveIri($attribs[RdfXmlParser::RDF_DATATYPE] ?? null);
4✔
265

266
        switch ($oldState->state) {
4✔
267
            case RdfXmlParserState::STATE_ROOT:
268
                $name === self::RDF_ROOT ? $this->onRoot($attribs) : $this->onNode($name, $attribs);
4✔
269
                break;
4✔
270
            case RdfXmlParserState::STATE_NODE:
271
                $this->onNode($name, $attribs);
4✔
272
                break;
4✔
273
            case RdfXmlParserState::STATE_PREDICATE:
274
                $this->onPredicate($name, $attribs);
4✔
275
                break;
4✔
276
            case RdfXmlParserState::STATE_VALUE:
277
                $this->onNode($name, $attribs);
2✔
278
                $this->state->isCDataPredicate = false;
2✔
279
                $this->state->isCollection     = false;
2✔
280
                break;
2✔
281
            case RdfXmlParserState::STATE_XMLLITERAL:
282
                $this->onXmlLiteralElement($name, $attribs);
1✔
283
                break;
1✔
284
            default:
UNCOV
285
                throw new RdfIoException("Unknown parser state " . $this->state->state);
×
286
        }
287

288
        //echo "START " . $oldState->state . "=>" . $this->state->state . " $name (" . $this->state->literalValueDepth . ")\n";
289
    }
290

291
    /**
292
     * 
293
     * @param array<string, string> $attributes
294
     * @return void
295
     */
296
    private function onRoot(array &$attributes): void {
297
        $this->state->state = RdfXmlParserState::STATE_NODE;
4✔
298
        if (isset($attributes[self::XML_BASE])) {
4✔
299
            $this->parseBaseUri($attributes[self::XML_BASE]);
2✔
300
        }
301
    }
302

303
    /**
304
     * 
305
     * @param string $tag
306
     * @param array<string, string> $attributes
307
     * @return void
308
     */
309
    private function onNode(string $tag, array &$attributes): void {
310
        // standard conformance
311
        if (isset($attributes[self::RDF_ABOUTEACH]) || isset($attributes[self::RDF_ABOUTEACHPREFIX])) {
4✔
312
            throw new RdfIoException("Obsolete attribute '" . (isset($attributes[self::RDF_ABOUTEACH]) ? self::RDF_ABOUTEACH : self::RDF_ABOUTEACHPREFIX) . "' used");
1✔
313
        }
314

315
        // create subject
316
        // https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-blank-nodes
317
        // https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-ID-xml-base
318
        if (isset($attributes[self::RDF_ABOUT])) {
4✔
319
            $subject = $this->dataFactory->namedNode($this->resolveIri($attributes[self::RDF_ABOUT]) ?? '');
3✔
320
        } elseif (isset($attributes[self::RDF_ID])) {
4✔
321
            $subject = $this->handleElementId($attributes[self::RDF_ID]);
2✔
322
        } elseif (isset($attributes[self::RDF_NODEID])) {
4✔
323
            $subject = $this->createBlankNode('_:' . $attributes[self::RDF_NODEID]);
4✔
324
        } else {
325
            $subject = $this->dataFactory->blankNode();
2✔
326
        }
327
        $this->state->subject = $subject;
4✔
328

329
        // type as tag
330
        // https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-typed-nodes
331
        if ($tag !== self::RDF_DESCRIPTION) {
4✔
332
            $this->addTriple($subject, RDF::RDF_TYPE, $tag);
2✔
333
        }
334

335
        // predicates & values as attributes
336
        // https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-property-attributes
337
        $attrToProcess = array_diff(array_keys($attributes), self::$skipAttributes);
4✔
338
        foreach ($attrToProcess as $attr) {
4✔
339
            $this->addTriple($subject, (string) $attr, $attributes[$attr], $this->state->lang, $this->state->datatype ?? '');
2✔
340
        }
341

342
        if ($this->state->isCollection) {
4✔
343
            $prevState            = $this->stack[count($this->stack) - 2];
1✔
344
            $collSubject          = $this->dataFactory->blankNode();
1✔
345
            $this->addTriple($prevState->subject, $this->state->predicate, $collSubject);
1✔
346
            $this->addTriple($collSubject, RDF::RDF_FIRST, $subject);
1✔
347
            $prevState->subject   = $collSubject;
1✔
348
            $prevState->predicate = $this->dataFactory->namedNode(RDF::RDF_REST);
1✔
349
        } elseif ($this->state->state === RdfXmlParserState::STATE_VALUE) {
4✔
350
            $prevState = $this->stack[count($this->stack) - 2];
2✔
351
            $this->addTriple($prevState->subject, $this->state->predicate, $subject);
2✔
352
        }
353

354
        // change the state
355
        $this->state->state = RdfXmlParserState::STATE_PREDICATE;
4✔
356
    }
357

358
    /**
359
     * 
360
     * @param string $tag
361
     * @param array<string, string> $attributes
362
     * @return void
363
     */
364
    private function onPredicate(string $tag, array &$attributes): void {
365
        $this->state->state             = RdfXmlParserState::STATE_VALUE;
4✔
366
        // https://www.w3.org/TR/rdf-syntax-grammar/#emptyPropertyElt
367
        $this->state->isCDataPredicate  = count(array_diff(array_keys($attributes), self::$literalAttributes)) === 0;
4✔
368
        $this->state->literalValue      = '';
4✔
369
        $this->state->literalValueDepth = 0;
4✔
370
        $this->state->predicate         = $this->dataFactory->namedNode($tag);
4✔
371
        $parseType                      = $attributes[self::RDF_PARSETYPE] ?? '';
4✔
372
        $subjectTmp                     = null;
4✔
373
        // https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-parsetype-Collection
374
        if ($parseType === self::PARSETYPE_COLLECTION) {
4✔
375
            $this->state->isCollection = true;
1✔
376
        }
377
        // rdf:li to rdf:_n promotion
378
        // https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-list-elements
379
        if ($tag === self::RDF_LI) {
4✔
380
            $prevState              = $this->stack[count($this->stack) - 2] ?? throw new RdfIoException('Empty stack');
2✔
381
            $this->state->predicate = $this->dataFactory->namedNode(self::RDF_COLLELPREFIX . $prevState->sequenceNo);
2✔
382
            $prevState->sequenceNo++;
2✔
383
        }
384

385
        if (isset($attributes[self::RDF_RESOURCE])) {
4✔
386
            // rdf:resource attribute
387
            $subjectTmp = $this->dataFactory->namedNode($this->resolveIri($attributes[self::RDF_RESOURCE]) ?? '');
3✔
388
            $this->addTriple(null, $this->state->predicate, $subjectTmp);
3✔
389
        } elseif (isset($attributes[self::RDF_NODEID])) {
4✔
390
            // rdf:nodeID attribute
391
            $subjectTmp = $this->createBlankNode($attributes[self::RDF_NODEID]);
3✔
392
            $this->addTriple(null, $this->state->predicate, $subjectTmp);
3✔
393
        }
394

395
        // attributes as nested triples with implicit intermidiate node
396
        // https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-property-attributes-on-property-element
397
        $attrToProcess = array_diff(array_keys($attributes), self::$skipAttributes);
4✔
398
        if (count($attrToProcess) > 0) {
4✔
399
            if ($subjectTmp === null) {
1✔
400
                $subjectTmp = $this->dataFactory->blankNode();
1✔
401
                $this->addTriple(null, $tag, $subjectTmp);
1✔
402
            }
403
            foreach ($attrToProcess as $attr) {
1✔
404
                $this->addTriple($subjectTmp, (string) $attr, $attributes[$attr], $this->state->lang, $this->state->datatype ?? '');
1✔
405
            }
406
        }
407

408
        // implicit blank node due to parseType="Resource"
409
        // https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-parsetype-resource
410
        if ($parseType === self::PARSETYPE_RESOURCE) {
4✔
411
            $blankNode            = $this->dataFactory->blankNode();
1✔
412
            $this->addTriple(null, $tag, $blankNode);
1✔
413
            $this->state          = $this->state->withState(RdfXmlParserState::STATE_PREDICATE)->withSubject($blankNode);
1✔
414
            $this->state->subject = $blankNode;
1✔
415
            $this->stack[]        = $this->state;
1✔
416
        }
417

418
        // XML literal value due to parseType="Literal"
419
        // https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-XML-literals
420
        if ($parseType === self::PARSETYPE_LITERAL) {
4✔
421
            $this->state->state = RdfXmlParserState::STATE_XMLLITERAL;
1✔
422
        }
423

424
        // reification
425
        // https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-reifying
426
        if (isset($attributes[self::RDF_ID])) {
4✔
427
            $this->state->reifyAs = $this->handleElementId($attributes[self::RDF_ID]);
1✔
428
        }
429
    }
430

431
    private function onElementEnd(string $name): void {
432
        /* @var $oldState RdfXmlParserState */
433
        $oldState                      = array_pop($this->stack) ?: throw new RdfIoException('Empty states stack');
4✔
434
        $this->state                   = end($this->stack) ?: throw new RdfIoException('Empty states stack');
4✔
435
        $this->state->isCDataPredicate = $this->state->isCDataPredicate && $oldState->isCDataPredicate;
4✔
436

437
        if ($oldState->state === RdfXmlParserState::STATE_VALUE && $oldState->isCDataPredicate === true) {
4✔
438
            $this->addTriple(null, $oldState->predicate, $oldState->literalValue ?? '', $oldState->lang, $oldState->datatype ?? '', $oldState->reifyAs);
4✔
439
        } elseif ($oldState->state === RdfXmlParserState::STATE_XMLLITERAL) {
4✔
440
            // literal XML
441
            // https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-XML-literals
442
            if ($oldState->literalValueDepth === 0) {
1✔
443
                $this->addTriple($oldState->subject, $oldState->predicate, $oldState->literalValue ?? '', '', RDF::RDF_XML_LITERAL);
1✔
444
            } else {
445
                $this->state->literalValue = $oldState->literalValue . "</" . $this->shorten($name) . ">";
1✔
446
            }
447
        }
448

449
        if ($oldState->isCollection) {
4✔
450
            $this->addTriple($oldState->subject, RDF::RDF_REST, RDF::RDF_NIL);
1✔
451
        }
452

453
        //echo "END $oldState->state=>" . $this->state->state . " $name ($oldState->isCDataPredicate,$oldState->literalValueDepth)\n";
454
    }
455

456
    private function onNamespaceStart(XMLParser $parser, string $prefix,
457
                                      string $uri): void {
458
        if (!isset($this->nmsp[$prefix])) {
4✔
459
            $this->nmsp[$prefix] = [];
4✔
460
        }
461
        $this->nmsp[$prefix][] = $uri;
4✔
462
    }
463

464
    private function onNamespaceEnd(XMLParser $parser, string $prefix): void {
UNCOV
465
        array_pop($this->nmsp[$prefix]);
×
466
    }
467

468
    private function onCData(string $data): void {
469
        if ($this->state->state === RdfXmlParserState::STATE_VALUE && $this->state->isCDataPredicate !== false || $this->state->state === RdfXmlParserState::STATE_XMLLITERAL) {
4✔
470
            $this->state->isCDataPredicate = true;
4✔
471
            $this->state->literalValue     .= $data;
4✔
472
        }
473
    }
474

475
    private function reportError(): void {
UNCOV
476
        $msg  = xml_error_string(xml_get_error_code($this->parser));
×
UNCOV
477
        $line = xml_get_current_line_number($this->parser);
×
UNCOV
478
        $col  = xml_get_current_column_number($this->parser);
×
UNCOV
479
        throw new RdfIoException("Error while parsing the file: $msg in line $line column $col");
×
480
    }
481

482
    private function addTriple(iBlankNode | iNamedNode | null $subject,
483
                               iNamedNode | string $predicate,
484
                               iNamedNode | iBlankNode | string $object,
485
                               ?string $lang = null, ?string $datatype = null,
486
                               iBlankNode | iNamedNode | null $reifyAs = null): void {
487
        $df = $this->dataFactory;
4✔
488

489
        $subject = $subject ?? $this->state->subject;
4✔
490
        if (!($predicate instanceof iNamedNode)) {
4✔
491
            $predicate = $df->namedNode($predicate);
2✔
492
        }
493
        if (!empty($lang) || $datatype !== null) {
4✔
494
            $object = $df->literal($object, $lang, $datatype);
4✔
495
        } elseif (is_string($object)) {
3✔
496
            $object = $df->namedNode($object);
2✔
497
        }
498
        $triple          = $df->quad($subject, $predicate, $object);
4✔
499
        $this->triples[] = $triple;
4✔
500
        //echo "adding $triple\n";
501
        // reification
502
        // https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-reifying
503
        if (!empty($reifyAs)) {
4✔
504
            $this->triples[] = $df->quad($reifyAs, $df->namedNode(RDF::RDF_SUBJECT), $subject);
1✔
505
            $this->triples[] = $df->quad($reifyAs, $df->namedNode(RDF::RDF_PREDICATE), $predicate);
1✔
506
            $this->triples[] = $df->quad($reifyAs, $df->namedNode(RDF::RDF_OBJECT), $object);
1✔
507
            $this->triples[] = $df->quad($reifyAs, $df->namedNode(RDF::RDF_TYPE), $df->namedNode(RDF::RDF_STATEMENT));
1✔
508
        }
509
    }
510

511
    /**
512
     * 
513
     * @param string $name
514
     * @param array<string, string> $attributes
515
     * @return void
516
     */
517
    private function onXmlLiteralElement(string $name, array &$attributes): void {
518
        $name                      = $this->shorten($name);
1✔
519
        $this->state->literalValue .= "<$name";
1✔
520
        if ($this->state->literalValueDepth === 0) {
1✔
521
            foreach ($this->nmsp as $alias => $prefix) {
1✔
522
                $this->state->literalValue .= ' xmlns:' . $alias . '="' . htmlspecialchars(end($prefix) ?: '', ENT_XML1, 'UTF-8') . '"';
1✔
523
            }
524
        }
525
        foreach ($attributes as $k => $v) {
1✔
526
            $this->state->literalValue .= ' ' . $this->shorten($k) . '="' . htmlspecialchars($v, ENT_XML1, 'UTF-8') . '"';
1✔
527
        }
528
        $this->state->literalValue .= ">";
1✔
529
        $this->state->literalValueDepth++;
1✔
530
    }
531

532
    private function shorten(string $uri): string {
533
        $longestPrefix       = '';
1✔
534
        $longestPrefixLength = 0;
1✔
535
        $bestAlias           = '';
1✔
536
        foreach ($this->nmsp as $alias => &$prefixes) {
1✔
537
            $prefix = end($prefixes) ?: '';
1✔
538
            $len    = strlen($prefix);
1✔
539
            if ($len > $longestPrefixLength && str_starts_with($uri, $prefix)) {
1✔
540
                $longestPrefix       = $prefix;
1✔
541
                $longestPrefixLength = $len;
1✔
542
                $bestAlias           = $alias;
1✔
543
            }
544
        }
545
        if (empty($bestAlias)) {
1✔
546
            return $uri;
1✔
547
        }
548
        return $bestAlias . ":" . substr($uri, $longestPrefixLength);
1✔
549
    }
550

551
    /**
552
     * https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-ID-xml-base
553
     * 
554
     * @param string $iri
555
     * @return string
556
     */
557
    private function resolveIri(?string $iri): ?string {
558
        if ($iri === null) {
4✔
559
            return null;
4✔
560
        }
561
        if (preg_match('`^[a-zA-Z][a-zA-Z0-9+-.]*://`', $iri)) {
3✔
562
            return $iri;
3✔
563
        } elseif (empty($iri)) {
2✔
564
            return $this->baseUriEmpty;
1✔
565
        } else {
566
            return $this->baseUri . $iri;
2✔
567
        }
568
    }
569

570
    private function handleElementId(string $id): iNamedNode {
571
        if (isset($this->elementIds[$id])) {
2✔
572
            throw new RdfIoException("Duplicated element id '$id'");
1✔
573
        }
574
        $this->elementIds[$id] = $this->dataFactory->namedNode($this->baseUriEmpty . '#' . $id);
2✔
575
        return $this->elementIds[$id];
2✔
576
    }
577

578
    private function parseBaseUri(string $baseUri): void {
579
        // https://www.w3.org/TR/rdf-syntax-grammar/#section-baseURIs
580
        $bu                 = parse_url($baseUri) ?: [];
4✔
581
        $path               = $bu['path'] ?? '/';
4✔
582
        $query              = isset($bu['query']) ? '?' . $bu['query'] : '';
4✔
583
        $baseUri            = (isset($bu['scheme']) ? $bu['scheme'] . '://' : '') .
4✔
584
            ($bu['host'] ?? '') .
4✔
585
            (isset($bu['port']) ? ':' . $bu['port'] : '');
4✔
586
        $this->baseUriEmpty = $baseUri . $path . $query;
4✔
587
        $path               = explode('/', $path);
4✔
588
        if (!empty(end($path))) {
4✔
589
            $path[count($path) - 1] = '';
2✔
590
        }
591
        $path          = implode('/', $path);
4✔
592
        $this->baseUri = $baseUri . $path;
4✔
593
    }
594
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc