Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.51% covered (warning)
75.51%
37 / 49
69.23% covered (warning)
69.23%
9 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Graph
75.51% covered (warning)
75.51%
37 / 49
69.23% covered (warning)
69.23%
9 / 13
39.52
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 add
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 remove
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 has
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 about
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 objects
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 firstObject
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 typesOf
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 subjects
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 listItems
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIterator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sorted
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace LambdaTwelve\OneRecord\Rdf;
6
7use ArrayIterator;
8use Countable;
9use Iterator;
10use IteratorAggregate;
11use LambdaTwelve\OneRecord\Spec\Namespaces;
12
13/**
14 * A set of triples with the lookups the rest of the package needs. Order is
15 * not significant; duplicates collapse. Mutable while being built, which is
16 * why the model layer wraps it rather than exposing it.
17 *
18 * @implements IteratorAggregate<int, Triple>
19 */
20final class Graph implements Countable, IteratorAggregate
21{
22    public const string RDF_TYPE = Namespaces::RDF . 'type';
23    public const string RDF_FIRST = Namespaces::RDF . 'first';
24    public const string RDF_REST = Namespaces::RDF . 'rest';
25    public const string RDF_NIL = Namespaces::RDF . 'nil';
26
27    /** @var array<string, Triple> keyed by N-Triples form, which makes the set free of duplicates */
28    private array $triples = [];
29
30    /** @var array<string, array<string, true>> subject key => triple keys */
31    private array $bySubject = [];
32
33    /**
34     * @param iterable<Triple> $triples
35     */
36    public function __construct(iterable $triples = [])
37    {
38        foreach ($triples as $triple) {
39            $this->add($triple);
40        }
41    }
42
43    public function add(Triple $triple): void
44    {
45        $key = $triple->toNTriples();
46        if (isset($this->triples[$key])) {
47            return;
48        }
49        $this->triples[$key] = $triple;
50        $this->bySubject[$triple->subject->toNTriples()][$key] = true;
51    }
52
53    public function remove(Triple $triple): bool
54    {
55        $key = $triple->toNTriples();
56        if (!isset($this->triples[$key])) {
57            return false;
58        }
59        unset($this->triples[$key], $this->bySubject[$triple->subject->toNTriples()][$key]);
60
61        return true;
62    }
63
64    public function has(Triple $triple): bool
65    {
66        return isset($this->triples[$triple->toNTriples()]);
67    }
68
69    /**
70     * @return list<Triple>
71     */
72    public function about(Iri|BlankNode $subject): array
73    {
74        $out = [];
75        foreach ($this->bySubject[$subject->toNTriples()] ?? [] as $key => $_) {
76            $out[] = $this->triples[$key];
77        }
78
79        return $out;
80    }
81
82    /**
83     * @return list<Term>
84     */
85    public function objects(Iri|BlankNode $subject, Iri|string $predicate): array
86    {
87        $predicate = $predicate instanceof Iri ? $predicate->value : $predicate;
88        $out = [];
89        foreach ($this->about($subject) as $triple) {
90            if ($triple->predicate->value === $predicate) {
91                $out[] = $triple->object;
92            }
93        }
94
95        return $out;
96    }
97
98    public function firstObject(Iri|BlankNode $subject, Iri|string $predicate): ?Term
99    {
100        return $this->objects($subject, $predicate)[0] ?? null;
101    }
102
103    /**
104     * @return list<Iri>
105     */
106    public function typesOf(Iri|BlankNode $subject): array
107    {
108        $out = [];
109        foreach ($this->objects($subject, self::RDF_TYPE) as $type) {
110            if ($type instanceof Iri) {
111                $out[] = $type;
112            }
113        }
114
115        return $out;
116    }
117
118    /**
119     * @return list<Iri|BlankNode>
120     */
121    public function subjects(): array
122    {
123        $out = [];
124        foreach ($this->bySubject as $keys) {
125            if ($keys === []) {
126                continue;
127            }
128            $out[] = $this->triples[array_key_first($keys)]->subject;
129        }
130
131        return $out;
132    }
133
134    /**
135     * Items of an RDF collection (rdf:first / rdf:rest chain) starting at $head.
136     *
137     * @return list<Term>
138     */
139    public function listItems(Term $head): array
140    {
141        $items = [];
142        $node = $head;
143        while ($node instanceof BlankNode || ($node instanceof Iri && $node->value !== self::RDF_NIL)) {
144            $first = $this->firstObject($node, self::RDF_FIRST);
145            if ($first === null) {
146                break;
147            }
148            $items[] = $first;
149            $node = $this->firstObject($node, self::RDF_REST) ?? new Iri(self::RDF_NIL);
150        }
151
152        return $items;
153    }
154
155    public function count(): int
156    {
157        return \count($this->triples);
158    }
159
160    /**
161     * @return Iterator<int, Triple>
162     */
163    public function getIterator(): Iterator
164    {
165        return new ArrayIterator(array_values($this->triples));
166    }
167
168    /**
169     * Triples in a stable order, for diffs and golden files.
170     *
171     * @return list<Triple>
172     */
173    public function sorted(): array
174    {
175        $keys = array_keys($this->triples);
176        sort($keys, SORT_STRING);
177
178        return array_map(fn(string $key): Triple => $this->triples[$key], $keys);
179    }
180}