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

sweetrdf / quickRdfIo / #164

18 Mar 2026 01:02PM UTC coverage: 90.776% (-0.05%) from 90.824%
#164

push

php-coveralls

zozlak
NQuadsSerializer: fix graphs handling

1 of 1 new or added line in 1 file covered. (100.0%)

14 existing lines in 2 files now uncovered.

866 of 954 relevant lines covered (90.78%)

6.45 hits per line

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

94.84
/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 setBaseUri(string $baseUri): void {
UNCOV
169
        $this->baseUriDefault = $baseUri;
×
170
    }
171

172
    public function current(): iQuad {
173
        return current($this->triples) ?: throw new \OutOfBoundsException();
4✔
174
    }
175

176
    public function key(): int | null {
UNCOV
177
        return key($this->triples) === null ? null : $this->key;
×
178
    }
179

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

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

208
        $this->input = $input;
4✔
209
        if (!empty($baseUri)) {
4✔
210
            $this->baseUriDefault = $baseUri;
2✔
211
        }
212
        return $this;
4✔
213
    }
214

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

236
    public function valid(): bool {
237
        return key($this->triples) !== null;
4✔
238
    }
239

240
    /**
241
     * 
242
     * @param string $name
243
     * @param array<string, string> $attribs
244
     * @return void
245
     * @throws RdfIoException
246
     */
247
    private function onElementStart(string $name, array &$attribs): void {
248
        $oldState      = $this->state;
4✔
249
        $this->state   = clone($oldState);
4✔
250
        $this->stack[] = $this->state;
4✔
251

252
        if (isset($attribs[RdfXmlParser::XML_LANG])) {
4✔
253
            $this->state->lang = $attribs[RdfXmlParser::XML_LANG];
3✔
254
        }
255
        $this->state->datatype = $this->resolveIri($attribs[RdfXmlParser::RDF_DATATYPE] ?? null);
4✔
256

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

279
        //echo "START " . $oldState->state . "=>" . $this->state->state . " $name (" . $this->state->literalValueDepth . ")\n";
280
    }
281

282
    /**
283
     * 
284
     * @param array<string, string> $attributes
285
     * @return void
286
     */
287
    private function onRoot(array &$attributes): void {
288
        $this->state->state = RdfXmlParserState::STATE_NODE;
4✔
289
        if (isset($attributes[self::XML_BASE])) {
4✔
290
            $this->parseBaseUri($attributes[self::XML_BASE]);
2✔
291
        }
292
    }
293

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

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

320
        // type as tag
321
        // https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-typed-nodes
322
        if ($tag !== self::RDF_DESCRIPTION) {
4✔
323
            $this->addTriple($subject, RDF::RDF_TYPE, $tag);
2✔
324
        }
325

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

333
        if ($this->state->isCollection) {
4✔
334
            $prevState            = $this->stack[count($this->stack) - 2];
1✔
335
            $collSubject          = $this->dataFactory->blankNode();
1✔
336
            $this->addTriple($prevState->subject, $this->state->predicate, $collSubject);
1✔
337
            $this->addTriple($collSubject, RDF::RDF_FIRST, $subject);
1✔
338
            $prevState->subject   = $collSubject;
1✔
339
            $prevState->predicate = $this->dataFactory->namedNode(RDF::RDF_REST);
1✔
340
        } elseif ($this->state->state === RdfXmlParserState::STATE_VALUE) {
4✔
341
            $prevState = $this->stack[count($this->stack) - 2];
2✔
342
            $this->addTriple($prevState->subject, $this->state->predicate, $subject);
2✔
343
        }
344

345
        // change the state
346
        $this->state->state = RdfXmlParserState::STATE_PREDICATE;
4✔
347
    }
348

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

376
        if (isset($attributes[self::RDF_RESOURCE])) {
4✔
377
            // rdf:resource attribute
378
            $subjectTmp = $this->dataFactory->namedNode($this->resolveIri($attributes[self::RDF_RESOURCE]) ?? '');
3✔
379
            $this->addTriple(null, $this->state->predicate, $subjectTmp);
3✔
380
        } elseif (isset($attributes[self::RDF_NODEID])) {
4✔
381
            // rdf:nodeID attribute
382
            $subjectTmp = $this->createBlankNode($attributes[self::RDF_NODEID]);
3✔
383
            $this->addTriple(null, $this->state->predicate, $subjectTmp);
3✔
384
        }
385

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

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

409
        // XML literal value due to parseType="Literal"
410
        // https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-XML-literals
411
        if ($parseType === self::PARSETYPE_LITERAL) {
4✔
412
            $this->state->state = RdfXmlParserState::STATE_XMLLITERAL;
1✔
413
        }
414

415
        // reification
416
        // https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-reifying
417
        if (isset($attributes[self::RDF_ID])) {
4✔
418
            $this->state->reifyAs = $this->handleElementId($attributes[self::RDF_ID]);
1✔
419
        }
420
    }
421

422
    private function onElementEnd(string $name): void {
423
        /* @var $oldState RdfXmlParserState */
424
        $oldState                      = array_pop($this->stack) ?: throw new RdfIoException('Empty states stack');
4✔
425
        $this->state                   = end($this->stack) ?: throw new RdfIoException('Empty states stack');
4✔
426
        $this->state->isCDataPredicate = $this->state->isCDataPredicate && $oldState->isCDataPredicate;
4✔
427

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

440
        if ($oldState->isCollection) {
4✔
441
            $this->addTriple($oldState->subject, RDF::RDF_REST, RDF::RDF_NIL);
1✔
442
        }
443

444
        //echo "END $oldState->state=>" . $this->state->state . " $name ($oldState->isCDataPredicate,$oldState->literalValueDepth)\n";
445
    }
446

447
    private function onNamespaceStart(XMLParser $parser, string $prefix,
448
                                      string $uri): void {
449
        if (!isset($this->nmsp[$prefix])) {
4✔
450
            $this->nmsp[$prefix] = [];
4✔
451
        }
452
        $this->nmsp[$prefix][] = $uri;
4✔
453
    }
454

455
    private function onNamespaceEnd(XMLParser $parser, string $prefix): void {
UNCOV
456
        array_pop($this->nmsp[$prefix]);
×
457
    }
458

459
    private function onCData(string $data): void {
460
        if ($this->state->state === RdfXmlParserState::STATE_VALUE && $this->state->isCDataPredicate !== false || $this->state->state === RdfXmlParserState::STATE_XMLLITERAL) {
4✔
461
            $this->state->isCDataPredicate = true;
4✔
462
            $this->state->literalValue     .= $data;
4✔
463
        }
464
    }
465

466
    private function reportError(): void {
UNCOV
467
        $msg  = xml_error_string(xml_get_error_code($this->parser));
×
UNCOV
468
        $line = xml_get_current_line_number($this->parser);
×
UNCOV
469
        $col  = xml_get_current_column_number($this->parser);
×
UNCOV
470
        throw new RdfIoException("Error while parsing the file: $msg in line $line column $col");
×
471
    }
472

473
    private function addTriple(iBlankNode | iNamedNode | null $subject,
474
                               iNamedNode | string $predicate,
475
                               iNamedNode | iBlankNode | string $object,
476
                               ?string $lang = null, ?string $datatype = null,
477
                               iBlankNode | iNamedNode | null $reifyAs = null): void {
478
        $df = $this->dataFactory;
4✔
479

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

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

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

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

561
    private function handleElementId(string $id): iNamedNode {
562
        if (isset($this->elementIds[$id])) {
2✔
563
            throw new RdfIoException("Duplicated element id '$id'");
1✔
564
        }
565
        $this->elementIds[$id] = $this->dataFactory->namedNode($this->baseUriEmpty . '#' . $id);
2✔
566
        return $this->elementIds[$id];
2✔
567
    }
568

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