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

sweetrdf / quickRdfIo / #163

13 Jan 2026 12:27PM UTC coverage: 90.824%. Remained the same
#163

push

php-coveralls

zozlak
Merge branch 'master' of github.com:zozlak/quickRdfIO

871 of 959 relevant lines covered (90.82%)

6.43 hits per line

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

94.88
/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 {
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 {
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✔
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✔
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✔
223
            $this->input->rewind();
×
224
        }
225
        $this->nmsp       = [];
4✔
226
        $this->elementIds = [];
4✔
227
        $this->state      = new RdfXmlParserState();
4✔
228
        $this->stack      = [$this->state];
4✔
229
        $this->parseBaseUri($this->baseUriDefault);
4✔
230
        $this->parser     = xml_parser_create_ns('UTF-8', '');
4✔
231
        xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING, 0);
4✔
232
        xml_parser_set_option($this->parser, XML_OPTION_SKIP_TAGSTART, 0);
4✔
233
        xml_parser_set_option($this->parser, XML_OPTION_SKIP_WHITE, 0);
4✔
234
        xml_set_element_handler($this->parser, fn($x, $y, $z) => $this->onElementStart($y, $z), fn($x, $y) => $this->onElementEnd($y));
4✔
235
        xml_set_character_data_handler($this->parser, fn($x, $y) => $this->onCData($y));
4✔
236
        xml_set_start_namespace_decl_handler($this->parser, fn($x, $y, $z) => $this->onNamespaceStart($x, $y, $z));
4✔
237
        xml_set_end_namespace_decl_handler($this->parser, fn($x, $y) => $this->onNamespaceEnd($x, $y));
4✔
238
        $this->triples    = [];
4✔
239
        $this->next();
4✔
240
    }
241

242
    public function valid(): bool {
243
        return key($this->triples) !== null;
4✔
244
    }
245

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

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

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

285
        //echo "START " . $oldState->state . "=>" . $this->state->state . " $name (" . $this->state->literalValueDepth . ")\n";
286
    }
287

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

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

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

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

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

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

351
        // change the state
352
        $this->state->state = RdfXmlParserState::STATE_PREDICATE;
4✔
353
    }
354

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

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

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

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

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

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

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

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

446
        if ($oldState->isCollection) {
4✔
447
            $this->addTriple($oldState->subject, RDF::RDF_REST, RDF::RDF_NIL);
1✔
448
        }
449

450
        //echo "END $oldState->state=>" . $this->state->state . " $name ($oldState->isCDataPredicate,$oldState->literalValueDepth)\n";
451
    }
452

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

461
    private function onNamespaceEnd(XMLParser $parser, string $prefix): void {
462
        array_pop($this->nmsp[$prefix]);
×
463
    }
464

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

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

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

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

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

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

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

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

575
    private function parseBaseUri(string $baseUri): void {
576
        // https://www.w3.org/TR/rdf-syntax-grammar/#section-baseURIs
577
        $bu                 = parse_url($baseUri) ?: [];
4✔
578
        $path               = $bu['path'] ?? '/';
4✔
579
        $query              = isset($bu['query']) ? '?' . $bu['query'] : '';
4✔
580
        $baseUri            = (isset($bu['scheme']) ? $bu['scheme'] . '://' : '') .
4✔
581
            ($bu['host'] ?? '') .
4✔
582
            (isset($bu['port']) ? ':' . $bu['port'] : '');
4✔
583
        $this->baseUriEmpty = $baseUri . $path . $query;
4✔
584
        $path               = explode('/', $path);
4✔
585
        if (!empty(end($path))) {
4✔
586
            $path[count($path) - 1] = '';
2✔
587
        }
588
        $path          = implode('/', $path);
4✔
589
        $this->baseUri = $baseUri . $path;
4✔
590
    }
591
}
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